@sweidos/eidos 1.0.16 → 1.0.19

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/dist/eidos-sw.js CHANGED
@@ -1,229 +1,213 @@
1
- // eidos-sw.js Eidos Service Worker v0.1.0
2
- // Two responsibilities:
3
- // 1. App shell caching — dashboard works offline after first visit
4
- // 2. API resource caching — strategies declared via resource()
5
-
6
- const CACHE_VERSION = 'v1'
7
- const SHELL_CACHE = `eidos-shell-${CACHE_VERSION}`
8
- const API_CACHE = `eidos-resources-${CACHE_VERSION}`
9
-
1
+ const CACHE_VERSION = "v1";
2
+ const CACHE_PREFIX = "eidos";
10
3
  const runtimeConfig = {
11
- resources: new Map(), // pathname → { strategy, cacheName }
12
- simulateOffline: false,
13
- }
14
-
15
- // ── Install ───────────────────────────────────────────────────────────────────
16
-
17
- self.addEventListener('install', (event) => {
18
- event.waitUntil(self.skipWaiting())
19
- })
20
-
21
- // ── Activate ──────────────────────────────────────────────────────────────────
22
-
23
- self.addEventListener('activate', (event) => {
4
+ resources: /* @__PURE__ */ new Map(),
5
+ simulateOffline: false
6
+ };
7
+ self.addEventListener("install", (event) => {
8
+ event.waitUntil(self.skipWaiting());
9
+ });
10
+ self.addEventListener("activate", (event) => {
24
11
  event.waitUntil(
25
12
  Promise.all([
26
13
  self.clients.claim(),
27
- caches.keys().then((keys) =>
28
- Promise.all(
29
- keys
30
- .filter((k) => k.startsWith('eidos') && k !== SHELL_CACHE && k !== API_CACHE)
31
- .map((k) => caches.delete(k))
14
+ // Purge stale caches from previous versions
15
+ caches.keys().then(
16
+ (keys) => Promise.all(
17
+ keys.filter((k) => k.startsWith(CACHE_PREFIX) && !k.endsWith(CACHE_VERSION)).map((k) => caches.delete(k))
32
18
  )
33
- ),
19
+ )
34
20
  ])
35
- )
36
- })
37
-
38
- // ── Message channel ───────────────────────────────────────────────────────────
39
-
40
- self.addEventListener('message', (event) => {
41
- const data = event.data
42
- if (!data?.type) return
43
-
21
+ );
22
+ });
23
+ self.addEventListener("message", (event) => {
24
+ const data = event.data;
25
+ if (!data?.type) return;
44
26
  switch (data.type) {
45
- case 'EIDOS_REGISTER_RESOURCE':
46
- runtimeConfig.resources.set(data.url, {
27
+ case "EIDOS_REGISTER_RESOURCE": {
28
+ const url = data.url;
29
+ const patternSrc = data.pattern;
30
+ runtimeConfig.resources.set(url, {
47
31
  strategy: data.strategy,
48
- cacheName: data.cacheName ?? API_CACHE,
49
- })
50
- event.source?.postMessage({ type: 'EIDOS_RESOURCE_REGISTERED', url: data.url })
51
- break
52
-
53
- case 'EIDOS_UNREGISTER_RESOURCE':
54
- runtimeConfig.resources.delete(data.url)
55
- break
56
-
57
- case 'EIDOS_SIMULATE_OFFLINE':
58
- runtimeConfig.simulateOffline = data.enabled
59
- break
60
-
61
- case 'EIDOS_CLEAR_CACHE': {
62
- caches.open(API_CACHE).then(async (cache) => {
63
- if (data.url) {
64
- const keys = await cache.keys()
32
+ cacheName: data.cacheName ?? `${CACHE_PREFIX}-resources-${CACHE_VERSION}`,
33
+ ...patternSrc !== void 0 && { pattern: new RegExp(patternSrc) }
34
+ });
35
+ event.source?.postMessage({ type: "EIDOS_RESOURCE_REGISTERED", url });
36
+ break;
37
+ }
38
+ case "EIDOS_UNREGISTER_RESOURCE": {
39
+ runtimeConfig.resources.delete(data.url);
40
+ break;
41
+ }
42
+ case "EIDOS_SIMULATE_OFFLINE": {
43
+ runtimeConfig.simulateOffline = data.enabled;
44
+ break;
45
+ }
46
+ case "EIDOS_CLEAR_CACHE": {
47
+ const targetUrl = data.url;
48
+ const reg = targetUrl ? runtimeConfig.resources.get(targetUrl) : void 0;
49
+ const cacheName = reg?.cacheName ?? `${CACHE_PREFIX}-resources-${CACHE_VERSION}`;
50
+ caches.open(cacheName).then(async (cache) => {
51
+ if (targetUrl) {
52
+ const keys = await cache.keys();
53
+ const isCrossOrigin = targetUrl.startsWith("http");
65
54
  await Promise.all(
66
- keys.filter((r) => new URL(r.url).pathname === data.url).map((r) => cache.delete(r))
67
- )
55
+ keys.filter((req) => {
56
+ const reqUrl = req.url;
57
+ const p = new URL(reqUrl).pathname;
58
+ if (reg?.pattern) {
59
+ return reg.pattern.test(isCrossOrigin ? reqUrl : p);
60
+ }
61
+ return isCrossOrigin ? reqUrl === targetUrl : p === targetUrl;
62
+ }).map((req) => cache.delete(req))
63
+ );
68
64
  } else {
69
- const keys = await cache.keys()
70
- await Promise.all(keys.map((k) => cache.delete(k)))
65
+ await cache.keys().then((keys) => Promise.all(keys.map((k) => cache.delete(k))));
71
66
  }
72
- notifyClients({ type: 'EIDOS_CACHE_CLEARED', url: data.url })
73
- })
74
- break
67
+ notifyClients({ type: "EIDOS_CACHE_CLEARED", url: targetUrl });
68
+ });
69
+ break;
75
70
  }
76
-
77
- case 'EIDOS_PING':
78
- event.source?.postMessage({ type: 'EIDOS_PONG' })
79
- break
71
+ case "EIDOS_PING":
72
+ event.source?.postMessage({ type: "EIDOS_PONG" });
73
+ break;
80
74
  }
81
- })
82
-
83
- // ── Fetch interception ────────────────────────────────────────────────────────
84
-
85
- self.addEventListener('fetch', (event) => {
86
- if (event.request.method !== 'GET') return
87
-
88
- const url = new URL(event.request.url)
89
- const pathname = url.pathname
90
-
91
- // 1. Registered API resources
92
- const reg = runtimeConfig.resources.get(pathname)
93
- if (reg) {
94
- event.respondWith(handleApiResource(event.request, pathname, reg))
95
- return
75
+ });
76
+ self.addEventListener("fetch", (event) => {
77
+ if (event.request.method !== "GET") return;
78
+ const requestUrl = event.request.url;
79
+ const pathname = new URL(requestUrl).pathname;
80
+ let reg = runtimeConfig.resources.get(requestUrl) ?? runtimeConfig.resources.get(pathname);
81
+ if (!reg) {
82
+ for (const [key, registration] of runtimeConfig.resources) {
83
+ if (!registration.pattern) continue;
84
+ const target = key.startsWith("http") ? requestUrl : pathname;
85
+ if (registration.pattern.test(target)) {
86
+ reg = registration;
87
+ break;
88
+ }
89
+ }
96
90
  }
97
-
98
- // 2. App shell — same-origin non-API assets (HTML, JS, CSS, fonts, images)
99
- // Stale-while-revalidate: serve from cache instantly, refresh in background.
100
- if (url.origin === self.location.origin && !pathname.startsWith('/api/')) {
101
- event.respondWith(appShell(event.request))
91
+ if (!reg) return;
92
+ if (reg.strategy === "stale-while-revalidate" && !runtimeConfig.simulateOffline) {
93
+ event.respondWith(staleWhileRevalidate(event, event.request, pathname, reg.cacheName));
94
+ return;
102
95
  }
103
- })
104
-
105
- // ── App shell strategy ────────────────────────────────────────────────────────
106
-
107
- async function appShell(request) {
108
- const cache = await caches.open(SHELL_CACHE)
109
- const cached = await cache.match(request)
110
-
111
- const refresh = fetch(request)
112
- .then((resp) => {
113
- if (resp.ok) cache.put(request, resp.clone())
114
- return resp
115
- })
116
- .catch(() => null)
117
-
118
- if (cached) {
119
- refresh // update in background
120
- return cached
96
+ event.respondWith(handleFetch(event.request, pathname, reg));
97
+ });
98
+ async function handleFetch(request, pathname, reg) {
99
+ if (runtimeConfig.simulateOffline) {
100
+ return serveOffline(request, pathname, reg.cacheName);
121
101
  }
122
-
123
- const fresh = await refresh
124
- return fresh ?? new Response(
125
- '<html><body><h2>Offline</h2><p>Visit this page while online first.</p></body></html>',
126
- { status: 503, headers: { 'Content-Type': 'text/html' } }
127
- )
128
- }
129
-
130
- // ── API caching strategies ────────────────────────────────────────────────────
131
-
132
- async function handleApiResource(request, pathname, reg) {
133
- if (runtimeConfig.simulateOffline) return serveOffline(request, pathname, reg.cacheName)
134
-
135
102
  switch (reg.strategy) {
136
- case 'cache-first': return cacheFirst(request, pathname, reg.cacheName)
137
- case 'stale-while-revalidate': return staleWhileRevalidate(request, pathname, reg.cacheName)
138
- case 'network-first': return networkFirst(request, pathname, reg.cacheName)
139
- default: return fetch(request)
103
+ case "cache-first":
104
+ return cacheFirst(request, pathname, reg.cacheName);
105
+ case "stale-while-revalidate":
106
+ return staleWhileRevalidate(null, request, pathname, reg.cacheName);
107
+ case "network-first":
108
+ return networkFirst(request, pathname, reg.cacheName);
109
+ default:
110
+ return fetch(request);
140
111
  }
141
112
  }
142
-
143
113
  async function cacheFirst(request, pathname, cacheName) {
144
- const cache = await caches.open(cacheName)
145
- const cached = await cache.match(request)
114
+ const cache = await caches.open(cacheName);
115
+ const cached = await cache.match(request);
146
116
  if (cached) {
147
- notifyClients({ type: 'EIDOS_CACHE_HIT', url: pathname, strategy: 'cache-first' })
148
- return cached
117
+ notifyClients({ type: "EIDOS_CACHE_HIT", url: pathname, strategy: "cache-first" });
118
+ return cached;
149
119
  }
150
120
  try {
151
- const response = await fetch(request)
121
+ const response = await fetch(request);
152
122
  if (response.ok) {
153
- await cache.put(request, response.clone())
154
- notifyClients({ type: 'EIDOS_CACHE_UPDATED', url: pathname, strategy: 'cache-first' })
123
+ await cache.put(request, response.clone());
124
+ notifyClients({ type: "EIDOS_CACHE_UPDATED", url: pathname, strategy: "cache-first" });
155
125
  }
156
- return response
126
+ return response;
157
127
  } catch {
158
- notifyClients({ type: 'EIDOS_NETWORK_ERROR', url: pathname })
159
- return offlineErrorResponse(pathname)
128
+ notifyClients({ type: "EIDOS_NETWORK_ERROR", url: pathname });
129
+ return offlineErrorResponse(pathname);
160
130
  }
161
131
  }
162
-
163
- async function staleWhileRevalidate(request, pathname, cacheName) {
164
- const cache = await caches.open(cacheName)
165
- const cached = await cache.match(request)
166
-
167
- const revalidate = fetch(request)
168
- .then(async (resp) => {
169
- if (resp.ok) {
170
- await cache.put(request, resp.clone())
171
- notifyClients({ type: 'EIDOS_CACHE_UPDATED', url: pathname, strategy: 'stale-while-revalidate' })
172
- }
173
- return resp
174
- })
175
- .catch(() => {
176
- notifyClients({ type: 'EIDOS_NETWORK_ERROR', url: pathname })
177
- return null
178
- })
179
-
132
+ async function staleWhileRevalidate(event, request, pathname, cacheName) {
133
+ const cache = await caches.open(cacheName);
134
+ const cached = await cache.match(request);
135
+ const revalidatePromise = fetch(request).then(async (response) => {
136
+ if (response.ok) {
137
+ await cache.put(request, response.clone());
138
+ notifyClients({
139
+ type: "EIDOS_CACHE_UPDATED",
140
+ url: pathname,
141
+ strategy: "stale-while-revalidate"
142
+ });
143
+ }
144
+ return response;
145
+ }).catch(() => {
146
+ notifyClients({ type: "EIDOS_NETWORK_ERROR", url: pathname, strategy: "stale-while-revalidate" });
147
+ });
180
148
  if (cached) {
181
- notifyClients({ type: 'EIDOS_CACHE_HIT', url: pathname, strategy: 'stale-while-revalidate' })
182
- return cached
149
+ event?.waitUntil(revalidatePromise);
150
+ notifyClients({
151
+ type: "EIDOS_CACHE_HIT",
152
+ url: pathname,
153
+ strategy: "stale-while-revalidate"
154
+ });
155
+ return cached;
183
156
  }
184
-
185
- const fresh = await revalidate
186
- return fresh ?? offlineErrorResponse(pathname)
157
+ const fresh = await revalidatePromise;
158
+ return fresh ?? offlineErrorResponse(pathname);
187
159
  }
188
-
189
160
  async function networkFirst(request, pathname, cacheName) {
190
- const cache = await caches.open(cacheName)
161
+ const cache = await caches.open(cacheName);
191
162
  try {
192
- const response = await fetch(request)
163
+ const response = await fetch(request, { signal: AbortSignal.timeout(3e3) });
193
164
  if (response.ok) {
194
- await cache.put(request, response.clone())
195
- notifyClients({ type: 'EIDOS_CACHE_UPDATED', url: pathname, strategy: 'network-first' })
165
+ await cache.put(request, response.clone());
166
+ notifyClients({ type: "EIDOS_CACHE_UPDATED", url: pathname, strategy: "network-first" });
196
167
  }
197
- return response
168
+ return response;
198
169
  } catch {
199
- const cached = await cache.match(request)
170
+ const cached = await cache.match(request);
200
171
  if (cached) {
201
- notifyClients({ type: 'EIDOS_CACHE_HIT', url: pathname, strategy: 'network-first' })
202
- return cached
172
+ notifyClients({ type: "EIDOS_CACHE_HIT", url: pathname, strategy: "network-first" });
173
+ return cached;
203
174
  }
204
- notifyClients({ type: 'EIDOS_NETWORK_ERROR', url: pathname })
205
- return offlineErrorResponse(pathname)
175
+ notifyClients({ type: "EIDOS_NETWORK_ERROR", url: pathname });
176
+ return offlineErrorResponse(pathname);
206
177
  }
207
178
  }
208
-
209
179
  async function serveOffline(request, pathname, cacheName) {
210
- const cache = await caches.open(cacheName)
211
- const cached = await cache.match(request)
180
+ const cache = await caches.open(cacheName);
181
+ const cached = await cache.match(request);
212
182
  if (cached) {
213
- notifyClients({ type: 'EIDOS_CACHE_HIT', url: pathname, strategy: 'offline-simulation', simulated: true })
214
- return cached
183
+ notifyClients({ type: "EIDOS_CACHE_HIT", url: pathname, strategy: "offline-simulation", simulated: true });
184
+ return cached;
215
185
  }
216
- return offlineErrorResponse(pathname)
186
+ return offlineErrorResponse(pathname);
217
187
  }
218
-
219
188
  function offlineErrorResponse(pathname) {
220
189
  return new Response(
221
- JSON.stringify({ error: 'offline', message: `No cached response for ${pathname}`, eidos: true }),
222
- { status: 503, headers: { 'Content-Type': 'application/json', 'X-Eidos-Offline': 'true' } }
223
- )
190
+ JSON.stringify({
191
+ error: "offline",
192
+ message: `No cached response available for ${pathname}`,
193
+ eidos: true
194
+ }),
195
+ {
196
+ status: 503,
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ "X-Eidos-Offline": "true"
200
+ }
201
+ }
202
+ );
224
203
  }
225
-
226
204
  async function notifyClients(message) {
227
- const clients = await self.clients.matchAll({ includeUncontrolled: true })
228
- clients.forEach((c) => c.postMessage(message))
205
+ const clients = await self.clients.matchAll({ includeUncontrolled: true });
206
+ clients.forEach((client) => client.postMessage(message));
229
207
  }
208
+ self.addEventListener("sync", (event) => {
209
+ const syncEvent = event;
210
+ if (syncEvent.tag === "eidos-queue-replay") {
211
+ syncEvent.waitUntil(notifyClients({ type: "EIDOS_BACKGROUND_SYNC" }));
212
+ }
213
+ });
package/dist/eidos.cjs.js CHANGED
@@ -58,6 +58,9 @@ const useEidosStore = {
58
58
  };
59
59
  let _registration = null;
60
60
  let _pendingMessages = [];
61
+ function getSwRegistration() {
62
+ return _registration;
63
+ }
61
64
  async function registerServiceWorker(swPath) {
62
65
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
63
66
  useEidosStore.getState().setSwStatus("unsupported");
@@ -106,11 +109,26 @@ function sendToWorker(message) {
106
109
  _pendingMessages.push(message);
107
110
  }
108
111
  }
112
+ let _bgSyncHandler = null;
113
+ function registerBgSyncHandler(fn) {
114
+ _bgSyncHandler = fn;
115
+ }
116
+ function isBgSyncSupported() {
117
+ try {
118
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && _registration !== null && "sync" in _registration;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
109
123
  function onSwMessage(event) {
110
124
  const data = event.data;
111
125
  if (!(data == null ? void 0 : data.type)) return;
112
126
  const store = useEidosStore.getState();
113
127
  const { type, url } = data;
128
+ if (type === "EIDOS_BACKGROUND_SYNC") {
129
+ _bgSyncHandler == null ? void 0 : _bgSyncHandler();
130
+ return;
131
+ }
114
132
  if (!url) return;
115
133
  switch (type) {
116
134
  case "EIDOS_CACHE_HIT": {
@@ -150,6 +168,10 @@ function flushPendingMessages() {
150
168
  _pendingMessages = [];
151
169
  }
152
170
  const _registry = /* @__PURE__ */ new Map();
171
+ let _queryInvalidator = null;
172
+ function setQueryInvalidator(fn) {
173
+ _queryInvalidator = fn;
174
+ }
153
175
  function isPattern(url) {
154
176
  return url.includes("*") || /:[^/]+/.test(url);
155
177
  }
@@ -296,6 +318,7 @@ function resource(url, config) {
296
318
  cacheMisses: 0
297
319
  });
298
320
  }
321
+ _queryInvalidator == null ? void 0 : _queryInvalidator(["eidos", url]);
299
322
  },
300
323
  unregister: () => {
301
324
  _registry.delete(url);
@@ -512,6 +535,13 @@ async function persistAndQueue(actionId, actionName, args, config) {
512
535
  };
513
536
  await idbAddToQueue(item);
514
537
  useEidosStore.getState().addQueueItem(item);
538
+ try {
539
+ const reg = getSwRegistration();
540
+ if (reg && "sync" in reg) {
541
+ await reg.sync.register("eidos-queue-replay");
542
+ }
543
+ } catch {
544
+ }
515
545
  return {
516
546
  queued: true,
517
547
  id,
@@ -605,6 +635,11 @@ async function initEidos(config = {}) {
605
635
  await registerServiceWorker(swPath);
606
636
  } catch {
607
637
  }
638
+ registerBgSyncHandler(() => {
639
+ if (useEidosStore.getState().isOnline) {
640
+ setTimeout(replayQueue, 200);
641
+ }
642
+ });
608
643
  if (autoReplay) {
609
644
  let prevIsOnline = useEidosStore.getState().isOnline;
610
645
  useEidosStore.subscribe(() => {
@@ -711,9 +746,11 @@ exports.eidosResource = eidosResource;
711
746
  exports.eidosStatus = eidosStatus;
712
747
  exports.eidosStore = eidosStore;
713
748
  exports.initEidos = initEidos;
749
+ exports.isBgSyncSupported = isBgSyncSupported;
714
750
  exports.replayQueue = replayQueue;
715
751
  exports.resource = resource;
716
752
  exports.setOfflineSimulation = setOfflineSimulation;
753
+ exports.setQueryInvalidator = setQueryInvalidator;
717
754
  exports.useEidos = useEidos;
718
755
  exports.useEidosAction = useEidosAction;
719
756
  exports.useEidosOnDrain = useEidosOnDrain;