@sweidos/eidos 1.2.0 → 2.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.
- package/README.md +62 -32
- package/dist/action.js +208 -152
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.js +247 -86
- package/dist/eidos-sw.js +8 -5
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +101 -35
- package/dist/index.js +44 -41
- package/dist/push.cjs +8 -5
- package/dist/push.js +8 -5
- package/dist/query.cjs +1 -1
- package/dist/query.d.ts +1 -2
- package/dist/query.js +1 -1
- package/dist/queue-storage.js.map +1 -0
- package/dist/queue-sync.js +34 -0
- package/dist/queue-sync.js.map +1 -0
- package/dist/react/Provider.js.map +1 -0
- package/dist/react/hooks.js +23 -23
- package/dist/react/hooks.js.map +1 -0
- package/dist/replay.js.map +1 -0
- package/dist/resource.js +121 -107
- package/dist/resource.js.map +1 -0
- package/dist/runtime.js +23 -22
- package/dist/runtime.js.map +1 -0
- package/dist/store-slices.js.map +1 -0
- package/dist/store.js.map +1 -0
- package/dist/stores.js +23 -31
- package/dist/stores.js.map +1 -0
- package/dist/sw-bridge.js.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -0
- package/package.json +2 -3
package/dist/push.cjs
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _sweidos_eidos = require("@sweidos/eidos");
|
|
3
|
+
//#region src/internal/url-base64.ts
|
|
4
|
+
/** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
|
|
5
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
6
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
7
|
+
const raw = atob(base64);
|
|
8
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
3
11
|
//#region src/push.ts
|
|
4
12
|
/**
|
|
5
13
|
* @sweidos/eidos/push
|
|
@@ -94,11 +102,6 @@ async function getCurrentPushSubscription() {
|
|
|
94
102
|
const subscription = await registration.pushManager.getSubscription();
|
|
95
103
|
return subscription ? subscription.toJSON() : null;
|
|
96
104
|
}
|
|
97
|
-
function urlBase64ToUint8Array(base64Url) {
|
|
98
|
-
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
99
|
-
const raw = atob(base64);
|
|
100
|
-
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
101
|
-
}
|
|
102
105
|
function uint8ArrayToUrlBase64(bytes) {
|
|
103
106
|
let binary = "";
|
|
104
107
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
package/dist/push.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { getSwRegistration, registerPushCallbacks, sendToWorker } from "@sweidos/eidos";
|
|
2
|
+
//#region src/internal/url-base64.ts
|
|
3
|
+
/** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
|
|
4
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
5
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
6
|
+
const raw = atob(base64);
|
|
7
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
2
10
|
//#region src/push.ts
|
|
3
11
|
/**
|
|
4
12
|
* @sweidos/eidos/push
|
|
@@ -93,11 +101,6 @@ async function getCurrentPushSubscription() {
|
|
|
93
101
|
const subscription = await registration.pushManager.getSubscription();
|
|
94
102
|
return subscription ? subscription.toJSON() : null;
|
|
95
103
|
}
|
|
96
|
-
function urlBase64ToUint8Array(base64Url) {
|
|
97
|
-
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
98
|
-
const raw = atob(base64);
|
|
99
|
-
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
100
|
-
}
|
|
101
104
|
function uint8ArrayToUrlBase64(bytes) {
|
|
102
105
|
let binary = "";
|
|
103
106
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
package/dist/query.cjs
CHANGED
|
@@ -118,7 +118,7 @@ function useEidosMutation(handle, options) {
|
|
|
118
118
|
if (invalidates?.length) {
|
|
119
119
|
await Promise.all(invalidates.map((h) => h.invalidate()));
|
|
120
120
|
if (!_globalClient && contextClient) invalidates.forEach((h) => {
|
|
121
|
-
contextClient.invalidateQueries({ queryKey: h.
|
|
121
|
+
contextClient.invalidateQueries({ queryKey: ["eidos", h.url] });
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
if (onSuccess) await onSuccess(...args);
|
package/dist/query.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UseQueryOptions, UseQueryResult, UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query';
|
|
2
|
-
import { ResourceHandle, ActionHandle, QueuedResult } from '@sweidos/eidos';
|
|
2
|
+
import { ResourceHandle, AnyResourceHandle, ActionHandle, QueuedResult } from '@sweidos/eidos';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Register a QueryClient with Eidos.
|
|
@@ -44,7 +44,6 @@ type EidosQueryOptions<T> = Omit<UseQueryOptions<T, Error, T, [string, string]>,
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
export declare function useEidosQuery<T>(handle: ResourceHandle<T>, options?: EidosQueryOptions<T>): UseQueryResult<T, Error>;
|
|
47
|
-
type AnyResourceHandle = ResourceHandle<any>;
|
|
48
47
|
export interface EidosMutationOptions<TArg, TData> extends Omit<UseMutationOptions<TData | QueuedResult, Error, TArg>, 'mutationFn' | 'networkMode'> {
|
|
49
48
|
/**
|
|
50
49
|
* Resource handles to invalidate (Cache Storage + TanStack Query) after
|
package/dist/query.js
CHANGED
|
@@ -117,7 +117,7 @@ function useEidosMutation(handle, options) {
|
|
|
117
117
|
if (invalidates?.length) {
|
|
118
118
|
await Promise.all(invalidates.map((h) => h.invalidate()));
|
|
119
119
|
if (!_globalClient && contextClient) invalidates.forEach((h) => {
|
|
120
|
-
contextClient.invalidateQueries({ queryKey: h.
|
|
120
|
+
contextClient.invalidateQueries({ queryKey: ["eidos", h.url] });
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
if (onSuccess) await onSuccess(...args);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-storage.js","names":[],"sources":["../src/queue-storage.ts"],"sourcesContent":["import type { ActionQueueItem } from './types';\n\nexport interface QueueStorage {\n add(item: ActionQueueItem): Promise<void>;\n getAll(): Promise<ActionQueueItem[]>;\n getPending(): Promise<ActionQueueItem[]>;\n update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;\n remove(id: string): Promise<void>;\n clear(): Promise<void>;\n}\n\nlet _storage: QueueStorage | null = null;\n\n/** Override the default IndexedDB queue with a custom storage backend (e.g. AsyncStorage for React Native). */\nexport function setQueueStorage(s: QueueStorage): void {\n _storage = s;\n}\n\nexport function _getQueueStorage(): QueueStorage | null {\n return _storage;\n}\n"],"mappings":"AAWA,IAAI,IAAgC;AAGpC,SAAgB,EAAgB,GAAuB;AACrD,EAAA,IAAW;AACb;AAEA,SAAgB,IAAwC;AACtD,SAAO;AACT"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEidosStore as o } from "./store.js";
|
|
2
|
+
var c = "eidos-queue-sync", a;
|
|
3
|
+
function r() {
|
|
4
|
+
return a !== void 0 || (a = typeof BroadcastChannel > "u" ? null : new BroadcastChannel(c)), a;
|
|
5
|
+
}
|
|
6
|
+
function i(e) {
|
|
7
|
+
r()?.postMessage(e);
|
|
8
|
+
}
|
|
9
|
+
function m() {
|
|
10
|
+
const e = r();
|
|
11
|
+
if (!e) return () => {
|
|
12
|
+
};
|
|
13
|
+
const s = (u) => {
|
|
14
|
+
const n = o.getState(), t = u.data;
|
|
15
|
+
switch (t.type) {
|
|
16
|
+
case "update":
|
|
17
|
+
n.updateQueueItem(t.id, t.update);
|
|
18
|
+
break;
|
|
19
|
+
case "batchUpdate":
|
|
20
|
+
n.batchUpdateQueueItems(t.updates);
|
|
21
|
+
break;
|
|
22
|
+
case "remove":
|
|
23
|
+
n.removeQueueItem(t.id);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return e.addEventListener("message", s), () => e.removeEventListener("message", s);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
i as broadcastQueueSync,
|
|
31
|
+
m as subscribeQueueSync
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=queue-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-sync.js","names":[],"sources":["../src/queue-sync.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport type { ActionQueueItem } from './types';\n\nconst CHANNEL_NAME = 'eidos-queue-sync';\n\ntype QueueSyncMessage =\n | { type: 'update'; id: string; update: Partial<ActionQueueItem> }\n | { type: 'batchUpdate'; updates: Array<{ id: string; update: Partial<ActionQueueItem> }> }\n | { type: 'remove'; id: string };\n\nlet _channel: BroadcastChannel | null | undefined;\n\nfunction getChannel(): BroadcastChannel | null {\n if (_channel !== undefined) return _channel;\n _channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL_NAME);\n return _channel;\n}\n\n/**\n * Broadcasts a queue-item status change to other tabs sharing the same\n * IndexedDB queue. The replay-lock holder (see `replayQueue` in action.ts)\n * is the only tab that mutates queue-item status, so non-leader tabs would\n * otherwise show stale status until their own store re-hydrates.\n *\n * No-ops in environments without BroadcastChannel (React Native, old Safari).\n */\nexport function broadcastQueueSync(message: QueueSyncMessage): void {\n getChannel()?.postMessage(message);\n}\n\n/**\n * Applies queue-item status updates broadcast by the replay-lock holder to\n * this tab's store. Returns an unsubscribe function.\n */\nexport function subscribeQueueSync(): () => void {\n const channel = getChannel();\n if (!channel) return () => {};\n\n const handler = (event: MessageEvent<QueueSyncMessage>) => {\n const store = useEidosStore.getState();\n const message = event.data;\n switch (message.type) {\n case 'update':\n store.updateQueueItem(message.id, message.update);\n break;\n case 'batchUpdate':\n store.batchUpdateQueueItems(message.updates);\n break;\n case 'remove':\n store.removeQueueItem(message.id);\n break;\n }\n };\n\n channel.addEventListener('message', handler);\n return () => channel.removeEventListener('message', handler);\n}\n\n/** Test-only: reset the cached channel so each test gets a fresh instance. */\nexport function _resetQueueSyncChannel(): void {\n _channel?.close();\n _channel = undefined;\n}\n"],"mappings":";AAGA,IAAM,IAAe,oBAOjB;AAEJ,SAAS,IAAsC;AAC7C,SAAI,MAAa,WACjB,IAAW,OAAO,mBAAqB,MAAc,OAAO,IAAI,iBAAiB,CAAY,IACtF;AACT;AAUA,SAAgB,EAAmB,GAAiC;AAClE,EAAA,EAAW,GAAG,YAAY,CAAO;AACnC;AAMA,SAAgB,IAAiC;AAC/C,QAAM,IAAU,EAAW;AAC3B,MAAI,CAAC,EAAS,QAAA,MAAa;AAAA,EAAC;AAE5B,QAAM,IAAA,CAAW,MAA0C;AACzD,UAAM,IAAQ,EAAc,SAAS,GAC/B,IAAU,EAAM;AACtB,YAAQ,EAAQ,MAAhB;AAAA,MACE,KAAK;AACH,QAAA,EAAM,gBAAgB,EAAQ,IAAI,EAAQ,MAAM;AAChD;AAAA,MACF,KAAK;AACH,QAAA,EAAM,sBAAsB,EAAQ,OAAO;AAC3C;AAAA,MACF,KAAK;AACH,QAAA,EAAM,gBAAgB,EAAQ,EAAE;AAChC;AAAA,IACJ;AAAA,EACF;AAEA,SAAA,EAAQ,iBAAiB,WAAW,CAAO,GAC3C,MAAa,EAAQ,oBAAoB,WAAW,CAAO;AAC7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Provider.js","names":[],"sources":["../../src/react/Provider.tsx"],"sourcesContent":["import { useEffect, type ReactNode } from 'react';\nimport { initEidos, type EidosConfig } from '../runtime';\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode;\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay });\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <>{children}</>;\n}\n"],"mappings":";;;AAgBA,SAAgB,EAAc,EAAE,UAAA,GAAU,QAAA,GAAQ,YAAA,EAAA,GAAkC;AAClF,SAAA,EAAA,MAAgB;AACd,IAAA,EAAU;AAAA,MAAE,QAAA;AAAA,MAAQ,YAAA;AAAA,IAAW,CAAC;AAAA,EAGlC,GAAG,CAAC,CAAC,GAEE,gBAAA,EAAA,GAAA,EAAG,UAAA,EAAW,CAAA;AACvB"}
|
package/dist/react/hooks.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import {
|
|
1
|
+
import { useEidosStore as o } from "../store.js";
|
|
2
|
+
import { countQueueByStatus as l } from "../types.js";
|
|
3
|
+
import { useEffect as i, useRef as c, useSyncExternalStore as p } from "react";
|
|
3
4
|
function u(e) {
|
|
4
5
|
const t = e ?? ((n) => n);
|
|
5
|
-
return
|
|
6
|
+
return p(o.subscribe, () => t(o.getState()));
|
|
6
7
|
}
|
|
7
|
-
function
|
|
8
|
+
function R() {
|
|
8
9
|
return u();
|
|
9
10
|
}
|
|
10
11
|
function q() {
|
|
11
12
|
return u((e) => e.resources);
|
|
12
13
|
}
|
|
13
|
-
function
|
|
14
|
+
function w(e) {
|
|
14
15
|
return u((t) => t.resources[e]);
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
+
function y() {
|
|
17
18
|
return u((e) => e.queue);
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
+
function $(e) {
|
|
20
21
|
return u((t) => t.queue.find((n) => n.id === e));
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
+
function O() {
|
|
23
24
|
return {
|
|
24
25
|
isOnline: u((e) => e.isOnline),
|
|
25
26
|
swStatus: u((e) => e.swStatus),
|
|
26
27
|
swError: u((e) => e.swError)
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
function
|
|
30
|
+
function Q() {
|
|
30
31
|
const [e, t, n, r] = u((s) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return `${o},${f},${c},${s.queue.length}`;
|
|
32
|
+
const { pending: f, failed: a, replaying: d, total: E } = l(s.queue);
|
|
33
|
+
return `${f},${a},${d},${E}`;
|
|
34
34
|
}).split(",");
|
|
35
35
|
return {
|
|
36
36
|
pending: +e,
|
|
@@ -39,23 +39,23 @@ function $() {
|
|
|
39
39
|
total: +r
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
function
|
|
43
|
-
const t = u((s) => s.queue.length), n =
|
|
44
|
-
|
|
42
|
+
function b(e) {
|
|
43
|
+
const t = u((s) => s.queue.length), n = c(0), r = c(e);
|
|
44
|
+
i(() => {
|
|
45
45
|
r.current = e;
|
|
46
|
-
}),
|
|
46
|
+
}), i(() => {
|
|
47
47
|
n.current > 0 && t === 0 && r.current(), n.current = t;
|
|
48
48
|
}, [t]);
|
|
49
49
|
}
|
|
50
50
|
export {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
R as useEidos,
|
|
52
|
+
$ as useEidosAction,
|
|
53
|
+
b as useEidosOnDrain,
|
|
54
|
+
y as useEidosQueue,
|
|
55
|
+
Q as useEidosQueueStats,
|
|
56
|
+
w as useEidosResource,
|
|
57
57
|
q as useEidosResources,
|
|
58
|
-
|
|
58
|
+
O as useEidosStatus
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../../src/react/hooks.ts"],"sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react';\nimport { useEidosStore } from '../store';\nimport type { EidosStore } from '../store';\nimport { countQueueByStatus } from '../types';\n\nfunction useStore(): EidosStore;\nfunction useStore<T>(selector: (state: EidosStore) => T): T;\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T);\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore();\n}\n\n/** All registered resources — only re-renders when the resources map changes, not on queue mutations. */\nexport function useEidosResources() {\n return useStore((s) => s.resources);\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url]);\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue);\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline);\n const swStatus = useStore((s) => s.swStatus);\n const swError = useStore((s) => s.swError);\n return { isOnline, swStatus, swError };\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n const { pending, failed, replaying, total } = countQueueByStatus(s.queue);\n return `${pending},${failed},${replaying},${total}`;\n });\n const [p, f, r, t] = encoded.split(',');\n return { pending: +p, failed: +f, replaying: +r, total: +t };\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length);\n const prevRef = useRef(0);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current();\n }\n prevRef.current = total;\n }, [total]);\n}\n"],"mappings":";;;AAOA,SAAS,EAAyB,GAAwC;AACxE,QAAM,IAAK,MAAA,CAAc,MAAkB;AAC3C,SAAO,EAAqB,EAAc,WAAA,MAAiB,EAAG,EAAc,SAAS,CAAC,CAAC;AACzF;AAGA,SAAgB,IAAW;AACzB,SAAO,EAAS;AAClB;AAGA,SAAgB,IAAoB;AAClC,SAAO,EAAA,CAAU,MAAM,EAAE,SAAS;AACpC;AAGA,SAAgB,EAAiB,GAAa;AAC5C,SAAO,EAAA,CAAU,MAAM,EAAE,UAAU,CAAA,CAAI;AACzC;AAGA,SAAgB,IAAgB;AAC9B,SAAO,EAAA,CAAU,MAAM,EAAE,KAAK;AAChC;AAOA,SAAgB,EAAe,GAAY;AACzC,SAAO,EAAA,CAAU,MAAM,EAAE,MAAM,KAAA,CAAM,MAAS,EAAK,OAAO,CAAE,CAAC;AAC/D;AAOA,SAAgB,IAAiB;AAI/B,SAAO;AAAA,IAAE,UAHQ,EAAA,CAAU,MAAM,EAAE,QAG1B;AAAA,IAAU,UAFF,EAAA,CAAU,MAAM,EAAE,QAEhB;AAAA,IAAU,SADb,EAAA,CAAU,MAAM,EAAE,OACL;AAAA,EAAQ;AACvC;AAOA,SAAgB,IAAqB;AAQnC,QAAM,CAAC,GAAG,GAAG,GAAG,CAAA,IAJA,EAAA,CAAU,MAAM;AAC9B,UAAM,EAAE,SAAA,GAAS,QAAA,GAAQ,WAAA,GAAW,OAAA,EAAA,IAAU,EAAmB,EAAE,KAAK;AACxE,WAAO,GAAG,CAAA,IAAW,CAAA,IAAU,CAAA,IAAa,CAAA;AAAA,EAC9C,CACqB,EAAQ,MAAM,GAAG;AACtC,SAAO;AAAA,IAAE,SAAS,CAAC;AAAA,IAAG,QAAQ,CAAC;AAAA,IAAG,WAAW,CAAC;AAAA,IAAG,OAAO,CAAC;AAAA,EAAE;AAC7D;AAUA,SAAgB,EAAgB,GAAsB;AACpD,QAAM,IAAQ,EAAA,CAAU,MAAM,EAAE,MAAM,MAAM,GACtC,IAAU,EAAO,CAAC,GAClB,IAAc,EAAO,CAAQ;AAEnC,EAAA,EAAA,MAAgB;AACd,IAAA,EAAY,UAAU;AAAA,EACxB,CAAC,GAED,EAAA,MAAgB;AACd,IAAI,EAAQ,UAAU,KAAK,MAAU,KACnC,EAAY,QAAQ,GAEtB,EAAQ,UAAU;AAAA,EACpB,GAAG,CAAC,CAAK,CAAC;AACZ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","names":[],"sources":["../src/replay.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { replayQueue } from './action';\n\n/**\n * Subscribe to online/offline transitions and trigger replayQueue() on\n * reconnect, plus replay any pending items left over from a previous session.\n *\n * Shared by the web (runtime.ts) and React Native (runtime-rn.ts) init paths.\n *\n * WHY subscribe to the store instead of window.addEventListener('online'):\n * setOfflineSimulation() updates the store directly but never fires a real\n * browser `online` event. Watching the store catches both:\n * • Real network reconnects (sw-bridge updates store on window.online)\n * • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n *\n * Returns an unsubscribe function.\n */\nexport function subscribeReplayOnReconnect(): () => void {\n let prevIsOnline = useEidosStore.getState().isOnline;\n\n const unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState();\n const justCameOnline = isOnline && !prevIsOnline;\n prevIsOnline = isOnline;\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600);\n }\n });\n\n // Replay any pending items that survived a reload/restart.\n // 'failed' items have already exhausted maxRetries and are never\n // re-replayed (see _doReplayQueue), so they don't count here.\n const store = useEidosStore.getState();\n const hasPending = store.queue.some((q) => q.status === 'pending');\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200);\n }\n\n return unsubscribe;\n}\n"],"mappings":";;AAiBA,SAAgB,IAAyC;AACvD,MAAI,IAAe,EAAc,SAAS,EAAE;AAE5C,QAAM,IAAc,EAAc,UAAA,MAAgB;AAChD,UAAM,EAAE,UAAA,EAAA,IAAa,EAAc,SAAS,GACtC,IAAiB,KAAY,CAAC;AACpC,IAAA,IAAe,GAEX,KAEF,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAKK,IAAQ,EAAc,SAAS,GAC/B,IAAa,EAAM,MAAM,KAAA,CAAM,MAAM,EAAE,WAAW,SAAS;AACjE,SAAI,EAAM,YAAY,KACpB,WAAW,GAAa,IAAI,GAGvB;AACT"}
|
package/dist/resource.js
CHANGED
|
@@ -1,149 +1,162 @@
|
|
|
1
1
|
import { useEidosStore as h } from "./store.js";
|
|
2
|
-
import { sendToWorker as
|
|
3
|
-
var
|
|
4
|
-
function
|
|
5
|
-
|
|
2
|
+
import { sendToWorker as p } from "./sw-bridge.js";
|
|
3
|
+
var d = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), g = null;
|
|
4
|
+
function _(e) {
|
|
5
|
+
g = e;
|
|
6
6
|
}
|
|
7
|
-
function
|
|
7
|
+
function f(e) {
|
|
8
8
|
return e.includes("*") || /:[^/]+/.test(e);
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function k(e) {
|
|
11
11
|
return "^" + e.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+").replace(/:[^/]+/g, "[^/]+") + "$";
|
|
12
12
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
function A(e, a) {
|
|
17
|
-
if (p.has(e)) return p.get(e);
|
|
18
|
-
const s = S(a), t = d(e) ? R(e) : void 0, c = {
|
|
13
|
+
function m(e, s) {
|
|
14
|
+
const a = b(s), t = f(e) ? k(e) : void 0, r = {
|
|
19
15
|
url: e,
|
|
20
|
-
config:
|
|
21
|
-
strategy:
|
|
16
|
+
config: s,
|
|
17
|
+
strategy: a,
|
|
22
18
|
status: "idle",
|
|
23
19
|
cacheHits: 0,
|
|
24
20
|
cacheMisses: 0
|
|
25
21
|
};
|
|
26
|
-
h.getState().registerResource(e,
|
|
22
|
+
return h.getState().registerResource(e, r), p({
|
|
27
23
|
type: "EIDOS_REGISTER_RESOURCE",
|
|
28
24
|
url: e,
|
|
29
|
-
strategy:
|
|
30
|
-
cacheName:
|
|
25
|
+
strategy: a.swStrategy,
|
|
26
|
+
cacheName: a.cacheName,
|
|
31
27
|
...t !== void 0 && { pattern: t }
|
|
32
|
-
})
|
|
33
|
-
|
|
28
|
+
}), {
|
|
29
|
+
strategy: a,
|
|
30
|
+
regexStr: t
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function y(e, s, a) {
|
|
34
|
+
return async () => {
|
|
35
|
+
p({
|
|
36
|
+
type: "EIDOS_CLEAR_CACHE",
|
|
37
|
+
url: e
|
|
38
|
+
});
|
|
39
|
+
const t = await caches.open(s.cacheName).catch(() => null);
|
|
40
|
+
if (t) {
|
|
41
|
+
const r = await t.keys(), n = a ? new RegExp(a) : null, c = e.startsWith("http");
|
|
42
|
+
await Promise.all(r.filter((i) => {
|
|
43
|
+
const o = i.url, u = new URL(o).pathname;
|
|
44
|
+
return n ? n.test(c ? o : u) : c ? o === e : o === e || u === e;
|
|
45
|
+
}).map((i) => t.delete(i)));
|
|
46
|
+
}
|
|
47
|
+
f(e) || h.getState().updateResource(e, {
|
|
48
|
+
status: "stale",
|
|
49
|
+
cachedAt: void 0,
|
|
50
|
+
lastEvent: "cache-cleared",
|
|
51
|
+
cacheHits: 0,
|
|
52
|
+
cacheMisses: 0
|
|
53
|
+
}), g?.(["eidos", e]);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function E(e) {
|
|
57
|
+
return () => {
|
|
58
|
+
d.delete(e), p({
|
|
59
|
+
type: "EIDOS_UNREGISTER_RESOURCE",
|
|
60
|
+
url: e
|
|
61
|
+
}), h.getState().unregisterResource(e);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function A(e, s) {
|
|
65
|
+
if (f(e)) throw new Error(`[eidos] resource('${e}') is a URL pattern — use resourcePattern('${e}', config) instead. Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`);
|
|
66
|
+
if (d.has(e)) return d.get(e);
|
|
67
|
+
const { strategy: a } = m(e, s), t = {
|
|
34
68
|
url: e,
|
|
35
|
-
config:
|
|
36
|
-
strategy:
|
|
69
|
+
config: s,
|
|
70
|
+
strategy: a,
|
|
37
71
|
fetch: async () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}), i.then((o) => o.clone());
|
|
44
|
-
},
|
|
45
|
-
json: async () => {
|
|
46
|
-
if (d(e)) throw w(e, "json");
|
|
47
|
-
return (await r.fetch()).json();
|
|
48
|
-
},
|
|
49
|
-
query: () => {
|
|
50
|
-
if (d(e)) throw w(e, "query");
|
|
51
|
-
return {
|
|
52
|
-
queryKey: ["eidos", e],
|
|
53
|
-
queryFn: () => r.json()
|
|
54
|
-
};
|
|
72
|
+
const r = l.get(e);
|
|
73
|
+
if (r) return r.then((c) => c.clone());
|
|
74
|
+
const n = S(e, s, a);
|
|
75
|
+
return l.set(e, n), n.finally(() => l.delete(e)).catch(() => {
|
|
76
|
+
}), n.then((c) => c.clone());
|
|
55
77
|
},
|
|
78
|
+
json: async () => (await t.fetch()).json(),
|
|
79
|
+
query: () => ({
|
|
80
|
+
queryKey: ["eidos", e],
|
|
81
|
+
queryFn: () => t.json()
|
|
82
|
+
}),
|
|
56
83
|
prefetch: async () => {
|
|
57
|
-
|
|
58
|
-
await r.fetch();
|
|
59
|
-
},
|
|
60
|
-
invalidate: async () => {
|
|
61
|
-
v({
|
|
62
|
-
type: "EIDOS_CLEAR_CACHE",
|
|
63
|
-
url: e
|
|
64
|
-
});
|
|
65
|
-
const n = await caches.open(s.cacheName).catch(() => null);
|
|
66
|
-
if (n) {
|
|
67
|
-
const i = await n.keys(), o = t ? new RegExp(t) : null, l = e.startsWith("http");
|
|
68
|
-
await Promise.all(i.filter((u) => {
|
|
69
|
-
const f = u.url, g = new URL(f).pathname;
|
|
70
|
-
return o ? o.test(l ? f : g) : l ? f === e : f === e || g === e;
|
|
71
|
-
}).map((u) => n.delete(u)));
|
|
72
|
-
}
|
|
73
|
-
d(e) || h.getState().updateResource(e, {
|
|
74
|
-
status: "stale",
|
|
75
|
-
cachedAt: void 0,
|
|
76
|
-
lastEvent: "cache-cleared",
|
|
77
|
-
cacheHits: 0,
|
|
78
|
-
cacheMisses: 0
|
|
79
|
-
}), E?.(["eidos", e]);
|
|
84
|
+
await t.fetch();
|
|
80
85
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
type: "EIDOS_UNREGISTER_RESOURCE",
|
|
84
|
-
url: e
|
|
85
|
-
}), h.getState().unregisterResource(e);
|
|
86
|
-
}
|
|
86
|
+
invalidate: y(e, a, void 0),
|
|
87
|
+
unregister: E(e)
|
|
87
88
|
};
|
|
88
|
-
return
|
|
89
|
+
return d.set(e, t), t;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
function $(e, s) {
|
|
92
|
+
if (!f(e)) throw new Error(`[eidos] resourcePattern('${e}') is not a URL pattern — use resource('${e}', config) instead.`);
|
|
93
|
+
if (d.has(e)) return d.get(e);
|
|
94
|
+
const { strategy: a, regexStr: t } = m(e, s), r = {
|
|
95
|
+
url: e,
|
|
96
|
+
config: s,
|
|
97
|
+
strategy: a,
|
|
98
|
+
invalidate: y(e, a, t),
|
|
99
|
+
unregister: E(e)
|
|
100
|
+
};
|
|
101
|
+
return d.set(e, r), r;
|
|
102
|
+
}
|
|
103
|
+
async function S(e, s, a) {
|
|
91
104
|
const t = h.getState();
|
|
92
105
|
t.updateResource(e, {
|
|
93
106
|
status: "fetching",
|
|
94
107
|
fetchedAt: Date.now()
|
|
95
108
|
});
|
|
96
|
-
const
|
|
109
|
+
const r = await caches.open(a.cacheName).catch(() => null);
|
|
97
110
|
try {
|
|
98
|
-
if (
|
|
99
|
-
const i =
|
|
100
|
-
if (i && !
|
|
111
|
+
if (a.swStrategy !== "network-first") {
|
|
112
|
+
const i = r ? await r.match(e).catch(() => null) : null, o = h.getState().resources[e], u = s.maxAge !== void 0 && o?.cachedAt !== void 0 && Date.now() - o.cachedAt > s.maxAge;
|
|
113
|
+
if (i && !u)
|
|
101
114
|
return t.updateResource(e, {
|
|
102
115
|
status: "fresh",
|
|
103
116
|
lastEvent: "cache-hit",
|
|
104
117
|
cacheHits: (o?.cacheHits ?? 0) + 1
|
|
105
|
-
}),
|
|
106
|
-
|
|
118
|
+
}), a.swStrategy === "stale-while-revalidate" && fetch(e, { signal: AbortSignal.timeout(5e3) }).then(async (w) => {
|
|
119
|
+
w.ok && r && (await r.put(e, w.clone()), h.getState().updateResource(e, {
|
|
107
120
|
cachedAt: Date.now(),
|
|
108
121
|
lastEvent: "cache-updated"
|
|
109
122
|
}));
|
|
110
123
|
}).catch(() => {
|
|
111
124
|
}), i;
|
|
112
|
-
const
|
|
113
|
-
t.updateResource(e, { cacheMisses: (
|
|
125
|
+
const R = h.getState().resources[e];
|
|
126
|
+
t.updateResource(e, { cacheMisses: (R?.cacheMisses ?? 0) + 1 });
|
|
114
127
|
}
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
return
|
|
128
|
+
const n = await fetch(e);
|
|
129
|
+
if (n.ok)
|
|
130
|
+
return r && await r.put(e, n.clone()), t.updateResource(e, {
|
|
118
131
|
status: "fresh",
|
|
119
132
|
cachedAt: Date.now(),
|
|
120
133
|
lastEvent: "cache-updated"
|
|
121
|
-
}),
|
|
122
|
-
t.updateResource(e, { status:
|
|
123
|
-
const
|
|
124
|
-
throw new Error(
|
|
125
|
-
} catch (
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
134
|
+
}), n;
|
|
135
|
+
t.updateResource(e, { status: n.status === 503 ? "offline" : "error" });
|
|
136
|
+
const c = n.headers.get("X-Eidos-Offline") === "true";
|
|
137
|
+
throw new Error(c ? `offline: no cached response for ${e}` : `${n.status} ${n.statusText}`);
|
|
138
|
+
} catch (n) {
|
|
139
|
+
const c = r ? await r.match(e).catch(() => null) : null;
|
|
140
|
+
if (c) {
|
|
128
141
|
const i = h.getState().resources[e];
|
|
129
142
|
return t.updateResource(e, {
|
|
130
143
|
status: "fresh",
|
|
131
144
|
lastEvent: "cache-hit",
|
|
132
145
|
cacheHits: (i?.cacheHits ?? 0) + 1
|
|
133
|
-
}),
|
|
146
|
+
}), c;
|
|
134
147
|
}
|
|
135
|
-
throw t.updateResource(e, { status: "error" }),
|
|
148
|
+
throw t.updateResource(e, { status: "error" }), n;
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
return e.offline ?
|
|
151
|
+
function b(e) {
|
|
152
|
+
const s = e.strategy;
|
|
153
|
+
return e.offline ? v(s ?? "stale-while-revalidate", e.cacheName, e.version) : v(s ?? "network-first", e.cacheName, e.version);
|
|
141
154
|
}
|
|
142
|
-
var
|
|
155
|
+
var x = {
|
|
143
156
|
"stale-while-revalidate": "StaleWhileRevalidate",
|
|
144
157
|
"cache-first": "CacheFirst",
|
|
145
158
|
"network-first": "NetworkFirst"
|
|
146
|
-
},
|
|
159
|
+
}, C = {
|
|
147
160
|
"stale-while-revalidate": {
|
|
148
161
|
reasoning: "offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.",
|
|
149
162
|
behavior: [
|
|
@@ -187,29 +200,30 @@ new NetworkFirst({
|
|
|
187
200
|
})`
|
|
188
201
|
}
|
|
189
202
|
};
|
|
190
|
-
function
|
|
191
|
-
const
|
|
203
|
+
function v(e, s, a) {
|
|
204
|
+
const t = C[e], r = s ?? "eidos-resources-v1";
|
|
192
205
|
return {
|
|
193
|
-
name:
|
|
206
|
+
name: x[e],
|
|
194
207
|
swStrategy: e,
|
|
195
|
-
cacheName: a
|
|
196
|
-
reasoning:
|
|
197
|
-
behavior:
|
|
208
|
+
cacheName: a !== void 0 ? `${r}-v${a}` : r,
|
|
209
|
+
reasoning: t.reasoning,
|
|
210
|
+
behavior: t.behavior,
|
|
198
211
|
equivalentCode: ""
|
|
199
212
|
};
|
|
200
213
|
}
|
|
201
|
-
async function
|
|
202
|
-
const
|
|
214
|
+
async function O(e) {
|
|
215
|
+
const s = await Promise.allSettled(e.map((t) => t.prefetch())), a = s.filter((t) => t.status === "rejected").map((t) => t.reason);
|
|
203
216
|
return {
|
|
204
|
-
warmed:
|
|
205
|
-
failed:
|
|
206
|
-
errors:
|
|
217
|
+
warmed: s.filter((t) => t.status === "fulfilled").length,
|
|
218
|
+
failed: a.length,
|
|
219
|
+
errors: a
|
|
207
220
|
};
|
|
208
221
|
}
|
|
209
222
|
export {
|
|
210
223
|
A as resource,
|
|
211
|
-
|
|
212
|
-
_ as
|
|
224
|
+
$ as resourcePattern,
|
|
225
|
+
_ as setQueryInvalidator,
|
|
226
|
+
O as warmCache
|
|
213
227
|
};
|
|
214
228
|
|
|
215
229
|
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","names":[],"sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n"],"mappings":"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"}
|