Skip to content

Commit 14ed755

Browse files
committed
offline navigations: cache fallback artifacts (4/10)
1 parent a7c6088 commit 14ed755

3 files changed

Lines changed: 103 additions & 5 deletions

File tree

packages/next/src/build/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4145,6 +4145,7 @@ export default async function build(
41454145
serviceWorkerPath,
41464146
createOfflineNavigationServiceWorker({
41474147
buildId,
4148+
cacheNamespace: manifest.cacheNamespace,
41484149
manifestHref: manifest.manifest.href,
41494150
})
41504151
)

packages/next/src/build/offline-navigation-service-worker.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,70 @@ export function getOfflineNavigationServiceWorkerFilePath(): string {
77
return path.join(CLIENT_STATIC_FILES_PATH, OFFLINE_NAVIGATION_SERVICE_WORKER)
88
}
99

10-
// Generate an app-local service worker for offline navigations. This first
11-
// slice is intentionally pass-through: it only installs and claims clients so
12-
// later slices can add cache population and fallback document handling.
10+
// Generate an app-local service worker for offline navigations. At this stage
11+
// it only installs build-scoped bootstrap artifacts into CacheStorage; document
12+
// navigations still go to the network until the fallback handling slice.
1313
export function createOfflineNavigationServiceWorker({
1414
buildId,
15+
cacheNamespace,
1516
manifestHref,
1617
}: {
1718
buildId: string
19+
cacheNamespace: string
1820
manifestHref: string
1921
}): string {
2022
const metadata = JSON.stringify({
2123
buildId,
24+
cacheNamespace,
2225
manifestHref,
2326
source: 'offline-navigation-service-worker',
2427
})
2528

2629
return `self.__NEXT_OFFLINE_NAVIGATION_SW=${metadata};
27-
self.addEventListener('install',(event)=>{event.waitUntil(self.skipWaiting())});
28-
self.addEventListener('activate',(event)=>{event.waitUntil(self.clients.claim())});
30+
const CACHE_PREFIX='next-offline-navigation-v1:';
31+
function withDeploymentQuery(href){
32+
const url=new URL(href,self.location.origin);
33+
const deploymentParams=new URLSearchParams(self.location.search);
34+
deploymentParams.forEach((value,key)=>{
35+
if(!url.searchParams.has(key)){
36+
url.searchParams.set(key,value);
37+
}
38+
});
39+
return url.href;
40+
}
41+
async function fetchRequiredResource(href){
42+
const response=await fetch(withDeploymentQuery(href),{cache:'no-store'});
43+
if(!response.ok){
44+
throw new Error('Failed to cache offline navigation resource: '+href);
45+
}
46+
return response;
47+
}
48+
async function cacheOfflineNavigationResources(){
49+
const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;
50+
const cache=await caches.open(metadata.cacheNamespace);
51+
const manifestResponse=await fetchRequiredResource(metadata.manifestHref);
52+
const manifest=await manifestResponse.clone().json();
53+
await cache.put(metadata.manifestHref,manifestResponse);
54+
const fallbackResponse=await fetchRequiredResource(manifest.fallbackDocument.href);
55+
await cache.put(manifest.fallbackDocument.href,fallbackResponse);
56+
}
57+
self.addEventListener('install',(event)=>{
58+
event.waitUntil((async()=>{
59+
await cacheOfflineNavigationResources();
60+
await self.skipWaiting();
61+
})());
62+
});
63+
self.addEventListener('activate',(event)=>{
64+
event.waitUntil((async()=>{
65+
const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;
66+
const cacheNames=await caches.keys();
67+
await Promise.all(cacheNames.map((cacheName)=>{
68+
if(cacheName.startsWith(CACHE_PREFIX)&&cacheName!==metadata.cacheNamespace){
69+
return caches.delete(cacheName);
70+
}
71+
}));
72+
await self.clients.claim();
73+
})());
74+
});
2975
`
3076
}

test/production/app-dir/offline-navigations/offline-navigations.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@ describe('offlineNavigations build artifacts', () => {
6464
expect(html).not.toContain('offline navigations page')
6565

6666
expect(serviceWorkerScript).toContain(`"buildId":"${buildId}"`)
67+
expect(serviceWorkerScript).toContain(
68+
`"cacheNamespace":"next-offline-navigation-v1:${buildId}:/docs"`
69+
)
6770
expect(serviceWorkerScript).toContain(
6871
`"manifestHref":"/docs/_next/static/${buildId}/_offline-navigation-manifest.json"`
6972
)
73+
expect(serviceWorkerScript).toContain('cacheOfflineNavigationResources')
74+
expect(serviceWorkerScript).toContain('caches.delete')
7075
expect(serviceWorkerScript).toContain('skipWaiting')
7176
expect(serviceWorkerScript).toContain('clients.claim')
7277
expect(serviceWorkerScript).not.toContain('respondWith')
@@ -99,6 +104,7 @@ describe('offlineNavigations build artifacts', () => {
99104
const buildResult = await next.build()
100105
expect(buildResult.exitCode).toBe(0)
101106

107+
const { buildId } = await getOfflineNavigationArtifactPaths()
102108
await next.start({ skipBuild: true })
103109

104110
try {
@@ -139,11 +145,56 @@ describe('offlineNavigations build artifacts', () => {
139145
})
140146
})
141147

148+
const cacheState = await browser.eval(async () => {
149+
const cacheNames = (await caches.keys()).filter((cacheName) =>
150+
cacheName.startsWith('next-offline-navigation-v1:')
151+
)
152+
const entries: Array<{ cacheName: string; pathname: string }> = []
153+
154+
for (const cacheName of cacheNames) {
155+
const cache = await caches.open(cacheName)
156+
const requests = await cache.keys()
157+
158+
for (const request of requests) {
159+
entries.push({
160+
cacheName,
161+
pathname: new URL(request.url).pathname,
162+
})
163+
}
164+
}
165+
166+
return { cacheNames, entries }
167+
})
168+
169+
const cacheName = `next-offline-navigation-v1:${buildId}:/docs`
170+
expect(cacheState.cacheNames).toContain(cacheName)
171+
expect(cacheState.entries).toEqual(
172+
expect.arrayContaining([
173+
{
174+
cacheName,
175+
pathname: `/docs/_next/static/${buildId}/_offline-navigation-manifest.json`,
176+
},
177+
{
178+
cacheName,
179+
pathname: `/docs/_next/static/${buildId}/_offline-navigation-fallback.html`,
180+
},
181+
])
182+
)
183+
142184
await browser.eval(async () => {
143185
if (!('serviceWorker' in navigator)) {
144186
return
145187
}
146188

189+
const cacheNames = await caches.keys()
190+
await Promise.all(
191+
cacheNames
192+
.filter((cacheName) =>
193+
cacheName.startsWith('next-offline-navigation-v1:')
194+
)
195+
.map((cacheName) => caches.delete(cacheName))
196+
)
197+
147198
const registrations = await navigator.serviceWorker.getRegistrations()
148199
await Promise.all(
149200
registrations.map((registration) => registration.unregister())

0 commit comments

Comments
 (0)