@rei-standard/amsg-sw 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/dist/index.cjs +409 -35
- package/dist/index.d.cts +495 -37
- package/dist/index.d.ts +495 -37
- package/dist/index.mjs +409 -35
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,10 @@ var REI_SW_MULTIPART_DONE_STORE = "multipart-done";
|
|
|
7
7
|
var REI_SW_MULTIPART_CHUNK_STORE = "multipart-chunk";
|
|
8
8
|
var REI_SW_DB_VERSION = 3;
|
|
9
9
|
var cachedDB = null;
|
|
10
|
+
var REI_AMSG_DEDUPE_DB_NAME = "rei_amsg_sw_dedupe_v1";
|
|
11
|
+
var REI_AMSG_DEDUPE_STORE = "delivery-dedupe";
|
|
12
|
+
var DEFAULT_DEDUPE_TTL_MS = 10 * 6e4;
|
|
13
|
+
var DEFAULT_DEDUPE_CLEANUP_INTERVAL_MS = 6e4;
|
|
10
14
|
var REI_SW_SYNC_TAG = "rei-sw-flush-request-outbox";
|
|
11
15
|
var MULTIPART_MESSAGE_KIND = "_multipart";
|
|
12
16
|
var MULTIPART_ENCODING = "json-utf8-base64url";
|
|
@@ -20,6 +24,8 @@ var DEFAULT_MULTIPART_OPTIONS = Object.freeze({
|
|
|
20
24
|
var memoryMultipartPending = /* @__PURE__ */ new Map();
|
|
21
25
|
var memoryMultipartDone = /* @__PURE__ */ new Map();
|
|
22
26
|
var memoryMultipartChunks = /* @__PURE__ */ new Map();
|
|
27
|
+
var multipartLocks = /* @__PURE__ */ new Map();
|
|
28
|
+
var dedupeDbCache = /* @__PURE__ */ new Map();
|
|
23
29
|
var REI_AMSG_POSTMESSAGE_TYPE = "REI_AMSG_PUSH";
|
|
24
30
|
var REI_SW_EVENT = Object.freeze({
|
|
25
31
|
CONTENT_RECEIVED: "rei-amsg-content-received",
|
|
@@ -31,27 +37,39 @@ var REI_SW_EVENT = Object.freeze({
|
|
|
31
37
|
});
|
|
32
38
|
var REI_SW_MESSAGE_TYPE = Object.freeze({
|
|
33
39
|
ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST",
|
|
40
|
+
DELIVER: "REI_AMSG_DELIVER",
|
|
34
41
|
FLUSH_QUEUE: "REI_FLUSH_QUEUE",
|
|
35
42
|
QUEUE_RESULT: "REI_QUEUE_RESULT"
|
|
36
43
|
});
|
|
44
|
+
var REI_AMSG_DELIVER_MESSAGE_TYPE = REI_SW_MESSAGE_TYPE.DELIVER;
|
|
37
45
|
function installReiSW(sw, opts = {}) {
|
|
38
46
|
const defaultIcon = opts.defaultIcon || "/icon-192x192.png";
|
|
39
47
|
const defaultBadge = opts.defaultBadge || "/badge-72x72.png";
|
|
40
48
|
const multipart = normalizeMultipartOptions(opts.multipart);
|
|
49
|
+
const dedupe = normalizeDedupeOptions(opts.dedupe);
|
|
41
50
|
let lastMultipartCleanupAt = 0;
|
|
51
|
+
let lastDedupeCleanupAt = 0;
|
|
52
|
+
const makeDeliveryContext = (source) => ({
|
|
53
|
+
defaultBadge,
|
|
54
|
+
defaultIcon,
|
|
55
|
+
dedupe,
|
|
56
|
+
multipart,
|
|
57
|
+
onDuplicate: opts.onDuplicate,
|
|
58
|
+
onBusinessPayload: opts.onBusinessPayload,
|
|
59
|
+
source,
|
|
60
|
+
getLastDedupeCleanupAt: () => lastDedupeCleanupAt,
|
|
61
|
+
setLastDedupeCleanupAt: (value) => {
|
|
62
|
+
lastDedupeCleanupAt = value;
|
|
63
|
+
},
|
|
64
|
+
getLastMultipartCleanupAt: () => lastMultipartCleanupAt,
|
|
65
|
+
setLastMultipartCleanupAt: (value) => {
|
|
66
|
+
lastMultipartCleanupAt = value;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
42
69
|
sw.addEventListener("push", (event) => {
|
|
43
70
|
const payload = readPushPayload(event);
|
|
44
71
|
if (!payload) return;
|
|
45
|
-
event.waitUntil(handlePushPayload(sw, payload,
|
|
46
|
-
defaultBadge,
|
|
47
|
-
defaultIcon,
|
|
48
|
-
multipart,
|
|
49
|
-
onBusinessPayload: opts.onBusinessPayload,
|
|
50
|
-
getLastMultipartCleanupAt: () => lastMultipartCleanupAt,
|
|
51
|
-
setLastMultipartCleanupAt: (value) => {
|
|
52
|
-
lastMultipartCleanupAt = value;
|
|
53
|
-
}
|
|
54
|
-
}));
|
|
72
|
+
event.waitUntil(handlePushPayload(sw, payload, makeDeliveryContext("webpush")));
|
|
55
73
|
});
|
|
56
74
|
sw.addEventListener("message", (event) => {
|
|
57
75
|
const message = event.data;
|
|
@@ -62,6 +80,10 @@ function installReiSW(sw, opts = {}) {
|
|
|
62
80
|
);
|
|
63
81
|
return;
|
|
64
82
|
}
|
|
83
|
+
if (message.type === REI_SW_MESSAGE_TYPE.DELIVER) {
|
|
84
|
+
event.waitUntil(handleDeliverMessage(sw, event, message, makeDeliveryContext()));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
65
87
|
if (message.type === REI_SW_MESSAGE_TYPE.FLUSH_QUEUE) {
|
|
66
88
|
event.waitUntil(flushQueuedRequests(sw));
|
|
67
89
|
}
|
|
@@ -77,16 +99,48 @@ async function handlePushPayload(sw, payload, ctx) {
|
|
|
77
99
|
if (!ctx.multipart.enabled) return;
|
|
78
100
|
const restoredPayload = await acceptMultipartChunk(sw, payload, ctx.multipart);
|
|
79
101
|
if (!restoredPayload) return;
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
return handlePushPayload(sw, restoredPayload, ctx);
|
|
103
|
+
}
|
|
104
|
+
const claim = await claimDedupe(payload, ctx);
|
|
105
|
+
if (claim.duplicate) {
|
|
106
|
+
const duplicateNotification = await maybeShowDuplicateNotification(sw, payload, claim, ctx);
|
|
107
|
+
claim.duplicateNotification = duplicateNotification;
|
|
108
|
+
await notifyDuplicate(payload, claim, ctx);
|
|
109
|
+
return { ...claim, duplicateNotification };
|
|
82
110
|
}
|
|
83
111
|
await dispatchBusinessPayload(sw, payload, {
|
|
84
112
|
defaultIcon: ctx.defaultIcon,
|
|
85
113
|
defaultBadge: ctx.defaultBadge,
|
|
86
114
|
onBusinessPayload: ctx.onBusinessPayload
|
|
115
|
+
}, async (intermediateResult) => {
|
|
116
|
+
await updateDedupeNotificationState(claim, ctx, intermediateResult);
|
|
87
117
|
});
|
|
118
|
+
return claim;
|
|
119
|
+
}
|
|
120
|
+
async function handleDeliverMessage(sw, event, message, ctx) {
|
|
121
|
+
let result = {};
|
|
122
|
+
try {
|
|
123
|
+
if (!Object.prototype.hasOwnProperty.call(message, "payload")) {
|
|
124
|
+
throw new Error("[rei-standard-amsg-sw] REI_AMSG_DELIVER requires payload");
|
|
125
|
+
}
|
|
126
|
+
const source = typeof message.source === "string" && message.source ? message.source : "message";
|
|
127
|
+
result = await handlePushPayload(sw, message.payload, { ...ctx, source }) || {};
|
|
128
|
+
respondToSender(event, {
|
|
129
|
+
ok: true,
|
|
130
|
+
duplicate: Boolean(result.duplicate),
|
|
131
|
+
key: result.key,
|
|
132
|
+
requestId: message.requestId
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
respondToSender(event, {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: error instanceof Error ? error.message : "Failed to deliver payload",
|
|
138
|
+
key: result && result.key,
|
|
139
|
+
requestId: message.requestId
|
|
140
|
+
});
|
|
141
|
+
}
|
|
88
142
|
}
|
|
89
|
-
async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
143
|
+
async function dispatchBusinessPayload(sw, payload, defaults, onNotificationSettled) {
|
|
90
144
|
const eventName = resolveEventName(payload);
|
|
91
145
|
let clientList = [];
|
|
92
146
|
try {
|
|
@@ -96,40 +150,41 @@ async function dispatchBusinessPayload(sw, payload, defaults) {
|
|
|
96
150
|
});
|
|
97
151
|
} catch (_matchError) {
|
|
98
152
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
shouldRenderNotification = !hasVisibleClient;
|
|
106
|
-
} else if (showOpt === false) {
|
|
107
|
-
shouldRenderNotification = false;
|
|
108
|
-
} else {
|
|
109
|
-
shouldRenderNotification = isNotificationKind(payload);
|
|
110
|
-
}
|
|
111
|
-
const work = [dispatchPushToClients(sw, eventName, payload, clientList)];
|
|
112
|
-
if (shouldRenderNotification) {
|
|
153
|
+
const notificationState = {
|
|
154
|
+
shouldRender: shouldRenderNotification(payload, clientList),
|
|
155
|
+
shown: false
|
|
156
|
+
};
|
|
157
|
+
const notificationWork = [dispatchPushToClients(sw, eventName, payload, clientList)];
|
|
158
|
+
if (notificationState.shouldRender) {
|
|
113
159
|
const notification = createNotificationFromPayload(payload, defaults);
|
|
114
160
|
if (notification) {
|
|
115
|
-
|
|
116
|
-
sw.registration.showNotification(notification.title, notification.options)
|
|
161
|
+
notificationWork.push(
|
|
162
|
+
sw.registration.showNotification(notification.title, notification.options).then(() => {
|
|
163
|
+
notificationState.shown = true;
|
|
164
|
+
})
|
|
117
165
|
);
|
|
118
166
|
}
|
|
119
167
|
}
|
|
168
|
+
let businessWork = null;
|
|
120
169
|
if (typeof defaults.onBusinessPayload === "function") {
|
|
121
170
|
try {
|
|
122
171
|
const result = defaults.onBusinessPayload(payload);
|
|
123
|
-
if (result
|
|
124
|
-
|
|
172
|
+
if (result && typeof result.then === "function") {
|
|
173
|
+
businessWork = Promise.resolve(result).catch((error) => {
|
|
125
174
|
console.error("[rei-standard-amsg-sw] onBusinessPayload promise rejected:", error);
|
|
126
|
-
})
|
|
175
|
+
});
|
|
127
176
|
}
|
|
128
177
|
} catch (error) {
|
|
129
178
|
console.error("[rei-standard-amsg-sw] onBusinessPayload error:", error);
|
|
130
179
|
}
|
|
131
180
|
}
|
|
132
|
-
await Promise.all(
|
|
181
|
+
await Promise.all(notificationWork);
|
|
182
|
+
const settledResult = { eventName, notification: notificationState };
|
|
183
|
+
if (typeof onNotificationSettled === "function") {
|
|
184
|
+
await onNotificationSettled(settledResult);
|
|
185
|
+
}
|
|
186
|
+
if (businessWork) await businessWork;
|
|
187
|
+
return settledResult;
|
|
133
188
|
}
|
|
134
189
|
function resolveEventName(payload) {
|
|
135
190
|
const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
|
|
@@ -152,6 +207,19 @@ function isNotificationKind(payload) {
|
|
|
152
207
|
if (kind === void 0 || kind === null) return true;
|
|
153
208
|
return kind === MESSAGE_KIND.CONTENT;
|
|
154
209
|
}
|
|
210
|
+
function shouldRenderNotification(payload, clientList) {
|
|
211
|
+
const showOpt = payload && payload.notification ? payload.notification.show : void 0;
|
|
212
|
+
if (showOpt === "always") {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (showOpt === "when-hidden") {
|
|
216
|
+
return !clientList.some((client) => client.visibilityState === "visible");
|
|
217
|
+
}
|
|
218
|
+
if (showOpt === false) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return isNotificationKind(payload);
|
|
222
|
+
}
|
|
155
223
|
async function dispatchPushToClients(sw, eventName, payload, preFetchedClientList = null) {
|
|
156
224
|
try {
|
|
157
225
|
const clientList = preFetchedClientList || await sw.clients.matchAll({
|
|
@@ -196,7 +264,7 @@ function createNotificationFromPayload(payload, defaults) {
|
|
|
196
264
|
};
|
|
197
265
|
}
|
|
198
266
|
const pushNotification = payload.notification && typeof payload.notification === "object" ? payload.notification : {};
|
|
199
|
-
const title = pushNotification.title || payload.title || payload.contactName || "New notification";
|
|
267
|
+
const title = pushNotification.title || payload.title || payload.contactName && `\u6765\u81EA ${payload.contactName}` || "New notification";
|
|
200
268
|
const body = pushNotification.body || payload.body || payload.message || "";
|
|
201
269
|
const data = pushNotification.data && typeof pushNotification.data === "object" ? { ...pushNotification.data } : payload.data && typeof payload.data === "object" ? { ...payload.data } : {};
|
|
202
270
|
if (data.payload == null) data.payload = payload;
|
|
@@ -211,7 +279,8 @@ function createNotificationFromPayload(payload, defaults) {
|
|
|
211
279
|
renotify: Boolean(pushNotification.renotify ?? payload.renotify ?? false),
|
|
212
280
|
requireInteraction: Boolean(
|
|
213
281
|
pushNotification.requireInteraction ?? payload.requireInteraction ?? false
|
|
214
|
-
)
|
|
282
|
+
),
|
|
283
|
+
silent: Boolean(pushNotification.silent ?? payload.silent ?? false)
|
|
215
284
|
}
|
|
216
285
|
};
|
|
217
286
|
}
|
|
@@ -231,15 +300,218 @@ function normalizeMultipartOptions(input) {
|
|
|
231
300
|
)
|
|
232
301
|
};
|
|
233
302
|
}
|
|
303
|
+
function normalizeDedupeOptions(input) {
|
|
304
|
+
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
305
|
+
if (Object.prototype.hasOwnProperty.call(source, "storeName")) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"[rei-standard-amsg-sw] dedupe.storeName \u4E0D\u518D\u53EF\u914D\u7F6E\u3002\u6539 storeName \u4F1A\u89E6\u53D1 IndexedDB \u7248\u672C\u5347\u7EA7\uFF0C\u672C\u5305\u4E0D\u7EF4\u62A4 migration \u903B\u8F91\u3002\u9700\u8981\u9694\u79BB\u53BB\u91CD\u6570\u636E\u8BF7\u6539\u7528 dedupe.dbName\uFF08\u6BCF\u4E2A dbName \u662F\u72EC\u7ACB IDB \u5B9E\u4F8B\uFF09\u3002"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
enabled: source.enabled !== false,
|
|
312
|
+
ttlMs: positiveIntegerOrDefault(source.ttlMs, DEFAULT_DEDUPE_TTL_MS),
|
|
313
|
+
cleanupIntervalMs: source.cleanupIntervalMs === 0 ? 0 : positiveIntegerOrDefault(
|
|
314
|
+
source.cleanupIntervalMs,
|
|
315
|
+
DEFAULT_DEDUPE_CLEANUP_INTERVAL_MS
|
|
316
|
+
),
|
|
317
|
+
key: typeof source.key === "function" ? source.key : null,
|
|
318
|
+
dbName: typeof source.dbName === "string" && source.dbName.trim() ? source.dbName.trim() : REI_AMSG_DEDUPE_DB_NAME,
|
|
319
|
+
storeName: REI_AMSG_DEDUPE_STORE,
|
|
320
|
+
_memoryStore: /* @__PURE__ */ new Map()
|
|
321
|
+
};
|
|
322
|
+
}
|
|
234
323
|
function positiveIntegerOrDefault(value, fallback) {
|
|
235
324
|
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
236
325
|
}
|
|
326
|
+
async function claimDedupe(payload, ctx) {
|
|
327
|
+
if (!ctx.dedupe || ctx.dedupe.enabled === false) {
|
|
328
|
+
return { duplicate: false, key: void 0 };
|
|
329
|
+
}
|
|
330
|
+
const key = resolveDedupeKey(payload, ctx.dedupe);
|
|
331
|
+
if (!key) return { duplicate: false, key: void 0 };
|
|
332
|
+
await maybeCleanupDedupe(ctx);
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
const record = {
|
|
335
|
+
key,
|
|
336
|
+
firstSeenAt: now,
|
|
337
|
+
expiresAt: now + ctx.dedupe.ttlMs,
|
|
338
|
+
source: ctx.source || "unknown",
|
|
339
|
+
messageKind: getPayloadMessageKind(payload),
|
|
340
|
+
notificationShown: false,
|
|
341
|
+
notificationStatePending: true
|
|
342
|
+
};
|
|
343
|
+
if (await addDedupeRecord(ctx.dedupe, record)) {
|
|
344
|
+
return { duplicate: false, key, record };
|
|
345
|
+
}
|
|
346
|
+
const existing = await readDedupeRecord(ctx.dedupe, key);
|
|
347
|
+
if (existing && existing.expiresAt <= now) {
|
|
348
|
+
await deleteDedupeRecord(ctx.dedupe, key);
|
|
349
|
+
if (await addDedupeRecord(ctx.dedupe, record)) {
|
|
350
|
+
return { duplicate: false, key, record };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
duplicate: true,
|
|
355
|
+
key,
|
|
356
|
+
record,
|
|
357
|
+
existing: existing || null
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async function updateDedupeNotificationState(claim, ctx, dispatchResult) {
|
|
361
|
+
if (!claim || claim.duplicate || !claim.key || !ctx.dedupe || ctx.dedupe.enabled === false) return;
|
|
362
|
+
if (!dispatchResult || !dispatchResult.notification) return;
|
|
363
|
+
const notification = dispatchResult.notification;
|
|
364
|
+
const next = {
|
|
365
|
+
...claim.record,
|
|
366
|
+
notificationShown: notification.shown === true,
|
|
367
|
+
notificationStatePending: false
|
|
368
|
+
};
|
|
369
|
+
try {
|
|
370
|
+
await putDedupeRecord(ctx.dedupe, next);
|
|
371
|
+
claim.record = next;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error("[rei-standard-amsg-sw] dedupe notification state update failed:", error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function maybeShowDuplicateNotification(sw, payload, claim, ctx) {
|
|
377
|
+
const existing = claim && claim.existing ? claim.existing : null;
|
|
378
|
+
if (!existing || existing.notificationShown === true) {
|
|
379
|
+
return { shown: false, reason: existing ? "already-shown" : "no-existing-record" };
|
|
380
|
+
}
|
|
381
|
+
if (existing.notificationStatePending === true) {
|
|
382
|
+
return { shown: false, reason: "first-delivery-pending" };
|
|
383
|
+
}
|
|
384
|
+
let clientList = [];
|
|
385
|
+
try {
|
|
386
|
+
clientList = await sw.clients.matchAll({
|
|
387
|
+
type: "window",
|
|
388
|
+
includeUncontrolled: true
|
|
389
|
+
});
|
|
390
|
+
} catch (_matchError) {
|
|
391
|
+
}
|
|
392
|
+
if (!shouldRenderNotification(payload, clientList)) {
|
|
393
|
+
return { shown: false, reason: "policy-suppressed" };
|
|
394
|
+
}
|
|
395
|
+
const notification = createNotificationFromPayload(payload, {
|
|
396
|
+
defaultIcon: ctx.defaultIcon,
|
|
397
|
+
defaultBadge: ctx.defaultBadge
|
|
398
|
+
});
|
|
399
|
+
if (!notification) {
|
|
400
|
+
return { shown: false, reason: "no-notification" };
|
|
401
|
+
}
|
|
402
|
+
await sw.registration.showNotification(notification.title, notification.options);
|
|
403
|
+
const next = {
|
|
404
|
+
...existing,
|
|
405
|
+
notificationShown: true,
|
|
406
|
+
notificationStatePending: false
|
|
407
|
+
};
|
|
408
|
+
await putDedupeRecord(ctx.dedupe, next);
|
|
409
|
+
return { shown: true, reason: "shown-from-duplicate" };
|
|
410
|
+
}
|
|
411
|
+
function resolveDedupeKey(payload, dedupe) {
|
|
412
|
+
if (typeof dedupe.key === "function") {
|
|
413
|
+
try {
|
|
414
|
+
const custom = dedupe.key(payload);
|
|
415
|
+
return typeof custom === "string" && custom.trim() ? custom.trim() : void 0;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error("[rei-standard-amsg-sw] dedupe.key error:", error);
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
422
|
+
for (const field of ["messageId", "id", "dedupeKey"]) {
|
|
423
|
+
const value = payload[field];
|
|
424
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
425
|
+
}
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
function getPayloadMessageKind(payload) {
|
|
429
|
+
return payload && typeof payload === "object" && typeof payload.messageKind === "string" ? payload.messageKind : void 0;
|
|
430
|
+
}
|
|
431
|
+
async function notifyDuplicate(payload, claim, ctx) {
|
|
432
|
+
if (typeof ctx.onDuplicate !== "function") return;
|
|
433
|
+
const existing = claim.existing || {};
|
|
434
|
+
const info = {
|
|
435
|
+
key: claim.key,
|
|
436
|
+
source: ctx.source || "unknown",
|
|
437
|
+
messageKind: getPayloadMessageKind(payload),
|
|
438
|
+
firstSeenAt: existing.firstSeenAt,
|
|
439
|
+
existingSource: existing.source,
|
|
440
|
+
existingMessageKind: existing.messageKind,
|
|
441
|
+
existingNotificationShown: existing.notificationShown === true,
|
|
442
|
+
duplicateNotificationShown: claim.duplicateNotification && claim.duplicateNotification.shown === true
|
|
443
|
+
};
|
|
444
|
+
try {
|
|
445
|
+
await ctx.onDuplicate(info);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error("[rei-standard-amsg-sw] onDuplicate error:", error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function maybeCleanupDedupe(ctx) {
|
|
451
|
+
if (!ctx.dedupe || ctx.dedupe.enabled === false || ctx.dedupe.cleanupIntervalMs === 0) return;
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const last = ctx.getLastDedupeCleanupAt ? ctx.getLastDedupeCleanupAt() : 0;
|
|
454
|
+
if (last && now - last < ctx.dedupe.cleanupIntervalMs) return;
|
|
455
|
+
if (ctx.setLastDedupeCleanupAt) ctx.setLastDedupeCleanupAt(now);
|
|
456
|
+
try {
|
|
457
|
+
await cleanupDedupeStore(ctx.dedupe, now);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error("[rei-standard-amsg-sw] dedupe cleanup failed:", error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function cleanupDedupeStore(dedupe, now) {
|
|
463
|
+
if (!hasIndexedDB()) {
|
|
464
|
+
const store = memoryDedupeStoreFor(dedupe);
|
|
465
|
+
for (const [key, record] of store.entries()) {
|
|
466
|
+
if (record.expiresAt <= now) store.delete(key);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
await withDedupeStore(dedupe, "readwrite", (store, resolve, reject) => {
|
|
471
|
+
const index = store.index("expiresAt");
|
|
472
|
+
const range = IDBKeyRange.upperBound(now);
|
|
473
|
+
let failed = false;
|
|
474
|
+
const request = index.openCursor(range);
|
|
475
|
+
request.onsuccess = () => {
|
|
476
|
+
if (failed) return;
|
|
477
|
+
const cursor = request.result;
|
|
478
|
+
if (!cursor) {
|
|
479
|
+
resolve(void 0);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const deleteRequest = cursor.delete();
|
|
483
|
+
deleteRequest.onsuccess = () => {
|
|
484
|
+
if (failed) return;
|
|
485
|
+
cursor.continue();
|
|
486
|
+
};
|
|
487
|
+
deleteRequest.onerror = () => {
|
|
488
|
+
if (!failed) {
|
|
489
|
+
failed = true;
|
|
490
|
+
reject(deleteRequest.error || new Error("Failed to delete expired dedupe record"));
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
request.onerror = () => reject(request.error || new Error("Failed to scan expired dedupe records"));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
237
497
|
function isMultipartPush(payload) {
|
|
238
498
|
return !!payload && typeof payload === "object" && payload.messageKind === MULTIPART_MESSAGE_KIND && payload.multipart && typeof payload.multipart === "object" && typeof payload.chunk === "string";
|
|
239
499
|
}
|
|
240
500
|
async function acceptMultipartChunk(sw, payload, options) {
|
|
241
501
|
const normalized = normalizeMultipartChunk(payload, options);
|
|
242
502
|
if (!normalized) return null;
|
|
503
|
+
const previous = multipartLocks.get(normalized.id) || Promise.resolve();
|
|
504
|
+
const current = previous.catch(() => void 0).then(() => acceptMultipartChunkInternal(sw, normalized, options));
|
|
505
|
+
multipartLocks.set(normalized.id, current);
|
|
506
|
+
try {
|
|
507
|
+
return await current;
|
|
508
|
+
} finally {
|
|
509
|
+
if (multipartLocks.get(normalized.id) === current) {
|
|
510
|
+
multipartLocks.delete(normalized.id);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function acceptMultipartChunkInternal(sw, normalized, options) {
|
|
243
515
|
if (normalized.expiresAt <= Date.now()) {
|
|
244
516
|
await dispatchMultipartExpired(sw, {
|
|
245
517
|
id: normalized.id,
|
|
@@ -538,6 +810,69 @@ function respondToSender(event, message) {
|
|
|
538
810
|
source.postMessage(message);
|
|
539
811
|
}
|
|
540
812
|
}
|
|
813
|
+
async function addDedupeRecord(dedupe, record) {
|
|
814
|
+
if (!hasIndexedDB()) {
|
|
815
|
+
const store = memoryDedupeStoreFor(dedupe);
|
|
816
|
+
if (store.has(record.key)) return false;
|
|
817
|
+
store.set(record.key, cloneRecord(record));
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
return withDedupeStore(dedupe, "readwrite", (store, resolve, reject) => {
|
|
821
|
+
let settled = false;
|
|
822
|
+
const request = store.add(record);
|
|
823
|
+
request.onsuccess = () => {
|
|
824
|
+
settled = true;
|
|
825
|
+
resolve(true);
|
|
826
|
+
};
|
|
827
|
+
request.onerror = (event) => {
|
|
828
|
+
settled = true;
|
|
829
|
+
if (request.error && request.error.name === "ConstraintError") {
|
|
830
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
831
|
+
resolve(false);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
reject(request.error || new Error("Failed to add dedupe record"));
|
|
835
|
+
};
|
|
836
|
+
store.transaction.onerror = () => {
|
|
837
|
+
if (!settled) reject(store.transaction.error || new Error("Dedupe transaction failed"));
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function readDedupeRecord(dedupe, key) {
|
|
842
|
+
if (!hasIndexedDB()) {
|
|
843
|
+
return Promise.resolve(cloneRecord(memoryDedupeStoreFor(dedupe).get(key) || null));
|
|
844
|
+
}
|
|
845
|
+
return withDedupeStore(dedupe, "readonly", (store, resolve, reject) => {
|
|
846
|
+
const request = store.get(key);
|
|
847
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
848
|
+
request.onerror = () => reject(request.error || new Error("Failed to read dedupe record"));
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function putDedupeRecord(dedupe, record) {
|
|
852
|
+
if (!record || typeof record.key !== "string" || !record.key) {
|
|
853
|
+
return Promise.resolve();
|
|
854
|
+
}
|
|
855
|
+
if (!hasIndexedDB()) {
|
|
856
|
+
memoryDedupeStoreFor(dedupe).set(record.key, cloneRecord(record));
|
|
857
|
+
return Promise.resolve();
|
|
858
|
+
}
|
|
859
|
+
return withDedupeStore(dedupe, "readwrite", (store, resolve, reject) => {
|
|
860
|
+
const request = store.put(record);
|
|
861
|
+
request.onsuccess = () => resolve(void 0);
|
|
862
|
+
request.onerror = () => reject(request.error || new Error("Failed to put dedupe record"));
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function deleteDedupeRecord(dedupe, key) {
|
|
866
|
+
if (!hasIndexedDB()) {
|
|
867
|
+
memoryDedupeStoreFor(dedupe).delete(key);
|
|
868
|
+
return Promise.resolve();
|
|
869
|
+
}
|
|
870
|
+
return withDedupeStore(dedupe, "readwrite", (store, resolve, reject) => {
|
|
871
|
+
const request = store.delete(key);
|
|
872
|
+
request.onsuccess = () => resolve(void 0);
|
|
873
|
+
request.onerror = () => reject(request.error || new Error("Failed to delete dedupe record"));
|
|
874
|
+
});
|
|
875
|
+
}
|
|
541
876
|
function readMultipartPending(id) {
|
|
542
877
|
return readStoreRecord(REI_SW_MULTIPART_STORE, id);
|
|
543
878
|
}
|
|
@@ -646,9 +981,22 @@ async function withDatabaseStore(storeName, mode, handler) {
|
|
|
646
981
|
Promise.resolve(handler(store, resolve, reject)).catch(reject);
|
|
647
982
|
});
|
|
648
983
|
}
|
|
984
|
+
async function withDedupeStore(dedupe, mode, handler) {
|
|
985
|
+
const db = await openDedupeDatabase(dedupe);
|
|
986
|
+
return new Promise((resolve, reject) => {
|
|
987
|
+
const transaction = db.transaction(dedupe.storeName, mode);
|
|
988
|
+
const store = transaction.objectStore(dedupe.storeName);
|
|
989
|
+
transaction.onerror = () => reject(transaction.error || new Error("Dedupe transaction failed"));
|
|
990
|
+
Promise.resolve(handler(store, resolve, reject)).catch(reject);
|
|
991
|
+
});
|
|
992
|
+
}
|
|
649
993
|
function hasIndexedDB() {
|
|
650
994
|
return typeof indexedDB !== "undefined" && indexedDB && typeof indexedDB.open === "function";
|
|
651
995
|
}
|
|
996
|
+
function memoryDedupeStoreFor(dedupe) {
|
|
997
|
+
if (!dedupe._memoryStore) dedupe._memoryStore = /* @__PURE__ */ new Map();
|
|
998
|
+
return dedupe._memoryStore;
|
|
999
|
+
}
|
|
652
1000
|
function memoryStoreFor(storeName) {
|
|
653
1001
|
if (storeName === REI_SW_MULTIPART_DONE_STORE) return memoryMultipartDone;
|
|
654
1002
|
if (storeName === REI_SW_MULTIPART_STORE) return memoryMultipartPending;
|
|
@@ -659,6 +1007,31 @@ function cloneRecord(record) {
|
|
|
659
1007
|
if (record == null) return null;
|
|
660
1008
|
return JSON.parse(JSON.stringify(record));
|
|
661
1009
|
}
|
|
1010
|
+
function openDedupeDatabase(dedupe) {
|
|
1011
|
+
const cacheKey = `${dedupe.dbName}:${dedupe.storeName}`;
|
|
1012
|
+
const cached = dedupeDbCache.get(cacheKey);
|
|
1013
|
+
if (cached) return Promise.resolve(cached);
|
|
1014
|
+
return new Promise((resolve, reject) => {
|
|
1015
|
+
const request = indexedDB.open(dedupe.dbName, 1);
|
|
1016
|
+
request.onupgradeneeded = () => {
|
|
1017
|
+
const db = request.result;
|
|
1018
|
+
const store = db.objectStoreNames.contains(dedupe.storeName) ? request.transaction.objectStore(dedupe.storeName) : db.createObjectStore(dedupe.storeName, { keyPath: "key" });
|
|
1019
|
+
if (store && !store.indexNames.contains("expiresAt")) {
|
|
1020
|
+
store.createIndex("expiresAt", "expiresAt", { unique: false });
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
request.onsuccess = () => {
|
|
1024
|
+
const db = request.result;
|
|
1025
|
+
dedupeDbCache.set(cacheKey, db);
|
|
1026
|
+
db.onversionchange = () => {
|
|
1027
|
+
db.close();
|
|
1028
|
+
dedupeDbCache.delete(cacheKey);
|
|
1029
|
+
};
|
|
1030
|
+
resolve(db);
|
|
1031
|
+
};
|
|
1032
|
+
request.onerror = () => reject(request.error || new Error("Failed to open dedupe database"));
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
662
1035
|
function openQueueDatabase() {
|
|
663
1036
|
if (cachedDB) return Promise.resolve(cachedDB);
|
|
664
1037
|
return new Promise((resolve, reject) => {
|
|
@@ -728,6 +1101,7 @@ async function removeQueuedRequest(id) {
|
|
|
728
1101
|
});
|
|
729
1102
|
}
|
|
730
1103
|
export {
|
|
1104
|
+
REI_AMSG_DELIVER_MESSAGE_TYPE,
|
|
731
1105
|
REI_AMSG_POSTMESSAGE_TYPE,
|
|
732
1106
|
REI_SW_EVENT,
|
|
733
1107
|
REI_SW_MESSAGE_TYPE,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-sw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "ReiStandard Active Messaging service worker SDK — three-axis push schema (content / reasoning / tool_request / error) with per-kind client postMessage events",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/Tosd0/ReiStandard",
|
|
7
|
+
"url": "git+https://github.com/Tosd0/ReiStandard.git",
|
|
8
8
|
"directory": "packages/rei-standard-amsg/sw"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=20"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@rei-standard/amsg-shared": "0.
|
|
36
|
+
"@rei-standard/amsg-shared": "0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.0.0",
|