@sweidos/eidos 1.0.24 → 1.0.30
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 +150 -11
- package/dist/action.js +98 -74
- package/dist/action.js.map +1 -1
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +597 -0
- package/dist/eidos.cjs.js +2 -2
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/index.d.ts +64 -7
- package/dist/index.js +29 -27
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +12 -0
- package/dist/react/Devtools.d.ts +7 -0
- package/dist/react/hooks.js +41 -32
- package/dist/react/hooks.js.map +1 -1
- package/dist/resource.js +65 -55
- package/dist/resource.js.map +1 -1
- package/dist/runtime.js +12 -12
- package/dist/runtime.js.map +1 -1
- package/dist/store.js +35 -26
- package/dist/store.js.map +1 -1
- package/dist/stores.js +34 -21
- package/dist/stores.js.map +1 -1
- package/dist/sveltekit.d.ts +19 -0
- package/dist/sveltekit.js +9 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -119,6 +119,15 @@ export const createOrder = action(
|
|
|
119
119
|
// Called only if maxRetries exhausted — revert the optimistic change
|
|
120
120
|
removeOptimisticOrder(payload)
|
|
121
121
|
},
|
|
122
|
+
onConflict: (error, [payload]) => {
|
|
123
|
+
// Called during replay when the server returns a 4xx (conflict, gone, etc.)
|
|
124
|
+
// Return 'skip' to silently drop the item, or 'retry' to keep retrying.
|
|
125
|
+
if (error instanceof Response && error.status === 409) {
|
|
126
|
+
removeOptimisticOrder(payload) // revert UI
|
|
127
|
+
return 'skip' // drop from queue — already handled server-side
|
|
128
|
+
}
|
|
129
|
+
return 'retry'
|
|
130
|
+
},
|
|
122
131
|
},
|
|
123
132
|
)
|
|
124
133
|
```
|
|
@@ -216,8 +225,10 @@ const createOrder = action(
|
|
|
216
225
|
reliability: 'neverLose', // persist to IndexedDB + replay on reconnect
|
|
217
226
|
maxRetries?: number, // default: 3
|
|
218
227
|
name?: string, // label in devtools
|
|
228
|
+
priority?: 'high' | 'normal' | 'low', // replay order (default: 'normal')
|
|
219
229
|
onOptimistic?: (...args) => void, // called immediately — update UI optimistically
|
|
220
230
|
onRollback?: (...args) => void, // called on permanent failure — revert UI
|
|
231
|
+
onConflict?: (error, args) => 'retry' | 'skip', // called on 4xx during replay
|
|
221
232
|
}
|
|
222
233
|
)
|
|
223
234
|
|
|
@@ -235,6 +246,37 @@ const result = await createOrder(payload)
|
|
|
235
246
|
|
|
236
247
|
**Exponential backoff:** `neverLose` actions that fail are retried with `2s × 2^retryCount` delay (capped at 5 min, ±20% jitter). Items not yet due are skipped on each replay pass.
|
|
237
248
|
|
|
249
|
+
**Conflict resolution:** when a 4xx HTTP response occurs during replay, `onConflict` is called with the thrown error and the original args. Return `'skip'` to silently remove the item from the queue without calling `onRollback`, or `'retry'` to continue normal retry/backoff behaviour.
|
|
250
|
+
|
|
251
|
+
A 4xx is detected when the thrown value is a `Response` with `status` in [400, 499], or any object with a `.status` property in that range.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
onConflict: (error, [payload]) => {
|
|
255
|
+
if (error instanceof Response && error.status === 409) {
|
|
256
|
+
// already created server-side — safe to drop and revert UI
|
|
257
|
+
removeOptimisticOrder(payload)
|
|
258
|
+
return 'skip'
|
|
259
|
+
}
|
|
260
|
+
return 'retry' // keep in queue for everything else
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Queue prioritization:** `priority` controls the replay order when multiple queued actions are pending. `'high'` items all complete before `'normal'` items start; `'normal'` all complete before `'low'` items start. Within each tier, items run in parallel. Default: `'normal'`.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// Critical write — replays before any normal/low actions
|
|
268
|
+
const saveDocument = action(api.saveDocument, {
|
|
269
|
+
reliability: 'neverLose',
|
|
270
|
+
priority: 'high',
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Background analytics — replays last, after user-visible writes
|
|
274
|
+
const logEvent = action(api.logEvent, {
|
|
275
|
+
reliability: 'neverLose',
|
|
276
|
+
priority: 'low',
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
238
280
|
---
|
|
239
281
|
|
|
240
282
|
### `replayQueue()`
|
|
@@ -247,13 +289,14 @@ import type { ReplayResult } from '@sweidos/eidos'
|
|
|
247
289
|
|
|
248
290
|
// Manual trigger — e.g. after a user clicks "Retry"
|
|
249
291
|
const result: ReplayResult = await replayQueue()
|
|
250
|
-
// { attempted: 3, succeeded: 2, failed: 0, retrying: 1, skipped: 0 }
|
|
292
|
+
// { attempted: 3, succeeded: 2, failed: 0, retrying: 1, skipped: 0, conflicted: 0 }
|
|
251
293
|
//
|
|
252
|
-
// attempted
|
|
253
|
-
// succeeded
|
|
254
|
-
// failed
|
|
255
|
-
// retrying
|
|
256
|
-
// skipped
|
|
294
|
+
// attempted — items where the fn was found and called
|
|
295
|
+
// succeeded — resolved successfully
|
|
296
|
+
// failed — maxRetries exceeded, stays in queue
|
|
297
|
+
// retrying — failed, will retry later (nextRetryAt set)
|
|
298
|
+
// skipped — fn not in registry (module not imported yet)
|
|
299
|
+
// conflicted — 4xx response, onConflict returned 'skip', removed from queue
|
|
257
300
|
```
|
|
258
301
|
|
|
259
302
|
---
|
|
@@ -391,6 +434,29 @@ const hits = eidosResource('/api/products').getState()?.cacheHits ?? 0
|
|
|
391
434
|
|
|
392
435
|
---
|
|
393
436
|
|
|
437
|
+
### `warmCache(handles[])`
|
|
438
|
+
|
|
439
|
+
Bulk-prefetch an array of resource handles concurrently — warms the cache for all of them in one call. Useful on login or app init when you know which resources the user will need offline.
|
|
440
|
+
|
|
441
|
+
Pattern handles (containing `*`, `**`, or `:param`) are counted as failed — they match multiple URLs so there is no single URL to prefetch.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { warmCache } from '@sweidos/eidos'
|
|
445
|
+
import type { WarmCacheResult } from '@sweidos/eidos'
|
|
446
|
+
|
|
447
|
+
// After login — warm the cache with the user's likely-needed data
|
|
448
|
+
const result: WarmCacheResult = await warmCache([products, userProfile, settings])
|
|
449
|
+
// { warmed: 3, failed: 0, errors: [] }
|
|
450
|
+
//
|
|
451
|
+
// warmed — handles prefetched successfully
|
|
452
|
+
// failed — handles that threw (network error, offline, pattern handle, etc.)
|
|
453
|
+
// errors — the raw thrown values for failed handles
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
In development, a `console.warn` is printed for each failed handle.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
394
460
|
### `setOfflineSimulation(enabled)`
|
|
395
461
|
|
|
396
462
|
Toggle offline simulation without physically disconnecting the network.
|
|
@@ -690,6 +756,79 @@ it('caches the resource after first fetch', async () => {
|
|
|
690
756
|
|
|
691
757
|
---
|
|
692
758
|
|
|
759
|
+
## SSR Adapters
|
|
760
|
+
|
|
761
|
+
Eidos is browser-only — Service Workers, Cache API, and IndexedDB are not available in Node.js. The runtime already no-ops safely when `window` is undefined, but two subpath exports make integration with SSR frameworks seamless.
|
|
762
|
+
|
|
763
|
+
### Next.js App Router (`@sweidos/eidos/nextjs`)
|
|
764
|
+
|
|
765
|
+
Imports from this subpath are pre-marked `'use client'`, so you can use `EidosProvider` and all hooks directly in your App Router layout without creating your own wrapper file.
|
|
766
|
+
|
|
767
|
+
```tsx
|
|
768
|
+
// app/providers.tsx ← no 'use client' needed here
|
|
769
|
+
import { EidosProvider, useEidosStatus } from '@sweidos/eidos/nextjs'
|
|
770
|
+
|
|
771
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
772
|
+
return <EidosProvider swPath="/eidos-sw.js">{children}</EidosProvider>
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
The `'use client'` boundary is on the published `dist/nextjs.js` — Next.js recognises it and marks everything imported through that entry as client code.
|
|
777
|
+
|
|
778
|
+
### SvelteKit (`@sweidos/eidos/sveltekit`)
|
|
779
|
+
|
|
780
|
+
Use `initEidosSvelteKit()` inside `onMount` in your root `+layout.svelte`. The helper returns an `onMount`-compatible callback that defers init to the browser, keeping SSR clean.
|
|
781
|
+
|
|
782
|
+
```svelte
|
|
783
|
+
<!-- src/routes/+layout.svelte -->
|
|
784
|
+
<script>
|
|
785
|
+
import { onMount } from 'svelte'
|
|
786
|
+
import { initEidosSvelteKit } from '@sweidos/eidos/sveltekit'
|
|
787
|
+
|
|
788
|
+
onMount(initEidosSvelteKit({ swPath: '/eidos-sw.js', autoReplay: true }))
|
|
789
|
+
</script>
|
|
790
|
+
|
|
791
|
+
<slot />
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Use the framework-agnostic stores (`eidosQueue`, `eidosStatus`, etc.) from the main `@sweidos/eidos` import in your Svelte components — they work with Svelte's `$` auto-subscribe prefix out of the box.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Devtools
|
|
799
|
+
|
|
800
|
+
`@sweidos/eidos/devtools` exports a floating panel component you can drop into any React app during development. It shows live queue state, cache entries, SW registration status, and lets you toggle offline simulation — all without leaving your app.
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
import { EidosDevtools } from '@sweidos/eidos/devtools'
|
|
804
|
+
|
|
805
|
+
// Add anywhere in your component tree (bottom-right by default)
|
|
806
|
+
export default function App() {
|
|
807
|
+
return (
|
|
808
|
+
<>
|
|
809
|
+
<YourApp />
|
|
810
|
+
{process.env.NODE_ENV === 'development' && <EidosDevtools />}
|
|
811
|
+
</>
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Props:**
|
|
817
|
+
|
|
818
|
+
| Prop | Type | Default | Description |
|
|
819
|
+
|------|------|---------|-------------|
|
|
820
|
+
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Corner to anchor the panel |
|
|
821
|
+
| `defaultOpen` | `boolean` | `false` | Start expanded |
|
|
822
|
+
|
|
823
|
+
**Panel features:**
|
|
824
|
+
- **Status bar** — online/offline indicator, SW registration status, offline simulation toggle (`setOfflineSimulation`)
|
|
825
|
+
- **Queue tab** — all queue items with status badges (`pending` / `replaying` / `succeeded` / `failed`), priority, retry count, plus Replay and Clear buttons
|
|
826
|
+
- **Cache tab** — all registered resources with cache status, strategy name, hit/miss counts, and last cached timestamp
|
|
827
|
+
|
|
828
|
+
The component is self-contained with inline styles — no CSS import needed, no style conflicts.
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
693
832
|
## Known Limitations
|
|
694
833
|
|
|
695
834
|
| Limitation | Detail |
|
|
@@ -717,17 +856,17 @@ it('caches the resource after first fetch', async () => {
|
|
|
717
856
|
|
|
718
857
|
**Core reliability**
|
|
719
858
|
- [x] Optimistic updates — `onOptimistic` / `onRollback` callbacks on `action()` for instant UI feedback before server confirms
|
|
720
|
-
- [
|
|
721
|
-
- [
|
|
859
|
+
- [x] Conflict resolution hook — `onConflict` callback when replaying a queued action returns 4xx; decide per-item: retry or skip
|
|
860
|
+
- [x] Queue prioritization — `priority: 'high' | 'normal' | 'low'` on `action()`; high-priority items replay first
|
|
722
861
|
|
|
723
862
|
**DX / Tooling**
|
|
724
|
-
- [
|
|
863
|
+
- [x] Devtools panel component — drop-in `<EidosDevtools />` showing cache entries, queue state, replay status, and offline toggle
|
|
725
864
|
- [x] Testing utilities (`@sweidos/eidos/testing`) — `mockOffline()`, `mockOnline()`, `drainQueue()`, `waitForQueueDrain()`, `getCachedEntry(url)`, `clearEidosCache()`, `resetEidos()`, `getEidosState()` for Vitest / Playwright
|
|
726
|
-
- [
|
|
865
|
+
- [x] SvelteKit / Next.js adapters — SSR-aware init helpers that skip SW registration server-side
|
|
727
866
|
|
|
728
867
|
**Performance**
|
|
729
868
|
- [x] Request deduplication — multiple simultaneous `resource.fetch()` calls share one in-flight network request; each caller gets an independent cloned `Response`
|
|
730
|
-
- [
|
|
869
|
+
- [x] Cache warming — `warmCache(handles[])` bulk-prefetches a list of resources on init (e.g. on login)
|
|
731
870
|
|
|
732
871
|
**Ecosystem**
|
|
733
872
|
- [ ] React Native support — AsyncStorage + fetch-based backend (no Cache API / SW); same `resource` / `action` API surface
|
package/dist/action.js
CHANGED
|
@@ -1,109 +1,133 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbClearQueue as I, idbGetPendingItems as
|
|
4
|
-
const
|
|
5
|
-
function
|
|
1
|
+
import { useEidosStore as d } from "./store.js";
|
|
2
|
+
import { getSwRegistration as h } from "./sw-bridge.js";
|
|
3
|
+
import { idbClearQueue as I, idbGetPendingItems as Q, idbAddToQueue as R, idbUpdateQueueItem as u, idbRemoveFromQueue as p } from "./idb.js";
|
|
4
|
+
const l = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), w = /* @__PURE__ */ new Map();
|
|
5
|
+
function m() {
|
|
6
6
|
return crypto.randomUUID();
|
|
7
7
|
}
|
|
8
|
-
function M(
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const s = async (...
|
|
12
|
-
var
|
|
13
|
-
const { isOnline:
|
|
14
|
-
if ((
|
|
15
|
-
if (!
|
|
16
|
-
return f(
|
|
8
|
+
function M(e, t) {
|
|
9
|
+
const r = t.name || e.name || m();
|
|
10
|
+
l.set(r, e), t.onRollback && y.set(r, t.onRollback), t.onConflict && w.set(r, t.onConflict);
|
|
11
|
+
const s = async (...a) => {
|
|
12
|
+
var i, o;
|
|
13
|
+
const { isOnline: n } = d.getState();
|
|
14
|
+
if ((i = t.onOptimistic) == null || i.call(t, ...a), t.reliability === "neverLose") {
|
|
15
|
+
if (!n)
|
|
16
|
+
return f(r, r, a, t);
|
|
17
17
|
try {
|
|
18
|
-
return await
|
|
18
|
+
return await e(...a);
|
|
19
19
|
} catch {
|
|
20
|
-
return f(
|
|
20
|
+
return f(r, r, a, t);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
try {
|
|
24
|
-
return await
|
|
25
|
-
} catch (
|
|
26
|
-
throw (
|
|
24
|
+
return await e(...a);
|
|
25
|
+
} catch (b) {
|
|
26
|
+
throw (o = t.onRollback) == null || o.call(t, ...a), b;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
return Object.defineProperty(s, "id", { value:
|
|
29
|
+
return Object.defineProperty(s, "id", { value: r, writable: !1 }), Object.defineProperty(s, "config", { value: t, writable: !1 }), s;
|
|
30
30
|
}
|
|
31
|
-
async function f(
|
|
32
|
-
const
|
|
33
|
-
id:
|
|
34
|
-
actionId:
|
|
31
|
+
async function f(e, t, r, s) {
|
|
32
|
+
const a = m(), n = {
|
|
33
|
+
id: a,
|
|
34
|
+
actionId: e,
|
|
35
35
|
actionName: t,
|
|
36
|
-
args:
|
|
36
|
+
args: r,
|
|
37
37
|
queuedAt: Date.now(),
|
|
38
38
|
retryCount: 0,
|
|
39
39
|
maxRetries: s.maxRetries ?? 3,
|
|
40
|
-
status: "pending"
|
|
40
|
+
status: "pending",
|
|
41
|
+
priority: s.priority ?? "normal"
|
|
41
42
|
};
|
|
42
|
-
await
|
|
43
|
+
await R(n), d.getState().addQueueItem(n);
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
45
|
+
const i = h();
|
|
46
|
+
i && "sync" in i && await i.sync.register("eidos-queue-replay");
|
|
46
47
|
} catch {
|
|
47
48
|
}
|
|
48
49
|
return {
|
|
49
50
|
queued: !0,
|
|
50
|
-
id:
|
|
51
|
+
id: a,
|
|
51
52
|
message: `"${t}" queued — will execute when online`
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
|
-
function g(
|
|
55
|
-
|
|
55
|
+
function g(e) {
|
|
56
|
+
if (e instanceof Response) return e.status >= 400 && e.status < 500;
|
|
57
|
+
if (typeof e == "object" && e !== null) {
|
|
58
|
+
const t = e.status;
|
|
59
|
+
if (typeof t == "number") return t >= 400 && t < 500;
|
|
60
|
+
}
|
|
61
|
+
return !1;
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
function k(e) {
|
|
64
|
+
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
65
|
+
}
|
|
66
|
+
let c = !1;
|
|
67
|
+
async function D() {
|
|
68
|
+
const e = d.getState();
|
|
69
|
+
if (!e.isOnline || c)
|
|
70
|
+
return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
71
|
+
c = !0;
|
|
63
72
|
try {
|
|
64
|
-
return await
|
|
73
|
+
return await C(e);
|
|
65
74
|
} finally {
|
|
66
|
-
|
|
75
|
+
c = !1;
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
|
-
async function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
async function S(e, t) {
|
|
79
|
+
var s;
|
|
80
|
+
const r = l.get(e.actionId);
|
|
81
|
+
if (!r) return "skipped";
|
|
82
|
+
try {
|
|
83
|
+
await r(...e.args);
|
|
84
|
+
const a = Date.now();
|
|
85
|
+
return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await u(e.id, { status: "succeeded", completedAt: a }), setTimeout(() => {
|
|
86
|
+
t.removeQueueItem(e.id), p(e.id);
|
|
87
|
+
}, 3e3), "succeeded";
|
|
88
|
+
} catch (a) {
|
|
89
|
+
if (g(a)) {
|
|
90
|
+
const i = w.get(e.actionId);
|
|
91
|
+
if (i && i(a, e.args) === "skip")
|
|
92
|
+
return t.removeQueueItem(e.id), await p(e.id), "conflicted";
|
|
93
|
+
}
|
|
94
|
+
const n = e.retryCount + 1;
|
|
95
|
+
if (n >= e.maxRetries)
|
|
96
|
+
return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await u(e.id, { status: "failed", error: String(a), retryCount: n }), (s = y.get(e.actionId)) == null || s(...e.args), "failed";
|
|
97
|
+
{
|
|
98
|
+
const i = Date.now() + k(n);
|
|
99
|
+
return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await u(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), "retrying";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function x(e, t, r) {
|
|
104
|
+
if (e.length === 0) return;
|
|
105
|
+
const s = e.filter((n) => l.has(n.actionId));
|
|
106
|
+
if (r.skipped += e.length - s.length, s.length > 0) {
|
|
107
|
+
t.batchUpdateQueueItems(s.map((n) => ({ id: n.id, update: { status: "replaying" } })));
|
|
108
|
+
for (const n of s)
|
|
109
|
+
u(n.id, { status: "replaying" });
|
|
110
|
+
}
|
|
111
|
+
const a = await Promise.allSettled(s.map((n) => S(n, t)));
|
|
112
|
+
for (const n of a) {
|
|
113
|
+
const i = n.status === "fulfilled" ? n.value : "failed";
|
|
114
|
+
i === "skipped" ? r.skipped++ : i === "conflicted" ? r.conflicted++ : (r.attempted++, r[i]++);
|
|
98
115
|
}
|
|
99
|
-
return r;
|
|
100
116
|
}
|
|
101
|
-
async function
|
|
102
|
-
await
|
|
117
|
+
async function C(e) {
|
|
118
|
+
const t = await Q(), r = Date.now(), s = t.filter((n) => !n.nextRetryAt || n.nextRetryAt <= r), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
119
|
+
for (const n of ["high", "normal", "low"]) {
|
|
120
|
+
const i = s.filter((o) => (o.priority ?? "normal") === n);
|
|
121
|
+
await x(i, e, a);
|
|
122
|
+
}
|
|
123
|
+
return a;
|
|
124
|
+
}
|
|
125
|
+
async function O() {
|
|
126
|
+
await I(), d.getState().hydrateQueue([]);
|
|
103
127
|
}
|
|
104
128
|
export {
|
|
105
129
|
M as action,
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
O as clearQueue,
|
|
131
|
+
D as replayQueue
|
|
108
132
|
};
|
|
109
133
|
//# sourceMappingURL=action.js.map
|
package/dist/action.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n\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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"}
|
|
1
|
+
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await idbAddToQueue(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await idbUpdateQueueItem(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await idbRemoveFromQueue(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await idbUpdateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await idbUpdateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n idbUpdateQueueItem(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (IDB + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await idbClearQueue()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","idbUpdateQueueItem","idbRemoveFromQueue","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","idbGetPendingItems","now","pending","tier","tierItems","clearQueue","idbClearQueue"],"mappings":"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"}
|