@sweidos/eidos 1.1.0 → 2.0.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 +127 -32
- package/dist/action.js +223 -112
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/cli.js +102 -0
- package/dist/devtools.js +208 -71
- package/dist/eidos-sw.js +283 -188
- 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 +160 -26
- package/dist/index.js +45 -41
- package/dist/push.cjs +123 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +116 -0
- 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 +37 -19
- 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 +44 -31
- 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 +11 -4
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(), v = null;
|
|
4
|
+
function _(e) {
|
|
5
|
+
v = e;
|
|
6
6
|
}
|
|
7
|
-
function
|
|
7
|
+
function u(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 = u(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, f = new URL(o).pathname;
|
|
44
|
+
return n ? n.test(c ? o : f) : c ? o === e : o === e || f === e;
|
|
45
|
+
}).map((i) => t.delete(i)));
|
|
46
|
+
}
|
|
47
|
+
u(e) || h.getState().updateResource(e, {
|
|
48
|
+
status: "stale",
|
|
49
|
+
cachedAt: void 0,
|
|
50
|
+
lastEvent: "cache-cleared",
|
|
51
|
+
cacheHits: 0,
|
|
52
|
+
cacheMisses: 0
|
|
53
|
+
}), v?.(["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 (u(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 O(e, s) {
|
|
92
|
+
if (!u(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], f = s.maxAge !== void 0 && o?.cachedAt !== void 0 && Date.now() - o.cachedAt > s.maxAge;
|
|
113
|
+
if (i && !f)
|
|
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 ? g(s ?? "stale-while-revalidate", e.cacheName) : g(s ?? "network-first", e.cacheName);
|
|
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 g(e, s) {
|
|
204
|
+
const a = C[e];
|
|
192
205
|
return {
|
|
193
|
-
name:
|
|
206
|
+
name: x[e],
|
|
194
207
|
swStrategy: e,
|
|
195
|
-
cacheName:
|
|
196
|
-
reasoning:
|
|
197
|
-
behavior:
|
|
208
|
+
cacheName: s ?? "eidos-resources-v1",
|
|
209
|
+
reasoning: a.reasoning,
|
|
210
|
+
behavior: a.behavior,
|
|
198
211
|
equivalentCode: ""
|
|
199
212
|
};
|
|
200
213
|
}
|
|
201
|
-
async function
|
|
202
|
-
const
|
|
214
|
+
async function T(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
|
+
O as resourcePattern,
|
|
225
|
+
_ as setQueryInvalidator,
|
|
226
|
+
T 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 ) {\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) return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName);\n return buildStrategy(explicit ?? 'network-first', config.cacheName);\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(swStrategy: CacheStrategy, cacheName?: string): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: cacheName ?? 'eidos-resources-v1',\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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"}
|
package/dist/runtime.js
CHANGED
|
@@ -1,32 +1,50 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { registerBgSyncHandler as
|
|
3
|
-
import { idbGetQueue as
|
|
1
|
+
import { useEidosStore as a } from "./store.js";
|
|
2
|
+
import { registerBgSyncHandler as c, registerServiceWorker as m } from "./sw-bridge.js";
|
|
3
|
+
import { idbGetQueue as y, idbQueueStorage as d } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as p } from "./queue-storage.js";
|
|
5
|
+
import { subscribeQueueSync as l } from "./queue-sync.js";
|
|
4
6
|
import { replayQueue as f } from "./action.js";
|
|
5
|
-
import { subscribeReplayOnReconnect as
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { subscribeReplayOnReconnect as g } from "./replay.js";
|
|
8
|
+
async function h(e) {
|
|
9
|
+
if (e.schemaVersion === 2 && e.idempotencyKey) return e;
|
|
10
|
+
const t = {
|
|
11
|
+
...e,
|
|
12
|
+
schemaVersion: 2,
|
|
13
|
+
idempotencyKey: e.idempotencyKey ?? crypto.randomUUID()
|
|
14
|
+
};
|
|
15
|
+
return await (p() ?? d).update(t.id, {
|
|
16
|
+
schemaVersion: t.schemaVersion,
|
|
17
|
+
idempotencyKey: t.idempotencyKey
|
|
18
|
+
}).catch(() => {
|
|
19
|
+
}), t;
|
|
20
|
+
}
|
|
21
|
+
var r = !1, i = null, o = null;
|
|
22
|
+
async function R(e = {}) {
|
|
23
|
+
if (typeof window > "u" || r) return;
|
|
24
|
+
r = !0;
|
|
25
|
+
const t = e.swPath ?? "/eidos-sw.js", s = e.autoReplay ?? !0;
|
|
11
26
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
27
|
+
const n = await y();
|
|
28
|
+
if (n.length > 0) {
|
|
29
|
+
const u = await Promise.all(n.map(h));
|
|
30
|
+
a.getState().hydrateQueue(u);
|
|
31
|
+
}
|
|
14
32
|
} catch {
|
|
15
33
|
}
|
|
16
34
|
try {
|
|
17
|
-
await
|
|
35
|
+
await m(t);
|
|
18
36
|
} catch {
|
|
19
37
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}),
|
|
38
|
+
c(() => {
|
|
39
|
+
a.getState().isOnline && setTimeout(f, 200);
|
|
40
|
+
}), s && (i = g()), o = l();
|
|
23
41
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
42
|
+
function V() {
|
|
43
|
+
i?.(), i = null, o?.(), o = null, r = !1;
|
|
26
44
|
}
|
|
27
45
|
export {
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
V as _resetEidos,
|
|
47
|
+
R as initEidos
|
|
30
48
|
};
|
|
31
49
|
|
|
32
50
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["import { registerServiceWorker, registerBgSyncHandler } from './sw-bridge';\nimport { replayQueue } from './action';\nimport { useEidosStore } from './store';\nimport { idbGetQueue, idbQueueStorage } from './idb';\nimport { _getQueueStorage } from './queue-storage';\nimport { subscribeReplayOnReconnect } from './replay';\nimport { subscribeQueueSync } from './queue-sync';\nimport { CURRENT_QUEUE_SCHEMA_VERSION } from './types';\nimport type { ActionQueueItem } from './types';\n\n// Items persisted before idempotencyKey/schemaVersion existed (v1) are migrated\n// in place: assign a fresh idempotencyKey and bump schemaVersion. A fresh key on\n// first replay after upgrade is correct — these items were never sent with one.\nasync function migrateQueueItem(item: ActionQueueItem): Promise<ActionQueueItem> {\n if (item.schemaVersion === CURRENT_QUEUE_SCHEMA_VERSION && item.idempotencyKey) {\n return item;\n }\n const migrated: ActionQueueItem = {\n ...item,\n schemaVersion: CURRENT_QUEUE_SCHEMA_VERSION,\n idempotencyKey: item.idempotencyKey ?? crypto.randomUUID(),\n };\n const storage = _getQueueStorage() ?? idbQueueStorage;\n await storage\n .update(migrated.id, {\n schemaVersion: migrated.schemaVersion,\n idempotencyKey: migrated.idempotencyKey,\n })\n .catch(() => {\n // Best-effort persist — item still gets the migrated shape in memory this session\n });\n return migrated;\n}\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;\nlet _unsubscribeQueueSync: (() => 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 const migrated = await Promise.all(persisted.map(migrateQueueItem));\n useEidosStore.getState().hydrateQueue(migrated);\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 _unsubscribe = subscribeReplayOnReconnect();\n }\n\n // Apply queue-item status changes broadcast by the replay-lock holder so\n // non-leader tabs reflect live status without waiting for re-hydration.\n _unsubscribeQueueSync = subscribeQueueSync();\n\n if (import.meta.env.DEV) {\n const store = useEidosStore.getState();\n console.groupCollapsed('%c⚡ Eidos', 'color:#22C55E;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 _unsubscribeQueueSync?.();\n _unsubscribeQueueSync = null;\n _initialized = false;\n}\n"],"mappings":";;;;;;;AAaA,eAAe,EAAiB,GAAiD;AAC/E,MAAI,EAAK,kBAAA,KAAkD,EAAK,eAC9D,QAAO;AAET,QAAM,IAA4B;AAAA,IAChC,GAAG;AAAA,IACH,eAAA;AAAA,IACA,gBAAgB,EAAK,kBAAkB,OAAO,WAAW;AAAA,EAC3D;AAEA,gBADgB,EAAiB,KAAK,GAEnC,OAAO,EAAS,IAAI;AAAA,IACnB,eAAe,EAAS;AAAA,IACxB,gBAAgB,EAAS;AAAA,EAC3B,CAAC,EACA,MAAA,MAAY;AAAA,EAEb,CAAC,GACI;AACT;AASA,IAAI,IAAe,IACf,IAAoC,MACpC,IAA6C;AAEjD,eAAsB,EAAU,IAAsB,CAAC,GAAkB;AAGvE,MADI,OAAO,SAAW,OAClB,EAAc;AAClB,EAAA,IAAe;AAEf,QAAM,IAAS,EAAO,UAAU,gBAC1B,IAAa,EAAO,cAAc;AAGxC,MAAI;AACF,UAAM,IAAY,MAAM,EAAY;AACpC,QAAI,EAAU,SAAS,GAAG;AACxB,YAAM,IAAW,MAAM,QAAQ,IAAI,EAAU,IAAI,CAAgB,CAAC;AAClE,MAAA,EAAc,SAAS,EAAE,aAAa,CAAQ;AAAA,IAChD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAM,EAAsB,CAAM;AAAA,EACpC,QAAQ;AAAA,EAER;AAKA,EAAA,EAAA,MAA4B;AAC1B,IAAI,EAAc,SAAS,EAAE,YAC3B,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAEG,MACF,IAAe,EAA2B,IAK5C,IAAwB,EAAmB;AAU7C;AAEA,SAAgB,IAAc;AAC5B,EAAA,IAAe,GACf,IAAe,MACf,IAAwB,GACxB,IAAwB,MACxB,IAAe;AACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-slices.js","names":[],"sources":["../src/store-slices.ts"],"sourcesContent":["import type { ResourceEntry, ActionQueueItem } from './types';\nimport type { EidosStore } from './store';\n\ntype Setter = (updater: (prev: EidosStore) => Partial<EidosStore>) => void;\n\n// ── Resource slice ────────────────────────────────────────────────────────────\n\nexport interface ResourceActions {\n registerResource: (url: string, entry: ResourceEntry) => void;\n updateResource: (url: string, update: Partial<ResourceEntry>) => void;\n unregisterResource: (url: string) => void;\n}\n\nexport function createResourceActions(set: Setter): ResourceActions {\n return {\n registerResource: (url, entry) => 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 resources: Object.fromEntries(Object.entries(s.resources).filter(([k]) => k !== url)),\n })),\n };\n}\n\n// ── Queue slice ───────────────────────────────────────────────────────────────\n\nexport interface QueueActions {\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\nexport function createQueueActions(set: Setter): QueueActions {\n return {\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}\n"],"mappings":"AAaA,SAAgB,EAAsB,GAA8B;AAClE,SAAO;AAAA,IACL,kBAAA,CAAmB,GAAK,MAAU,EAAA,CAAK,OAAO,EAAE,WAAW;AAAA,MAAE,GAAG,EAAE;AAAA,OAAY,CAAA,GAAM;AAAA,IAAM,EAAE,EAAE;AAAA,IAE9F,gBAAA,CAAiB,GAAK,MACpB,EAAA,CAAK,OAAO,EACV,WAAW;AAAA,MACT,GAAG,EAAE;AAAA,OACJ,CAAA,GAAM,EAAE,UAAU,CAAA,IAAO;AAAA,QAAE,GAAG,EAAE,UAAU,CAAA;AAAA,QAAM,GAAG;AAAA,MAAO,IAAI,EAAE,UAAU,CAAA;AAAA,IAC7E,EACF,EAAE;AAAA,IAEJ,oBAAA,CAAqB,MACnB,EAAA,CAAK,OAAO,EACV,WAAW,OAAO,YAAY,OAAO,QAAQ,EAAE,SAAS,EAAE,OAAA,CAAQ,CAAC,CAAA,MAAO,MAAM,CAAG,CAAC,EACtF,EAAE;AAAA,EACN;AACF;AAYA,SAAgB,EAAmB,GAA2B;AAC5D,SAAO;AAAA,IACL,cAAA,CAAe,MAAS,EAAA,CAAK,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAI,EAAE,EAAE;AAAA,IAElE,iBAAA,CAAkB,GAAI,MACpB,EAAA,CAAK,OAAO,EACV,OAAO,EAAE,MAAM,IAAA,CAAK,MAAU,EAAK,OAAO,IAAK;AAAA,MAAE,GAAG;AAAA,MAAM,GAAG;AAAA,IAAO,IAAI,CAAK,EAC/E,EAAE;AAAA,IAEJ,uBAAA,CAAwB,MACtB,EAAA,CAAK,MAAM;AACT,YAAM,IAAM,IAAI,IAAI,EAAQ,IAAA,CAAK,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACxD,aAAO,EACL,OAAO,EAAE,MAAM,IAAA,CAAK,MAAS;AAC3B,cAAM,IAAI,EAAI,IAAI,EAAK,EAAE;AACzB,eAAO,IAAI;AAAA,UAAE,GAAG;AAAA,UAAM,GAAG;AAAA,QAAE,IAAI;AAAA,MACjC,CAAC,EACH;AAAA,IACF,CAAC;AAAA,IAEH,iBAAA,CAAkB,MAAO,EAAA,CAAK,OAAO,EAAE,OAAO,EAAE,MAAM,OAAA,CAAQ,MAAS,EAAK,OAAO,CAAE,EAAE,EAAE;AAAA,IAEzF,cAAA,CAAe,MAAU,EAAA,OAAW,EAAE,OAAO,EAAM,EAAE;AAAA,EACvD;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { EidosState } from './types';\nimport { createResourceActions, createQueueActions } from './store-slices';\nimport type { ResourceActions, QueueActions } from './store-slices';\n\nexport interface EidosStore extends EidosState, ResourceActions, QueueActions {\n // Online\n setOnline: (online: boolean) => void;\n // SW\n setSwStatus: (status: EidosState['swStatus'], error?: string) => 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 ...createResourceActions(_set),\n ...createQueueActions(_set),\n};\n\nfunction _getState() {\n return _state;\n}\n\nfunction _subscribe(listener: Listener) {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\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"],"mappings":";AAaA,IAAI,GACE,IAAa,oBAAI,IAAc;AAErC,SAAS,IAAU;AACjB,EAAA,EAAW,QAAA,CAAS,MAAO,EAAG,CAAC;AACjC;AAEA,SAAS,EAAK,GAAoD;AAChE,EAAA,IAAS;AAAA,IAAE,GAAG;AAAA,IAAQ,GAAG,EAAQ,CAAM;AAAA,EAAE,GACzC,EAAQ;AACV;AAEA,IAAS;AAAA,EAEP,UAAU,OAAO,YAAc,OAAe,UAAU,WAAW;AAAA,EACnE,UAAU;AAAA,EACV,SAAS;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,OAAO,CAAC;AAAA,EAER,WAAA,CAAY,MAAa,EAAA,OAAY,EAAE,UAAA,EAAS,EAAE;AAAA,EAElD,aAAA,CAAc,GAAU,MAAY,EAAA,OAAY;AAAA,IAAE,UAAA;AAAA,IAAU,SAAA;AAAA,EAAQ,EAAE;AAAA,EAEtE,GAAG,EAAsB,CAAI;AAAA,EAC7B,GAAG,EAAmB,CAAI;AAC5B;AAEA,SAAS,IAAY;AACnB,SAAO;AACT;AAEA,SAAS,EAAW,GAAoB;AACtC,SAAA,EAAW,IAAI,CAAQ,GACvB,MAAa;AACX,IAAA,EAAW,OAAO,CAAQ;AAAA,EAC5B;AACF;AAEA,IAAa,IAAgB;AAAA,EAC3B,UAAU;AAAA,EACV,WAAW;AAAA,EAEX,UAAA,CAAW,MAA4E;AACrF,UAAM,IAAS,OAAO,KAAY,aAAa,EAAQ,CAAM,IAAI;AACjE,IAAA,IAAS;AAAA,MAAE,GAAG;AAAA,MAAQ,GAAG;AAAA,IAAO,GAChC,EAAQ;AAAA,EACV;AACF"}
|
package/dist/stores.js
CHANGED
|
@@ -1,54 +1,46 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
|
|
1
|
+
import { useEidosStore as n } from "./store.js";
|
|
2
|
+
import { countQueueByStatus as a } from "./types.js";
|
|
3
|
+
function c(e, t) {
|
|
3
4
|
const r = Object.keys(e);
|
|
4
5
|
if (r.length !== Object.keys(t).length) return !1;
|
|
5
|
-
for (const
|
|
6
|
+
for (const u of r) if (e[u] !== t[u]) return !1;
|
|
6
7
|
return !0;
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
-
return
|
|
9
|
+
function i(e, t) {
|
|
10
|
+
return c(e, t);
|
|
10
11
|
}
|
|
11
|
-
function
|
|
12
|
+
function s(e, t = Object.is) {
|
|
12
13
|
return {
|
|
13
14
|
subscribe(r) {
|
|
14
|
-
let
|
|
15
|
-
return r(
|
|
16
|
-
const
|
|
17
|
-
t(
|
|
15
|
+
let u = e(n.getState());
|
|
16
|
+
return r(u), n.subscribe(() => {
|
|
17
|
+
const o = e(n.getState());
|
|
18
|
+
t(u, o) || (u = o, r(o));
|
|
18
19
|
});
|
|
19
20
|
},
|
|
20
21
|
getState() {
|
|
21
|
-
return e(
|
|
22
|
+
return e(n.getState());
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
|
-
var
|
|
26
|
+
var S = s((e) => e), d = s((e) => e.queue), b = s((e) => ({
|
|
26
27
|
isOnline: e.isOnline,
|
|
27
28
|
swStatus: e.swStatus,
|
|
28
29
|
swError: e.swError
|
|
29
|
-
}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
pending: t,
|
|
34
|
-
failed: r,
|
|
35
|
-
replaying: s,
|
|
36
|
-
total: e.queue.length
|
|
37
|
-
};
|
|
38
|
-
}, o);
|
|
39
|
-
function S(e) {
|
|
40
|
-
return u((t) => t.resources[e]);
|
|
30
|
+
}), i), g = s((e) => a(e.queue), i);
|
|
31
|
+
function w(e) {
|
|
32
|
+
return s((t) => t.resources[e]);
|
|
41
33
|
}
|
|
42
|
-
function
|
|
43
|
-
return
|
|
34
|
+
function q(e) {
|
|
35
|
+
return s((t) => t.queue.find((r) => r.id === e));
|
|
44
36
|
}
|
|
45
37
|
export {
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
q as eidosAction,
|
|
39
|
+
d as eidosQueue,
|
|
48
40
|
g as eidosQueueStats,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
w as eidosResource,
|
|
42
|
+
b as eidosStatus,
|
|
43
|
+
S as eidosStore
|
|
52
44
|
};
|
|
53
45
|
|
|
54
46
|
//# sourceMappingURL=stores.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.js","names":[],"sources":["../src/stores.ts"],"sourcesContent":["/**\n * Framework-agnostic reactive stores — compatible with Svelte's store protocol,\n * Vue's watchEffect, RxJS, and vanilla JS. Zero framework dependencies.\n *\n * Svelte: use the `$` prefix — `$eidosQueue`, `$eidosStatus`, etc.\n * Vue: call `.subscribe()` inside a composable with `onUnmounted` cleanup.\n * Vanilla: call `.subscribe(run)` directly; the return value unsubscribes.\n *\n * Each store calls its subscriber whenever any part of the Eidos state changes.\n * For fine-grained subscriptions, use `.getState()` to read the current snapshot\n * and compare manually in the subscriber callback.\n */\n\nimport { useEidosStore } from './store';\nimport type { EidosStore } from './store';\nimport { countQueueByStatus } from './types';\nimport type { ActionQueueItem, ResourceEntry } from './types';\n\n// ── Readable<T> — compatible with Svelte's Readable interface ─────────────────\n\nexport interface EidosReadable<T> {\n /** Subscribe to value changes. Returns an unsubscribe function. */\n subscribe(run: (value: T) => void): () => void;\n /** Read the current value synchronously without subscribing. */\n getState(): T;\n}\n\nfunction shallowEqual<T extends Record<string, unknown>>(a: T, b: T): boolean {\n const keys = Object.keys(a) as (keyof T)[];\n if (keys.length !== Object.keys(b).length) return false;\n for (const k of keys) if (a[k] !== b[k]) return false;\n return true;\n}\n\n// Typed comparator alias so call sites don't need inline casts.\nfunction shallowEq<T extends Record<string, unknown>>(a: T, b: T): boolean {\n return shallowEqual(a, b);\n}\n\nfunction readable<T>(\n selector: (s: EidosStore) => T,\n equal: (a: T, b: T) => boolean = Object.is,\n): EidosReadable<T> {\n return {\n subscribe(run) {\n // Emit current value immediately (Svelte store contract)\n let last = selector(useEidosStore.getState());\n run(last);\n return useEidosStore.subscribe(() => {\n const next = selector(useEidosStore.getState());\n if (!equal(last, next)) {\n last = next;\n run(next);\n }\n });\n },\n getState() {\n return selector(useEidosStore.getState());\n },\n };\n}\n\n// ── Static stores (created once at module scope) ──────────────────────────────\n\n/** Full Eidos state snapshot. Prefer the narrower stores below. */\nexport const eidosStore: EidosReadable<EidosStore> = readable((s) => s);\n\n/** The action queue. Re-notifies on every queue mutation. */\nexport const eidosQueue: EidosReadable<ActionQueueItem[]> = readable((s) => s.queue);\n\n/**\n * Online status + SW lifecycle.\n * Only re-emits when isOnline, swStatus, or swError actually changes.\n */\nexport const eidosStatus: EidosReadable<{\n isOnline: boolean;\n swStatus: EidosStore['swStatus'];\n swError: string | undefined;\n}> = readable(\n (s) => ({ isOnline: s.isOnline, swStatus: s.swStatus, swError: s.swError }),\n shallowEq,\n);\n\n/**\n * Queue counts. Re-emits only when a count actually changes, not on every\n * queue mutation (e.g. a status transition that doesn't change counts is skipped).\n */\nexport const eidosQueueStats: EidosReadable<{\n pending: number;\n failed: number;\n replaying: number;\n total: number;\n}> = readable((s) => countQueueByStatus(s.queue), shallowEq);\n\n// ── Dynamic stores (created per URL / ID) ─────────────────────────────────────\n\n/**\n * Live cache state for a single registered resource URL.\n * @example\n * // Svelte\n * const entry = eidosResource('/api/products')\n * $: hits = $entry?.cacheHits ?? 0\n */\nexport function eidosResource(url: string): EidosReadable<ResourceEntry | undefined> {\n return readable((s) => s.resources[url]);\n}\n\n/**\n * Live state for a single queue item by ID. Returns `undefined` once the item\n * is removed from the queue (after a successful replay or `clearQueue()`).\n * @example\n * // Svelte\n * const item = eidosAction(queuedResult.id)\n * $: status = $item?.status // 'pending' | 'replaying' | 'succeeded' | 'failed' | undefined\n */\nexport function eidosAction(id: string): EidosReadable<ActionQueueItem | undefined> {\n return readable((s) => s.queue.find((item) => item.id === id));\n}\n"],"mappings":";;AA2BA,SAAS,EAAgD,GAAM,GAAe;AAC5E,QAAM,IAAO,OAAO,KAAK,CAAC;AAC1B,MAAI,EAAK,WAAW,OAAO,KAAK,CAAC,EAAE,OAAQ,QAAO;AAClD,aAAW,KAAK,EAAM,KAAI,EAAE,CAAA,MAAO,EAAE,CAAA,EAAI,QAAO;AAChD,SAAO;AACT;AAGA,SAAS,EAA6C,GAAM,GAAe;AACzE,SAAO,EAAa,GAAG,CAAC;AAC1B;AAEA,SAAS,EACP,GACA,IAAiC,OAAO,IACtB;AAClB,SAAO;AAAA,IACL,UAAU,GAAK;AAEb,UAAI,IAAO,EAAS,EAAc,SAAS,CAAC;AAC5C,aAAA,EAAI,CAAI,GACD,EAAc,UAAA,MAAgB;AACnC,cAAM,IAAO,EAAS,EAAc,SAAS,CAAC;AAC9C,QAAK,EAAM,GAAM,CAAI,MACnB,IAAO,GACP,EAAI,CAAI;AAAA,MAEZ,CAAC;AAAA,IACH;AAAA,IACA,WAAW;AACT,aAAO,EAAS,EAAc,SAAS,CAAC;AAAA,IAC1C;AAAA,EACF;AACF;AAKA,IAAa,IAAwC,EAAA,CAAU,MAAM,CAAC,GAGzD,IAA+C,EAAA,CAAU,MAAM,EAAE,KAAK,GAMtE,IAIR,EAAA,CACF,OAAO;AAAA,EAAE,UAAU,EAAE;AAAA,EAAU,UAAU,EAAE;AAAA,EAAU,SAAS,EAAE;AAAQ,IACzE,CACF,GAMa,IAKR,EAAA,CAAU,MAAM,EAAmB,EAAE,KAAK,GAAG,CAAS;AAW3D,SAAgB,EAAc,GAAuD;AACnF,SAAO,EAAA,CAAU,MAAM,EAAE,UAAU,CAAA,CAAI;AACzC;AAUA,SAAgB,EAAY,GAAwD;AAClF,SAAO,EAAA,CAAU,MAAM,EAAE,MAAM,KAAA,CAAM,MAAS,EAAK,OAAO,CAAE,CAAC;AAC/D"}
|