@sweidos/eidos 1.0.30 → 1.0.31
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 +136 -2
- package/dist/action.js +58 -47
- package/dist/action.js.map +1 -1
- package/dist/async-storage-adapter.js +42 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.js +39 -9
- package/dist/eidos.cjs.js +2 -2
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/idb.js.map +1 -1
- package/dist/index.d.ts +41 -2
- package/dist/index.js +38 -33
- package/dist/index.js.map +1 -1
- package/dist/queue-storage.js +12 -0
- package/dist/queue-storage.js.map +1 -0
- package/dist/react/ProviderRN.d.ts +23 -0
- package/dist/react-native.d.ts +8 -0
- package/dist/react-native.js +59 -0
- package/dist/runtime-rn.d.ts +18 -0
- package/dist/store.js +20 -19
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -756,6 +756,63 @@ it('caches the resource after first fetch', async () => {
|
|
|
756
756
|
|
|
757
757
|
---
|
|
758
758
|
|
|
759
|
+
## OpenAPI Codegen
|
|
760
|
+
|
|
761
|
+
`eidos-gen` is a standalone CLI that reads an OpenAPI 3.x spec (JSON or YAML) and generates a fully-typed Eidos declarations file — `resource()` for every GET endpoint, `action()` for every POST / PUT / PATCH / DELETE.
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
npx eidos-gen openapi.json
|
|
765
|
+
# → writes eidos.generated.ts
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Example output** (from a Store API spec):
|
|
769
|
+
|
|
770
|
+
```ts
|
|
771
|
+
// Generated by eidos-gen — edit function bodies freely, re-run to refresh declarations.
|
|
772
|
+
import { resource, action } from '@sweidos/eidos'
|
|
773
|
+
|
|
774
|
+
export interface Product { id: string; name: string; price: number; inStock?: boolean }
|
|
775
|
+
export interface CreateProductRequest { name: string; price: number }
|
|
776
|
+
|
|
777
|
+
// Resources (GET)
|
|
778
|
+
export const listProducts = resource('/api/products', { offline: true })
|
|
779
|
+
export const getProduct = resource('/api/products/:id', { offline: true })
|
|
780
|
+
|
|
781
|
+
// Actions (POST / PUT / PATCH / DELETE)
|
|
782
|
+
export const createProduct = action(
|
|
783
|
+
async (payload: CreateProductRequest): Promise<Product> => {
|
|
784
|
+
const res = await fetch('/api/products', { method: 'POST', ... })
|
|
785
|
+
return res.json()
|
|
786
|
+
},
|
|
787
|
+
{ reliability: 'neverLose', name: 'createProduct' },
|
|
788
|
+
)
|
|
789
|
+
export const deleteProduct = action(
|
|
790
|
+
async (payload: { id: string }): Promise<void> => {
|
|
791
|
+
const res = await fetch(`/api/products/${payload.id}`, { method: 'DELETE' })
|
|
792
|
+
...
|
|
793
|
+
},
|
|
794
|
+
{ reliability: 'neverLose', name: 'deleteProduct' },
|
|
795
|
+
)
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
`eidos-gen` handles:
|
|
799
|
+
- **Path params** — `{id}` → `:id` on resources; `{ id: string } & RequestBody` on actions with template-literal URL interpolation
|
|
800
|
+
- **Type generation** — interfaces from `components/schemas` (objects, enums, unions, arrays)
|
|
801
|
+
- **`$ref` resolution** — schema references inline as type names
|
|
802
|
+
- **Response types** — `200`/`201`/`202` response body type used as the action return type
|
|
803
|
+
- **DELETE with no body** — omits `Content-Type` / `body`, handles 204 no-content
|
|
804
|
+
|
|
805
|
+
**Options:**
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
npx eidos-gen <spec> # JSON or YAML
|
|
809
|
+
npx eidos-gen <spec> --out src/lib/eidos.ts
|
|
810
|
+
npx eidos-gen <spec> --no-offline # set offline:false on resources
|
|
811
|
+
npx eidos-gen <spec> --eidos ./my-fork # custom import path
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
759
816
|
## SSR Adapters
|
|
760
817
|
|
|
761
818
|
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.
|
|
@@ -793,6 +850,83 @@ Use `initEidosSvelteKit()` inside `onMount` in your root `+layout.svelte`. The h
|
|
|
793
850
|
|
|
794
851
|
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
852
|
|
|
853
|
+
### React Native (`@sweidos/eidos/react-native`)
|
|
854
|
+
|
|
855
|
+
The React Native subpath swaps the browser-specific backends (IndexedDB, Service Worker, Cache API) for a pluggable `AsyncStorage`-backed queue while keeping the same `action()` / `resource()` API surface.
|
|
856
|
+
|
|
857
|
+
**Setup**
|
|
858
|
+
|
|
859
|
+
```bash
|
|
860
|
+
# peer deps
|
|
861
|
+
npm install @react-native-async-storage/async-storage @react-native-community/netinfo
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
// index.js — before rendering anything
|
|
866
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
867
|
+
import { initEidosRN } from '@sweidos/eidos/react-native'
|
|
868
|
+
|
|
869
|
+
await initEidosRN({ storage: AsyncStorage })
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
```tsx
|
|
873
|
+
// App.tsx
|
|
874
|
+
import { useNetInfo } from '@react-native-community/netinfo'
|
|
875
|
+
import { EidosProviderRN } from '@sweidos/eidos/react-native'
|
|
876
|
+
|
|
877
|
+
export function App() {
|
|
878
|
+
const { isConnected } = useNetInfo()
|
|
879
|
+
return (
|
|
880
|
+
<EidosProviderRN isConnected={isConnected ?? true}>
|
|
881
|
+
<Navigation />
|
|
882
|
+
</EidosProviderRN>
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
```ts
|
|
888
|
+
// Declare actions exactly as you would in a web app
|
|
889
|
+
import { action } from '@sweidos/eidos'
|
|
890
|
+
|
|
891
|
+
export const createOrder = action(
|
|
892
|
+
async (payload: CreateOrderInput) => {
|
|
893
|
+
const res = await fetch('/api/orders', { method: 'POST', body: JSON.stringify(payload) })
|
|
894
|
+
if (!res.ok) throw res
|
|
895
|
+
return res.json()
|
|
896
|
+
},
|
|
897
|
+
{ reliability: 'neverLose', name: 'createOrder' },
|
|
898
|
+
)
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Actions queued while offline are persisted to AsyncStorage and replayed automatically when the device reconnects.
|
|
902
|
+
|
|
903
|
+
**Custom storage**
|
|
904
|
+
|
|
905
|
+
`AsyncStorageLike` accepts any key-value store that implements `getItem` / `setItem` / `removeItem` — you can use MMKV or SQLite instead of AsyncStorage:
|
|
906
|
+
|
|
907
|
+
```ts
|
|
908
|
+
import { MMKV } from 'react-native-mmkv'
|
|
909
|
+
import { AsyncStorageQueueStorage, setQueueStorage } from '@sweidos/eidos/react-native'
|
|
910
|
+
|
|
911
|
+
const mmkv = new MMKV()
|
|
912
|
+
setQueueStorage(new AsyncStorageQueueStorage({
|
|
913
|
+
getItem: async (key) => mmkv.getString(key) ?? null,
|
|
914
|
+
setItem: async (key, value) => mmkv.set(key, value),
|
|
915
|
+
removeItem: async (key) => mmkv.delete(key),
|
|
916
|
+
}))
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**What works in RN vs web**
|
|
920
|
+
|
|
921
|
+
| Feature | Web | React Native |
|
|
922
|
+
|---------|-----|--------------|
|
|
923
|
+
| `action()` queue + replay | ✅ IndexedDB | ✅ AsyncStorage |
|
|
924
|
+
| Offline-aware (auto-queue) | ✅ | ✅ |
|
|
925
|
+
| `resource()` in-memory caching | ✅ | ✅ (in-memory only — no SW) |
|
|
926
|
+
| `resource()` offline persistence | ✅ Cache API + SW | ❌ (fetch from API when online) |
|
|
927
|
+
| `useEidos`, `useEidosQueue` hooks | ✅ | ✅ |
|
|
928
|
+
| Background Sync | ✅ | ❌ (App must be foregrounded) |
|
|
929
|
+
|
|
796
930
|
---
|
|
797
931
|
|
|
798
932
|
## Devtools
|
|
@@ -869,8 +1003,8 @@ The component is self-contained with inline styles — no CSS import needed, no
|
|
|
869
1003
|
- [x] Cache warming — `warmCache(handles[])` bulk-prefetches a list of resources on init (e.g. on login)
|
|
870
1004
|
|
|
871
1005
|
**Ecosystem**
|
|
872
|
-
- [
|
|
873
|
-
- [
|
|
1006
|
+
- [x] React Native support — `@sweidos/eidos/react-native`; AsyncStorage-backed queue, same `action()` API surface; `EidosProviderRN` syncs NetInfo connectivity into the replay loop
|
|
1007
|
+
- [x] OpenAPI codegen CLI — `npx eidos-gen ./openapi.json` generates typed `resource()` and `action()` declarations
|
|
874
1008
|
|
|
875
1009
|
---
|
|
876
1010
|
|
package/dist/action.js
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import { useEidosStore as d } from "./store.js";
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbClearQueue as
|
|
4
|
-
|
|
2
|
+
import { getSwRegistration as b } from "./sw-bridge.js";
|
|
3
|
+
import { idbClearQueue as g, idbRemoveFromQueue as Q, idbUpdateQueueItem as h, idbGetPendingItems as I, idbGetQueue as R, idbAddToQueue as k } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as v } from "./queue-storage.js";
|
|
5
|
+
const l = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), S = {
|
|
6
|
+
add: (e) => k(e),
|
|
7
|
+
getAll: () => R(),
|
|
8
|
+
getPending: () => I(),
|
|
9
|
+
update: (e, t) => h(e, t),
|
|
10
|
+
remove: (e) => Q(e),
|
|
11
|
+
clear: () => g()
|
|
12
|
+
};
|
|
13
|
+
function o() {
|
|
14
|
+
return v() ?? S;
|
|
15
|
+
}
|
|
5
16
|
function m() {
|
|
6
17
|
return crypto.randomUUID();
|
|
7
18
|
}
|
|
8
|
-
function
|
|
19
|
+
function U(e, t) {
|
|
9
20
|
const r = t.name || e.name || m();
|
|
10
|
-
l.set(r, e), t.onRollback &&
|
|
11
|
-
const
|
|
12
|
-
var i,
|
|
21
|
+
l.set(r, e), t.onRollback && f.set(r, t.onRollback), t.onConflict && y.set(r, t.onConflict);
|
|
22
|
+
const u = async (...a) => {
|
|
23
|
+
var i, s;
|
|
13
24
|
const { isOnline: n } = d.getState();
|
|
14
25
|
if ((i = t.onOptimistic) == null || i.call(t, ...a), t.reliability === "neverLose") {
|
|
15
26
|
if (!n)
|
|
16
|
-
return
|
|
27
|
+
return p(r, r, a, t);
|
|
17
28
|
try {
|
|
18
29
|
return await e(...a);
|
|
19
30
|
} catch {
|
|
20
|
-
return
|
|
31
|
+
return p(r, r, a, t);
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
try {
|
|
24
35
|
return await e(...a);
|
|
25
|
-
} catch (
|
|
26
|
-
throw (
|
|
36
|
+
} catch (w) {
|
|
37
|
+
throw (s = t.onRollback) == null || s.call(t, ...a), w;
|
|
27
38
|
}
|
|
28
39
|
};
|
|
29
|
-
return Object.defineProperty(
|
|
40
|
+
return Object.defineProperty(u, "id", { value: r, writable: !1 }), Object.defineProperty(u, "config", { value: t, writable: !1 }), u;
|
|
30
41
|
}
|
|
31
|
-
async function
|
|
42
|
+
async function p(e, t, r, u) {
|
|
32
43
|
const a = m(), n = {
|
|
33
44
|
id: a,
|
|
34
45
|
actionId: e,
|
|
@@ -36,13 +47,13 @@ async function f(e, t, r, s) {
|
|
|
36
47
|
args: r,
|
|
37
48
|
queuedAt: Date.now(),
|
|
38
49
|
retryCount: 0,
|
|
39
|
-
maxRetries:
|
|
50
|
+
maxRetries: u.maxRetries ?? 3,
|
|
40
51
|
status: "pending",
|
|
41
|
-
priority:
|
|
52
|
+
priority: u.priority ?? "normal"
|
|
42
53
|
};
|
|
43
|
-
await
|
|
54
|
+
await o().add(n), d.getState().addQueueItem(n);
|
|
44
55
|
try {
|
|
45
|
-
const i =
|
|
56
|
+
const i = b();
|
|
46
57
|
i && "sync" in i && await i.sync.register("eidos-queue-replay");
|
|
47
58
|
} catch {
|
|
48
59
|
}
|
|
@@ -52,7 +63,7 @@ async function f(e, t, r, s) {
|
|
|
52
63
|
message: `"${t}" queued — will execute when online`
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
|
-
function
|
|
66
|
+
function _(e) {
|
|
56
67
|
if (e instanceof Response) return e.status >= 400 && e.status < 500;
|
|
57
68
|
if (typeof e == "object" && e !== null) {
|
|
58
69
|
const t = e.status;
|
|
@@ -60,74 +71,74 @@ function g(e) {
|
|
|
60
71
|
}
|
|
61
72
|
return !1;
|
|
62
73
|
}
|
|
63
|
-
function
|
|
74
|
+
function x(e) {
|
|
64
75
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
65
76
|
}
|
|
66
77
|
let c = !1;
|
|
67
|
-
async function
|
|
78
|
+
async function j() {
|
|
68
79
|
const e = d.getState();
|
|
69
80
|
if (!e.isOnline || c)
|
|
70
81
|
return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
71
82
|
c = !0;
|
|
72
83
|
try {
|
|
73
|
-
return await
|
|
84
|
+
return await M(e);
|
|
74
85
|
} finally {
|
|
75
86
|
c = !1;
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
|
-
async function
|
|
79
|
-
var
|
|
89
|
+
async function A(e, t) {
|
|
90
|
+
var u;
|
|
80
91
|
const r = l.get(e.actionId);
|
|
81
92
|
if (!r) return "skipped";
|
|
82
93
|
try {
|
|
83
94
|
await r(...e.args);
|
|
84
95
|
const a = Date.now();
|
|
85
|
-
return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await
|
|
86
|
-
t.removeQueueItem(e.id),
|
|
96
|
+
return t.updateQueueItem(e.id, { status: "succeeded", completedAt: a }), await o().update(e.id, { status: "succeeded", completedAt: a }), setTimeout(() => {
|
|
97
|
+
t.removeQueueItem(e.id), o().remove(e.id);
|
|
87
98
|
}, 3e3), "succeeded";
|
|
88
99
|
} catch (a) {
|
|
89
|
-
if (
|
|
90
|
-
const i =
|
|
100
|
+
if (_(a)) {
|
|
101
|
+
const i = y.get(e.actionId);
|
|
91
102
|
if (i && i(a, e.args) === "skip")
|
|
92
|
-
return t.removeQueueItem(e.id), await
|
|
103
|
+
return t.removeQueueItem(e.id), await o().remove(e.id), "conflicted";
|
|
93
104
|
}
|
|
94
105
|
const n = e.retryCount + 1;
|
|
95
106
|
if (n >= e.maxRetries)
|
|
96
|
-
return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await
|
|
107
|
+
return t.updateQueueItem(e.id, { status: "failed", error: String(a), retryCount: n }), await o().update(e.id, { status: "failed", error: String(a), retryCount: n }), (u = f.get(e.actionId)) == null || u(...e.args), "failed";
|
|
97
108
|
{
|
|
98
|
-
const i = Date.now() +
|
|
99
|
-
return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await
|
|
109
|
+
const i = Date.now() + x(n);
|
|
110
|
+
return t.updateQueueItem(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), await o().update(e.id, { status: "pending", retryCount: n, nextRetryAt: i }), "retrying";
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
|
-
async function
|
|
114
|
+
async function C(e, t, r) {
|
|
104
115
|
if (e.length === 0) return;
|
|
105
|
-
const
|
|
106
|
-
if (r.skipped += e.length -
|
|
107
|
-
t.batchUpdateQueueItems(
|
|
108
|
-
for (const n of
|
|
109
|
-
|
|
116
|
+
const u = e.filter((n) => l.has(n.actionId));
|
|
117
|
+
if (r.skipped += e.length - u.length, u.length > 0) {
|
|
118
|
+
t.batchUpdateQueueItems(u.map((n) => ({ id: n.id, update: { status: "replaying" } })));
|
|
119
|
+
for (const n of u)
|
|
120
|
+
o().update(n.id, { status: "replaying" });
|
|
110
121
|
}
|
|
111
|
-
const a = await Promise.allSettled(
|
|
122
|
+
const a = await Promise.allSettled(u.map((n) => A(n, t)));
|
|
112
123
|
for (const n of a) {
|
|
113
124
|
const i = n.status === "fulfilled" ? n.value : "failed";
|
|
114
125
|
i === "skipped" ? r.skipped++ : i === "conflicted" ? r.conflicted++ : (r.attempted++, r[i]++);
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
|
-
async function
|
|
118
|
-
const t = await
|
|
128
|
+
async function M(e) {
|
|
129
|
+
const t = await o().getPending(), r = Date.now(), u = t.filter((n) => !n.nextRetryAt || n.nextRetryAt <= r), a = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
119
130
|
for (const n of ["high", "normal", "low"]) {
|
|
120
|
-
const i =
|
|
121
|
-
await
|
|
131
|
+
const i = u.filter((s) => (s.priority ?? "normal") === n);
|
|
132
|
+
await C(i, e, a);
|
|
122
133
|
}
|
|
123
134
|
return a;
|
|
124
135
|
}
|
|
125
|
-
async function
|
|
126
|
-
await
|
|
136
|
+
async function T() {
|
|
137
|
+
await o().clear(), d.getState().hydrateQueue([]);
|
|
127
138
|
}
|
|
128
139
|
export {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
U as action,
|
|
141
|
+
T as clearQueue,
|
|
142
|
+
j as replayQueue
|
|
132
143
|
};
|
|
133
144
|
//# 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// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await idbAddToQueue(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await idbUpdateQueueItem(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n idbRemoveFromQueue(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await idbRemoveFromQueue(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await idbUpdateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await idbUpdateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n idbUpdateQueueItem(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await idbGetPendingItems()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (IDB + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await idbClearQueue()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","id","item","idbAddToQueue","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","idbUpdateQueueItem","idbRemoveFromQueue","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","idbGetPendingItems","now","pending","tier","tierItems","clearQueue","idbClearQueue"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action.js","sources":["../src/action.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { getSwRegistration } from './sw-bridge'\nimport {\n idbAddToQueue,\n idbGetQueue,\n idbGetPendingItems,\n idbUpdateQueueItem,\n idbRemoveFromQueue,\n idbClearQueue,\n} from './idb'\nimport { _getQueueStorage } from './queue-storage'\nimport type { QueueStorage } from './queue-storage'\nimport type {\n ActionConfig,\n ActionHandle,\n ActionFn,\n ActionQueueItem,\n QueuedResult,\n ReplayResult,\n} from './types'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _actionRegistry = new Map<string, ActionFn<any[], any>>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _rollbackRegistry = new Map<string, (...args: any[]) => void>()\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst _conflictRegistry = new Map<string, (error: unknown, args: any[]) => 'retry' | 'skip'>()\n\n// IDB fallback — used when no custom storage is set (default browser behavior).\nconst _idbFallback: QueueStorage = {\n add: (item) => idbAddToQueue(item),\n getAll: () => idbGetQueue(),\n getPending: () => idbGetPendingItems(),\n update: (id, patch) => idbUpdateQueueItem(id, patch),\n remove: (id) => idbRemoveFromQueue(id),\n clear: () => idbClearQueue(),\n}\n\nfunction qs(): QueueStorage {\n return _getQueueStorage() ?? _idbFallback\n}\n\nfunction uid() {\n return crypto.randomUUID()\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function action<TArgs extends any[], TReturn>(\n fn: ActionFn<TArgs, TReturn>,\n config: ActionConfig,\n): ActionHandle<TArgs, TReturn> {\n // || not ?? — fn.name can be '' (anonymous arrow fn) which ?? treats as a\n // valid value, causing all anonymous actions to share actionId ''.\n const actionId = config.name || fn.name || uid()\n\n if (import.meta.env.DEV && config.reliability === 'neverLose' && !config.name && !fn.name) {\n console.warn(\n `[eidos] action() registered with neverLose but no stable name was found (fn.name=\"${fn.name}\"). Pass config.name so queued items survive a page reload and can be replayed.`,\n )\n }\n\n // Registering here means the function is available for replay after\n // the user refreshes the page (actions are defined at module scope).\n _actionRegistry.set(actionId, fn as ActionFn<unknown[], unknown>)\n\n if (config.onRollback) {\n _rollbackRegistry.set(actionId, config.onRollback)\n }\n\n if (config.onConflict) {\n _conflictRegistry.set(actionId, config.onConflict)\n }\n\n const wrapped = async (...args: TArgs): Promise<TReturn | QueuedResult> => {\n const { isOnline } = useEidosStore.getState()\n\n config.onOptimistic?.(...args)\n\n if (config.reliability === 'neverLose') {\n if (!isOnline) {\n return persistAndQueue(actionId, actionId, args, config)\n }\n // Online + neverLose: execute, queue on failure\n try {\n return await fn(...args)\n } catch {\n return persistAndQueue(actionId, actionId, args, config)\n }\n }\n\n // best-effort: execute directly, rollback on failure\n try {\n return await fn(...args)\n } catch (err) {\n config.onRollback?.(...args)\n throw err\n }\n }\n\n Object.defineProperty(wrapped, 'id', { value: actionId, writable: false })\n Object.defineProperty(wrapped, 'config', { value: config, writable: false })\n\n return wrapped as unknown as ActionHandle<TArgs, TReturn>\n}\n\nfunction isJsonSerializable(value: unknown): boolean {\n try {\n JSON.stringify(value)\n return true\n } catch {\n return false\n }\n}\n\nasync function persistAndQueue(\n actionId: string,\n actionName: string,\n args: unknown[],\n config: ActionConfig,\n): Promise<QueuedResult> {\n if (import.meta.env.DEV && !isJsonSerializable(args)) {\n console.warn(\n `[eidos] action \"${actionName}\" queued with non-JSON-serializable args. These args will be lost after a page reload. Use plain JSON values for neverLose actions.`,\n args,\n )\n }\n\n const id = uid()\n const item: ActionQueueItem = {\n id,\n actionId,\n actionName,\n args,\n queuedAt: Date.now(),\n retryCount: 0,\n maxRetries: config.maxRetries ?? 3,\n status: 'pending',\n priority: config.priority ?? 'normal',\n }\n\n await qs().add(item)\n useEidosStore.getState().addQueueItem(item)\n\n // Register Background Sync tag so the browser can wake up open clients\n // when connectivity returns, even if the user navigated away briefly.\n // Graceful no-op when Background Sync is unsupported.\n try {\n const reg = getSwRegistration()\n if (reg && 'sync' in reg) {\n await (reg as unknown as { sync: { register(tag: string): Promise<void> } }).sync.register('eidos-queue-replay')\n }\n } catch {\n // Background Sync not available — online-event replay remains the fallback\n }\n\n return {\n queued: true,\n id,\n message: `\"${actionName}\" queued — will execute when online`,\n }\n}\n\nfunction isClientError(err: unknown): boolean {\n if (err instanceof Response) return err.status >= 400 && err.status < 500\n if (typeof err === 'object' && err !== null) {\n const s = (err as Record<string, unknown>).status\n if (typeof s === 'number') return s >= 400 && s < 500\n }\n return false\n}\n\n// Base delay 2s, doubles per retry, capped at 5 minutes, ±20% jitter\nfunction backoffMs(retryCount: number): number {\n const base = Math.min(2000 * 2 ** retryCount, 300_000)\n return base * (0.8 + Math.random() * 0.4)\n}\n\nlet _replaying = false\n\nexport async function replayQueue(): Promise<ReplayResult> {\n const store = useEidosStore.getState()\n if (!store.isOnline || _replaying) {\n return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n }\n _replaying = true\n try {\n return await _doReplayQueue(store)\n } finally {\n _replaying = false\n }\n}\n\ntype ItemOutcome = 'succeeded' | 'failed' | 'retrying' | 'skipped' | 'conflicted'\n\nasync function _replayItem(\n item: ActionQueueItem,\n store: ReturnType<typeof useEidosStore.getState>,\n): Promise<ItemOutcome> {\n const fn = _actionRegistry.get(item.actionId)\n if (!fn) return 'skipped'\n\n try {\n await fn(...(item.args as unknown[]))\n const completedAt = Date.now()\n store.updateQueueItem(item.id, { status: 'succeeded', completedAt })\n await qs().update(item.id, { status: 'succeeded', completedAt })\n\n // Remove from queue after a short delay so UI can show the success state briefly\n setTimeout(() => {\n store.removeQueueItem(item.id)\n qs().remove(item.id)\n }, 3000)\n return 'succeeded'\n } catch (err) {\n // 4xx: give onConflict a chance to decide before normal retry/fail logic\n if (isClientError(err)) {\n const onConflict = _conflictRegistry.get(item.actionId)\n if (onConflict) {\n const resolution = onConflict(err, item.args as unknown[])\n if (resolution === 'skip') {\n store.removeQueueItem(item.id)\n await qs().remove(item.id)\n return 'conflicted'\n }\n // 'retry' falls through to normal retry/fail logic below\n }\n }\n\n const retryCount = item.retryCount + 1\n if (retryCount >= item.maxRetries) {\n store.updateQueueItem(item.id, { status: 'failed', error: String(err), retryCount })\n await qs().update(item.id, { status: 'failed', error: String(err), retryCount })\n _rollbackRegistry.get(item.actionId)?.(...(item.args as unknown[]))\n return 'failed'\n } else {\n const nextRetryAt = Date.now() + backoffMs(retryCount)\n store.updateQueueItem(item.id, { status: 'pending', retryCount, nextRetryAt })\n await qs().update(item.id, { status: 'pending', retryCount, nextRetryAt })\n return 'retrying'\n }\n }\n}\n\nasync function _replayTier(\n items: ActionQueueItem[],\n store: ReturnType<typeof useEidosStore.getState>,\n result: ReplayResult,\n): Promise<void> {\n if (items.length === 0) return\n\n // Batch 'replaying' status update — N items → 1 store notify.\n // IDB write is fire-and-forget: on reload items stay 'pending', safe to re-replay.\n const replayable = items.filter((item) => _actionRegistry.has(item.actionId))\n result.skipped += items.length - replayable.length\n\n if (replayable.length > 0) {\n store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: 'replaying' } })))\n for (const item of replayable) {\n qs().update(item.id, { status: 'replaying' })\n }\n }\n\n const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)))\n\n for (const o of outcomes) {\n const outcome = o.status === 'fulfilled' ? o.value : 'failed'\n if (outcome === 'skipped') { result.skipped++ }\n else if (outcome === 'conflicted') { result.conflicted++ }\n else { result.attempted++; result[outcome]++ }\n }\n}\n\nasync function _doReplayQueue(store: ReturnType<typeof useEidosStore.getState>): Promise<ReplayResult> {\n const candidates = await qs().getPending()\n const now = Date.now()\n const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now)\n\n const result: ReplayResult = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 }\n\n // Process tiers sequentially: high items complete before normal, normal before low.\n // Within each tier items run in parallel via Promise.allSettled.\n for (const tier of ['high', 'normal', 'low'] as const) {\n const tierItems = pending.filter((item) => (item.priority ?? 'normal') === tier)\n await _replayTier(tierItems, store, result)\n }\n\n return result\n}\n\n/** Remove all items from the action queue (storage + in-memory store). */\nexport async function clearQueue(): Promise<void> {\n await qs().clear()\n useEidosStore.getState().hydrateQueue([])\n}\n"],"names":["_actionRegistry","_rollbackRegistry","_conflictRegistry","_idbFallback","item","idbAddToQueue","idbGetQueue","idbGetPendingItems","id","patch","idbUpdateQueueItem","idbRemoveFromQueue","idbClearQueue","qs","_getQueueStorage","uid","action","fn","config","actionId","wrapped","args","isOnline","useEidosStore","_a","persistAndQueue","err","_b","actionName","reg","getSwRegistration","isClientError","s","backoffMs","retryCount","_replaying","replayQueue","store","_doReplayQueue","_replayItem","completedAt","onConflict","nextRetryAt","_replayTier","items","result","replayable","outcomes","o","outcome","candidates","now","pending","tier","tierItems","clearQueue"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const i = "@eidos:queue";
|
|
2
|
+
class l {
|
|
3
|
+
constructor(t) {
|
|
4
|
+
this.storage = t;
|
|
5
|
+
}
|
|
6
|
+
async readAll() {
|
|
7
|
+
try {
|
|
8
|
+
const t = await this.storage.getItem(i);
|
|
9
|
+
return t ? JSON.parse(t) : [];
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async writeAll(t) {
|
|
15
|
+
await this.storage.setItem(i, JSON.stringify(t));
|
|
16
|
+
}
|
|
17
|
+
async add(t) {
|
|
18
|
+
const e = await this.readAll();
|
|
19
|
+
e.push(t), await this.writeAll(e);
|
|
20
|
+
}
|
|
21
|
+
async getAll() {
|
|
22
|
+
return this.readAll();
|
|
23
|
+
}
|
|
24
|
+
async getPending() {
|
|
25
|
+
return (await this.readAll()).filter((e) => e.status === "pending" || e.status === "failed");
|
|
26
|
+
}
|
|
27
|
+
async update(t, e) {
|
|
28
|
+
const a = await this.readAll(), s = a.findIndex((r) => r.id === t);
|
|
29
|
+
s !== -1 && (a[s] = { ...a[s], ...e }), await this.writeAll(a);
|
|
30
|
+
}
|
|
31
|
+
async remove(t) {
|
|
32
|
+
const e = await this.readAll();
|
|
33
|
+
await this.writeAll(e.filter((a) => a.id !== t));
|
|
34
|
+
}
|
|
35
|
+
async clear() {
|
|
36
|
+
await this.storage.removeItem(i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
l as AsyncStorageQueueStorage
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=async-storage-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-storage-adapter.js","sources":["../src/async-storage-adapter.ts"],"sourcesContent":["import type { ActionQueueItem } from './types'\nimport type { QueueStorage } from './queue-storage'\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n removeItem(key: string): Promise<void>\n}\n\nconst QUEUE_KEY = '@eidos:queue'\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY)\n if (!raw) return []\n return JSON.parse(raw) as ActionQueueItem[]\n } catch {\n return []\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items))\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll()\n items.push(item)\n await this.writeAll(items)\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll()\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll()\n return items.filter((i) => i.status === 'pending' || i.status === 'failed')\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll()\n const idx = items.findIndex((i) => i.id === id)\n if (idx !== -1) items[idx] = { ...items[idx], ...patch }\n await this.writeAll(items)\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll()\n await this.writeAll(items.filter((i) => i.id !== id))\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY)\n }\n}\n"],"names":["QUEUE_KEY","AsyncStorageQueueStorage","storage","raw","items","item","i","id","patch","idx"],"mappings":"AAUA,MAAMA,IAAY;AAOX,MAAMC,EAAiD;AAAA,EAC5D,YAA6BC,GAA2B;AAA3B,SAAA,UAAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAMC,IAAM,MAAM,KAAK,QAAQ,QAAQH,CAAS;AAChD,aAAKG,IACE,KAAK,MAAMA,CAAG,IADJ,CAAA;AAAA,IAEnB,QAAQ;AACN,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,SAASC,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQJ,GAAW,KAAK,UAAUI,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAIC,GAAsC;AAC9C,UAAMD,IAAQ,MAAM,KAAK,QAAA;AACzB,IAAAA,EAAM,KAAKC,CAAI,GACf,MAAM,KAAK,SAASD,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAM,aAAyC;AAE7C,YADc,MAAM,KAAK,QAAA,GACZ,OAAO,CAACE,MAAMA,EAAE,WAAW,aAAaA,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAOC,GAAYC,GAAgD;AACvE,UAAMJ,IAAQ,MAAM,KAAK,QAAA,GACnBK,IAAML,EAAM,UAAU,CAACE,MAAMA,EAAE,OAAOC,CAAE;AAC9C,IAAIE,MAAQ,OAAIL,EAAMK,CAAG,IAAI,EAAE,GAAGL,EAAMK,CAAG,GAAG,GAAGD,EAAA,IACjD,MAAM,KAAK,SAASJ,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAOG,GAA2B;AACtC,UAAMH,IAAQ,MAAM,KAAK,QAAA;AACzB,UAAM,KAAK,SAASA,EAAM,OAAO,CAACE,MAAMA,EAAE,OAAOC,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAWP,CAAS;AAAA,EACzC;AACF;"}
|
package/dist/devtools.js
CHANGED
|
@@ -11,7 +11,8 @@ function _set(updater) {
|
|
|
11
11
|
_notify();
|
|
12
12
|
}
|
|
13
13
|
_state = {
|
|
14
|
-
|
|
14
|
+
// navigator.onLine is undefined in React Native — default to true unless explicitly false
|
|
15
|
+
isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
|
|
15
16
|
swStatus: "idle",
|
|
16
17
|
swError: void 0,
|
|
17
18
|
resources: {},
|
|
@@ -119,6 +120,24 @@ function openDB() {
|
|
|
119
120
|
req.onerror = () => reject(req.error);
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
async function idbAddToQueue(item) {
|
|
124
|
+
const db = await openDB();
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
127
|
+
tx.objectStore(QUEUE_STORE).add(item);
|
|
128
|
+
tx.oncomplete = () => resolve();
|
|
129
|
+
tx.onerror = () => reject(tx.error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function idbGetQueue() {
|
|
133
|
+
const db = await openDB();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
136
|
+
const req = tx.objectStore(QUEUE_STORE).getAll();
|
|
137
|
+
req.onsuccess = () => resolve(req.result);
|
|
138
|
+
req.onerror = () => reject(req.error);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
122
141
|
async function idbUpdateQueueItem(id, update) {
|
|
123
142
|
const db = await openDB();
|
|
124
143
|
return new Promise((resolve, reject) => {
|
|
@@ -189,6 +208,17 @@ async function idbClearQueue() {
|
|
|
189
208
|
const _actionRegistry = /* @__PURE__ */ new Map();
|
|
190
209
|
const _rollbackRegistry = /* @__PURE__ */ new Map();
|
|
191
210
|
const _conflictRegistry = /* @__PURE__ */ new Map();
|
|
211
|
+
const _idbFallback = {
|
|
212
|
+
add: (item) => idbAddToQueue(item),
|
|
213
|
+
getAll: () => idbGetQueue(),
|
|
214
|
+
getPending: () => idbGetPendingItems(),
|
|
215
|
+
update: (id, patch) => idbUpdateQueueItem(id, patch),
|
|
216
|
+
remove: (id) => idbRemoveFromQueue(id),
|
|
217
|
+
clear: () => idbClearQueue()
|
|
218
|
+
};
|
|
219
|
+
function qs() {
|
|
220
|
+
return _idbFallback;
|
|
221
|
+
}
|
|
192
222
|
function isClientError(err) {
|
|
193
223
|
if (err instanceof Response) return err.status >= 400 && err.status < 500;
|
|
194
224
|
if (typeof err === "object" && err !== null) {
|
|
@@ -222,10 +252,10 @@ async function _replayItem(item, store) {
|
|
|
222
252
|
await fn(...item.args);
|
|
223
253
|
const completedAt = Date.now();
|
|
224
254
|
store.updateQueueItem(item.id, { status: "succeeded", completedAt });
|
|
225
|
-
await
|
|
255
|
+
await qs().update(item.id, { status: "succeeded", completedAt });
|
|
226
256
|
setTimeout(() => {
|
|
227
257
|
store.removeQueueItem(item.id);
|
|
228
|
-
|
|
258
|
+
qs().remove(item.id);
|
|
229
259
|
}, 3e3);
|
|
230
260
|
return "succeeded";
|
|
231
261
|
} catch (err) {
|
|
@@ -235,7 +265,7 @@ async function _replayItem(item, store) {
|
|
|
235
265
|
const resolution = onConflict(err, item.args);
|
|
236
266
|
if (resolution === "skip") {
|
|
237
267
|
store.removeQueueItem(item.id);
|
|
238
|
-
await
|
|
268
|
+
await qs().remove(item.id);
|
|
239
269
|
return "conflicted";
|
|
240
270
|
}
|
|
241
271
|
}
|
|
@@ -243,13 +273,13 @@ async function _replayItem(item, store) {
|
|
|
243
273
|
const retryCount = item.retryCount + 1;
|
|
244
274
|
if (retryCount >= item.maxRetries) {
|
|
245
275
|
store.updateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
|
|
246
|
-
await
|
|
276
|
+
await qs().update(item.id, { status: "failed", error: String(err), retryCount });
|
|
247
277
|
(_a = _rollbackRegistry.get(item.actionId)) == null ? void 0 : _a(...item.args);
|
|
248
278
|
return "failed";
|
|
249
279
|
} else {
|
|
250
280
|
const nextRetryAt = Date.now() + backoffMs(retryCount);
|
|
251
281
|
store.updateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
|
|
252
|
-
await
|
|
282
|
+
await qs().update(item.id, { status: "pending", retryCount, nextRetryAt });
|
|
253
283
|
return "retrying";
|
|
254
284
|
}
|
|
255
285
|
}
|
|
@@ -261,7 +291,7 @@ async function _replayTier(items, store, result) {
|
|
|
261
291
|
if (replayable.length > 0) {
|
|
262
292
|
store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: "replaying" } })));
|
|
263
293
|
for (const item of replayable) {
|
|
264
|
-
|
|
294
|
+
qs().update(item.id, { status: "replaying" });
|
|
265
295
|
}
|
|
266
296
|
}
|
|
267
297
|
const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
|
|
@@ -278,7 +308,7 @@ async function _replayTier(items, store, result) {
|
|
|
278
308
|
}
|
|
279
309
|
}
|
|
280
310
|
async function _doReplayQueue(store) {
|
|
281
|
-
const candidates = await
|
|
311
|
+
const candidates = await qs().getPending();
|
|
282
312
|
const now = Date.now();
|
|
283
313
|
const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now);
|
|
284
314
|
const result = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
|
|
@@ -289,7 +319,7 @@ async function _doReplayQueue(store) {
|
|
|
289
319
|
return result;
|
|
290
320
|
}
|
|
291
321
|
async function clearQueue() {
|
|
292
|
-
await
|
|
322
|
+
await qs().clear();
|
|
293
323
|
useEidosStore.getState().hydrateQueue([]);
|
|
294
324
|
}
|
|
295
325
|
const C = {
|