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 });