cross-tab-worker-databus 0.21.9 → 0.21.10

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
  ## [Unreleased]
2
2
 
3
+ ## [0.21.10] - 2026-09-23
4
+
5
+ ### Fixed
6
+ - `heartbeatIntervalMs: Infinity` did the opposite of what it documented, in shared mode. It is accepted by `assertHeartbeatInterval`, appears in `docs/configuration.md`'s option table as disabling the PING heartbeat "entirely", and does stop the PINGs — pinned by `disables PING entirely when heartbeatIntervalMs is Infinity` — and the transport *advertises* the choice, because `buildInitMessage()` includes `heartbeatIntervalMs` whenever it differs from the default. `PortReaper.setTimeout()` then ran that value through the same `Number.isFinite` guard as `NaN`, `0` and negatives, fell back to the 10 s default, and so installed a **30 s session timeout on the one port guaranteed never to send another message**. The configuration whose stated purpose is "the reaper is not needed" therefore guaranteed a reap 30 s after connecting, with no heartbeat left to prevent the next one. Now `Infinity` means "never judged silent" while `NaN`/`0`/negative keep the fallback, because those three are rejected by the transport constructor and can only reach the Worker from a main thread that is not this library — `Infinity` is the one non-finite value the public API both accepts and acts on. One new test kills three mutants at three different assertions (`never reaps a port whose heartbeat was disabled with Infinity`): the shipped behaviour fails `exempt.port.closed` after ten simulated minutes; a variant where an exempt port clears the reaper's interval fails the *second* tab's `normal.port.closed`, which is the per-port half of the contract; and a variant where the exempt port simply leaves the reaper's tracking maps fails the follow-up `dispose()` assertion, which the pre-existing dispose tests cannot supply because they never pass `Infinity`. That third mutant is worth the detail, because before this test existed it appeared to be caught already — by `falls back to the default interval for non-positive or NaN heartbeat configs`, whose bad-value loop calls `+Infinity` and then `-Infinity` on the same port, so the last call silently undid the exemption. An ordering accident, not an assertion, so `+Infinity` has been taken out of that list with a note saying why. One further assertion in the existing PING test pins the wire half — `INIT` must carry the `Infinity` — because a reaper-side fix alone would leave the main thread unable to say *why* it disabled reaping, and measured, normalising non-finite values out of `INIT` fails it. The old malformed-INIT test's comment had been listing `Infinity` among the malformed payloads, which is how two green tests hid one defect: a test that asserts a fallback is not evidence about which inputs deserve one. Whole-tree coverage unchanged at 99.01 / 96.90 / 99.26 / 99.69 over **891** tests, and `port-reaper.ts` still reports three zero-count branch arms — the two `??` fallbacks in `reap()` and a `dispose()` guard — so the arm this fix adds is exercised.
7
+
8
+ ### Documentation
9
+ - Two sentences in the shipped `docs/configuration.md` and its Chinese counterpart said things the code does not do. **Shutdown cleanup** claimed that "when the SharedWorker shuts down, `PortReaper.dispose()` stops the timer and closes/stops every still-tracked session". `dispose()` has no Worker-side caller: `dispose` appears exactly once in `dist/centrifuge.shared.worker.js`, as the method definition, and no invocation of it exists anywhere in `dist/`. That is the same fact the reaper's own header records about the port — a `MessagePort` has no close event, so there is no "the SharedWorker is shutting down" signal for a hook to hang on — and the bullet now says so and names the two paths a port actually leaves tracking through. And the heartbeat and session-timeout bullets now state what `Infinity` does to the *reaper* rather than only to the PINGs, in both languages, together with the cost of choosing it: a tab configured that way and then crashed leaves its session and its WebSocket in place, because the one thing that would have noticed was switched off. The same sentence reached `docs/api.md` / `docs/zh/api.md`, whose option entries said only that `Infinity` "disables heartbeats". The **Failure isolation** bullet, written before 0.21.9's announcement, now covers `notify()` as well. No further behaviour change and no coverage movement.
10
+
11
+
12
+ ### Tests
13
+ - One test in the IndexedDB replay harness passed while breaking the gate. Its three loads fail on real 0/15/40 ms timers, and the third rejection was asserted *after* a healthy reopen round trip — so on a runner where that round trip takes longer than 40 ms, the promise rejects while nothing is attached to it, and `pnpm test:coverage` exits 1 with `Unhandled Rejection: Error: transaction aborted` alongside a report of **34 passed** and a checkmark on every test. Observed locally at 1-minute load average 86–157; the same file at lower load had been clean all along, which is what makes this runner-dependent rather than deterministic. The three assertions are now created next to the three calls and awaited where the ordering belongs, and the causal link was measured on the real file with the round trip forced to 200 ms: attached at the assertion it exits 1 with the unhandled block and 34/34 passing, attached up front the same run exits 0. Nothing in `src/` is implicated — the orphaned promise is one the test itself created and had not yet awaited — and `AGENTS.md` gained the rule, because "every checkmark green, the run red" reads like infrastructure until someone attaches the exit code to it.
14
+
3
15
  ## [0.21.9] - 2026-09-23
4
16
 
5
17
  ### Fixed
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-75ZDU7VG.js";
10
+ } from "./chunk-L5P66THO.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -5377,12 +5377,18 @@ var PortReaper = class {
5377
5377
  this.lastSeenAt.set(port, this.now());
5378
5378
  }
5379
5379
  /** Override a port's session timeout from its INIT heartbeat config.
5380
- * A non-finite or non-positive value falls back to the default so a bad
5381
- * payload cannot degenerate the reaper into a busy loop or silence it.
5382
- * No-op for untracked ports (e.g. setTimeout arrives after remove/STOP). */
5380
+ * `Infinity` is the documented way to disable the PING heartbeat, and it means
5381
+ * this port can never be judged silent: `startHeartbeat()` sends nothing at all
5382
+ * once configured that way, so a timeout would be a death sentence with no
5383
+ * possible appeal. A non-finite-or-non-positive *other* value falls back to the
5384
+ * default so a bad payload cannot degenerate the reaper into a busy loop or
5385
+ * silence it — those three (NaN, 0, negative) are rejected by
5386
+ * `assertHeartbeatInterval` at the transport constructor and so can only arrive
5387
+ * from a main thread that is not this library. No-op for untracked ports (e.g.
5388
+ * setTimeout arrives after remove/STOP). */
5383
5389
  setTimeout(port, heartbeatIntervalMs) {
5384
5390
  if (!this.targets.has(port)) return;
5385
- const safe = Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0 ? heartbeatIntervalMs : DEFAULT_HEARTBEAT_INTERVAL_MS;
5391
+ const safe = heartbeatIntervalMs === Infinity ? Infinity : Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0 ? heartbeatIntervalMs : DEFAULT_HEARTBEAT_INTERVAL_MS;
5386
5392
  this.sessionTimeoutMs.set(port, safe * DEFAULT_SESSION_TIMEOUT_MULTIPLIER);
5387
5393
  this.schedule();
5388
5394
  }