@sweidos/eidos 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +290 -22
  2. package/dist/action.d.ts +22 -0
  3. package/dist/action.js +47 -47
  4. package/dist/action.js.map +1 -1
  5. package/dist/async-storage-adapter.d.ts +25 -0
  6. package/dist/debug.d.ts +46 -0
  7. package/dist/debug.js +43 -0
  8. package/dist/debug.js.map +1 -0
  9. package/dist/devtools.js +350 -21
  10. package/dist/eidos-sw.js +60 -19
  11. package/dist/eidos.cjs +5 -5
  12. package/dist/eidos.cjs.map +1 -1
  13. package/dist/idb.d.ts +10 -0
  14. package/dist/index.d.ts +20 -586
  15. package/dist/index.js +47 -41
  16. package/dist/internal/url-base64.d.ts +2 -0
  17. package/dist/query.d.ts +1 -2
  18. package/dist/queue-storage.d.ts +12 -0
  19. package/dist/queue-sync.d.ts +32 -0
  20. package/dist/react/Provider.d.ts +16 -0
  21. package/dist/react/ProviderRN.d.ts +0 -1
  22. package/dist/react/hooks.d.ts +51 -0
  23. package/dist/react/hooks.js +30 -27
  24. package/dist/react/hooks.js.map +1 -1
  25. package/dist/replay.d.ts +15 -0
  26. package/dist/resource.d.ts +32 -0
  27. package/dist/resource.js +80 -78
  28. package/dist/resource.js.map +1 -1
  29. package/dist/runtime-rn.d.ts +0 -1
  30. package/dist/runtime.d.ts +39 -0
  31. package/dist/runtime.js +32 -24
  32. package/dist/runtime.js.map +1 -1
  33. package/dist/store-slices.d.ts +26 -0
  34. package/dist/store-slices.js +31 -20
  35. package/dist/store-slices.js.map +1 -1
  36. package/dist/store.d.ts +15 -0
  37. package/dist/store.js +22 -19
  38. package/dist/store.js.map +1 -1
  39. package/dist/stores.d.ts +64 -0
  40. package/dist/stores.js +31 -22
  41. package/dist/stores.js.map +1 -1
  42. package/dist/sveltekit.d.ts +0 -1
  43. package/dist/sw-bridge.d.ts +24 -0
  44. package/dist/sw-bridge.js +69 -54
  45. package/dist/sw-bridge.js.map +1 -1
  46. package/dist/testing.cjs +3 -2
  47. package/dist/testing.d.ts +1 -2
  48. package/dist/testing.js +3 -2
  49. package/dist/types.d.ts +305 -0
  50. package/dist/types.js +19 -8
  51. package/dist/types.js.map +1 -1
  52. package/dist/version.d.ts +1 -0
  53. package/dist/version.js +1 -1
  54. package/dist/version.js.map +1 -1
  55. package/dist/vite.d.ts +0 -1
  56. package/package.json +9 -7
package/dist/resource.js CHANGED
@@ -1,79 +1,81 @@
1
1
  import { useEidosStore as h } from "./store.js";
2
- import { sendToWorker as p } from "./sw-bridge.js";
3
- var d = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), g = null;
4
- function _(e) {
5
- g = e;
2
+ import { sendToWorker as g } from "./sw-bridge.js";
3
+ var d = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), v = null;
4
+ function q(e) {
5
+ v = e;
6
6
  }
7
- function f(e) {
7
+ function u(e) {
8
8
  return e.includes("*") || /:[^/]+/.test(e);
9
9
  }
10
- function k(e) {
10
+ function R(e) {
11
11
  return "^" + e.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+").replace(/:[^/]+/g, "[^/]+") + "$";
12
12
  }
13
- function m(e, s) {
14
- const a = b(s), t = f(e) ? k(e) : void 0, r = {
13
+ function w(e, a) {
14
+ const s = k(a), t = u(e) ? R(e) : void 0, r = {
15
15
  url: e,
16
- config: s,
17
- strategy: a,
16
+ config: a,
17
+ strategy: s,
18
18
  status: "idle",
19
19
  cacheHits: 0,
20
20
  cacheMisses: 0
21
21
  };
22
- return h.getState().registerResource(e, r), p({
22
+ return h.getState().registerResource(e, r), g({
23
23
  type: "EIDOS_REGISTER_RESOURCE",
24
24
  url: e,
25
- strategy: a.swStrategy,
26
- cacheName: a.cacheName,
27
- ...t !== void 0 && { pattern: t }
25
+ strategy: s.swStrategy,
26
+ cacheName: s.cacheName,
27
+ ...t !== void 0 && { pattern: t },
28
+ ...a.maxAge !== void 0 && { maxAge: a.maxAge },
29
+ ...a.maxEntries !== void 0 && { maxEntries: a.maxEntries }
28
30
  }), {
29
- strategy: a,
31
+ strategy: s,
30
32
  regexStr: t
31
33
  };
32
34
  }
33
- function y(e, s, a) {
35
+ function y(e, a, s) {
34
36
  return async () => {
35
- p({
37
+ g({
36
38
  type: "EIDOS_CLEAR_CACHE",
37
39
  url: e
38
40
  });
39
- const t = await caches.open(s.cacheName).catch(() => null);
41
+ const t = await caches.open(a.cacheName).catch(() => null);
40
42
  if (t) {
41
- const r = await t.keys(), n = a ? new RegExp(a) : null, c = e.startsWith("http");
42
- await Promise.all(r.filter((i) => {
43
- const o = i.url, u = new URL(o).pathname;
44
- return n ? n.test(c ? o : u) : c ? o === e : o === e || u === e;
45
- }).map((i) => t.delete(i)));
43
+ const r = await t.keys(), n = s ? new RegExp(s) : null, i = e.startsWith("http");
44
+ await Promise.all(r.filter((c) => {
45
+ const o = c.url, f = new URL(o).pathname;
46
+ return n ? n.test(i ? o : f) : i ? o === e : o === e || f === e;
47
+ }).map((c) => t.delete(c)));
46
48
  }
47
- f(e) || h.getState().updateResource(e, {
49
+ u(e) || h.getState().updateResource(e, {
48
50
  status: "stale",
49
51
  cachedAt: void 0,
50
52
  lastEvent: "cache-cleared",
51
53
  cacheHits: 0,
52
54
  cacheMisses: 0
53
- }), g?.(["eidos", e]);
55
+ }), v?.(["eidos", e]);
54
56
  };
55
57
  }
56
58
  function E(e) {
57
59
  return () => {
58
- d.delete(e), p({
60
+ d.delete(e), g({
59
61
  type: "EIDOS_UNREGISTER_RESOURCE",
60
62
  url: e
61
63
  }), h.getState().unregisterResource(e);
62
64
  };
63
65
  }
64
- function A(e, s) {
65
- if (f(e)) throw new Error(`[eidos] resource('${e}') is a URL pattern — use resourcePattern('${e}', config) instead. Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`);
66
+ function _(e, a) {
67
+ 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
68
  if (d.has(e)) return d.get(e);
67
- const { strategy: a } = m(e, s), t = {
69
+ const { strategy: s } = w(e, a), t = {
68
70
  url: e,
69
- config: s,
70
- strategy: a,
71
+ config: a,
72
+ strategy: s,
71
73
  fetch: async () => {
72
74
  const r = l.get(e);
73
- if (r) return r.then((c) => c.clone());
74
- const n = S(e, s, a);
75
+ if (r) return r.then((i) => i.clone());
76
+ const n = S(e, a, s);
75
77
  return l.set(e, n), n.finally(() => l.delete(e)).catch(() => {
76
- }), n.then((c) => c.clone());
78
+ }), n.then((i) => i.clone());
77
79
  },
78
80
  json: async () => (await t.fetch()).json(),
79
81
  query: () => ({
@@ -83,47 +85,47 @@ function A(e, s) {
83
85
  prefetch: async () => {
84
86
  await t.fetch();
85
87
  },
86
- invalidate: y(e, a, void 0),
88
+ invalidate: y(e, s, void 0),
87
89
  unregister: E(e)
88
90
  };
89
91
  return d.set(e, t), t;
90
92
  }
91
- function $(e, s) {
92
- if (!f(e)) throw new Error(`[eidos] resourcePattern('${e}') is not a URL pattern — use resource('${e}', config) instead.`);
93
+ function $(e, a) {
94
+ if (!u(e)) throw new Error(`[eidos] resourcePattern('${e}') is not a URL pattern — use resource('${e}', config) instead.`);
93
95
  if (d.has(e)) return d.get(e);
94
- const { strategy: a, regexStr: t } = m(e, s), r = {
96
+ const { strategy: s, regexStr: t } = w(e, a), r = {
95
97
  url: e,
96
- config: s,
97
- strategy: a,
98
- invalidate: y(e, a, t),
98
+ config: a,
99
+ strategy: s,
100
+ invalidate: y(e, s, t),
99
101
  unregister: E(e)
100
102
  };
101
103
  return d.set(e, r), r;
102
104
  }
103
- async function S(e, s, a) {
105
+ async function S(e, a, s) {
104
106
  const t = h.getState();
105
107
  t.updateResource(e, {
106
108
  status: "fetching",
107
109
  fetchedAt: Date.now()
108
110
  });
109
- const r = await caches.open(a.cacheName).catch(() => null);
111
+ const r = await caches.open(s.cacheName).catch(() => null);
110
112
  try {
111
- if (a.swStrategy !== "network-first") {
112
- const i = r ? await r.match(e).catch(() => null) : null, o = h.getState().resources[e], u = s.maxAge !== void 0 && o?.cachedAt !== void 0 && Date.now() - o.cachedAt > s.maxAge;
113
- if (i && !u)
113
+ if (s.swStrategy !== "network-first") {
114
+ const c = r ? await r.match(e).catch(() => null) : null, o = h.getState().resources[e], f = a.maxAge !== void 0 && o?.cachedAt !== void 0 && Date.now() - o.cachedAt > a.maxAge;
115
+ if (c && !f)
114
116
  return t.updateResource(e, {
115
117
  status: "fresh",
116
118
  lastEvent: "cache-hit",
117
119
  cacheHits: (o?.cacheHits ?? 0) + 1
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, {
120
+ }), s.swStrategy === "stale-while-revalidate" && fetch(e, { signal: AbortSignal.timeout(5e3) }).then(async (m) => {
121
+ m.ok && r && (await r.put(e, m.clone()), h.getState().updateResource(e, {
120
122
  cachedAt: Date.now(),
121
123
  lastEvent: "cache-updated"
122
124
  }));
123
125
  }).catch(() => {
124
- }), i;
125
- const R = h.getState().resources[e];
126
- t.updateResource(e, { cacheMisses: (R?.cacheMisses ?? 0) + 1 });
126
+ }), c;
127
+ const x = h.getState().resources[e];
128
+ t.updateResource(e, { cacheMisses: (x?.cacheMisses ?? 0) + 1 });
127
129
  }
128
130
  const n = await fetch(e);
129
131
  if (n.ok)
@@ -133,30 +135,30 @@ async function S(e, s, a) {
133
135
  lastEvent: "cache-updated"
134
136
  }), n;
135
137
  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
+ const i = n.headers.get("X-Eidos-Offline") === "true";
139
+ throw new Error(i ? `offline: no cached response for ${e}` : `${n.status} ${n.statusText}`);
138
140
  } catch (n) {
139
- const c = r ? await r.match(e).catch(() => null) : null;
140
- if (c) {
141
- const i = h.getState().resources[e];
141
+ const i = r ? await r.match(e).catch(() => null) : null;
142
+ if (i) {
143
+ const c = h.getState().resources[e];
142
144
  return t.updateResource(e, {
143
145
  status: "fresh",
144
146
  lastEvent: "cache-hit",
145
- cacheHits: (i?.cacheHits ?? 0) + 1
146
- }), c;
147
+ cacheHits: (c?.cacheHits ?? 0) + 1
148
+ }), i;
147
149
  }
148
150
  throw t.updateResource(e, { status: "error" }), n;
149
151
  }
150
152
  }
151
- function b(e) {
152
- const s = e.strategy;
153
- return e.offline ? v(s ?? "stale-while-revalidate", e.cacheName, e.version) : v(s ?? "network-first", e.cacheName, e.version);
153
+ function k(e) {
154
+ const a = e.strategy;
155
+ return e.offline ? p(a ?? "stale-while-revalidate", e.cacheName, e.version) : p(a ?? "network-first", e.cacheName, e.version);
154
156
  }
155
- var x = {
157
+ var A = {
156
158
  "stale-while-revalidate": "StaleWhileRevalidate",
157
159
  "cache-first": "CacheFirst",
158
160
  "network-first": "NetworkFirst"
159
- }, C = {
161
+ }, b = {
160
162
  "stale-while-revalidate": {
161
163
  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.",
162
164
  behavior: [
@@ -165,10 +167,10 @@ var x = {
165
167
  "Offline → return cached version if available, 503 if not",
166
168
  "Reconnect → next request triggers a background refresh"
167
169
  ],
168
- equivalentCode: `// Workbox equivalent
170
+ equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)
169
171
  new StaleWhileRevalidate({
170
172
  cacheName: 'eidos-resources-v1',
171
- plugins: [new ExpirationPlugin({ maxEntries: 60 })],
173
+ plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],
172
174
  })`
173
175
  },
174
176
  "cache-first": {
@@ -177,12 +179,12 @@ new StaleWhileRevalidate({
177
179
  "Cache hit → return immediately, no network request made",
178
180
  "Cache miss → fetch from network, cache the response, return it",
179
181
  "Offline → return cached version, 503 if cache is empty",
180
- "Cache never expires unless explicitly invalidated"
182
+ "Cache never expires unless maxAge is set or explicitly invalidated"
181
183
  ],
182
- equivalentCode: `// Workbox equivalent
184
+ equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)
183
185
  new CacheFirst({
184
186
  cacheName: 'eidos-resources-v1',
185
- plugins: [new ExpirationPlugin({ maxEntries: 60 })],
187
+ plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],
186
188
  })`
187
189
  },
188
190
  "network-first": {
@@ -200,29 +202,29 @@ new NetworkFirst({
200
202
  })`
201
203
  }
202
204
  };
203
- function v(e, s, a) {
204
- const t = C[e], r = s ?? "eidos-resources-v1";
205
+ function p(e, a, s) {
206
+ const t = b[e], r = a ?? "eidos-resources-v1";
205
207
  return {
206
- name: x[e],
208
+ name: A[e],
207
209
  swStrategy: e,
208
- cacheName: a !== void 0 ? `${r}-v${a}` : r,
210
+ cacheName: s !== void 0 ? `${r}-v${s}` : r,
209
211
  reasoning: t.reasoning,
210
212
  behavior: t.behavior,
211
213
  equivalentCode: ""
212
214
  };
213
215
  }
214
216
  async function O(e) {
215
- const s = await Promise.allSettled(e.map((t) => t.prefetch())), a = s.filter((t) => t.status === "rejected").map((t) => t.reason);
217
+ const a = await Promise.allSettled(e.map((t) => t.prefetch())), s = a.filter((t) => t.status === "rejected").map((t) => t.reason);
216
218
  return {
217
- warmed: s.filter((t) => t.status === "fulfilled").length,
218
- failed: a.length,
219
- errors: a
219
+ warmed: a.filter((t) => t.status === "fulfilled").length,
220
+ failed: s.length,
221
+ errors: s
220
222
  };
221
223
  }
222
224
  export {
223
- A as resource,
225
+ _ as resource,
224
226
  $ as resourcePattern,
225
- _ as setQueryInvalidator,
227
+ q as setQueryInvalidator,
226
228
  O as warmCache
227
229
  };
228
230
 
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","names":[],"sources":["../src/resource.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { sendToWorker } from './sw-bridge';\nimport type {\n ResourceConfig,\n ResourceHandle,\n PatternResourceHandle,\n ResourceEntry,\n GeneratedStrategy,\n CacheStrategy,\n WarmCacheResult,\n} from './types';\n\nconst _registry = new Map<string, ResourceHandle | PatternResourceHandle>();\n\n// ── Request deduplication ─────────────────────────────────────────────────────\n// If multiple callers invoke handle.fetch() simultaneously for the same URL,\n// only one network request is made. Each caller gets its own cloned Response.\n// Keyed by URL; entry is deleted when the request settles.\nconst _inflightRequests = /* @__PURE__ */ new Map<string, Promise<Response>>();\n\n// ── TanStack Query bridge (optional) ─────────────────────────────────────────\n// Set by @sweidos/eidos/query when withEidosQueryClient() is called.\n// Lets handle.invalidate() also invalidate the matching TQ cache entry.\ntype QueryInvalidator = (queryKey: [string, string]) => void;\nlet _queryInvalidator: QueryInvalidator | null = null;\n\n/** @internal Called by @sweidos/eidos/query. */\nexport function setQueryInvalidator(fn: QueryInvalidator): void {\n _queryInvalidator = fn;\n}\n\n// ── URL pattern helpers ───────────────────────────────────────────────────────\n\n/** Returns true if `url` contains wildcard or :param segments. */\nfunction isPattern(url: string): boolean {\n return url.includes('*') || /:[^/]+/.test(url);\n}\n\n/**\n * Converts a URL pattern to a regex source string for SW fetch matching.\n * `**` → multi-segment wildcard (`.+`)\n * `*` → single-segment wildcard (`[^/]+`)\n * `:param` → named single segment (`[^/]+`)\n *\n * Special regex characters in the pattern (e.g. `.`) are escaped first so\n * they match literally.\n *\n * @example\n * patternToRegexStr('/api/products/*') // '^/api/products/[^/]+$'\n * patternToRegexStr('/api/products/**') // '^/api/products/.+$'\n * patternToRegexStr('/api/users/:id') // '^/api/users/[^/]+$'\n */\nfunction patternToRegexStr(pattern: string): string {\n // Escape all regex-special chars except `*`, `/`, `:` (handled below)\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return (\n '^' +\n escaped\n .replace(/\\*\\*/g, '.+') // ** → multi-segment wildcard\n .replace(/\\*/g, '[^/]+') // * → single-segment wildcard\n .replace(/:[^/]+/g, '[^/]+') + // :param → single-segment wildcard\n '$'\n );\n}\n\n/** Shared setup for resource()/resourcePattern(): strategy derivation, store + SW registration. */\nfunction _register(\n url: string,\n config: ResourceConfig,\n): { strategy: GeneratedStrategy; regexStr: string | undefined } {\n const strategy = deriveStrategy(config);\n const regexStr = isPattern(url) ? patternToRegexStr(url) : undefined;\n\n const entry: ResourceEntry = {\n url,\n config,\n strategy,\n status: 'idle',\n cacheHits: 0,\n cacheMisses: 0,\n };\n\n useEidosStore.getState().registerResource(url, entry);\n\n sendToWorker({\n type: 'EIDOS_REGISTER_RESOURCE',\n url,\n strategy: strategy.swStrategy,\n cacheName: strategy.cacheName,\n ...(regexStr !== undefined && { pattern: regexStr }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'cache-first': {\n reasoning:\n 'cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.',\n behavior: [\n 'Cache hit → return immediately, no network request made',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version, 503 if cache is empty',\n 'Cache never expires unless explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: 60 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n"],"mappings":"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"}
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 ...(config.maxAge !== undefined && { maxAge: config.maxAge }),\n ...(config.maxEntries !== undefined && { maxEntries: config.maxEntries }),\n });\n\n return { strategy, regexStr };\n}\n\nfunction _invalidate(\n url: string,\n strategy: GeneratedStrategy,\n regexStr: string | undefined,\n): () => Promise<void> {\n return async () => {\n sendToWorker({ type: 'EIDOS_CLEAR_CACHE', url });\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n if (cache) {\n const keys = await cache.keys();\n const patternRe = regexStr ? new RegExp(regexStr) : null;\n const isCrossOrigin = url.startsWith('http');\n await Promise.all(\n keys\n .filter((r) => {\n const rUrl = r.url;\n const p = new URL(rUrl).pathname;\n if (patternRe) {\n // Cross-origin patterns were compiled from absolute URLs; test full URL.\n return patternRe.test(isCrossOrigin ? rUrl : p);\n }\n return isCrossOrigin ? rUrl === url : rUrl === url || p === url;\n })\n .map((r) => cache.delete(r)),\n );\n }\n // For exact-URL resources update the store entry; patterns don't have a\n // single entry to update (individual URLs are not tracked per-pattern).\n if (!isPattern(url)) {\n useEidosStore.getState().updateResource(url, {\n status: 'stale',\n cachedAt: undefined,\n lastEvent: 'cache-cleared',\n cacheHits: 0,\n cacheMisses: 0,\n });\n }\n // Notify TanStack Query bridge if registered.\n _queryInvalidator?.(['eidos', url]);\n };\n}\n\nfunction _unregister(url: string): () => void {\n return () => {\n _registry.delete(url);\n sendToWorker({ type: 'EIDOS_UNREGISTER_RESOURCE', url });\n useEidosStore.getState().unregisterResource(url);\n };\n}\n\nfunction _warnIfReregisteredWithDifferentConfig(\n url: string,\n existing: ResourceHandle | PatternResourceHandle,\n config: ResourceConfig,\n factoryName: string,\n): void {\n if (!import.meta.env.DEV) return;\n const existingCfg = existing.config;\n if (\n existingCfg.offline !== config.offline ||\n existingCfg.strategy !== config.strategy ||\n existingCfg.cacheName !== config.cacheName ||\n existingCfg.version !== config.version\n ) {\n console.warn(\n `[eidos] ${factoryName}('${url}') already registered with a different config — returning cached handle. Call handle.unregister() first to re-register.`,\n { registered: existingCfg, ignored: config },\n );\n }\n}\n\n// ── resource() ────────────────────────────────────────────────────────────────\n\n/**\n * Registers a concrete-URL resource. For URL patterns (`/api/products/*`,\n * `/api/users/:id`, `**`), use `resourcePattern()` instead.\n */\nexport function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T> {\n if (isPattern(url)) {\n throw new Error(\n `[eidos] resource('${url}') is a URL pattern — use resourcePattern('${url}', config) instead. ` +\n `Pattern handles only support invalidate()/unregister(); the SW intercepts matching requests automatically.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resource');\n return existing as ResourceHandle<T>;\n }\n\n const { strategy } = _register(url, config);\n\n const handle: ResourceHandle<T> = {\n url,\n config,\n strategy,\n\n fetch: async () => {\n // ── Deduplication: coalesce concurrent fetches for the same URL ─────\n // If a request is already in-flight, piggyback on it and return a clone\n // so each caller gets an independent readable Response body.\n const existing = _inflightRequests.get(url);\n if (existing) return existing.then((r) => r.clone());\n\n // Store the raw-response promise. All callers (including the primary)\n // receive a clone — the raw response stays unconsumed in the map so\n // any caller arriving while the promise is still pending can clone it.\n const task = _fetchResource(url, config, strategy);\n _inflightRequests.set(url, task);\n // .catch() silences the unhandled-rejection on the cleanup promise;\n // the error still propagates to callers via task.then() below.\n task.finally(() => _inflightRequests.delete(url)).catch(() => {});\n return task.then((r) => r.clone());\n },\n\n json: async () => {\n const res = await handle.fetch();\n return res.json() as Promise<T>;\n },\n\n query: () => ({\n queryKey: ['eidos', url] as [string, string],\n queryFn: () => handle.json(),\n }),\n\n prefetch: async () => {\n await handle.fetch();\n },\n\n invalidate: _invalidate(url, strategy, undefined),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── resourcePattern() ────────────────────────────────────────────────────────\n\n/**\n * Registers a URL pattern (`/api/products/*`, `/api/users/:id`, `**`). The SW\n * intercepts all matching requests automatically — there is no single URL to\n * fetch/cache directly, so the returned handle only supports cache management\n * (`invalidate`/`unregister`). For a fetchable resource, use `resource()`.\n */\nexport function resourcePattern(url: string, config: ResourceConfig): PatternResourceHandle {\n if (!isPattern(url)) {\n throw new Error(\n `[eidos] resourcePattern('${url}') is not a URL pattern — use resource('${url}', config) instead.`,\n );\n }\n\n if (_registry.has(url)) {\n const existing = _registry.get(url)!;\n _warnIfReregisteredWithDifferentConfig(url, existing, config, 'resourcePattern');\n return existing as PatternResourceHandle;\n }\n\n const { strategy, regexStr } = _register(url, config);\n\n const handle: PatternResourceHandle = {\n url,\n config,\n strategy,\n invalidate: _invalidate(url, strategy, regexStr),\n unregister: _unregister(url),\n };\n\n _registry.set(url, handle);\n return handle;\n}\n\n// ── _fetchResource ─────────────────────────────────────────────────────────────\n// The actual network/cache implementation. Separated from handle.fetch() so the\n// deduplication wrapper can store the Promise and share it across concurrent callers.\n// Returns the raw (unconsumed) Response — callers MUST .clone() before reading body.\nasync function _fetchResource(\n url: string,\n config: ResourceConfig,\n strategy: GeneratedStrategy,\n): Promise<Response> {\n const store = useEidosStore.getState();\n store.updateResource(url, { status: 'fetching', fetchedAt: Date.now() });\n\n // Open cache once and reuse across try/catch — avoids a redundant\n // caches.open() call in the error fallback path.\n const cache = await caches.open(strategy.cacheName).catch(() => null);\n\n try {\n // ── network-first: skip cache check, go straight to network ─────────\n // For cache-first / SWR the cache check below is correct. For\n // network-first, reading cache first and returning early would\n // contradict the strategy — fresh data is the priority.\n if (strategy.swStrategy !== 'network-first') {\n // ── Direct Cache API check ─────────────────────────────────────────\n // We read the cache in the main thread rather than waiting for\n // an async SW postMessage. This gives instant, reliable status\n // updates regardless of SW message timing.\n const cached = cache ? await cache.match(url).catch(() => null) : null;\n\n // Treat cache as miss if maxAge exceeded\n const current = useEidosStore.getState().resources[url];\n const expired =\n config.maxAge !== undefined &&\n current?.cachedAt !== undefined &&\n Date.now() - current.cachedAt > config.maxAge;\n\n if (cached && !expired) {\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n\n // Background revalidation for SWR (stale-while-revalidate)\n if (strategy.swStrategy === 'stale-while-revalidate') {\n fetch(url, { signal: AbortSignal.timeout(5000) })\n .then(async (resp) => {\n if (resp.ok && cache) {\n await cache.put(url, resp.clone());\n useEidosStore.getState().updateResource(url, {\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n }\n })\n .catch(() => {\n /* offline or timed out — cached version stays valid */\n });\n }\n\n return cached;\n }\n\n // Cache miss (or expired)\n const storeEntry = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n cacheMisses: (storeEntry?.cacheMisses ?? 0) + 1,\n });\n }\n\n const response = await fetch(url);\n\n if (response.ok) {\n if (cache) await cache.put(url, response.clone());\n store.updateResource(url, {\n status: 'fresh',\n cachedAt: Date.now(),\n lastEvent: 'cache-updated',\n });\n return response;\n }\n\n // Non-2xx response (e.g. 503 from offline SW) — update status and throw\n // so callers get a proper error instead of a plain-object body they can't use.\n store.updateResource(url, { status: response.status === 503 ? 'offline' : 'error' });\n\n // Check if the SW tagged this as an offline response\n const isOffline = response.headers.get('X-Eidos-Offline') === 'true';\n throw new Error(\n isOffline\n ? `offline: no cached response for ${url}`\n : `${response.status} ${response.statusText}`,\n );\n } catch (err) {\n // Network failure — try cache one more time as fallback\n const fallback = cache ? await cache.match(url).catch(() => null) : null;\n\n if (fallback) {\n const current = useEidosStore.getState().resources[url];\n store.updateResource(url, {\n status: 'fresh',\n lastEvent: 'cache-hit',\n cacheHits: (current?.cacheHits ?? 0) + 1,\n });\n return fallback;\n }\n\n store.updateResource(url, { status: 'error' });\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Strategy derivation — intent → deterministic caching strategy\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction deriveStrategy(config: ResourceConfig): GeneratedStrategy {\n const explicit = config.strategy;\n if (config.offline) {\n return buildStrategy(explicit ?? 'stale-while-revalidate', config.cacheName, config.version);\n }\n return buildStrategy(explicit ?? 'network-first', config.cacheName, config.version);\n}\n\n// Strategy display names — always included (tiny, used by devtools).\nconst STRATEGY_NAMES: Record<CacheStrategy, string> = {\n 'stale-while-revalidate': 'StaleWhileRevalidate',\n 'cache-first': 'CacheFirst',\n 'network-first': 'NetworkFirst',\n};\n\n// Heavy descriptive strings — stripped from production bundles by Vite's\n// import.meta.env.DEV dead-code elimination. Only the names above ship in prod.\ntype StrategyDevInfo = Pick<GeneratedStrategy, 'reasoning' | 'behavior' | 'equivalentCode'>;\nconst _STRATEGY_DEV_META: Record<CacheStrategy, StrategyDevInfo> = {\n 'stale-while-revalidate': {\n reasoning:\n 'offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.',\n behavior: [\n 'Cache hit → return immediately, kick off background revalidation',\n 'Cache miss → fetch from network, cache the response, return it',\n 'Offline → return cached version if available, 503 if not',\n 'Reconnect → next request triggers a background refresh',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew StaleWhileRevalidate({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\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 maxAge is set or explicitly invalidated',\n ],\n equivalentCode: `// Workbox equivalent (maxEntries/maxAge are configured via ResourceConfig)\nnew CacheFirst({\n cacheName: 'eidos-resources-v1',\n plugins: [new ExpirationPlugin({ maxEntries: config.maxEntries, maxAgeSeconds: config.maxAge && config.maxAge / 1000 })],\n})`,\n },\n 'network-first': {\n reasoning:\n 'network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.',\n behavior: [\n 'Always try network first',\n 'Network success → update cache, return fresh response',\n 'Network failure → fall back to cached version',\n 'Offline with empty cache → return 503 error response',\n ],\n equivalentCode: `// Workbox equivalent\nnew NetworkFirst({\n cacheName: 'eidos-resources-v1',\n networkTimeoutSeconds: 3,\n})`,\n },\n};\n\nfunction buildStrategy(\n swStrategy: CacheStrategy,\n cacheName?: string,\n version?: string | number,\n): GeneratedStrategy {\n const meta = _STRATEGY_DEV_META[swStrategy];\n const baseName = cacheName ?? 'eidos-resources-v1';\n return {\n name: STRATEGY_NAMES[swStrategy],\n swStrategy,\n cacheName: version !== undefined ? `${baseName}-v${version}` : baseName,\n // reasoning + behavior are rendered by the playground UI from live ResourceEntry objects —\n // keep them in all builds. equivalentCode is a static code block only used in DEV tools.\n reasoning: meta.reasoning,\n behavior: meta.behavior,\n equivalentCode: import.meta.env.DEV ? meta.equivalentCode : '',\n };\n}\n\n// ── warmCache ─────────────────────────────────────────────────────────────────\n\n/**\n * Bulk-prefetch an array of resource handles concurrently, warming the cache\n * for each one. Useful on login / app init when you know which resources the\n * user will need offline.\n *\n * Pattern handles (containing `*`, `**`, or `:param`) are silently skipped —\n * they match multiple URLs so there is no single URL to prefetch.\n *\n * @example\n * import { warmCache } from '@sweidos/eidos'\n *\n * // In EidosProvider's onReady, or after login:\n * const { warmed, failed } = await warmCache([products, userProfile, settings])\n */\nexport async function warmCache(handles: ResourceHandle[]): Promise<WarmCacheResult> {\n const results = await Promise.allSettled(handles.map((h) => h.prefetch()));\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => r.reason);\n\n if (import.meta.env.DEV && errors.length > 0) {\n console.warn(`[eidos] warmCache: ${errors.length} handle(s) failed to prefetch`, errors);\n }\n\n return {\n warmed: results.filter((r) => r.status === 'fulfilled').length,\n failed: errors.length,\n errors,\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { AsyncStorageLike } from './index.ts';
2
-
3
2
  export interface EidosRNConfig {
4
3
  /** AsyncStorage singleton (or any AsyncStorageLike key-value store). */
5
4
  storage: AsyncStorageLike;
@@ -0,0 +1,39 @@
1
+ import { ReliabilityStats } from './types';
2
+ export interface EidosConfig {
3
+ /** Path to the eidos service worker. Defaults to '/eidos-sw.js'. */
4
+ swPath?: string;
5
+ /** Automatically replay the action queue on reconnect. Default: true. */
6
+ autoReplay?: boolean;
7
+ /**
8
+ * When `true` (default), the new service worker activates immediately when an
9
+ * update is available — matching the pre-v2.3 behaviour. Set to `false` to opt
10
+ * into a toast-then-reload pattern: `onUpdateAvailable` fires instead and you
11
+ * call `triggerSwUpdate()` when the user confirms the reload.
12
+ *
13
+ * Note: avoid calling `triggerSwUpdate()` while `neverLose` actions are mid-replay
14
+ * — the BroadcastChannel/Web-Locks replay coordination survives SW activation, but
15
+ * triggering an update during an active replay pass is unnecessary churn.
16
+ */
17
+ skipWaiting?: boolean;
18
+ /**
19
+ * Called when a new service worker version has installed and is waiting to
20
+ * activate. Use this to show a "reload to update" toast. Only fires when
21
+ * `skipWaiting: false`; with the default `skipWaiting: true` the update
22
+ * applies automatically and this callback is never needed.
23
+ *
24
+ * Call `triggerSwUpdate()` when the user confirms the reload — it tells the
25
+ * waiting SW to activate, then reload the page.
26
+ */
27
+ onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;
28
+ /**
29
+ * Opt-in reliability telemetry. Called with a snapshot of cumulative
30
+ * `neverLose` queue outcome counters (`ReliabilityStats`) every
31
+ * `reliabilityReportInterval` ms — wire this up to your analytics backend.
32
+ * Not called if omitted.
33
+ */
34
+ onReliabilityReport?: (stats: ReliabilityStats) => void;
35
+ /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */
36
+ reliabilityReportInterval?: number;
37
+ }
38
+ export declare function initEidos(config?: EidosConfig): Promise<void>;
39
+ export declare function _resetEidos(): void;
package/dist/runtime.js CHANGED
@@ -1,50 +1,58 @@
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";
6
- import { replayQueue as f } from "./action.js";
7
- import { subscribeReplayOnReconnect as g } from "./replay.js";
8
- async function h(e) {
1
+ import { useEidosStore as n } from "./store.js";
2
+ import { registerBgSyncHandler as p, registerServiceWorker as y } from "./sw-bridge.js";
3
+ import { idbGetQueue as m, idbQueueStorage as d } from "./idb.js";
4
+ import { _getQueueStorage as b } from "./queue-storage.js";
5
+ import { subscribeQueueSync as f } from "./queue-sync.js";
6
+ import { replayQueue as h } from "./action.js";
7
+ import { subscribeReplayOnReconnect as v } from "./replay.js";
8
+ async function R(e) {
9
9
  if (e.schemaVersion === 2 && e.idempotencyKey) return e;
10
10
  const t = {
11
11
  ...e,
12
12
  schemaVersion: 2,
13
13
  idempotencyKey: e.idempotencyKey ?? crypto.randomUUID()
14
14
  };
15
- return await (p() ?? d).update(t.id, {
15
+ return await (b() ?? d).update(t.id, {
16
16
  schemaVersion: t.schemaVersion,
17
17
  idempotencyKey: t.idempotencyKey
18
18
  }).catch(() => {
19
19
  }), t;
20
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;
21
+ var o = !1, s = null, u = null, r = null;
22
+ async function U(e = {}) {
23
+ if (typeof window > "u" || o) return;
24
+ o = !0;
25
+ const t = e.swPath ?? "/eidos-sw.js", l = e.autoReplay ?? !0, c = e.skipWaiting ?? !0;
26
26
  try {
27
- const n = await y();
28
- if (n.length > 0) {
29
- const u = await Promise.all(n.map(h));
30
- a.getState().hydrateQueue(u);
27
+ const i = await m();
28
+ if (i.length > 0) {
29
+ const a = await Promise.all(i.map(R));
30
+ n.getState().hydrateQueue(a);
31
31
  }
32
32
  } catch {
33
33
  }
34
34
  try {
35
- await m(t);
35
+ await y(t, {
36
+ skipWaiting: c,
37
+ onUpdateAvailable: e.onUpdateAvailable
38
+ });
36
39
  } catch {
37
40
  }
38
- c(() => {
39
- a.getState().isOnline && setTimeout(f, 200);
40
- }), s && (i = g()), o = l();
41
+ if (p(() => {
42
+ n.getState().isOnline && setTimeout(h, 200);
43
+ }), l && (s = v()), u = f(), e.onReliabilityReport) {
44
+ const i = e.reliabilityReportInterval ?? 6e4, a = e.onReliabilityReport;
45
+ r = setInterval(() => {
46
+ a(n.getState().reliability);
47
+ }, i);
48
+ }
41
49
  }
42
50
  function V() {
43
- i?.(), i = null, o?.(), o = null, r = !1;
51
+ s?.(), s = null, u?.(), u = null, r && clearInterval(r), r = null, o = !1;
44
52
  }
45
53
  export {
46
54
  V as _resetEidos,
47
- R as initEidos
55
+ U as initEidos
48
56
  };
49
57
 
50
58
  //# sourceMappingURL=runtime.js.map
@@ -1 +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"}
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, ReliabilityStats } 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 * When `true` (default), the new service worker activates immediately when an\n * update is available — matching the pre-v2.3 behaviour. Set to `false` to opt\n * into a toast-then-reload pattern: `onUpdateAvailable` fires instead and you\n * call `triggerSwUpdate()` when the user confirms the reload.\n *\n * Note: avoid calling `triggerSwUpdate()` while `neverLose` actions are mid-replay\n * — the BroadcastChannel/Web-Locks replay coordination survives SW activation, but\n * triggering an update during an active replay pass is unnecessary churn.\n */\n skipWaiting?: boolean;\n /**\n * Called when a new service worker version has installed and is waiting to\n * activate. Use this to show a \"reload to update\" toast. Only fires when\n * `skipWaiting: false`; with the default `skipWaiting: true` the update\n * applies automatically and this callback is never needed.\n *\n * Call `triggerSwUpdate()` when the user confirms the reload — it tells the\n * waiting SW to activate, then reload the page.\n */\n onUpdateAvailable?: (registration: ServiceWorkerRegistration) => void;\n /**\n * Opt-in reliability telemetry. Called with a snapshot of cumulative\n * `neverLose` queue outcome counters (`ReliabilityStats`) every\n * `reliabilityReportInterval` ms — wire this up to your analytics backend.\n * Not called if omitted.\n */\n onReliabilityReport?: (stats: ReliabilityStats) => void;\n /** Interval (ms) between `onReliabilityReport` calls. Default: 60000. */\n reliabilityReportInterval?: number;\n}\n\nlet _initialized = false;\nlet _unsubscribe: (() => void) | null = null;\nlet _unsubscribeQueueSync: (() => void) | null = null;\nlet _reliabilityReportTimer: ReturnType<typeof setInterval> | 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 const skipWaiting = config.skipWaiting ?? 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 skipWaiting,\n onUpdateAvailable: config.onUpdateAvailable,\n });\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 (config.onReliabilityReport) {\n const interval = config.reliabilityReportInterval ?? 60_000;\n const report = config.onReliabilityReport;\n _reliabilityReportTimer = setInterval(() => {\n report(useEidosStore.getState().reliability);\n }, interval);\n }\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 if (_reliabilityReportTimer) clearInterval(_reliabilityReportTimer);\n _reliabilityReportTimer = 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;AAuCA,IAAI,IAAe,IACf,IAAoC,MACpC,IAA6C,MAC7C,IAAiE;AAErE,eAAsB,EAAU,IAAsB,CAAC,GAAkB;AAGvE,MADI,OAAO,SAAW,OAClB,EAAc;AAClB,EAAA,IAAe;AAEf,QAAM,IAAS,EAAO,UAAU,gBAC1B,IAAa,EAAO,cAAc,IAClC,IAAc,EAAO,eAAe;AAG1C,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,GAAQ;AAAA,MAClC,aAAA;AAAA,MACA,mBAAmB,EAAO;AAAA,IAC5B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AAmBA,MAdA,EAAA,MAA4B;AAC1B,IAAI,EAAc,SAAS,EAAE,YAC3B,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAEG,MACF,IAAe,EAA2B,IAK5C,IAAwB,EAAmB,GAEvC,EAAO,qBAAqB;AAC9B,UAAM,IAAW,EAAO,6BAA6B,KAC/C,IAAS,EAAO;AACtB,IAAA,IAA0B,YAAA,MAAkB;AAC1C,MAAA,EAAO,EAAc,SAAS,EAAE,WAAW;AAAA,IAC7C,GAAG,CAAQ;AAAA,EACb;AAUF;AAEA,SAAgB,IAAc;AAC5B,EAAA,IAAe,GACf,IAAe,MACf,IAAwB,GACxB,IAAwB,MACpB,KAAyB,cAAc,CAAuB,GAClE,IAA0B,MAC1B,IAAe;AACjB"}
@@ -0,0 +1,26 @@
1
+ import { ResourceEntry, ActionQueueItem, ReliabilityStats } from './types';
2
+ import { EidosStore } from './store';
3
+ type Setter = (updater: (prev: EidosStore) => Partial<EidosStore>) => void;
4
+ export interface ResourceActions {
5
+ registerResource: (url: string, entry: ResourceEntry) => void;
6
+ updateResource: (url: string, update: Partial<ResourceEntry>) => void;
7
+ unregisterResource: (url: string) => void;
8
+ }
9
+ export declare function createResourceActions(set: Setter): ResourceActions;
10
+ export interface QueueActions {
11
+ addQueueItem: (item: ActionQueueItem) => void;
12
+ updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;
13
+ batchUpdateQueueItems: (updates: Array<{
14
+ id: string;
15
+ update: Partial<ActionQueueItem>;
16
+ }>) => void;
17
+ removeQueueItem: (id: string) => void;
18
+ hydrateQueue: (items: ActionQueueItem[]) => void;
19
+ }
20
+ export declare function createQueueActions(set: Setter): QueueActions;
21
+ export interface ReliabilityActions {
22
+ recordReliabilityEvent: (event: keyof ReliabilityStats) => void;
23
+ resetReliabilityStats: () => void;
24
+ }
25
+ export declare function createReliabilityActions(set: Setter): ReliabilityActions;
26
+ export {};