@sweidos/eidos 1.0.34 → 1.1.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 (48) hide show
  1. package/README.md +96 -89
  2. package/dist/action.js +119 -86
  3. package/dist/async-storage-adapter.js +15 -12
  4. package/dist/devtools.js +953 -555
  5. package/dist/eidos.cjs +15 -0
  6. package/dist/idb.js +59 -56
  7. package/dist/index.d.ts +37 -15
  8. package/dist/index.js +42 -41
  9. package/dist/nextjs.js +1 -10
  10. package/dist/query.cjs +131 -0
  11. package/dist/query.js +121 -41
  12. package/dist/queue-storage.js +5 -4
  13. package/dist/react/Devtools.d.ts +1 -1
  14. package/dist/react/Provider.js +11 -7
  15. package/dist/react/hooks.js +48 -38
  16. package/dist/react-native.js +47 -53
  17. package/dist/replay.js +15 -0
  18. package/dist/resource.js +77 -79
  19. package/dist/runtime.js +22 -28
  20. package/dist/store-slices.js +43 -0
  21. package/dist/store.js +32 -49
  22. package/dist/stores.js +25 -22
  23. package/dist/sveltekit.js +22 -6
  24. package/dist/sw-bridge.js +48 -46
  25. package/dist/testing.cjs +165 -0
  26. package/dist/testing.js +140 -70
  27. package/dist/version.js +4 -3
  28. package/dist/vite.cjs +48 -0
  29. package/dist/vite.js +45 -29
  30. package/package.json +48 -27
  31. package/dist/action.js.map +0 -1
  32. package/dist/async-storage-adapter.js.map +0 -1
  33. package/dist/eidos.cjs.js +0 -14
  34. package/dist/eidos.cjs.js.map +0 -1
  35. package/dist/idb.js.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/query.cjs.js +0 -48
  38. package/dist/queue-storage.js.map +0 -1
  39. package/dist/react/Provider.js.map +0 -1
  40. package/dist/react/hooks.js.map +0 -1
  41. package/dist/resource.js.map +0 -1
  42. package/dist/runtime.js.map +0 -1
  43. package/dist/store.js.map +0 -1
  44. package/dist/stores.js.map +0 -1
  45. package/dist/sw-bridge.js.map +0 -1
  46. package/dist/testing.cjs.js +0 -86
  47. package/dist/version.js.map +0 -1
  48. package/dist/vite.cjs.js +0 -31
package/dist/devtools.js CHANGED
@@ -1,620 +1,1018 @@
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 };
84
- }
111
+ return {
112
+ isOnline: useStore((s) => s.isOnline),
113
+ swStatus: useStore((s) => s.swStatus),
114
+ swError: useStore((s) => s.swError)
115
+ };
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);
100
- }
101
- const DB_NAME = "eidos";
102
- const DB_VERSION = 1;
103
- const QUEUE_STORE = "action-queue";
104
- let _db = null;
147
+ sendToWorker({
148
+ type: "EIDOS_SIMULATE_OFFLINE",
149
+ enabled
150
+ });
151
+ useEidosStore.getState().setOnline(!enabled);
152
+ }
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();
210
269
  function qs() {
211
- return _idbFallback;
270
+ return _getQueueStorage() ?? idbQueueStorage;
212
271
  }
213
272
  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;
273
+ if (err instanceof Response) return err.status >= 400 && err.status < 500;
274
+ if (typeof err === "object" && err !== null) {
275
+ const s = err.status;
276
+ if (typeof s === "number") return s >= 400 && s < 500;
277
+ }
278
+ return false;
220
279
  }
221
280
  function backoffMs(retryCount) {
222
- const base = Math.min(2e3 * 2 ** retryCount, 3e5);
223
- return base * (0.8 + Math.random() * 0.4);
281
+ return Math.min(2e3 * 2 ** retryCount, 3e5) * (.8 + Math.random() * .4);
224
282
  }
225
- let _replaying = false;
283
+ var _replaying = false;
226
284
  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
- }
285
+ const store = useEidosStore.getState();
286
+ if (!store.isOnline || _replaying) return {
287
+ attempted: 0,
288
+ succeeded: 0,
289
+ failed: 0,
290
+ retrying: 0,
291
+ skipped: 0,
292
+ conflicted: 0
293
+ };
294
+ _replaying = true;
295
+ try {
296
+ return await _doReplayQueue(store);
297
+ } finally {
298
+ _replaying = false;
299
+ }
237
300
  }
238
301
  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
- }
302
+ const fn = _actionRegistry.get(item.actionId);
303
+ if (!fn) return "skipped";
304
+ try {
305
+ await fn(...item.args);
306
+ const completedAt = Date.now();
307
+ store.updateQueueItem(item.id, {
308
+ status: "succeeded",
309
+ completedAt
310
+ });
311
+ await qs().update(item.id, {
312
+ status: "succeeded",
313
+ completedAt
314
+ });
315
+ setTimeout(() => {
316
+ store.removeQueueItem(item.id);
317
+ qs().remove(item.id);
318
+ }, 3e3);
319
+ return "succeeded";
320
+ } catch (err) {
321
+ if (isClientError(err)) {
322
+ const onConflict = _conflictRegistry.get(item.actionId);
323
+ if (onConflict) {
324
+ if (onConflict(err, item.args) === "skip") {
325
+ store.removeQueueItem(item.id);
326
+ await qs().remove(item.id);
327
+ return "conflicted";
328
+ }
329
+ }
330
+ }
331
+ const retryCount = item.retryCount + 1;
332
+ if (retryCount >= item.maxRetries) {
333
+ store.updateQueueItem(item.id, {
334
+ status: "failed",
335
+ error: String(err),
336
+ retryCount
337
+ });
338
+ await qs().update(item.id, {
339
+ status: "failed",
340
+ error: String(err),
341
+ retryCount
342
+ });
343
+ _rollbackRegistry.get(item.actionId)?.(...item.args);
344
+ return "failed";
345
+ } else {
346
+ const nextRetryAt = Date.now() + backoffMs(retryCount);
347
+ store.updateQueueItem(item.id, {
348
+ status: "pending",
349
+ retryCount,
350
+ nextRetryAt
351
+ });
352
+ await qs().update(item.id, {
353
+ status: "pending",
354
+ retryCount,
355
+ nextRetryAt
356
+ });
357
+ return "retrying";
358
+ }
359
+ }
277
360
  }
278
361
  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
- }
362
+ if (items.length === 0) return;
363
+ const replayable = items.filter((item) => _actionRegistry.has(item.actionId));
364
+ result.skipped += items.length - replayable.length;
365
+ if (replayable.length > 0) {
366
+ store.batchUpdateQueueItems(replayable.map((item) => ({
367
+ id: item.id,
368
+ update: { status: "replaying" }
369
+ })));
370
+ for (const item of replayable) qs().update(item.id, { status: "replaying" });
371
+ }
372
+ const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
373
+ for (const o of outcomes) {
374
+ const outcome = o.status === "fulfilled" ? o.value : "failed";
375
+ if (outcome === "skipped") result.skipped++;
376
+ else if (outcome === "conflicted") result.conflicted++;
377
+ else {
378
+ result.attempted++;
379
+ result[outcome]++;
380
+ }
381
+ }
300
382
  }
301
383
  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;
313
- }
384
+ const candidates = await qs().getPending();
385
+ const now = Date.now();
386
+ const pending = candidates.filter((item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now));
387
+ const result = {
388
+ attempted: 0,
389
+ succeeded: 0,
390
+ failed: 0,
391
+ retrying: 0,
392
+ skipped: 0,
393
+ conflicted: 0
394
+ };
395
+ for (const tier of [
396
+ "high",
397
+ "normal",
398
+ "low"
399
+ ]) await _replayTier(pending.filter((item) => (item.priority ?? "normal") === tier), store, result);
400
+ return result;
401
+ }
402
+ /** Remove all items from the action queue (storage + in-memory store). */
314
403
  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"
404
+ await qs().clear();
405
+ useEidosStore.getState().hydrateQueue([]);
406
+ }
407
+ //#endregion
408
+ //#region src/react/Devtools.tsx
409
+ var C = {
410
+ bg: "#0f1117",
411
+ surface: "#1a1d27",
412
+ border: "#2a2d3a",
413
+ text: "#e2e8f0",
414
+ muted: "#64748b",
415
+ green: "#22c55e",
416
+ red: "#ef4444",
417
+ yellow: "#f59e0b",
418
+ blue: "#3b82f6",
419
+ purple: "#a855f7",
420
+ cyan: "#06b6d4"
330
421
  };
331
422
  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
- };
423
+ return {
424
+ display: "inline-flex",
425
+ alignItems: "center",
426
+ padding: "1px 7px",
427
+ borderRadius: 9999,
428
+ fontSize: 10,
429
+ fontWeight: 600,
430
+ background: `${color}22`,
431
+ color,
432
+ border: `1px solid ${color}44`,
433
+ fontFamily: "inherit"
434
+ };
344
435
  }
345
436
  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 };
437
+ const base = {
438
+ cursor: "pointer",
439
+ border: "none",
440
+ borderRadius: 6,
441
+ padding: "3px 10px",
442
+ fontSize: 11,
443
+ fontWeight: 500,
444
+ fontFamily: "inherit",
445
+ display: "inline-flex",
446
+ alignItems: "center",
447
+ gap: 5,
448
+ transition: "background-color 0.15s, color 0.15s"
449
+ };
450
+ if (variant === "danger") return {
451
+ ...base,
452
+ background: `${C.red}22`,
453
+ color: C.red
454
+ };
455
+ if (variant === "primary") return {
456
+ ...base,
457
+ background: `${C.blue}22`,
458
+ color: C.blue
459
+ };
460
+ return {
461
+ ...base,
462
+ background: C.surface,
463
+ color: C.muted
464
+ };
465
+ }
466
+ var focusRing = {
467
+ outline: `2px solid ${C.cyan}`,
468
+ outlineOffset: 1
469
+ };
470
+ function withFocusRing(handlers = {}) {
471
+ return {
472
+ onFocus: (e) => {
473
+ Object.assign(e.currentTarget.style, focusRing);
474
+ handlers.onFocus?.();
475
+ },
476
+ onBlur: (e) => {
477
+ e.currentTarget.style.outline = "none";
478
+ handlers.onBlur?.();
479
+ }
480
+ };
359
481
  }
360
482
  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;
483
+ if (s === "active") return C.green;
484
+ if (s === "registering") return C.yellow;
485
+ if (s === "error" || s === "unsupported") return C.red;
486
+ return C.muted;
365
487
  }
366
488
  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;
489
+ if (s === "succeeded") return C.green;
490
+ if (s === "failed") return C.red;
491
+ if (s === "replaying") return C.yellow;
492
+ return C.blue;
371
493
  }
372
494
  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;
378
- }
495
+ if (s === "fresh") return C.green;
496
+ if (s === "stale" || s === "offline") return C.yellow;
497
+ if (s === "error") return C.red;
498
+ if (s === "fetching") return C.cyan;
499
+ return C.muted;
500
+ }
501
+ function Icon({ path, size = 12, strokeWidth = 2 }) {
502
+ return /* @__PURE__ */ jsx("svg", {
503
+ width: size,
504
+ height: size,
505
+ viewBox: "0 0 24 24",
506
+ fill: "none",
507
+ stroke: "currentColor",
508
+ strokeWidth,
509
+ strokeLinecap: "round",
510
+ strokeLinejoin: "round",
511
+ "aria-hidden": "true",
512
+ style: { flexShrink: 0 },
513
+ children: /* @__PURE__ */ jsx("path", { d: path })
514
+ });
515
+ }
516
+ var ICONS = {
517
+ bolt: "M13 2 3 14h7l-1 8 10-12h-7l1-8z",
518
+ 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",
519
+ 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",
520
+ play: "M6 4l13 8-13 8V4z",
521
+ 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",
522
+ arrowUp: "M12 19V5M5 12l7-7 7 7",
523
+ arrowDown: "M12 5v14M19 12l-7 7-7-7",
524
+ clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2"
525
+ };
379
526
  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 };
527
+ const base = {
528
+ position: "fixed",
529
+ zIndex: 99999
530
+ };
531
+ if (p === "bottom-left") return {
532
+ ...base,
533
+ bottom: 16,
534
+ left: 16
535
+ };
536
+ if (p === "top-right") return {
537
+ ...base,
538
+ top: 16,
539
+ right: 16
540
+ };
541
+ if (p === "top-left") return {
542
+ ...base,
543
+ top: 16,
544
+ left: 16
545
+ };
546
+ return {
547
+ ...base,
548
+ bottom: 16,
549
+ right: 16
550
+ };
385
551
  }
386
552
  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
- ] });
553
+ const [open, setOpen] = useState(defaultOpen);
554
+ const [tab, setTab] = useState("queue");
555
+ const [simOffline, setSimOffline] = useState(false);
556
+ const { isOnline, swStatus, swError } = useEidosStatus();
557
+ const queue = useEidosQueue();
558
+ const { pending, failed, replaying } = useEidosQueueStats();
559
+ const resources = useEidosResources();
560
+ const resourceList = Object.values(resources);
561
+ const badgeCount = pending + failed + replaying;
562
+ const toggleOffline = useCallback(() => {
563
+ const next = !simOffline;
564
+ setSimOffline(next);
565
+ setOfflineSimulation(next);
566
+ }, [simOffline]);
567
+ const handleReplay = useCallback(() => {
568
+ replayQueue();
569
+ }, []);
570
+ const handleClear = useCallback(() => {
571
+ clearQueue();
572
+ }, []);
573
+ const toggleBtn = /* @__PURE__ */ jsxs("button", {
574
+ onClick: () => setOpen((v) => !v),
575
+ "aria-expanded": open,
576
+ "aria-label": open ? "Close Eidos Devtools" : "Open Eidos Devtools",
577
+ title: "Eidos Devtools",
578
+ ...withFocusRing(),
579
+ style: {
580
+ display: "flex",
581
+ alignItems: "center",
582
+ gap: 6,
583
+ padding: "5px 10px",
584
+ background: C.bg,
585
+ border: `1px solid ${C.border}`,
586
+ borderRadius: 9999,
587
+ cursor: "pointer",
588
+ color: C.text,
589
+ fontSize: 11,
590
+ fontWeight: 600,
591
+ fontFamily: "ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace",
592
+ boxShadow: "0 2px 12px rgba(0,0,0,0.5)",
593
+ userSelect: "none",
594
+ minHeight: 32
595
+ },
596
+ children: [
597
+ /* @__PURE__ */ jsx("span", {
598
+ "aria-hidden": "true",
599
+ style: {
600
+ width: 7,
601
+ height: 7,
602
+ borderRadius: "50%",
603
+ background: isOnline ? C.green : C.red,
604
+ flexShrink: 0
605
+ }
606
+ }),
607
+ /* @__PURE__ */ jsx("span", {
608
+ style: {
609
+ color: C.cyan,
610
+ display: "inline-flex"
611
+ },
612
+ children: /* @__PURE__ */ jsx(Icon, {
613
+ path: ICONS.bolt,
614
+ size: 12
615
+ })
616
+ }),
617
+ /* @__PURE__ */ jsx("span", { children: "eidos" }),
618
+ badgeCount > 0 && /* @__PURE__ */ jsx("span", {
619
+ "aria-label": `${badgeCount} ${failed > 0 ? "failed" : "pending"} queue item${badgeCount !== 1 ? "s" : ""}`,
620
+ style: {
621
+ background: failed > 0 ? C.red : C.yellow,
622
+ color: "#fff",
623
+ borderRadius: 9999,
624
+ minWidth: 16,
625
+ height: 16,
626
+ display: "inline-flex",
627
+ alignItems: "center",
628
+ justifyContent: "center",
629
+ fontSize: 9,
630
+ fontWeight: 700,
631
+ padding: "0 4px"
632
+ },
633
+ children: badgeCount
634
+ })
635
+ ]
636
+ });
637
+ if (!open) return /* @__PURE__ */ jsx("div", {
638
+ style: positionStyle(position),
639
+ children: toggleBtn
640
+ });
641
+ return /* @__PURE__ */ jsxs("div", {
642
+ style: {
643
+ ...positionStyle(position),
644
+ display: "flex",
645
+ flexDirection: "column",
646
+ alignItems: "flex-end",
647
+ gap: 6
648
+ },
649
+ children: [/* @__PURE__ */ jsxs("div", {
650
+ style: {
651
+ width: 340,
652
+ maxHeight: 480,
653
+ display: "flex",
654
+ flexDirection: "column",
655
+ background: C.bg,
656
+ border: `1px solid ${C.border}`,
657
+ borderRadius: 10,
658
+ overflow: "hidden",
659
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
660
+ fontFamily: "ui-monospace, \"Cascadia Code\", \"Fira Mono\", monospace",
661
+ fontSize: 11,
662
+ color: C.text
663
+ },
664
+ children: [
665
+ /* @__PURE__ */ jsxs("div", {
666
+ style: {
667
+ display: "flex",
668
+ alignItems: "center",
669
+ gap: 8,
670
+ padding: "8px 12px",
671
+ background: C.surface,
672
+ borderBottom: `1px solid ${C.border}`,
673
+ flexShrink: 0
674
+ },
675
+ children: [
676
+ /* @__PURE__ */ jsx("span", {
677
+ style: {
678
+ color: C.cyan,
679
+ fontSize: 13
680
+ },
681
+ children: ""
682
+ }),
683
+ /* @__PURE__ */ jsx("span", {
684
+ style: {
685
+ fontWeight: 700,
686
+ fontSize: 12,
687
+ color: C.text,
688
+ flex: 1
689
+ },
690
+ children: "Eidos Devtools"
691
+ }),
692
+ /* @__PURE__ */ jsxs("div", {
693
+ style: {
694
+ display: "flex",
695
+ alignItems: "center",
696
+ gap: 6
697
+ },
698
+ children: [/* @__PURE__ */ jsx("span", {
699
+ style: pill(isOnline ? C.green : C.red),
700
+ children: isOnline ? "online" : "offline"
701
+ }), /* @__PURE__ */ jsxs("button", {
702
+ onClick: toggleOffline,
703
+ "aria-pressed": simOffline,
704
+ title: simOffline ? "Disable offline simulation" : "Enable offline simulation",
705
+ ...withFocusRing(),
706
+ style: {
707
+ ...btn("ghost"),
708
+ background: simOffline ? `${C.yellow}22` : C.surface,
709
+ color: simOffline ? C.yellow : C.muted,
710
+ border: `1px solid ${simOffline ? C.yellow + "44" : C.border}`,
711
+ fontSize: 10,
712
+ minHeight: 22
713
+ },
714
+ children: [/* @__PURE__ */ jsx(Icon, {
715
+ path: simOffline ? ICONS.satelliteOff : ICONS.satellite,
716
+ size: 11
717
+ }), simOffline ? "simulating offline" : "sim offline"]
718
+ })]
719
+ })
720
+ ]
721
+ }),
722
+ /* @__PURE__ */ jsxs("div", {
723
+ style: {
724
+ display: "flex",
725
+ alignItems: "center",
726
+ gap: 8,
727
+ padding: "6px 12px",
728
+ borderBottom: `1px solid ${C.border}`,
729
+ flexShrink: 0,
730
+ background: C.bg
731
+ },
732
+ children: [
733
+ /* @__PURE__ */ jsx("span", {
734
+ style: {
735
+ color: C.muted,
736
+ fontSize: 10
737
+ },
738
+ children: "SW"
739
+ }),
740
+ /* @__PURE__ */ jsx("span", {
741
+ style: pill(swStatusColor(swStatus)),
742
+ children: swStatus
743
+ }),
744
+ swError && /* @__PURE__ */ jsx("span", {
745
+ style: {
746
+ color: C.red,
747
+ fontSize: 10,
748
+ flex: 1,
749
+ overflow: "hidden",
750
+ textOverflow: "ellipsis",
751
+ whiteSpace: "nowrap"
752
+ },
753
+ children: swError
754
+ })
755
+ ]
756
+ }),
757
+ /* @__PURE__ */ jsx("div", {
758
+ style: {
759
+ display: "flex",
760
+ borderBottom: `1px solid ${C.border}`,
761
+ flexShrink: 0,
762
+ background: C.surface
763
+ },
764
+ children: ["queue", "cache"].map((t) => /* @__PURE__ */ jsx("button", {
765
+ role: "tab",
766
+ "aria-selected": tab === t,
767
+ onClick: () => setTab(t),
768
+ ...withFocusRing(),
769
+ style: {
770
+ flex: 1,
771
+ padding: "6px 0",
772
+ minHeight: 32,
773
+ background: "none",
774
+ border: "none",
775
+ borderBottom: tab === t ? `2px solid ${C.cyan}` : "2px solid transparent",
776
+ cursor: "pointer",
777
+ color: tab === t ? C.cyan : C.muted,
778
+ fontSize: 11,
779
+ fontWeight: tab === t ? 600 : 400,
780
+ fontFamily: "inherit",
781
+ textTransform: "uppercase",
782
+ letterSpacing: "0.05em",
783
+ transition: "color 0.15s, border-color 0.15s"
784
+ },
785
+ children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
786
+ }, t))
787
+ }),
788
+ /* @__PURE__ */ jsx("div", {
789
+ style: {
790
+ flex: 1,
791
+ overflowY: "auto",
792
+ minHeight: 0
793
+ },
794
+ children: tab === "queue" ? /* @__PURE__ */ jsx(QueueTab, {
795
+ queue,
796
+ onReplay: handleReplay,
797
+ onClear: handleClear
798
+ }) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList })
799
+ })
800
+ ]
801
+ }), toggleBtn]
802
+ });
803
+ }
804
+ function QueueTab({ queue, onReplay, onClear }) {
805
+ return /* @__PURE__ */ jsxs("div", {
806
+ style: {
807
+ display: "flex",
808
+ flexDirection: "column",
809
+ height: "100%"
810
+ },
811
+ children: [/* @__PURE__ */ jsxs("div", {
812
+ style: {
813
+ display: "flex",
814
+ gap: 6,
815
+ padding: "8px 12px",
816
+ borderBottom: `1px solid ${C.border}`,
817
+ flexShrink: 0
818
+ },
819
+ children: [
820
+ /* @__PURE__ */ jsxs("button", {
821
+ onClick: onReplay,
822
+ ...withFocusRing(),
823
+ style: {
824
+ ...btn("primary"),
825
+ minHeight: 24
826
+ },
827
+ children: [/* @__PURE__ */ jsx(Icon, {
828
+ path: ICONS.play,
829
+ size: 11
830
+ }), "Replay queue"]
831
+ }),
832
+ /* @__PURE__ */ jsxs("button", {
833
+ onClick: onClear,
834
+ ...withFocusRing(),
835
+ style: {
836
+ ...btn("danger"),
837
+ minHeight: 24
838
+ },
839
+ children: [/* @__PURE__ */ jsx(Icon, {
840
+ path: ICONS.trash,
841
+ size: 11
842
+ }), "Clear queue"]
843
+ }),
844
+ /* @__PURE__ */ jsxs("span", {
845
+ style: {
846
+ marginLeft: "auto",
847
+ color: C.muted,
848
+ fontSize: 10,
849
+ alignSelf: "center"
850
+ },
851
+ children: [
852
+ queue.length,
853
+ " item",
854
+ queue.length !== 1 ? "s" : ""
855
+ ]
856
+ })
857
+ ]
858
+ }), /* @__PURE__ */ jsx("div", {
859
+ style: {
860
+ flex: 1,
861
+ overflowY: "auto"
862
+ },
863
+ children: queue.length === 0 ? /* @__PURE__ */ jsx("div", {
864
+ style: {
865
+ padding: "20px 12px",
866
+ textAlign: "center",
867
+ color: C.muted
868
+ },
869
+ children: "Queue empty"
870
+ }) : queue.map((item) => /* @__PURE__ */ jsxs("div", {
871
+ style: {
872
+ display: "flex",
873
+ alignItems: "center",
874
+ gap: 8,
875
+ padding: "7px 12px",
876
+ borderBottom: `1px solid ${C.border}`
877
+ },
878
+ children: [
879
+ /* @__PURE__ */ jsx("span", {
880
+ style: pill(queueStatusColor(item.status)),
881
+ children: item.status
882
+ }),
883
+ item.priority && item.priority !== "normal" && /* @__PURE__ */ jsx("span", {
884
+ style: pill(item.priority === "high" ? C.purple : C.muted),
885
+ children: item.priority
886
+ }),
887
+ /* @__PURE__ */ jsx("span", {
888
+ style: {
889
+ flex: 1,
890
+ overflow: "hidden",
891
+ textOverflow: "ellipsis",
892
+ whiteSpace: "nowrap",
893
+ color: C.text
894
+ },
895
+ children: item.actionName
896
+ }),
897
+ item.retryCount > 0 && /* @__PURE__ */ jsxs("span", {
898
+ style: {
899
+ color: C.muted,
900
+ fontSize: 10,
901
+ flexShrink: 0
902
+ },
903
+ children: [
904
+ "×",
905
+ item.retryCount,
906
+ "/",
907
+ item.maxRetries
908
+ ]
909
+ })
910
+ ]
911
+ }, item.id))
912
+ })]
913
+ });
580
914
  }
581
915
  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
- };
916
+ return /* @__PURE__ */ jsx("div", { children: resources.length === 0 ? /* @__PURE__ */ jsx("div", {
917
+ style: {
918
+ padding: "20px 12px",
919
+ textAlign: "center",
920
+ color: C.muted
921
+ },
922
+ children: "No resources registered"
923
+ }) : resources.map((res) => /* @__PURE__ */ jsxs("div", {
924
+ style: {
925
+ padding: "7px 12px",
926
+ borderBottom: `1px solid ${C.border}`
927
+ },
928
+ children: [
929
+ /* @__PURE__ */ jsxs("div", {
930
+ style: {
931
+ display: "flex",
932
+ alignItems: "center",
933
+ gap: 6,
934
+ marginBottom: 3
935
+ },
936
+ children: [/* @__PURE__ */ jsx("span", {
937
+ style: pill(resourceStatusColor(res.status)),
938
+ children: res.status
939
+ }), /* @__PURE__ */ jsx("span", {
940
+ style: {
941
+ color: C.muted,
942
+ fontSize: 10
943
+ },
944
+ children: res.strategy.name
945
+ })]
946
+ }),
947
+ /* @__PURE__ */ jsx("div", {
948
+ style: {
949
+ color: C.text,
950
+ overflow: "hidden",
951
+ textOverflow: "ellipsis",
952
+ whiteSpace: "nowrap",
953
+ fontSize: 10,
954
+ marginBottom: 2
955
+ },
956
+ children: res.url
957
+ }),
958
+ /* @__PURE__ */ jsxs("div", {
959
+ style: {
960
+ display: "flex",
961
+ gap: 10,
962
+ color: C.muted,
963
+ fontSize: 10
964
+ },
965
+ children: [
966
+ /* @__PURE__ */ jsxs("span", {
967
+ title: "Cache hits",
968
+ style: {
969
+ display: "inline-flex",
970
+ alignItems: "center",
971
+ gap: 3
972
+ },
973
+ children: [
974
+ /* @__PURE__ */ jsx(Icon, {
975
+ path: ICONS.arrowUp,
976
+ size: 10
977
+ }),
978
+ res.cacheHits,
979
+ " hit",
980
+ res.cacheHits !== 1 ? "s" : ""
981
+ ]
982
+ }),
983
+ /* @__PURE__ */ jsxs("span", {
984
+ title: "Cache misses",
985
+ style: {
986
+ display: "inline-flex",
987
+ alignItems: "center",
988
+ gap: 3
989
+ },
990
+ children: [
991
+ /* @__PURE__ */ jsx(Icon, {
992
+ path: ICONS.arrowDown,
993
+ size: 10
994
+ }),
995
+ res.cacheMisses,
996
+ " miss",
997
+ res.cacheMisses !== 1 ? "es" : ""
998
+ ]
999
+ }),
1000
+ res.cachedAt && /* @__PURE__ */ jsxs("span", {
1001
+ title: "Cached at",
1002
+ style: {
1003
+ display: "inline-flex",
1004
+ alignItems: "center",
1005
+ gap: 3
1006
+ },
1007
+ children: [/* @__PURE__ */ jsx(Icon, {
1008
+ path: ICONS.clock,
1009
+ size: 10
1010
+ }), new Date(res.cachedAt).toLocaleTimeString()]
1011
+ })
1012
+ ]
1013
+ })
1014
+ ]
1015
+ }, res.url)) });
1016
+ }
1017
+ //#endregion
1018
+ export { EidosDevtools };