@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 +52 -32
- package/dist/action.js +188 -150
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.js +162 -85
- package/dist/eidos-sw.js +8 -5
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +74 -35
- package/dist/index.js +33 -32
- package/dist/push.cjs +8 -5
- package/dist/push.js +8 -5
- package/dist/query.cjs +1 -1
- package/dist/query.d.ts +1 -2
- package/dist/query.js +1 -1
- package/dist/queue-storage.js.map +1 -0
- package/dist/queue-sync.js +34 -0
- package/dist/queue-sync.js.map +1 -0
- package/dist/react/Provider.js.map +1 -0
- package/dist/react/hooks.js +23 -23
- package/dist/react/hooks.js.map +1 -0
- package/dist/replay.js.map +1 -0
- package/dist/resource.js +121 -107
- package/dist/resource.js.map +1 -0
- package/dist/runtime.js +23 -22
- package/dist/runtime.js.map +1 -0
- package/dist/store-slices.js.map +1 -0
- package/dist/store.js.map +1 -0
- package/dist/stores.js +23 -31
- package/dist/stores.js.map +1 -0
- package/dist/sw-bridge.js.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -0
- package/package.json +2 -3
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** | `
|
|
124
|
-
| **
|
|
125
|
-
| **
|
|
126
|
-
| **
|
|
127
|
-
| **
|
|
128
|
-
| **
|
|
129
|
-
| **
|
|
130
|
-
| **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
393
|
-
|
|
|
394
|
-
| Service worker setup
|
|
395
|
-
| Caching strategy
|
|
396
|
-
| Offline writes
|
|
397
|
-
| Framework support
|
|
398
|
-
| TanStack Query bridge
|
|
399
|
-
| Bundle size (core
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
1
|
+
import { useEidosStore as f } from "./store.js";
|
|
2
2
|
import { getSwRegistration as _ } from "./sw-bridge.js";
|
|
3
|
-
import { idbQueueStorage as
|
|
4
|
-
import { _getQueueStorage as
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
10
|
+
function b() {
|
|
10
11
|
return crypto.randomUUID();
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
-
return e(...t,
|
|
13
|
+
function h(e, t, o) {
|
|
14
|
+
return e(...t, o);
|
|
14
15
|
}
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
23
|
-
|
|
24
|
+
const l = new AbortController();
|
|
25
|
+
y.set(u, l), p = l.signal;
|
|
24
26
|
}
|
|
25
|
-
const
|
|
26
|
-
idempotencyKey:
|
|
27
|
+
const w = {
|
|
28
|
+
idempotencyKey: u,
|
|
27
29
|
attempt: 0,
|
|
28
|
-
signal:
|
|
30
|
+
signal: p
|
|
29
31
|
};
|
|
30
|
-
t.onOptimistic?.(...n,
|
|
32
|
+
t.onOptimistic?.(...n, w);
|
|
31
33
|
try {
|
|
32
34
|
if (t.reliability === "neverLose") {
|
|
33
|
-
if (!s) return R(a, a, n, t,
|
|
35
|
+
if (!s) return R(a, a, n, t, u);
|
|
34
36
|
try {
|
|
35
|
-
return await
|
|
36
|
-
} catch (
|
|
37
|
-
if (
|
|
38
|
-
return R(a, a, n, t,
|
|
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
|
|
43
|
-
} catch (
|
|
44
|
-
throw t.onRollback?.(...n),
|
|
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 &&
|
|
49
|
+
t.cancellable && y.delete(u);
|
|
48
50
|
}
|
|
49
|
-
},
|
|
50
|
-
const s =
|
|
51
|
+
}, i = async (n) => {
|
|
52
|
+
const s = y.get(n);
|
|
51
53
|
if (s)
|
|
52
54
|
return s.abort(), !0;
|
|
53
|
-
const
|
|
54
|
-
return
|
|
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(
|
|
61
|
+
return Object.defineProperty(r, "id", {
|
|
57
62
|
value: a,
|
|
58
63
|
writable: !1
|
|
59
|
-
}), Object.defineProperty(
|
|
64
|
+
}), Object.defineProperty(r, "config", {
|
|
60
65
|
value: t,
|
|
61
66
|
writable: !1
|
|
62
|
-
}), Object.defineProperty(
|
|
63
|
-
value:
|
|
67
|
+
}), Object.defineProperty(r, "cancel", {
|
|
68
|
+
value: i,
|
|
64
69
|
writable: !1
|
|
65
|
-
}),
|
|
70
|
+
}), r;
|
|
66
71
|
}
|
|
67
|
-
async function R(e, t,
|
|
68
|
-
const
|
|
72
|
+
async function R(e, t, o, a, r) {
|
|
73
|
+
const i = b(), n = {
|
|
69
74
|
schemaVersion: 2,
|
|
70
|
-
id:
|
|
75
|
+
id: i,
|
|
71
76
|
actionId: e,
|
|
72
77
|
actionName: t,
|
|
73
|
-
idempotencyKey:
|
|
74
|
-
args:
|
|
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
|
|
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:
|
|
94
|
+
id: i,
|
|
90
95
|
message: `"${t}" queued — will execute when online`
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
|
-
function
|
|
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
|
|
109
|
+
function M(e) {
|
|
105
110
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
106
111
|
}
|
|
107
|
-
function
|
|
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
|
|
119
|
-
async function
|
|
120
|
-
const e =
|
|
121
|
-
if (!e.isOnline) return
|
|
122
|
-
if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(
|
|
123
|
-
if (
|
|
124
|
-
|
|
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
|
-
|
|
133
|
+
v = !1;
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
|
-
async function
|
|
132
|
-
const
|
|
133
|
-
|
|
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
|
|
225
|
+
let r;
|
|
136
226
|
if (a) {
|
|
137
227
|
const n = new AbortController();
|
|
138
|
-
|
|
228
|
+
y.set(e.idempotencyKey, n), r = n.signal;
|
|
139
229
|
}
|
|
140
|
-
const
|
|
230
|
+
const i = {
|
|
141
231
|
idempotencyKey: e.idempotencyKey,
|
|
142
232
|
attempt: e.retryCount,
|
|
143
|
-
signal:
|
|
233
|
+
signal: r
|
|
144
234
|
};
|
|
145
235
|
try {
|
|
146
|
-
await
|
|
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 (
|
|
159
|
-
return t.removeQueueItem(e.id),
|
|
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
|
|
162
|
-
|
|
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 &&
|
|
249
|
+
a && y.delete(e.idempotencyKey);
|
|
216
250
|
}
|
|
217
251
|
}
|
|
218
|
-
async function
|
|
252
|
+
async function j(e, t, o) {
|
|
219
253
|
if (e.length === 0) return;
|
|
220
|
-
const a = e.filter((
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
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
|
-
|
|
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
|
|
229
|
-
for (const
|
|
230
|
-
const n =
|
|
231
|
-
n === "skipped" ?
|
|
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
|
|
236
|
-
for (const
|
|
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
|
|
241
|
-
return
|
|
278
|
+
]) await j(a.filter((n) => (n.priority ?? "normal") === i), e, r);
|
|
279
|
+
return r;
|
|
242
280
|
}
|
|
243
|
-
async function
|
|
244
|
-
await
|
|
281
|
+
async function Y() {
|
|
282
|
+
await c().clear(), f.getState().hydrateQueue([]);
|
|
245
283
|
}
|
|
246
284
|
export {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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"}
|