diff --git a/.changeset/fix-media-chunk-errors.md b/.changeset/fix-media-chunk-errors.md new file mode 100644 index 000000000..d6c55186a --- /dev/null +++ b/.changeset/fix-media-chunk-errors.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fixed unhandled promise rejections in media blob cache and added automatic retry for chunk loading failures after deployments. diff --git a/src/app/hooks/useBlobCache.ts b/src/app/hooks/useBlobCache.ts index a3d5493cf..7b40e292c 100644 --- a/src/app/hooks/useBlobCache.ts +++ b/src/app/hooks/useBlobCache.ts @@ -23,15 +23,21 @@ export function useBlobCache(url?: string): string | undefined { const fetchBlob = async () => { if (inflightRequests.has(url)) { - const existingBlobUrl = await inflightRequests.get(url); - if (isMounted) setCacheState({ sourceUrl: url, blobUrl: existingBlobUrl }); + try { + const existingBlobUrl = await inflightRequests.get(url); + if (isMounted) setCacheState({ sourceUrl: url, blobUrl: existingBlobUrl }); + } catch { + // Inflight request failed, silently ignore (consistent with fetchBlob behavior) + } return; } const requestPromise = (async () => { try { const res = await fetch(url, { mode: 'cors' }); - if (!res.ok) throw new Error(); + if (!res.ok) { + throw new Error(`Failed to fetch blob: ${res.status} ${res.statusText}`); + } const blob = await res.blob(); const objectUrl = URL.createObjectURL(blob); diff --git a/src/index.tsx b/src/index.tsx index 76ebaf91b..3248458ba 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -139,6 +139,41 @@ const injectIOSMetaTags = () => { injectIOSMetaTags(); +// Handle chunk loading failures with automatic retry +const CHUNK_RETRY_KEY = 'cinny_chunk_retry_count'; +const MAX_CHUNK_RETRIES = 2; + +window.addEventListener('error', (event) => { + // Check if this is a chunk loading error + const isChunkLoadError = + event.message?.includes('dynamically imported module') || + event.message?.includes('Failed to fetch') || + event.error?.name === 'ChunkLoadError'; + + if (isChunkLoadError) { + const retryCount = parseInt(sessionStorage.getItem(CHUNK_RETRY_KEY) ?? '0', 10); + + if (retryCount < MAX_CHUNK_RETRIES) { + // Increment retry count and reload + sessionStorage.setItem(CHUNK_RETRY_KEY, String(retryCount + 1)); + log.warn(`Chunk load failed, reloading (attempt ${retryCount + 1}/${MAX_CHUNK_RETRIES})`); + window.location.reload(); + + // Prevent default error handling since we're reloading + event.preventDefault(); + } else { + // Max retries exceeded, clear counter and let error bubble up + sessionStorage.removeItem(CHUNK_RETRY_KEY); + log.error('Chunk load failed after max retries, showing error'); + } + } +}); + +// Clear chunk retry counter on successful page load +window.addEventListener('load', () => { + sessionStorage.removeItem(CHUNK_RETRY_KEY); +}); + const mountApp = () => { const rootContainer = document.getElementById('root'); diff --git a/src/sw.ts b/src/sw.ts index 9f3314889..4486f418c 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -91,9 +91,11 @@ async function cleanupDeadClients() { function setSession(clientId: string, accessToken: unknown, baseUrl: unknown) { if (typeof accessToken === 'string' && typeof baseUrl === 'string') { sessions.set(clientId, { accessToken, baseUrl }); + console.debug('[SW] setSession: stored', clientId, baseUrl); } else { // Logout or invalid session sessions.delete(clientId); + console.debug('[SW] setSession: removed', clientId); } const resolveSession = clientToResolve.get(clientId); @@ -124,12 +126,18 @@ async function requestSessionWithTimeout( timeoutMs = 3000 ): Promise { const client = await self.clients.get(clientId); - if (!client) return undefined; + if (!client) { + console.warn('[SW] requestSessionWithTimeout: client not found', clientId); + return undefined; + } const sessionPromise = requestSession(client); const timeout = new Promise((resolve) => { - setTimeout(() => resolve(undefined), timeoutMs); + setTimeout(() => { + console.warn('[SW] requestSessionWithTimeout: timed out after', timeoutMs, 'ms', clientId); + resolve(undefined); + }, timeoutMs); }); return Promise.race([sessionPromise, timeout]); @@ -274,6 +282,11 @@ self.addEventListener('fetch', (event: FetchEvent) => { if (s && validMediaRequest(url, s.baseUrl)) { return fetch(url, { ...fetchConfig(s.accessToken), redirect }); } + console.warn( + '[SW fetch] No valid session for media request', + { url, clientId, hasSession: !!s }, + 'falling back to unauthenticated fetch' + ); return fetch(event.request); }) );