@@ -2,32 +2,199 @@ import path from 'node:path'
22
33import { CLIENT_STATIC_FILES_PATH } from '../shared/lib/constants'
44import { OFFLINE_NAVIGATION_SERVICE_WORKER } from '../shared/lib/offline-navigation'
5+ import { OFFLINE_NAVIGATION_CACHE_STATIC_ASSETS } from '../shared/lib/offline-navigation-constants'
56
67export function getOfflineNavigationServiceWorkerFilePath ( ) : string {
78 return path . join ( CLIENT_STATIC_FILES_PATH , OFFLINE_NAVIGATION_SERVICE_WORKER )
89}
910
11+ export function getOfflineNavigationCacheNamespace ( {
12+ basePath,
13+ buildId,
14+ } : {
15+ basePath : string
16+ buildId : string
17+ } ) : string {
18+ return `next-offline-navigation-v1:${ buildId } :${ basePath || '/' } `
19+ }
20+
1021function renderServiceWorkerMetadata ( metadata : unknown ) : string {
1122 return `self.__NEXT_OFFLINE_NAVIGATION_SW=${ JSON . stringify ( metadata ) } ;`
1223}
1324
14- const serviceWorkerInstallListener =
15- "self.addEventListener('install',(event)=>{event.waitUntil(self.skipWaiting())});"
25+ const cachePrefixSource = "const CACHE_PREFIX='next-offline-navigation-v1:';"
26+
27+ const hrefNormalizationSource = [
28+ 'function normalizeHref(href){' ,
29+ 'const url=new URL(href,self.location.origin);' ,
30+ "url.search='';" ,
31+ "url.hash='';" ,
32+ 'return url.href;' ,
33+ '}' ,
34+ 'function withDeploymentQuery(href){' ,
35+ 'const url=new URL(normalizeHref(href));' ,
36+ 'const deploymentParams=new URLSearchParams(self.location.search);' ,
37+ 'deploymentParams.forEach((value,key)=>{' ,
38+ 'if(!url.searchParams.has(key)){url.searchParams.set(key,value);}' ,
39+ '});' ,
40+ 'return url.href;' ,
41+ '}' ,
42+ ] . join ( '' )
43+
44+ const requiredResourceCachingSource = [
45+ 'async function fetchRequiredResource(href,withDeploymentId){' ,
46+ 'const normalizedHref=normalizeHref(href);' ,
47+ 'const resourceHref=withDeploymentId?withDeploymentQuery(normalizedHref):normalizedHref;' ,
48+ 'let response;' ,
49+ "try{response=await fetch(resourceHref,{cache:'no-store'});}" ,
50+ 'catch(err){' ,
51+ 'if(withDeploymentId){throw err;}' ,
52+ "response=await fetch(resourceHref,{cache:'no-store',mode:'no-cors'});" ,
53+ '}' ,
54+ "if(!response.ok&&response.type!=='opaque'){" ,
55+ "throw new Error('Failed to cache offline navigation resource: '+href);" ,
56+ '}' ,
57+ 'return response;' ,
58+ '}' ,
59+ 'async function cacheOfflineNavigationResources(){' ,
60+ 'const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;' ,
61+ 'const cache=await caches.open(metadata.cacheNamespace);' ,
62+ 'const fallbackResponse=await fetchRequiredResource(metadata.fallbackDocumentHref,true);' ,
63+ 'await cache.put(normalizeHref(metadata.fallbackDocumentHref),fallbackResponse);' ,
64+ 'await Promise.all(metadata.fallbackAssetHrefs.map(async(href)=>{' ,
65+ 'const response=await fetchRequiredResource(href,false);' ,
66+ 'await cache.put(normalizeHref(href),response);' ,
67+ '}));' ,
68+ '}' ,
69+ ] . join ( '' )
70+
71+ const assetCacheKeySource = [
72+ 'function getFallbackAssetCacheKey(request){' ,
73+ "if(request.method!=='GET'){return null;}" ,
74+ 'const requestHref=normalizeHref(request.url);' ,
75+ 'const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;' ,
76+ 'return metadata.fallbackAssetHrefs.some((href)=>normalizeHref(href)===requestHref)?requestHref:null;' ,
77+ '}' ,
78+ 'function getManagedStaticAssetCacheKey(request){' ,
79+ "if(request.method!=='GET'){return null;}" ,
80+ 'return getManagedStaticAssetCacheKeyFromHref(request.url);' ,
81+ '}' ,
82+ 'function getManagedStaticAssetCacheKeyFromHref(href){' ,
83+ 'let url;' ,
84+ 'try{url=new URL(href,self.location.origin);}' ,
85+ 'catch(err){return null;}' ,
86+ "if(!url.pathname.includes('/_next/static/')||url.pathname.includes('/_offline-navigation-')){return null;}" ,
87+ "url.search='';" ,
88+ "url.hash='';" ,
89+ 'return url.href;' ,
90+ '}' ,
91+ 'function getStaticAssetPromotionRequestHref(href){' ,
92+ 'let url;' ,
93+ 'try{url=new URL(href,self.location.origin);}' ,
94+ 'catch(err){return null;}' ,
95+ 'if(url.origin!==self.location.origin||getManagedStaticAssetCacheKeyFromHref(url.href)===null){return null;}' ,
96+ "url.hash='';" ,
97+ 'return url.href;' ,
98+ '}' ,
99+ ] . join ( '' )
16100
17- const serviceWorkerActivateListener =
18- "self.addEventListener('activate',(event)=>{event.waitUntil(self.clients.claim())});"
101+ const staticAssetFetchSource = [
102+ 'async function fetchManagedStaticAsset(request){' ,
103+ 'const cacheKey=getFallbackAssetCacheKey(request)||getManagedStaticAssetCacheKey(request);' ,
104+ 'if(cacheKey===null){return fetch(request);}' ,
105+ 'const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;' ,
106+ 'const cache=await caches.open(metadata.cacheNamespace);' ,
107+ 'const cachedResponse=await cache.match(cacheKey);' ,
108+ 'if(cachedResponse){return cachedResponse;}' ,
109+ 'const response=await fetch(request);' ,
110+ "if(response.ok||response.type==='opaque'){await cache.put(cacheKey,response.clone());}" ,
111+ 'return response;' ,
112+ '}' ,
113+ ] . join ( '' )
19114
20- // Generate an app-local service worker for offline navigations. This slice is
21- // pass-through: it only installs and claims clients so later slices can add
22- // cache population and fallback document handling.
23- export function createOfflineNavigationServiceWorker ( ) : string {
24- const metadata = {
25- source : 'offline-navigation-service-worker' ,
26- }
115+ const currentAssetPromotionSource = [
116+ 'async function cacheCurrentStaticAssets(hrefs){' ,
117+ 'if(!Array.isArray(hrefs)||hrefs.length===0){return;}' ,
118+ 'const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;' ,
119+ 'const cache=await caches.open(metadata.cacheNamespace);' ,
120+ 'await Promise.all(hrefs.slice(0,128).map(async(href)=>{' ,
121+ "if(typeof href!=='string'||href.length>4096){return;}" ,
122+ 'const cacheKey=getManagedStaticAssetCacheKeyFromHref(href);' ,
123+ 'if(cacheKey===null){return;}' ,
124+ 'const cachedResponse=await cache.match(cacheKey);' ,
125+ 'if(cachedResponse){return;}' ,
126+ 'const requestHref=getStaticAssetPromotionRequestHref(href);' ,
127+ 'if(requestHref===null){return;}' ,
128+ 'let response;' ,
129+ "try{response=await fetch(requestHref,{cache:'only-if-cached',mode:'same-origin'});}" ,
130+ 'catch(err){return;}' ,
131+ 'if(response.ok){await cache.put(cacheKey,response);}' ,
132+ '}));' ,
133+ '}' ,
134+ ] . join ( '' )
135+
136+ const installAndActivateListenersSource = [
137+ "self.addEventListener('install',(event)=>{" ,
138+ 'event.waitUntil((async()=>{' ,
139+ 'await cacheOfflineNavigationResources();' ,
140+ 'await self.skipWaiting();' ,
141+ '})());' ,
142+ '});' ,
143+ "self.addEventListener('activate',(event)=>{" ,
144+ 'event.waitUntil((async()=>{' ,
145+ 'const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;' ,
146+ 'const cacheNames=await caches.keys();' ,
147+ 'await Promise.all(cacheNames.map((cacheName)=>{' ,
148+ 'if(cacheName.startsWith(CACHE_PREFIX)&&cacheName!==metadata.cacheNamespace){' ,
149+ 'return caches.delete(cacheName);' ,
150+ '}' ,
151+ '}));' ,
152+ 'await self.clients.claim();' ,
153+ '})());' ,
154+ '});' ,
155+ ] . join ( '' )
156+
157+ const fetchListenerSource = [
158+ "self.addEventListener('fetch',(event)=>{" ,
159+ 'if(getFallbackAssetCacheKey(event.request)!==null||getManagedStaticAssetCacheKey(event.request)!==null){' ,
160+ 'event.respondWith(fetchManagedStaticAsset(event.request));' ,
161+ '}' ,
162+ '});' ,
163+ ] . join ( '' )
164+
165+ function renderMessageListener ( messageType : string ) : string {
166+ return `self.addEventListener('message',(event)=>{const data=event.data;if(data&&data.type===${ JSON . stringify (
167+ messageType
168+ ) } ){event.waitUntil(cacheCurrentStaticAssets(data.hrefs));}});`
169+ }
27170
171+ // Offline navigations use a generated, app-local service worker for the
172+ // document fallback and the bootstrap assets referenced by that fallback. It
173+ // does not interpret route data; later client-router slices own route replay.
174+ export function createOfflineNavigationServiceWorker ( {
175+ cacheNamespace,
176+ fallbackAssetHrefs,
177+ fallbackDocumentHref,
178+ } : {
179+ cacheNamespace : string
180+ fallbackAssetHrefs : string [ ]
181+ fallbackDocumentHref : string
182+ } ) : string {
28183 return [
29- renderServiceWorkerMetadata ( metadata ) ,
30- serviceWorkerInstallListener ,
31- serviceWorkerActivateListener ,
184+ renderServiceWorkerMetadata ( {
185+ cacheNamespace,
186+ fallbackAssetHrefs,
187+ fallbackDocumentHref,
188+ source : 'offline-navigation-service-worker' ,
189+ } ) ,
190+ cachePrefixSource ,
191+ hrefNormalizationSource ,
192+ requiredResourceCachingSource ,
193+ assetCacheKeySource ,
194+ staticAssetFetchSource ,
195+ currentAssetPromotionSource ,
196+ installAndActivateListenersSource ,
197+ fetchListenerSource ,
198+ renderMessageListener ( OFFLINE_NAVIGATION_CACHE_STATIC_ASSETS ) ,
32199 ] . join ( '' )
33200}
0 commit comments