@sweidos/eidos 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,20 +114,22 @@ if ('queued' in result) {
114
114
 
115
115
  ## What you get
116
116
 
117
- | Feature | Description |
118
- | --------------------------- | ------------------------------------------------------------------------------------ |
119
- | **Auto strategy selection** | `offline: true` → StaleWhileRevalidate. No config needed. Override when you want. |
120
- | **Persistent action queue** | Failed writes go to IndexedDB and replay with exponential backoff on reconnect. |
121
- | **Request deduplication** | Concurrent `resource.fetch()` calls share one in-flight request. |
122
- | **Optimistic updates** | `onOptimistic` / `onRollback` callbacks for instant UI feedback. |
123
- | **Conflict resolution** | `onConflict` decides per 4xx whether to retry or drop a queued action. |
124
- | **Queue prioritization** | `priority: 'high' \| 'normal' \| 'low'` — high items replay before normal. |
125
- | **Cache warming** | `warmCache(handles[])` bulk-prefetches resources on login/init. |
126
- | **URL patterns** | `/api/products/*`, `/api/users/:id`, `**` wildcardsSW intercepts all matches. |
127
- | **Background Sync** | Registers a `sync` tag so queued actions replay even after tab close. |
128
- | **Devtools panel** | `<EidosDevtools />` live queue, cache state, offline toggle, no CSS import. |
129
- | **Testing helpers** | `mockOffline`, `drainQueue`, `resetEidos`, `getCachedEntry` for Vitest/Jest. |
130
- | **OpenAPI codegen** | `npx eidos-gen openapi.json` generates typed `resource()` + `action()` declarations. |
117
+ | Feature | Description |
118
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
119
+ | **Auto strategy selection** | `offline: true` → StaleWhileRevalidate. No config needed. Override when you want. |
120
+ | **Persistent action queue** | Failed writes go to IndexedDB and replay with exponential backoff on reconnect. |
121
+ | **Request deduplication** | Concurrent `resource.fetch()` calls share one in-flight request. |
122
+ | **Optimistic updates** | `onOptimistic` / `onRollback` callbacks for instant UI feedback. |
123
+ | **Conflict resolution** | `conflict: { strategy: 'serverWins' \| 'clientWins' \| 'merge' \| 'custom' }` on 4xx replay responses. |
124
+ | **Idempotent replay** | Stable `idempotencyKey` per invocation, forwarded to `fn` via `ActionContext` safe retries even after a dropped response. |
125
+ | **Cancellable actions** | `cancellable: true` `AbortSignal` per call, plus `handle.cancel(idempotencyKey)`. |
126
+ | **Queue prioritization** | `priority: 'high' \| 'normal' \| 'low'` high items replay before normal. |
127
+ | **Cache warming** | `warmCache(handles[])` bulk-prefetches resources on login/init. |
128
+ | **URL patterns** | `/api/products/*`, `/api/users/:id`, `**` wildcards SW intercepts all matches. |
129
+ | **Background Sync** | Registers a `sync` tag so queued actions replay even after tab close. |
130
+ | **Devtools panel** | `<EidosDevtools />` live queue, cache state, offline toggle, no CSS import. |
131
+ | **Testing helpers** | `mockOffline`, `drainQueue`, `resetEidos`, `getCachedEntry` for Vitest/Jest. |
132
+ | **OpenAPI codegen** | `npx eidos-gen openapi.json` generates typed `resource()` + `action()` declarations. |
131
133
 
132
134
  ---
133
135
 
@@ -175,20 +177,40 @@ products.query() // { queryKey, queryFn } for useQuery
175
177
  | `offline: true, strategy: 'cache-first'` | CacheFirst | Static assets, config data |
176
178
  | `offline: true, strategy: 'network-first'` | NetworkFirst | Always-fresh with offline fallback |
177
179
 
178
- URL patterns work on any handle: `/api/products/*`, `/api/users/:id`, `**`
180
+ ### `resourcePattern(pattern, config)`
181
+
182
+ For URL patterns — `/api/products/*`, `/api/users/:id`, `**` — the SW intercepts
183
+ all matching requests automatically, so there's no single URL to fetch. Use
184
+ `resourcePattern()` instead of `resource()`; it returns a handle with only
185
+ `invalidate()` and `unregister()`:
186
+
187
+ ```ts
188
+ const productPattern = resourcePattern('/api/products/*', { offline: true });
189
+
190
+ await productPattern.invalidate(); // clear all cached entries matching the pattern
191
+ productPattern.unregister();
192
+ ```
179
193
 
180
194
  ### `action(fn, config)`
181
195
 
182
196
  ```ts
183
- const createOrder = action(async (payload: OrderPayload) => { ... }, {
197
+ const createOrder = action(async (payload: OrderPayload, ctx: ActionContext) => { ... }, {
184
198
  reliability: 'neverLose', // persist to IDB + replay on reconnect
185
199
  name: 'createOrder', // stable name for post-reload replay
200
+ namespace?: string, // prefix actionId — avoids collisions across modules
186
201
  maxRetries?: number, // default: 3
187
202
  priority?: 'high' | 'normal' | 'low',
203
+ cancellable?: boolean, // adds AbortSignal to ctx, enables handle.cancel(key)
188
204
  onOptimistic?: (...args) => void, // instant UI update
189
205
  onRollback?: (...args) => void, // revert on permanent failure
190
- onConflict?: (error, args) => 'retry' | 'skip', // 4xx handler
206
+ conflict?: { // 4xx replay handling
207
+ strategy: 'serverWins' | 'clientWins' | 'merge' | 'custom',
208
+ resolve?: (ctx) => 'retry' | 'skip' | { resolved: args },
209
+ },
191
210
  })
211
+
212
+ // ctx.idempotencyKey is stable across retries — forward as e.g. an
213
+ // `Idempotency-Key` header so the server can dedupe replayed writes.
192
214
  ```
193
215
 
194
216
  ### React hooks
@@ -389,21 +411,19 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
389
411
 
390
412
  ## How it compares
391
413
 
392
- | | **Eidos** | Workbox | RTK Query / TanStack Query |
393
- | -------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------- |
394
- | Service worker setup | Generated for you — `resource()`/`action()` declarations drive the SW | Hand-write `routing` + `strategies` config | None — no SW |
395
- | Caching strategy | Auto-derived from intent (`offline: true` → SWR, etc.), inspectable via devtools | Manually chosen per route | Configurable `staleTime`/`gcTime`, no Cache Storage integration |
396
- | Offline writes | `action()` + `reliability: 'neverLose'` → IndexedDB queue, auto-replay, exponential backoff | Background Sync plugin, you wire the queue | No built-in offline mutation queue |
397
- | Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library (RTK Query = Redux, TanStack = many) |
398
- | TanStack Query bridge | `@sweidos/eidos/query` — drop-in `useEidosQuery`/`useEidosMutation` | — | Native |
399
- | Bundle size (core, brotli) | ~5.4 kB | ~3-6 kB (modular) | ~13 kB (TanStack Query core) |
400
-
401
- Eidos isn't a replacement for TanStack Query — `@sweidos/eidos/query` is a thin
402
- adapter so you keep TQ's cache/devtools while Eidos owns the offline layer
403
- (SW caching + IndexedDB write queue). Workbox is a lower-level toolkit Eidos
404
- generates strategies _for_; Eidos picks and configures the strategy from your
405
- `resource()`/`action()` declarations instead of you writing `workbox-*` config
406
- by hand.
414
+ | | **Eidos** | Workbox | RTK Query / TanStack Query |
415
+ | --------------------- | ----------------------------------------------------- | ---------------------------- | -------------------------- |
416
+ | Service worker setup | Generated from `resource()`/`action()` declarations | Hand-written routing config | None — no SW |
417
+ | Caching strategy | Auto-derived from intent, inspectable via devtools | Manually chosen per route | `staleTime`/`gcTime` only |
418
+ | Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
419
+ | Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
420
+ | TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
421
+ | Bundle size (core) | ~6 kB brotli | ~3-6 kB (modular) | ~13 kB |
422
+
423
+ Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
424
+ you keep TQ's cache/devtools while Eidos owns the offline layer. Workbox is a
425
+ lower-level toolkit; Eidos picks and configures strategies for you instead of
426
+ hand-written `workbox-*` config.
407
427
 
408
428
  ---
409
429
 
package/dist/action.js CHANGED
@@ -1,84 +1,89 @@
1
- import { useEidosStore as y } from "./store.js";
1
+ import { useEidosStore as f } from "./store.js";
2
2
  import { getSwRegistration as _ } from "./sw-bridge.js";
3
- import { idbQueueStorage as S } from "./idb.js";
4
- import { _getQueueStorage as M } from "./queue-storage.js";
5
- var h = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), C = /* @__PURE__ */ new Map(), Q = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map();
6
- function u() {
7
- return M() ?? S;
3
+ import { idbQueueStorage as A } from "./idb.js";
4
+ import { _getQueueStorage as S } from "./queue-storage.js";
5
+ import { broadcastQueueSync as d } from "./queue-sync.js";
6
+ var m = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), Q = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map();
7
+ function c() {
8
+ return S() ?? A;
8
9
  }
9
- function m() {
10
+ function b() {
10
11
  return crypto.randomUUID();
11
12
  }
12
- function v(e, t, i) {
13
- return e(...t, i);
13
+ function h(e, t, o) {
14
+ return e(...t, o);
14
15
  }
15
- function U(e, t) {
16
- const i = t.name || e.name || m(), a = t.namespace ? `${t.namespace}::${i}` : i;
17
- h.set(a, e), x.set(a, t), t.onRollback && k.set(a, t.onRollback), t.onConflict && C.set(a, t.onConflict), t.conflict && Q.set(a, t.conflict);
18
- const c = async (...n) => {
19
- const { isOnline: s } = y.getState(), l = t.reliability === "neverLose" || t.cancellable, o = l ? m() : "";
20
- let d;
16
+ function F(e, t) {
17
+ const o = t.name || e.name || b(), a = t.namespace ? `${t.namespace}::${o}` : o;
18
+ if (m.has(a)) throw new Error(`[eidos] duplicate action id "${a}" an action with this id is already registered. Pass a unique config.name or config.namespace.`);
19
+ m.set(a, e), x.set(a, t), t.onRollback && k.set(a, t.onRollback), t.conflict && Q.set(a, t.conflict);
20
+ const r = async (...n) => {
21
+ const { isOnline: s } = f.getState(), u = b();
22
+ let p;
21
23
  if (t.cancellable) {
22
- const f = new AbortController();
23
- p.set(o, f), d = f.signal;
24
+ const l = new AbortController();
25
+ y.set(u, l), p = l.signal;
24
26
  }
25
- const g = {
26
- idempotencyKey: o,
27
+ const w = {
28
+ idempotencyKey: u,
27
29
  attempt: 0,
28
- signal: d
30
+ signal: p
29
31
  };
30
- t.onOptimistic?.(...n, g);
32
+ t.onOptimistic?.(...n, w);
31
33
  try {
32
34
  if (t.reliability === "neverLose") {
33
- if (!s) return R(a, a, n, t, o);
35
+ if (!s) return R(a, a, n, t, u);
34
36
  try {
35
- return await v(e, n, g);
36
- } catch (f) {
37
- if (A(f)) throw f;
38
- return R(a, a, n, t, o);
37
+ return await h(e, n, w);
38
+ } catch (l) {
39
+ if (C(l)) throw l;
40
+ return R(a, a, n, t, u);
39
41
  }
40
42
  }
41
43
  try {
42
- return l ? await v(e, n, g) : await e(...n);
43
- } catch (f) {
44
- throw t.onRollback?.(...n), f;
44
+ return await h(e, n, w);
45
+ } catch (l) {
46
+ throw t.onRollback?.(...n, w), l;
45
47
  }
46
48
  } finally {
47
- t.cancellable && p.delete(o);
49
+ t.cancellable && y.delete(u);
48
50
  }
49
- }, r = async (n) => {
50
- const s = p.get(n);
51
+ }, i = async (n) => {
52
+ const s = y.get(n);
51
53
  if (s)
52
54
  return s.abort(), !0;
53
- const l = (await u().getAll()).find((o) => o.idempotencyKey === n && o.status === "pending");
54
- return l ? (y.getState().removeQueueItem(l.id), await u().remove(l.id), !0) : !1;
55
+ const u = (await c().getAll()).find((p) => p.idempotencyKey === n && p.status === "pending");
56
+ return u ? (f.getState().removeQueueItem(u.id), d({
57
+ type: "remove",
58
+ id: u.id
59
+ }), await c().remove(u.id), !0) : !1;
55
60
  };
56
- return Object.defineProperty(c, "id", {
61
+ return Object.defineProperty(r, "id", {
57
62
  value: a,
58
63
  writable: !1
59
- }), Object.defineProperty(c, "config", {
64
+ }), Object.defineProperty(r, "config", {
60
65
  value: t,
61
66
  writable: !1
62
- }), Object.defineProperty(c, "cancel", {
63
- value: r,
67
+ }), Object.defineProperty(r, "cancel", {
68
+ value: i,
64
69
  writable: !1
65
- }), c;
70
+ }), r;
66
71
  }
67
- async function R(e, t, i, a, c) {
68
- const r = m(), n = {
72
+ async function R(e, t, o, a, r) {
73
+ const i = b(), n = {
69
74
  schemaVersion: 2,
70
- id: r,
75
+ id: i,
71
76
  actionId: e,
72
77
  actionName: t,
73
- idempotencyKey: c,
74
- args: i,
78
+ idempotencyKey: r,
79
+ args: o,
75
80
  queuedAt: Date.now(),
76
81
  retryCount: 0,
77
82
  maxRetries: a.maxRetries ?? 3,
78
83
  status: "pending",
79
84
  priority: a.priority ?? "normal"
80
85
  };
81
- await u().add(n), y.getState().addQueueItem(n);
86
+ await c().add(n), f.getState().addQueueItem(n);
82
87
  try {
83
88
  const s = _();
84
89
  s && "sync" in s && await s.sync.register("eidos-queue-replay");
@@ -86,11 +91,11 @@ async function R(e, t, i, a, c) {
86
91
  }
87
92
  return {
88
93
  queued: !0,
89
- id: r,
94
+ id: i,
90
95
  message: `"${t}" queued — will execute when online`
91
96
  };
92
97
  }
93
- function A(e) {
98
+ function C(e) {
94
99
  return e instanceof DOMException && e.name === "AbortError";
95
100
  }
96
101
  function K(e) {
@@ -101,10 +106,10 @@ function K(e) {
101
106
  }
102
107
  return !1;
103
108
  }
104
- function O(e) {
109
+ function M(e) {
105
110
  return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
106
111
  }
107
- function w() {
112
+ function g() {
108
113
  return {
109
114
  attempted: 0,
110
115
  succeeded: 0,
@@ -115,138 +120,171 @@ function w() {
115
120
  cancelled: 0
116
121
  };
117
122
  }
118
- var b = !1, q = "eidos-queue-replay";
119
- async function $() {
120
- const e = y.getState();
121
- if (!e.isOnline) return w();
122
- if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(q, { ifAvailable: !0 }, async (t) => t ? I(e) : w());
123
- if (b) return w();
124
- b = !0;
123
+ var v = !1, O = "eidos-queue-replay";
124
+ async function V() {
125
+ const e = f.getState();
126
+ if (!e.isOnline) return g();
127
+ if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(O, { ifAvailable: !0 }, async (t) => t ? I(e) : g());
128
+ if (v) return g();
129
+ v = !0;
125
130
  try {
126
131
  return await I(e);
127
132
  } finally {
128
- b = !1;
133
+ v = !1;
129
134
  }
130
135
  }
131
- async function E(e, t) {
132
- const i = h.get(e.actionId);
133
- if (!i) return "skipped";
136
+ async function q(e, t) {
137
+ const o = Date.now();
138
+ t.updateQueueItem(e.id, {
139
+ status: "succeeded",
140
+ completedAt: o
141
+ }), d({
142
+ type: "update",
143
+ id: e.id,
144
+ update: {
145
+ status: "succeeded",
146
+ completedAt: o
147
+ }
148
+ }), await c().update(e.id, {
149
+ status: "succeeded",
150
+ completedAt: o
151
+ }), setTimeout(() => {
152
+ t.removeQueueItem(e.id), d({
153
+ type: "remove",
154
+ id: e.id
155
+ }), c().remove(e.id);
156
+ }, 3e3);
157
+ }
158
+ async function E(e, t, o) {
159
+ const a = Q.get(e.actionId);
160
+ let r;
161
+ if (a) switch (a.strategy) {
162
+ case "serverWins":
163
+ r = "skip";
164
+ break;
165
+ case "clientWins":
166
+ r = "retry";
167
+ break;
168
+ case "merge":
169
+ case "custom": {
170
+ const i = {
171
+ error: o,
172
+ args: e.args,
173
+ attempt: e.retryCount,
174
+ idempotencyKey: e.idempotencyKey
175
+ };
176
+ r = a.resolve?.(i) ?? "retry";
177
+ break;
178
+ }
179
+ }
180
+ if (r === "skip")
181
+ return t.removeQueueItem(e.id), d({
182
+ type: "remove",
183
+ id: e.id
184
+ }), await c().remove(e.id), "conflicted";
185
+ r && typeof r == "object" && (e.args = r.resolved, t.updateQueueItem(e.id, { args: r.resolved }), d({
186
+ type: "update",
187
+ id: e.id,
188
+ update: { args: r.resolved }
189
+ }), await c().update(e.id, { args: r.resolved }));
190
+ }
191
+ async function P(e, t, o) {
192
+ const a = e.retryCount + 1;
193
+ if (a >= e.maxRetries) {
194
+ const i = {
195
+ status: "failed",
196
+ error: String(o),
197
+ retryCount: a
198
+ };
199
+ t.updateQueueItem(e.id, i), d({
200
+ type: "update",
201
+ id: e.id,
202
+ update: i
203
+ }), await c().update(e.id, i);
204
+ const n = {
205
+ idempotencyKey: e.idempotencyKey,
206
+ attempt: a
207
+ };
208
+ return k.get(e.actionId)?.(...e.args, n), "failed";
209
+ }
210
+ const r = {
211
+ status: "pending",
212
+ retryCount: a,
213
+ nextRetryAt: Date.now() + M(a)
214
+ };
215
+ return t.updateQueueItem(e.id, r), d({
216
+ type: "update",
217
+ id: e.id,
218
+ update: r
219
+ }), await c().update(e.id, r), "retrying";
220
+ }
221
+ async function D(e, t) {
222
+ const o = m.get(e.actionId);
223
+ if (!o) return "skipped";
134
224
  const a = x.get(e.actionId)?.cancellable;
135
- let c;
225
+ let r;
136
226
  if (a) {
137
227
  const n = new AbortController();
138
- p.set(e.idempotencyKey, n), c = n.signal;
228
+ y.set(e.idempotencyKey, n), r = n.signal;
139
229
  }
140
- const r = {
230
+ const i = {
141
231
  idempotencyKey: e.idempotencyKey,
142
232
  attempt: e.retryCount,
143
- signal: c
233
+ signal: r
144
234
  };
145
235
  try {
146
- await v(i, e.args, r);
147
- const n = Date.now();
148
- return t.updateQueueItem(e.id, {
149
- status: "succeeded",
150
- completedAt: n
151
- }), await u().update(e.id, {
152
- status: "succeeded",
153
- completedAt: n
154
- }), setTimeout(() => {
155
- t.removeQueueItem(e.id), u().remove(e.id);
156
- }, 3e3), "succeeded";
236
+ return await h(o, e.args, i), await q(e, t), "succeeded";
157
237
  } catch (n) {
158
- if (A(n))
159
- return t.removeQueueItem(e.id), await u().remove(e.id), "cancelled";
238
+ if (C(n))
239
+ return t.removeQueueItem(e.id), d({
240
+ type: "remove",
241
+ id: e.id
242
+ }), await c().remove(e.id), "cancelled";
160
243
  if (K(n)) {
161
- const l = Q.get(e.actionId);
162
- let o;
163
- if (l) switch (l.strategy) {
164
- case "serverWins":
165
- o = "skip";
166
- break;
167
- case "clientWins":
168
- case "lastWriteWins":
169
- o = "retry";
170
- break;
171
- case "merge":
172
- case "custom": {
173
- const d = {
174
- error: n,
175
- args: e.args,
176
- attempt: e.retryCount,
177
- idempotencyKey: e.idempotencyKey
178
- };
179
- o = l.resolve?.(d) ?? "retry";
180
- break;
181
- }
182
- }
183
- else {
184
- const d = C.get(e.actionId);
185
- d && (o = d(n, e.args));
186
- }
187
- if (o === "skip")
188
- return t.removeQueueItem(e.id), await u().remove(e.id), "conflicted";
189
- o && typeof o == "object" && (e.args = o.resolved, t.updateQueueItem(e.id, { args: o.resolved }), await u().update(e.id, { args: o.resolved }));
190
- }
191
- const s = e.retryCount + 1;
192
- if (s >= e.maxRetries)
193
- return t.updateQueueItem(e.id, {
194
- status: "failed",
195
- error: String(n),
196
- retryCount: s
197
- }), await u().update(e.id, {
198
- status: "failed",
199
- error: String(n),
200
- retryCount: s
201
- }), k.get(e.actionId)?.(...e.args), "failed";
202
- {
203
- const l = Date.now() + O(s);
204
- return t.updateQueueItem(e.id, {
205
- status: "pending",
206
- retryCount: s,
207
- nextRetryAt: l
208
- }), await u().update(e.id, {
209
- status: "pending",
210
- retryCount: s,
211
- nextRetryAt: l
212
- }), "retrying";
244
+ const s = await E(e, t, n);
245
+ if (s) return s;
213
246
  }
247
+ return P(e, t, n);
214
248
  } finally {
215
- a && p.delete(e.idempotencyKey);
249
+ a && y.delete(e.idempotencyKey);
216
250
  }
217
251
  }
218
- async function D(e, t, i) {
252
+ async function j(e, t, o) {
219
253
  if (e.length === 0) return;
220
- const a = e.filter((r) => h.has(r.actionId));
221
- if (i.skipped += e.length - a.length, a.length > 0) {
222
- t.batchUpdateQueueItems(a.map((r) => ({
223
- id: r.id,
254
+ const a = e.filter((i) => m.has(i.actionId));
255
+ if (o.skipped += e.length - a.length, a.length > 0) {
256
+ const i = a.map((n) => ({
257
+ id: n.id,
224
258
  update: { status: "replaying" }
225
- })));
226
- for (const r of a) u().update(r.id, { status: "replaying" });
259
+ }));
260
+ t.batchUpdateQueueItems(i), d({
261
+ type: "batchUpdate",
262
+ updates: i
263
+ });
264
+ for (const n of a) c().update(n.id, { status: "replaying" });
227
265
  }
228
- const c = await Promise.allSettled(a.map((r) => E(r, t)));
229
- for (const r of c) {
230
- const n = r.status === "fulfilled" ? r.value : "failed";
231
- n === "skipped" ? i.skipped++ : n === "conflicted" ? i.conflicted++ : n === "cancelled" ? i.cancelled++ : (i.attempted++, i[n]++);
266
+ const r = await Promise.allSettled(a.map((i) => D(i, t)));
267
+ for (const i of r) {
268
+ const n = i.status === "fulfilled" ? i.value : "failed";
269
+ n === "skipped" ? o.skipped++ : n === "conflicted" ? o.conflicted++ : n === "cancelled" ? o.cancelled++ : (o.attempted++, o[n]++);
232
270
  }
233
271
  }
234
272
  async function I(e) {
235
- const t = await u().getPending(), i = Date.now(), a = t.filter((r) => r.retryCount < r.maxRetries && (!r.nextRetryAt || r.nextRetryAt <= i)), c = w();
236
- for (const r of [
273
+ const t = await c().getPending(), o = Date.now(), a = t.filter((i) => i.retryCount < i.maxRetries && (!i.nextRetryAt || i.nextRetryAt <= o)), r = g();
274
+ for (const i of [
237
275
  "high",
238
276
  "normal",
239
277
  "low"
240
- ]) await D(a.filter((n) => (n.priority ?? "normal") === r), e, c);
241
- return c;
278
+ ]) await j(a.filter((n) => (n.priority ?? "normal") === i), e, r);
279
+ return r;
242
280
  }
243
- async function T() {
244
- await u().clear(), y.getState().hydrateQueue([]);
281
+ async function Y() {
282
+ await c().clear(), f.getState().hydrateQueue([]);
245
283
  }
246
284
  export {
247
- U as action,
248
- T as clearQueue,
249
- $ as replayQueue
285
+ F as action,
286
+ Y as clearQueue,
287
+ V as replayQueue
250
288
  };
251
289
 
252
290
  //# sourceMappingURL=action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.js","names":[],"sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { broadcastQueueSync } from './queue-sync';\nimport type { QueueStorage } from './queue-storage';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type {\n ActionConfig,\n ActionContext,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n ConflictConfig,\n ConflictContext,\n ConflictResolution,\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>();\nconst _conflictConfigRegistry = new Map<string, ConflictConfig>();\nconst _configRegistry = new Map<string, ActionConfig>();\n\n// In-flight AbortControllers for `cancellable` actions, keyed by idempotencyKey.\n// Populated for direct calls and replays alike; removed once the call settles.\nconst _inflightControllers = new Map<string, AbortController>();\n\nfunction qs(): QueueStorage {\n // idbQueueStorage is the default browser fallback when no custom storage is set.\n return _getQueueStorage() ?? idbQueueStorage;\n}\n\nfunction uid() {\n return crypto.randomUUID();\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction callWithContext<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n args: TArgs,\n ctx: ActionContext,\n): Promise<TReturn> {\n return fn(...args, ctx);\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<TArgs>,\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 baseId = config.name || fn.name || uid();\n const actionId = config.namespace ? `${config.namespace}::${baseId}` : baseId;\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 if (_actionRegistry.has(actionId)) {\n throw new Error(\n `[eidos] duplicate action id \"${actionId}\" — an action with this id is already registered. Pass a unique config.name or config.namespace.`,\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 _configRegistry.set(actionId, config as ActionConfig);\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback);\n }\n\n if (config.conflict) {\n if (\n import.meta.env.DEV &&\n (config.conflict.strategy === 'merge' || config.conflict.strategy === 'custom') &&\n !config.conflict.resolve\n ) {\n console.error(\n `[eidos] action \"${actionId}\" has conflict.strategy \"${config.conflict.strategy}\" but no resolve() — items will retry indefinitely on 4xx.`,\n );\n }\n _conflictConfigRegistry.set(actionId, config.conflict);\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState();\n\n // Generated for every invocation — reused across every retry/replay of a\n // neverLose item, and used to key handle.cancel() for in-flight cancellable calls.\n const idempotencyKey = uid();\n\n let signal: AbortSignal | undefined;\n if (config.cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = { idempotencyKey, attempt: 0, signal };\n\n config.onOptimistic?.(...args, ctx);\n\n try {\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n if (isAbortError(err)) throw err;\n return persistAndQueue(actionId, actionId, args, config, idempotencyKey);\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await callWithContext(fn, args, ctx);\n } catch (err) {\n config.onRollback?.(...args, ctx);\n throw err;\n }\n } finally {\n if (config.cancellable) _inflightControllers.delete(idempotencyKey);\n }\n };\n\n const cancel = async (idempotencyKey: string): Promise<boolean> => {\n const controller = _inflightControllers.get(idempotencyKey);\n if (controller) {\n controller.abort();\n return true;\n }\n\n // Not in flight — check for a not-yet-replayed queued item with this key.\n const items = await qs().getAll();\n const item = items.find((i) => i.idempotencyKey === idempotencyKey && i.status === 'pending');\n if (!item) return false;\n\n useEidosStore.getState().removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return true;\n };\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false });\n Object.defineProperty(wrapped, 'config', { value: config, writable: false });\n Object.defineProperty(wrapped, 'cancel', { value: cancel, 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\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function persistAndQueue<TArgs extends any[]>(\n actionId: string,\n actionName: string,\n args: TArgs,\n config: ActionConfig<TArgs>,\n idempotencyKey: string,\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 schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n id,\n actionId,\n actionName,\n idempotencyKey,\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(\n 'eidos-queue-replay',\n );\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 isAbortError(err: unknown): boolean {\n return err instanceof DOMException && err.name === 'AbortError';\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\nfunction emptyReplayResult(): ReplayResult {\n return {\n attempted: 0,\n succeeded: 0,\n failed: 0,\n retrying: 0,\n skipped: 0,\n conflicted: 0,\n cancelled: 0,\n };\n}\n\nlet _replaying = false;\nconst REPLAY_LOCK_NAME = 'eidos-queue-replay';\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState();\n if (!store.isOnline) return emptyReplayResult();\n\n // Web Locks coordinate replay across tabs sharing the same IndexedDB queue —\n // only the lock holder replays; other tabs no-op rather than re-executing\n // the same queued actions in parallel.\n if (typeof navigator !== 'undefined' && navigator.locks) {\n return navigator.locks.request(REPLAY_LOCK_NAME, { ifAvailable: true }, async (lock) => {\n if (!lock) return emptyReplayResult();\n return _doReplayQueue(store);\n });\n }\n\n // Fallback for environments without the Web Locks API (older Safari, React\n // Native, test runners) — guards against concurrent replay within this tab only.\n if (_replaying) return emptyReplayResult();\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' | 'cancelled';\n\nasync function _markSucceeded(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<void> {\n const completedAt = Date.now();\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt });\n broadcastQueueSync({ type: 'update', id: item.id, update: { 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 broadcastQueueSync({ type: 'remove', id: item.id });\n qs().remove(item.id);\n }, 3000);\n}\n\n/**\n * Resolves a 4xx error against the action's conflict strategy.\n * Returns 'conflicted' if the item was dropped, undefined if normal\n * retry/fail logic should run (possibly with `item.args` rewritten by `merge`).\n */\nasync function _resolveConflict(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome | undefined> {\n const conflictConfig = _conflictConfigRegistry.get(item.actionId);\n let resolution: ConflictResolution | undefined;\n\n if (conflictConfig) {\n switch (conflictConfig.strategy) {\n case 'serverWins':\n resolution = 'skip';\n break;\n case 'clientWins':\n resolution = 'retry';\n break;\n case 'merge':\n case 'custom': {\n const ctx: ConflictContext = {\n error: err,\n args: item.args as unknown[],\n attempt: item.retryCount,\n idempotencyKey: item.idempotencyKey,\n };\n resolution = conflictConfig.resolve?.(ctx) ?? 'retry';\n break;\n }\n }\n }\n\n if (resolution === 'skip') {\n store.removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'conflicted';\n }\n if (resolution && typeof resolution === 'object') {\n item.args = resolution.resolved;\n store.updateQueueItem(item.id, { args: resolution.resolved });\n broadcastQueueSync({ type: 'update', id: item.id, update: { args: resolution.resolved } });\n await qs().update(item.id, { args: resolution.resolved });\n }\n // 'retry' (or merged args) falls through to normal retry/fail logic\n return undefined;\n}\n\nasync function _scheduleRetryOrFail(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n err: unknown,\n): Promise<ItemOutcome> {\n const retryCount = item.retryCount + 1;\n if (retryCount >= item.maxRetries) {\n const update = { status: 'failed' as const, error: String(err), retryCount };\n store.updateQueueItem(item.id, update);\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n const ctx: ActionContext = { idempotencyKey: item.idempotencyKey, attempt: retryCount };\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]), ctx);\n return 'failed';\n }\n\n const nextRetryAt = Date.now() + backoffMs(retryCount);\n const update = { status: 'pending' as const, retryCount, nextRetryAt };\n store.updateQueueItem(item.id, update);\n broadcastQueueSync({ type: 'update', id: item.id, update });\n await qs().update(item.id, update);\n return 'retrying';\n}\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 const cancellable = _configRegistry.get(item.actionId)?.cancellable;\n let signal: AbortSignal | undefined;\n if (cancellable) {\n const controller = new AbortController();\n _inflightControllers.set(item.idempotencyKey, controller);\n signal = controller.signal;\n }\n\n const ctx: ActionContext = {\n idempotencyKey: item.idempotencyKey,\n attempt: item.retryCount,\n signal,\n };\n\n try {\n await callWithContext(fn, item.args as unknown[], ctx);\n await _markSucceeded(item, store);\n return 'succeeded';\n } catch (err) {\n // Cancelled via handle.cancel(idempotencyKey) — drop the item, no rollback/retry.\n if (isAbortError(err)) {\n store.removeQueueItem(item.id);\n broadcastQueueSync({ type: 'remove', id: item.id });\n await qs().remove(item.id);\n return 'cancelled';\n }\n\n // 4xx: give the conflict strategy a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const outcome = await _resolveConflict(item, store, err);\n if (outcome) return outcome;\n }\n\n return _scheduleRetryOrFail(item, store, err);\n } finally {\n if (cancellable) _inflightControllers.delete(item.idempotencyKey);\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 const updates = replayable.map((item) => ({\n id: item.id,\n update: { status: 'replaying' as const },\n }));\n store.batchUpdateQueueItems(updates);\n broadcastQueueSync({ type: 'batchUpdate', updates });\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') {\n result.skipped++;\n } else if (outcome === 'conflicted') {\n result.conflicted++;\n } else if (outcome === 'cancelled') {\n result.cancelled++;\n } else {\n result.attempted++;\n result[outcome]++;\n }\n }\n}\n\nasync function _doReplayQueue(\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ReplayResult> {\n const candidates = await qs().getPending();\n const now = Date.now();\n // getPending() includes 'failed' items (for UI/queue-stats visibility), but\n // items that already exhausted maxRetries must not be auto-replayed again —\n // otherwise every reconnect re-executes the action and re-fires onRollback.\n // Those items stay 'failed' until the host app explicitly clears/re-queues them.\n const pending = candidates.filter(\n (item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now),\n );\n\n const result: ReplayResult = emptyReplayResult();\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"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage-adapter.js","names":[],"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"],"mappings":"AAUA,IAAM,IAAY,gBAOL,IAAb,MAA8D;AAAA,EAC5D,YAAY,GAA4C;AAA3B,SAAA,UAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAM,IAAM,MAAM,KAAK,QAAQ,QAAQ,CAAS;AAChD,aAAK,IACE,KAAK,MAAM,CAAG,IADJ,CAAC;AAAA,IAEpB,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAc,SAAS,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQ,GAAW,KAAK,UAAU,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAI,GAAsC;AAC9C,UAAM,IAAQ,MAAM,KAAK,QAAQ;AACjC,IAAA,EAAM,KAAK,CAAI,GACf,MAAM,KAAK,SAAS,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,MAAM,aAAyC;AAE7C,YAAO,MADa,KAAK,QAAQ,GACpB,OAAA,CAAQ,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAO,GAAY,GAAgD;AACvE,UAAM,IAAQ,MAAM,KAAK,QAAQ,GAC3B,IAAM,EAAM,UAAA,CAAW,MAAM,EAAE,OAAO,CAAE;AAC9C,IAAI,MAAQ,OAAI,EAAM,CAAA,IAAO;AAAA,MAAE,GAAG,EAAM,CAAA;AAAA,MAAM,GAAG;AAAA,IAAM,IACvD,MAAM,KAAK,SAAS,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAO,GAA2B;AACtC,UAAM,IAAQ,MAAM,KAAK,QAAQ;AACjC,UAAM,KAAK,SAAS,EAAM,OAAA,CAAQ,MAAM,EAAE,OAAO,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAW,CAAS;AAAA,EACzC;AACF"}