@sweidos/eidos 1.0.34 → 1.2.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.
Files changed (53) hide show
  1. package/README.md +171 -89
  2. package/dist/action.js +197 -91
  3. package/dist/async-storage-adapter.js +15 -12
  4. package/dist/cli.js +102 -0
  5. package/dist/devtools.js +1009 -551
  6. package/dist/eidos-sw.js +280 -188
  7. package/dist/eidos.cjs +15 -0
  8. package/dist/idb.js +59 -56
  9. package/dist/index.d.ts +135 -18
  10. package/dist/index.js +46 -42
  11. package/dist/nextjs.js +1 -10
  12. package/dist/push.cjs +120 -0
  13. package/dist/push.d.ts +28 -0
  14. package/dist/push.js +113 -0
  15. package/dist/query.cjs +131 -0
  16. package/dist/query.js +121 -41
  17. package/dist/queue-storage.js +5 -4
  18. package/dist/react/Devtools.d.ts +1 -1
  19. package/dist/react/Provider.js +11 -7
  20. package/dist/react/hooks.js +48 -38
  21. package/dist/react-native.js +47 -53
  22. package/dist/replay.js +15 -0
  23. package/dist/resource.js +77 -79
  24. package/dist/runtime.js +39 -28
  25. package/dist/store-slices.js +43 -0
  26. package/dist/store.js +32 -49
  27. package/dist/stores.js +25 -22
  28. package/dist/sveltekit.js +22 -6
  29. package/dist/sw-bridge.js +64 -49
  30. package/dist/testing.cjs +165 -0
  31. package/dist/testing.js +140 -70
  32. package/dist/version.js +4 -3
  33. package/dist/vite.cjs +48 -0
  34. package/dist/vite.js +45 -29
  35. package/package.json +57 -28
  36. package/dist/action.js.map +0 -1
  37. package/dist/async-storage-adapter.js.map +0 -1
  38. package/dist/eidos.cjs.js +0 -14
  39. package/dist/eidos.cjs.js.map +0 -1
  40. package/dist/idb.js.map +0 -1
  41. package/dist/index.js.map +0 -1
  42. package/dist/query.cjs.js +0 -48
  43. package/dist/queue-storage.js.map +0 -1
  44. package/dist/react/Provider.js.map +0 -1
  45. package/dist/react/hooks.js.map +0 -1
  46. package/dist/resource.js.map +0 -1
  47. package/dist/runtime.js.map +0 -1
  48. package/dist/store.js.map +0 -1
  49. package/dist/stores.js.map +0 -1
  50. package/dist/sw-bridge.js.map +0 -1
  51. package/dist/testing.cjs.js +0 -86
  52. package/dist/version.js.map +0 -1
  53. package/dist/vite.cjs.js +0 -31
package/dist/devtools.js CHANGED
@@ -1,620 +1,1078 @@
1
1
  "use client";
2
+ import { useCallback, useState, useSyncExternalStore } from "react";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useSyncExternalStore, useState, useCallback } from "react";
4
- let _state;
5
- const _listeners = /* @__PURE__ */ new Set();
4
+ //#region src/store-slices.ts
5
+ function createResourceActions(set) {
6
+ return {
7
+ registerResource: (url, entry) => set((s) => ({ resources: {
8
+ ...s.resources,
9
+ [url]: entry
10
+ } })),
11
+ updateResource: (url, update) => set((s) => ({ resources: {
12
+ ...s.resources,
13
+ [url]: s.resources[url] ? {
14
+ ...s.resources[url],
15
+ ...update
16
+ } : s.resources[url]
17
+ } })),
18
+ unregisterResource: (url) => set((s) => ({ resources: Object.fromEntries(Object.entries(s.resources).filter(([k]) => k !== url)) }))
19
+ };
20
+ }
21
+ function createQueueActions(set) {
22
+ return {
23
+ addQueueItem: (item) => set((s) => ({ queue: [...s.queue, item] })),
24
+ updateQueueItem: (id, update) => set((s) => ({ queue: s.queue.map((item) => item.id === id ? {
25
+ ...item,
26
+ ...update
27
+ } : item) })),
28
+ batchUpdateQueueItems: (updates) => set((s) => {
29
+ const map = new Map(updates.map((u) => [u.id, u.update]));
30
+ return { queue: s.queue.map((item) => {
31
+ const u = map.get(item.id);
32
+ return u ? {
33
+ ...item,
34
+ ...u
35
+ } : item;
36
+ }) };
37
+ }),
38
+ removeQueueItem: (id) => set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),
39
+ hydrateQueue: (items) => set(() => ({ queue: items }))
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/store.ts
44
+ var _state;
45
+ var _listeners = /* @__PURE__ */ new Set();
6
46
  function _notify() {
7
- _listeners.forEach((fn) => fn());
47
+ _listeners.forEach((fn) => fn());
8
48
  }
9
49
  function _set(updater) {
10
- _state = { ..._state, ...updater(_state) };
11
- _notify();
50
+ _state = {
51
+ ..._state,
52
+ ...updater(_state)
53
+ };
54
+ _notify();
12
55
  }
13
56
  _state = {
14
- // navigator.onLine is undefined in React Native — default to true unless explicitly false
15
- isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
16
- swStatus: "idle",
17
- swError: void 0,
18
- resources: {},
19
- queue: [],
20
- setOnline: (isOnline) => _set(() => ({ isOnline })),
21
- setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),
22
- registerResource: (url, entry) => _set((s) => ({ resources: { ...s.resources, [url]: entry } })),
23
- updateResource: (url, update) => _set((s) => ({
24
- resources: {
25
- ...s.resources,
26
- [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url]
27
- }
28
- })),
29
- unregisterResource: (url) => _set((s) => ({
30
- resources: Object.fromEntries(
31
- Object.entries(s.resources).filter(([k]) => k !== url)
32
- )
33
- })),
34
- addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),
35
- updateQueueItem: (id, update) => _set((s) => ({
36
- queue: s.queue.map((item) => item.id === id ? { ...item, ...update } : item)
37
- })),
38
- batchUpdateQueueItems: (updates) => _set((s) => {
39
- const map = new Map(updates.map((u) => [u.id, u.update]));
40
- return {
41
- queue: s.queue.map((item) => {
42
- const u = map.get(item.id);
43
- return u ? { ...item, ...u } : item;
44
- })
45
- };
46
- }),
47
- removeQueueItem: (id) => _set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),
48
- hydrateQueue: (items) => _set(() => ({ queue: items }))
57
+ isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
58
+ swStatus: "idle",
59
+ swError: void 0,
60
+ resources: {},
61
+ queue: [],
62
+ setOnline: (isOnline) => _set(() => ({ isOnline })),
63
+ setSwStatus: (swStatus, swError) => _set(() => ({
64
+ swStatus,
65
+ swError
66
+ })),
67
+ ...createResourceActions(_set),
68
+ ...createQueueActions(_set)
49
69
  };
50
70
  function _getState() {
51
- return _state;
71
+ return _state;
52
72
  }
53
73
  function _subscribe(listener) {
54
- _listeners.add(listener);
55
- return () => {
56
- _listeners.delete(listener);
57
- };
58
- }
59
- const useEidosStore = {
60
- getState: _getState,
61
- subscribe: _subscribe,
62
- // Test/devtools helper — merges partial state, preserves action methods.
63
- setState: (partial) => {
64
- const update = typeof partial === "function" ? partial(_state) : partial;
65
- _state = { ..._state, ...update };
66
- _notify();
67
- }
74
+ _listeners.add(listener);
75
+ return () => {
76
+ _listeners.delete(listener);
77
+ };
78
+ }
79
+ var useEidosStore = {
80
+ getState: _getState,
81
+ subscribe: _subscribe,
82
+ setState: (partial) => {
83
+ const update = typeof partial === "function" ? partial(_state) : partial;
84
+ _state = {
85
+ ..._state,
86
+ ...update
87
+ };
88
+ _notify();
89
+ }
68
90
  };
91
+ //#endregion
92
+ //#region src/react/hooks.ts
69
93
  function useStore(selector) {
70
- const fn = selector ?? ((s) => s);
71
- return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));
94
+ const fn = selector ?? ((s) => s);
95
+ return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));
72
96
  }
97
+ /** All registered resources — only re-renders when the resources map changes, not on queue mutations. */
73
98
  function useEidosResources() {
74
- return useStore((s) => s.resources);
99
+ return useStore((s) => s.resources);
75
100
  }
101
+ /** The current action queue. */
76
102
  function useEidosQueue() {
77
- return useStore((s) => s.queue);
103
+ return useStore((s) => s.queue);
78
104
  }
105
+ /**
106
+ * Online + SW status — cheap subscription, safe to use in header components.
107
+ * Three separate primitive selectors so each only triggers a re-render when
108
+ * its own value changes (no object-reference churn from a combined selector).
109
+ */
79
110
  function useEidosStatus() {
80
- const isOnline = useStore((s) => s.isOnline);
81
- const swStatus = useStore((s) => s.swStatus);
82
- const swError = useStore((s) => s.swError);
83
- return { isOnline, swStatus, swError };
111
+ return {
112
+ isOnline: useStore((s) => s.isOnline),
113
+ swStatus: useStore((s) => s.swStatus),
114
+ swError: useStore((s) => s.swError)
115
+ };
84
116
  }
117
+ /**
118
+ * Queue counts — single subscription, single loop. Re-renders only when a
119
+ * count changes, not on every queue mutation. Use for badges and status bars
120
+ * instead of `useEidosQueue()` when you only need numbers, not full items.
121
+ */
85
122
  function useEidosQueueStats() {
86
- const encoded = useStore((s) => {
87
- let pending = 0, failed = 0, replaying = 0;
88
- for (const q of s.queue) {
89
- if (q.status === "pending") pending++;
90
- else if (q.status === "failed") failed++;
91
- else if (q.status === "replaying") replaying++;
92
- }
93
- return `${pending},${failed},${replaying},${s.queue.length}`;
94
- });
95
- const [p, f, r, t] = encoded.split(",");
96
- return { pending: +p, failed: +f, replaying: +r, total: +t };
123
+ const [p, f, r, t] = useStore((s) => {
124
+ let pending = 0, failed = 0, replaying = 0;
125
+ for (const q of s.queue) if (q.status === "pending") pending++;
126
+ else if (q.status === "failed") failed++;
127
+ else if (q.status === "replaying") replaying++;
128
+ return `${pending},${failed},${replaying},${s.queue.length}`;
129
+ }).split(",");
130
+ return {
131
+ pending: +p,
132
+ failed: +f,
133
+ replaying: +r,
134
+ total: +t
135
+ };
136
+ }
137
+ //#endregion
138
+ //#region src/sw-bridge.ts
139
+ var _registration = null;
140
+ var _pendingMessages = [];
141
+ function sendToWorker(message) {
142
+ const sw = _registration?.active;
143
+ if (sw) sw.postMessage(message);
144
+ else _pendingMessages.push(message);
97
145
  }
98
146
  function setOfflineSimulation(enabled) {
99
- useEidosStore.getState().setOnline(!enabled);
147
+ sendToWorker({
148
+ type: "EIDOS_SIMULATE_OFFLINE",
149
+ enabled
150
+ });
151
+ useEidosStore.getState().setOnline(!enabled);
100
152
  }
101
- const DB_NAME = "eidos";
102
- const DB_VERSION = 1;
103
- const QUEUE_STORE = "action-queue";
104
- let _db = null;
153
+ //#endregion
154
+ //#region src/idb.ts
155
+ var DB_NAME = "eidos";
156
+ var DB_VERSION = 1;
157
+ var QUEUE_STORE = "action-queue";
158
+ var _db = null;
105
159
  function openDB() {
106
- if (_db) return Promise.resolve(_db);
107
- return new Promise((resolve, reject) => {
108
- const req = indexedDB.open(DB_NAME, DB_VERSION);
109
- req.onupgradeneeded = (event) => {
110
- const db = event.target.result;
111
- if (!db.objectStoreNames.contains(QUEUE_STORE)) {
112
- const store = db.createObjectStore(QUEUE_STORE, { keyPath: "id" });
113
- store.createIndex("status", "status", { unique: false });
114
- store.createIndex("actionId", "actionId", { unique: false });
115
- }
116
- };
117
- req.onsuccess = () => {
118
- _db = req.result;
119
- resolve(req.result);
120
- };
121
- req.onerror = () => reject(req.error);
122
- });
160
+ if (_db) return Promise.resolve(_db);
161
+ return new Promise((resolve, reject) => {
162
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
163
+ req.onupgradeneeded = (event) => {
164
+ const db = event.target.result;
165
+ if (!db.objectStoreNames.contains(QUEUE_STORE)) {
166
+ const store = db.createObjectStore(QUEUE_STORE, { keyPath: "id" });
167
+ store.createIndex("status", "status", { unique: false });
168
+ store.createIndex("actionId", "actionId", { unique: false });
169
+ }
170
+ };
171
+ req.onsuccess = () => {
172
+ _db = req.result;
173
+ resolve(req.result);
174
+ };
175
+ req.onerror = () => reject(req.error);
176
+ });
123
177
  }
124
178
  async function idbAddToQueue(item) {
125
- const db = await openDB();
126
- return new Promise((resolve, reject) => {
127
- const tx = db.transaction(QUEUE_STORE, "readwrite");
128
- tx.objectStore(QUEUE_STORE).add(item);
129
- tx.oncomplete = () => resolve();
130
- tx.onerror = () => reject(tx.error);
131
- });
179
+ const db = await openDB();
180
+ return new Promise((resolve, reject) => {
181
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
182
+ tx.objectStore(QUEUE_STORE).add(item);
183
+ tx.oncomplete = () => resolve();
184
+ tx.onerror = () => reject(tx.error);
185
+ });
132
186
  }
133
187
  async function idbGetQueue() {
134
- const db = await openDB();
135
- return new Promise((resolve, reject) => {
136
- const tx = db.transaction(QUEUE_STORE, "readonly");
137
- const req = tx.objectStore(QUEUE_STORE).getAll();
138
- req.onsuccess = () => resolve(req.result);
139
- req.onerror = () => reject(req.error);
140
- });
188
+ const db = await openDB();
189
+ return new Promise((resolve, reject) => {
190
+ const req = db.transaction(QUEUE_STORE, "readonly").objectStore(QUEUE_STORE).getAll();
191
+ req.onsuccess = () => resolve(req.result);
192
+ req.onerror = () => reject(req.error);
193
+ });
141
194
  }
142
195
  async function idbUpdateQueueItem(id, update) {
143
- const db = await openDB();
144
- return new Promise((resolve, reject) => {
145
- const tx = db.transaction(QUEUE_STORE, "readwrite");
146
- const store = tx.objectStore(QUEUE_STORE);
147
- const get = store.get(id);
148
- get.onsuccess = () => {
149
- if (get.result) {
150
- store.put({ ...get.result, ...update });
151
- }
152
- };
153
- tx.oncomplete = () => resolve();
154
- tx.onerror = () => reject(tx.error);
155
- });
196
+ const db = await openDB();
197
+ return new Promise((resolve, reject) => {
198
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
199
+ const store = tx.objectStore(QUEUE_STORE);
200
+ const get = store.get(id);
201
+ get.onsuccess = () => {
202
+ if (get.result) store.put({
203
+ ...get.result,
204
+ ...update
205
+ });
206
+ };
207
+ tx.oncomplete = () => resolve();
208
+ tx.onerror = () => reject(tx.error);
209
+ });
156
210
  }
157
211
  async function idbRemoveFromQueue(id) {
158
- const db = await openDB();
159
- return new Promise((resolve, reject) => {
160
- const tx = db.transaction(QUEUE_STORE, "readwrite");
161
- tx.objectStore(QUEUE_STORE).delete(id);
162
- tx.oncomplete = () => resolve();
163
- tx.onerror = () => reject(tx.error);
164
- });
212
+ const db = await openDB();
213
+ return new Promise((resolve, reject) => {
214
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
215
+ tx.objectStore(QUEUE_STORE).delete(id);
216
+ tx.oncomplete = () => resolve();
217
+ tx.onerror = () => reject(tx.error);
218
+ });
165
219
  }
166
220
  async function idbGetPendingItems() {
167
- const db = await openDB();
168
- function cursorToArray(status) {
169
- return new Promise((resolve, reject) => {
170
- const tx = db.transaction(QUEUE_STORE, "readonly");
171
- const index = tx.objectStore(QUEUE_STORE).index("status");
172
- const items = [];
173
- const req = index.openCursor(IDBKeyRange.only(status));
174
- req.onsuccess = (e) => {
175
- const cursor = e.target.result;
176
- if (cursor) {
177
- items.push(cursor.value);
178
- cursor.continue();
179
- } else resolve(items);
180
- };
181
- req.onerror = () => reject(req.error);
182
- });
183
- }
184
- const [pending, failed] = await Promise.all([
185
- cursorToArray("pending"),
186
- cursorToArray("failed")
187
- ]);
188
- return [...pending, ...failed];
221
+ const db = await openDB();
222
+ function cursorToArray(status) {
223
+ return new Promise((resolve, reject) => {
224
+ const index = db.transaction(QUEUE_STORE, "readonly").objectStore(QUEUE_STORE).index("status");
225
+ const items = [];
226
+ const req = index.openCursor(IDBKeyRange.only(status));
227
+ req.onsuccess = (e) => {
228
+ const cursor = e.target.result;
229
+ if (cursor) {
230
+ items.push(cursor.value);
231
+ cursor.continue();
232
+ } else resolve(items);
233
+ };
234
+ req.onerror = () => reject(req.error);
235
+ });
236
+ }
237
+ const [pending, failed] = await Promise.all([cursorToArray("pending"), cursorToArray("failed")]);
238
+ return [...pending, ...failed];
189
239
  }
190
240
  async function idbClearQueue() {
191
- const db = await openDB();
192
- return new Promise((resolve, reject) => {
193
- const tx = db.transaction(QUEUE_STORE, "readwrite");
194
- tx.objectStore(QUEUE_STORE).clear();
195
- tx.oncomplete = () => resolve();
196
- tx.onerror = () => reject(tx.error);
197
- });
198
- }
199
- const _actionRegistry = /* @__PURE__ */ new Map();
200
- const _rollbackRegistry = /* @__PURE__ */ new Map();
201
- const _conflictRegistry = /* @__PURE__ */ new Map();
202
- const _idbFallback = {
203
- add: (item) => idbAddToQueue(item),
204
- getAll: () => idbGetQueue(),
205
- getPending: () => idbGetPendingItems(),
206
- update: (id, patch) => idbUpdateQueueItem(id, patch),
207
- remove: (id) => idbRemoveFromQueue(id),
208
- clear: () => idbClearQueue()
241
+ const db = await openDB();
242
+ return new Promise((resolve, reject) => {
243
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
244
+ tx.objectStore(QUEUE_STORE).clear();
245
+ tx.oncomplete = () => resolve();
246
+ tx.onerror = () => reject(tx.error);
247
+ });
248
+ }
249
+ /** IndexedDB-backed QueueStorage implementation (default for browser environments). */
250
+ var idbQueueStorage = {
251
+ add: idbAddToQueue,
252
+ getAll: idbGetQueue,
253
+ getPending: idbGetPendingItems,
254
+ update: idbUpdateQueueItem,
255
+ remove: idbRemoveFromQueue,
256
+ clear: idbClearQueue
209
257
  };
258
+ //#endregion
259
+ //#region src/queue-storage.ts
260
+ var _storage = null;
261
+ function _getQueueStorage() {
262
+ return _storage;
263
+ }
264
+ //#endregion
265
+ //#region src/action.ts
266
+ var _actionRegistry = /* @__PURE__ */ new Map();
267
+ var _rollbackRegistry = /* @__PURE__ */ new Map();
268
+ var _conflictRegistry = /* @__PURE__ */ new Map();
269
+ var _conflictConfigRegistry = /* @__PURE__ */ new Map();
270
+ var _configRegistry = /* @__PURE__ */ new Map();
271
+ var _inflightControllers = /* @__PURE__ */ new Map();
210
272
  function qs() {
211
- return _idbFallback;
273
+ return _getQueueStorage() ?? idbQueueStorage;
274
+ }
275
+ function callWithContext(fn, args, ctx) {
276
+ return fn(...args, ctx);
277
+ }
278
+ function isAbortError(err) {
279
+ return err instanceof DOMException && err.name === "AbortError";
212
280
  }
213
281
  function isClientError(err) {
214
- if (err instanceof Response) return err.status >= 400 && err.status < 500;
215
- if (typeof err === "object" && err !== null) {
216
- const s = err.status;
217
- if (typeof s === "number") return s >= 400 && s < 500;
218
- }
219
- return false;
282
+ if (err instanceof Response) return err.status >= 400 && err.status < 500;
283
+ if (typeof err === "object" && err !== null) {
284
+ const s = err.status;
285
+ if (typeof s === "number") return s >= 400 && s < 500;
286
+ }
287
+ return false;
220
288
  }
221
289
  function backoffMs(retryCount) {
222
- const base = Math.min(2e3 * 2 ** retryCount, 3e5);
223
- return base * (0.8 + Math.random() * 0.4);
290
+ return Math.min(2e3 * 2 ** retryCount, 3e5) * (.8 + Math.random() * .4);
291
+ }
292
+ function emptyReplayResult() {
293
+ return {
294
+ attempted: 0,
295
+ succeeded: 0,
296
+ failed: 0,
297
+ retrying: 0,
298
+ skipped: 0,
299
+ conflicted: 0,
300
+ cancelled: 0
301
+ };
224
302
  }
225
- let _replaying = false;
303
+ var _replaying = false;
304
+ var REPLAY_LOCK_NAME = "eidos-queue-replay";
226
305
  async function replayQueue() {
227
- const store = useEidosStore.getState();
228
- if (!store.isOnline || _replaying) {
229
- return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
230
- }
231
- _replaying = true;
232
- try {
233
- return await _doReplayQueue(store);
234
- } finally {
235
- _replaying = false;
236
- }
306
+ const store = useEidosStore.getState();
307
+ if (!store.isOnline) return emptyReplayResult();
308
+ if (typeof navigator !== "undefined" && navigator.locks) return navigator.locks.request(REPLAY_LOCK_NAME, { ifAvailable: true }, async (lock) => {
309
+ if (!lock) return emptyReplayResult();
310
+ return _doReplayQueue(store);
311
+ });
312
+ if (_replaying) return emptyReplayResult();
313
+ _replaying = true;
314
+ try {
315
+ return await _doReplayQueue(store);
316
+ } finally {
317
+ _replaying = false;
318
+ }
237
319
  }
238
320
  async function _replayItem(item, store) {
239
- var _a;
240
- const fn = _actionRegistry.get(item.actionId);
241
- if (!fn) return "skipped";
242
- try {
243
- await fn(...item.args);
244
- const completedAt = Date.now();
245
- store.updateQueueItem(item.id, { status: "succeeded", completedAt });
246
- await qs().update(item.id, { status: "succeeded", completedAt });
247
- setTimeout(() => {
248
- store.removeQueueItem(item.id);
249
- qs().remove(item.id);
250
- }, 3e3);
251
- return "succeeded";
252
- } catch (err) {
253
- if (isClientError(err)) {
254
- const onConflict = _conflictRegistry.get(item.actionId);
255
- if (onConflict) {
256
- const resolution = onConflict(err, item.args);
257
- if (resolution === "skip") {
258
- store.removeQueueItem(item.id);
259
- await qs().remove(item.id);
260
- return "conflicted";
261
- }
262
- }
263
- }
264
- const retryCount = item.retryCount + 1;
265
- if (retryCount >= item.maxRetries) {
266
- store.updateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
267
- await qs().update(item.id, { status: "failed", error: String(err), retryCount });
268
- (_a = _rollbackRegistry.get(item.actionId)) == null ? void 0 : _a(...item.args);
269
- return "failed";
270
- } else {
271
- const nextRetryAt = Date.now() + backoffMs(retryCount);
272
- store.updateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
273
- await qs().update(item.id, { status: "pending", retryCount, nextRetryAt });
274
- return "retrying";
275
- }
276
- }
321
+ const fn = _actionRegistry.get(item.actionId);
322
+ if (!fn) return "skipped";
323
+ const cancellable = _configRegistry.get(item.actionId)?.cancellable;
324
+ let signal;
325
+ if (cancellable) {
326
+ const controller = new AbortController();
327
+ _inflightControllers.set(item.idempotencyKey, controller);
328
+ signal = controller.signal;
329
+ }
330
+ const ctx = {
331
+ idempotencyKey: item.idempotencyKey,
332
+ attempt: item.retryCount,
333
+ signal
334
+ };
335
+ try {
336
+ await callWithContext(fn, item.args, ctx);
337
+ const completedAt = Date.now();
338
+ store.updateQueueItem(item.id, {
339
+ status: "succeeded",
340
+ completedAt
341
+ });
342
+ await qs().update(item.id, {
343
+ status: "succeeded",
344
+ completedAt
345
+ });
346
+ setTimeout(() => {
347
+ store.removeQueueItem(item.id);
348
+ qs().remove(item.id);
349
+ }, 3e3);
350
+ return "succeeded";
351
+ } catch (err) {
352
+ if (isAbortError(err)) {
353
+ store.removeQueueItem(item.id);
354
+ await qs().remove(item.id);
355
+ return "cancelled";
356
+ }
357
+ if (isClientError(err)) {
358
+ const conflictConfig = _conflictConfigRegistry.get(item.actionId);
359
+ let resolution;
360
+ if (conflictConfig) switch (conflictConfig.strategy) {
361
+ case "serverWins":
362
+ resolution = "skip";
363
+ break;
364
+ case "clientWins":
365
+ case "lastWriteWins":
366
+ resolution = "retry";
367
+ break;
368
+ case "merge":
369
+ case "custom": {
370
+ const ctx = {
371
+ error: err,
372
+ args: item.args,
373
+ attempt: item.retryCount,
374
+ idempotencyKey: item.idempotencyKey
375
+ };
376
+ resolution = conflictConfig.resolve?.(ctx) ?? "retry";
377
+ break;
378
+ }
379
+ }
380
+ else {
381
+ const onConflict = _conflictRegistry.get(item.actionId);
382
+ if (onConflict) resolution = onConflict(err, item.args);
383
+ }
384
+ if (resolution === "skip") {
385
+ store.removeQueueItem(item.id);
386
+ await qs().remove(item.id);
387
+ return "conflicted";
388
+ }
389
+ if (resolution && typeof resolution === "object") {
390
+ item.args = resolution.resolved;
391
+ store.updateQueueItem(item.id, { args: resolution.resolved });
392
+ await qs().update(item.id, { args: resolution.resolved });
393
+ }
394
+ }
395
+ const retryCount = item.retryCount + 1;
396
+ if (retryCount >= item.maxRetries) {
397
+ store.updateQueueItem(item.id, {
398
+ status: "failed",
399
+ error: String(err),
400
+ retryCount
401
+ });
402
+ await qs().update(item.id, {
403
+ status: "failed",
404
+ error: String(err),
405
+ retryCount
406
+ });
407
+ _rollbackRegistry.get(item.actionId)?.(...item.args);
408
+ return "failed";
409
+ } else {
410
+ const nextRetryAt = Date.now() + backoffMs(retryCount);
411
+ store.updateQueueItem(item.id, {
412
+ status: "pending",
413
+ retryCount,
414
+ nextRetryAt
415
+ });
416
+ await qs().update(item.id, {
417
+ status: "pending",
418
+ retryCount,
419
+ nextRetryAt
420
+ });
421
+ return "retrying";
422
+ }
423
+ } finally {
424
+ if (cancellable) _inflightControllers.delete(item.idempotencyKey);
425
+ }
277
426
  }
278
427
  async function _replayTier(items, store, result) {
279
- if (items.length === 0) return;
280
- const replayable = items.filter((item) => _actionRegistry.has(item.actionId));
281
- result.skipped += items.length - replayable.length;
282
- if (replayable.length > 0) {
283
- store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: "replaying" } })));
284
- for (const item of replayable) {
285
- qs().update(item.id, { status: "replaying" });
286
- }
287
- }
288
- const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
289
- for (const o of outcomes) {
290
- const outcome = o.status === "fulfilled" ? o.value : "failed";
291
- if (outcome === "skipped") {
292
- result.skipped++;
293
- } else if (outcome === "conflicted") {
294
- result.conflicted++;
295
- } else {
296
- result.attempted++;
297
- result[outcome]++;
298
- }
299
- }
428
+ if (items.length === 0) return;
429
+ const replayable = items.filter((item) => _actionRegistry.has(item.actionId));
430
+ result.skipped += items.length - replayable.length;
431
+ if (replayable.length > 0) {
432
+ store.batchUpdateQueueItems(replayable.map((item) => ({
433
+ id: item.id,
434
+ update: { status: "replaying" }
435
+ })));
436
+ for (const item of replayable) qs().update(item.id, { status: "replaying" });
437
+ }
438
+ const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
439
+ for (const o of outcomes) {
440
+ const outcome = o.status === "fulfilled" ? o.value : "failed";
441
+ if (outcome === "skipped") result.skipped++;
442
+ else if (outcome === "conflicted") result.conflicted++;
443
+ else if (outcome === "cancelled") result.cancelled++;
444
+ else {
445
+ result.attempted++;
446
+ result[outcome]++;
447
+ }
448
+ }
300
449
  }
301
450
  async function _doReplayQueue(store) {
302
- const candidates = await qs().getPending();
303
- const now = Date.now();
304
- const pending = candidates.filter(
305
- (item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now)
306
- );
307
- const result = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
308
- for (const tier of ["high", "normal", "low"]) {
309
- const tierItems = pending.filter((item) => (item.priority ?? "normal") === tier);
310
- await _replayTier(tierItems, store, result);
311
- }
312
- return result;
451
+ const candidates = await qs().getPending();
452
+ const now = Date.now();
453
+ const pending = candidates.filter((item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now));
454
+ const result = emptyReplayResult();
455
+ for (const tier of [
456
+ "high",
457
+ "normal",
458
+ "low"
459
+ ]) await _replayTier(pending.filter((item) => (item.priority ?? "normal") === tier), store, result);
460
+ return result;
313
461
  }
462
+ /** Remove all items from the action queue (storage + in-memory store). */
314
463
  async function clearQueue() {
315
- await qs().clear();
316
- useEidosStore.getState().hydrateQueue([]);
317
- }
318
- const C = {
319
- bg: "#0f1117",
320
- surface: "#1a1d27",
321
- border: "#2a2d3a",
322
- text: "#e2e8f0",
323
- muted: "#64748b",
324
- green: "#22c55e",
325
- red: "#ef4444",
326
- yellow: "#f59e0b",
327
- blue: "#3b82f6",
328
- purple: "#a855f7",
329
- cyan: "#06b6d4"
464
+ await qs().clear();
465
+ useEidosStore.getState().hydrateQueue([]);
466
+ }
467
+ //#endregion
468
+ //#region src/react/Devtools.tsx
469
+ var C = {
470
+ bg: "#0f1117",
471
+ surface: "#1a1d27",
472
+ border: "#2a2d3a",
473
+ text: "#e2e8f0",
474
+ muted: "#64748b",
475
+ green: "#22c55e",
476
+ red: "#ef4444",
477
+ yellow: "#f59e0b",
478
+ blue: "#3b82f6",
479
+ purple: "#a855f7",
480
+ cyan: "#06b6d4"
330
481
  };
331
482
  function pill(color) {
332
- return {
333
- display: "inline-flex",
334
- alignItems: "center",
335
- padding: "1px 7px",
336
- borderRadius: 9999,
337
- fontSize: 10,
338
- fontWeight: 600,
339
- background: `${color}22`,
340
- color,
341
- border: `1px solid ${color}44`,
342
- fontFamily: "inherit"
343
- };
483
+ return {
484
+ display: "inline-flex",
485
+ alignItems: "center",
486
+ padding: "1px 7px",
487
+ borderRadius: 9999,
488
+ fontSize: 10,
489
+ fontWeight: 600,
490
+ background: `${color}22`,
491
+ color,
492
+ border: `1px solid ${color}44`,
493
+ fontFamily: "inherit"
494
+ };
344
495
  }
345
496
  function btn(variant = "ghost") {
346
- const base = {
347
- cursor: "pointer",
348
- border: "none",
349
- borderRadius: 6,
350
- padding: "3px 10px",
351
- fontSize: 11,
352
- fontWeight: 500,
353
- fontFamily: "inherit",
354
- transition: "opacity 0.15s"
355
- };
356
- if (variant === "danger") return { ...base, background: `${C.red}22`, color: C.red };
357
- if (variant === "primary") return { ...base, background: `${C.blue}22`, color: C.blue };
358
- return { ...base, background: C.surface, color: C.muted };
497
+ const base = {
498
+ cursor: "pointer",
499
+ border: "none",
500
+ borderRadius: 6,
501
+ padding: "3px 10px",
502
+ fontSize: 11,
503
+ fontWeight: 500,
504
+ fontFamily: "inherit",
505
+ display: "inline-flex",
506
+ alignItems: "center",
507
+ gap: 5,
508
+ transition: "background-color 0.15s, color 0.15s"
509
+ };
510
+ if (variant === "danger") return {
511
+ ...base,
512
+ background: `${C.red}22`,
513
+ color: C.red
514
+ };
515
+ if (variant === "primary") return {
516
+ ...base,
517
+ background: `${C.blue}22`,
518
+ color: C.blue
519
+ };
520
+ return {
521
+ ...base,
522
+ background: C.surface,
523
+ color: C.muted
524
+ };
525
+ }
526
+ var focusRing = {
527
+ outline: `2px solid ${C.cyan}`,
528
+ outlineOffset: 1
529
+ };
530
+ function withFocusRing(handlers = {}) {
531
+ return {
532
+ onFocus: (e) => {
533
+ Object.assign(e.currentTarget.style, focusRing);
534
+ handlers.onFocus?.();
535
+ },
536
+ onBlur: (e) => {
537
+ e.currentTarget.style.outline = "none";
538
+ handlers.onBlur?.();
539
+ }
540
+ };
359
541
  }
360
542
  function swStatusColor(s) {
361
- if (s === "active") return C.green;
362
- if (s === "registering") return C.yellow;
363
- if (s === "error" || s === "unsupported") return C.red;
364
- return C.muted;
543
+ if (s === "active") return C.green;
544
+ if (s === "registering") return C.yellow;
545
+ if (s === "error" || s === "unsupported") return C.red;
546
+ return C.muted;
365
547
  }
366
548
  function queueStatusColor(s) {
367
- if (s === "succeeded") return C.green;
368
- if (s === "failed") return C.red;
369
- if (s === "replaying") return C.yellow;
370
- return C.blue;
549
+ if (s === "succeeded") return C.green;
550
+ if (s === "failed") return C.red;
551
+ if (s === "replaying") return C.yellow;
552
+ return C.blue;
371
553
  }
372
554
  function resourceStatusColor(s) {
373
- if (s === "fresh") return C.green;
374
- if (s === "stale" || s === "offline") return C.yellow;
375
- if (s === "error") return C.red;
376
- if (s === "fetching") return C.cyan;
377
- return C.muted;
555
+ if (s === "fresh") return C.green;
556
+ if (s === "stale" || s === "offline") return C.yellow;
557
+ if (s === "error") return C.red;
558
+ if (s === "fetching") return C.cyan;
559
+ return C.muted;
560
+ }
561
+ function Icon({ path, size = 12, strokeWidth = 2 }) {
562
+ return /* @__PURE__ */ jsx("svg", {
563
+ width: size,
564
+ height: size,
565
+ viewBox: "0 0 24 24",
566
+ fill: "none",
567
+ stroke: "currentColor",
568
+ strokeWidth,
569
+ strokeLinecap: "round",
570
+ strokeLinejoin: "round",
571
+ "aria-hidden": "true",
572
+ style: { flexShrink: 0 },
573
+ children: /* @__PURE__ */ jsx("path", { d: path })
574
+ });
378
575
  }
576
+ var ICONS = {
577
+ bolt: "M13 2 3 14h7l-1 8 10-12h-7l1-8z",
578
+ satellite: "M5 13a8.5 8.5 0 0 0 8 8M11 3a12 12 0 0 1 10 10M5 13l-3 3 6 6 3-3M14 6l4 4M9.5 8.5l6 6",
579
+ satelliteOff: "M2 2l20 20M5 13a8.5 8.5 0 0 0 8 8M14 6l4 4M9.5 8.5l6 6M5 13l-3 3 6 6 3-3",
580
+ play: "M6 4l13 8-13 8V4z",
581
+ trash: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0-1 14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1L4 6h16z",
582
+ arrowUp: "M12 19V5M5 12l7-7 7 7",
583
+ arrowDown: "M12 5v14M19 12l-7 7-7-7",
584
+ clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2"
585
+ };
379
586
  function positionStyle(p) {
380
- const base = { position: "fixed", zIndex: 99999 };
381
- if (p === "bottom-left") return { ...base, bottom: 16, left: 16 };
382
- if (p === "top-right") return { ...base, top: 16, right: 16 };
383
- if (p === "top-left") return { ...base, top: 16, left: 16 };
384
- return { ...base, bottom: 16, right: 16 };
587
+ const base = {
588
+ position: "fixed",
589
+ zIndex: 99999
590
+ };
591
+ if (p === "bottom-left") return {
592
+ ...base,
593
+ bottom: 16,
594
+ left: 16
595
+ };
596
+ if (p === "top-right") return {
597
+ ...base,
598
+ top: 16,
599
+ right: 16
600
+ };
601
+ if (p === "top-left") return {
602
+ ...base,
603
+ top: 16,
604
+ left: 16
605
+ };
606
+ return {
607
+ ...base,
608
+ bottom: 16,
609
+ right: 16
610
+ };
385
611
  }
386
612
  function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
387
- const [open, setOpen] = useState(defaultOpen);
388
- const [tab, setTab] = useState("queue");
389
- const [simOffline, setSimOffline] = useState(false);
390
- const { isOnline, swStatus, swError } = useEidosStatus();
391
- const queue = useEidosQueue();
392
- const { pending, failed, replaying } = useEidosQueueStats();
393
- const resources = useEidosResources();
394
- const resourceList = Object.values(resources);
395
- const badgeCount = pending + failed + replaying;
396
- const toggleOffline = useCallback(() => {
397
- const next = !simOffline;
398
- setSimOffline(next);
399
- setOfflineSimulation(next);
400
- }, [simOffline]);
401
- const handleReplay = useCallback(() => {
402
- void replayQueue();
403
- }, []);
404
- const handleClear = useCallback(() => {
405
- void clearQueue();
406
- }, []);
407
- const toggleBtn = /* @__PURE__ */ jsxs(
408
- "button",
409
- {
410
- onClick: () => setOpen((v) => !v),
411
- title: "Eidos Devtools",
412
- style: {
413
- display: "flex",
414
- alignItems: "center",
415
- gap: 6,
416
- padding: "5px 10px",
417
- background: C.bg,
418
- border: `1px solid ${C.border}`,
419
- borderRadius: 9999,
420
- cursor: "pointer",
421
- color: C.text,
422
- fontSize: 11,
423
- fontWeight: 600,
424
- fontFamily: 'ui-monospace, "Cascadia Code", "Fira Mono", monospace',
425
- boxShadow: "0 2px 12px rgba(0,0,0,0.5)",
426
- userSelect: "none"
427
- },
428
- children: [
429
- /* @__PURE__ */ jsx("span", { style: { color: isOnline ? C.green : C.red, fontSize: 8 }, children: "●" }),
430
- /* @__PURE__ */ jsx("span", { style: { color: C.cyan }, children: "⚡" }),
431
- /* @__PURE__ */ jsx("span", { children: "eidos" }),
432
- badgeCount > 0 && /* @__PURE__ */ jsx("span", { style: {
433
- background: failed > 0 ? C.red : C.yellow,
434
- color: "#fff",
435
- borderRadius: 9999,
436
- minWidth: 16,
437
- height: 16,
438
- display: "inline-flex",
439
- alignItems: "center",
440
- justifyContent: "center",
441
- fontSize: 9,
442
- fontWeight: 700,
443
- padding: "0 4px"
444
- }, children: badgeCount })
445
- ]
446
- }
447
- );
448
- if (!open) {
449
- return /* @__PURE__ */ jsx("div", { style: positionStyle(position), children: toggleBtn });
450
- }
451
- return /* @__PURE__ */ jsxs("div", { style: { ...positionStyle(position), display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6 }, children: [
452
- /* @__PURE__ */ jsxs("div", { style: {
453
- width: 340,
454
- maxHeight: 480,
455
- display: "flex",
456
- flexDirection: "column",
457
- background: C.bg,
458
- border: `1px solid ${C.border}`,
459
- borderRadius: 10,
460
- overflow: "hidden",
461
- boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
462
- fontFamily: 'ui-monospace, "Cascadia Code", "Fira Mono", monospace',
463
- fontSize: 11,
464
- color: C.text
465
- }, children: [
466
- /* @__PURE__ */ jsxs("div", { style: {
467
- display: "flex",
468
- alignItems: "center",
469
- gap: 8,
470
- padding: "8px 12px",
471
- background: C.surface,
472
- borderBottom: `1px solid ${C.border}`,
473
- flexShrink: 0
474
- }, children: [
475
- /* @__PURE__ */ jsx("span", { style: { color: C.cyan, fontSize: 13 }, children: "⚡" }),
476
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: 12, color: C.text, flex: 1 }, children: "Eidos Devtools" }),
477
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
478
- /* @__PURE__ */ jsx("span", { style: pill(isOnline ? C.green : C.red), children: isOnline ? "online" : "offline" }),
479
- /* @__PURE__ */ jsx(
480
- "button",
481
- {
482
- onClick: toggleOffline,
483
- title: simOffline ? "Disable offline simulation" : "Enable offline simulation",
484
- style: {
485
- ...btn("ghost"),
486
- background: simOffline ? `${C.yellow}22` : C.surface,
487
- color: simOffline ? C.yellow : C.muted,
488
- border: `1px solid ${simOffline ? C.yellow + "44" : C.border}`,
489
- fontSize: 10
490
- },
491
- children: simOffline ? "📡 simulating" : "✈ sim offline"
492
- }
493
- )
494
- ] })
495
- ] }),
496
- /* @__PURE__ */ jsxs("div", { style: {
497
- display: "flex",
498
- alignItems: "center",
499
- gap: 8,
500
- padding: "6px 12px",
501
- borderBottom: `1px solid ${C.border}`,
502
- flexShrink: 0,
503
- background: C.bg
504
- }, children: [
505
- /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 10 }, children: "SW" }),
506
- /* @__PURE__ */ jsx("span", { style: pill(swStatusColor(swStatus)), children: swStatus }),
507
- swError && /* @__PURE__ */ jsx("span", { style: { color: C.red, fontSize: 10, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: swError })
508
- ] }),
509
- /* @__PURE__ */ jsx("div", { style: {
510
- display: "flex",
511
- borderBottom: `1px solid ${C.border}`,
512
- flexShrink: 0,
513
- background: C.surface
514
- }, children: ["queue", "cache"].map((t) => /* @__PURE__ */ jsx(
515
- "button",
516
- {
517
- onClick: () => setTab(t),
518
- style: {
519
- flex: 1,
520
- padding: "6px 0",
521
- background: "none",
522
- border: "none",
523
- borderBottom: tab === t ? `2px solid ${C.cyan}` : "2px solid transparent",
524
- cursor: "pointer",
525
- color: tab === t ? C.cyan : C.muted,
526
- fontSize: 11,
527
- fontWeight: tab === t ? 600 : 400,
528
- fontFamily: "inherit",
529
- textTransform: "uppercase",
530
- letterSpacing: "0.05em"
531
- },
532
- children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
533
- },
534
- t
535
- )) }),
536
- /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto", minHeight: 0 }, children: tab === "queue" ? /* @__PURE__ */ jsx(QueueTab, { queue, onReplay: handleReplay, onClear: handleClear }) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList }) })
537
- ] }),
538
- toggleBtn
539
- ] });
540
- }
541
- function QueueTab({
542
- queue,
543
- onReplay,
544
- onClear
545
- }) {
546
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
547
- /* @__PURE__ */ jsxs("div", { style: {
548
- display: "flex",
549
- gap: 6,
550
- padding: "8px 12px",
551
- borderBottom: `1px solid ${C.border}`,
552
- flexShrink: 0
553
- }, children: [
554
- /* @__PURE__ */ jsx("button", { onClick: onReplay, style: btn("primary"), children: "▶ Replay queue" }),
555
- /* @__PURE__ */ jsx("button", { onClick: onClear, style: btn("danger"), children: "✕ Clear queue" }),
556
- /* @__PURE__ */ jsxs("span", { style: { marginLeft: "auto", color: C.muted, fontSize: 10, alignSelf: "center" }, children: [
557
- queue.length,
558
- " item",
559
- queue.length !== 1 ? "s" : ""
560
- ] })
561
- ] }),
562
- /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto" }, children: queue.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "20px 12px", textAlign: "center", color: C.muted }, children: "Queue empty" }) : queue.map((item) => /* @__PURE__ */ jsxs("div", { style: {
563
- display: "flex",
564
- alignItems: "center",
565
- gap: 8,
566
- padding: "7px 12px",
567
- borderBottom: `1px solid ${C.border}`
568
- }, children: [
569
- /* @__PURE__ */ jsx("span", { style: pill(queueStatusColor(item.status)), children: item.status }),
570
- item.priority && item.priority !== "normal" && /* @__PURE__ */ jsx("span", { style: pill(item.priority === "high" ? C.purple : C.muted), children: item.priority }),
571
- /* @__PURE__ */ jsx("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: C.text }, children: item.actionName }),
572
- item.retryCount > 0 && /* @__PURE__ */ jsxs("span", { style: { color: C.muted, fontSize: 10, flexShrink: 0 }, children: [
573
- "×",
574
- item.retryCount,
575
- "/",
576
- item.maxRetries
577
- ] })
578
- ] }, item.id)) })
579
- ] });
613
+ const [open, setOpen] = useState(defaultOpen);
614
+ const [tab, setTab] = useState("queue");
615
+ const [simOffline, setSimOffline] = useState(false);
616
+ const { isOnline, swStatus, swError } = useEidosStatus();
617
+ const queue = useEidosQueue();
618
+ const { pending, failed, replaying } = useEidosQueueStats();
619
+ const resources = useEidosResources();
620
+ const resourceList = Object.values(resources);
621
+ const badgeCount = pending + failed + replaying;
622
+ const toggleOffline = useCallback(() => {
623
+ const next = !simOffline;
624
+ setSimOffline(next);
625
+ setOfflineSimulation(next);
626
+ }, [simOffline]);
627
+ const handleReplay = useCallback(() => {
628
+ replayQueue();
629
+ }, []);
630
+ const handleClear = useCallback(() => {
631
+ clearQueue();
632
+ }, []);
633
+ const toggleBtn = /* @__PURE__ */ jsxs("button", {
634
+ onClick: () => setOpen((v) => !v),
635
+ "aria-expanded": open,
636
+ "aria-label": open ? "Close Eidos Devtools" : "Open Eidos Devtools",
637
+ title: "Eidos Devtools",
638
+ ...withFocusRing(),
639
+ style: {
640
+ display: "flex",
641
+ alignItems: "center",
642
+ gap: 6,
643
+ padding: "5px 10px",
644
+ background: C.bg,
645
+ border: `1px solid ${C.border}`,
646
+ borderRadius: 9999,
647
+ cursor: "pointer",
648
+ color: C.text,
649
+ fontSize: 11,
650
+ fontWeight: 600,
651
+ fontFamily: "ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace",
652
+ boxShadow: "0 2px 12px rgba(0,0,0,0.5)",
653
+ userSelect: "none",
654
+ minHeight: 32
655
+ },
656
+ children: [
657
+ /* @__PURE__ */ jsx("span", {
658
+ "aria-hidden": "true",
659
+ style: {
660
+ width: 7,
661
+ height: 7,
662
+ borderRadius: "50%",
663
+ background: isOnline ? C.green : C.red,
664
+ flexShrink: 0
665
+ }
666
+ }),
667
+ /* @__PURE__ */ jsx("span", {
668
+ style: {
669
+ color: C.cyan,
670
+ display: "inline-flex"
671
+ },
672
+ children: /* @__PURE__ */ jsx(Icon, {
673
+ path: ICONS.bolt,
674
+ size: 12
675
+ })
676
+ }),
677
+ /* @__PURE__ */ jsx("span", { children: "eidos" }),
678
+ badgeCount > 0 && /* @__PURE__ */ jsx("span", {
679
+ "aria-label": `${badgeCount} ${failed > 0 ? "failed" : "pending"} queue item${badgeCount !== 1 ? "s" : ""}`,
680
+ style: {
681
+ background: failed > 0 ? C.red : C.yellow,
682
+ color: "#fff",
683
+ borderRadius: 9999,
684
+ minWidth: 16,
685
+ height: 16,
686
+ display: "inline-flex",
687
+ alignItems: "center",
688
+ justifyContent: "center",
689
+ fontSize: 9,
690
+ fontWeight: 700,
691
+ padding: "0 4px"
692
+ },
693
+ children: badgeCount
694
+ })
695
+ ]
696
+ });
697
+ if (!open) return /* @__PURE__ */ jsx("div", {
698
+ style: positionStyle(position),
699
+ children: toggleBtn
700
+ });
701
+ return /* @__PURE__ */ jsxs("div", {
702
+ style: {
703
+ ...positionStyle(position),
704
+ display: "flex",
705
+ flexDirection: "column",
706
+ alignItems: "flex-end",
707
+ gap: 6
708
+ },
709
+ children: [/* @__PURE__ */ jsxs("div", {
710
+ style: {
711
+ width: 340,
712
+ maxHeight: 480,
713
+ display: "flex",
714
+ flexDirection: "column",
715
+ background: C.bg,
716
+ border: `1px solid ${C.border}`,
717
+ borderRadius: 10,
718
+ overflow: "hidden",
719
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
720
+ fontFamily: "ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace",
721
+ fontSize: 11,
722
+ color: C.text
723
+ },
724
+ children: [
725
+ /* @__PURE__ */ jsxs("div", {
726
+ style: {
727
+ display: "flex",
728
+ alignItems: "center",
729
+ gap: 8,
730
+ padding: "8px 12px",
731
+ background: C.surface,
732
+ borderBottom: `1px solid ${C.border}`,
733
+ flexShrink: 0
734
+ },
735
+ children: [
736
+ /* @__PURE__ */ jsx("span", {
737
+ style: {
738
+ color: C.cyan,
739
+ fontSize: 13
740
+ },
741
+ children: ""
742
+ }),
743
+ /* @__PURE__ */ jsx("span", {
744
+ style: {
745
+ fontWeight: 700,
746
+ fontSize: 12,
747
+ color: C.text,
748
+ flex: 1
749
+ },
750
+ children: "Eidos Devtools"
751
+ }),
752
+ /* @__PURE__ */ jsxs("div", {
753
+ style: {
754
+ display: "flex",
755
+ alignItems: "center",
756
+ gap: 6
757
+ },
758
+ children: [/* @__PURE__ */ jsx("span", {
759
+ style: pill(isOnline ? C.green : C.red),
760
+ children: isOnline ? "online" : "offline"
761
+ }), /* @__PURE__ */ jsxs("button", {
762
+ onClick: toggleOffline,
763
+ "aria-pressed": simOffline,
764
+ title: simOffline ? "Disable offline simulation" : "Enable offline simulation",
765
+ ...withFocusRing(),
766
+ style: {
767
+ ...btn("ghost"),
768
+ background: simOffline ? `${C.yellow}22` : C.surface,
769
+ color: simOffline ? C.yellow : C.muted,
770
+ border: `1px solid ${simOffline ? C.yellow + "44" : C.border}`,
771
+ fontSize: 10,
772
+ minHeight: 22
773
+ },
774
+ children: [/* @__PURE__ */ jsx(Icon, {
775
+ path: simOffline ? ICONS.satelliteOff : ICONS.satellite,
776
+ size: 11
777
+ }), simOffline ? "simulating offline" : "sim offline"]
778
+ })]
779
+ })
780
+ ]
781
+ }),
782
+ /* @__PURE__ */ jsxs("div", {
783
+ style: {
784
+ display: "flex",
785
+ alignItems: "center",
786
+ gap: 8,
787
+ padding: "6px 12px",
788
+ borderBottom: `1px solid ${C.border}`,
789
+ flexShrink: 0,
790
+ background: C.bg
791
+ },
792
+ children: [
793
+ /* @__PURE__ */ jsx("span", {
794
+ style: {
795
+ color: C.muted,
796
+ fontSize: 10
797
+ },
798
+ children: "SW"
799
+ }),
800
+ /* @__PURE__ */ jsx("span", {
801
+ style: pill(swStatusColor(swStatus)),
802
+ children: swStatus
803
+ }),
804
+ swError && /* @__PURE__ */ jsx("span", {
805
+ style: {
806
+ color: C.red,
807
+ fontSize: 10,
808
+ flex: 1,
809
+ overflow: "hidden",
810
+ textOverflow: "ellipsis",
811
+ whiteSpace: "nowrap"
812
+ },
813
+ children: swError
814
+ })
815
+ ]
816
+ }),
817
+ /* @__PURE__ */ jsx("div", {
818
+ style: {
819
+ display: "flex",
820
+ borderBottom: `1px solid ${C.border}`,
821
+ flexShrink: 0,
822
+ background: C.surface
823
+ },
824
+ children: ["queue", "cache"].map((t) => /* @__PURE__ */ jsx("button", {
825
+ role: "tab",
826
+ "aria-selected": tab === t,
827
+ onClick: () => setTab(t),
828
+ ...withFocusRing(),
829
+ style: {
830
+ flex: 1,
831
+ padding: "6px 0",
832
+ minHeight: 32,
833
+ background: "none",
834
+ border: "none",
835
+ borderBottom: tab === t ? `2px solid ${C.cyan}` : "2px solid transparent",
836
+ cursor: "pointer",
837
+ color: tab === t ? C.cyan : C.muted,
838
+ fontSize: 11,
839
+ fontWeight: tab === t ? 600 : 400,
840
+ fontFamily: "inherit",
841
+ textTransform: "uppercase",
842
+ letterSpacing: "0.05em",
843
+ transition: "color 0.15s, border-color 0.15s"
844
+ },
845
+ children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
846
+ }, t))
847
+ }),
848
+ /* @__PURE__ */ jsx("div", {
849
+ style: {
850
+ flex: 1,
851
+ overflowY: "auto",
852
+ minHeight: 0
853
+ },
854
+ children: tab === "queue" ? /* @__PURE__ */ jsx(QueueTab, {
855
+ queue,
856
+ onReplay: handleReplay,
857
+ onClear: handleClear
858
+ }) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList })
859
+ })
860
+ ]
861
+ }), toggleBtn]
862
+ });
863
+ }
864
+ function QueueTab({ queue, onReplay, onClear }) {
865
+ return /* @__PURE__ */ jsxs("div", {
866
+ style: {
867
+ display: "flex",
868
+ flexDirection: "column",
869
+ height: "100%"
870
+ },
871
+ children: [/* @__PURE__ */ jsxs("div", {
872
+ style: {
873
+ display: "flex",
874
+ gap: 6,
875
+ padding: "8px 12px",
876
+ borderBottom: `1px solid ${C.border}`,
877
+ flexShrink: 0
878
+ },
879
+ children: [
880
+ /* @__PURE__ */ jsxs("button", {
881
+ onClick: onReplay,
882
+ ...withFocusRing(),
883
+ style: {
884
+ ...btn("primary"),
885
+ minHeight: 24
886
+ },
887
+ children: [/* @__PURE__ */ jsx(Icon, {
888
+ path: ICONS.play,
889
+ size: 11
890
+ }), "Replay queue"]
891
+ }),
892
+ /* @__PURE__ */ jsxs("button", {
893
+ onClick: onClear,
894
+ ...withFocusRing(),
895
+ style: {
896
+ ...btn("danger"),
897
+ minHeight: 24
898
+ },
899
+ children: [/* @__PURE__ */ jsx(Icon, {
900
+ path: ICONS.trash,
901
+ size: 11
902
+ }), "Clear queue"]
903
+ }),
904
+ /* @__PURE__ */ jsxs("span", {
905
+ style: {
906
+ marginLeft: "auto",
907
+ color: C.muted,
908
+ fontSize: 10,
909
+ alignSelf: "center"
910
+ },
911
+ children: [
912
+ queue.length,
913
+ " item",
914
+ queue.length !== 1 ? "s" : ""
915
+ ]
916
+ })
917
+ ]
918
+ }), /* @__PURE__ */ jsx("div", {
919
+ style: {
920
+ flex: 1,
921
+ overflowY: "auto"
922
+ },
923
+ children: queue.length === 0 ? /* @__PURE__ */ jsx("div", {
924
+ style: {
925
+ padding: "20px 12px",
926
+ textAlign: "center",
927
+ color: C.muted
928
+ },
929
+ children: "Queue empty"
930
+ }) : queue.map((item) => /* @__PURE__ */ jsxs("div", {
931
+ style: {
932
+ display: "flex",
933
+ alignItems: "center",
934
+ gap: 8,
935
+ padding: "7px 12px",
936
+ borderBottom: `1px solid ${C.border}`
937
+ },
938
+ children: [
939
+ /* @__PURE__ */ jsx("span", {
940
+ style: pill(queueStatusColor(item.status)),
941
+ children: item.status
942
+ }),
943
+ item.priority && item.priority !== "normal" && /* @__PURE__ */ jsx("span", {
944
+ style: pill(item.priority === "high" ? C.purple : C.muted),
945
+ children: item.priority
946
+ }),
947
+ /* @__PURE__ */ jsx("span", {
948
+ style: {
949
+ flex: 1,
950
+ overflow: "hidden",
951
+ textOverflow: "ellipsis",
952
+ whiteSpace: "nowrap",
953
+ color: C.text
954
+ },
955
+ children: item.actionName
956
+ }),
957
+ item.retryCount > 0 && /* @__PURE__ */ jsxs("span", {
958
+ style: {
959
+ color: C.muted,
960
+ fontSize: 10,
961
+ flexShrink: 0
962
+ },
963
+ children: [
964
+ "×",
965
+ item.retryCount,
966
+ "/",
967
+ item.maxRetries
968
+ ]
969
+ })
970
+ ]
971
+ }, item.id))
972
+ })]
973
+ });
580
974
  }
581
975
  function CacheTab({ resources }) {
582
- return /* @__PURE__ */ jsx("div", { children: resources.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "20px 12px", textAlign: "center", color: C.muted }, children: "No resources registered" }) : resources.map((res) => /* @__PURE__ */ jsxs("div", { style: {
583
- padding: "7px 12px",
584
- borderBottom: `1px solid ${C.border}`
585
- }, children: [
586
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }, children: [
587
- /* @__PURE__ */ jsx("span", { style: pill(resourceStatusColor(res.status)), children: res.status }),
588
- /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 10 }, children: res.strategy.name })
589
- ] }),
590
- /* @__PURE__ */ jsx("div", { style: {
591
- color: C.text,
592
- overflow: "hidden",
593
- textOverflow: "ellipsis",
594
- whiteSpace: "nowrap",
595
- fontSize: 10,
596
- marginBottom: 2
597
- }, children: res.url }),
598
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, color: C.muted, fontSize: 10 }, children: [
599
- /* @__PURE__ */ jsxs("span", { title: "Cache hits", children: [
600
- "↑",
601
- res.cacheHits,
602
- " hit",
603
- res.cacheHits !== 1 ? "s" : ""
604
- ] }),
605
- /* @__PURE__ */ jsxs("span", { title: "Cache misses", children: [
606
- "↓",
607
- res.cacheMisses,
608
- " miss",
609
- res.cacheMisses !== 1 ? "es" : ""
610
- ] }),
611
- res.cachedAt && /* @__PURE__ */ jsxs("span", { title: "Cached at", children: [
612
- "⏱ ",
613
- new Date(res.cachedAt).toLocaleTimeString()
614
- ] })
615
- ] })
616
- ] }, res.url)) });
617
- }
618
- export {
619
- EidosDevtools
620
- };
976
+ return /* @__PURE__ */ jsx("div", { children: resources.length === 0 ? /* @__PURE__ */ jsx("div", {
977
+ style: {
978
+ padding: "20px 12px",
979
+ textAlign: "center",
980
+ color: C.muted
981
+ },
982
+ children: "No resources registered"
983
+ }) : resources.map((res) => /* @__PURE__ */ jsxs("div", {
984
+ style: {
985
+ padding: "7px 12px",
986
+ borderBottom: `1px solid ${C.border}`
987
+ },
988
+ children: [
989
+ /* @__PURE__ */ jsxs("div", {
990
+ style: {
991
+ display: "flex",
992
+ alignItems: "center",
993
+ gap: 6,
994
+ marginBottom: 3
995
+ },
996
+ children: [/* @__PURE__ */ jsx("span", {
997
+ style: pill(resourceStatusColor(res.status)),
998
+ children: res.status
999
+ }), /* @__PURE__ */ jsx("span", {
1000
+ style: {
1001
+ color: C.muted,
1002
+ fontSize: 10
1003
+ },
1004
+ children: res.strategy.name
1005
+ })]
1006
+ }),
1007
+ /* @__PURE__ */ jsx("div", {
1008
+ style: {
1009
+ color: C.text,
1010
+ overflow: "hidden",
1011
+ textOverflow: "ellipsis",
1012
+ whiteSpace: "nowrap",
1013
+ fontSize: 10,
1014
+ marginBottom: 2
1015
+ },
1016
+ children: res.url
1017
+ }),
1018
+ /* @__PURE__ */ jsxs("div", {
1019
+ style: {
1020
+ display: "flex",
1021
+ gap: 10,
1022
+ color: C.muted,
1023
+ fontSize: 10
1024
+ },
1025
+ children: [
1026
+ /* @__PURE__ */ jsxs("span", {
1027
+ title: "Cache hits",
1028
+ style: {
1029
+ display: "inline-flex",
1030
+ alignItems: "center",
1031
+ gap: 3
1032
+ },
1033
+ children: [
1034
+ /* @__PURE__ */ jsx(Icon, {
1035
+ path: ICONS.arrowUp,
1036
+ size: 10
1037
+ }),
1038
+ res.cacheHits,
1039
+ " hit",
1040
+ res.cacheHits !== 1 ? "s" : ""
1041
+ ]
1042
+ }),
1043
+ /* @__PURE__ */ jsxs("span", {
1044
+ title: "Cache misses",
1045
+ style: {
1046
+ display: "inline-flex",
1047
+ alignItems: "center",
1048
+ gap: 3
1049
+ },
1050
+ children: [
1051
+ /* @__PURE__ */ jsx(Icon, {
1052
+ path: ICONS.arrowDown,
1053
+ size: 10
1054
+ }),
1055
+ res.cacheMisses,
1056
+ " miss",
1057
+ res.cacheMisses !== 1 ? "es" : ""
1058
+ ]
1059
+ }),
1060
+ res.cachedAt && /* @__PURE__ */ jsxs("span", {
1061
+ title: "Cached at",
1062
+ style: {
1063
+ display: "inline-flex",
1064
+ alignItems: "center",
1065
+ gap: 3
1066
+ },
1067
+ children: [/* @__PURE__ */ jsx(Icon, {
1068
+ path: ICONS.clock,
1069
+ size: 10
1070
+ }), new Date(res.cachedAt).toLocaleTimeString()]
1071
+ })
1072
+ ]
1073
+ })
1074
+ ]
1075
+ }, res.url)) });
1076
+ }
1077
+ //#endregion
1078
+ export { EidosDevtools };