@sweidos/eidos 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/action.js +167 -94
- package/dist/cli.js +102 -0
- package/dist/devtools.js +80 -20
- package/dist/eidos-sw.js +280 -188
- package/dist/eidos.cjs +2 -2
- package/dist/index.d.ts +99 -4
- package/dist/index.js +42 -39
- package/dist/push.cjs +120 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +113 -0
- package/dist/runtime.js +37 -20
- package/dist/sw-bridge.js +44 -31
- package/dist/version.js +1 -1
- package/package.json +10 -2
package/dist/devtools.js
CHANGED
|
@@ -266,9 +266,18 @@ function _getQueueStorage() {
|
|
|
266
266
|
var _actionRegistry = /* @__PURE__ */ new Map();
|
|
267
267
|
var _rollbackRegistry = /* @__PURE__ */ new Map();
|
|
268
268
|
var _conflictRegistry = /* @__PURE__ */ new Map();
|
|
269
|
+
var _conflictConfigRegistry = /* @__PURE__ */ new Map();
|
|
270
|
+
var _configRegistry = /* @__PURE__ */ new Map();
|
|
271
|
+
var _inflightControllers = /* @__PURE__ */ new Map();
|
|
269
272
|
function qs() {
|
|
270
273
|
return _getQueueStorage() ?? idbQueueStorage;
|
|
271
274
|
}
|
|
275
|
+
function callWithContext(fn, args, ctx) {
|
|
276
|
+
return fn(...args, ctx);
|
|
277
|
+
}
|
|
278
|
+
function isAbortError(err) {
|
|
279
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
280
|
+
}
|
|
272
281
|
function isClientError(err) {
|
|
273
282
|
if (err instanceof Response) return err.status >= 400 && err.status < 500;
|
|
274
283
|
if (typeof err === "object" && err !== null) {
|
|
@@ -280,17 +289,27 @@ function isClientError(err) {
|
|
|
280
289
|
function backoffMs(retryCount) {
|
|
281
290
|
return Math.min(2e3 * 2 ** retryCount, 3e5) * (.8 + Math.random() * .4);
|
|
282
291
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const store = useEidosStore.getState();
|
|
286
|
-
if (!store.isOnline || _replaying) return {
|
|
292
|
+
function emptyReplayResult() {
|
|
293
|
+
return {
|
|
287
294
|
attempted: 0,
|
|
288
295
|
succeeded: 0,
|
|
289
296
|
failed: 0,
|
|
290
297
|
retrying: 0,
|
|
291
298
|
skipped: 0,
|
|
292
|
-
conflicted: 0
|
|
299
|
+
conflicted: 0,
|
|
300
|
+
cancelled: 0
|
|
293
301
|
};
|
|
302
|
+
}
|
|
303
|
+
var _replaying = false;
|
|
304
|
+
var REPLAY_LOCK_NAME = "eidos-queue-replay";
|
|
305
|
+
async function replayQueue() {
|
|
306
|
+
const store = useEidosStore.getState();
|
|
307
|
+
if (!store.isOnline) return emptyReplayResult();
|
|
308
|
+
if (typeof navigator !== "undefined" && navigator.locks) return navigator.locks.request(REPLAY_LOCK_NAME, { ifAvailable: true }, async (lock) => {
|
|
309
|
+
if (!lock) return emptyReplayResult();
|
|
310
|
+
return _doReplayQueue(store);
|
|
311
|
+
});
|
|
312
|
+
if (_replaying) return emptyReplayResult();
|
|
294
313
|
_replaying = true;
|
|
295
314
|
try {
|
|
296
315
|
return await _doReplayQueue(store);
|
|
@@ -301,8 +320,20 @@ async function replayQueue() {
|
|
|
301
320
|
async function _replayItem(item, store) {
|
|
302
321
|
const fn = _actionRegistry.get(item.actionId);
|
|
303
322
|
if (!fn) return "skipped";
|
|
323
|
+
const cancellable = _configRegistry.get(item.actionId)?.cancellable;
|
|
324
|
+
let signal;
|
|
325
|
+
if (cancellable) {
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
_inflightControllers.set(item.idempotencyKey, controller);
|
|
328
|
+
signal = controller.signal;
|
|
329
|
+
}
|
|
330
|
+
const ctx = {
|
|
331
|
+
idempotencyKey: item.idempotencyKey,
|
|
332
|
+
attempt: item.retryCount,
|
|
333
|
+
signal
|
|
334
|
+
};
|
|
304
335
|
try {
|
|
305
|
-
await fn
|
|
336
|
+
await callWithContext(fn, item.args, ctx);
|
|
306
337
|
const completedAt = Date.now();
|
|
307
338
|
store.updateQueueItem(item.id, {
|
|
308
339
|
status: "succeeded",
|
|
@@ -318,15 +349,48 @@ async function _replayItem(item, store) {
|
|
|
318
349
|
}, 3e3);
|
|
319
350
|
return "succeeded";
|
|
320
351
|
} catch (err) {
|
|
352
|
+
if (isAbortError(err)) {
|
|
353
|
+
store.removeQueueItem(item.id);
|
|
354
|
+
await qs().remove(item.id);
|
|
355
|
+
return "cancelled";
|
|
356
|
+
}
|
|
321
357
|
if (isClientError(err)) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
358
|
+
const conflictConfig = _conflictConfigRegistry.get(item.actionId);
|
|
359
|
+
let resolution;
|
|
360
|
+
if (conflictConfig) switch (conflictConfig.strategy) {
|
|
361
|
+
case "serverWins":
|
|
362
|
+
resolution = "skip";
|
|
363
|
+
break;
|
|
364
|
+
case "clientWins":
|
|
365
|
+
case "lastWriteWins":
|
|
366
|
+
resolution = "retry";
|
|
367
|
+
break;
|
|
368
|
+
case "merge":
|
|
369
|
+
case "custom": {
|
|
370
|
+
const ctx = {
|
|
371
|
+
error: err,
|
|
372
|
+
args: item.args,
|
|
373
|
+
attempt: item.retryCount,
|
|
374
|
+
idempotencyKey: item.idempotencyKey
|
|
375
|
+
};
|
|
376
|
+
resolution = conflictConfig.resolve?.(ctx) ?? "retry";
|
|
377
|
+
break;
|
|
328
378
|
}
|
|
329
379
|
}
|
|
380
|
+
else {
|
|
381
|
+
const onConflict = _conflictRegistry.get(item.actionId);
|
|
382
|
+
if (onConflict) resolution = onConflict(err, item.args);
|
|
383
|
+
}
|
|
384
|
+
if (resolution === "skip") {
|
|
385
|
+
store.removeQueueItem(item.id);
|
|
386
|
+
await qs().remove(item.id);
|
|
387
|
+
return "conflicted";
|
|
388
|
+
}
|
|
389
|
+
if (resolution && typeof resolution === "object") {
|
|
390
|
+
item.args = resolution.resolved;
|
|
391
|
+
store.updateQueueItem(item.id, { args: resolution.resolved });
|
|
392
|
+
await qs().update(item.id, { args: resolution.resolved });
|
|
393
|
+
}
|
|
330
394
|
}
|
|
331
395
|
const retryCount = item.retryCount + 1;
|
|
332
396
|
if (retryCount >= item.maxRetries) {
|
|
@@ -356,6 +420,8 @@ async function _replayItem(item, store) {
|
|
|
356
420
|
});
|
|
357
421
|
return "retrying";
|
|
358
422
|
}
|
|
423
|
+
} finally {
|
|
424
|
+
if (cancellable) _inflightControllers.delete(item.idempotencyKey);
|
|
359
425
|
}
|
|
360
426
|
}
|
|
361
427
|
async function _replayTier(items, store, result) {
|
|
@@ -374,6 +440,7 @@ async function _replayTier(items, store, result) {
|
|
|
374
440
|
const outcome = o.status === "fulfilled" ? o.value : "failed";
|
|
375
441
|
if (outcome === "skipped") result.skipped++;
|
|
376
442
|
else if (outcome === "conflicted") result.conflicted++;
|
|
443
|
+
else if (outcome === "cancelled") result.cancelled++;
|
|
377
444
|
else {
|
|
378
445
|
result.attempted++;
|
|
379
446
|
result[outcome]++;
|
|
@@ -384,14 +451,7 @@ async function _doReplayQueue(store) {
|
|
|
384
451
|
const candidates = await qs().getPending();
|
|
385
452
|
const now = Date.now();
|
|
386
453
|
const pending = candidates.filter((item) => item.retryCount < item.maxRetries && (!item.nextRetryAt || item.nextRetryAt <= now));
|
|
387
|
-
const result =
|
|
388
|
-
attempted: 0,
|
|
389
|
-
succeeded: 0,
|
|
390
|
-
failed: 0,
|
|
391
|
-
retrying: 0,
|
|
392
|
-
skipped: 0,
|
|
393
|
-
conflicted: 0
|
|
394
|
-
};
|
|
454
|
+
const result = emptyReplayResult();
|
|
395
455
|
for (const tier of [
|
|
396
456
|
"high",
|
|
397
457
|
"normal",
|
package/dist/eidos-sw.js
CHANGED
|
@@ -1,213 +1,305 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
//#region src/sw.ts
|
|
2
|
+
var CACHE_VERSION = "v1";
|
|
3
|
+
var CACHE_PREFIX = "eidos";
|
|
4
|
+
var runtimeConfig = {
|
|
5
|
+
resources: /* @__PURE__ */ new Map(),
|
|
6
|
+
simulateOffline: false
|
|
6
7
|
};
|
|
7
8
|
self.addEventListener("install", (event) => {
|
|
8
|
-
|
|
9
|
+
event.waitUntil(self.skipWaiting());
|
|
9
10
|
});
|
|
10
11
|
self.addEventListener("activate", (event) => {
|
|
11
|
-
|
|
12
|
-
Promise.all([
|
|
13
|
-
self.clients.claim(),
|
|
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))
|
|
18
|
-
)
|
|
19
|
-
)
|
|
20
|
-
])
|
|
21
|
-
);
|
|
12
|
+
event.waitUntil(Promise.all([self.clients.claim(), caches.keys().then((keys) => Promise.all(keys.filter((k) => k.startsWith(CACHE_PREFIX) && !k.endsWith(CACHE_VERSION)).map((k) => caches.delete(k))))]));
|
|
22
13
|
});
|
|
23
14
|
self.addEventListener("message", (event) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
15
|
+
const data = event.data;
|
|
16
|
+
if (!data?.type) return;
|
|
17
|
+
switch (data.type) {
|
|
18
|
+
case "EIDOS_REGISTER_RESOURCE": {
|
|
19
|
+
const url = data.url;
|
|
20
|
+
const patternSrc = data.pattern;
|
|
21
|
+
runtimeConfig.resources.set(url, {
|
|
22
|
+
strategy: data.strategy,
|
|
23
|
+
cacheName: data.cacheName ?? `${CACHE_PREFIX}-resources-${CACHE_VERSION}`,
|
|
24
|
+
...patternSrc !== void 0 && { pattern: new RegExp(patternSrc) }
|
|
25
|
+
});
|
|
26
|
+
event.source?.postMessage({
|
|
27
|
+
type: "EIDOS_RESOURCE_REGISTERED",
|
|
28
|
+
url
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case "EIDOS_UNREGISTER_RESOURCE":
|
|
33
|
+
runtimeConfig.resources.delete(data.url);
|
|
34
|
+
break;
|
|
35
|
+
case "EIDOS_SIMULATE_OFFLINE":
|
|
36
|
+
runtimeConfig.simulateOffline = data.enabled;
|
|
37
|
+
break;
|
|
38
|
+
case "EIDOS_CLEAR_CACHE": {
|
|
39
|
+
const targetUrl = data.url;
|
|
40
|
+
const reg = targetUrl ? runtimeConfig.resources.get(targetUrl) : void 0;
|
|
41
|
+
const cacheName = reg?.cacheName ?? `${CACHE_PREFIX}-resources-${CACHE_VERSION}`;
|
|
42
|
+
caches.open(cacheName).then(async (cache) => {
|
|
43
|
+
if (targetUrl) {
|
|
44
|
+
const keys = await cache.keys();
|
|
45
|
+
const isCrossOrigin = targetUrl.startsWith("http");
|
|
46
|
+
await Promise.all(keys.filter((req) => {
|
|
47
|
+
const reqUrl = req.url;
|
|
48
|
+
const p = new URL(reqUrl).pathname;
|
|
49
|
+
if (reg?.pattern) return reg.pattern.test(isCrossOrigin ? reqUrl : p);
|
|
50
|
+
return isCrossOrigin ? reqUrl === targetUrl : p === targetUrl;
|
|
51
|
+
}).map((req) => cache.delete(req)));
|
|
52
|
+
} else await cache.keys().then((keys) => Promise.all(keys.map((k) => cache.delete(k))));
|
|
53
|
+
notifyClients({
|
|
54
|
+
type: "EIDOS_CACHE_CLEARED",
|
|
55
|
+
url: targetUrl
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "EIDOS_PING":
|
|
61
|
+
event.source?.postMessage({ type: "EIDOS_PONG" });
|
|
62
|
+
break;
|
|
63
|
+
case "EIDOS_CACHE_VAPID_KEY":
|
|
64
|
+
idbSet("vapidPublicKey", data.key);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
75
67
|
});
|
|
76
68
|
self.addEventListener("fetch", (event) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
event.respondWith(handleFetch(event.request, pathname, reg));
|
|
69
|
+
if (event.request.method !== "GET") return;
|
|
70
|
+
const requestUrl = event.request.url;
|
|
71
|
+
const pathname = new URL(requestUrl).pathname;
|
|
72
|
+
let reg = runtimeConfig.resources.get(requestUrl) ?? runtimeConfig.resources.get(pathname);
|
|
73
|
+
if (!reg) for (const [key, registration] of runtimeConfig.resources) {
|
|
74
|
+
if (!registration.pattern) continue;
|
|
75
|
+
const target = key.startsWith("http") ? requestUrl : pathname;
|
|
76
|
+
if (registration.pattern.test(target)) {
|
|
77
|
+
reg = registration;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!reg) return;
|
|
82
|
+
if (reg.strategy === "stale-while-revalidate" && !runtimeConfig.simulateOffline) {
|
|
83
|
+
event.respondWith(staleWhileRevalidate(event, event.request, pathname, reg.cacheName));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
event.respondWith(handleFetch(event.request, pathname, reg));
|
|
97
87
|
});
|
|
98
88
|
async function handleFetch(request, pathname, reg) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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);
|
|
111
|
-
}
|
|
89
|
+
if (runtimeConfig.simulateOffline) return serveOffline(request, pathname, reg.cacheName);
|
|
90
|
+
switch (reg.strategy) {
|
|
91
|
+
case "cache-first": return cacheFirst(request, pathname, reg.cacheName);
|
|
92
|
+
case "stale-while-revalidate": return staleWhileRevalidate(null, request, pathname, reg.cacheName);
|
|
93
|
+
case "network-first": return networkFirst(request, pathname, reg.cacheName);
|
|
94
|
+
default: return fetch(request);
|
|
95
|
+
}
|
|
112
96
|
}
|
|
113
97
|
async function cacheFirst(request, pathname, cacheName) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
98
|
+
const cache = await caches.open(cacheName);
|
|
99
|
+
const cached = await cache.match(request);
|
|
100
|
+
if (cached) {
|
|
101
|
+
notifyClients({
|
|
102
|
+
type: "EIDOS_CACHE_HIT",
|
|
103
|
+
url: pathname,
|
|
104
|
+
strategy: "cache-first"
|
|
105
|
+
});
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(request);
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
await cache.put(request, response.clone());
|
|
112
|
+
notifyClients({
|
|
113
|
+
type: "EIDOS_CACHE_UPDATED",
|
|
114
|
+
url: pathname,
|
|
115
|
+
strategy: "cache-first"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return response;
|
|
119
|
+
} catch {
|
|
120
|
+
notifyClients({
|
|
121
|
+
type: "EIDOS_NETWORK_ERROR",
|
|
122
|
+
url: pathname
|
|
123
|
+
});
|
|
124
|
+
return offlineErrorResponse(pathname);
|
|
125
|
+
}
|
|
131
126
|
}
|
|
132
127
|
async function staleWhileRevalidate(event, request, pathname, cacheName) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
128
|
+
const cache = await caches.open(cacheName);
|
|
129
|
+
const cached = await cache.match(request);
|
|
130
|
+
const revalidatePromise = fetch(request).then(async (response) => {
|
|
131
|
+
if (response.ok) {
|
|
132
|
+
await cache.put(request, response.clone());
|
|
133
|
+
notifyClients({
|
|
134
|
+
type: "EIDOS_CACHE_UPDATED",
|
|
135
|
+
url: pathname,
|
|
136
|
+
strategy: "stale-while-revalidate"
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return response;
|
|
140
|
+
}).catch(() => {
|
|
141
|
+
notifyClients({
|
|
142
|
+
type: "EIDOS_NETWORK_ERROR",
|
|
143
|
+
url: pathname,
|
|
144
|
+
strategy: "stale-while-revalidate"
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
if (cached) {
|
|
148
|
+
event?.waitUntil(revalidatePromise);
|
|
149
|
+
notifyClients({
|
|
150
|
+
type: "EIDOS_CACHE_HIT",
|
|
151
|
+
url: pathname,
|
|
152
|
+
strategy: "stale-while-revalidate"
|
|
153
|
+
});
|
|
154
|
+
return cached;
|
|
155
|
+
}
|
|
156
|
+
return await revalidatePromise ?? offlineErrorResponse(pathname);
|
|
159
157
|
}
|
|
160
158
|
async function networkFirst(request, pathname, cacheName) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
159
|
+
const cache = await caches.open(cacheName);
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch(request, { signal: AbortSignal.timeout(3e3) });
|
|
162
|
+
if (response.ok) {
|
|
163
|
+
await cache.put(request, response.clone());
|
|
164
|
+
notifyClients({
|
|
165
|
+
type: "EIDOS_CACHE_UPDATED",
|
|
166
|
+
url: pathname,
|
|
167
|
+
strategy: "network-first"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return response;
|
|
171
|
+
} catch {
|
|
172
|
+
const cached = await cache.match(request);
|
|
173
|
+
if (cached) {
|
|
174
|
+
notifyClients({
|
|
175
|
+
type: "EIDOS_CACHE_HIT",
|
|
176
|
+
url: pathname,
|
|
177
|
+
strategy: "network-first"
|
|
178
|
+
});
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
notifyClients({
|
|
182
|
+
type: "EIDOS_NETWORK_ERROR",
|
|
183
|
+
url: pathname
|
|
184
|
+
});
|
|
185
|
+
return offlineErrorResponse(pathname);
|
|
186
|
+
}
|
|
178
187
|
}
|
|
179
188
|
async function serveOffline(request, pathname, cacheName) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
const cached = await (await caches.open(cacheName)).match(request);
|
|
190
|
+
if (cached) {
|
|
191
|
+
notifyClients({
|
|
192
|
+
type: "EIDOS_CACHE_HIT",
|
|
193
|
+
url: pathname,
|
|
194
|
+
strategy: "offline-simulation",
|
|
195
|
+
simulated: true
|
|
196
|
+
});
|
|
197
|
+
return cached;
|
|
198
|
+
}
|
|
199
|
+
return offlineErrorResponse(pathname);
|
|
187
200
|
}
|
|
188
201
|
function offlineErrorResponse(pathname) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
);
|
|
202
|
+
return new Response(JSON.stringify({
|
|
203
|
+
error: "offline",
|
|
204
|
+
message: `No cached response available for ${pathname}`,
|
|
205
|
+
eidos: true
|
|
206
|
+
}), {
|
|
207
|
+
status: 503,
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
"X-Eidos-Offline": "true"
|
|
211
|
+
}
|
|
212
|
+
});
|
|
203
213
|
}
|
|
204
214
|
async function notifyClients(message) {
|
|
205
|
-
|
|
206
|
-
clients.forEach((client) => client.postMessage(message));
|
|
215
|
+
(await self.clients.matchAll({ includeUncontrolled: true })).forEach((client) => client.postMessage(message));
|
|
207
216
|
}
|
|
208
217
|
self.addEventListener("sync", (event) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
syncEvent.waitUntil(notifyClients({ type: "EIDOS_BACKGROUND_SYNC" }));
|
|
212
|
-
}
|
|
218
|
+
const syncEvent = event;
|
|
219
|
+
if (syncEvent.tag === "eidos-queue-replay") syncEvent.waitUntil(notifyClients({ type: "EIDOS_BACKGROUND_SYNC" }));
|
|
213
220
|
});
|
|
221
|
+
self.addEventListener("push", (event) => {
|
|
222
|
+
let payload = null;
|
|
223
|
+
try {
|
|
224
|
+
payload = event.data?.json();
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!payload?.title) return;
|
|
229
|
+
event.waitUntil(self.registration.showNotification(payload.title, {
|
|
230
|
+
body: payload.body,
|
|
231
|
+
icon: payload.icon,
|
|
232
|
+
badge: payload.badge,
|
|
233
|
+
tag: payload.tag,
|
|
234
|
+
data: payload.data
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
self.addEventListener("notificationclick", (event) => {
|
|
238
|
+
event.notification.close();
|
|
239
|
+
const data = event.notification.data ?? {};
|
|
240
|
+
event.waitUntil(self.clients.matchAll({
|
|
241
|
+
type: "window",
|
|
242
|
+
includeUncontrolled: true
|
|
243
|
+
}).then((clients) => {
|
|
244
|
+
const client = clients[0];
|
|
245
|
+
if (client) {
|
|
246
|
+
client.focus();
|
|
247
|
+
client.postMessage({
|
|
248
|
+
type: "EIDOS_NOTIFICATION_CLICK",
|
|
249
|
+
data
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (data.url) return self.clients.openWindow(data.url);
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
256
|
+
self.addEventListener("pushsubscriptionchange", (event) => {
|
|
257
|
+
const psEvent = event;
|
|
258
|
+
psEvent.waitUntil((async () => {
|
|
259
|
+
const vapidPublicKey = await idbGet("vapidPublicKey");
|
|
260
|
+
if (!vapidPublicKey) return;
|
|
261
|
+
await notifyClients({
|
|
262
|
+
type: "EIDOS_SUBSCRIPTION_EXPIRED",
|
|
263
|
+
subscription: (psEvent.newSubscription ?? await self.registration.pushManager.subscribe({
|
|
264
|
+
userVisibleOnly: true,
|
|
265
|
+
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
|
|
266
|
+
})).toJSON()
|
|
267
|
+
});
|
|
268
|
+
})());
|
|
269
|
+
});
|
|
270
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
271
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
272
|
+
const raw = atob(base64);
|
|
273
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
274
|
+
}
|
|
275
|
+
var META_DB = "eidos-sw-meta";
|
|
276
|
+
var META_STORE = "kv";
|
|
277
|
+
function openMetaDb() {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const req = indexedDB.open(META_DB, 1);
|
|
280
|
+
req.onupgradeneeded = () => req.result.createObjectStore(META_STORE);
|
|
281
|
+
req.onsuccess = () => resolve(req.result);
|
|
282
|
+
req.onerror = () => reject(req.error);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async function idbSet(key, value) {
|
|
286
|
+
const db = await openMetaDb();
|
|
287
|
+
await new Promise((resolve, reject) => {
|
|
288
|
+
const tx = db.transaction(META_STORE, "readwrite");
|
|
289
|
+
tx.objectStore(META_STORE).put(value, key);
|
|
290
|
+
tx.oncomplete = () => resolve();
|
|
291
|
+
tx.onerror = () => reject(tx.error);
|
|
292
|
+
});
|
|
293
|
+
db.close();
|
|
294
|
+
}
|
|
295
|
+
async function idbGet(key) {
|
|
296
|
+
const db = await openMetaDb();
|
|
297
|
+
const value = await new Promise((resolve, reject) => {
|
|
298
|
+
const req = db.transaction(META_STORE, "readonly").objectStore(META_STORE).get(key);
|
|
299
|
+
req.onsuccess = () => resolve(req.result);
|
|
300
|
+
req.onerror = () => reject(req.error);
|
|
301
|
+
});
|
|
302
|
+
db.close();
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|