@sweidos/eidos 1.0.24 → 1.0.30

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
@@ -119,6 +119,15 @@ export const createOrder = action(
119
119
  // Called only if maxRetries exhausted — revert the optimistic change
120
120
  removeOptimisticOrder(payload)
121
121
  },
122
+ onConflict: (error, [payload]) => {
123
+ // Called during replay when the server returns a 4xx (conflict, gone, etc.)
124
+ // Return 'skip' to silently drop the item, or 'retry' to keep retrying.
125
+ if (error instanceof Response && error.status === 409) {
126
+ removeOptimisticOrder(payload) // revert UI
127
+ return 'skip' // drop from queue — already handled server-side
128
+ }
129
+ return 'retry'
130
+ },
122
131
  },
123
132
  )
124
133
  ```
@@ -216,8 +225,10 @@ const createOrder = action(
216
225
  reliability: 'neverLose', // persist to IndexedDB + replay on reconnect
217
226
  maxRetries?: number, // default: 3
218
227
  name?: string, // label in devtools
228
+ priority?: 'high' | 'normal' | 'low', // replay order (default: 'normal')
219
229
  onOptimistic?: (...args) => void, // called immediately — update UI optimistically
220
230
  onRollback?: (...args) => void, // called on permanent failure — revert UI
231
+ onConflict?: (error, args) => 'retry' | 'skip', // called on 4xx during replay
221
232
  }
222
233
  )
223
234
 
@@ -235,6 +246,37 @@ const result = await createOrder(payload)
235
246
 
236
247
  **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.
237
248
 
249
+ **Conflict resolution:** when a 4xx HTTP response occurs during replay, `onConflict` is called with the thrown error and the original args. Return `'skip'` to silently remove the item from the queue without calling `onRollback`, or `'retry'` to continue normal retry/backoff behaviour.
250
+
251
+ A 4xx is detected when the thrown value is a `Response` with `status` in [400, 499], or any object with a `.status` property in that range.
252
+
253
+ ```ts
254
+ onConflict: (error, [payload]) => {
255
+ if (error instanceof Response && error.status === 409) {
256
+ // already created server-side — safe to drop and revert UI
257
+ removeOptimisticOrder(payload)
258
+ return 'skip'
259
+ }
260
+ return 'retry' // keep in queue for everything else
261
+ }
262
+ ```
263
+
264
+ **Queue prioritization:** `priority` controls the replay order when multiple queued actions are pending. `'high'` items all complete before `'normal'` items start; `'normal'` all complete before `'low'` items start. Within each tier, items run in parallel. Default: `'normal'`.
265
+
266
+ ```ts
267
+ // Critical write — replays before any normal/low actions
268
+ const saveDocument = action(api.saveDocument, {
269
+ reliability: 'neverLose',
270
+ priority: 'high',
271
+ })
272
+
273
+ // Background analytics — replays last, after user-visible writes
274
+ const logEvent = action(api.logEvent, {
275
+ reliability: 'neverLose',
276
+ priority: 'low',
277
+ })
278
+ ```
279
+
238
280
  ---
239
281
 
240
282
  ### `replayQueue()`
@@ -247,13 +289,14 @@ import type { ReplayResult } from '@sweidos/eidos'
247
289
 
248
290
  // Manual trigger — e.g. after a user clicks "Retry"
249
291
  const result: ReplayResult = await replayQueue()
250
- // { attempted: 3, succeeded: 2, failed: 0, retrying: 1, skipped: 0 }
292
+ // { attempted: 3, succeeded: 2, failed: 0, retrying: 1, skipped: 0, conflicted: 0 }
251
293
  //
252
- // attempted — items where the fn was found and called
253
- // succeeded — resolved successfully
254
- // failed — maxRetries exceeded, stays in queue
255
- // retrying — failed, will retry later (nextRetryAt set)
256
- // skipped — fn not in registry (module not imported yet)
294
+ // attempted — items where the fn was found and called
295
+ // succeeded — resolved successfully
296
+ // failed — maxRetries exceeded, stays in queue
297
+ // retrying — failed, will retry later (nextRetryAt set)
298
+ // skipped — fn not in registry (module not imported yet)
299
+ // conflicted — 4xx response, onConflict returned 'skip', removed from queue
257
300
  ```
258
301
 
259
302
  ---
@@ -391,6 +434,29 @@ const hits = eidosResource('/api/products').getState()?.cacheHits ?? 0
391
434
 
392
435
  ---
393
436
 
437
+ ### `warmCache(handles[])`
438
+
439
+ Bulk-prefetch an array of resource handles concurrently — warms the cache for all of them in one call. Useful on login or app init when you know which resources the user will need offline.
440
+
441
+ Pattern handles (containing `*`, `**`, or `:param`) are counted as failed — they match multiple URLs so there is no single URL to prefetch.
442
+
443
+ ```ts
444
+ import { warmCache } from '@sweidos/eidos'
445
+ import type { WarmCacheResult } from '@sweidos/eidos'
446
+
447
+ // After login — warm the cache with the user's likely-needed data
448
+ const result: WarmCacheResult = await warmCache([products, userProfile, settings])
449
+ // { warmed: 3, failed: 0, errors: [] }
450
+ //
451
+ // warmed — handles prefetched successfully
452
+ // failed — handles that threw (network error, offline, pattern handle, etc.)
453
+ // errors — the raw thrown values for failed handles
454
+ ```
455
+
456
+ In development, a `console.warn` is printed for each failed handle.
457
+
458
+ ---
459
+
394
460
  ### `setOfflineSimulation(enabled)`
395
461
 
396
462
  Toggle offline simulation without physically disconnecting the network.
@@ -690,6 +756,79 @@ it('caches the resource after first fetch', async () => {
690
756
 
691
757
  ---
692
758
 
759
+ ## SSR Adapters
760
+
761
+ Eidos is browser-only — Service Workers, Cache API, and IndexedDB are not available in Node.js. The runtime already no-ops safely when `window` is undefined, but two subpath exports make integration with SSR frameworks seamless.
762
+
763
+ ### Next.js App Router (`@sweidos/eidos/nextjs`)
764
+
765
+ Imports from this subpath are pre-marked `'use client'`, so you can use `EidosProvider` and all hooks directly in your App Router layout without creating your own wrapper file.
766
+
767
+ ```tsx
768
+ // app/providers.tsx ← no 'use client' needed here
769
+ import { EidosProvider, useEidosStatus } from '@sweidos/eidos/nextjs'
770
+
771
+ export function Providers({ children }: { children: React.ReactNode }) {
772
+ return <EidosProvider swPath="/eidos-sw.js">{children}</EidosProvider>
773
+ }
774
+ ```
775
+
776
+ The `'use client'` boundary is on the published `dist/nextjs.js` — Next.js recognises it and marks everything imported through that entry as client code.
777
+
778
+ ### SvelteKit (`@sweidos/eidos/sveltekit`)
779
+
780
+ Use `initEidosSvelteKit()` inside `onMount` in your root `+layout.svelte`. The helper returns an `onMount`-compatible callback that defers init to the browser, keeping SSR clean.
781
+
782
+ ```svelte
783
+ <!-- src/routes/+layout.svelte -->
784
+ <script>
785
+ import { onMount } from 'svelte'
786
+ import { initEidosSvelteKit } from '@sweidos/eidos/sveltekit'
787
+
788
+ onMount(initEidosSvelteKit({ swPath: '/eidos-sw.js', autoReplay: true }))
789
+ </script>
790
+
791
+ <slot />
792
+ ```
793
+
794
+ Use the framework-agnostic stores (`eidosQueue`, `eidosStatus`, etc.) from the main `@sweidos/eidos` import in your Svelte components — they work with Svelte's `$` auto-subscribe prefix out of the box.
795
+
796
+ ---
797
+
798
+ ## Devtools
799
+
800
+ `@sweidos/eidos/devtools` exports a floating panel component you can drop into any React app during development. It shows live queue state, cache entries, SW registration status, and lets you toggle offline simulation — all without leaving your app.
801
+
802
+ ```tsx
803
+ import { EidosDevtools } from '@sweidos/eidos/devtools'
804
+
805
+ // Add anywhere in your component tree (bottom-right by default)
806
+ export default function App() {
807
+ return (
808
+ <>
809
+ <YourApp />
810
+ {process.env.NODE_ENV === 'development' && <EidosDevtools />}
811
+ </>
812
+ )
813
+ }
814
+ ```
815
+
816
+ **Props:**
817
+
818
+ | Prop | Type | Default | Description |
819
+ |------|------|---------|-------------|
820
+ | `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Corner to anchor the panel |
821
+ | `defaultOpen` | `boolean` | `false` | Start expanded |
822
+
823
+ **Panel features:**
824
+ - **Status bar** — online/offline indicator, SW registration status, offline simulation toggle (`setOfflineSimulation`)
825
+ - **Queue tab** — all queue items with status badges (`pending` / `replaying` / `succeeded` / `failed`), priority, retry count, plus Replay and Clear buttons
826
+ - **Cache tab** — all registered resources with cache status, strategy name, hit/miss counts, and last cached timestamp
827
+
828
+ The component is self-contained with inline styles — no CSS import needed, no style conflicts.
829
+
830
+ ---
831
+
693
832
  ## Known Limitations
694
833
 
695
834
  | Limitation | Detail |
@@ -717,17 +856,17 @@ it('caches the resource after first fetch', async () => {
717
856
 
718
857
  **Core reliability**
719
858
  - [x] Optimistic updates — `onOptimistic` / `onRollback` callbacks on `action()` for instant UI feedback before server confirms
720
- - [ ] Conflict resolution hook — `onConflict` callback when replaying a queued action returns 4xx; decide per-item: retry, skip, or merge
721
- - [ ] Queue prioritization — `priority: 'high' | 'normal' | 'low'` on `action()`; high-priority items replay first
859
+ - [x] Conflict resolution hook — `onConflict` callback when replaying a queued action returns 4xx; decide per-item: retry or skip
860
+ - [x] Queue prioritization — `priority: 'high' | 'normal' | 'low'` on `action()`; high-priority items replay first
722
861
 
723
862
  **DX / Tooling**
724
- - [ ] Devtools panel component — drop-in `<EidosDevtools />` showing cache entries, queue state, replay status, and offline toggle
863
+ - [x] Devtools panel component — drop-in `<EidosDevtools />` showing cache entries, queue state, replay status, and offline toggle
725
864
  - [x] Testing utilities (`@sweidos/eidos/testing`) — `mockOffline()`, `mockOnline()`, `drainQueue()`, `waitForQueueDrain()`, `getCachedEntry(url)`, `clearEidosCache()`, `resetEidos()`, `getEidosState()` for Vitest / Playwright
726
- - [ ] SvelteKit / Next.js adapters — SSR-aware init helpers that skip SW registration server-side
865
+ - [x] SvelteKit / Next.js adapters — SSR-aware init helpers that skip SW registration server-side
727
866
 
728
867
  **Performance**
729
868
  - [x] Request deduplication — multiple simultaneous `resource.fetch()` calls share one in-flight network request; each caller gets an independent cloned `Response`
730
- - [ ] Cache warming — `warmCache(handles[])` bulk-prefetches a list of resources on init (e.g. on login)
869
+ - [x] Cache warming — `warmCache(handles[])` bulk-prefetches a list of resources on init (e.g. on login)
731
870
 
732
871
  **Ecosystem**
733
872
  - [ ] React Native support — AsyncStorage + fetch-based backend (no Cache API / SW); same `resource` / `action` API surface
package/dist/action.js CHANGED
@@ -1,109 +1,133 @@
1
- import { useEidosStore as p } from "./store.js";
2
- import { getSwRegistration as R } from "./sw-bridge.js";
3
- import { idbClearQueue as I, idbGetPendingItems as k, idbUpdateQueueItem as l, idbRemoveFromQueue as h, idbAddToQueue as S } from "./idb.js";
4
- const m = /* @__PURE__ */ new Map(), b = /* @__PURE__ */ new Map();
5
- function Q() {
1
+ import { useEidosStore as d } from "./store.js";
2
+ import { getSwRegistration as h } from "./sw-bridge.js";
3
+ import { idbClearQueue as I, idbGetPendingItems as Q, idbAddToQueue as R, idbUpdateQueueItem as u, idbRemoveFromQueue as p } from "./idb.js";
4
+ const l = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), w = /* @__PURE__ */ new Map();
5
+ function m() {
6
6
  return crypto.randomUUID();
7
7
  }
8
- function M(a, t) {
9
- const n = t.name || a.name || Q();
10
- m.set(n, a), t.onRollback && b.set(n, t.onRollback);
11
- const s = async (...r) => {
12
- var e, u;
13
- const { isOnline: i } = p.getState();
14
- if ((e = t.onOptimistic) == null || e.call(t, ...r), t.reliability === "neverLose") {
15
- if (!i)
16
- return f(n, n, r, t);
8
+ function M(e, t) {
9
+ const r = t.name || e.name || m();
10
+ l.set(r, e), t.onRollback && y.set(r, t.onRollback), t.onConflict && w.set(r, t.onConflict);
11
+ const s = async (...a) => {
12
+ var i, o;
13
+ const { isOnline: n } = d.getState();
14
+ if ((i = t.onOptimistic) == null || i.call(t, ...a), t.reliability === "neverLose") {
15
+ if (!n)
16
+ return f(r, r, a, t);
17
17
  try {
18
- return await a(...r);
18
+ return await e(...a);
19
19
  } catch {
20
- return f(n, n, r, t);
20
+ return f(r, r, a, t);
21
21
  }
22
22
  }
23
23
  try {
24
- return await a(...r);
25
- } catch (c) {
26
- throw (u = t.onRollback) == null || u.call(t, ...r), c;
24
+ return await e(...a);
25
+ } catch (b) {
26
+ throw (o = t.onRollback) == null || o.call(t, ...a), b;
27
27
  }
28
28
  };
29
- return Object.defineProperty(s, "id", { value: n, writable: !1 }), Object.defineProperty(s, "config", { value: t, writable: !1 }), s;
29
+ return Object.defineProperty(s, "id", { value: r, writable: !1 }), Object.defineProperty(s, "config", { value: t, writable: !1 }), s;
30
30
  }
31
- async function f(a, t, n, s) {
32
- const r = Q(), i = {
33
- id: r,
34
- actionId: a,
31
+ async function f(e, t, r, s) {
32
+ const a = m(), n = {
33
+ id: a,
34
+ actionId: e,
35
35
  actionName: t,
36
- args: n,
36
+ args: r,
37
37
  queuedAt: Date.now(),
38
38
  retryCount: 0,
39
39
  maxRetries: s.maxRetries ?? 3,
40
- status: "pending"
40
+ status: "pending",
41
+ priority: s.priority ?? "normal"
41
42
  };
42
- await S(i), p.getState().addQueueItem(i);
43
+ await R(n), d.getState().addQueueItem(n);
43
44
  try {
44
- const e = R();
45
- e && "sync" in e && await e.sync.register("eidos-queue-replay");
45
+ const i = h();
46
+ i && "sync" in i && await i.sync.register("eidos-queue-replay");
46
47
  } catch {
47
48
  }
48
49
  return {
49
50
  queued: !0,
50
- id: r,
51
+ id: a,
51
52
  message: `"${t}" queued — will execute when online`
52
53
  };
53
54
  }
54
- function g(a) {
55
- return Math.min(2e3 * 2 ** a, 3e5) * (0.8 + Math.random() * 0.4);
55
+ function g(e) {
56
+ if (e instanceof Response) return e.status >= 400 && e.status < 500;
57
+ if (typeof e == "object" && e !== null) {
58
+ const t = e.status;
59
+ if (typeof t == "number") return t >= 400 && t < 500;
60
+ }
61
+ return !1;
56
62
  }
57
- let y = !1;
58
- async function O() {
59
- const a = p.getState();
60
- if (!a.isOnline || y)
61
- return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 };
62
- y = !0;
63
+ function k(e) {
64
+ return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
65
+ }
66
+ let c = !1;
67
+ async function D() {
68
+ const e = d.getState();
69
+ if (!e.isOnline || c)
70
+ return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
71
+ c = !0;
63
72
  try {
64
- return await x(a);
73
+ return await C(e);
65
74
  } finally {
66
- y = !1;
75
+ c = !1;
67
76
  }
68
77
  }
69
- async function x(a) {
70
- const t = await k(), n = Date.now(), s = t.filter(
71
- (e) => !e.nextRetryAt || e.nextRetryAt <= n
72
- ), r = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }, i = await Promise.allSettled(
73
- s.map(async (e) => {
74
- var c;
75
- const u = m.get(e.actionId);
76
- if (!u) return "skipped";
77
- a.updateQueueItem(e.id, { status: "replaying" }), await l(e.id, { status: "replaying" });
78
- try {
79
- await u(...e.args);
80
- const o = Date.now();
81
- return a.updateQueueItem(e.id, { status: "succeeded", completedAt: o }), await l(e.id, { status: "succeeded", completedAt: o }), setTimeout(() => {
82
- a.removeQueueItem(e.id), h(e.id);
83
- }, 3e3), "succeeded";
84
- } catch (o) {
85
- const d = e.retryCount + 1;
86
- if (d >= e.maxRetries)
87
- return a.updateQueueItem(e.id, { status: "failed", error: String(o), retryCount: d }), await l(e.id, { status: "failed", error: String(o), retryCount: d }), (c = b.get(e.actionId)) == null || c(...e.args), "failed";
88
- {
89
- const w = Date.now() + g(d);
90
- return a.updateQueueItem(e.id, { status: "pending", retryCount: d, nextRetryAt: w }), await l(e.id, { status: "pending", retryCount: d, nextRetryAt: w }), "retrying";
91
- }
92
- }
93
- })
94
- );
95
- for (const e of i) {
96
- const u = e.status === "fulfilled" ? e.value : "failed";
97
- u === "skipped" ? r.skipped++ : (r.attempted++, r[u]++);
78
+ async function S(e, t) {
79
+ var s;
80
+ const r = l.get(e.actionId);
81
+ if (!r) return "skipped";
82
+ try {
83
+ await r(...e.args);
84
+ const a = Date.now();
85
+ return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await u(e.id, { status: "succeeded", completedAt: a }), setTimeout(() => {
86
+ t.removeQueueItem(e.id), p(e.id);
87
+ }, 3e3), "succeeded";
88
+ } catch (a) {
89
+ if (g(a)) {
90
+ const i = w.get(e.actionId);
91
+ if (i && i(a, e.args) === "skip")
92
+ return t.removeQueueItem(e.id), await p(e.id), "conflicted";
93
+ }
94
+ const n = e.retryCount + 1;
95
+ if (n >= e.maxRetries)
96
+ return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await u(e.id, { status: "failed", error: String(a), retryCount: n }), (s = y.get(e.actionId)) == null || s(...e.args), "failed";
97
+ {
98
+ const i = Date.now() + k(n);
99
+ return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await u(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), "retrying";
100
+ }
101
+ }
102
+ }
103
+ async function x(e, t, r) {
104
+ if (e.length === 0) return;
105
+ const s = e.filter((n) => l.has(n.actionId));
106
+ if (r.skipped += e.length - s.length, s.length > 0) {
107
+ t.batchUpdateQueueItems(s.map((n) => ({ id: n.id, update: { status: "replaying" } })));
108
+ for (const n of s)
109
+ u(n.id, { status: "replaying" });
110
+ }
111
+ const a = await Promise.allSettled(s.map((n) => S(n, t)));
112
+ for (const n of a) {
113
+ const i = n.status === "fulfilled" ? n.value : "failed";
114
+ i === "skipped" ? r.skipped++ : i === "conflicted" ? r.conflicted++ : (r.attempted++, r[i]++);
98
115
  }
99
- return r;
100
116
  }
101
- async function q() {
102
- await I(), p.getState().hydrateQueue([]);
117
+ async function C(e) {
118
+ const t = await Q(), r = Date.now(), s = t.filter((n) => !n.nextRetryAt || n.nextRetryAt <= r), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
119
+ for (const n of ["high", "normal", "low"]) {
120
+ const i = s.filter((o) => (o.priority ?? "normal") === n);
121
+ await x(i, e, a);
122
+ }
123
+ return a;
124
+ }
125
+ async function O() {
126
+ await I(), d.getState().hydrateQueue([]);
103
127
  }
104
128
  export {
105
129
  M as action,
106
- q as clearQueue,
107
- O as replayQueue
130
+ O as clearQueue,
131
+ D as replayQueue
108
132
  };
109
133
  //# sourceMappingURL=action.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n }\n\n await idbAddToQueue(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter(\n (item) => !item.nextRetryAt || item.nextRetryAt <= now,\n )\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n\n const outcomes = await Promise.allSettled(\n pending.map(async (item): Promise<'succeeded' | 'failed' | 'retrying' | 'skipped'> => {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n store.updateQueueItem(item.id, { status: 'replaying' })\n await idbUpdateQueueItem(item.id, { status: 'replaying' })\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await idbUpdateQueueItem(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a delay so the UI can show the success state\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await idbUpdateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await idbUpdateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n }),\n )\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else { result.attempted++; result[outcome]++ }\n }\n\n return result\n}\n\n/** Remove all items from the action queue (IDB + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await idbClearQueue()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","candidates","idbGetPendingItems","now","pending","result","outcomes","idbUpdateQueueItem","completedAt","idbRemoveFromQueue","nextRetryAt","o","outcome","clearQueue","idbClearQueue"],"mappings":"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"}
1
+ {"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await idbAddToQueue(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await idbUpdateQueueItem(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await idbRemoveFromQueue(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await idbUpdateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await idbUpdateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n idbUpdateQueueItem(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (IDB + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await idbClearQueue()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","idbUpdateQueueItem","idbRemoveFromQueue","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","idbGetPendingItems","now","pending","tier","tierItems","clearQueue","idbClearQueue"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { EidosDevtools } from './react/Devtools';
2
+ export type { EidosDevtoolsProps } from './react/Devtools';