@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 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(next)
95
+ ### Generic multipart transport(2.1.0+)
47
96
 
48
- next 阶段移除了旧 reasoning 专用 `chunkIndex` / `totalChunks` wire format。现在 `_multipart` 是统一 transport kind,任何原始 payload 都可以被包起来:
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-next.3)离线持久化等业务拦截钩子:
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` 也弹通知的业务:必须自行在 app 内监听上面的 postMessage 事件、调 `Notification` `registration.showNotification`。SW 默认不再为它们弹通知。
105
- - 应用级 SW 可以删除旧 reasoning `chunkIndex` / `totalChunks` 拼接逻辑;next 版本只会把完整还原后的 reasoning payload 发给 client。
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 REI_SW_DB_VERSION = 2;
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
- const shouldRenderNotification = isNotificationKind(payload);
114
- const work = [dispatchPushToClients(sw, eventName, payload)];
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 "content":
163
+ case import_amsg_shared.MESSAGE_KIND.CONTENT:
141
164
  return REI_SW_EVENT.CONTENT_RECEIVED;
142
- case "reasoning":
165
+ case import_amsg_shared.MESSAGE_KIND.REASONING:
143
166
  return REI_SW_EVENT.REASONING_RECEIVED;
144
- case "tool_request":
167
+ case import_amsg_shared.MESSAGE_KIND.TOOL_REQUEST:
145
168
  return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
146
- case "error":
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 === "content";
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 && `\u6765\u81EA ${payload.contactName}` || "New notification";
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
- chunks: {},
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
- if (base.chunks[String(normalized.index)] !== void 0) {
280
- return null;
281
- }
282
- base.chunks[String(normalized.index)] = normalized.chunk;
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
- const received = Object.keys(base.chunks).length;
289
- if (received < base.total) {
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 chunk = record.chunks[String(index)];
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
- const pending = await listMultipartPending();
372
- for (const record of pending) {
373
- if (record.expiresAt > now) continue;
374
- await deleteMultipartPending(record.id);
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 done = await listMultipartDone();
378
- for (const record of done) {
379
- if (record.expiresAt <= now) {
380
- await deleteMultipartDone(record.id);
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: record.chunks && typeof record.chunks === "object" ? Object.keys(record.chunks).length : 0,
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 listMultipartDone() {
541
- return listStoreRecords(REI_SW_MULTIPART_DONE_STORE);
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
- try {
588
- return await new Promise((resolve, reject) => {
589
- const transaction = db.transaction(storeName, mode);
590
- const store = transaction.objectStore(storeName);
591
- transaction.onerror = () => reject(transaction.error || new Error("Database transaction failed"));
592
- Promise.resolve(handler(store, resolve, reject)).catch(reject);
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
- createObjectStoreIfMissing(db, REI_SW_DB_STORE, { keyPath: "id", autoIncrement: true });
616
- createObjectStoreIfMissing(db, REI_SW_MULTIPART_STORE, { keyPath: "id" });
617
- createObjectStoreIfMissing(db, REI_SW_MULTIPART_DONE_STORE, { keyPath: "id" });
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
- try {
630
- return await new Promise((resolve, reject) => {
631
- const transaction = db.transaction(REI_SW_DB_STORE, mode);
632
- const store = transaction.objectStore(REI_SW_DB_STORE);
633
- transaction.oncomplete = () => resolve(void 0);
634
- transaction.onerror = () => reject(transaction.error || new Error("Queue transaction failed"));
635
- Promise.resolve(handler(store, resolve, reject)).catch(reject);
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) => {