cross-tab-worker-databus 0.21.12 → 0.21.13

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/centrifuge-protocol.d.ts.map +1 -1
  3. package/dist/centrifuge-session.d.ts +10 -3
  4. package/dist/centrifuge-session.d.ts.map +1 -1
  5. package/dist/centrifuge.d.ts +10 -4
  6. package/dist/centrifuge.d.ts.map +1 -1
  7. package/dist/centrifuge.js +23 -11
  8. package/dist/centrifuge.js.map +2 -2
  9. package/dist/centrifuge.shared.worker.js +28 -11
  10. package/dist/centrifuge.shared.worker.js.map +2 -2
  11. package/dist/centrifuge.worker.js +10 -3
  12. package/dist/centrifuge.worker.js.map +2 -2
  13. package/dist/{chunk-TSTV3SDC.js → chunk-4HW6PYIR.js} +67 -33
  14. package/dist/chunk-4HW6PYIR.js.map +7 -0
  15. package/dist/{chunk-2TTJ7LGZ.js → chunk-NPFTD7YE.js} +1 -1
  16. package/dist/chunk-NPFTD7YE.js.map +7 -0
  17. package/dist/cjs/centrifuge.cjs +86 -40
  18. package/dist/cjs/centrifuge.cjs.map +2 -2
  19. package/dist/cjs/hooks.cjs.map +2 -2
  20. package/dist/cjs/index.cjs +80 -36
  21. package/dist/cjs/index.cjs.map +2 -2
  22. package/dist/cjs/vue.cjs.map +2 -2
  23. package/dist/core/cluster.d.ts +33 -16
  24. package/dist/core/cluster.d.ts.map +1 -1
  25. package/dist/core/data-bus.d.ts +19 -5
  26. package/dist/core/data-bus.d.ts.map +1 -1
  27. package/dist/core/dedup-manager.d.ts +6 -3
  28. package/dist/core/dedup-manager.d.ts.map +1 -1
  29. package/dist/core/environment.d.ts +14 -5
  30. package/dist/core/environment.d.ts.map +1 -1
  31. package/dist/core/hash.d.ts +10 -4
  32. package/dist/core/hash.d.ts.map +1 -1
  33. package/dist/core/replay-manager.d.ts +2 -1
  34. package/dist/core/replay-manager.d.ts.map +1 -1
  35. package/dist/core/routing.d.ts +18 -9
  36. package/dist/core/routing.d.ts.map +1 -1
  37. package/dist/core/storage-batch.d.ts +2 -2
  38. package/dist/core/storage-batch.d.ts.map +1 -1
  39. package/dist/core/trace.d.ts +21 -9
  40. package/dist/core/trace.d.ts.map +1 -1
  41. package/dist/core/types.d.ts +13 -5
  42. package/dist/core/types.d.ts.map +1 -1
  43. package/dist/hooks.d.ts.map +1 -1
  44. package/dist/hooks.js +1 -1
  45. package/dist/hooks.js.map +2 -2
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +17 -7
  49. package/dist/index.js.map +2 -2
  50. package/dist/utils/constants.d.ts +7 -4
  51. package/dist/utils/constants.d.ts.map +1 -1
  52. package/dist/utils/validation.d.ts +12 -4
  53. package/dist/utils/validation.d.ts.map +1 -1
  54. package/dist/vue.js +1 -1
  55. package/dist/websocket.d.ts +24 -6
  56. package/dist/websocket.d.ts.map +1 -1
  57. package/dist/workers/port-reaper.d.ts +18 -8
  58. package/dist/workers/port-reaper.d.ts.map +1 -1
  59. package/docs/api.md +6 -5
  60. package/docs/benchmarks.md +9 -9
  61. package/docs/release-checklist.md +3 -3
  62. package/docs/roadmap.md +31 -1
  63. package/docs/transports.md +3 -2
  64. package/docs/zh/api.md +6 -5
  65. package/docs/zh/benchmarks.md +9 -9
  66. package/docs/zh/release-checklist.md +3 -3
  67. package/docs/zh/roadmap.md +22 -1
  68. package/docs/zh/transports.md +2 -1
  69. package/package.json +2 -1
  70. package/dist/chunk-2TTJ7LGZ.js.map +0 -7
  71. package/dist/chunk-TSTV3SDC.js.map +0 -7
@@ -23,7 +23,7 @@ import {
23
23
  WORKER_MODE,
24
24
  WORKER_ROLE,
25
25
  WORKER_STATUS
26
- } from "./chunk-2TTJ7LGZ.js";
26
+ } from "./chunk-NPFTD7YE.js";
27
27
 
28
28
  // src/core/environment.ts
29
29
  function getStorage(name) {
@@ -479,8 +479,8 @@ var BatchingStorageWriter = class {
479
479
  retryHandle = null;
480
480
  retryDelayMs = INITIAL_RETRY_DELAY_MS;
481
481
  /** Number of writes queued in memory but not yet flushed to storage.
482
- * Used by tests to assert the coalescing window and by flush() to detect
483
- * the all-drained state. */
482
+ * Test-only as a reader: `flush()` tests `pending.size` directly rather than
483
+ * going through this getter, so it is not part of the drain decision. */
484
484
  get pendingSize() {
485
485
  return this.pending.size;
486
486
  }
@@ -654,8 +654,9 @@ var WorkerClusterRuntime = class {
654
654
  // Topics this tab has subscribed to (local interest, plaintext).
655
655
  subscribedTopics = /* @__PURE__ */ new Set();
656
656
  // Topics assigned to this Worker as owner (topicKey → topic). Authoritative:
657
- // membership drives isAssigned() and load. Grows only via CONTROL/SUBSCRIBE
658
- // (or local self-subscribe), never via the reverse cache.
657
+ // membership drives isAssigned() and load. Grown by exactly three writers —
658
+ // CONTROL/SUBSCRIBE, ROUTE_RELEASED (completing a handoff this worker accepted),
659
+ // and local self-subscribe — and never by the reverse cache.
659
660
  assignedTopics = /* @__PURE__ */ new Map();
660
661
  routeOwnerCache = /* @__PURE__ */ new Map();
661
662
  routeOwnerCacheMax;
@@ -1049,9 +1050,12 @@ var WorkerClusterRuntime = class {
1049
1050
  windowMs,
1050
1051
  messageCount: this.throughputWindow.messageCount,
1051
1052
  byteCount: this.throughputWindow.byteCount,
1052
- // How much later the heartbeat landed than its nominal interval. This
1053
- // window is anchored at the previous writeRecord (one heartbeat tick),
1054
- // so a starved event loop stretches windowMs past the interval and the
1053
+ // How much later the last record write landed relative to a nominal
1054
+ // heartbeat interval. The window is anchored at the previous writeRecord,
1055
+ // which the heartbeat tick causes but activation, a status change, a
1056
+ // visibility change and a load update also cause — so a worker writing on
1057
+ // any of those shorter paths yields a small window and a clamped zero.
1058
+ // A starved event loop stretches the window past the interval, and the
1055
1059
  // positive excess is the scheduling-overrun signal.
1056
1060
  overrunMs: Math.max(0, windowMs - this.heartbeatIntervalMs),
1057
1061
  sampledAt: now
@@ -1216,11 +1220,13 @@ var WorkerClusterRuntime = class {
1216
1220
  else this.handlers.onControl(message.action, message.topic, message.data);
1217
1221
  if (message.action !== CONTROL_ACTION.PUBLISH) this.updateLoad();
1218
1222
  }
1219
- /**
1220
- * When this worker is the previous owner in a graceful handoff and the new
1221
- * owner asks us to unsubscribe, release the old transport subscription and
1222
- * ACK the handoff with ROUTE_RELEASED. Returns true when the message was a
1223
- * handoff release (the generic CONTROL dispatch must not run as well).
1223
+ /** Drop this worker's ownership of the topic, then — only when this worker is
1224
+ * the previous owner in a graceful handoff and the new owner is asking us to
1225
+ * unsubscribe — release the old transport subscription and ACK the handoff with
1226
+ * ROUTE_RELEASED. The ownership delete runs for *every* CONTROL/UNSUBSCRIBE,
1227
+ * before the handoff test; the handoff legs below do not gate it. Returns true
1228
+ * when the message was a handoff release (the generic CONTROL dispatch must not
1229
+ * run as well).
1224
1230
  */
1225
1231
  releaseHandoffOnUnsubscribe(message) {
1226
1232
  this.assignedTopics.delete(message.topicKey);
@@ -1442,7 +1448,10 @@ var WorkerClusterRuntime = class {
1442
1448
  }
1443
1449
  return Array.from(subscribers);
1444
1450
  }
1445
- /** Read the current route for `topicKey`, returning null when no storage layer exists. */
1451
+ /** Read the current route for `topicKey`. With a storage layer this returns
1452
+ * the durable route, or null when none is recorded. Without one (degraded
1453
+ * mode) it returns the synthesized local route from `buildLocalRoute`, which
1454
+ * is itself null unless this worker actually holds the topic. */
1446
1455
  readRoute(topicKey) {
1447
1456
  if (!this.storage) return this.buildLocalRoute(topicKey);
1448
1457
  return readJson(this.storage, this.routeStorageKey(topicKey));
@@ -1523,8 +1532,9 @@ var WorkerClusterRuntime = class {
1523
1532
  * @param notify — when true, broadcast a REGISTRY nudge so peers reconcile
1524
1533
  * immediately instead of waiting for the next heartbeat. False on the
1525
1534
  * periodic heartbeat tick (peers will notice on their own heartbeat) to
1526
- * avoid a REGISTRY storm every 3 s; true on status/role changes that
1527
- * peers should observe promptly. */
1535
+ * avoid a REGISTRY storm every 3 s. Which sites pass what: activation,
1536
+ * status, visibility and load changes pass true; the heartbeat tick and a
1537
+ * role change pass false. */
1528
1538
  writeRecord(notify) {
1529
1539
  const now = this.environment.now();
1530
1540
  const sample = this.loadWeighting !== void 0 ? this.sampleThroughput(now) : void 0;
@@ -1560,10 +1570,15 @@ var WorkerClusterRuntime = class {
1560
1570
  * Despite the name, this is NOT a cache lookup — it unconditionally writes
1561
1571
  * the `topicKey → topic` pair. Hashing is cheap enough that a caller needing
1562
1572
  * the key should always call this rather than check `knownTopics` first;
1563
- * the cache's FIFO eviction below keeps it bounded. Only `isAssigned`
1564
- * deliberately bypasses this (it must not pollute the cache on a read-only
1565
- * query), so if you add a new call site, prefer `rememberTopic` unless you
1566
- * have the same "read-only query" reason.
1573
+ * the cache's FIFO eviction below keeps it bounded. Two sites deliberately
1574
+ * bypass it, and for the same kind of reason — neither may write a plaintext
1575
+ * into the reverse cache before it is authorized: `isAssigned` re-derives the
1576
+ * key as a read-only query, and the `ROUTE_RELEASED` handler hashes the
1577
+ * frame's own `topic` only to compare it against `topicKey`, on a channel
1578
+ * where that field is attacker-controlled until the comparison passes. (The
1579
+ * `CONTROL` handler performs the same re-derivation but then *does* call this
1580
+ * method, so it is not a bypass.) If you add a new call site, prefer
1581
+ * `rememberTopic` unless you have one of those reasons.
1567
1582
  */
1568
1583
  rememberTopic(topic) {
1569
1584
  const topicKey = createOpaqueKey(topic);
@@ -1637,8 +1652,10 @@ var DataBusTraceReporter = class {
1637
1652
  this.asyncSink = options?.asyncSink ?? false;
1638
1653
  this.now = now;
1639
1654
  }
1640
- /** Start the periodic metrics flush interval. No-op when mode is 'events'
1641
- * (no metrics to emit), when disabled, or when already running. */
1655
+ /** Start the periodic metrics flush interval. Only the `!enabled` return is a
1656
+ * true no-op: in 'events' mode, or with an interval already armed, it returns
1657
+ * without arming anything but has already cleared `stopped` one statement
1658
+ * earlier — which is what re-opens the event stream after a `stop()`. */
1642
1659
  start() {
1643
1660
  if (!this.enabled) return;
1644
1661
  this.stopped = false;
@@ -1745,9 +1762,11 @@ var DataBusTraceReporter = class {
1745
1762
  recordDedupSuppressed() {
1746
1763
  if (this.metricsActive) this.dedupSuppressed += 1;
1747
1764
  }
1748
- /** True when metrics recording is active: enabled and mode includes metrics.
1749
- * Extracted so the four record / flush methods share one guard expression
1750
- * instead of repeating `!this.enabled || this.mode === 'events'` at each. */
1765
+ /** True when metrics recording is active: enabled, not stopped, and the mode
1766
+ * includes metrics. Extracted so the seven readers — `getMetrics`, the five
1767
+ * `record*` methods and `flush` — share one guard instead of repeating it.
1768
+ * The `!stopped` term is part of the contract, not a bonus: `event()` does not
1769
+ * go through this getter and gates on `stopped` itself. */
1751
1770
  get metricsActive() {
1752
1771
  return this.enabled && !this.stopped && this.mode !== TRACE_MODE.EVENTS;
1753
1772
  }
@@ -2011,7 +2030,8 @@ var ReplayManager = class {
2011
2030
  }
2012
2031
  }
2013
2032
  }
2014
- /** Clear replay history for one exact topic, including durable storage. */
2033
+ /** Clear replay history for one exact topic, and for its durable history when
2034
+ * the injected adapter provides the optional `clearTopic`. */
2015
2035
  async clearTopic(topic) {
2016
2036
  if (!this.buffers) return;
2017
2037
  const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
@@ -2390,7 +2410,7 @@ var DedupManager = class {
2390
2410
  };
2391
2411
 
2392
2412
  // src/core/version.ts
2393
- var SDK_VERSION = true ? "0.21.12" : "";
2413
+ var SDK_VERSION = true ? "0.21.13" : "";
2394
2414
 
2395
2415
  // src/core/data-bus.ts
2396
2416
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -2921,7 +2941,10 @@ var CrossTabDataBus = class {
2921
2941
  async clearReplay() {
2922
2942
  await this.replayManager.clearAll();
2923
2943
  }
2924
- /** Clear replay history for one exact topic, including durable storage. */
2944
+ /** Clear replay history for one exact topic and, when the persistence adapter
2945
+ * implements `clearTopic`, its durable history too — an adapter without that
2946
+ * optional method leaves the persisted rows in place, exactly as `clearReplay`
2947
+ * does. */
2925
2948
  async clearReplayTopic(topic) {
2926
2949
  await this.replayManager.clearTopic(topic);
2927
2950
  }
@@ -3316,9 +3339,14 @@ var CrossTabDataBus = class {
3316
3339
  });
3317
3340
  }
3318
3341
  /** Emit the coordination trace snapshot from the current cluster state.
3319
- * Called after a transport opens (start and recovery), when the role and
3320
- * route picture has settled — the synchronous pre-open snapshot would see no
3321
- * routes because their writes are still coalesced in the batch writer. */
3342
+ * Called once per successful `start()`: `reopenTransport()`'s settle arms do not
3343
+ * emit, so a recovery produces no second snapshot. It runs after the opening
3344
+ * resolves — and not because a pre-open read would see no routes, which is the
3345
+ * wrong mechanism and was the shipped claim until it was measured: a snapshot
3346
+ * taken with the opening still unresolved already listed the route, its
3347
+ * confirmation and both tabs' worker records, because
3348
+ * `BatchingStorageWriter` serves pending writes on read (`storage-batch.ts:79-82`
3349
+ * for `getItem`, `:143-155` for the `keys()` union that `readAllByPrefix` uses). */
3322
3350
  emitCoordinationTrace() {
3323
3351
  const snapshot = this.cluster.getSnapshot();
3324
3352
  this.trace.event({
@@ -3512,7 +3540,13 @@ var CrossTabDataBus = class {
3512
3540
  }
3513
3541
  /**
3514
3542
  * Ensure the DataBus is started, throwing if no initialConfig was provided.
3515
- * Called automatically by subscribe/publish/ready when autoStart is true.
3543
+ * subscribe/publish/ready call this *unconditionally*: the gate is the
3544
+ * `hasInitialConfig` check below, not the `autoStart` option. `autoStart`
3545
+ * participates in exactly one place, the constructor's
3546
+ * `if (autoStart ?? this.hasInitialConfig)`, where it overrides the default rather
3547
+ * than enabling a later path — so `autoStart: true` with no `initialConfig` throws
3548
+ * from the constructor. `docs/configuration.md`'s "`true` when `initialConfig` is
3549
+ * provided" row describes that single site.
3516
3550
  */
3517
3551
  ensureStarted() {
3518
3552
  if (this.started) return;
@@ -3592,4 +3626,4 @@ export {
3592
3626
  parseDataBusPublication,
3593
3627
  selectWorkerBackend
3594
3628
  };
3595
- //# sourceMappingURL=chunk-TSTV3SDC.js.map
3629
+ //# sourceMappingURL=chunk-4HW6PYIR.js.map