@sweidos/eidos 2.0.0 → 2.1.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 +11 -1
- package/dist/action.js +147 -129
- package/dist/action.js.map +1 -1
- package/dist/devtools.js +85 -1
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +32 -30
- package/dist/resource.js +22 -22
- package/dist/resource.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,9 @@ const products = resource('/api/products', {
|
|
|
160
160
|
strategy?: 'cache-first' | 'stale-while-revalidate' | 'network-first',
|
|
161
161
|
cacheName?: string, // custom cache bucket
|
|
162
162
|
maxAge?: number, // TTL in ms — re-fetch after expiry
|
|
163
|
+
version?: string | number, // bump when the response shape changes —
|
|
164
|
+
// appended to cacheName (e.g. 'eidos-resources-v1-v2')
|
|
165
|
+
// so old-shaped cache entries aren't served
|
|
163
166
|
})
|
|
164
167
|
|
|
165
168
|
await products.fetch() // Promise<Response>
|
|
@@ -211,6 +214,13 @@ const createOrder = action(async (payload: OrderPayload, ctx: ActionContext) =>
|
|
|
211
214
|
|
|
212
215
|
// ctx.idempotencyKey is stable across retries — forward as e.g. an
|
|
213
216
|
// `Idempotency-Key` header so the server can dedupe replayed writes.
|
|
217
|
+
|
|
218
|
+
// Module-level helpers (used by the devtools queue inspector, and usable
|
|
219
|
+
// directly): cancel/remove a queue item by idempotency key, or reset a
|
|
220
|
+
// 'failed' item back to 'pending' for the next replayQueue().
|
|
221
|
+
import { cancelByIdempotencyKey, requeueItem } from '@sweidos/eidos'
|
|
222
|
+
await cancelByIdempotencyKey(idempotencyKey) // true if cancelled/removed
|
|
223
|
+
await requeueItem(queueItemId) // true if it was 'failed'
|
|
214
224
|
```
|
|
215
225
|
|
|
216
226
|
### React hooks
|
|
@@ -418,7 +428,7 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
|
|
|
418
428
|
| Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
|
|
419
429
|
| Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
|
|
420
430
|
| TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
|
|
421
|
-
| Bundle size (core) | ~6 kB brotli
|
|
431
|
+
| Bundle size (core) | ~6.3 kB brotli | ~3-6 kB (modular) | ~13 kB |
|
|
422
432
|
|
|
423
433
|
Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
|
|
424
434
|
you keep TQ's cache/devtools while Eidos owns the offline layer. Workbox is a
|
package/dist/action.js
CHANGED
|
@@ -1,101 +1,117 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbQueueStorage as
|
|
4
|
-
import { _getQueueStorage as
|
|
5
|
-
import { broadcastQueueSync as
|
|
6
|
-
var
|
|
1
|
+
import { useEidosStore as l } from "./store.js";
|
|
2
|
+
import { getSwRegistration as A } from "./sw-bridge.js";
|
|
3
|
+
import { idbQueueStorage as C } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as _ } from "./queue-storage.js";
|
|
5
|
+
import { broadcastQueueSync as u } from "./queue-sync.js";
|
|
6
|
+
var w = /* @__PURE__ */ new Map(), R = /* @__PURE__ */ new Map(), Q = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map();
|
|
7
7
|
function c() {
|
|
8
|
-
return
|
|
8
|
+
return _() ?? C;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function m() {
|
|
11
11
|
return crypto.randomUUID();
|
|
12
12
|
}
|
|
13
|
-
function
|
|
14
|
-
return e(...t,
|
|
13
|
+
function v(e, t, n) {
|
|
14
|
+
return e(...t, n);
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const { isOnline:
|
|
22
|
-
let
|
|
16
|
+
function B(e, t) {
|
|
17
|
+
const n = t.name || e.name || m(), a = t.namespace ? `${t.namespace}::${n}` : n;
|
|
18
|
+
if (w.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
|
+
w.set(a, e), k.set(a, t), t.onRollback && R.set(a, t.onRollback), t.conflict && Q.set(a, t.conflict);
|
|
20
|
+
const i = async (...r) => {
|
|
21
|
+
const { isOnline: o } = l.getState(), s = m();
|
|
22
|
+
let b;
|
|
23
23
|
if (t.cancellable) {
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const d = new AbortController();
|
|
25
|
+
p.set(s, d), b = d.signal;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
idempotencyKey:
|
|
27
|
+
const y = {
|
|
28
|
+
idempotencyKey: s,
|
|
29
29
|
attempt: 0,
|
|
30
|
-
signal:
|
|
30
|
+
signal: b
|
|
31
31
|
};
|
|
32
|
-
t.onOptimistic?.(...
|
|
32
|
+
t.onOptimistic?.(...r, y);
|
|
33
33
|
try {
|
|
34
34
|
if (t.reliability === "neverLose") {
|
|
35
|
-
if (!
|
|
35
|
+
if (!o) return h(a, a, r, t, s);
|
|
36
36
|
try {
|
|
37
|
-
return await
|
|
38
|
-
} catch (
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
37
|
+
return await v(e, r, y);
|
|
38
|
+
} catch (d) {
|
|
39
|
+
if (x(d)) throw d;
|
|
40
|
+
return h(a, a, r, t, s);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
return await
|
|
45
|
-
} catch (
|
|
46
|
-
throw t.onRollback?.(...
|
|
44
|
+
return await v(e, r, y);
|
|
45
|
+
} catch (d) {
|
|
46
|
+
throw t.onRollback?.(...r, y), d;
|
|
47
47
|
}
|
|
48
48
|
} finally {
|
|
49
|
-
t.cancellable &&
|
|
49
|
+
t.cancellable && p.delete(s);
|
|
50
50
|
}
|
|
51
|
-
}, i = async (n) => {
|
|
52
|
-
const s = y.get(n);
|
|
53
|
-
if (s)
|
|
54
|
-
return s.abort(), !0;
|
|
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;
|
|
60
51
|
};
|
|
61
|
-
return Object.defineProperty(
|
|
52
|
+
return Object.defineProperty(i, "id", {
|
|
62
53
|
value: a,
|
|
63
54
|
writable: !1
|
|
64
|
-
}), Object.defineProperty(
|
|
55
|
+
}), Object.defineProperty(i, "config", {
|
|
65
56
|
value: t,
|
|
66
57
|
writable: !1
|
|
67
|
-
}), Object.defineProperty(
|
|
68
|
-
value:
|
|
58
|
+
}), Object.defineProperty(i, "cancel", {
|
|
59
|
+
value: S,
|
|
69
60
|
writable: !1
|
|
70
|
-
}),
|
|
61
|
+
}), i;
|
|
62
|
+
}
|
|
63
|
+
async function S(e) {
|
|
64
|
+
const t = p.get(e);
|
|
65
|
+
if (t)
|
|
66
|
+
return t.abort(), !0;
|
|
67
|
+
const n = (await c().getAll()).find((a) => a.idempotencyKey === e && a.status === "pending");
|
|
68
|
+
return n ? (l.getState().removeQueueItem(n.id), u({
|
|
69
|
+
type: "remove",
|
|
70
|
+
id: n.id
|
|
71
|
+
}), await c().remove(n.id), !0) : !1;
|
|
72
|
+
}
|
|
73
|
+
async function F(e) {
|
|
74
|
+
const t = (await c().getAll()).find((a) => a.id === e);
|
|
75
|
+
if (!t || t.status !== "failed") return !1;
|
|
76
|
+
const n = {
|
|
77
|
+
status: "pending",
|
|
78
|
+
error: void 0,
|
|
79
|
+
nextRetryAt: void 0,
|
|
80
|
+
retryCount: 0
|
|
81
|
+
};
|
|
82
|
+
return l.getState().updateQueueItem(e, n), u({
|
|
83
|
+
type: "update",
|
|
84
|
+
id: e,
|
|
85
|
+
update: n
|
|
86
|
+
}), await c().update(e, n), !0;
|
|
71
87
|
}
|
|
72
|
-
async function
|
|
73
|
-
const
|
|
88
|
+
async function h(e, t, n, a, i) {
|
|
89
|
+
const r = m(), o = {
|
|
74
90
|
schemaVersion: 2,
|
|
75
|
-
id:
|
|
91
|
+
id: r,
|
|
76
92
|
actionId: e,
|
|
77
93
|
actionName: t,
|
|
78
|
-
idempotencyKey:
|
|
79
|
-
args:
|
|
94
|
+
idempotencyKey: i,
|
|
95
|
+
args: n,
|
|
80
96
|
queuedAt: Date.now(),
|
|
81
97
|
retryCount: 0,
|
|
82
98
|
maxRetries: a.maxRetries ?? 3,
|
|
83
99
|
status: "pending",
|
|
84
100
|
priority: a.priority ?? "normal"
|
|
85
101
|
};
|
|
86
|
-
await c().add(
|
|
102
|
+
await c().add(o), l.getState().addQueueItem(o);
|
|
87
103
|
try {
|
|
88
|
-
const s =
|
|
104
|
+
const s = A();
|
|
89
105
|
s && "sync" in s && await s.sync.register("eidos-queue-replay");
|
|
90
106
|
} catch {
|
|
91
107
|
}
|
|
92
108
|
return {
|
|
93
109
|
queued: !0,
|
|
94
|
-
id:
|
|
110
|
+
id: r,
|
|
95
111
|
message: `"${t}" queued — will execute when online`
|
|
96
112
|
};
|
|
97
113
|
}
|
|
98
|
-
function
|
|
114
|
+
function x(e) {
|
|
99
115
|
return e instanceof DOMException && e.name === "AbortError";
|
|
100
116
|
}
|
|
101
117
|
function K(e) {
|
|
@@ -109,7 +125,7 @@ function K(e) {
|
|
|
109
125
|
function M(e) {
|
|
110
126
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
111
127
|
}
|
|
112
|
-
function
|
|
128
|
+
function f() {
|
|
113
129
|
return {
|
|
114
130
|
attempted: 0,
|
|
115
131
|
succeeded: 0,
|
|
@@ -120,171 +136,173 @@ function g() {
|
|
|
120
136
|
cancelled: 0
|
|
121
137
|
};
|
|
122
138
|
}
|
|
123
|
-
var
|
|
139
|
+
var g = !1, q = "eidos-queue-replay";
|
|
124
140
|
async function V() {
|
|
125
|
-
const e =
|
|
126
|
-
if (!e.isOnline) return
|
|
127
|
-
if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(
|
|
128
|
-
if (
|
|
129
|
-
|
|
141
|
+
const e = l.getState();
|
|
142
|
+
if (!e.isOnline) return f();
|
|
143
|
+
if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(q, { ifAvailable: !0 }, async (t) => t ? I(e) : f());
|
|
144
|
+
if (g) return f();
|
|
145
|
+
g = !0;
|
|
130
146
|
try {
|
|
131
147
|
return await I(e);
|
|
132
148
|
} finally {
|
|
133
|
-
|
|
149
|
+
g = !1;
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
|
-
async function
|
|
137
|
-
const
|
|
152
|
+
async function O(e, t) {
|
|
153
|
+
const n = Date.now();
|
|
138
154
|
t.updateQueueItem(e.id, {
|
|
139
155
|
status: "succeeded",
|
|
140
|
-
completedAt:
|
|
141
|
-
}),
|
|
156
|
+
completedAt: n
|
|
157
|
+
}), u({
|
|
142
158
|
type: "update",
|
|
143
159
|
id: e.id,
|
|
144
160
|
update: {
|
|
145
161
|
status: "succeeded",
|
|
146
|
-
completedAt:
|
|
162
|
+
completedAt: n
|
|
147
163
|
}
|
|
148
164
|
}), await c().update(e.id, {
|
|
149
165
|
status: "succeeded",
|
|
150
|
-
completedAt:
|
|
166
|
+
completedAt: n
|
|
151
167
|
}), setTimeout(() => {
|
|
152
|
-
t.removeQueueItem(e.id),
|
|
168
|
+
t.removeQueueItem(e.id), u({
|
|
153
169
|
type: "remove",
|
|
154
170
|
id: e.id
|
|
155
171
|
}), c().remove(e.id);
|
|
156
172
|
}, 3e3);
|
|
157
173
|
}
|
|
158
|
-
async function E(e, t,
|
|
174
|
+
async function E(e, t, n) {
|
|
159
175
|
const a = Q.get(e.actionId);
|
|
160
|
-
let
|
|
176
|
+
let i;
|
|
161
177
|
if (a) switch (a.strategy) {
|
|
162
178
|
case "serverWins":
|
|
163
|
-
|
|
179
|
+
i = "skip";
|
|
164
180
|
break;
|
|
165
181
|
case "clientWins":
|
|
166
|
-
|
|
182
|
+
i = "retry";
|
|
167
183
|
break;
|
|
168
184
|
case "merge":
|
|
169
185
|
case "custom": {
|
|
170
|
-
const
|
|
171
|
-
error:
|
|
186
|
+
const r = {
|
|
187
|
+
error: n,
|
|
172
188
|
args: e.args,
|
|
173
189
|
attempt: e.retryCount,
|
|
174
190
|
idempotencyKey: e.idempotencyKey
|
|
175
191
|
};
|
|
176
|
-
|
|
192
|
+
i = a.resolve?.(r) ?? "retry";
|
|
177
193
|
break;
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
|
-
if (
|
|
181
|
-
return t.removeQueueItem(e.id),
|
|
196
|
+
if (i === "skip")
|
|
197
|
+
return t.removeQueueItem(e.id), u({
|
|
182
198
|
type: "remove",
|
|
183
199
|
id: e.id
|
|
184
200
|
}), await c().remove(e.id), "conflicted";
|
|
185
|
-
|
|
201
|
+
i && typeof i == "object" && (e.args = i.resolved, t.updateQueueItem(e.id, { args: i.resolved }), u({
|
|
186
202
|
type: "update",
|
|
187
203
|
id: e.id,
|
|
188
|
-
update: { args:
|
|
189
|
-
}), await c().update(e.id, { args:
|
|
204
|
+
update: { args: i.resolved }
|
|
205
|
+
}), await c().update(e.id, { args: i.resolved }));
|
|
190
206
|
}
|
|
191
|
-
async function P(e, t,
|
|
207
|
+
async function P(e, t, n) {
|
|
192
208
|
const a = e.retryCount + 1;
|
|
193
209
|
if (a >= e.maxRetries) {
|
|
194
|
-
const
|
|
210
|
+
const r = {
|
|
195
211
|
status: "failed",
|
|
196
|
-
error: String(
|
|
212
|
+
error: String(n),
|
|
197
213
|
retryCount: a
|
|
198
214
|
};
|
|
199
|
-
t.updateQueueItem(e.id,
|
|
215
|
+
t.updateQueueItem(e.id, r), u({
|
|
200
216
|
type: "update",
|
|
201
217
|
id: e.id,
|
|
202
|
-
update:
|
|
203
|
-
}), await c().update(e.id,
|
|
204
|
-
const
|
|
218
|
+
update: r
|
|
219
|
+
}), await c().update(e.id, r);
|
|
220
|
+
const o = {
|
|
205
221
|
idempotencyKey: e.idempotencyKey,
|
|
206
222
|
attempt: a
|
|
207
223
|
};
|
|
208
|
-
return
|
|
224
|
+
return R.get(e.actionId)?.(...e.args, o), "failed";
|
|
209
225
|
}
|
|
210
|
-
const
|
|
226
|
+
const i = {
|
|
211
227
|
status: "pending",
|
|
212
228
|
retryCount: a,
|
|
213
229
|
nextRetryAt: Date.now() + M(a)
|
|
214
230
|
};
|
|
215
|
-
return t.updateQueueItem(e.id,
|
|
231
|
+
return t.updateQueueItem(e.id, i), u({
|
|
216
232
|
type: "update",
|
|
217
233
|
id: e.id,
|
|
218
|
-
update:
|
|
219
|
-
}), await c().update(e.id,
|
|
234
|
+
update: i
|
|
235
|
+
}), await c().update(e.id, i), "retrying";
|
|
220
236
|
}
|
|
221
237
|
async function D(e, t) {
|
|
222
|
-
const
|
|
223
|
-
if (!
|
|
224
|
-
const a =
|
|
225
|
-
let
|
|
238
|
+
const n = w.get(e.actionId);
|
|
239
|
+
if (!n) return "skipped";
|
|
240
|
+
const a = k.get(e.actionId)?.cancellable;
|
|
241
|
+
let i;
|
|
226
242
|
if (a) {
|
|
227
|
-
const
|
|
228
|
-
|
|
243
|
+
const o = new AbortController();
|
|
244
|
+
p.set(e.idempotencyKey, o), i = o.signal;
|
|
229
245
|
}
|
|
230
|
-
const
|
|
246
|
+
const r = {
|
|
231
247
|
idempotencyKey: e.idempotencyKey,
|
|
232
248
|
attempt: e.retryCount,
|
|
233
|
-
signal:
|
|
249
|
+
signal: i
|
|
234
250
|
};
|
|
235
251
|
try {
|
|
236
|
-
return await
|
|
237
|
-
} catch (
|
|
238
|
-
if (
|
|
239
|
-
return t.removeQueueItem(e.id),
|
|
252
|
+
return await v(n, e.args, r), await O(e, t), "succeeded";
|
|
253
|
+
} catch (o) {
|
|
254
|
+
if (x(o))
|
|
255
|
+
return t.removeQueueItem(e.id), u({
|
|
240
256
|
type: "remove",
|
|
241
257
|
id: e.id
|
|
242
258
|
}), await c().remove(e.id), "cancelled";
|
|
243
|
-
if (K(
|
|
244
|
-
const s = await E(e, t,
|
|
259
|
+
if (K(o)) {
|
|
260
|
+
const s = await E(e, t, o);
|
|
245
261
|
if (s) return s;
|
|
246
262
|
}
|
|
247
|
-
return P(e, t,
|
|
263
|
+
return P(e, t, o);
|
|
248
264
|
} finally {
|
|
249
|
-
a &&
|
|
265
|
+
a && p.delete(e.idempotencyKey);
|
|
250
266
|
}
|
|
251
267
|
}
|
|
252
|
-
async function j(e, t,
|
|
268
|
+
async function j(e, t, n) {
|
|
253
269
|
if (e.length === 0) return;
|
|
254
|
-
const a = e.filter((
|
|
255
|
-
if (
|
|
256
|
-
const
|
|
257
|
-
id:
|
|
270
|
+
const a = e.filter((r) => w.has(r.actionId));
|
|
271
|
+
if (n.skipped += e.length - a.length, a.length > 0) {
|
|
272
|
+
const r = a.map((o) => ({
|
|
273
|
+
id: o.id,
|
|
258
274
|
update: { status: "replaying" }
|
|
259
275
|
}));
|
|
260
|
-
t.batchUpdateQueueItems(
|
|
276
|
+
t.batchUpdateQueueItems(r), u({
|
|
261
277
|
type: "batchUpdate",
|
|
262
|
-
updates:
|
|
278
|
+
updates: r
|
|
263
279
|
});
|
|
264
|
-
for (const
|
|
280
|
+
for (const o of a) c().update(o.id, { status: "replaying" });
|
|
265
281
|
}
|
|
266
|
-
const
|
|
267
|
-
for (const
|
|
268
|
-
const
|
|
269
|
-
|
|
282
|
+
const i = await Promise.allSettled(a.map((r) => D(r, t)));
|
|
283
|
+
for (const r of i) {
|
|
284
|
+
const o = r.status === "fulfilled" ? r.value : "failed";
|
|
285
|
+
o === "skipped" ? n.skipped++ : o === "conflicted" ? n.conflicted++ : o === "cancelled" ? n.cancelled++ : (n.attempted++, n[o]++);
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
288
|
async function I(e) {
|
|
273
|
-
const t = await c().getPending(),
|
|
274
|
-
for (const
|
|
289
|
+
const t = await c().getPending(), n = Date.now(), a = t.filter((r) => r.retryCount < r.maxRetries && (!r.nextRetryAt || r.nextRetryAt <= n)), i = f();
|
|
290
|
+
for (const r of [
|
|
275
291
|
"high",
|
|
276
292
|
"normal",
|
|
277
293
|
"low"
|
|
278
|
-
]) await j(a.filter((
|
|
279
|
-
return
|
|
294
|
+
]) await j(a.filter((o) => (o.priority ?? "normal") === r), e, i);
|
|
295
|
+
return i;
|
|
280
296
|
}
|
|
281
297
|
async function Y() {
|
|
282
|
-
await c().clear(),
|
|
298
|
+
await c().clear(), l.getState().hydrateQueue([]);
|
|
283
299
|
}
|
|
284
300
|
export {
|
|
285
|
-
|
|
301
|
+
B as action,
|
|
302
|
+
S as cancelByIdempotencyKey,
|
|
286
303
|
Y as clearQueue,
|
|
287
|
-
V as replayQueue
|
|
304
|
+
V as replayQueue,
|
|
305
|
+
F as requeueItem
|
|
288
306
|
};
|
|
289
307
|
|
|
290
308
|
//# sourceMappingURL=action.js.map
|
package/dist/action.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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 Object.defineProperty(wrapped, 'id', { value: actionId, writable: false });\n Object.defineProperty(wrapped, 'config', { value: config, writable: false });\n Object.defineProperty(wrapped, 'cancel', { value: cancelByIdempotencyKey, writable: false });\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>;\n}\n\n/**\n * Cancel an invocation by its `idempotencyKey` (from `ActionContext` /\n * `onOptimistic`). Aborts the in-flight call if `cancellable: true` and\n * still running, otherwise removes a not-yet-replayed queued item.\n * Returns `true` if something was cancelled/removed.\n *\n * Shared by every `ActionHandle.cancel()` and by devtools, which can't\n * address a specific handle from a queue item alone.\n */\nexport async function cancelByIdempotencyKey(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/**\n * Reset a `'failed'` queue item back to `'pending'` so the next\n * `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets\n * `retryCount` to 0. Returns `true` if the item existed and was failed.\n * Used by devtools' per-item \"Retry\" action.\n */\nexport async function requeueItem(id: string): Promise<boolean> {\n const items = await qs().getAll();\n const item = items.find((i) => i.id === id);\n if (!item || item.status !== 'failed') return false;\n\n const update: Partial<ActionQueueItem> = {\n status: 'pending',\n error: undefined,\n nextRetryAt: undefined,\n retryCount: 0,\n };\n useEidosStore.getState().updateQueueItem(id, update);\n broadcastQueueSync({ type: 'update', id, update });\n await qs().update(id, update);\n return true;\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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"}
|