@sweidos/eidos 1.1.0 → 2.0.0
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 +127 -32
- package/dist/action.js +223 -112
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/cli.js +102 -0
- package/dist/devtools.js +208 -71
- package/dist/eidos-sw.js +283 -188
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +160 -26
- package/dist/index.js +45 -41
- package/dist/push.cjs +123 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +116 -0
- package/dist/query.cjs +1 -1
- package/dist/query.d.ts +1 -2
- package/dist/query.js +1 -1
- package/dist/queue-storage.js.map +1 -0
- package/dist/queue-sync.js +34 -0
- package/dist/queue-sync.js.map +1 -0
- package/dist/react/Provider.js.map +1 -0
- package/dist/react/hooks.js +23 -23
- package/dist/react/hooks.js.map +1 -0
- package/dist/replay.js.map +1 -0
- package/dist/resource.js +121 -107
- package/dist/resource.js.map +1 -0
- package/dist/runtime.js +37 -19
- package/dist/runtime.js.map +1 -0
- package/dist/store-slices.js.map +1 -0
- package/dist/store.js.map +1 -0
- package/dist/stores.js +23 -31
- package/dist/stores.js.map +1 -0
- package/dist/sw-bridge.js +44 -31
- package/dist/sw-bridge.js.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -114,20 +114,22 @@ if ('queued' in result) {
|
|
|
114
114
|
|
|
115
115
|
## What you get
|
|
116
116
|
|
|
117
|
-
| Feature | Description
|
|
118
|
-
| --------------------------- |
|
|
119
|
-
| **Auto strategy selection** | `offline: true` → StaleWhileRevalidate. No config needed. Override when you want.
|
|
120
|
-
| **Persistent action queue** | Failed writes go to IndexedDB and replay with exponential backoff on reconnect.
|
|
121
|
-
| **Request deduplication** | Concurrent `resource.fetch()` calls share one in-flight request.
|
|
122
|
-
| **Optimistic updates** | `onOptimistic` / `onRollback` callbacks for instant UI feedback.
|
|
123
|
-
| **Conflict resolution** | `
|
|
124
|
-
| **
|
|
125
|
-
| **
|
|
126
|
-
| **
|
|
127
|
-
| **
|
|
128
|
-
| **
|
|
129
|
-
| **
|
|
130
|
-
| **
|
|
117
|
+
| Feature | Description |
|
|
118
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| **Auto strategy selection** | `offline: true` → StaleWhileRevalidate. No config needed. Override when you want. |
|
|
120
|
+
| **Persistent action queue** | Failed writes go to IndexedDB and replay with exponential backoff on reconnect. |
|
|
121
|
+
| **Request deduplication** | Concurrent `resource.fetch()` calls share one in-flight request. |
|
|
122
|
+
| **Optimistic updates** | `onOptimistic` / `onRollback` callbacks for instant UI feedback. |
|
|
123
|
+
| **Conflict resolution** | `conflict: { strategy: 'serverWins' \| 'clientWins' \| 'merge' \| 'custom' }` on 4xx replay responses. |
|
|
124
|
+
| **Idempotent replay** | Stable `idempotencyKey` per invocation, forwarded to `fn` via `ActionContext` — safe retries even after a dropped response. |
|
|
125
|
+
| **Cancellable actions** | `cancellable: true` → `AbortSignal` per call, plus `handle.cancel(idempotencyKey)`. |
|
|
126
|
+
| **Queue prioritization** | `priority: 'high' \| 'normal' \| 'low'` — high items replay before normal. |
|
|
127
|
+
| **Cache warming** | `warmCache(handles[])` bulk-prefetches resources on login/init. |
|
|
128
|
+
| **URL patterns** | `/api/products/*`, `/api/users/:id`, `**` wildcards — SW intercepts all matches. |
|
|
129
|
+
| **Background Sync** | Registers a `sync` tag so queued actions replay even after tab close. |
|
|
130
|
+
| **Devtools panel** | `<EidosDevtools />` — live queue, cache state, offline toggle, no CSS import. |
|
|
131
|
+
| **Testing helpers** | `mockOffline`, `drainQueue`, `resetEidos`, `getCachedEntry` for Vitest/Jest. |
|
|
132
|
+
| **OpenAPI codegen** | `npx eidos-gen openapi.json` generates typed `resource()` + `action()` declarations. |
|
|
131
133
|
|
|
132
134
|
---
|
|
133
135
|
|
|
@@ -175,20 +177,40 @@ products.query() // { queryKey, queryFn } for useQuery
|
|
|
175
177
|
| `offline: true, strategy: 'cache-first'` | CacheFirst | Static assets, config data |
|
|
176
178
|
| `offline: true, strategy: 'network-first'` | NetworkFirst | Always-fresh with offline fallback |
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
### `resourcePattern(pattern, config)`
|
|
181
|
+
|
|
182
|
+
For URL patterns — `/api/products/*`, `/api/users/:id`, `**` — the SW intercepts
|
|
183
|
+
all matching requests automatically, so there's no single URL to fetch. Use
|
|
184
|
+
`resourcePattern()` instead of `resource()`; it returns a handle with only
|
|
185
|
+
`invalidate()` and `unregister()`:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const productPattern = resourcePattern('/api/products/*', { offline: true });
|
|
189
|
+
|
|
190
|
+
await productPattern.invalidate(); // clear all cached entries matching the pattern
|
|
191
|
+
productPattern.unregister();
|
|
192
|
+
```
|
|
179
193
|
|
|
180
194
|
### `action(fn, config)`
|
|
181
195
|
|
|
182
196
|
```ts
|
|
183
|
-
const createOrder = action(async (payload: OrderPayload) => { ... }, {
|
|
197
|
+
const createOrder = action(async (payload: OrderPayload, ctx: ActionContext) => { ... }, {
|
|
184
198
|
reliability: 'neverLose', // persist to IDB + replay on reconnect
|
|
185
199
|
name: 'createOrder', // stable name for post-reload replay
|
|
200
|
+
namespace?: string, // prefix actionId — avoids collisions across modules
|
|
186
201
|
maxRetries?: number, // default: 3
|
|
187
202
|
priority?: 'high' | 'normal' | 'low',
|
|
203
|
+
cancellable?: boolean, // adds AbortSignal to ctx, enables handle.cancel(key)
|
|
188
204
|
onOptimistic?: (...args) => void, // instant UI update
|
|
189
205
|
onRollback?: (...args) => void, // revert on permanent failure
|
|
190
|
-
|
|
206
|
+
conflict?: { // 4xx replay handling
|
|
207
|
+
strategy: 'serverWins' | 'clientWins' | 'merge' | 'custom',
|
|
208
|
+
resolve?: (ctx) => 'retry' | 'skip' | { resolved: args },
|
|
209
|
+
},
|
|
191
210
|
})
|
|
211
|
+
|
|
212
|
+
// ctx.idempotencyKey is stable across retries — forward as e.g. an
|
|
213
|
+
// `Idempotency-Key` header so the server can dedupe replayed writes.
|
|
192
214
|
```
|
|
193
215
|
|
|
194
216
|
### React hooks
|
|
@@ -233,6 +255,81 @@ const mutation = useEidosMutation(createOrder, {
|
|
|
233
255
|
|
|
234
256
|
---
|
|
235
257
|
|
|
258
|
+
## Push Notifications
|
|
259
|
+
|
|
260
|
+
Headless, framework-agnostic Web Push. Tree-shaken via a separate subpath — adds zero bytes unless imported.
|
|
261
|
+
|
|
262
|
+
**1. Generate VAPID keys (one-time):**
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
npx @sweidos/eidos generate-vapid-keys
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Detects your framework (Vite/Next/SvelteKit/Nuxt) and writes a correctly-prefixed
|
|
269
|
+
public key + an unprefixed private key to `.env.local`:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
VITE_EIDOS_VAPID_PUBLIC_KEY=...
|
|
273
|
+
EIDOS_VAPID_PRIVATE_KEY=...
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Give `EIDOS_VAPID_PRIVATE_KEY` (and the public key) to your backend. What the
|
|
277
|
+
backend does with them — language, storage, send timing — is entirely its own
|
|
278
|
+
concern; Eidos never talks to it directly.
|
|
279
|
+
|
|
280
|
+
**2. Register handlers once at app init (any tab, no permission prompt):**
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { registerPushHandlers } from '@sweidos/eidos/push';
|
|
284
|
+
|
|
285
|
+
registerPushHandlers({
|
|
286
|
+
onNotificationClick: (data) => router.push(data.url),
|
|
287
|
+
onSubscriptionExpired: (sub) =>
|
|
288
|
+
fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**3. Subscribe from a user gesture (e.g. an "Enable notifications" button):**
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { subscribeToPush, isPushSupported, getPushPermissionState } from '@sweidos/eidos/push';
|
|
296
|
+
|
|
297
|
+
async function onEnableClick() {
|
|
298
|
+
const result = await subscribeToPush({
|
|
299
|
+
vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
|
|
300
|
+
onSubscribe: (sub) =>
|
|
301
|
+
fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (result.status === 'subscribed') toast('Notifications enabled');
|
|
305
|
+
else if (result.status === 'denied') toast('Permission denied');
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`isPushSupported()` / `getPushPermissionState()` / `getPushUnsupportedReason()`
|
|
310
|
+
let you hide the button when push is unavailable (e.g. iOS Safari outside an
|
|
311
|
+
installed PWA returns `'ios-not-installed'`).
|
|
312
|
+
|
|
313
|
+
### Server payload schema
|
|
314
|
+
|
|
315
|
+
The service worker shows whatever your server sends — Eidos never renders UI:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"title": "Order shipped",
|
|
320
|
+
"body": "Your order #1234 is on its way",
|
|
321
|
+
"icon": "/icon.png",
|
|
322
|
+
"badge": "/badge.png",
|
|
323
|
+
"tag": "order-1234",
|
|
324
|
+
"data": { "url": "/orders/1234" }
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Click behavior: if the app is open, `data` is delivered to `onNotificationClick`
|
|
329
|
+
for client-side routing; otherwise the SW opens `data.url` directly.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
236
333
|
## Testing
|
|
237
334
|
|
|
238
335
|
```ts
|
|
@@ -314,21 +411,19 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
|
|
|
314
411
|
|
|
315
412
|
## How it compares
|
|
316
413
|
|
|
317
|
-
|
|
|
318
|
-
|
|
|
319
|
-
| Service worker setup
|
|
320
|
-
| Caching strategy
|
|
321
|
-
| Offline writes
|
|
322
|
-
| Framework support
|
|
323
|
-
| TanStack Query bridge
|
|
324
|
-
| Bundle size (core
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
`resource()`/`action()` declarations instead of you writing `workbox-*` config
|
|
331
|
-
by hand.
|
|
414
|
+
| | **Eidos** | Workbox | RTK Query / TanStack Query |
|
|
415
|
+
| --------------------- | ----------------------------------------------------- | ---------------------------- | -------------------------- |
|
|
416
|
+
| Service worker setup | Generated from `resource()`/`action()` declarations | Hand-written routing config | None — no SW |
|
|
417
|
+
| Caching strategy | Auto-derived from intent, inspectable via devtools | Manually chosen per route | `staleTime`/`gcTime` only |
|
|
418
|
+
| Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
|
|
419
|
+
| Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
|
|
420
|
+
| TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
|
|
421
|
+
| Bundle size (core) | ~6 kB brotli | ~3-6 kB (modular) | ~13 kB |
|
|
422
|
+
|
|
423
|
+
Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
|
|
424
|
+
you keep TQ's cache/devtools while Eidos owns the offline layer. Workbox is a
|
|
425
|
+
lower-level toolkit; Eidos picks and configures strategies for you instead of
|
|
426
|
+
hand-written `workbox-*` config.
|
|
332
427
|
|
|
333
428
|
---
|
|
334
429
|
|
package/dist/action.js
CHANGED
|
@@ -1,57 +1,92 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbQueueStorage as
|
|
4
|
-
import { _getQueueStorage as
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { useEidosStore as f } from "./store.js";
|
|
2
|
+
import { getSwRegistration as _ } from "./sw-bridge.js";
|
|
3
|
+
import { idbQueueStorage as A } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as S } from "./queue-storage.js";
|
|
5
|
+
import { broadcastQueueSync as d } from "./queue-sync.js";
|
|
6
|
+
var m = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), Q = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map();
|
|
7
|
+
function c() {
|
|
8
|
+
return S() ?? A;
|
|
9
|
+
}
|
|
10
|
+
function b() {
|
|
10
11
|
return crypto.randomUUID();
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
function h(e, t, o) {
|
|
14
|
+
return e(...t, o);
|
|
15
|
+
}
|
|
16
|
+
function F(e, t) {
|
|
17
|
+
const o = t.name || e.name || b(), a = t.namespace ? `${t.namespace}::${o}` : o;
|
|
18
|
+
if (m.has(a)) throw new Error(`[eidos] duplicate action id "${a}" — an action with this id is already registered. Pass a unique config.name or config.namespace.`);
|
|
19
|
+
m.set(a, e), x.set(a, t), t.onRollback && k.set(a, t.onRollback), t.conflict && Q.set(a, t.conflict);
|
|
20
|
+
const r = async (...n) => {
|
|
21
|
+
const { isOnline: s } = f.getState(), u = b();
|
|
22
|
+
let p;
|
|
23
|
+
if (t.cancellable) {
|
|
24
|
+
const l = new AbortController();
|
|
25
|
+
y.set(u, l), p = l.signal;
|
|
24
26
|
}
|
|
27
|
+
const w = {
|
|
28
|
+
idempotencyKey: u,
|
|
29
|
+
attempt: 0,
|
|
30
|
+
signal: p
|
|
31
|
+
};
|
|
32
|
+
t.onOptimistic?.(...n, w);
|
|
25
33
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
if (t.reliability === "neverLose") {
|
|
35
|
+
if (!s) return R(a, a, n, t, u);
|
|
36
|
+
try {
|
|
37
|
+
return await h(e, n, w);
|
|
38
|
+
} catch (l) {
|
|
39
|
+
if (C(l)) throw l;
|
|
40
|
+
return R(a, a, n, t, u);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return await h(e, n, w);
|
|
45
|
+
} catch (l) {
|
|
46
|
+
throw t.onRollback?.(...n, w), l;
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
t.cancellable && y.delete(u);
|
|
29
50
|
}
|
|
51
|
+
}, i = async (n) => {
|
|
52
|
+
const s = y.get(n);
|
|
53
|
+
if (s)
|
|
54
|
+
return s.abort(), !0;
|
|
55
|
+
const u = (await c().getAll()).find((p) => p.idempotencyKey === n && p.status === "pending");
|
|
56
|
+
return u ? (f.getState().removeQueueItem(u.id), d({
|
|
57
|
+
type: "remove",
|
|
58
|
+
id: u.id
|
|
59
|
+
}), await c().remove(u.id), !0) : !1;
|
|
30
60
|
};
|
|
31
|
-
return Object.defineProperty(
|
|
32
|
-
value:
|
|
61
|
+
return Object.defineProperty(r, "id", {
|
|
62
|
+
value: a,
|
|
33
63
|
writable: !1
|
|
34
|
-
}), Object.defineProperty(
|
|
64
|
+
}), Object.defineProperty(r, "config", {
|
|
35
65
|
value: t,
|
|
36
66
|
writable: !1
|
|
37
|
-
}),
|
|
67
|
+
}), Object.defineProperty(r, "cancel", {
|
|
68
|
+
value: i,
|
|
69
|
+
writable: !1
|
|
70
|
+
}), r;
|
|
38
71
|
}
|
|
39
|
-
async function
|
|
40
|
-
const i =
|
|
72
|
+
async function R(e, t, o, a, r) {
|
|
73
|
+
const i = b(), n = {
|
|
74
|
+
schemaVersion: 2,
|
|
41
75
|
id: i,
|
|
42
76
|
actionId: e,
|
|
43
77
|
actionName: t,
|
|
44
|
-
|
|
78
|
+
idempotencyKey: r,
|
|
79
|
+
args: o,
|
|
45
80
|
queuedAt: Date.now(),
|
|
46
81
|
retryCount: 0,
|
|
47
82
|
maxRetries: a.maxRetries ?? 3,
|
|
48
83
|
status: "pending",
|
|
49
84
|
priority: a.priority ?? "normal"
|
|
50
85
|
};
|
|
51
|
-
await
|
|
86
|
+
await c().add(n), f.getState().addQueueItem(n);
|
|
52
87
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
88
|
+
const s = _();
|
|
89
|
+
s && "sync" in s && await s.sync.register("eidos-queue-replay");
|
|
55
90
|
} catch {
|
|
56
91
|
}
|
|
57
92
|
return {
|
|
@@ -60,7 +95,10 @@ async function l(e, t, r, a) {
|
|
|
60
95
|
message: `"${t}" queued — will execute when online`
|
|
61
96
|
};
|
|
62
97
|
}
|
|
63
|
-
function
|
|
98
|
+
function C(e) {
|
|
99
|
+
return e instanceof DOMException && e.name === "AbortError";
|
|
100
|
+
}
|
|
101
|
+
function K(e) {
|
|
64
102
|
if (e instanceof Response) return e.status >= 400 && e.status < 500;
|
|
65
103
|
if (typeof e == "object" && e !== null) {
|
|
66
104
|
const t = e.status;
|
|
@@ -68,112 +106,185 @@ function h(e) {
|
|
|
68
106
|
}
|
|
69
107
|
return !1;
|
|
70
108
|
}
|
|
71
|
-
function
|
|
109
|
+
function M(e) {
|
|
72
110
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
73
111
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const e = s.getState();
|
|
77
|
-
if (!e.isOnline || c) return {
|
|
112
|
+
function g() {
|
|
113
|
+
return {
|
|
78
114
|
attempted: 0,
|
|
79
115
|
succeeded: 0,
|
|
80
116
|
failed: 0,
|
|
81
117
|
retrying: 0,
|
|
82
118
|
skipped: 0,
|
|
83
|
-
conflicted: 0
|
|
119
|
+
conflicted: 0,
|
|
120
|
+
cancelled: 0
|
|
84
121
|
};
|
|
85
|
-
|
|
122
|
+
}
|
|
123
|
+
var v = !1, O = "eidos-queue-replay";
|
|
124
|
+
async function V() {
|
|
125
|
+
const e = f.getState();
|
|
126
|
+
if (!e.isOnline) return g();
|
|
127
|
+
if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(O, { ifAvailable: !0 }, async (t) => t ? I(e) : g());
|
|
128
|
+
if (v) return g();
|
|
129
|
+
v = !0;
|
|
86
130
|
try {
|
|
87
|
-
return await
|
|
131
|
+
return await I(e);
|
|
88
132
|
} finally {
|
|
89
|
-
|
|
133
|
+
v = !1;
|
|
90
134
|
}
|
|
91
135
|
}
|
|
92
|
-
async function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
async function q(e, t) {
|
|
137
|
+
const o = Date.now();
|
|
138
|
+
t.updateQueueItem(e.id, {
|
|
139
|
+
status: "succeeded",
|
|
140
|
+
completedAt: o
|
|
141
|
+
}), d({
|
|
142
|
+
type: "update",
|
|
143
|
+
id: e.id,
|
|
144
|
+
update: {
|
|
99
145
|
status: "succeeded",
|
|
100
|
-
completedAt:
|
|
101
|
-
}), await u().update(e.id, {
|
|
102
|
-
status: "succeeded",
|
|
103
|
-
completedAt: a
|
|
104
|
-
}), setTimeout(() => {
|
|
105
|
-
t.removeQueueItem(e.id), u().remove(e.id);
|
|
106
|
-
}, 3e3), "succeeded";
|
|
107
|
-
} catch (a) {
|
|
108
|
-
if (h(a)) {
|
|
109
|
-
const n = f.get(e.actionId);
|
|
110
|
-
if (n && n(a, e.args) === "skip")
|
|
111
|
-
return t.removeQueueItem(e.id), await u().remove(e.id), "conflicted";
|
|
146
|
+
completedAt: o
|
|
112
147
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
}), await c().update(e.id, {
|
|
149
|
+
status: "succeeded",
|
|
150
|
+
completedAt: o
|
|
151
|
+
}), setTimeout(() => {
|
|
152
|
+
t.removeQueueItem(e.id), d({
|
|
153
|
+
type: "remove",
|
|
154
|
+
id: e.id
|
|
155
|
+
}), c().remove(e.id);
|
|
156
|
+
}, 3e3);
|
|
157
|
+
}
|
|
158
|
+
async function E(e, t, o) {
|
|
159
|
+
const a = Q.get(e.actionId);
|
|
160
|
+
let r;
|
|
161
|
+
if (a) switch (a.strategy) {
|
|
162
|
+
case "serverWins":
|
|
163
|
+
r = "skip";
|
|
164
|
+
break;
|
|
165
|
+
case "clientWins":
|
|
166
|
+
r = "retry";
|
|
167
|
+
break;
|
|
168
|
+
case "merge":
|
|
169
|
+
case "custom": {
|
|
170
|
+
const i = {
|
|
171
|
+
error: o,
|
|
172
|
+
args: e.args,
|
|
173
|
+
attempt: e.retryCount,
|
|
174
|
+
idempotencyKey: e.idempotencyKey
|
|
175
|
+
};
|
|
176
|
+
r = a.resolve?.(i) ?? "retry";
|
|
177
|
+
break;
|
|
135
178
|
}
|
|
136
179
|
}
|
|
180
|
+
if (r === "skip")
|
|
181
|
+
return t.removeQueueItem(e.id), d({
|
|
182
|
+
type: "remove",
|
|
183
|
+
id: e.id
|
|
184
|
+
}), await c().remove(e.id), "conflicted";
|
|
185
|
+
r && typeof r == "object" && (e.args = r.resolved, t.updateQueueItem(e.id, { args: r.resolved }), d({
|
|
186
|
+
type: "update",
|
|
187
|
+
id: e.id,
|
|
188
|
+
update: { args: r.resolved }
|
|
189
|
+
}), await c().update(e.id, { args: r.resolved }));
|
|
137
190
|
}
|
|
138
|
-
async function
|
|
191
|
+
async function P(e, t, o) {
|
|
192
|
+
const a = e.retryCount + 1;
|
|
193
|
+
if (a >= e.maxRetries) {
|
|
194
|
+
const i = {
|
|
195
|
+
status: "failed",
|
|
196
|
+
error: String(o),
|
|
197
|
+
retryCount: a
|
|
198
|
+
};
|
|
199
|
+
t.updateQueueItem(e.id, i), d({
|
|
200
|
+
type: "update",
|
|
201
|
+
id: e.id,
|
|
202
|
+
update: i
|
|
203
|
+
}), await c().update(e.id, i);
|
|
204
|
+
const n = {
|
|
205
|
+
idempotencyKey: e.idempotencyKey,
|
|
206
|
+
attempt: a
|
|
207
|
+
};
|
|
208
|
+
return k.get(e.actionId)?.(...e.args, n), "failed";
|
|
209
|
+
}
|
|
210
|
+
const r = {
|
|
211
|
+
status: "pending",
|
|
212
|
+
retryCount: a,
|
|
213
|
+
nextRetryAt: Date.now() + M(a)
|
|
214
|
+
};
|
|
215
|
+
return t.updateQueueItem(e.id, r), d({
|
|
216
|
+
type: "update",
|
|
217
|
+
id: e.id,
|
|
218
|
+
update: r
|
|
219
|
+
}), await c().update(e.id, r), "retrying";
|
|
220
|
+
}
|
|
221
|
+
async function D(e, t) {
|
|
222
|
+
const o = m.get(e.actionId);
|
|
223
|
+
if (!o) return "skipped";
|
|
224
|
+
const a = x.get(e.actionId)?.cancellable;
|
|
225
|
+
let r;
|
|
226
|
+
if (a) {
|
|
227
|
+
const n = new AbortController();
|
|
228
|
+
y.set(e.idempotencyKey, n), r = n.signal;
|
|
229
|
+
}
|
|
230
|
+
const i = {
|
|
231
|
+
idempotencyKey: e.idempotencyKey,
|
|
232
|
+
attempt: e.retryCount,
|
|
233
|
+
signal: r
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
return await h(o, e.args, i), await q(e, t), "succeeded";
|
|
237
|
+
} catch (n) {
|
|
238
|
+
if (C(n))
|
|
239
|
+
return t.removeQueueItem(e.id), d({
|
|
240
|
+
type: "remove",
|
|
241
|
+
id: e.id
|
|
242
|
+
}), await c().remove(e.id), "cancelled";
|
|
243
|
+
if (K(n)) {
|
|
244
|
+
const s = await E(e, t, n);
|
|
245
|
+
if (s) return s;
|
|
246
|
+
}
|
|
247
|
+
return P(e, t, n);
|
|
248
|
+
} finally {
|
|
249
|
+
a && y.delete(e.idempotencyKey);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function j(e, t, o) {
|
|
139
253
|
if (e.length === 0) return;
|
|
140
|
-
const a = e.filter((
|
|
141
|
-
if (
|
|
142
|
-
|
|
254
|
+
const a = e.filter((i) => m.has(i.actionId));
|
|
255
|
+
if (o.skipped += e.length - a.length, a.length > 0) {
|
|
256
|
+
const i = a.map((n) => ({
|
|
143
257
|
id: n.id,
|
|
144
258
|
update: { status: "replaying" }
|
|
145
|
-
}))
|
|
146
|
-
|
|
259
|
+
}));
|
|
260
|
+
t.batchUpdateQueueItems(i), d({
|
|
261
|
+
type: "batchUpdate",
|
|
262
|
+
updates: i
|
|
263
|
+
});
|
|
264
|
+
for (const n of a) c().update(n.id, { status: "replaying" });
|
|
147
265
|
}
|
|
148
|
-
const
|
|
149
|
-
for (const
|
|
150
|
-
const
|
|
151
|
-
|
|
266
|
+
const r = await Promise.allSettled(a.map((i) => D(i, t)));
|
|
267
|
+
for (const i of r) {
|
|
268
|
+
const n = i.status === "fulfilled" ? i.value : "failed";
|
|
269
|
+
n === "skipped" ? o.skipped++ : n === "conflicted" ? o.conflicted++ : n === "cancelled" ? o.cancelled++ : (o.attempted++, o[n]++);
|
|
152
270
|
}
|
|
153
271
|
}
|
|
154
|
-
async function
|
|
155
|
-
const t = await
|
|
156
|
-
|
|
157
|
-
succeeded: 0,
|
|
158
|
-
failed: 0,
|
|
159
|
-
retrying: 0,
|
|
160
|
-
skipped: 0,
|
|
161
|
-
conflicted: 0
|
|
162
|
-
};
|
|
163
|
-
for (const n of [
|
|
272
|
+
async function I(e) {
|
|
273
|
+
const t = await c().getPending(), o = Date.now(), a = t.filter((i) => i.retryCount < i.maxRetries && (!i.nextRetryAt || i.nextRetryAt <= o)), r = g();
|
|
274
|
+
for (const i of [
|
|
164
275
|
"high",
|
|
165
276
|
"normal",
|
|
166
277
|
"low"
|
|
167
|
-
]) await
|
|
168
|
-
return
|
|
278
|
+
]) await j(a.filter((n) => (n.priority ?? "normal") === i), e, r);
|
|
279
|
+
return r;
|
|
169
280
|
}
|
|
170
|
-
async function
|
|
171
|
-
await
|
|
281
|
+
async function Y() {
|
|
282
|
+
await c().clear(), f.getState().hydrateQueue([]);
|
|
172
283
|
}
|
|
173
284
|
export {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
285
|
+
F as action,
|
|
286
|
+
Y as clearQueue,
|
|
287
|
+
V as replayQueue
|
|
177
288
|
};
|
|
178
289
|
|
|
179
290
|
//# sourceMappingURL=action.js.map
|