Archive

lemon-mint committed Oct 28, 2025 at 12:11 UTC 1159aeffd8747bacd90ea3afed2025a148f3cdcb
7 files changed +1007
cmd/webclient/httpjs/http_js.go new
+1
@@ -0,0 +1 @@
1 +package httpjs
cmd/webclient/index.html new
+11
@@ -0,0 +1,11 @@
1 +<!DOCTYPE html>
2 +<head>
3 + <meta charset="UTF-8">
4 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
5 + <title>RelayDNS WebClient</title>
6 +</head>
7 +<body>
8 + <h1>RelayDNS WebClient</h1>
9 + <hr/>
10 +</body>
11 +</html>
\ No newline at end of file
cmd/webclient/main_js.go new
+12
@@ -0,0 +1,12 @@
1 +package main
2 +
3 +import "runtime"
4 +
5 +func main() {
6 + if runtime.Compiler == "tinygo" || runtime.GOARCH != "wasm" {
7 + return
8 + }
9 +
10 + ch := make(chan struct{})
11 + <-ch
12 +}
cmd/webclient/service-worker.js new
+124
@@ -0,0 +1,124 @@
1 +// 1. WASM 실행 환경 임포트
2 +// (CDN을 사용하거나 로컬 경로를 사용할 수 있습니다)
3 +// const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.19/misc/wasm/wasm_exec.js";
4 +const wasm_exec_URL = "/wasm_exec.js";
5 +importScripts(wasm_exec_URL);
6 +
7 +// --- 전역 상수 및 변수 ---
8 +
9 +const wasm_URL = "/main.wasm";
10 +// importScripts와 경로 일치
11 +const CACHE_NAME = "WASM_Cache_v1";
12 +
13 +// WASM 로딩 상태를 관리하기 위한 Promise (중복 로드 방지)
14 +let wasmReadyPromise = null;
15 +
16 +/**
17 + * Go WASM을 로드하고 실행합니다.
18 + */
19 +async function runWASM() {
20 + const go = new Go();
21 + const cache = await caches.open(CACHE_NAME);
22 + let wasm_file;
23 +
24 + const cache_wasm = await cache.match(wasm_URL);
25 +
26 + if (cache_wasm) {
27 + console.log("Service Worker: 캐시에서 WASM 로드 중...");
28 + wasm_file = await cache_wasm.arrayBuffer();
29 + } else {
30 + console.warn("Service Worker: 캐시에 WASM이 없습니다. 네트워크에서 가져옵니다...");
31 + const resp = await fetch(wasm_URL);
32 + wasm_file = await resp.arrayBuffer();
33 + await cache.put(wasm_URL, new Response(wasm_file.slice(0)));
34 + }
35 +
36 + console.log("Service Worker: WebAssembly 인스턴스화...");
37 + const { instance } = await WebAssembly.instantiate(wasm_file, go.importObject);
38 +
39 + // go.run()은 Go의 main()을 실행하고,
40 + // _relaydns_http 콜백이 등록되면 리턴합니다.
41 + go.run(instance);
42 + console.log("Service Worker: Go WASM 실행 완료. _relaydns_http가 준비되었습니다.");
43 +}
44 +
45 +/**
46 + * runWASM()이 한 번만 실행되도록 보장하는 래퍼 함수입니다.
47 + * @returns {Promise<void>} WASM이 준비되면 resolve되는 Promise
48 + */
49 +function getWasmReady() {
50 + if (!wasmReadyPromise) {
51 + console.log("Service Worker: WASM 로딩 시작...");
52 + wasmReadyPromise = runWASM().catch(err => {
53 + console.error("Service Worker: WASM 실행 실패:", err);
54 + wasmReadyPromise = null; // 실패 시 다음 요청에서 재시도 허용
55 + throw err; // 에러를 호출자(fetch 핸들러)에게 전파
56 + });
57 + }
58 + return wasmReadyPromise;
59 +}
60 +
61 +
62 +// --- 1. 설치 (Install) 이벤트 리스너 ---
63 +self.addEventListener('install', (event) => {
64 + console.log('Service Worker: 설치 중...');
65 +
66 + event.waitUntil(
67 + (async () => {
68 + const cache = await caches.open(CACHE_NAME);
69 + console.log('Service Worker: 필수 에셋 캐싱 중...');
70 + await cache.addAll([
71 + wasm_URL,
72 + wasm_exec_URL,
73 + ]);
74 + await self.skipWaiting();
75 + })()
76 + );
77 +});
78 +
79 +// --- 2. 활성화 (Activate) 이벤트 리스너 ---
80 +self.addEventListener('activate', (event) => {
81 + console.log('Service Worker: 활성화 됨.');
82 +
83 + event.waitUntil(
84 + (async () => {
85 + await self.clients.claim();
86 + // WASM을 미리 로드하여 다음 fetch 요청에 대비
87 + console.log('Service Worker: Go WASM 선제적 로딩 시작...');
88 + await getWasmReady();
89 + console.log('Service Worker: Go WASM 선제적 로딩 완료.');
90 + })()
91 + );
92 +});
93 +
94 +
95 +// --- 3. 페치 (Fetch) 이벤트 리스너 ---
96 +// 모든 요청을 Go 핸들러로 전달합니다.
97 +self.addEventListener('fetch', (event) => {
98 + const url = new URL(event.request.url);
99 + console.log(`Service Worker: Go 핸들러로 요청 전달: ${url.pathname}`);
100 +
101 + event.respondWith((async () => {
102 + try {
103 + // WASM이 준비될 때까지 기다림
104 + await getWasmReady();
105 +
106 + if (typeof _relaydns_http !== 'undefined') {
107 + // WASM이 준비되었고 핸들러 함수가 존재함
108 + const resp = await _relaydns_http(event.request);
109 + return resp;
110 + } else {
111 + // getWasmReady()가 성공했는데도 함수가 없는 비정상 상황
112 + console.error("Service Worker: WASM 로드는 성공했으나 _relaydns_http가 정의되지 않았습니다.");
113 + return new Response("WASM 핸들러를 사용할 수 없습니다.", { status: 500 });
114 + }
115 + } catch (err) {
116 + // 1. getWasmReady() 실패 (WASM 로드/실행 실패)
117 + // 2. _relaydns_http(event.request) 실패 (Go 핸들러 내부 에러)
118 + console.error(`Service Worker: Go 핸들러 처리 실패 (네트워크로 폴백): ${err}`, event.request.url);
119 +
120 + // WASM 핸들러 실패 시 네트워크로 폴백
121 + return fetch(event.request);
122 + }
123 + })());
124 +});
\ No newline at end of file
cmd/webclient/streamjs/stream_js.go new
+135
@@ -0,0 +1,135 @@
1 +package streamjs
2 +
3 +import (
4 + "io"
5 + "sync"
6 + "syscall/js"
7 +)
8 +
9 +var (
10 + _ReadableStream = js.Global().Get("ReadableStream")
11 + _Object = js.Global().Get("Object")
12 + _Promise = js.Global().Get("Promise")
13 + _Error = js.Global().Get("Error")
14 + _Uint8Array = js.Global().Get("Uint8Array")
15 +)
16 +
17 +type ReadableStream struct {
18 + js.Value
19 + r io.ReadCloser
20 + closeOnce sync.Once
21 +
22 + // 데이터를 읽기 위한 버퍼
23 + buffer []byte
24 +
25 + funcsToBeReleased []js.Func
26 +}
27 +
28 +// NewReadableStream는 Go의 io.ReadCloser를 JS ReadableStream으로 래핑합니다.
29 +func NewReadableStream(r io.ReadCloser) *ReadableStream {
30 + // 1. Go 래퍼 구조체를 먼저 생성합니다.
31 + rs := &ReadableStream{
32 + r: r,
33 + buffer: make([]byte, 4096), // 4KB 버퍼로 초기화
34 + }
35 +
36 + // 2. JS 콜백 함수들을 정의합니다. 이 함수들은 'rs' 포인터를 클로저로 캡처합니다.
37 + var onStart, onPull, onCancel js.Func
38 +
39 + // start: 스트림이 시작될 때 호출됨 (보통 비워둠)
40 + onStart = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
41 + // controller := args[0]
42 + return nil
43 + })
44 +
45 + // pull: JS 런타임이 데이터를 요청할 때 호출됨 (가장 중요)
46 + onPull = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
47 + controller := args[0]
48 +
49 + // 3. Promise를 생성하여 반환합니다. (비동기 작업)
50 + // JS 스레드를 차단하지 않기 위해 Go 루틴에서 실제 I/O를 수행합니다.
51 + var promiseFn js.Func
52 + promiseFn = js.FuncOf(func(this js.Value, pArgs []js.Value) interface{} {
53 + resolve := pArgs[0]
54 + reject := pArgs[1]
55 +
56 + // 4. 고루틴에서 (잠재적으로 블로킹되는) Read 수행
57 + go func() {
58 + defer promiseFn.Release()
59 +
60 + n, err := rs.r.Read(rs.buffer)
61 +
62 + // 5. 에러 처리
63 + if err != nil {
64 + if err == io.EOF {
65 + // 5a. 파일 끝 (EOF) -> 스트림 정상 종료
66 + controller.Call("close")
67 + } else {
68 + // 5b. 실제 읽기 오류 -> 스트림 에러 종료
69 + jsErr := _Error.New(err.Error())
70 + controller.Call("error", jsErr)
71 + reject.Invoke(jsErr) // Promise 거부
72 + }
73 + resolve.Invoke() // Promise 이행 (pull 작업 완료)
74 + return
75 + }
76 +
77 + // 6. 성공적으로 데이터를 읽은 경우
78 + if n > 0 {
79 + // 6a. 읽은 만큼(n 바이트) JS Uint8Array 생성
80 + jsChunk := _Uint8Array.New(n)
81 +
82 + // 6b. Go 버퍼(rs.buffer[:n])에서 JS Uint8Array로 바이트 복사
83 + js.CopyBytesToJS(jsChunk, rs.buffer[:n])
84 +
85 + // 6c. JS 스트림 컨트롤러에 데이터 추가 (enqueue)
86 + controller.Call("enqueue", jsChunk)
87 + }
88 +
89 + // 7. pull 작업이 성공적으로 완료되었음을 알림 (Promise 이행)
90 + resolve.Invoke()
91 + }()
92 +
93 + return nil
94 + })
95 +
96 + return _Promise.New(promiseFn)
97 + })
98 +
99 + // cancel: 스트림이 JS 쪽에서 취소될 때 호출됨
100 + onCancel = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
101 + // Go 리더기(ReadCloser)를 닫아 리소스를 정리합니다.
102 + rs.closeOnce.Do(func() {
103 + rs.r.Close()
104 + })
105 + return nil
106 + })
107 +
108 + // 8. JS 'underlyingSource' 객체 생성
109 + underlyingSource := _Object.New()
110 + underlyingSource.Set("start", onStart)
111 + underlyingSource.Set("pull", onPull)
112 + underlyingSource.Set("cancel", onCancel)
113 + underlyingSource.Set("type", "bytes")
114 +
115 + // 9. JS ReadableStream 인스턴스 생성
116 + stream := _ReadableStream.New(underlyingSource)
117 +
118 + // 10. Go 래퍼 구조체 필드 완성
119 + rs.Value = stream
120 + rs.funcsToBeReleased = []js.Func{onStart, onPull, onCancel}
121 +
122 + return rs
123 +}
124 +
125 +// Close는 스트림을 닫고 할당된 JS 함수들을 해제(release)합니다.
126 +func (rs *ReadableStream) Close() {
127 + for _, f := range rs.funcsToBeReleased {
128 + f.Release()
129 + }
130 +
131 + // Go 리더기도 닫아줍니다.
132 + rs.closeOnce.Do(func() {
133 + rs.r.Close()
134 + })
135 +}
cmd/webclient/wasm_exec.js new
+604
@@ -0,0 +1,604 @@
1 +// Copyright 2018 The Go Authors. All rights reserved.
2 +// Use of this source code is governed by a BSD-style
3 +//
4 +// Copyright 2009 The Go Authors.
5 +//
6 +// Redistribution and use in source and binary forms, with or without
7 +// modification, are permitted provided that the following conditions are
8 +// met:
9 +//
10 +// * Redistributions of source code must retain the above copyright
11 +// notice, this list of conditions and the following disclaimer.
12 +// * Redistributions in binary form must reproduce the above
13 +// copyright notice, this list of conditions and the following disclaimer
14 +// in the documentation and/or other materials provided with the
15 +// distribution.
16 +// * Neither the name of Google LLC nor the names of its
17 +// contributors may be used to endorse or promote products derived from
18 +// this software without specific prior written permission.
19 +//
20 +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 +//
32 +
33 +
34 +"use strict";
35 +
36 +(() => {
37 + const enosys = () => {
38 + const err = new Error("not implemented");
39 + err.code = "ENOSYS";
40 + return err;
41 + };
42 +
43 + if (!globalThis.fs) {
44 + let outputBuf = "";
45 + globalThis.fs = {
46 + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
47 + writeSync(fd, buf) {
48 + outputBuf += decoder.decode(buf);
49 + const nl = outputBuf.lastIndexOf("\n");
50 + if (nl != -1) {
51 + console.log(outputBuf.substring(0, nl));
52 + outputBuf = outputBuf.substring(nl + 1);
53 + }
54 + return buf.length;
55 + },
56 + write(fd, buf, offset, length, position, callback) {
57 + if (offset !== 0 || length !== buf.length || position !== null) {
58 + callback(enosys());
59 + return;
60 + }
61 + const n = this.writeSync(fd, buf);
62 + callback(null, n);
63 + },
64 + chmod(path, mode, callback) { callback(enosys()); },
65 + chown(path, uid, gid, callback) { callback(enosys()); },
66 + close(fd, callback) { callback(enosys()); },
67 + fchmod(fd, mode, callback) { callback(enosys()); },
68 + fchown(fd, uid, gid, callback) { callback(enosys()); },
69 + fstat(fd, callback) { callback(enosys()); },
70 + fsync(fd, callback) { callback(null); },
71 + ftruncate(fd, length, callback) { callback(enosys()); },
72 + lchown(path, uid, gid, callback) { callback(enosys()); },
73 + link(path, link, callback) { callback(enosys()); },
74 + lstat(path, callback) { callback(enosys()); },
75 + mkdir(path, perm, callback) { callback(enosys()); },
76 + open(path, flags, mode, callback) { callback(enosys()); },
77 + read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
78 + readdir(path, callback) { callback(enosys()); },
79 + readlink(path, callback) { callback(enosys()); },
80 + rename(from, to, callback) { callback(enosys()); },
81 + rmdir(path, callback) { callback(enosys()); },
82 + stat(path, callback) { callback(enosys()); },
83 + symlink(path, link, callback) { callback(enosys()); },
84 + truncate(path, length, callback) { callback(enosys()); },
85 + unlink(path, callback) { callback(enosys()); },
86 + utimes(path, atime, mtime, callback) { callback(enosys()); },
87 + };
88 + }
89 +
90 + if (!globalThis.process) {
91 + globalThis.process = {
92 + getuid() { return -1; },
93 + getgid() { return -1; },
94 + geteuid() { return -1; },
95 + getegid() { return -1; },
96 + getgroups() { throw enosys(); },
97 + pid: -1,
98 + ppid: -1,
99 + umask() { throw enosys(); },
100 + cwd() { throw enosys(); },
101 + chdir() { throw enosys(); },
102 + }
103 + }
104 +
105 + if (!globalThis.path) {
106 + globalThis.path = {
107 + resolve(...pathSegments) {
108 + return pathSegments.join("/");
109 + }
110 + }
111 + }
112 +
113 + if (!globalThis.crypto) {
114 + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
115 + }
116 +
117 + if (!globalThis.performance) {
118 + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
119 + }
120 +
121 + if (!globalThis.TextEncoder) {
122 + throw new Error("globalThis.TextEncoder is not available, polyfill required");
123 + }
124 +
125 + if (!globalThis.TextDecoder) {
126 + throw new Error("globalThis.TextDecoder is not available, polyfill required");
127 + }
128 +
129 + const encoder = new TextEncoder("utf-8");
130 + const decoder = new TextDecoder("utf-8");
131 +
132 + globalThis.Go = class {
133 + constructor() {
134 + this.argv = ["js"];
135 + this.env = {};
136 + this.exit = (code) => {
137 + if (code !== 0) {
138 + console.warn("exit code:", code);
139 + }
140 + };
141 + this._exitPromise = new Promise((resolve) => {
142 + this._resolveExitPromise = resolve;
143 + });
144 + this._pendingEvent = null;
145 + this._scheduledTimeouts = new Map();
146 + this._nextCallbackTimeoutID = 1;
147 +
148 + const setInt64 = (addr, v) => {
149 + this.mem.setUint32(addr + 0, v, true);
150 + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
151 + }
152 +
153 + const setInt32 = (addr, v) => {
154 + this.mem.setUint32(addr + 0, v, true);
155 + }
156 +
157 + const getInt64 = (addr) => {
158 + const low = this.mem.getUint32(addr + 0, true);
159 + const high = this.mem.getInt32(addr + 4, true);
160 + return low + high * 4294967296;
161 + }
162 +
163 + const loadValue = (addr) => {
164 + const f = this.mem.getFloat64(addr, true);
165 + if (f === 0) {
166 + return undefined;
167 + }
168 + if (!isNaN(f)) {
169 + return f;
170 + }
171 +
172 + const id = this.mem.getUint32(addr, true);
173 + return this._values[id];
174 + }
175 +
176 + const storeValue = (addr, v) => {
177 + const nanHead = 0x7FF80000;
178 +
179 + if (typeof v === "number" && v !== 0) {
180 + if (isNaN(v)) {
181 + this.mem.setUint32(addr + 4, nanHead, true);
182 + this.mem.setUint32(addr, 0, true);
183 + return;
184 + }
185 + this.mem.setFloat64(addr, v, true);
186 + return;
187 + }
188 +
189 + if (v === undefined) {
190 + this.mem.setFloat64(addr, 0, true);
191 + return;
192 + }
193 +
194 + let id = this._ids.get(v);
195 + if (id === undefined) {
196 + id = this._idPool.pop();
197 + if (id === undefined) {
198 + id = this._values.length;
199 + }
200 + this._values[id] = v;
201 + this._goRefCounts[id] = 0;
202 + this._ids.set(v, id);
203 + }
204 + this._goRefCounts[id]++;
205 + let typeFlag = 0;
206 + switch (typeof v) {
207 + case "object":
208 + if (v !== null) {
209 + typeFlag = 1;
210 + }
211 + break;
212 + case "string":
213 + typeFlag = 2;
214 + break;
215 + case "symbol":
216 + typeFlag = 3;
217 + break;
218 + case "function":
219 + typeFlag = 4;
220 + break;
221 + }
222 + this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
223 + this.mem.setUint32(addr, id, true);
224 + }
225 +
226 + const loadSlice = (addr) => {
227 + const array = getInt64(addr + 0);
228 + const len = getInt64(addr + 8);
229 + return new Uint8Array(this._inst.exports.mem.buffer, array, len);
230 + }
231 +
232 + const loadSliceOfValues = (addr) => {
233 + const array = getInt64(addr + 0);
234 + const len = getInt64(addr + 8);
235 + const a = new Array(len);
236 + for (let i = 0; i < len; i++) {
237 + a[i] = loadValue(array + i * 8);
238 + }
239 + return a;
240 + }
241 +
242 + const loadString = (addr) => {
243 + const saddr = getInt64(addr + 0);
244 + const len = getInt64(addr + 8);
245 + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
246 + }
247 +
248 + const testCallExport = (a, b) => {
249 + this._inst.exports.testExport0();
250 + return this._inst.exports.testExport(a, b);
251 + }
252 +
253 + const timeOrigin = Date.now() - performance.now();
254 + this.importObject = {
255 + _gotest: {
256 + add: (a, b) => a + b,
257 + callExport: testCallExport,
258 + },
259 + gojs: {
260 + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
261 + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
262 + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
263 + // This changes the SP, thus we have to update the SP used by the imported function.
264 +
265 + // func wasmExit(code int32)
266 + "runtime.wasmExit": (sp) => {
267 + sp >>>= 0;
268 + const code = this.mem.getInt32(sp + 8, true);
269 + this.exited = true;
270 + delete this._inst;
271 + delete this._values;
272 + delete this._goRefCounts;
273 + delete this._ids;
274 + delete this._idPool;
275 + this.exit(code);
276 + },
277 +
278 + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
279 + "runtime.wasmWrite": (sp) => {
280 + sp >>>= 0;
281 + const fd = getInt64(sp + 8);
282 + const p = getInt64(sp + 16);
283 + const n = this.mem.getInt32(sp + 24, true);
284 + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
285 + },
286 +
287 + // func resetMemoryDataView()
288 + "runtime.resetMemoryDataView": (sp) => {
289 + sp >>>= 0;
290 + this.mem = new DataView(this._inst.exports.mem.buffer);
291 + },
292 +
293 + // func nanotime1() int64
294 + "runtime.nanotime1": (sp) => {
295 + sp >>>= 0;
296 + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
297 + },
298 +
299 + // func walltime() (sec int64, nsec int32)
300 + "runtime.walltime": (sp) => {
301 + sp >>>= 0;
302 + const msec = (new Date).getTime();
303 + setInt64(sp + 8, msec / 1000);
304 + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
305 + },
306 +
307 + // func scheduleTimeoutEvent(delay int64) int32
308 + "runtime.scheduleTimeoutEvent": (sp) => {
309 + sp >>>= 0;
310 + const id = this._nextCallbackTimeoutID;
311 + this._nextCallbackTimeoutID++;
312 + this._scheduledTimeouts.set(id, setTimeout(
313 + () => {
314 + this._resume();
315 + while (this._scheduledTimeouts.has(id)) {
316 + // for some reason Go failed to register the timeout event, log and try again
317 + // (temporary workaround for https://github.com/golang/go/issues/28975)
318 + console.warn("scheduleTimeoutEvent: missed timeout event");
319 + this._resume();
320 + }
321 + },
322 + getInt64(sp + 8),
323 + ));
324 + this.mem.setInt32(sp + 16, id, true);
325 + },
326 +
327 + // func clearTimeoutEvent(id int32)
328 + "runtime.clearTimeoutEvent": (sp) => {
329 + sp >>>= 0;
330 + const id = this.mem.getInt32(sp + 8, true);
331 + clearTimeout(this._scheduledTimeouts.get(id));
332 + this._scheduledTimeouts.delete(id);
333 + },
334 +
335 + // func getRandomData(r []byte)
336 + "runtime.getRandomData": (sp) => {
337 + sp >>>= 0;
338 + crypto.getRandomValues(loadSlice(sp + 8));
339 + },
340 +
341 + // func finalizeRef(v ref)
342 + "syscall/js.finalizeRef": (sp) => {
343 + sp >>>= 0;
344 + const id = this.mem.getUint32(sp + 8, true);
345 + this._goRefCounts[id]--;
346 + if (this._goRefCounts[id] === 0) {
347 + const v = this._values[id];
348 + this._values[id] = null;
349 + this._ids.delete(v);
350 + this._idPool.push(id);
351 + }
352 + },
353 +
354 + // func stringVal(value string) ref
355 + "syscall/js.stringVal": (sp) => {
356 + sp >>>= 0;
357 + storeValue(sp + 24, loadString(sp + 8));
358 + },
359 +
360 + // func valueGet(v ref, p string) ref
361 + "syscall/js.valueGet": (sp) => {
362 + sp >>>= 0;
363 + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
364 + sp = this._inst.exports.getsp() >>> 0; // see comment above
365 + storeValue(sp + 32, result);
366 + },
367 +
368 + // func valueSet(v ref, p string, x ref)
369 + "syscall/js.valueSet": (sp) => {
370 + sp >>>= 0;
371 + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
372 + },
373 +
374 + // func valueDelete(v ref, p string)
375 + "syscall/js.valueDelete": (sp) => {
376 + sp >>>= 0;
377 + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
378 + },
379 +
380 + // func valueIndex(v ref, i int) ref
381 + "syscall/js.valueIndex": (sp) => {
382 + sp >>>= 0;
383 + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
384 + },
385 +
386 + // valueSetIndex(v ref, i int, x ref)
387 + "syscall/js.valueSetIndex": (sp) => {
388 + sp >>>= 0;
389 + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
390 + },
391 +
392 + // func valueCall(v ref, m string, args []ref) (ref, bool)
393 + "syscall/js.valueCall": (sp) => {
394 + sp >>>= 0;
395 + try {
396 + const v = loadValue(sp + 8);
397 + const m = Reflect.get(v, loadString(sp + 16));
398 + const args = loadSliceOfValues(sp + 32);
399 + const result = Reflect.apply(m, v, args);
400 + sp = this._inst.exports.getsp() >>> 0; // see comment above
401 + storeValue(sp + 56, result);
402 + this.mem.setUint8(sp + 64, 1);
403 + } catch (err) {
404 + sp = this._inst.exports.getsp() >>> 0; // see comment above
405 + storeValue(sp + 56, err);
406 + this.mem.setUint8(sp + 64, 0);
407 + }
408 + },
409 +
410 + // func valueInvoke(v ref, args []ref) (ref, bool)
411 + "syscall/js.valueInvoke": (sp) => {
412 + sp >>>= 0;
413 + try {
414 + const v = loadValue(sp + 8);
415 + const args = loadSliceOfValues(sp + 16);
416 + const result = Reflect.apply(v, undefined, args);
417 + sp = this._inst.exports.getsp() >>> 0; // see comment above
418 + storeValue(sp + 40, result);
419 + this.mem.setUint8(sp + 48, 1);
420 + } catch (err) {
421 + sp = this._inst.exports.getsp() >>> 0; // see comment above
422 + storeValue(sp + 40, err);
423 + this.mem.setUint8(sp + 48, 0);
424 + }
425 + },
426 +
427 + // func valueNew(v ref, args []ref) (ref, bool)
428 + "syscall/js.valueNew": (sp) => {
429 + sp >>>= 0;
430 + try {
431 + const v = loadValue(sp + 8);
432 + const args = loadSliceOfValues(sp + 16);
433 + const result = Reflect.construct(v, args);
434 + sp = this._inst.exports.getsp() >>> 0; // see comment above
435 + storeValue(sp + 40, result);
436 + this.mem.setUint8(sp + 48, 1);
437 + } catch (err) {
438 + sp = this._inst.exports.getsp() >>> 0; // see comment above
439 + storeValue(sp + 40, err);
440 + this.mem.setUint8(sp + 48, 0);
441 + }
442 + },
443 +
444 + // func valueLength(v ref) int
445 + "syscall/js.valueLength": (sp) => {
446 + sp >>>= 0;
447 + setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
448 + },
449 +
450 + // valuePrepareString(v ref) (ref, int)
451 + "syscall/js.valuePrepareString": (sp) => {
452 + sp >>>= 0;
453 + const str = encoder.encode(String(loadValue(sp + 8)));
454 + storeValue(sp + 16, str);
455 + setInt64(sp + 24, str.length);
456 + },
457 +
458 + // valueLoadString(v ref, b []byte)
459 + "syscall/js.valueLoadString": (sp) => {
460 + sp >>>= 0;
461 + const str = loadValue(sp + 8);
462 + loadSlice(sp + 16).set(str);
463 + },
464 +
465 + // func valueInstanceOf(v ref, t ref) bool
466 + "syscall/js.valueInstanceOf": (sp) => {
467 + sp >>>= 0;
468 + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
469 + },
470 +
471 + // func copyBytesToGo(dst []byte, src ref) (int, bool)
472 + "syscall/js.copyBytesToGo": (sp) => {
473 + sp >>>= 0;
474 + const dst = loadSlice(sp + 8);
475 + const src = loadValue(sp + 32);
476 + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
477 + this.mem.setUint8(sp + 48, 0);
478 + return;
479 + }
480 + const toCopy = src.subarray(0, dst.length);
481 + dst.set(toCopy);
482 + setInt64(sp + 40, toCopy.length);
483 + this.mem.setUint8(sp + 48, 1);
484 + },
485 +
486 + // func copyBytesToJS(dst ref, src []byte) (int, bool)
487 + "syscall/js.copyBytesToJS": (sp) => {
488 + sp >>>= 0;
489 + const dst = loadValue(sp + 8);
490 + const src = loadSlice(sp + 16);
491 + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
492 + this.mem.setUint8(sp + 48, 0);
493 + return;
494 + }
495 + const toCopy = src.subarray(0, dst.length);
496 + dst.set(toCopy);
497 + setInt64(sp + 40, toCopy.length);
498 + this.mem.setUint8(sp + 48, 1);
499 + },
500 +
501 + "debug": (value) => {
502 + console.log(value);
503 + },
504 + }
505 + };
506 + }
507 +
508 + async run(instance) {
509 + if (!(instance instanceof WebAssembly.Instance)) {
510 + throw new Error("Go.run: WebAssembly.Instance expected");
511 + }
512 + this._inst = instance;
513 + this.mem = new DataView(this._inst.exports.mem.buffer);
514 + this._values = [ // JS values that Go currently has references to, indexed by reference id
515 + NaN,
516 + 0,
517 + null,
518 + true,
519 + false,
520 + globalThis,
521 + this,
522 + ];
523 + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
524 + this._ids = new Map([ // mapping from JS values to reference ids
525 + [0, 1],
526 + [null, 2],
527 + [true, 3],
528 + [false, 4],
529 + [globalThis, 5],
530 + [this, 6],
531 + ]);
532 + this._idPool = []; // unused ids that have been garbage collected
533 + this.exited = false; // whether the Go program has exited
534 +
535 + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
536 + let offset = 4096;
537 +
538 + const strPtr = (str) => {
539 + const ptr = offset;
540 + const bytes = encoder.encode(str + "\0");
541 + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
542 + offset += bytes.length;
543 + if (offset % 8 !== 0) {
544 + offset += 8 - (offset % 8);
545 + }
546 + return ptr;
547 + };
548 +
549 + const argc = this.argv.length;
550 +
551 + const argvPtrs = [];
552 + this.argv.forEach((arg) => {
553 + argvPtrs.push(strPtr(arg));
554 + });
555 + argvPtrs.push(0);
556 +
557 + const keys = Object.keys(this.env).sort();
558 + keys.forEach((key) => {
559 + argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
560 + });
561 + argvPtrs.push(0);
562 +
563 + const argv = offset;
564 + argvPtrs.forEach((ptr) => {
565 + this.mem.setUint32(offset, ptr, true);
566 + this.mem.setUint32(offset + 4, 0, true);
567 + offset += 8;
568 + });
569 +
570 + // The linker guarantees global data starts from at least wasmMinDataAddr.
571 + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
572 + const wasmMinDataAddr = 4096 + 8192;
573 + if (offset >= wasmMinDataAddr) {
574 + throw new Error("total length of command line and environment variables exceeds limit");
575 + }
576 +
577 + this._inst.exports.run(argc, argv);
578 + if (this.exited) {
579 + this._resolveExitPromise();
580 + }
581 + await this._exitPromise;
582 + }
583 +
584 + _resume() {
585 + if (this.exited) {
586 + throw new Error("Go program has already exited");
587 + }
588 + this._inst.exports.resume();
589 + if (this.exited) {
590 + this._resolveExitPromise();
591 + }
592 + }
593 +
594 + _makeFuncWrapper(id) {
595 + const go = this;
596 + return function () {
597 + const event = { id: id, this: this, args: arguments };
598 + go._pendingEvent = event;
599 + go._resume();
600 + return event.result;
601 + };
602 + }
603 + }
604 +})();
\ No newline at end of file
cmd/webclient/wsjs/ws_js.go new
+120
@@ -0,0 +1,120 @@
1 +package wsjs
2 +
3 +import (
4 + "errors"
5 + "syscall/js"
6 +)
7 +
8 +var (
9 + ErrFailedToDial = errors.New("failed to dial websocket")
10 + ErrClosed = errors.New("websocket connection closed")
11 +)
12 +
13 +var (
14 + _WebSocket = js.Global().Get("WebSocket")
15 + _ArrayBuffer = js.Global().Get("ArrayBuffer")
16 + _Uint8Array = js.Global().Get("Uint8Array")
17 +)
18 +
19 +type Conn struct {
20 + ws js.Value
21 +
22 + messageChan chan []byte
23 + closeChan chan struct{}
24 +
25 + funcsToBeReleased []js.Func
26 +}
27 +
28 +func (conn *Conn) freeFuncs() {
29 + for _, f := range conn.funcsToBeReleased {
30 + f.Release()
31 + }
32 +}
33 +
34 +func Dial(uri string) (*Conn, error) {
35 + errCh := make(chan error, 1)
36 +
37 + ws := _WebSocket.New(uri)
38 + ws.Set("binaryType", "arraybuffer")
39 +
40 + conn := &Conn{
41 + ws: ws,
42 + messageChan: make(chan []byte, 128),
43 + closeChan: make(chan struct{}, 1),
44 + }
45 +
46 + onOpen := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
47 + errCh <- nil
48 + return nil
49 + })
50 +
51 + onError := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
52 + errCh <- ErrFailedToDial
53 + return nil
54 + })
55 +
56 + onMessage := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
57 + jsData := args[0].Get("data")
58 + if jsData.Type() == js.TypeString {
59 + // text frame
60 + data := []byte(jsData.String())
61 +
62 + conn.messageChan <- data
63 + } else if jsData.InstanceOf(_ArrayBuffer) {
64 + // binary frame
65 + array := _Uint8Array.New(jsData)
66 + byteLength := array.Get("byteLength").Int()
67 + data := make([]byte, byteLength)
68 + js.CopyBytesToGo(data, array)
69 +
70 + conn.messageChan <- data
71 + }
72 +
73 + return nil
74 + })
75 +
76 + onClose := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
77 + close(conn.closeChan)
78 + return nil
79 + })
80 +
81 + conn.funcsToBeReleased = append(conn.funcsToBeReleased, onOpen, onError, onMessage, onClose)
82 +
83 + conn.ws.Call("addEventListener", "open", onOpen)
84 + conn.ws.Call("addEventListener", "error", onError)
85 + conn.ws.Call("addEventListener", "message", onMessage)
86 + conn.ws.Call("addEventListener", "close", onClose)
87 +
88 + err := <-errCh
89 + if err != nil {
90 + conn.freeFuncs()
91 + return nil, err
92 + }
93 +
94 + return conn, nil
95 +}
96 +
97 +func (conn *Conn) Close() error {
98 + conn.ws.Call("close")
99 + <-conn.closeChan
100 + conn.freeFuncs()
101 + return nil
102 +}
103 +
104 +func (conn *Conn) NextMessage() ([]byte, error) {
105 + select {
106 + case msg := <-conn.messageChan:
107 + return msg, nil
108 + case <-conn.closeChan:
109 + return nil, ErrClosed
110 + }
111 +}
112 +
113 +func (conn *Conn) Send(data []byte) error {
114 + buffer := _ArrayBuffer.New(len(data))
115 + array := _Uint8Array.New(buffer)
116 + js.CopyBytesToJS(array, data)
117 +
118 + conn.ws.Call("send", buffer)
119 + return nil
120 +}