@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.
@@ -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: '/'
@@ -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 wont block the installation of the service worker
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
- const retrieveFromCache = caches.match(request);
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
- const preloadResponse = await Promise.resolve(event.preloadResponse);
148
- const responseFromPreloadOrFetch =
149
- preloadResponse || (await fetch(request));
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 (error) {
165
- console.error("Network fetch failed:", error, request.url);
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 non-HTML requests, look in cache first, fall back to network
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 retrieveFromCache;
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
- } else {
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-frontend",
3
- "version": "1.0.0-beta.32",
3
+ "version": "1.0.0-beta.33",
4
4
  "description": "Frontend components for Indiekit (fork with floating toolbar)",
5
5
  "keywords": [
6
6
  "express",