cross-tab-worker-databus 0.21.9 → 0.21.10
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 +12 -0
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.shared.worker.js +10 -4
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/{chunk-75ZDU7VG.js → chunk-L5P66THO.js} +2 -2
- package/dist/{chunk-75ZDU7VG.js.map → chunk-L5P66THO.js.map} +1 -1
- package/dist/cjs/centrifuge.cjs +1 -1
- package/dist/cjs/centrifuge.cjs.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/workers/port-reaper.d.ts +9 -3
- package/dist/workers/port-reaper.d.ts.map +1 -1
- package/docs/api.md +1 -1
- package/docs/benchmarks.md +7 -7
- package/docs/configuration.md +5 -5
- package/docs/roadmap.md +27 -1
- package/docs/zh/api.md +1 -1
- package/docs/zh/benchmarks.md +7 -7
- package/docs/zh/configuration.md +5 -5
- package/docs/zh/roadmap.md +19 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,9 +44,15 @@ export declare class PortReaper {
|
|
|
44
44
|
* resurrect it in the reaper's tracking maps. */
|
|
45
45
|
touch(port: MessagePort): void;
|
|
46
46
|
/** Override a port's session timeout from its INIT heartbeat config.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* `Infinity` is the documented way to disable the PING heartbeat, and it means
|
|
48
|
+
* this port can never be judged silent: `startHeartbeat()` sends nothing at all
|
|
49
|
+
* once configured that way, so a timeout would be a death sentence with no
|
|
50
|
+
* possible appeal. A non-finite-or-non-positive *other* value falls back to the
|
|
51
|
+
* default so a bad payload cannot degenerate the reaper into a busy loop or
|
|
52
|
+
* silence it — those three (NaN, 0, negative) are rejected by
|
|
53
|
+
* `assertHeartbeatInterval` at the transport constructor and so can only arrive
|
|
54
|
+
* from a main thread that is not this library. No-op for untracked ports (e.g.
|
|
55
|
+
* setTimeout arrives after remove/STOP). */
|
|
50
56
|
setTimeout(port: MessagePort, heartbeatIntervalMs: number): void;
|
|
51
57
|
/** Remove a port (STOP message). The caller is responsible for closing the
|
|
52
58
|
* port and stopping the session. No-op for untracked ports so a duplicate
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;+DAE+D;AAC/D,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAED,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAC7D,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAiC;gBAGhC,GAAG,GAAE,MAAM,MAAiB,EAC7C,QAAQ,GAAE,QAA2E,EACrF,UAAU,GAAE,UAA4C;IAM1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;qDAEiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B
|
|
1
|
+
{"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;+DAE+D;AAC/D,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAED,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAC7D,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAiC;gBAGhC,GAAG,GAAE,MAAM,MAAiB,EAC7C,QAAQ,GAAE,QAA2E,EACrF,UAAU,GAAE,UAA4C;IAM1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;qDAEiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B;;;;;;;;;gDAS4C;IAC5C,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAWhE;;4EAEwE;IACxE,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B;2EACuE;IACvE,OAAO,IAAI,IAAI;IAgBf;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ;IAahB;;8DAE0D;IAC1D,OAAO,CAAC,mBAAmB;IAQ3B;;qEAEiE;IACjE,OAAO,CAAC,IAAI;CAoDb"}
|
package/docs/api.md
CHANGED
|
@@ -362,7 +362,7 @@ Available options:
|
|
|
362
362
|
|
|
363
363
|
- `workerMode`: `'dedicated'` (default), `'shared'`, or `'auto'`; the `auto` degradation chain is SharedWorker -> Dedicated Worker -> local mode
|
|
364
364
|
- `transferable`: `boolean`, default `false`; when enabled, ArrayBuffer payloads use Transferable transport, while the object message API remains unchanged
|
|
365
|
-
- `heartbeatIntervalMs`: `number`, default `10000`; SharedWorker PING heartbeat interval in ms. `Infinity` disables heartbeats. Must be a positive number or `Infinity` — `0`, a negative number, or `NaN` throws a `TypeError` in the transport constructor. See [configuration](./configuration.md#sharedworker-session-reaper) for details
|
|
365
|
+
- `heartbeatIntervalMs`: `number`, default `10000`; SharedWorker PING heartbeat interval in ms. `Infinity` disables heartbeats and exempts that port from reaping. Must be a positive number or `Infinity` — `0`, a negative number, or `NaN` throws a `TypeError` in the transport constructor. See [configuration](./configuration.md#sharedworker-session-reaper) for details
|
|
366
366
|
- `workerFactory`: custom Dedicated Worker loading method
|
|
367
367
|
- `sharedWorkerFactory`: custom SharedWorker loading method
|
|
368
368
|
|
package/docs/benchmarks.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
# Browser Benchmark Trend
|
|
4
4
|
|
|
5
|
-
> Data through 2026-09-
|
|
5
|
+
> Data through 2026-09-23, from the 71 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 |
|
|
13
|
-
| publish per-message (ms, lower is better) — shared |
|
|
14
|
-
| wildcard dispatch ×1000 (ms, lower is better) |
|
|
15
|
-
| publishBatch ×1000 (ms, lower is better) |
|
|
16
|
-
| dedup ×1000 (ms, lower is better) |
|
|
17
|
-
| trace + publish ×1000 (ms, lower is better) |
|
|
12
|
+
| publish per-message (ms, lower is better) — dedicated | 55.4416 | 50.6133 | -4.83 | 50.6133 |
|
|
13
|
+
| publish per-message (ms, lower is better) — shared | 58.2441 | 33.8955 | -24.35 | 33.8955 |
|
|
14
|
+
| wildcard dispatch ×1000 (ms, lower is better) | 5.2 | 5.7 | +0.50 | 5.2 |
|
|
15
|
+
| publishBatch ×1000 (ms, lower is better) | 3.5 | 3.7 | +0.20 | 3.5 |
|
|
16
|
+
| dedup ×1000 (ms, lower is better) | 13 | 11 | -2.00 | 11 |
|
|
17
|
+
| trace + publish ×1000 (ms, lower is better) | 4.6 | 4.7 | +0.10 | 4.6 |
|
|
18
18
|
| first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
|
|
19
19
|
<!-- BENCH-TREND:END -->
|
|
20
20
|
|
package/docs/configuration.md
CHANGED
|
@@ -162,7 +162,7 @@ Main configuration for `createCentrifugeDataBus<TData>(options)`:
|
|
|
162
162
|
| `clusterKey` | `string` | `connection.url` | Manually isolate logical clusters |
|
|
163
163
|
| `workerMode` | `'dedicated' \| 'shared' \| 'auto'` | `'dedicated'` | Worker transport runtime mode; `auto` degrades via SharedWorker -> Dedicated Worker -> local mode, explicit `dedicated` degrades via Dedicated Worker -> SharedWorker -> local mode |
|
|
164
164
|
| `transferable` | `boolean` | `false` | When enabled, `publish(topic, ArrayBuffer)` uses Transferable transport; ArrayBuffer publications on the receiving side also follow the transfer path |
|
|
165
|
-
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely. Distinct from the Core cluster heartbeat (default 3000
|
|
165
|
+
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely and exempts that port from reaping. Distinct from the Core cluster heartbeat (default 3000 ms) which tracks worker liveness via localStorage |
|
|
166
166
|
| `workerFactory` | `() => Worker` | Built-in Worker | For testing or custom Worker loading |
|
|
167
167
|
| `sharedWorkerFactory` | `() => SharedWorker` | Built-in SharedWorker | For testing or custom SharedWorker loading |
|
|
168
168
|
| `credentialProvider` | `{ getToken?, getChannelToken? }` | `undefined` | Async credential refresh bridge: the Worker asks the main thread for each fresh token (`getToken` / `getChannelToken`) and this provider supplies it from application context. Required because function-valued Centrifuge options cannot be structured-cloned into the Worker |
|
|
@@ -210,13 +210,13 @@ Storage events are delivered to other documents only. Two bus runtimes in the *s
|
|
|
210
210
|
|
|
211
211
|
A `MessagePort` has no `close` event, so the SharedWorker cannot know when a tab has crashed or been closed without sending a `STOP` message. To avoid leaking a `CentrifugeSession` (and its WebSocket) for a dead tab, the transport sends a periodic **PING heartbeat** to the SharedWorker, and the SharedWorker runs a **reaper** that closes any session whose port has been silent for longer than its timeout.
|
|
212
212
|
|
|
213
|
-
- **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely —
|
|
214
|
-
- **Session timeout**: `3 × heartbeatIntervalMs` (default `30000`
|
|
213
|
+
- **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely: the transport sends no `PING` at all, and it tells the Worker so by carrying that same `Infinity` in its `INIT`, which **exempts the port from reaping** — nothing on that port can ever be judged silent. Use this only when the SharedWorker reaper is not needed (e.g. the tab always stops the bus itself, so every port leaves through `STOP`). The cost is symmetrical and worth stating: a tab configured this way and then *crashed* leaves its `CentrifugeSession` and its WebSocket in place, because the one thing that would have noticed was the heartbeat that was switched off.
|
|
214
|
+
- **Session timeout**: `3 × heartbeatIntervalMs` (default `30000` ms). A port silent for longer than its timeout is reaped: its session is stopped and its WebSocket closed. A port whose `INIT` carried `heartbeatIntervalMs: Infinity` has no timeout and is never reaped. This is distinct from the Core cluster heartbeat (default `3000` ms) which tracks worker liveness via localStorage — see the note below.
|
|
215
215
|
- **Adaptive cadence**: the reaper runs at the smallest configured heartbeat interval across active ports, so a port with a short heartbeat is reaped promptly. When the last port disconnects, the reaper interval is cleared so a long-lived SharedWorker does not run a perpetual no-op interval between connection bursts.
|
|
216
216
|
- **The tab is told before the port goes**: a port that is being reclaimed receives one final `SESSION_REAPED` message, then is closed, then its session is stopped. The order is the point — a `MessagePort` has no close event, and posting into a port the other side closed succeeds while delivering nothing, so this is the only thing a starved tab can ever learn from. Without it a tab that was merely stalled rather than gone (a long synchronous task, or the throttling a backgrounded tab gets) kept its cluster role and its topic routes while every publication it sent vanished into the dead port, and `getHealthSummary()` went on reporting `healthy` / `connected`. On receiving the message the transport discards the backend and reports the failure through `onError` and an `error` status, which is what lets the normal recovery path rebuild the session on a fresh port. Measured in a real browser: after a 34 s stall the tab re-established its subscription on its own — no user gesture, no lifecycle event — and the replacement connection was under a second old when it was observed.
|
|
217
217
|
- **Port closed before session stop**: when a port is reaped, the port is closed first and the session stopped after. Closing the port discards the session's `disconnected` status post (so it never reaches a live-but-slow main thread) and guarantees a closed port can never deliver a later message that would resurrect the session outside the reaper's tracking.
|
|
218
|
-
- **Failure isolation**:
|
|
219
|
-
-
|
|
218
|
+
- **Failure isolation**: reaping wraps `target.notify()`/`close()`/`stop()` in try-catch and `dispose()` wraps `close()`/`stop()`, so a single failing port cannot abort the reaping pass or leave subsequent dead tabs uncollected. A step that throws does skip the steps after it — for that one port only.
|
|
219
|
+
- **`dispose()` has no Worker-side caller**: it clears the reaper interval and closes/stops **every** still-tracked session, and the unit tests exercise it, but the shipped SharedWorker never invokes it — a `MessagePort` has no close event, so there is no "the SharedWorker is shutting down" signal for such a hook to hang on. Every port leaves tracking through its own `STOP` handler or through the reaper, and the interval the reaper runs on is cleared once the last tracked port is gone.
|
|
220
220
|
|
|
221
221
|
This is the mechanism that recovers sessions for tabs that crash without sending `STOP`, and the announcement it makes is what lets a tab that did *not* crash notice and reconnect. Lower `heartbeatIntervalMs` to reap dead sessions faster, at the cost of more frequent PING messages on the port — and note that a value below roughly a third of the longest task your page runs will start reaping live tabs, each of which recovers by rebuilding its session.
|
|
222
222
|
|
package/docs/roadmap.md
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.10 was released on September 23, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
|
|
4
|
+
|
|
5
|
+
## 0.21.10 delivered scope
|
|
6
|
+
|
|
7
|
+
- `heartbeatIntervalMs: Infinity` — the documented way to switch the SharedWorker PING
|
|
8
|
+
heartbeat off — was being read by the reaper as a malformed number. It fell through the
|
|
9
|
+
same `Number.isFinite` guard as `NaN`, `0` and negatives and took the default 10 s, so
|
|
10
|
+
the port was given a 30 s session timeout on the strength of being the one port that would
|
|
11
|
+
never send another message: the option whose stated purpose is "the reaper is not needed"
|
|
12
|
+
guaranteed a reap 30 s after connecting. `Infinity` now exempts that port from reaping,
|
|
13
|
+
while the three values the transport constructor rejects (and which therefore cannot come
|
|
14
|
+
from this library at all) keep the fallback. Pinned by one test that kills three mutants at
|
|
15
|
+
three different assertions, plus an assertion that the `INIT` actually carries the value —
|
|
16
|
+
the two halves of this defect were each covered by a green test, which is how it survived.
|
|
17
|
+
- The shipped reaper documentation stopped promising things the code does not do, in both
|
|
18
|
+
languages: no `PortReaper.dispose()` shutdown hook runs "when the SharedWorker shuts down"
|
|
19
|
+
(nothing calls it — a `MessagePort` has no close event, so there is no such signal to hang a
|
|
20
|
+
hook on), and what disabling the heartbeat costs is stated next to what it disables (a tab
|
|
21
|
+
configured that way and then crashed keeps its session and its WebSocket).
|
|
22
|
+
- A test that passed while breaking the gate: an IndexedDB replay assertion attached its
|
|
23
|
+
rejection handler after an await that a real 40 ms timer could overtake, so on a loaded
|
|
24
|
+
runner `pnpm test:coverage` exited 1 with `Unhandled Rejection` and every test green. The
|
|
25
|
+
handlers are created next to their promises now, measured in both directions by forcing the
|
|
26
|
+
round trip to 200 ms.
|
|
27
|
+
- No wire-format, storage-layout or cluster-protocol change. The behaviour change is confined
|
|
28
|
+
to shared mode configured with `heartbeatIntervalMs: Infinity`, which could not work as
|
|
29
|
+
documented before.
|
|
4
30
|
|
|
5
31
|
## 0.21.9 delivered scope
|
|
6
32
|
|
package/docs/zh/api.md
CHANGED
|
@@ -361,7 +361,7 @@ const transport = new CentrifugeWorkerTransport({
|
|
|
361
361
|
|
|
362
362
|
- `workerMode`:`'dedicated'`(默认)、`'shared'` 或 `'auto'`;`auto` 的降级链路为 SharedWorker → Dedicated Worker → 本地模式
|
|
363
363
|
- `transferable`:`boolean`,默认 `false`;开启后 ArrayBuffer payload 使用 Transferable 传输,对象消息 API 不变
|
|
364
|
-
- `heartbeatIntervalMs`:`number`,默认 `10000`;SharedWorker PING 心跳间隔(毫秒)。传 `Infinity`
|
|
364
|
+
- `heartbeatIntervalMs`:`number`,默认 `10000`;SharedWorker PING 心跳间隔(毫秒)。传 `Infinity` 完全禁用心跳,该端口也因此豁免于回收。详见 [配置](./configuration.md#sharedworker-会话回收)
|
|
365
365
|
- `workerFactory`:自定义 Dedicated Worker 加载方式
|
|
366
366
|
- `sharedWorkerFactory`:自定义 SharedWorker 加载方式
|
|
367
367
|
|
package/docs/zh/benchmarks.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
# 浏览器基准趋势
|
|
4
4
|
|
|
5
|
-
> 数据截至 2026-09-
|
|
5
|
+
> 数据截至 2026-09-23,基于 71 份归档的 `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 |
|
|
13
|
-
| publish per-message (ms, lower is better) — shared |
|
|
14
|
-
| wildcard dispatch ×1000 (ms, lower is better) |
|
|
15
|
-
| publishBatch ×1000 (ms, lower is better) |
|
|
16
|
-
| dedup ×1000 (ms, lower is better) |
|
|
17
|
-
| trace + publish ×1000 (ms, lower is better) |
|
|
12
|
+
| publish per-message (ms, lower is better) — dedicated | 55.4416 | 50.6133 | -4.83 | 50.6133 |
|
|
13
|
+
| publish per-message (ms, lower is better) — shared | 58.2441 | 33.8955 | -24.35 | 33.8955 |
|
|
14
|
+
| wildcard dispatch ×1000 (ms, lower is better) | 5.2 | 5.7 | +0.50 | 5.2 |
|
|
15
|
+
| publishBatch ×1000 (ms, lower is better) | 3.5 | 3.7 | +0.20 | 3.5 |
|
|
16
|
+
| dedup ×1000 (ms, lower is better) | 13 | 11 | -2.00 | 11 |
|
|
17
|
+
| trace + publish ×1000 (ms, lower is better) | 4.6 | 4.7 | +0.10 | 4.6 |
|
|
18
18
|
| first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
|
|
19
19
|
<!-- BENCH-TREND:END -->
|
|
20
20
|
|
package/docs/zh/configuration.md
CHANGED
|
@@ -155,7 +155,7 @@ const bus = createCentrifugeDataBus({
|
|
|
155
155
|
| `clusterKey` | `string` | `connection.url` | 手动隔离逻辑集群 |
|
|
156
156
|
| `workerMode` | `'dedicated' \| 'shared' \| 'auto'` | `'dedicated'` | Worker transport 运行模式;`auto` 按 SharedWorker → Dedicated Worker → 本地模式降级,显式 `dedicated` 按 Dedicated Worker → SharedWorker → 本地模式降级 |
|
|
157
157
|
| `transferable` | `boolean` | `false` | 开启后 `publish(topic, ArrayBuffer)` 使用 Transferable 传输,接收侧 ArrayBuffer publication 也走转移路径 |
|
|
158
|
-
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING 心跳间隔(见下方 SharedWorker 会话回收);传 `Infinity`
|
|
158
|
+
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING 心跳间隔(见下方 SharedWorker 会话回收);传 `Infinity` 完全禁用心跳,该端口也因此豁免于回收。与 Core 集群心跳(默认 3000 ms,通过 localStorage 跟踪 worker 存活)相互独立 |
|
|
159
159
|
| `workerFactory` | `() => Worker` | 内置 Worker | 测试或自定义 Worker 加载方式 |
|
|
160
160
|
| `sharedWorkerFactory` | `() => SharedWorker` | 内置 SharedWorker | 测试或自定义 SharedWorker 加载方式 |
|
|
161
161
|
| `credentialProvider` | `{ getToken?, getChannelToken? }` | `undefined` | 异步凭证刷新桥:Worker 向主线程请求每个新 token(`getToken` / `getChannelToken`),由该 provider 从应用上下文提供。必要原因:函数型 Centrifuge 选项无法 structured-clone 进 Worker |
|
|
@@ -203,13 +203,13 @@ storage 事件只在其他 document 中派发。因此同一 document 内共享
|
|
|
203
203
|
|
|
204
204
|
`MessagePort` 没有 `close` 事件,因此 SharedWorker 无法在 Tab 崩溃或关闭时获知(除非收到 `STOP` 消息)。为避免泄漏已死 Tab 的 `CentrifugeSession`(及其 WebSocket),transport 定期向 SharedWorker 发送 **PING 心跳**,SharedWorker 运行一个**回收器**来关闭超过静默超时的端口会话。
|
|
205
205
|
|
|
206
|
-
- **心跳间隔**:`heartbeatIntervalMs`(默认 `10000`
|
|
207
|
-
- **会话超时**:`3 × heartbeatIntervalMs`(默认 `30000`
|
|
206
|
+
- **心跳间隔**:`heartbeatIntervalMs`(默认 `10000` ms)。主线程按此间隔发送 `PING`。传 `Infinity` 即完全禁用心跳:transport 不再发送任何 `PING`,并在 `INIT` 里带上同一个 `Infinity`,因此**该端口被豁免于回收**——它永远不会被判定为静默。仅在确实不需要 SharedWorker 回收器时使用(例如 Tab 总会自行 `stop()`,每个端口都经由 `STOP` 离开)。代价同样值得说明:这样配置的 Tab 如果*崩溃*,它的 `CentrifugeSession` 与 WebSocket 会原地保留,因为本会察觉的那颗心跳已经被关掉了。
|
|
207
|
+
- **会话超时**:`3 × heartbeatIntervalMs`(默认 `30000` ms)。超过超时未收到消息的端口会被回收:其会话停止,WebSocket 关闭。`INIT` 中携带 `heartbeatIntervalMs: Infinity` 的端口没有超时,永不被回收。这与 Core 集群心跳(默认 `3000` ms,通过 localStorage 跟踪 worker 存活)相互独立——见下方说明。
|
|
208
208
|
- **自适应频率**:回收器以所有活动端口中最小的心跳间隔运行,使短心跳端口的会话能被及时回收。当最后一个端口断开时,回收器定时器清除,避免长时间存在的 SharedWorker 在连接爆发间隙运行永久的空循环。
|
|
209
209
|
- **关闭端口前先通知 Tab**:被回收的端口会先收到最后一条 `SESSION_REAPED` 消息,然后才被关闭、其会话才被停止。顺序正是关键所在——`MessagePort` 没有 close 事件,而向一个对端已关闭的端口发送消息会“成功”却什么都不送达,这是被饿死的 Tab 唯一能得知真相的途径。此前,一个只是被长时间阻塞、并没有消失的 Tab(一个很长的同步任务,或后台 Tab 受到的定时器节流)会保留自己的集群角色与 topic 路由,而它发出的每条发布都消失在已关闭的端口里,`getHealthSummary()` 仍在报告 `healthy` / `connected`。收到该消息后,transport 会丢弃这个 backend,并通过 `onError` 与 `error` 状态报告失败,正常恢复路径随后在新端口上重建会话。真实浏览器实测:34 秒阻塞之后,Tab 自行重建了订阅——无需用户操作、无需生命周期事件——观察时这条替代连接建立尚不到一秒。
|
|
210
210
|
- **先关闭端口再停止会话**:回收端口时,先关闭端口,再停止会话。关闭端口会丢弃会话的 `disconnected` 状态通知(使其不会到达可能仍在运行但缓慢的主线程),并保证已关闭的端口永远无法传递后续消息,从而在回收器追踪之外复活僵尸会话。
|
|
211
|
-
-
|
|
212
|
-
-
|
|
211
|
+
- **失败隔离**:回收用 try-catch 包裹 `target.notify()`/`close()`/`stop()`,`dispose()` 则包裹 `close()`/`stop()`,单个异常端口不会中断本轮回收,也不会让后续死 Tab 无人回收。抛出的那一步之后的步骤确实会被跳过——但只跳过**这一个**端口的。
|
|
212
|
+
- **`dispose()` 没有 Worker 侧调用者**:它会清除回收定时器并关闭/停止**所有**仍被追踪的会话,单元测试也覆盖了它,但发布出去的 SharedWorker 从不调用它——`MessagePort` 没有 close 事件,因此根本不存在"SharedWorker 正在关闭"这种可以挂靠的信号。每个端口都通过自己的 `STOP` 处理或回收器离开追踪,而最后一个被追踪的端口消失后,回收器的定时器随即清除。
|
|
213
213
|
|
|
214
214
|
这是从崩溃(未发送 `STOP`)的 Tab 中恢复会话的机制;而它发出的那条通知,正是没有崩溃、只是被饿死的 Tab 能够察觉并重连的原因。降低 `heartbeatIntervalMs` 可更快回收死会话,代价是端口上更频繁的 PING 消息——另外请注意:如果它的值低于页面最长任务耗时的约三分之一,活着的 Tab 也会被开始回收,而每一次回收都要靠重建会话来恢复。
|
|
215
215
|
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.10 已于 2026 年 9 月 23 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
|
|
4
|
+
|
|
5
|
+
## 0.21.10 已完成范围
|
|
6
|
+
|
|
7
|
+
- `heartbeatIntervalMs: Infinity`——文档中关闭 SharedWorker PING 心跳的方式——此前被回收器当成了一个
|
|
8
|
+
畸形数字:它与 `NaN`、`0`、负数走同一道 `Number.isFinite` 检查,于是回退到默认 10 秒,也就给
|
|
9
|
+
了这个"注定再也不会发消息"的端口安上一个 30 秒的会话超时。这个选项的本意是"不需要回收器",结果却保证
|
|
10
|
+
连接 30 秒后必被回收。`Infinity` 现在让该端口豁免于回收,而那三个连 transport 构造函数都会拒绝(因此
|
|
11
|
+
根本不可能来自本库)的值仍走回退路径。用一个测试钉住:三个变异体分别死在三条不同断言上;另有一条断言钉住
|
|
12
|
+
`INIT` 确实带着这个值——这个缺陷的两半各有绿灯测试覆盖,正是它活到今天的原因。
|
|
13
|
+
- 发布出去的回收器文档不再承诺代码没有做的事(中英双语):不存在"SharedWorker 关闭时"运行的
|
|
14
|
+
`PortReaper.dispose()` 钩子(没有任何地方调用它——`MessagePort` 没有 close 事件,因此压根没有可供挂靠的
|
|
15
|
+
信号);而关闭心跳的代价也和它关掉了什么一起写明了(这样配置的 Tab 若崩溃,其会话与 WebSocket 会原地保留)。
|
|
16
|
+
- 一个"测试全绿却把门禁弄红"的缺陷:IndexedDB 回放的一条断言在某个 await 之后才挂上 rejection handler,
|
|
17
|
+
而那个 await 可能被真实的 40 毫秒定时器超过,于是在繁忙的 runner 上 `pnpm test:coverage` 会以
|
|
18
|
+
`Unhandled Rejection` 退出 1,而所有测试都显示通过。现在 handler 与 promise 一起创建——把往返强行拉长到
|
|
19
|
+
200 毫秒后两个方向都实测过。
|
|
20
|
+
- 没有改变线路格式、存储布局或集群协议。行为变化仅限于 shared 模式下配置 `heartbeatIntervalMs: Infinity`
|
|
21
|
+
的情形,而那种配置此前根本无法按文档所说工作。
|
|
4
22
|
|
|
5
23
|
## 0.21.9 已完成范围
|
|
6
24
|
|
package/package.json
CHANGED