@ultimat3/pwa 8.0.0 → 9.0.0
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/CLAUDE.md +2 -0
- package/README.md +6 -5
- package/package.json +3 -3
- package/src/index.ts +5 -7
- package/src/version-skew.ts +15 -63
package/CLAUDE.md
CHANGED
|
@@ -22,6 +22,8 @@ Tier 4. May import tiers 0–3: `core`, `schema`, `i18n`, `money`, `time`, `cach
|
|
|
22
22
|
| Cache names | always `cacheNamespace(buildId, kind)`. An unkeyed cache name is a rejected change. |
|
|
23
23
|
| Offline fallback | `requireOfflineFallback` runs inside `generateServiceWorker`. Never optional. |
|
|
24
24
|
| Capabilities | gate the manifest member **and** the SW block, where the capability has one. Disabled → zero bytes. `push`, `backgroundSync` and `badging` emit worker code; `shareTarget`, `fileHandlers` and `protocolHandlers` are **manifest-only** — the OS delivers to a route the app already serves — and their `CAPABILITY_SW_MARKERS` list is empty, which `service-worker.test.ts` checks in both directions: every declared marker is in the worker when its capability is on, none is when they are all off. `shareTarget` named `/_x/share-target` there while no block emitted it. |
|
|
25
|
+
| `AppUpdateAvailable` | declares exactly the fields the emitted `activate` block posts, and no more. `version-skew.test.ts` reads the literal back out of the generated source and compares it to a fixture typed `Required<AppUpdateAvailable>` — a field added to the interface stops compiling, a field added to the worker fails the assertion. It declared five and posted two until 9.0.0; the three extra described a forced reload nothing performed. |
|
|
26
|
+
| Forced reload | **not a capability of this package.** `updateSignal`/`updatePolicy` computed `forced`/`deadlineAt` with no runtime caller and were deleted in 9.0.0. The two runtimes holding both build ids are BELOW this one — `http`'s `ctx.clientBuildId` (tier 2), `sync`'s `update-available` frame (tier 3) — so neither could ever have called into tier 4 to act on one. The app renders its own affordance; the framework never navigates a client. |
|
|
25
27
|
| Outbox | queue lives in `@ultimat3/realtime`. This package only registers the sync trigger. |
|
|
26
28
|
| Push strings | i18n keys only, rendered per subscriber locale. Never a literal. |
|
|
27
29
|
| Push URLs | `PushPayload.url` is a PATH and `WindowClient.url` is absolute, so `notificationclick` resolves against `self.location.origin` before comparing. Unresolved, the focus-existing-tab loop matched nothing and every tap opened a second window. |
|
package/README.md
CHANGED
|
@@ -38,13 +38,14 @@ and the app dies with a blank screen and no error anyone can act on.
|
|
|
38
38
|
| Client → server | every SW-proxied request carries `x-ultimate-build` |
|
|
39
39
|
| Retention | `retentionPlan(deploys, keep)` keeps the last N deploys' assets alive (default 3) |
|
|
40
40
|
| Stale client | gets `AppUpdateAvailable`, never a 404 |
|
|
41
|
-
| Forced reload |
|
|
41
|
+
| Forced reload | none. This package never navigates a client — the app decides what to do with the message |
|
|
42
42
|
| Preview deploys | cache names are `x-<kind>-<buildId>`, so a branch build cannot poison production |
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
45
|
+
// The generated worker posts this to every page it controls, on activation — and this is the
|
|
46
|
+
// whole message, which `version-skew.test.ts` holds the interface to.
|
|
47
|
+
// { type: 'AppUpdateAvailable', to: BUILD_ID }
|
|
48
|
+
detectSkew(clientBuildId, message.to); // 'current' | 'stale' | 'unknown'
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
`unknown` means no id was sent — a first load or a crawler — and is never treated as stale.
|
|
@@ -87,7 +88,7 @@ capability nothing implements.
|
|
|
87
88
|
| `generateServiceWorker` | `sw.js` from the route table; deterministic for identical input |
|
|
88
89
|
| `strategyFor`, `MODE_STRATEGY`, `cacheFirst`, … | the four strategies + the mapping table |
|
|
89
90
|
| `buildPrecacheManifest` | precache entries (url + content-hash revision), size warnings |
|
|
90
|
-
| `buildId`, `detectSkew`, `retentionPlan
|
|
91
|
+
| `buildId`, `detectSkew`, `retentionPlan` | version skew |
|
|
91
92
|
| `generateWebManifest` | the manifest + `theme-color` metas for both schemes |
|
|
92
93
|
| `planIcons`, `requireSourceIcon`, `maskableSafeZone` | icons and splashes from one source |
|
|
93
94
|
| `BuiltinImagePipeline` | renders that plan: one square PNG per entry, deterministic |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/pwa",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
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": "
|
|
35
|
-
"@ultimat3/seo": "
|
|
34
|
+
"@ultimat3/core": "9.0.0",
|
|
35
|
+
"@ultimat3/seo": "9.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -137,17 +137,18 @@ export {
|
|
|
137
137
|
staleWhileRevalidate,
|
|
138
138
|
strategyFor,
|
|
139
139
|
} from './strategies';
|
|
140
|
+
// The forced-reload half of this module is gone as of 9.0.0 — `updateSignal`, `updatePolicy`,
|
|
141
|
+
// `DEFAULT_GRACE_MS`, `ForceReason`, `UpdatePolicy`, `UpdatePolicyInput`, `UpdateSignalInput`.
|
|
142
|
+
// It computed `forced`/`deadlineAt` for a client-side reload no code in the framework performed,
|
|
143
|
+
// from a caller that never existed. What remains is what runs: an id, a comparison, a retention
|
|
144
|
+
// plan, and the message the generated worker really posts.
|
|
140
145
|
export type {
|
|
141
146
|
AppUpdateAvailable,
|
|
142
147
|
BuildIdInput,
|
|
143
148
|
Deploy,
|
|
144
149
|
DeployChannel,
|
|
145
|
-
ForceReason,
|
|
146
150
|
RetentionPlan,
|
|
147
151
|
SkewState,
|
|
148
|
-
UpdatePolicy,
|
|
149
|
-
UpdatePolicyInput,
|
|
150
|
-
UpdateSignalInput,
|
|
151
152
|
} from './version-skew';
|
|
152
153
|
export {
|
|
153
154
|
APP_UPDATE_AVAILABLE,
|
|
@@ -156,9 +157,6 @@ export {
|
|
|
156
157
|
BUILD_ID_META,
|
|
157
158
|
buildId,
|
|
158
159
|
cacheNamespace,
|
|
159
|
-
DEFAULT_GRACE_MS,
|
|
160
160
|
detectSkew,
|
|
161
161
|
retentionPlan,
|
|
162
|
-
updatePolicy,
|
|
163
|
-
updateSignal,
|
|
164
162
|
} from './version-skew';
|
package/src/version-skew.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* immutable build id per deploy → the client sends it on every request →
|
|
7
7
|
* old builds' assets are retained for N deploys → a stale client gets
|
|
8
|
-
* `AppUpdateAvailable`, never a 404
|
|
8
|
+
* `AppUpdateAvailable`, never a 404.
|
|
9
|
+
*
|
|
10
|
+
* The app decides what to do with that message. This package never navigates a client.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { BuildIdMissingError } from './errors';
|
|
@@ -115,73 +117,23 @@ export function detectSkew(
|
|
|
115
117
|
return clientBuildId === serverBuildId ? 'current' : 'stale';
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
export type ForceReason = 'security' | 'breaking-protocol' | 'never';
|
|
119
|
-
|
|
120
|
-
export interface UpdatePolicyInput {
|
|
121
|
-
/** How long a stale client may keep running before the reload is forced. */
|
|
122
|
-
readonly graceMs?: number;
|
|
123
|
-
readonly forceOn?: readonly ForceReason[];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface UpdatePolicy {
|
|
127
|
-
readonly graceMs: number;
|
|
128
|
-
readonly forceOn: readonly ForceReason[];
|
|
129
|
-
shouldForce(reason: ForceReason, staleForMs: number): boolean;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const DEFAULT_GRACE_MS = 6 * 60 * 60 * 1000;
|
|
133
|
-
|
|
134
|
-
export function updatePolicy(input: UpdatePolicyInput = {}): UpdatePolicy {
|
|
135
|
-
const graceMs = input.graceMs ?? DEFAULT_GRACE_MS;
|
|
136
|
-
const forceOn = input.forceOn ?? ['security'];
|
|
137
|
-
return {
|
|
138
|
-
graceMs,
|
|
139
|
-
forceOn,
|
|
140
|
-
// A security patch still respects the grace window; it just does not wait forever.
|
|
141
|
-
shouldForce: (reason, staleForMs) => forceOn.includes(reason) && staleForMs >= graceMs,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
120
|
/**
|
|
146
|
-
* The client-side contract
|
|
147
|
-
*
|
|
148
|
-
*
|
|
121
|
+
* The client-side contract, and the whole of it: what the generated worker posts to every page it
|
|
122
|
+
* controls on activation. The page compares `to` against its own `BUILD_ID_META` — `detectSkew`
|
|
123
|
+
* is that comparison — and renders its own "refresh to update" affordance.
|
|
124
|
+
*
|
|
125
|
+
* It declared `from`, `forced` and `deadlineAt` too, for a forced reload after a grace period that
|
|
126
|
+
* NOTHING performed: `updateSignal`/`updatePolicy` computed the three and had no runtime caller,
|
|
127
|
+
* and `x deploy --critical`, the flag that was to have set the reason, was removed in 4.0.0 for
|
|
128
|
+
* being read by nobody. The two runtimes that hold both build ids cannot call into this package
|
|
129
|
+
* anyway — `http`'s `ctx.clientBuildId` (tier 2) and `sync`'s `update-available` frame (tier 3)
|
|
130
|
+
* are both BELOW `pwa`, and imports only go down. `version-skew.test.ts` holds this interface to
|
|
131
|
+
* the literal the worker emits, so the two can no longer differ.
|
|
149
132
|
*/
|
|
150
133
|
export interface AppUpdateAvailable {
|
|
151
134
|
readonly type: 'AppUpdateAvailable';
|
|
152
|
-
|
|
135
|
+
/** The build the worker that posted this was generated for. */
|
|
153
136
|
readonly to: string;
|
|
154
|
-
readonly forced: boolean;
|
|
155
|
-
/** Epoch milliseconds after which the client reloads itself. Null when not forced. */
|
|
156
|
-
readonly deadlineAt: number | null;
|
|
157
137
|
}
|
|
158
138
|
|
|
159
139
|
export const APP_UPDATE_AVAILABLE = 'AppUpdateAvailable' as const;
|
|
160
|
-
|
|
161
|
-
export interface UpdateSignalInput {
|
|
162
|
-
readonly clientBuildId: string | null | undefined;
|
|
163
|
-
readonly serverBuildId: string;
|
|
164
|
-
readonly policy: UpdatePolicy;
|
|
165
|
-
readonly reason?: ForceReason;
|
|
166
|
-
readonly staleForMs?: number;
|
|
167
|
-
readonly now?: number;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Null when the client is current or unknown — no signal, no nag. */
|
|
171
|
-
export function updateSignal(input: UpdateSignalInput): AppUpdateAvailable | null {
|
|
172
|
-
const state = detectSkew(input.clientBuildId, input.serverBuildId);
|
|
173
|
-
if (state !== 'stale') return null;
|
|
174
|
-
|
|
175
|
-
const reason = input.reason ?? 'never';
|
|
176
|
-
const staleForMs = input.staleForMs ?? 0;
|
|
177
|
-
const forced = input.policy.shouldForce(reason, staleForMs);
|
|
178
|
-
const now = input.now ?? Date.now();
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
type: APP_UPDATE_AVAILABLE,
|
|
182
|
-
from: String(input.clientBuildId),
|
|
183
|
-
to: input.serverBuildId,
|
|
184
|
-
forced,
|
|
185
|
-
deadlineAt: forced ? now : null,
|
|
186
|
-
};
|
|
187
|
-
}
|