@sweidos/eidos 1.0.16 → 1.0.17

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 CHANGED
@@ -145,6 +145,15 @@ const handle = resource('/api/products', {
145
145
  cacheName?: string, // custom Cache Storage bucket (default: 'eidos-resources-v1')
146
146
  maxAge?: number, // TTL in ms — expired entries are re-fetched from network
147
147
  })
148
+
149
+ // URL patterns — SW intercepts all matching requests automatically
150
+ resource('/api/products/*', { offline: true }) // single segment: /api/products/123
151
+ resource('/api/products/**', { offline: true }) // multi-segment: /api/products/123/reviews
152
+ resource('/api/users/:id', { offline: true }) // named segment: /api/users/abc
153
+
154
+ // Cross-origin resources — pass the full URL (including origin)
155
+ resource('https://api.example.com/products', { offline: true })
156
+ resource('https://cdn.example.com/assets/*', { offline: true }) // patterns work too
148
157
  ```
149
158
 
150
159
  **Auto-selected strategy:**
@@ -287,6 +296,79 @@ const state = useEidos()
287
296
 
288
297
  ---
289
298
 
299
+ ### Vue / Svelte / Vanilla JS Stores
300
+
301
+ Framework-agnostic reactive stores — no React dependency, zero extra peer deps. These implement the [Svelte store contract](https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values) (`subscribe(run): unsubscribe`) so they work natively with Svelte's `$` prefix. They also wire up cleanly in Vue composables and plain JS.
302
+
303
+ ```ts
304
+ import {
305
+ eidosQueue, eidosStatus, eidosQueueStats,
306
+ eidosResource, eidosAction, eidosStore,
307
+ } from '@sweidos/eidos'
308
+ ```
309
+
310
+ **Svelte:**
311
+
312
+ ```svelte
313
+ <script>
314
+ import { eidosQueue, eidosStatus, eidosQueueStats, eidosResource } from '@sweidos/eidos'
315
+ // Use $ prefix — Svelte auto-subscribes and unsubscribes
316
+ </script>
317
+
318
+ <p>Online: {$eidosStatus.isOnline}</p>
319
+ <p>Pending: {$eidosQueueStats.pending}</p>
320
+ <p>Cache hits: {$eidosResource('/api/products')?.cacheHits ?? 0}</p>
321
+
322
+ {#each $eidosQueue as item}
323
+ <div>{item.actionName} — {item.status}</div>
324
+ {/each}
325
+ ```
326
+
327
+ **Vue (Composition API):**
328
+
329
+ ```ts
330
+ import { ref, onUnmounted } from 'vue'
331
+ import { eidosStatus, eidosQueue } from '@sweidos/eidos'
332
+
333
+ export function useEidosStatusVue() {
334
+ const status = ref(eidosStatus.getState())
335
+ const unsub = eidosStatus.subscribe((v) => { status.value = v })
336
+ onUnmounted(unsub)
337
+ return status
338
+ }
339
+
340
+ export function useEidosQueueVue() {
341
+ const queue = ref(eidosQueue.getState())
342
+ const unsub = eidosQueue.subscribe((v) => { queue.value = v })
343
+ onUnmounted(unsub)
344
+ return queue
345
+ }
346
+ ```
347
+
348
+ **Vanilla JS:**
349
+
350
+ ```ts
351
+ import { eidosStatus, eidosResource } from '@sweidos/eidos'
352
+
353
+ const unsub = eidosStatus.subscribe(({ isOnline }) => {
354
+ document.title = isOnline ? 'App' : 'App (offline)'
355
+ })
356
+
357
+ // Read current value once without subscribing
358
+ const hits = eidosResource('/api/products').getState()?.cacheHits ?? 0
359
+ ```
360
+
361
+ | Store | Type | Emits when |
362
+ |-------|------|-----------|
363
+ | `eidosQueue` | `ActionQueueItem[]` | Any queue mutation |
364
+ | `eidosStatus` | `{ isOnline, swStatus, swError }` | Online or SW status changes |
365
+ | `eidosQueueStats` | `{ pending, failed, replaying, total }` | Any queue mutation |
366
+ | `eidosResource(url)` | `ResourceEntry \| undefined` | Resource registered or updated |
367
+ | `eidosAction(id)` | `ActionQueueItem \| undefined` | Item status changes or removal |
368
+ | `eidosStore` | `EidosStore` | Any state change |
369
+
370
+ ---
371
+
290
372
  ### `setOfflineSimulation(enabled)`
291
373
 
292
374
  Toggle offline simulation without physically disconnecting the network.
@@ -300,6 +382,21 @@ setOfflineSimulation(false) // restore normal behaviour
300
382
 
301
383
  ---
302
384
 
385
+ ### `isBgSyncSupported()`
386
+
387
+ Returns `true` when the active SW registration supports the Background Sync API (Chrome 49+, Edge 79+, Safari 16+). Use to conditionally surface sync status in your UI.
388
+
389
+ ```ts
390
+ import { isBgSyncSupported } from '@sweidos/eidos'
391
+
392
+ if (isBgSyncSupported()) {
393
+ // browser will fire 'eidos-queue-replay' sync tag when connectivity returns,
394
+ // even if the user briefly navigated away from the page
395
+ }
396
+ ```
397
+
398
+ ---
399
+
303
400
  ## Performance
304
401
 
305
402
  Performance is a first-class concern in Eidos. Every design decision optimises for low overhead.
@@ -369,6 +466,7 @@ Performance is a first-class concern in Eidos. Every design decision optimises f
369
466
  | `EIDOS_CACHE_UPDATED` | Cache entry refreshed from network |
370
467
  | `EIDOS_NETWORK_ERROR` | Network request failed |
371
468
  | `EIDOS_CACHE_CLEARED` | Cache was cleared |
469
+ | `EIDOS_BACKGROUND_SYNC` | Browser fired `sync` event — runtime calls `replayQueue()` |
372
470
 
373
471
  ---
374
472
 
@@ -428,7 +526,7 @@ function eidosPlugin() {
428
526
  | Limitation | Detail |
429
527
  |------------|--------|
430
528
  | GET-only caching | SW intercepts `GET` only. `POST`/`PUT`/`DELETE` are not cached (but *are* queued via `action()`). |
431
- | Pathname matching | Resources match by pathname. `/api/products?page=2` and `/api/products` share the same SW rule but are cached separately. |
529
+ | Query string ignored | Resources match by pathname (or full URL for cross-origin). `/api/products?page=2` and `/api/products` share the same SW rule but are cached as separate entries. |
432
530
  | Module-scope actions | `action()` must be called at module scope so functions are registered before a page reload triggers queue replay. |
433
531
  | Single SW | `EidosProvider` assumes one SW at `/eidos-sw.js`. Multiple registrations are unsupported. |
434
532
 
@@ -440,11 +538,11 @@ function eidosPlugin() {
440
538
  - [x] Exponential backoff with jitter for queue replay
441
539
  - [x] Per-resource `cacheName` override
442
540
  - [x] `resource.unregister()` for cleanup
443
- - [ ] URL pattern matching (wildcards, regex)
444
- - [ ] Cross-origin resource support
445
- - [ ] Background Sync API integration
541
+ - [x] URL pattern matching (`*`, `**`, `:param`)
542
+ - [x] Cross-origin resource support
543
+ - [x] Background Sync API integration
446
544
  - [ ] Vite plugin (first-class, published separately)
447
- - [ ] Vue / Svelte bindings
545
+ - [x] Vue / Svelte bindings (framework-agnostic reactive stores)
448
546
  - [ ] TanStack Query integration package
449
547
 
450
548
  ---
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": {
@@ -512,6 +530,13 @@ async function persistAndQueue(actionId, actionName, args, config) {
512
530
  };
513
531
  await idbAddToQueue(item);
514
532
  useEidosStore.getState().addQueueItem(item);
533
+ try {
534
+ const reg = getSwRegistration();
535
+ if (reg && "sync" in reg) {
536
+ await reg.sync.register("eidos-queue-replay");
537
+ }
538
+ } catch {
539
+ }
515
540
  return {
516
541
  queued: true,
517
542
  id,
@@ -605,6 +630,11 @@ async function initEidos(config = {}) {
605
630
  await registerServiceWorker(swPath);
606
631
  } catch {
607
632
  }
633
+ registerBgSyncHandler(() => {
634
+ if (useEidosStore.getState().isOnline) {
635
+ setTimeout(replayQueue, 200);
636
+ }
637
+ });
608
638
  if (autoReplay) {
609
639
  let prevIsOnline = useEidosStore.getState().isOnline;
610
640
  useEidosStore.subscribe(() => {
@@ -711,6 +741,7 @@ exports.eidosResource = eidosResource;
711
741
  exports.eidosStatus = eidosStatus;
712
742
  exports.eidosStore = eidosStore;
713
743
  exports.initEidos = initEidos;
744
+ exports.isBgSyncSupported = isBgSyncSupported;
714
745
  exports.replayQueue = replayQueue;
715
746
  exports.resource = resource;
716
747
  exports.setOfflineSimulation = setOfflineSimulation;