cross-tab-worker-databus 0.21.20 → 0.21.22

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/docs/roadmap.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Roadmap
2
2
 
3
- 0.21.20 was released on September 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
3
+ 0.21.22 was released on September 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.21.22 delivered scope
6
+
7
+ - **One method, applied to every multi-term guard the previous scan ranked**: delete a single operand, run the whole suite, and classify which of three answers that mutant gave — pinned, incapable of deciding, or live and unnamed. Nine behaviors are now named that no test had named (three socket-staleness terms in `websocket.ts`, two of `trace.ts`'s four dedup/activity counters, two operands of the `storage-event` envelope, two option legs of `DedupManager.start()`'s sweep gate), and the suite went 923 → 929.
8
+ - **The ledger did not move, and that is the result.** `src/` holds the same **47** zero-count branch arms at the end of six phases as at the start, with only the denominator changing (1962 → 1958, from two dominated operands being deleted). Every operand probed is evaluated on every call, so no coverage total distinguishes "runs" from "decides" — the class is invisible to the arm ledger and visible only to a whole-suite mutant.
9
+ - **A joint assertion was the recurring cause, not an absent test.** `trace.ts`'s activity check looked pinned because one existing case raised *both* dedup counters in the same window, so whichever operand survived still emitted; the storage envelope's `seq` and `message` tests were both tripped by its single malformed write. Splitting the inputs, not adding coverage, is what made each leg die on its own.
10
+ - **Five operands closed as incapable of deciding anything, by differential.** Two in the envelope and all three `typeof value !== 'number'` tests in `validation.ts` were proven rather than argued: a fixed input vector (23 stored payloads; 23 values × 3 validators × 4 variants) produced byte-identical accept/reject-and-message results for the guard as written and for each mutant. The kept `typeof` leg then had its own justification tested both ways — construct the coercing-global swap it exists to catch, and observe that it kills nothing while the leg is present and turns `heartbeatIntervalMs: '3000'` into an accepted option once the leg is gone.
11
+ - **Two dominated operands deleted, and a clean bill plus a thin pin recorded instead of averaged away.** `getMetrics()` and `flush()` each re-read a field the same expression had just computed, which cannot decide anything and was silently absorbing the mutation that would have proved the getter's contract; deleted, with both kill sets in the comment. `ReplayManager.start()`'s four-term gate turned out fully pinned all along — the parity check between two same-shaped managers, which is exactly where `DedupManager`'s gap was. And five of `validation.ts`'s six surviving-leg deaths come from several cases while the sixth comes from one, so the site says so instead of reporting "six of nine".
12
+
13
+ ## 0.21.21 delivered scope
14
+
15
+ - **One more branch arm closed by a call no test had made** — and this one needed a caller that does not exist in a browser. `queueStartAfterStop()`'s `stopPromise ?? Promise.resolve()` had never taken its fallback, which is the state a *failed initial open* lives in (its teardown is owned by `pendingStop`, so no gate exists); the only synchronous application-code seam inside that window is the caller's own `ClusterEnvironment` port, so the case wraps the port and calls back into `start()` from a storage write. `src/` goes from 48 zero arms to 47.
16
+ - **Which half of that expression has teeth.** Not the leg the test was written for. The fallback operand is consequence-free — `start()` chains the reopen behind `pendingStop` independently, so the measured `start` → `stop` → `start` order survives deleting the read entirely. What the case does pin is the routing one line above it: `if (this.stopping)` → `if (this.stopPromise)` reddens it, and 192 of the file's other tests stay green doing it.
17
+ - **A candidate test written, measured, and deleted.** A second case queued a restart behind a transport whose `stop()` was held open, which is the obvious pin for the gate operand — and it passed under all four single-leg mutations tried. A bounded number of awaited microtasks cannot see a wait whose wakeup needs the timer queue, so the test was removed and its zero kill rate recorded at the site, which is the finding; the number 194/194 under deletion is the statement, not a gap waiting for a better test.
18
+ - **The guard whose deletion is a crash rather than a failed assertion.** Removing that read turns the restart's "wake, discover we are still stopping, re-queue" into a microtask busy-wait that starves the macrotask the transport stop needs: `tests/lifecycle-invariants.test.ts` aborts its worker ~40 s later inside `Builtins_RunMicrotasks` with an out-of-memory, and every test in `data-bus` still passes. The site comment now says the fuzz is that line's only witness, which is also a warning to anyone who trims its stop/restart interleavings for speed.
19
+ - **Three shipped claims retracted against the code, in the two ways claims go stale.** A count that had drifted (the `?? this.currentRecord` fallbacks are three, not "the identical pair", and the third carried no site text at all), a measurement that no longer reproduces (the reopen absorb's "fails exactly one test" is 194/194, 194/194, 51-of-194 and 194/194 across the four experiments now recorded in its place), and eleven line-number citations into our own files of which six pointed at the wrong place — two of them into another comment. Every position is now a symbol or a condition, the surviving numbers are the dependency's (`centrifuge@5.7.4`, re-verified to its last `emit('error')`), and the whole prose change is proven code-free by comparing esbuild's emitted JavaScript against the previous revision.
4
20
 
5
21
  ## 0.21.20 delivered scope
6
22
 
@@ -2,20 +2,20 @@
2
2
 
3
3
  # 浏览器基准趋势
4
4
 
5
- > 数据截至 2026-09-23,基于 87 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
5
+ > 数据截至 2026-09-24,基于 91 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
6
6
 
7
7
  发布门禁的对比基线是 `pnpm bench:compare --fail-above-pct 50`:将最新报告与其之前若干报告中同一指标的中位数比较,*并且*要求新值超过这些样本中的最大值(50% 上限用于吸收共享 runner 的噪声;此前只看"上一份报告"并不稳定,因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳;而在两档交替的样本上,中位数只有在这两档混合分布时才稳定,因此单看百分比仍可能纯粹因为档位切换而失败)。被第二个条件放过的指标会显式列在 “within-baseline, not gated” 里,而不是悄悄算作通过。本文记录长期趋势:数值为逐指标延迟,越低越好;最后一列是最近 5 份报告内的最优值,而不是历史纪录——页内基准矩阵在 2026 年 9 月初变更过测量语义,更早的报告不可比。
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | 指标 | 上次 (ms) | 本次 (ms) | Δ | 近 5 次最优 (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 38.4709 | 36.5875 | -1.88 | 36.5875 |
13
- | publish per-message (ms, lower is better) — shared | 33.9845 | 33.7984 | -0.19 | 33.7984 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 5.5 | 5.7 | +0.20 | 5.5 |
15
- | publishBatch ×1000 (ms, lower is better) | 3.5 | 3.6 | +0.10 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 29.2 | 11 | -18.20 | 11 |
17
- | trace + publish ×1000 (ms, lower is better) | 7 | 4.6 | -2.40 | 4.6 |
18
- | first-packet cold dispatch (ms, lower is better) | 0.1 | 0 | -0.10 | 0 |
12
+ | publish per-message (ms, lower is better) — dedicated | 40.1172 | 41.3311 | +1.21 | 36.5875 |
13
+ | publish per-message (ms, lower is better) — shared | 33.9868 | 34.1115 | +0.12 | 33.7984 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 6.5 | 6.7 | +0.20 | 5.7 |
15
+ | publishBatch ×1000 (ms, lower is better) | 4.6 | 4.4 | -0.20 | 3.6 |
16
+ | dedup ×1000 (ms, lower is better) | 15.6 | 19 | +3.40 | 11 |
17
+ | trace + publish ×1000 (ms, lower is better) | 4.6 | 6.6 | +2.00 | 4.6 |
18
+ | first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
19
19
  <!-- BENCH-TREND:END -->
20
20
 
21
21
  说明:
@@ -1,6 +1,22 @@
1
1
  # 路线图
2
2
 
3
- 0.21.20 已于 2026 年 9 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
3
+ 0.21.22 已于 2026 年 9 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
4
+
5
+ ## 0.21.22 已完成范围
6
+
7
+ - **一套方法,用上一轮扫描排出的每一个多操作数守卫**:只删一个操作数,跑整个测试套件,然后判断这个变异给出了三种答案里的哪一种——被钉住了、根本没有裁决能力、活着却没有任何测试命名它。于是九处从未被测试命名的行为有了名字(`websocket.ts` 里三个判断连接是否过期的项、`trace.ts` 四个活跃/去重计数器中的两个、`storage-event` 信封的两个操作数、`DedupManager.start()` 清扫闸门的两条选项腿),套件从 923 增至 929。
8
+ - **账本一次都没有动,而这正是结论。** 六个阶段结束时 `src/` 仍然是 **47** 条零计数分支,与开始时相同,变的只有分母(1962 → 1958,来自删掉的两条被支配操作数)。被探测的每一个操作数都在每次调用里求值,所以没有任何覆盖率总量能区分“执行了”和“裁决了”——这一类问题对分支账本是隐形的,只有整套件变异能看见。
9
+ - **反复出现的原因是联合断言,不是缺测试。** `trace.ts` 的活跃性检查看上去早已被钉住,因为那个既有用例在同一个窗口里把*两个*去重计数器都抬了起来,于是留下哪一条腿都照样产出;信封的 `seq` 与 `message` 两条测试也同时被它唯一那条畸形写入触发。让每条腿各自致死的,是把输入拆开,而不是补覆盖率。
10
+ - **五条操作数被判定为“无法裁决任何东西”,而且是差分证明的。** 信封里的两条,加上 `validation.ts` 三条 `typeof value !== 'number'`,全部以固定的输入向量证明而非推论(23 个存储载荷;23 个值 × 3 个校验器 × 4 个变体),原写法与每个变异体得到的“接受/拒绝及其消息”结果逐字节相同。被保留的那条 `typeof` 腿随后也被两头检验了自己的保留理由——把它存在的意义就是防住的那个“换成会强制转换的全局函数”的改写构造出来,结果带着这条腿时该改写杀不死任何东西,而把腿也删掉后 `heartbeatIntervalMs: '3000'` 就成了被接受的选项。
11
+ - **删掉两条被支配操作数,另外记下一次的满分和一次的薄。** `getMetrics()` 与 `flush()` 各自把同一个表达式刚算过的字段又读了一遍,这种读取不可能裁决任何东西,却正在悄悄吞掉那个本可以证明 getter 契约的变异;已删除,两组击杀集都写在注释里。`ReplayManager.start()` 的四项闸门则被证实每一条腿早已被钉住——这是两个同形管理器之间的对等检查,也正是 `DedupManager` 缺口的所在。而 `validation.ts` 六条致死里有五条来自多个用例、第六条只来自一个,所以代码位置上如实写明,而不是汇成一句“九之六”。
12
+
13
+ ## 0.21.21 已完成范围
14
+
15
+ - **又一条未覆盖分支被“从来没有测试发出过的调用”关闭了**——而这一次需要的调用方在浏览器里并不存在。`queueStartAfterStop()` 的 `stopPromise ?? Promise.resolve()` 从未走过它的兜底分支,而那正是*首次打开失败*时所处的状态(它的清理由 `pendingStop` 掌管,所以根本不存在那道闩);那个窗口内唯一会同步回调进应用代码的缝隙,是调用方自己提供的 `ClusterEnvironment` 端口,于是用例把端口包了一层,从一次存储写入里回调进 `start()`。`src/` 的零计数分支从 48 条降到 47 条。
16
+ - **那个表达式里真正有后果的是哪一半。** 不是这条测试当初为它而写的那一半。兜底操作数是没有后果的——`start()` 自己就会把重开挂在 `pendingStop` 之后,所以把整个读取删掉,实测的 `start` → `stop` → `start` 顺序依然成立。这条用例真正钉住的是它上面一行的路由:把 `if (this.stopping)` 改成 `if (this.stopPromise)` 会让它变红,而文件里其余 192 个测试对此毫无反应。
17
+ - **一个候选测试被写出、被测量、然后被删除。** 第二个用例把重开排在一个 `stop()` 被挂住的传输后面,看起来正是那个闩的显然钉法——可它在四种单腿变异下全部通过。有限次数的 await 微任务看不见一个必须等定时器队列的等待,于是这个测试被删掉,而它的零击杀率写在了代码位置上;“删掉它 194/194 依旧全绿”这个数字本身就是结论,而不是一个等着更好测试来填的空洞。
18
+ - **有一条守卫,删除它的表现是崩溃,不是断言失败。** 把那个读取删掉,重开的“醒来—发现还在 stopping—重新排队”就变成一个微任务忙等,饿死传输停止所依赖的那个宏任务:`tests/lifecycle-invariants.test.ts` 在约 40 秒后于 `Builtins_RunMicrotasks` 内因内存耗尽中止自己的 worker,而 `data-bus` 里的每个测试仍然全绿。位置注释现在写明:那条 fuzz 就是这一行的唯一见证——这也是对任何为了提速而删掉它 stop/restart 交错序列的人的警告。
19
+ - **三处随包发布的声明被按代码撤回,正好对应声明腐坏的两种方式。** 一个计数漂了(`?? this.currentRecord` 兜底有三处,不是“那相同的一对”,而且第三处压根没有任何位置说明);一个测量不再复现(重开兜底那句“只失败一个测试”,被现在写在它原地的四个实验取代:194/194、194/194、51/194、194/194);以及十一处指向本仓库自己文件的 `:NNN` 行号引用里有六处指向了别处——其中两处落进了*另一段注释*。所有位置引用现在都改成符号或条件;保留下来的编号是依赖的(`centrifuge@5.7.4`,连它最后一个 `emit('error')` 都重新核对过);而整场纯文字改动是靠比对 esbuild 产出的 JavaScript 与上一个 revision 来证明没有碰到代码的。
4
20
 
5
21
  ## 0.21.20 已完成范围
6
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.21.20",
3
+ "version": "0.21.22",
4
4
  "description": "Framework-agnostic cross-tab data bus with Dedicated/Shared Worker clustering and Centrifuge support.",
5
5
  "type": "module",
6
6
  "license": "MIT",