Skip to content

Commit 829dcd8

Browse files
committed
Cache offline navigation artifacts in service worker
1 parent a7a800c commit 829dcd8

3 files changed

Lines changed: 100 additions & 2 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: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,65 @@ export function getOfflineNavigationServiceWorkerFilePath(): string {
99

1010
export function createOfflineNavigationServiceWorker({
1111
buildId,
12+
cacheNamespace,
1213
manifestHref,
1314
}: {
1415
buildId: string
16+
cacheNamespace: string
1517
manifestHref: string
1618
}): string {
1719
const metadata = JSON.stringify({
1820
buildId,
21+
cacheNamespace,
1922
manifestHref,
2023
source: 'offline-navigation-service-worker',
2124
})
2225

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

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)