cross-tab-worker-databus 0.11.0 → 0.20.7
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/CHANGELOG.md +216 -0
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.shared.worker.js +4 -2
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +4 -2
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-NX76TAV3.js → chunk-LPS4XOK4.js} +152 -20
- package/dist/{chunk-NX76TAV3.js.map → chunk-LPS4XOK4.js.map} +2 -2
- package/dist/cjs/centrifuge.cjs +151 -19
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/hooks.cjs +4 -1
- package/dist/cjs/hooks.cjs.map +2 -2
- package/dist/cjs/index.cjs +301 -68
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/vue.cjs +7 -1
- package/dist/cjs/vue.cjs.map +2 -2
- package/dist/core/data-bus.d.ts +32 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/publication.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/trace.d.ts +4 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +4 -1
- package/dist/hooks.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -50
- package/dist/index.js.map +2 -2
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +7 -1
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/api.md +10 -3
- package/docs/configuration.md +21 -1
- package/docs/release-checklist.md +22 -0
- package/docs/roadmap.md +136 -3
- package/docs/zh/README.md +1 -0
- package/docs/zh/api.md +10 -3
- package/docs/zh/configuration.md +21 -1
- package/docs/zh/release-checklist.md +22 -0
- package/docs/zh/roadmap.md +137 -2
- package/package.json +3 -1
package/dist/cjs/hooks.cjs
CHANGED
|
@@ -28,13 +28,16 @@ module.exports = __toCommonJS(hooks_exports);
|
|
|
28
28
|
var import_react = require("react");
|
|
29
29
|
function useCrossTabDataBus(create, deps = []) {
|
|
30
30
|
const [bus, setBus] = (0, import_react.useState)(null);
|
|
31
|
+
const lifecycleGeneration = (0, import_react.useRef)(0);
|
|
31
32
|
(0, import_react.useEffect)(() => {
|
|
33
|
+
const generation = ++lifecycleGeneration.current;
|
|
32
34
|
const instance = create();
|
|
35
|
+
if (generation !== lifecycleGeneration.current) return;
|
|
33
36
|
setBus(instance);
|
|
34
37
|
void instance.ready().catch(() => {
|
|
35
38
|
});
|
|
36
39
|
return () => {
|
|
37
|
-
setBus(null);
|
|
40
|
+
if (generation === lifecycleGeneration.current) setBus(null);
|
|
38
41
|
void instance.stop();
|
|
39
42
|
};
|
|
40
43
|
}, deps);
|
package/dist/cjs/hooks.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * React hooks adapter for cross-tab-worker-databus.\n *\n * A thin, transport-agnostic bridge between the imperative CrossTabDataBus\n * API and React component lifecycles. React is an optional peer dependency \u2014\n * this module is a separate entry point so consumers who don't use React\n * never load it.\n *\n * - `useCrossTabDataBus` owns the bus lifecycle: created on mount, stopped on\n * unmount. It is StrictMode-safe: the double-invoked effect exercises the\n * same stop/recreate path as BFCache suspend/resume.\n * - `useCrossTabSubscription` attaches a message handler with automatic\n * cleanup; the handler is read through a ref, so you can pass inline\n * closures without resubscribing on every render.\n * - `useCrossTabStatus` mirrors `bus.onStatus()` into React state.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport type { DependencyList } from 'react';\nimport type { CrossTabDataBus } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\n\n/**\n * Create a CrossTabDataBus for the component's lifetime.\n *\n * @param create Factory invoked once per effect run. Return a fresh bus \u2014\n * do not share a bus instance between effects, or StrictMode's\n * mount \u2192 stop \u2192 mount cycle will stop the shared instance out from\n * under the second mount.\n * @param deps Re-create the bus when these change (default: create once).\n * @returns The active bus, or `null` before the first effect has run (SSR\n * and the initial render).\n */\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: DependencyList = []\n): CrossTabDataBus<TConfig, TData> | null {\n const [bus, setBus] = useState<CrossTabDataBus<TConfig, TData> | null>(null);\n useEffect(() => {\n const instance = create();\n setBus(instance);\n void instance.ready().catch(() => {});\n return () => {\n setBus(null);\n void instance.stop();\n };\n // The factory is intentionally not a dependency: callers pass an inline\n // closure and key recreation through `deps` instead.\n }, deps);\n return bus;\n}\n\n/**\n * Subscribe to `topic` for the component's lifetime. The handler is read\n * through a ref on each delivery, so inline closures are safe without\n * unsubscribing/resubscribing on re-renders.\n *\n * When `bus` is null (not yet created) the subscription is queued until the\n * bus appears \u2014 the bus itself queues it until the transport is ready.\n */\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n topic: string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n if (!bus) return;\n return bus.subscribe(topic, message => handlerRef.current(message));\n }, [bus, topic]);\n}\n\n/**\n * Mirror the bus connection status into React state. Reports the live value\n * via `onStatus` and reads the current value synchronously whenever `bus`\n * changes identity.\n */\nexport function useCrossTabStatus<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null\n): WorkerStatus {\n const [status, setStatus] = useState<WorkerStatus>('connecting');\n useEffect(() => {\n if (!bus) {\n setStatus('connecting');\n return;\n }\n setStatus(bus.getStatus());\n return bus.onStatus(setStatus);\n }, [bus]);\n return status;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,mBAA4C;AAgBrC,SAAS,mBACd,QACA,OAAuB,CAAC,GACgB;AACxC,QAAM,CAAC,KAAK,MAAM,QAAI,uBAAiD,IAAI;AAC3E,8BAAU,MAAM;AACd,UAAM,WAAW,OAAO;AACxB,WAAO,QAAQ;AACf,SAAK,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACpC,WAAO,MAAM;AACX,
|
|
4
|
+
"sourcesContent": ["/**\n * React hooks adapter for cross-tab-worker-databus.\n *\n * A thin, transport-agnostic bridge between the imperative CrossTabDataBus\n * API and React component lifecycles. React is an optional peer dependency \u2014\n * this module is a separate entry point so consumers who don't use React\n * never load it.\n *\n * - `useCrossTabDataBus` owns the bus lifecycle: created on mount, stopped on\n * unmount. It is StrictMode-safe: the double-invoked effect exercises the\n * same stop/recreate path as BFCache suspend/resume.\n * - `useCrossTabSubscription` attaches a message handler with automatic\n * cleanup; the handler is read through a ref, so you can pass inline\n * closures without resubscribing on every render.\n * - `useCrossTabStatus` mirrors `bus.onStatus()` into React state.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport type { DependencyList } from 'react';\nimport type { CrossTabDataBus } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\n\n/**\n * Create a CrossTabDataBus for the component's lifetime.\n *\n * @param create Factory invoked once per effect run. Return a fresh bus \u2014\n * do not share a bus instance between effects, or StrictMode's\n * mount \u2192 stop \u2192 mount cycle will stop the shared instance out from\n * under the second mount.\n * @param deps Re-create the bus when these change (default: create once).\n * @returns The active bus, or `null` before the first effect has run (SSR\n * and the initial render).\n */\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: DependencyList = []\n): CrossTabDataBus<TConfig, TData> | null {\n const [bus, setBus] = useState<CrossTabDataBus<TConfig, TData> | null>(null);\n const lifecycleGeneration = useRef(0);\n useEffect(() => {\n const generation = ++lifecycleGeneration.current;\n const instance = create();\n if (generation !== lifecycleGeneration.current) return;\n setBus(instance);\n void instance.ready().catch(() => {});\n return () => {\n if (generation === lifecycleGeneration.current) setBus(null);\n void instance.stop();\n };\n // The factory is intentionally not a dependency: callers pass an inline\n // closure and key recreation through `deps` instead.\n }, deps);\n return bus;\n}\n\n/**\n * Subscribe to `topic` for the component's lifetime. The handler is read\n * through a ref on each delivery, so inline closures are safe without\n * unsubscribing/resubscribing on re-renders.\n *\n * When `bus` is null (not yet created) the subscription is queued until the\n * bus appears \u2014 the bus itself queues it until the transport is ready.\n */\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n topic: string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n if (!bus) return;\n return bus.subscribe(topic, message => handlerRef.current(message));\n }, [bus, topic]);\n}\n\n/**\n * Mirror the bus connection status into React state. Reports the live value\n * via `onStatus` and reads the current value synchronously whenever `bus`\n * changes identity.\n */\nexport function useCrossTabStatus<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null\n): WorkerStatus {\n const [status, setStatus] = useState<WorkerStatus>('connecting');\n useEffect(() => {\n if (!bus) {\n setStatus('connecting');\n return;\n }\n setStatus(bus.getStatus());\n return bus.onStatus(setStatus);\n }, [bus]);\n return status;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,mBAA4C;AAgBrC,SAAS,mBACd,QACA,OAAuB,CAAC,GACgB;AACxC,QAAM,CAAC,KAAK,MAAM,QAAI,uBAAiD,IAAI;AAC3E,QAAM,0BAAsB,qBAAO,CAAC;AACpC,8BAAU,MAAM;AACd,UAAM,aAAa,EAAE,oBAAoB;AACzC,UAAM,WAAW,OAAO;AACxB,QAAI,eAAe,oBAAoB,QAAS;AAChD,WAAO,QAAQ;AACf,SAAK,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACpC,WAAO,MAAM;AACX,UAAI,eAAe,oBAAoB,QAAS,QAAO,IAAI;AAC3D,WAAK,SAAS,KAAK;AAAA,IACrB;AAAA,EAGF,GAAG,IAAI;AACP,SAAO;AACT;AAUO,SAAS,wBACd,KACA,OACA,SACM;AACN,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AACrB,8BAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,WAAO,IAAI,UAAU,OAAO,aAAW,WAAW,QAAQ,OAAO,CAAC;AAAA,EACpE,GAAG,CAAC,KAAK,KAAK,CAAC;AACjB;AAOO,SAAS,kBACd,KACc;AACd,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAuB,YAAY;AAC/D,8BAAU,MAAM;AACd,QAAI,CAAC,KAAK;AACR,gBAAU,YAAY;AACtB;AAAA,IACF;AACA,cAAU,IAAI,UAAU,CAAC;AACzB,WAAO,IAAI,SAAS,SAAS;AAAA,EAC/B,GAAG,CAAC,GAAG,CAAC;AACR,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1093,7 +1093,7 @@ var DataBusTraceReporter = class {
|
|
|
1093
1093
|
latencySumMs = 0;
|
|
1094
1094
|
dedupAccepted = 0;
|
|
1095
1095
|
dedupSuppressed = 0;
|
|
1096
|
-
constructor(options, now = Date.now) {
|
|
1096
|
+
constructor(options, now = options?.now ?? Date.now) {
|
|
1097
1097
|
this.enabled = options?.enabled ?? false;
|
|
1098
1098
|
this.mode = options?.mode ?? "all";
|
|
1099
1099
|
this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
|
|
@@ -1255,6 +1255,12 @@ function roundMs(value) {
|
|
|
1255
1255
|
// src/core/data-bus.ts
|
|
1256
1256
|
var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
|
|
1257
1257
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
1258
|
+
var PersistenceRetryCancelledError = class extends Error {
|
|
1259
|
+
constructor() {
|
|
1260
|
+
super("Persistence retry cancelled by lifecycle transition.");
|
|
1261
|
+
this.name = "PersistenceRetryCancelledError";
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1258
1264
|
var CrossTabDataBus = class _CrossTabDataBus {
|
|
1259
1265
|
transport;
|
|
1260
1266
|
cluster;
|
|
@@ -1271,13 +1277,25 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1271
1277
|
replayMaxPerTopic;
|
|
1272
1278
|
replayPersistence;
|
|
1273
1279
|
replayRetentionMs;
|
|
1280
|
+
replayRetentionSweepMs;
|
|
1281
|
+
persistenceRetryMaxAttempts;
|
|
1282
|
+
persistenceRetryBackoffMs;
|
|
1283
|
+
persistenceRetryGeneration = 0;
|
|
1274
1284
|
replayHydration;
|
|
1285
|
+
// Retention cleanup is coalesced so a burst of publications does not issue
|
|
1286
|
+
// one IndexedDB read/write transaction per message. The newest cutoff wins.
|
|
1287
|
+
replayRetentionCleanup = null;
|
|
1288
|
+
replayRetentionCutoff = null;
|
|
1289
|
+
replayRetentionTimer = null;
|
|
1275
1290
|
initialConfig;
|
|
1276
1291
|
hasInitialConfig;
|
|
1277
1292
|
trace;
|
|
1278
1293
|
dedupMaxEntries;
|
|
1279
1294
|
dedupTtlMs;
|
|
1295
|
+
dedupSweepMs;
|
|
1296
|
+
dedupSweepTimer = null;
|
|
1280
1297
|
dedupEnabled;
|
|
1298
|
+
now;
|
|
1281
1299
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1282
1300
|
dedupSuppressed = 0;
|
|
1283
1301
|
dedupAccepted = 0;
|
|
@@ -1322,14 +1340,28 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1322
1340
|
if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
|
|
1323
1341
|
throw new TypeError("replay.retentionMs must be a positive finite number.");
|
|
1324
1342
|
}
|
|
1325
|
-
this.
|
|
1343
|
+
this.replayRetentionSweepMs = replay?.retentionSweepMs;
|
|
1344
|
+
if (this.replayRetentionSweepMs !== void 0 && (!Number.isFinite(this.replayRetentionSweepMs) || this.replayRetentionSweepMs <= 0)) {
|
|
1345
|
+
throw new TypeError("replay.retentionSweepMs must be a positive finite number.");
|
|
1346
|
+
}
|
|
1347
|
+
this.persistenceRetryMaxAttempts = replay?.persistenceRetry?.maxAttempts ?? 1;
|
|
1348
|
+
this.persistenceRetryBackoffMs = replay?.persistenceRetry?.backoffMs ?? 50;
|
|
1349
|
+
if (!Number.isSafeInteger(this.persistenceRetryMaxAttempts) || this.persistenceRetryMaxAttempts <= 0) {
|
|
1350
|
+
throw new TypeError("replay.persistenceRetry.maxAttempts must be a positive safe integer.");
|
|
1351
|
+
}
|
|
1352
|
+
if (!Number.isFinite(this.persistenceRetryBackoffMs) || this.persistenceRetryBackoffMs < 0) {
|
|
1353
|
+
throw new TypeError("replay.persistenceRetry.backoffMs must be a non-negative finite number.");
|
|
1354
|
+
}
|
|
1326
1355
|
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1356
|
+
this.now = dedup?.now ?? Date.now;
|
|
1357
|
+
this.replayHydration = this.hydrateReplay();
|
|
1327
1358
|
this.transport = transport;
|
|
1328
1359
|
this.initialConfig = initialConfig;
|
|
1329
1360
|
this.hasInitialConfig = "initialConfig" in options;
|
|
1330
1361
|
this.trace = new DataBusTraceReporter(trace);
|
|
1331
1362
|
this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
|
|
1332
1363
|
this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
|
|
1364
|
+
this.dedupSweepMs = dedup?.sweepMs;
|
|
1333
1365
|
this.dedupEnabled = dedup !== void 0;
|
|
1334
1366
|
if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
|
|
1335
1367
|
throw new TypeError("dedup.maxEntries must be a positive safe integer.");
|
|
@@ -1337,6 +1369,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1337
1369
|
if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
|
|
1338
1370
|
throw new TypeError("dedup.ttlMs must be a positive finite number.");
|
|
1339
1371
|
}
|
|
1372
|
+
if (this.dedupSweepMs !== void 0 && (!Number.isFinite(this.dedupSweepMs) || this.dedupSweepMs <= 0)) {
|
|
1373
|
+
throw new TypeError("dedup.sweepMs must be a positive finite number.");
|
|
1374
|
+
}
|
|
1340
1375
|
this.cluster = new WorkerClusterRuntime({
|
|
1341
1376
|
...clusterOptions,
|
|
1342
1377
|
handlers: {
|
|
@@ -1377,11 +1412,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1377
1412
|
onSuspend: () => {
|
|
1378
1413
|
if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
|
|
1379
1414
|
this.trace.pause();
|
|
1415
|
+
this.persistenceRetryGeneration += 1;
|
|
1416
|
+
this.stopDedupSweep();
|
|
1417
|
+
this.stopReplayRetentionSweep();
|
|
1380
1418
|
this.suspendTransport();
|
|
1381
1419
|
},
|
|
1382
1420
|
onResume: () => {
|
|
1383
1421
|
this.trace.event({ type: "lifecycle", action: "resume" });
|
|
1384
1422
|
this.trace.start();
|
|
1423
|
+
this.startDedupSweep();
|
|
1424
|
+
this.startReplayRetentionSweep();
|
|
1385
1425
|
this.resumeTransport();
|
|
1386
1426
|
},
|
|
1387
1427
|
onDiagnostic: (event) => {
|
|
@@ -1409,6 +1449,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1409
1449
|
this.lastError = null;
|
|
1410
1450
|
this.trace.event({ type: "lifecycle", action: "start" });
|
|
1411
1451
|
this.trace.start();
|
|
1452
|
+
this.startDedupSweep();
|
|
1453
|
+
this.startReplayRetentionSweep();
|
|
1412
1454
|
this.updateStatus("connecting");
|
|
1413
1455
|
this.cluster.start();
|
|
1414
1456
|
const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
|
|
@@ -1533,7 +1575,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1533
1575
|
this.topicHandlers.delete(topic);
|
|
1534
1576
|
this.replayBuffers?.delete(topic);
|
|
1535
1577
|
if (this.replayPersistence?.clearTopic) {
|
|
1536
|
-
void this.replayPersistence.clearTopic(topic).catch((error) => this.
|
|
1578
|
+
void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
|
|
1537
1579
|
}
|
|
1538
1580
|
this.cluster.unsubscribe(topic);
|
|
1539
1581
|
}
|
|
@@ -1542,9 +1584,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1542
1584
|
this.replayBuffers?.clear();
|
|
1543
1585
|
if (this.replayPersistence?.clear) {
|
|
1544
1586
|
try {
|
|
1545
|
-
await this.replayPersistence.clear();
|
|
1587
|
+
await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
|
|
1546
1588
|
} catch (error) {
|
|
1547
|
-
this.
|
|
1589
|
+
this.reportPersistenceError(error);
|
|
1548
1590
|
throw error;
|
|
1549
1591
|
}
|
|
1550
1592
|
}
|
|
@@ -1554,9 +1596,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1554
1596
|
this.replayBuffers?.delete(topic);
|
|
1555
1597
|
if (this.replayPersistence?.clearTopic) {
|
|
1556
1598
|
try {
|
|
1557
|
-
await this.replayPersistence.clearTopic(topic);
|
|
1599
|
+
await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
|
|
1558
1600
|
} catch (error) {
|
|
1559
|
-
this.
|
|
1601
|
+
this.reportPersistenceError(error);
|
|
1560
1602
|
throw error;
|
|
1561
1603
|
}
|
|
1562
1604
|
}
|
|
@@ -1566,12 +1608,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1566
1608
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1567
1609
|
if (this.replayBuffers) {
|
|
1568
1610
|
for (const [topic, messages] of this.replayBuffers) {
|
|
1569
|
-
const kept = messages.filter((message) =>
|
|
1611
|
+
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
1570
1612
|
if (kept.length) this.replayBuffers.set(topic, kept);
|
|
1571
1613
|
else this.replayBuffers.delete(topic);
|
|
1572
1614
|
}
|
|
1573
1615
|
}
|
|
1574
|
-
if (this.replayPersistence?.clearBefore)
|
|
1616
|
+
if (this.replayPersistence?.clearBefore) {
|
|
1617
|
+
try {
|
|
1618
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(timestamp));
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
this.reportPersistenceError(error);
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1575
1624
|
}
|
|
1576
1625
|
/** Return bounded deduplication counters for diagnostics and health checks. */
|
|
1577
1626
|
getDedupStats() {
|
|
@@ -1629,8 +1678,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1629
1678
|
async stop() {
|
|
1630
1679
|
if (!this.started) return;
|
|
1631
1680
|
this.stopping = true;
|
|
1681
|
+
this.persistenceRetryGeneration += 1;
|
|
1632
1682
|
this.trace.event({ type: "lifecycle", action: "stop" });
|
|
1633
1683
|
this.trace.stop();
|
|
1684
|
+
this.stopDedupSweep();
|
|
1685
|
+
this.stopReplayRetentionSweep();
|
|
1634
1686
|
this.topicHandlers.clear();
|
|
1635
1687
|
this.replayBuffers?.clear();
|
|
1636
1688
|
this.cluster.stop();
|
|
@@ -1641,7 +1693,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1641
1693
|
else await this.transport.stop();
|
|
1642
1694
|
} finally {
|
|
1643
1695
|
this.transportSubscribedTopics.clear();
|
|
1644
|
-
this.
|
|
1696
|
+
this.resetDedup();
|
|
1645
1697
|
this.started = false;
|
|
1646
1698
|
this.stopping = false;
|
|
1647
1699
|
this.suspended = false;
|
|
@@ -1674,7 +1726,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1674
1726
|
}
|
|
1675
1727
|
isDuplicate(message) {
|
|
1676
1728
|
if (!this.dedupEnabled || !message.messageId) return false;
|
|
1677
|
-
const now =
|
|
1729
|
+
const now = this.now();
|
|
1678
1730
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1679
1731
|
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1680
1732
|
}
|
|
@@ -1694,6 +1746,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1694
1746
|
}
|
|
1695
1747
|
return false;
|
|
1696
1748
|
}
|
|
1749
|
+
startDedupSweep() {
|
|
1750
|
+
if (this.dedupSweepTimer || !this.dedupEnabled || !this.dedupSweepMs) return;
|
|
1751
|
+
this.dedupSweepTimer = setInterval(() => this.pruneExpiredDedup(), this.dedupSweepMs);
|
|
1752
|
+
}
|
|
1753
|
+
stopDedupSweep() {
|
|
1754
|
+
if (this.dedupSweepTimer) clearInterval(this.dedupSweepTimer);
|
|
1755
|
+
this.dedupSweepTimer = null;
|
|
1756
|
+
}
|
|
1757
|
+
pruneExpiredDedup() {
|
|
1758
|
+
const cutoff = this.now() - this.dedupTtlMs;
|
|
1759
|
+
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1760
|
+
if (timestamp < cutoff) this.seenMessageIds.delete(id);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1697
1763
|
/** Deliver a message to every local handler registered for its topic,
|
|
1698
1764
|
* plus every handler registered with a wildcard subscription that matches
|
|
1699
1765
|
* (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
|
|
@@ -1720,9 +1786,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1720
1786
|
buffer.push(storedMessage);
|
|
1721
1787
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1722
1788
|
if (this.replayPersistence) {
|
|
1723
|
-
void this.replayPersistence.append(storedMessage).catch((error) => this.
|
|
1789
|
+
void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
|
|
1724
1790
|
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1725
|
-
|
|
1791
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1726
1792
|
}
|
|
1727
1793
|
}
|
|
1728
1794
|
}
|
|
@@ -1732,9 +1798,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1732
1798
|
}
|
|
1733
1799
|
try {
|
|
1734
1800
|
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1735
|
-
await this.replayPersistence.clearBefore(
|
|
1801
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
|
|
1736
1802
|
}
|
|
1737
|
-
for (const message of await this.replayPersistence.load()) {
|
|
1803
|
+
for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
|
|
1738
1804
|
let buffer = this.replayBuffers.get(message.topic);
|
|
1739
1805
|
if (!buffer) {
|
|
1740
1806
|
buffer = [];
|
|
@@ -1744,7 +1810,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1744
1810
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1745
1811
|
}
|
|
1746
1812
|
} catch (error) {
|
|
1747
|
-
this.
|
|
1813
|
+
this.reportPersistenceError(error);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
scheduleReplayRetentionCleanup(cutoff) {
|
|
1817
|
+
if (!this.replayPersistence?.clearBefore) return;
|
|
1818
|
+
if (this.replayRetentionCutoff === null || cutoff > this.replayRetentionCutoff) {
|
|
1819
|
+
this.replayRetentionCutoff = cutoff;
|
|
1820
|
+
}
|
|
1821
|
+
if (this.replayRetentionCleanup) return;
|
|
1822
|
+
this.replayRetentionCleanup = (async () => {
|
|
1823
|
+
while (this.replayRetentionCutoff !== null) {
|
|
1824
|
+
const nextCutoff = this.replayRetentionCutoff;
|
|
1825
|
+
this.replayRetentionCutoff = null;
|
|
1826
|
+
try {
|
|
1827
|
+
await this.replayPersistence.clearBefore(nextCutoff);
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
this.reportPersistenceError(error);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
})().finally(() => {
|
|
1833
|
+
this.replayRetentionCleanup = null;
|
|
1834
|
+
if (this.replayRetentionCutoff !== null) {
|
|
1835
|
+
this.scheduleReplayRetentionCleanup(this.replayRetentionCutoff);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
startReplayRetentionSweep() {
|
|
1840
|
+
if (this.replayRetentionTimer || !this.replayRetentionMs || !this.replayRetentionSweepMs || !this.replayPersistence?.clearBefore) return;
|
|
1841
|
+
this.replayRetentionTimer = setInterval(() => {
|
|
1842
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1843
|
+
}, this.replayRetentionSweepMs);
|
|
1844
|
+
}
|
|
1845
|
+
stopReplayRetentionSweep() {
|
|
1846
|
+
if (this.replayRetentionTimer) clearInterval(this.replayRetentionTimer);
|
|
1847
|
+
this.replayRetentionTimer = null;
|
|
1848
|
+
}
|
|
1849
|
+
async withPersistenceRetry(persistenceOperation, operation) {
|
|
1850
|
+
const generation = this.persistenceRetryGeneration;
|
|
1851
|
+
let attempt = 0;
|
|
1852
|
+
let delay = this.persistenceRetryBackoffMs;
|
|
1853
|
+
while (true) {
|
|
1854
|
+
attempt += 1;
|
|
1855
|
+
try {
|
|
1856
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1857
|
+
return await operation();
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
if (error instanceof PersistenceRetryCancelledError || generation !== this.persistenceRetryGeneration) {
|
|
1860
|
+
throw new PersistenceRetryCancelledError();
|
|
1861
|
+
}
|
|
1862
|
+
if (attempt >= this.persistenceRetryMaxAttempts) throw error;
|
|
1863
|
+
this.trace.event({
|
|
1864
|
+
type: "reliability",
|
|
1865
|
+
operation: "persistence_retry",
|
|
1866
|
+
persistenceOperation,
|
|
1867
|
+
attempt
|
|
1868
|
+
});
|
|
1869
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1870
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1871
|
+
delay = Math.min(delay * 2, 1600);
|
|
1872
|
+
}
|
|
1748
1873
|
}
|
|
1749
1874
|
}
|
|
1750
1875
|
/** Deliver buffered history to a newly-registered handler. For an exact
|
|
@@ -1782,7 +1907,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1782
1907
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
1783
1908
|
}
|
|
1784
1909
|
if (status === "error" && this.started && !this.stopping) {
|
|
1785
|
-
const now =
|
|
1910
|
+
const now = this.now();
|
|
1786
1911
|
if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
|
|
1787
1912
|
this.lastRecoveryAt = now;
|
|
1788
1913
|
this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
|
|
@@ -1799,6 +1924,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1799
1924
|
this.trace.event({ type: "error", source: "transport" });
|
|
1800
1925
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
|
|
1801
1926
|
}
|
|
1927
|
+
reportPersistenceError(error) {
|
|
1928
|
+
if (error instanceof PersistenceRetryCancelledError) return;
|
|
1929
|
+
this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
|
|
1930
|
+
this.reportError(error);
|
|
1931
|
+
}
|
|
1802
1932
|
traceSubscription(action, topic) {
|
|
1803
1933
|
this.trace.event({
|
|
1804
1934
|
type: "subscription",
|
|
@@ -1952,74 +2082,165 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
1952
2082
|
throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);
|
|
1953
2083
|
}
|
|
1954
2084
|
let dbPromise = null;
|
|
2085
|
+
const invalidate = (db) => {
|
|
2086
|
+
if (dbPromise) {
|
|
2087
|
+
void dbPromise.then((current) => {
|
|
2088
|
+
if (current === db) {
|
|
2089
|
+
current.close();
|
|
2090
|
+
dbPromise = null;
|
|
2091
|
+
}
|
|
2092
|
+
}, () => void 0);
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
let mutationQueue = Promise.resolve();
|
|
2096
|
+
const serializeMutation = (mutation) => {
|
|
2097
|
+
const next = mutationQueue.then(mutation, mutation);
|
|
2098
|
+
mutationQueue = next.catch(() => void 0);
|
|
2099
|
+
return next;
|
|
2100
|
+
};
|
|
1955
2101
|
const open = () => {
|
|
1956
2102
|
if (dbPromise) return dbPromise;
|
|
1957
|
-
|
|
2103
|
+
const pending = new Promise((resolve, reject) => {
|
|
1958
2104
|
const request = indexedDb.open(dbName, 1);
|
|
1959
2105
|
request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: "topic" });
|
|
1960
|
-
request.onsuccess = () =>
|
|
2106
|
+
request.onsuccess = () => {
|
|
2107
|
+
const db = request.result;
|
|
2108
|
+
db.onversionchange = () => {
|
|
2109
|
+
db.close();
|
|
2110
|
+
if (dbPromise) dbPromise = null;
|
|
2111
|
+
};
|
|
2112
|
+
resolve(db);
|
|
2113
|
+
};
|
|
1961
2114
|
request.onerror = () => reject(request.error ?? new Error("Failed to open replay database."));
|
|
1962
2115
|
});
|
|
1963
|
-
|
|
2116
|
+
dbPromise = pending;
|
|
2117
|
+
void pending.catch(() => {
|
|
2118
|
+
if (dbPromise === pending) dbPromise = null;
|
|
2119
|
+
});
|
|
2120
|
+
return pending;
|
|
1964
2121
|
};
|
|
1965
2122
|
return {
|
|
1966
2123
|
async load() {
|
|
1967
2124
|
const db = await open();
|
|
1968
2125
|
return new Promise((resolve, reject) => {
|
|
1969
|
-
|
|
2126
|
+
let request;
|
|
2127
|
+
try {
|
|
2128
|
+
request = db.transaction(storeName, "readonly").objectStore(storeName).getAll();
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
invalidate(db);
|
|
2131
|
+
reject(error);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
1970
2134
|
request.onsuccess = () => resolve(request.result.flatMap((record) => record.messages));
|
|
1971
|
-
request.onerror = () =>
|
|
2135
|
+
request.onerror = () => {
|
|
2136
|
+
invalidate(db);
|
|
2137
|
+
reject(request.error ?? new Error("Failed to load replay history."));
|
|
2138
|
+
};
|
|
1972
2139
|
});
|
|
1973
2140
|
},
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2141
|
+
append(message) {
|
|
2142
|
+
return serializeMutation(async () => {
|
|
2143
|
+
const db = await open();
|
|
2144
|
+
await new Promise((resolve, reject) => {
|
|
2145
|
+
let transaction;
|
|
2146
|
+
try {
|
|
2147
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
invalidate(db);
|
|
2150
|
+
reject(error);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const store = transaction.objectStore(storeName);
|
|
2154
|
+
const request = store.get(message.topic);
|
|
2155
|
+
request.onsuccess = () => {
|
|
2156
|
+
const messages = (request.result?.messages ?? []).concat(message).slice(-maxPerTopic);
|
|
2157
|
+
store.put({ topic: message.topic, messages });
|
|
2158
|
+
};
|
|
2159
|
+
request.onerror = () => {
|
|
2160
|
+
invalidate(db);
|
|
2161
|
+
reject(request.error ?? new Error("Failed to read replay history."));
|
|
2162
|
+
};
|
|
2163
|
+
transaction.oncomplete = () => resolve();
|
|
2164
|
+
transaction.onerror = () => {
|
|
2165
|
+
invalidate(db);
|
|
2166
|
+
reject(transaction.error ?? new Error("Failed to persist replay history."));
|
|
2167
|
+
};
|
|
2168
|
+
});
|
|
1987
2169
|
});
|
|
1988
2170
|
},
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2171
|
+
clear() {
|
|
2172
|
+
return serializeMutation(async () => {
|
|
2173
|
+
const db = await open();
|
|
2174
|
+
await new Promise((resolve, reject) => {
|
|
2175
|
+
let transaction;
|
|
2176
|
+
try {
|
|
2177
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
invalidate(db);
|
|
2180
|
+
reject(error);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
transaction.objectStore(storeName).clear();
|
|
2184
|
+
transaction.oncomplete = () => resolve();
|
|
2185
|
+
transaction.onerror = () => {
|
|
2186
|
+
invalidate(db);
|
|
2187
|
+
reject(transaction.error ?? new Error("Failed to clear replay history."));
|
|
2188
|
+
};
|
|
2189
|
+
});
|
|
1996
2190
|
});
|
|
1997
2191
|
},
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2192
|
+
clearTopic(topic) {
|
|
2193
|
+
return serializeMutation(async () => {
|
|
2194
|
+
const db = await open();
|
|
2195
|
+
await new Promise((resolve, reject) => {
|
|
2196
|
+
let transaction;
|
|
2197
|
+
try {
|
|
2198
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
invalidate(db);
|
|
2201
|
+
reject(error);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
transaction.objectStore(storeName).delete(topic);
|
|
2205
|
+
transaction.oncomplete = () => resolve();
|
|
2206
|
+
transaction.onerror = () => {
|
|
2207
|
+
invalidate(db);
|
|
2208
|
+
reject(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
2209
|
+
};
|
|
2210
|
+
});
|
|
2005
2211
|
});
|
|
2006
2212
|
},
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2213
|
+
clearBefore(timestamp) {
|
|
2214
|
+
return serializeMutation(async () => {
|
|
2215
|
+
const db = await open();
|
|
2216
|
+
await new Promise((resolve, reject) => {
|
|
2217
|
+
let transaction;
|
|
2218
|
+
try {
|
|
2219
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
invalidate(db);
|
|
2222
|
+
reject(error);
|
|
2223
|
+
return;
|
|
2018
2224
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2225
|
+
const store = transaction.objectStore(storeName);
|
|
2226
|
+
const request = store.getAll();
|
|
2227
|
+
request.onsuccess = () => {
|
|
2228
|
+
for (const record of request.result) {
|
|
2229
|
+
const messages = record.messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
2230
|
+
if (messages.length === 0) store.delete(record.topic);
|
|
2231
|
+
else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
request.onerror = () => {
|
|
2235
|
+
invalidate(db);
|
|
2236
|
+
reject(request.error ?? new Error("Failed to read replay history."));
|
|
2237
|
+
};
|
|
2238
|
+
transaction.oncomplete = () => resolve();
|
|
2239
|
+
transaction.onerror = () => {
|
|
2240
|
+
invalidate(db);
|
|
2241
|
+
reject(transaction.error ?? new Error("Failed to prune replay history."));
|
|
2242
|
+
};
|
|
2243
|
+
});
|
|
2023
2244
|
});
|
|
2024
2245
|
}
|
|
2025
2246
|
};
|
|
@@ -2037,11 +2258,13 @@ function parseDataBusPublication(value, fallbackTopic) {
|
|
|
2037
2258
|
if (!topic) return null;
|
|
2038
2259
|
const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
|
|
2039
2260
|
const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
|
|
2261
|
+
const messageId = typeof publication.messageId === "string" && publication.messageId.length > 0 ? publication.messageId : void 0;
|
|
2262
|
+
const timestamp = typeof publication.timestamp === "number" && Number.isFinite(publication.timestamp) ? publication.timestamp : void 0;
|
|
2040
2263
|
return {
|
|
2041
2264
|
topic,
|
|
2042
2265
|
data,
|
|
2043
|
-
...
|
|
2044
|
-
...
|
|
2266
|
+
...messageId === void 0 ? {} : { messageId },
|
|
2267
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
2045
2268
|
};
|
|
2046
2269
|
}
|
|
2047
2270
|
|
|
@@ -2077,7 +2300,9 @@ var WebSocketTransport = class {
|
|
|
2077
2300
|
};
|
|
2078
2301
|
socket.onclose = () => handlers.onStatus("disconnected");
|
|
2079
2302
|
socket.onerror = () => handlers.onStatus("error");
|
|
2080
|
-
socket.onmessage = (event) =>
|
|
2303
|
+
socket.onmessage = (event) => {
|
|
2304
|
+
void this.handleMessage(event.data);
|
|
2305
|
+
};
|
|
2081
2306
|
this.socket = socket;
|
|
2082
2307
|
}
|
|
2083
2308
|
/** Idempotent: re-subscribing an active topic re-sends the frame but does
|
|
@@ -2153,7 +2378,15 @@ var WebSocketTransport = class {
|
|
|
2153
2378
|
/** Parse a server frame. Only objects carrying a string `topic` are
|
|
2154
2379
|
* publications; malformed JSON and unknown shapes are ignored so a chatty
|
|
2155
2380
|
* server cannot crash the message path. */
|
|
2156
|
-
handleMessage(raw) {
|
|
2381
|
+
async handleMessage(raw) {
|
|
2382
|
+
if (typeof Blob !== "undefined" && raw instanceof Blob) {
|
|
2383
|
+
try {
|
|
2384
|
+
await this.handleMessage(await raw.arrayBuffer());
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
this.handlers?.onError(error);
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2157
2390
|
let parsed;
|
|
2158
2391
|
if (raw instanceof ArrayBuffer) {
|
|
2159
2392
|
const bytes = new Uint8Array(raw);
|