cross-tab-worker-databus 0.21.4 → 0.21.5

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,42 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.21.5] - 2026-09-22
4
+
5
+ ### Changed
6
+ - The last two "hunt this for a test" items on the `data-bus.ts` arm ledger turned out to be unreachable by their own call sites, and now say so. `getQueuedStartReady()`'s `if (!queued)` rejection and `queueStartAfterStop()`'s `if (this.queuedStart) return this.queuedStart` dedupe each sit behind an *identical* check performed one statement earlier by the only caller (`ready()` and `start()` respectively), synchronously, with nothing in between that could run user code. Neither was deleted, for the asymmetric-failure reason: dropping the first replaces a documented `"No queued start is in flight."` rejection with `TypeError: Cannot read properties of null` for any future second caller, and dropping the second lets one burst of operations behind a single stop queue several restarts, which is exactly the contract that line states. Zero coverage movement, measured after rebasing onto #177: `data-bus.ts` stays at 14 uncovered branch arms and 2 uncovered functions, 37 files / 874 tests.
7
+ - Two `openTransport()` conditions with permanently false arms, one of which had a comment describing a case that cannot happen. The `this.pendingStop === chainedPendingStop` check explained itself by "a stop created concurrently (e.g. by `suspendTransport()`) is a different promise", and the `!this.suspended && !this.stopping` guard below it explained nothing at all; the enumeration that rules both out is the same one. `pendingStop` is written in four places — this line, the failed-open cleanup, `performStop()`'s finally and `suspendTransport()`'s chained stop — and of those, the three that could change it out from under this continuation all run behind a `lifecycleEpoch` increment, while `suspended` has one writer and `stopping` two and each raises its flag only after bumping the epoch. An opening whose captured epoch is still current therefore cannot observe either condition false. Both stay uncovered by construction, so this moves no number; both are kept, because the asymmetric failure is the one worth guarding — an unconditional `pendingStop = null` would drop a live stop and let `stop()` issue a second `transport.stop()` the moment any transition suspends without superseding, and a dropped ready-guard would mark a hidden transport ready.
8
+ - A comment in `createStopPromise()` that asserted the opposite of the truth: it claimed `performStop()` "never rejects", which had turned the rejection arm of the teardown gate's `.then(f, g)` into prose-evidenced defensive filler. It can reject. Its `catch` reports through `reportError()` → `notifyError()` → `invokeHandlers(errorHandlers, …, ERROR_HANDLER)`, and that leg absorbs a throwing error subscriber only by writing to `console.warn` — so a subscriber that throws *and* a `console.warn` that throws escape the `catch` and reject the teardown. The comment now names that chain, and names which half of the arm is load-bearing: `resolveGate()` is the only thing that ever settles `await bus.stop()` on this path (the transport shutdown has already finished, so nothing else can wake the caller), while the `stopPromise = null` beside it is defensive for the reason `stop()`'s own comment already gives — `stopping` is authoritative and a settled gate is stale — which is measured, not argued: deleting that line changes no assertion in the file.
9
+ - One redundant rejection handler deleted from `reopenTransport()`, on a proof that needs no enumeration: the `opening.then(onFulfilled, onRejected)` a few lines above passes an `onRejected`, which registers *that* as a handler of `opening` — so `opening` can never surface as an unhandled rejection on that path whatever it rejects with, and the trailing `void opening.catch(() => undefined)` could only ever duplicate a handler that was already installed. Unlike the `0.21.2` / `0.21.3` deletions this argument rests on Promise semantics rather than on a list of assignment sites, which is also why it does not depend on `reportError` staying total.
10
+ - The two rejection swallows that remain in that method are now each labelled with what they actually are, because they are two different kinds of leg and only one of them is deletable. The early-return arm's `void opening.catch(...)` is the *only* handler that opening ever gets on that path (`resumeTransport()` calls with `void`, and the `.then(f, g)` below is unreachable from the arm), so it is a guard, not a gap. The one at the top of the method is dominated — `pending` provably resolves — but is deliberately kept, and the reason is recorded with a measurement: forcing `pending` to reject fails exactly one test whether the line is present or deleted, so nothing asserts the premise; with the absorb a superseded resume still reopens, without it the bus silently stays closed after a pageshow. A guard whose deletion turns "degraded" into "stuck" is worth its uncovered function.
11
+ - Corrects a claim this pass initially made about itself: the deleted handler was **not** one of `data-bus.ts`'s three uncovered functions — its callback ran in existing tests, so the module's uncovered-function count stays at three and its uncovered-line count is unchanged. What was removed was a handler that executed and could not matter. Coverage movement here is zero, and saying so is the point: the change is justified by the proof, not by a number. (That count of three is the state *at that change*; the `createStopPromise()` arm below is what actually moved it.)
12
+
13
+ ### Fixed
14
+ - A control frame can no longer rename the channel its recipient owns. `topicKey` is a pure function of `topic`, so every frame this library sends has the two agreeing — but `handleControlMessage` checked only `targetWorkerId` and the durable route, both keyed by `topicKey`, and then used the frame's `topic` as the plaintext to remember, assign, and hand to the transport. A `BroadcastChannel` is unauthenticated: any same-origin script can post into it, and one frame carrying a real `topicKey` next to an arbitrary `topic` would have that worker take ownership *under the substituted name*, because the route authorization passed. Incoming control frames are now rejected when `createOpaqueKey(topic) !== topicKey`. Legitimate peers are unaffected — measured on the full suite, including the coordination fuzzer, which forges frames with the two fields deliberately consistent — and the new test fails without the guard as `expected "vi.fn()" to not be called at all, but actually been called 1 times`.
15
+ - The seeded fuzzers' wall-clock fuse could not fire on exactly the runner it was written for. Both harnesses bounded depth with `if (completed >= MIN_SEEDS && elapsed > SEED_BUDGET_MS) break`, so a host too slow to clear the depth floor inside the budget never became eligible to stop: the sweep ran as long as the machine was slow and then died on the test's own 120s ceiling reporting nothing but `Test timed out`. Reproduced on `main` (1,046/5,000 seeds after 509s against a 60s fuse, no invariant violation) and twice on a PR that turned out not to be the cause. The budget is now unconditional and the floor stays an assertion, so a slow runner degrades depth and says so in ~60s instead of hanging for ~8 minutes; demonstrated locally by setting the budget to 0 — the gated form still ran 100 seeds and passed, the unconditional one stops at once and fails with `explored only 0 seeds`.
16
+ - The budget clock had a single source whose immunity was contextual rather than inherent. `realNowMs()` trusted `node:perf_hooks`' `performance`, which survives fake timers only because the global binding and that export are different objects in some Node contexts; a worker thread makes them the same object, so a fake-timer configuration that includes `performance` freezes the budget at 0 elapsed. It now takes the max of that source and `process.uptime()`, which no fake-timer toolbox replaces — measured by blocking 250ms of real time inside a fake window with `Atomics.wait`: `node:perf_hooks` 260, `process.uptime()` 260, and global `performance.now()`, `Date.now()` and `process.hrtime.bigint()` all 0.
17
+ - The clock's own pin asserted only the harmless direction. "60 simulated seconds must not cost 60 measured ones" passes trivially when the clock is frozen, so it could not have caught the bug above; the pin now also requires that real blocked time inside a fake window still measures as having passed. Verified against a mutant returning a constant: it passes the original assertion and fails only the new one.
18
+ - A truncated sweep now logs its slowest seed next to the depth reached. A fuse sampled between iterations cannot bound an iteration, so a seed that wedges on an `await` needing a timer the fake clock never advances was previously visible only as an unexplained total.
19
+
20
+ ### Coverage
21
+ - `src/core/data-bus.ts` goes from 17 uncovered branch arms to 16 by pinning the right operand of `return this.stopPromise ?? Promise.resolve()` in `start()`'s superseded-resume arm. Only a re-entered `stop()` ever assigns `stopPromise`, and the re-entry that reaches *this* line is a re-entered `start()`, which bumps the epoch with no stop in flight and leaves it null — so the fallback had never been taken in any test. The contract it protects is `start()`'s declared `Promise<void>`: replacing the line with `return this.stopPromise!` makes the new assertion fail as `expected null to be an instance of Promise`, which is the shape a caller would meet at `bus.start(c).then(...)`. The count is re-measured on this branch rather than inherited from the `0.21.4` entry, since `reopenTransport()` changed the same file in between.
22
+ - Whole-suite 98.84 / 96.46 / 99.08 / 99.57 over 37 files / 872 tests, against unchanged ceilings of 96 / 92 / 96 / 97.
23
+ - `data-bus.ts` then goes 16 uncovered branch arms → 15 and, for the first time in this ledger, three uncovered functions → 2, because the leg that closed is a handler *function* that had never been entered: `createStopPromise()`'s rejection arm. Driving it needs the double failure that its corrected comment rules out — an error subscriber that throws plus a `console.warn` that throws — and its remaining half is the fall-through of `if (this.stopPromise === stopGate)`, i.e. a stop whose gate had already been replaced by a newer one. Module totals move 97.9 / 96.32 / 97.43 / 98.86 → 98.38 / 96.55 / 98.29 / 99.24 with the whole suite at 98.94 / 96.52 / 99.26 / 99.65 over 37 files / 873 tests. Teeth were checked on the load-bearing statement, not on the arm: deleting `resolveGate()` from that arm fails the new test as `expected 'hung' to be 'settled'` in 250ms, and deleting the `stopPromise = null` next to it leaves all 184 tests in the file passing, which is why the comment calls one a contract and the other defensive.
24
+ - `data-bus.ts` goes 15 uncovered branch arms → 14 (functions stay at 2) by pinning the second conjunct of `invokeHandlers()`' logging guard. A throwing error subscriber is absorbed by writing to `console.warn`, and that write is conditional on `typeof console.warn === 'function'`; with a console that has no `warn`, an unguarded call would raise `TypeError: console.warn is not a function` from inside the handler meant to contain the failure — out through `reportError()`, out of the dispatch loop, into the transport's message callback. The new test stubs such a console and asserts the publication dispatch does not throw and the dispatch failure still lands in the unified ledger. Mutation-checked: dropping the conjunct fails it as `expected [Function] to not throw an error but 'TypeError: console.warn is not a func…' was thrown`. Whole-suite branch coverage 96.52 → 96.57, statements/functions/lines unchanged, 37 files / 874 tests.
25
+ - One arm was *not* closed, and the attempt is recorded so it is not re-attempted blind: `if (!this.recoveryExhausted)` inside the exhausted-recovery branch still has no test that takes its false arm. The first hypothesis — that the existing `caps automatic recovery attempts` test never really reached the second exhaustion because its environment clock was a constant — was measured with a scratch probe against the unmodified test and was **false**: `attempt` reaches 3 and exactly one `exhausted` event is emitted even with the frozen clock, so the assertions there are not vacuous and the speculative rewrite of that test was reverted rather than shipped with its (now disproved) comments. What the probe did establish is that a fourth `setStatus('error')` does not advance the attempt counter at all, for reasons the ledger does not yet explain, and that making the guard unconditional emits no second event — so no current assertion depends on that leg either way.
26
+
27
+ - `data-bus.ts` goes 14 uncovered branch arms → 13 by pinning the `default:` arm of the cluster's `onControl` handler, which is a live leg rather than a defensive one: the BroadcastChannel a cluster listens on is unauthenticated, and `handleControlMessage` forwards `message.action` straight through — its own `switch` has a `default: break` that *falls through to the same call*, then runs load accounting because the verb is not `PUBLISH`. So the handler's `default: break` is the only thing preventing an unknown verb from being acted upon as one of the three known ones. The new test posts a `CONTROL` frame with `action: 'DESTROY'` at the bus's own worker id over the cluster channel and asserts no publication handler ran, no transport subscribe/unsubscribe/publish call appeared, and the bus still reports healthy. Teeth verified by mutation: making the arm act on the topic fails the test as `expected [ 'topic' ] to deeply equal []`. Suite is 37 files / 875 tests.
28
+
29
+ ### Tests
30
+ - Both seeded fuzzers now announce their depth *while* sweeping, not only after: the first five seeds and then every fiftieth print `starting seed N at <elapsed>ms (depth D)`. This exists because a runner killed by the test's own 120s ceiling produces `Error: Test timed out in 120000ms` and nothing else — the truncation log the fuse writes sits after the loop, so it never runs on exactly the failure it was written to explain. With the heartbeat the same log says whether the sweep was merely slow (depth climbing, elapsed growing) or wedged inside one seed (last line `starting seed 7`, no successor), and those two have different fixes.
31
+
32
+ ### Documentation
33
+ - `AGENTS.md` corrects the claim that the budget clock "is immune wherever it is called" to what was measured, records that the depth floor must never gate the fuse, and adds the rule that a clock guard has to be pinned in both directions — with `blockRealTimeMs()` as the way to do it, since a `while (Date.now() < stop)` busy wait never terminates inside a fake window where `Date` is frozen.
34
+ - `AGENTS.md` gains the tier distinction these two legs demonstrate (a guard that is dominated, a guard that is the only handler, and a handler that runs but is redundant are three different verdicts, and only the middle one is a coverage gap), and the stale cross-reference from `reopenTransport()`'s `activeConfig` guard to `performStop()`'s absorber — deleted in `0.21.2` — now points at the live comparison in the same method.
35
+ - `AGENTS.md` gains the rule this pass was written by: a comment that a promise "never rejects" is a claim about every path through the handler it runs, not about the happy path, and when such a leg is the only thing that settles an `await`, construct the throw instead of trusting the prose. It also records the shape to prefer for that pin — race the promise against a short real-timer watchdog, so deleting the leg fails as `expected 'hung' to be 'settled'` rather than as a test timeout that names nothing.
36
+ - `AGENTS.md` adds the discipline this pass's discarded change was bought with: measure a "this test passes vacuously" theory against the *unmodified* test before rewriting it, because the rewrite brings a comment that the next reader will trust. is a claim about every path through the handler it runs, not about the happy path, and when such a leg is the only thing that settles an `await`, construct the throw instead of trusting the prose. It also records the shape to prefer for that pin — race the promise against a short real-timer watchdog, so deleting the leg fails as `expected 'hung' to be 'settled'` rather than as a test timeout that names nothing.
37
+
38
+
39
+
3
40
  ## [0.21.4] - 2026-09-22
4
41
 
5
42
  ### Fixed
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-WFVM7HPE.js";
10
+ } from "./chunk-RWUDSL24.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -1171,6 +1171,7 @@ var WorkerClusterRuntime = class {
1171
1171
  /** Handle a point-to-point CONTROL message (SUBSCRIBE / UNSUBSCRIBE / PUBLISH). */
1172
1172
  handleControlMessage(message) {
1173
1173
  if (message.targetWorkerId !== this.workerId) return;
1174
+ if (message.topicKey !== void 0 && createOpaqueKey(message.topic) !== message.topicKey) return;
1174
1175
  this.rememberTopic(message.topic);
1175
1176
  switch (message.action) {
1176
1177
  case CONTROL_ACTION.SUBSCRIBE: {
@@ -2387,7 +2388,7 @@ var DedupManager = class {
2387
2388
  };
2388
2389
 
2389
2390
  // src/core/version.ts
2390
- var SDK_VERSION = true ? "0.21.4" : "";
2391
+ var SDK_VERSION = true ? "0.21.5" : "";
2391
2392
 
2392
2393
  // src/core/data-bus.ts
2393
2394
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -3447,7 +3448,6 @@ var CrossTabDataBus = class {
3447
3448
  }
3448
3449
  }
3449
3450
  );
3450
- void opening.catch(() => void 0);
3451
3451
  return opening;
3452
3452
  }
3453
3453
  /**
@@ -3590,4 +3590,4 @@ export {
3590
3590
  parseDataBusPublication,
3591
3591
  selectWorkerBackend
3592
3592
  };
3593
- //# sourceMappingURL=chunk-WFVM7HPE.js.map
3593
+ //# sourceMappingURL=chunk-RWUDSL24.js.map