@rei-standard/amsg-sw 2.1.0-next.3 → 2.1.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 +54 -5
- package/dist/index.cjs +188 -98
- package/dist/index.d.cts +205 -119
- package/dist/index.d.ts +205 -119
- package/dist/index.mjs +187 -97
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,13 +39,62 @@ navigator.serviceWorker.addEventListener('message', (e) => {
|
|
|
39
39
|
});
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### 通知显示策略 (Notification Rendering)
|
|
43
|
+
|
|
44
|
+
默认情况下:
|
|
45
|
+
- `content` 和老式 payload:自动弹系统通知。
|
|
46
|
+
- `reasoning` / `tool_request` / `error`:不弹通知,只触发 client 事件。
|
|
47
|
+
|
|
48
|
+
通过 `payload.notification.show`,你可以显式覆盖这个默认行为。此字段由服务端或产生 payload 时指定:
|
|
49
|
+
|
|
50
|
+
- `"auto"` 或不传:保持默认行为。
|
|
51
|
+
- `"always"`:强制弹系统通知,无视 `messageKind`。
|
|
52
|
+
- `"when-hidden"`:仅当没有 `visibilityState === "visible"` 的客户端时才弹系统通知。如果应用在前台,则静默。
|
|
53
|
+
- `false`:强制不弹通知,即使是 `content`。适合完全交给应用自行接管或自绘弹窗的场景。
|
|
54
|
+
|
|
55
|
+
当设置了弹通知时,通知文案完全由 `payload.notification` 决定(支持 `title`, `body`, `icon`, `badge`, `tag`, `data` 等字段)。如果缺省,会后备到 payload 根级属性。
|
|
56
|
+
|
|
57
|
+
#### 场景示例
|
|
58
|
+
|
|
59
|
+
**1. tool_request 需要用户处理**
|
|
60
|
+
某些 Agent loop 跑到 `tool_request` 时需要用户在界面上确认或执行。由于默认 `tool_request` 不弹通知,用户如果在后台可能会漏掉:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"messageKind": "tool_request",
|
|
65
|
+
"sessionId": "...",
|
|
66
|
+
"toolCalls": [],
|
|
67
|
+
"notification": {
|
|
68
|
+
"show": "when-hidden",
|
|
69
|
+
"title": "需要继续处理",
|
|
70
|
+
"body": "点开应用继续完成工具调用"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**2. Content 消息完全由前端接管**
|
|
76
|
+
应用层想在页面前台做非常定制的 Toast,不想弹系统级别通知:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"messageKind": "content",
|
|
81
|
+
"message": "...",
|
|
82
|
+
"notification": {
|
|
83
|
+
"show": false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **注意:对于 multipart 传输**
|
|
89
|
+
> 当 payload 通过 `_multipart` 分片时,未收齐前不仅不派发业务事件,也**绝不**弹系统通知。收齐并还原为原始 payload 后,再按原始 payload 的 `notification.show` 策略执行判定。
|
|
90
|
+
|
|
42
91
|
### Blob envelope
|
|
43
92
|
|
|
44
93
|
当 `amsg-instant` 检测到 payload 超过 `maxInlineBytes` 时会改发 blob envelope `{ _blob: true, key, url, messageKind?, type? }`。SW **不会** 自动 fetch blob 内容(那是 client 的职责),但仍然会按 envelope 上的 `messageKind` 分发对应事件,让 client 知道有什么类型的内容即将到达,自己决定要不要拉取。Blob envelope 也只在 `messageKind === 'content'`(或缺失)时才渲染占位通知,与普通 push 行为一致。
|
|
45
94
|
|
|
46
|
-
### Generic multipart transport(
|
|
95
|
+
### Generic multipart transport(2.1.0+)
|
|
47
96
|
|
|
48
|
-
|
|
97
|
+
2.1.0 移除了旧 reasoning 专用 `chunkIndex` / `totalChunks` wire format。现在 `_multipart` 是统一 transport kind,任何原始 payload 都可以被包起来:
|
|
49
98
|
|
|
50
99
|
```json
|
|
51
100
|
{
|
|
@@ -79,7 +128,7 @@ installReiSW(self, {
|
|
|
79
128
|
maxChunks: 128,
|
|
80
129
|
cleanupIntervalMs: 15 * 60_000
|
|
81
130
|
},
|
|
82
|
-
// (新增于 2.1.0
|
|
131
|
+
// (新增于 2.1.0)离线持久化等业务拦截钩子:
|
|
83
132
|
onBusinessPayload: async (payload) => {
|
|
84
133
|
// 收到完整 payload 时触发,由于内置在 event.waitUntil 中,能够确保离线写库完毕再允许 SW 休眠
|
|
85
134
|
// await db.saveIncomingMessage(payload);
|
|
@@ -101,8 +150,8 @@ TTL 到期仍未收齐时,SW 会清理 pending 并广播:
|
|
|
101
150
|
|
|
102
151
|
### 升级注意事项
|
|
103
152
|
|
|
104
|
-
- 想给 `reasoning` / `tool_request` / `error`
|
|
105
|
-
- 应用级 SW 可以删除旧 reasoning `chunkIndex` / `totalChunks` 拼接逻辑;
|
|
153
|
+
- 想给 `reasoning` / `tool_request` / `error` 弹通知的业务:SW 默认不再为它们弹通知,但可以通过设置 `payload.notification.show = "always"` 或 `"when-hidden"` 来让 SW 在包层直接弹通知。无需再强求在 app 内自绘。
|
|
154
|
+
- 应用级 SW 可以删除旧 reasoning `chunkIndex` / `totalChunks` 拼接逻辑;2.1.0+ 版本只会把完整还原后的 reasoning payload 发给 client。
|
|
106
155
|
- 客户端代码继续兼容只有 `installReiSW` + `REI_SW_MESSAGE_TYPE`(队列)的 2.0.x 写法——新增导出不破坏既有 API。
|
|
107
156
|
- 想拿到 push 类型相关的 TS 类型:从 `@rei-standard/amsg-shared` 引 `AmsgPush` 等类型(本包通过 JSDoc 引用同一份类型)。
|
|
108
157
|
|
package/dist/index.cjs
CHANGED
|
@@ -25,11 +25,14 @@ __export(src_exports, {
|
|
|
25
25
|
installReiSW: () => installReiSW
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
var import_amsg_shared = require("@rei-standard/amsg-shared");
|
|
28
29
|
var REI_SW_DB_NAME = "rei-sw";
|
|
29
30
|
var REI_SW_DB_STORE = "request-outbox";
|
|
30
31
|
var REI_SW_MULTIPART_STORE = "multipart-pending";
|
|
31
32
|
var REI_SW_MULTIPART_DONE_STORE = "multipart-done";
|
|
32
|
-
var
|
|
33
|
+
var REI_SW_MULTIPART_CHUNK_STORE = "multipart-chunk";
|
|
34
|
+
var REI_SW_DB_VERSION = 3;
|
|
35
|
+
var cachedDB = null;
|
|
33
36
|
var REI_SW_SYNC_TAG = "rei-sw-flush-request-outbox";
|
|
34
37
|
var MULTIPART_MESSAGE_KIND = "_multipart";
|
|
35
38
|
var MULTIPART_ENCODING = "json-utf8-base64url";
|
|
@@ -42,6 +45,7 @@ var DEFAULT_MULTIPART_OPTIONS = Object.freeze({
|
|
|
42
45
|
});
|
|
43
46
|
var memoryMultipartPending = /* @__PURE__ */ new Map();
|
|
44
47
|
var memoryMultipartDone = /* @__PURE__ */ new Map();
|
|
48
|
+
var memoryMultipartChunks = /* @__PURE__ */ new Map();
|
|
45
49
|
var REI_AMSG_POSTMESSAGE_TYPE = "REI_AMSG_PUSH";
|
|
46
50
|
var REI_SW_EVENT = Object.freeze({
|
|
47
51
|
CONTENT_RECEIVED: "rei-amsg-content-received",
|
|
@@ -110,8 +114,27 @@ async function handlePushPayload(sw, payload, ctx) {
|
|
|
110
114
|
}
|
|
111
115
|
async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
112
116
|
const eventName = resolveEventName(payload);
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
let clientList = [];
|
|
118
|
+
try {
|
|
119
|
+
clientList = await sw.clients.matchAll({
|
|
120
|
+
type: "window",
|
|
121
|
+
includeUncontrolled: true
|
|
122
|
+
});
|
|
123
|
+
} catch (_matchError) {
|
|
124
|
+
}
|
|
125
|
+
let shouldRenderNotification = false;
|
|
126
|
+
const showOpt = payload && payload.notification ? payload.notification.show : void 0;
|
|
127
|
+
if (showOpt === "always") {
|
|
128
|
+
shouldRenderNotification = true;
|
|
129
|
+
} else if (showOpt === "when-hidden") {
|
|
130
|
+
const hasVisibleClient = clientList.some((client) => client.visibilityState === "visible");
|
|
131
|
+
shouldRenderNotification = !hasVisibleClient;
|
|
132
|
+
} else if (showOpt === false) {
|
|
133
|
+
shouldRenderNotification = false;
|
|
134
|
+
} else {
|
|
135
|
+
shouldRenderNotification = isNotificationKind(payload);
|
|
136
|
+
}
|
|
137
|
+
const work = [dispatchPushToClients(sw, eventName, payload, clientList)];
|
|
115
138
|
if (shouldRenderNotification) {
|
|
116
139
|
const notification = createNotificationFromPayload(payload, defaults);
|
|
117
140
|
if (notification) {
|
|
@@ -137,13 +160,13 @@ async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
|
137
160
|
function resolveEventName(payload) {
|
|
138
161
|
const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
|
|
139
162
|
switch (kind) {
|
|
140
|
-
case
|
|
163
|
+
case import_amsg_shared.MESSAGE_KIND.CONTENT:
|
|
141
164
|
return REI_SW_EVENT.CONTENT_RECEIVED;
|
|
142
|
-
case
|
|
165
|
+
case import_amsg_shared.MESSAGE_KIND.REASONING:
|
|
143
166
|
return REI_SW_EVENT.REASONING_RECEIVED;
|
|
144
|
-
case
|
|
167
|
+
case import_amsg_shared.MESSAGE_KIND.TOOL_REQUEST:
|
|
145
168
|
return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
|
|
146
|
-
case
|
|
169
|
+
case import_amsg_shared.MESSAGE_KIND.ERROR:
|
|
147
170
|
return REI_SW_EVENT.ERROR_RECEIVED;
|
|
148
171
|
default:
|
|
149
172
|
return REI_SW_EVENT.UNKNOWN_RECEIVED;
|
|
@@ -153,11 +176,11 @@ function isNotificationKind(payload) {
|
|
|
153
176
|
if (!payload || typeof payload !== "object") return false;
|
|
154
177
|
const kind = payload.messageKind;
|
|
155
178
|
if (kind === void 0 || kind === null) return true;
|
|
156
|
-
return kind ===
|
|
179
|
+
return kind === import_amsg_shared.MESSAGE_KIND.CONTENT;
|
|
157
180
|
}
|
|
158
|
-
async function dispatchPushToClients(sw, eventName, payload) {
|
|
181
|
+
async function dispatchPushToClients(sw, eventName, payload, preFetchedClientList = null) {
|
|
159
182
|
try {
|
|
160
|
-
const clientList = await sw.clients.matchAll({
|
|
183
|
+
const clientList = preFetchedClientList || await sw.clients.matchAll({
|
|
161
184
|
type: "window",
|
|
162
185
|
includeUncontrolled: true
|
|
163
186
|
});
|
|
@@ -199,9 +222,9 @@ function createNotificationFromPayload(payload, defaults) {
|
|
|
199
222
|
};
|
|
200
223
|
}
|
|
201
224
|
const pushNotification = payload.notification && typeof payload.notification === "object" ? payload.notification : {};
|
|
202
|
-
const title = pushNotification.title || payload.title || payload.contactName
|
|
225
|
+
const title = pushNotification.title || payload.title || payload.contactName || "New notification";
|
|
203
226
|
const body = pushNotification.body || payload.body || payload.message || "";
|
|
204
|
-
const data = payload.data && typeof payload.data === "object" ? { ...payload.data } : {};
|
|
227
|
+
const data = pushNotification.data && typeof pushNotification.data === "object" ? { ...pushNotification.data } : payload.data && typeof payload.data === "object" ? { ...payload.data } : {};
|
|
205
228
|
if (data.payload == null) data.payload = payload;
|
|
206
229
|
return {
|
|
207
230
|
title,
|
|
@@ -269,34 +292,43 @@ async function acceptMultipartChunk(sw, payload, options) {
|
|
|
269
292
|
total: normalized.total,
|
|
270
293
|
originalMessageKind: normalized.originalMessageKind,
|
|
271
294
|
encoding: normalized.encoding,
|
|
272
|
-
|
|
295
|
+
receivedCount: 0,
|
|
273
296
|
receivedBytes: 0
|
|
274
297
|
};
|
|
275
298
|
if (base.total !== normalized.total || base.encoding !== normalized.encoding) {
|
|
276
299
|
await deleteMultipartPending(normalized.id);
|
|
300
|
+
await deleteMultipartChunks(normalized.id, base.total);
|
|
277
301
|
return null;
|
|
278
302
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
base.
|
|
303
|
+
const chunkId = `${normalized.id}_${normalized.index}`;
|
|
304
|
+
const chunkExists = await hasMultipartChunk(chunkId);
|
|
305
|
+
if (chunkExists) return null;
|
|
306
|
+
base.receivedCount++;
|
|
283
307
|
base.receivedBytes = positiveIntegerOrDefault(base.receivedBytes, 0) + normalized.chunkBytes.byteLength;
|
|
284
308
|
if (base.receivedBytes > options.maxTotalBytes) {
|
|
285
309
|
await deleteMultipartPending(normalized.id);
|
|
310
|
+
await deleteMultipartChunks(normalized.id, base.total);
|
|
286
311
|
return null;
|
|
287
312
|
}
|
|
288
|
-
|
|
289
|
-
|
|
313
|
+
await writeMultipartChunk({
|
|
314
|
+
id_index: chunkId,
|
|
315
|
+
id: normalized.id,
|
|
316
|
+
index: normalized.index,
|
|
317
|
+
chunk: normalized.chunk
|
|
318
|
+
});
|
|
319
|
+
if (base.receivedCount < base.total) {
|
|
290
320
|
await writeMultipartPending(base);
|
|
291
321
|
return null;
|
|
292
322
|
}
|
|
293
323
|
await deleteMultipartPending(base.id);
|
|
294
324
|
let restored;
|
|
295
325
|
try {
|
|
296
|
-
restored = restoreMultipartPayload(base, options);
|
|
326
|
+
restored = await restoreMultipartPayload(base, options);
|
|
297
327
|
} catch (_error) {
|
|
328
|
+
await deleteMultipartChunks(base.id, base.total);
|
|
298
329
|
return null;
|
|
299
330
|
}
|
|
331
|
+
await deleteMultipartChunks(base.id, base.total);
|
|
300
332
|
const doneTtlMs = Math.max(base.ttlMs * 2, base.ttlMs + 1);
|
|
301
333
|
await writeMultipartDone({
|
|
302
334
|
id: base.id,
|
|
@@ -314,8 +346,9 @@ function normalizeMultipartChunk(payload, options) {
|
|
|
314
346
|
if (meta.index <= 0 || meta.index > meta.total) return null;
|
|
315
347
|
let chunkBytes;
|
|
316
348
|
try {
|
|
317
|
-
chunkBytes = base64UrlToBytes(payload.chunk);
|
|
349
|
+
chunkBytes = (0, import_amsg_shared.base64UrlToBytes)(payload.chunk);
|
|
318
350
|
} catch (_error) {
|
|
351
|
+
console.error("RESTORE ERROR", _error);
|
|
319
352
|
return null;
|
|
320
353
|
}
|
|
321
354
|
const now = Date.now();
|
|
@@ -338,22 +371,22 @@ function normalizeMultipartChunk(payload, options) {
|
|
|
338
371
|
chunkBytes
|
|
339
372
|
};
|
|
340
373
|
}
|
|
341
|
-
function restoreMultipartPayload(record, options) {
|
|
374
|
+
async function restoreMultipartPayload(record, options) {
|
|
342
375
|
const chunks = [];
|
|
343
376
|
let totalBytes = 0;
|
|
344
377
|
for (let index = 1; index <= record.total; index++) {
|
|
345
|
-
const
|
|
346
|
-
if (typeof chunk !== "string") {
|
|
378
|
+
const chunkRecord = await readMultipartChunk(record.id, index);
|
|
379
|
+
if (!chunkRecord || typeof chunkRecord.chunk !== "string") {
|
|
347
380
|
throw new Error("[rei-standard-amsg-sw] multipart missing chunk");
|
|
348
381
|
}
|
|
349
|
-
const bytes = base64UrlToBytes(chunk);
|
|
382
|
+
const bytes = (0, import_amsg_shared.base64UrlToBytes)(chunkRecord.chunk);
|
|
350
383
|
totalBytes += bytes.byteLength;
|
|
351
384
|
if (totalBytes > options.maxTotalBytes) {
|
|
352
385
|
throw new Error("[rei-standard-amsg-sw] multipart payload exceeds maxTotalBytes");
|
|
353
386
|
}
|
|
354
387
|
chunks.push(bytes);
|
|
355
388
|
}
|
|
356
|
-
const json = new TextDecoder("utf-8", { fatal: false }).decode(concatBytes(chunks));
|
|
389
|
+
const json = new TextDecoder("utf-8", { fatal: false }).decode((0, import_amsg_shared.concatBytes)(...chunks));
|
|
357
390
|
return JSON.parse(json);
|
|
358
391
|
}
|
|
359
392
|
async function maybeCleanupMultipart(sw, ctx) {
|
|
@@ -365,50 +398,62 @@ async function maybeCleanupMultipart(sw, ctx) {
|
|
|
365
398
|
try {
|
|
366
399
|
await cleanupMultipartStores(sw, now);
|
|
367
400
|
} catch (_error) {
|
|
401
|
+
console.error("RESTORE ERROR", _error);
|
|
368
402
|
}
|
|
369
403
|
}
|
|
370
404
|
async function cleanupMultipartStores(sw, now) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
if (!hasIndexedDB()) {
|
|
406
|
+
for (const [id, record] of memoryMultipartPending.entries()) {
|
|
407
|
+
if (record.expiresAt <= now) {
|
|
408
|
+
memoryMultipartPending.delete(id);
|
|
409
|
+
await deleteMultipartChunks(id, record.total);
|
|
410
|
+
await dispatchMultipartExpired(sw, record);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const [id, record] of memoryMultipartDone.entries()) {
|
|
414
|
+
if (record.expiresAt <= now) {
|
|
415
|
+
memoryMultipartDone.delete(id);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const pendingExpired = await withDatabaseStore(REI_SW_MULTIPART_STORE, "readonly", (store, resolve, reject) => {
|
|
421
|
+
const index = store.index("expiresAt");
|
|
422
|
+
const range = IDBKeyRange.upperBound(now);
|
|
423
|
+
const req = index.getAll(range);
|
|
424
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
425
|
+
req.onerror = () => reject(req.error);
|
|
426
|
+
});
|
|
427
|
+
for (const record of pendingExpired) {
|
|
428
|
+
await deleteStoreRecord(REI_SW_MULTIPART_STORE, record.id);
|
|
429
|
+
await deleteMultipartChunks(record.id, record.total);
|
|
375
430
|
await dispatchMultipartExpired(sw, record);
|
|
376
431
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
432
|
+
const doneExpiredKeys = await withDatabaseStore(REI_SW_MULTIPART_DONE_STORE, "readonly", (store, resolve, reject) => {
|
|
433
|
+
const index = store.index("expiresAt");
|
|
434
|
+
const range = IDBKeyRange.upperBound(now);
|
|
435
|
+
if (index.getAllKeys) {
|
|
436
|
+
const req = index.getAllKeys(range);
|
|
437
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
438
|
+
req.onerror = () => reject(req.error);
|
|
439
|
+
} else {
|
|
440
|
+
const req = index.getAll(range);
|
|
441
|
+
req.onsuccess = () => resolve((req.result || []).map((r) => r.id));
|
|
442
|
+
req.onerror = () => reject(req.error);
|
|
381
443
|
}
|
|
444
|
+
});
|
|
445
|
+
for (const id of doneExpiredKeys) {
|
|
446
|
+
await deleteStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
382
447
|
}
|
|
383
448
|
}
|
|
384
449
|
async function dispatchMultipartExpired(sw, record) {
|
|
385
450
|
await dispatchPushToClients(sw, REI_SW_EVENT.MULTIPART_EXPIRED, {
|
|
386
451
|
id: record.id,
|
|
387
|
-
received:
|
|
452
|
+
received: typeof record.receivedCount === "number" ? record.receivedCount : 0,
|
|
388
453
|
total: record.total,
|
|
389
454
|
originalMessageKind: record.originalMessageKind
|
|
390
455
|
});
|
|
391
456
|
}
|
|
392
|
-
function base64UrlToBytes(input) {
|
|
393
|
-
const s = String(input).replace(/-/g, "+").replace(/_/g, "/");
|
|
394
|
-
const pad = (4 - s.length % 4) % 4;
|
|
395
|
-
const padded = s + "=".repeat(pad);
|
|
396
|
-
const bin = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("binary");
|
|
397
|
-
const out = new Uint8Array(bin.length);
|
|
398
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
399
|
-
return out;
|
|
400
|
-
}
|
|
401
|
-
function concatBytes(chunks) {
|
|
402
|
-
let total = 0;
|
|
403
|
-
for (const chunk of chunks) total += chunk.byteLength;
|
|
404
|
-
const out = new Uint8Array(total);
|
|
405
|
-
let offset = 0;
|
|
406
|
-
for (const chunk of chunks) {
|
|
407
|
-
out.set(chunk, offset);
|
|
408
|
-
offset += chunk.byteLength;
|
|
409
|
-
}
|
|
410
|
-
return out;
|
|
411
|
-
}
|
|
412
457
|
async function enqueueAndFlush(sw, event, requestPayload) {
|
|
413
458
|
try {
|
|
414
459
|
const request = normalizeQueuedRequest(requestPayload);
|
|
@@ -468,6 +513,7 @@ function normalizeRequestBody(bodyInput) {
|
|
|
468
513
|
try {
|
|
469
514
|
return JSON.stringify(bodyInput);
|
|
470
515
|
} catch (_error) {
|
|
516
|
+
console.error("RESTORE ERROR", _error);
|
|
471
517
|
throw new Error("[rei-standard-amsg-sw] request body is not serializable");
|
|
472
518
|
}
|
|
473
519
|
}
|
|
@@ -494,6 +540,7 @@ async function trySendQueuedRequest(queuedRequest) {
|
|
|
494
540
|
}
|
|
495
541
|
return false;
|
|
496
542
|
} catch (_error) {
|
|
543
|
+
console.error("RESTORE ERROR", _error);
|
|
497
544
|
return false;
|
|
498
545
|
}
|
|
499
546
|
}
|
|
@@ -503,6 +550,7 @@ async function registerFlushSync(sw) {
|
|
|
503
550
|
try {
|
|
504
551
|
await syncManager.register(REI_SW_SYNC_TAG);
|
|
505
552
|
} catch (_error) {
|
|
553
|
+
console.error("RESTORE ERROR", _error);
|
|
506
554
|
}
|
|
507
555
|
}
|
|
508
556
|
function respondToSender(event, message) {
|
|
@@ -525,9 +573,6 @@ function writeMultipartPending(record) {
|
|
|
525
573
|
function deleteMultipartPending(id) {
|
|
526
574
|
return deleteStoreRecord(REI_SW_MULTIPART_STORE, id);
|
|
527
575
|
}
|
|
528
|
-
function listMultipartPending() {
|
|
529
|
-
return listStoreRecords(REI_SW_MULTIPART_STORE);
|
|
530
|
-
}
|
|
531
576
|
function readMultipartDone(id) {
|
|
532
577
|
return readStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
533
578
|
}
|
|
@@ -537,8 +582,54 @@ function writeMultipartDone(record) {
|
|
|
537
582
|
function deleteMultipartDone(id) {
|
|
538
583
|
return deleteStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
539
584
|
}
|
|
540
|
-
function
|
|
541
|
-
return
|
|
585
|
+
async function hasMultipartChunk(id_index) {
|
|
586
|
+
if (!hasIndexedDB()) return memoryMultipartChunks.has(id_index);
|
|
587
|
+
return withDatabaseStore(REI_SW_MULTIPART_CHUNK_STORE, "readonly", (store, resolve, reject) => {
|
|
588
|
+
const request = store.count(id_index);
|
|
589
|
+
request.onsuccess = () => resolve(request.result > 0);
|
|
590
|
+
request.onerror = () => reject(request.error);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function writeMultipartChunk(record) {
|
|
594
|
+
if (!hasIndexedDB()) {
|
|
595
|
+
memoryMultipartChunks.set(record.id_index, cloneRecord(record));
|
|
596
|
+
return Promise.resolve();
|
|
597
|
+
}
|
|
598
|
+
return putStoreRecord(REI_SW_MULTIPART_CHUNK_STORE, record);
|
|
599
|
+
}
|
|
600
|
+
function readMultipartChunk(id, index) {
|
|
601
|
+
const id_index = `${id}_${index}`;
|
|
602
|
+
if (!hasIndexedDB()) {
|
|
603
|
+
return Promise.resolve(cloneRecord(memoryMultipartChunks.get(id_index) || null));
|
|
604
|
+
}
|
|
605
|
+
return readStoreRecord(REI_SW_MULTIPART_CHUNK_STORE, id_index);
|
|
606
|
+
}
|
|
607
|
+
async function deleteMultipartChunks(id, total) {
|
|
608
|
+
if (!hasIndexedDB()) {
|
|
609
|
+
for (let index = 1; index <= total; index++) {
|
|
610
|
+
memoryMultipartChunks.delete(`${id}_${index}`);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
return withDatabaseStore(REI_SW_MULTIPART_CHUNK_STORE, "readwrite", (store, resolve, reject) => {
|
|
615
|
+
let pending = total;
|
|
616
|
+
let failed = false;
|
|
617
|
+
for (let index = 1; index <= total; index++) {
|
|
618
|
+
const request = store.delete(`${id}_${index}`);
|
|
619
|
+
request.onsuccess = () => {
|
|
620
|
+
if (failed) return;
|
|
621
|
+
pending--;
|
|
622
|
+
if (pending === 0) resolve(void 0);
|
|
623
|
+
};
|
|
624
|
+
request.onerror = () => {
|
|
625
|
+
if (!failed) {
|
|
626
|
+
failed = true;
|
|
627
|
+
reject(request.error);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (total === 0) resolve(void 0);
|
|
632
|
+
});
|
|
542
633
|
}
|
|
543
634
|
async function readStoreRecord(storeName, id) {
|
|
544
635
|
if (!hasIndexedDB()) {
|
|
@@ -572,28 +663,14 @@ async function deleteStoreRecord(storeName, id) {
|
|
|
572
663
|
request.onerror = () => reject(request.error || new Error(`Failed to delete ${storeName}`));
|
|
573
664
|
});
|
|
574
665
|
}
|
|
575
|
-
async function listStoreRecords(storeName) {
|
|
576
|
-
if (!hasIndexedDB()) {
|
|
577
|
-
return Array.from(memoryStoreFor(storeName).values()).map(cloneRecord);
|
|
578
|
-
}
|
|
579
|
-
return withDatabaseStore(storeName, "readonly", (store, resolve, reject) => {
|
|
580
|
-
const request = store.getAll();
|
|
581
|
-
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
|
|
582
|
-
request.onerror = () => reject(request.error || new Error(`Failed to list ${storeName}`));
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
666
|
async function withDatabaseStore(storeName, mode, handler) {
|
|
586
667
|
const db = await openQueueDatabase();
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
});
|
|
594
|
-
} finally {
|
|
595
|
-
db.close();
|
|
596
|
-
}
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
const transaction = db.transaction(storeName, mode);
|
|
670
|
+
const store = transaction.objectStore(storeName);
|
|
671
|
+
transaction.onerror = () => reject(transaction.error || new Error("Database transaction failed"));
|
|
672
|
+
Promise.resolve(handler(store, resolve, reject)).catch(reject);
|
|
673
|
+
});
|
|
597
674
|
}
|
|
598
675
|
function hasIndexedDB() {
|
|
599
676
|
return typeof indexedDB !== "undefined" && indexedDB && typeof indexedDB.open === "function";
|
|
@@ -601,6 +678,7 @@ function hasIndexedDB() {
|
|
|
601
678
|
function memoryStoreFor(storeName) {
|
|
602
679
|
if (storeName === REI_SW_MULTIPART_DONE_STORE) return memoryMultipartDone;
|
|
603
680
|
if (storeName === REI_SW_MULTIPART_STORE) return memoryMultipartPending;
|
|
681
|
+
if (storeName === REI_SW_MULTIPART_CHUNK_STORE) return memoryMultipartChunks;
|
|
604
682
|
throw new Error(`[rei-standard-amsg-sw] unknown memory store: ${storeName}`);
|
|
605
683
|
}
|
|
606
684
|
function cloneRecord(record) {
|
|
@@ -608,35 +686,47 @@ function cloneRecord(record) {
|
|
|
608
686
|
return JSON.parse(JSON.stringify(record));
|
|
609
687
|
}
|
|
610
688
|
function openQueueDatabase() {
|
|
689
|
+
if (cachedDB) return Promise.resolve(cachedDB);
|
|
611
690
|
return new Promise((resolve, reject) => {
|
|
612
691
|
const request = indexedDB.open(REI_SW_DB_NAME, REI_SW_DB_VERSION);
|
|
613
692
|
request.onupgradeneeded = () => {
|
|
614
693
|
const db = request.result;
|
|
615
|
-
|
|
616
|
-
createObjectStoreIfMissing(db,
|
|
617
|
-
createObjectStoreIfMissing(db,
|
|
694
|
+
const tx = request.transaction;
|
|
695
|
+
createObjectStoreIfMissing(db, tx, REI_SW_DB_STORE, { keyPath: "id", autoIncrement: true });
|
|
696
|
+
const mpStore = createObjectStoreIfMissing(db, tx, REI_SW_MULTIPART_STORE, { keyPath: "id" });
|
|
697
|
+
const mpDoneStore = createObjectStoreIfMissing(db, tx, REI_SW_MULTIPART_DONE_STORE, { keyPath: "id" });
|
|
698
|
+
createObjectStoreIfMissing(db, tx, REI_SW_MULTIPART_CHUNK_STORE, { keyPath: "id_index" });
|
|
699
|
+
if (mpStore && !mpStore.indexNames.contains("expiresAt")) {
|
|
700
|
+
mpStore.createIndex("expiresAt", "expiresAt", { unique: false });
|
|
701
|
+
}
|
|
702
|
+
if (mpDoneStore && !mpDoneStore.indexNames.contains("expiresAt")) {
|
|
703
|
+
mpDoneStore.createIndex("expiresAt", "expiresAt", { unique: false });
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
request.onsuccess = () => {
|
|
707
|
+
cachedDB = request.result;
|
|
708
|
+
cachedDB.onversionchange = () => {
|
|
709
|
+
cachedDB.close();
|
|
710
|
+
cachedDB = null;
|
|
711
|
+
};
|
|
712
|
+
resolve(cachedDB);
|
|
618
713
|
};
|
|
619
|
-
request.onsuccess = () => resolve(request.result);
|
|
620
714
|
request.onerror = () => reject(request.error || new Error("Failed to open queue database"));
|
|
621
715
|
});
|
|
622
716
|
}
|
|
623
|
-
function createObjectStoreIfMissing(db, name, options) {
|
|
624
|
-
if (db.objectStoreNames.contains(name)) return;
|
|
625
|
-
db.createObjectStore(name, options);
|
|
717
|
+
function createObjectStoreIfMissing(db, tx, name, options) {
|
|
718
|
+
if (db.objectStoreNames.contains(name)) return tx.objectStore(name);
|
|
719
|
+
return db.createObjectStore(name, options);
|
|
626
720
|
}
|
|
627
721
|
async function withQueueStore(mode, handler) {
|
|
628
722
|
const db = await openQueueDatabase();
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
637
|
-
} finally {
|
|
638
|
-
db.close();
|
|
639
|
-
}
|
|
723
|
+
return new Promise((resolve, reject) => {
|
|
724
|
+
const transaction = db.transaction(REI_SW_DB_STORE, mode);
|
|
725
|
+
const store = transaction.objectStore(REI_SW_DB_STORE);
|
|
726
|
+
transaction.oncomplete = () => resolve(void 0);
|
|
727
|
+
transaction.onerror = () => reject(transaction.error || new Error("Queue transaction failed"));
|
|
728
|
+
Promise.resolve(handler(store, resolve, reject)).catch(reject);
|
|
729
|
+
});
|
|
640
730
|
}
|
|
641
731
|
async function addQueuedRequest(request) {
|
|
642
732
|
return withQueueStore("readwrite", (store, resolve, reject) => {
|