cross-tab-worker-databus 0.20.63 → 0.20.65

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.65] - 2026-09-05
4
+
5
+ ### Added
6
+ - Added opt-in adaptive dedup TTL sampling with bounded `minMs`/`maxMs` controls and diagnostics exposure.
7
+ - Added replay `pruneStrategy` configuration (`count`, `age`, `both`) for memory history trimming while preserving legacy defaults.
8
+
9
+ ## [0.20.64] - 2026-09-05
10
+
11
+ ### Added
12
+ - Added opt-in `asyncSink` trace mode, batching sink delivery onto a microtask while preserving event order and sink error isolation.
13
+ - Added regression coverage for asynchronous trace batching (339 unit tests).
14
+
3
15
  ## [0.20.63] - 2026-09-05
4
16
 
5
17
  ### Added
@@ -2,7 +2,7 @@ import {
2
2
  CrossTabDataBus,
3
3
  parseDataBusPublication,
4
4
  selectWorkerBackend
5
- } from "./chunk-ZHCGYGL3.js";
5
+ } from "./chunk-KKSZ5GUC.js";
6
6
 
7
7
  // src/centrifuge-session.ts
8
8
  import { Centrifuge } from "centrifuge";
@@ -1158,6 +1158,9 @@ var DataBusTraceReporter = class {
1158
1158
  metricsIntervalMs;
1159
1159
  sink;
1160
1160
  now;
1161
+ asyncSink;
1162
+ pendingEvents = [];
1163
+ sinkFlushScheduled = false;
1161
1164
  intervalHandle = null;
1162
1165
  intervalStartedAt = 0;
1163
1166
  // A reporter may be flushed explicitly before start(), but once stop() is
@@ -1179,6 +1182,7 @@ var DataBusTraceReporter = class {
1179
1182
  this.mode = options?.mode ?? "all";
1180
1183
  this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
1181
1184
  this.sink = options?.sink ?? (() => void 0);
1185
+ this.asyncSink = options?.asyncSink ?? false;
1182
1186
  this.now = now;
1183
1187
  }
1184
1188
  /** Start the periodic metrics flush interval. No-op when mode is 'events'
@@ -1305,6 +1309,22 @@ var DataBusTraceReporter = class {
1305
1309
  this.dedupSuppressed = 0;
1306
1310
  }
1307
1311
  emit(event) {
1312
+ if (this.asyncSink) {
1313
+ this.pendingEvents.push(event);
1314
+ if (!this.sinkFlushScheduled) {
1315
+ this.sinkFlushScheduled = true;
1316
+ queueMicrotask(() => {
1317
+ this.sinkFlushScheduled = false;
1318
+ const events = this.pendingEvents;
1319
+ this.pendingEvents = [];
1320
+ for (const queued of events) this.emitSync(queued);
1321
+ });
1322
+ }
1323
+ return;
1324
+ }
1325
+ this.emitSync(event);
1326
+ }
1327
+ emitSync(event) {
1308
1328
  try {
1309
1329
  this.sink(event);
1310
1330
  } catch (error) {
@@ -1360,6 +1380,7 @@ var CrossTabDataBus = class {
1360
1380
  replayMaxPerTopic;
1361
1381
  replayPersistence;
1362
1382
  replayRetentionMs;
1383
+ replayPruneStrategy;
1363
1384
  replayRetentionSweepMs;
1364
1385
  persistenceRetryMaxAttempts;
1365
1386
  persistenceRetryBackoffMs;
@@ -1375,6 +1396,10 @@ var CrossTabDataBus = class {
1375
1396
  trace;
1376
1397
  dedupMaxEntries;
1377
1398
  dedupTtlMs;
1399
+ dedupAdaptiveBounds;
1400
+ dedupWindowStartedAt = 0;
1401
+ dedupWindowAccepted = 0;
1402
+ dedupAdaptiveWindowMs = 5e3;
1378
1403
  dedupSweepMs;
1379
1404
  dedupSweepTimer = null;
1380
1405
  dedupEnabled;
@@ -1433,6 +1458,8 @@ var CrossTabDataBus = class {
1433
1458
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1434
1459
  this.replayPersistence = replay?.persistence ?? null;
1435
1460
  this.replayRetentionMs = replay?.retentionMs;
1461
+ this.replayPruneStrategy = replay?.pruneStrategy ?? "count";
1462
+ if (!["count", "age", "both"].includes(this.replayPruneStrategy)) throw new TypeError("replay.pruneStrategy must be count, age, or both.");
1436
1463
  if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
1437
1464
  throw new TypeError("replay.retentionMs must be a positive finite number.");
1438
1465
  }
@@ -1465,6 +1492,9 @@ var CrossTabDataBus = class {
1465
1492
  this.trace = new DataBusTraceReporter(trace);
1466
1493
  this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1467
1494
  this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1495
+ this.dedupAdaptiveBounds = dedup?.adaptiveTtl;
1496
+ if (this.dedupAdaptiveBounds && (this.dedupAdaptiveBounds.minMs <= 0 || this.dedupAdaptiveBounds.maxMs < this.dedupAdaptiveBounds.minMs)) throw new TypeError("dedup.adaptiveTtl bounds are invalid.");
1497
+ this.dedupWindowStartedAt = this.now();
1468
1498
  this.dedupSweepMs = dedup?.sweepMs;
1469
1499
  this.dedupEnabled = dedup !== void 0;
1470
1500
  if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
@@ -1738,7 +1768,8 @@ var CrossTabDataBus = class {
1738
1768
  enabled: this.dedupEnabled,
1739
1769
  tracked: this.seenMessageIds.size,
1740
1770
  suppressed: this.dedupSuppressed,
1741
- accepted: this.dedupAccepted
1771
+ accepted: this.dedupAccepted,
1772
+ ...this.dedupAdaptiveBounds ? { ttlMs: this.currentDedupTtl() } : {}
1742
1773
  };
1743
1774
  }
1744
1775
  /** Drop all remembered IDs and reset dedup counters. */
@@ -1910,6 +1941,7 @@ var CrossTabDataBus = class {
1910
1941
  }
1911
1942
  this.seenMessageIds.set(message.messageId, now);
1912
1943
  this.dedupAccepted += 1;
1944
+ this.dedupWindowAccepted += 1;
1913
1945
  this.trace.recordDedupAccepted();
1914
1946
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1915
1947
  const oldest = this.seenMessageIds.keys().next().value;
@@ -1927,11 +1959,24 @@ var CrossTabDataBus = class {
1927
1959
  this.dedupSweepTimer = null;
1928
1960
  }
1929
1961
  pruneExpiredDedup() {
1930
- const cutoff = this.now() - this.dedupTtlMs;
1962
+ const cutoff = this.now() - this.currentDedupTtl();
1931
1963
  for (const [id, timestamp] of this.seenMessageIds) {
1932
1964
  if (timestamp < cutoff) this.seenMessageIds.delete(id);
1933
1965
  }
1934
1966
  }
1967
+ currentDedupTtl() {
1968
+ if (!this.dedupAdaptiveBounds) return this.dedupTtlMs;
1969
+ const now = this.now();
1970
+ const elapsed = now - this.dedupWindowStartedAt;
1971
+ if (elapsed >= this.dedupAdaptiveWindowMs) {
1972
+ this.dedupWindowStartedAt = now;
1973
+ this.dedupWindowAccepted = 0;
1974
+ return this.dedupAdaptiveBounds.maxMs;
1975
+ }
1976
+ const rate = this.dedupWindowAccepted / Math.max(1, elapsed);
1977
+ const factor = Math.min(1, rate / 0.01);
1978
+ return this.dedupAdaptiveBounds.maxMs - (this.dedupAdaptiveBounds.maxMs - this.dedupAdaptiveBounds.minMs) * factor;
1979
+ }
1935
1980
  /** Deliver a message to every local handler registered for its topic,
1936
1981
  * plus every handler registered with a wildcard subscription that matches
1937
1982
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1956,7 +2001,17 @@ var CrossTabDataBus = class {
1956
2001
  }
1957
2002
  const storedMessage = message;
1958
2003
  buffer.push(storedMessage);
1959
- if (buffer.length > this.replayMaxPerTopic) buffer.shift();
2004
+ if (this.replayPruneStrategy !== "age") {
2005
+ while (buffer.length > this.replayMaxPerTopic) buffer.shift();
2006
+ }
2007
+ if (this.replayPruneStrategy !== "count" && this.replayRetentionMs !== void 0) {
2008
+ const cutoff = this.now() - this.replayRetentionMs;
2009
+ while (buffer.length > 0) {
2010
+ const first = buffer[0];
2011
+ if (!first || first.timestamp === void 0 || first.timestamp >= cutoff) break;
2012
+ buffer.shift();
2013
+ }
2014
+ }
1960
2015
  if (this.replayPersistence) {
1961
2016
  void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
1962
2017
  if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
@@ -2309,4 +2364,4 @@ export {
2309
2364
  parseDataBusPublication,
2310
2365
  selectWorkerBackend
2311
2366
  };
2312
- //# sourceMappingURL=chunk-ZHCGYGL3.js.map
2367
+ //# sourceMappingURL=chunk-KKSZ5GUC.js.map