cross-tab-worker-databus 0.21.30 → 0.21.32

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,54 @@
1
+ ## [0.21.32] - 2026-09-25
2
+
3
+ A delivery bug, found while looking for something else. A topic's **first** publication from a tab that held a matching wildcard was dispatched locally and never forwarded, even while a live durable route named a different owner for it — and delivery is at-most-once, so that publication was gone for good. The fix is a deletion: the memo that made the decision decided nothing.
4
+
5
+ ### Fixed
6
+
7
+ - **Routing: a topic's first `publish()` / `publishBatch()` now reaches that topic's concrete owner.** Both methods carried a textually identical fast path: while `wildcardPublishCache` held no entry for a topic, it scanned the local wildcard patterns and, on a match, addressed the control frame to *this* worker without consulting the durable route — while every later publication for the same topic did consult it. Measured with one runtime holding `chat.*` and a peer concretely owning `chat.room.1` (both `isAssigned` true, the route record naming the peer): three publications from the wildcard holder landed `1,0,0` on the sender and `0,1,1` on the owner. The first never left the tab. The same shape reproduced for a two-item batch, whose frame the receiver unpacks per item.
8
+ - **This is `0.20.58` from the other side, which is why an eviction rule would not have fixed it.** There, a cached `null` short-circuited the owner lookup and the fix was to keep consulting the route; here a cache *miss* did the same thing. The stored value decided nothing either way — `pattern` and `null` were both read only as "have I scanned this topic" — so the memo is removed rather than bounded, and every non-owned publication now resolves its target through `resolvePublishTarget()`. Wildcard ownership is still honoured: with no live concrete route that call answers with this worker, and the receiving ownership gate matches the pattern. Removing the map also retires the unbounded growth documented in `0.21.31`; that growth was a side effect of this fast path, not a separate defect, so the eviction rule it was framed as needing is no longer needed at all.
9
+
10
+ - **Repository tooling: a scratch directory inside the linted tree made `pnpm lint` red without naming any product code.** A mutation-probe harness was written to `.gate-logs/` — in-tree on purpose, since `/tmp` had evicted a failing log mid-session and `pnpm test:coverage` deletes `coverage/`, where logs had also been written — and `eslint .` reported 9 errors from it (`no-undef` on `console`/`process`, `no-control-regex`) as a lint failure of the release tree. `.gate-logs/` is now ignored by both git and eslint, with the reason recorded beside the entry in `.gitignore`. `tests/regression.test.ts`'s "keeps scripts/ covered by ESLint" still passes, so the release tooling stays linted and only scratch is excluded.
11
+
12
+ ### Tests
13
+
14
+ - **Two regressions, one per entry point, each asserted one publication at a time** so a regression names *which* publication was lost instead of reporting a count short by one: `forwards a topic's FIRST publish to its concrete remote owner, not to a matching local wildcard` and `forwards a batch's FIRST publish to the concrete remote owner as well`. Measured both ways — without the fix they fail at `first publish must reach the concrete owner: expected +0 to be 1` and `expected [] to deeply equal [1, 2]`; with it the file is green. Three pre-existing cases that named the removed cache in their titles were retitled to what they actually assert, and the single-runtime pair among them is now labelled as the characterization it is: a lone runtime always wins its own election, so it cannot distinguish the two paths.
15
+ - **`trace.ts`'s flush-activity guard was re-measured on the tree that shipped this fix**, not carried forward: deleting `received` alone reddens 3 tests, `dispatched` 2, `dedupAccepted` 1, `dedupSuppressed` 1 — each operand's own kill set, all four restorations byte-identical.
16
+
17
+ ### Documentation
18
+
19
+ - **`trace.ts`: each kill now names the file it comes from.** The note recorded which test dies to each operand deletion but not where it lives, and the two `dispatched` kills sit in two different files (`tests/data-bus.test.ts` and `tests/trace.test.ts`).
20
+ - **The publication-lag samples were extended, and one of them had been undercounted.** `0.21.31`'s tag run is the first to exercise the raised 720 s published-consumer ceiling: green on attempt 1 at 308 s, with its registry record 127.2 s after the publish ack and the remaining ~181 s spent against the cache lifetime — so the 310 s maximum the ceiling is sized from still stands, and the ceiling does not need raising again. The other half of that sentence was wrong in the cheap direction: the checklist and `AGENTS.md` credited the 304–305 s regularity to "three consecutive green tag runs", where the five runs from 0.21.25 to 0.21.29 took 307, 304, 305, 304 and 304 s, six samples in a 5-second band once 0.21.31's 308 s is counted. The lag list, the run-by-run durations and the `gh api` lookup that re-derives both are now written out in `docs/release-checklist.md` (en + zh), the workflow's own comment, and `tests/workflows.test.ts`.
21
+ - **`docs/architecture.md` (en + zh): the wildcard publish memo is removed, not capped.** Both copies had described it as the per-topic map the documented `knownTopics` cap does not reach, with an open question about which of its two value shapes could be forgotten; neither half survives, so the paragraph now says what replaced it and records the 1,200-versus-500 measurement beside the delivery counts that made the mechanism visible.
22
+ - **`publish()`'s surviving comment described the deleted mechanism.** Above the `assignedTopics.has(topicKey)` early return it still said wildcard holders "use the same fast path" — which that lookup never did: `assignedTopics` is keyed by the plaintext it was handed, so a holder of `chat.*` carries the *pattern's* key and a concrete topic misses the `has()`, exactly the fall-through the fix relies on. The sentence now names which lookup answers for which assignment, checked against `isAssigned()`'s own `topicMatchesPattern` loop and against `publishBatch()`, which carried no such claim.
23
+ - **The upstream-blocked toolchain item was re-checked, and both channels still exclude TypeScript 7.** `typescript-eslint`'s latest (8.70.1) and its canary (8.70.2-alpha.7) both declare `typescript >=4.8.4 <6.1.0`, so TS 7.0.2 would break the lint gate before touching our own types. The roadmap entry now says how to look that up without being fooled: read `dist-tags.canary`, because sorting version strings puts `8.9.1-alpha.9` last — `9` sorts after `70` — while the line actually worth watching is 8.70.x. The English entry's date had also drifted from the Chinese one (2026-09-23 against 2026-09-22) for the same check; both now carry one date and the same two versions.
24
+
25
+ ### Compatibility
26
+
27
+ One behavioural change: a first publication for a wildcard-covered topic that has a live concrete owner is now forwarded to that owner instead of being dispatched locally. No export, option, default, frame or storage key changed; `wildcardPublishCache` was a private field and is absent from the emitted `dist/core/cluster.d.ts`. `verify:compat`, `verify:types` and `verify:pack` run against `v0.21.31`.
28
+
29
+ ## [0.21.31] - 2026-09-25
30
+
31
+ The `0.21.30` tag run failed its blocking published-consumer gate after a *successful* publish. Diagnosing that gave the retry budget a measured size, retired a floor in the gate whose job was to keep that failure out, and retracted a note this repository wrote about the same failure less than an hour before it was corrected. No library code changed.
32
+
33
+ ### Fixed
34
+
35
+ - **Release tooling: the published-consumer retry ceiling sat below the propagation lag it exists to absorb.** `PUBLISHED_VERIFY_ATTEMPTS=48` × 7500 ms is 360 s, and the 0.21.30 tag run consumed all of it on `ETARGET` while that version's tarball was already fetchable; the re-run against the unchanged tag then passed in 1 s. Two measured terms now size the number. (1) `npm publish`'s success ack precedes the registry's own `time[<version>]` record — by 74.8 s on 0.21.29, 96.8 s on 0.21.28, 248.7 s on 0.21.27 and 310.0 s on 0.21.30. (2) The packument that `npm pack` resolves against is served `cache-control: public, max-age=300`, so a copy of it anywhere in the path may be a full cache lifetime stale by design; that term is also what the three preceding green tag runs were reporting when they consumed 305 s, 304 s and 304 s of the ceiling in turn. The budget is now 96 × 7500 ms = 720 s, clearing the 310 + 300 = 610 s the two add to. Which copy served this run's stale views is **not** established: a purely npm-local-cache account predicts the retry should have waited for the same 300 s object and it did not, so the comment records the open question instead of naming a cache.
36
+ - **`tests/workflows.test.ts` pinned a floor that the value which just failed satisfied.** The assertion was `totalMs >= 5 * 60 * 1000`, so the 360 s budget that had just turned a good release red passed the gate whose entire purpose is to stop that happening, and nothing would have objected to lowering it again. The floor is now the sum above, written as two named constants beside the rule that re-derives each. Measured against it: setting the workflow's attempts from 96 back to 48 fails exactly that test, and both the 360 s budget that failed here and the 120 s one 0.20.89 exhausted are rejected by it.
37
+
38
+ ### Documentation
39
+
40
+ - **`cluster.ts`: the one per-topic map the documented `knownTopics` cap does not cover now says so.** `wildcardPublishCache` — concrete topic → the local wildcard pattern that matched it, or `null` for "scanned, nothing matched" — has no cap, and is cleared only by the two lifecycle teardowns whose `wildcardPublishCache.clear()` sits beside each `routeOwnerCache.clear()`. Measured: 1,200 `publish()` calls on distinct topic names leave `knownTopics` at its 500-entry cap while this map holds 1,200. The note records why a number is not the fix — evicting a *pattern* re-runs the first local dispatch the entry exists to prevent, while evicting a `null` re-scans and, if a wildcard has been assigned in between, gives that publication the fan-out of a first one — so bounding the map needs a rule about which of the two halves may be forgotten. `docs/architecture.md` and its Chinese counterpart gained the same statement in the paragraph after "Cap and eviction", which is where a reader looking for the memory bound would look.
41
+ - **`trace.ts`: the metrics guard's kill sets are named by test instead of counted.** Its flush-activity note carried "(three cases for `received`, two for `dispatched`)". Those multiplicities are exactly the kind that go false on an unrelated commit, and the five verbatim test names now stand in their place — including the fact the count hid, that `dispatched`'s two cases live in two different files.
42
+ - **`docs/release-checklist.md` (en + zh): how to adjudicate a publish, plus one retraction.** The note written during the same release claimed the package document endpoint and the tarball URL had disagreed "in the same minute". Its own timestamps say otherwise: the stale-document read was 21:43:15Z and the registry's record of that version is 21:44:12.987Z, so the read was not lagging an artifact that already existed — it was correctly reporting a publish the registry had not finished writing. The corrected text gives the sequence with its clock readings, states that absence from `versions`/`latest` minutes after a green publish step is the *expected* state of a successful release, names the genuine disagreement (a HEAD and a pack from one machine good by 21:44:35Z while the runner was still on `ETARGET` at 21:45:07Z), and tells the reader to probe from the machine that is failing. The "twelve times the manual default / ~6-minute ceiling / expect ~39 retries of 48" budget prose is replaced by the derivation above, with the two lookups needed to re-measure it.
43
+
44
+ ### Tests
45
+
46
+ - **`tests/version-compat.test.ts` got its own per-test ceiling, because the global one was load-dependent for that file alone.** Every case there builds a throwaway git repository and runs `scripts/verify-version-compat.mjs` as a child process — `git init`, `git add`, `git commit`, `git tag` and one `node` spawn per test — so a case costs five process starts plus temp-tree I/O, not the object comparison it asserts on. Measured on an 8-core host at load average 400+: under `pnpm test:coverage` with nothing else of ours running, one case died with `Error: Test timed out in 15000ms` while 940 of 941 passed; the same file alone with a competing `pnpm bench` on the machine lost two cases and reported 3–19 s for the survivors (that reading is confounded by the concurrency and is the slowest of the three); and the same file alone with the contention gone passed all 18 in 23.7 s total, about 1.3 s per case. Which case crossed the line moved between runs, which is the signature of scheduling rather than of a hang. The ceiling is therefore raised for this file only, leaving every in-process test on the global 15 s. Proven wired rather than assumed: setting the value to `1` makes 17 of the 18 cases fail with `Test timed out in 1ms` and the run exit 1 — a control that could not fail would say nothing about whether the call binds.
47
+
48
+ ### Compatibility
49
+
50
+ No library code, export, frame, option, default or storage key changed; `verify:compat`, `verify:types` and `verify:pack` pass against `v0.21.30`. The behavioural changes are confined to `.github/workflows/release.yml`, `tests/workflows.test.ts` and `tests/version-compat.test.ts`, none of which ships inside the package; the workflow change takes effect on the next tag push.
51
+
1
52
  ## [0.21.30] - 2026-09-25
2
53
 
3
54
  The claim queue's remaining sites, worked one measurement at a time. Two shipped absolutes turned out to be false, four counts were made re-derivable where they had only been asserted, and one release gate was found able to pass while verifying the wrong artifact. No library code changed.
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-ESJJV7F4.js";
10
+ } from "./chunk-PNEOQX4O.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -672,7 +672,6 @@ var WorkerClusterRuntime = class {
672
672
  this.routeOwnerCache.delete(oldest);
673
673
  }
674
674
  }
675
- wildcardPublishCache = /* @__PURE__ */ new Map();
676
675
  // Reverse mapping: opaque topicKey → plaintext topic. A bounded cache with
677
676
  // FIFO eviction — NOT authoritative. It can hold a topicKey that is also in
678
677
  // assignedTopics (the owned guard prevents evicting those), because it is
@@ -749,7 +748,6 @@ var WorkerClusterRuntime = class {
749
748
  this.subscribedTopics.clear();
750
749
  this.assignedTopics.clear();
751
750
  this.routeOwnerCache.clear();
752
- this.wildcardPublishCache.clear();
753
751
  this.knownTopics.clear();
754
752
  this.suspended = false;
755
753
  }
@@ -817,7 +815,6 @@ var WorkerClusterRuntime = class {
817
815
  this.handoffAssignedTopics();
818
816
  this.assignedTopics.clear();
819
817
  this.routeOwnerCache.clear();
820
- this.wildcardPublishCache.clear();
821
818
  this.removeStorage(this.workerStorageKey(this.workerId));
822
819
  this.flushStorage();
823
820
  this.notifyRegistry();
@@ -924,16 +921,6 @@ var WorkerClusterRuntime = class {
924
921
  if (this.assignedTopics.has(topicKey)) {
925
922
  return this.sendControl(this.workerId, CONTROL_ACTION.PUBLISH, topic, topicKey, data, metadata);
926
923
  }
927
- const cachedPattern = this.wildcardPublishCache.get(topic);
928
- if (cachedPattern === void 0) {
929
- for (const pattern of this.assignedTopics.values()) {
930
- if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
931
- this.wildcardPublishCache.set(topic, pattern);
932
- return this.sendControl(this.workerId, CONTROL_ACTION.PUBLISH, topic, topicKey, data, metadata);
933
- }
934
- }
935
- this.wildcardPublishCache.set(topic, null);
936
- }
937
924
  return this.sendControl(this.resolvePublishTarget(topic, topicKey), CONTROL_ACTION.PUBLISH, topic, topicKey, data, metadata);
938
925
  }
939
926
  /**
@@ -958,17 +945,6 @@ var WorkerClusterRuntime = class {
958
945
  this.dispatchLocalPublishBatch(topic, topicKey, items);
959
946
  return true;
960
947
  }
961
- const cachedPattern = this.wildcardPublishCache.get(topic);
962
- if (cachedPattern === void 0) {
963
- for (const pattern of this.assignedTopics.values()) {
964
- if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
965
- this.wildcardPublishCache.set(topic, pattern);
966
- this.dispatchLocalPublishBatch(topic, topicKey, items);
967
- return true;
968
- }
969
- }
970
- this.wildcardPublishCache.set(topic, null);
971
- }
972
948
  const target = this.resolvePublishTarget(topic, topicKey);
973
949
  if (target === this.workerId) {
974
950
  this.dispatchLocalPublishBatch(topic, topicKey, items);
@@ -2499,7 +2475,7 @@ var SDK_VERSION = (
2499
2475
  // declaration's type is `string | undefined` and the export's is `string`
2500
2476
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2501
2477
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2502
- true ? "0.21.30" : ""
2478
+ true ? "0.21.32" : ""
2503
2479
  );
2504
2480
 
2505
2481
  // src/core/data-bus.ts
@@ -3739,4 +3715,4 @@ export {
3739
3715
  parseDataBusPublication,
3740
3716
  selectWorkerBackend
3741
3717
  };
3742
- //# sourceMappingURL=chunk-ESJJV7F4.js.map
3718
+ //# sourceMappingURL=chunk-PNEOQX4O.js.map