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;