Skip to content

Commit cafdfb7

Browse files
committed
Poll for PWA updates
1 parent 6bf13e2 commit cafdfb7

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { startPWAUpdatePolling } from './pwaUpdatePolling';
2+
3+
describe('startPWAUpdatePolling', () => {
4+
beforeEach(() => {
5+
jest.useFakeTimers();
6+
});
7+
8+
afterEach(() => {
9+
jest.useRealTimers();
10+
});
11+
12+
it('checks for updates immediately and on the polling interval while visible', () => {
13+
const registration = { update: jest.fn() };
14+
15+
const stopPolling = startPWAUpdatePolling(registration, {
16+
intervalMs: 1_000,
17+
isVisible: () => true,
18+
});
19+
20+
expect(registration.update).toHaveBeenCalledTimes(1);
21+
22+
jest.advanceTimersByTime(2_000);
23+
24+
expect(registration.update).toHaveBeenCalledTimes(3);
25+
26+
stopPolling();
27+
jest.advanceTimersByTime(1_000);
28+
29+
expect(registration.update).toHaveBeenCalledTimes(3);
30+
});
31+
32+
it('skips update checks while the tab is hidden', () => {
33+
const registration = { update: jest.fn() };
34+
35+
startPWAUpdatePolling(registration, {
36+
intervalMs: 1_000,
37+
isVisible: () => false,
38+
});
39+
40+
jest.advanceTimersByTime(2_000);
41+
42+
expect(registration.update).not.toHaveBeenCalled();
43+
});
44+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export const PWA_UPDATE_CHECK_INTERVAL_MS = 60_000;
2+
3+
type UpdateableServiceWorkerRegistration = Pick<ServiceWorkerRegistration, 'update'>;
4+
5+
interface StartPWAUpdatePollingOptions {
6+
intervalMs?: number;
7+
isVisible?: () => boolean;
8+
}
9+
10+
export function startPWAUpdatePolling(
11+
registration: UpdateableServiceWorkerRegistration,
12+
{
13+
intervalMs = PWA_UPDATE_CHECK_INTERVAL_MS,
14+
isVisible = () => document.visibilityState === 'visible',
15+
}: StartPWAUpdatePollingOptions = {},
16+
) {
17+
const checkForUpdate = () => {
18+
if (isVisible()) {
19+
void registration.update();
20+
}
21+
};
22+
23+
checkForUpdate();
24+
25+
const intervalId = window.setInterval(checkForUpdate, intervalMs);
26+
27+
return () => window.clearInterval(intervalId);
28+
}

src/hooks/useRegisterSW/useRegisterSW.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import { registerSW } from 'virtual:pwa-register';
22
import { useEffect, useRef, useState } from 'react';
3+
import { startPWAUpdatePolling } from './pwaUpdatePolling';
34

45
export function usePWAUpdate() {
56
const [updateAvailable, setUpdateAvailable] = useState(false);
67
const updateSWRef = useRef<(reloadPage?: boolean) => Promise<void>>();
8+
const stopUpdatePollingRef = useRef<() => void>();
79

810
useEffect(() => {
911
if (!import.meta.env.PROD) {
1012
return;
1113
}
1214

1315
updateSWRef.current = registerSW({
16+
immediate: true,
1417
onNeedRefresh() {
1518
setUpdateAvailable(true);
1619
},
20+
onRegisteredSW(_swUrl, registration) {
21+
if (!registration) {
22+
return;
23+
}
24+
25+
stopUpdatePollingRef.current?.();
26+
stopUpdatePollingRef.current = startPWAUpdatePolling(registration);
27+
},
1728
onOfflineReady() {
1829
// optionally notify
1930
},
2031
});
32+
33+
return () => {
34+
stopUpdatePollingRef.current?.();
35+
};
2136
}, []);
2237

2338
const updateSW = async (reloadPage = true) => {

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig({
2727
viteTsconfigPaths(),
2828
ViteYaml(),
2929
VitePWA({
30-
registerType: 'autoUpdate',
30+
registerType: 'prompt',
3131
workbox: {
3232
importScripts: ['notification-sw.js'],
3333
},

0 commit comments

Comments
 (0)