@sweidos/eidos 2.2.0 → 2.3.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/README.md CHANGED
@@ -139,15 +139,15 @@ if ('queued' in result) {
139
139
  | -------------------------- | ----------------------------- | -------------------------------------------------------------- |
140
140
  | **React** | `@sweidos/eidos` | Hooks + `EidosProvider` |
141
141
  | **Next.js App Router** | `@sweidos/eidos/nextjs` | Pre-marked `'use client'` — no wrapper needed |
142
- | **Next.js Server Actions** | `@eidos/next` | `serverAction()` neverLose wrapper + idempotency context |
142
+ | **Next.js Server Actions** | `@sweidos/next` | `serverAction()` neverLose wrapper + idempotency context |
143
143
  | **SvelteKit** | `@sweidos/eidos/sveltekit` | `initEidosSvelteKit()` in `onMount`, framework-agnostic stores |
144
144
  | **Vue** | `@sweidos/eidos` | Framework-agnostic stores via `eidosStatus.subscribe()` |
145
145
  | **React Native** | `@sweidos/eidos/react-native` | AsyncStorage-backed queue, same `action()` API |
146
146
  | **Vanilla JS** | `@sweidos/eidos` | `eidosStatus`, `eidosQueue`, `eidosQueueStats` stores |
147
147
  | **Vite** | `@sweidos/eidos/vite` | Plugin auto-copies `eidos-sw.js` on every build |
148
- | **CRDT merge (Yjs)** | `@eidos/crdt-yjs` | `createYjsMergeResolver()` for `conflict.strategy: 'merge'` |
148
+ | **CRDT merge (Yjs)** | `@sweidos/crdt-yjs` | `createYjsMergeResolver()` for `conflict.strategy: 'merge'` |
149
149
  | **TanStack Query** | `@sweidos/eidos/query` | `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient` |
150
- | **Tauri / Electron** | `@eidos/sqlite-storage` | SQLite-backed `QueueStorage`, same `action()` API |
150
+ | **Tauri / Electron** | `@sweidos/sqlite-storage` | SQLite-backed `QueueStorage`, same `action()` API |
151
151
 
152
152
  ---
153
153
 
@@ -162,10 +162,14 @@ const products = resource('/api/products', {
162
162
  offline: true, // enable SW interception + caching
163
163
  strategy?: 'cache-first' | 'stale-while-revalidate' | 'network-first',
164
164
  cacheName?: string, // custom cache bucket
165
- maxAge?: number, // TTL in ms — re-fetch after expiry
165
+ maxAge?: number, // TTL in ms — enforced by the SW on all requests (not just handle.fetch())
166
+ maxEntries?: number, // max cache entries; oldest evicted (FIFO) when exceeded
166
167
  version?: string | number, // bump when the response shape changes —
167
168
  // appended to cacheName (e.g. 'eidos-resources-v1-v2')
168
- // so old-shaped cache entries aren't served
169
+ // so old-shaped cache entries aren't served.
170
+ // NOTE: this is separate from the SW-internal CACHE_VERSION
171
+ // (bumped only on Eidos releases to purge old cache buckets).
172
+ // Bump `version` for your data shape; Eidos bumps CACHE_VERSION.
169
173
  })
170
174
 
171
175
  await products.fetch() // Promise<Response>
@@ -197,6 +201,19 @@ await productPattern.invalidate(); // clear all cached entries matching the patt
197
201
  productPattern.unregister();
198
202
  ```
199
203
 
204
+ | Token | Matches |
205
+ | -------- | ------------------------------------------------------------ |
206
+ | `*` | One path segment — `/api/products/*` ↔ `/api/products/4` |
207
+ | `**` | Any number of segments — `https://cdn.example.com/assets/**` |
208
+ | `:param` | A named segment — `/api/users/:id/orders` |
209
+
210
+ Pass the full URL (including origin) for cross-origin resources, e.g.
211
+ `resourcePattern('https://cdn.example.com/assets/**', { offline: true })`.
212
+
213
+ See it live in the [playground docs → Examples → URL patterns, one
214
+ registration per
215
+ family](https://sweidos.vercel.app/docs/examples#url-patterns-one-registration-per-family).
216
+
200
217
  ### `action(fn, config)`
201
218
 
202
219
  ```ts
@@ -226,6 +243,62 @@ await cancelByIdempotencyKey(idempotencyKey) // true if cancelled/removed
226
243
  await requeueItem(queueItemId) // true if it was 'failed'
227
244
  ```
228
245
 
246
+ ### Conflict resolution
247
+
248
+ A `neverLose` action can sit in the queue for a while — by the time it
249
+ replays, the world may have moved on (the requested stock sold out, the
250
+ record was deleted, etc.). `conflict` decides what happens when a replay gets
251
+ a 4xx response, instead of retrying forever or silently dropping the write:
252
+
253
+ ```ts
254
+ class StockConflictError extends Error {
255
+ status = 409;
256
+ constructor(public available: number) {
257
+ super('insufficient stock');
258
+ }
259
+ }
260
+
261
+ export const reserveStock = action(
262
+ async (payload: { productId: number; quantity: number }) => {
263
+ const res = await fetch('/api/inventory', {
264
+ method: 'POST',
265
+ body: JSON.stringify(payload),
266
+ });
267
+ if (res.status === 409) {
268
+ const { available } = await res.json();
269
+ throw new StockConflictError(available);
270
+ }
271
+ if (!res.ok) throw new Error('Reservation failed');
272
+ return res.json();
273
+ },
274
+ {
275
+ reliability: 'neverLose',
276
+ name: 'reserveStock',
277
+ conflict: {
278
+ strategy: 'custom',
279
+ resolve: ({ error, args, attempt }) => {
280
+ if (error instanceof StockConflictError && error.available > 0) {
281
+ const [payload] = args;
282
+ // Rewrite the queued args and retry with what's actually available
283
+ return { resolved: [{ ...payload, quantity: error.available }] };
284
+ }
285
+ return 'skip'; // nothing left to reserve — drop the write
286
+ },
287
+ },
288
+ },
289
+ );
290
+ ```
291
+
292
+ | Strategy | Behavior on 4xx replay |
293
+ | ------------ | --------------------------------------------------------------------- |
294
+ | `serverWins` | Drop the queued item — the server's current state is authoritative. |
295
+ | `clientWins` | Keep retrying — the write should eventually succeed. |
296
+ | `merge` | Call `resolve(ctx)`; typically used to combine client + server state. |
297
+ | `custom` | Call `resolve(ctx)`; return `'retry'`, `'skip'`, or `{ resolved }`. |
298
+
299
+ See it live in the [playground docs → Examples → Conflict resolution on
300
+ replay](https://sweidos.vercel.app/docs/examples#conflict-resolution-on-replay).
301
+
229
302
  ### React hooks
230
303
 
231
304
  ```ts
@@ -266,6 +339,86 @@ initEidos({
266
339
  The same counters are visible live in `<EidosDevtools />` under the
267
340
  "Reliability" tab.
268
341
 
342
+ ### Handling SW updates
343
+
344
+ By default, when a new service worker is available it activates immediately
345
+ (`skipWaiting: true`). This matches standard PWA behaviour but can interrupt
346
+ in-flight requests on pages that are mid-navigation.
347
+
348
+ Opt into the **toast-then-reload** pattern with `skipWaiting: false`:
349
+
350
+ ```ts
351
+ import { initEidos, triggerSwUpdate } from '@sweidos/eidos';
352
+
353
+ initEidos({
354
+ skipWaiting: false,
355
+ onUpdateAvailable: (_registration) => {
356
+ // Show a toast, banner, or dialog — then call triggerSwUpdate() when the
357
+ // user confirms they're ready to reload.
358
+ showToast({
359
+ message: 'App update ready',
360
+ action: { label: 'Reload', onClick: triggerSwUpdate },
361
+ });
362
+ },
363
+ });
364
+ ```
365
+
366
+ `triggerSwUpdate()` tells the waiting service worker to activate, then the
367
+ browser reloads the page. With `skipWaiting: true` (default) `onUpdateAvailable`
368
+ is never called and `triggerSwUpdate()` is not needed.
369
+
370
+ > **Tip**: avoid calling `triggerSwUpdate()` while `neverLose` actions are
371
+ > mid-replay. The replay coordination (BroadcastChannel + Web Locks) survives
372
+ > SW activation, but triggering an update during an active replay pass adds
373
+ > unnecessary churn. Wait until the queue drains or use `waitForQueueDrain()`
374
+ > from `@sweidos/eidos/testing` in tests.
375
+
376
+ ### Queue management
377
+
378
+ Inspect and manage the offline action queue directly — handy for "pending
379
+ changes" panels, manual retry buttons, or a "discard my offline edits" action:
380
+
381
+ ```ts
382
+ import {
383
+ useEidosQueue,
384
+ cancelByIdempotencyKey,
385
+ requeueItem,
386
+ clearQueue,
387
+ replayQueue,
388
+ } from '@sweidos/eidos';
389
+
390
+ function QueuePanel() {
391
+ const queue = useEidosQueue(); // live list of pending/replaying/failed items
392
+
393
+ return queue.map((item) => (
394
+ <li key={item.id}>
395
+ {item.actionName} — {item.status}
396
+
397
+ {/* Drop a write before it ever reaches the server */}
398
+ {item.status === 'pending' && (
399
+ <button onClick={() => cancelByIdempotencyKey(item.idempotencyKey)}>
400
+ Cancel
401
+ </button>
402
+ )}
403
+
404
+ {/* Reset a failed item to 'pending' and replay it */}
405
+ {item.status === 'failed' && (
406
+ <button onClick={() => requeueItem(item.id)}>Retry</button>
407
+ )}
408
+ </li>
409
+ ));
410
+ }
411
+
412
+ // Drop every queued write — e.g. on "discard offline changes"
413
+ await clearQueue();
414
+
415
+ // Force a replay pass — normally triggered automatically on reconnect
416
+ await replayQueue();
417
+ ```
418
+
419
+ See it live in the [playground docs → Examples → Queue management &
420
+ reliability stats](https://sweidos.vercel.app/docs/examples#queue-management-reliability-stats).
421
+
269
422
  ---
270
423
 
271
424
  ## TanStack Query
@@ -365,6 +518,10 @@ for client-side routing; otherwise the SW opens `data.url` directly.
365
518
 
366
519
  ## Testing
367
520
 
521
+ `@sweidos/eidos/testing` runs entirely at the JS layer — no real Service
522
+ Worker needed — and gives Vitest/Jest/Playwright direct control over online
523
+ state, the action queue, and the resource cache.
524
+
368
525
  ```ts
369
526
  import {
370
527
  mockOffline,
@@ -380,29 +537,82 @@ import {
380
537
  beforeEach(() => resetEidos());
381
538
 
382
539
  it('queues action while offline', async () => {
383
- mockOffline();
384
- await createOrder({ productId: 1, qty: 2 });
540
+ mockOffline({ stubFetch: true });
541
+ await createOrder({ productId: 1, quantity: 2 });
385
542
  expect(getEidosState().queue).toHaveLength(1);
543
+ expect(getEidosState().queue[0].actionName).toBe('createOrder');
386
544
  });
387
545
 
388
546
  it('replays on reconnect', async () => {
389
547
  mockOffline();
390
- await createOrder({ productId: 1, qty: 2 });
548
+ await createOrder({ productId: 1, quantity: 2 });
549
+
550
+ // Forces isOnline = true and replays immediately
391
551
  const result = await drainQueue();
392
552
  expect(result.succeeded).toBe(1);
553
+
554
+ // ...or for code that replays itself on the 'online' event:
555
+ await waitForQueueDrain({ timeout: 2000 });
556
+ expect(getEidosState().queue).toHaveLength(0);
557
+ });
558
+
559
+ it('caches GET responses for offline use', async () => {
560
+ await products.json();
561
+ const cached = await getCachedEntry('/api/products');
562
+ expect(cached).toBeDefined();
393
563
  });
564
+
565
+ afterEach(() => clearEidosCache());
394
566
  ```
395
567
 
396
568
  ---
397
569
 
398
570
  ## OpenAPI codegen
399
571
 
572
+ `eidos-gen` reads an OpenAPI 3.x spec (JSON or YAML) and writes typed
573
+ `resource()` + `action()` declarations — request/response interfaces from
574
+ `$ref` schemas, `{id}` → `:id` path-param conversion, and DELETE body
575
+ omission.
576
+
400
577
  ```bash
401
- npx eidos-gen openapi.json
402
- # → writes eidos.generated.ts with typed resource() + action() declarations
578
+ npx eidos-gen openapi.json --out src/lib/eidos.generated.ts
579
+
580
+ eidos-gen: reading openapi.json
581
+ eidos-gen: wrote src/lib/eidos.generated.ts
582
+ 2 resource(s), 2 action(s)
583
+ 2 type(s)
403
584
  ```
404
585
 
405
- Handles path params, `$ref` resolution, request/response types, DELETE body omission.
586
+ ```ts
587
+ // eidos.generated.ts
588
+ import { resource, action } from '@sweidos/eidos';
589
+
590
+ export interface Product {
591
+ id: number;
592
+ name: string;
593
+ tags?: string[];
594
+ }
595
+
596
+ export const listProducts = resource('/products', { offline: true });
597
+
598
+ export const createProduct = action(
599
+ async (payload: Product): Promise<Product> => {
600
+ const res = await fetch('/products', {
601
+ method: 'POST',
602
+ headers: { 'Content-Type': 'application/json' },
603
+ body: JSON.stringify(payload),
604
+ });
605
+ return res.json();
606
+ },
607
+ { reliability: 'neverLose', name: 'createProduct' },
608
+ );
609
+ ```
610
+
611
+ | Flag | Effect |
612
+ | -------------- | ------------------------------------------------------------ |
613
+ | `--out, -o` | Output file path (default: `eidos.generated.ts`) |
614
+ | `--no-offline` | Set `offline: false` on every generated `resource()` |
615
+ | `--eidos` | Import path for `@sweidos/eidos` (default: `@sweidos/eidos`) |
406
616
 
407
617
  ---
408
618
 
@@ -419,21 +629,50 @@ import { EidosDevtools } from '@sweidos/eidos/devtools';
419
629
 
420
630
  Panel shows: live queue state · cache entries · SW status · offline simulation toggle.
421
631
 
632
+ ### `eidosDebug()`
633
+
634
+ Returns a plain-object snapshot of the full Eidos runtime state — safe to `JSON.stringify`, useful for bug reports or attaching to error-tracking breadcrumbs:
635
+
636
+ ```ts
637
+ import { eidosDebug } from '@sweidos/eidos';
638
+
639
+ // Print for a bug report
640
+ console.log(JSON.stringify(eidosDebug(), null, 2));
641
+
642
+ // Attach to a Sentry breadcrumb
643
+ Sentry.addBreadcrumb({ data: eidosDebug() });
644
+ ```
645
+
646
+ Snapshot includes: `version`, `swStatus`, `isOnline`, `resourceCount`, `resources` (per-URL status/hits/cachedAt), `queue` (item list with idempotencyKey/retryCount), `reliability` counters, and `swRegistration` state.
647
+
648
+ ---
649
+
650
+ ## Troubleshooting
651
+
652
+ Eidos emits plain-English `console.warn` messages in development (`import.meta.env.DEV`) for common setup problems:
653
+
654
+ | Warning | Cause | Fix |
655
+ | --------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
656
+ | `Service workers require a secure context` | `initEidos()` called on HTTP (non-localhost) | Use `localhost` for dev or deploy to HTTPS |
657
+ | `Service worker file not found at "/eidos-sw.js"` | SW file missing from `public/` | Add `eidos()` to `vite.config.ts` plugins, or copy `node_modules/@sweidos/eidos/dist/eidos-sw.js → public/eidos-sw.js` manually |
658
+ | `Service worker registration failed: …` | Unexpected registration error | Check `eidosDebug().swError` for the full browser error message |
659
+ | `Service workers are not supported in this context` | Old browser, or SW API absent | No fix needed — Eidos degrades gracefully; only SW-side caching is disabled |
660
+
422
661
  ---
423
662
 
424
663
  ## SSR adapters
425
664
 
426
665
  **Next.js** — import from `@sweidos/eidos/nextjs`. Pre-marked `'use client'`, works in App Router layouts without a wrapper.
427
666
 
428
- **Next.js Server Actions** — `@eidos/next`'s `serverAction()` wraps a `'use server'` function with `action()` (`reliability: 'neverLose'` by default), keyed by `config.name` + `config.namespace`. `getActionContext()` / `idempotencyHeaders()` recover the `idempotencyKey`/`attempt` inside the action body.
667
+ **Next.js Server Actions** — `@sweidos/next`'s `serverAction()` wraps a `'use server'` function with `action()` (`reliability: 'neverLose'` by default), keyed by `config.name` + `config.namespace`. `getActionContext()` / `idempotencyHeaders()` recover the `idempotencyKey`/`attempt` inside the action body.
429
668
 
430
669
  **SvelteKit** — `initEidosSvelteKit()` inside `onMount`. Framework-agnostic stores (`$eidosQueue`, `$eidosStatus`) work with Svelte's `$` auto-subscribe.
431
670
 
432
671
  **React Native** — `@sweidos/eidos/react-native` with AsyncStorage-backed queue. Same `action()` API surface, no Service Worker dependency.
433
672
 
434
- **Tauri / Electron** — `@eidos/sqlite-storage` with a SQLite-backed `QueueStorage`. Pass a `@tauri-apps/plugin-sql` `Database` directly, or wrap `better-sqlite3` with the `SqliteLike` interface. Same `action()` API surface, no Service Worker dependency.
673
+ **Tauri / Electron** — `@sweidos/sqlite-storage` with a SQLite-backed `QueueStorage`. Pass a `@tauri-apps/plugin-sql` `Database` directly, or wrap `better-sqlite3` with the `SqliteLike` interface. Same `action()` API surface, no Service Worker dependency.
435
674
 
436
- **CRDT merge (Yjs)** — `@eidos/crdt-yjs`'s `createYjsMergeResolver()` builds a `conflict.resolve` for the `'merge'`/`'custom'` strategy that applies the server's Yjs state and the queued local update to a `Y.Doc`, then rewrites the queued args with the merged update — automatic, loss-free reconciliation of concurrent edits instead of a hand-written `resolve()`.
675
+ **CRDT merge (Yjs)** — `@sweidos/crdt-yjs`'s `createYjsMergeResolver()` builds a `conflict.resolve` for the `'merge'`/`'custom'` strategy that applies the server's Yjs state and the queued local update to a `Y.Doc`, then rewrites the queued args with the merged update — automatic, loss-free reconciliation of concurrent edits instead of a hand-written `resolve()`.
437
676
 
438
677
  ---
439
678
 
@@ -457,7 +696,7 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
457
696
  | Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
458
697
  | Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
459
698
  | TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
460
- | Bundle size (core) | ~6.5 kB brotli | ~3-6 kB (modular) | ~13 kB |
699
+ | Bundle size (core) | ~6.7 kB brotli | ~3-6 kB (modular) | ~13 kB |
461
700
 
462
701
  Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
463
702
  you keep TQ's cache/devtools while Eidos owns the offline layer. Workbox is a
@@ -0,0 +1,22 @@
1
+ import { ActionConfig, ActionHandle, ActionFn, ReplayResult } from './types';
2
+ export declare function action<TArgs extends any[], TReturn>(fn: ActionFn<TArgs, TReturn>, config: ActionConfig<TArgs>): ActionHandle<TArgs, TReturn>;
3
+ /**
4
+ * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /
5
+ * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and
6
+ * still running, otherwise removes a not-yet-replayed queued item.
7
+ * Returns `true` if something was cancelled/removed.
8
+ *
9
+ * Shared by every `ActionHandle.cancel()` and by devtools, which can't
10
+ * address a specific handle from a queue item alone.
11
+ */
12
+ export declare function cancelByIdempotencyKey(idempotencyKey: string): Promise<boolean>;
13
+ /**
14
+ * Reset a `'failed'` queue item back to `'pending'` so the next
15
+ * `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets
16
+ * `retryCount` to 0. Returns `true` if the item existed and was failed.
17
+ * Used by devtools' per-item "Retry" action.
18
+ */
19
+ export declare function requeueItem(id: string): Promise<boolean>;
20
+ export declare function replayQueue(): Promise<ReplayResult>;
21
+ /** Remove all items from the action queue (storage + in-memory store). */
22
+ export declare function clearQueue(): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { ActionQueueItem } from './types';
2
+ import { QueueStorage } from './queue-storage';
3
+ /** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */
4
+ export interface AsyncStorageLike {
5
+ getItem(key: string): Promise<string | null>;
6
+ setItem(key: string, value: string): Promise<void>;
7
+ removeItem(key: string): Promise<void>;
8
+ }
9
+ /**
10
+ * QueueStorage implementation backed by any AsyncStorage-compatible API.
11
+ * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage
12
+ * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).
13
+ */
14
+ export declare class AsyncStorageQueueStorage implements QueueStorage {
15
+ private readonly storage;
16
+ constructor(storage: AsyncStorageLike);
17
+ private readAll;
18
+ private writeAll;
19
+ add(item: ActionQueueItem): Promise<void>;
20
+ getAll(): Promise<ActionQueueItem[]>;
21
+ getPending(): Promise<ActionQueueItem[]>;
22
+ update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;
23
+ remove(id: string): Promise<void>;
24
+ clear(): Promise<void>;
25
+ }
@@ -0,0 +1,46 @@
1
+ import { ReliabilityStats } from './types';
2
+ export interface EidosDebugSnapshot {
3
+ version: string;
4
+ swStatus: string;
5
+ swError?: string;
6
+ isOnline: boolean;
7
+ resourceCount: number;
8
+ resources: Record<string, {
9
+ url: string;
10
+ strategy: string;
11
+ status: string;
12
+ cacheHits: number;
13
+ cacheMisses: number;
14
+ cachedAt?: number;
15
+ }>;
16
+ queue: {
17
+ id: string;
18
+ actionId: string;
19
+ actionName: string;
20
+ status: string;
21
+ retryCount: number;
22
+ maxRetries: number;
23
+ idempotencyKey: string;
24
+ schemaVersion: number;
25
+ queuedAt: number;
26
+ }[];
27
+ reliability: ReliabilityStats;
28
+ swRegistration: {
29
+ scope: string;
30
+ scriptURL: string;
31
+ state: 'installing' | 'waiting' | 'active' | null;
32
+ } | null;
33
+ }
34
+ /**
35
+ * Returns a plain-object snapshot of the current Eidos runtime state.
36
+ * Safe to serialize with `JSON.stringify`. Useful for bug reports,
37
+ * attaching to error tracking breadcrumbs, and the devtools SW tab.
38
+ *
39
+ * @example
40
+ * // Print for a bug report:
41
+ * console.log(JSON.stringify(eidosDebug(), null, 2));
42
+ *
43
+ * // Attach to a Sentry breadcrumb:
44
+ * Sentry.addBreadcrumb({ data: eidosDebug() });
45
+ */
46
+ export declare function eidosDebug(): EidosDebugSnapshot;
package/dist/debug.js ADDED
@@ -0,0 +1,43 @@
1
+ import { useEidosStore as a } from "./store.js";
2
+ import { getSwRegistration as r } from "./sw-bridge.js";
3
+ import { VERSION as c } from "./version.js";
4
+ function l() {
5
+ const t = a.getState(), s = r();
6
+ return {
7
+ version: c,
8
+ swStatus: t.swStatus,
9
+ ...t.swError !== void 0 && { swError: t.swError },
10
+ isOnline: t.isOnline,
11
+ resourceCount: Object.keys(t.resources).length,
12
+ resources: Object.fromEntries(Object.entries(t.resources).map(([e, i]) => [e, {
13
+ url: i.url,
14
+ strategy: i.strategy.swStrategy,
15
+ status: i.status,
16
+ cacheHits: i.cacheHits,
17
+ cacheMisses: i.cacheMisses,
18
+ ...i.cachedAt !== void 0 && { cachedAt: i.cachedAt }
19
+ }])),
20
+ queue: t.queue.map((e) => ({
21
+ id: e.id,
22
+ actionId: e.actionId,
23
+ actionName: e.actionName,
24
+ status: e.status,
25
+ retryCount: e.retryCount,
26
+ maxRetries: e.maxRetries,
27
+ idempotencyKey: e.idempotencyKey,
28
+ schemaVersion: e.schemaVersion,
29
+ queuedAt: e.queuedAt
30
+ })),
31
+ reliability: { ...t.reliability },
32
+ swRegistration: s ? {
33
+ scope: s.scope,
34
+ scriptURL: (s.active ?? s.waiting ?? s.installing)?.scriptURL ?? "",
35
+ state: s.installing ? "installing" : s.waiting ? "waiting" : s.active ? "active" : null
36
+ } : null
37
+ };
38
+ }
39
+ export {
40
+ l as eidosDebug
41
+ };
42
+
43
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.js","names":[],"sources":["../src/debug.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { VERSION } from './version';\nimport type { ReliabilityStats } from './types';\n\nexport interface EidosDebugSnapshot {\n version: string;\n swStatus: string;\n swError?: string;\n isOnline: boolean;\n resourceCount: number;\n resources: Record<\n string,\n {\n url: string;\n strategy: string;\n status: string;\n cacheHits: number;\n cacheMisses: number;\n cachedAt?: number;\n }\n >;\n queue: {\n id: string;\n actionId: string;\n actionName: string;\n status: string;\n retryCount: number;\n maxRetries: number;\n idempotencyKey: string;\n schemaVersion: number;\n queuedAt: number;\n }[];\n reliability: ReliabilityStats;\n swRegistration: {\n scope: string;\n scriptURL: string;\n state: 'installing' | 'waiting' | 'active' | null;\n } | null;\n}\n\n/**\n * Returns a plain-object snapshot of the current Eidos runtime state.\n * Safe to serialize with `JSON.stringify`. Useful for bug reports,\n * attaching to error tracking breadcrumbs, and the devtools SW tab.\n *\n * @example\n * // Print for a bug report:\n * console.log(JSON.stringify(eidosDebug(), null, 2));\n *\n * // Attach to a Sentry breadcrumb:\n * Sentry.addBreadcrumb({ data: eidosDebug() });\n */\nexport function eidosDebug(): EidosDebugSnapshot {\n const state = useEidosStore.getState();\n const swReg = getSwRegistration();\n\n return {\n version: VERSION,\n swStatus: state.swStatus,\n ...(state.swError !== undefined && { swError: state.swError }),\n isOnline: state.isOnline,\n resourceCount: Object.keys(state.resources).length,\n resources: Object.fromEntries(\n Object.entries(state.resources).map(([url, entry]) => [\n url,\n {\n url: entry.url,\n strategy: entry.strategy.swStrategy,\n status: entry.status,\n cacheHits: entry.cacheHits,\n cacheMisses: entry.cacheMisses,\n ...(entry.cachedAt !== undefined && { cachedAt: entry.cachedAt }),\n },\n ]),\n ),\n queue: state.queue.map((item) => ({\n id: item.id,\n actionId: item.actionId,\n actionName: item.actionName,\n status: item.status,\n retryCount: item.retryCount,\n maxRetries: item.maxRetries,\n idempotencyKey: item.idempotencyKey,\n schemaVersion: item.schemaVersion,\n queuedAt: item.queuedAt,\n })),\n reliability: { ...state.reliability },\n swRegistration: swReg\n ? {\n scope: swReg.scope,\n scriptURL: (swReg.active ?? swReg.waiting ?? swReg.installing)?.scriptURL ?? '',\n state: swReg.installing\n ? 'installing'\n : swReg.waiting\n ? 'waiting'\n : swReg.active\n ? 'active'\n : null,\n }\n : null,\n };\n}\n"],"mappings":";;;AAqDA,SAAgB,IAAiC;AAC/C,QAAM,IAAQ,EAAc,SAAS,GAC/B,IAAQ,EAAkB;AAEhC,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU,EAAM;AAAA,IAChB,GAAI,EAAM,YAAY,UAAa,EAAE,SAAS,EAAM,QAAQ;AAAA,IAC5D,UAAU,EAAM;AAAA,IAChB,eAAe,OAAO,KAAK,EAAM,SAAS,EAAE;AAAA,IAC5C,WAAW,OAAO,YAChB,OAAO,QAAQ,EAAM,SAAS,EAAE,IAAA,CAAK,CAAC,GAAK,CAAA,MAAW,CACpD,GACA;AAAA,MACE,KAAK,EAAM;AAAA,MACX,UAAU,EAAM,SAAS;AAAA,MACzB,QAAQ,EAAM;AAAA,MACd,WAAW,EAAM;AAAA,MACjB,aAAa,EAAM;AAAA,MACnB,GAAI,EAAM,aAAa,UAAa,EAAE,UAAU,EAAM,SAAS;AAAA,IACjE,CACF,CAAC,CACH;AAAA,IACA,OAAO,EAAM,MAAM,IAAA,CAAK,OAAU;AAAA,MAChC,IAAI,EAAK;AAAA,MACT,UAAU,EAAK;AAAA,MACf,YAAY,EAAK;AAAA,MACjB,QAAQ,EAAK;AAAA,MACb,YAAY,EAAK;AAAA,MACjB,YAAY,EAAK;AAAA,MACjB,gBAAgB,EAAK;AAAA,MACrB,eAAe,EAAK;AAAA,MACpB,UAAU,EAAK;AAAA,IACjB,EAAE;AAAA,IACF,aAAa,EAAE,GAAG,EAAM,YAAY;AAAA,IACpC,gBAAgB,IACZ;AAAA,MACE,OAAO,EAAM;AAAA,MACb,YAAY,EAAM,UAAU,EAAM,WAAW,EAAM,aAAa,aAAa;AAAA,MAC7E,OAAO,EAAM,aACT,eACA,EAAM,UACJ,YACA,EAAM,SACJ,WACA;AAAA,IACV,IACA;AAAA,EACN;AACF"}