@ultimat3/pwa 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 developerz.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # 📲 @ultimat3/pwa
2
+
3
+ **You never open `sw.js`.** It is emitted from the route table. That is this package's
4
+ whole thesis: a hand-written service worker encodes routing decisions a second time, and
5
+ the second copy is the one nobody updates.
6
+
7
+ ```ts
8
+ const { source, precache, warnings } = generateServiceWorker(describeRoutes(), config, buildId);
9
+ ```
10
+
11
+ ## Render mode → runtime strategy
12
+
13
+ | Render mode | Strategy | Why |
14
+ |---|---|---|
15
+ | `static` | cache-first | built once; the URL's bytes only change on deploy |
16
+ | `isr` | stale-while-revalidate | stale is correct by construction, refresh behind |
17
+ | `stream` | stale-while-revalidate | shell is reusable, holes come from the network |
18
+ | `ssr` | network-first | freshness is the point; cache is the offline safety net |
19
+ | `spa` | cache-first | the shell is identical for every actor |
20
+
21
+ Overrides: `offline: 'network-only'` forces `network-only`; a per-route `strategy` wins over
22
+ everything. `api/` routes get no cache rule at all.
23
+
24
+ | `offline` | Meaning |
25
+ |---|---|
26
+ | `precache` | fetched at install, keyed by content hash |
27
+ | `runtime` | cached on first visit under the render-mode strategy |
28
+ | `network-only` | never served from a cache |
29
+
30
+ ## Version skew — the thing that actually breaks PWAs
31
+
32
+ A client loaded build A hours ago. Build B deletes A's chunks. The next lazy import 404s
33
+ and the app dies with a blank screen and no error anyone can act on.
34
+
35
+ | Mechanism | Rule |
36
+ |---|---|
37
+ | Build id | immutable per deploy, derived from the commit sha; `X_BUILD_ID_MISSING` if absent |
38
+ | Client → server | every SW-proxied request carries `x-ultimate-build` |
39
+ | Retention | `retentionPlan(deploys, keep)` keeps the last N deploys' assets alive (default 3) |
40
+ | Stale client | gets `AppUpdateAvailable`, never a 404 |
41
+ | Forced reload | only for `forceOn` reasons, only after `graceMs` (default 6h) |
42
+ | Preview deploys | cache names are `x-<kind>-<buildId>`, so a branch build cannot poison production |
43
+
44
+ ```ts
45
+ detectSkew(clientBuildId, serverBuildId); // 'current' | 'stale' | 'unknown'
46
+ updateSignal({ clientBuildId, serverBuildId, policy: updatePolicy() });
47
+ // → { type: 'AppUpdateAvailable', from, to, forced, deadlineAt }
48
+ ```
49
+
50
+ `unknown` means no id was sent — a first load or a crawler — and is never treated as stale.
51
+
52
+ ## The offline fallback is mandatory in the type
53
+
54
+ ```
55
+ X_PWA_NO_OFFLINE_FALLBACK: no offline fallback route
56
+ cause: app.config.ts has no `offline` block, so an offline navigation would show the browser's error page
57
+ fix: create app/offline.tsx and set offline.fallback
58
+ ```
59
+
60
+ `requireOfflineFallback(config)` runs inside `generateServiceWorker`, so the build fails
61
+ before an un-shippable PWA exists.
62
+
63
+ ## Capabilities are opt-in, and gate bytes
64
+
65
+ | Capability | Manifest member | SW code |
66
+ |---|---|---|
67
+ | `push` | — | `push` + `notificationclick` listeners |
68
+ | `backgroundSync` | — | `sync` listener + outbox flush |
69
+ | `badging` | — | `navigator.setAppBadge` after a push |
70
+ | `shareTarget` | `share_target` | share-target route rule |
71
+ | `fileHandlers` | `file_handlers` | — |
72
+ | `protocolHandlers` | `protocol_handlers` | — |
73
+
74
+ A disabled capability emits neither the manifest member nor the SW code. An unused
75
+ capability ships zero bytes and asks for zero permissions.
76
+
77
+ ## Public API
78
+
79
+ | Export | Owns |
80
+ |---|---|
81
+ | `generateServiceWorker` | `sw.js` from the route table; deterministic for identical input |
82
+ | `strategyFor`, `MODE_STRATEGY`, `cacheFirst`, … | the four strategies + the mapping table |
83
+ | `buildPrecacheManifest` | precache entries (url + content-hash revision), size warnings |
84
+ | `buildId`, `detectSkew`, `retentionPlan`, `updatePolicy`, `updateSignal` | version skew |
85
+ | `generateWebManifest` | the manifest + `theme-color` metas for both schemes |
86
+ | `planIcons`, `requireSourceIcon`, `maskableSafeZone` | icons and splashes from one source |
87
+ | `BuiltinImagePipeline` | renders that plan: one square PNG per entry, deterministic |
88
+ | `requireOfflineFallback` | the mandatory offline route |
89
+ | `backgroundSyncSource`, `retryDelayMs` | the Background Sync trigger |
90
+ | `renderPushPayload`, `pushSource`, `subscribeSource` | Web Push, per-locale bodies |
91
+ | `createInstallController`, `iosInstallGuidance` | install prompt, never on first paint |
92
+
93
+ ## Notes
94
+
95
+ - **Theme colours come from the design tokens for both schemes.** The manifest spec carries
96
+ one `theme_color`, so the dark value is emitted as a media-scoped
97
+ `<meta name="theme-color">` — otherwise an installed dark app launches with a light status
98
+ bar every time.
99
+ - **Precache revisions are content hashes, never the build id.** Keying on the build id
100
+ re-downloads every asset on every deploy.
101
+ - **The mutation queue lives in `@ultimat3/realtime`, not here** (SRP). This package owns
102
+ only the Background Sync trigger that asks realtime to flush.
103
+ - **Push bodies are rendered server-side per subscriber locale**, from the locale stored on
104
+ the subscription. A notification in the wrong language is a real bug, and the sending
105
+ server has no request context to infer one from.
106
+ - **Icons come from one source image.** `X_PWA_ICON_MISSING` names the file to add;
107
+ `BuiltinImagePipeline` renders the whole matrix from it through `@ultimat3/core`'s image
108
+ pipeline — no `sharp`, no vendor image CDN, no native build step. Every output is a square
109
+ PNG, because `type: 'image/png'` is what the manifest declares. A maskable icon's artwork
110
+ lands exactly inside `maskableSafeZone(size)`; the ring around it is `background`, which is
111
+ hex or `transparent` (there are no named colours). Same bytes in, same bytes out.
112
+ - **Route data arrives as data.** `@ultimat3/render` and `@ultimat3/pwa` are both tier 4, so
113
+ `PwaRoute` is a structural view of `RouteDescriptor`, never an import.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ultimat3/pwa",
3
+ "version": "1.0.0",
4
+ "description": "Generated service worker, web manifest, icons, push and version-skew handling.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/developerz-ai/ultimate.git",
10
+ "directory": "packages/pwa"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.3.0"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit -p tsconfig.json",
30
+ "test": "bun test"
31
+ },
32
+ "dependencies": {
33
+ "@ultimat3/core": "1.0.0"
34
+ }
35
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Background Sync registration for the offline mutation queue.
3
+ *
4
+ * SRP: the queue itself — the outbox, the optimistic local twins, the conflict policy —
5
+ * belongs to `@ultimat3/realtime`. This file owns only the browser-side trigger: register
6
+ * a sync tag, and when the platform says connectivity is back, ask realtime to flush.
7
+ * Nothing here knows what a mutation is, and it must stay that way.
8
+ *
9
+ * The two failures below (`X_PWA_SYNC_FLUSH_FAILED`, `X_PWA_SYNC_INCOMPLETE`, documented in
10
+ * `./errors.ts`) run inside the string emitted into `sw.js` — the browser's service-worker
11
+ * realm, which has no bundler and cannot import `@ultimat3/core`. What it *can* have is a class
12
+ * of its own, so `SYNC_ERROR_CLASS` emits one: a bare `Error` carries a message and nothing a
13
+ * caller can read, while the emitted class exposes the same `code`, `cause`, `fix` and `docs` an
14
+ * `UltimateError` does everywhere else in the framework.
15
+ */
16
+
17
+ import { PwaSyncFlushFailedError, PwaSyncIncompleteError } from './errors';
18
+ import { BUILD_ID_HEADER } from './version-skew';
19
+
20
+ export const SYNC_TAG = 'x-outbox';
21
+ export const PERIODIC_SYNC_TAG = 'x-refresh';
22
+
23
+ export interface RetryPolicy {
24
+ readonly maxAttempts: number;
25
+ readonly baseDelayMs: number;
26
+ readonly maxDelayMs: number;
27
+ }
28
+
29
+ export const DEFAULT_RETRY: RetryPolicy = Object.freeze({
30
+ maxAttempts: 6,
31
+ baseDelayMs: 1_000,
32
+ maxDelayMs: 5 * 60 * 1000,
33
+ });
34
+
35
+ /**
36
+ * Deterministic exponential backoff — no jitter here on purpose: the Background Sync
37
+ * scheduler already spreads wake-ups across clients, and a deterministic delay is
38
+ * testable and reproducible in a bug report.
39
+ */
40
+ export function retryDelayMs(attempt: number, policy: RetryPolicy = DEFAULT_RETRY): number {
41
+ const clamped = Math.max(1, Math.min(attempt, policy.maxAttempts));
42
+ return Math.min(policy.baseDelayMs * 2 ** (clamped - 1), policy.maxDelayMs);
43
+ }
44
+
45
+ export function shouldRetry(attempt: number, policy: RetryPolicy = DEFAULT_RETRY): boolean {
46
+ return attempt < policy.maxAttempts;
47
+ }
48
+
49
+ export interface BackgroundSyncOptions {
50
+ /** Endpoint `@ultimat3/realtime` exposes to flush the outbox. */
51
+ readonly flushEndpoint?: string;
52
+ readonly retry?: RetryPolicy;
53
+ /** Minimum interval for periodic sync, when the platform grants it. */
54
+ readonly periodicMinIntervalMs?: number;
55
+ }
56
+
57
+ export const DEFAULT_FLUSH_ENDPOINT = '/_x/outbox/flush';
58
+
59
+ /**
60
+ * Where the emitted class sends a reader. The same host `./errors.ts` documents these two codes
61
+ * at — retyped here because the SW builds its URL from the code at throw time, and
62
+ * `background-sync.test.ts` asserts the two halves still agree.
63
+ */
64
+ const SYNC_DOCS_BASE = 'https://ultimate.dev/errors/';
65
+
66
+ /**
67
+ * The generated realm's own coded error, as source. Small on purpose — this ships in `sw.js` — and
68
+ * deliberately not a bare `Error`: `code` is what a reporting hook groups on, `fix` is what the
69
+ * developer in devtools acts on, and neither survives being flattened into a message alone. The
70
+ * message still renders the contract's own line shape, because an uncaught `waitUntil` rejection
71
+ * prints nothing else.
72
+ */
73
+ const SYNC_ERROR_CLASS = `
74
+ class PwaSyncError extends Error{
75
+ constructor(code,cause,fix){
76
+ const docs=${JSON.stringify(SYNC_DOCS_BASE)}+code;
77
+ super(code+': '+cause+'\\n fix: '+fix+'\\n docs: '+docs);
78
+ this.name='PwaSyncError';this.code=code;this.cause=cause;this.fix=fix;this.docs=docs;
79
+ }
80
+ }`.trim();
81
+
82
+ /**
83
+ * Emitted into `sw.js` only when the `backgroundSync` capability is on. The handler posts
84
+ * to realtime's flush endpoint; a non-2xx keeps the sync registration alive so the
85
+ * platform retries with its own scheduling.
86
+ */
87
+ export function backgroundSyncSource(options: BackgroundSyncOptions = {}): string {
88
+ const endpoint = options.flushEndpoint ?? DEFAULT_FLUSH_ENDPOINT;
89
+ const retry = options.retry ?? DEFAULT_RETRY;
90
+ return `
91
+ const SYNC_TAG=${JSON.stringify(SYNC_TAG)};
92
+ const FLUSH_ENDPOINT=${JSON.stringify(endpoint)};
93
+ const SYNC_MAX_ATTEMPTS=${retry.maxAttempts};
94
+ ${SYNC_ERROR_CLASS}
95
+ async function flushOutbox(){
96
+ const res=await fetch(FLUSH_ENDPOINT,{method:'POST',headers:{${JSON.stringify(BUILD_ID_HEADER)}:BUILD_ID}});
97
+ if(!res.ok)throw new PwaSyncError(${JSON.stringify(PwaSyncFlushFailedError.code)},'outbox flush POST '+FLUSH_ENDPOINT+' returned '+res.status,'curl -i -X POST '+FLUSH_ENDPOINT+' — @ultimat3/realtime must mount it and answer 2xx');
98
+ const body=await res.json().catch(()=>({remaining:0}));
99
+ if(body.remaining>0)throw new PwaSyncError(${JSON.stringify(PwaSyncIncompleteError.code)},'outbox flush at '+FLUSH_ENDPOINT+' left '+body.remaining+' mutation(s) queued','x dev --role sync # drain the outbox, or raise pwa.backgroundSync.retry.maxAttempts in app.config.ts');
100
+ }
101
+ self.addEventListener('sync',(event)=>{
102
+ if(event.tag!==SYNC_TAG)return;
103
+ // Rejecting keeps the registration so the platform retries on its own schedule.
104
+ event.waitUntil(flushOutbox());
105
+ });`.trim();
106
+ }
107
+
108
+ /** Client-side registration. Falls back to an `online` listener where sync is missing. */
109
+ export function registerBackgroundSyncSource(): string {
110
+ return `
111
+ export async function registerOutboxSync(registration){
112
+ const sync=registration.sync;
113
+ if(sync&&typeof sync.register==='function'){await sync.register(${JSON.stringify(SYNC_TAG)});return 'sync'}
114
+ addEventListener('online',()=>{navigator.serviceWorker.controller?.postMessage({type:'flush-outbox'})});
115
+ return 'fallback'
116
+ }`.trim();
117
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The opt-in capability flags from `app.config.ts`. Each flag gates BOTH the manifest
3
+ * entry and the service-worker code, so a capability you did not ask for ships zero
4
+ * bytes and requests zero permissions.
5
+ */
6
+
7
+ export const CAPABILITIES = [
8
+ 'push',
9
+ 'backgroundSync',
10
+ 'badging',
11
+ 'shareTarget',
12
+ 'fileHandlers',
13
+ 'protocolHandlers',
14
+ ] as const;
15
+
16
+ export type Capability = (typeof CAPABILITIES)[number];
17
+
18
+ export type CapabilityFlags = { readonly [K in Capability]?: boolean };
19
+ export type ResolvedCapabilities = Readonly<Record<Capability, boolean>>;
20
+
21
+ /** Everything is off unless the app turns it on. */
22
+ export function resolveCapabilities(flags: CapabilityFlags = {}): ResolvedCapabilities {
23
+ const resolved: Record<Capability, boolean> = {
24
+ push: false,
25
+ backgroundSync: false,
26
+ badging: false,
27
+ shareTarget: false,
28
+ fileHandlers: false,
29
+ protocolHandlers: false,
30
+ };
31
+ for (const capability of CAPABILITIES) {
32
+ resolved[capability] = flags[capability] === true;
33
+ }
34
+ return resolved;
35
+ }
36
+
37
+ export function isEnabled(capabilities: ResolvedCapabilities, capability: Capability): boolean {
38
+ return capabilities[capability];
39
+ }
40
+
41
+ export function enabledCapabilities(capabilities: ResolvedCapabilities): readonly Capability[] {
42
+ return CAPABILITIES.filter((capability) => capabilities[capability]);
43
+ }
44
+
45
+ /** Manifest members a capability owns. Absent capability → absent member. */
46
+ export const CAPABILITY_MANIFEST_KEYS: Readonly<Record<Capability, readonly string[]>> =
47
+ Object.freeze({
48
+ push: [],
49
+ backgroundSync: [],
50
+ badging: [],
51
+ shareTarget: ['share_target'],
52
+ fileHandlers: ['file_handlers'],
53
+ protocolHandlers: ['protocol_handlers'],
54
+ });
55
+
56
+ /**
57
+ * The service-worker code each capability emits — its listener, and anything that listener alone
58
+ * needs. Used to assert nothing leaks when disabled: `PwaSyncError` is the background-sync
59
+ * handler's own error class, so it ships with the handler and never without it.
60
+ */
61
+ export const CAPABILITY_SW_MARKERS: Readonly<Record<Capability, readonly string[]>> = Object.freeze(
62
+ {
63
+ push: ["addEventListener('push'", "addEventListener('notificationclick'"],
64
+ backgroundSync: ["addEventListener('sync'", 'class PwaSyncError'],
65
+ badging: ['navigator.setAppBadge'],
66
+ shareTarget: ['/_x/share-target'],
67
+ fileHandlers: [],
68
+ protocolHandlers: [],
69
+ },
70
+ );
package/src/errors.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * PWA error codes. Everything that breaks a PWA in production — a missing offline
3
+ * fallback, a missing build id, a bad scope — fails here at build time instead.
4
+ */
5
+
6
+ import { registerErrorCodes, UltimateError } from '@ultimat3/core';
7
+
8
+ /** Codes this package declares and owns. */
9
+ export const PWA_OWNED_ERROR_CODES = [
10
+ 'X_PWA_NO_OFFLINE_FALLBACK',
11
+ 'X_PWA_ICON_MISSING',
12
+ 'X_PWA_MANIFEST_INVALID',
13
+ 'X_BUILD_ID_MISSING',
14
+ 'X_SW_SCOPE_INVALID',
15
+ 'X_PWA_STRATEGY_EXHAUSTED',
16
+ 'X_PWA_SYNC_FLUSH_FAILED',
17
+ 'X_PWA_SYNC_INCOMPLETE',
18
+ ] as const;
19
+
20
+ /**
21
+ * `X_NOT_IMPLEMENTED` is `@ultimat3/core`'s. Thrown here, titled only there — the copy this file
22
+ * used to keep was a second title that could drift from core's with nothing to catch it.
23
+ */
24
+ export const PWA_BORROWED_ERROR_CODES = ['X_NOT_IMPLEMENTED'] as const;
25
+
26
+ /** Every code pwa can throw: the ones it owns plus the one it borrows. */
27
+ export const PWA_ERROR_CODES = [...PWA_OWNED_ERROR_CODES, ...PWA_BORROWED_ERROR_CODES] as const;
28
+
29
+ export type PwaOwnedErrorCode = (typeof PWA_OWNED_ERROR_CODES)[number];
30
+ export type PwaErrorCode = (typeof PWA_ERROR_CODES)[number];
31
+
32
+ export const PWA_ERROR_TITLES: Readonly<Record<PwaOwnedErrorCode, string>> = {
33
+ X_PWA_NO_OFFLINE_FALLBACK: 'pwa.offline.fallback is not set',
34
+ X_PWA_ICON_MISSING: 'no source icon to generate from',
35
+ X_PWA_MANIFEST_INVALID: 'the generated web manifest failed validation',
36
+ X_BUILD_ID_MISSING: 'no immutable build ID',
37
+ X_SW_SCOPE_INVALID: 'the service-worker scope cannot serve the routes it precaches',
38
+ X_PWA_STRATEGY_EXHAUSTED: 'a caching strategy had no cache, no network and no fallback',
39
+ X_PWA_SYNC_FLUSH_FAILED: 'the background-sync outbox flush was rejected',
40
+ X_PWA_SYNC_INCOMPLETE: 'the background-sync outbox flush left mutations queued',
41
+ };
42
+
43
+ // One unconditional call, so a second package claiming one of pwa's codes throws
44
+ // X_ERROR_CODE_DUPLICATE instead of losing silently to whichever module imported first.
45
+ registerErrorCodes(
46
+ Object.fromEntries(Object.entries(PWA_ERROR_TITLES).map(([code, title]) => [code, { title }])),
47
+ );
48
+
49
+ const docsFor = (code: PwaErrorCode): string => `https://ultimate.dev/errors/${code}`;
50
+
51
+ /** No `app/offline.tsx`. You cannot ship a PWA that has nothing to show offline. */
52
+ export class PwaNoOfflineFallbackError extends UltimateError {
53
+ static readonly code = 'X_PWA_NO_OFFLINE_FALLBACK' as const;
54
+ constructor(cause: string, fix: string) {
55
+ super({
56
+ code: PwaNoOfflineFallbackError.code,
57
+ cause,
58
+ fix,
59
+ docs: docsFor(PwaNoOfflineFallbackError.code),
60
+ });
61
+ }
62
+ }
63
+
64
+ /** The single source icon is missing or too small to derive the size matrix from. */
65
+ export class PwaIconMissingError extends UltimateError {
66
+ static readonly code = 'X_PWA_ICON_MISSING' as const;
67
+ constructor(cause: string, fix: string) {
68
+ super({
69
+ code: PwaIconMissingError.code,
70
+ cause,
71
+ fix,
72
+ docs: docsFor(PwaIconMissingError.code),
73
+ });
74
+ }
75
+ }
76
+
77
+ /** The generated web manifest would be rejected by the browser. */
78
+ export class PwaManifestInvalidError extends UltimateError {
79
+ static readonly code = 'X_PWA_MANIFEST_INVALID' as const;
80
+ constructor(cause: string, fix: string) {
81
+ super({
82
+ code: PwaManifestInvalidError.code,
83
+ cause,
84
+ fix,
85
+ docs: docsFor(PwaManifestInvalidError.code),
86
+ });
87
+ }
88
+ }
89
+
90
+ /** No immutable build id, so caches cannot be keyed and skew cannot be detected. */
91
+ export class BuildIdMissingError extends UltimateError {
92
+ static readonly code = 'X_BUILD_ID_MISSING' as const;
93
+ constructor(cause: string, fix: string) {
94
+ super({
95
+ code: BuildIdMissingError.code,
96
+ cause,
97
+ fix,
98
+ docs: docsFor(BuildIdMissingError.code),
99
+ });
100
+ }
101
+ }
102
+
103
+ /** The service worker's scope cannot cover the URLs it is asked to control. */
104
+ export class SwScopeInvalidError extends UltimateError {
105
+ static readonly code = 'X_SW_SCOPE_INVALID' as const;
106
+ constructor(cause: string, fix: string) {
107
+ super({
108
+ code: SwScopeInvalidError.code,
109
+ cause,
110
+ fix,
111
+ docs: docsFor(SwScopeInvalidError.code),
112
+ });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * A strategy exhausted the cache, the network, and any declared fallback. Runs in-process (the
118
+ * `STRATEGY_FNS` half of `strategies.ts`, tested for parity with the `STRATEGY_SOURCE` emitted
119
+ * into `sw.js`), so it can import core the way the generated bundle below cannot.
120
+ */
121
+ export class PwaStrategyExhaustedError extends UltimateError {
122
+ static readonly code = 'X_PWA_STRATEGY_EXHAUSTED' as const;
123
+ constructor(input: { cacheName: string }) {
124
+ super({
125
+ code: PwaStrategyExhaustedError.code,
126
+ cause: `no cached response and the network failed for "${input.cacheName}"`,
127
+ fix: 'pass options.fallback to staleWhileRevalidate(request, env, options), or set pwa.offline.fallback in app.config.ts',
128
+ docs: docsFor(PwaStrategyExhaustedError.code),
129
+ });
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Titles one of the two failures `backgroundSyncSource()` emits into `sw.js`, and owns the `code`
135
+ * the emitted source throws. The generated code runs in the browser's service-worker realm, which
136
+ * has no bundler and no `@ultimat3/core` to import — so it defines a local `PwaSyncError` carrying
137
+ * this same code, cause, fix and docs rather than constructing this class. This class is what gives
138
+ * the code one title and one wiki row, the same as every other code in this file.
139
+ */
140
+ export class PwaSyncFlushFailedError extends UltimateError {
141
+ static readonly code = 'X_PWA_SYNC_FLUSH_FAILED' as const;
142
+ constructor(cause: string, fix: string) {
143
+ super({
144
+ code: PwaSyncFlushFailedError.code,
145
+ cause,
146
+ fix,
147
+ docs: docsFor(PwaSyncFlushFailedError.code),
148
+ });
149
+ }
150
+ }
151
+
152
+ /** Documented for the same reason as {@link PwaSyncFlushFailedError} — see its comment. */
153
+ export class PwaSyncIncompleteError extends UltimateError {
154
+ static readonly code = 'X_PWA_SYNC_INCOMPLETE' as const;
155
+ constructor(cause: string, fix: string) {
156
+ super({
157
+ code: PwaSyncIncompleteError.code,
158
+ cause,
159
+ fix,
160
+ docs: docsFor(PwaSyncIncompleteError.code),
161
+ });
162
+ }
163
+ }
164
+
165
+ /** A driver whose interface exists and whose backing implementation does not, yet. */
166
+ export class NotImplementedError extends UltimateError {
167
+ static readonly code = 'X_NOT_IMPLEMENTED' as const;
168
+ constructor(cause: string, fix: string) {
169
+ super({
170
+ code: NotImplementedError.code,
171
+ cause,
172
+ fix,
173
+ docs: docsFor(NotImplementedError.code),
174
+ });
175
+ }
176
+ }