@sweidos/eidos 1.0.30 → 1.0.32
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 +149 -717
- package/dist/action.js +58 -47
- package/dist/action.js.map +1 -1
- package/dist/async-storage-adapter.js +42 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.js +65 -44
- package/dist/eidos.cjs.js +4 -4
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/idb.js +56 -63
- package/dist/idb.js.map +1 -1
- package/dist/index.d.ts +41 -2
- package/dist/index.js +38 -33
- package/dist/index.js.map +1 -1
- package/dist/queue-storage.js +12 -0
- package/dist/queue-storage.js.map +1 -0
- package/dist/react/ProviderRN.d.ts +23 -0
- package/dist/react-native.d.ts +8 -0
- package/dist/react-native.js +59 -0
- package/dist/resource.js +29 -22
- package/dist/resource.js.map +1 -1
- package/dist/runtime-rn.d.ts +18 -0
- package/dist/store.js +26 -24
- package/dist/store.js.map +1 -1
- package/dist/stores.js +15 -13
- package/dist/stores.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +26 -5
package/dist/action.js
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import { useEidosStore as d } from "./store.js";
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbClearQueue as
|
|
4
|
-
|
|
2
|
+
import { getSwRegistration as b } from "./sw-bridge.js";
|
|
3
|
+
import { idbClearQueue as g, idbRemoveFromQueue as Q, idbUpdateQueueItem as h, idbGetPendingItems as I, idbGetQueue as R, idbAddToQueue as k } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as v } from "./queue-storage.js";
|
|
5
|
+
const l = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), S = {
|
|
6
|
+
add: (e) => k(e),
|
|
7
|
+
getAll: () => R(),
|
|
8
|
+
getPending: () => I(),
|
|
9
|
+
update: (e, t) => h(e, t),
|
|
10
|
+
remove: (e) => Q(e),
|
|
11
|
+
clear: () => g()
|
|
12
|
+
};
|
|
13
|
+
function o() {
|
|
14
|
+
return v() ?? S;
|
|
15
|
+
}
|
|
5
16
|
function m() {
|
|
6
17
|
return crypto.randomUUID();
|
|
7
18
|
}
|
|
8
|
-
function
|
|
19
|
+
function U(e, t) {
|
|
9
20
|
const r = t.name || e.name || m();
|
|
10
|
-
l.set(r, e), t.onRollback &&
|
|
11
|
-
const
|
|
12
|
-
var i,
|
|
21
|
+
l.set(r, e), t.onRollback && f.set(r, t.onRollback), t.onConflict && y.set(r, t.onConflict);
|
|
22
|
+
const u = async (...a) => {
|
|
23
|
+
var i, s;
|
|
13
24
|
const { isOnline: n } = d.getState();
|
|
14
25
|
if ((i = t.onOptimistic) == null || i.call(t, ...a), t.reliability === "neverLose") {
|
|
15
26
|
if (!n)
|
|
16
|
-
return
|
|
27
|
+
return p(r, r, a, t);
|
|
17
28
|
try {
|
|
18
29
|
return await e(...a);
|
|
19
30
|
} catch {
|
|
20
|
-
return
|
|
31
|
+
return p(r, r, a, t);
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
try {
|
|
24
35
|
return await e(...a);
|
|
25
|
-
} catch (
|
|
26
|
-
throw (
|
|
36
|
+
} catch (w) {
|
|
37
|
+
throw (s = t.onRollback) == null || s.call(t, ...a), w;
|
|
27
38
|
}
|
|
28
39
|
};
|
|
29
|
-
return Object.defineProperty(
|
|
40
|
+
return Object.defineProperty(u, "id", { value: r, writable: !1 }), Object.defineProperty(u, "config", { value: t, writable: !1 }), u;
|
|
30
41
|
}
|
|
31
|
-
async function
|
|
42
|
+
async function p(e, t, r, u) {
|
|
32
43
|
const a = m(), n = {
|
|
33
44
|
id: a,
|
|
34
45
|
actionId: e,
|
|
@@ -36,13 +47,13 @@ async function f(e, t, r, s) {
|
|
|
36
47
|
args: r,
|
|
37
48
|
queuedAt: Date.now(),
|
|
38
49
|
retryCount: 0,
|
|
39
|
-
maxRetries:
|
|
50
|
+
maxRetries: u.maxRetries ?? 3,
|
|
40
51
|
status: "pending",
|
|
41
|
-
priority:
|
|
52
|
+
priority: u.priority ?? "normal"
|
|
42
53
|
};
|
|
43
|
-
await
|
|
54
|
+
await o().add(n), d.getState().addQueueItem(n);
|
|
44
55
|
try {
|
|
45
|
-
const i =
|
|
56
|
+
const i = b();
|
|
46
57
|
i && "sync" in i && await i.sync.register("eidos-queue-replay");
|
|
47
58
|
} catch {
|
|
48
59
|
}
|
|
@@ -52,7 +63,7 @@ async function f(e, t, r, s) {
|
|
|
52
63
|
message: `"${t}" queued — will execute when online`
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
|
-
function
|
|
66
|
+
function _(e) {
|
|
56
67
|
if (e instanceof Response) return e.status >= 400 && e.status < 500;
|
|
57
68
|
if (typeof e == "object" && e !== null) {
|
|
58
69
|
const t = e.status;
|
|
@@ -60,74 +71,74 @@ function g(e) {
|
|
|
60
71
|
}
|
|
61
72
|
return !1;
|
|
62
73
|
}
|
|
63
|
-
function
|
|
74
|
+
function x(e) {
|
|
64
75
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
65
76
|
}
|
|
66
77
|
let c = !1;
|
|
67
|
-
async function
|
|
78
|
+
async function j() {
|
|
68
79
|
const e = d.getState();
|
|
69
80
|
if (!e.isOnline || c)
|
|
70
81
|
return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
71
82
|
c = !0;
|
|
72
83
|
try {
|
|
73
|
-
return await
|
|
84
|
+
return await M(e);
|
|
74
85
|
} finally {
|
|
75
86
|
c = !1;
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
|
-
async function
|
|
79
|
-
var
|
|
89
|
+
async function A(e, t) {
|
|
90
|
+
var u;
|
|
80
91
|
const r = l.get(e.actionId);
|
|
81
92
|
if (!r) return "skipped";
|
|
82
93
|
try {
|
|
83
94
|
await r(...e.args);
|
|
84
95
|
const a = Date.now();
|
|
85
|
-
return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await
|
|
86
|
-
t.removeQueueItem(e.id),
|
|
96
|
+
return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await o().update(e.id, { status: "succeeded", completedAt: a }), setTimeout(() => {
|
|
97
|
+
t.removeQueueItem(e.id), o().remove(e.id);
|
|
87
98
|
}, 3e3), "succeeded";
|
|
88
99
|
} catch (a) {
|
|
89
|
-
if (
|
|
90
|
-
const i =
|
|
100
|
+
if (_(a)) {
|
|
101
|
+
const i = y.get(e.actionId);
|
|
91
102
|
if (i && i(a, e.args) === "skip")
|
|
92
|
-
return t.removeQueueItem(e.id), await
|
|
103
|
+
return t.removeQueueItem(e.id), await o().remove(e.id), "conflicted";
|
|
93
104
|
}
|
|
94
105
|
const n = e.retryCount + 1;
|
|
95
106
|
if (n >= e.maxRetries)
|
|
96
|
-
return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await
|
|
107
|
+
return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await o().update(e.id, { status: "failed", error: String(a), retryCount: n }), (u = f.get(e.actionId)) == null || u(...e.args), "failed";
|
|
97
108
|
{
|
|
98
|
-
const i = Date.now() +
|
|
99
|
-
return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await
|
|
109
|
+
const i = Date.now() + x(n);
|
|
110
|
+
return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await o().update(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), "retrying";
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
|
-
async function
|
|
114
|
+
async function C(e, t, r) {
|
|
104
115
|
if (e.length === 0) return;
|
|
105
|
-
const
|
|
106
|
-
if (r.skipped += e.length -
|
|
107
|
-
t.batchUpdateQueueItems(
|
|
108
|
-
for (const n of
|
|
109
|
-
|
|
116
|
+
const u = e.filter((n) => l.has(n.actionId));
|
|
117
|
+
if (r.skipped += e.length - u.length, u.length > 0) {
|
|
118
|
+
t.batchUpdateQueueItems(u.map((n) => ({ id: n.id, update: { status: "replaying" } })));
|
|
119
|
+
for (const n of u)
|
|
120
|
+
o().update(n.id, { status: "replaying" });
|
|
110
121
|
}
|
|
111
|
-
const a = await Promise.allSettled(
|
|
122
|
+
const a = await Promise.allSettled(u.map((n) => A(n, t)));
|
|
112
123
|
for (const n of a) {
|
|
113
124
|
const i = n.status === "fulfilled" ? n.value : "failed";
|
|
114
125
|
i === "skipped" ? r.skipped++ : i === "conflicted" ? r.conflicted++ : (r.attempted++, r[i]++);
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
|
-
async function
|
|
118
|
-
const t = await
|
|
128
|
+
async function M(e) {
|
|
129
|
+
const t = await o().getPending(), r = Date.now(), u = t.filter((n) => !n.nextRetryAt || n.nextRetryAt <= r), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
119
130
|
for (const n of ["high", "normal", "low"]) {
|
|
120
|
-
const i =
|
|
121
|
-
await
|
|
131
|
+
const i = u.filter((s) => (s.priority ?? "normal") === n);
|
|
132
|
+
await C(i, e, a);
|
|
122
133
|
}
|
|
123
134
|
return a;
|
|
124
135
|
}
|
|
125
|
-
async function
|
|
126
|
-
await
|
|
136
|
+
async function T() {
|
|
137
|
+
await o().clear(), d.getState().hydrateQueue([]);
|
|
127
138
|
}
|
|
128
139
|
export {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
U as action,
|
|
141
|
+
T as clearQueue,
|
|
142
|
+
j as replayQueue
|
|
132
143
|
};
|
|
133
144
|
//# sourceMappingURL=action.js.map
|
package/dist/action.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await idbAddToQueue(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await idbUpdateQueueItem(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await idbRemoveFromQueue(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await idbUpdateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await idbUpdateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n idbUpdateQueueItem(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (IDB + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await idbClearQueue()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","idbUpdateQueueItem","idbRemoveFromQueue","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","idbGetPendingItems","now","pending","tier","tierItems","clearQueue","idbClearQueue"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport { _getQueueStorage } from './queue-storage'\nimport type { QueueStorage } from './queue-storage'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\n// IDB fallback — used when no custom storage is set (default browser behavior).\nconst _idbFallback: QueueStorage = {\n add: (item) => idbAddToQueue(item),\n getAll: () => idbGetQueue(),\n getPending: () => idbGetPendingItems(),\n update: (id, patch) => idbUpdateQueueItem(id, patch),\n remove: (id) => idbRemoveFromQueue(id),\n clear: () => idbClearQueue(),\n}\n\nfunction qs(): QueueStorage {\n return _getQueueStorage() ?? _idbFallback\n}\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await qs().add(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await qs().update(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n qs().remove(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await qs().remove(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await qs().update(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await qs().update(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n qs().update(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await qs().getPending()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (storage + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await qs().clear()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","_idbFallback","item","idbAddToQueue","idbGetQueue","idbGetPendingItems","id","patch","idbUpdateQueueItem","idbRemoveFromQueue","idbClearQueue","qs","_getQueueStorage","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","now","pending","tier","tierItems","clearQueue"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const i = "@eidos:queue";
|
|
2
|
+
class l {
|
|
3
|
+
constructor(t) {
|
|
4
|
+
this.storage = t;
|
|
5
|
+
}
|
|
6
|
+
async readAll() {
|
|
7
|
+
try {
|
|
8
|
+
const t = await this.storage.getItem(i);
|
|
9
|
+
return t ? JSON.parse(t) : [];
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async writeAll(t) {
|
|
15
|
+
await this.storage.setItem(i, JSON.stringify(t));
|
|
16
|
+
}
|
|
17
|
+
async add(t) {
|
|
18
|
+
const e = await this.readAll();
|
|
19
|
+
e.push(t), await this.writeAll(e);
|
|
20
|
+
}
|
|
21
|
+
async getAll() {
|
|
22
|
+
return this.readAll();
|
|
23
|
+
}
|
|
24
|
+
async getPending() {
|
|
25
|
+
return (await this.readAll()).filter((e) => e.status === "pending" || e.status === "failed");
|
|
26
|
+
}
|
|
27
|
+
async update(t, e) {
|
|
28
|
+
const a = await this.readAll(), s = a.findIndex((r) => r.id === t);
|
|
29
|
+
s !== -1 && (a[s] = { ...a[s], ...e }), await this.writeAll(a);
|
|
30
|
+
}
|
|
31
|
+
async remove(t) {
|
|
32
|
+
const e = await this.readAll();
|
|
33
|
+
await this.writeAll(e.filter((a) => a.id !== t));
|
|
34
|
+
}
|
|
35
|
+
async clear() {
|
|
36
|
+
await this.storage.removeItem(i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
l as AsyncStorageQueueStorage
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=async-storage-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-storage-adapter.js","sources":["../src/async-storage-adapter.ts"],"sourcesContent":["import type { ActionQueueItem } from './types'\nimport type { QueueStorage } from './queue-storage'\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n removeItem(key: string): Promise<void>\n}\n\nconst QUEUE_KEY = '@eidos:queue'\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY)\n if (!raw) return []\n return JSON.parse(raw) as ActionQueueItem[]\n } catch {\n return []\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items))\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll()\n items.push(item)\n await this.writeAll(items)\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll()\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll()\n return items.filter((i) => i.status === 'pending' || i.status === 'failed')\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll()\n const idx = items.findIndex((i) => i.id === id)\n if (idx !== -1) items[idx] = { ...items[idx], ...patch }\n await this.writeAll(items)\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll()\n await this.writeAll(items.filter((i) => i.id !== id))\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY)\n }\n}\n"],"names":["QUEUE_KEY","AsyncStorageQueueStorage","storage","raw","items","item","i","id","patch","idx"],"mappings":"AAUA,MAAMA,IAAY;AAOX,MAAMC,EAAiD;AAAA,EAC5D,YAA6BC,GAA2B;AAA3B,SAAA,UAAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAMC,IAAM,MAAM,KAAK,QAAQ,QAAQH,CAAS;AAChD,aAAKG,IACE,KAAK,MAAMA,CAAG,IADJ,CAAA;AAAA,IAEnB,QAAQ;AACN,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,SAASC,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQJ,GAAW,KAAK,UAAUI,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAIC,GAAsC;AAC9C,UAAMD,IAAQ,MAAM,KAAK,QAAA;AACzB,IAAAA,EAAM,KAAKC,CAAI,GACf,MAAM,KAAK,SAASD,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAM,aAAyC;AAE7C,YADc,MAAM,KAAK,QAAA,GACZ,OAAO,CAACE,MAAMA,EAAE,WAAW,aAAaA,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAOC,GAAYC,GAAgD;AACvE,UAAMJ,IAAQ,MAAM,KAAK,QAAA,GACnBK,IAAML,EAAM,UAAU,CAACE,MAAMA,EAAE,OAAOC,CAAE;AAC9C,IAAIE,MAAQ,OAAIL,EAAMK,CAAG,IAAI,EAAE,GAAGL,EAAMK,CAAG,GAAG,GAAGD,EAAA,IACjD,MAAM,KAAK,SAASJ,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAOG,GAA2B;AACtC,UAAMH,IAAQ,MAAM,KAAK,QAAA;AACzB,UAAM,KAAK,SAASA,EAAM,OAAO,CAACE,MAAMA,EAAE,OAAOC,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAWP,CAAS;AAAA,EACzC;AACF;"}
|
package/dist/devtools.js
CHANGED
|
@@ -11,7 +11,8 @@ function _set(updater) {
|
|
|
11
11
|
_notify();
|
|
12
12
|
}
|
|
13
13
|
_state = {
|
|
14
|
-
|
|
14
|
+
// navigator.onLine is undefined in React Native — default to true unless explicitly false
|
|
15
|
+
isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
|
|
15
16
|
swStatus: "idle",
|
|
16
17
|
swError: void 0,
|
|
17
18
|
resources: {},
|
|
@@ -25,10 +26,11 @@ _state = {
|
|
|
25
26
|
[url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url]
|
|
26
27
|
}
|
|
27
28
|
})),
|
|
28
|
-
unregisterResource: (url) => _set((s) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
unregisterResource: (url) => _set((s) => ({
|
|
30
|
+
resources: Object.fromEntries(
|
|
31
|
+
Object.entries(s.resources).filter(([k]) => k !== url)
|
|
32
|
+
)
|
|
33
|
+
})),
|
|
32
34
|
addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),
|
|
33
35
|
updateQueueItem: (id, update) => _set((s) => ({
|
|
34
36
|
queue: s.queue.map((item) => item.id === id ? { ...item, ...update } : item)
|
|
@@ -119,6 +121,24 @@ function openDB() {
|
|
|
119
121
|
req.onerror = () => reject(req.error);
|
|
120
122
|
});
|
|
121
123
|
}
|
|
124
|
+
async function idbAddToQueue(item) {
|
|
125
|
+
const db = await openDB();
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
128
|
+
tx.objectStore(QUEUE_STORE).add(item);
|
|
129
|
+
tx.oncomplete = () => resolve();
|
|
130
|
+
tx.onerror = () => reject(tx.error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function idbGetQueue() {
|
|
134
|
+
const db = await openDB();
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
137
|
+
const req = tx.objectStore(QUEUE_STORE).getAll();
|
|
138
|
+
req.onsuccess = () => resolve(req.result);
|
|
139
|
+
req.onerror = () => reject(req.error);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
122
142
|
async function idbUpdateQueueItem(id, update) {
|
|
123
143
|
const db = await openDB();
|
|
124
144
|
return new Promise((resolve, reject) => {
|
|
@@ -145,37 +165,27 @@ async function idbRemoveFromQueue(id) {
|
|
|
145
165
|
}
|
|
146
166
|
async function idbGetPendingItems() {
|
|
147
167
|
const db = await openDB();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const failedReq = index.openCursor(IDBKeyRange.only("failed"));
|
|
170
|
-
failedReq.onsuccess = (e) => {
|
|
171
|
-
const cursor = e.target.result;
|
|
172
|
-
if (cursor) {
|
|
173
|
-
results.push(cursor.value);
|
|
174
|
-
cursor.continue();
|
|
175
|
-
} else finish();
|
|
176
|
-
};
|
|
177
|
-
failedReq.onerror = () => finish(failedReq.error);
|
|
178
|
-
});
|
|
168
|
+
function cursorToArray(status) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
171
|
+
const index = tx.objectStore(QUEUE_STORE).index("status");
|
|
172
|
+
const items = [];
|
|
173
|
+
const req = index.openCursor(IDBKeyRange.only(status));
|
|
174
|
+
req.onsuccess = (e) => {
|
|
175
|
+
const cursor = e.target.result;
|
|
176
|
+
if (cursor) {
|
|
177
|
+
items.push(cursor.value);
|
|
178
|
+
cursor.continue();
|
|
179
|
+
} else resolve(items);
|
|
180
|
+
};
|
|
181
|
+
req.onerror = () => reject(req.error);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const [pending, failed] = await Promise.all([
|
|
185
|
+
cursorToArray("pending"),
|
|
186
|
+
cursorToArray("failed")
|
|
187
|
+
]);
|
|
188
|
+
return [...pending, ...failed];
|
|
179
189
|
}
|
|
180
190
|
async function idbClearQueue() {
|
|
181
191
|
const db = await openDB();
|
|
@@ -189,6 +199,17 @@ async function idbClearQueue() {
|
|
|
189
199
|
const _actionRegistry = /* @__PURE__ */ new Map();
|
|
190
200
|
const _rollbackRegistry = /* @__PURE__ */ new Map();
|
|
191
201
|
const _conflictRegistry = /* @__PURE__ */ new Map();
|
|
202
|
+
const _idbFallback = {
|
|
203
|
+
add: (item) => idbAddToQueue(item),
|
|
204
|
+
getAll: () => idbGetQueue(),
|
|
205
|
+
getPending: () => idbGetPendingItems(),
|
|
206
|
+
update: (id, patch) => idbUpdateQueueItem(id, patch),
|
|
207
|
+
remove: (id) => idbRemoveFromQueue(id),
|
|
208
|
+
clear: () => idbClearQueue()
|
|
209
|
+
};
|
|
210
|
+
function qs() {
|
|
211
|
+
return _idbFallback;
|
|
212
|
+
}
|
|
192
213
|
function isClientError(err) {
|
|
193
214
|
if (err instanceof Response) return err.status >= 400 && err.status < 500;
|
|
194
215
|
if (typeof err === "object" && err !== null) {
|
|
@@ -222,10 +243,10 @@ async function _replayItem(item, store) {
|
|
|
222
243
|
await fn(...item.args);
|
|
223
244
|
const completedAt = Date.now();
|
|
224
245
|
store.updateQueueItem(item.id, { status: "succeeded", completedAt });
|
|
225
|
-
await
|
|
246
|
+
await qs().update(item.id, { status: "succeeded", completedAt });
|
|
226
247
|
setTimeout(() => {
|
|
227
248
|
store.removeQueueItem(item.id);
|
|
228
|
-
|
|
249
|
+
qs().remove(item.id);
|
|
229
250
|
}, 3e3);
|
|
230
251
|
return "succeeded";
|
|
231
252
|
} catch (err) {
|
|
@@ -235,7 +256,7 @@ async function _replayItem(item, store) {
|
|
|
235
256
|
const resolution = onConflict(err, item.args);
|
|
236
257
|
if (resolution === "skip") {
|
|
237
258
|
store.removeQueueItem(item.id);
|
|
238
|
-
await
|
|
259
|
+
await qs().remove(item.id);
|
|
239
260
|
return "conflicted";
|
|
240
261
|
}
|
|
241
262
|
}
|
|
@@ -243,13 +264,13 @@ async function _replayItem(item, store) {
|
|
|
243
264
|
const retryCount = item.retryCount + 1;
|
|
244
265
|
if (retryCount >= item.maxRetries) {
|
|
245
266
|
store.updateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
|
|
246
|
-
await
|
|
267
|
+
await qs().update(item.id, { status: "failed", error: String(err), retryCount });
|
|
247
268
|
(_a = _rollbackRegistry.get(item.actionId)) == null ? void 0 : _a(...item.args);
|
|
248
269
|
return "failed";
|
|
249
270
|
} else {
|
|
250
271
|
const nextRetryAt = Date.now() + backoffMs(retryCount);
|
|
251
272
|
store.updateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
|
|
252
|
-
await
|
|
273
|
+
await qs().update(item.id, { status: "pending", retryCount, nextRetryAt });
|
|
253
274
|
return "retrying";
|
|
254
275
|
}
|
|
255
276
|
}
|
|
@@ -261,7 +282,7 @@ async function _replayTier(items, store, result) {
|
|
|
261
282
|
if (replayable.length > 0) {
|
|
262
283
|
store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: "replaying" } })));
|
|
263
284
|
for (const item of replayable) {
|
|
264
|
-
|
|
285
|
+
qs().update(item.id, { status: "replaying" });
|
|
265
286
|
}
|
|
266
287
|
}
|
|
267
288
|
const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
|
|
@@ -278,7 +299,7 @@ async function _replayTier(items, store, result) {
|
|
|
278
299
|
}
|
|
279
300
|
}
|
|
280
301
|
async function _doReplayQueue(store) {
|
|
281
|
-
const candidates = await
|
|
302
|
+
const candidates = await qs().getPending();
|
|
282
303
|
const now = Date.now();
|
|
283
304
|
const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now);
|
|
284
305
|
const result = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
@@ -289,7 +310,7 @@ async function _doReplayQueue(store) {
|
|
|
289
310
|
return result;
|
|
290
311
|
}
|
|
291
312
|
async function clearQueue() {
|
|
292
|
-
await
|
|
313
|
+
await qs().clear();
|
|
293
314
|
useEidosStore.getState().hydrateQueue([]);
|
|
294
315
|
}
|
|
295
316
|
const C = {
|