feat(frontend): implement cache busting for WASM files using SHA256 hashes

- Modified Dockerfile.frontend to build WASM with hashed filename and generate manifest.json containing file name and hash - Updated service-worker.js to fetch manifest, use dynamic WASM URL, version caches by hash, and clean up old caches - This prevents browser caching issues by ensuring unique filenames for updated WASM builds

lemon-mint committed Nov 3, 2025 at 16:38 UTC f686346bd10968f8a3820ad8de670da5245c895f
2 files changed +54 -6
Dockerfile.frontend
+9 -2
@@ -8,7 +8,12 @@ RUN --mount=type=cache,target=/go/pkg/mod \
8
9 COPY . .
10
11 -RUN GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o bin/main.wasm ./cmd/webclient
11 +RUN GOOS=js GOARCH=wasm go build -trimpath -ldflags="-s -w" -o bin/main_hashstr.wasm ./cmd/webclient
12 +
13 +# Calculate SHA256 hash and rename WASM file
14 +RUN HASH=$(sha256sum bin/main_hashstr.wasm | cut -d' ' -f1) && \
15 + mv bin/main_hashstr.wasm bin/main_${HASH}.wasm && \
16 + echo "{\"wasmFile\": \"main_${HASH}.wasm\", \"hash\": \"${HASH}\"}" > bin/manifest.json
17
18 FROM nginx
19
@@ -16,7 +21,9 @@ COPY --from=builder /src/cmd/webclient/index.html /usr/share/nginx/html/index.ht
21 COPY --from=builder /src/cmd/webclient/portal.mp4 /usr/share/nginx/html/portal.mp4
22 COPY --from=builder /src/cmd/webclient/service-worker.js /usr/share/nginx/html/service-worker.js
23 COPY --from=builder /src/cmd/webclient/wasm_exec.js /usr/share/nginx/html/wasm_exec.js
19 -COPY --from=builder /src/bin/main.wasm /usr/share/nginx/html/main.wasm
24 +COPY --from=builder /src/bin/main_*.wasm /usr/share/nginx/html/
25 +COPY --from=builder /src/bin/manifest.json /usr/share/nginx/html/manifest.json
26 +
27
28 EXPOSE 80
29
cmd/webclient/service-worker.js
+45 -4
@@ -1,10 +1,31 @@
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 -const wasm_URL = "/main.wasm";
3 +const manifest_URL = "/manifest.json";
4 +
5 importScripts(wasm_exec_URL);
6
7 let loading = false;
8 let initError = null;
9 +let wasmManifest = null;
10 +let currentCacheVersion = null;
11 +
12 +// Fetch manifest to get current WASM filename
13 +async function fetchManifest() {
14 + if (wasmManifest) return wasmManifest;
15 +
16 + try {
17 + const response = await fetch(manifest_URL);
18 + if (!response.ok) {
19 + throw new Error(`Failed to fetch manifest: ${response.status}`);
20 + }
21 + wasmManifest = await response.json();
22 + currentCacheVersion = `WASM_Cache_${wasmManifest.hash}`;
23 + return wasmManifest;
24 + } catch (error) {
25 + console.error('[SW] Failed to fetch manifest:', error);
26 + throw error;
27 + }
28 +}
29
30 // Send error to all clients
31 async function notifyClientsOfError(error) {
@@ -45,9 +66,12 @@ async function runWASM() {
66 }
67
68 try {
69 + const manifest = await fetchManifest();
70 + const wasm_URL = `/${manifest.wasmFile}`;
71 +
72 const go = new Go();
73
50 - const cache = await caches.open("WASM_Cache_v1");
74 + const cache = await caches.open(currentCacheVersion);
75
76 let wasm_file;
77 const cache_wasm = await cache.match(wasm_URL);
@@ -76,10 +100,14 @@ self.addEventListener('install', (e) => {
100 self.skipWaiting();
101 async function LoadCache() {
102 try {
79 - const cache = await caches.open("WASM_Cache_v1");
103 + const manifest = await fetchManifest();
104 + const wasm_URL = `/${manifest.wasmFile}`;
105 + const cache = await caches.open(currentCacheVersion);
106 +
107 await cache.addAll([
108 wasm_URL,
109 wasm_exec_URL,
110 + manifest_URL,
111 ]);
112 } catch (error) {
113 console.error('[SW] Cache loading failed:', error);
@@ -92,6 +120,18 @@ self.addEventListener('install', (e) => {
120 self.addEventListener('activate', (e) => {
121 e.waitUntil((async () => {
122 try {
123 + // Delete old caches
124 + const manifest = await fetchManifest();
125 + const cacheNames = await caches.keys();
126 + await Promise.all(
127 + cacheNames.map(cacheName => {
128 + if (cacheName !== currentCacheVersion && cacheName.startsWith('WASM_Cache_')) {
129 + console.log('[SW] Deleting old cache:', cacheName);
130 + return caches.delete(cacheName);
131 + }
132 + })
133 + );
134 +
135 // Claim clients first to take control immediately
136 await self.clients.claim();
137
@@ -144,8 +184,9 @@ self.addEventListener('fetch', (e) => {
184 if (url.pathname === '/portal.mp4') {
185 e.respondWith((async () => {
186 try {
187 + await fetchManifest();
188 // Try to get from cache first
148 - const cache = await caches.open("WASM_Cache_v1");
189 + const cache = await caches.open(currentCacheVersion);
190 const cachedResponse = await cache.match('/portal.mp4');
191 if (cachedResponse) {
192 return cachedResponse;