refactor: implement infinite recovery system for service worker
Hee Sung Son committed
Nov 16, 2025 at 09:50 UTC
d71b42e57e0a00ae8824f6caad6f86d787b6b724
1 file changed
+620
-114
cmd/webclient/service-worker.js
+620
-114
@@ -1,21 +1,230 @@
1
//const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/lib/wasm/wasm_exec.js";
2
let BASE_PATH = "<PORTAL_UI_URL>";
3
let wasmManifestString = '"<WASM_MANIFEST>"';
4
-let wasmManifest = JSON.parse(wasmManifestString);
4
+let wasmManifest;
5
+
6
+// Debug mode detection (disable verbose logging in production)
7
+const DEBUG_MODE = self.location.hostname === 'localhost' ||
8
+ self.location.hostname === '127.0.0.1' ||
9
+ self.location.hostname.endsWith('.localhost');
10
+
11
+function debugLog(...args) {
12
+ if (DEBUG_MODE) {
13
+ console.log(...args);
14
+ }
15
+}
16
+
17
+// Parse manifest with error handling
18
+try {
19
+ wasmManifest = JSON.parse(wasmManifestString);
20
+ debugLog("[SW] Manifest parsed successfully:", wasmManifest);
21
+} catch (error) {
22
+ console.error("[SW] Failed to parse WASM manifest:", error);
23
+ console.error("[SW] Manifest string:", wasmManifestString);
24
+ // Use fallback manifest
25
+ wasmManifest = {
26
+ wasmFile: "main.wasm",
27
+ wasmUrl: null
28
+ };
29
+ console.warn("[SW] Using fallback manifest:", wasmManifest);
30
+}
31
32
let wasm_exec_URL = BASE_PATH + "/frontend/wasm_exec.js";
7
-if (new URL(BASE_PATH).protocol === "http:") {
8
- wasm_exec_URL = "/frontend/wasm_exec.js";
33
+try {
34
+ if (new URL(BASE_PATH).protocol === "http:") {
35
+ wasm_exec_URL = "/frontend/wasm_exec.js";
36
+ }
37
+ debugLog("[SW] Loading wasm_exec.js from:", wasm_exec_URL);
38
+ importScripts(wasm_exec_URL);
39
+ debugLog("[SW] wasm_exec.js loaded successfully");
40
+} catch (error) {
41
+ console.error("[SW] Failed to load wasm_exec.js:", error);
42
+ throw new Error(`Failed to load wasm_exec.js from ${wasm_exec_URL}: ${error.message}`);
43
}
10
-importScripts(wasm_exec_URL);
44
45
let loading = false;
46
let initError = null;
47
let _lastReload = Date.now();
48
+let initPromise = null; // Prevent concurrent initialization
49
+
50
+// Service Worker version for debugging
51
+const SW_VERSION = "1.0.0";
52
+
53
+debugLog(`[SW] Service Worker v${SW_VERSION} loaded`);
54
+
55
+// Service Worker readiness stages
56
+const ReadinessStage = {
57
+ UNINITIALIZED: 0, // No handlers available
58
+ WASM_LOADING: 1, // Loading in progress
59
+ WASM_LOADED: 2, // __go_jshttp available
60
+ READY: 3, // Both __go_jshttp and __sdk_message_handler available (fully operational)
61
+};
62
+
63
+let declaredStage = ReadinessStage.UNINITIALIZED; // What we think the stage is
64
+
65
+// Check handler availability
66
+function areHandlersAvailable() {
67
+ return {
68
+ http: typeof __go_jshttp !== "undefined",
69
+ sdk: typeof __sdk_message_handler !== "undefined"
70
+ };
71
+}
72
+
73
+// Compute actual stage based on runtime state
74
+function getCurrentStage() {
75
+ if (loading) {
76
+ return ReadinessStage.WASM_LOADING;
77
+ }
78
+
79
+ const { http, sdk } = areHandlersAvailable();
80
+
81
+ if (http && sdk) {
82
+ return ReadinessStage.READY;
83
+ } else if (http && !sdk) {
84
+ return ReadinessStage.WASM_LOADED;
85
+ } else {
86
+ return ReadinessStage.UNINITIALIZED;
87
+ }
88
+}
89
+
90
+// Check if error is recoverable (handler missing) or fatal (other errors)
91
+function isRecoverableError(error, currentStage, targetStage) {
92
+ // Handlers missing = recoverable (can retry infinitely)
93
+ if (targetStage === ReadinessStage.READY) {
94
+ const { http, sdk } = areHandlersAvailable();
95
+ if (!http || !sdk) {
96
+ return true;
97
+ }
98
+ }
99
+
100
+ // Check for fatal errors that we should not retry infinitely
101
+ const errorMsg = error.message.toLowerCase();
102
+
103
+ // Fatal errors - should throw immediately
104
+ if (errorMsg.includes('out of memory') ||
105
+ errorMsg.includes('rangeerror') ||
106
+ errorMsg.includes('404') ||
107
+ errorMsg.includes('403') ||
108
+ errorMsg.includes('invalid wasm') ||
109
+ errorMsg.includes('bad magic number')) {
110
+ return false;
111
+ }
112
+
113
+ // Temporary/recoverable errors - can retry
114
+ if (errorMsg.includes('timeout') ||
115
+ errorMsg.includes('network') ||
116
+ errorMsg.includes('fetch') ||
117
+ errorMsg.includes('offline')) {
118
+ return true;
119
+ }
120
16
-// Fetch manifest to get current WASM filename
17
-async function fetchManifest() {
18
- return wasmManifest;
121
+ // Default: if handlers are missing, it's recoverable
122
+ return currentStage < targetStage;
123
+}
124
+
125
+// Simple recovery system: check state → recover if needed → execute
126
+async function ensureReady(targetStage = ReadinessStage.READY) {
127
+ let attempt = 0;
128
+
129
+ while (true) {
130
+ // Step 1: Check current state
131
+ const currentStage = getCurrentStage();
132
+ if (currentStage >= targetStage) {
133
+ debugLog(`[SW] Already at stage ${currentStage}, ready`);
134
+ return;
135
+ }
136
+
137
+ // Step 2: Recover to desired state
138
+ try {
139
+ if (attempt > 0) {
140
+ const delay = Math.min(100 * Math.pow(2, attempt - 1), 5000);
141
+ debugLog(`[SW] Retry ${attempt + 1} after ${delay}ms...`);
142
+ await new Promise(resolve => setTimeout(resolve, delay));
143
+ }
144
+
145
+ // Reset state before recovery
146
+ declaredStage = ReadinessStage.UNINITIALIZED;
147
+ loading = false;
148
+ initError = null;
149
+
150
+ // Load WASM
151
+ await ensureStage(targetStage);
152
+
153
+ // Verify success
154
+ const finalStage = getCurrentStage();
155
+ if (finalStage >= targetStage) {
156
+ console.log(`[SW] Recovery successful, reached stage ${finalStage}`);
157
+ return;
158
+ }
159
+
160
+ throw new Error(`Recovery incomplete: expected ${targetStage}, got ${finalStage}`);
161
+ } catch (error) {
162
+ attempt++;
163
+ console.warn(`[SW] Recovery attempt ${attempt} failed:`, error.message);
164
+
165
+ // Check if this is a fatal error
166
+ const currentStageNow = getCurrentStage();
167
+ if (!isRecoverableError(error, currentStageNow, targetStage)) {
168
+ console.error(`[SW] Fatal error, cannot recover:`, error);
169
+ throw error;
170
+ }
171
+
172
+ // Continue loop for recoverable errors
173
+ }
174
+ }
175
+}
176
+
177
+// Sync declared stage with actual stage
178
+function syncStage() {
179
+ const actualStage = getCurrentStage();
180
+ if (declaredStage !== actualStage) {
181
+ debugLog(`[SW] Stage sync: ${declaredStage} -> ${actualStage}`);
182
+ declaredStage = actualStage;
183
+ }
184
+ return actualStage;
185
+}
186
+
187
+// Mobile detection and optimization
188
+const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
189
+const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
190
+const isAndroid = /Android/.test(navigator.userAgent);
191
+const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
192
+
193
+debugLog(`[SW] Platform: ${isMobile ? 'Mobile' : 'Desktop'}, iOS: ${isIOS}, Android: ${isAndroid}, Safari: ${isSafari}`);
194
+
195
+// Network utilities with retry logic
196
+async function fetchWithRetry(url, options = {}, maxRetries = 3) {
197
+ let lastError;
198
+
199
+ for (let i = 0; i < maxRetries; i++) {
200
+ try {
201
+ console.log(`[SW] Fetching ${url} (attempt ${i + 1}/${maxRetries})`);
202
+ const response = await fetch(url, options);
203
+
204
+ if (!response.ok) {
205
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
206
+ }
207
+
208
+ return response;
209
+ } catch (error) {
210
+ lastError = error;
211
+ console.warn(`[SW] Fetch attempt ${i + 1} failed:`, error.message);
212
+
213
+ // Don't retry on certain errors
214
+ if (error.message.includes('404') || error.message.includes('403')) {
215
+ throw error;
216
+ }
217
+
218
+ // Wait before retry (exponential backoff)
219
+ if (i < maxRetries - 1) {
220
+ const delay = Math.min(1000 * Math.pow(2, i), 5000);
221
+ console.log(`[SW] Retrying in ${delay}ms...`);
222
+ await new Promise(resolve => setTimeout(resolve, delay));
223
+ }
224
+ }
225
+ }
226
+
227
+ throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
228
}
229
230
// Send error to all clients
@@ -35,87 +244,356 @@ async function notifyClientsOfError(error) {
244
}
245
}
246
38
-async function init() {
39
- if (loading) return;
247
+// Stage-based initialization with automatic dependency resolution
248
+async function ensureStage(targetStage) {
249
+ // Sync stage before checking
250
+ const current = syncStage();
251
+ debugLog(`[SW] Ensuring stage: ${targetStage}, current: ${current}`);
252
+
253
+ // Already at or past the target stage
254
+ if (current >= targetStage) {
255
+ debugLog(`[SW] Already at stage ${current}, no action needed`);
256
+ return true;
257
+ }
258
+
259
+ // Recursive dependency resolution
260
+ switch (targetStage) {
261
+ case ReadinessStage.WASM_LOADED:
262
+ await ensureWASMLoaded();
263
+ break;
264
+
265
+ case ReadinessStage.READY:
266
+ // First ensure WASM is loaded
267
+ await ensureStage(ReadinessStage.WASM_LOADED);
268
+ await ensureHandlersRegistered();
269
+ break;
270
+ }
271
+
272
+ // Verify we reached the target stage
273
+ const finalStage = syncStage();
274
+ if (finalStage < targetStage) {
275
+ throw new Error(`Failed to reach stage ${targetStage}, stuck at ${finalStage}`);
276
+ }
277
+
278
+ return true;
279
+}
280
+
281
+// Ensure WASM is loaded
282
+async function ensureWASMLoaded() {
283
+ // Verify current stage
284
+ const current = syncStage();
285
+
286
+ // If already loaded, return immediately
287
+ if (current >= ReadinessStage.WASM_LOADED) {
288
+ debugLog("[SW] WASM already loaded (verified by handler check)");
289
+ return true;
290
+ }
291
+
292
+ // Prevent concurrent initialization attempts
293
+ if (initPromise) {
294
+ debugLog("[SW] Init already in progress, reusing existing promise");
295
+ await initPromise;
296
+ return syncStage() >= ReadinessStage.WASM_LOADED;
297
+ }
298
+
299
+ if (loading) {
300
+ debugLog("[SW] Init already loading, waiting...");
301
+ // Wait for loading to complete
302
+ while (loading && syncStage() < ReadinessStage.WASM_LOADED) {
303
+ await new Promise(resolve => setTimeout(resolve, 100));
304
+ }
305
+ return syncStage() >= ReadinessStage.WASM_LOADED;
306
+ }
307
+
308
loading = true;
41
- try {
42
- await runWASM();
43
- initError = null;
44
- } catch (error) {
45
- console.error("[SW] Error initializing WASM:", error);
46
- initError = error;
47
- await notifyClientsOfError(error);
48
- throw error; // Re-throw to prevent further processing
49
- } finally {
50
- // loading = false;
309
+ declaredStage = ReadinessStage.WASM_LOADING;
310
+
311
+ initPromise = (async () => {
312
+ try {
313
+ debugLog("[SW] Starting WASM initialization...");
314
+
315
+ await runWASM();
316
+
317
+ // Stage will be auto-synced based on handler availability
318
+ initError = null;
319
+
320
+ // Verify we actually reached the expected stage
321
+ const finalStage = syncStage();
322
+ debugLog(`[SW] WASM initialization complete, stage: ${finalStage}`);
323
+ } catch (error) {
324
+ console.error("[SW] Error initializing WASM:", error);
325
+ initError = error;
326
+ declaredStage = ReadinessStage.UNINITIALIZED;
327
+ loading = false;
328
+ await notifyClientsOfError(error);
329
+ throw error;
330
+ } finally {
331
+ initPromise = null;
332
+ // Don't reset loading here - leave it true until WASM exits
333
+ }
334
+ })();
335
+
336
+ await initPromise;
337
+ return syncStage() >= ReadinessStage.WASM_LOADED;
338
+}
339
+
340
+// Ensure handlers are registered
341
+async function ensureHandlersRegistered() {
342
+ // Verify current stage
343
+ const current = syncStage();
344
+
345
+ // Check if handlers exist
346
+ if (current >= ReadinessStage.READY) {
347
+ debugLog("[SW] Handlers already registered (verified by handler check)");
348
+ return true;
349
}
350
+
351
+ debugLog("[SW] Handlers not registered, waiting...");
352
+
353
+ // Wait for handlers to be registered (max 10 seconds)
354
+ let waitCount = 0;
355
+ const maxWait = 100;
356
+
357
+ while (waitCount < maxWait) {
358
+ const stage = syncStage();
359
+ if (stage >= ReadinessStage.READY) {
360
+ debugLog("[SW] Handlers registered successfully");
361
+ return true;
362
+ }
363
+
364
+ await new Promise(resolve => setTimeout(resolve, 100));
365
+ waitCount++;
366
+ }
367
+
368
+ // If handlers still not available, WASM might have failed
369
+ console.warn("[SW] Handlers not available after waiting, WASM may need reloading");
370
+ declaredStage = ReadinessStage.UNINITIALIZED;
371
+ loading = false;
372
+
373
+ // Retry WASM load
374
+ return await ensureWASMLoaded();
375
+}
376
+
377
+// Legacy init function for backward compatibility - with infinite retry
378
+async function init() {
379
+ return await ensureReady(ReadinessStage.READY);
380
}
381
382
async function runWASM() {
55
- if (typeof __go_jshttp !== "undefined") {
383
+ // Check actual runtime state, not just if handler exists
384
+ const currentStage = getCurrentStage();
385
+ if (currentStage >= ReadinessStage.WASM_LOADED) {
386
+ debugLog("[SW] WASM already loaded and verified");
387
return;
388
}
389
390
try {
60
- const manifest = await fetchManifest();
61
- // Use unified cache path from manifest (full URL)
391
+ // Determine WASM URL from manifest
392
let wasm_URL;
63
- if (manifest.wasmUrl && new URL(manifest.wasmUrl).protocol !== "http:") {
64
- wasm_URL = manifest.wasmUrl;
393
+ if (wasmManifest.wasmUrl && new URL(wasmManifest.wasmUrl).protocol !== "http:") {
394
+ wasm_URL = wasmManifest.wasmUrl;
395
} else {
66
- wasm_URL = `/frontend/${manifest.wasmFile}`;
396
+ wasm_URL = `/frontend/${wasmManifest.wasmFile}`;
397
}
398
+ debugLog("[SW] WASM URL:", wasm_URL);
399
400
+ // Create Go runtime
401
const go = new Go();
402
71
- const response = await fetch(wasm_URL);
72
- if (!response.ok) {
73
- throw new Error(
74
- `Failed to fetch WASM: ${response.status} ${response.statusText}`
403
+ // Fetch WASM file with retry logic
404
+ debugLog("[SW] Fetching WASM file...");
405
+ let instance;
406
+
407
+ // Set timeout for WASM instantiation (especially important on mobile)
408
+ const instantiateTimeout = isMobile ? 30000 : 15000; // 30s mobile, 15s desktop
409
+
410
+ try {
411
+ // Use compileStreaming if available (most efficient)
412
+ if (WebAssembly.compileStreaming) {
413
+ const response = await fetchWithRetry(wasm_URL, {}, isMobile ? 5 : 3);
414
+
415
+ // Check Content-Type before streaming
416
+ const contentType = response.headers.get('content-type') || '';
417
+ debugLog("[SW] WASM response Content-Type:", contentType);
418
+
419
+ if (contentType.includes('text/html')) {
420
+ throw new Error(
421
+ `Received HTML instead of WASM file. This usually means Service Worker is not properly intercepting requests. ` +
422
+ `Content-Type: ${contentType}, URL: ${wasm_URL}`
423
+ );
424
+ }
425
+
426
+ debugLog("[SW] WASM file fetched, size:", response.headers.get('content-length'), "bytes");
427
+
428
+ // Use instantiateStreaming for optimal performance
429
+ const instantiatePromise = WebAssembly.instantiateStreaming(
430
+ Promise.resolve(response),
431
+ go.importObject
432
+ );
433
+
434
+ const timeoutPromise = new Promise((_, reject) =>
435
+ setTimeout(() => reject(new Error(`WebAssembly instantiation timeout after ${instantiateTimeout}ms`)), instantiateTimeout)
436
+ );
437
+
438
+ instance = await Promise.race([instantiatePromise, timeoutPromise]);
439
+ debugLog("[SW] WebAssembly instantiated successfully via streaming");
440
+ }
441
+ } catch (streamError) {
442
+ // Fallback to traditional instantiate
443
+ console.warn("[SW] compileStreaming failed, falling back to traditional method:", streamError.message);
444
+
445
+ const response = await fetchWithRetry(wasm_URL, {}, isMobile ? 5 : 3);
446
+
447
+ // Check Content-Type to detect if we got HTML instead of WASM
448
+ const contentType = response.headers.get('content-type') || '';
449
+ debugLog("[SW] WASM response Content-Type:", contentType);
450
+
451
+ if (contentType.includes('text/html')) {
452
+ throw new Error(
453
+ `Received HTML instead of WASM file. Content-Type: ${contentType}, URL: ${wasm_URL}`
454
+ );
455
+ }
456
+
457
+ debugLog("[SW] WASM file fetched, size:", response.headers.get('content-length'), "bytes");
458
+
459
+ const wasm_file = await response.arrayBuffer();
460
+ debugLog("[SW] WASM ArrayBuffer size:", wasm_file.byteLength, "bytes");
461
+
462
+ // Additional validation: Check WASM magic number (0x00 0x61 0x73 0x6d)
463
+ const magicNumber = new Uint8Array(wasm_file, 0, 4);
464
+ if (magicNumber[0] !== 0x00 || magicNumber[1] !== 0x61 ||
465
+ magicNumber[2] !== 0x73 || magicNumber[3] !== 0x6d) {
466
+ // Try to detect if it's HTML
467
+ const decoder = new TextDecoder();
468
+ const firstBytes = decoder.decode(new Uint8Array(wasm_file, 0, Math.min(100, wasm_file.byteLength)));
469
+
470
+ if (firstBytes.includes('<!DOCTYPE') || firstBytes.includes('<html>')) {
471
+ throw new Error(
472
+ `Received HTML document instead of WASM file. ` +
473
+ `This indicates Service Worker is not active or not intercepting requests properly. ` +
474
+ `First bytes: ${firstBytes.substring(0, 50)}...`
475
+ );
476
+ } else {
477
+ throw new Error(
478
+ `Invalid WASM file (bad magic number). ` +
479
+ `Expected: [0x00, 0x61, 0x73, 0x6d], Got: [${Array.from(magicNumber).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')}]`
480
+ );
481
+ }
482
+ }
483
+ debugLog("[SW] WASM magic number validated");
484
+
485
+ // Instantiate WebAssembly with timeout
486
+ debugLog("[SW] Instantiating WebAssembly...");
487
+
488
+ const instantiatePromise = WebAssembly.instantiate(wasm_file, go.importObject);
489
+ const timeoutPromise = new Promise((_, reject) =>
490
+ setTimeout(() => reject(new Error(`WebAssembly instantiation timeout after ${instantiateTimeout}ms`)), instantiateTimeout)
491
);
76
- }
77
- const wasm_file = await response.arrayBuffer();
492
79
- const instance = await WebAssembly.instantiate(wasm_file, go.importObject);
493
+ instance = await Promise.race([instantiatePromise, timeoutPromise]);
494
+ debugLog("[SW] WebAssembly instantiated successfully");
495
+ }
496
497
const onExit = () => {
82
- console.log("[SW] Go Program Exited");
498
+ console.warn("[SW] Go Program Exited - handlers will be undefined");
499
__go_jshttp = undefined;
500
+ __sdk_message_handler = undefined;
501
loading = false;
502
+ initError = null;
503
+ syncStage(); // Auto-sync to UNINITIALIZED
504
};
505
506
+ // Run Go program
507
+ debugLog("[SW] Running Go program...");
508
go.run(instance.instance)
509
.then(onExit)
510
.catch((error) => {
90
- console.error("[SW] Go Program Error:", error);
511
+ console.error("[SW] Go Program Runtime Error:", error);
512
onExit();
513
});
514
+
515
+ debugLog("[SW] WASM initialization completed successfully");
516
} catch (error) {
94
- console.error("[SW] WASM initialization failed:", error);
95
- throw new Error(`WASM Initialization: ${error.message}`);
517
+ console.error("[SW] WASM initialization failed at:", error.stack || error);
518
+ console.error("[SW] Error details:", {
519
+ name: error.name,
520
+ message: error.message,
521
+ stack: error.stack
522
+ });
523
+
524
+ // Check for specific error types
525
+ let errorType = "unknown";
526
+ let userMessage = error.message;
527
+
528
+ if (error.message.includes("memory") || error.message.includes("RangeError")) {
529
+ errorType = "out_of_memory";
530
+ userMessage = "Not enough memory to load application. Please close other tabs and try again.";
531
+ console.error("[SW] Out of memory error detected");
532
+ } else if (error.message.includes("timeout")) {
533
+ errorType = "timeout";
534
+ userMessage = "Loading timed out. Please check your connection and try again.";
535
+ console.error("[SW] Timeout error detected");
536
+ } else if (error.message.includes("offline") || error.message.includes("Failed to fetch")) {
537
+ errorType = "network";
538
+ userMessage = "Network error. Please check your connection.";
539
+ console.error("[SW] Network error detected");
540
+ } else if (error.message.includes("HTML")) {
541
+ errorType = "service_worker_not_active";
542
+ userMessage = "Service Worker not active. Please refresh the page.";
543
+ console.error("[SW] Service Worker activation issue detected");
544
+ }
545
+
546
+ throw new Error(`WASM Initialization (${errorType}): ${userMessage}`);
547
}
548
}
549
550
self.addEventListener("install", (e) => {
100
- e.waitUntil(init());
101
- self.skipWaiting();
551
+ debugLog("[SW] Install event triggered");
552
+
553
+ e.waitUntil(
554
+ (async () => {
555
+ try {
556
+ await init();
557
+ // Only skipWaiting if initialization succeeded
558
+ // WARNING: skipWaiting() can cause version mismatch issues
559
+ // Consider removing this in production if updates can wait for page reload
560
+ await self.skipWaiting();
561
+ debugLog("[SW] Skipped waiting phase");
562
+ } catch (error) {
563
+ console.error("[SW] Installation failed:", error);
564
+ // Don't skipWaiting on error - let the old SW keep running
565
+ throw error;
566
+ }
567
+ })()
568
+ );
569
});
570
571
self.addEventListener("activate", (e) => {
572
+ debugLog("[SW] Activation event triggered");
573
+
574
e.waitUntil(
575
(async () => {
576
try {
577
+ // Delete old caches to free up space (especially important on mobile)
578
+ const cacheKeys = await caches.keys();
579
+ const oldCaches = cacheKeys.filter(key => key.startsWith('portal-') && key !== `portal-v${SW_VERSION}`);
580
+ if (oldCaches.length > 0) {
581
+ console.log(`[SW] Deleting ${oldCaches.length} old caches:`, oldCaches);
582
+ await Promise.all(oldCaches.map(key => caches.delete(key)));
583
+ }
584
+
585
// Claim clients first to take control immediately
586
await self.clients.claim();
587
+ debugLog("[SW] Clients claimed");
588
+
589
+ // Safari/iOS specific: Wait a bit before initializing WASM
590
+ if (isSafari || isIOS) {
591
+ debugLog("[SW] Safari/iOS detected, waiting 100ms before WASM init");
592
+ await new Promise(resolve => setTimeout(resolve, 100));
593
+ }
594
595
// Then initialize WASM in background (don't block activation)
112
- init().catch((error) => {
113
- console.error(
114
- "[SW] WASM initialization failed after activation:",
115
- error
116
- );
117
- notifyClientsOfError(error);
118
- });
596
+ await init();
597
} catch (error) {
598
console.error("[SW] Activation failed:", error);
599
await notifyClientsOfError(error);
@@ -135,6 +613,44 @@ async function broadcastToClients(message) {
613
// Expose to WASM
614
self.__sdk_post_message = broadcastToClients;
615
616
+// Periodic health check (only in debug mode or when errors occur)
617
+// Adjust interval based on mode: debug = 30s, production = 5min
618
+const healthCheckInterval = DEBUG_MODE ? 30000 : 5 * 60 * 1000;
619
+
620
+setInterval(() => {
621
+ const stage = syncStage();
622
+ const handlers = areHandlersAvailable();
623
+ const health = {
624
+ stage: stage,
625
+ stageName: Object.keys(ReadinessStage).find(key => ReadinessStage[key] === stage),
626
+ wasmActive: handlers.http,
627
+ sdkActive: handlers.sdk,
628
+ loading: loading,
629
+ initError: initError ? initError.message : null,
630
+ uptime: Date.now() - _lastReload
631
+ };
632
+
633
+ // Only log in debug mode or if there's an issue
634
+ if (DEBUG_MODE || !handlers.http || initError) {
635
+ debugLog("[SW] Health Check:", health);
636
+ }
637
+
638
+ // Auto-recovery if stage is too low
639
+ const recoveryStage = syncStage(); // Get actual stage for recovery check
640
+ if (recoveryStage < ReadinessStage.READY && !loading) {
641
+ // Allow recovery even if initError exists (clear it and try again)
642
+ if (initError) {
643
+ console.warn("[SW] Previous init error detected, clearing and retrying...", initError.message);
644
+ initError = null;
645
+ }
646
+ console.warn("[SW] Stage too low, attempting recovery...", health);
647
+ ensureStage(ReadinessStage.READY).catch(err => {
648
+ console.error("[SW] Recovery failed:", err);
649
+ initError = err; // Store new error
650
+ });
651
+ }
652
+}, healthCheckInterval);
653
+
654
self.addEventListener("message", (event) => {
655
if (event.data && event.data.type === "CLAIM_CLIENTS") {
656
self.clients
@@ -154,13 +670,31 @@ self.addEventListener("message", (event) => {
670
671
// Handle SDK messages (SDK_CONNECT, SDK_SEND, SDK_CLOSE)
672
if (event.data && event.data.type && event.data.type.startsWith("SDK_")) {
157
- if (typeof __sdk_message_handler === "undefined") {
158
- console.error("[SW] SDK message handler not available");
159
- return;
160
- }
673
+ (async () => {
674
+ try {
675
+ // Centralized recovery: Wait until handlers are ready
676
+ debugLog("[SW] SDK message received, ensuring handlers are ready...");
677
+ await ensureReady(ReadinessStage.READY);
678
+
679
+ // Handlers should now be available
680
+ if (typeof __sdk_message_handler === "undefined") {
681
+ throw new Error("SDK message handler still not available after centralized recovery");
682
+ }
683
162
- // Call WASM message handler
163
- __sdk_message_handler(event.data.type, event.data);
684
+ // Call WASM message handler
685
+ __sdk_message_handler(event.data.type, event.data);
686
+ } catch (error) {
687
+ console.error("[SW] SDK message handling failed:", error);
688
+ // Send error back to client
689
+ if (event.data.clientId) {
690
+ await broadcastToClients({
691
+ type: event.data.type.replace("SDK_", "SDK_") + "_ERROR",
692
+ clientId: event.data.clientId,
693
+ error: "Handler unavailable: " + error.message,
694
+ });
695
+ }
696
+ }
697
+ })();
698
}
699
});
700
@@ -173,29 +707,34 @@ self.addEventListener("fetch", (e) => {
707
return;
708
}
709
710
+ // Skip Service Worker infrastructure files (prevent infinite loop during initialization)
711
+ if (url.pathname.startsWith("/frontend/") ||
712
+ url.pathname === "/service-worker.js" ||
713
+ url.pathname === "/portal.mp4") {
714
+ e.respondWith(fetch(e.request));
715
+ return;
716
+ }
717
+
718
// Health check endpoint - check WASM status
719
if (url.pathname === "/e8c2c70c-ec4a-40b2-b8af-d5638264f831") {
720
e.respondWith(
721
(async () => {
180
- // Try to initialize if not ready
181
- if (typeof __go_jshttp === "undefined" && !loading) {
182
- try {
183
- await init();
184
- } catch (error) {
185
- console.error("[SW] Health check init failed:", error);
722
+ try {
723
+ // Centralized recovery: Wait until handlers are ready
724
+ await ensureReady(ReadinessStage.READY);
725
+
726
+ if (typeof __go_jshttp !== "undefined") {
727
+ return new Response("ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
728
+ status: 200,
729
+ });
730
}
731
+ } catch (error) {
732
+ console.error("[SW] Health check failed:", error);
733
}
734
189
- // Return status based on WASM availability
190
- if (typeof __go_jshttp !== "undefined") {
191
- return new Response("ACK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
192
- status: 200,
193
- });
194
- } else {
195
- return new Response("NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
196
- status: 503,
197
- });
198
- }
735
+ return new Response("NAK-e8c2c70c-ec4a-40b2-b8af-d5638264f831", {
736
+ status: 503,
737
+ });
738
})()
739
);
740
return;
@@ -203,63 +742,30 @@ self.addEventListener("fetch", (e) => {
742
743
e.respondWith(
744
(async () => {
206
- if (typeof __go_jshttp === "undefined" && !loading) {
207
- try {
208
- await init();
209
- } catch (error) {
210
- console.error("[SW] Init failed:", error);
211
- return new Response(
212
- "WASM initialization failed. Please refresh the page.",
213
- {
214
- status: 503,
215
- statusText: "Service Unavailable",
216
- }
217
- );
745
+ try {
746
+ // Centralized recovery: Wait until handlers are ready
747
+ debugLog("[SW] Fetch request received, ensuring handlers are ready...");
748
+ await ensureReady(ReadinessStage.READY);
749
+
750
+ // Handler should now be available
751
+ if (typeof __go_jshttp === "undefined") {
752
+ throw new Error("__go_jshttp still not available after centralized recovery");
753
}
219
- }
754
221
- // Wait for WASM to be ready (increased timeout for Safari)
222
- let waitCount = 0;
223
- const maxWait = 100; // 10 seconds (100 × 100ms)
224
- while (typeof __go_jshttp === "undefined" && waitCount < maxWait) {
225
- await new Promise((resolve) => setTimeout(resolve, 100));
226
- waitCount++;
227
- }
755
+ // Process request
756
+ const resp = await __go_jshttp(e.request);
757
+ return resp;
758
+ } catch (error) {
759
+ console.error("[SW] Request handling failed:", error);
760
229
- // If still not ready after timeout, return error
230
- if (typeof __go_jshttp === "undefined") {
231
- console.error("[SW] WASM not ready after timeout");
761
return new Response(
233
- "WASM initialization timeout. Please refresh the page.",
762
+ "Service temporarily unavailable. Please refresh the page.",
763
{
764
status: 503,
765
statusText: "Service Unavailable",
766
}
767
);
768
}
240
-
241
- try {
242
- const resp = await __go_jshttp(e.request);
243
- return resp;
244
- } catch (error) {
245
- console.error("[SW] Request handling error:", error);
246
- __go_jshttp = undefined;
247
- await init();
248
-
249
- // Wait again after reinit
250
- waitCount = 0;
251
- while (typeof __go_jshttp === "undefined" && waitCount < maxWait) {
252
- await new Promise((resolve) => setTimeout(resolve, 100));
253
- waitCount++;
254
- }
255
-
256
- if (typeof __go_jshttp === "undefined") {
257
- throw new Error("WASM reinitialization failed");
258
- }
259
-
260
- const resp = await __go_jshttp(e.request);
261
- return resp;
262
- }
769
})()
770
);
771
});