@ultimat3/pwa 19.1.1 → 19.1.3
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/package.json +3 -3
- package/src/service-worker.ts +45 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/pwa",
|
|
3
|
-
"version": "19.1.
|
|
3
|
+
"version": "19.1.3",
|
|
4
4
|
"description": "Generated service worker, web manifest, icons, push and version-skew handling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "19.1.
|
|
35
|
-
"@ultimat3/seo": "19.1.
|
|
34
|
+
"@ultimat3/core": "19.1.3",
|
|
35
|
+
"@ultimat3/seo": "19.1.3"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/service-worker.ts
CHANGED
|
@@ -252,7 +252,8 @@ const RUNTIME=${JSON.stringify(cacheNamespace(buildId, 'runtime'))};
|
|
|
252
252
|
const PAGES=${JSON.stringify(cacheNamespace(buildId, 'pages'))};
|
|
253
253
|
const RETAINED=${JSON.stringify(retainedCaches(retained))};
|
|
254
254
|
const NEVER_CACHE=${JSON.stringify(neverCache)};
|
|
255
|
-
const BUILD_HEADER=${JSON.stringify(BUILD_ID_HEADER)}
|
|
255
|
+
const BUILD_HEADER=${JSON.stringify(BUILD_ID_HEADER)};
|
|
256
|
+
let SKEWED=false;`.trim();
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
function retainedCaches(buildIds: readonly string[]): readonly string[] {
|
|
@@ -316,6 +317,30 @@ self.addEventListener('activate',(event)=>{
|
|
|
316
317
|
});`.trim();
|
|
317
318
|
}
|
|
318
319
|
|
|
320
|
+
/**
|
|
321
|
+
* The status `@ultimat3/http` answers a stale build id with. Declared here as a number rather
|
|
322
|
+
* than imported: tier-4 packages do not import each other, and the wire status is the contract
|
|
323
|
+
* between them — `X_BUILD_SKEW` maps to 409 in `packages/http/src/error-map.ts`.
|
|
324
|
+
*/
|
|
325
|
+
const SKEW_STATUS = 409;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The fetch block, plus the one escape from a deadlock the skew guard can otherwise create.
|
|
329
|
+
*
|
|
330
|
+
* The guard is sound in both halves and lethal together: this worker stamps its own `BUILD_ID`
|
|
331
|
+
* on every request it proxies, and a server on a newer build answers that with a 409 whose body
|
|
332
|
+
* is a refusal page rather than the app. The refusal page is not the app, so nothing in it posts
|
|
333
|
+
* `skip-waiting`; the replacement worker stays `installed` and waiting, because a waiting worker
|
|
334
|
+
* activates only once every client is released; and a reload re-enters the same worker, which
|
|
335
|
+
* stamps the same id and earns the same 409. Measured on a dev restart: `waiting: "installed"`,
|
|
336
|
+
* `active: "activated"`, and every navigation 409 until the registration was cleared by hand —
|
|
337
|
+
* while the error's own advice was "reload the page".
|
|
338
|
+
*
|
|
339
|
+
* `healSkew` breaks it at the only point still under this worker's control: having been told it
|
|
340
|
+
* is stale, it stops stamping, re-issues the request untagged so the document actually loads, and
|
|
341
|
+
* tells every window an update is waiting. Skew detection is not lost — it has already fired, and
|
|
342
|
+
* its whole purpose is to get the client onto the new build, which is what the app now does.
|
|
343
|
+
*/
|
|
319
344
|
function fetchBlock(): string {
|
|
320
345
|
return `
|
|
321
346
|
function ruleFor(url){
|
|
@@ -339,10 +364,27 @@ self.addEventListener('fetch',(event)=>{
|
|
|
339
364
|
if(!fn)return;
|
|
340
365
|
// Every proxied request carries the client's build id so the server can detect skew.
|
|
341
366
|
const tagged=new Request(req,{headers:withBuild(req.headers)});
|
|
342
|
-
event.respondWith(fn(tagged,cacheName(rule.c),()=>offlineFallback(req)));
|
|
367
|
+
event.respondWith(fn(tagged,cacheName(rule.c),()=>offlineFallback(req)).then((res)=>healSkew(req,res)));
|
|
343
368
|
});
|
|
344
369
|
function withBuild(headers){
|
|
345
|
-
const h=new Headers(headers);
|
|
370
|
+
const h=new Headers(headers);
|
|
371
|
+
// Once the server has told this worker it is stale, stamping the id again only earns
|
|
372
|
+
// another refusal. See healSkew below.
|
|
373
|
+
if(!SKEWED)h.set(BUILD_HEADER,BUILD_ID);
|
|
374
|
+
return h
|
|
375
|
+
}
|
|
376
|
+
async function healSkew(req,res){
|
|
377
|
+
if(SKEWED||res.status!==${SKEW_STATUS})return res;
|
|
378
|
+
const server=res.headers.get(BUILD_HEADER);
|
|
379
|
+
if(server===null||server===BUILD_ID)return res;
|
|
380
|
+
// This worker is provably the old build, and it cannot activate its own replacement: a
|
|
381
|
+
// waiting worker takes over only when every client is released, and the refusal page is
|
|
382
|
+
// what the client is now holding, so the app that would post skip-waiting never runs.
|
|
383
|
+
// Answering the request for real is what breaks that loop.
|
|
384
|
+
SKEWED=true;
|
|
385
|
+
const cs=await self.clients.matchAll({type:'window'});
|
|
386
|
+
for(const c of cs)c.postMessage({type:${JSON.stringify(APP_UPDATE_AVAILABLE)},to:server});
|
|
387
|
+
return fetch(new Request(req,{headers:withBuild(req.headers)}));
|
|
346
388
|
}`.trim();
|
|
347
389
|
}
|
|
348
390
|
|