cross-tab-worker-databus 0.21.7 → 0.21.8

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,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.21.8] - 2026-09-23
4
+
5
+ ### Coverage
6
+ - A guard in the cluster's load bookkeeping turned out to be reachable through the public API and had no test behind it. `sendControl()` dispatches a **self-addressed** SUBSCRIBE by calling `handlers.onControl` synchronously, which at the bus layer runs through `subscribeTransport()` into `transport.subscribe()` — a method the host implements. So application code can call `stop()` inside its own subscription path, and when the stack unwinds, `updateLoad()` still has a changed assignment set to act on. `if (this.started) this.writeRecord(true);` is what keeps that write from republishing the worker record `pause()` had just removed; the hazard is not cosmetic, because liveness is read out of that record, so a peer keeps routing topics and publications to a channel that is closed until the TTL expires. Deleting the guard now fails one test — `publishes no worker record when a control handler stops the cluster mid-subscribe`, which asserts both that no record survives the teardown and that a runtime joining afterwards sees only itself — with the other 883 green, and the site says so. The post-handler write surface was enumerated rather than assumed: `confirmRoute()` runs before the handler, and `notifyRegistry()` goes through `send()`, which returns early once the channel is null, so that one line is the whole window. `cluster.ts` uncovered branch arms 22 → 21 and its branch coverage 94.28% → 94.54%; whole tree 99.01 / 96.84 → 99.01 / 96.89 over 884 tests. No behaviour change. Two of this pass's mutation runs removed the call instead of only its guard, which broke four unrelated tests and let the new one pass — the inverted signature of a leg that is already pinned — so `AGENTS.md` now states the rule that settled it: diff the mutant before believing what it kills.
7
+ - The two `selectLeastLoadedWorker(…) ?? this.currentRecord` fallbacks on the cluster's ownership paths are now labelled with what makes them unreachable, because both look like a gap and neither is. `subscribe()` refuses to run before `start()`, `reconcileSubscriptions()` is reached only from `reconcile()` which refuses the same, and `readWorkers()` appends this worker's own record whenever storage names nobody while the runtime is started — so the candidate list cannot be empty and the election cannot answer undefined. Measured two ways: deleting both fallbacks leaves all 883 tests green (vitest does not type check), and `tsc --noEmit` then rejects `writeRoute(…, owner, …)` and `owner.workerId` as `WorkerRecord | undefined`. The compiler states the same fact the runtime cannot reach, which is why the fallback stays rather than becoming a non-null assertion. No behaviour change and no coverage movement; a probe of the scenario that looked like its counterexample — a peer expiring this worker's record and then departing, leaving nothing in storage — measured `readWorkers()` returning this worker's own record, so it never reaches the right arm at all.
8
+ - The IndexedDB replay adapter now has a test for the failure signals that arrive after its connection cache has moved on, and it closes two legs that had only ever run one way. `invalidate(db)` is called from every failure path in the adapter, and each time it ran the cached slot was non-null and named the same connection the caller held. A stale signal is reachable: one aborted transaction dispatches `error` and then `abort`, and a real implementation queues each as its own task, so an invalidation can land after an earlier one cleared the slot or after a healthy operation reopened a different connection. The second case is the one with teeth — without the `current === db` check the stale signal closes the *live* connection and evicts it, which the new test reports as a third reopen (`expected 3 to be 2`). Without the `if (dbPromise)` guard the empty-slot case throws a `TypeError` out of the IndexedDB handler and the operation never settles, so that assertion is raced against a 500 ms watchdog and reads `expected 'hung' to be 'rejected'` rather than as a bare test timeout that names nothing. `replay-persistence.ts` branch coverage 89.74% → 92.30%; whole tree 99.01 / 96.74 → 99.01 / 96.84 over 883 tests, with the module's ledger down to six zero-count arms and one uncovered function.
9
+ - The same pass measured the four `settled` latches in `load`/`clear`/`clearTopic`/`clearBefore`, which are what stop the second signal of one outcome from invalidating again, and they are dominated: deleting all four leaves the suite green (measured at 882 tests, before the test above existed) and the reopen count unchanged, because both signals are dispatched inside one microtask batch and their callbacks run before the serialized queue resumes. They stay, and the reason is recorded at the first of the four — that proof rests on dispatch timing outside the file, while a latch does not. Two remaining legs are likewise recorded rather than pinned: the false arms of the two `if (dbPromise …)` guards inside `open()` are excluded by the invariant that a slot is only replaced by the path that clears it, and `invalidate`'s rejection handler is the only thing between a racing open failure and an unhandled rejection, but reaching it needs a scheduled failure to land while a rejecting reopen is still in flight. No behaviour change.
10
+ - The trace reporter's `sink` default keeps its zero-count arm and now says what that zero measures. `sink` is a **required** member of `DataBusTraceOptions` and `docs/configuration.md` documents it as Required in en and zh, and `DataBusTraceReporter` has no package export, so the only caller that can select `?? (() => undefined)` on an active reporter is untyped JavaScript. The reason the arm reads 0 is narrower than it looks: the branch region begins inside the arrow's body, so the counter tracks that closure's **invocations**, not the `??` selection. Measured both ways — a coverage run of `tests/property.test.ts` alone selects the default 300 times (a no-arg reporter is inert, since `enabled` defaults to false) and still reports `[300, 0]`, while a single `enabled: true` construction that emits two events moves it to `[2, 2]`. Deleting the default — cast in place so the field type still checks out — leaves all 884 tests green and turns each of those two emissions into a `[cross-tab-worker-databus] trace sink threw:` `console.warn` carrying a `TypeError`. Nothing in the supported surface reaches it, so it is recorded at the site rather than pinned with a cast that fakes a forbidden call; the earlier classification stands, and what changed is that the site now carries the measurement instead of the prose. `AGENTS.md` gained the general rule, because a repo-wide scan for arms whose region opens on an arrow function found this one as the only instance and the shape is easy to misread as "no caller omits the sink". No behaviour change and no coverage movement.
11
+
12
+ ### Tests
13
+ - Both seeded invariant harnesses were run far past the depth they ship with, to find out whether anything is left for them to find: `coordination-invariants` completed **400,000 of 400,000** seeds in 753.7 s and `lifecycle-invariants` **200,000 of 200,000** in 262.6 s, both green, with no owner-uniqueness, transport-holder, exactly-once-fan-out, departed-topic or lifecycle-flag violation and no entry in either failure list. The first attempt at that depth did not fail but looked like it: the runs died at exactly 120 s with a bare test timeout, because each `it()` passes its own ceiling as a third argument, and a wall-clock fuse, a seed cap, and that ceiling are three separate limits. The two harness headers now carry the measured rates and that three-limits note, so the next reader knows the shipped 5,000 / 1,500 are a CI-time compromise and not a sufficiency claim, and knows a unit run scheduled *beside* a sweep like this one starves (19 minutes without finishing, against 12.2 s on an idle machine). No change to the shipped constants.
14
+ - The shared-mode teardown E2E was asserting on the demo server's **global** connection count, and that is not a per-test number: `playwright.config.ts` runs specs in parallel, so every other spec with a live tab contributes. `--repeat-each=3` reproduces it — three copies of the test, each waiting for exactly two of six connections, and all three time out at the precondition. It also passed vacuously when it did pass: on a passing run the two sockets behind the "2" were one subscriber of the test's own channel and one subscribed to **nothing**, because in a shared cluster only the owner transport subscribes a channel. Neither defect is load, which is what every previous adjudication of this spec assumed. The assertions now address sockets by the channel each holds — `uniqueTopic()` already makes those names private to the test — and say the thing the name always claimed: the closed tab's socket id leaves the server's list, *and* the surviving tab's id is still in it after the close, which a count cannot distinguish from a server that dropped both and let the survivor reconnect. `/debug/connections` gained `details: [{ id, ageMs, channels }]` to make that addressable (`centrifugo` is unchanged), the socket's accept time is pinned in `tests/demo-centrifuge-server.test.ts` alongside its id, and the test polls the whole trajectory and logs the reap latency — measured at 27–32 ms in one poll, so the 45 s budget is three orders of magnitude of headroom and any future failure arrives with the evidence to say why. Ownership of each topic is waited for as a precondition rather than assumed, since channel-based addressing is only valid while the cluster keeps each topic with the tab that asked for it. 36 E2E tests pass, and the same spec now passes under `--repeat-each=3`, which is the run that broke the old version. `AGENTS.md`'s E2E bullet now states the counter rule beside the `uniqueTopic()` rule it broke, including why two tabs of one shared topic show as one channel-carrying socket and one with none.
15
+
16
+ ### Documentation
17
+ - `docs/api.md` said that no public trace event contains a raw topic. It was false, in English and in Chinese, and `docs/configuration.md` documented the opposite in both languages: subscription events carry their topic so an integrator can correlate an ownership change with the channel it moved, and that page then tells the reader to redact topic conventions before forwarding a sink to telemetry. The code has always been with `configuration.md` — `traceSubscription()` puts the plaintext topic in the event, and the route-scoped `reliability` operations do the same. The corrected sentence in both languages now states what was measured instead: no event carries a message payload, a connection address, or an error body; topic plaintext appears in `subscription` and in `reliability` when it names a route; every other event is topic-free and `coordination` identifies routes by their opaque `topicKey`. Because a shipped document making a false privacy claim is what this is, the correction is a patch release on its own. Pinned by `keeps message payloads and error bodies out of trace events, and names the event that carries a topic` in `tests/data-bus.test.ts`, which drives a payload sentinel and an error-body sentinel through a real bus and asserts the sink sees neither: leaking `lastFailure.message` into the `error` event fails that test and nothing else in the file (1 failed, 187 passed), so the absence claim had no witness before this. The same test asserts which event types may name a topic, which is a duplicate of the existing coordination-format assertion for the route half — recorded as such rather than claimed as new coverage.
18
+
3
19
  ## [0.21.7] - 2026-09-23
4
20
 
5
21
  ### Changed
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-JQEKC5WW.js";
10
+ } from "./chunk-DBKAWZBG.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -2390,7 +2390,7 @@ var DedupManager = class {
2390
2390
  };
2391
2391
 
2392
2392
  // src/core/version.ts
2393
- var SDK_VERSION = true ? "0.21.7" : "";
2393
+ var SDK_VERSION = true ? "0.21.8" : "";
2394
2394
 
2395
2395
  // src/core/data-bus.ts
2396
2396
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -3592,4 +3592,4 @@ export {
3592
3592
  parseDataBusPublication,
3593
3593
  selectWorkerBackend
3594
3594
  };
3595
- //# sourceMappingURL=chunk-JQEKC5WW.js.map
3595
+ //# sourceMappingURL=chunk-DBKAWZBG.js.map