@sweidos/eidos 1.0.22 → 1.0.24
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 +91 -3
- package/dist/action.js +64 -58
- package/dist/action.js.map +1 -1
- package/dist/eidos.cjs.js +2 -2
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +23 -22
- package/dist/runtime.js +23 -19
- package/dist/runtime.js.map +1 -1
- package/dist/testing.cjs.js +86 -0
- package/dist/testing.d.ts +98 -0
- package/dist/testing.js +86 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -108,7 +108,18 @@ export const createOrder = action(
|
|
|
108
108
|
})
|
|
109
109
|
return res.json()
|
|
110
110
|
},
|
|
111
|
-
{
|
|
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
|
|
|
@@ -602,6 +615,81 @@ Registers a `QueryClient` with Eidos. After calling this:
|
|
|
602
615
|
|
|
603
616
|
---
|
|
604
617
|
|
|
618
|
+
## Testing Utilities
|
|
619
|
+
|
|
620
|
+
`@sweidos/eidos/testing` provides first-class helpers for Vitest, Jest, and Playwright. Import only in test files.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
import {
|
|
624
|
+
mockOffline, mockOnline,
|
|
625
|
+
drainQueue, waitForQueueDrain,
|
|
626
|
+
getCachedEntry, clearEidosCache,
|
|
627
|
+
resetEidos, getEidosState,
|
|
628
|
+
} from '@sweidos/eidos/testing'
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### `resetEidos()` — `beforeEach` cleanup
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
beforeEach(async () => {
|
|
635
|
+
await resetEidos()
|
|
636
|
+
// ✓ queue cleared, resources cleared, online restored, _initialized reset
|
|
637
|
+
})
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Testing offline queuing
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
it('queues action while offline', async () => {
|
|
644
|
+
mockOffline()
|
|
645
|
+
await savePost({ title: 'Draft' })
|
|
646
|
+
|
|
647
|
+
expect(getEidosState().queue).toHaveLength(1)
|
|
648
|
+
expect(getEidosState().isOnline).toBe(false)
|
|
649
|
+
})
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Testing queue replay
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
it('replays queue on reconnect', async () => {
|
|
656
|
+
mockOffline()
|
|
657
|
+
await savePost({ title: 'Draft' })
|
|
658
|
+
|
|
659
|
+
const result = await drainQueue() // forces online + replays
|
|
660
|
+
expect(result.succeeded).toBe(1)
|
|
661
|
+
})
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Testing cache state
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
it('caches the resource after first fetch', async () => {
|
|
668
|
+
const products = resource('/api/products', { offline: true })
|
|
669
|
+
await products.fetch()
|
|
670
|
+
|
|
671
|
+
const cached = await getCachedEntry('/api/products')
|
|
672
|
+
expect(cached).toBeDefined()
|
|
673
|
+
const body = await cached!.json()
|
|
674
|
+
expect(body).toEqual([...])
|
|
675
|
+
})
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### API summary
|
|
679
|
+
|
|
680
|
+
| Helper | Description |
|
|
681
|
+
|--------|-------------|
|
|
682
|
+
| `mockOffline(opts?)` | Set `isOnline = false`. Pass `{ stubFetch: true }` to also make `fetch()` throw. |
|
|
683
|
+
| `mockOnline()` | Restore `isOnline = true`. Removes fetch stub if present. |
|
|
684
|
+
| `drainQueue()` | Force-replay queue now. Returns `ReplayResult`. |
|
|
685
|
+
| `waitForQueueDrain(opts?)` | Wait until no pending/replaying items. Timeout default 5s. |
|
|
686
|
+
| `getCachedEntry(url, name?)` | Read a `Response` from Cache Storage. Returns `undefined` if missing. |
|
|
687
|
+
| `clearEidosCache(name?)` | Delete an entire cache namespace (default: `eidos-resources-v1`). |
|
|
688
|
+
| `resetEidos()` | Full teardown: queue, resources, SW status, online state, runtime flag. |
|
|
689
|
+
| `getEidosState()` | Plain-object snapshot of store state (no store methods). |
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
605
693
|
## Known Limitations
|
|
606
694
|
|
|
607
695
|
| Limitation | Detail |
|
|
@@ -628,13 +716,13 @@ Registers a `QueryClient` with Eidos. After calling this:
|
|
|
628
716
|
- [x] TanStack Query integration (`@sweidos/eidos/query` subpath — `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient`)
|
|
629
717
|
|
|
630
718
|
**Core reliability**
|
|
631
|
-
- [
|
|
719
|
+
- [x] Optimistic updates — `onOptimistic` / `onRollback` callbacks on `action()` for instant UI feedback before server confirms
|
|
632
720
|
- [ ] Conflict resolution hook — `onConflict` callback when replaying a queued action returns 4xx; decide per-item: retry, skip, or merge
|
|
633
721
|
- [ ] Queue prioritization — `priority: 'high' | 'normal' | 'low'` on `action()`; high-priority items replay first
|
|
634
722
|
|
|
635
723
|
**DX / Tooling**
|
|
636
724
|
- [ ] Devtools panel component — drop-in `<EidosDevtools />` showing cache entries, queue state, replay status, and offline toggle
|
|
637
|
-
- [
|
|
725
|
+
- [x] Testing utilities (`@sweidos/eidos/testing`) — `mockOffline()`, `mockOnline()`, `drainQueue()`, `waitForQueueDrain()`, `getCachedEntry(url)`, `clearEidosCache()`, `resetEidos()`, `getEidosState()` for Vitest / Playwright
|
|
638
726
|
- [ ] SvelteKit / Next.js adapters — SSR-aware init helpers that skip SW registration server-side
|
|
639
727
|
|
|
640
728
|
**Performance**
|
package/dist/action.js
CHANGED
|
@@ -1,103 +1,109 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbClearQueue as
|
|
4
|
-
const
|
|
5
|
-
function
|
|
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
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
16
|
+
return f(n, n, r, t);
|
|
16
17
|
try {
|
|
17
|
-
return await
|
|
18
|
+
return await a(...r);
|
|
18
19
|
} catch {
|
|
19
|
-
return f(
|
|
20
|
+
return f(n, n, r, t);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
|
|
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(
|
|
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,
|
|
27
|
-
const
|
|
28
|
-
id:
|
|
29
|
-
actionId:
|
|
30
|
-
actionName:
|
|
31
|
-
args:
|
|
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:
|
|
39
|
+
maxRetries: s.maxRetries ?? 3,
|
|
35
40
|
status: "pending"
|
|
36
41
|
};
|
|
37
|
-
await
|
|
42
|
+
await S(i), p.getState().addQueueItem(i);
|
|
38
43
|
try {
|
|
39
|
-
const e =
|
|
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:
|
|
46
|
-
message: `"${
|
|
50
|
+
id: r,
|
|
51
|
+
message: `"${t}" queued — will execute when online`
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
|
-
function
|
|
50
|
-
return Math.min(2e3 * 2 **
|
|
54
|
+
function g(a) {
|
|
55
|
+
return Math.min(2e3 * 2 ** a, 3e5) * (0.8 + Math.random() * 0.4);
|
|
51
56
|
}
|
|
52
|
-
let
|
|
53
|
-
async function
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
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
|
-
|
|
62
|
+
y = !0;
|
|
58
63
|
try {
|
|
59
|
-
return await x(
|
|
64
|
+
return await x(a);
|
|
60
65
|
} finally {
|
|
61
|
-
|
|
66
|
+
y = !1;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
|
-
async function x(
|
|
65
|
-
const
|
|
66
|
-
(e) => !e.nextRetryAt || e.nextRetryAt <=
|
|
67
|
-
),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
79
|
+
await u(...e.args);
|
|
74
80
|
const o = Date.now();
|
|
75
|
-
return
|
|
76
|
-
|
|
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
|
|
80
|
-
if (
|
|
81
|
-
return
|
|
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
|
|
84
|
-
return
|
|
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
|
|
91
|
-
|
|
96
|
+
const u = e.status === "fulfilled" ? e.value : "failed";
|
|
97
|
+
u === "skipped" ? r.skipped++ : (r.attempted++, r[u]++);
|
|
92
98
|
}
|
|
93
|
-
return
|
|
99
|
+
return r;
|
|
94
100
|
}
|
|
95
101
|
async function q() {
|
|
96
|
-
await
|
|
102
|
+
await I(), p.getState().hydrateQueue([]);
|
|
97
103
|
}
|
|
98
104
|
export {
|
|
99
|
-
|
|
105
|
+
M as action,
|
|
100
106
|
q as clearQueue,
|
|
101
|
-
|
|
107
|
+
O as replayQueue
|
|
102
108
|
};
|
|
103
109
|
//# sourceMappingURL=action.js.map
|
package/dist/action.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n\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":"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"}
|
|
1
|
+
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n 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
|
|
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 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 $(){return y}function G(e){return D.add(e),()=>{D.delete(e)}}const c={getState:$,subscribe:G,setState:e=>{const t=typeof e=="function"?e(y):e;y={...y,...t},W()}};let w=null,P=[];function K(){return w}async function V(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 Y(w),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=w==null?void 0:w.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&&w!==null&&"sync"in w}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=w==null?void 0:w.active;if(e){for(const t of P)e.postMessage(t);P=[]}}const b=new Map,Q=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=Q.get(e);if(a)return a.then(u=>u.clone());const i=re(e,t,s);return Q.set(e,i),i.finally(()=>Q.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,f=e.startsWith("http");await Promise.all(i.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),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],f=t.maxAge!==void 0&&(u==null?void 0:u.cachedAt)!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(i&&!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(()=>{}),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
|
|
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 A=null;function m(){return A?Promise.resolve(A):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=()=>{A=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 l=d.target.result;l?(o.push(l.value),l.continue()):i()},u.onerror=()=>i(u.error);const f=n.openCursor(IDBKeyRange.only("failed"));f.onsuccess=d=>{const l=d.target.result;l?(o.push(l.value),l.continue()):i()},f.onerror=()=>i(f.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 B=new Map,H=new Map;function L(){return crypto.randomUUID()}function pe(e,t){const s=t.name||e.name||L();B.set(s,e),t.onRollback&&H.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=L(),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=B.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 f=Date.now();return e.updateQueueItem(a.id,{status:"succeeded",completedAt:f}),await O(a.id,{status:"succeeded",completedAt:f}),setTimeout(()=>{e.removeQueueItem(a.id),le(a.id)},3e3),"succeeded"}catch(f){const d=a.retryCount+1;if(d>=a.maxRetries)return e.updateQueueItem(a.id,{status:"failed",error:String(f),retryCount:d}),await O(a.id,{status:"failed",error:String(f),retryCount:d}),(u=H.get(a.actionId))==null||u(...a.args),"failed";{const l=Date.now()+we(d);return e.updateQueueItem(a.id,{status:"pending",retryCount:d,nextRetryAt:l}),await O(a.id,{status:"pending",retryCount:d,nextRetryAt:l}),"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 F(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(()=>{F({swPath:t,autoReplay:s})},[]),M.jsx(M.Fragment,{children:e})}function p(e){const t=e??(s=>s);return R.useSyncExternalStore(c.subscribe,()=>t(c.getState()))}function Ee(){return p()}function ve(e){return p(t=>t.resources[e])}function Re(){return p(e=>e.queue)}function be(e){return p(t=>t.queue.find(s=>s.id===e))}function ke(){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(o=>o.status==="pending").length),t=p(n=>n.queue.filter(o=>o.status==="failed").length),s=p(n=>n.queue.filter(o=>o.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 xe="1.0.24";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),Qe=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError})),Ae=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=Ae;exports.eidosResource=Ce;exports.eidosStatus=Qe;exports.eidosStore=Ie;exports.initEidos=F;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
|