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
+}