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