@sweidos/eidos 1.0.30 → 1.0.31

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
@@ -756,6 +756,63 @@ it('caches the resource after first fetch', async () => {
756
756
 
757
757
  ---
758
758
 
759
+ ## OpenAPI Codegen
760
+
761
+ `eidos-gen` is a standalone CLI that reads an OpenAPI 3.x spec (JSON or YAML) and generates a fully-typed Eidos declarations file — `resource()` for every GET endpoint, `action()` for every POST / PUT / PATCH / DELETE.
762
+
763
+ ```bash
764
+ npx eidos-gen openapi.json
765
+ # → writes eidos.generated.ts
766
+ ```
767
+
768
+ **Example output** (from a Store API spec):
769
+
770
+ ```ts
771
+ // Generated by eidos-gen — edit function bodies freely, re-run to refresh declarations.
772
+ import { resource, action } from '@sweidos/eidos'
773
+
774
+ export interface Product { id: string; name: string; price: number; inStock?: boolean }
775
+ export interface CreateProductRequest { name: string; price: number }
776
+
777
+ // Resources (GET)
778
+ export const listProducts = resource('/api/products', { offline: true })
779
+ export const getProduct = resource('/api/products/:id', { offline: true })
780
+
781
+ // Actions (POST / PUT / PATCH / DELETE)
782
+ export const createProduct = action(
783
+ async (payload: CreateProductRequest): Promise<Product> => {
784
+ const res = await fetch('/api/products', { method: 'POST', ... })
785
+ return res.json()
786
+ },
787
+ { reliability: 'neverLose', name: 'createProduct' },
788
+ )
789
+ export const deleteProduct = action(
790
+ async (payload: { id: string }): Promise<void> => {
791
+ const res = await fetch(`/api/products/${payload.id}`, { method: 'DELETE' })
792
+ ...
793
+ },
794
+ { reliability: 'neverLose', name: 'deleteProduct' },
795
+ )
796
+ ```
797
+
798
+ `eidos-gen` handles:
799
+ - **Path params** — `{id}` → `:id` on resources; `{ id: string } & RequestBody` on actions with template-literal URL interpolation
800
+ - **Type generation** — interfaces from `components/schemas` (objects, enums, unions, arrays)
801
+ - **`$ref` resolution** — schema references inline as type names
802
+ - **Response types** — `200`/`201`/`202` response body type used as the action return type
803
+ - **DELETE with no body** — omits `Content-Type` / `body`, handles 204 no-content
804
+
805
+ **Options:**
806
+
807
+ ```bash
808
+ npx eidos-gen <spec> # JSON or YAML
809
+ npx eidos-gen <spec> --out src/lib/eidos.ts
810
+ npx eidos-gen <spec> --no-offline # set offline:false on resources
811
+ npx eidos-gen <spec> --eidos ./my-fork # custom import path
812
+ ```
813
+
814
+ ---
815
+
759
816
  ## SSR Adapters
760
817
 
761
818
  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.
@@ -793,6 +850,83 @@ Use `initEidosSvelteKit()` inside `onMount` in your root `+layout.svelte`. The h
793
850
 
794
851
  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
852
 
853
+ ### React Native (`@sweidos/eidos/react-native`)
854
+
855
+ The React Native subpath swaps the browser-specific backends (IndexedDB, Service Worker, Cache API) for a pluggable `AsyncStorage`-backed queue while keeping the same `action()` / `resource()` API surface.
856
+
857
+ **Setup**
858
+
859
+ ```bash
860
+ # peer deps
861
+ npm install @react-native-async-storage/async-storage @react-native-community/netinfo
862
+ ```
863
+
864
+ ```ts
865
+ // index.js — before rendering anything
866
+ import AsyncStorage from '@react-native-async-storage/async-storage'
867
+ import { initEidosRN } from '@sweidos/eidos/react-native'
868
+
869
+ await initEidosRN({ storage: AsyncStorage })
870
+ ```
871
+
872
+ ```tsx
873
+ // App.tsx
874
+ import { useNetInfo } from '@react-native-community/netinfo'
875
+ import { EidosProviderRN } from '@sweidos/eidos/react-native'
876
+
877
+ export function App() {
878
+ const { isConnected } = useNetInfo()
879
+ return (
880
+ <EidosProviderRN isConnected={isConnected ?? true}>
881
+ <Navigation />
882
+ </EidosProviderRN>
883
+ )
884
+ }
885
+ ```
886
+
887
+ ```ts
888
+ // Declare actions exactly as you would in a web app
889
+ import { action } from '@sweidos/eidos'
890
+
891
+ export const createOrder = action(
892
+ async (payload: CreateOrderInput) => {
893
+ const res = await fetch('/api/orders', { method: 'POST', body: JSON.stringify(payload) })
894
+ if (!res.ok) throw res
895
+ return res.json()
896
+ },
897
+ { reliability: 'neverLose', name: 'createOrder' },
898
+ )
899
+ ```
900
+
901
+ Actions queued while offline are persisted to AsyncStorage and replayed automatically when the device reconnects.
902
+
903
+ **Custom storage**
904
+
905
+ `AsyncStorageLike` accepts any key-value store that implements `getItem` / `setItem` / `removeItem` — you can use MMKV or SQLite instead of AsyncStorage:
906
+
907
+ ```ts
908
+ import { MMKV } from 'react-native-mmkv'
909
+ import { AsyncStorageQueueStorage, setQueueStorage } from '@sweidos/eidos/react-native'
910
+
911
+ const mmkv = new MMKV()
912
+ setQueueStorage(new AsyncStorageQueueStorage({
913
+ getItem: async (key) => mmkv.getString(key) ?? null,
914
+ setItem: async (key, value) => mmkv.set(key, value),
915
+ removeItem: async (key) => mmkv.delete(key),
916
+ }))
917
+ ```
918
+
919
+ **What works in RN vs web**
920
+
921
+ | Feature | Web | React Native |
922
+ |---------|-----|--------------|
923
+ | `action()` queue + replay | ✅ IndexedDB | ✅ AsyncStorage |
924
+ | Offline-aware (auto-queue) | ✅ | ✅ |
925
+ | `resource()` in-memory caching | ✅ | ✅ (in-memory only — no SW) |
926
+ | `resource()` offline persistence | ✅ Cache API + SW | ❌ (fetch from API when online) |
927
+ | `useEidos`, `useEidosQueue` hooks | ✅ | ✅ |
928
+ | Background Sync | ✅ | ❌ (App must be foregrounded) |
929
+
796
930
  ---
797
931
 
798
932
  ## Devtools
@@ -869,8 +1003,8 @@ The component is self-contained with inline styles — no CSS import needed, no
869
1003
  - [x] Cache warming — `warmCache(handles[])` bulk-prefetches a list of resources on init (e.g. on login)
870
1004
 
871
1005
  **Ecosystem**
872
- - [ ] React Native support — AsyncStorage + fetch-based backend (no Cache API / SW); same `resource` / `action` API surface
873
- - [ ] OpenAPI codegen CLI — `npx eidos-gen ./openapi.json` generates typed `resource()` and `action()` declarations
1006
+ - [x] React Native support — `@sweidos/eidos/react-native`; AsyncStorage-backed queue, same `action()` API surface; `EidosProviderRN` syncs NetInfo connectivity into the replay loop
1007
+ - [x] OpenAPI codegen CLI — `npx eidos-gen ./openapi.json` generates typed `resource()` and `action()` declarations
874
1008
 
875
1009
  ---
876
1010
 
package/dist/action.js CHANGED
@@ -1,34 +1,45 @@
1
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();
2
+ import { getSwRegistration as b } from "./sw-bridge.js";
3
+ import { idbClearQueue as g, idbRemoveFromQueue as Q, idbUpdateQueueItem as h, idbGetPendingItems as I, idbGetQueue as R, idbAddToQueue as k } from "./idb.js";
4
+ import { _getQueueStorage as v } from "./queue-storage.js";
5
+ const l = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), S = {
6
+ add: (e) => k(e),
7
+ getAll: () => R(),
8
+ getPending: () => I(),
9
+ update: (e, t) => h(e, t),
10
+ remove: (e) => Q(e),
11
+ clear: () => g()
12
+ };
13
+ function o() {
14
+ return v() ?? S;
15
+ }
5
16
  function m() {
6
17
  return crypto.randomUUID();
7
18
  }
8
- function M(e, t) {
19
+ function U(e, t) {
9
20
  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;
21
+ l.set(r, e), t.onRollback && f.set(r, t.onRollback), t.onConflict && y.set(r, t.onConflict);
22
+ const u = async (...a) => {
23
+ var i, s;
13
24
  const { isOnline: n } = d.getState();
14
25
  if ((i = t.onOptimistic) == null || i.call(t, ...a), t.reliability === "neverLose") {
15
26
  if (!n)
16
- return f(r, r, a, t);
27
+ return p(r, r, a, t);
17
28
  try {
18
29
  return await e(...a);
19
30
  } catch {
20
- return f(r, r, a, t);
31
+ return p(r, r, a, t);
21
32
  }
22
33
  }
23
34
  try {
24
35
  return await e(...a);
25
- } catch (b) {
26
- throw (o = t.onRollback) == null || o.call(t, ...a), b;
36
+ } catch (w) {
37
+ throw (s = t.onRollback) == null || s.call(t, ...a), w;
27
38
  }
28
39
  };
29
- return Object.defineProperty(s, "id", { value: r, writable: !1 }), Object.defineProperty(s, "config", { value: t, writable: !1 }), s;
40
+ return Object.defineProperty(u, "id", { value: r, writable: !1 }), Object.defineProperty(u, "config", { value: t, writable: !1 }), u;
30
41
  }
31
- async function f(e, t, r, s) {
42
+ async function p(e, t, r, u) {
32
43
  const a = m(), n = {
33
44
  id: a,
34
45
  actionId: e,
@@ -36,13 +47,13 @@ async function f(e, t, r, s) {
36
47
  args: r,
37
48
  queuedAt: Date.now(),
38
49
  retryCount: 0,
39
- maxRetries: s.maxRetries ?? 3,
50
+ maxRetries: u.maxRetries ?? 3,
40
51
  status: "pending",
41
- priority: s.priority ?? "normal"
52
+ priority: u.priority ?? "normal"
42
53
  };
43
- await R(n), d.getState().addQueueItem(n);
54
+ await o().add(n), d.getState().addQueueItem(n);
44
55
  try {
45
- const i = h();
56
+ const i = b();
46
57
  i && "sync" in i && await i.sync.register("eidos-queue-replay");
47
58
  } catch {
48
59
  }
@@ -52,7 +63,7 @@ async function f(e, t, r, s) {
52
63
  message: `"${t}" queued — will execute when online`
53
64
  };
54
65
  }
55
- function g(e) {
66
+ function _(e) {
56
67
  if (e instanceof Response) return e.status >= 400 && e.status < 500;
57
68
  if (typeof e == "object" && e !== null) {
58
69
  const t = e.status;
@@ -60,74 +71,74 @@ function g(e) {
60
71
  }
61
72
  return !1;
62
73
  }
63
- function k(e) {
74
+ function x(e) {
64
75
  return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
65
76
  }
66
77
  let c = !1;
67
- async function D() {
78
+ async function j() {
68
79
  const e = d.getState();
69
80
  if (!e.isOnline || c)
70
81
  return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
71
82
  c = !0;
72
83
  try {
73
- return await C(e);
84
+ return await M(e);
74
85
  } finally {
75
86
  c = !1;
76
87
  }
77
88
  }
78
- async function S(e, t) {
79
- var s;
89
+ async function A(e, t) {
90
+ var u;
80
91
  const r = l.get(e.actionId);
81
92
  if (!r) return "skipped";
82
93
  try {
83
94
  await r(...e.args);
84
95
  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);
96
+ return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await o().update(e.id, { status: "succeeded", completedAt: a }), setTimeout(() => {
97
+ t.removeQueueItem(e.id), o().remove(e.id);
87
98
  }, 3e3), "succeeded";
88
99
  } catch (a) {
89
- if (g(a)) {
90
- const i = w.get(e.actionId);
100
+ if (_(a)) {
101
+ const i = y.get(e.actionId);
91
102
  if (i && i(a, e.args) === "skip")
92
- return t.removeQueueItem(e.id), await p(e.id), "conflicted";
103
+ return t.removeQueueItem(e.id), await o().remove(e.id), "conflicted";
93
104
  }
94
105
  const n = e.retryCount + 1;
95
106
  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";
107
+ return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await o().update(e.id, { status: "failed", error: String(a), retryCount: n }), (u = f.get(e.actionId)) == null || u(...e.args), "failed";
97
108
  {
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";
109
+ const i = Date.now() + x(n);
110
+ return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await o().update(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), "retrying";
100
111
  }
101
112
  }
102
113
  }
103
- async function x(e, t, r) {
114
+ async function C(e, t, r) {
104
115
  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" });
116
+ const u = e.filter((n) => l.has(n.actionId));
117
+ if (r.skipped += e.length - u.length, u.length > 0) {
118
+ t.batchUpdateQueueItems(u.map((n) => ({ id: n.id, update: { status: "replaying" } })));
119
+ for (const n of u)
120
+ o().update(n.id, { status: "replaying" });
110
121
  }
111
- const a = await Promise.allSettled(s.map((n) => S(n, t)));
122
+ const a = await Promise.allSettled(u.map((n) => A(n, t)));
112
123
  for (const n of a) {
113
124
  const i = n.status === "fulfilled" ? n.value : "failed";
114
125
  i === "skipped" ? r.skipped++ : i === "conflicted" ? r.conflicted++ : (r.attempted++, r[i]++);
115
126
  }
116
127
  }
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 };
128
+ async function M(e) {
129
+ const t = await o().getPending(), r = Date.now(), u = t.filter((n) => !n.nextRetryAt || n.nextRetryAt <= r), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
119
130
  for (const n of ["high", "normal", "low"]) {
120
- const i = s.filter((o) => (o.priority ?? "normal") === n);
121
- await x(i, e, a);
131
+ const i = u.filter((s) => (s.priority ?? "normal") === n);
132
+ await C(i, e, a);
122
133
  }
123
134
  return a;
124
135
  }
125
- async function O() {
126
- await I(), d.getState().hydrateQueue([]);
136
+ async function T() {
137
+ await o().clear(), d.getState().hydrateQueue([]);
127
138
  }
128
139
  export {
129
- M as action,
130
- O as clearQueue,
131
- D as replayQueue
140
+ U as action,
141
+ T as clearQueue,
142
+ j as replayQueue
132
143
  };
133
144
  //# 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// 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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"}
1
+ {"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport { _getQueueStorage } from './queue-storage'\nimport type { QueueStorage } from './queue-storage'\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\n// IDB fallback — used when no custom storage is set (default browser behavior).\nconst _idbFallback: QueueStorage = {\n add: (item) => idbAddToQueue(item),\n getAll: () => idbGetQueue(),\n getPending: () => idbGetPendingItems(),\n update: (id, patch) => idbUpdateQueueItem(id, patch),\n remove: (id) => idbRemoveFromQueue(id),\n clear: () => idbClearQueue(),\n}\n\nfunction qs(): QueueStorage {\n return _getQueueStorage() ?? _idbFallback\n}\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 qs().add(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 qs().update(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 qs().remove(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 qs().remove(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 qs().update(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 qs().update(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 qs().update(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 qs().getPending()\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 (storage + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await qs().clear()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","_idbFallback","item","idbAddToQueue","idbGetQueue","idbGetPendingItems","id","patch","idbUpdateQueueItem","idbRemoveFromQueue","idbClearQueue","qs","_getQueueStorage","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","now","pending","tier","tierItems","clearQueue"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ const i = "@eidos:queue";
2
+ class l {
3
+ constructor(t) {
4
+ this.storage = t;
5
+ }
6
+ async readAll() {
7
+ try {
8
+ const t = await this.storage.getItem(i);
9
+ return t ? JSON.parse(t) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ }
14
+ async writeAll(t) {
15
+ await this.storage.setItem(i, JSON.stringify(t));
16
+ }
17
+ async add(t) {
18
+ const e = await this.readAll();
19
+ e.push(t), await this.writeAll(e);
20
+ }
21
+ async getAll() {
22
+ return this.readAll();
23
+ }
24
+ async getPending() {
25
+ return (await this.readAll()).filter((e) => e.status === "pending" || e.status === "failed");
26
+ }
27
+ async update(t, e) {
28
+ const a = await this.readAll(), s = a.findIndex((r) => r.id === t);
29
+ s !== -1 && (a[s] = { ...a[s], ...e }), await this.writeAll(a);
30
+ }
31
+ async remove(t) {
32
+ const e = await this.readAll();
33
+ await this.writeAll(e.filter((a) => a.id !== t));
34
+ }
35
+ async clear() {
36
+ await this.storage.removeItem(i);
37
+ }
38
+ }
39
+ export {
40
+ l as AsyncStorageQueueStorage
41
+ };
42
+ //# sourceMappingURL=async-storage-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage-adapter.js","sources":["../src/async-storage-adapter.ts"],"sourcesContent":["import type { ActionQueueItem } from './types'\nimport type { QueueStorage } from './queue-storage'\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n removeItem(key: string): Promise<void>\n}\n\nconst QUEUE_KEY = '@eidos:queue'\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY)\n if (!raw) return []\n return JSON.parse(raw) as ActionQueueItem[]\n } catch {\n return []\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items))\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll()\n items.push(item)\n await this.writeAll(items)\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll()\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll()\n return items.filter((i) => i.status === 'pending' || i.status === 'failed')\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll()\n const idx = items.findIndex((i) => i.id === id)\n if (idx !== -1) items[idx] = { ...items[idx], ...patch }\n await this.writeAll(items)\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll()\n await this.writeAll(items.filter((i) => i.id !== id))\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY)\n }\n}\n"],"names":["QUEUE_KEY","AsyncStorageQueueStorage","storage","raw","items","item","i","id","patch","idx"],"mappings":"AAUA,MAAMA,IAAY;AAOX,MAAMC,EAAiD;AAAA,EAC5D,YAA6BC,GAA2B;AAA3B,SAAA,UAAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAMC,IAAM,MAAM,KAAK,QAAQ,QAAQH,CAAS;AAChD,aAAKG,IACE,KAAK,MAAMA,CAAG,IADJ,CAAA;AAAA,IAEnB,QAAQ;AACN,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,SAASC,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQJ,GAAW,KAAK,UAAUI,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAIC,GAAsC;AAC9C,UAAMD,IAAQ,MAAM,KAAK,QAAA;AACzB,IAAAA,EAAM,KAAKC,CAAI,GACf,MAAM,KAAK,SAASD,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAM,aAAyC;AAE7C,YADc,MAAM,KAAK,QAAA,GACZ,OAAO,CAACE,MAAMA,EAAE,WAAW,aAAaA,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAOC,GAAYC,GAAgD;AACvE,UAAMJ,IAAQ,MAAM,KAAK,QAAA,GACnBK,IAAML,EAAM,UAAU,CAACE,MAAMA,EAAE,OAAOC,CAAE;AAC9C,IAAIE,MAAQ,OAAIL,EAAMK,CAAG,IAAI,EAAE,GAAGL,EAAMK,CAAG,GAAG,GAAGD,EAAA,IACjD,MAAM,KAAK,SAASJ,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAOG,GAA2B;AACtC,UAAMH,IAAQ,MAAM,KAAK,QAAA;AACzB,UAAM,KAAK,SAASA,EAAM,OAAO,CAACE,MAAMA,EAAE,OAAOC,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAWP,CAAS;AAAA,EACzC;AACF;"}
package/dist/devtools.js CHANGED
@@ -11,7 +11,8 @@ function _set(updater) {
11
11
  _notify();
12
12
  }
13
13
  _state = {
14
- isOnline: typeof navigator !== "undefined" ? navigator.onLine : true,
14
+ // navigator.onLine is undefined in React Native — default to true unless explicitly false
15
+ isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
15
16
  swStatus: "idle",
16
17
  swError: void 0,
17
18
  resources: {},
@@ -119,6 +120,24 @@ function openDB() {
119
120
  req.onerror = () => reject(req.error);
120
121
  });
121
122
  }
123
+ async function idbAddToQueue(item) {
124
+ const db = await openDB();
125
+ return new Promise((resolve, reject) => {
126
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
127
+ tx.objectStore(QUEUE_STORE).add(item);
128
+ tx.oncomplete = () => resolve();
129
+ tx.onerror = () => reject(tx.error);
130
+ });
131
+ }
132
+ async function idbGetQueue() {
133
+ const db = await openDB();
134
+ return new Promise((resolve, reject) => {
135
+ const tx = db.transaction(QUEUE_STORE, "readonly");
136
+ const req = tx.objectStore(QUEUE_STORE).getAll();
137
+ req.onsuccess = () => resolve(req.result);
138
+ req.onerror = () => reject(req.error);
139
+ });
140
+ }
122
141
  async function idbUpdateQueueItem(id, update) {
123
142
  const db = await openDB();
124
143
  return new Promise((resolve, reject) => {
@@ -189,6 +208,17 @@ async function idbClearQueue() {
189
208
  const _actionRegistry = /* @__PURE__ */ new Map();
190
209
  const _rollbackRegistry = /* @__PURE__ */ new Map();
191
210
  const _conflictRegistry = /* @__PURE__ */ new Map();
211
+ const _idbFallback = {
212
+ add: (item) => idbAddToQueue(item),
213
+ getAll: () => idbGetQueue(),
214
+ getPending: () => idbGetPendingItems(),
215
+ update: (id, patch) => idbUpdateQueueItem(id, patch),
216
+ remove: (id) => idbRemoveFromQueue(id),
217
+ clear: () => idbClearQueue()
218
+ };
219
+ function qs() {
220
+ return _idbFallback;
221
+ }
192
222
  function isClientError(err) {
193
223
  if (err instanceof Response) return err.status >= 400 && err.status < 500;
194
224
  if (typeof err === "object" && err !== null) {
@@ -222,10 +252,10 @@ async function _replayItem(item, store) {
222
252
  await fn(...item.args);
223
253
  const completedAt = Date.now();
224
254
  store.updateQueueItem(item.id, { status: "succeeded", completedAt });
225
- await idbUpdateQueueItem(item.id, { status: "succeeded", completedAt });
255
+ await qs().update(item.id, { status: "succeeded", completedAt });
226
256
  setTimeout(() => {
227
257
  store.removeQueueItem(item.id);
228
- idbRemoveFromQueue(item.id);
258
+ qs().remove(item.id);
229
259
  }, 3e3);
230
260
  return "succeeded";
231
261
  } catch (err) {
@@ -235,7 +265,7 @@ async function _replayItem(item, store) {
235
265
  const resolution = onConflict(err, item.args);
236
266
  if (resolution === "skip") {
237
267
  store.removeQueueItem(item.id);
238
- await idbRemoveFromQueue(item.id);
268
+ await qs().remove(item.id);
239
269
  return "conflicted";
240
270
  }
241
271
  }
@@ -243,13 +273,13 @@ async function _replayItem(item, store) {
243
273
  const retryCount = item.retryCount + 1;
244
274
  if (retryCount >= item.maxRetries) {
245
275
  store.updateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
246
- await idbUpdateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
276
+ await qs().update(item.id, { status: "failed", error: String(err), retryCount });
247
277
  (_a = _rollbackRegistry.get(item.actionId)) == null ? void 0 : _a(...item.args);
248
278
  return "failed";
249
279
  } else {
250
280
  const nextRetryAt = Date.now() + backoffMs(retryCount);
251
281
  store.updateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
252
- await idbUpdateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
282
+ await qs().update(item.id, { status: "pending", retryCount, nextRetryAt });
253
283
  return "retrying";
254
284
  }
255
285
  }
@@ -261,7 +291,7 @@ async function _replayTier(items, store, result) {
261
291
  if (replayable.length > 0) {
262
292
  store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: "replaying" } })));
263
293
  for (const item of replayable) {
264
- idbUpdateQueueItem(item.id, { status: "replaying" });
294
+ qs().update(item.id, { status: "replaying" });
265
295
  }
266
296
  }
267
297
  const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
@@ -278,7 +308,7 @@ async function _replayTier(items, store, result) {
278
308
  }
279
309
  }
280
310
  async function _doReplayQueue(store) {
281
- const candidates = await idbGetPendingItems();
311
+ const candidates = await qs().getPending();
282
312
  const now = Date.now();
283
313
  const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now);
284
314
  const result = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
@@ -289,7 +319,7 @@ async function _doReplayQueue(store) {
289
319
  return result;
290
320
  }
291
321
  async function clearQueue() {
292
- await idbClearQueue();
322
+ await qs().clear();
293
323
  useEidosStore.getState().hydrateQueue([]);
294
324
  }
295
325
  const C = {