@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 +103 -5
- package/dist/eidos-sw.js +164 -180
- package/dist/eidos.cjs.js +31 -0
- package/dist/eidos.cjs.js.map +1 -1
- package/dist/eidos.es.js +31 -0
- package/dist/eidos.es.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/package.json +1 -1
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
|
-
|
|
|
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
|
-
- [
|
|
444
|
-
- [
|
|
445
|
-
- [
|
|
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
|
-
- [
|
|
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
|
-
|
|
2
|
-
|
|
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(),
|
|
12
|
-
simulateOffline: false
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
self.addEventListener(
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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 ??
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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((
|
|
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
|
-
|
|
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:
|
|
73
|
-
})
|
|
74
|
-
break
|
|
67
|
+
notifyClients({ type: "EIDOS_CACHE_CLEARED", url: targetUrl });
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
break
|
|
71
|
+
case "EIDOS_PING":
|
|
72
|
+
event.source?.postMessage({ type: "EIDOS_PONG" });
|
|
73
|
+
break;
|
|
80
74
|
}
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
case
|
|
139
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
159
|
-
return offlineErrorResponse(pathname)
|
|
128
|
+
notifyClients({ type: "EIDOS_NETWORK_ERROR", url: pathname });
|
|
129
|
+
return offlineErrorResponse(pathname);
|
|
160
130
|
}
|
|
161
131
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
202
|
-
return cached
|
|
172
|
+
notifyClients({ type: "EIDOS_CACHE_HIT", url: pathname, strategy: "network-first" });
|
|
173
|
+
return cached;
|
|
203
174
|
}
|
|
204
|
-
notifyClients({ type:
|
|
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
|
|
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:
|
|
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({
|
|
222
|
-
|
|
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((
|
|
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;
|