@sweidos/eidos 1.0.23 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,7 +108,18 @@ export const createOrder = action(
108
108
  })
109
109
  return res.json()
110
110
  },
111
- { reliability: 'neverLose', name: 'createOrder' },
111
+ {
112
+ reliability: 'neverLose',
113
+ name: 'createOrder',
114
+ onOptimistic: (payload) => {
115
+ // Called immediately — update UI before the server responds
116
+ addOptimisticOrder(payload)
117
+ },
118
+ onRollback: (payload) => {
119
+ // Called only if maxRetries exhausted — revert the optimistic change
120
+ removeOptimisticOrder(payload)
121
+ },
122
+ },
112
123
  )
113
124
  ```
114
125
 
@@ -205,6 +216,8 @@ const createOrder = action(
205
216
  reliability: 'neverLose', // persist to IndexedDB + replay on reconnect
206
217
  maxRetries?: number, // default: 3
207
218
  name?: string, // label in devtools
219
+ onOptimistic?: (...args) => void, // called immediately — update UI optimistically
220
+ onRollback?: (...args) => void, // called on permanent failure — revert UI
208
221
  }
209
222
  )
210
223
 
@@ -703,7 +716,7 @@ it('caches the resource after first fetch', async () => {
703
716
  - [x] TanStack Query integration (`@sweidos/eidos/query` subpath — `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient`)
704
717
 
705
718
  **Core reliability**
706
- - [ ] Optimistic updates — `onOptimistic` / `onRollback` callbacks on `action()` for instant UI feedback before server confirms
719
+ - [x] Optimistic updates — `onOptimistic` / `onRollback` callbacks on `action()` for instant UI feedback before server confirms
707
720
  - [ ] Conflict resolution hook — `onConflict` callback when replaying a queued action returns 4xx; decide per-item: retry, skip, or merge
708
721
  - [ ] Queue prioritization — `priority: 'high' | 'normal' | 'low'` on `action()`; high-priority items replay first
709
722
 
package/dist/action.js CHANGED
@@ -1,103 +1,109 @@
1
- import { useEidosStore as l } from "./store.js";
2
- import { getSwRegistration as g } from "./sw-bridge.js";
3
- import { idbClearQueue as Q, idbGetPendingItems as b, idbUpdateQueueItem as c, idbRemoveFromQueue as I, idbAddToQueue as R } from "./idb.js";
4
- const w = /* @__PURE__ */ new Map();
5
- function m() {
1
+ import { useEidosStore as p } from "./store.js";
2
+ import { getSwRegistration as R } from "./sw-bridge.js";
3
+ import { idbClearQueue as I, idbGetPendingItems as k, idbUpdateQueueItem as l, idbRemoveFromQueue as h, idbAddToQueue as S } from "./idb.js";
4
+ const m = /* @__PURE__ */ new Map(), b = /* @__PURE__ */ new Map();
5
+ function Q() {
6
6
  return crypto.randomUUID();
7
7
  }
8
- function v(t, n) {
9
- const r = n.name || t.name || m();
10
- w.set(r, t);
11
- const u = async (...a) => {
12
- const { isOnline: i } = l.getState();
13
- if (n.reliability === "neverLose") {
8
+ function M(a, t) {
9
+ const n = t.name || a.name || Q();
10
+ m.set(n, a), t.onRollback && b.set(n, t.onRollback);
11
+ const s = async (...r) => {
12
+ var e, u;
13
+ const { isOnline: i } = p.getState();
14
+ if ((e = t.onOptimistic) == null || e.call(t, ...r), t.reliability === "neverLose") {
14
15
  if (!i)
15
- return f(r, r, a, n);
16
+ return f(n, n, r, t);
16
17
  try {
17
- return await t(...a);
18
+ return await a(...r);
18
19
  } catch {
19
- return f(r, r, a, n);
20
+ return f(n, n, r, t);
20
21
  }
21
22
  }
22
- return t(...a);
23
+ try {
24
+ return await a(...r);
25
+ } catch (c) {
26
+ throw (u = t.onRollback) == null || u.call(t, ...r), c;
27
+ }
23
28
  };
24
- return Object.defineProperty(u, "id", { value: r, writable: !1 }), Object.defineProperty(u, "config", { value: n, writable: !1 }), u;
29
+ return Object.defineProperty(s, "id", { value: n, writable: !1 }), Object.defineProperty(s, "config", { value: t, writable: !1 }), s;
25
30
  }
26
- async function f(t, n, r, u) {
27
- const a = m(), i = {
28
- id: a,
29
- actionId: t,
30
- actionName: n,
31
- args: r,
31
+ async function f(a, t, n, s) {
32
+ const r = Q(), i = {
33
+ id: r,
34
+ actionId: a,
35
+ actionName: t,
36
+ args: n,
32
37
  queuedAt: Date.now(),
33
38
  retryCount: 0,
34
- maxRetries: u.maxRetries ?? 3,
39
+ maxRetries: s.maxRetries ?? 3,
35
40
  status: "pending"
36
41
  };
37
- await R(i), l.getState().addQueueItem(i);
42
+ await S(i), p.getState().addQueueItem(i);
38
43
  try {
39
- const e = g();
44
+ const e = R();
40
45
  e && "sync" in e && await e.sync.register("eidos-queue-replay");
41
46
  } catch {
42
47
  }
43
48
  return {
44
49
  queued: !0,
45
- id: a,
46
- message: `"${n}" queued — will execute when online`
50
+ id: r,
51
+ message: `"${t}" queued — will execute when online`
47
52
  };
48
53
  }
49
- function S(t) {
50
- return Math.min(2e3 * 2 ** t, 3e5) * (0.8 + Math.random() * 0.4);
54
+ function g(a) {
55
+ return Math.min(2e3 * 2 ** a, 3e5) * (0.8 + Math.random() * 0.4);
51
56
  }
52
- let p = !1;
53
- async function D() {
54
- const t = l.getState();
55
- if (!t.isOnline || p)
57
+ let y = !1;
58
+ async function O() {
59
+ const a = p.getState();
60
+ if (!a.isOnline || y)
56
61
  return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 };
57
- p = !0;
62
+ y = !0;
58
63
  try {
59
- return await x(t);
64
+ return await x(a);
60
65
  } finally {
61
- p = !1;
66
+ y = !1;
62
67
  }
63
68
  }
64
- async function x(t) {
65
- const n = await b(), r = Date.now(), u = n.filter(
66
- (e) => !e.nextRetryAt || e.nextRetryAt <= r
67
- ), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }, i = await Promise.allSettled(
68
- u.map(async (e) => {
69
- const d = w.get(e.actionId);
70
- if (!d) return "skipped";
71
- t.updateQueueItem(e.id, { status: "replaying" }), await c(e.id, { status: "replaying" });
69
+ async function x(a) {
70
+ const t = await k(), n = Date.now(), s = t.filter(
71
+ (e) => !e.nextRetryAt || e.nextRetryAt <= n
72
+ ), r = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }, i = await Promise.allSettled(
73
+ s.map(async (e) => {
74
+ var c;
75
+ const u = m.get(e.actionId);
76
+ if (!u) return "skipped";
77
+ a.updateQueueItem(e.id, { status: "replaying" }), await l(e.id, { status: "replaying" });
72
78
  try {
73
- await d(...e.args);
79
+ await u(...e.args);
74
80
  const o = Date.now();
75
- return t.updateQueueItem(e.id, { status: "succeeded", completedAt: o }), await c(e.id, { status: "succeeded", completedAt: o }), setTimeout(() => {
76
- t.removeQueueItem(e.id), I(e.id);
81
+ return a.updateQueueItem(e.id, { status: "succeeded", completedAt: o }), await l(e.id, { status: "succeeded", completedAt: o }), setTimeout(() => {
82
+ a.removeQueueItem(e.id), h(e.id);
77
83
  }, 3e3), "succeeded";
78
84
  } catch (o) {
79
- const s = e.retryCount + 1;
80
- if (s >= e.maxRetries)
81
- return t.updateQueueItem(e.id, { status: "failed", error: String(o), retryCount: s }), await c(e.id, { status: "failed", error: String(o), retryCount: s }), "failed";
85
+ const d = e.retryCount + 1;
86
+ if (d >= e.maxRetries)
87
+ return a.updateQueueItem(e.id, { status: "failed", error: String(o), retryCount: d }), await l(e.id, { status: "failed", error: String(o), retryCount: d }), (c = b.get(e.actionId)) == null || c(...e.args), "failed";
82
88
  {
83
- const y = Date.now() + S(s);
84
- return t.updateQueueItem(e.id, { status: "pending", retryCount: s, nextRetryAt: y }), await c(e.id, { status: "pending", retryCount: s, nextRetryAt: y }), "retrying";
89
+ const w = Date.now() + g(d);
90
+ return a.updateQueueItem(e.id, { status: "pending", retryCount: d, nextRetryAt: w }), await l(e.id, { status: "pending", retryCount: d, nextRetryAt: w }), "retrying";
85
91
  }
86
92
  }
87
93
  })
88
94
  );
89
95
  for (const e of i) {
90
- const d = e.status === "fulfilled" ? e.value : "failed";
91
- d === "skipped" ? a.skipped++ : (a.attempted++, a[d]++);
96
+ const u = e.status === "fulfilled" ? e.value : "failed";
97
+ u === "skipped" ? r.skipped++ : (r.attempted++, r[u]++);
92
98
  }
93
- return a;
99
+ return r;
94
100
  }
95
101
  async function q() {
96
- await Q(), l.getState().hydrateQueue([]);
102
+ await I(), p.getState().hydrateQueue([]);
97
103
  }
98
104
  export {
99
- v as action,
105
+ M as action,
100
106
  q as clearQueue,
101
- D as replayQueue
107
+ O as replayQueue
102
108
  };
103
109
  //# 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\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 const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\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, no queuing\n return fn(...args)\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 }\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\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 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter(\n (item) => !item.nextRetryAt || item.nextRetryAt <= now,\n )\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n\n const outcomes = await Promise.allSettled(\n pending.map(async (item): Promise<'succeeded' | 'failed' | 'retrying' | 'skipped'> => {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n store.updateQueueItem(item.id, { status: 'replaying' })\n await idbUpdateQueueItem(item.id, { status: 'replaying' })\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 delay so the UI can show the success state\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\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 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 )\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else { result.attempted++; result[outcome]++ }\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","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","persistAndQueue","actionName","id","item","idbAddToQueue","reg","getSwRegistration","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","candidates","idbGetPendingItems","now","pending","result","outcomes","idbUpdateQueueItem","completedAt","idbRemoveFromQueue","err","nextRetryAt","o","outcome","clearQueue","idbClearQueue"],"mappings":";;;AAmBA,MAAMA,wBAAsB,IAAA;AAE5B,SAASC,IAAM;AACb,SAAO,OAAO,WAAA;AAChB;AAGO,SAASC,EACdC,GACAC,GAC8B;AAG9B,QAAMC,IAAWD,EAAO,QAAQD,EAAG,QAAQF,EAAA;AAU3C,EAAAD,EAAgB,IAAIK,GAAUF,CAAkC;AAEhE,QAAMG,IAAU,UAAUC,MAAiD;AACzE,UAAM,EAAE,UAAAC,EAAA,IAAaC,EAAc,SAAA;AAEnC,QAAIL,EAAO,gBAAgB,aAAa;AACtC,UAAI,CAACI;AACH,eAAOE,EAAgBL,GAAUA,GAAUE,GAAMH,CAAM;AAGzD,UAAI;AACF,eAAO,MAAMD,EAAG,GAAGI,CAAI;AAAA,MACzB,QAAQ;AACN,eAAOG,EAAgBL,GAAUA,GAAUE,GAAMH,CAAM;AAAA,MACzD;AAAA,IACF;AAGA,WAAOD,EAAG,GAAGI,CAAI;AAAA,EACnB;AAEA,gBAAO,eAAeD,GAAS,MAAM,EAAE,OAAOD,GAAU,UAAU,IAAO,GACzE,OAAO,eAAeC,GAAS,UAAU,EAAE,OAAOF,GAAQ,UAAU,IAAO,GAEpEE;AACT;AAWA,eAAeI,EACbL,GACAM,GACAJ,GACAH,GACuB;AAQvB,QAAMQ,IAAKX,EAAA,GACLY,IAAwB;AAAA,IAC5B,IAAAD;AAAA,IACA,UAAAP;AAAA,IACA,YAAAM;AAAA,IACA,MAAAJ;AAAA,IACA,UAAU,KAAK,IAAA;AAAA,IACf,YAAY;AAAA,IACZ,YAAYH,EAAO,cAAc;AAAA,IACjC,QAAQ;AAAA,EAAA;AAGV,QAAMU,EAAcD,CAAI,GACxBJ,EAAc,SAAA,EAAW,aAAaI,CAAI;AAK1C,MAAI;AACF,UAAME,IAAMC,EAAA;AACZ,IAAID,KAAO,UAAUA,KACnB,MAAOA,EAAsE,KAAK,SAAS,oBAAoB;AAAA,EAEnH,QAAQ;AAAA,EAER;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,IAAAH;AAAA,IACA,SAAS,IAAID,CAAU;AAAA,EAAA;AAE3B;AAGA,SAASM,EAAUC,GAA4B;AAE7C,SADa,KAAK,IAAI,MAAO,KAAKA,GAAY,GAAO,KACtC,MAAM,KAAK,OAAA,IAAW;AACvC;AAEA,IAAIC,IAAa;AAEjB,eAAsBC,IAAqC;AACzD,QAAMC,IAAQZ,EAAc,SAAA;AAC5B,MAAI,CAACY,EAAM,YAAYF;AACrB,WAAO,EAAE,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,EAAA;AAExE,EAAAA,IAAa;AACb,MAAI;AACF,WAAO,MAAMG,EAAeD,CAAK;AAAA,EACnC,UAAA;AACE,IAAAF,IAAa;AAAA,EACf;AACF;AAEA,eAAeG,EAAeD,GAAyE;AAErG,QAAME,IAAa,MAAMC,EAAA,GACnBC,IAAM,KAAK,IAAA,GACXC,IAAUH,EAAW;AAAA,IACzB,CAACV,MAAS,CAACA,EAAK,eAAeA,EAAK,eAAeY;AAAA,EAAA,GAG/CE,IAAuB,EAAE,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,EAAA,GAEtFC,IAAW,MAAM,QAAQ;AAAA,IAC7BF,EAAQ,IAAI,OAAOb,MAAmE;AACpF,YAAMV,IAAKH,EAAgB,IAAIa,EAAK,QAAQ;AAC5C,UAAI,CAACV,EAAI,QAAO;AAEhB,MAAAkB,EAAM,gBAAgBR,EAAK,IAAI,EAAE,QAAQ,aAAa,GACtD,MAAMgB,EAAmBhB,EAAK,IAAI,EAAE,QAAQ,aAAa;AAEzD,UAAI;AACF,cAAMV,EAAG,GAAIU,EAAK,IAAkB;AACpC,cAAMiB,IAAc,KAAK,IAAA;AACzB,eAAAT,EAAM,gBAAgBR,EAAK,IAAI,EAAE,QAAQ,aAAa,aAAAiB,GAAa,GACnE,MAAMD,EAAmBhB,EAAK,IAAI,EAAE,QAAQ,aAAa,aAAAiB,GAAa,GAGtE,WAAW,MAAM;AACf,UAAAT,EAAM,gBAAgBR,EAAK,EAAE,GAC7BkB,EAAmBlB,EAAK,EAAE;AAAA,QAC5B,GAAG,GAAI,GACA;AAAA,MACT,SAASmB,GAAK;AACZ,cAAMd,IAAaL,EAAK,aAAa;AACrC,YAAIK,KAAcL,EAAK;AACrB,iBAAAQ,EAAM,gBAAgBR,EAAK,IAAI,EAAE,QAAQ,UAAU,OAAO,OAAOmB,CAAG,GAAG,YAAAd,EAAA,CAAY,GACnF,MAAMW,EAAmBhB,EAAK,IAAI,EAAE,QAAQ,UAAU,OAAO,OAAOmB,CAAG,GAAG,YAAAd,EAAA,CAAY,GAC/E;AACF;AACL,gBAAMe,IAAc,KAAK,IAAA,IAAQhB,EAAUC,CAAU;AACrD,iBAAAG,EAAM,gBAAgBR,EAAK,IAAI,EAAE,QAAQ,WAAW,YAAAK,GAAY,aAAAe,GAAa,GAC7E,MAAMJ,EAAmBhB,EAAK,IAAI,EAAE,QAAQ,WAAW,YAAAK,GAAY,aAAAe,GAAa,GACzE;AAAA,QACT;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EAAA;AAGH,aAAWC,KAAKN,GAAU;AACxB,UAAMO,IAAUD,EAAE,WAAW,cAAcA,EAAE,QAAQ;AACrD,IAAIC,MAAY,YAAaR,EAAO,aAC7BA,EAAO,aAAaA,EAAOQ,CAAO;AAAA,EAC3C;AAEA,SAAOR;AACT;AAGA,eAAsBS,IAA4B;AAChD,QAAMC,EAAA,GACN5B,EAAc,SAAA,EAAW,aAAa,EAAE;AAC1C;"}
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\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 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 }\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\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 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter(\n (item) => !item.nextRetryAt || item.nextRetryAt <= now,\n )\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n\n const outcomes = await Promise.allSettled(\n pending.map(async (item): Promise<'succeeded' | 'failed' | 'retrying' | 'skipped'> => {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n store.updateQueueItem(item.id, { status: 'replaying' })\n await idbUpdateQueueItem(item.id, { status: 'replaying' })\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 delay so the UI can show the success state\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\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 )\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else { result.attempted++; result[outcome]++ }\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","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","candidates","idbGetPendingItems","now","pending","result","outcomes","idbUpdateQueueItem","completedAt","idbRemoveFromQueue","nextRetryAt","o","outcome","clearQueue","idbClearQueue"],"mappings":"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"}
package/dist/eidos.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const j=require("react/jsx-runtime"),R=require("react");let y;const D=new Set;function W(){D.forEach(e=>e())}function g(e){y={...y,...e(y)},W()}y={isOnline:typeof navigator<"u"?navigator.onLine:!0,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>g(()=>({isOnline:e})),setSwStatus:(e,t)=>g(()=>({swStatus:e,swError:t})),registerResource:(e,t)=>g(s=>({resources:{...s.resources,[e]:t}})),updateResource:(e,t)=>g(s=>({resources:{...s.resources,[e]:s.resources[e]?{...s.resources[e],...t}:s.resources[e]}})),unregisterResource:e=>g(t=>{const{[e]:s,...r}=t.resources;return{resources:r}}),addQueueItem:e=>g(t=>({queue:[...t.queue,e]})),updateQueueItem:(e,t)=>g(s=>({queue:s.queue.map(r=>r.id===e?{...r,...t}:r)})),removeQueueItem:e=>g(t=>({queue:t.queue.filter(s=>s.id!==e)})),hydrateQueue:e=>g(()=>({queue:e}))};function F(){return y}function $(e){return D.add(e),()=>{D.delete(e)}}const c={getState:F,subscribe:$,setState:e=>{const t=typeof e=="function"?e(y):e;y={...y,...t},W()}};let w=null,P=[];function G(){return w}async function K(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){c.getState().setSwStatus("unsupported");return}const t=c.getState();t.setSwStatus("registering");try{w=await navigator.serviceWorker.register(e,{scope:"/"}),await V(w),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",X),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),Z()}catch(s){t.setSwStatus("error",String(s))}}function V(e){return new Promise(t=>{if(e.active){t();return}const s=e.installing??e.waiting;if(!s){t();return}const r=setTimeout(t,1e4);s.addEventListener("statechange",function n(){s.state==="activated"&&(clearTimeout(r),s.removeEventListener("statechange",n),t())})})}function k(e){const t=w==null?void 0:w.active;t?t.postMessage(e):P.push(e)}let x=null;function Y(e){x=e}function z(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&w!==null&&"sync"in w}catch{return!1}}function X(e){const t=e.data;if(!(t!=null&&t.type))return;const s=c.getState(),{type:r,url:n}=t;if(r==="EIDOS_BACKGROUND_SYNC"){x==null||x();return}if(n)switch(r){case"EIDOS_CACHE_HIT":{const i=s.resources[n];s.updateResource(n,{status:"fresh",lastEvent:"cache-hit",cacheHits:((i==null?void 0:i.cacheHits)??0)+1});break}case"EIDOS_CACHE_UPDATED":{s.updateResource(n,{status:"fresh",lastEvent:"cache-updated",cachedAt:Date.now()});break}case"EIDOS_NETWORK_ERROR":{s.updateResource(n,{status:"error",lastEvent:"network-error"});break}}}function J(e){k({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),c.getState().setOnline(!e)}function Z(){const e=w==null?void 0:w.active;if(e){for(const t of P)e.postMessage(t);P=[]}}const b=new Map,_=new Map;let I=null;function ee(e){I=e}function S(e){return e.includes("*")||/:[^/]+/.test(e)}function te(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function O(e,t){return new Error(`[eidos] resource('${e}') is a URL pattern — ${t}() is not supported on pattern handles. The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`)}function se(e,t){if(b.has(e))return b.get(e);const s=re(e,t),r=S(e)?te(e):void 0,n={url:e,config:t,strategy:s,status:"idle",cacheHits:0,cacheMisses:0};c.getState().registerResource(e,n),k({type:"EIDOS_REGISTER_RESOURCE",url:e,strategy:s.swStrategy,cacheName:s.cacheName,...r!==void 0&&{pattern:r}});const i={url:e,config:t,strategy:s,fetch:async()=>{if(S(e))throw O(e,"fetch");const a=_.get(e);if(a)return a.then(u=>u.clone());const o=ne(e,t,s);return _.set(e,o),o.finally(()=>_.delete(e)),o.then(u=>u.clone())},json:async()=>{if(S(e))throw O(e,"json");return(await i.fetch()).json()},query:()=>{if(S(e))throw O(e,"query");return{queryKey:["eidos",e],queryFn:()=>i.json()}},prefetch:async()=>{if(S(e))throw O(e,"prefetch");await i.fetch()},invalidate:async()=>{k({type:"EIDOS_CLEAR_CACHE",url:e});const a=await caches.open(s.cacheName).catch(()=>null);if(a){const o=await a.keys(),u=r?new RegExp(r):null,f=e.startsWith("http");await Promise.all(o.filter(d=>{const l=d.url,T=new URL(l).pathname;return u?u.test(f?l:T):f?l===e:l===e||T===e}).map(d=>a.delete(d)))}S(e)||c.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),I==null||I(["eidos",e])},unregister:()=>{b.delete(e),k({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),c.getState().unregisterResource(e)}};return b.set(e,i),i}async function ne(e,t,s){const r=c.getState();r.updateResource(e,{status:"fetching",fetchedAt:Date.now()});const n=await caches.open(s.cacheName).catch(()=>null);try{if(s.swStrategy!=="network-first"){const o=n?await n.match(e).catch(()=>null):null,u=c.getState().resources[e],f=t.maxAge!==void 0&&(u==null?void 0:u.cachedAt)!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(o&&!f)return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:((u==null?void 0:u.cacheHits)??0)+1}),s.swStrategy==="stale-while-revalidate"&&fetch(e).then(async l=>{l.ok&&n&&(await n.put(e,l.clone()),c.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),o;const d=c.getState().resources[e];r.updateResource(e,{cacheMisses:((d==null?void 0:d.cacheMisses)??0)+1})}const i=await fetch(e);if(i.ok)return n&&await n.put(e,i.clone()),r.updateResource(e,{status:"fresh",cachedAt:Date.now(),lastEvent:"cache-updated"}),i;r.updateResource(e,{status:i.status===503?"offline":"error"});const a=i.headers.get("X-Eidos-Offline")==="true";throw new Error(a?`offline: no cached response for ${e}`:`${i.status} ${i.statusText}`)}catch(i){const a=n?await n.match(e).catch(()=>null):null;if(a){const o=c.getState().resources[e];return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:((o==null?void 0:o.cacheHits)??0)+1}),a}throw r.updateResource(e,{status:"error"}),i}}function re(e,t){const s=t.strategy;return t.offline?M(s??"stale-while-revalidate",e,t.cacheName):M(s??"network-first",e,t.cacheName)}const ae={"stale-while-revalidate":{name:"StaleWhileRevalidate",reasoning:"offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.",behavior:["Cache hit → return immediately, kick off background revalidation","Cache miss → fetch from network, cache the response, return it","Offline → return cached version if available, 503 if not","Reconnect → next request triggers a background refresh"],equivalentCode:`// Workbox equivalent
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("react/jsx-runtime"),R=require("react");let y;const D=new Set;function W(){D.forEach(e=>e())}function w(e){y={...y,...e(y)},W()}y={isOnline:typeof navigator<"u"?navigator.onLine:!0,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>w(()=>({isOnline:e})),setSwStatus:(e,t)=>w(()=>({swStatus:e,swError:t})),registerResource:(e,t)=>w(s=>({resources:{...s.resources,[e]:t}})),updateResource:(e,t)=>w(s=>({resources:{...s.resources,[e]:s.resources[e]?{...s.resources[e],...t}:s.resources[e]}})),unregisterResource:e=>w(t=>{const{[e]:s,...r}=t.resources;return{resources:r}}),addQueueItem:e=>w(t=>({queue:[...t.queue,e]})),updateQueueItem:(e,t)=>w(s=>({queue:s.queue.map(r=>r.id===e?{...r,...t}:r)})),removeQueueItem:e=>w(t=>({queue:t.queue.filter(s=>s.id!==e)})),hydrateQueue:e=>w(()=>({queue:e}))};function F(){return y}function G(e){return D.add(e),()=>{D.delete(e)}}const c={getState:F,subscribe:G,setState:e=>{const t=typeof e=="function"?e(y):e;y={...y,...t},W()}};let p=null,P=[];function K(){return p}async function V(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){c.getState().setSwStatus("unsupported");return}const t=c.getState();t.setSwStatus("registering");try{p=await navigator.serviceWorker.register(e,{scope:"/"}),await Y(p),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",J),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),ee()}catch(s){t.setSwStatus("error",String(s))}}function Y(e){return new Promise(t=>{if(e.active){t();return}const s=e.installing??e.waiting;if(!s){t();return}const r=setTimeout(t,1e4);s.addEventListener("statechange",function n(){s.state==="activated"&&(clearTimeout(r),s.removeEventListener("statechange",n),t())})})}function q(e){const t=p==null?void 0:p.active;t?t.postMessage(e):P.push(e)}let x=null;function z(e){x=e}function X(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&p!==null&&"sync"in p}catch{return!1}}function J(e){const t=e.data;if(!(t!=null&&t.type))return;const s=c.getState(),{type:r,url:n}=t;if(r==="EIDOS_BACKGROUND_SYNC"){x==null||x();return}if(n)switch(r){case"EIDOS_CACHE_HIT":{const o=s.resources[n];s.updateResource(n,{status:"fresh",lastEvent:"cache-hit",cacheHits:((o==null?void 0:o.cacheHits)??0)+1});break}case"EIDOS_CACHE_UPDATED":{s.updateResource(n,{status:"fresh",lastEvent:"cache-updated",cachedAt:Date.now()});break}case"EIDOS_NETWORK_ERROR":{s.updateResource(n,{status:"error",lastEvent:"network-error"});break}}}function Z(e){q({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),c.getState().setOnline(!e)}function ee(){const e=p==null?void 0:p.active;if(e){for(const t of P)e.postMessage(t);P=[]}}const b=new Map,A=new Map;let I=null;function te(e){I=e}function S(e){return e.includes("*")||/:[^/]+/.test(e)}function se(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function k(e,t){return new Error(`[eidos] resource('${e}') is a URL pattern — ${t}() is not supported on pattern handles. The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`)}function ne(e,t){if(b.has(e))return b.get(e);const s=ae(e,t),r=S(e)?se(e):void 0,n={url:e,config:t,strategy:s,status:"idle",cacheHits:0,cacheMisses:0};c.getState().registerResource(e,n),q({type:"EIDOS_REGISTER_RESOURCE",url:e,strategy:s.swStrategy,cacheName:s.cacheName,...r!==void 0&&{pattern:r}});const o={url:e,config:t,strategy:s,fetch:async()=>{if(S(e))throw k(e,"fetch");const a=A.get(e);if(a)return a.then(u=>u.clone());const i=re(e,t,s);return A.set(e,i),i.finally(()=>A.delete(e)),i.then(u=>u.clone())},json:async()=>{if(S(e))throw k(e,"json");return(await o.fetch()).json()},query:()=>{if(S(e))throw k(e,"query");return{queryKey:["eidos",e],queryFn:()=>o.json()}},prefetch:async()=>{if(S(e))throw k(e,"prefetch");await o.fetch()},invalidate:async()=>{q({type:"EIDOS_CLEAR_CACHE",url:e});const a=await caches.open(s.cacheName).catch(()=>null);if(a){const i=await a.keys(),u=r?new RegExp(r):null,l=e.startsWith("http");await Promise.all(i.filter(d=>{const f=d.url,T=new URL(f).pathname;return u?u.test(l?f:T):l?f===e:f===e||T===e}).map(d=>a.delete(d)))}S(e)||c.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),I==null||I(["eidos",e])},unregister:()=>{b.delete(e),q({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),c.getState().unregisterResource(e)}};return b.set(e,o),o}async function re(e,t,s){const r=c.getState();r.updateResource(e,{status:"fetching",fetchedAt:Date.now()});const n=await caches.open(s.cacheName).catch(()=>null);try{if(s.swStrategy!=="network-first"){const i=n?await n.match(e).catch(()=>null):null,u=c.getState().resources[e],l=t.maxAge!==void 0&&(u==null?void 0:u.cachedAt)!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(i&&!l)return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:((u==null?void 0:u.cacheHits)??0)+1}),s.swStrategy==="stale-while-revalidate"&&fetch(e,{signal:AbortSignal.timeout(5e3)}).then(async f=>{f.ok&&n&&(await n.put(e,f.clone()),c.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),i;const d=c.getState().resources[e];r.updateResource(e,{cacheMisses:((d==null?void 0:d.cacheMisses)??0)+1})}const o=await fetch(e);if(o.ok)return n&&await n.put(e,o.clone()),r.updateResource(e,{status:"fresh",cachedAt:Date.now(),lastEvent:"cache-updated"}),o;r.updateResource(e,{status:o.status===503?"offline":"error"});const a=o.headers.get("X-Eidos-Offline")==="true";throw new Error(a?`offline: no cached response for ${e}`:`${o.status} ${o.statusText}`)}catch(o){const a=n?await n.match(e).catch(()=>null):null;if(a){const i=c.getState().resources[e];return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:((i==null?void 0:i.cacheHits)??0)+1}),a}throw r.updateResource(e,{status:"error"}),o}}function ae(e,t){const s=t.strategy;return t.offline?j(s??"stale-while-revalidate",e,t.cacheName):j(s??"network-first",e,t.cacheName)}const oe={"stale-while-revalidate":{name:"StaleWhileRevalidate",reasoning:"offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.",behavior:["Cache hit → return immediately, kick off background revalidation","Cache miss → fetch from network, cache the response, return it","Offline → return cached version if available, 503 if not","Reconnect → next request triggers a background refresh"],equivalentCode:`// Workbox equivalent
2
2
  new StaleWhileRevalidate({
3
3
  cacheName: 'eidos-resources-v1',
4
4
  plugins: [new ExpirationPlugin({ maxEntries: 60 })],
@@ -10,5 +10,5 @@ new CacheFirst({
10
10
  new NetworkFirst({
11
11
  cacheName: 'eidos-resources-v1',
12
12
  networkTimeoutSeconds: 3,
13
- })`}};function M(e,t,s){return{...ae[e],swStrategy:e,cacheName:s??"eidos-resources-v1"}}const ie="eidos",oe=1,h="action-queue";let A=null;function m(){return A?Promise.resolve(A):new Promise((e,t)=>{const s=indexedDB.open(ie,oe);s.onupgradeneeded=r=>{const n=r.target.result;if(!n.objectStoreNames.contains(h)){const i=n.createObjectStore(h,{keyPath:"id"});i.createIndex("status","status",{unique:!1}),i.createIndex("actionId","actionId",{unique:!1})}},s.onsuccess=()=>{A=s.result,e(s.result)},s.onerror=()=>t(s.error)})}async function ce(e){const t=await m();return new Promise((s,r)=>{const n=t.transaction(h,"readwrite");n.objectStore(h).add(e),n.oncomplete=()=>s(),n.onerror=()=>r(n.error)})}async function ue(){const e=await m();return new Promise((t,s)=>{const n=e.transaction(h,"readonly").objectStore(h).getAll();n.onsuccess=()=>t(n.result),n.onerror=()=>s(n.error)})}async function q(e,t){const s=await m();return new Promise((r,n)=>{const i=s.transaction(h,"readwrite"),a=i.objectStore(h),o=a.get(e);o.onsuccess=()=>{o.result&&a.put({...o.result,...t})},i.oncomplete=()=>r(),i.onerror=()=>n(i.error)})}async function de(e){const t=await m();return new Promise((s,r)=>{const n=t.transaction(h,"readwrite");n.objectStore(h).delete(e),n.oncomplete=()=>s(),n.onerror=()=>r(n.error)})}async function fe(){const e=await m();return new Promise((t,s)=>{const n=e.transaction(h,"readonly").objectStore(h).index("status"),i=[];let a=0;function o(d){if(d){s(d);return}++a===2&&t(i)}const u=n.openCursor(IDBKeyRange.only("pending"));u.onsuccess=d=>{const l=d.target.result;l?(i.push(l.value),l.continue()):o()},u.onerror=()=>o(u.error);const f=n.openCursor(IDBKeyRange.only("failed"));f.onsuccess=d=>{const l=d.target.result;l?(i.push(l.value),l.continue()):o()},f.onerror=()=>o(f.error)})}async function le(){const e=await m();return new Promise((t,s)=>{const r=e.transaction(h,"readwrite");r.objectStore(h).clear(),r.oncomplete=()=>t(),r.onerror=()=>s(r.error)})}const B=new Map;function H(){return crypto.randomUUID()}function he(e,t){const s=t.name||e.name||H();B.set(s,e);const r=async(...n)=>{const{isOnline:i}=c.getState();if(t.reliability==="neverLose"){if(!i)return U(s,s,n,t);try{return await e(...n)}catch{return U(s,s,n,t)}}return e(...n)};return Object.defineProperty(r,"id",{value:s,writable:!1}),Object.defineProperty(r,"config",{value:t,writable:!1}),r}async function U(e,t,s,r){const n=H(),i={id:n,actionId:e,actionName:t,args:s,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending"};await ce(i),c.getState().addQueueItem(i);try{const a=G();a&&"sync"in a&&await a.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:n,message:`"${t}" queued — will execute when online`}}function pe(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}let C=!1;async function Q(){const e=c.getState();if(!e.isOnline||C)return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0};C=!0;try{return await we(e)}finally{C=!1}}async function we(e){const t=await fe(),s=Date.now(),r=t.filter(a=>!a.nextRetryAt||a.nextRetryAt<=s),n={attempted:0,succeeded:0,failed:0,retrying:0,skipped:0},i=await Promise.allSettled(r.map(async a=>{const o=B.get(a.actionId);if(!o)return"skipped";e.updateQueueItem(a.id,{status:"replaying"}),await q(a.id,{status:"replaying"});try{await o(...a.args);const u=Date.now();return e.updateQueueItem(a.id,{status:"succeeded",completedAt:u}),await q(a.id,{status:"succeeded",completedAt:u}),setTimeout(()=>{e.removeQueueItem(a.id),de(a.id)},3e3),"succeeded"}catch(u){const f=a.retryCount+1;if(f>=a.maxRetries)return e.updateQueueItem(a.id,{status:"failed",error:String(u),retryCount:f}),await q(a.id,{status:"failed",error:String(u),retryCount:f}),"failed";{const d=Date.now()+pe(f);return e.updateQueueItem(a.id,{status:"pending",retryCount:f,nextRetryAt:d}),await q(a.id,{status:"pending",retryCount:f,nextRetryAt:d}),"retrying"}}}));for(const a of i){const o=a.status==="fulfilled"?a.value:"failed";o==="skipped"?n.skipped++:(n.attempted++,n[o]++)}return n}async function ge(){await le(),c.getState().hydrateQueue([])}let N=!1,v=null;async function L(e={}){if(N)return;N=!0;const t=e.swPath??"/eidos-sw.js",s=e.autoReplay??!0;try{const r=await ue();r.length>0&&c.getState().hydrateQueue(r)}catch{}try{await K(t)}catch{}if(Y(()=>{c.getState().isOnline&&setTimeout(Q,200)}),s){let r=c.getState().isOnline;v=c.subscribe(()=>{const{isOnline:a}=c.getState(),o=a&&!r;r=a,o&&setTimeout(Q,600)});const n=c.getState(),i=n.queue.some(a=>a.status==="pending"||a.status==="failed");n.isOnline&&i&&setTimeout(Q,1200)}}function ye(){v==null||v(),v=null,N=!1}function Se({children:e,swPath:t,autoReplay:s}){return R.useEffect(()=>{L({swPath:t,autoReplay:s})},[]),j.jsx(j.Fragment,{children:e})}function p(e){const t=e??(s=>s);return R.useSyncExternalStore(c.subscribe,()=>t(c.getState()))}function me(){return p()}function Ee(e){return p(t=>t.resources[e])}function ve(){return p(e=>e.queue)}function Re(e){return p(t=>t.queue.find(s=>s.id===e))}function be(){const e=p(r=>r.isOnline),t=p(r=>r.swStatus),s=p(r=>r.swError);return{isOnline:e,swStatus:t,swError:s}}function Oe(){const e=p(n=>n.queue.filter(i=>i.status==="pending").length),t=p(n=>n.queue.filter(i=>i.status==="failed").length),s=p(n=>n.queue.filter(i=>i.status==="replaying").length),r=p(n=>n.queue.length);return{pending:e,failed:t,replaying:s,total:r}}function qe(e){const t=p(n=>n.queue.length),s=R.useRef(0),r=R.useRef(e);r.current=e,R.useEffect(()=>{s.current>0&&t===0&&r.current(),s.current=t},[t])}const ke="1.0.23";function E(e){return{subscribe(t){return t(e(c.getState())),c.subscribe(()=>t(e(c.getState())))},getState(){return e(c.getState())}}}const xe=E(e=>e),Ie=E(e=>e.queue),Qe=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError})),_e=E(e=>{let t=0,s=0,r=0;for(const n of e.queue)n.status==="pending"?t++:n.status==="failed"?s++:n.status==="replaying"&&r++;return{pending:t,failed:s,replaying:r,total:e.queue.length}});function Ae(e){return E(t=>t.resources[e])}function Ce(e){return E(t=>t.queue.find(s=>s.id===e))}exports.EidosProvider=Se;exports.VERSION=ke;exports._resetEidos=ye;exports.action=he;exports.clearQueue=ge;exports.eidosAction=Ce;exports.eidosQueue=Ie;exports.eidosQueueStats=_e;exports.eidosResource=Ae;exports.eidosStatus=Qe;exports.eidosStore=xe;exports.initEidos=L;exports.isBgSyncSupported=z;exports.replayQueue=Q;exports.resource=se;exports.setOfflineSimulation=J;exports.setQueryInvalidator=ee;exports.useEidos=me;exports.useEidosAction=Re;exports.useEidosOnDrain=qe;exports.useEidosQueue=ve;exports.useEidosQueueStats=Oe;exports.useEidosResource=Ee;exports.useEidosStatus=be;exports.useEidosStore=c;
13
+ })`}};function j(e,t,s){return{...oe[e],swStrategy:e,cacheName:s??"eidos-resources-v1"}}const ie="eidos",ce=1,h="action-queue";let Q=null;function m(){return Q?Promise.resolve(Q):new Promise((e,t)=>{const s=indexedDB.open(ie,ce);s.onupgradeneeded=r=>{const n=r.target.result;if(!n.objectStoreNames.contains(h)){const o=n.createObjectStore(h,{keyPath:"id"});o.createIndex("status","status",{unique:!1}),o.createIndex("actionId","actionId",{unique:!1})}},s.onsuccess=()=>{Q=s.result,e(s.result)},s.onerror=()=>t(s.error)})}async function ue(e){const t=await m();return new Promise((s,r)=>{const n=t.transaction(h,"readwrite");n.objectStore(h).add(e),n.oncomplete=()=>s(),n.onerror=()=>r(n.error)})}async function de(){const e=await m();return new Promise((t,s)=>{const n=e.transaction(h,"readonly").objectStore(h).getAll();n.onsuccess=()=>t(n.result),n.onerror=()=>s(n.error)})}async function O(e,t){const s=await m();return new Promise((r,n)=>{const o=s.transaction(h,"readwrite"),a=o.objectStore(h),i=a.get(e);i.onsuccess=()=>{i.result&&a.put({...i.result,...t})},o.oncomplete=()=>r(),o.onerror=()=>n(o.error)})}async function le(e){const t=await m();return new Promise((s,r)=>{const n=t.transaction(h,"readwrite");n.objectStore(h).delete(e),n.oncomplete=()=>s(),n.onerror=()=>r(n.error)})}async function fe(){const e=await m();return new Promise((t,s)=>{const n=e.transaction(h,"readonly").objectStore(h).index("status"),o=[];let a=0;function i(d){if(d){s(d);return}++a===2&&t(o)}const u=n.openCursor(IDBKeyRange.only("pending"));u.onsuccess=d=>{const f=d.target.result;f?(o.push(f.value),f.continue()):i()},u.onerror=()=>i(u.error);const l=n.openCursor(IDBKeyRange.only("failed"));l.onsuccess=d=>{const f=d.target.result;f?(o.push(f.value),f.continue()):i()},l.onerror=()=>i(l.error)})}async function he(){const e=await m();return new Promise((t,s)=>{const r=e.transaction(h,"readwrite");r.objectStore(h).clear(),r.oncomplete=()=>t(),r.onerror=()=>s(r.error)})}const $=new Map,B=new Map;function H(){return crypto.randomUUID()}function pe(e,t){const s=t.name||e.name||H();$.set(s,e),t.onRollback&&B.set(s,t.onRollback);const r=async(...n)=>{var a,i;const{isOnline:o}=c.getState();if((a=t.onOptimistic)==null||a.call(t,...n),t.reliability==="neverLose"){if(!o)return U(s,s,n,t);try{return await e(...n)}catch{return U(s,s,n,t)}}try{return await e(...n)}catch(u){throw(i=t.onRollback)==null||i.call(t,...n),u}};return Object.defineProperty(r,"id",{value:s,writable:!1}),Object.defineProperty(r,"config",{value:t,writable:!1}),r}async function U(e,t,s,r){const n=H(),o={id:n,actionId:e,actionName:t,args:s,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending"};await ue(o),c.getState().addQueueItem(o);try{const a=K();a&&"sync"in a&&await a.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:n,message:`"${t}" queued — will execute when online`}}function we(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}let C=!1;async function _(){const e=c.getState();if(!e.isOnline||C)return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0};C=!0;try{return await ge(e)}finally{C=!1}}async function ge(e){const t=await fe(),s=Date.now(),r=t.filter(a=>!a.nextRetryAt||a.nextRetryAt<=s),n={attempted:0,succeeded:0,failed:0,retrying:0,skipped:0},o=await Promise.allSettled(r.map(async a=>{var u;const i=$.get(a.actionId);if(!i)return"skipped";e.updateQueueItem(a.id,{status:"replaying"}),await O(a.id,{status:"replaying"});try{await i(...a.args);const l=Date.now();return e.updateQueueItem(a.id,{status:"succeeded",completedAt:l}),await O(a.id,{status:"succeeded",completedAt:l}),setTimeout(()=>{e.removeQueueItem(a.id),le(a.id)},3e3),"succeeded"}catch(l){const d=a.retryCount+1;if(d>=a.maxRetries)return e.updateQueueItem(a.id,{status:"failed",error:String(l),retryCount:d}),await O(a.id,{status:"failed",error:String(l),retryCount:d}),(u=B.get(a.actionId))==null||u(...a.args),"failed";{const f=Date.now()+we(d);return e.updateQueueItem(a.id,{status:"pending",retryCount:d,nextRetryAt:f}),await O(a.id,{status:"pending",retryCount:d,nextRetryAt:f}),"retrying"}}}));for(const a of o){const i=a.status==="fulfilled"?a.value:"failed";i==="skipped"?n.skipped++:(n.attempted++,n[i]++)}return n}async function ye(){await he(),c.getState().hydrateQueue([])}let N=!1,v=null;async function L(e={}){if(N)return;N=!0;const t=e.swPath??"/eidos-sw.js",s=e.autoReplay??!0;try{const r=await de();r.length>0&&c.getState().hydrateQueue(r)}catch{}try{await V(t)}catch{}if(z(()=>{c.getState().isOnline&&setTimeout(_,200)}),s){let r=c.getState().isOnline;v=c.subscribe(()=>{const{isOnline:a}=c.getState(),i=a&&!r;r=a,i&&setTimeout(_,600)});const n=c.getState(),o=n.queue.some(a=>a.status==="pending"||a.status==="failed");n.isOnline&&o&&setTimeout(_,1200)}}function Se(){v==null||v(),v=null,N=!1}function me({children:e,swPath:t,autoReplay:s}){return R.useEffect(()=>{L({swPath:t,autoReplay:s})},[]),M.jsx(M.Fragment,{children:e})}function g(e){const t=e??(s=>s);return R.useSyncExternalStore(c.subscribe,()=>t(c.getState()))}function Ee(){return g()}function ve(e){return g(t=>t.resources[e])}function Re(){return g(e=>e.queue)}function be(e){return g(t=>t.queue.find(s=>s.id===e))}function ke(){const e=g(r=>r.isOnline),t=g(r=>r.swStatus),s=g(r=>r.swError);return{isOnline:e,swStatus:t,swError:s}}function Oe(){const e=g(o=>{let a=0,i=0,u=0;for(const l of o.queue)l.status==="pending"?a++:l.status==="failed"?i++:l.status==="replaying"&&u++;return`${a},${i},${u},${o.queue.length}`}),[t,s,r,n]=e.split(",");return{pending:+t,failed:+s,replaying:+r,total:+n}}function qe(e){const t=g(n=>n.queue.length),s=R.useRef(0),r=R.useRef(e);r.current=e,R.useEffect(()=>{s.current>0&&t===0&&r.current(),s.current=t},[t])}const xe="1.0.25";function E(e){return{subscribe(t){return t(e(c.getState())),c.subscribe(()=>t(e(c.getState())))},getState(){return e(c.getState())}}}const Ie=E(e=>e),_e=E(e=>e.queue),Ae=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError})),Qe=E(e=>{let t=0,s=0,r=0;for(const n of e.queue)n.status==="pending"?t++:n.status==="failed"?s++:n.status==="replaying"&&r++;return{pending:t,failed:s,replaying:r,total:e.queue.length}});function Ce(e){return E(t=>t.resources[e])}function De(e){return E(t=>t.queue.find(s=>s.id===e))}exports.EidosProvider=me;exports.VERSION=xe;exports._resetEidos=Se;exports.action=pe;exports.clearQueue=ye;exports.eidosAction=De;exports.eidosQueue=_e;exports.eidosQueueStats=Qe;exports.eidosResource=Ce;exports.eidosStatus=Ae;exports.eidosStore=Ie;exports.initEidos=L;exports.isBgSyncSupported=X;exports.replayQueue=_;exports.resource=ne;exports.setOfflineSimulation=Z;exports.setQueryInvalidator=te;exports.useEidos=Ee;exports.useEidosAction=be;exports.useEidosOnDrain=qe;exports.useEidosQueue=Re;exports.useEidosQueueStats=Oe;exports.useEidosResource=ve;exports.useEidosStatus=ke;exports.useEidosStore=c;
14
14
  //# sourceMappingURL=eidos.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eidos.cjs.js","sources":["../src/store.ts","../src/sw-bridge.ts","../src/resource.ts","../src/idb.ts","../src/action.ts","../src/runtime.ts","../src/react/Provider.tsx","../src/react/hooks.ts","../src/version.ts","../src/stores.ts"],"sourcesContent":["import type { EidosState, ResourceEntry, ActionQueueItem } from './types'\n\nexport interface EidosStore extends EidosState {\n // Online\n setOnline: (online: boolean) => void\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void\n // Resources\n registerResource: (url: string, entry: ResourceEntry) => void\n updateResource: (url: string, update: Partial<ResourceEntry>) => void\n unregisterResource: (url: string) => void\n // Queue\n addQueueItem: (item: ActionQueueItem) => void\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void\n removeQueueItem: (id: string) => void\n hydrateQueue: (items: ActionQueueItem[]) => void\n}\n\ntype Listener = () => void\n\nlet _state: EidosStore\nconst _listeners = new Set<Listener>()\n\nfunction _notify() {\n _listeners.forEach((fn) => fn())\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) }\n _notify()\n}\n\n_state = {\n isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,\n swStatus: 'idle',\n swError: undefined,\n resources: {},\n queue: [],\n\n setOnline: (isOnline) => _set(() => ({ isOnline })),\n\n setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),\n\n registerResource: (url, entry) =>\n _set((s) => ({ resources: { ...s.resources, [url]: entry } })),\n\n updateResource: (url, update) =>\n _set((s) => ({\n resources: {\n ...s.resources,\n [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url],\n },\n })),\n\n unregisterResource: (url) =>\n _set((s) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { [url]: _removed, ...rest } = s.resources\n return { resources: rest }\n }),\n\n addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),\n\n updateQueueItem: (id, update) =>\n _set((s) => ({\n queue: s.queue.map((item) => (item.id === id ? { ...item, ...update } : item)),\n })),\n\n removeQueueItem: (id) => _set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),\n\n hydrateQueue: (items) => _set(() => ({ queue: items })),\n}\n\nfunction _getState() {\n return _state\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener)\n return () => { _listeners.delete(listener) }\n}\n\nexport const useEidosStore = {\n getState: _getState,\n subscribe: _subscribe,\n // Test/devtools helper — merges partial state, preserves action methods.\n setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => {\n const update = typeof partial === 'function' ? partial(_state) : partial\n _state = { ..._state, ...update }\n _notify()\n },\n}\n","import { useEidosStore } from './store'\n\nlet _registration: ServiceWorkerRegistration | null = null\n// Messages sent before the SW activates are buffered here and flushed once\n// the SW is ready. Covers resource registrations, cache clears, offline\n// simulation — anything sent at module scope before EidosProvider mounts.\nlet _pendingMessages: Record<string, unknown>[] = []\n\nexport function getSwRegistration() {\n return _registration\n}\n\nexport async function registerServiceWorker(swPath: string): Promise<void> {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {\n useEidosStore.getState().setSwStatus('unsupported')\n return\n }\n\n const store = useEidosStore.getState()\n store.setSwStatus('registering')\n\n try {\n _registration = await navigator.serviceWorker.register(swPath, { scope: '/' })\n\n await waitForActivation(_registration)\n\n store.setSwStatus('active')\n\n // Receive messages from SW\n navigator.serviceWorker.addEventListener('message', onSwMessage)\n\n // Track online/offline\n window.addEventListener('online', () => store.setOnline(true))\n window.addEventListener('offline', () => store.setOnline(false))\n\n flushPendingMessages()\n } catch (err) {\n store.setSwStatus('error', String(err))\n }\n}\n\nfunction waitForActivation(reg: ServiceWorkerRegistration): Promise<void> {\n return new Promise((resolve) => {\n if (reg.active) { resolve(); return }\n const sw = reg.installing ?? reg.waiting\n if (!sw) { resolve(); return }\n\n // Resolve after 10s regardless — another tab may be blocking activation\n const timer = setTimeout(resolve, 10_000)\n\n sw.addEventListener('statechange', function handler() {\n if (sw.state === 'activated') {\n clearTimeout(timer)\n sw.removeEventListener('statechange', handler)\n resolve()\n }\n })\n })\n}\n\nexport function sendToWorker(message: Record<string, unknown>): void {\n const sw = _registration?.active\n if (sw) {\n sw.postMessage(message)\n } else {\n _pendingMessages.push(message)\n }\n}\n\nlet _bgSyncHandler: (() => void) | null = null\n\nexport function registerBgSyncHandler(fn: () => void): void {\n _bgSyncHandler = fn\n}\n\nexport function isBgSyncSupported(): boolean {\n try {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n _registration !== null &&\n 'sync' in _registration\n )\n } catch {\n return false\n }\n}\n\nfunction onSwMessage(event: MessageEvent): void {\n const data = event.data as { type: string; url?: string; strategy?: string }\n if (!data?.type) return\n\n const store = useEidosStore.getState()\n const { type, url } = data\n\n if (type === 'EIDOS_BACKGROUND_SYNC') {\n _bgSyncHandler?.()\n return\n }\n\n if (!url) return\n\n switch (type) {\n case 'EIDOS_CACHE_HIT': {\n const current = store.resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n break\n }\n case 'EIDOS_CACHE_UPDATED': {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-updated',\n cachedAt: Date.now(),\n })\n break\n }\n case 'EIDOS_NETWORK_ERROR': {\n store.updateResource(url, {\n status: 'error',\n lastEvent: 'network-error',\n })\n break\n }\n }\n}\n\nexport function setOfflineSimulation(enabled: boolean): void {\n sendToWorker({ type: 'EIDOS_SIMULATE_OFFLINE', enabled })\n useEidosStore.getState().setOnline(!enabled)\n}\n\nfunction flushPendingMessages(): void {\n const sw = _registration?.active\n if (!sw) return\n for (const msg of _pendingMessages) sw.postMessage(msg)\n _pendingMessages = []\n}\n","import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>()\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void\nlet _queryInvalidator: QueryInvalidator | null = null\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url)\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') // :param → single-segment wildcard\n + '$'\n )\n}\n\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, config)\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n }\n\n useEidosStore.getState().registerResource(url, entry)\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n })\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url)\n if (existing) return existing.then((r) => r.clone())\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy)\n _inflightRequests.set(url, task)\n task.finally(() => _inflightRequests.delete(url))\n return task.then((r) => r.clone())\n },\n\n json: async () => {\n if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url })\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n if (cache) {\n const keys = await cache.keys()\n const patternRe = regexStr ? new RegExp(regexStr) : null\n const isCrossOrigin = url.startsWith('http')\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url\n const p = new URL(rUrl).pathname\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p)\n }\n return isCrossOrigin ? rUrl === url : (rUrl === url || p === url)\n })\n .map((r) => cache.delete(r)),\n )\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n })\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url])\n },\n\n unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\n }\n\n _registry.set(url, handle)\n return handle\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState()\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() })\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url]\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url)\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone())\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n }\n })\n .catch(() => {\n /* offline — cached version stays valid */\n })\n }\n\n return cached\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n })\n }\n\n const response = await fetch(url)\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone())\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n return response\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' })\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true'\n throw new Error(\n isOffline ? `offline: no cached response for ${url}` : `${response.status} ${response.statusText}`,\n )\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n return fallback\n }\n\n store.updateResource(url, { status: 'error' })\n throw err\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n name: 'CacheFirst',\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n name: 'NetworkFirst',\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n}\n\nfunction buildStrategy(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\n }\n}\n","import type { ActionQueueItem } from './types'\n\nconst DB_NAME = 'eidos'\nconst DB_VERSION = 1\nconst QUEUE_STORE = 'action-queue'\n\nlet _db: IDBDatabase | null = null\n\nfunction openDB(): Promise<IDBDatabase> {\n if (_db) return Promise.resolve(_db)\n\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION)\n\n req.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(QUEUE_STORE)) {\n const store = db.createObjectStore(QUEUE_STORE, { keyPath: 'id' })\n store.createIndex('status', 'status', { unique: false })\n store.createIndex('actionId', 'actionId', { unique: false })\n }\n }\n\n req.onsuccess = () => {\n _db = req.result\n resolve(req.result)\n }\n\n req.onerror = () => reject(req.error)\n })\n}\n\nexport async function idbAddToQueue(item: ActionQueueItem): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).add(item)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nexport async function idbGetQueue(): Promise<ActionQueueItem[]> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly')\n const req = tx.objectStore(QUEUE_STORE).getAll()\n req.onsuccess = () => resolve(req.result as ActionQueueItem[])\n req.onerror = () => reject(req.error)\n })\n}\n\nexport async function idbUpdateQueueItem(\n id: string,\n update: Partial<ActionQueueItem>,\n): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n const store = tx.objectStore(QUEUE_STORE)\n const get = store.get(id)\n get.onsuccess = () => {\n if (get.result) {\n store.put({ ...get.result, ...update })\n } else if (import.meta.env.DEV) {\n console.warn(`[eidos] idbUpdateQueueItem: item \"${id}\" not found — store/IDB may have diverged`)\n }\n }\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nexport async function idbRemoveFromQueue(id: string): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).delete(id)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\n// Uses the status index to fetch only pending/failed items — avoids a full\n// table scan when the queue has many succeeded/replaying entries.\nexport async function idbGetPendingItems(): Promise<ActionQueueItem[]> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly')\n const index = tx.objectStore(QUEUE_STORE).index('status')\n const results: ActionQueueItem[] = []\n\n let done = 0\n function finish(err?: DOMException | null) {\n if (err) { reject(err); return }\n if (++done === 2) resolve(results)\n }\n\n const pendingReq = index.openCursor(IDBKeyRange.only('pending'))\n pendingReq.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result\n if (cursor) { results.push(cursor.value as ActionQueueItem); cursor.continue() }\n else finish()\n }\n pendingReq.onerror = () => finish(pendingReq.error)\n\n const failedReq = index.openCursor(IDBKeyRange.only('failed'))\n failedReq.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result\n if (cursor) { results.push(cursor.value as ActionQueueItem); cursor.continue() }\n else finish()\n }\n failedReq.onerror = () => finish(failedReq.error)\n })\n}\n\nexport async function idbClearQueue(): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).clear()\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n","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\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 const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\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, no queuing\n return fn(...args)\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 }\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\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 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter(\n (item) => !item.nextRetryAt || item.nextRetryAt <= now,\n )\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n\n const outcomes = await Promise.allSettled(\n pending.map(async (item): Promise<'succeeded' | 'failed' | 'retrying' | 'skipped'> => {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n store.updateQueueItem(item.id, { status: 'replaying' })\n await idbUpdateQueueItem(item.id, { status: 'replaying' })\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 delay so the UI can show the success state\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\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 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 )\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else { result.attempted++; result[outcome]++ }\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","import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge'\nimport { replayQueue } from './action'\nimport { useEidosStore } from './store'\nimport { idbGetQueue } from './idb'\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean\n}\n\nlet _initialized = false\nlet _unsubscribe: (() => void) | null = null\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n if (_initialized) return\n _initialized = true\n\n const swPath = config.swPath ?? '/eidos-sw.js'\n const autoReplay = config.autoReplay ?? true\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue()\n if (persisted.length > 0) {\n useEidosStore.getState().hydrateQueue(persisted)\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath)\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200)\n }\n })\n\n if (autoReplay) {\n // ── Subscribe to the store instead of window.addEventListener('online')\n //\n // WHY: setOfflineSimulation() updates the store directly but never fires a\n // real browser `online` event. Watching the store catches both:\n // • Real network reconnects (sw-bridge updates store on window.online)\n // • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n //\n let prevIsOnline = useEidosStore.getState().isOnline\n\n _unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState()\n const justCameOnline = isOnline && !prevIsOnline\n prevIsOnline = isOnline\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600)\n }\n })\n\n // Replay any pending items that survived a page reload\n const store = useEidosStore.getState()\n const hasPending = store.queue.some((q) => q.status === 'pending' || q.status === 'failed')\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200)\n }\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState()\n console.groupCollapsed('%c⚡ Eidos', 'color:#38bdf8;font-weight:bold')\n console.log('SW path :', swPath)\n console.log('Auto-replay:', autoReplay)\n console.log('SW status :', store.swStatus)\n console.groupEnd()\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.()\n _unsubscribe = null\n _initialized = false\n}\n","import { useEffect, type ReactNode } from 'react'\nimport { initEidos, type EidosConfig } from '../runtime'\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay })\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return <>{children}</>\n}\n","import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport { useEidosStore } from '../store'\nimport type { EidosStore } from '../store'\n\nfunction useStore(): EidosStore\nfunction useStore<T>(selector: (state: EidosStore) => T): T\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T)\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()))\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore()\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url])\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue)\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id))\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline)\n const swStatus = useStore((s) => s.swStatus)\n const swError = useStore((s) => s.swError)\n return { isOnline, swStatus, swError }\n}\n\n/**\n * Queue counts — four independent primitive selectors. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n const pending = useStore((s) => s.queue.filter((q) => q.status === 'pending').length)\n const failed = useStore((s) => s.queue.filter((q) => q.status === 'failed').length)\n const replaying = useStore((s) => s.queue.filter((q) => q.status === 'replaying').length)\n const total = useStore((s) => s.queue.length)\n return { pending, failed, replaying, total }\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length)\n const prevRef = useRef(0)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current()\n }\n prevRef.current = total\n }, [total])\n}\n","export const VERSION = '1.0.23'\n","/**\n * Framework-agnostic reactive stores — compatible with Svelte's store protocol,\n * Vue's watchEffect, RxJS, and vanilla JS. Zero framework dependencies.\n *\n * Svelte: use the `$` prefix — `$eidosQueue`, `$eidosStatus`, etc.\n * Vue: call `.subscribe()` inside a composable with `onUnmounted` cleanup.\n * Vanilla: call `.subscribe(run)` directly; the return value unsubscribes.\n *\n * Each store calls its subscriber whenever any part of the Eidos state changes.\n * For fine-grained subscriptions, use `.getState()` to read the current snapshot\n * and compare manually in the subscriber callback.\n */\n\nimport { useEidosStore } from './store'\nimport type { EidosStore } from './store'\nimport type { ActionQueueItem, ResourceEntry } from './types'\n\n// ── Readable<T> — compatible with Svelte's Readable interface ─────────────────\n\nexport interface EidosReadable<T> {\n /** Subscribe to value changes. Returns an unsubscribe function. */\n subscribe(run: (value: T) => void): () => void\n /** Read the current value synchronously without subscribing. */\n getState(): T\n}\n\nfunction readable<T>(selector: (s: EidosStore) => T): EidosReadable<T> {\n return {\n subscribe(run) {\n // Emit current value immediately (Svelte store contract)\n run(selector(useEidosStore.getState()))\n return useEidosStore.subscribe(() => run(selector(useEidosStore.getState())))\n },\n getState() {\n return selector(useEidosStore.getState())\n },\n }\n}\n\n// ── Static stores (created once at module scope) ──────────────────────────────\n\n/** Full Eidos state snapshot. Prefer the narrower stores below. */\nexport const eidosStore: EidosReadable<EidosStore> = readable((s) => s)\n\n/** The action queue. Re-notifies on every queue mutation. */\nexport const eidosQueue: EidosReadable<ActionQueueItem[]> = readable((s) => s.queue)\n\n/**\n * Online status + SW lifecycle.\n * Object identity changes on every notification — destructure or compare fields\n * in the subscriber if you need to avoid unnecessary work.\n */\nexport const eidosStatus: EidosReadable<{\n isOnline: boolean\n swStatus: EidosStore['swStatus']\n swError: string | undefined\n}> = readable((s) => ({\n isOnline: s.isOnline,\n swStatus: s.swStatus,\n swError: s.swError,\n}))\n\n/**\n * Queue counts. Re-notifies on any queue mutation — compare values inside the\n * subscriber callback to skip work when counts haven't changed.\n */\nexport const eidosQueueStats: EidosReadable<{\n pending: number\n failed: number\n replaying: number\n total: number\n}> = readable((s) => {\n // Single pass over the queue — avoids three separate .filter() calls.\n let pending = 0, failed = 0, replaying = 0\n for (const q of s.queue) {\n if (q.status === 'pending') pending++\n else if (q.status === 'failed') failed++\n else if (q.status === 'replaying') replaying++\n }\n return { pending, failed, replaying, total: s.queue.length }\n})\n\n// ── Dynamic stores (created per URL / ID) ─────────────────────────────────────\n\n/**\n * Live cache state for a single registered resource URL.\n * @example\n * // Svelte\n * const entry = eidosResource('/api/products')\n * $: hits = $entry?.cacheHits ?? 0\n */\nexport function eidosResource(url: string): EidosReadable<ResourceEntry | undefined> {\n return readable((s) => s.resources[url])\n}\n\n/**\n * Live state for a single queue item by ID. Returns `undefined` once the item\n * is removed from the queue (after a successful replay or `clearQueue()`).\n * @example\n * // Svelte\n * const item = eidosAction(queuedResult.id)\n * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined\n */\nexport function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined> {\n return readable((s) => s.queue.find((item) => item.id === id))\n}\n"],"names":["_state","_listeners","_notify","fn","_set","updater","isOnline","swStatus","swError","url","entry","update","s","_removed","rest","item","id","items","_getState","_subscribe","listener","useEidosStore","partial","_registration","_pendingMessages","getSwRegistration","registerServiceWorker","swPath","store","waitForActivation","onSwMessage","flushPendingMessages","err","reg","resolve","sw","timer","handler","sendToWorker","message","_bgSyncHandler","registerBgSyncHandler","isBgSyncSupported","event","data","type","current","setOfflineSimulation","enabled","msg","_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","isPattern","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","cached","expired","resp","storeEntry","response","isOffline","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName","DB_NAME","DB_VERSION","QUEUE_STORE","_db","openDB","reject","req","db","idbAddToQueue","tx","idbGetQueue","idbUpdateQueueItem","get","idbRemoveFromQueue","idbGetPendingItems","index","results","done","finish","pendingReq","e","cursor","failedReq","idbClearQueue","_actionRegistry","uid","action","actionId","wrapped","args","persistAndQueue","actionName","backoffMs","retryCount","_replaying","replayQueue","_doReplayQueue","candidates","now","pending","result","outcomes","completedAt","nextRetryAt","o","outcome","clearQueue","_initialized","_unsubscribe","initEidos","autoReplay","persisted","prevIsOnline","justCameOnline","hasPending","q","_resetEidos","EidosProvider","children","useEffect","useStore","selector","useSyncExternalStore","useEidos","useEidosResource","useEidosQueue","useEidosAction","useEidosStatus","useEidosQueueStats","failed","replaying","total","useEidosOnDrain","callback","prevRef","useRef","callbackRef","VERSION","readable","run","eidosStore","eidosQueue","eidosStatus","eidosQueueStats","eidosResource","eidosAction"],"mappings":"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"}
1
+ {"version":3,"file":"eidos.cjs.js","sources":["../src/store.ts","../src/sw-bridge.ts","../src/resource.ts","../src/idb.ts","../src/action.ts","../src/runtime.ts","../src/react/Provider.tsx","../src/react/hooks.ts","../src/version.ts","../src/stores.ts"],"sourcesContent":["import type { EidosState, ResourceEntry, ActionQueueItem } from './types'\n\nexport interface EidosStore extends EidosState {\n // Online\n setOnline: (online: boolean) => void\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void\n // Resources\n registerResource: (url: string, entry: ResourceEntry) => void\n updateResource: (url: string, update: Partial<ResourceEntry>) => void\n unregisterResource: (url: string) => void\n // Queue\n addQueueItem: (item: ActionQueueItem) => void\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void\n removeQueueItem: (id: string) => void\n hydrateQueue: (items: ActionQueueItem[]) => void\n}\n\ntype Listener = () => void\n\nlet _state: EidosStore\nconst _listeners = new Set<Listener>()\n\nfunction _notify() {\n _listeners.forEach((fn) => fn())\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) }\n _notify()\n}\n\n_state = {\n isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,\n swStatus: 'idle',\n swError: undefined,\n resources: {},\n queue: [],\n\n setOnline: (isOnline) => _set(() => ({ isOnline })),\n\n setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),\n\n registerResource: (url, entry) =>\n _set((s) => ({ resources: { ...s.resources, [url]: entry } })),\n\n updateResource: (url, update) =>\n _set((s) => ({\n resources: {\n ...s.resources,\n [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url],\n },\n })),\n\n unregisterResource: (url) =>\n _set((s) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { [url]: _removed, ...rest } = s.resources\n return { resources: rest }\n }),\n\n addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),\n\n updateQueueItem: (id, update) =>\n _set((s) => ({\n queue: s.queue.map((item) => (item.id === id ? { ...item, ...update } : item)),\n })),\n\n removeQueueItem: (id) => _set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),\n\n hydrateQueue: (items) => _set(() => ({ queue: items })),\n}\n\nfunction _getState() {\n return _state\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener)\n return () => { _listeners.delete(listener) }\n}\n\nexport const useEidosStore = {\n getState: _getState,\n subscribe: _subscribe,\n // Test/devtools helper — merges partial state, preserves action methods.\n setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => {\n const update = typeof partial === 'function' ? partial(_state) : partial\n _state = { ..._state, ...update }\n _notify()\n },\n}\n","import { useEidosStore } from './store'\n\nlet _registration: ServiceWorkerRegistration | null = null\n// Messages sent before the SW activates are buffered here and flushed once\n// the SW is ready. Covers resource registrations, cache clears, offline\n// simulation — anything sent at module scope before EidosProvider mounts.\nlet _pendingMessages: Record<string, unknown>[] = []\n\nexport function getSwRegistration() {\n return _registration\n}\n\nexport async function registerServiceWorker(swPath: string): Promise<void> {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {\n useEidosStore.getState().setSwStatus('unsupported')\n return\n }\n\n const store = useEidosStore.getState()\n store.setSwStatus('registering')\n\n try {\n _registration = await navigator.serviceWorker.register(swPath, { scope: '/' })\n\n await waitForActivation(_registration)\n\n store.setSwStatus('active')\n\n // Receive messages from SW\n navigator.serviceWorker.addEventListener('message', onSwMessage)\n\n // Track online/offline\n window.addEventListener('online', () => store.setOnline(true))\n window.addEventListener('offline', () => store.setOnline(false))\n\n flushPendingMessages()\n } catch (err) {\n store.setSwStatus('error', String(err))\n }\n}\n\nfunction waitForActivation(reg: ServiceWorkerRegistration): Promise<void> {\n return new Promise((resolve) => {\n if (reg.active) { resolve(); return }\n const sw = reg.installing ?? reg.waiting\n if (!sw) { resolve(); return }\n\n // Resolve after 10s regardless — another tab may be blocking activation\n const timer = setTimeout(resolve, 10_000)\n\n sw.addEventListener('statechange', function handler() {\n if (sw.state === 'activated') {\n clearTimeout(timer)\n sw.removeEventListener('statechange', handler)\n resolve()\n }\n })\n })\n}\n\nexport function sendToWorker(message: Record<string, unknown>): void {\n const sw = _registration?.active\n if (sw) {\n sw.postMessage(message)\n } else {\n _pendingMessages.push(message)\n }\n}\n\nlet _bgSyncHandler: (() => void) | null = null\n\nexport function registerBgSyncHandler(fn: () => void): void {\n _bgSyncHandler = fn\n}\n\nexport function isBgSyncSupported(): boolean {\n try {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n _registration !== null &&\n 'sync' in _registration\n )\n } catch {\n return false\n }\n}\n\nfunction onSwMessage(event: MessageEvent): void {\n const data = event.data as { type: string; url?: string; strategy?: string }\n if (!data?.type) return\n\n const store = useEidosStore.getState()\n const { type, url } = data\n\n if (type === 'EIDOS_BACKGROUND_SYNC') {\n _bgSyncHandler?.()\n return\n }\n\n if (!url) return\n\n switch (type) {\n case 'EIDOS_CACHE_HIT': {\n const current = store.resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n break\n }\n case 'EIDOS_CACHE_UPDATED': {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-updated',\n cachedAt: Date.now(),\n })\n break\n }\n case 'EIDOS_NETWORK_ERROR': {\n store.updateResource(url, {\n status: 'error',\n lastEvent: 'network-error',\n })\n break\n }\n }\n}\n\nexport function setOfflineSimulation(enabled: boolean): void {\n sendToWorker({ type: 'EIDOS_SIMULATE_OFFLINE', enabled })\n useEidosStore.getState().setOnline(!enabled)\n}\n\nfunction flushPendingMessages(): void {\n const sw = _registration?.active\n if (!sw) return\n for (const msg of _pendingMessages) sw.postMessage(msg)\n _pendingMessages = []\n}\n","import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>()\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void\nlet _queryInvalidator: QueryInvalidator | null = null\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url)\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') // :param → single-segment wildcard\n + '$'\n )\n}\n\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, config)\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n }\n\n useEidosStore.getState().registerResource(url, entry)\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n })\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url)\n if (existing) return existing.then((r) => r.clone())\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy)\n _inflightRequests.set(url, task)\n task.finally(() => _inflightRequests.delete(url))\n return task.then((r) => r.clone())\n },\n\n json: async () => {\n if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url })\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n if (cache) {\n const keys = await cache.keys()\n const patternRe = regexStr ? new RegExp(regexStr) : null\n const isCrossOrigin = url.startsWith('http')\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url\n const p = new URL(rUrl).pathname\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p)\n }\n return isCrossOrigin ? rUrl === url : (rUrl === url || p === url)\n })\n .map((r) => cache.delete(r)),\n )\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n })\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url])\n },\n\n unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\n }\n\n _registry.set(url, handle)\n return handle\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState()\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() })\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url]\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone())\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n })\n }\n\n return cached\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n })\n }\n\n const response = await fetch(url)\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone())\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n return response\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' })\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true'\n throw new Error(\n isOffline ? `offline: no cached response for ${url}` : `${response.status} ${response.statusText}`,\n )\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n return fallback\n }\n\n store.updateResource(url, { status: 'error' })\n throw err\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n name: 'CacheFirst',\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n name: 'NetworkFirst',\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n}\n\nfunction buildStrategy(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\n }\n}\n","import type { ActionQueueItem } from './types'\n\nconst DB_NAME = 'eidos'\nconst DB_VERSION = 1\nconst QUEUE_STORE = 'action-queue'\n\nlet _db: IDBDatabase | null = null\n\nfunction openDB(): Promise<IDBDatabase> {\n if (_db) return Promise.resolve(_db)\n\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION)\n\n req.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(QUEUE_STORE)) {\n const store = db.createObjectStore(QUEUE_STORE, { keyPath: 'id' })\n store.createIndex('status', 'status', { unique: false })\n store.createIndex('actionId', 'actionId', { unique: false })\n }\n }\n\n req.onsuccess = () => {\n _db = req.result\n resolve(req.result)\n }\n\n req.onerror = () => reject(req.error)\n })\n}\n\nexport async function idbAddToQueue(item: ActionQueueItem): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).add(item)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nexport async function idbGetQueue(): Promise<ActionQueueItem[]> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly')\n const req = tx.objectStore(QUEUE_STORE).getAll()\n req.onsuccess = () => resolve(req.result as ActionQueueItem[])\n req.onerror = () => reject(req.error)\n })\n}\n\nexport async function idbUpdateQueueItem(\n id: string,\n update: Partial<ActionQueueItem>,\n): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n const store = tx.objectStore(QUEUE_STORE)\n const get = store.get(id)\n get.onsuccess = () => {\n if (get.result) {\n store.put({ ...get.result, ...update })\n } else if (import.meta.env.DEV) {\n console.warn(`[eidos] idbUpdateQueueItem: item \"${id}\" not found — store/IDB may have diverged`)\n }\n }\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\nexport async function idbRemoveFromQueue(id: string): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).delete(id)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n\n// Uses the status index to fetch only pending/failed items — avoids a full\n// table scan when the queue has many succeeded/replaying entries.\nexport async function idbGetPendingItems(): Promise<ActionQueueItem[]> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readonly')\n const index = tx.objectStore(QUEUE_STORE).index('status')\n const results: ActionQueueItem[] = []\n\n let done = 0\n function finish(err?: DOMException | null) {\n if (err) { reject(err); return }\n if (++done === 2) resolve(results)\n }\n\n const pendingReq = index.openCursor(IDBKeyRange.only('pending'))\n pendingReq.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result\n if (cursor) { results.push(cursor.value as ActionQueueItem); cursor.continue() }\n else finish()\n }\n pendingReq.onerror = () => finish(pendingReq.error)\n\n const failedReq = index.openCursor(IDBKeyRange.only('failed'))\n failedReq.onsuccess = (e) => {\n const cursor = (e.target as IDBRequest<IDBCursorWithValue>).result\n if (cursor) { results.push(cursor.value as ActionQueueItem); cursor.continue() }\n else finish()\n }\n failedReq.onerror = () => finish(failedReq.error)\n })\n}\n\nexport async function idbClearQueue(): Promise<void> {\n const db = await openDB()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(QUEUE_STORE, 'readwrite')\n tx.objectStore(QUEUE_STORE).clear()\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n })\n}\n","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\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 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 }\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\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 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter(\n (item) => !item.nextRetryAt || item.nextRetryAt <= now,\n )\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0 }\n\n const outcomes = await Promise.allSettled(\n pending.map(async (item): Promise<'succeeded' | 'failed' | 'retrying' | 'skipped'> => {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n store.updateQueueItem(item.id, { status: 'replaying' })\n await idbUpdateQueueItem(item.id, { status: 'replaying' })\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 delay so the UI can show the success state\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\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 )\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else { result.attempted++; result[outcome]++ }\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","import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge'\nimport { replayQueue } from './action'\nimport { useEidosStore } from './store'\nimport { idbGetQueue } from './idb'\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean\n}\n\nlet _initialized = false\nlet _unsubscribe: (() => void) | null = null\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n if (_initialized) return\n _initialized = true\n\n const swPath = config.swPath ?? '/eidos-sw.js'\n const autoReplay = config.autoReplay ?? true\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue()\n if (persisted.length > 0) {\n useEidosStore.getState().hydrateQueue(persisted)\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath)\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200)\n }\n })\n\n if (autoReplay) {\n // ── Subscribe to the store instead of window.addEventListener('online')\n //\n // WHY: setOfflineSimulation() updates the store directly but never fires a\n // real browser `online` event. Watching the store catches both:\n // • Real network reconnects (sw-bridge updates store on window.online)\n // • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n //\n let prevIsOnline = useEidosStore.getState().isOnline\n\n _unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState()\n const justCameOnline = isOnline && !prevIsOnline\n prevIsOnline = isOnline\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600)\n }\n })\n\n // Replay any pending items that survived a page reload\n const store = useEidosStore.getState()\n const hasPending = store.queue.some((q) => q.status === 'pending' || q.status === 'failed')\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200)\n }\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState()\n console.groupCollapsed('%c⚡ Eidos', 'color:#38bdf8;font-weight:bold')\n console.log('SW path :', swPath)\n console.log('Auto-replay:', autoReplay)\n console.log('SW status :', store.swStatus)\n console.groupEnd()\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.()\n _unsubscribe = null\n _initialized = false\n}\n","import { useEffect, type ReactNode } from 'react'\nimport { initEidos, type EidosConfig } from '../runtime'\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay })\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return <>{children}</>\n}\n","import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport { useEidosStore } from '../store'\nimport type { EidosStore } from '../store'\n\nfunction useStore(): EidosStore\nfunction useStore<T>(selector: (state: EidosStore) => T): T\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T)\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()))\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore()\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url])\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue)\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id))\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline)\n const swStatus = useStore((s) => s.swStatus)\n const swError = useStore((s) => s.swError)\n return { isOnline, swStatus, swError }\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n let pending = 0, failed = 0, replaying = 0\n for (const q of s.queue) {\n if (q.status === 'pending') pending++\n else if (q.status === 'failed') failed++\n else if (q.status === 'replaying') replaying++\n }\n return `${pending},${failed},${replaying},${s.queue.length}`\n })\n const [p, f, r, t] = encoded.split(',')\n return { pending: +p, failed: +f, replaying: +r, total: +t }\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length)\n const prevRef = useRef(0)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current()\n }\n prevRef.current = total\n }, [total])\n}\n","export const VERSION = '1.0.25'\n","/**\n * Framework-agnostic reactive stores — compatible with Svelte's store protocol,\n * Vue's watchEffect, RxJS, and vanilla JS. Zero framework dependencies.\n *\n * Svelte: use the `$` prefix — `$eidosQueue`, `$eidosStatus`, etc.\n * Vue: call `.subscribe()` inside a composable with `onUnmounted` cleanup.\n * Vanilla: call `.subscribe(run)` directly; the return value unsubscribes.\n *\n * Each store calls its subscriber whenever any part of the Eidos state changes.\n * For fine-grained subscriptions, use `.getState()` to read the current snapshot\n * and compare manually in the subscriber callback.\n */\n\nimport { useEidosStore } from './store'\nimport type { EidosStore } from './store'\nimport type { ActionQueueItem, ResourceEntry } from './types'\n\n// ── Readable<T> — compatible with Svelte's Readable interface ─────────────────\n\nexport interface EidosReadable<T> {\n /** Subscribe to value changes. Returns an unsubscribe function. */\n subscribe(run: (value: T) => void): () => void\n /** Read the current value synchronously without subscribing. */\n getState(): T\n}\n\nfunction readable<T>(selector: (s: EidosStore) => T): EidosReadable<T> {\n return {\n subscribe(run) {\n // Emit current value immediately (Svelte store contract)\n run(selector(useEidosStore.getState()))\n return useEidosStore.subscribe(() => run(selector(useEidosStore.getState())))\n },\n getState() {\n return selector(useEidosStore.getState())\n },\n }\n}\n\n// ── Static stores (created once at module scope) ──────────────────────────────\n\n/** Full Eidos state snapshot. Prefer the narrower stores below. */\nexport const eidosStore: EidosReadable<EidosStore> = readable((s) => s)\n\n/** The action queue. Re-notifies on every queue mutation. */\nexport const eidosQueue: EidosReadable<ActionQueueItem[]> = readable((s) => s.queue)\n\n/**\n * Online status + SW lifecycle.\n * Object identity changes on every notification — destructure or compare fields\n * in the subscriber if you need to avoid unnecessary work.\n */\nexport const eidosStatus: EidosReadable<{\n isOnline: boolean\n swStatus: EidosStore['swStatus']\n swError: string | undefined\n}> = readable((s) => ({\n isOnline: s.isOnline,\n swStatus: s.swStatus,\n swError: s.swError,\n}))\n\n/**\n * Queue counts. Re-notifies on any queue mutation — compare values inside the\n * subscriber callback to skip work when counts haven't changed.\n */\nexport const eidosQueueStats: EidosReadable<{\n pending: number\n failed: number\n replaying: number\n total: number\n}> = readable((s) => {\n // Single pass over the queue — avoids three separate .filter() calls.\n let pending = 0, failed = 0, replaying = 0\n for (const q of s.queue) {\n if (q.status === 'pending') pending++\n else if (q.status === 'failed') failed++\n else if (q.status === 'replaying') replaying++\n }\n return { pending, failed, replaying, total: s.queue.length }\n})\n\n// ── Dynamic stores (created per URL / ID) ─────────────────────────────────────\n\n/**\n * Live cache state for a single registered resource URL.\n * @example\n * // Svelte\n * const entry = eidosResource('/api/products')\n * $: hits = $entry?.cacheHits ?? 0\n */\nexport function eidosResource(url: string): EidosReadable<ResourceEntry | undefined> {\n return readable((s) => s.resources[url])\n}\n\n/**\n * Live state for a single queue item by ID. Returns `undefined` once the item\n * is removed from the queue (after a successful replay or `clearQueue()`).\n * @example\n * // Svelte\n * const item = eidosAction(queuedResult.id)\n * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined\n */\nexport function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined> {\n return readable((s) => s.queue.find((item) => item.id === id))\n}\n"],"names":["_state","_listeners","_notify","fn","_set","updater","isOnline","swStatus","swError","url","entry","update","s","_removed","rest","item","id","items","_getState","_subscribe","listener","useEidosStore","partial","_registration","_pendingMessages","getSwRegistration","registerServiceWorker","swPath","store","waitForActivation","onSwMessage","flushPendingMessages","err","reg","resolve","sw","timer","handler","sendToWorker","message","_bgSyncHandler","registerBgSyncHandler","isBgSyncSupported","event","data","type","current","setOfflineSimulation","enabled","msg","_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","isPattern","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","cached","expired","resp","storeEntry","response","isOffline","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName","DB_NAME","DB_VERSION","QUEUE_STORE","_db","openDB","reject","req","db","idbAddToQueue","tx","idbGetQueue","idbUpdateQueueItem","get","idbRemoveFromQueue","idbGetPendingItems","index","results","done","finish","pendingReq","e","cursor","failedReq","idbClearQueue","_actionRegistry","_rollbackRegistry","uid","action","actionId","wrapped","args","_a","persistAndQueue","_b","actionName","backoffMs","retryCount","_replaying","replayQueue","_doReplayQueue","candidates","now","pending","result","outcomes","completedAt","nextRetryAt","o","outcome","clearQueue","_initialized","_unsubscribe","initEidos","autoReplay","persisted","prevIsOnline","justCameOnline","hasPending","q","_resetEidos","EidosProvider","children","useEffect","useStore","selector","useSyncExternalStore","useEidos","useEidosResource","useEidosQueue","useEidosAction","useEidosStatus","useEidosQueueStats","encoded","failed","replaying","f","t","useEidosOnDrain","callback","total","prevRef","useRef","callbackRef","VERSION","readable","run","eidosStore","eidosQueue","eidosStatus","eidosQueueStats","eidosResource","eidosAction"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -13,6 +13,19 @@ export declare interface ActionConfig {
13
13
  maxRetries?: number;
14
14
  /** Human-readable name for the action (used in devtools). */
15
15
  name?: string;
16
+ /**
17
+ * Called immediately before the async function executes, with the same args.
18
+ * Use to apply an optimistic UI update (add item, mark as pending, etc.).
19
+ * Called on every invocation — online, offline, and during queue replay.
20
+ */
21
+ onOptimistic?: (...args: any[]) => void;
22
+ /**
23
+ * Called when the action permanently fails and will not be retried.
24
+ * - `best-effort`: called on first throw.
25
+ * - `neverLose`: called when `maxRetries` is exhausted (status → `'failed'`).
26
+ * Use to revert the optimistic update.
27
+ */
28
+ onRollback?: (...args: any[]) => void;
16
29
  }
17
30
 
18
31
  declare type ActionFn<TArgs extends any[], TReturn> = (...args: TArgs) => Promise<TReturn>;
@@ -258,7 +271,7 @@ export declare function useEidosOnDrain(callback: () => void): void;
258
271
  export declare function useEidosQueue(): ActionQueueItem[];
259
272
 
260
273
  /**
261
- * Queue counts — four independent primitive selectors. Re-renders only when a
274
+ * Queue counts — single subscription, single loop. Re-renders only when a
262
275
  * count changes, not on every queue mutation. Use for badges and status bars
263
276
  * instead of `useEidosQueue()` when you only need numbers, not full items.
264
277
  */
@@ -289,6 +302,6 @@ export declare const useEidosStore: {
289
302
  setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => void;
290
303
  };
291
304
 
292
- export declare const VERSION = "1.0.23";
305
+ export declare const VERSION = "1.0.25";
293
306
 
294
307
  export { }
@@ -1,42 +1,47 @@
1
- import { useRef as i, useEffect as f, useSyncExternalStore as l } from "react";
2
- import { useEidosStore as c } from "../store.js";
3
- function n(e) {
4
- const t = e ?? ((u) => u);
5
- return l(c.subscribe, () => t(c.getState()));
6
- }
7
- function E() {
8
- return n();
9
- }
10
- function g(e) {
11
- return n((t) => t.resources[e]);
12
- }
13
- function S() {
14
- return n((e) => e.queue);
15
- }
16
- function p(e) {
17
- return n((t) => t.queue.find((u) => u.id === e));
1
+ import { useRef as d, useEffect as p, useSyncExternalStore as E } from "react";
2
+ import { useEidosStore as l } from "../store.js";
3
+ function s(e) {
4
+ const t = e ?? ((n) => n);
5
+ return E(l.subscribe, () => t(l.getState()));
18
6
  }
19
7
  function q() {
20
- const e = n((s) => s.isOnline), t = n((s) => s.swStatus), u = n((s) => s.swError);
21
- return { isOnline: e, swStatus: t, swError: u };
22
- }
23
- function h() {
24
- const e = n((r) => r.queue.filter((o) => o.status === "pending").length), t = n((r) => r.queue.filter((o) => o.status === "failed").length), u = n((r) => r.queue.filter((o) => o.status === "replaying").length), s = n((r) => r.queue.length);
25
- return { pending: e, failed: t, replaying: u, total: s };
8
+ return s();
26
9
  }
27
10
  function m(e) {
28
- const t = n((r) => r.queue.length), u = i(0), s = i(e);
29
- s.current = e, f(() => {
30
- u.current > 0 && t === 0 && s.current(), u.current = t;
11
+ return s((t) => t.resources[e]);
12
+ }
13
+ function w() {
14
+ return s((e) => e.queue);
15
+ }
16
+ function y(e) {
17
+ return s((t) => t.queue.find((n) => n.id === e));
18
+ }
19
+ function R() {
20
+ const e = s((u) => u.isOnline), t = s((u) => u.swStatus), n = s((u) => u.swError);
21
+ return { isOnline: e, swStatus: t, swError: n };
22
+ }
23
+ function $() {
24
+ const e = s((i) => {
25
+ let c = 0, f = 0, a = 0;
26
+ for (const o of i.queue)
27
+ o.status === "pending" ? c++ : o.status === "failed" ? f++ : o.status === "replaying" && a++;
28
+ return `${c},${f},${a},${i.queue.length}`;
29
+ }), [t, n, u, r] = e.split(",");
30
+ return { pending: +t, failed: +n, replaying: +u, total: +r };
31
+ }
32
+ function O(e) {
33
+ const t = s((r) => r.queue.length), n = d(0), u = d(e);
34
+ u.current = e, p(() => {
35
+ n.current > 0 && t === 0 && u.current(), n.current = t;
31
36
  }, [t]);
32
37
  }
33
38
  export {
34
- E as useEidos,
35
- p as useEidosAction,
36
- m as useEidosOnDrain,
37
- S as useEidosQueue,
38
- h as useEidosQueueStats,
39
- g as useEidosResource,
40
- q as useEidosStatus
39
+ q as useEidos,
40
+ y as useEidosAction,
41
+ O as useEidosOnDrain,
42
+ w as useEidosQueue,
43
+ $ as useEidosQueueStats,
44
+ m as useEidosResource,
45
+ R as useEidosStatus
41
46
  };
42
47
  //# sourceMappingURL=hooks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sources":["../../src/react/hooks.ts"],"sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport { useEidosStore } from '../store'\nimport type { EidosStore } from '../store'\n\nfunction useStore(): EidosStore\nfunction useStore<T>(selector: (state: EidosStore) => T): T\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T)\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()))\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore()\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url])\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue)\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id))\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline)\n const swStatus = useStore((s) => s.swStatus)\n const swError = useStore((s) => s.swError)\n return { isOnline, swStatus, swError }\n}\n\n/**\n * Queue counts — four independent primitive selectors. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n const pending = useStore((s) => s.queue.filter((q) => q.status === 'pending').length)\n const failed = useStore((s) => s.queue.filter((q) => q.status === 'failed').length)\n const replaying = useStore((s) => s.queue.filter((q) => q.status === 'replaying').length)\n const total = useStore((s) => s.queue.length)\n return { pending, failed, replaying, total }\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length)\n const prevRef = useRef(0)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current()\n }\n prevRef.current = total\n }, [total])\n}\n"],"names":["useStore","selector","fn","s","useSyncExternalStore","useEidosStore","useEidos","useEidosResource","url","useEidosQueue","useEidosAction","id","item","useEidosStatus","isOnline","swStatus","swError","useEidosQueueStats","pending","q","failed","replaying","total","useEidosOnDrain","callback","prevRef","useRef","callbackRef","useEffect"],"mappings":";;AAMA,SAASA,EAAyBC,GAAwC;AACxE,QAAMC,IAAKD,MAAa,CAACE,MAAkBA;AAC3C,SAAOC,EAAqBC,EAAc,WAAW,MAAMH,EAAGG,EAAc,SAAA,CAAU,CAAC;AACzF;AAGO,SAASC,IAAW;AACzB,SAAON,EAAA;AACT;AAGO,SAASO,EAAiBC,GAAa;AAC5C,SAAOR,EAAS,CAACG,MAAMA,EAAE,UAAUK,CAAG,CAAC;AACzC;AAGO,SAASC,IAAgB;AAC9B,SAAOT,EAAS,CAACG,MAAMA,EAAE,KAAK;AAChC;AAOO,SAASO,EAAeC,GAAY;AACzC,SAAOX,EAAS,CAACG,MAAMA,EAAE,MAAM,KAAK,CAACS,MAASA,EAAK,OAAOD,CAAE,CAAC;AAC/D;AAOO,SAASE,IAAiB;AAC/B,QAAMC,IAAWd,EAAS,CAAC,MAAM,EAAE,QAAQ,GACrCe,IAAWf,EAAS,CAAC,MAAM,EAAE,QAAQ,GACrCgB,IAAUhB,EAAS,CAAC,MAAM,EAAE,OAAO;AACzC,SAAO,EAAE,UAAAc,GAAU,UAAAC,GAAU,SAAAC,EAAA;AAC/B;AAOO,SAASC,IAAqB;AACnC,QAAMC,IAAYlB,EAAS,CAACG,MAAMA,EAAE,MAAM,OAAO,CAACgB,MAAMA,EAAE,WAAW,SAAS,EAAE,MAAM,GAChFC,IAAYpB,EAAS,CAACG,MAAMA,EAAE,MAAM,OAAO,CAACgB,MAAMA,EAAE,WAAW,QAAQ,EAAE,MAAM,GAC/EE,IAAYrB,EAAS,CAACG,MAAMA,EAAE,MAAM,OAAO,CAACgB,MAAMA,EAAE,WAAW,WAAW,EAAE,MAAM,GAClFG,IAAYtB,EAAS,CAACG,MAAMA,EAAE,MAAM,MAAM;AAChD,SAAO,EAAE,SAAAe,GAAS,QAAAE,GAAQ,WAAAC,GAAW,OAAAC,EAAA;AACvC;AAUO,SAASC,EAAgBC,GAAsB;AACpD,QAAMF,IAAWtB,EAAS,CAACG,MAAMA,EAAE,MAAM,MAAM,GACzCsB,IAAWC,EAAO,CAAC,GACnBC,IAAcD,EAAOF,CAAQ;AACnC,EAAAG,EAAY,UAAUH,GAEtBI,EAAU,MAAM;AACd,IAAIH,EAAQ,UAAU,KAAKH,MAAU,KACnCK,EAAY,QAAA,GAEdF,EAAQ,UAAUH;AAAA,EACpB,GAAG,CAACA,CAAK,CAAC;AACZ;"}
1
+ {"version":3,"file":"hooks.js","sources":["../../src/react/hooks.ts"],"sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport { useEidosStore } from '../store'\nimport type { EidosStore } from '../store'\n\nfunction useStore(): EidosStore\nfunction useStore<T>(selector: (state: EidosStore) => T): T\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T)\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()))\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore()\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url])\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue)\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id))\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline)\n const swStatus = useStore((s) => s.swStatus)\n const swError = useStore((s) => s.swError)\n return { isOnline, swStatus, swError }\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n let pending = 0, failed = 0, replaying = 0\n for (const q of s.queue) {\n if (q.status === 'pending') pending++\n else if (q.status === 'failed') failed++\n else if (q.status === 'replaying') replaying++\n }\n return `${pending},${failed},${replaying},${s.queue.length}`\n })\n const [p, f, r, t] = encoded.split(',')\n return { pending: +p, failed: +f, replaying: +r, total: +t }\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length)\n const prevRef = useRef(0)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current()\n }\n prevRef.current = total\n }, [total])\n}\n"],"names":["useStore","selector","fn","s","useSyncExternalStore","useEidosStore","useEidos","useEidosResource","url","useEidosQueue","useEidosAction","id","item","useEidosStatus","isOnline","swStatus","swError","useEidosQueueStats","encoded","pending","failed","replaying","q","p","f","r","t","useEidosOnDrain","callback","total","prevRef","useRef","callbackRef","useEffect"],"mappings":";;AAMA,SAASA,EAAyBC,GAAwC;AACxE,QAAMC,IAAKD,MAAa,CAACE,MAAkBA;AAC3C,SAAOC,EAAqBC,EAAc,WAAW,MAAMH,EAAGG,EAAc,SAAA,CAAU,CAAC;AACzF;AAGO,SAASC,IAAW;AACzB,SAAON,EAAA;AACT;AAGO,SAASO,EAAiBC,GAAa;AAC5C,SAAOR,EAAS,CAACG,MAAMA,EAAE,UAAUK,CAAG,CAAC;AACzC;AAGO,SAASC,IAAgB;AAC9B,SAAOT,EAAS,CAACG,MAAMA,EAAE,KAAK;AAChC;AAOO,SAASO,EAAeC,GAAY;AACzC,SAAOX,EAAS,CAACG,MAAMA,EAAE,MAAM,KAAK,CAACS,MAASA,EAAK,OAAOD,CAAE,CAAC;AAC/D;AAOO,SAASE,IAAiB;AAC/B,QAAMC,IAAWd,EAAS,CAACG,MAAMA,EAAE,QAAQ,GACrCY,IAAWf,EAAS,CAACG,MAAMA,EAAE,QAAQ,GACrCa,IAAUhB,EAAS,CAACG,MAAMA,EAAE,OAAO;AACzC,SAAO,EAAE,UAAAW,GAAU,UAAAC,GAAU,SAAAC,EAAA;AAC/B;AAOO,SAASC,IAAqB;AAInC,QAAMC,IAAUlB,EAAS,CAACG,MAAM;AAC9B,QAAIgB,IAAU,GAAGC,IAAS,GAAGC,IAAY;AACzC,eAAWC,KAAKnB,EAAE;AAChB,MAAImB,EAAE,WAAW,YAAWH,MACnBG,EAAE,WAAW,WAAUF,MACvBE,EAAE,WAAW,eAAaD;AAErC,WAAO,GAAGF,CAAO,IAAIC,CAAM,IAAIC,CAAS,IAAIlB,EAAE,MAAM,MAAM;AAAA,EAC5D,CAAC,GACK,CAACoB,GAAGC,GAAGC,GAAGC,CAAC,IAAIR,EAAQ,MAAM,GAAG;AACtC,SAAO,EAAE,SAAS,CAACK,GAAG,QAAQ,CAACC,GAAG,WAAW,CAACC,GAAG,OAAO,CAACC,EAAA;AAC3D;AAUO,SAASC,EAAgBC,GAAsB;AACpD,QAAMC,IAAW7B,EAAS,CAACG,MAAMA,EAAE,MAAM,MAAM,GACzC2B,IAAWC,EAAO,CAAC,GACnBC,IAAcD,EAAOH,CAAQ;AACnC,EAAAI,EAAY,UAAUJ,GAEtBK,EAAU,MAAM;AACd,IAAIH,EAAQ,UAAU,KAAKD,MAAU,KACnCG,EAAY,QAAA,GAEdF,EAAQ,UAAUD;AAAA,EACpB,GAAG,CAACA,CAAK,CAAC;AACZ;"}
package/dist/resource.js CHANGED
@@ -98,7 +98,7 @@ async function E(e, t, a) {
98
98
  status: "fresh",
99
99
  lastEvent: "cache-hit",
100
100
  cacheHits: ((c == null ? void 0 : c.cacheHits) ?? 0) + 1
101
- }), a.swStrategy === "stale-while-revalidate" && fetch(e).then(async (d) => {
101
+ }), a.swStrategy === "stale-while-revalidate" && fetch(e, { signal: AbortSignal.timeout(5e3) }).then(async (d) => {
102
102
  d.ok && o && (await o.put(e, d.clone()), h.getState().updateResource(e, {
103
103
  cachedAt: Date.now(),
104
104
  lastEvent: "cache-updated"
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>()\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void\nlet _queryInvalidator: QueryInvalidator | null = null\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url)\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') // :param → single-segment wildcard\n + '$'\n )\n}\n\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, config)\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n }\n\n useEidosStore.getState().registerResource(url, entry)\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n })\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url)\n if (existing) return existing.then((r) => r.clone())\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy)\n _inflightRequests.set(url, task)\n task.finally(() => _inflightRequests.delete(url))\n return task.then((r) => r.clone())\n },\n\n json: async () => {\n if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url })\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n if (cache) {\n const keys = await cache.keys()\n const patternRe = regexStr ? new RegExp(regexStr) : null\n const isCrossOrigin = url.startsWith('http')\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url\n const p = new URL(rUrl).pathname\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p)\n }\n return isCrossOrigin ? rUrl === url : (rUrl === url || p === url)\n })\n .map((r) => cache.delete(r)),\n )\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n })\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url])\n },\n\n unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\n }\n\n _registry.set(url, handle)\n return handle\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState()\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() })\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url]\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url)\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone())\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n }\n })\n .catch(() => {\n /* offline — cached version stays valid */\n })\n }\n\n return cached\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n })\n }\n\n const response = await fetch(url)\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone())\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n return response\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' })\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true'\n throw new Error(\n isOffline ? `offline: no cached response for ${url}` : `${response.status} ${response.statusText}`,\n )\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n return fallback\n }\n\n store.updateResource(url, { status: 'error' })\n throw err\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n name: 'CacheFirst',\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n name: 'NetworkFirst',\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n}\n\nfunction buildStrategy(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\n }\n}\n"],"names":["_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","fn","isPattern","url","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","entry","useEidosStore","sendToWorker","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","store","cached","current","expired","resp","storeEntry","response","isOffline","err","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName"],"mappings":"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"}
1
+ {"version":3,"file":"resource.js","sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>()\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void\nlet _queryInvalidator: QueryInvalidator | null = null\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url)\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') // :param → single-segment wildcard\n + '$'\n )\n}\n\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, config)\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n }\n\n useEidosStore.getState().registerResource(url, entry)\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n })\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url)\n if (existing) return existing.then((r) => r.clone())\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy)\n _inflightRequests.set(url, task)\n task.finally(() => _inflightRequests.delete(url))\n return task.then((r) => r.clone())\n },\n\n json: async () => {\n if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url })\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n if (cache) {\n const keys = await cache.keys()\n const patternRe = regexStr ? new RegExp(regexStr) : null\n const isCrossOrigin = url.startsWith('http')\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url\n const p = new URL(rUrl).pathname\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p)\n }\n return isCrossOrigin ? rUrl === url : (rUrl === url || p === url)\n })\n .map((r) => cache.delete(r)),\n )\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n })\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url])\n },\n\n unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\n }\n\n _registry.set(url, handle)\n return handle\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState()\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() })\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null)\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url]\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone())\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n })\n }\n\n return cached\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n })\n }\n\n const response = await fetch(url)\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone())\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n })\n return response\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' })\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true'\n throw new Error(\n isOffline ? `offline: no cached response for ${url}` : `${response.status} ${response.statusText}`,\n )\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url]\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n })\n return fallback\n }\n\n store.updateResource(url, { status: 'error' })\n throw err\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n name: 'CacheFirst',\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n name: 'NetworkFirst',\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n}\n\nfunction buildStrategy(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\n }\n}\n"],"names":["_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","fn","isPattern","url","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","entry","useEidosStore","sendToWorker","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","store","cached","current","expired","resp","storeEntry","response","isOffline","err","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName"],"mappings":"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"}
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const o = "1.0.23";
1
+ const o = "1.0.25";
2
2
  export {
3
3
  o as VERSION
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sources":["../src/version.ts"],"sourcesContent":["export const VERSION = '1.0.23'\n"],"names":["VERSION"],"mappings":"AAAO,MAAMA,IAAU;"}
1
+ {"version":3,"file":"version.js","sources":["../src/version.ts"],"sourcesContent":["export const VERSION = '1.0.25'\n"],"names":["VERSION"],"mappings":"AAAO,MAAMA,IAAU;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweidos/eidos",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Describe intent. The runtime figures out how. An abstraction layer for offline-first web apps.",
5
5
  "author": "Aditya Raj",
6
6
  "license": "MIT",