Skip to content

Commit b769674

Browse files
committed
Allow offline manifest fallback when offline
1 parent cf6f099 commit b769674

2 files changed

Lines changed: 130 additions & 31 deletions

File tree

index.html

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ <h2 id="offline-title">Offline bundle</h2>
541541
const resetBusyLabel = resetButton && resetButton.dataset && resetButton.dataset.busyLabel
542542
? resetButton.dataset.busyLabel
543543
: 'Refreshing offline bundle…';
544+
const METADATA_CACHE_NAME = 'toolbox-offline-metadata';
544545

545546
if (!statusEl || !progressWrapper || !progressBar || !progressFill || !progressDetail) {
546547
return;
@@ -597,6 +598,23 @@ <h2 id="offline-title">Offline bundle</h2>
597598
return `${formatter.format(Number(value.toFixed(fractionDigits)))} ${units[unitIndex]}`;
598599
}
599600

601+
async function readCachedManifest() {
602+
try {
603+
const cache = await caches.open(METADATA_CACHE_NAME);
604+
const requestUrl = new URL('__toolbox-offline-manifest__', self.location.href).toString();
605+
const response = await cache.match(requestUrl);
606+
if (!response) {
607+
return null;
608+
}
609+
610+
const manifest = await response.json();
611+
return manifest && typeof manifest === 'object' ? manifest : null;
612+
} catch (error) {
613+
console.warn('Failed to read cached offline manifest', error);
614+
return null;
615+
}
616+
}
617+
600618
function getCommitLabel(commit) {
601619
if (!commit || typeof commit !== 'object') {
602620
return '';
@@ -837,42 +855,54 @@ <h2 id="offline-title">Offline bundle</h2>
837855
return;
838856
}
839857

858+
let manifest;
859+
let manifestSource = 'network';
860+
840861
try {
841862
const response = await fetch('offline-manifest.json', { cache: 'no-store' });
842863
if (!response.ok) {
843864
throw new Error(`Manifest request failed with status ${response.status}`);
844865
}
845866

846-
const manifest = await response.json();
847-
state.manifest = manifest;
848-
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
849-
const totalBytes = Number.isFinite(manifest.totalBytes)
850-
? manifest.totalBytes
851-
: assets.reduce((total, asset) => total + (Number(asset.bytes) || 0), 0);
852-
853-
if (!Number.isFinite(manifest.totalBytes)) {
854-
manifest.totalBytes = totalBytes;
855-
}
856-
if (typeof manifest.totalAssets !== 'number') {
857-
manifest.totalAssets = assets.length;
867+
manifest = await response.json();
868+
} catch (error) {
869+
console.warn('Failed to load offline manifest from network, attempting cached copy.', error);
870+
manifest = await readCachedManifest();
871+
if (!manifest) {
872+
console.error('Unable to load offline manifest from network or cache', error);
873+
setStatus('Unable to load the offline manifest. Refresh when you are back online.');
874+
return;
858875
}
876+
manifestSource = 'cache';
877+
}
859878

860-
const summary = buildBundleSummary({
861-
version: manifest.version,
862-
totalAssets: assets.length,
863-
totalBytes,
864-
commit: manifest.commit || null
865-
});
879+
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
880+
const totalBytes = Number.isFinite(manifest.totalBytes)
881+
? manifest.totalBytes
882+
: assets.reduce((total, asset) => total + (Number(asset.bytes) || 0), 0);
866883

867-
updateMeta(summary);
868-
setStatus('Offline bundle ready to download.');
869-
updateResetAvailability();
870-
} catch (error) {
871-
console.error('Failed to load offline manifest', error);
872-
setStatus('Unable to load the offline manifest. Refresh when you are back online.');
873-
return;
884+
if (!Number.isFinite(manifest.totalBytes)) {
885+
manifest.totalBytes = totalBytes;
886+
}
887+
if (typeof manifest.totalAssets !== 'number') {
888+
manifest.totalAssets = assets.length;
874889
}
875890

891+
state.manifest = manifest;
892+
893+
const summary = buildBundleSummary({
894+
version: manifest.version,
895+
totalAssets: assets.length,
896+
totalBytes,
897+
commit: manifest.commit || null
898+
});
899+
900+
updateMeta(summary);
901+
setStatus(manifestSource === 'cache' ? 'Offline bundle ready for offline use.' : 'Offline bundle ready to download.');
902+
updateResetAvailability();
903+
904+
const shouldRequestCaching = manifestSource === 'network';
905+
876906
try {
877907
const registration = await navigator.serviceWorker.register('service-worker.js');
878908
navigator.serviceWorker.addEventListener('message', handleMessage);
@@ -886,7 +916,9 @@ <h2 id="offline-title">Offline bundle</h2>
886916
const readyRegistration = await navigator.serviceWorker.ready;
887917
state.registration = readyRegistration;
888918
updateResetAvailability();
889-
requestCaching(readyRegistration);
919+
if (shouldRequestCaching) {
920+
requestCaching(readyRegistration);
921+
}
890922
} catch (error) {
891923
console.error('Service worker registration failed', error);
892924
setStatus('Unable to register the offline service worker.');

service-worker.js

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const OFFLINE_MANIFEST_PATH = new URL('offline-manifest.json', self.location.ori
66

77
let activeCacheName = null;
88
let pendingUpdate = null;
9+
let cachedManifest = null;
10+
let activeCacheLookupPromise = null;
911

1012
self.addEventListener('install', (event) => {
1113
event.waitUntil(self.skipWaiting());
@@ -51,22 +53,71 @@ self.addEventListener('fetch', (event) => {
5153
}
5254

5355
if (url.pathname === OFFLINE_MANIFEST_PATH) {
54-
event.respondWith(handleManifestRequest(event.request));
56+
event.respondWith((async () => {
57+
await ensureActiveCacheName();
58+
return handleManifestRequest(event.request);
59+
})());
5560
return;
5661
}
5762

5863
if (event.request.mode === 'navigate') {
59-
event.respondWith(handleNavigationRequest(event.request));
64+
event.respondWith((async () => {
65+
await ensureActiveCacheName();
66+
return handleNavigationRequest(event.request);
67+
})());
6068
return;
6169
}
6270

63-
event.respondWith(handleAssetRequest(event.request));
71+
event.respondWith((async () => {
72+
await ensureActiveCacheName();
73+
return handleAssetRequest(event.request);
74+
})());
6475
});
6576

6677
function getCacheName(version) {
6778
return `${CACHE_PREFIX}${version}`;
6879
}
6980

81+
async function ensureActiveCacheName() {
82+
if (activeCacheName) {
83+
return activeCacheName;
84+
}
85+
86+
if (!activeCacheLookupPromise) {
87+
activeCacheLookupPromise = (async () => {
88+
let stored = null;
89+
try {
90+
stored = await readStoredManifest();
91+
} catch (error) {
92+
console.warn('Failed to read stored manifest while determining active cache', error);
93+
}
94+
95+
if (stored && typeof stored.version === 'string' && stored.version) {
96+
activeCacheName = getCacheName(stored.version);
97+
return activeCacheName;
98+
}
99+
100+
try {
101+
const keys = await caches.keys();
102+
const fallback = keys.find((key) => key.startsWith(CACHE_PREFIX));
103+
if (fallback) {
104+
activeCacheName = fallback;
105+
}
106+
} catch (error) {
107+
console.warn('Failed to inspect caches for active cache name', error);
108+
}
109+
110+
return activeCacheName;
111+
})();
112+
}
113+
114+
try {
115+
return await activeCacheLookupPromise;
116+
} finally {
117+
activeCacheLookupPromise = null;
118+
}
119+
}
120+
70121
async function queueManifestUpdate(manifest, options = {}) {
71122
if (!manifest || typeof manifest.version !== 'string' || !Array.isArray(manifest.assets)) {
72123
return;
@@ -100,6 +151,7 @@ async function applyManifest(manifest, options = {}) {
100151
? manifest.totalBytes
101152
: assets.reduce((sum, asset) => sum + (Number(asset.bytes) || 0), 0);
102153
const cacheName = getCacheName(version);
154+
await ensureActiveCacheName();
103155
const existingManifest = await readStoredManifest();
104156
const cacheKeys = await caches.keys();
105157
const cacheExists = cacheKeys.includes(cacheName);
@@ -385,16 +437,29 @@ async function broadcast(message) {
385437
}
386438

387439
async function readStoredManifest() {
388-
const cache = await caches.open(METADATA_CACHE);
440+
if (cachedManifest) {
441+
return cachedManifest;
442+
}
443+
444+
let cache;
445+
try {
446+
cache = await caches.open(METADATA_CACHE);
447+
} catch (error) {
448+
console.warn('Failed to open offline metadata cache', error);
449+
return null;
450+
}
451+
389452
const response = await cache.match(METADATA_REQUEST);
390453
if (!response) {
391454
return null;
392455
}
393456

394457
try {
395-
return await response.json();
458+
cachedManifest = await response.json();
459+
return cachedManifest;
396460
} catch (error) {
397461
console.warn('Failed to parse stored offline manifest', error);
462+
cachedManifest = null;
398463
return null;
399464
}
400465
}
@@ -407,6 +472,7 @@ async function writeStoredManifest(manifest) {
407472
}
408473
});
409474
await cache.put(METADATA_REQUEST, response);
475+
cachedManifest = manifest;
410476
}
411477

412478
async function cleanupCaches(currentName) {
@@ -437,6 +503,7 @@ async function clearOfflineCaches() {
437503

438504
return Promise.resolve(false);
439505
}));
506+
cachedManifest = null;
440507
}
441508

442509
function getCommitInfo(rawCommit) {

0 commit comments

Comments
 (0)