@rei-standard/amsg-sw 2.0.1 → 2.1.0-next.2

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
@@ -3,9 +3,108 @@
3
3
  `@rei-standard/amsg-sw` 是 ReiStandard 主动消息标准的 Service Worker 插件包,目标是让推送展示和离线重试“开箱即用”。
4
4
 
5
5
 
6
+ ## v2.1.0 — 按 kind 分发的客户端事件
7
+
8
+ 2.1.0 跟随 `@rei-standard/amsg-shared` 的三轴 push schema:每条 push 现在通过 `payload.messageKind`(`content` / `reasoning` / `tool_request` / `error`)区分内容类型。SW 在收到 push 后会做两件事:
9
+
10
+ 1. **永远** 通过 `postMessage` 把 payload 广播给所有受控窗口(包括 `includeUncontrolled: true` 的未受控窗口)。
11
+ 2. **仅当** `messageKind === 'content'` 或 payload 没有 `messageKind`(2.0.x 老 payload 的回退路径)时,才调用 `showNotification`。`reasoning` / `tool_request` / `error` 三种 kind 一律不弹通知——业务在 app 内通过 postMessage 通道自行渲染。
12
+
13
+ ### 新增导出 `REI_SW_EVENT`
14
+
15
+ 事件名由 SW 在每次广播时打在 `e.data.event` 上:
16
+
17
+ | 常量 | 字符串值 | 触发条件 |
18
+ |------|---------|---------|
19
+ | `REI_SW_EVENT.CONTENT_RECEIVED` | `'rei-amsg-content-received'` | `payload.messageKind === 'content'` |
20
+ | `REI_SW_EVENT.REASONING_RECEIVED` | `'rei-amsg-reasoning-received'` | `payload.messageKind === 'reasoning'` |
21
+ | `REI_SW_EVENT.TOOL_REQUEST_RECEIVED` | `'rei-amsg-tool-request-received'` | `payload.messageKind === 'tool_request'` |
22
+ | `REI_SW_EVENT.ERROR_RECEIVED` | `'rei-amsg-error-received'` | `payload.messageKind === 'error'` |
23
+ | `REI_SW_EVENT.MULTIPART_EXPIRED` | `'rei-amsg-multipart-expired'` | `_multipart` 分片 TTL 到期仍未收齐 |
24
+ | `REI_SW_EVENT.UNKNOWN_RECEIVED` | `'rei-amsg-unknown-received'` | 缺 `messageKind`(2.0.x 老 payload / blob envelope) |
25
+
26
+ ### 客户端订阅示例
27
+
28
+ ```js
29
+ navigator.serviceWorker.addEventListener('message', (e) => {
30
+ if (e.data?.type !== 'REI_AMSG_PUSH') return;
31
+ switch (e.data.event) {
32
+ case 'rei-amsg-content-received': /* 渲染 app 内消息 */ break;
33
+ case 'rei-amsg-reasoning-received': /* 渲染思考中 UI */ break;
34
+ case 'rei-amsg-tool-request-received': /* 弹出工具执行确认 */ break;
35
+ case 'rei-amsg-error-received': /* 显示错误 toast */ break;
36
+ case 'rei-amsg-multipart-expired': /* 观测 transport 缺片 */ break;
37
+ case 'rei-amsg-unknown-received': /* 2.0.x 老 payload 的兼容路径 */ break;
38
+ }
39
+ });
40
+ ```
41
+
42
+ ### Blob envelope
43
+
44
+ 当 `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
+
46
+ ### Generic multipart transport(next)
47
+
48
+ next 阶段移除了旧 reasoning 专用 `chunkIndex` / `totalChunks` wire format。现在 `_multipart` 是统一 transport kind,任何原始 payload 都可以被包起来:
49
+
50
+ ```json
51
+ {
52
+ "messageKind": "_multipart",
53
+ "multipart": {
54
+ "version": 1,
55
+ "id": "mp_<uuid>",
56
+ "index": 1,
57
+ "total": 4,
58
+ "encoding": "json-utf8-base64url",
59
+ "originalMessageKind": "reasoning",
60
+ "createdAt": 1710000000000,
61
+ "ttlMs": 60000
62
+ },
63
+ "chunk": "base64url..."
64
+ }
65
+ ```
66
+
67
+ SW 收到 `_multipart` 后会先写 IndexedDB,支持乱序、重复分片和 SW 重启恢复。未收齐时不 `postMessage`、不 `showNotification`。收齐后按 `index` 拼回原始 JSON payload,删除 pending,写短期 done 标记避免推送服务重投递造成二次业务事件,然后递归走普通 `messageKind` 分发。
68
+
69
+ 配置:
70
+
71
+ ```js
72
+ installReiSW(self, {
73
+ defaultIcon: '/icon-192x192.png',
74
+ defaultBadge: '/badge-72x72.png',
75
+ multipart: {
76
+ enabled: true,
77
+ ttlMs: 60_000,
78
+ maxTotalBytes: 256_000,
79
+ maxChunks: 128,
80
+ cleanupIntervalMs: 15 * 60_000
81
+ }
82
+ });
83
+ ```
84
+
85
+ TTL 到期仍未收齐时,SW 会清理 pending 并广播:
86
+
87
+ ```js
88
+ {
89
+ type: 'REI_AMSG_PUSH',
90
+ event: 'rei-amsg-multipart-expired',
91
+ payload: { id, received, total, originalMessageKind }
92
+ }
93
+ ```
94
+
95
+ 业务应用只订阅普通事件即可。`content` multipart 收齐后照常弹通知;`reasoning` / `tool_request` / `error` 仍默认不弹通知。
96
+
97
+ ### 升级注意事项
98
+
99
+ - 想给 `reasoning` / `tool_request` / `error` 也弹通知的业务:必须自行在 app 内监听上面的 postMessage 事件、调 `Notification` 或 `registration.showNotification`。SW 默认不再为它们弹通知。
100
+ - 应用级 SW 可以删除旧 reasoning `chunkIndex` / `totalChunks` 拼接逻辑;next 版本只会把完整还原后的 reasoning payload 发给 client。
101
+ - 客户端代码继续兼容只有 `installReiSW` + `REI_SW_MESSAGE_TYPE`(队列)的 2.0.x 写法——新增导出不破坏既有 API。
102
+ - 想拿到 push 类型相关的 TS 类型:从 `@rei-standard/amsg-shared` 引 `AmsgPush` 等类型(本包通过 JSDoc 引用同一份类型)。
103
+
6
104
  ## 功能概览
7
105
 
8
- - 处理 `push` 事件:自动解析 payload 并展示通知
106
+ - 处理 `push` 事件:按 `messageKind` 三轴 schema 分发到客户端 + 仅 `content` 走 `showNotification`
107
+ - 透明重组 `_multipart` transport:应用层只收到完整原始 payload
9
108
  - 处理 `message` 事件:支持离线请求入队与主动冲刷队列
10
109
  - 处理 `sync` 事件:在网络恢复后自动重试队列请求
11
110
  - 使用 IndexedDB 存储待发送请求,避免页面关闭后丢失
@@ -25,7 +124,8 @@ import { installReiSW } from '@rei-standard/amsg-sw';
25
124
 
26
125
  installReiSW(self, {
27
126
  defaultIcon: '/icon-192x192.png',
28
- defaultBadge: '/badge-72x72.png'
127
+ defaultBadge: '/badge-72x72.png',
128
+ multipart: { enabled: true }
29
129
  });
30
130
 
31
131
  // 业务侧自行实现点击跳转
@@ -97,8 +197,19 @@ export async function enqueueRequestToSW(requestPayload) {
97
197
  ## 导出 API(Exports)
98
198
 
99
199
  - `installReiSW`
200
+ - `REI_SW_EVENT` — 2.1.0 新增,按 kind 分发的客户端事件名
201
+ - `REI_AMSG_POSTMESSAGE_TYPE` — 2.1.0 新增,SW → client 广播信封的 `type` 字段(恒为 `'REI_AMSG_PUSH'`)
100
202
  - `REI_SW_MESSAGE_TYPE`
101
203
 
204
+ `REI_SW_EVENT` 包含(详见上文 v2.1.0 章节):
205
+
206
+ - `CONTENT_RECEIVED`
207
+ - `REASONING_RECEIVED`
208
+ - `TOOL_REQUEST_RECEIVED`
209
+ - `ERROR_RECEIVED`
210
+ - `MULTIPART_EXPIRED`
211
+ - `UNKNOWN_RECEIVED`
212
+
102
213
  `REI_SW_MESSAGE_TYPE` 包含:
103
214
 
104
215
  - `ENQUEUE_REQUEST`
package/dist/index.cjs CHANGED
@@ -19,14 +19,38 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/index.js
20
20
  var src_exports = {};
21
21
  __export(src_exports, {
22
+ REI_AMSG_POSTMESSAGE_TYPE: () => REI_AMSG_POSTMESSAGE_TYPE,
23
+ REI_SW_EVENT: () => REI_SW_EVENT,
22
24
  REI_SW_MESSAGE_TYPE: () => REI_SW_MESSAGE_TYPE,
23
25
  installReiSW: () => installReiSW
24
26
  });
25
27
  module.exports = __toCommonJS(src_exports);
26
28
  var REI_SW_DB_NAME = "rei-sw";
27
29
  var REI_SW_DB_STORE = "request-outbox";
28
- var REI_SW_DB_VERSION = 1;
30
+ var REI_SW_MULTIPART_STORE = "multipart-pending";
31
+ var REI_SW_MULTIPART_DONE_STORE = "multipart-done";
32
+ var REI_SW_DB_VERSION = 2;
29
33
  var REI_SW_SYNC_TAG = "rei-sw-flush-request-outbox";
34
+ var MULTIPART_MESSAGE_KIND = "_multipart";
35
+ var MULTIPART_ENCODING = "json-utf8-base64url";
36
+ var DEFAULT_MULTIPART_OPTIONS = Object.freeze({
37
+ enabled: true,
38
+ ttlMs: 6e4,
39
+ maxTotalBytes: 256e3,
40
+ maxChunks: 128,
41
+ cleanupIntervalMs: 15 * 6e4
42
+ });
43
+ var memoryMultipartPending = /* @__PURE__ */ new Map();
44
+ var memoryMultipartDone = /* @__PURE__ */ new Map();
45
+ var REI_AMSG_POSTMESSAGE_TYPE = "REI_AMSG_PUSH";
46
+ var REI_SW_EVENT = Object.freeze({
47
+ CONTENT_RECEIVED: "rei-amsg-content-received",
48
+ REASONING_RECEIVED: "rei-amsg-reasoning-received",
49
+ TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received",
50
+ ERROR_RECEIVED: "rei-amsg-error-received",
51
+ MULTIPART_EXPIRED: "rei-amsg-multipart-expired",
52
+ UNKNOWN_RECEIVED: "rei-amsg-unknown-received"
53
+ });
30
54
  var REI_SW_MESSAGE_TYPE = Object.freeze({
31
55
  ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST",
32
56
  FLUSH_QUEUE: "REI_FLUSH_QUEUE",
@@ -35,17 +59,20 @@ var REI_SW_MESSAGE_TYPE = Object.freeze({
35
59
  function installReiSW(sw, opts = {}) {
36
60
  const defaultIcon = opts.defaultIcon || "/icon-192x192.png";
37
61
  const defaultBadge = opts.defaultBadge || "/badge-72x72.png";
62
+ const multipart = normalizeMultipartOptions(opts.multipart);
63
+ let lastMultipartCleanupAt = 0;
38
64
  sw.addEventListener("push", (event) => {
39
65
  const payload = readPushPayload(event);
40
66
  if (!payload) return;
41
- const notification = createNotificationFromPayload(payload, {
67
+ event.waitUntil(handlePushPayload(sw, payload, {
68
+ defaultBadge,
42
69
  defaultIcon,
43
- defaultBadge
44
- });
45
- if (!notification) return;
46
- event.waitUntil(
47
- sw.registration.showNotification(notification.title, notification.options)
48
- );
70
+ multipart,
71
+ getLastMultipartCleanupAt: () => lastMultipartCleanupAt,
72
+ setLastMultipartCleanupAt: (value) => {
73
+ lastMultipartCleanupAt = value;
74
+ }
75
+ }));
49
76
  });
50
77
  sw.addEventListener("message", (event) => {
51
78
  const message = event.data;
@@ -65,6 +92,75 @@ function installReiSW(sw, opts = {}) {
65
92
  event.waitUntil(flushQueuedRequests(sw));
66
93
  });
67
94
  }
95
+ async function handlePushPayload(sw, payload, ctx) {
96
+ await maybeCleanupMultipart(sw, ctx);
97
+ if (isMultipartPush(payload)) {
98
+ if (!ctx.multipart.enabled) return;
99
+ const restoredPayload = await acceptMultipartChunk(sw, payload, ctx.multipart);
100
+ if (!restoredPayload) return;
101
+ await handlePushPayload(sw, restoredPayload, ctx);
102
+ return;
103
+ }
104
+ await dispatchBusinessPayload(sw, payload, {
105
+ defaultIcon: ctx.defaultIcon,
106
+ defaultBadge: ctx.defaultBadge
107
+ });
108
+ }
109
+ async function dispatchBusinessPayload(sw, payload, defaults) {
110
+ const eventName = resolveEventName(payload);
111
+ const shouldRenderNotification = isNotificationKind(payload);
112
+ const work = [dispatchPushToClients(sw, eventName, payload)];
113
+ if (shouldRenderNotification) {
114
+ const notification = createNotificationFromPayload(payload, defaults);
115
+ if (notification) {
116
+ work.push(
117
+ sw.registration.showNotification(notification.title, notification.options)
118
+ );
119
+ }
120
+ }
121
+ await Promise.all(work);
122
+ }
123
+ function resolveEventName(payload) {
124
+ const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
125
+ switch (kind) {
126
+ case "content":
127
+ return REI_SW_EVENT.CONTENT_RECEIVED;
128
+ case "reasoning":
129
+ return REI_SW_EVENT.REASONING_RECEIVED;
130
+ case "tool_request":
131
+ return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
132
+ case "error":
133
+ return REI_SW_EVENT.ERROR_RECEIVED;
134
+ default:
135
+ return REI_SW_EVENT.UNKNOWN_RECEIVED;
136
+ }
137
+ }
138
+ function isNotificationKind(payload) {
139
+ if (!payload || typeof payload !== "object") return false;
140
+ const kind = payload.messageKind;
141
+ if (kind === void 0 || kind === null) return true;
142
+ return kind === "content";
143
+ }
144
+ async function dispatchPushToClients(sw, eventName, payload) {
145
+ try {
146
+ const clientList = await sw.clients.matchAll({
147
+ type: "window",
148
+ includeUncontrolled: true
149
+ });
150
+ const envelope = {
151
+ type: REI_AMSG_POSTMESSAGE_TYPE,
152
+ event: eventName,
153
+ payload
154
+ };
155
+ for (const client of clientList) {
156
+ try {
157
+ client.postMessage(envelope);
158
+ } catch (_postError) {
159
+ }
160
+ }
161
+ } catch (_matchError) {
162
+ }
163
+ }
68
164
  function readPushPayload(event) {
69
165
  if (!event.data) return null;
70
166
  try {
@@ -89,7 +185,7 @@ function createNotificationFromPayload(payload, defaults) {
89
185
  };
90
186
  }
91
187
  const pushNotification = payload.notification && typeof payload.notification === "object" ? payload.notification : {};
92
- const title = pushNotification.title || payload.title || "New notification";
188
+ const title = pushNotification.title || payload.title || payload.contactName && `\u6765\u81EA ${payload.contactName}` || "New notification";
93
189
  const body = pushNotification.body || payload.body || payload.message || "";
94
190
  const data = payload.data && typeof payload.data === "object" ? { ...payload.data } : {};
95
191
  if (data.payload == null) data.payload = payload;
@@ -108,6 +204,197 @@ function createNotificationFromPayload(payload, defaults) {
108
204
  }
109
205
  };
110
206
  }
207
+ function normalizeMultipartOptions(input) {
208
+ const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
209
+ return {
210
+ enabled: source.enabled !== false,
211
+ ttlMs: positiveIntegerOrDefault(source.ttlMs, DEFAULT_MULTIPART_OPTIONS.ttlMs),
212
+ maxTotalBytes: positiveIntegerOrDefault(
213
+ source.maxTotalBytes,
214
+ DEFAULT_MULTIPART_OPTIONS.maxTotalBytes
215
+ ),
216
+ maxChunks: positiveIntegerOrDefault(source.maxChunks, DEFAULT_MULTIPART_OPTIONS.maxChunks),
217
+ cleanupIntervalMs: source.cleanupIntervalMs === 0 ? 0 : positiveIntegerOrDefault(
218
+ source.cleanupIntervalMs,
219
+ DEFAULT_MULTIPART_OPTIONS.cleanupIntervalMs
220
+ )
221
+ };
222
+ }
223
+ function positiveIntegerOrDefault(value, fallback) {
224
+ return Number.isInteger(value) && value > 0 ? value : fallback;
225
+ }
226
+ function isMultipartPush(payload) {
227
+ return !!payload && typeof payload === "object" && payload.messageKind === MULTIPART_MESSAGE_KIND && payload.multipart && typeof payload.multipart === "object" && typeof payload.chunk === "string";
228
+ }
229
+ async function acceptMultipartChunk(sw, payload, options) {
230
+ const normalized = normalizeMultipartChunk(payload, options);
231
+ if (!normalized) return null;
232
+ if (normalized.expiresAt <= Date.now()) {
233
+ await dispatchMultipartExpired(sw, {
234
+ id: normalized.id,
235
+ chunks: {},
236
+ total: normalized.total,
237
+ originalMessageKind: normalized.originalMessageKind
238
+ });
239
+ return null;
240
+ }
241
+ const done = await readMultipartDone(normalized.id);
242
+ if (done && done.expiresAt > Date.now()) return null;
243
+ if (done) await deleteMultipartDone(normalized.id);
244
+ const now = Date.now();
245
+ const existing = await readMultipartPending(normalized.id);
246
+ if (existing && existing.expiresAt <= now) {
247
+ await deleteMultipartPending(existing.id);
248
+ await dispatchMultipartExpired(sw, existing);
249
+ }
250
+ const base = existing && existing.expiresAt > now ? existing : {
251
+ id: normalized.id,
252
+ createdAt: normalized.createdAt,
253
+ expiresAt: normalized.expiresAt,
254
+ ttlMs: normalized.ttlMs,
255
+ total: normalized.total,
256
+ originalMessageKind: normalized.originalMessageKind,
257
+ encoding: normalized.encoding,
258
+ chunks: {},
259
+ receivedBytes: 0
260
+ };
261
+ if (base.total !== normalized.total || base.encoding !== normalized.encoding) {
262
+ await deleteMultipartPending(normalized.id);
263
+ return null;
264
+ }
265
+ if (base.chunks[String(normalized.index)] !== void 0) {
266
+ return null;
267
+ }
268
+ base.chunks[String(normalized.index)] = normalized.chunk;
269
+ base.receivedBytes = positiveIntegerOrDefault(base.receivedBytes, 0) + normalized.chunkBytes.byteLength;
270
+ if (base.receivedBytes > options.maxTotalBytes) {
271
+ await deleteMultipartPending(normalized.id);
272
+ return null;
273
+ }
274
+ const received = Object.keys(base.chunks).length;
275
+ if (received < base.total) {
276
+ await writeMultipartPending(base);
277
+ return null;
278
+ }
279
+ await deleteMultipartPending(base.id);
280
+ let restored;
281
+ try {
282
+ restored = restoreMultipartPayload(base, options);
283
+ } catch (_error) {
284
+ return null;
285
+ }
286
+ const doneTtlMs = Math.max(base.ttlMs * 2, base.ttlMs + 1);
287
+ await writeMultipartDone({
288
+ id: base.id,
289
+ expiresAt: Date.now() + doneTtlMs
290
+ });
291
+ return restored;
292
+ }
293
+ function normalizeMultipartChunk(payload, options) {
294
+ const meta = payload.multipart;
295
+ if (!meta || typeof meta !== "object") return null;
296
+ if (meta.version !== 1 || meta.encoding !== MULTIPART_ENCODING) return null;
297
+ if (typeof meta.id !== "string" || !meta.id) return null;
298
+ if (!Number.isInteger(meta.index) || !Number.isInteger(meta.total)) return null;
299
+ if (meta.total <= 0 || meta.total > options.maxChunks) return null;
300
+ if (meta.index <= 0 || meta.index > meta.total) return null;
301
+ let chunkBytes;
302
+ try {
303
+ chunkBytes = base64UrlToBytes(payload.chunk);
304
+ } catch (_error) {
305
+ return null;
306
+ }
307
+ const now = Date.now();
308
+ const ttlMs = Math.min(
309
+ positiveIntegerOrDefault(meta.ttlMs, options.ttlMs),
310
+ options.ttlMs
311
+ );
312
+ const createdAt = Number.isFinite(meta.createdAt) ? Number(meta.createdAt) : now;
313
+ const expiresAt = createdAt + ttlMs;
314
+ return {
315
+ id: meta.id,
316
+ createdAt,
317
+ expiresAt,
318
+ ttlMs,
319
+ total: meta.total,
320
+ index: meta.index,
321
+ originalMessageKind: typeof meta.originalMessageKind === "string" ? meta.originalMessageKind : null,
322
+ encoding: meta.encoding,
323
+ chunk: payload.chunk,
324
+ chunkBytes
325
+ };
326
+ }
327
+ function restoreMultipartPayload(record, options) {
328
+ const chunks = [];
329
+ let totalBytes = 0;
330
+ for (let index = 1; index <= record.total; index++) {
331
+ const chunk = record.chunks[String(index)];
332
+ if (typeof chunk !== "string") {
333
+ throw new Error("[rei-standard-amsg-sw] multipart missing chunk");
334
+ }
335
+ const bytes = base64UrlToBytes(chunk);
336
+ totalBytes += bytes.byteLength;
337
+ if (totalBytes > options.maxTotalBytes) {
338
+ throw new Error("[rei-standard-amsg-sw] multipart payload exceeds maxTotalBytes");
339
+ }
340
+ chunks.push(bytes);
341
+ }
342
+ const json = new TextDecoder("utf-8", { fatal: false }).decode(concatBytes(chunks));
343
+ return JSON.parse(json);
344
+ }
345
+ async function maybeCleanupMultipart(sw, ctx) {
346
+ if (!ctx.multipart.enabled) return;
347
+ const now = Date.now();
348
+ const last = ctx.getLastMultipartCleanupAt();
349
+ if (last && now - last < ctx.multipart.cleanupIntervalMs) return;
350
+ ctx.setLastMultipartCleanupAt(now);
351
+ try {
352
+ await cleanupMultipartStores(sw, now);
353
+ } catch (_error) {
354
+ }
355
+ }
356
+ async function cleanupMultipartStores(sw, now) {
357
+ const pending = await listMultipartPending();
358
+ for (const record of pending) {
359
+ if (record.expiresAt > now) continue;
360
+ await deleteMultipartPending(record.id);
361
+ await dispatchMultipartExpired(sw, record);
362
+ }
363
+ const done = await listMultipartDone();
364
+ for (const record of done) {
365
+ if (record.expiresAt <= now) {
366
+ await deleteMultipartDone(record.id);
367
+ }
368
+ }
369
+ }
370
+ async function dispatchMultipartExpired(sw, record) {
371
+ await dispatchPushToClients(sw, REI_SW_EVENT.MULTIPART_EXPIRED, {
372
+ id: record.id,
373
+ received: record.chunks && typeof record.chunks === "object" ? Object.keys(record.chunks).length : 0,
374
+ total: record.total,
375
+ originalMessageKind: record.originalMessageKind
376
+ });
377
+ }
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
+ }
111
398
  async function enqueueAndFlush(sw, event, requestPayload) {
112
399
  try {
113
400
  const request = normalizeQueuedRequest(requestPayload);
@@ -215,18 +502,114 @@ function respondToSender(event, message) {
215
502
  source.postMessage(message);
216
503
  }
217
504
  }
505
+ function readMultipartPending(id) {
506
+ return readStoreRecord(REI_SW_MULTIPART_STORE, id);
507
+ }
508
+ function writeMultipartPending(record) {
509
+ return putStoreRecord(REI_SW_MULTIPART_STORE, record);
510
+ }
511
+ function deleteMultipartPending(id) {
512
+ return deleteStoreRecord(REI_SW_MULTIPART_STORE, id);
513
+ }
514
+ function listMultipartPending() {
515
+ return listStoreRecords(REI_SW_MULTIPART_STORE);
516
+ }
517
+ function readMultipartDone(id) {
518
+ return readStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
519
+ }
520
+ function writeMultipartDone(record) {
521
+ return putStoreRecord(REI_SW_MULTIPART_DONE_STORE, record);
522
+ }
523
+ function deleteMultipartDone(id) {
524
+ return deleteStoreRecord(REI_SW_MULTIPART_DONE_STORE, id);
525
+ }
526
+ function listMultipartDone() {
527
+ return listStoreRecords(REI_SW_MULTIPART_DONE_STORE);
528
+ }
529
+ async function readStoreRecord(storeName, id) {
530
+ if (!hasIndexedDB()) {
531
+ return cloneRecord(memoryStoreFor(storeName).get(id));
532
+ }
533
+ return withDatabaseStore(storeName, "readonly", (store, resolve, reject) => {
534
+ const request = store.get(id);
535
+ request.onsuccess = () => resolve(request.result || null);
536
+ request.onerror = () => reject(request.error || new Error(`Failed to read ${storeName}`));
537
+ });
538
+ }
539
+ async function putStoreRecord(storeName, record) {
540
+ if (!hasIndexedDB()) {
541
+ memoryStoreFor(storeName).set(record.id, cloneRecord(record));
542
+ return;
543
+ }
544
+ return withDatabaseStore(storeName, "readwrite", (store, resolve, reject) => {
545
+ const request = store.put(record);
546
+ request.onsuccess = () => resolve(void 0);
547
+ request.onerror = () => reject(request.error || new Error(`Failed to write ${storeName}`));
548
+ });
549
+ }
550
+ async function deleteStoreRecord(storeName, id) {
551
+ if (!hasIndexedDB()) {
552
+ memoryStoreFor(storeName).delete(id);
553
+ return;
554
+ }
555
+ return withDatabaseStore(storeName, "readwrite", (store, resolve, reject) => {
556
+ const request = store.delete(id);
557
+ request.onsuccess = () => resolve(void 0);
558
+ request.onerror = () => reject(request.error || new Error(`Failed to delete ${storeName}`));
559
+ });
560
+ }
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
+ async function withDatabaseStore(storeName, mode, handler) {
572
+ const db = await openQueueDatabase();
573
+ try {
574
+ return await new Promise((resolve, reject) => {
575
+ const transaction = db.transaction(storeName, mode);
576
+ const store = transaction.objectStore(storeName);
577
+ transaction.onerror = () => reject(transaction.error || new Error("Database transaction failed"));
578
+ Promise.resolve(handler(store, resolve, reject)).catch(reject);
579
+ });
580
+ } finally {
581
+ db.close();
582
+ }
583
+ }
584
+ function hasIndexedDB() {
585
+ return typeof indexedDB !== "undefined" && indexedDB && typeof indexedDB.open === "function";
586
+ }
587
+ function memoryStoreFor(storeName) {
588
+ if (storeName === REI_SW_MULTIPART_DONE_STORE) return memoryMultipartDone;
589
+ if (storeName === REI_SW_MULTIPART_STORE) return memoryMultipartPending;
590
+ throw new Error(`[rei-standard-amsg-sw] unknown memory store: ${storeName}`);
591
+ }
592
+ function cloneRecord(record) {
593
+ if (record == null) return null;
594
+ return JSON.parse(JSON.stringify(record));
595
+ }
218
596
  function openQueueDatabase() {
219
597
  return new Promise((resolve, reject) => {
220
598
  const request = indexedDB.open(REI_SW_DB_NAME, REI_SW_DB_VERSION);
221
599
  request.onupgradeneeded = () => {
222
600
  const db = request.result;
223
- if (db.objectStoreNames.contains(REI_SW_DB_STORE)) return;
224
- db.createObjectStore(REI_SW_DB_STORE, { keyPath: "id", autoIncrement: true });
601
+ createObjectStoreIfMissing(db, REI_SW_DB_STORE, { keyPath: "id", autoIncrement: true });
602
+ createObjectStoreIfMissing(db, REI_SW_MULTIPART_STORE, { keyPath: "id" });
603
+ createObjectStoreIfMissing(db, REI_SW_MULTIPART_DONE_STORE, { keyPath: "id" });
225
604
  };
226
605
  request.onsuccess = () => resolve(request.result);
227
606
  request.onerror = () => reject(request.error || new Error("Failed to open queue database"));
228
607
  });
229
608
  }
609
+ function createObjectStoreIfMissing(db, name, options) {
610
+ if (db.objectStoreNames.contains(name)) return;
611
+ db.createObjectStore(name, options);
612
+ }
230
613
  async function withQueueStore(mode, handler) {
231
614
  const db = await openQueueDatabase();
232
615
  try {