cross-tab-worker-databus 0.21.14 → 0.21.15
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 +17 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-3XBN7FFJ.js → chunk-VQJIMGJN.js} +2 -2
- package/dist/cjs/centrifuge.cjs +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/docs/release-checklist.md +1 -1
- package/docs/roadmap.md +34 -1
- package/docs/zh/release-checklist.md +1 -1
- package/docs/zh/roadmap.md +29 -1
- package/package.json +1 -1
- /package/dist/{chunk-3XBN7FFJ.js.map → chunk-VQJIMGJN.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.21.15] - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Tests
|
|
6
|
+
|
|
7
|
+
- `handleControlMessage`'s addressee check is now pinned. `handleMessage` dispatches on `type` alone and the cluster channel is broadcast, so that first line is the only thing keeping a runtime from acting on a CONTROL frame addressed to someone else — and nothing asserted it. Measured by deleting it: 369 tests across `cluster`/`stability`/`data-bus`/`centrifuge`/`worker-mode` stayed green while the three-tab coordination fuzz churned 16+ minutes of CPU against a ~1-minute baseline. The fuzz reaches the leg constantly (every point-to-point frame is a non-target for the other tabs) and cannot assert it, because its invariants are end-state checks and a slower convergence still passes.
|
|
8
|
+
- The pin asserts the non-target's own behavior — no `onControl` dispatch, no assignment, and no `knownTopics` entry for a plaintext it was never meant to serve — plus the addressee's, which is what shows the frame was deliverable rather than malformed. That distinction was earned: the first version died against the mutant too, but at the *addressee's* assertion, which pinned something other than the guard. Why the mutant also suppresses the addressee's claim is not yet explained, and is recorded as an open question rather than as a passing test.
|
|
9
|
+
- `handleRouteReleasedMessage`'s addressee check is now pinned on the one input where nothing else can reject it. Its staleness test compares the frame against the *durable route*, so a non-target that does not own the topic is already turned away by that — which is why the existing forged-`topicKey` case never reached this line. The pin's frame is addressed to a third worker while the route genuinely names this runtime, with a matching `generation` and `handoffFromWorkerId`, so every staleness term passes and only `targetWorkerId` stands. Measured by deleting it: 358 tests across `cluster`/`stability`/`data-bus`/`centrifuge` stayed green, and the failure was attributed to the new case alone. `tests/coordination-invariants.test.ts` cannot reach it at all — `forgeSubscribe()` is the only frame that harness sends, so no seed ever posts a `ROUTE_RELEASED` to the wrong worker.
|
|
10
|
+
- One assertion inside that pin was wrong before it shipped and is recorded so it is not re-introduced: `isAssigned()` reads true from the route record the scenario itself writes, because `isAssigned` falls through to `readRoute(topicKey)?.workerId === this.workerId`. It asserted nothing about the frame. The `assignedTopics` map is what the handler actually writes, and that is what the test now checks.
|
|
11
|
+
- The coordination fuzz gained a third limit, because the two it had were both made of time and the failure that has been turning `verify` red is not a waiting failure. `tests/coordination-invariants.test.ts` now arms a **channel delivery budget** on `ChannelHub`: past 50,000 posts in one seed the hub stops delivering, so a coordination loop that never converges is cut at the one place both halves of it are visible. The await cap reported `0 cut short` while one seed spent 463s of a CI run (and 151s in an earlier one) because that time is CPU spent inside microtasks — nothing on the same thread can preempt it, so no deadline can bound it. The threshold is taken from a measured distribution rather than taste: instrumenting `ChannelHub.send` across a full 5,000-seed sweep on an idle desktop gave p50 = 28 posts, p99 = 90, p99.9 = 5,429, maximum 16,763, with 18 seeds over 500 — so 50,000 is ~3x the worst interleaving this machine produces and cannot fire on a healthy seed. A cut seed prints `[CHURN] seed=N deliveries=… ops=…`, is excluded from the depth floor (its end state was never reached, so asserting on it would be a false failure), and the sweep summary now prints the churn count even when the sweep completed — which is the case in which the number is otherwise invisible.
|
|
12
|
+
- The budget's own teeth are pinned in both directions, like the await cap: a hub must deliver everything under the limit and nothing over it. Measured against two mutants — dropping the `return` (never cut) fails at "posts past the budget must not be delivered: expected 6 to be 3", and comparing with `>=` (cut one post early) fails at "a budget must not drop anything under the limit: expected 2 to be 3". A guard that can never fire would let the wedge through looking like a working limit.
|
|
13
|
+
- `docs/progress.md`'s phase log has a structural gate. Phase entries are appended at one shared anchor, and the two PRs for the same phase (#233 closed in favor of #234) landed as **two `## Phase 142` blocks** — one of them pushed below the `## Next candidates` heading the log is meant to sit above — so the record of a single change was duplicated and out of order, and read that way until a `grep -c "^## Phase 142"` returned 2. `tests/documentation.test.ts` now requires every `## Phase N / …` heading to number strictly above the previous entry and to appear above that heading. The scope is the current convention on purpose: 30 phase numbers repeat in the older part of the log (numbers 2–51, where an entry pairs a `## Phase N (in progress …)` heading with a `## Phase N result …` line), and none of those headings uses the `## Phase N / Title` form the gate covers — that form begins at Phase 84 and has never been duplicated. Measured on two scratch trees, one per leg: a second `## Phase 142` inserted above the anchor fails the numbering assertion alone, and a correctly numbered `## Phase 145` inserted below `## Next candidates` fails the placement assertion alone.
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
- Both release checklists now record that `pnpm bench:compare` can be closed by the act of investigating it. Its baseline — the median *and* the maximum leg — is drawn from the same rolling archive every `pnpm bench:browser` run appends to, so re-runs taken to "check the spread" become the baseline that excuses the failure. Measured while preparing 0.21.14: two runs at host load 7.7–15.1 failed the ceiling on `publish/dedicated` (77.3 ms against a ~38 ms fast mode), five further re-runs reported OK with the baseline median slid to 70.6 ms, and one clean run after removing those samples showed every metric up together — the contention signature, not a regression. The instruction is now: read `uptime` before re-running, record a suspect failure as deferred with its load, keep off-mode samples out of the archive, and do not regenerate `docs/benchmarks.md` from them.
|
|
18
|
+
- **The churn loop is the harness, not the library — and the earlier diagnosis of it is retracted.** The bullet above described the loop the budget cuts as "a real behavior and not fixed", and named the open question as *which read answers stale between the re-election's `writeRoute` and the next pass*. Neither the framing nor the implied mechanism survives measurement. Replaying seed 1046 with counters at the decision points in the two handlers and the route writer gave: **11,542 `handleControlMessage` accepts, 11,541 `reconcileSubscriptions` re-sends, 11,538 `confirmRoute` skips, 6 `confirmRoute` writes**, and at the storage boundary **27,571 reads of route keys against 6 writes that reached the store** — so there is no re-election loop at all (the re-send leg writes nothing, and the durable record was rewritten a handful of times across the whole seed). What actually happens is a *visibility* disagreement of one step: the route owner confirms the assignment, its confirmation sits in its own `BatchingStorageWriter.pending`, and a peer reading the flushed record still sees `confirmedAt === undefined`, so it re-sends; the owner's `confirmRoute` then returns early because *its own* read sees the confirmation. The one event that ends that cycle is the microtask flush — and the harness never yields to one, because `ChannelHub.send()` delivers every frame **synchronously**, so a whole `reconcile → post → reconcile` cascade runs inside a single call stack. Measured directly: deferring delivery by one `queueMicrotask` in the hub takes the sweep from ~18 seeds over 500 posts (in 5,000) to **zero seeds over 500 in 1,050**, with no `[CHURN]` and no budget trip. A browser cannot produce this loop: `BroadcastChannel` delivers in a later task, so the flush always lands before the peer reacts, and the re-send is bounded by one heartbeat. The delivery budget therefore stays exactly where it belongs — it is immunity against a wedge the *harness* can create — and the number it reports is no longer a goal for a product fix, because there is none to make. What the experiment did open, and did not answer: with asynchronous delivery the same sweep fails six invariant checks — seeds 100, 727, 1129, 1487 and 1756 report `no subscriber left but owners=[] transportSubscriptions=[a]`, and 1853 reports `1 subscriber(s) need one owner that holds the transport, got owners=[b] transportSubscriptions=[a b]` — and every one of them is a seed that opens with an injected `a:forge:<topic>` frame. So raising the harness's fidelity is its own change, not a one-line edit (making *every* hub asynchronous breaks ~16 tests in `cluster`/`data-bus`/`stability` that post a frame and assert without awaiting, so the deferral has to be opt-in and scoped to the fuzz), and whether a route-less `CONTROL/SUBSCRIBE` accepted under the documented missing-route tolerance can leave a transport subscription held after the assignment sweeps it is a product question this run surfaced but did not settle.
|
|
19
|
+
|
|
3
20
|
|
|
4
21
|
## [0.21.14] - 2026-09-24
|
|
5
22
|
|
package/dist/centrifuge.js
CHANGED
|
@@ -2410,7 +2410,7 @@ var DedupManager = class {
|
|
|
2410
2410
|
};
|
|
2411
2411
|
|
|
2412
2412
|
// src/core/version.ts
|
|
2413
|
-
var SDK_VERSION = true ? "0.21.
|
|
2413
|
+
var SDK_VERSION = true ? "0.21.15" : "";
|
|
2414
2414
|
|
|
2415
2415
|
// src/core/data-bus.ts
|
|
2416
2416
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -3626,4 +3626,4 @@ export {
|
|
|
3626
3626
|
parseDataBusPublication,
|
|
3627
3627
|
selectWorkerBackend
|
|
3628
3628
|
};
|
|
3629
|
-
//# sourceMappingURL=chunk-
|
|
3629
|
+
//# sourceMappingURL=chunk-VQJIMGJN.js.map
|
package/dist/cjs/centrifuge.cjs
CHANGED
package/dist/cjs/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ Only `pnpm bench:browser` / `pnpm bench:compare` / `pnpm bench:trend` stay local
|
|
|
20
20
|
1. Update `package.json`, `CHANGELOG.md`, and both roadmap files.
|
|
21
21
|
2. Run `pnpm check`, `pnpm lint`, `pnpm test:coverage`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, `pnpm verify:compat`, `pnpm verify:types`, and `git diff --check`. (`verify:compat` asserts the package `exports` subpaths and type fields present in the `COMPAT_BASE_TAG` baseline still exist; `verify:pack` steam-imports the full root public surface plus every subpath in ESM and CJS from the packed tarball; `verify:types` freezes every public entry's declared export names against the same baseline and asserts no public signature names a type that no entry exports — a type-only export emits no runtime key, so neither the `Object.keys()` freeze in `tests/dual-format.test.ts` nor `verify:compat` can see one change. `verify:compat` and `verify:types` resolve their baseline from the most recent release tag, so a shallow or tag-less clone must first run `git fetch --tags` or they fail with "no version tag found".)
|
|
22
22
|
3. Gate dependency security: `pnpm audit --registry=https://registry.npmjs.org` (the configured mirror registry lacks the audit endpoint; CI runs it on the public registry in the verify job). Fail the release on any known-vulnerability advisory; `pnpm-workspace.yaml` overrides pin patched ranges.
|
|
23
|
-
4. Gate browser benchmark regressions: run `pnpm bench:browser` (twice at minimum; the gate compares the newest report against the median of the same metric in up to the five preceding archived reports, **and additionally requires the new value to exceed the highest of those samples**) and then `pnpm bench:compare --fail-above-pct 50`. The median baseline exists because the in-page hot-path metrics alternate between a fast and a slow mode on identical code — two consecutive runs of `dedup1000Ms` measured 12.7 ms then 25.6 ms, and a single noisy previous report could fail the documented ceiling with no code change at all. The second leg exists because a median over an alternating sample is only stable while the modes stay mixed: when several consecutive reports land in the same mode the median itself jumps, which is how one comment-only tree read +121% against one baseline and −56.7% against another minutes apart. A metric excused by that leg is printed under `within-baseline, not gated` rather than passing silently — measured on the current archive, `publish/dedicated/perMessageMs`, `publish/shared/perMessageMs` and `databus/publishBatch1000Ms` were all inside their own five-report range, so treat that list as the metric's real resolution, not as a clean bill. `publish/dedicated/perMessageMs` is noisy the same way: two consecutive runs of identical code measured 49.9 ms then 71.1 ms, failing the gate at +75.7% while `dedup1000Ms` and `traceAndPublish1000Ms` *improved* in the same report — that single-metric-up-everything-else-down shape is the noise signature, and the next run reported 49.4 ms and passed. Before accepting or acting on a failure, re-run and check the spread; if the change touches a hot path, size it directly (a micro-benchmark of the 0.20.95 bounded-map rewrites measured 80–86 ns/op across all three variants, i.e. inside run-to-run variance). A baseline shift (e.g. a metric becoming real instead of a no-op) is an expected one-time failure. Refresh the long-run trend doc with `pnpm bench:trend` and commit it when the tables change.
|
|
23
|
+
4. Gate browser benchmark regressions: run `pnpm bench:browser` (twice at minimum; the gate compares the newest report against the median of the same metric in up to the five preceding archived reports, **and additionally requires the new value to exceed the highest of those samples**) and then `pnpm bench:compare --fail-above-pct 50`. The median baseline exists because the in-page hot-path metrics alternate between a fast and a slow mode on identical code — two consecutive runs of `dedup1000Ms` measured 12.7 ms then 25.6 ms, and a single noisy previous report could fail the documented ceiling with no code change at all. The second leg exists because a median over an alternating sample is only stable while the modes stay mixed: when several consecutive reports land in the same mode the median itself jumps, which is how one comment-only tree read +121% against one baseline and −56.7% against another minutes apart. A metric excused by that leg is printed under `within-baseline, not gated` rather than passing silently — measured on the current archive, `publish/dedicated/perMessageMs`, `publish/shared/perMessageMs` and `databus/publishBatch1000Ms` were all inside their own five-report range, so treat that list as the metric's real resolution, not as a clean bill. `publish/dedicated/perMessageMs` is noisy the same way: two consecutive runs of identical code measured 49.9 ms then 71.1 ms, failing the gate at +75.7% while `dedup1000Ms` and `traceAndPublish1000Ms` *improved* in the same report — that single-metric-up-everything-else-down shape is the noise signature, and the next run reported 49.4 ms and passed. Before accepting or acting on a failure, re-run and check the spread; if the change touches a hot path, size it directly (a micro-benchmark of the 0.20.95 bounded-map rewrites measured 80–86 ns/op across all three variants, i.e. inside run-to-run variance). A baseline shift (e.g. a metric becoming real instead of a no-op) is an expected one-time failure. Refresh the long-run trend doc with `pnpm bench:trend` and commit it when the tables change. **Re-running a failure can manufacture its own excuse, and that was measured.** `bench:compare`'s baseline — both the median and the maximum leg — is drawn from the most recent archived reports, and every `bench:browser` run appends to that same archive, so the samples you add while investigating a failure become the baseline that judges the next attempt. On 2026-09-24, at host load averages 7.7–15.1 with a zero non-comment `src/` diff in the tree, the first two runs measured `publish/dedicated 77.3 ms` and `publish/shared 67.7 ms` against a ~38/~34 ms fast mode and failed the 50% ceiling; five further re-runs reported **OK**, because the baseline median had slid to `70.6`. One clean run after taking those samples out of the archive showed every metric rising together (`dedicated +92.5%`, `shared +99.2%`, `dedup +43.4%`, `trace +21.0%`), which is the contention signature and the inverse of the single-metric-up shape above. So read the load average *before* re-running, and when a suspect failure occurs on a loaded host, record it as deferred with its numbers and move the off-mode samples aside instead of letting them into the baseline — and do not regenerate `docs/benchmarks.md` from them.
|
|
24
24
|
5. Confirm the package contains only intended files with `npm pack --dry-run --json`.
|
|
25
25
|
6. Commit on a feature branch, push that branch, and merge its green PR using squash or fast-forward (no merge commit). Fetch the merged commit, tag that exact commit, and push only the specific version tag; never push `main`/`master` directly or force-push. The workflow runs `node scripts/verify-release-version.mjs` to require `RELEASE_TAG` to equal `v` + the package version and to require exactly one non-empty CHANGELOG section.
|
|
26
26
|
|
package/docs/roadmap.md
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.15 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.15 delivered scope
|
|
6
|
+
|
|
7
|
+
- Both point-to-frame addressee guards are now pinned by name. `handleMessage` dispatches on `type` alone and
|
|
8
|
+
the cluster channel is broadcast, so `targetWorkerId !== this.workerId` is the only thing keeping a runtime
|
|
9
|
+
from acting on a frame meant for someone else; measured by deleting each line in turn, 369 and then 358
|
|
10
|
+
tests stayed green, and the coordination fuzz cannot assert either one because its invariants are end-state
|
|
11
|
+
checks and a slower convergence still passes.
|
|
12
|
+
- The `ROUTE_RELEASED` pin needed a construction its staleness test could not refuse: a durable route that
|
|
13
|
+
genuinely names this runtime, with a matching `generation` and `handoffFromWorkerId`, and the ACK addressed
|
|
14
|
+
to a third worker. That is also why the older forged-`topicKey` case never reached the line.
|
|
15
|
+
- The coordination fuzz gained a third limit, placed where neither half of a loop owns it. Its two existing
|
|
16
|
+
bounds were both made of time, and the failure that had been turning `verify` red spent its time in
|
|
17
|
+
microtasks, where no same-thread deadline can reach it; `ChannelHub` now stops delivering past 50,000
|
|
18
|
+
posts in one seed, with the threshold taken from a measured distribution (p50 = 28, p99 = 90, max = 16,763)
|
|
19
|
+
rather than from the size of the wedge, and a cut seed is printed by name and excluded from the depth floor.
|
|
20
|
+
- What that loop actually is was then measured instead of inferred, and it is the harness rather than the
|
|
21
|
+
library: 11,542 accepts against 11,538 `confirmRoute` skips and 6 writes reaching storage, because the
|
|
22
|
+
owner's confirmation sits in its own batching writer while a peer reads the flushed record — and the cycle
|
|
23
|
+
cannot yield to a microtask because the test hub delivers every frame on the caller's stack. Deferring
|
|
24
|
+
delivery by one microtask removes the storm outright. The earlier note in this file that called it an open
|
|
25
|
+
product defect is corrected here, and the six injected-frame seeds that correction leaves behind are named
|
|
26
|
+
as the next question rather than resolved.
|
|
27
|
+
- `docs/release-checklist.md`, in both languages, now records that `pnpm bench:compare` can be closed by the
|
|
28
|
+
act of investigating it: its baseline is drawn from the same rolling archive every `bench:browser` run
|
|
29
|
+
appends to, so the instruction is to read the host load first and record a suspect failure as deferred
|
|
30
|
+
instead of chasing a green.
|
|
31
|
+
- `docs/progress.md` has a structural gate. Two PRs for one phase had left two `## Phase 142` blocks, with one
|
|
32
|
+
of them pushed below the anchor every entry is appended at; `tests/documentation.test.ts` now requires each
|
|
33
|
+
phase number to be new, later than the previous entry, and above the standing sections.
|
|
34
|
+
|
|
35
|
+
No behavior, wire-format, storage-layout or cluster-protocol change: `src/` is untouched in this version, and
|
|
36
|
+
the release is owed to `docs/release-checklist.md` and `CHANGELOG.md`, both of which ship in the package.
|
|
4
37
|
|
|
5
38
|
## 0.21.14 delivered scope
|
|
6
39
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
1. 更新 `package.json`、`CHANGELOG.md` 和中英文 roadmap。
|
|
21
21
|
2. 运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`、`pnpm bench`、`pnpm test:e2e`、`pnpm bench:browser`、`pnpm verify:pack`、`pnpm verify:compat`、`pnpm verify:types` 以及 `git diff --check`(`verify:compat` 断言 `COMPAT_BASE_TAG` 基线中的 package `exports` 子路径与类型字段仍然存在;`verify:pack` 从打包产物冒烟导入完整根公共面与全部子路径的 ESM/CJS;`verify:types` 用同一基线冻结每个公共入口声明层导出的名字,并断言没有任何公共签名引用了「任何入口都未导出」的类型——纯类型导出不会在运行时产生任何键,因此 `tests/dual-format.test.ts` 的 `Object.keys()` 冻结和 `verify:compat` 都看不到它的变化。`verify:compat` 与 `verify:types` 从最近的发布 tag 解析基线,因此浅克隆或缺少 tag 的克隆需先执行 `git fetch --tags`,否则会以 "no version tag found" 失败)。
|
|
22
22
|
3. 依赖安全门禁:`pnpm audit --registry=https://registry.npmjs.org`(配置的镜像 registry 缺少 audit 端点;CI 在 verify job 中于公共 registry 运行)。任一已知漏洞公告即视为发布失败;`pnpm-workspace.yaml` overrides 钉住补丁版本。
|
|
23
|
-
4. 浏览器基准回归门禁:运行 `pnpm bench:browser`(至少两次;门禁将最新报告与之前最多五份归档报告中同一指标的中位数比较,**并且额外要求新值超过这些样本中的最大值**),随后执行 `pnpm bench:compare --fail-above-pct 50`。采用中位数基线是因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳——`dedup1000Ms` 相邻两次实测分别为 12.7 ms 与 25.6 ms,只看"上一份报告"时,一次偶发噪声就能在无代码改动的情况下击穿既定上限。第二个条件则是因为:在两档交替的样本上,中位数只有当两档保持混合时才稳定——一旦连续几份报告落在同一档,中位数本身就会跳变,这正是同一份纯注释改动在一版基线前读出 +121%、几分钟后在另一版基线前读出 −56.7% 的原因。被该条件放过的指标会打印在 `within-baseline, not gated` 下,而不是静默通过——在当前归档上实测,`publish/dedicated/perMessageMs`、`publish/shared/perMessageMs` 与 `databus/publishBatch1000Ms` 三项都落在各自五份报告的区间内,因此应把这份清单读作该指标的真实分辨率,而不是一张合格证。`publish/dedicated/perMessageMs` 同样如此:同一份代码相邻两次实测为 49.9 ms 与 71.1 ms,令门禁以 +75.7% 失败,而同一份报告里 `dedup1000Ms` 与 `traceAndPublish1000Ms` 反而*变快*——"单指标上涨、其余下降"正是噪声的特征,第三次实测为 49.4 ms 并通过。因此在采信或处理失败前先复跑并观察离散度;若改动涉及热路径,就直接量化其开销(0.20.95 有界映射改写的微基准显示三种写法均为 80–86 ns/op,落在运行间波动之内)。基线迁移(例如某指标从空操作变为真实路径)属预期内的一次性失败。用 `pnpm bench:trend`
|
|
23
|
+
4. 浏览器基准回归门禁:运行 `pnpm bench:browser`(至少两次;门禁将最新报告与之前最多五份归档报告中同一指标的中位数比较,**并且额外要求新值超过这些样本中的最大值**),随后执行 `pnpm bench:compare --fail-above-pct 50`。采用中位数基线是因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳——`dedup1000Ms` 相邻两次实测分别为 12.7 ms 与 25.6 ms,只看"上一份报告"时,一次偶发噪声就能在无代码改动的情况下击穿既定上限。第二个条件则是因为:在两档交替的样本上,中位数只有当两档保持混合时才稳定——一旦连续几份报告落在同一档,中位数本身就会跳变,这正是同一份纯注释改动在一版基线前读出 +121%、几分钟后在另一版基线前读出 −56.7% 的原因。被该条件放过的指标会打印在 `within-baseline, not gated` 下,而不是静默通过——在当前归档上实测,`publish/dedicated/perMessageMs`、`publish/shared/perMessageMs` 与 `databus/publishBatch1000Ms` 三项都落在各自五份报告的区间内,因此应把这份清单读作该指标的真实分辨率,而不是一张合格证。`publish/dedicated/perMessageMs` 同样如此:同一份代码相邻两次实测为 49.9 ms 与 71.1 ms,令门禁以 +75.7% 失败,而同一份报告里 `dedup1000Ms` 与 `traceAndPublish1000Ms` 反而*变快*——"单指标上涨、其余下降"正是噪声的特征,第三次实测为 49.4 ms 并通过。因此在采信或处理失败前先复跑并观察离散度;若改动涉及热路径,就直接量化其开销(0.20.95 有界映射改写的微基准显示三种写法均为 80–86 ns/op,落在运行间波动之内)。基线迁移(例如某指标从空操作变为真实路径)属预期内的一次性失败。用 `pnpm bench:trend` 刷新长期趋势文档,表格变化时一并提交。**复跑失败可能亲手制造出让它通过的基线,这一点已被实测。** `bench:compare` 的基线(中位数与最大值两个条件)取自最近若干份归档报告,而每一次 `pnpm bench:browser` 都会往同一个归档里追加——于是排查失败时新增的样本,就变成了裁决下一次尝试的基线。2026-09-24:主机负载均值 7.7–15.1、待发布树的非注释 `src/` 差异为 0 行,前两次实测 `publish/dedicated 77.3 ms`、`publish/shared 67.7 ms`,相对约 38/34 ms 的快档在 50% 上限上失败;随后五次复跑报告 **OK**,因为基线中位数已经滑到 `70.6`。把这些样本移出归档后的一次干净实测显示所有指标同时上涨(`dedicated +92.5%`、`shared +99.2%`、`dedup +43.4%`、`trace +21.0%`),那是资源竞争的形态,与上文"单指标上涨、其余下降"的噪声形态相反。因此:复跑之前先看负载(`uptime`);在满载主机上遇到可疑失败,就把数字连同负载一起记为"本轮推迟",并将偏离常态的样本移出基线——也不要据它们重新生成 `docs/benchmarks.md`。
|
|
24
24
|
5. 用 `npm pack --dry-run --json` 确认发布包只包含预期文件。
|
|
25
25
|
6. 在功能分支提交并推送该分支,PR 验证通过后使用 squash 或 fast-forward 合入(不创建 merge commit)。获取合入后的精确提交并打 tag,只推送该版本 tag;禁止直接推送 `main`/`master` 或 force-push。工作流运行 `node scripts/verify-release-version.mjs`,要求 `RELEASE_TAG` 等于 `v` 加 package 版本,且 CHANGELOG 中恰好有一个非空的对应版本章节。
|
|
26
26
|
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.15 已于 2026 年 9 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
|
|
4
|
+
|
|
5
|
+
## 0.21.15 已完成范围
|
|
6
|
+
|
|
7
|
+
- 两个点对点寻址守卫现在都被具名钉住了。`handleMessage` 只按 `type` 分发,而集群频道是广播的,所以
|
|
8
|
+
`targetWorkerId !== this.workerId` 这一行是唯一阻止某个 runtime 处理发给别人的帧的东西;逐行删除后测量,
|
|
9
|
+
先后有 369 个和 358 个测试仍然通过,而协调模糊测试无法断言它,因为它的不变式是结束态检查,收敛得慢一点
|
|
10
|
+
依然算通过。
|
|
11
|
+
- `ROUTE_RELEASED` 的那颗钉子需要一个它的陈旧性测试无法拒绝的构造:一条确实把本 runtime 写成 owner 的持久
|
|
12
|
+
路由,配上匹配的 `generation` 与 `handoffFromWorkerId`,再把 ACK 投给第三个 worker。这也正是较早那个伪造
|
|
13
|
+
`topicKey` 的用例从来走不到这一行的原因。
|
|
14
|
+
- 协调模糊测试增加了第三层限制,位置选在环路两边都不拥有的地方。原有的两层都由时间构成,而把 `verify` 跑红
|
|
15
|
+
的那个失败把时间花在微任务里,同线程上的任何截止时间都碰不到它;`ChannelHub` 现在会在单个 seed 超过 50,000
|
|
16
|
+
次投递后停止投递,阈值取自实测分布(p50 = 28、p99 = 90、最大值 = 16,763)而不是卡死的大小,被切断的 seed
|
|
17
|
+
会按名字打印并且不计入深度下限。
|
|
18
|
+
- 那条环路到底是什么,随后是被测量而不是被推断出来的,而它是测试装置而不是库:11,542 次接受对应 11,538 次
|
|
19
|
+
`confirmRoute` 跳过,而落到存储的写只有 6 次——因为 owner 的确认还躺在它自己的合批写入器里,对端读到的却是
|
|
20
|
+
已刷出的记录;而这个循环之所以让不出微任务,是测试用的 hub 把每一帧都在调用栈上同步投递。把投递推迟一个
|
|
21
|
+
微任务,风暴就彻底消失。本文件里此前把它写成"未修复的产品缺陷"的说法在此更正,而更正之后留下的六个注入帧
|
|
22
|
+
seed 作为下一个问题被点名,而不是被搪塞过去。
|
|
23
|
+
- `docs/release-checklist.md` 两种语言现在都写明:`pnpm bench:compare` 会被"调查它"这个动作本身关掉。它的
|
|
24
|
+
基线取自同一个滚动归档,而每次 `bench:browser` 都在往那个归档追加,所以规程变成:先看主机负载,把可疑失败
|
|
25
|
+
连同负载记为 deferred,而不是去追一个绿色。
|
|
26
|
+
- `docs/progress.md` 有了结构性门禁。同一个阶段的两个 PR 曾留下两个 `## Phase 142` 块,其中一个被挤到所有
|
|
27
|
+
条目都追加在前的那个锚点之下;`tests/documentation.test.ts` 现在要求每个阶段编号既是新的、又晚于上一条、
|
|
28
|
+
并且在常驻章节之上。
|
|
29
|
+
|
|
30
|
+
没有行为、线格式、存储布局或集群协议变更:本版本 `src/` 未改动,发布义务来自随包发布的
|
|
31
|
+
`docs/release-checklist.md` 与 `CHANGELOG.md`。
|
|
4
32
|
|
|
5
33
|
## 0.21.14 已完成范围
|
|
6
34
|
|
package/package.json
CHANGED
|
File without changes
|