@rei-standard/amsg-sw 2.1.0-next.2 → 2.1.0-next.4
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 +59 -2
- package/dist/index.cjs +203 -99
- package/dist/index.d.cts +221 -119
- package/dist/index.d.ts +221 -119
- package/dist/index.mjs +202 -98
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,55 @@ 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 行为一致。
|
|
@@ -78,6 +127,11 @@ installReiSW(self, {
|
|
|
78
127
|
maxTotalBytes: 256_000,
|
|
79
128
|
maxChunks: 128,
|
|
80
129
|
cleanupIntervalMs: 15 * 60_000
|
|
130
|
+
},
|
|
131
|
+
// (新增于 2.1.0-next.3)离线持久化等业务拦截钩子:
|
|
132
|
+
onBusinessPayload: async (payload) => {
|
|
133
|
+
// 收到完整 payload 时触发,由于内置在 event.waitUntil 中,能够确保离线写库完毕再允许 SW 休眠
|
|
134
|
+
// await db.saveIncomingMessage(payload);
|
|
81
135
|
}
|
|
82
136
|
});
|
|
83
137
|
```
|
|
@@ -96,7 +150,7 @@ TTL 到期仍未收齐时,SW 会清理 pending 并广播:
|
|
|
96
150
|
|
|
97
151
|
### 升级注意事项
|
|
98
152
|
|
|
99
|
-
- 想给 `reasoning` / `tool_request` / `error`
|
|
153
|
+
- 想给 `reasoning` / `tool_request` / `error` 弹通知的业务:SW 默认不再为它们弹通知,但可以通过设置 `payload.notification.show = "always"` 或 `"when-hidden"` 来让 SW 在包层直接弹通知。无需再强求在 app 内自绘。
|
|
100
154
|
- 应用级 SW 可以删除旧 reasoning `chunkIndex` / `totalChunks` 拼接逻辑;next 版本只会把完整还原后的 reasoning payload 发给 client。
|
|
101
155
|
- 客户端代码继续兼容只有 `installReiSW` + `REI_SW_MESSAGE_TYPE`(队列)的 2.0.x 写法——新增导出不破坏既有 API。
|
|
102
156
|
- 想拿到 push 类型相关的 TS 类型:从 `@rei-standard/amsg-shared` 引 `AmsgPush` 等类型(本包通过 JSDoc 引用同一份类型)。
|
|
@@ -125,7 +179,10 @@ import { installReiSW } from '@rei-standard/amsg-sw';
|
|
|
125
179
|
installReiSW(self, {
|
|
126
180
|
defaultIcon: '/icon-192x192.png',
|
|
127
181
|
defaultBadge: '/badge-72x72.png',
|
|
128
|
-
multipart: { enabled: true }
|
|
182
|
+
multipart: { enabled: true },
|
|
183
|
+
onBusinessPayload: async (payload) => {
|
|
184
|
+
// 这里可安全地进行应用级别的离线数据库存储
|
|
185
|
+
}
|
|
129
186
|
});
|
|
130
187
|
|
|
131
188
|
// 业务侧自行实现点击跳转
|
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",
|
|
@@ -68,6 +72,7 @@ function installReiSW(sw, opts = {}) {
|
|
|
68
72
|
defaultBadge,
|
|
69
73
|
defaultIcon,
|
|
70
74
|
multipart,
|
|
75
|
+
onBusinessPayload: opts.onBusinessPayload,
|
|
71
76
|
getLastMultipartCleanupAt: () => lastMultipartCleanupAt,
|
|
72
77
|
setLastMultipartCleanupAt: (value) => {
|
|
73
78
|
lastMultipartCleanupAt = value;
|
|
@@ -103,13 +108,33 @@ async function handlePushPayload(sw, payload, ctx) {
|
|
|
103
108
|
}
|
|
104
109
|
await dispatchBusinessPayload(sw, payload, {
|
|
105
110
|
defaultIcon: ctx.defaultIcon,
|
|
106
|
-
defaultBadge: ctx.defaultBadge
|
|
111
|
+
defaultBadge: ctx.defaultBadge,
|
|
112
|
+
onBusinessPayload: ctx.onBusinessPayload
|
|
107
113
|
});
|
|
108
114
|
}
|
|
109
115
|
async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
110
116
|
const eventName = resolveEventName(payload);
|
|
111
|
-
|
|
112
|
-
|
|
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)];
|
|
113
138
|
if (shouldRenderNotification) {
|
|
114
139
|
const notification = createNotificationFromPayload(payload, defaults);
|
|
115
140
|
if (notification) {
|
|
@@ -118,18 +143,30 @@ async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
|
118
143
|
);
|
|
119
144
|
}
|
|
120
145
|
}
|
|
146
|
+
if (typeof defaults.onBusinessPayload === "function") {
|
|
147
|
+
try {
|
|
148
|
+
const result = defaults.onBusinessPayload(payload);
|
|
149
|
+
if (result instanceof Promise) {
|
|
150
|
+
work.push(result.catch((error) => {
|
|
151
|
+
console.error("[rei-standard-amsg-sw] onBusinessPayload promise rejected:", error);
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("[rei-standard-amsg-sw] onBusinessPayload error:", error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
121
158
|
await Promise.all(work);
|
|
122
159
|
}
|
|
123
160
|
function resolveEventName(payload) {
|
|
124
161
|
const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
|
|
125
162
|
switch (kind) {
|
|
126
|
-
case
|
|
163
|
+
case import_amsg_shared.MESSAGE_KIND.CONTENT:
|
|
127
164
|
return REI_SW_EVENT.CONTENT_RECEIVED;
|
|
128
|
-
case
|
|
165
|
+
case import_amsg_shared.MESSAGE_KIND.REASONING:
|
|
129
166
|
return REI_SW_EVENT.REASONING_RECEIVED;
|
|
130
|
-
case
|
|
167
|
+
case import_amsg_shared.MESSAGE_KIND.TOOL_REQUEST:
|
|
131
168
|
return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
|
|
132
|
-
case
|
|
169
|
+
case import_amsg_shared.MESSAGE_KIND.ERROR:
|
|
133
170
|
return REI_SW_EVENT.ERROR_RECEIVED;
|
|
134
171
|
default:
|
|
135
172
|
return REI_SW_EVENT.UNKNOWN_RECEIVED;
|
|
@@ -139,11 +176,11 @@ function isNotificationKind(payload) {
|
|
|
139
176
|
if (!payload || typeof payload !== "object") return false;
|
|
140
177
|
const kind = payload.messageKind;
|
|
141
178
|
if (kind === void 0 || kind === null) return true;
|
|
142
|
-
return kind ===
|
|
179
|
+
return kind === import_amsg_shared.MESSAGE_KIND.CONTENT;
|
|
143
180
|
}
|
|
144
|
-
async function dispatchPushToClients(sw, eventName, payload) {
|
|
181
|
+
async function dispatchPushToClients(sw, eventName, payload, preFetchedClientList = null) {
|
|
145
182
|
try {
|
|
146
|
-
const clientList = await sw.clients.matchAll({
|
|
183
|
+
const clientList = preFetchedClientList || await sw.clients.matchAll({
|
|
147
184
|
type: "window",
|
|
148
185
|
includeUncontrolled: true
|
|
149
186
|
});
|
|
@@ -185,9 +222,9 @@ function createNotificationFromPayload(payload, defaults) {
|
|
|
185
222
|
};
|
|
186
223
|
}
|
|
187
224
|
const pushNotification = payload.notification && typeof payload.notification === "object" ? payload.notification : {};
|
|
188
|
-
const title = pushNotification.title || payload.title || payload.contactName
|
|
225
|
+
const title = pushNotification.title || payload.title || payload.contactName || "New notification";
|
|
189
226
|
const body = pushNotification.body || payload.body || payload.message || "";
|
|
190
|
-
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 } : {};
|
|
191
228
|
if (data.payload == null) data.payload = payload;
|
|
192
229
|
return {
|
|
193
230
|
title,
|
|
@@ -255,34 +292,43 @@ async function acceptMultipartChunk(sw, payload, options) {
|
|
|
255
292
|
total: normalized.total,
|
|
256
293
|
originalMessageKind: normalized.originalMessageKind,
|
|
257
294
|
encoding: normalized.encoding,
|
|
258
|
-
|
|
295
|
+
receivedCount: 0,
|
|
259
296
|
receivedBytes: 0
|
|
260
297
|
};
|
|
261
298
|
if (base.total !== normalized.total || base.encoding !== normalized.encoding) {
|
|
262
299
|
await deleteMultipartPending(normalized.id);
|
|
300
|
+
await deleteMultipartChunks(normalized.id, base.total);
|
|
263
301
|
return null;
|
|
264
302
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
base.
|
|
303
|
+
const chunkId = `${normalized.id}_${normalized.index}`;
|
|
304
|
+
const chunkExists = await hasMultipartChunk(chunkId);
|
|
305
|
+
if (chunkExists) return null;
|
|
306
|
+
base.receivedCount++;
|
|
269
307
|
base.receivedBytes = positiveIntegerOrDefault(base.receivedBytes, 0) + normalized.chunkBytes.byteLength;
|
|
270
308
|
if (base.receivedBytes > options.maxTotalBytes) {
|
|
271
309
|
await deleteMultipartPending(normalized.id);
|
|
310
|
+
await deleteMultipartChunks(normalized.id, base.total);
|
|
272
311
|
return null;
|
|
273
312
|
}
|
|
274
|
-
|
|
275
|
-
|
|
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) {
|
|
276
320
|
await writeMultipartPending(base);
|
|
277
321
|
return null;
|
|
278
322
|
}
|
|
279
323
|
await deleteMultipartPending(base.id);
|
|
280
324
|
let restored;
|
|
281
325
|
try {
|
|
282
|
-
restored = restoreMultipartPayload(base, options);
|
|
326
|
+
restored = await restoreMultipartPayload(base, options);
|
|
283
327
|
} catch (_error) {
|
|
328
|
+
await deleteMultipartChunks(base.id, base.total);
|
|
284
329
|
return null;
|
|
285
330
|
}
|
|
331
|
+
await deleteMultipartChunks(base.id, base.total);
|
|
286
332
|
const doneTtlMs = Math.max(base.ttlMs * 2, base.ttlMs + 1);
|
|
287
333
|
await writeMultipartDone({
|
|
288
334
|
id: base.id,
|
|
@@ -300,8 +346,9 @@ function normalizeMultipartChunk(payload, options) {
|
|
|
300
346
|
if (meta.index <= 0 || meta.index > meta.total) return null;
|
|
301
347
|
let chunkBytes;
|
|
302
348
|
try {
|
|
303
|
-
chunkBytes = base64UrlToBytes(payload.chunk);
|
|
349
|
+
chunkBytes = (0, import_amsg_shared.base64UrlToBytes)(payload.chunk);
|
|
304
350
|
} catch (_error) {
|
|
351
|
+
console.error("RESTORE ERROR", _error);
|
|
305
352
|
return null;
|
|
306
353
|
}
|
|
307
354
|
const now = Date.now();
|
|
@@ -324,22 +371,22 @@ function normalizeMultipartChunk(payload, options) {
|
|
|
324
371
|
chunkBytes
|
|
325
372
|
};
|
|
326
373
|
}
|
|
327
|
-
function restoreMultipartPayload(record, options) {
|
|
374
|
+
async function restoreMultipartPayload(record, options) {
|
|
328
375
|
const chunks = [];
|
|
329
376
|
let totalBytes = 0;
|
|
330
377
|
for (let index = 1; index <= record.total; index++) {
|
|
331
|
-
const
|
|
332
|
-
if (typeof chunk !== "string") {
|
|
378
|
+
const chunkRecord = await readMultipartChunk(record.id, index);
|
|
379
|
+
if (!chunkRecord || typeof chunkRecord.chunk !== "string") {
|
|
333
380
|
throw new Error("[rei-standard-amsg-sw] multipart missing chunk");
|
|
334
381
|
}
|
|
335
|
-
const bytes = base64UrlToBytes(chunk);
|
|
382
|
+
const bytes = (0, import_amsg_shared.base64UrlToBytes)(chunkRecord.chunk);
|
|
336
383
|
totalBytes += bytes.byteLength;
|
|
337
384
|
if (totalBytes > options.maxTotalBytes) {
|
|
338
385
|
throw new Error("[rei-standard-amsg-sw] multipart payload exceeds maxTotalBytes");
|
|
339
386
|
}
|
|
340
387
|
chunks.push(bytes);
|
|
341
388
|
}
|
|
342
|
-
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));
|
|
343
390
|
return JSON.parse(json);
|
|
344
391
|
}
|
|
345
392
|
async function maybeCleanupMultipart(sw, ctx) {
|
|
@@ -351,50 +398,62 @@ async function maybeCleanupMultipart(sw, ctx) {
|
|
|
351
398
|
try {
|
|
352
399
|
await cleanupMultipartStores(sw, now);
|
|
353
400
|
} catch (_error) {
|
|
401
|
+
console.error("RESTORE ERROR", _error);
|
|
354
402
|
}
|
|
355
403
|
}
|
|
356
404
|
async function cleanupMultipartStores(sw, now) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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);
|
|
361
430
|
await dispatchMultipartExpired(sw, record);
|
|
362
431
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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);
|
|
367
443
|
}
|
|
444
|
+
});
|
|
445
|
+
for (const id of doneExpiredKeys) {
|
|
446
|
+
await deleteStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
368
447
|
}
|
|
369
448
|
}
|
|
370
449
|
async function dispatchMultipartExpired(sw, record) {
|
|
371
450
|
await dispatchPushToClients(sw, REI_SW_EVENT.MULTIPART_EXPIRED, {
|
|
372
451
|
id: record.id,
|
|
373
|
-
received:
|
|
452
|
+
received: typeof record.receivedCount === "number" ? record.receivedCount : 0,
|
|
374
453
|
total: record.total,
|
|
375
454
|
originalMessageKind: record.originalMessageKind
|
|
376
455
|
});
|
|
377
456
|
}
|
|
378
|
-
function base64UrlToBytes(input) {
|
|
379
|
-
const s = String(input).replace(/-/g, "+").replace(/_/g, "/");
|
|
380
|
-
const pad = (4 - s.length % 4) % 4;
|
|
381
|
-
const padded = s + "=".repeat(pad);
|
|
382
|
-
const bin = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("binary");
|
|
383
|
-
const out = new Uint8Array(bin.length);
|
|
384
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
385
|
-
return out;
|
|
386
|
-
}
|
|
387
|
-
function concatBytes(chunks) {
|
|
388
|
-
let total = 0;
|
|
389
|
-
for (const chunk of chunks) total += chunk.byteLength;
|
|
390
|
-
const out = new Uint8Array(total);
|
|
391
|
-
let offset = 0;
|
|
392
|
-
for (const chunk of chunks) {
|
|
393
|
-
out.set(chunk, offset);
|
|
394
|
-
offset += chunk.byteLength;
|
|
395
|
-
}
|
|
396
|
-
return out;
|
|
397
|
-
}
|
|
398
457
|
async function enqueueAndFlush(sw, event, requestPayload) {
|
|
399
458
|
try {
|
|
400
459
|
const request = normalizeQueuedRequest(requestPayload);
|
|
@@ -454,6 +513,7 @@ function normalizeRequestBody(bodyInput) {
|
|
|
454
513
|
try {
|
|
455
514
|
return JSON.stringify(bodyInput);
|
|
456
515
|
} catch (_error) {
|
|
516
|
+
console.error("RESTORE ERROR", _error);
|
|
457
517
|
throw new Error("[rei-standard-amsg-sw] request body is not serializable");
|
|
458
518
|
}
|
|
459
519
|
}
|
|
@@ -480,6 +540,7 @@ async function trySendQueuedRequest(queuedRequest) {
|
|
|
480
540
|
}
|
|
481
541
|
return false;
|
|
482
542
|
} catch (_error) {
|
|
543
|
+
console.error("RESTORE ERROR", _error);
|
|
483
544
|
return false;
|
|
484
545
|
}
|
|
485
546
|
}
|
|
@@ -489,6 +550,7 @@ async function registerFlushSync(sw) {
|
|
|
489
550
|
try {
|
|
490
551
|
await syncManager.register(REI_SW_SYNC_TAG);
|
|
491
552
|
} catch (_error) {
|
|
553
|
+
console.error("RESTORE ERROR", _error);
|
|
492
554
|
}
|
|
493
555
|
}
|
|
494
556
|
function respondToSender(event, message) {
|
|
@@ -511,9 +573,6 @@ function writeMultipartPending(record) {
|
|
|
511
573
|
function deleteMultipartPending(id) {
|
|
512
574
|
return deleteStoreRecord(REI_SW_MULTIPART_STORE, id);
|
|
513
575
|
}
|
|
514
|
-
function listMultipartPending() {
|
|
515
|
-
return listStoreRecords(REI_SW_MULTIPART_STORE);
|
|
516
|
-
}
|
|
517
576
|
function readMultipartDone(id) {
|
|
518
577
|
return readStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
519
578
|
}
|
|
@@ -523,8 +582,54 @@ function writeMultipartDone(record) {
|
|
|
523
582
|
function deleteMultipartDone(id) {
|
|
524
583
|
return deleteStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
|
|
525
584
|
}
|
|
526
|
-
function
|
|
527
|
-
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
|
+
});
|
|
528
633
|
}
|
|
529
634
|
async function readStoreRecord(storeName, id) {
|
|
530
635
|
if (!hasIndexedDB()) {
|
|
@@ -558,28 +663,14 @@ async function deleteStoreRecord(storeName, id) {
|
|
|
558
663
|
request.onerror = () => reject(request.error || new Error(`Failed to delete ${storeName}`));
|
|
559
664
|
});
|
|
560
665
|
}
|
|
561
|
-
async function listStoreRecords(storeName) {
|
|
562
|
-
if (!hasIndexedDB()) {
|
|
563
|
-
return Array.from(memoryStoreFor(storeName).values()).map(cloneRecord);
|
|
564
|
-
}
|
|
565
|
-
return withDatabaseStore(storeName, "readonly", (store, resolve, reject) => {
|
|
566
|
-
const request = store.getAll();
|
|
567
|
-
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
|
|
568
|
-
request.onerror = () => reject(request.error || new Error(`Failed to list ${storeName}`));
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
666
|
async function withDatabaseStore(storeName, mode, handler) {
|
|
572
667
|
const db = await openQueueDatabase();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
});
|
|
580
|
-
} finally {
|
|
581
|
-
db.close();
|
|
582
|
-
}
|
|
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
|
+
});
|
|
583
674
|
}
|
|
584
675
|
function hasIndexedDB() {
|
|
585
676
|
return typeof indexedDB !== "undefined" && indexedDB && typeof indexedDB.open === "function";
|
|
@@ -587,6 +678,7 @@ function hasIndexedDB() {
|
|
|
587
678
|
function memoryStoreFor(storeName) {
|
|
588
679
|
if (storeName === REI_SW_MULTIPART_DONE_STORE) return memoryMultipartDone;
|
|
589
680
|
if (storeName === REI_SW_MULTIPART_STORE) return memoryMultipartPending;
|
|
681
|
+
if (storeName === REI_SW_MULTIPART_CHUNK_STORE) return memoryMultipartChunks;
|
|
590
682
|
throw new Error(`[rei-standard-amsg-sw] unknown memory store: ${storeName}`);
|
|
591
683
|
}
|
|
592
684
|
function cloneRecord(record) {
|
|
@@ -594,35 +686,47 @@ function cloneRecord(record) {
|
|
|
594
686
|
return JSON.parse(JSON.stringify(record));
|
|
595
687
|
}
|
|
596
688
|
function openQueueDatabase() {
|
|
689
|
+
if (cachedDB) return Promise.resolve(cachedDB);
|
|
597
690
|
return new Promise((resolve, reject) => {
|
|
598
691
|
const request = indexedDB.open(REI_SW_DB_NAME, REI_SW_DB_VERSION);
|
|
599
692
|
request.onupgradeneeded = () => {
|
|
600
693
|
const db = request.result;
|
|
601
|
-
|
|
602
|
-
createObjectStoreIfMissing(db,
|
|
603
|
-
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);
|
|
604
713
|
};
|
|
605
|
-
request.onsuccess = () => resolve(request.result);
|
|
606
714
|
request.onerror = () => reject(request.error || new Error("Failed to open queue database"));
|
|
607
715
|
});
|
|
608
716
|
}
|
|
609
|
-
function createObjectStoreIfMissing(db, name, options) {
|
|
610
|
-
if (db.objectStoreNames.contains(name)) return;
|
|
611
|
-
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);
|
|
612
720
|
}
|
|
613
721
|
async function withQueueStore(mode, handler) {
|
|
614
722
|
const db = await openQueueDatabase();
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
});
|
|
623
|
-
} finally {
|
|
624
|
-
db.close();
|
|
625
|
-
}
|
|
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
|
+
});
|
|
626
730
|
}
|
|
627
731
|
async function addQueuedRequest(request) {
|
|
628
732
|
return withQueueStore("readwrite", (store, resolve, reject) => {
|