cross-tab-worker-databus 0.21.24 → 0.21.25

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,3 +1,25 @@
1
+ ## [0.21.25] - 2026-09-25
2
+
3
+ A publication's fan-out set is now fixed when its delivery begins, which closes a shape that did not terminate: a handler that registers a fresh closure for its own topic made the message that triggered the registration keep delivering to the handlers it created.
4
+
5
+ ### Fixed
6
+
7
+ - **`dispatch()` collects before it invokes.** Handler lists are held in a `Set` per topic, and `Set` iteration visits entries appended after the cursor while skipping entries deleted before it, so both directions of mutation were observable inside one delivery: a handler that subscribed another one for the same topic received the message that caused the subscription, and a handler that unsubscribed a later one cut it out of a delivery already in flight (measured: a two-handler topic delivered `['first']`). The first half does not terminate and did not need a hostile application to reach — the ordinary "re-arm this one-shot handler on every message" pattern, which registers a *new* closure each time, ran **500 handlers for a single publication** (that number is the probe's own ceiling, not a bound the library imposed) and the next publication delivered to **999**, because the registrations persist. Re-subscribing the *same* function reference is a no-op by `Set` identity, which is why the shape survived every prior pass over this file: the first probe written for it used a named function and measured one invocation.
8
+ - **What the contract is now.** Both passes — the exact-topic handlers and the handlers of every matching wildcard pattern — are collected before the first handler runs, so a message is delivered to the subscribers that existed when its delivery began, and a late `unsubscribe()` takes effect on the next publication instead of mid-message.
9
+
10
+ ### Tests
11
+
12
+ - **Three cases, and each one's failure message is the measurement.** `delivers a publication to the handlers that existed when its delivery began` fails as `expected 500 to be 1` against the previous code, `keeps a handler in a delivery that another handler already started` as `expected [ 'first' ] to deeply equal [ 'first', 'second' ]`, and `does not deliver a publication to a wildcard subscription made during it` as `expected [ 'exact', 'pattern' ] to deeply equal [ 'exact' ]`. The first is written with an explicit ceiling so a regression reports a count rather than hanging the runner. All three pass against the fix, and the 933 tests that already existed are unchanged by it — no prior test depended on mutating the fan-out mid-delivery.
13
+ - **The hot path was sized, not assumed.** The five `tests/perf-gate.test.ts` gates cover hashing, owner selection and pattern matching, not dispatch, so the snapshot was measured directly: best-of-seven over 200k iterations, **9-23 ns per dispatch** for 1-20 handlers and **+15 ns** for a matching wildcard pattern holding two handlers, against a shipped per-message budget in the tens of microseconds. Coverage did not move — 46 zero-count branch arms of 1963, aggregate 99.02 / 97.65 / 99.27 / 99.69 — because both arms of every branch added here were already exercised. Tests 933 → 936.
14
+
15
+ ### Documentation
16
+
17
+ - **`docs/api.md` and its Chinese mirror each gained one bullet** stating the delivery-set contract in the `subscribe()` list, so the rule is visible where a consumer reads it rather than only in the source.
18
+
19
+ ### Compatibility
20
+
21
+ Delivery to a handler registered *during* a dispatch, and cancellation of a handler unsubscribed *during* one, both change: those handlers no longer see the in-flight message, and that handler does. A consumer relying on either needed the pattern this fix stops (a self-growing fan-out), and no shipped default, frame, storage key, export or option moved. `verify:compat`, `verify:types` and `verify:pack` pass against `v0.21.24`.
22
+
1
23
  ## [0.21.24] - 2026-09-25
2
24
 
3
25
  Two lifecycle defects that the coverage ledger handed over as a by-product: a cluster runtime stopped during its own activation rebuilt everything its teardown had removed, and a `stop()` issued from one of its callbacks was dropped on the floor. A verdict now sits at every zero-count site the ledger had left bare.
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-IFSC44DS.js";
10
+ } from "./chunk-ORGPJ2CX.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -2491,7 +2491,7 @@ var SDK_VERSION = (
2491
2491
  // declaration's type is `string | undefined` and the export's is `string`
2492
2492
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2493
2493
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2494
- true ? "0.21.24" : ""
2494
+ true ? "0.21.25" : ""
2495
2495
  );
2496
2496
 
2497
2497
  // src/core/data-bus.ts
@@ -3305,15 +3305,21 @@ var CrossTabDataBus = class {
3305
3305
  }
3306
3306
  /** Deliver a message to every local handler registered for its topic,
3307
3307
  * plus every handler registered with a wildcard subscription that matches
3308
- * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
3308
+ * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). Both sets are
3309
+ * collected before the first handler runs, so a publication is delivered to the
3310
+ * subscribers that existed when its delivery began — see the note in the body. */
3309
3311
  dispatch(message) {
3310
3312
  this.trace.recordDispatched(message.topic);
3311
- this.invokeHandlers(this.topicHandlers.get(message.topic) ?? [], (handler) => handler(message));
3313
+ const exactHandlers = this.topicHandlers.get(message.topic);
3314
+ const delivered = exactHandlers ? Array.from(exactHandlers) : [];
3315
+ const wildcards = [];
3312
3316
  for (const [pattern, handlers] of this.topicHandlers) {
3313
3317
  if (pattern !== message.topic && topicMatchesPattern(pattern, message.topic)) {
3314
- this.invokeHandlers(handlers, (handler) => handler(message));
3318
+ for (const handler of handlers) wildcards.push(handler);
3315
3319
  }
3316
3320
  }
3321
+ this.invokeHandlers(delivered, (handler) => handler(message));
3322
+ this.invokeHandlers(wildcards, (handler) => handler(message));
3317
3323
  this.replayManager.record(message);
3318
3324
  }
3319
3325
  /**
@@ -3725,4 +3731,4 @@ export {
3725
3731
  parseDataBusPublication,
3726
3732
  selectWorkerBackend
3727
3733
  };
3728
- //# sourceMappingURL=chunk-IFSC44DS.js.map
3734
+ //# sourceMappingURL=chunk-ORGPJ2CX.js.map