@sweidos/eidos 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -22
- package/dist/action.d.ts +22 -0
- package/dist/action.js +47 -47
- package/dist/action.js.map +1 -1
- package/dist/async-storage-adapter.d.ts +25 -0
- package/dist/debug.d.ts +46 -0
- package/dist/debug.js +43 -0
- package/dist/debug.js.map +1 -0
- package/dist/devtools.js +350 -21
- package/dist/eidos-sw.js +60 -19
- package/dist/eidos.cjs +5 -5
- package/dist/eidos.cjs.map +1 -1
- package/dist/idb.d.ts +10 -0
- package/dist/index.d.ts +20 -586
- package/dist/index.js +47 -41
- package/dist/internal/url-base64.d.ts +2 -0
- package/dist/query.d.ts +1 -2
- package/dist/queue-storage.d.ts +12 -0
- package/dist/queue-sync.d.ts +32 -0
- package/dist/react/Provider.d.ts +16 -0
- package/dist/react/ProviderRN.d.ts +0 -1
- package/dist/react/hooks.d.ts +51 -0
- package/dist/react/hooks.js +30 -27
- package/dist/react/hooks.js.map +1 -1
- package/dist/replay.d.ts +15 -0
- package/dist/resource.d.ts +32 -0
- package/dist/resource.js +80 -78
- package/dist/resource.js.map +1 -1
- package/dist/runtime-rn.d.ts +0 -1
- package/dist/runtime.d.ts +39 -0
- package/dist/runtime.js +32 -24
- package/dist/runtime.js.map +1 -1
- package/dist/store-slices.d.ts +26 -0
- package/dist/store-slices.js +31 -20
- package/dist/store-slices.js.map +1 -1
- package/dist/store.d.ts +15 -0
- package/dist/store.js +22 -19
- package/dist/store.js.map +1 -1
- package/dist/stores.d.ts +64 -0
- package/dist/stores.js +31 -22
- package/dist/stores.js.map +1 -1
- package/dist/sveltekit.d.ts +0 -1
- package/dist/sw-bridge.d.ts +24 -0
- package/dist/sw-bridge.js +69 -54
- package/dist/sw-bridge.js.map +1 -1
- package/dist/testing.cjs +3 -2
- package/dist/testing.d.ts +1 -2
- package/dist/testing.js +3 -2
- package/dist/types.d.ts +305 -0
- package/dist/types.js +19 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/vite.d.ts +0 -1
- package/package.json +9 -7
package/dist/devtools.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useCallback, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/types.ts
|
|
5
|
+
function emptyReliabilityStats() {
|
|
6
|
+
return {
|
|
7
|
+
queued: 0,
|
|
8
|
+
succeeded: 0,
|
|
9
|
+
failed: 0,
|
|
10
|
+
retried: 0,
|
|
11
|
+
conflicted: 0,
|
|
12
|
+
cancelled: 0
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Single pass over the queue — avoids separate .filter() calls per status. */
|
|
16
|
+
function countQueueByStatus(queue) {
|
|
17
|
+
let pending = 0, failed = 0, replaying = 0;
|
|
18
|
+
for (const q of queue) if (q.status === "pending") pending++;
|
|
19
|
+
else if (q.status === "failed") failed++;
|
|
20
|
+
else if (q.status === "replaying") replaying++;
|
|
21
|
+
return {
|
|
22
|
+
pending,
|
|
23
|
+
failed,
|
|
24
|
+
replaying,
|
|
25
|
+
total: queue.length
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
4
29
|
//#region src/store-slices.ts
|
|
5
30
|
function createResourceActions(set) {
|
|
6
31
|
return {
|
|
@@ -39,6 +64,15 @@ function createQueueActions(set) {
|
|
|
39
64
|
hydrateQueue: (items) => set(() => ({ queue: items }))
|
|
40
65
|
};
|
|
41
66
|
}
|
|
67
|
+
function createReliabilityActions(set) {
|
|
68
|
+
return {
|
|
69
|
+
recordReliabilityEvent: (event) => set((s) => ({ reliability: {
|
|
70
|
+
...s.reliability,
|
|
71
|
+
[event]: s.reliability[event] + 1
|
|
72
|
+
} })),
|
|
73
|
+
resetReliabilityStats: () => set(() => ({ reliability: emptyReliabilityStats() }))
|
|
74
|
+
};
|
|
75
|
+
}
|
|
42
76
|
//#endregion
|
|
43
77
|
//#region src/store.ts
|
|
44
78
|
var _state;
|
|
@@ -59,13 +93,15 @@ _state = {
|
|
|
59
93
|
swError: void 0,
|
|
60
94
|
resources: {},
|
|
61
95
|
queue: [],
|
|
96
|
+
reliability: emptyReliabilityStats(),
|
|
62
97
|
setOnline: (isOnline) => _set(() => ({ isOnline })),
|
|
63
98
|
setSwStatus: (swStatus, swError) => _set(() => ({
|
|
64
99
|
swStatus,
|
|
65
100
|
swError
|
|
66
101
|
})),
|
|
67
102
|
...createResourceActions(_set),
|
|
68
|
-
...createQueueActions(_set)
|
|
103
|
+
...createQueueActions(_set),
|
|
104
|
+
...createReliabilityActions(_set)
|
|
69
105
|
};
|
|
70
106
|
function _getState() {
|
|
71
107
|
return _state;
|
|
@@ -89,21 +125,6 @@ var useEidosStore = {
|
|
|
89
125
|
}
|
|
90
126
|
};
|
|
91
127
|
//#endregion
|
|
92
|
-
//#region src/types.ts
|
|
93
|
-
/** Single pass over the queue — avoids separate .filter() calls per status. */
|
|
94
|
-
function countQueueByStatus(queue) {
|
|
95
|
-
let pending = 0, failed = 0, replaying = 0;
|
|
96
|
-
for (const q of queue) if (q.status === "pending") pending++;
|
|
97
|
-
else if (q.status === "failed") failed++;
|
|
98
|
-
else if (q.status === "replaying") replaying++;
|
|
99
|
-
return {
|
|
100
|
-
pending,
|
|
101
|
-
failed,
|
|
102
|
-
replaying,
|
|
103
|
-
total: queue.length
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
//#endregion
|
|
107
128
|
//#region src/react/hooks.ts
|
|
108
129
|
function useStore(selector) {
|
|
109
130
|
const fn = selector ?? ((s) => s);
|
|
@@ -146,10 +167,29 @@ function useEidosQueueStats() {
|
|
|
146
167
|
total: +t
|
|
147
168
|
};
|
|
148
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Calls `callback` once each time the action queue drains from non-empty → 0.
|
|
172
|
+
* Stable callback reference not required — always calls the latest version.
|
|
173
|
+
* Use for "all offline actions synced!" toasts.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* useEidosOnDrain(() => toast.success('All offline actions synced!'))
|
|
177
|
+
*/
|
|
178
|
+
/**
|
|
179
|
+
* Cumulative, session-scoped `neverLose` queue outcome counters — opt-in
|
|
180
|
+
* reliability telemetry for dashboards/devtools. Re-renders only when a
|
|
181
|
+
* counter changes.
|
|
182
|
+
*/
|
|
183
|
+
function useEidosReliabilityStats() {
|
|
184
|
+
return useStore((s) => s.reliability);
|
|
185
|
+
}
|
|
149
186
|
//#endregion
|
|
150
187
|
//#region src/sw-bridge.ts
|
|
151
188
|
var _registration = null;
|
|
152
189
|
var _pendingMessages = [];
|
|
190
|
+
function getSwRegistration() {
|
|
191
|
+
return _registration;
|
|
192
|
+
}
|
|
153
193
|
function sendToWorker(message) {
|
|
154
194
|
const sw = _registration?.active;
|
|
155
195
|
if (sw) sw.postMessage(message);
|
|
@@ -162,6 +202,14 @@ function setOfflineSimulation(enabled) {
|
|
|
162
202
|
});
|
|
163
203
|
useEidosStore.getState().setOnline(!enabled);
|
|
164
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Tells the waiting service worker to activate immediately, then reloads the page.
|
|
207
|
+
* Only relevant when `skipWaiting: false` — call this after the user confirms
|
|
208
|
+
* a "reload to update" toast shown via `onUpdateAvailable`.
|
|
209
|
+
*/
|
|
210
|
+
function triggerSwUpdate() {
|
|
211
|
+
_registration?.waiting?.postMessage({ type: "EIDOS_SKIP_WAITING" });
|
|
212
|
+
}
|
|
165
213
|
//#endregion
|
|
166
214
|
//#region src/idb.ts
|
|
167
215
|
var DB_NAME = "eidos";
|
|
@@ -403,6 +451,7 @@ async function _markSucceeded(item, store) {
|
|
|
403
451
|
status: "succeeded",
|
|
404
452
|
completedAt
|
|
405
453
|
});
|
|
454
|
+
store.recordReliabilityEvent("succeeded");
|
|
406
455
|
broadcastQueueSync({
|
|
407
456
|
type: "update",
|
|
408
457
|
id: item.id,
|
|
@@ -453,6 +502,7 @@ async function _resolveConflict(item, store, err) {
|
|
|
453
502
|
}
|
|
454
503
|
if (resolution === "skip") {
|
|
455
504
|
store.removeQueueItem(item.id);
|
|
505
|
+
store.recordReliabilityEvent("conflicted");
|
|
456
506
|
broadcastQueueSync({
|
|
457
507
|
type: "remove",
|
|
458
508
|
id: item.id
|
|
@@ -480,6 +530,7 @@ async function _scheduleRetryOrFail(item, store, err) {
|
|
|
480
530
|
retryCount
|
|
481
531
|
};
|
|
482
532
|
store.updateQueueItem(item.id, update);
|
|
533
|
+
store.recordReliabilityEvent("failed");
|
|
483
534
|
broadcastQueueSync({
|
|
484
535
|
type: "update",
|
|
485
536
|
id: item.id,
|
|
@@ -499,6 +550,7 @@ async function _scheduleRetryOrFail(item, store, err) {
|
|
|
499
550
|
nextRetryAt: Date.now() + backoffMs(retryCount)
|
|
500
551
|
};
|
|
501
552
|
store.updateQueueItem(item.id, update);
|
|
553
|
+
store.recordReliabilityEvent("retried");
|
|
502
554
|
broadcastQueueSync({
|
|
503
555
|
type: "update",
|
|
504
556
|
id: item.id,
|
|
@@ -529,6 +581,7 @@ async function _replayItem(item, store) {
|
|
|
529
581
|
} catch (err) {
|
|
530
582
|
if (isAbortError(err)) {
|
|
531
583
|
store.removeQueueItem(item.id);
|
|
584
|
+
store.recordReliabilityEvent("cancelled");
|
|
532
585
|
broadcastQueueSync({
|
|
533
586
|
type: "remove",
|
|
534
587
|
id: item.id
|
|
@@ -709,7 +762,10 @@ var ICONS = {
|
|
|
709
762
|
arrowDown: "M12 5v14M19 12l-7 7-7-7",
|
|
710
763
|
clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2",
|
|
711
764
|
x: "M18 6 6 18M6 6l12 12",
|
|
712
|
-
rotateCcw: "M3 12a9 9 0 1 0 2.6-6.4M3 12V5m0 7h7"
|
|
765
|
+
rotateCcw: "M3 12a9 9 0 1 0 2.6-6.4M3 12V5m0 7h7",
|
|
766
|
+
activity: "M22 12h-4l-3 9L9 3l-3 9H2",
|
|
767
|
+
shield: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
|
|
768
|
+
refreshCw: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M3 21v-5h5"
|
|
713
769
|
};
|
|
714
770
|
function positionStyle(p) {
|
|
715
771
|
const base = {
|
|
@@ -746,6 +802,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
|
|
|
746
802
|
const { pending, failed, replaying } = useEidosQueueStats();
|
|
747
803
|
const resources = useEidosResources();
|
|
748
804
|
const resourceList = Object.values(resources);
|
|
805
|
+
const reliability = useEidosReliabilityStats();
|
|
749
806
|
const badgeCount = pending + failed + replaying;
|
|
750
807
|
const toggleOffline = useCallback(() => {
|
|
751
808
|
const next = !simOffline;
|
|
@@ -949,7 +1006,12 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
|
|
|
949
1006
|
flexShrink: 0,
|
|
950
1007
|
background: C.surface
|
|
951
1008
|
},
|
|
952
|
-
children: [
|
|
1009
|
+
children: [
|
|
1010
|
+
"queue",
|
|
1011
|
+
"cache",
|
|
1012
|
+
"reliability",
|
|
1013
|
+
"sw"
|
|
1014
|
+
].map((t) => /* @__PURE__ */ jsx("button", {
|
|
953
1015
|
role: "tab",
|
|
954
1016
|
"aria-selected": tab === t,
|
|
955
1017
|
onClick: () => setTab(t),
|
|
@@ -970,7 +1032,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
|
|
|
970
1032
|
letterSpacing: "0.05em",
|
|
971
1033
|
transition: "color 0.15s, border-color 0.15s"
|
|
972
1034
|
},
|
|
973
|
-
children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
|
|
1035
|
+
children: t === "queue" ? `Queue (${queue.length})` : t === "cache" ? `Cache (${resourceList.length})` : t === "reliability" ? "Reliability" : "SW"
|
|
974
1036
|
}, t))
|
|
975
1037
|
}),
|
|
976
1038
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -983,7 +1045,7 @@ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
|
|
|
983
1045
|
queue,
|
|
984
1046
|
onReplay: handleReplay,
|
|
985
1047
|
onClear: handleClear
|
|
986
|
-
}) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList })
|
|
1048
|
+
}) : tab === "cache" ? /* @__PURE__ */ jsx(CacheTab, { resources: resourceList }) : tab === "reliability" ? /* @__PURE__ */ jsx(ReliabilityTab, { stats: reliability }) : /* @__PURE__ */ jsx(SwTab, { resources: resourceList })
|
|
987
1049
|
})
|
|
988
1050
|
]
|
|
989
1051
|
}), toggleBtn]
|
|
@@ -1235,5 +1297,272 @@ function CacheTab({ resources }) {
|
|
|
1235
1297
|
]
|
|
1236
1298
|
}, res.url)) });
|
|
1237
1299
|
}
|
|
1300
|
+
function ReliabilityTab({ stats }) {
|
|
1301
|
+
const rows = [
|
|
1302
|
+
{
|
|
1303
|
+
label: "Queued",
|
|
1304
|
+
key: "queued",
|
|
1305
|
+
color: C.blue
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
label: "Succeeded",
|
|
1309
|
+
key: "succeeded",
|
|
1310
|
+
color: C.green
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
label: "Retried",
|
|
1314
|
+
key: "retried",
|
|
1315
|
+
color: C.yellow
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
label: "Failed",
|
|
1319
|
+
key: "failed",
|
|
1320
|
+
color: C.red
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
label: "Conflicted",
|
|
1324
|
+
key: "conflicted",
|
|
1325
|
+
color: C.purple
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
label: "Cancelled",
|
|
1329
|
+
key: "cancelled",
|
|
1330
|
+
color: C.muted
|
|
1331
|
+
}
|
|
1332
|
+
];
|
|
1333
|
+
const total = stats.queued;
|
|
1334
|
+
const successRate = total > 0 ? Math.round(stats.succeeded / total * 100) : null;
|
|
1335
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1336
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1337
|
+
style: {
|
|
1338
|
+
display: "flex",
|
|
1339
|
+
alignItems: "center",
|
|
1340
|
+
gap: 8,
|
|
1341
|
+
padding: "8px 12px",
|
|
1342
|
+
borderBottom: `1px solid ${C.border}`
|
|
1343
|
+
},
|
|
1344
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1345
|
+
style: {
|
|
1346
|
+
color: C.muted,
|
|
1347
|
+
display: "inline-flex"
|
|
1348
|
+
},
|
|
1349
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1350
|
+
path: ICONS.activity,
|
|
1351
|
+
size: 12
|
|
1352
|
+
})
|
|
1353
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1354
|
+
style: {
|
|
1355
|
+
color: C.muted,
|
|
1356
|
+
fontSize: 10
|
|
1357
|
+
},
|
|
1358
|
+
children: successRate === null ? "No queued actions yet this session" : `${successRate}% succeeded`
|
|
1359
|
+
})]
|
|
1360
|
+
}),
|
|
1361
|
+
rows.map(({ label, key, color }) => /* @__PURE__ */ jsxs("div", {
|
|
1362
|
+
style: {
|
|
1363
|
+
display: "flex",
|
|
1364
|
+
alignItems: "center",
|
|
1365
|
+
justifyContent: "space-between",
|
|
1366
|
+
padding: "7px 12px",
|
|
1367
|
+
borderBottom: `1px solid ${C.border}`
|
|
1368
|
+
},
|
|
1369
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1370
|
+
style: { color: C.text },
|
|
1371
|
+
children: label
|
|
1372
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1373
|
+
style: pill(color),
|
|
1374
|
+
children: stats[key]
|
|
1375
|
+
})]
|
|
1376
|
+
}, key)),
|
|
1377
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1378
|
+
style: {
|
|
1379
|
+
padding: "8px 12px",
|
|
1380
|
+
color: C.muted,
|
|
1381
|
+
fontSize: 10
|
|
1382
|
+
},
|
|
1383
|
+
children: [
|
|
1384
|
+
"Session-only counters — reset on reload. Wire up",
|
|
1385
|
+
" ",
|
|
1386
|
+
/* @__PURE__ */ jsx("code", {
|
|
1387
|
+
style: { color: C.cyan },
|
|
1388
|
+
children: "onReliabilityReport"
|
|
1389
|
+
}),
|
|
1390
|
+
" in",
|
|
1391
|
+
" ",
|
|
1392
|
+
/* @__PURE__ */ jsx("code", {
|
|
1393
|
+
style: { color: C.cyan },
|
|
1394
|
+
children: "initEidos()"
|
|
1395
|
+
}),
|
|
1396
|
+
" to forward these to analytics."
|
|
1397
|
+
]
|
|
1398
|
+
})
|
|
1399
|
+
] });
|
|
1400
|
+
}
|
|
1401
|
+
function readSwSnapshot() {
|
|
1402
|
+
const reg = getSwRegistration();
|
|
1403
|
+
if (!reg) return {
|
|
1404
|
+
activeUrl: null,
|
|
1405
|
+
hasWaiting: false,
|
|
1406
|
+
hasInstalling: false
|
|
1407
|
+
};
|
|
1408
|
+
return {
|
|
1409
|
+
activeUrl: reg.active?.scriptURL ?? null,
|
|
1410
|
+
hasWaiting: reg.waiting !== null,
|
|
1411
|
+
hasInstalling: reg.installing !== null
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function SwTab({ resources }) {
|
|
1415
|
+
const [snap, setSnap] = useState(readSwSnapshot);
|
|
1416
|
+
useEffect(() => {
|
|
1417
|
+
const id = setInterval(() => setSnap(readSwSnapshot()), 1e3);
|
|
1418
|
+
return () => clearInterval(id);
|
|
1419
|
+
}, []);
|
|
1420
|
+
const buckets = resources.reduce((acc, res) => {
|
|
1421
|
+
const name = res.strategy.cacheName;
|
|
1422
|
+
acc[name] = (acc[name] ?? 0) + 1;
|
|
1423
|
+
return acc;
|
|
1424
|
+
}, {});
|
|
1425
|
+
const bucketList = Object.entries(buckets);
|
|
1426
|
+
const swState = snap.hasInstalling ? "installing" : snap.hasWaiting ? "waiting" : snap.activeUrl ? "active" : "none";
|
|
1427
|
+
const stateColor = swState === "active" ? C.green : swState === "waiting" || swState === "installing" ? C.yellow : C.muted;
|
|
1428
|
+
const shortUrl = snap.activeUrl ? snap.activeUrl.replace(/^https?:\/\/[^/]+/, "") || snap.activeUrl : null;
|
|
1429
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1430
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1431
|
+
style: {
|
|
1432
|
+
display: "flex",
|
|
1433
|
+
alignItems: "center",
|
|
1434
|
+
gap: 8,
|
|
1435
|
+
padding: "8px 12px",
|
|
1436
|
+
borderBottom: `1px solid ${C.border}`
|
|
1437
|
+
},
|
|
1438
|
+
children: [
|
|
1439
|
+
/* @__PURE__ */ jsx("span", {
|
|
1440
|
+
style: {
|
|
1441
|
+
color: C.muted,
|
|
1442
|
+
display: "inline-flex"
|
|
1443
|
+
},
|
|
1444
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1445
|
+
path: ICONS.shield,
|
|
1446
|
+
size: 12
|
|
1447
|
+
})
|
|
1448
|
+
}),
|
|
1449
|
+
/* @__PURE__ */ jsx("span", {
|
|
1450
|
+
style: pill(stateColor),
|
|
1451
|
+
children: swState
|
|
1452
|
+
}),
|
|
1453
|
+
shortUrl !== null ? /* @__PURE__ */ jsx("span", {
|
|
1454
|
+
title: snap.activeUrl ?? void 0,
|
|
1455
|
+
style: {
|
|
1456
|
+
flex: 1,
|
|
1457
|
+
color: C.muted,
|
|
1458
|
+
fontSize: 10,
|
|
1459
|
+
overflow: "hidden",
|
|
1460
|
+
textOverflow: "ellipsis",
|
|
1461
|
+
whiteSpace: "nowrap"
|
|
1462
|
+
},
|
|
1463
|
+
children: shortUrl
|
|
1464
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1465
|
+
style: {
|
|
1466
|
+
flex: 1,
|
|
1467
|
+
color: C.muted,
|
|
1468
|
+
fontSize: 10
|
|
1469
|
+
},
|
|
1470
|
+
children: "No SW registered"
|
|
1471
|
+
}),
|
|
1472
|
+
/* @__PURE__ */ jsx("button", {
|
|
1473
|
+
onClick: () => setSnap(readSwSnapshot()),
|
|
1474
|
+
title: "Refresh SW state",
|
|
1475
|
+
"aria-label": "Refresh SW state",
|
|
1476
|
+
...withFocusRing(),
|
|
1477
|
+
style: {
|
|
1478
|
+
...btn("ghost"),
|
|
1479
|
+
padding: "2px 6px",
|
|
1480
|
+
minHeight: 20
|
|
1481
|
+
},
|
|
1482
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1483
|
+
path: ICONS.refreshCw,
|
|
1484
|
+
size: 10
|
|
1485
|
+
})
|
|
1486
|
+
})
|
|
1487
|
+
]
|
|
1488
|
+
}),
|
|
1489
|
+
snap.hasWaiting ? /* @__PURE__ */ jsxs("div", {
|
|
1490
|
+
style: {
|
|
1491
|
+
display: "flex",
|
|
1492
|
+
alignItems: "center",
|
|
1493
|
+
gap: 8,
|
|
1494
|
+
padding: "8px 12px",
|
|
1495
|
+
borderBottom: `1px solid ${C.border}`,
|
|
1496
|
+
background: `${C.yellow}11`
|
|
1497
|
+
},
|
|
1498
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1499
|
+
style: {
|
|
1500
|
+
color: C.yellow,
|
|
1501
|
+
fontSize: 10,
|
|
1502
|
+
flex: 1
|
|
1503
|
+
},
|
|
1504
|
+
children: "Update ready — new SW is waiting to activate."
|
|
1505
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1506
|
+
onClick: triggerSwUpdate,
|
|
1507
|
+
title: "Activate the waiting SW now",
|
|
1508
|
+
...withFocusRing(),
|
|
1509
|
+
style: {
|
|
1510
|
+
...btn("primary"),
|
|
1511
|
+
minHeight: 22,
|
|
1512
|
+
fontSize: 10
|
|
1513
|
+
},
|
|
1514
|
+
children: "Force update"
|
|
1515
|
+
})]
|
|
1516
|
+
}) : null,
|
|
1517
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1518
|
+
style: {
|
|
1519
|
+
padding: "6px 12px",
|
|
1520
|
+
color: C.muted,
|
|
1521
|
+
fontSize: 10,
|
|
1522
|
+
borderBottom: `1px solid ${C.border}`
|
|
1523
|
+
},
|
|
1524
|
+
children: [
|
|
1525
|
+
"Cache buckets (",
|
|
1526
|
+
bucketList.length,
|
|
1527
|
+
")"
|
|
1528
|
+
]
|
|
1529
|
+
}),
|
|
1530
|
+
bucketList.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
1531
|
+
style: {
|
|
1532
|
+
padding: "16px 12px",
|
|
1533
|
+
textAlign: "center",
|
|
1534
|
+
color: C.muted,
|
|
1535
|
+
fontSize: 10
|
|
1536
|
+
},
|
|
1537
|
+
children: "No resources registered"
|
|
1538
|
+
}) : bucketList.map(([name, count]) => /* @__PURE__ */ jsxs("div", {
|
|
1539
|
+
style: {
|
|
1540
|
+
display: "flex",
|
|
1541
|
+
alignItems: "center",
|
|
1542
|
+
justifyContent: "space-between",
|
|
1543
|
+
padding: "7px 12px",
|
|
1544
|
+
borderBottom: `1px solid ${C.border}`
|
|
1545
|
+
},
|
|
1546
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1547
|
+
style: {
|
|
1548
|
+
color: C.text,
|
|
1549
|
+
fontSize: 10,
|
|
1550
|
+
overflow: "hidden",
|
|
1551
|
+
textOverflow: "ellipsis",
|
|
1552
|
+
whiteSpace: "nowrap",
|
|
1553
|
+
flex: 1
|
|
1554
|
+
},
|
|
1555
|
+
children: name
|
|
1556
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1557
|
+
style: pill(C.blue),
|
|
1558
|
+
children: [
|
|
1559
|
+
count,
|
|
1560
|
+
" resource",
|
|
1561
|
+
count !== 1 ? "s" : ""
|
|
1562
|
+
]
|
|
1563
|
+
})]
|
|
1564
|
+
}, name))
|
|
1565
|
+
] });
|
|
1566
|
+
}
|
|
1238
1567
|
//#endregion
|
|
1239
1568
|
export { EidosDevtools };
|
package/dist/eidos-sw.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region src/internal/url-base64.ts
|
|
2
2
|
/** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
|
|
3
3
|
function urlBase64ToUint8Array(base64Url) {
|
|
4
4
|
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -13,9 +13,7 @@ var runtimeConfig = {
|
|
|
13
13
|
resources: /* @__PURE__ */ new Map(),
|
|
14
14
|
simulateOffline: false
|
|
15
15
|
};
|
|
16
|
-
self.addEventListener("install", (
|
|
17
|
-
event.waitUntil(self.skipWaiting());
|
|
18
|
-
});
|
|
16
|
+
self.addEventListener("install", () => {});
|
|
19
17
|
self.addEventListener("activate", (event) => {
|
|
20
18
|
event.waitUntil(Promise.all([self.clients.claim(), caches.keys().then((keys) => Promise.all(keys.filter((k) => k.startsWith(CACHE_PREFIX) && !k.endsWith(CACHE_VERSION)).map((k) => caches.delete(k))))]));
|
|
21
19
|
});
|
|
@@ -29,7 +27,9 @@ self.addEventListener("message", (event) => {
|
|
|
29
27
|
runtimeConfig.resources.set(url, {
|
|
30
28
|
strategy: data.strategy,
|
|
31
29
|
cacheName: data.cacheName ?? `${CACHE_PREFIX}-resources-${CACHE_VERSION}`,
|
|
32
|
-
...patternSrc !== void 0 && { pattern: new RegExp(patternSrc) }
|
|
30
|
+
...patternSrc !== void 0 && { pattern: new RegExp(patternSrc) },
|
|
31
|
+
...data.maxAge !== void 0 && { maxAge: data.maxAge },
|
|
32
|
+
...data.maxEntries !== void 0 && { maxEntries: data.maxEntries }
|
|
33
33
|
});
|
|
34
34
|
event.source?.postMessage({
|
|
35
35
|
type: "EIDOS_RESOURCE_REGISTERED",
|
|
@@ -65,6 +65,9 @@ self.addEventListener("message", (event) => {
|
|
|
65
65
|
});
|
|
66
66
|
break;
|
|
67
67
|
}
|
|
68
|
+
case "EIDOS_SKIP_WAITING":
|
|
69
|
+
self.skipWaiting();
|
|
70
|
+
break;
|
|
68
71
|
case "EIDOS_PING":
|
|
69
72
|
event.source?.postMessage({ type: "EIDOS_PONG" });
|
|
70
73
|
break;
|
|
@@ -88,7 +91,7 @@ self.addEventListener("fetch", (event) => {
|
|
|
88
91
|
}
|
|
89
92
|
if (!reg) return;
|
|
90
93
|
if (reg.strategy === "stale-while-revalidate" && !runtimeConfig.simulateOffline) {
|
|
91
|
-
event.respondWith(staleWhileRevalidate(event, event.request, pathname, reg
|
|
94
|
+
event.respondWith(staleWhileRevalidate(event, event.request, pathname, reg));
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
97
|
event.respondWith(handleFetch(event.request, pathname, reg));
|
|
@@ -96,16 +99,45 @@ self.addEventListener("fetch", (event) => {
|
|
|
96
99
|
async function handleFetch(request, pathname, reg) {
|
|
97
100
|
if (runtimeConfig.simulateOffline) return serveOffline(request, pathname, reg.cacheName);
|
|
98
101
|
switch (reg.strategy) {
|
|
99
|
-
case "cache-first": return cacheFirst(request, pathname, reg
|
|
100
|
-
case "stale-while-revalidate": return staleWhileRevalidate(null, request, pathname, reg
|
|
101
|
-
case "network-first": return networkFirst(request, pathname, reg
|
|
102
|
+
case "cache-first": return cacheFirst(request, pathname, reg);
|
|
103
|
+
case "stale-while-revalidate": return staleWhileRevalidate(null, request, pathname, reg);
|
|
104
|
+
case "network-first": return networkFirst(request, pathname, reg);
|
|
102
105
|
default: return fetch(request);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
|
-
|
|
108
|
+
var CACHED_AT_HEADER = "X-Eidos-Cached-At";
|
|
109
|
+
/**
|
|
110
|
+
* Puts a response into cache with a `X-Eidos-Cached-At` timestamp header so
|
|
111
|
+
* the SW can enforce `maxAge` on subsequent cache hits.
|
|
112
|
+
* Caller must pass a clone of the response — `response.body` is consumed here.
|
|
113
|
+
*/
|
|
114
|
+
async function putCached(cache, request, response) {
|
|
115
|
+
const headers = new Headers(response.headers);
|
|
116
|
+
headers.set(CACHED_AT_HEADER, String(Date.now()));
|
|
117
|
+
await cache.put(request, new Response(response.body, {
|
|
118
|
+
status: response.status,
|
|
119
|
+
statusText: response.statusText,
|
|
120
|
+
headers
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
/** Returns true if the cached response has exceeded `maxAge`. */
|
|
124
|
+
function isExpired(cached, maxAge) {
|
|
125
|
+
if (maxAge === void 0) return false;
|
|
126
|
+
const cachedAt = Number(cached.headers.get(CACHED_AT_HEADER) ?? "0");
|
|
127
|
+
return cachedAt > 0 && Date.now() - cachedAt > maxAge;
|
|
128
|
+
}
|
|
129
|
+
/** Evicts the oldest (first-inserted) entries when the cache exceeds `maxEntries`. */
|
|
130
|
+
async function evictIfNeeded(cache, maxEntries) {
|
|
131
|
+
if (maxEntries === void 0) return;
|
|
132
|
+
const keys = await cache.keys();
|
|
133
|
+
const overflow = keys.length - maxEntries;
|
|
134
|
+
if (overflow > 0) await Promise.all(keys.slice(0, overflow).map((k) => cache.delete(k)));
|
|
135
|
+
}
|
|
136
|
+
async function cacheFirst(request, pathname, reg) {
|
|
137
|
+
const { cacheName, maxAge, maxEntries } = reg;
|
|
106
138
|
const cache = await caches.open(cacheName);
|
|
107
139
|
const cached = await cache.match(request);
|
|
108
|
-
if (cached) {
|
|
140
|
+
if (cached && !isExpired(cached, maxAge)) {
|
|
109
141
|
notifyClients({
|
|
110
142
|
type: "EIDOS_CACHE_HIT",
|
|
111
143
|
url: pathname,
|
|
@@ -113,10 +145,12 @@ async function cacheFirst(request, pathname, cacheName) {
|
|
|
113
145
|
});
|
|
114
146
|
return cached;
|
|
115
147
|
}
|
|
148
|
+
if (cached) await cache.delete(request);
|
|
116
149
|
try {
|
|
117
150
|
const response = await fetch(request);
|
|
118
151
|
if (response.ok) {
|
|
119
|
-
await cache
|
|
152
|
+
await putCached(cache, request, response.clone());
|
|
153
|
+
await evictIfNeeded(cache, maxEntries);
|
|
120
154
|
notifyClients({
|
|
121
155
|
type: "EIDOS_CACHE_UPDATED",
|
|
122
156
|
url: pathname,
|
|
@@ -132,12 +166,17 @@ async function cacheFirst(request, pathname, cacheName) {
|
|
|
132
166
|
return offlineErrorResponse(pathname);
|
|
133
167
|
}
|
|
134
168
|
}
|
|
135
|
-
async function staleWhileRevalidate(event, request, pathname,
|
|
169
|
+
async function staleWhileRevalidate(event, request, pathname, reg) {
|
|
170
|
+
const { cacheName, maxAge, maxEntries } = reg;
|
|
136
171
|
const cache = await caches.open(cacheName);
|
|
137
172
|
const cached = await cache.match(request);
|
|
173
|
+
const expired = cached ? isExpired(cached, maxAge) : false;
|
|
174
|
+
if (expired) await cache.delete(request);
|
|
175
|
+
const effectiveCached = expired ? null : cached;
|
|
138
176
|
const revalidatePromise = fetch(request).then(async (response) => {
|
|
139
177
|
if (response.ok) {
|
|
140
|
-
await cache
|
|
178
|
+
await putCached(cache, request, response.clone());
|
|
179
|
+
await evictIfNeeded(cache, maxEntries);
|
|
141
180
|
notifyClients({
|
|
142
181
|
type: "EIDOS_CACHE_UPDATED",
|
|
143
182
|
url: pathname,
|
|
@@ -152,23 +191,25 @@ async function staleWhileRevalidate(event, request, pathname, cacheName) {
|
|
|
152
191
|
strategy: "stale-while-revalidate"
|
|
153
192
|
});
|
|
154
193
|
});
|
|
155
|
-
if (
|
|
194
|
+
if (effectiveCached) {
|
|
156
195
|
event?.waitUntil(revalidatePromise);
|
|
157
196
|
notifyClients({
|
|
158
197
|
type: "EIDOS_CACHE_HIT",
|
|
159
198
|
url: pathname,
|
|
160
199
|
strategy: "stale-while-revalidate"
|
|
161
200
|
});
|
|
162
|
-
return
|
|
201
|
+
return effectiveCached;
|
|
163
202
|
}
|
|
164
203
|
return await revalidatePromise ?? offlineErrorResponse(pathname);
|
|
165
204
|
}
|
|
166
|
-
async function networkFirst(request, pathname,
|
|
205
|
+
async function networkFirst(request, pathname, reg) {
|
|
206
|
+
const { cacheName, maxAge, maxEntries } = reg;
|
|
167
207
|
const cache = await caches.open(cacheName);
|
|
168
208
|
try {
|
|
169
209
|
const response = await fetch(request, { signal: AbortSignal.timeout(3e3) });
|
|
170
210
|
if (response.ok) {
|
|
171
|
-
await cache
|
|
211
|
+
await putCached(cache, request, response.clone());
|
|
212
|
+
await evictIfNeeded(cache, maxEntries);
|
|
172
213
|
notifyClients({
|
|
173
214
|
type: "EIDOS_CACHE_UPDATED",
|
|
174
215
|
url: pathname,
|
|
@@ -178,7 +219,7 @@ async function networkFirst(request, pathname, cacheName) {
|
|
|
178
219
|
return response;
|
|
179
220
|
} catch {
|
|
180
221
|
const cached = await cache.match(request);
|
|
181
|
-
if (cached) {
|
|
222
|
+
if (cached && !isExpired(cached, maxAge)) {
|
|
182
223
|
notifyClients({
|
|
183
224
|
type: "EIDOS_CACHE_HIT",
|
|
184
225
|
url: pathname,
|