@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 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 | only for `forceOn` reasons, only after `graceMs` (default 6h) |
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
- detectSkew(clientBuildId, serverBuildId); // 'current' | 'stale' | 'unknown'
46
- updateSignal({ clientBuildId, serverBuildId, policy: updatePolicy() });
47
- // → { type: 'AppUpdateAvailable', from, to, forced, deadlineAt }
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`, `updatePolicy`, `updateSignal` | version skew |
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": "8.0.0",
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": "8.0.0",
35
- "@ultimat3/seo": "8.0.0"
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';
@@ -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 → forced reload only after a grace period.
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. The SW posts this to every controlled page; the app shows an
147
- * unobtrusive "refresh to update" affordance and reloads on the user's terms — unless
148
- * `forced`, in which case it reloads at `deadlineAt`.
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
- readonly from: string;
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
- }