cross-tab-worker-databus 0.21.5 → 0.21.6

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,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.21.6] - 2026-09-22
4
+
5
+ ### Fixed
6
+ - A batched `PUBLISH` control frame is now dropped unless its `items` is a real, non-empty array. `handleControlMessage` gated the batch on `message.items && message.items.length > 0` and then either iterated it or handed it straight to the batch handler, and `publishBatch()` is the only producer — it builds `items` with `Array.prototype.map`. A hand-built frame could therefore carry a `length` without an iterator (`{ length: 2 }`), which threw a `TypeError: message.items is not iterable` out of the cluster's own `BroadcastChannel` listener, or an iterable non-array (`"ab"`), which reached the transport as two publications whose `data` was `undefined` — under the receiving worker's own session, on a channel the sender does not own. Presence and emptiness are checked in one place because a frame with `items` carries no `data`: previously an empty batch satisfied neither half of the old test and fell through to the single-publication tail, publishing `undefined` once instead of nothing. Legitimate traffic is untouched, including a cross-worker `publish(topic, undefined)`, whose frame simply has no `items` key. Item *content* stays unvalidated by decision, recorded in `AGENTS.md`: `{ data: anything }` is what a well-formed item looks like, so per-item shape checks stop nothing a conforming frame cannot do, and `messageId`/`timestamp` are only forwarded into `transport.publish()` where the receiving side already discards a non-string id and a non-finite timestamp. Pinned by two forged-frame tests that each fail three different ways against the old gate, and separately against a guard that keeps only the array check or only the empty check, so both halves are shown to be load-bearing on different legs.
7
+
8
+ - A `ROUTE_RELEASED` ACK can no longer complete a handoff under a channel name the recipient does not own. This is the second instance of the 0.21.5 substitution, in a handler the 0.21.5 guard never covered: the pairing check landed in `handleControlMessage`, while `handleRouteReleasedMessage` reads both `topicKey` and `topic` too — it authorizes itself against the durable route stored under the key, then writes the frame's plaintext into `assignedTopics` and subscribes the transport to it. Route records are ordinary localStorage, so a same-origin script that knows the cluster key can read the real `topicKey`, the previous owner's id and the generation, satisfy all three staleness checks, and answer an in-flight handoff with its own channel name; measured before the fix, the recipient's `onControl` fired twice for a topic nobody had subscribed. The ACK senders derive the pair (one of them reads it back out of `assignedTopics`, which only this hole could poison), so conforming peers are unaffected. Pinned by a forged-ACK test that fails without the guard, alongside the existing generation and previous-owner staleness pins that already covered this handler.
9
+
10
+ ### Coverage
11
+ - The `recoveryExhausted` one-shot is now pinned, which closes the last arm on the `data-bus.ts` ledger that a mutation had been shown to survive. `caps automatic recovery attempts…` already asserted "exactly one `exhausted` diagnostic" a second time after the cap was spent, and that second assertion had never re-entered the branch: once the last reopen has failed, the handlers installed on the transport belong to a superseded lifecycle, so `isCurrentLifecycle()` drops a subsequent `setStatus('error')` before the attempt counter is reached — measured with a spy, `updateStatus` ran zero times for such a call. Deleting the guard therefore passed the whole suite. The test now drives a second failure through the path an application actually takes after exhaustion — an explicit retry, which reopens and installs a live closure — and while that reopen fails, `recoveryExhausted` is still set because the only reset on the path sits in the success arm. With the guard made unconditional the test now fails as `expected […] to have a length of 1 but got 2`. `data-bus.ts` uncovered branch arms 13 → 12 and its branch coverage 97.01% → 97.24%, both measured on this module before and after; on the rebased tree whole-suite coverage is 98.98 / 96.69 / 99.26 / 99.69 over 879 tests. No source change.
12
+
3
13
  ## [0.21.5] - 2026-09-22
4
14
 
5
15
  ### Changed
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-RWUDSL24.js";
10
+ } from "./chunk-LXAHMO7L.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -1188,7 +1188,8 @@ var WorkerClusterRuntime = class {
1188
1188
  if (this.releaseHandoffOnUnsubscribe(message)) return;
1189
1189
  break;
1190
1190
  case CONTROL_ACTION.PUBLISH:
1191
- if (message.items && message.items.length > 0) {
1191
+ if (message.items !== void 0) {
1192
+ if (!Array.isArray(message.items) || message.items.length === 0) return;
1192
1193
  if (this.handlers.onPublishBatch) {
1193
1194
  this.handlers.onPublishBatch(message.topic, message.items);
1194
1195
  return;
@@ -1249,6 +1250,7 @@ var WorkerClusterRuntime = class {
1249
1250
  */
1250
1251
  handleRouteReleasedMessage(message) {
1251
1252
  if (message.targetWorkerId !== this.workerId) return;
1253
+ if (createOpaqueKey(message.topic) !== message.topicKey) return;
1252
1254
  const route = this.readRoute(message.topicKey);
1253
1255
  if (!route || this.isStaleRouteRelease(route, message)) return;
1254
1256
  this.assignedTopics.set(message.topicKey, message.topic);
@@ -2388,7 +2390,7 @@ var DedupManager = class {
2388
2390
  };
2389
2391
 
2390
2392
  // src/core/version.ts
2391
- var SDK_VERSION = true ? "0.21.5" : "";
2393
+ var SDK_VERSION = true ? "0.21.6" : "";
2392
2394
 
2393
2395
  // src/core/data-bus.ts
2394
2396
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -3590,4 +3592,4 @@ export {
3590
3592
  parseDataBusPublication,
3591
3593
  selectWorkerBackend
3592
3594
  };
3593
- //# sourceMappingURL=chunk-RWUDSL24.js.map
3595
+ //# sourceMappingURL=chunk-LXAHMO7L.js.map