cross-tab-worker-databus 0.20.96 → 0.20.97

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.
@@ -2466,7 +2466,7 @@ var DedupManager = class {
2466
2466
  };
2467
2467
 
2468
2468
  // src/core/version.ts
2469
- var SDK_VERSION = true ? "0.20.96" : "";
2469
+ var SDK_VERSION = true ? "0.20.97" : "";
2470
2470
 
2471
2471
  // src/core/data-bus.ts
2472
2472
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  selectRebalanceTarget,
21
21
  selectWorkerBackend,
22
22
  topicMatchesPattern
23
- } from "./chunk-SSSJNWCH.js";
23
+ } from "./chunk-IVTDQ2KD.js";
24
24
  import {
25
25
  DEFAULT_STORAGE_PREFIX,
26
26
  PRUNE_STRATEGY,
@@ -550,6 +550,7 @@ These invariants are pinned by regression tests (see `tests/stability.test.ts` a
550
550
  - **Handoff channel close ordering.** `pause()` defers the physical `channel.close()` by one task. Closing synchronously would discard messages still queued for delivery — including the handoff's `ROUTE_RELEASED` — stranding the handoff target with an unconfirmed route.
551
551
  - **Unserved route drop.** A departing owner migrates only the routes that still have subscribers. When every remaining subscriber record of a route it owns has gone — a peer tab that died without releasing — the route is deleted instead of handed to a live peer, which would otherwise carry a transport subscription no Tab listens to. `pause()` runs no orphan-route cleanup (that lives in `reconcile`), so this is the only synchronous release of that record (pinned by regression).
552
552
  - **Last-subscriber release.** An owning Worker learns a topic is unserved from the last subscriber's `CONTROL/UNSUBSCRIBE`: it drops the assignment *and* dispatches the release to its transport. Consuming the message inside the handoff short-circuit alone would leave the server subscription — and its inbound fan-out — alive after every tab has left.
553
+ - **Assignment drift repair.** Every reconcile tick drops any topic this Worker holds in `assignedTopics` once the durable route no longer names it, releasing the transport subscription as it goes. Ownership thus follows the storage record rather than whichever message arrived last: a stale `CONTROL/SUBSCRIBE`, a lost route write, or a tab that claimed a route it does not own all converge back to exactly one owner within a heartbeat. `tests/coordination-invariants.test.ts` fuzzes three tabs through those corruptions and asserts the quiescent result (pinned by regression: emptying this sweep makes the fuzzed end state fail).
553
554
  - **Stranded-handoff recovery.** If the previous owner is gone and its `ROUTE_RELEASED` never arrives (dropped channel message under load, or a crash between the route write and the ACK), the reconcile loop re-elects a live owner once the unconfirmed handoff has been stuck longer than a worker TTL (10 s default): the route is rewritten with a fresh generation and the handoff marker cleared, so the normal confirmation path completes (pinned by regression). The age gate matters — a fresh unconfirmed route may simply be waiting out its confirmation flush — and while the previous owner is still alive the new owner keeps waiting, so the strict handoff keeps its no-overlap guarantee.
554
555
  - **Loss and recovery matrix.** Each coordination message has a bounded recovery path: a lost `CONTROL/SUBSCRIBE` is re-sent by the heartbeat reconcile for any route still lacking `confirmedAt`; a lost `REGISTRY` nudge costs at most one heartbeat interval (3 s default) because every tick reconciles anyway; a lost `ROUTE_RELEASED` is recovered by reconcile re-election once the previous owner is gone and the handoff has been stuck longer than a worker TTL (see the stranded-handoff invariant above, pinned by regression); publications dropped during a transport disconnect window are the one documented unrecoverable loss (transport contract). The storage-event fallback channel guarantees value-change delivery via a monotonic sequence in the envelope, and a dropped dispatch recovers through the same reconcile loop.
555
556
  - **Recovery diagnostics.** `getHealthSummary()` derives a single readiness verdict from the lifecycle flags (`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`); the unified `lastFailure` ledger and persistence counters reset on every explicit `start()`.
@@ -2,20 +2,20 @@
2
2
 
3
3
  # Browser Benchmark Trend
4
4
 
5
- > Data through 2026-09-21, from the 42 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
5
+ > Data through 2026-09-22, from the 44 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
6
6
 
7
7
  The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50`, which compares the newest report against the median of the same metric in the preceding reports (50% ceiling absorbs shared-runner noise; a single previous report was a fragile baseline because the in-page hot-path metrics alternate between a fast and a slow mode on identical code). This doc records the long-run picture: values are per-metric latencies where lower is better, and the last column is the best run inside the most recent 5 reports — not an all-time record, because the in-page matrix changed measurement semantics in early September 2026 and older reports are not comparable.
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | Metric | Previous (ms) | Latest (ms) | Δ | Best of last 5 runs (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 38.6243 | 46.6942 | +8.07 | 38.6243 |
13
- | publish per-message (ms, lower is better) — shared | 33.7123 | 44.7673 | +11.05 | 33.7123 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 5.8 | 6.7 | +0.90 | 5.4 |
15
- | publishBatch ×1000 (ms, lower is better) | 3.7 | 4.6 | +0.90 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 24.3 | 15.3 | -9.00 | 10.6 |
17
- | trace + publish ×1000 (ms, lower is better) | 6.6 | 7.1 | +0.50 | 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 | 50.6466 | 51.8687 | +1.22 | 38.6243 |
13
+ | publish per-message (ms, lower is better) — shared | 34.0188 | 33.7528 | -0.27 | 33.7123 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 7.2 | 8.1 | +0.90 | 5.8 |
15
+ | publishBatch ×1000 (ms, lower is better) | 4.7 | 4.7 | +0.00 | 3.5 |
16
+ | dedup ×1000 (ms, lower is better) | 14.8 | 16.1 | +1.30 | 10.6 |
17
+ | trace + publish ×1000 (ms, lower is better) | 6.3 | 5.7 | -0.60 | 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
  Notes:
@@ -150,6 +150,8 @@ Open `http://localhost:4173/examples/demo/` and open it in multiple browser Tabs
150
150
  - Data flow animations, event stream, distribution latency metrics, and cluster Worker routing status
151
151
  - SDK capability, transport configuration, active/standby Worker, and visible/hidden Tab state
152
152
 
153
+ Two more pages are served next to the demo. `http://localhost:4173/examples/react/` wires the framework-agnostic core by hand (effect, `subscribe`, status), while `http://localhost:4173/examples/vue/` uses nothing but the shipped `cross-tab-worker-databus/vue` composables. Both point at the same local demo endpoint, so a publication from one is received by a tab on any of the three pages that uses the same Topic — which is also how the adapter pages are covered by the browser suite (`e2e/adapters.spec.ts`).
154
+
153
155
  When consuming the repository directly through a Git dependency, use a pinned commit. The repository ships `dist` so consumers do not need to build the SDK during installation.
154
156
 
155
157
  ## 9. Explicit Start
package/docs/roadmap.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # Roadmap
2
2
 
3
- 0.20.96 was released on September 22, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
3
+ 0.20.97 was released on September 22, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.20.97 delivered scope
6
+
7
+ - No public surface and no observable runtime behavior changed. The shipped `src/` delta is `centrifuge.ts` only: two unreachable `typeof Worker` / `typeof SharedWorker` throws removed, and comments recording why one guard survives while another does not. Everything else is example, test, CI and documentation work.
8
+ - The Vue adapter finally runs where it ships: `examples/vue/` mounts the real `cross-tab-worker-databus/vue` composables against the local demo endpoint from the installed `vue` package (no CDN), and `e2e/adapters.spec.ts` drives it in real Chromium tabs — publish/receive across two tabs, a reactive topic change releasing the old channel server-side, and delivery surviving the owning tab's close. Before this the Vue entry had never executed outside jsdom.
9
+ - The cluster's promise *between* tabs is now fuzzed rather than hand-ordered: three buses over one storage registry and BroadcastChannel through randomized subscribe/unsubscribe/publish/hide/show/stop/start/heartbeat/dropped-frame/forged-`SUBSCRIBE` interleavings, asserting at quiescence one owner and one transport holder per live topic, no residue for a departed topic, and exactly-once fan-out. Five mutations kill those arms; three guards survive because `reconcileAssignedTopics()` repairs them, which is recorded as the reason those belong to frame-level regressions.
10
+ - Two release-gate timing failures were traced to their causes instead of retried. The coordination fuzzer's `Date.now()` budget was reading a clock the suite fakes — a leaked fake `Date` in a reused worker made the same file stop at its floor after 16.4s and burn 539s on CI — so `tests/setup.ts` now restores real timers after every test and the fuzzers budget on `performance.now()`. The hot-path performance gates moved to their own sequential step, because an absolute-millisecond ceiling only measures code on an unscheduled core.
11
+ - The credential-guard investigation ended with the guard kept: a PR proposing its deletion as unreachable was shown to be reachable through application code (`getToken()` may `stop()` the transport before throwing), so it gained a mutation-proved regression test, and the rule distinguishing that from a genuinely dominated branch is now written into `AGENTS.md` and the source.
12
+ - The CommonJS-only default-Worker failure is pinned from the built artifacts: `dist/cjs` cannot resolve a Worker URL through esbuild's `import.meta` shim, so `start()` must report the actionable "provide workerFactory explicitly" rather than `TypeError: Invalid URL` — asserted on both factories, with the ESM artifact checked in the same case to tie the failure to the module format.
13
+ - Whole-suite coverage moved from 98.62 / 96.02 / 98.54 / 99.38 to 98.68 / 96.17 / 98.54 / 99.46 without lowering any ceiling; `docs/transports.md` (en + zh) documents that a publication carrying its own string `topic` is re-addressed to it, which is how wildcard delivery names a concrete topic and why `{ "topic": … }` payloads can go nowhere.
4
14
 
5
15
  ## 0.20.96 delivered scope
6
16
 
7
17
  - Opened the project's first deprecation cycle: an empty topic (`""`) now warns once per bus in `subscribe()`, `publish()` and `publishBatch()` instead of silently registering a channel no transport can address. Behaviour is unchanged; a future minor rejects it at that boundary, following the pre-1.0 policy. The contract is documented in the API reference and in the upgrading guide, in both languages.
8
- - Closed the coverage-leg ledger with proof instead of assumption: seven behaviors that could not fail their tests are now mutation-verified — the departing-owner handoff that deletes an unserved route rather than migrating it onto a live peer, the owner releasing its transport subscription when the last remote subscriber leaves, a cancelled durable-retention sweep staying silent, a superseded hydration snapshot being dropped instead of merged, the default platform `WebSocket` construction including subprotocols, `ready()` surfacing the recorded transport error after the recovery budget is spent, and the Vue adapter containing a rejected `ready()`.
18
+ - Closed the coverage-leg ledger with proof instead of assumption: eleven behaviors that could not fail their tests are now mutation-verified — the departing-owner handoff that deletes an unserved route rather than migrating it onto a live peer, the owner releasing its transport subscription when the last remote subscriber leaves, a cancelled durable-retention sweep staying silent, a superseded hydration snapshot being dropped instead of merged, the default platform `WebSocket` construction including subprotocols, `ready()` surfacing the recorded transport error after the recovery budget is spent, the Vue adapter containing a rejected `ready()`, a `connectTimeoutMs` of `0` or `Infinity` genuinely waiting forever, a private topic leaving no route record when its only holder hides, the trace reporter containing a throwing sink on a runtime with no `console.warn`, and an unrelated owned wildcard pattern failing to capture a batch bound for a remote topic.
9
19
  - Every remaining zero-count branch in `src` is now classified as dominated, unreachable by construction, or a missing boundary check (which is what produced the deprecation above). `vue.ts` and `hooks.ts` reach 100% on all four metrics and `cluster.ts` is down to a single uncovered line.
10
20
  - `AGENTS.md` records the traps that made early drafts decorative: a `ChannelHub`-sharing peer heals the state under observation, a peer started without a microtask flush is invisible, and textually duplicated guards let a non-global mutation edit the wrong copy.
11
21
  - `typescript-eslint` refreshed to `8.70.1`; TypeScript stays at `6.0.3` while the lint toolchain's peer range excludes 7.x.
@@ -149,6 +149,7 @@ Wire protocol (JSON text frames):
149
149
 
150
150
  - client → server: `{"op":"subscribe"|"unsubscribe"|"publish","topic":"...","data":...,"messageId"?:"...","timestamp"?:123}`
151
151
  - server → client: the canonical publication is `{"op":"publication","publication":{"topic":"...","data":...,"messageId"?:"...","timestamp"?:123}}`. The legacy flat `{"topic":"...","data":...}` frame remains accepted. Frames without a string topic are ignored; malformed JSON is reported through `handlers.onError` without throwing.
152
+ - A publication that carries its own string `topic` is addressed by **that** value rather than by the channel it arrived on — that is how a server delivering through a wildcard channel (`chat.*`) names the concrete topic. A payload whose top level happens to contain a `topic` string is therefore re-addressed, and dropped when no tab owns the resulting topic. On Centrifuge the channel normally arrives out of band, which is why the rule needs stating here: it is the one transport where "the topic was already in the frame" is not otherwise visible.
152
153
 
153
154
  When `data` is an `ArrayBuffer`, publish uses a binary frame with a small
154
155
  header (`0xc7`, UTF-8 topic length, topic, payload). Servers may echo the same
@@ -521,6 +521,7 @@ Transport 消息 → isAssigned(topic)? → 是 → broadcastEvent(EVENT)
521
521
  - **交接通道关闭顺序。** `pause()` 将物理 `channel.close()` 推迟一个任务。同步关闭会丢弃仍在排队等待投递的消息(包括交接的 `ROUTE_RELEASED`),使交接目标持有未确认路由。
522
522
  - **无人订阅路由的删除。** 离开的 owner 只交接仍有订阅者的路由。若它拥有的某条路由,其剩余订阅记录已全部消失(某个 peer Tab 未释放就死亡),则该路由被删除而不是交给仍然存活的 peer——否则那个 peer 会背负一个没有任何 Tab 监听的 transport 订阅。`pause()` 不执行孤儿路由清理(该逻辑在 `reconcile` 中),因此这是对该记录唯一的同步释放(由回归测试固定)。
523
523
  - **最后一个订阅者的释放。** owner Worker 通过最后一个订阅者发来的 `CONTROL/UNSUBSCRIBE` 得知某 Topic 已无人订阅:它会同时丢弃本地 ownership 并把释放动作派发给自己的 transport。若只在交接短路里消费该消息,server 订阅(以及它的入站扇出)会在所有 Tab 离开后依然存活。
524
+ - **归属漂移的修复。** 每一轮 reconcile 都会丢弃那些 `assignedTopics` 里仍持有、但持久路由已不再指向本 Worker 的 Topic,并顺带释放对应的 transport 订阅。于是 ownership 跟随存储记录,而不是最后到达的那条消息:陈旧的 `CONTROL/SUBSCRIBE`、丢失的路由写入、或某个抢到了不属于自己的路由的 Tab,都会在一个 heartbeat 内收敛回“恰好一个 owner”。`tests/coordination-invariants.test.ts` 会针对三个 Tab 随机制造这些破坏并断言静默后的结果(由回归测试固定:把这一轮清扫掏空,模糊测试的终态就会失败)。
524
525
  - **悬挂交接恢复。** 若前任 owner 已消失而其 `ROUTE_RELEASED` 始终未到达(高负载下通道消息丢失,或 route 写入与 ACK 发送之间崩溃),reconcile 循环会在该未确认交接悬挂超过一个 worker TTL(默认 10 秒)后重新选举存活 owner:路由以全新 generation 重写并清除交接标记,使常规确认路径得以完成(已有回归固化)。年龄门限很关键——刚写入的未确认路由可能只是在等确认落盘,不能误判为悬挂;而只要前任 owner 仍然存活,新 owner 会继续等待,因此严格交接的无重叠保证不受影响。
525
526
  - **丢失与恢复矩阵。** 每类协调消息都有有界恢复路径:丢失的 `CONTROL/SUBSCRIBE` 由心跳 reconcile 对未确认路由重发;丢失的 `REGISTRY` 通知最多损失一个心跳间隔(默认 3 秒),因为每次 tick 都会 reconcile;丢失的 `ROUTE_RELEASED` 由 reconcile 在前任 owner 消失且交接悬挂超过一个 worker TTL 后重新选举恢复(见上文悬挂交接不变量,已有回归固化);transport 断连窗口内被丢弃的 publication 是唯一文档化的不可恢复丢失(transport 契约)。storage-event 降级通道通过信封内的单调序列号保证变值投递,丢失的派发由同一 reconcile 循环恢复。
526
527
  - **恢复诊断。** `getHealthSummary()` 从生命周期标志推导单一就绪判定(`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`);统一的 `lastFailure` 账本与持久化计数在每次显式 `start()` 后重置。
@@ -2,20 +2,20 @@
2
2
 
3
3
  # 浏览器基准趋势
4
4
 
5
- > 数据截至 2026-09-21,基于 42 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
5
+ > 数据截至 2026-09-22,基于 44 份归档的 `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 的噪声;此前只看"上一份报告"并不稳定,因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳)。本文记录长期趋势:数值为逐指标延迟,越低越好;最后一列是最近 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.6243 | 46.6942 | +8.07 | 38.6243 |
13
- | publish per-message (ms, lower is better) — shared | 33.7123 | 44.7673 | +11.05 | 33.7123 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 5.8 | 6.7 | +0.90 | 5.4 |
15
- | publishBatch ×1000 (ms, lower is better) | 3.7 | 4.6 | +0.90 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 24.3 | 15.3 | -9.00 | 10.6 |
17
- | trace + publish ×1000 (ms, lower is better) | 6.6 | 7.1 | +0.50 | 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 | 50.6466 | 51.8687 | +1.22 | 38.6243 |
13
+ | publish per-message (ms, lower is better) — shared | 34.0188 | 33.7528 | -0.27 | 33.7123 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 7.2 | 8.1 | +0.90 | 5.8 |
15
+ | publishBatch ×1000 (ms, lower is better) | 4.7 | 4.7 | +0.00 | 3.5 |
16
+ | dedup ×1000 (ms, lower is better) | 14.8 | 16.1 | +1.30 | 10.6 |
17
+ | trace + publish ×1000 (ms, lower is better) | 6.3 | 5.7 | -0.60 | 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
  说明:
@@ -150,6 +150,8 @@ pnpm examples
150
150
  - 数据流动画、事件流、分发延迟指标和集群 Worker 路由状态
151
151
  - SDK 能力、transport 配置、活跃/等待 Worker 与可见/隐藏 Tab 状态
152
152
 
153
+ 演示页之外还有两个页面:`http://localhost:4173/examples/react/` 手写接入与框架无关的核心 API(effect、`subscribe`、状态),`http://localhost:4173/examples/vue/` 则只使用已发布的 `cross-tab-worker-databus/vue` 组合式 API。二者都连到同一个本地 demo 端点,因此其中一个页面发布的数据会被三个页面里任何使用同一 Topic 的标签页收到——适配器页面也正是这样被浏览器测试覆盖的(`e2e/adapters.spec.ts`)。
154
+
153
155
  通过 Git 依赖直接接入仓库时,应固定到具体 commit。仓库随代码提供 `dist`,消费方安装时无需构建 SDK。
154
156
 
155
157
  ## 9. 显式启动
@@ -1,11 +1,21 @@
1
1
  # 路线图
2
2
 
3
- 0.20.96 已于 2026 年 9 月 22 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
3
+ 0.20.97 已于 2026 年 9 月 22 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
4
+
5
+ ## 0.20.97 已完成范围
6
+
7
+ - 公开接口与可观察的运行时行为均未改变。发布的 `src/` 变更只涉及 `centrifuge.ts`:删除两处不可达的 `typeof Worker` / `typeof SharedWorker` 抛错,并以注释记录为何一个守卫被保留、另一个被移除。其余全部是示例、测试、CI 与文档工作。
8
+ - Vue 适配层终于运行在它真正交付的环境里:`examples/vue/` 用本地安装的 `vue` 包(不依赖 CDN)挂载真实的 `cross-tab-worker-databus/vue` 组合式 API 并连上本地 demo 端点,`e2e/adapters.spec.ts` 在真实 Chromium 标签页中驱动它——两个标签互收消息、响应式切换 topic 后服务端释放旧 channel、owner 标签关闭后仍能送达。在此之前 Vue 入口从未在 jsdom 之外执行过。
9
+ - 跨 tab 之间的核心契约由手工排列变成随机模糊:三个 bus 共享一套存储注册表与 BroadcastChannel,随机交织订阅/退订/发布/隐藏/显示/停止/启动/心跳/丢弃帧/伪造 `SUBSCRIBE`,在静默态断言每个存活 topic 恰有一个 owner 与恰一个 transport 持有者、已离开的 topic 不留残留、一次发布对每个存活订阅者恰好扇出一次。五个变异体能杀死这些断言;三个守卫存活,因为 `reconcileAssignedTopics()` 会修复它们——这一点被记录下来,也正是它们属于帧级回归的原因。
10
+ - 两个发布门禁的计时失败被追到根因而非重试了事。协调模糊测试的 `Date.now()` 预算读的是本套件自己伪造的时钟——复用 worker 里残留的假 `Date` 让同一个文件一次在 16.4 秒停在下限、另一次在 CI 上烧掉 539 秒——因此 `tests/setup.ts` 现在在每个测试后恢复真实计时器,模糊测试改用 `performance.now()` 计预算。热路径性能门禁移入独立的顺序步骤,因为绝对毫秒上限只有在未被调度的核上才度量代码。
11
+ - 一次凭证守卫的调查结果恰是"守卫留下":一个以不可达为由删除它的 PR 被证明是可达的——入口代码(`getToken()`)可以在抛错前先 `stop()` 传输——于是它获得了经变异验证的回归用例,而区分"可达"与"确实被上层支配"的规则写进了 `AGENTS.md` 与源码。
12
+ - 仅 CommonJS 才有的默认 Worker 失败由构建产物钉住:`dist/cjs` 无法经 esbuild 的 `import.meta` 垫片解析 Worker URL,因此 `start()` 必须给出可操作的 "provide workerFactory explicitly" 而非 `TypeError: Invalid URL`;两个工厂函数都被断言,且同一用例同时检查 ESM 产物,从而把失败绑定到模块格式而不是文件名。
13
+ - 全量覆盖率从 98.62 / 96.02 / 98.54 / 99.38 提升到 98.68 / 96.17 / 98.54 / 99.46,且未下调任何阈值;`docs/transports.md`(中英)记录了"自带字符串 `topic` 的发布会被重新寻址到该 topic"这一规则——它既是通配符投递指明具体 topic 的方式,也是 `{ "topic": … }` 载荷可能无声丢失的原因。
4
14
 
5
15
  ## 0.20.96 已完成范围
6
16
 
7
17
  - 开启项目第一个弃用周期:空 topic(`""`)现在会在 `subscribe()`、`publish()`、`publishBatch()` 中每个 bus 实例告警一次,而不再静默登记一个没有任何 transport 能寻址的 channel。行为保持不变;按 pre-1.0 策略,后续小版本会在该边界直接拒绝。契约已写入 API 参考与升级指南(中英双语)。
8
- - 用证明取代假设,清空覆盖率分支台账:七个"测试根本不可能失败"的行为获得经变异验证的回归用例——离开方 owner 删除无人订阅路由而非交接给存活 peer、最后一个远端订阅者离开时 owner 释放自身 transport 订阅、被取消的持久化清扫保持静默、被取代的水合快照被丢弃而非合并、默认平台 `WebSocket` 构造(含子协议)、恢复预算耗尽后 `ready()` 交出真实传输错误,以及 Vue 适配层吞掉被拒绝的 `ready()`。
18
+ - 用证明取代假设,清空覆盖率分支台账:十一个"测试根本不可能失败"的行为获得经变异验证的回归用例——离开方 owner 删除无人订阅路由而非交接给存活 peer、最后一个远端订阅者离开时 owner 释放自身 transport 订阅、被取消的持久化清扫保持静默、被取代的水合快照被丢弃而非合并、默认平台 `WebSocket` 构造(含子协议)、恢复预算耗尽后 `ready()` 交出真实传输错误、Vue 适配层吞掉被拒绝的 `ready()`、把 `connectTimeoutMs` 设为 0 或 Infinity 时确实无限等待、私有 topic 在其唯一持有者隐藏时不留路由记录、在没有 `console.warn` 的运行时吞掉抛错的 trace sink,以及无关的已持有通配符 pattern 无法截获发往远端 topic 的批量消息。
9
19
  - `src` 中其余零计数分支已全部归类:被支配、按构造不可达,或属于缺失的边界校验(正是它引出了上面的弃用项)。`vue.ts` 与 `hooks.ts` 四项指标均达 100%,`cluster.ts` 只剩一行未覆盖。
10
20
  - `AGENTS.md` 记下让早期草稿变成装饰性测试的陷阱:共享 `ChannelHub` 的 peer 会自愈被观测的状态、缺少一次 microtask flush 的 peer 对其他 Tab 不可见、文本重复的守卫会让非全局替换改到错的那一份。
11
21
  - `typescript-eslint` 升级到 `8.70.1`;在 lint 工具链的 peer 范围仍排除 7.x 期间,TypeScript 保持 `6.0.3`。
@@ -135,6 +135,7 @@ const bus = createWebSocketDataBus({
135
135
 
136
136
  - client → server:`{"op":"subscribe"|"unsubscribe"|"publish","topic":"...","data":...,"messageId"?:"...","timestamp"?:123}`
137
137
  - server → client:标准 publication 为 `{"op":"publication","publication":{"topic":"...","data":...,"messageId"?:"...","timestamp"?:123}}`;旧的扁平 `{"topic":"...","data":...}` 帧仍然兼容。没有字符串 topic 的帧会被忽略;非法 JSON 通过 `handlers.onError` 上报而不会抛出。
138
+ - 自带字符串 `topic` 的 publication 会按**该值**寻址,而不是按它到达的 channel——这正是 server 通过通配 channel(`chat.*`)投递时指明具体 topic 的方式。因此顶层恰好含 `topic` 字段的负载会被重新寻址,若集群中没有任何 Tab 拥有重定向后的 topic,就会被丢弃。Centrifuge 的 channel 通常由客户端库在带外给出,这也是必须在此说明该规则的原因:只有在这条链路上,"topic 本来就写在帧里"这件事才可见。
138
139
 
139
140
  当 `data` 是 `ArrayBuffer` 时,publish 使用二进制帧:帧头为 `0xc7`,随后是
140
141
  UTF-8 topic 长度、topic 和 payload。服务器可以原样回显该帧;其他 payload 仍走
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.20.96",
3
+ "version": "0.20.97",
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",
@@ -89,10 +89,11 @@
89
89
  ],
90
90
  "scripts": {
91
91
  "build": "node scripts/build.mjs",
92
- "check": "pnpm typecheck && pnpm build && pnpm test",
92
+ "check": "pnpm typecheck && pnpm build && pnpm test && pnpm test:perf",
93
93
  "examples": "node scripts/serve-examples.mjs",
94
94
  "lint": "eslint .",
95
95
  "test": "vitest run",
96
+ "test:perf": "DATABUS_PERF_GATES=1 vitest run tests/perf-gate.test.ts",
96
97
  "test:watch": "vitest",
97
98
  "test:coverage": "vitest run --coverage",
98
99
  "bench": "vitest bench --run",