@sweidos/eidos 0.2.0 → 1.0.1

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/README.md CHANGED
@@ -1,63 +1,55 @@
1
1
  # Eidos
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@sweidos/eidos)](https://www.npmjs.com/package/@sweidos/eidos)
4
+ [![CI](https://github.com/iamadi11/eidos/actions/workflows/deploy.yml/badge.svg)](https://github.com/iamadi11/eidos/actions/workflows/deploy.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+
3
7
  > Describe intent. The runtime figures out how.
4
8
 
5
- Eidos is a small, opinionated abstraction layer for building offline-first web applications. Instead of configuring Service Workers, Cache API strategies, and IndexedDB queues directly, you declare **what you want** and the runtime generates the required behaviour.
9
+ Eidos is a small, opinionated abstraction layer for building offline-first web apps. Instead of configuring Service Workers, Cache API strategies, and IndexedDB queues by hand, you declare **what you want** and the runtime handles the rest.
6
10
 
7
11
  ```ts
8
12
  import { resource, action } from '@sweidos/eidos'
9
13
 
10
- // "I want this resource to work offline."
11
- const products = resource('/api/products', {
12
- offline: true,
13
- })
14
+ // "I want this resource available offline."
15
+ const products = resource('/api/products', { offline: true })
14
16
 
15
17
  // "I never want to lose this action."
16
- const createOrder = action(orderApi.create, {
17
- reliability: 'neverLose',
18
- })
18
+ const createOrder = action(orderApi.create, { reliability: 'neverLose' })
19
19
  ```
20
20
 
21
- That's it. No service worker file to write. No cache strategy to configure. No retry logic to implement.
21
+ No service worker file to write. No cache strategy to configure. No retry logic to implement.
22
+
23
+ **[→ Live playground](https://playground-iamadi11s-projects.vercel.app)**
22
24
 
23
25
  ---
24
26
 
25
27
  ## The Problem
26
28
 
27
- Building offline-capable web apps today requires a working knowledge of:
29
+ Building offline-capable apps today requires deep knowledge of:
28
30
 
29
31
  - Service Worker registration and lifecycle management
30
- - Cache API and caching strategies (cache-first, network-first, SWR)
32
+ - Cache API strategies (cache-first, network-first, stale-while-revalidate)
31
33
  - Fetch event interception and URL routing
32
- - IndexedDB schema design for persistent action queues
33
- - Background Sync API and exponential retry logic
34
+ - IndexedDB schema design for persistent queues
35
+ - Exponential backoff and retry logic
34
36
  - Cache versioning and stale entry cleanup
35
37
 
36
- This is a large surface area, separate from your application logic, that every team re-implements from scratch.
37
-
38
- ## The Vision
38
+ Every team re-implements this surface area from scratch.
39
39
 
40
- Developers should describe **what they want**, not **how the browser should implement it**.
40
+ ## The Solution
41
41
 
42
42
  ```ts
43
- // Before Eidos
44
- // workbox-config.js
43
+ // Before — workbox-config.js + service-worker.js (40+ lines)
45
44
  registerRoute(
46
45
  ({ url }) => url.pathname === '/api/products',
47
- new StaleWhileRevalidate({
48
- cacheName: 'api-cache',
49
- plugins: [new ExpirationPlugin({ maxEntries: 60 })],
50
- }),
46
+ new StaleWhileRevalidate({ cacheName: 'api-cache', plugins: [...] }),
51
47
  )
52
-
53
- // service-worker.js
54
- self.addEventListener('sync', (event) => {
55
- if (event.tag === 'create-order') {
56
- event.waitUntil(replayOrders())
57
- }
48
+ self.addEventListener('sync', event => {
49
+ if (event.tag === 'create-order') event.waitUntil(replayOrders())
58
50
  })
59
51
 
60
- // After Eidos
52
+ // After — eidos (2 lines)
61
53
  resource('/api/products', { offline: true })
62
54
  action(createOrder, { reliability: 'neverLose' })
63
55
  ```
@@ -66,28 +58,27 @@ action(createOrder, { reliability: 'neverLose' })
66
58
 
67
59
  ## Quick Start
68
60
 
69
- ### Install
61
+ ### 1. Install
70
62
 
71
63
  ```bash
72
- npm install eidos
64
+ npm install @sweidos/eidos
73
65
  # or
74
- pnpm add eidos
66
+ pnpm add @sweidos/eidos
75
67
  ```
76
68
 
77
- ### Add the service worker
78
-
79
- Copy `eidos-sw.js` to your project's `public/` directory:
69
+ ### 2. Add the service worker
80
70
 
81
71
  ```bash
82
- cp node_modules/eidos/dist/eidos-sw.js public/eidos-sw.js
72
+ cp node_modules/@sweidos/eidos/dist/eidos-sw.js public/eidos-sw.js
83
73
  ```
84
74
 
85
- > **Vite users** — you can also add a plugin to do this automatically. See [setup guide](#vite-plugin).
75
+ > **Vite users** — automate this with the [Vite plugin snippet](#vite-plugin).
86
76
 
87
- ### Wrap your app
77
+ ### 3. Wrap your app
88
78
 
89
79
  ```tsx
90
80
  import { EidosProvider } from '@sweidos/eidos'
81
+ import { createRoot } from 'react-dom/client'
91
82
 
92
83
  createRoot(document.getElementById('root')!).render(
93
84
  <EidosProvider swPath="/eidos-sw.js">
@@ -96,14 +87,17 @@ createRoot(document.getElementById('root')!).render(
96
87
  )
97
88
  ```
98
89
 
99
- ### Declare resources and actions
90
+ ### 4. Declare resources and actions at module scope
100
91
 
101
92
  ```ts
102
- // src/lib/eidos.ts — module scope, so replay survives page reload
93
+ // src/lib/eidos.ts
94
+ // Module scope is required — actions must be registered before page reload
95
+ // for queue replay to work.
103
96
  import { resource, action } from '@sweidos/eidos'
104
97
 
105
98
  export const products = resource('/api/products', {
106
- offline: true,
99
+ offline: true, // → StaleWhileRevalidate auto-selected
100
+ maxAge: 5 * 60 * 1000, // optional: treat cache as stale after 5 min
107
101
  })
108
102
 
109
103
  export const createOrder = action(
@@ -114,24 +108,25 @@ export const createOrder = action(
114
108
  })
115
109
  return res.json()
116
110
  },
117
- { reliability: 'neverLose' },
111
+ { reliability: 'neverLose', name: 'createOrder' },
118
112
  )
119
113
  ```
120
114
 
121
- ### Use in components
115
+ ### 5. Use in components
122
116
 
123
117
  ```tsx
124
- // With TanStack Query
125
- const { data } = useQuery(products.query())
118
+ // TanStack Query
119
+ const { data } = useQuery(products.query<Product[]>())
126
120
 
127
- // Or plain
128
- const data = await products.json()
121
+ // Or plain async
122
+ const data = await products.json<Product[]>()
129
123
 
130
124
  // Actions work identically online and offline
131
125
  const result = await createOrder({ productId: 1, qty: 2 })
132
126
 
133
127
  if ('queued' in result) {
134
- console.log(result.message) // "createOrder queued — will execute when online"
128
+ // Persisted to IndexedDB — will replay automatically on reconnect
129
+ console.log(result.message)
135
130
  }
136
131
  ```
137
132
 
@@ -141,114 +136,134 @@ if ('queued' in result) {
141
136
 
142
137
  ### `resource(url, config)`
143
138
 
144
- Registers a URL as an offline-capable resource. Returns a handle for fetching and cache management.
139
+ Registers a URL as an offline-capable resource. Returns a `ResourceHandle`.
145
140
 
146
141
  ```ts
147
- const products = resource('/api/products', {
148
- offline: true, // required: enables SW interception
142
+ const handle = resource('/api/products', {
143
+ offline: true, // required enables SW interception
149
144
  strategy?: 'cache-first' | 'stale-while-revalidate' | 'network-first',
150
- cacheName?: string, // custom cache bucket
145
+ cacheName?: string, // custom Cache Storage bucket (default: 'eidos-resources-v1')
146
+ maxAge?: number, // TTL in ms — expired entries are re-fetched from network
151
147
  })
148
+ ```
149
+
150
+ **Auto-selected strategy:**
151
+
152
+ | Config | Strategy | When to use |
153
+ |--------|----------|-------------|
154
+ | `offline: true` | `StaleWhileRevalidate` | Default — instant response + background refresh |
155
+ | `offline: true, strategy: 'cache-first'` | `CacheFirst` | Static assets, rarely-changing data |
156
+ | `offline: true, strategy: 'network-first'` | `NetworkFirst` | Always-fresh data with offline fallback |
152
157
 
153
- // Handle methods
154
- products.fetch() // → Promise<Response>
155
- products.json<T>() // → Promise<T>
156
- products.query() // { queryKey, queryFn } for TanStack Query
157
- products.prefetch() // Promise<void>
158
- products.invalidate() // Promise<void>clears SW cache entry
159
-
160
- // Handle properties
161
- products.url // '/api/products'
162
- products.strategy // generated GeneratedStrategy object
163
- products.config // the config you passed in
158
+ **Handle methods:**
159
+
160
+ ```ts
161
+ handle.fetch() // Promise<Response> fetches, respects maxAge
162
+ handle.json<T>() // Promise<T> — fetch() + response.json()
163
+ handle.query<T>() // { queryKey, queryFn } TanStack Query compatible
164
+ handle.prefetch() // Promise<void> — warm the cache
165
+ handle.invalidate() // Promise<void> — evict cached entries
166
+ handle.unregister() // void — remove from SW registry (required to re-register with different config)
164
167
  ```
165
168
 
166
- **Strategy selection:**
169
+ **Handle properties:**
170
+
171
+ ```ts
172
+ handle.url // '/api/products'
173
+ handle.config // the config you passed in
174
+ handle.strategy // { name, swStrategy, cacheName, reasoning, behavior, equivalentCode }
175
+ ```
167
176
 
168
- | Intent | Generated Strategy | Reasoning |
169
- |---|---|---|
170
- | `offline: true` | `StaleWhileRevalidate` | Best balance of speed and freshness for resilient resources |
171
- | `offline: true, strategy: 'cache-first'` | `CacheFirst` | Maximum speed, data rarely changes |
172
- | `offline: true, strategy: 'network-first'` | `NetworkFirst` | Freshness critical, cache as fallback only |
177
+ ---
173
178
 
174
179
  ### `action(fn, config)`
175
180
 
176
- Wraps an async function with reliability guarantees. The wrapped function is a drop-in replacement — calling it is identical whether you're online or offline.
181
+ Wraps any async function with reliability guarantees. The wrapped function is a drop-in replacement.
177
182
 
178
183
  ```ts
179
184
  const createOrder = action(
180
- async (payload: OrderPayload): Promise<Order> => {
181
- // your existing async function, unchanged
182
- },
185
+ async (payload: OrderPayload): Promise<Order> => { /* your fn */ },
183
186
  {
184
- reliability: 'neverLose', // persist to IndexedDB if call fails or offline
185
- maxRetries?: number, // default: 3
186
- name?: string, // label shown in devtools
187
+ reliability: 'neverLose', // persist to IndexedDB + replay on reconnect
188
+ maxRetries?: number, // default: 3
189
+ name?: string, // label in devtools
187
190
  }
188
191
  )
189
192
 
190
- // Returns TReturn when successful, QueuedResult when queued
191
193
  const result = await createOrder(payload)
194
+ // → Order when successful
195
+ // → { queued: true, id, message } when offline or network fails
192
196
  ```
193
197
 
194
198
  **Reliability modes:**
195
199
 
196
200
  | Mode | Behaviour |
197
- |---|---|
198
- | `best-effort` | Call directly. No persistence, no retry. |
199
- | `neverLose` | Persist args to IndexedDB before executing. Replay on reconnect. |
201
+ |------|-----------|
202
+ | `best-effort` | Execute directly. No persistence, no retry. |
203
+ | `neverLose` | Persist args to IndexedDB before executing. Replay on reconnect with exponential backoff. |
204
+
205
+ **Exponential backoff:** `neverLose` actions that fail are retried with `2s × 2^retryCount` delay (capped at 5 min, ±20% jitter). Items not yet due are skipped on each replay pass.
206
+
207
+ ---
200
208
 
201
209
  ### `replayQueue()`
202
210
 
203
- Manually trigger queue replay. Called automatically on the `online` event when `autoReplay: true` (the default).
211
+ Manually trigger queue replay. Called automatically on reconnect when `autoReplay: true`.
204
212
 
205
213
  ```ts
206
214
  import { replayQueue } from '@sweidos/eidos'
207
215
 
208
- window.addEventListener('online', replayQueue)
216
+ // Manual trigger — e.g. after a user clicks "Retry"
217
+ await replayQueue()
209
218
  ```
210
219
 
220
+ ---
221
+
211
222
  ### `EidosProvider`
212
223
 
213
- Root provider that registers the SW and initialises the runtime.
224
+ React root component. Registers the SW and initialises the runtime.
214
225
 
215
226
  ```tsx
216
227
  <EidosProvider
217
- swPath="/eidos-sw.js" // default
218
- autoReplay={true} // replay queue on reconnect, default: true
228
+ swPath="/eidos-sw.js" // default
229
+ autoReplay={true} // replay queue on reconnect, default: true
219
230
  >
220
231
  <App />
221
232
  </EidosProvider>
222
233
  ```
223
234
 
235
+ ---
236
+
224
237
  ### React Hooks
225
238
 
226
239
  ```ts
227
240
  import { useEidosStatus, useEidosResource, useEidosQueue } from '@sweidos/eidos'
228
241
 
229
- // Online + SW status — cheap, safe in headers
230
- const { isOnline, swStatus } = useEidosStatus()
242
+ // Online status + SW lifecycle — cheap subscription, safe in headers
243
+ const { isOnline, swStatus, swError } = useEidosStatus()
231
244
 
232
- // Live state for a single resource
245
+ // Live cache state for a single resource URL
233
246
  const entry = useEidosResource('/api/products')
234
- // → { status, cacheHits, cachedAt, strategy, ... }
247
+ // entry → { status, cacheHits, cacheMisses, cachedAt, strategy, config, ... }
235
248
 
236
- // The full action queue
249
+ // The full action queue, reactive
237
250
  const queue = useEidosQueue()
238
251
 
239
- // Full store (use sparingly)
252
+ // Full Zustand store use sparingly
240
253
  const state = useEidos()
241
254
  ```
242
255
 
256
+ ---
257
+
243
258
  ### `setOfflineSimulation(enabled)`
244
259
 
245
- Toggle offline simulation from devtools or tests. Sends a message to the SW to serve only cached responses.
260
+ Toggle offline simulation without physically disconnecting the network.
246
261
 
247
262
  ```ts
248
263
  import { setOfflineSimulation } from '@sweidos/eidos'
249
264
 
250
- setOfflineSimulation(true) // force offline
251
- setOfflineSimulation(false) // restore normal
265
+ setOfflineSimulation(true) // SW serves only cached responses
266
+ setOfflineSimulation(false) // restore normal behaviour
252
267
  ```
253
268
 
254
269
  ---
@@ -260,43 +275,43 @@ setOfflineSimulation(false) // restore normal
260
275
  │ Application Layer │
261
276
  │ resource() · action() · EidosProvider │ ← you write this
262
277
  └────────────────┬────────────────────────────┘
263
- │ postMessage(EIDOS_REGISTER_RESOURCE)
278
+ EIDOS_REGISTER_RESOURCE (postMessage)
264
279
  ┌────────────────▼────────────────────────────┐
265
- │ Runtime Layer (packages/core)
266
- │ Strategy derivation · Zustand store │ ← eidos npm package
267
- │ SW bridge · IDB queue
280
+ │ Runtime Layer (@sweidos/eidos)
281
+ │ Strategy derivation · Zustand store │
282
+ │ SW bridge · IDB queue · exponential backoff
268
283
  └────────────────┬────────────────────────────┘
269
284
  │ fetch intercept
270
285
  ┌────────────────▼────────────────────────────┐
271
- │ Worker Layer (eidos-sw.js)
272
- │ CacheFirst · StaleWhileRevalidate │ ← generated SW
286
+ │ Worker Layer (eidos-sw.js)
287
+ │ CacheFirst · StaleWhileRevalidate │
273
288
  │ NetworkFirst · Offline simulation │
274
289
  └────────────────┬────────────────────────────┘
275
290
  │ Cache API · IndexedDB
276
291
  ┌────────────────▼────────────────────────────┐
277
292
  │ Storage Layer │
278
- │ Cache Storage · IndexedDB (action queue) │ ← browser APIs
293
+ │ Cache Storage · IndexedDB (action queue) │
279
294
  └─────────────────────────────────────────────┘
280
295
  ```
281
296
 
282
- ### Service Worker protocol
297
+ ### SW message protocol
283
298
 
284
- The runtime communicates with `eidos-sw.js` via `postMessage`. Messages sent from the app:
299
+ **App SW:**
285
300
 
286
301
  | Message | Purpose |
287
- |---|---|
288
- | `EIDOS_REGISTER_RESOURCE` | Add a fetch-intercept rule |
302
+ |---------|---------|
303
+ | `EIDOS_REGISTER_RESOURCE` | Register a fetch-intercept rule |
289
304
  | `EIDOS_UNREGISTER_RESOURCE` | Remove a rule |
290
- | `EIDOS_CLEAR_CACHE` | Evict cache entries |
291
- | `EIDOS_SIMULATE_OFFLINE` | Toggle offline simulation |
305
+ | `EIDOS_CLEAR_CACHE` | Evict cache entries for a URL |
306
+ | `EIDOS_SIMULATE_OFFLINE` | Toggle offline simulation mode |
292
307
  | `EIDOS_PING` | Health check |
293
308
 
294
- Messages received from the SW:
309
+ **SW App:**
295
310
 
296
311
  | Message | Purpose |
297
- |---|---|
298
- | `EIDOS_CACHE_HIT` | A cached response was served |
299
- | `EIDOS_CACHE_UPDATED` | Cache entry was refreshed from network |
312
+ |---------|---------|
313
+ | `EIDOS_CACHE_HIT` | Cached response was served |
314
+ | `EIDOS_CACHE_UPDATED` | Cache entry refreshed from network |
300
315
  | `EIDOS_NETWORK_ERROR` | Network request failed |
301
316
  | `EIDOS_CACHE_CLEARED` | Cache was cleared |
302
317
 
@@ -306,51 +321,35 @@ Messages received from the SW:
306
321
 
307
322
  ```
308
323
  eidos/
324
+ ├── api/ Vercel serverless functions (demo endpoints)
309
325
  ├── packages/
310
- │ ├── core/ eidos npm package
326
+ │ ├── core/ @sweidos/eidos npm package
311
327
  │ │ └── src/
312
328
  │ │ ├── types.ts
313
- │ │ ├── resource.ts resource() implementation
314
- │ │ ├── action.ts action() + queue replay
315
- │ │ ├── runtime.ts init + SW registration
329
+ │ │ ├── resource.ts resource() — caching + handle
330
+ │ │ ├── action.ts action() + exponential backoff queue replay
331
+ │ │ ├── runtime.ts initEidos + SW registration
316
332
  │ │ ├── store.ts Zustand store
317
333
  │ │ ├── sw-bridge.ts postMessage channel
318
- │ │ ├── idb.ts IndexedDB wrapper
334
+ │ │ ├── idb.ts IndexedDB CRUD wrapper
319
335
  │ │ └── react/ EidosProvider + hooks
320
- │ └── worker/ SW typed source
321
- │ └── src/sw.ts → compiles to eidos-sw.js
336
+ │ └── worker/ SW typed source
337
+ │ └── src/sw.ts → compiles to eidos-sw.js
322
338
  ├── apps/
323
- │ └── playground/ interactive demo dashboard
339
+ │ └── playground/ Interactive demo dashboard
324
340
  │ └── public/
325
- │ └── eidos-sw.js compiled service worker
326
- └── examples/ (planned)
327
- ```
328
-
329
- ---
330
-
331
- ## Dev Dashboard
332
-
333
- The playground at `apps/playground` is a full interactive dashboard that demonstrates every feature:
334
-
335
- ```bash
336
- pnpm dev # → http://localhost:3000
341
+ │ └── eidos-sw.js compiled service worker
342
+ └── .github/workflows/ CI/CD — deploy + npm release on push to main
337
343
  ```
338
344
 
339
- It includes:
340
-
341
- - **Overview** — live status + interactive products/orders demos
342
- - **Resources** — every registered resource with cache stats and strategy detail
343
- - **Action Queue** — live queue with per-item status and replay controls
344
- - **Intent Inspector** — step-by-step trace from intent declaration to SW rule
345
- - **How It Works** — architecture diagrams and lifecycle walkthroughs
346
-
347
345
  ---
348
346
 
349
347
  ## Vite Plugin
350
348
 
351
- To automatically copy `eidos-sw.js` into `public/` during dev and build, add this to your `vite.config.ts`:
349
+ Automatically copy `eidos-sw.js` into `public/` on build:
352
350
 
353
351
  ```ts
352
+ // vite.config.ts
354
353
  import { copyFileSync } from 'fs'
355
354
  import { resolve } from 'path'
356
355
 
@@ -359,7 +358,7 @@ function eidosPlugin() {
359
358
  name: 'eidos-sw',
360
359
  buildStart() {
361
360
  copyFileSync(
362
- resolve('./node_modules/eidos/dist/eidos-sw.js'),
361
+ resolve('./node_modules/@sweidos/eidos/dist/eidos-sw.js'),
363
362
  resolve('./public/eidos-sw.js'),
364
363
  )
365
364
  },
@@ -371,26 +370,26 @@ function eidosPlugin() {
371
370
 
372
371
  ## Known Limitations
373
372
 
374
- These are real limitations in v0.1. They are documented so you know exactly what you're getting.
375
-
376
373
  | Limitation | Detail |
377
- |---|---|
378
- | GET-only caching | The SW only intercepts `GET` requests. `POST`/`PUT`/`DELETE` are never cached. |
379
- | Pathname matching | Resources match by pathname only. Cross-origin URLs require the full URL to be registered. |
380
- | Module-scope actions | `action()` must be called at module scope for replay to work after a page reload. |
381
- | No TTL | Cached resources do not expire automatically. Call `resource.invalidate()` to clear. |
382
- | Single SW | `EidosProvider` assumes `/eidos-sw.js`. Multiple SW registrations in one app are unsupported. |
374
+ |------------|--------|
375
+ | GET-only caching | SW intercepts `GET` only. `POST`/`PUT`/`DELETE` are not cached (but *are* queued via `action()`). |
376
+ | Pathname matching | Resources match by pathname. `/api/products?page=2` and `/api/products` share the same SW rule but are cached separately. |
377
+ | Module-scope actions | `action()` must be called at module scope so functions are registered before a page reload triggers queue replay. |
378
+ | Single SW | `EidosProvider` assumes one SW at `/eidos-sw.js`. Multiple registrations are unsupported. |
383
379
 
384
380
  ---
385
381
 
386
382
  ## Roadmap
387
383
 
384
+ - [x] Cache TTL / `maxAge` expiration
385
+ - [x] Exponential backoff with jitter for queue replay
386
+ - [x] Per-resource `cacheName` override
387
+ - [x] `resource.unregister()` for cleanup
388
388
  - [ ] URL pattern matching (wildcards, regex)
389
- - [ ] Cache TTL / expiration
390
389
  - [ ] Cross-origin resource support
391
- - [ ] Background Sync integration (native browser API)
390
+ - [ ] Background Sync API integration
392
391
  - [ ] Vite plugin (first-class, published separately)
393
- - [ ] React Native / Expo adapter
392
+ - [ ] Vue / Svelte bindings
394
393
  - [ ] TanStack Query integration package
395
394
 
396
395
  ---
@@ -398,22 +397,13 @@ These are real limitations in v0.1. They are documented so you know exactly what
398
397
  ## Contributing
399
398
 
400
399
  ```bash
401
- # Install
402
- pnpm install
403
-
404
- # Run the playground
405
- pnpm dev
406
-
407
- # Type-check everything
408
- pnpm type-check
409
-
410
- # Build the core package
411
- pnpm build:core
400
+ pnpm install # install all workspace deps
401
+ pnpm dev # run playground at localhost:3000
402
+ pnpm type-check # typecheck all packages
403
+ pnpm --filter @sweidos/eidos build # build core package
412
404
  ```
413
405
 
414
- The project uses pnpm workspaces. TypeScript strict mode is enabled everywhere.
415
-
416
- The naming (`Eidos`) is a placeholder. All references are easy to find/replace — the package name, SW filename, and message prefix are the only places the name appears.
406
+ The project uses pnpm workspaces. TypeScript strict mode throughout.
417
407
 
418
408
  ---
419
409