@rmdes/indiekit-frontend 1.0.0-beta.32 → 1.0.0-beta.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/layouts/default.njk +6 -0
- package/lib/serviceworker.js +128 -45
- package/package.json +1 -1
package/layouts/default.njk
CHANGED
|
@@ -122,6 +122,12 @@
|
|
|
122
122
|
{% endblock %}
|
|
123
123
|
<script type="module">
|
|
124
124
|
if (navigator.serviceWorker) {
|
|
125
|
+
// Reload page when a new service worker activates (new deploy)
|
|
126
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
127
|
+
if (event.data && event.data.command === "SW_UPDATED") {
|
|
128
|
+
window.location.reload();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
125
131
|
window.addEventListener("load", () => {
|
|
126
132
|
navigator.serviceWorker.register("/serviceworker.js", {
|
|
127
133
|
scope: '/'
|
package/lib/serviceworker.js
CHANGED
|
@@ -8,6 +8,15 @@ const cacheList = new Set([assetCacheName, pagesCacheName, imageCacheName]);
|
|
|
8
8
|
const placeholderImage = `<svg xmlns="http://www.w3.org/2000/svg"><defs><path id="icon" fill="#AAA" d="M24 32a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm-6.9-11.9 4.1 4.1a17 17 0 0 0-9.7 5.3L8 26a22 22 0 0 1 9-6Zm22.5 5.4L36 29l-.8-.8L26 19a22 22 0 0 1 13.5 6.4ZM8.2 11.2l3.7 3.7a24.7 24.7 0 0 0-8.4 6.6l-3.6-3.6c2.4-2.7 5.2-5 8.3-6.7ZM24 7a32 32 0 0 1 23.4 10.2l-3.5 3.6a27 27 0 0 0-24.5-8.4l-4.2-4.2A32 32 0 0 1 24 7ZM2 5l3-3 41 41-3 3L2 5Z" opacity=".7"/>
|
|
9
9
|
</defs><rect fill="#000" width="100%" height="100%" opacity="0.075"/><use href="#icon" x="50%" y="50%" transform="translate(-24 -24)"/></svg>`;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Check if a URL is a hashed asset (content-addressable, immutable)
|
|
13
|
+
* @param {string} url - Request URL
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function isHashedAsset(url) {
|
|
17
|
+
return /\/assets\/app-[a-f0-9]+\.(js|css)$/.test(url);
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* Update asset cache
|
|
13
22
|
* @returns {Promise<Cache>} - Updated asset cache
|
|
@@ -16,7 +25,7 @@ async function updateAssetCache() {
|
|
|
16
25
|
try {
|
|
17
26
|
const assetCache = await caches.open(assetCacheName);
|
|
18
27
|
|
|
19
|
-
// These items won
|
|
28
|
+
// These items won't block the installation of the service worker
|
|
20
29
|
assetCache.addAll(["/app.webmanifest"]);
|
|
21
30
|
|
|
22
31
|
// These items must be cached for service worker to complete installation
|
|
@@ -67,6 +76,27 @@ async function clearOldCaches() {
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Clear the pages cache so stale HTML (with old asset references) is not served
|
|
81
|
+
*/
|
|
82
|
+
async function clearPagesCache() {
|
|
83
|
+
try {
|
|
84
|
+
await caches.delete(pagesCacheName);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("Error clearing pages cache", error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Notify all clients that the service worker has been updated
|
|
92
|
+
*/
|
|
93
|
+
async function notifyClients() {
|
|
94
|
+
const allClients = await clients.matchAll({ includeUncontrolled: true });
|
|
95
|
+
for (const client of allClients) {
|
|
96
|
+
client.postMessage({ command: "SW_UPDATED", version: "APP_VERSION" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
/**
|
|
71
101
|
* Trim cache
|
|
72
102
|
* @param {string} cacheName - Name of cache
|
|
@@ -100,7 +130,9 @@ self.addEventListener("activate", async (event) => {
|
|
|
100
130
|
event.waitUntil(
|
|
101
131
|
(async () => {
|
|
102
132
|
await clearOldCaches();
|
|
133
|
+
await clearPagesCache();
|
|
103
134
|
await clients.claim();
|
|
135
|
+
await notifyClients();
|
|
104
136
|
})(),
|
|
105
137
|
);
|
|
106
138
|
});
|
|
@@ -126,47 +158,40 @@ self.addEventListener("fetch", (event) => {
|
|
|
126
158
|
return;
|
|
127
159
|
}
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// For HTML requests, try network, fall back to cache, else show offline page
|
|
161
|
+
// For HTML requests, try network with timeout, fall back to cache
|
|
132
162
|
if (
|
|
133
163
|
request.mode === "navigate" ||
|
|
134
164
|
request.headers.get("Accept").includes("text/html")
|
|
135
165
|
) {
|
|
136
166
|
event.respondWith(
|
|
137
167
|
(async () => {
|
|
138
|
-
// CHECK CACHE
|
|
139
|
-
const timer = setTimeout(async () => {
|
|
140
|
-
const responseFromCache = await retrieveFromCache;
|
|
141
|
-
if (responseFromCache) {
|
|
142
|
-
return responseFromCache;
|
|
143
|
-
}
|
|
144
|
-
}, timeout);
|
|
145
|
-
|
|
146
168
|
try {
|
|
147
|
-
|
|
148
|
-
const responseFromPreloadOrFetch =
|
|
149
|
-
|
|
169
|
+
// Race network against a timeout for faster fallback on slow connections
|
|
170
|
+
const responseFromPreloadOrFetch = await Promise.race([
|
|
171
|
+
(async () => {
|
|
172
|
+
const preloadResponse = await Promise.resolve(
|
|
173
|
+
event.preloadResponse,
|
|
174
|
+
);
|
|
175
|
+
return preloadResponse || (await fetch(request));
|
|
176
|
+
})(),
|
|
177
|
+
new Promise((_, reject) =>
|
|
178
|
+
setTimeout(() => reject(new Error("Network timeout")), timeout),
|
|
179
|
+
),
|
|
180
|
+
]);
|
|
150
181
|
|
|
151
|
-
// NETWORK
|
|
152
|
-
// Save a copy of page to pages cache
|
|
153
|
-
clearTimeout(timer);
|
|
182
|
+
// NETWORK succeeded — cache and serve
|
|
154
183
|
try {
|
|
155
184
|
const copy = responseFromPreloadOrFetch.clone();
|
|
156
185
|
const pagesCache = await caches.open(pagesCacheName);
|
|
157
186
|
await pagesCache.put(request, copy);
|
|
158
187
|
} catch (cacheError) {
|
|
159
|
-
// Cache put failed (e.g., network error response), but continue serving the response
|
|
160
188
|
console.error("Failed to cache page:", cacheError);
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
return responseFromPreloadOrFetch;
|
|
164
|
-
} catch
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// CACHE or OFFLINE PAGE
|
|
168
|
-
clearTimeout(timer);
|
|
169
|
-
const responseFromCache = await retrieveFromCache;
|
|
192
|
+
} catch {
|
|
193
|
+
// NETWORK failed or timed out — fall back to cache
|
|
194
|
+
const responseFromCache = await caches.match(request);
|
|
170
195
|
const offlineResponse = await caches.match("/offline");
|
|
171
196
|
return (
|
|
172
197
|
responseFromCache ||
|
|
@@ -184,33 +209,93 @@ self.addEventListener("fetch", (event) => {
|
|
|
184
209
|
return;
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
// For
|
|
212
|
+
// For hashed assets (e.g. /assets/app-abc123.js): cache-first
|
|
213
|
+
// These URLs are content-addressable — the content never changes for a given hash
|
|
214
|
+
if (isHashedAsset(request.url)) {
|
|
215
|
+
event.respondWith(
|
|
216
|
+
(async () => {
|
|
217
|
+
const responseFromCache = await caches.match(request);
|
|
218
|
+
if (responseFromCache) {
|
|
219
|
+
return responseFromCache;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const responseFromFetch = await fetch(request);
|
|
224
|
+
const copy = responseFromFetch.clone();
|
|
225
|
+
const assetCache = await caches.open(assetCacheName);
|
|
226
|
+
await assetCache.put(request, copy);
|
|
227
|
+
return responseFromFetch;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error("Fetch failed for hashed asset:", error, request.url);
|
|
230
|
+
return new Response("Network error", {
|
|
231
|
+
status: 503,
|
|
232
|
+
statusText: "Service Unavailable",
|
|
233
|
+
headers: { "Content-Type": "text/plain" },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
})(),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// For other non-HTML requests: stale-while-revalidate
|
|
243
|
+
// Serve from cache immediately, update cache in background
|
|
188
244
|
event.respondWith(
|
|
189
245
|
(async () => {
|
|
190
246
|
try {
|
|
191
|
-
const responseFromCache = await
|
|
247
|
+
const responseFromCache = await caches.match(request);
|
|
248
|
+
|
|
249
|
+
// Start network fetch regardless (to update cache)
|
|
250
|
+
const fetchPromise = fetch(request)
|
|
251
|
+
.then(async (responseFromFetch) => {
|
|
252
|
+
// Update cache with fresh response
|
|
253
|
+
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
|
|
254
|
+
try {
|
|
255
|
+
const copy = responseFromFetch.clone();
|
|
256
|
+
const imagesCache = await caches.open(imageCacheName);
|
|
257
|
+
await imagesCache.put(request, copy);
|
|
258
|
+
} catch (cacheError) {
|
|
259
|
+
console.error("Failed to cache image:", cacheError);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return responseFromFetch;
|
|
263
|
+
})
|
|
264
|
+
.catch((error) => {
|
|
265
|
+
console.error(
|
|
266
|
+
"Background fetch failed:",
|
|
267
|
+
error,
|
|
268
|
+
request.url,
|
|
269
|
+
);
|
|
270
|
+
return null;
|
|
271
|
+
});
|
|
192
272
|
|
|
193
273
|
if (responseFromCache) {
|
|
194
|
-
// CACHE
|
|
274
|
+
// CACHE HIT — serve cached, update in background
|
|
195
275
|
return responseFromCache;
|
|
196
|
-
}
|
|
197
|
-
const responseFromFetch = await fetch(request);
|
|
198
|
-
|
|
199
|
-
// NETWORK
|
|
200
|
-
// If request is for an image, save a copy to images cache
|
|
201
|
-
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
|
|
202
|
-
try {
|
|
203
|
-
const copy = responseFromFetch.clone();
|
|
204
|
-
const imagesCache = await caches.open(imageCacheName);
|
|
205
|
-
await imagesCache.put(request, copy);
|
|
206
|
-
} catch (cacheError) {
|
|
207
|
-
// Cache put failed (e.g., network error response), but continue serving the response
|
|
208
|
-
console.error("Failed to cache image:", cacheError);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
276
|
+
}
|
|
211
277
|
|
|
278
|
+
// CACHE MISS — wait for network
|
|
279
|
+
const responseFromFetch = await fetchPromise;
|
|
280
|
+
if (responseFromFetch) {
|
|
212
281
|
return responseFromFetch;
|
|
213
282
|
}
|
|
283
|
+
|
|
284
|
+
// OFFLINE IMAGE
|
|
285
|
+
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
|
|
286
|
+
return new Response(placeholderImage, {
|
|
287
|
+
headers: {
|
|
288
|
+
"Content-Type": "image/svg+xml",
|
|
289
|
+
"Cache-Control": "no-store",
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return new Response("Network error", {
|
|
295
|
+
status: 503,
|
|
296
|
+
statusText: "Service Unavailable",
|
|
297
|
+
headers: { "Content-Type": "text/plain" },
|
|
298
|
+
});
|
|
214
299
|
} catch (error) {
|
|
215
300
|
console.error(
|
|
216
301
|
"Fetch failed for non-HTML resource:",
|
|
@@ -218,7 +303,6 @@ self.addEventListener("fetch", (event) => {
|
|
|
218
303
|
request.url,
|
|
219
304
|
);
|
|
220
305
|
|
|
221
|
-
// OFFLINE IMAGE
|
|
222
306
|
if (/\.(jpe?g|png|gif|svg|webp)/.test(request.url)) {
|
|
223
307
|
return new Response(placeholderImage, {
|
|
224
308
|
headers: {
|
|
@@ -228,7 +312,6 @@ self.addEventListener("fetch", (event) => {
|
|
|
228
312
|
});
|
|
229
313
|
}
|
|
230
314
|
|
|
231
|
-
// For other resources, return a network error response
|
|
232
315
|
return new Response("Network error", {
|
|
233
316
|
status: 503,
|
|
234
317
|
statusText: "Service Unavailable",
|