@sweidos/eidos 1.0.25 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -13
- package/dist/action.js +111 -76
- package/dist/action.js.map +1 -1
- package/dist/async-storage-adapter.js +42 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +627 -0
- package/dist/eidos.cjs.js +2 -2
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/idb.js.map +1 -1
- package/dist/index.d.ts +103 -7
- package/dist/index.js +41 -34
- package/dist/index.js.map +1 -1
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +12 -0
- package/dist/queue-storage.js +12 -0
- package/dist/queue-storage.js.map +1 -0
- package/dist/react/Devtools.d.ts +7 -0
- package/dist/react/ProviderRN.d.ts +23 -0
- package/dist/react/hooks.js +17 -13
- package/dist/react/hooks.js.map +1 -1
- package/dist/react-native.d.ts +8 -0
- package/dist/react-native.js +59 -0
- package/dist/resource.js +65 -55
- package/dist/resource.js.map +1 -1
- package/dist/runtime-rn.d.ts +18 -0
- package/dist/runtime.js +12 -12
- package/dist/runtime.js.map +1 -1
- package/dist/store.js +27 -17
- package/dist/store.js.map +1 -1
- package/dist/stores.js +34 -21
- package/dist/stores.js.map +1 -1
- package/dist/sveltekit.d.ts +19 -0
- package/dist/sveltekit.js +9 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +18 -2
package/dist/resource.js
CHANGED
|
@@ -5,24 +5,24 @@ let m = null;
|
|
|
5
5
|
function N(e) {
|
|
6
6
|
m = e;
|
|
7
7
|
}
|
|
8
|
-
function
|
|
8
|
+
function l(e) {
|
|
9
9
|
return e.includes("*") || /:[^/]+/.test(e);
|
|
10
10
|
}
|
|
11
11
|
function k(e) {
|
|
12
12
|
return "^" + e.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+").replace(/:[^/]+/g, "[^/]+") + "$";
|
|
13
13
|
}
|
|
14
|
-
function w(e,
|
|
14
|
+
function w(e, a) {
|
|
15
15
|
return new Error(
|
|
16
|
-
`[eidos] resource('${e}') is a URL pattern — ${
|
|
16
|
+
`[eidos] resource('${e}') is a URL pattern — ${a}() is not supported on pattern handles. The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
|
-
function q(e,
|
|
19
|
+
function q(e, a) {
|
|
20
20
|
if (p.has(e))
|
|
21
21
|
return p.get(e);
|
|
22
|
-
const
|
|
22
|
+
const s = E(e, a), t = l(e) ? k(e) : void 0, o = {
|
|
23
23
|
url: e,
|
|
24
|
-
config:
|
|
25
|
-
strategy:
|
|
24
|
+
config: a,
|
|
25
|
+
strategy: s,
|
|
26
26
|
status: "idle",
|
|
27
27
|
cacheHits: 0,
|
|
28
28
|
cacheMisses: 0
|
|
@@ -30,49 +30,50 @@ function q(e, t) {
|
|
|
30
30
|
h.getState().registerResource(e, o), g({
|
|
31
31
|
type: "EIDOS_REGISTER_RESOURCE",
|
|
32
32
|
url: e,
|
|
33
|
-
strategy:
|
|
34
|
-
cacheName:
|
|
35
|
-
...
|
|
33
|
+
strategy: s.swStrategy,
|
|
34
|
+
cacheName: s.cacheName,
|
|
35
|
+
...t !== void 0 && { pattern: t }
|
|
36
36
|
});
|
|
37
|
-
const
|
|
37
|
+
const c = {
|
|
38
38
|
url: e,
|
|
39
|
-
config:
|
|
40
|
-
strategy:
|
|
39
|
+
config: a,
|
|
40
|
+
strategy: s,
|
|
41
41
|
fetch: async () => {
|
|
42
|
-
if (
|
|
42
|
+
if (l(e)) throw w(e, "fetch");
|
|
43
43
|
const r = y.get(e);
|
|
44
|
-
if (r) return r.then((
|
|
45
|
-
const i =
|
|
46
|
-
return y.set(e, i), i.finally(() => y.delete(e))
|
|
44
|
+
if (r) return r.then((n) => n.clone());
|
|
45
|
+
const i = S(e, a, s);
|
|
46
|
+
return y.set(e, i), i.finally(() => y.delete(e)).catch(() => {
|
|
47
|
+
}), i.then((n) => n.clone());
|
|
47
48
|
},
|
|
48
49
|
json: async () => {
|
|
49
|
-
if (
|
|
50
|
-
return (await
|
|
50
|
+
if (l(e)) throw w(e, "json");
|
|
51
|
+
return (await c.fetch()).json();
|
|
51
52
|
},
|
|
52
53
|
query: () => {
|
|
53
|
-
if (
|
|
54
|
+
if (l(e)) throw w(e, "query");
|
|
54
55
|
return {
|
|
55
56
|
queryKey: ["eidos", e],
|
|
56
|
-
queryFn: () =>
|
|
57
|
+
queryFn: () => c.json()
|
|
57
58
|
};
|
|
58
59
|
},
|
|
59
60
|
prefetch: async () => {
|
|
60
|
-
if (
|
|
61
|
-
await
|
|
61
|
+
if (l(e)) throw w(e, "prefetch");
|
|
62
|
+
await c.fetch();
|
|
62
63
|
},
|
|
63
64
|
invalidate: async () => {
|
|
64
65
|
g({ type: "EIDOS_CLEAR_CACHE", url: e });
|
|
65
|
-
const r = await caches.open(
|
|
66
|
+
const r = await caches.open(s.cacheName).catch(() => null);
|
|
66
67
|
if (r) {
|
|
67
|
-
const i = await r.keys(),
|
|
68
|
+
const i = await r.keys(), n = t ? new RegExp(t) : null, u = e.startsWith("http");
|
|
68
69
|
await Promise.all(
|
|
69
70
|
i.filter((f) => {
|
|
70
71
|
const d = f.url, v = new URL(d).pathname;
|
|
71
|
-
return
|
|
72
|
+
return n ? n.test(u ? d : v) : u ? d === e : d === e || v === e;
|
|
72
73
|
}).map((f) => r.delete(f))
|
|
73
74
|
);
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
+
l(e) || h.getState().updateResource(e, {
|
|
76
77
|
status: "stale",
|
|
77
78
|
cachedAt: void 0,
|
|
78
79
|
lastEvent: "cache-cleared",
|
|
@@ -84,21 +85,21 @@ function q(e, t) {
|
|
|
84
85
|
p.delete(e), g({ type: "EIDOS_UNREGISTER_RESOURCE", url: e }), h.getState().unregisterResource(e);
|
|
85
86
|
}
|
|
86
87
|
};
|
|
87
|
-
return p.set(e,
|
|
88
|
+
return p.set(e, c), c;
|
|
88
89
|
}
|
|
89
|
-
async function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const o = await caches.open(
|
|
90
|
+
async function S(e, a, s) {
|
|
91
|
+
const t = h.getState();
|
|
92
|
+
t.updateResource(e, { status: "fetching", fetchedAt: Date.now() });
|
|
93
|
+
const o = await caches.open(s.cacheName).catch(() => null);
|
|
93
94
|
try {
|
|
94
|
-
if (
|
|
95
|
-
const i = o ? await o.match(e).catch(() => null) : null,
|
|
96
|
-
if (i && !
|
|
97
|
-
return
|
|
95
|
+
if (s.swStrategy !== "network-first") {
|
|
96
|
+
const i = o ? await o.match(e).catch(() => null) : null, n = h.getState().resources[e], u = a.maxAge !== void 0 && (n == null ? void 0 : n.cachedAt) !== void 0 && Date.now() - n.cachedAt > a.maxAge;
|
|
97
|
+
if (i && !u)
|
|
98
|
+
return t.updateResource(e, {
|
|
98
99
|
status: "fresh",
|
|
99
100
|
lastEvent: "cache-hit",
|
|
100
|
-
cacheHits: ((
|
|
101
|
-
}),
|
|
101
|
+
cacheHits: ((n == null ? void 0 : n.cacheHits) ?? 0) + 1
|
|
102
|
+
}), s.swStrategy === "stale-while-revalidate" && fetch(e, { signal: AbortSignal.timeout(5e3) }).then(async (d) => {
|
|
102
103
|
d.ok && o && (await o.put(e, d.clone()), h.getState().updateResource(e, {
|
|
103
104
|
cachedAt: Date.now(),
|
|
104
105
|
lastEvent: "cache-updated"
|
|
@@ -106,38 +107,38 @@ async function E(e, t, a) {
|
|
|
106
107
|
}).catch(() => {
|
|
107
108
|
}), i;
|
|
108
109
|
const f = h.getState().resources[e];
|
|
109
|
-
|
|
110
|
+
t.updateResource(e, {
|
|
110
111
|
cacheMisses: ((f == null ? void 0 : f.cacheMisses) ?? 0) + 1
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
return o && await o.put(e,
|
|
114
|
+
const c = await fetch(e);
|
|
115
|
+
if (c.ok)
|
|
116
|
+
return o && await o.put(e, c.clone()), t.updateResource(e, {
|
|
116
117
|
status: "fresh",
|
|
117
118
|
cachedAt: Date.now(),
|
|
118
119
|
lastEvent: "cache-updated"
|
|
119
|
-
}),
|
|
120
|
-
|
|
121
|
-
const r =
|
|
120
|
+
}), c;
|
|
121
|
+
t.updateResource(e, { status: c.status === 503 ? "offline" : "error" });
|
|
122
|
+
const r = c.headers.get("X-Eidos-Offline") === "true";
|
|
122
123
|
throw new Error(
|
|
123
|
-
r ? `offline: no cached response for ${e}` : `${
|
|
124
|
+
r ? `offline: no cached response for ${e}` : `${c.status} ${c.statusText}`
|
|
124
125
|
);
|
|
125
|
-
} catch (
|
|
126
|
+
} catch (c) {
|
|
126
127
|
const r = o ? await o.match(e).catch(() => null) : null;
|
|
127
128
|
if (r) {
|
|
128
129
|
const i = h.getState().resources[e];
|
|
129
|
-
return
|
|
130
|
+
return t.updateResource(e, {
|
|
130
131
|
status: "fresh",
|
|
131
132
|
lastEvent: "cache-hit",
|
|
132
133
|
cacheHits: ((i == null ? void 0 : i.cacheHits) ?? 0) + 1
|
|
133
134
|
}), r;
|
|
134
135
|
}
|
|
135
|
-
throw
|
|
136
|
+
throw t.updateResource(e, { status: "error" }), c;
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
return
|
|
139
|
+
function E(e, a) {
|
|
140
|
+
const s = a.strategy;
|
|
141
|
+
return a.offline ? R(s ?? "stale-while-revalidate", e, a.cacheName) : R(s ?? "network-first", e, a.cacheName);
|
|
141
142
|
}
|
|
142
143
|
const x = {
|
|
143
144
|
"stale-while-revalidate": {
|
|
@@ -186,15 +187,24 @@ new NetworkFirst({
|
|
|
186
187
|
})`
|
|
187
188
|
}
|
|
188
189
|
};
|
|
189
|
-
function R(e,
|
|
190
|
+
function R(e, a, s) {
|
|
190
191
|
return {
|
|
191
192
|
...x[e],
|
|
192
193
|
swStrategy: e,
|
|
193
|
-
cacheName:
|
|
194
|
+
cacheName: s ?? "eidos-resources-v1"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function A(e) {
|
|
198
|
+
const a = await Promise.allSettled(e.map((t) => t.prefetch())), s = a.filter((t) => t.status === "rejected").map((t) => t.reason);
|
|
199
|
+
return {
|
|
200
|
+
warmed: a.filter((t) => t.status === "fulfilled").length,
|
|
201
|
+
failed: s.length,
|
|
202
|
+
errors: s
|
|
194
203
|
};
|
|
195
204
|
}
|
|
196
205
|
export {
|
|
197
206
|
q as resource,
|
|
198
|
-
N as setQueryInvalidator
|
|
207
|
+
N as setQueryInvalidator,
|
|
208
|
+
A as warmCache
|
|
199
209
|
};
|
|
200
210
|
//# sourceMappingURL=resource.js.map
|
package/dist/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\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\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, 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 const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\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 task.finally(() => _inflightRequests.delete(url))\n return task.then((r) => r.clone())\n },\n\n json: async () => {\n if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: 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 unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\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 ? `offline: no cached response for ${url}` : `${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(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\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 name: 'CacheFirst',\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 name: 'NetworkFirst',\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(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\n }\n}\n"],"names":["_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","fn","isPattern","url","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","entry","useEidosStore","sendToWorker","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","store","cached","current","expired","resp","storeEntry","response","isOffline","err","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resource.js","sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store'\nimport { sendToWorker } from './sw-bridge'\nimport type {\n ResourceConfig,\n ResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types'\n\nconst _registry = new Map<string, ResourceHandle>()\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\nfunction _patternError(url: string, method: string): Error {\n return new Error(\n `[eidos] resource('${url}') is a URL pattern — ${method}() is not supported on pattern handles. ` +\n `The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`,\n )\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\nexport function resource<T = unknown>(\n url: string,\n config: ResourceConfig,\n): ResourceHandle<T> {\n if (_registry.has(url)) {\n if (import.meta.env.DEV) {\n const existing = _registry.get(url)!\n const existingCfg = existing.config\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName\n ) {\n console.warn(\n `[eidos] resource('${url}') already registered with a different config — returning cached handle. Call resource.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n )\n }\n }\n return _registry.get(url) as ResourceHandle<T>\n }\n\n const strategy = deriveStrategy(url, 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 const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'fetch')\n\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 if (isPattern(url)) throw _patternError(url, 'json')\n const res = await handle.fetch()\n return res.json() as Promise<T>\n },\n\n query: () => {\n if (isPattern(url)) throw _patternError(url, 'query')\n return {\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }\n },\n\n prefetch: async () => {\n if (isPattern(url)) throw _patternError(url, 'prefetch')\n await handle.fetch()\n },\n\n invalidate: 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 unregister: () => {\n _registry.delete(url)\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url })\n useEidosStore.getState().unregisterResource(url)\n },\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 ? `offline: no cached response for ${url}` : `${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(url: string, config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy\n if (config.offline) return buildStrategy(explicit ?? 'stale-while-revalidate', url, config.cacheName)\n return buildStrategy(explicit ?? 'network-first', url, config.cacheName)\n}\n\nconst STRATEGY_META: Record<CacheStrategy, Omit<GeneratedStrategy, 'swStrategy' | 'cacheName'>> = {\n 'stale-while-revalidate': {\n name: 'StaleWhileRevalidate',\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 name: 'CacheFirst',\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 name: 'NetworkFirst',\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(swStrategy: CacheStrategy, _url: string, cacheName?: string): GeneratedStrategy {\n return {\n ...STRATEGY_META[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\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"],"names":["_registry","_inflightRequests","_queryInvalidator","setQueryInvalidator","fn","isPattern","url","patternToRegexStr","pattern","_patternError","method","resource","config","strategy","deriveStrategy","regexStr","entry","useEidosStore","sendToWorker","handle","existing","r","task","_fetchResource","cache","keys","patternRe","isCrossOrigin","rUrl","p","store","cached","current","expired","resp","storeEntry","response","isOffline","err","fallback","explicit","buildStrategy","STRATEGY_META","swStrategy","_url","cacheName","warmCache","handles","results","h","errors"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AsyncStorageLike } from './index.ts';
|
|
2
|
+
|
|
3
|
+
export interface EidosRNConfig {
|
|
4
|
+
/** AsyncStorage singleton (or any AsyncStorageLike key-value store). */
|
|
5
|
+
storage: AsyncStorageLike;
|
|
6
|
+
/** Replay the queue automatically when the app comes back online. Default: true. */
|
|
7
|
+
autoReplay?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Initialize Eidos for React Native.
|
|
11
|
+
* Call this once at app startup (outside any component) before rendering.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
15
|
+
* await initEidosRN({ storage: AsyncStorage })
|
|
16
|
+
*/
|
|
17
|
+
export declare function initEidosRN(config: EidosRNConfig): Promise<void>;
|
|
18
|
+
export declare function _resetEidosRN(): void;
|
package/dist/runtime.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { registerServiceWorker as
|
|
1
|
+
import { registerServiceWorker as d, registerBgSyncHandler as m } from "./sw-bridge.js";
|
|
2
2
|
import { replayQueue as n } from "./action.js";
|
|
3
3
|
import { useEidosStore as e } from "./store.js";
|
|
4
4
|
import { idbGetQueue as p } from "./idb.js";
|
|
5
5
|
let o = !1, s = null;
|
|
6
|
-
async function
|
|
7
|
-
if (o) return;
|
|
6
|
+
async function w(r = {}) {
|
|
7
|
+
if (typeof window > "u" || o) return;
|
|
8
8
|
o = !0;
|
|
9
9
|
const u = r.swPath ?? "/eidos-sw.js", l = r.autoReplay ?? !0;
|
|
10
10
|
try {
|
|
@@ -13,26 +13,26 @@ async function O(r = {}) {
|
|
|
13
13
|
} catch {
|
|
14
14
|
}
|
|
15
15
|
try {
|
|
16
|
-
await
|
|
16
|
+
await d(u);
|
|
17
17
|
} catch {
|
|
18
18
|
}
|
|
19
|
-
if (
|
|
19
|
+
if (m(() => {
|
|
20
20
|
e.getState().isOnline && setTimeout(n, 200);
|
|
21
21
|
}), l) {
|
|
22
22
|
let t = e.getState().isOnline;
|
|
23
23
|
s = e.subscribe(() => {
|
|
24
|
-
const { isOnline: i } = e.getState(),
|
|
25
|
-
t = i,
|
|
24
|
+
const { isOnline: i } = e.getState(), c = i && !t;
|
|
25
|
+
t = i, c && setTimeout(n, 600);
|
|
26
26
|
});
|
|
27
|
-
const a = e.getState(),
|
|
28
|
-
a.isOnline &&
|
|
27
|
+
const a = e.getState(), f = a.queue.some((i) => i.status === "pending" || i.status === "failed");
|
|
28
|
+
a.isOnline && f && setTimeout(n, 1200);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
function
|
|
31
|
+
function O() {
|
|
32
32
|
s == null || s(), s = null, o = !1;
|
|
33
33
|
}
|
|
34
34
|
export {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
O as _resetEidos,
|
|
36
|
+
w as initEidos
|
|
37
37
|
};
|
|
38
38
|
//# sourceMappingURL=runtime.js.map
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sources":["../src/runtime.ts"],"sourcesContent":["import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge'\nimport { replayQueue } from './action'\nimport { useEidosStore } from './store'\nimport { idbGetQueue } from './idb'\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean\n}\n\nlet _initialized = false\nlet _unsubscribe: (() => void) | null = null\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n if (_initialized) return\n _initialized = true\n\n const swPath = config.swPath ?? '/eidos-sw.js'\n const autoReplay = config.autoReplay ?? true\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue()\n if (persisted.length > 0) {\n useEidosStore.getState().hydrateQueue(persisted)\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath)\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200)\n }\n })\n\n if (autoReplay) {\n // ── Subscribe to the store instead of window.addEventListener('online')\n //\n // WHY: setOfflineSimulation() updates the store directly but never fires a\n // real 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 let prevIsOnline = useEidosStore.getState().isOnline\n\n _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 page reload\n const store = useEidosStore.getState()\n const hasPending = store.queue.some((q) => q.status === 'pending' || q.status === 'failed')\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200)\n }\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState()\n console.groupCollapsed('%c⚡ Eidos', 'color:#38bdf8;font-weight:bold')\n console.log('SW path :', swPath)\n console.log('Auto-replay:', autoReplay)\n console.log('SW status :', store.swStatus)\n console.groupEnd()\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.()\n _unsubscribe = null\n _initialized = false\n}\n"],"names":["_initialized","_unsubscribe","initEidos","config","swPath","autoReplay","persisted","idbGetQueue","useEidosStore","registerServiceWorker","registerBgSyncHandler","replayQueue","prevIsOnline","isOnline","justCameOnline","store","hasPending","q","_resetEidos"],"mappings":";;;;AAYA,IAAIA,IAAe,IACfC,IAAoC;AAExC,eAAsBC,EAAUC,IAAsB,IAAmB;
|
|
1
|
+
{"version":3,"file":"runtime.js","sources":["../src/runtime.ts"],"sourcesContent":["import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge'\nimport { replayQueue } from './action'\nimport { useEidosStore } from './store'\nimport { idbGetQueue } from './idb'\n\nexport interface EidosConfig {\n /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */\n swPath?: string\n /** Automatically replay the action queue on reconnect. Default: true. */\n autoReplay?: boolean\n}\n\nlet _initialized = false\nlet _unsubscribe: (() => void) | null = null\n\nexport async function initEidos(config: EidosConfig = {}): Promise<void> {\n // Skip silently during SSR — SW, IndexedDB, and window are browser-only.\n if (typeof window === 'undefined') return\n if (_initialized) return\n _initialized = true\n\n const swPath = config.swPath ?? '/eidos-sw.js'\n const autoReplay = config.autoReplay ?? true\n\n // Restore persisted queue from IndexedDB on startup\n try {\n const persisted = await idbGetQueue()\n if (persisted.length > 0) {\n useEidosStore.getState().hydrateQueue(persisted)\n }\n } catch {\n // IndexedDB unavailable (Firefox private browsing) — silent fallback\n }\n\n try {\n await registerServiceWorker(swPath)\n } catch {\n // SW registration failed; app continues without offline support\n }\n\n // When the SW fires the Background Sync tag, replay the queue in the main thread.\n // This path runs even if the user briefly navigated away and back — the browser\n // triggers the sync event on the SW, which wakes up all open clients.\n registerBgSyncHandler(() => {\n if (useEidosStore.getState().isOnline) {\n setTimeout(replayQueue, 200)\n }\n })\n\n if (autoReplay) {\n // ── Subscribe to the store instead of window.addEventListener('online')\n //\n // WHY: setOfflineSimulation() updates the store directly but never fires a\n // real 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 let prevIsOnline = useEidosStore.getState().isOnline\n\n _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 page reload\n const store = useEidosStore.getState()\n const hasPending = store.queue.some((q) => q.status === 'pending' || q.status === 'failed')\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200)\n }\n }\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState()\n console.groupCollapsed('%c⚡ Eidos', 'color:#38bdf8;font-weight:bold')\n console.log('SW path :', swPath)\n console.log('Auto-replay:', autoReplay)\n console.log('SW status :', store.swStatus)\n console.groupEnd()\n }\n}\n\nexport function _resetEidos() {\n _unsubscribe?.()\n _unsubscribe = null\n _initialized = false\n}\n"],"names":["_initialized","_unsubscribe","initEidos","config","swPath","autoReplay","persisted","idbGetQueue","useEidosStore","registerServiceWorker","registerBgSyncHandler","replayQueue","prevIsOnline","isOnline","justCameOnline","store","hasPending","q","_resetEidos"],"mappings":";;;;AAYA,IAAIA,IAAe,IACfC,IAAoC;AAExC,eAAsBC,EAAUC,IAAsB,IAAmB;AAGvE,MADI,OAAO,SAAW,OAClBH,EAAc;AAClB,EAAAA,IAAe;AAEf,QAAMI,IAASD,EAAO,UAAU,gBAC1BE,IAAaF,EAAO,cAAc;AAGxC,MAAI;AACF,UAAMG,IAAY,MAAMC,EAAA;AACxB,IAAID,EAAU,SAAS,KACrBE,EAAc,SAAA,EAAW,aAAaF,CAAS;AAAA,EAEnD,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAMG,EAAsBL,CAAM;AAAA,EACpC,QAAQ;AAAA,EAER;AAWA,MANAM,EAAsB,MAAM;AAC1B,IAAIF,EAAc,SAAA,EAAW,YAC3B,WAAWG,GAAa,GAAG;AAAA,EAE/B,CAAC,GAEGN,GAAY;AAQd,QAAIO,IAAeJ,EAAc,SAAA,EAAW;AAE5C,IAAAP,IAAeO,EAAc,UAAU,MAAM;AAC3C,YAAM,EAAE,UAAAK,EAAA,IAAaL,EAAc,SAAA,GAC7BM,IAAiBD,KAAY,CAACD;AACpC,MAAAA,IAAeC,GAEXC,KAEF,WAAWH,GAAa,GAAG;AAAA,IAE/B,CAAC;AAGD,UAAMI,IAAQP,EAAc,SAAA,GACtBQ,IAAaD,EAAM,MAAM,KAAK,CAACE,MAAMA,EAAE,WAAW,aAAaA,EAAE,WAAW,QAAQ;AAC1F,IAAIF,EAAM,YAAYC,KACpB,WAAWL,GAAa,IAAI;AAAA,EAEhC;AAUF;AAEO,SAASO,IAAc;AAC5B,EAAAjB,KAAA,QAAAA,KACAA,IAAe,MACfD,IAAe;AACjB;"}
|
package/dist/store.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
let
|
|
1
|
+
let o;
|
|
2
2
|
const n = /* @__PURE__ */ new Set();
|
|
3
|
-
function
|
|
3
|
+
function a() {
|
|
4
4
|
n.forEach((e) => e());
|
|
5
5
|
}
|
|
6
6
|
function r(e) {
|
|
7
|
-
|
|
7
|
+
o = { ...o, ...e(o) }, a();
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
o = {
|
|
10
|
+
// navigator.onLine is undefined in React Native — default to true unless explicitly false
|
|
11
|
+
isOnline: typeof navigator > "u" || navigator.onLine !== !1,
|
|
11
12
|
swStatus: "idle",
|
|
12
13
|
swError: void 0,
|
|
13
14
|
resources: {},
|
|
@@ -22,34 +23,43 @@ s = {
|
|
|
22
23
|
}
|
|
23
24
|
})),
|
|
24
25
|
unregisterResource: (e) => r((u) => {
|
|
25
|
-
const { [e]: t, ...
|
|
26
|
-
return { resources:
|
|
26
|
+
const { [e]: t, ...s } = u.resources;
|
|
27
|
+
return { resources: s };
|
|
27
28
|
}),
|
|
28
29
|
addQueueItem: (e) => r((u) => ({ queue: [...u.queue, e] })),
|
|
29
30
|
updateQueueItem: (e, u) => r((t) => ({
|
|
30
|
-
queue: t.queue.map((
|
|
31
|
+
queue: t.queue.map((s) => s.id === e ? { ...s, ...u } : s)
|
|
31
32
|
})),
|
|
33
|
+
batchUpdateQueueItems: (e) => r((u) => {
|
|
34
|
+
const t = new Map(e.map((s) => [s.id, s.update]));
|
|
35
|
+
return {
|
|
36
|
+
queue: u.queue.map((s) => {
|
|
37
|
+
const c = t.get(s.id);
|
|
38
|
+
return c ? { ...s, ...c } : s;
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
32
42
|
removeQueueItem: (e) => r((u) => ({ queue: u.queue.filter((t) => t.id !== e) })),
|
|
33
43
|
hydrateQueue: (e) => r(() => ({ queue: e }))
|
|
34
44
|
};
|
|
35
|
-
function
|
|
36
|
-
return
|
|
45
|
+
function d() {
|
|
46
|
+
return o;
|
|
37
47
|
}
|
|
38
|
-
function
|
|
48
|
+
function i(e) {
|
|
39
49
|
return n.add(e), () => {
|
|
40
50
|
n.delete(e);
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
|
-
const
|
|
44
|
-
getState:
|
|
45
|
-
subscribe:
|
|
53
|
+
const f = {
|
|
54
|
+
getState: d,
|
|
55
|
+
subscribe: i,
|
|
46
56
|
// Test/devtools helper — merges partial state, preserves action methods.
|
|
47
57
|
setState: (e) => {
|
|
48
|
-
const u = typeof e == "function" ? e(
|
|
49
|
-
|
|
58
|
+
const u = typeof e == "function" ? e(o) : e;
|
|
59
|
+
o = { ...o, ...u }, a();
|
|
50
60
|
}
|
|
51
61
|
};
|
|
52
62
|
export {
|
|
53
|
-
|
|
63
|
+
f as useEidosStore
|
|
54
64
|
};
|
|
55
65
|
//# sourceMappingURL=store.js.map
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sources":["../src/store.ts"],"sourcesContent":["import type { EidosState, ResourceEntry, ActionQueueItem } from './types'\n\nexport interface EidosStore extends EidosState {\n // Online\n setOnline: (online: boolean) => void\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void\n // Resources\n registerResource: (url: string, entry: ResourceEntry) => void\n updateResource: (url: string, update: Partial<ResourceEntry>) => void\n unregisterResource: (url: string) => void\n // Queue\n addQueueItem: (item: ActionQueueItem) => void\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void\n removeQueueItem: (id: string) => void\n hydrateQueue: (items: ActionQueueItem[]) => void\n}\n\ntype Listener = () => void\n\nlet _state: EidosStore\nconst _listeners = new Set<Listener>()\n\nfunction _notify() {\n _listeners.forEach((fn) => fn())\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) }\n _notify()\n}\n\n_state = {\n isOnline: typeof navigator
|
|
1
|
+
{"version":3,"file":"store.js","sources":["../src/store.ts"],"sourcesContent":["import type { EidosState, ResourceEntry, ActionQueueItem } from './types'\n\nexport interface EidosStore extends EidosState {\n // Online\n setOnline: (online: boolean) => void\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => void\n // Resources\n registerResource: (url: string, entry: ResourceEntry) => void\n updateResource: (url: string, update: Partial<ResourceEntry>) => void\n unregisterResource: (url: string) => void\n // Queue\n addQueueItem: (item: ActionQueueItem) => void\n updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void\n batchUpdateQueueItems: (updates: Array<{ id: string; update: Partial<ActionQueueItem> }>) => void\n removeQueueItem: (id: string) => void\n hydrateQueue: (items: ActionQueueItem[]) => void\n}\n\ntype Listener = () => void\n\nlet _state: EidosStore\nconst _listeners = new Set<Listener>()\n\nfunction _notify() {\n _listeners.forEach((fn) => fn())\n}\n\nfunction _set(updater: (prev: EidosStore) => Partial<EidosStore>) {\n _state = { ..._state, ...updater(_state) }\n _notify()\n}\n\n_state = {\n // navigator.onLine is undefined in React Native — default to true unless explicitly false\n isOnline: typeof navigator === 'undefined' || navigator.onLine !== false,\n swStatus: 'idle',\n swError: undefined,\n resources: {},\n queue: [],\n\n setOnline: (isOnline) => _set(() => ({ isOnline })),\n\n setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),\n\n registerResource: (url, entry) =>\n _set((s) => ({ resources: { ...s.resources, [url]: entry } })),\n\n updateResource: (url, update) =>\n _set((s) => ({\n resources: {\n ...s.resources,\n [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url],\n },\n })),\n\n unregisterResource: (url) =>\n _set((s) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { [url]: _removed, ...rest } = s.resources\n return { resources: rest }\n }),\n\n addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),\n\n updateQueueItem: (id, update) =>\n _set((s) => ({\n queue: s.queue.map((item) => (item.id === id ? { ...item, ...update } : item)),\n })),\n\n batchUpdateQueueItems: (updates) =>\n _set((s) => {\n const map = new Map(updates.map((u) => [u.id, u.update]))\n return {\n queue: s.queue.map((item) => {\n const u = map.get(item.id)\n return u ? { ...item, ...u } : item\n }),\n }\n }),\n\n removeQueueItem: (id) => _set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),\n\n hydrateQueue: (items) => _set(() => ({ queue: items })),\n}\n\nfunction _getState() {\n return _state\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener)\n return () => { _listeners.delete(listener) }\n}\n\nexport const useEidosStore = {\n getState: _getState,\n subscribe: _subscribe,\n // Test/devtools helper — merges partial state, preserves action methods.\n setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => {\n const update = typeof partial === 'function' ? partial(_state) : partial\n _state = { ..._state, ...update }\n _notify()\n },\n}\n"],"names":["_state","_listeners","_notify","fn","_set","updater","isOnline","swStatus","swError","url","entry","s","update","_removed","rest","item","id","updates","map","u","items","_getState","_subscribe","listener","useEidosStore","partial"],"mappings":"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"}
|
package/dist/stores.js
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
1
|
import { useEidosStore as i } from "./store.js";
|
|
2
|
-
function
|
|
2
|
+
function o(e, t) {
|
|
3
|
+
const s = Object.keys(e);
|
|
4
|
+
if (s.length !== Object.keys(t).length) return !1;
|
|
5
|
+
for (const n of s)
|
|
6
|
+
if (e[n] !== t[n]) return !1;
|
|
7
|
+
return !0;
|
|
8
|
+
}
|
|
9
|
+
function u(e, t = Object.is) {
|
|
3
10
|
return {
|
|
4
|
-
subscribe(
|
|
5
|
-
|
|
11
|
+
subscribe(s) {
|
|
12
|
+
let n = e(i.getState());
|
|
13
|
+
return s(n), i.subscribe(() => {
|
|
14
|
+
const r = e(i.getState());
|
|
15
|
+
t(n, r) || (n = r, s(r));
|
|
16
|
+
});
|
|
6
17
|
},
|
|
7
18
|
getState() {
|
|
8
19
|
return e(i.getState());
|
|
9
20
|
}
|
|
10
21
|
};
|
|
11
22
|
}
|
|
12
|
-
const
|
|
13
|
-
isOnline: e.isOnline,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const f = u((e) => e), l = u((e) => e.queue), c = u(
|
|
24
|
+
(e) => ({ isOnline: e.isOnline, swStatus: e.swStatus, swError: e.swError }),
|
|
25
|
+
o
|
|
26
|
+
), d = u(
|
|
27
|
+
(e) => {
|
|
28
|
+
let t = 0, s = 0, n = 0;
|
|
29
|
+
for (const r of e.queue)
|
|
30
|
+
r.status === "pending" ? t++ : r.status === "failed" ? s++ : r.status === "replaying" && n++;
|
|
31
|
+
return { pending: t, failed: s, replaying: n, total: e.queue.length };
|
|
32
|
+
},
|
|
33
|
+
o
|
|
34
|
+
);
|
|
35
|
+
function g(e) {
|
|
36
|
+
return u((t) => t.resources[e]);
|
|
24
37
|
}
|
|
25
38
|
function S(e) {
|
|
26
|
-
return
|
|
39
|
+
return u((t) => t.queue.find((s) => s.id === e));
|
|
27
40
|
}
|
|
28
41
|
export {
|
|
29
42
|
S as eidosAction,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
l as eidosQueue,
|
|
44
|
+
d as eidosQueueStats,
|
|
45
|
+
g as eidosResource,
|
|
46
|
+
c as eidosStatus,
|
|
47
|
+
f as eidosStore
|
|
35
48
|
};
|
|
36
49
|
//# sourceMappingURL=stores.js.map
|