From 662ddfb93244dc1900c10ba6711f0e87a0f96ca0 Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 17 Dec 2025 18:48:05 +0200 Subject: [PATCH 1/9] testing sw.js for improvisations --- public/sw.js | 263 +++++++++++++++++++++------------------------------ 1 file changed, 108 insertions(+), 155 deletions(-) diff --git a/public/sw.js b/public/sw.js index 21a57f712de..240904b1c76 100644 --- a/public/sw.js +++ b/public/sw.js @@ -16,48 +16,58 @@ console.log(`[SW v${version}] Service Worker loaded.`); // -------------------- // Helper Functions // -------------------- -const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; -const getOfflinePageUrl = service => `/${service}/offline`; +const getServiceFromUrl = url => { + try { + return new URL(url).pathname.split('/')[1]; + } catch { + return null; + } +}; + +const getOfflinePagePath = service => `/${service}/offline`; + +const getOfflinePageUrl = service => + new URL(getOfflinePagePath(service), self.location.origin).href; const cacheResource = async (cache, url) => { try { const response = await fetch(url); if (response.ok) await cache.put(url, response.clone()); return response; - } catch (err) { - console.error(`[SW v${version}] Failed to cache ${url}:`, err); + } catch { return null; } }; const cacheOfflinePageAndResources = async service => { + if (!service) return; + const cache = await caches.open(cacheName); - const offlinePageUrl = new URL( - getOfflinePageUrl(service), - self.location.origin, - ).href; - if (await cache.match(offlinePageUrl)) return; + const offlineUrl = getOfflinePageUrl(service); - const resp = await cacheResource(cache, offlinePageUrl); - if (!resp || !resp.ok) return; + if (await cache.match(offlineUrl)) return; - console.log(`[SW v${version}] Cached offline page for ${service}`); + const resp = await cacheResource(cache, offlineUrl); + if (!resp?.ok) return; const html = await resp.text(); - const scriptSrcs = [ + + const resources = [ ...html.matchAll(/]+src=["']([^"']+)["']/g), - ].map(m => m[1]); - const linkHrefs = [...html.matchAll(/]+href=["']([^"']+)["']/g)].map( - m => m[1], - ); - const resources = [...scriptSrcs, ...linkHrefs] + ...html.matchAll(/]+href=["']([^"']+)["']/g), + ] + .map(m => m[1]) .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) .map(url => new URL(url, self.location.origin).href); await Promise.allSettled(resources.map(url => cacheResource(cache, url))); + + console.log(`[SW v${version}] Cached offline page for ${service}`); }; -// Cache patterns +// -------------------- +// Cacheable file patterns +// -------------------- const CACHEABLE_FILES = [ // Reverb /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, @@ -65,7 +75,7 @@ const CACHEABLE_FILES = [ 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', // Fonts /\.woff2$/, - // Frosted Promo (test and live environments only) + // Frosted Promo /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, // Moment /\/moment-lib+.*?\.js$/, @@ -74,46 +84,16 @@ const CACHEABLE_FILES = [ ]; const WEBP_IMAGE = - /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; + /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+\.webp$/; -// -------------Install event ------- -self.addEventListener('install', event => { +// -------------------- +// Install / Activate +// -------------------- +self.addEventListener('install', () => { console.log(`[SW v${version}] Installing...`); - - event.waitUntil( - (async () => { - const cache = await caches.open(cacheName); - const clients = await self.clients.matchAll({ type: 'window' }); - - // Get unique services from PWA clients only - const pwaServices = [ - ...new Set( - clients - .filter(client => pwaClients.get(client.id)) - .map(client => getServiceFromUrl(client.url)) - .filter(Boolean), - ), - ]; - - if (pwaServices.length > 0) { - console.log( - `[SW v${version}] Caching offline pages for PWA:`, - pwaServices, - ); - } - - // Cache offline pages for PWA services only - await Promise.allSettled( - pwaServices.map(async service => { - return cacheOfflinePageAndResources(service); - }), - ); - self.skipWaiting(); - })(), - ); + self.skipWaiting(); }); -// -------Activate Handler------------- self.addEventListener('activate', event => { console.log(`[SW v${version}] Activating...`); event.waitUntil( @@ -127,129 +107,102 @@ self.addEventListener('activate', event => { ); }); -// -------Message Event------------- -self.addEventListener('message', async event => { - console.log(`[SW v${version}] Message received:`, event.data); - - if (event.data?.type === 'PWA_STATUS') { - const clientId = event.source.id; - const { isPWA } = event.data; - pwaClients.set(clientId, isPWA); - - if (isPWA) { - const cache = await caches.open(cacheName); - await cache.put('pwa_installed', new Response('true')); - const service = getServiceFromUrl(event.source.url); - await cacheOfflinePageAndResources(service); - } else { - const cache = await caches.open(cacheName); - await cache.delete('pwa_installed'); - } - } -}); - -// -------Fetch Handler------------- -const fetchEventHandler = async event => { - console.log(`[SW FETCH] Request: ${event.request.url}`); - const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => - new RegExp(cacheableFile).test(event.request.url), - ); +// -------------------- +// Message Handler +// -------------------- +self.addEventListener('message', event => { + if (event.data?.type !== 'PWA_STATUS') return; - const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); + const clientId = event.source?.id; + if (!clientId) return; - if (isRequestForWebpImage) { - const req = event.request.clone(); + if (event.data.isPWA) { + pwaClients.set(clientId, true); - // Inspect the accept header for WebP support + const service = getServiceFromUrl(event.source.url); + event.waitUntil(cacheOfflinePageAndResources(service)); + } else { + pwaClients.delete(clientId); + } +}); - const supportsWebp = - req.headers.has('accept') && req.headers.get('accept').includes('webp'); +// -------------------- +// Fetch Handler +// -------------------- +self.addEventListener('fetch', event => { + const { request } = event; - // if supports webp is false in request header then don't use it - // if accept header doesn't indicate support for webp remove .webp extension + // -------- WebP handling -------- + if (WEBP_IMAGE.test(request.url)) { + const acceptsWebp = request.headers.get('accept')?.includes('webp'); - if (!supportsWebp) { - const imageUrlWithoutWebp = req.url.replace('.webp', ''); - event.respondWith( - fetch(imageUrlWithoutWebp, { - mode: 'no-cors', - }), - ); + if (!acceptsWebp) { + const fallbackUrl = request.url.replace('.webp', ''); + event.respondWith(fetch(fallbackUrl, { mode: 'no-cors' })); + return; } - } else if (isRequestForCacheableFile) { + } + + // -------- Cacheable static files -------- + const isCacheable = CACHEABLE_FILES.some(pattern => + new RegExp(pattern).test(request.url), + ); + + if (isCacheable) { event.respondWith( (async () => { const cache = await caches.open(cacheName); - let response = await cache.match(event.request); - if (!response) { - response = await fetch(event.request.url); - cache.put(event.request, response.clone()); + const cached = await cache.match(request); + if (cached) return cached; + + const response = await fetch(request); + if (response.ok) { + cache.put(request, response.clone()); } return response; })(), ); - } else if (event.request.mode === 'navigate') { - const { url } = event.request; - console.log(`[SW FETCH] Navigation: ${url}`); + return; + } + // -------- Navigation (PWA vs Website) -------- + if (request.mode === 'navigate') { event.respondWith( (async () => { + let isPWA = false; + + if (event.clientId) { + const client = await self.clients.get(event.clientId); + isPWA = + pwaClients.has(event.clientId) || + client?.displayMode === 'standalone'; + } + + // Website → browser handles offline + if (!isPWA) { + return fetch(request); + } + + // PWA navigation try { - // Use preload if available - const preloadResp = await event.preloadResponse; - if (preloadResp) return preloadResp; - - const networkResp = await fetch(event.request); - - // Cache offline page if in PWA mode - if (networkResp && networkResp.ok && event.clientId) { - console.log('[SW] Caching offline page if PWA if network is ok'); - const client = await self.clients.get(event.clientId); - const isPWA = client && pwaClients.get(client.id); - if (isPWA) { - const service = getServiceFromUrl(url); - cacheOfflinePageAndResources(service).catch(err => - console.error('[SW] Cache offline fail:', err), - ); - } - } - - return networkResp; - } catch (err) { - console.log('[SW] Navigation failed:', url, err); + const preload = await event.preloadResponse; + if (preload) return preload; + return await fetch(request); + } catch { const cache = await caches.open(cacheName); - const pwaMarker = await cache.match('pwa_installed'); - console.log('[SW] PWA Marker:', pwaMarker); - - // Only show offline page for installed PWA - if (pwaMarker) { - const service = getServiceFromUrl(url); - const offlineUrl = new URL( - getOfflinePageUrl(service), - self.location.origin, - ).href; - - const cachedOffline = await cache.match(offlineUrl); - if (cachedOffline) { - return cachedOffline; - } - } - - // Canonical site offline fallback - return new Response( - 'You are offline. Please check your network and reload the page', - { + const service = getServiceFromUrl(request.url); + const offlineUrl = getOfflinePageUrl(service); + + return ( + (await cache.match(offlineUrl)) || + new Response('You are offline. Please reconnect.', { status: 503, headers: { 'Content-Type': 'text/plain' }, - }, + }) ); } })(), ); } - - return; -}; - -onfetch = fetchEventHandler; +}); From 0bd5d8d353af62f04c849cf4169918f10da412e9 Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 17 Dec 2025 19:59:56 +0200 Subject: [PATCH 2/9] test 2 with no install code --- public/sw.js | 320 +++++++++++++++++++++++++++------------------------ 1 file changed, 167 insertions(+), 153 deletions(-) diff --git a/public/sw.js b/public/sw.js index 240904b1c76..4a77bdc4494 100644 --- a/public/sw.js +++ b/public/sw.js @@ -8,66 +8,104 @@ const version = 'v0.3.1'; const cacheName = 'simorghCache_v1'; -// Track PWA clients -const pwaClients = new Map(); - -console.log(`[SW v${version}] Service Worker loaded.`); - -// -------------------- -// Helper Functions -// -------------------- -const getServiceFromUrl = url => { - try { - return new URL(url).pathname.split('/')[1]; - } catch { - return null; - } -}; - -const getOfflinePagePath = service => `/${service}/offline`; +const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; +const getOfflinePagePath = serviceName => `/${serviceName}/offline`; +const getOfflinePageUrl = serviceName => + new URL(getOfflinePagePath(serviceName), self.location.origin).href; -const getOfflinePageUrl = service => - new URL(getOfflinePagePath(service), self.location.origin).href; +const cacheOfflinePageAndResources = async serviceName => { + if (!serviceName) return; -const cacheResource = async (cache, url) => { - try { - const response = await fetch(url); - if (response.ok) await cache.put(url, response.clone()); - return response; - } catch { - return null; - } + const cache = await caches.open(cacheName); + const offlinePageUrl = getOfflinePageUrl(serviceName); + + const response = await fetch(offlinePageUrl); + if (!response?.ok) return; + console.log(`[SW v${version}] Caching offline page for ${serviceName}`); + + await cache.put(offlinePageUrl, response.clone()); + + const html = await response.text(); + const scriptMatches = html.matchAll(/]+src=["']([^"']+)["']/gi); + const linkMatches = html.matchAll(/]+href=["']([^"']+)["'][^>]*>/gi); + + const scriptUrls = Array.from(scriptMatches) + .map(match => match[1]) + .filter(src => src && !src.startsWith('http')) + .map(src => new URL(src, self.location.origin).href); + + const linkUrls = Array.from(linkMatches) + .map(match => match[1]) + .filter( + href => + href && + !href.startsWith('http') && + (href.endsWith('.css') || href.includes('stylesheet')), + ) + .map(href => new URL(href, self.location.origin).href); + + const resourcesToCache = [...scriptUrls, ...linkUrls]; + + await Promise.all( + resourcesToCache.map(async url => { + try { + const res = await fetch(url); + if (res.ok) { + await cache.put(url, res); + } + } catch (error) { + // Ignore failed resource + } + }), + ); }; -const cacheOfflinePageAndResources = async service => { - if (!service) return; - - const cache = await caches.open(cacheName); - const offlineUrl = getOfflinePageUrl(service); +// Track which clients are in PWA mode +const pwaClients = new Set(); - if (await cache.match(offlineUrl)) return; +// -------Message Event------------- +// Listen for messages from clients about their display mode +self.addEventListener('message', event => { + console.log(`[SW v${version}] Message received:`, event.data); - const resp = await cacheResource(cache, offlineUrl); - if (!resp?.ok) return; + if (event.data && event.data.type === 'PWA_STATUS') { + const clientId = event.source?.id; + if (!clientId) return; - const html = await resp.text(); + if (event.data.isPWA) { + pwaClients.add(clientId); - const resources = [ - ...html.matchAll(/]+src=["']([^"']+)["']/g), - ...html.matchAll(/]+href=["']([^"']+)["']/g), - ] - .map(m => m[1]) - .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) - .map(url => new URL(url, self.location.origin).href); + const serviceName = getServiceFromUrl(event.source?.url); + cacheOfflinePageAndResources(serviceName).catch(() => null); + } else { + pwaClients.delete(clientId); + } + } +}); - await Promise.allSettled(resources.map(url => cacheResource(cache, url))); +// -------------Install event ------- +self.addEventListener('install', () => { + console.log(`[SW v${version}] Installing...`); + self.skipWaiting(); +}); - console.log(`[SW v${version}] Cached offline page for ${service}`); -}; +// -------Activate Handler------------- +self.addEventListener('activate', event => { + console.log(`[SW v${version}] Activating...`); + event.waitUntil( + (async () => { + // Delete old caches + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames + .filter(name => name !== cacheName) + .map(name => caches.delete(name)), + ); + await self.clients.claim(); + })(), + ); +}); -// -------------------- -// Cacheable file patterns -// -------------------- const CACHEABLE_FILES = [ // Reverb /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, @@ -75,7 +113,7 @@ const CACHEABLE_FILES = [ 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', // Fonts /\.woff2$/, - // Frosted Promo + // Frosted Promo (test and live environments only) /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, // Moment /\/moment-lib+.*?\.js$/, @@ -84,125 +122,101 @@ const CACHEABLE_FILES = [ ]; const WEBP_IMAGE = - /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+\.webp$/; + /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; -// -------------------- -// Install / Activate -// -------------------- -self.addEventListener('install', () => { - console.log(`[SW v${version}] Installing...`); - self.skipWaiting(); -}); +// -------Fetch Handler------------- -self.addEventListener('activate', event => { - console.log(`[SW v${version}] Activating...`); - event.waitUntil( - (async () => { - const keys = await caches.keys(); - await Promise.all( - keys.map(key => key !== cacheName && caches.delete(key)), - ); - await self.clients.claim(); - })(), - ); -}); +const fetchEventHandler = async event => { + const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => { + if (cacheableFile instanceof RegExp) { + return cacheableFile.test(event.request.url); + } -// -------------------- -// Message Handler -// -------------------- -self.addEventListener('message', event => { - if (event.data?.type !== 'PWA_STATUS') return; + return event.request.url === cacheableFile; + }); - const clientId = event.source?.id; - if (!clientId) return; + const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); - if (event.data.isPWA) { - pwaClients.set(clientId, true); + if (isRequestForWebpImage) { + const req = event.request.clone(); - const service = getServiceFromUrl(event.source.url); - event.waitUntil(cacheOfflinePageAndResources(service)); - } else { - pwaClients.delete(clientId); - } -}); + // Inspect the accept header for WebP support -// -------------------- -// Fetch Handler -// -------------------- -self.addEventListener('fetch', event => { - const { request } = event; + const supportsWebp = + req.headers.has('accept') && req.headers.get('accept').includes('webp'); - // -------- WebP handling -------- - if (WEBP_IMAGE.test(request.url)) { - const acceptsWebp = request.headers.get('accept')?.includes('webp'); + // if supports webp is false in request header then don't use it + // if accept header doesn't indicate support for webp remove .webp extension - if (!acceptsWebp) { - const fallbackUrl = request.url.replace('.webp', ''); - event.respondWith(fetch(fallbackUrl, { mode: 'no-cors' })); - return; + if (!supportsWebp) { + const imageUrlWithoutWebp = req.url.replace('.webp', ''); + event.respondWith( + fetch(imageUrlWithoutWebp, { + mode: 'no-cors', + }), + ); } - } - - // -------- Cacheable static files -------- - const isCacheable = CACHEABLE_FILES.some(pattern => - new RegExp(pattern).test(request.url), - ); - - if (isCacheable) { + } else if (isRequestForCacheableFile) { event.respondWith( (async () => { const cache = await caches.open(cacheName); - const cached = await cache.match(request); - if (cached) return cached; - - const response = await fetch(request); - if (response.ok) { - cache.put(request, response.clone()); + let response = await cache.match(event.request); + if (!response) { + try { + response = await fetch(event.request.url); + cache.put(event.request, response.clone()); + } catch (error) { + // File not in cache and network unavailable + return new Response('', { + status: 408, + statusText: + 'You are offline . Please check your network and reload the page', + }); + } } return response; })(), ); - return; - } - - // -------- Navigation (PWA vs Website) -------- - if (request.mode === 'navigate') { - event.respondWith( - (async () => { - let isPWA = false; - - if (event.clientId) { - const client = await self.clients.get(event.clientId); - isPWA = - pwaClients.has(event.clientId) || - client?.displayMode === 'standalone'; - } - - // Website → browser handles offline - if (!isPWA) { - return fetch(request); - } - - // PWA navigation - try { - const preload = await event.preloadResponse; - if (preload) return preload; - - return await fetch(request); - } catch { - const cache = await caches.open(cacheName); - const service = getServiceFromUrl(request.url); - const offlineUrl = getOfflinePageUrl(service); - - return ( - (await cache.match(offlineUrl)) || - new Response('You are offline. Please reconnect.', { - status: 503, - headers: { 'Content-Type': 'text/plain' }, - }) - ); - } - })(), + } else if (event.request.mode === 'navigate') { + const clientId = event.clientId || event.resultingClientId; + const isInPWAMode = clientId && pwaClients.has(clientId); + console.log( + '[SW] Fetch event for navigation. isInPWAMode:', + isInPWAMode, + clientId, ); + // Only intercept navigation for PWA clients to avoid loop in browser mode when offline + if (isInPWAMode) { + event.respondWith( + (async () => { + try { + const preloadResponse = await event.preloadResponse; + if (preloadResponse) return preloadResponse; + return await fetch(event.request); + } catch (error) { + console.log('[SW] Navigation failed:', event.request.url, error); + const cache = await caches.open(cacheName); + const serviceName = getServiceFromUrl(event.request.url); + const offlinePageUrl = getOfflinePageUrl(serviceName); + const cachedResponse = await cache.match(offlinePageUrl); + + return ( + cachedResponse || + new Response( + 'You are offline. Please check your network and reload the page', + { + status: 503, + headers: { 'Content-Type': 'text/plain' }, + }, + ) + ); + } + })(), + ); + } } -}); + // For all other requests, let the browser handle it normally + return; +}; + +self.addEventListener('fetch', fetchEventHandler); From 02c270d4b0e879ec43716657c5ef40611f1032c3 Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 17 Dec 2025 21:07:50 +0200 Subject: [PATCH 3/9] test 3 -isInPWAMode more stricter with displaymode=standalone --- public/sw.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/public/sw.js b/public/sw.js index 4a77bdc4494..3703e3c6279 100644 --- a/public/sw.js +++ b/public/sw.js @@ -179,22 +179,42 @@ const fetchEventHandler = async event => { ); } else if (event.request.mode === 'navigate') { const clientId = event.clientId || event.resultingClientId; - const isInPWAMode = clientId && pwaClients.has(clientId); + + // detect PWA using both pwaClients set and displayMode + let isInPWAMode = false; + const client = await self.clients.get(clientId); + + if (clientId) { + const clientInfo = await self.clients.get(clientId); + isInPWAMode = + pwaClients.has(clientId) || clientInfo?.displayMode === 'standalone'; + } + console.log( '[SW] Fetch event for navigation. isInPWAMode:', isInPWAMode, clientId, + client?.displayMode, ); - // Only intercept navigation for PWA clients to avoid loop in browser mode when offline + if (isInPWAMode) { event.respondWith( (async () => { try { const preloadResponse = await event.preloadResponse; if (preloadResponse) return preloadResponse; - return await fetch(event.request); + + const networkResp = await fetch(event.request); + + // Dynamically cache offline page for PWA clients + if (networkResp.ok && clientId) { + pwaClients.add(clientId); + const serviceName = getServiceFromUrl(event.request.url); + cacheOfflinePageAndResources(serviceName).catch(() => null); + } + + return networkResp; } catch (error) { - console.log('[SW] Navigation failed:', event.request.url, error); const cache = await caches.open(cacheName); const serviceName = getServiceFromUrl(event.request.url); const offlinePageUrl = getOfflinePageUrl(serviceName); @@ -204,10 +224,7 @@ const fetchEventHandler = async event => { cachedResponse || new Response( 'You are offline. Please check your network and reload the page', - { - status: 503, - headers: { 'Content-Type': 'text/plain' }, - }, + { status: 503, headers: { 'Content-Type': 'text/plain' } }, ) ); } @@ -219,4 +236,4 @@ const fetchEventHandler = async event => { return; }; -self.addEventListener('fetch', fetchEventHandler); +onfetch = fetchEventHandler; From dc41d06a381bafdf6af53257a85ef6a108f03946 Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 18 Dec 2025 13:50:39 +0200 Subject: [PATCH 4/9] test -4 - removed pwa_instaleld var , used pendingnaviagtion queue instead --- public/sw.js | 295 +++++++++++++++++++++++++-------------------------- 1 file changed, 145 insertions(+), 150 deletions(-) diff --git a/public/sw.js b/public/sw.js index 3703e3c6279..e14c15e6335 100644 --- a/public/sw.js +++ b/public/sw.js @@ -8,80 +8,79 @@ const version = 'v0.3.1'; const cacheName = 'simorghCache_v1'; -const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; -const getOfflinePagePath = serviceName => `/${serviceName}/offline`; -const getOfflinePageUrl = serviceName => - new URL(getOfflinePagePath(serviceName), self.location.origin).href; - -const cacheOfflinePageAndResources = async serviceName => { - if (!serviceName) return; - - const cache = await caches.open(cacheName); - const offlinePageUrl = getOfflinePageUrl(serviceName); +// Track PWA clients +const pwaClients = new Map(); - const response = await fetch(offlinePageUrl); - if (!response?.ok) return; - console.log(`[SW v${version}] Caching offline page for ${serviceName}`); +// Track pending navigation fetches until we know PWA status +const pendingNavigations = new Map(); - await cache.put(offlinePageUrl, response.clone()); +console.log(`[SW v${version}] Service Worker loaded.`); - const html = await response.text(); - const scriptMatches = html.matchAll(/]+src=["']([^"']+)["']/gi); - const linkMatches = html.matchAll(/]+href=["']([^"']+)["'][^>]*>/gi); +// -------------------- +// Helper Functions +// -------------------- +const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; +const getOfflinePageUrl = service => `/${service}/offline`; + +const cacheResource = async (cache, url) => { + try { + const response = await fetch(url); + if (response.ok) await cache.put(url, response.clone()); + return response; + } catch (err) { + console.error(`[SW v${version}] Failed to cache ${url}:`, err); + return null; + } +}; - const scriptUrls = Array.from(scriptMatches) - .map(match => match[1]) - .filter(src => src && !src.startsWith('http')) - .map(src => new URL(src, self.location.origin).href); +const cacheOfflinePageAndResources = async service => { + const cache = await caches.open(cacheName); + const offlinePageUrl = new URL( + getOfflinePageUrl(service), + self.location.origin, + ).href; + if (await cache.match(offlinePageUrl)) return; - const linkUrls = Array.from(linkMatches) - .map(match => match[1]) - .filter( - href => - href && - !href.startsWith('http') && - (href.endsWith('.css') || href.includes('stylesheet')), - ) - .map(href => new URL(href, self.location.origin).href); + const resp = await cacheResource(cache, offlinePageUrl); + if (!resp || !resp.ok) return; - const resourcesToCache = [...scriptUrls, ...linkUrls]; + console.log(`[SW v${version}] Cached offline page for ${service}`); - await Promise.all( - resourcesToCache.map(async url => { - try { - const res = await fetch(url); - if (res.ok) { - await cache.put(url, res); - } - } catch (error) { - // Ignore failed resource - } - }), + const html = await resp.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + const scriptSrcs = Array.from(doc.querySelectorAll('script[src]')).map(el => + el.getAttribute('src'), + ); + const linkHrefs = Array.from(doc.querySelectorAll('link[href]')).map(el => + el.getAttribute('href'), ); -}; - -// Track which clients are in PWA mode -const pwaClients = new Set(); -// -------Message Event------------- -// Listen for messages from clients about their display mode -self.addEventListener('message', event => { - console.log(`[SW v${version}] Message received:`, event.data); + const resources = [...scriptSrcs, ...linkHrefs] + .filter(Boolean) + .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) + .map(url => new URL(url, self.location.origin).href); - if (event.data && event.data.type === 'PWA_STATUS') { - const clientId = event.source?.id; - if (!clientId) return; + await Promise.allSettled(resources.map(url => cacheResource(cache, url))); +}; - if (event.data.isPWA) { - pwaClients.add(clientId); +// Cache patterns +const CACHEABLE_FILES = [ + // Reverb + /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, + // Smart Tag + 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', + // Fonts + /\.woff2$/, + // Frosted Promo (test and live environments only) + /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, + // Moment + /\/moment-lib+.*?\.js$/, + // PWA Icons + /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, +]; - const serviceName = getServiceFromUrl(event.source?.url); - cacheOfflinePageAndResources(serviceName).catch(() => null); - } else { - pwaClients.delete(clientId); - } - } -}); +const WEBP_IMAGE = + /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; // -------------Install event ------- self.addEventListener('install', () => { @@ -94,46 +93,50 @@ self.addEventListener('activate', event => { console.log(`[SW v${version}] Activating...`); event.waitUntil( (async () => { - // Delete old caches - const cacheNames = await caches.keys(); + const keys = await caches.keys(); await Promise.all( - cacheNames - .filter(name => name !== cacheName) - .map(name => caches.delete(name)), + keys.map(key => key !== cacheName && caches.delete(key)), ); await self.clients.claim(); })(), ); }); -const CACHEABLE_FILES = [ - // Reverb - /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, - // Smart Tag - 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', - // Fonts - /\.woff2$/, - // Frosted Promo (test and live environments only) - /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, - // Moment - /\/moment-lib+.*?\.js$/, - // PWA Icons - /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, -]; +// -------Message Event------------- +self.addEventListener('message', async event => { + console.log(`[SW v${version}] Message received:`, event.data); -const WEBP_IMAGE = - /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; + if (event.data?.type === 'PWA_STATUS') { + const clientId = event.source.id; + const { isPWA } = event.data; + if (isPWA) { + pwaClients.set(clientId, true); -// -------Fetch Handler------------- + // Cache offline page/resources for this client + const service = getServiceFromUrl(event.source.url); + await cacheOfflinePageAndResources(service); + } else { + pwaClients.delete(clientId); + } -const fetchEventHandler = async event => { - const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => { - if (cacheableFile instanceof RegExp) { - return cacheableFile.test(event.request.url); + // Process any pending navigations for this client + if (pendingNavigations.has(clientId)) { + const pendingList = pendingNavigations.get(clientId); + pendingNavigations.delete(clientId); + + pendingList.forEach(respondFn => { + respondFn(); // respondFn internally uses clientId to check PWA status now + }); } + } +}); - return event.request.url === cacheableFile; - }); +// -------Fetch Handler------------- +const fetchEventHandler = async event => { + console.log(`[SW FETCH] Request: ${event.request.url}`); + const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => + new RegExp(cacheableFile).test(event.request.url), + ); const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); @@ -162,77 +165,69 @@ const fetchEventHandler = async event => { const cache = await caches.open(cacheName); let response = await cache.match(event.request); if (!response) { - try { - response = await fetch(event.request.url); - cache.put(event.request, response.clone()); - } catch (error) { - // File not in cache and network unavailable - return new Response('', { - status: 408, - statusText: - 'You are offline . Please check your network and reload the page', - }); - } + response = await fetch(event.request.url); + cache.put(event.request, response.clone()); } return response; })(), ); } else if (event.request.mode === 'navigate') { - const clientId = event.clientId || event.resultingClientId; - - // detect PWA using both pwaClients set and displayMode - let isInPWAMode = false; - const client = await self.clients.get(clientId); + const { clientId } = event; - if (clientId) { - const clientInfo = await self.clients.get(clientId); - isInPWAMode = - pwaClients.has(clientId) || clientInfo?.displayMode === 'standalone'; - } + const respondWithNavigation = async () => { + const isPWA = clientId && pwaClients.get(clientId); - console.log( - '[SW] Fetch event for navigation. isInPWAMode:', - isInPWAMode, - clientId, - client?.displayMode, - ); + try { + // Use preload if available + const preloadResp = await event.preloadResponse; + if (preloadResp) return preloadResp; + + const networkResp = await fetch(event.request); + + // Cache offline page if in PWA mode + if (networkResp && networkResp.ok && isPWA) { + const service = getServiceFromUrl(event.request.url); + cacheOfflinePageAndResources(service).catch(err => + console.error('[SW] Cache offline fail:', err), + ); + } - if (isInPWAMode) { - event.respondWith( - (async () => { - try { - const preloadResponse = await event.preloadResponse; - if (preloadResponse) return preloadResponse; - - const networkResp = await fetch(event.request); - - // Dynamically cache offline page for PWA clients - if (networkResp.ok && clientId) { - pwaClients.add(clientId); - const serviceName = getServiceFromUrl(event.request.url); - cacheOfflinePageAndResources(serviceName).catch(() => null); - } - - return networkResp; - } catch (error) { - const cache = await caches.open(cacheName); - const serviceName = getServiceFromUrl(event.request.url); - const offlinePageUrl = getOfflinePageUrl(serviceName); - const cachedResponse = await cache.match(offlinePageUrl); - - return ( - cachedResponse || - new Response( - 'You are offline. Please check your network and reload the page', - { status: 503, headers: { 'Content-Type': 'text/plain' } }, - ) - ); + return networkResp; + } catch (err) { + console.log('[SW] Navigation failed:', event.request.url, err); + + if (isPWA) { + const service = getServiceFromUrl(event.request.url); + const cache = await caches.open(cacheName); + const offlineUrl = new URL( + getOfflinePageUrl(service), + self.location.origin, + ).href; + + const cachedOffline = await cache.match(offlineUrl); + if (cachedOffline) { + return cachedOffline; } - })(), - ); + } + + // Fallback to browser default + throw err; + } + }; + + // If PWA status is unknown yet, queue this navigation + if (!clientId || pwaClients.has(clientId)) { + event.respondWith(respondWithNavigation()); + } else { + if (!pendingNavigations.has(clientId)) { + pendingNavigations.set(clientId, []); + } + pendingNavigations + .get(clientId) + .push(() => event.respondWith(respondWithNavigation())); } } - // For all other requests, let the browser handle it normally + return; }; From f63e876ff51ae33b4ea78b009decbecd55d5e824 Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 18 Dec 2025 15:22:53 +0200 Subject: [PATCH 5/9] test -5 - added setTimout for small delay to get client id --- public/sw.js | 139 ++++++++++++++++++--------------------------------- 1 file changed, 50 insertions(+), 89 deletions(-) diff --git a/public/sw.js b/public/sw.js index e14c15e6335..829b3ed1877 100644 --- a/public/sw.js +++ b/public/sw.js @@ -8,12 +8,9 @@ const version = 'v0.3.1'; const cacheName = 'simorghCache_v1'; -// Track PWA clients +// Track PWA clients per clientId const pwaClients = new Map(); -// Track pending navigation fetches until we know PWA status -const pendingNavigations = new Map(); - console.log(`[SW v${version}] Service Worker loaded.`); // -------------------- @@ -65,17 +62,11 @@ const cacheOfflinePageAndResources = async service => { // Cache patterns const CACHEABLE_FILES = [ - // Reverb /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, - // Smart Tag 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', - // Fonts /\.woff2$/, - // Frosted Promo (test and live environments only) /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, - // Moment /\/moment-lib+.*?\.js$/, - // PWA Icons /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, ]; @@ -109,126 +100,96 @@ self.addEventListener('message', async event => { if (event.data?.type === 'PWA_STATUS') { const clientId = event.source.id; const { isPWA } = event.data; + if (isPWA) { pwaClients.set(clientId, true); - - // Cache offline page/resources for this client const service = getServiceFromUrl(event.source.url); await cacheOfflinePageAndResources(service); } else { pwaClients.delete(clientId); } - - // Process any pending navigations for this client - if (pendingNavigations.has(clientId)) { - const pendingList = pendingNavigations.get(clientId); - pendingNavigations.delete(clientId); - - pendingList.forEach(respondFn => { - respondFn(); // respondFn internally uses clientId to check PWA status now - }); - } } }); // -------Fetch Handler------------- const fetchEventHandler = async event => { - console.log(`[SW FETCH] Request: ${event.request.url}`); + const { request } = event; + console.log(`[SW FETCH] Request: ${request.url}`); + const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => - new RegExp(cacheableFile).test(event.request.url), + new RegExp(cacheableFile).test(request.url), ); - const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); + const isRequestForWebpImage = WEBP_IMAGE.test(request.url); if (isRequestForWebpImage) { - const req = event.request.clone(); - - // Inspect the accept header for WebP support - + const req = request.clone(); const supportsWebp = req.headers.has('accept') && req.headers.get('accept').includes('webp'); - // if supports webp is false in request header then don't use it - // if accept header doesn't indicate support for webp remove .webp extension - if (!supportsWebp) { const imageUrlWithoutWebp = req.url.replace('.webp', ''); - event.respondWith( - fetch(imageUrlWithoutWebp, { - mode: 'no-cors', - }), - ); + event.respondWith(fetch(imageUrlWithoutWebp, { mode: 'no-cors' })); } } else if (isRequestForCacheableFile) { event.respondWith( (async () => { const cache = await caches.open(cacheName); - let response = await cache.match(event.request); + let response = await cache.match(request); if (!response) { - response = await fetch(event.request.url); - cache.put(event.request, response.clone()); + response = await fetch(request.url); + cache.put(request, response.clone()); } return response; })(), ); - } else if (event.request.mode === 'navigate') { + } else if (request.mode === 'navigate') { const { clientId } = event; - const respondWithNavigation = async () => { - const isPWA = clientId && pwaClients.get(clientId); - - try { - // Use preload if available - const preloadResp = await event.preloadResponse; - if (preloadResp) return preloadResp; + event.respondWith( + (async () => { + // Wait briefly if PWA status is not known yet + let isPWA = clientId && pwaClients.get(clientId); + if (clientId && isPWA === undefined) { + await new Promise(resolve => { + setTimeout(resolve, 50); + }); + isPWA = pwaClients.get(clientId); + } - const networkResp = await fetch(event.request); + try { + const preloadResp = await event.preloadResponse; + if (preloadResp) return preloadResp; - // Cache offline page if in PWA mode - if (networkResp && networkResp.ok && isPWA) { - const service = getServiceFromUrl(event.request.url); - cacheOfflinePageAndResources(service).catch(err => - console.error('[SW] Cache offline fail:', err), - ); - } + const networkResp = await fetch(request); - return networkResp; - } catch (err) { - console.log('[SW] Navigation failed:', event.request.url, err); - - if (isPWA) { - const service = getServiceFromUrl(event.request.url); - const cache = await caches.open(cacheName); - const offlineUrl = new URL( - getOfflinePageUrl(service), - self.location.origin, - ).href; - - const cachedOffline = await cache.match(offlineUrl); - if (cachedOffline) { - return cachedOffline; + if (networkResp.ok && isPWA) { + const service = getServiceFromUrl(request.url); + cacheOfflinePageAndResources(service).catch(console.error); } - } - // Fallback to browser default - throw err; - } - }; + return networkResp; + } catch (err) { + console.log('[SW] Navigation failed:', request.url, err); - // If PWA status is unknown yet, queue this navigation - if (!clientId || pwaClients.has(clientId)) { - event.respondWith(respondWithNavigation()); - } else { - if (!pendingNavigations.has(clientId)) { - pendingNavigations.set(clientId, []); - } - pendingNavigations - .get(clientId) - .push(() => event.respondWith(respondWithNavigation())); - } - } + if (isPWA) { + const service = getServiceFromUrl(request.url); + const cache = await caches.open(cacheName); + const offlineUrl = new URL( + getOfflinePageUrl(service), + self.location.origin, + ).href; - return; + const cachedOffline = await cache.match(offlineUrl); + if (cachedOffline) return cachedOffline; + } + + // fallback to browser default behavior + return Response.error(); + } + })(), + ); + } }; onfetch = fetchEventHandler; From 377904235eacf425670025c216f6d833455e4bef Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 18 Dec 2025 16:58:02 +0200 Subject: [PATCH 6/9] test6-addee pwa_installed var again --- public/sw.js | 194 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 163 insertions(+), 31 deletions(-) diff --git a/public/sw.js b/public/sw.js index 829b3ed1877..0610a0fca32 100644 --- a/public/sw.js +++ b/public/sw.js @@ -112,84 +112,216 @@ self.addEventListener('message', async event => { }); // -------Fetch Handler------------- -const fetchEventHandler = async event => { - const { request } = event; - console.log(`[SW FETCH] Request: ${request.url}`); +// const fetchEventHandler = async event => { +// const { request } = event; +// console.log(`[SW FETCH] Request: ${request.url}`); + +// const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => +// new RegExp(cacheableFile).test(request.url), +// ); + +// const isRequestForWebpImage = WEBP_IMAGE.test(request.url); + +// if (isRequestForWebpImage) { +// const req = request.clone(); +// const supportsWebp = +// req.headers.has('accept') && req.headers.get('accept').includes('webp'); + +// if (!supportsWebp) { +// const imageUrlWithoutWebp = req.url.replace('.webp', ''); +// event.respondWith(fetch(imageUrlWithoutWebp, { mode: 'no-cors' })); +// } +// } else if (isRequestForCacheableFile) { +// event.respondWith( +// (async () => { +// const cache = await caches.open(cacheName); +// let response = await cache.match(request); +// if (!response) { +// response = await fetch(request.url); +// cache.put(request, response.clone()); +// } +// return response; +// })(), +// ); +// } else if (request.mode === 'navigate') { +// const { clientId } = event; + +// event.respondWith( +// (async () => { +// // Wait briefly if PWA status is not known yet +// let isPWA = clientId && pwaClients.get(clientId); +// if (clientId && isPWA === undefined) { +// await new Promise(resolve => { +// setTimeout(resolve, 50); +// }); +// isPWA = pwaClients.get(clientId); +// } + +// try { +// const preloadResp = await event.preloadResponse; +// if (preloadResp) return preloadResp; + +// const networkResp = await fetch(request); + +// if (networkResp.ok && isPWA) { +// const service = getServiceFromUrl(request.url); +// cacheOfflinePageAndResources(service).catch(console.error); +// } + +// return networkResp; +// } catch (err) { +// console.log('[SW] Navigation failed:', request.url, err); + +// if (isPWA) { +// const service = getServiceFromUrl(request.url); +// const cache = await caches.open(cacheName); +// const offlineUrl = new URL( +// getOfflinePageUrl(service), +// self.location.origin, +// ).href; + +// const cachedOffline = await cache.match(offlineUrl); +// if (cachedOffline) return cachedOffline; +// } + +// // fallback to browser default behavior +// return Response.error(); +// } +// })(), +// ); +// } +// }; + +// onfetch = fetchEventHandler; +// -------Fetch Handler------------- +const fetchEventHandler = async event => { + console.log(`[SW FETCH] Request: ${event.request.url}`); const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => - new RegExp(cacheableFile).test(request.url), + new RegExp(cacheableFile).test(event.request.url), ); - const isRequestForWebpImage = WEBP_IMAGE.test(request.url); + const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); if (isRequestForWebpImage) { - const req = request.clone(); + const req = event.request.clone(); + + // Inspect the accept header for WebP support + const supportsWebp = req.headers.has('accept') && req.headers.get('accept').includes('webp'); + // if supports webp is false in request header then don't use it + // if accept header doesn't indicate support for webp remove .webp extension + if (!supportsWebp) { const imageUrlWithoutWebp = req.url.replace('.webp', ''); - event.respondWith(fetch(imageUrlWithoutWebp, { mode: 'no-cors' })); + event.respondWith( + fetch(imageUrlWithoutWebp, { + mode: 'no-cors', + }), + ); } } else if (isRequestForCacheableFile) { event.respondWith( (async () => { const cache = await caches.open(cacheName); - let response = await cache.match(request); + let response = await cache.match(event.request); if (!response) { - response = await fetch(request.url); - cache.put(request, response.clone()); + response = await fetch(event.request.url); + cache.put(event.request, response.clone()); } return response; })(), ); - } else if (request.mode === 'navigate') { - const { clientId } = event; + } else if (event.request.url.includes('/_next/static/')) { + // Network-first for Next.js chunks (dev mode compatibility) + event.respondWith( + (async () => { + try { + const networkResp = await fetch(event.request); + const cache = await caches.open(cacheName); + cache.put(event.request, networkResp.clone()); + return networkResp; + } catch (err) { + const cache = await caches.open(cacheName); + const cachedResp = await cache.match(event.request); + if (cachedResp) return cachedResp; + throw err; + } + })(), + ); + } else if (event.request.mode === 'navigate') { + const { url } = event.request; event.respondWith( (async () => { - // Wait briefly if PWA status is not known yet - let isPWA = clientId && pwaClients.get(clientId); - if (clientId && isPWA === undefined) { - await new Promise(resolve => { - setTimeout(resolve, 50); - }); - isPWA = pwaClients.get(clientId); + const client = await self.clients.get(event.clientId); + const isPWA = client && pwaClients.get(client.id); + const cache = await caches.open(cacheName); + console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); + + const pwaMarkerExists = await cache.match('pwa_installed'); + if (!isPWA && pwaMarkerExists) { + await cache.delete('pwa_installed'); } try { + // Use preload if available const preloadResp = await event.preloadResponse; if (preloadResp) return preloadResp; - const networkResp = await fetch(request); - - if (networkResp.ok && isPWA) { - const service = getServiceFromUrl(request.url); - cacheOfflinePageAndResources(service).catch(console.error); + const networkResp = await fetch(event.request); + + // Cache offline page if in PWA mode + if (networkResp && networkResp.ok && event.clientId) { + console.log('[SW] Caching offline page if PWA if network is ok'); + // const client = await self.clients.get(event.clientId); + // const isPWA = client && pwaClients.get(client.id); + if (isPWA) { + const service = getServiceFromUrl(url); + cacheOfflinePageAndResources(service).catch(err => + console.error('[SW] Cache offline fail:', err), + ); + } } return networkResp; } catch (err) { - console.log('[SW] Navigation failed:', request.url, err); + console.log('[SW] Navigation failed:', url, err); - if (isPWA) { - const service = getServiceFromUrl(request.url); - const cache = await caches.open(cacheName); + const pwaMarker = await cache.match('pwa_installed'); + console.log('[SW] PWA Marker:', pwaMarker); + + // Only show offline page for installed PWA + if (pwaMarker || isPWA) { + const service = getServiceFromUrl(url); const offlineUrl = new URL( getOfflinePageUrl(service), self.location.origin, ).href; const cachedOffline = await cache.match(offlineUrl); - if (cachedOffline) return cachedOffline; + if (cachedOffline) { + console.log('[SW] Serving cached offline page'); + return cachedOffline; + } } - // fallback to browser default behavior - return Response.error(); + // Canonical site offline fallback + return new Response( + 'You are offline. Please check your network and reload the page', + { + status: 503, + headers: { 'Content-Type': 'text/plain' }, + }, + ); } })(), ); } + + return; }; onfetch = fetchEventHandler; From 1cfedc6a87e7fcd9f8ed73ca4677030f0f2d9d1b Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 18 Dec 2025 18:28:18 +0200 Subject: [PATCH 7/9] test7- added pwa_installed in install --- public/sw.js | 109 +++++++-------------------------------------------- 1 file changed, 14 insertions(+), 95 deletions(-) diff --git a/public/sw.js b/public/sw.js index 0610a0fca32..2ab679d8508 100644 --- a/public/sw.js +++ b/public/sw.js @@ -8,7 +8,7 @@ const version = 'v0.3.1'; const cacheName = 'simorghCache_v1'; -// Track PWA clients per clientId +// Track PWA clients const pwaClients = new Map(); console.log(`[SW v${version}] Service Worker loaded.`); @@ -62,11 +62,17 @@ const cacheOfflinePageAndResources = async service => { // Cache patterns const CACHEABLE_FILES = [ + // Reverb /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, + // Smart Tag 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', + // Fonts /\.woff2$/, + // Frosted Promo (test and live environments only) /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, + // Moment /\/moment-lib+.*?\.js$/, + // PWA Icons /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, ]; @@ -74,7 +80,7 @@ const WEBP_IMAGE = /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; // -------------Install event ------- -self.addEventListener('install', () => { +self.addEventListener('install', event => { console.log(`[SW v${version}] Installing...`); self.skipWaiting(); }); @@ -100,100 +106,20 @@ self.addEventListener('message', async event => { if (event.data?.type === 'PWA_STATUS') { const clientId = event.source.id; const { isPWA } = event.data; + pwaClients.set(clientId, isPWA); if (isPWA) { - pwaClients.set(clientId, true); + const cache = await caches.open(cacheName); + await cache.put('pwa_installed', new Response('true')); const service = getServiceFromUrl(event.source.url); await cacheOfflinePageAndResources(service); } else { - pwaClients.delete(clientId); + const cache = await caches.open(cacheName); + await cache.delete('pwa_installed'); } } }); -// -------Fetch Handler------------- -// const fetchEventHandler = async event => { -// const { request } = event; -// console.log(`[SW FETCH] Request: ${request.url}`); - -// const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => -// new RegExp(cacheableFile).test(request.url), -// ); - -// const isRequestForWebpImage = WEBP_IMAGE.test(request.url); - -// if (isRequestForWebpImage) { -// const req = request.clone(); -// const supportsWebp = -// req.headers.has('accept') && req.headers.get('accept').includes('webp'); - -// if (!supportsWebp) { -// const imageUrlWithoutWebp = req.url.replace('.webp', ''); -// event.respondWith(fetch(imageUrlWithoutWebp, { mode: 'no-cors' })); -// } -// } else if (isRequestForCacheableFile) { -// event.respondWith( -// (async () => { -// const cache = await caches.open(cacheName); -// let response = await cache.match(request); -// if (!response) { -// response = await fetch(request.url); -// cache.put(request, response.clone()); -// } -// return response; -// })(), -// ); -// } else if (request.mode === 'navigate') { -// const { clientId } = event; - -// event.respondWith( -// (async () => { -// // Wait briefly if PWA status is not known yet -// let isPWA = clientId && pwaClients.get(clientId); -// if (clientId && isPWA === undefined) { -// await new Promise(resolve => { -// setTimeout(resolve, 50); -// }); -// isPWA = pwaClients.get(clientId); -// } - -// try { -// const preloadResp = await event.preloadResponse; -// if (preloadResp) return preloadResp; - -// const networkResp = await fetch(request); - -// if (networkResp.ok && isPWA) { -// const service = getServiceFromUrl(request.url); -// cacheOfflinePageAndResources(service).catch(console.error); -// } - -// return networkResp; -// } catch (err) { -// console.log('[SW] Navigation failed:', request.url, err); - -// if (isPWA) { -// const service = getServiceFromUrl(request.url); -// const cache = await caches.open(cacheName); -// const offlineUrl = new URL( -// getOfflinePageUrl(service), -// self.location.origin, -// ).href; - -// const cachedOffline = await cache.match(offlineUrl); -// if (cachedOffline) return cachedOffline; -// } - -// // fallback to browser default behavior -// return Response.error(); -// } -// })(), -// ); -// } -// }; - -// onfetch = fetchEventHandler; - // -------Fetch Handler------------- const fetchEventHandler = async event => { console.log(`[SW FETCH] Request: ${event.request.url}`); @@ -308,14 +234,7 @@ const fetchEventHandler = async event => { } } - // Canonical site offline fallback - return new Response( - 'You are offline. Please check your network and reload the page', - { - status: 503, - headers: { 'Content-Type': 'text/plain' }, - }, - ); + throw err; } })(), ); From 26739bf5854bf1c239554f4fb80d94fed6b80a02 Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 18 Dec 2025 20:27:54 +0200 Subject: [PATCH 8/9] checked isPWA!== undefined --- public/sw.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/sw.js b/public/sw.js index 2ab679d8508..27b0a0a96b2 100644 --- a/public/sw.js +++ b/public/sw.js @@ -188,7 +188,7 @@ const fetchEventHandler = async event => { console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); const pwaMarkerExists = await cache.match('pwa_installed'); - if (!isPWA && pwaMarkerExists) { + if (!isPWA && pwaMarkerExists && isPWA !== undefined) { await cache.delete('pwa_installed'); } @@ -233,8 +233,8 @@ const fetchEventHandler = async event => { return cachedOffline; } } - - throw err; + // fallback to browser default behavior + return Response.error(); } })(), ); From 83691d950cd5af1c3f6fb9c5ba769969fbe9d1a5 Mon Sep 17 00:00:00 2001 From: jinidev Date: Fri, 19 Dec 2025 17:22:46 +0200 Subject: [PATCH 9/9] removed pwa_installed var --- public/sw.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/public/sw.js b/public/sw.js index 27b0a0a96b2..7c39ba46bbd 100644 --- a/public/sw.js +++ b/public/sw.js @@ -106,16 +106,18 @@ self.addEventListener('message', async event => { if (event.data?.type === 'PWA_STATUS') { const clientId = event.source.id; const { isPWA } = event.data; - pwaClients.set(clientId, isPWA); + // pwaClients.set(clientId, isPWA); if (isPWA) { - const cache = await caches.open(cacheName); - await cache.put('pwa_installed', new Response('true')); + // const cache = await caches.open(cacheName); + // await cache.put('pwa_installed', new Response('true')); + pwaClients.set(clientId, isPWA); + const service = getServiceFromUrl(event.source.url); await cacheOfflinePageAndResources(service); } else { - const cache = await caches.open(cacheName); - await cache.delete('pwa_installed'); + // const cache = await caches.open(cacheName); + // await cache.delete('pwa_installed'); } } }); @@ -185,13 +187,18 @@ const fetchEventHandler = async event => { const client = await self.clients.get(event.clientId); const isPWA = client && pwaClients.get(client.id); const cache = await caches.open(cacheName); - console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); - - const pwaMarkerExists = await cache.match('pwa_installed'); - if (!isPWA && pwaMarkerExists && isPWA !== undefined) { - await cache.delete('pwa_installed'); - } + // const pwaMarkerExists = await cache.match('pwa_installed'); + // if (!isPWA && pwaMarkerExists && isPWA !== undefined) { + // await cache.delete('pwa_installed'); + // } + console.log('[SW FETCH] Navigation', { + url: event.request.url, + clientId: event.clientId, + isPWA, + client, + event, + }); try { // Use preload if available const preloadResp = await event.preloadResponse; @@ -216,11 +223,11 @@ const fetchEventHandler = async event => { } catch (err) { console.log('[SW] Navigation failed:', url, err); - const pwaMarker = await cache.match('pwa_installed'); - console.log('[SW] PWA Marker:', pwaMarker); + // const pwaMarker = await cache.match('pwa_installed'); + // console.log('[SW] PWA Marker:', pwaMarker); // Only show offline page for installed PWA - if (pwaMarker || isPWA) { + if (isPWA) { const service = getServiceFromUrl(url); const offlineUrl = new URL( getOfflinePageUrl(service),