Skip to content

Commit 0627f51

Browse files
committed
Bridge offline navigation messages to useOffline
1 parent 77b9d6f commit 0627f51

7 files changed

Lines changed: 144 additions & 2 deletions

File tree

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'node:path'
22

33
import { CLIENT_STATIC_FILES_PATH } from '../shared/lib/constants'
4+
import { OFFLINE_NAVIGATION_FALLBACK_SERVED } from '../shared/lib/offline-navigation-constants'
45

56
export const OFFLINE_NAVIGATION_SERVICE_WORKER =
67
'_offline-navigation-service-worker.js'
@@ -27,6 +28,9 @@ export function createOfflineNavigationServiceWorker({
2728
manifestHref,
2829
source: 'offline-navigation-service-worker',
2930
})
31+
const fallbackServedMessageType = JSON.stringify(
32+
OFFLINE_NAVIGATION_FALLBACK_SERVED
33+
)
3034

3135
return `self.__NEXT_OFFLINE_NAVIGATION_SW=${metadata};
3236
const CACHE_PREFIX='next-offline-navigation-v1:';
@@ -71,11 +75,21 @@ async function fetchDocumentNavigation(request){
7175
const cache=await caches.open(metadata.cacheNamespace);
7276
const fallbackResponse=await cache.match(metadata.fallbackDocumentHref);
7377
if(fallbackResponse){
78+
await notifyClients({
79+
type:${fallbackServedMessageType},
80+
buildId:metadata.buildId,
81+
reason:'network-error',
82+
url:request.url
83+
});
7484
return fallbackResponse;
7585
}
7686
throw err;
7787
}
7888
}
89+
async function notifyClients(message){
90+
const clients=await self.clients.matchAll({type:'window',includeUncontrolled:true});
91+
await Promise.all(clients.map((client)=>client.postMessage(message)));
92+
}
7993
self.addEventListener('install',(event)=>{
8094
event.waitUntil((async()=>{
8195
await cacheOfflineNavigationResources();

packages/next/src/client/components/offline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function getOffline(): OfflineState | null {
7777
* Enters the offline state if not already in it, and starts the
7878
* connectivity polling loop.
7979
*/
80-
function notifyOffline(): OfflineState {
80+
export function notifyOffline(): OfflineState {
8181
if (offlineState !== null) {
8282
return offlineState
8383
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import { getDeploymentIdQuery } from '../shared/lib/deployment-id'
2+
import { OFFLINE_NAVIGATION_FALLBACK_SERVED } from '../shared/lib/offline-navigation-constants'
23

34
const OFFLINE_NAVIGATION_SERVICE_WORKER =
45
'_offline-navigation-service-worker.js'
56

7+
let isListeningForServiceWorkerMessages = false
8+
9+
type OfflineNavigationFallbackServedMessage = {
10+
type: typeof OFFLINE_NAVIGATION_FALLBACK_SERVED
11+
buildId?: string
12+
reason?: 'network-error'
13+
url?: string
14+
}
15+
616
function getBasePath(): string {
717
return (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
818
}
@@ -16,6 +26,38 @@ function getServiceWorkerScope(): string {
1626
return basePath ? `${basePath}/` : '/'
1727
}
1828

29+
function listenForOfflineNavigationMessages(): void {
30+
if (isListeningForServiceWorkerMessages) {
31+
return
32+
}
33+
isListeningForServiceWorkerMessages = true
34+
35+
navigator.serviceWorker.addEventListener('message', (event) => {
36+
handleOfflineNavigationServiceWorkerMessage(event.data)
37+
})
38+
}
39+
40+
export function handleOfflineNavigationServiceWorkerMessage(data: unknown) {
41+
if (!isOfflineNavigationFallbackServedMessage(data)) {
42+
return
43+
}
44+
45+
const { notifyOffline } =
46+
require('./components/offline') as typeof import('./components/offline')
47+
notifyOffline()
48+
}
49+
50+
function isOfflineNavigationFallbackServedMessage(
51+
data: unknown
52+
): data is OfflineNavigationFallbackServedMessage {
53+
return (
54+
typeof data === 'object' &&
55+
data !== null &&
56+
(data as Partial<OfflineNavigationFallbackServedMessage>).type ===
57+
OFFLINE_NAVIGATION_FALLBACK_SERVED
58+
)
59+
}
60+
1961
export function registerOfflineNavigationServiceWorker(): void {
2062
if (
2163
process.env.__NEXT_DEV_SERVER ||
@@ -26,6 +68,8 @@ export function registerOfflineNavigationServiceWorker(): void {
2668
return
2769
}
2870

71+
listenForOfflineNavigationMessages()
72+
2973
navigator.serviceWorker
3074
.register(getServiceWorkerHref(), {
3175
scope: getServiceWorkerScope(),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const OFFLINE_NAVIGATION_FALLBACK_SERVED =
2+
'next-offline-navigation-fallback-served'
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client'
2+
3+
import { useOffline } from 'next/offline'
4+
5+
export function OfflineStatus() {
6+
const isOffline = useOffline()
7+
return <p id="offline-status">{isOffline ? 'offline' : 'online'}</p>
8+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { OfflineStatus } from './offline-status'
2+
13
export default function Page() {
2-
return <p>offline navigations page</p>
4+
return (
5+
<>
6+
<p>offline navigations page</p>
7+
<OfflineStatus />
8+
</>
9+
)
310
}

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { nextTestSetup } from 'e2e-utils'
44
import { retry } from 'next-test-utils'
55
import type * as Playwright from 'playwright'
66

7+
const OFFLINE_NAVIGATION_FALLBACK_SERVED =
8+
'next-offline-navigation-fallback-served'
9+
710
describe('offlineNavigations build artifacts', () => {
811
const { next } = nextTestSetup({
912
files: __dirname,
@@ -76,6 +79,7 @@ describe('offlineNavigations build artifacts', () => {
7679
)
7780
expect(serviceWorkerScript).toContain('cacheOfflineNavigationResources')
7881
expect(serviceWorkerScript).toContain('caches.delete')
82+
expect(serviceWorkerScript).toContain(OFFLINE_NAVIGATION_FALLBACK_SERVED)
7983
expect(serviceWorkerScript).toContain('isDocumentNavigationRequest')
8084
expect(serviceWorkerScript).toContain('skipWaiting')
8185
expect(serviceWorkerScript).toContain('clients.claim')
@@ -159,6 +163,59 @@ describe('offlineNavigations build artifacts', () => {
159163
await browser.eval(() => Boolean(navigator.serviceWorker.controller))
160164
).toBe(true)
161165
})
166+
expect(await browser.elementById('offline-status').text()).toBe('online')
167+
168+
await browser.eval((messageType) => {
169+
const win = window as typeof window & {
170+
__restoreOfflineNavigationFetch?: () => void
171+
}
172+
const originalFetch = window.fetch
173+
win.__restoreOfflineNavigationFetch = () => {
174+
window.fetch = originalFetch
175+
delete win.__restoreOfflineNavigationFetch
176+
}
177+
window.fetch = async () => {
178+
throw new TypeError('offline navigation test')
179+
}
180+
navigator.serviceWorker.dispatchEvent(
181+
new MessageEvent('message', {
182+
data: {
183+
type: messageType,
184+
reason: 'network-error',
185+
url: location.href,
186+
},
187+
})
188+
)
189+
}, OFFLINE_NAVIGATION_FALLBACK_SERVED)
190+
await retry(async () => {
191+
expect(await browser.elementById('offline-status').text()).toBe(
192+
'offline'
193+
)
194+
})
195+
await browser.eval(() => {
196+
const win = window as typeof window & {
197+
__restoreOfflineNavigationFetch?: () => void
198+
}
199+
win.__restoreOfflineNavigationFetch?.()
200+
window.dispatchEvent(new Event('online'))
201+
})
202+
await retry(async () => {
203+
expect(await browser.elementById('offline-status').text()).toBe(
204+
'online'
205+
)
206+
})
207+
208+
await browser.eval((messageType) => {
209+
localStorage.removeItem('__nextOfflineNavigationMessage')
210+
navigator.serviceWorker.addEventListener('message', (event) => {
211+
if (event.data?.type === messageType) {
212+
localStorage.setItem(
213+
'__nextOfflineNavigationMessage',
214+
JSON.stringify(event.data)
215+
)
216+
}
217+
})
218+
}, OFFLINE_NAVIGATION_FALLBACK_SERVED)
162219

163220
const cacheState = await browser.eval(async () => {
164221
const cacheNames = (await caches.keys()).filter((cacheName) =>
@@ -221,6 +278,16 @@ describe('offlineNavigations build artifacts', () => {
221278
)
222279
)
223280
).toBe(true)
281+
const serviceWorkerMessage = await browser.eval(() => {
282+
const message = localStorage.getItem('__nextOfflineNavigationMessage')
283+
return message === null ? null : JSON.parse(message)
284+
})
285+
expect(serviceWorkerMessage).toMatchObject({
286+
type: OFFLINE_NAVIGATION_FALLBACK_SERVED,
287+
buildId,
288+
reason: 'network-error',
289+
url: `${next.url}/docs/offline-navigation-cache-miss`,
290+
})
224291
await page!.context().setOffline(false)
225292

226293
await browser.eval(async () => {

0 commit comments

Comments
 (0)