refactor(service-worker): simplify WASM loading and add retry mechanism

- Removed promise-based duplicate loading prevention for WASM - Updated Go version in commented wasm_exec URL to 1.25.3 - Introduced proxy_handler with error handling and automatic retry on failure - Streamlined activate event to directly call runWASM instead of wrapper This refactor improves reliability by allowing retries when the portal proxy fails, while simplifying the code structure.

lemon-mint committed Oct 31, 2025 at 12:22 UTC cba90c7bc7b14a8d03167e9a0f98f63db214ef68
3 files changed +70 -77
Dockerfile.frontend new
+22
@@ -0,0 +1,22 @@
1 +FROM golang:1 AS builder
2 +
3 +WORKDIR /src
4 +
5 +COPY go.mod go.sum ./
6 +RUN --mount=type=cache,target=/go/pkg/mod \
7 + go mod download
8 +
9 +COPY . .
10 +
11 +RUN GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o bin/main.wasm ./cmd/webclient
12 +
13 +FROM nginx
14 +
15 +COPY --from=builder /src/cmd/webclient/index.html /usr/share/nginx/html/index.html
16 +COPY --from=builder /src/cmd/webclient/service-worker.js /usr/share/nginx/html/service-worker.js
17 +COPY --from=builder /src/cmd/webclient/wasm_exec.js /usr/share/nginx/html/wasm_exec.js
18 +COPY --from=builder /src/bin/main.wasm /usr/share/nginx/html/main.wasm
19 +
20 +EXPOSE 80
21 +
22 +CMD ["nginx", "-g", "daemon off;"]
cmd/webclient/main.wasm
Binary files a/cmd/webclient/main.wasm and /dev/null differ
cmd/webclient/service-worker.js
+48 -77
@@ -1,68 +1,40 @@
1 -// 1. Import WASM execution environment
2 -// (You can use CDN or local path)
3 -// const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.19/misc/wasm/wasm_exec.js";
1 +// const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/misc/wasm/wasm_exec.js";
2 const wasm_exec_URL = "/wasm_exec.js";
3 importScripts(wasm_exec_URL);
4
7 -// --- Global constants and variables ---
8 -
5 +let _portal_proxy;
6 const wasm_URL = "/main.wasm";
10 -// Path matching with importScripts
7 const CACHE_NAME = "WASM_Cache_v1";
8
13 -// Promise to manage WASM loading state (prevents duplicate loading)
14 -let wasmReadyPromise = null;
15 -
16 -/**
17 - * Loads and executes Go WASM.
18 - */
9 async function runWASM() {
20 - const go = new Go();
21 - const cache = await caches.open(CACHE_NAME);
22 - let wasm_file;
10 + const go = new Go();
11 + const cache = await caches.open(CACHE_NAME);
12 + let wasm_file;
13
24 - const cache_wasm = await cache.match(wasm_URL);
25 -
26 - if (cache_wasm) {
27 - console.log("Service Worker: Loading WASM from cache...");
28 - wasm_file = await cache_wasm.arrayBuffer();
29 - } else {
30 - console.warn("Service Worker: WASM not in cache. Fetching from network...");
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 - }
14 + const cache_wasm = await cache.match(wasm_URL);
15
36 - console.log("Service Worker: Instantiating WebAssembly...");
37 - const { instance } = await WebAssembly.instantiate(wasm_file, go.importObject);
16 + if (cache_wasm) {
17 + console.log("Service Worker: Loading WASM from cache...");
18 + wasm_file = await cache_wasm.arrayBuffer();
19 + } else {
20 + console.warn("Service Worker: WASM not in cache. Fetching from network...");
21 + const resp = await fetch(wasm_URL);
22 + wasm_file = await resp.arrayBuffer();
23 + await cache.put(wasm_URL, new Response(wasm_file.slice(0)));
24 + }
25
39 - // go.run() executes Go's main() and returns
40 - // when _portal_proxy callback is registered
41 - go.run(instance);
42 - console.log("Service Worker: Go WASM execution complete. _portal_proxy is ready.");
43 -}
26 + console.log("Service Worker: Instantiating WebAssembly...");
27 + const { instance } = await WebAssembly.instantiate(wasm_file, go.importObject);
28
45 -/**
46 - * Wrapper function that ensures runWASM() is executed only once.
47 - * @returns {Promise<void>} Promise that resolves when WASM is ready
48 - */
49 -function getWasmReady() {
50 - if (!wasmReadyPromise) {
51 - console.log("Service Worker: Starting WASM loading...");
52 - wasmReadyPromise = runWASM().catch(err => {
53 - console.error("Service Worker: WASM execution failed:", err);
54 - wasmReadyPromise = null; // Allow retry on next request if failed
55 - throw err; // Propagate error to caller (fetch handler)
56 - });
57 - }
58 - return wasmReadyPromise;
29 + // go.run() executes Go's main() and returns
30 + // when _portal_proxy callback is registered
31 + go.run(instance);
32 + console.log("Service Worker: Go WASM execution complete. _portal_proxy is ready.");
33 }
34
61 -
62 -// --- 1. Install event listener ---
35 self.addEventListener('install', (event) => {
36 console.log('Service Worker: Installing...');
65 -
37 +
38 event.waitUntil(
39 (async () => {
40 const cache = await caches.open(CACHE_NAME);
@@ -85,40 +57,39 @@ self.addEventListener('activate', (event) => {
57 await self.clients.claim();
58 // Preload WASM to prepare for next fetch requests
59 console.log('Service Worker: Starting Go WASM preloading...');
88 - await getWasmReady();
60 + await runWASM();
61 console.log('Service Worker: Go WASM preloading complete.');
62 })()
63 );
64 });
65
66
95 -// --- 3. Fetch event listener ---
96 -// Pass all requests to Go handler.
97 -self.addEventListener('fetch', (event) => {
98 - const url = new URL(event.request.url);
99 - console.log(`Service Worker: Forwarding request to Portal Proxy handler: ${url.pathname}`);
100 -
101 - event.respondWith((async () => {
102 - try {
103 - // Wait until WASM is ready
104 - await getWasmReady();
67 +async function sleep(ms) {
68 + return new Promise(resolve => setTimeout(resolve, ms));
69 +}
70
106 - if (typeof _portal_proxy !== 'undefined') {
107 - // WASM is ready and handler function exists
71 +async function proxy_handler(event) {
72 + console.log('Service Worker: Fetch event:', event.request);
73 + if (typeof _portal_proxy != 'undefined') {
74 + event.respondWith((async () => {
75 + try {
76 + const resp = await _portal_proxy(event.request);
77 + return resp;
78 + } catch {
79 + _portal_proxy = undefined;
80 + await runWASM();
81 const resp = await _portal_proxy(event.request);
82 return resp;
110 - } else {
111 - // Abnormal situation where function doesn't exist even though getWasmReady() succeeded
112 - console.error("Service Worker: WASM loading succeeded but _portal_proxy is not defined.");
113 - return new Response("WASM handler is not available.", { status: 500 });
83 }
115 - } catch (err) {
116 - // 1. getWasmReady() failure (WASM load/execution failure)
117 - // 2. _portal_http(event.request) failure (Go handler internal error)
118 - console.error(`Service Worker: Portal Proxy handler processing failed (falling back to network): ${err}`, event.request.url);
119 -
120 - // Fallback to network when WASM handler fails
121 - return fetch(event.request);
122 - }
123 - })());
124 -});
\ No newline at end of file
84 + })());
85 + return;
86 + }
87 + console.log('Service Worker: _portal_proxy is not defined. Fetch event ignored.');
88 +
89 + _portal_proxy = undefined;
90 + await runWASM();
91 + await sleep(100);
92 + await proxy_handler(event);
93 +}
94 +
95 +self.addEventListener('fetch', proxy_handler);