cross-tab-worker-databus 0.20.71 → 0.20.86

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +216 -2
  2. package/README.md +8 -0
  3. package/README.zh.md +4 -0
  4. package/dist/centrifuge-protocol.d.ts +59 -26
  5. package/dist/centrifuge-protocol.d.ts.map +1 -1
  6. package/dist/centrifuge-session.d.ts +9 -0
  7. package/dist/centrifuge-session.d.ts.map +1 -1
  8. package/dist/centrifuge.d.ts +12 -2
  9. package/dist/centrifuge.d.ts.map +1 -1
  10. package/dist/centrifuge.js +168 -88
  11. package/dist/centrifuge.js.map +3 -3
  12. package/dist/centrifuge.shared.worker.js +146 -38
  13. package/dist/centrifuge.shared.worker.js.map +3 -3
  14. package/dist/centrifuge.worker.js +140 -33
  15. package/dist/centrifuge.worker.js.map +3 -3
  16. package/dist/{chunk-D2SIT473.js → chunk-SDOV3UHG.js} +1304 -515
  17. package/dist/chunk-SDOV3UHG.js.map +7 -0
  18. package/dist/chunk-TZ7ZP7YD.js +175 -0
  19. package/dist/chunk-TZ7ZP7YD.js.map +7 -0
  20. package/dist/cjs/centrifuge.cjs +1608 -650
  21. package/dist/cjs/centrifuge.cjs.map +4 -4
  22. package/dist/cjs/hooks.cjs +37 -2
  23. package/dist/cjs/hooks.cjs.map +3 -3
  24. package/dist/cjs/index.cjs +1692 -747
  25. package/dist/cjs/index.cjs.map +4 -4
  26. package/dist/cjs/vue.cjs +45 -2
  27. package/dist/cjs/vue.cjs.map +3 -3
  28. package/dist/core/cluster.d.ts +40 -4
  29. package/dist/core/cluster.d.ts.map +1 -1
  30. package/dist/core/data-bus.d.ts +134 -77
  31. package/dist/core/data-bus.d.ts.map +1 -1
  32. package/dist/core/dedup-manager.d.ts +98 -0
  33. package/dist/core/dedup-manager.d.ts.map +1 -0
  34. package/dist/core/environment.d.ts +43 -3
  35. package/dist/core/environment.d.ts.map +1 -1
  36. package/dist/core/replay-manager.d.ts +129 -0
  37. package/dist/core/replay-manager.d.ts.map +1 -0
  38. package/dist/core/replay-persistence.d.ts +2 -1
  39. package/dist/core/replay-persistence.d.ts.map +1 -1
  40. package/dist/core/replay-pruning.d.ts +21 -0
  41. package/dist/core/replay-pruning.d.ts.map +1 -0
  42. package/dist/core/routing.d.ts +22 -3
  43. package/dist/core/routing.d.ts.map +1 -1
  44. package/dist/core/storage-batch.d.ts +0 -4
  45. package/dist/core/storage-batch.d.ts.map +1 -1
  46. package/dist/core/trace.d.ts +55 -15
  47. package/dist/core/trace.d.ts.map +1 -1
  48. package/dist/core/types.d.ts +58 -9
  49. package/dist/core/types.d.ts.map +1 -1
  50. package/dist/core/version.d.ts +2 -0
  51. package/dist/core/version.d.ts.map +1 -0
  52. package/dist/hooks.d.ts +12 -1
  53. package/dist/hooks.d.ts.map +1 -1
  54. package/dist/hooks.js +28 -2
  55. package/dist/hooks.js.map +2 -2
  56. package/dist/index.d.ts +6 -6
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +263 -166
  59. package/dist/index.js.map +3 -3
  60. package/dist/utils/constants.d.ts +170 -0
  61. package/dist/utils/constants.d.ts.map +1 -0
  62. package/dist/utils/error-utils.d.ts +28 -0
  63. package/dist/utils/error-utils.d.ts.map +1 -0
  64. package/dist/utils/metadata.d.ts +16 -0
  65. package/dist/utils/metadata.d.ts.map +1 -0
  66. package/dist/utils/storage-utils.d.ts +24 -0
  67. package/dist/utils/storage-utils.d.ts.map +1 -0
  68. package/dist/utils/validation.d.ts +76 -0
  69. package/dist/utils/validation.d.ts.map +1 -0
  70. package/dist/vue.d.ts +13 -1
  71. package/dist/vue.d.ts.map +1 -1
  72. package/dist/vue.js +36 -2
  73. package/dist/vue.js.map +2 -2
  74. package/dist/websocket.d.ts +6 -1
  75. package/dist/websocket.d.ts.map +1 -1
  76. package/dist/worker-mode.d.ts +6 -2
  77. package/dist/worker-mode.d.ts.map +1 -1
  78. package/docs/README.md +1 -0
  79. package/docs/api.md +135 -4
  80. package/docs/architecture.md +92 -1
  81. package/docs/benchmarks.md +24 -0
  82. package/docs/capabilities.md +24 -3
  83. package/docs/configuration.md +58 -0
  84. package/docs/getting-started.md +35 -0
  85. package/docs/release-checklist.md +28 -5
  86. package/docs/roadmap.md +81 -5
  87. package/docs/zh/README.md +2 -1
  88. package/docs/zh/api.md +134 -4
  89. package/docs/zh/architecture.md +61 -1
  90. package/docs/zh/benchmarks.md +24 -0
  91. package/docs/zh/capabilities.md +21 -3
  92. package/docs/zh/configuration.md +53 -0
  93. package/docs/zh/getting-started.md +35 -0
  94. package/docs/zh/release-checklist.md +29 -6
  95. package/docs/zh/roadmap.md +93 -5
  96. package/package.json +39 -16
  97. package/dist/chunk-D2SIT473.js.map +0 -7
@@ -56,6 +56,27 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
56
56
 
57
57
  `replay.persistenceRetry` optionally controls transient persistence recovery. `maxAttempts` is the total number of attempts (default `1`), and `backoffMs` is the initial delay before retry (default `50`). Delays grow exponentially and are capped; final failures retain the existing `onError` and reliability behavior.
58
58
 
59
+ ### Replay Options
60
+
61
+ | Config | Type | Default | Description |
62
+ |---|---|---|---|
63
+ | `maxPerTopic` | `number` | `100` | Maximum buffered publications per topic under `count`/`both`; oldest are evicted first. Under `age`, timestamped entries are retention-bounded and timestamp-less legacy entries are capped by this value (positive safe integer) |
64
+ | `persistence` | `DataBusReplayPersistence` | — | Optional durable backend (`createIndexedDbReplayPersistence`); omitted keeps history in memory only |
65
+ | `retentionMs` | `number` | — | Producer-timestamp retention window; history older than the cutoff is pruned through the adapter's `clearBefore` |
66
+ | `pruneStrategy` | `'count' \| 'age' \| 'both'` | `'count'` | `count` caps each topic at `maxPerTopic`; `age` prunes timestamped history by `retentionMs` and caps timestamp-less legacy entries by `maxPerTopic`; `both` applies both. `age` without `retentionMs` has nothing to prune by and falls back to the count cap |
67
+ | `retentionSweepMs` | `number` | — | Periodic durable-retention sweep for quiet topics; requires `retentionMs` and a `clearBefore` adapter |
68
+ | `persistenceRetry` | `{ maxAttempts, backoffMs }` | `1` / `50` | Bounded retry for transient persistence failures; delays grow exponentially and are capped |
69
+
70
+ ### Deduplication Options
71
+
72
+ | Config | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `maxEntries` | `number` | `1000` | Remembered message IDs before the oldest (FIFO) entry is evicted (positive safe integer) |
75
+ | `ttlMs` | `number` | `60000` | How long a remembered ID suppresses a repeat (positive finite) |
76
+ | `sweepMs` | `number` | — | Optional periodic sweep that expires quiet IDs; disabled by default |
77
+ | `now` | `() => number` | `Date.now` | Injectable clock for deterministic TTL tests and non-wall-clock hosts |
78
+ | `adaptiveTtl` | `{ minMs, maxMs }` | — | Optional bounded adaptive TTL: a high recent message rate shortens the window toward `minMs`, a quiet period relaxes it to `maxMs` (both required, `minMs <= maxMs`) |
79
+
59
80
  When tracing is enabled, each retry before the final attempt emits a bounded `reliability` event with `operation: persistence_retry`, `persistenceOperation` (`load`, `append`, `clear`, `clearTopic`, or `clearBefore`), and the failed attempt number. No payload, URL, credential, or error body is included.
60
81
 
61
82
  WebSocket binary frames may arrive as either `ArrayBuffer` or browser `Blob`; both use the same compact binary publication format. Blob conversion is asynchronous and conversion failures are reported through the transport error handler.
@@ -100,6 +121,36 @@ The active set is used only when a Topic needs a new owner. An existing live own
100
121
  - `2-3`: Balances resource reuse and fault recovery.
101
122
  - Larger values: Suitable for scenarios with many Topics and where a single connection faces server-side limits.
102
123
 
124
+ ### Adaptive Owner Weighting
125
+
126
+ By default a new Topic is assigned to the owner with the fewest owned Topics. The
127
+ opt-in `loadWeighting` option adds traffic and scheduling signals so new routes
128
+ favor quieter, healthier Workers — existing routes stay sticky and never migrate.
129
+
130
+ ```ts
131
+ const bus = createCentrifugeDataBus({
132
+ connection: { url: getConnectionUrl() },
133
+ loadWeighting: {
134
+ messageRateWeight: 0.5, // weight per message-per-second
135
+ byteRateWeight: 0.001, // weight per byte-per-second
136
+ scheduleLagWeight: 2 // weight for heartbeat scheduling-lag ratio
137
+ }
138
+ });
139
+ ```
140
+
141
+ | Option | Default | Effect |
142
+ |---|---|---|
143
+ | `messageRateWeight` | `0` | Each Worker samples its own fan-out message rate per heartbeat window; the normalized rate is added to the effective load. |
144
+ | `byteRateWeight` | `0` | Same, for approximate payload bytes per second. |
145
+ | `scheduleLagWeight` | `0` | Weights the scheduling-lag ratio (`overrunMs ÷ windowMs`). A starved event loop lands heartbeats late, so this steers new routes away from a throttled Worker. |
146
+
147
+ All weights default to `0`, keeping the legacy pure topic-count behavior byte-identical.
148
+ Each weight must be a non-negative finite number; a negative or non-finite weight throws a
149
+ `TypeError` at construction, since a negative weight would bias new routes toward the
150
+ *busiest* Worker instead.
151
+ The per-Worker sample (`WorkerThroughputSample`) is published with the heartbeat and
152
+ carries `windowMs`, `messageCount`, `byteCount`, `overrunMs`, and `sampledAt`.
153
+
103
154
  ## Centrifuge Configuration
104
155
 
105
156
  Main configuration for `createCentrifugeDataBus<TData>(options)`:
@@ -114,6 +165,7 @@ Main configuration for `createCentrifugeDataBus<TData>(options)`:
114
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 ms) which tracks worker liveness via localStorage |
115
166
  | `workerFactory` | `() => Worker` | Built-in Worker | For testing or custom Worker loading |
116
167
  | `sharedWorkerFactory` | `() => SharedWorker` | Built-in SharedWorker | For testing or custom SharedWorker loading |
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 |
117
169
  | Other Core config | Corresponding type | Core defaults | `storagePrefix`, heartbeat, TTL, etc. |
118
170
 
119
171
  ```ts
@@ -146,6 +198,12 @@ The full chain for `auto` is **SharedWorker -> Dedicated Worker -> main-thread l
146
198
 
147
199
  When neither `sharedWorkerFactory` nor `workerFactory` is provided, the transport detects global `SharedWorker` / `Worker` capability at startup. When a custom factory is provided, the corresponding backend is considered available, avoiding false negatives from global capability detection in Node, SSR, or embedded environments. All modes perform the same structured clone validation; config and `publish` data must be structured-clonable.
148
200
 
201
+ ## Coordination Channel Fallback (BroadcastChannel unavailable)
202
+
203
+ When `BroadcastChannel` is unavailable (some WebViews, older browsers), the cluster normally degrades to local-only operation: the transport works, but tabs do not coordinate ownership.
204
+
205
+ `createBrowserEnvironment({ channelFallback: 'storage-event' })` opts into a fallback `ClusterChannel` backed by localStorage `storage` events, preserving cross-tab coordination. It is opt-in because of a security trade-off: BroadcastChannel messages live in memory only, while the fallback writes coordination payloads (which carry plaintext topic names) to localStorage under the `cross-tab-worker-databus:channel:` key namespace — at least transiently, and indefinitely after a tab crash. The key is removed when the channel closes.
206
+
149
207
  ## SharedWorker Session Reaper
150
208
 
151
209
  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.
@@ -168,3 +168,38 @@ const config = await loadTransportConfig();
168
168
  await bus.start(config);
169
169
  bus.subscribe('resource.changed', handleResourceEvent);
170
170
  ```
171
+
172
+ ## 10. Health Summary & Coordination Fallback
173
+
174
+ Readiness probes and dashboards can consume the single-object health verdict instead of assembling diagnostics:
175
+
176
+ ```ts
177
+ const health = bus.getHealthSummary();
178
+ // { healthy: true, state: 'healthy', recovery: { attempt, … }, lastFailure: null, … }
179
+ ```
180
+
181
+ React and Vue adapters mirror it (`useCrossTabHealth`), refreshing on status changes, errors, and an interval.
182
+
183
+ When `BroadcastChannel` is unavailable, tabs normally degrade to local-only coordination. Opt into a localStorage storage-event fallback channel:
184
+
185
+ ```ts
186
+ import { createBrowserEnvironment } from 'cross-tab-worker-databus';
187
+
188
+ const bus = new CrossTabDataBus({
189
+ /* … */
190
+ environment: createBrowserEnvironment({ channelFallback: 'storage-event' })
191
+ });
192
+ ```
193
+
194
+ The fallback is opt-in because coordination payloads (plaintext topic names) then persist to localStorage — see [configuration.md](./configuration.md#coordination-channel-fallback-broadcastchannel-unavailable).
195
+
196
+ ## 11. Upgrading & Deprecation
197
+
198
+ Before `1.0.0` the SDK is allowed to grow additively and, behind an explicit deprecation cycle, remove APIs. The root export surface is pinned by the regression suite and a tag-to-tag compatibility gate, so an unintended removal fails CI rather than silently breaking consumers.
199
+
200
+ When you upgrade:
201
+
202
+ - Read the CHANGELOG for each version between your current and target version. Every removal is called out there, and the deprecation cycle itself only forwards through minor versions.
203
+ - If a runtime `console.warn` mentions a deprecated alias, migrate off it before the next minor — the alias survives for at least one minor after the warning first appears.
204
+ - Mixed-version tabs keep coordinating: the cluster carries a protocol version in diagnostics (`getDiagnostics().protocol`), and legacy message frames continue to be parsed for one minor after the protocol change.
205
+ - Pre-1.0, prefer the documented entry points in [api.md](./api.md) and the capabilities matrix in [capabilities.md](./capabilities.md). The published package is verified against both ESM and CommonJS consumers for every release.
@@ -2,16 +2,39 @@
2
2
 
3
3
  Use this checklist for each pre-1.0 release. The repository does not publish from the assistant; run the final npm command manually after reviewing the packed artifact.
4
4
 
5
+ ## Public API stability and deprecation policy (pre-1.0)
6
+
7
+ - The root export surface is pinned by `tests/dual-format.test.ts` (freeze test) and `scripts/verify-version-compat.mjs` (tag-to-tag compatibility gate). Adding or removing an export is a deliberate, reviewed change; add it to `docs/api.md` and the CHANGELOG in the same commit.
8
+ - Pre-1.0 breaking changes are allowed only with a deprecation cycle: keep the legacy alias for at least one minor version, emit a `console.warn` at first use, and remove it only in a minor whose CHANGELOG calls the removal out explicitly.
9
+ - Protocol aliases (worker/cluster/transport message shapes) follow the same rule: keep parsing legacy frames for one minor after deprecation so mixed-version peers stay compatible (see the protocol-version diagnostics in `getDiagnostics().protocol`).
10
+ - Advancing to `1.0.0` requires the public API and protocol deprecation policy to be frozen and a migration guide published (see `docs/roadmap.md`, 0.13.0 candidates).
11
+
12
+ ## Automated gates (CI)
13
+
14
+ The `CI` workflow's `verify` job runs `pnpm check`, `pnpm lint`, `pnpm test:coverage` (the `vitest.config.ts` floors: 85% statements / 80% branches / 90% functions / 85% lines), `pnpm verify:compat`, `pnpm verify:pack`, `pnpm bench`, and `pnpm audit` on every push and pull request; the `browser` job runs the Playwright E2E suite. The `Release` workflow repeats `verify:compat` and `verify:pack` before publishing, then runs the blocking `verify:published` gate. The checkout in both workflows uses `fetch-depth: 0` + `fetch-tags: true` because `verify:compat` resolves its baseline from the most recent release tag.
15
+
16
+ Only `pnpm bench:browser` / `pnpm bench:compare` stay local-only: shared-runner timing noise makes numeric CI gates unreliable.
17
+
5
18
  ## Before tagging
6
19
 
7
20
  1. Update `package.json`, `CHANGELOG.md`, and both roadmap files.
8
- 2. Run `pnpm check`, `pnpm lint`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, and `git diff --check`.
9
- 3. Confirm the package contains only intended files with `npm pack --dry-run --json`.
10
- 4. Commit, tag the exact version, and push `main --tags`.
21
+ 2. Run `pnpm check`, `pnpm lint`, `pnpm test:coverage`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, `pnpm verify:compat`, 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:compat` resolves its baseline from the most recent release tag, so a shallow or tag-less clone must first run `git fetch --tags` or it fails with "no version tag found".)
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: `pnpm bench:compare --fail-above-pct 50` after two `pnpm bench:browser` runs, using a 50% ceiling so unrelated runner noise (see the known shared-runner jitter note) cannot fail the gate; 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.
24
+ 5. Confirm the package contains only intended files with `npm pack --dry-run --json`.
25
+ 6. Commit, tag the exact version, and push `main --tags`.
26
+
27
+ ## Security and dependency scanning
28
+
29
+ The repository runs CodeQL (`javascript-typescript`; on push, on pull request, and weekly) and Dependabot (weekly npm + GitHub Actions updates). CodeQL alerts surface as pull-request checks; a Dependabot pull request must pass its verify (the full `pnpm check`) and CodeQL checks before merge, with the known shared-runner browser-E2E flake re-run as usual.
30
+
31
+ ## Tagged-release workflow
32
+
33
+ Pushing a version tag triggers the `Release` GitHub Action: it runs `pnpm check` and `pnpm lint` (a tag can point at a commit that never passed CI's lint step), runs `verify:compat` and `verify:pack`, opens the GitHub release from the `CHANGELOG` section, publishes to npm when the `NPM_TOKEN` secret is set, and then runs the **blocking** published-consumer verification with the same budget as a manual run (`PUBLISHED_VERIFY_ATTEMPTS=24`, `PUBLISHED_VERIFY_DELAY_MS=5000`). A release whose published package cannot be imported by a clean consumer fails the workflow — treat every `verify:published` failure as a failed release and republish the tag after fixing it. When no token is configured the publish step is skipped, but verification still passes against whatever version is already on npm (e.g. one published manually).
11
34
 
12
35
  ## Publishing
13
36
 
14
- Run `npm publish --access public` from the tagged checkout. A version already present on npm cannot be published again. Historical versions missing from npm must be rebuilt from their exact git tags and reviewed individually; never publish the current tree under an old version.
37
+ For a manual release (no `NPM_TOKEN` in the workflow), run `npm publish --access public` from the tagged checkout. A version already present on npm cannot be published again. Historical versions missing from npm must be rebuilt from their exact git tags and reviewed individually; never publish the current tree under an old version.
15
38
 
16
39
  ## After publishing
17
40
 
@@ -19,4 +42,4 @@ Run `npm publish --access public` from the tagged checkout. A version already pr
19
42
  2. Install the published tarball or version in a clean consumer and import the root plus every public subpath.
20
43
  3. Record the result in the release notes. Do not advance to `1.0.0` until the public API and protocol deprecation policy are explicitly frozen.
21
44
 
22
- Run `PUBLISHED_VERSION=0.20.8 pnpm verify:published` after npm publication. The verifier retries while the registry propagates the tarball; tune `PUBLISHED_VERIFY_ATTEMPTS` and `PUBLISHED_VERIFY_DELAY_MS` only for unusually slow mirrors.
45
+ The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. Tune `PUBLISHED_VERIFY_ATTEMPTS` and `PUBLISHED_VERIFY_DELAY_MS` only for unusually slow mirrors.
package/docs/roadmap.md CHANGED
@@ -1,6 +1,82 @@
1
1
  # Roadmap
2
2
 
3
- 0.20.71 is the current development line. The project is intentionally continuing through reliability-focused minor releases before a 1.0.0 stability freeze.
3
+ 0.20.86 is the current development line. The project is intentionally continuing through reliability-focused minor releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.20.86 delivered scope
6
+
7
+ - Lifecycle hardening across explicit stop/start boundaries: queued restarts are serialized with in-flight stops, canceled by a newer stop, and observable through `ready()`; superseded asynchronous opens cannot tear down newer suspend/resume transitions; stop-time `subscribe()` and non-empty `publish()`/`publishBatch()` calls now report through `onError` instead of mutating teardown state or being silently dropped.
8
+ - Explicit `start()` now performs the documented manual recovery after automatic recovery exhaustion, while preserving cluster state, subscriptions, and replay history.
9
+ - IndexedDB replay persistence settles all mutations on transaction abort (including connection-loss aborts) so the serialized queue cannot remain blocked, and replay age pruning now uses one shared, position-independent policy for in-memory and persisted history.
10
+ - The configuration reference documents the full replay/dedup public option surface in both languages, with declaration-derived documentation guards; seeded property invariants cover active-worker selection and rebalance targets.
11
+
12
+ ## 0.20.85 delivered scope
13
+
14
+ - A seeded property suite (`tests/property.test.ts`) for the pure hot-path helpers and the stateful managers: finiteness/totality, order-independent selection, cycle-safe sizing, publication topic/metadata validity, `serializeError` cloneability, and dedup/replay bounds under long random operation sequences.
15
+ - `effectiveWorkerLoad` is total against a corrupt stored base load — a non-finite value (JSON `1e999` → `Infinity`) can no longer leak into owner selection and re-introduce array-order dependence.
16
+ - `approximatePayloadBytes` is depth-bounded, so a cyclic payload (structured clone preserves cycles) can no longer overflow the stack in the replay-buffer footprint or the adaptive-load sampler.
17
+ - `serializeError` always produces a structured-cloneable result; a non-cloneable context (function/symbol) is dropped instead of making the error report itself throw `DataCloneError`.
18
+ - Replay history is bounded when `pruneStrategy: 'age'` is set without a `retentionMs`: the count cap now applies in both the in-memory ring and the IndexedDB record.
19
+
20
+ ## 0.20.84 delivered scope
21
+
22
+ - Release/CI gates are enforced instead of advisory: `pnpm test:coverage`, `pnpm verify:compat`, and `pnpm verify:pack` run in the CI verify job, the Release workflow re-runs lint + compat + pack before publishing, and both checkouts fetch full history and tags so the compat baseline resolves.
23
+ - Coordination recovery hardening: a lost handoff ACK, a crashed owner, or the previous owner's departure now recovers through a worker-TTL-gated re-election with a single writer and projected-load spreading, and each route acknowledgment / migration / recovery is observable as a bounded `reliability` trace event.
24
+ - Three real correctness fixes: the Vue `useCrossTabDataBus` unmount leak (a pending start could create a bus with no owner), the adaptive dedup TTL not taking effect on the hot path, and `effectiveWorkerLoad` leaking a non-finite score from a corrupt stored load back into owner selection.
25
+ - Product-demo observability: the event feed renders reliability / subscription / coordination trace events, chaos toggles exercise the dropped-ACK and crash recovery paths in a real browser, and the config panel shows the active chaos mode.
26
+ - Coverage and toolchain: direct `ReplayManager` / `DedupManager` suites, transport error-isolation and partial-metadata coverage, vitest 5 (benchmark API migrated) and the eslint 10 lint config, with TypeScript 7 deferred until typescript-eslint supports it.
27
+
28
+ ## 0.20.83 delivered scope
29
+
30
+ - Adapter edge-case coverage for the health hook, archived browser benchmarks with a comparison script, and a README feature list aligned with current capabilities.
31
+ - A large internal cleanup: every runtime string literal centralized in `utils/constants.ts` with literal-derived types, and replay/dedup split out of `CrossTabDataBus` into self-contained `ReplayManager` / `DedupManager` classes.
32
+ - The demo's "批量 10" publishBatch button with `/debug/wsstats` frame counting and a single-frame E2E, `asyncSink: true` delivery-semantics documentation, and a release checklist aligned with the blocking published-consumer gate.
33
+
34
+ ## 0.20.82 delivered scope
35
+
36
+ - A 20 s default E2E assertion ceiling, structured-clone rejection coverage, and shared-mode session lifecycle verified end to end through the examples server's connection-count endpoint.
37
+
38
+ ## 0.20.81 delivered scope
39
+
40
+ - The browser benchmark gained the data-bus hot-path matrix, health summaries are asserted end to end in E2E, and the storage-event channel plus transport batching joined the API docs and capabilities matrix.
41
+
42
+ ## 0.20.80 delivered scope
43
+
44
+ - E2E reliability governance: failure traces/videos with longer retention, converge-before-publish patterns for reload tests, a staggered burst pattern within documented guarantees, and the loss-and-recovery matrix in the architecture docs.
45
+
46
+ ## 0.20.79 delivered scope
47
+
48
+ - Published-consumer verification became a blocking release gate, and the lost-handoff-ACK recovery chain (TTL cleanup + resume re-election) is pinned by a regression.
49
+
50
+ ## 0.20.78 delivered scope
51
+
52
+ - E2E now asserts the real transport backend per tab, the deferred-close handoff invariant is pinned by a regression test and documented, and the getting-started guide covers health summaries and the coordination fallback.
53
+
54
+ ## 0.20.77 delivered scope
55
+
56
+ - Fixed a silent local-session degradation for factory-less consumers (bundled Workers are now actually used), added default-backend and channel loss-recovery coverage, and surfaced coordination-channel diagnostics plus the fallback toggle in the demo.
57
+
58
+ ## 0.20.76 delivered scope
59
+
60
+ - Opt-in storage-event coordination fallback for BroadcastChannel-less environments, with an owner-election integration test over the fallback channel and updated degradation documentation and capabilities matrix.
61
+
62
+ ## 0.20.75 delivered scope
63
+
64
+ - Hot-path performance gates joined the unit suite, and IndexedDB replay persistence gained scripted fault-injection coverage for its invalidate-and-recover error paths. The Release workflow's published-consumer verification was audited and confirmed complete.
65
+
66
+ ## 0.20.74 delivered scope
67
+
68
+ - Optional `DataBusTransport.publishBatch` with a one-frame WebSocket implementation, demo-server support, and per-item fallback; `useCrossTabHealth` bindings for React and Vue; health verdict now honors the live transport status.
69
+
70
+ ## 0.20.73 delivered scope
71
+
72
+ - IndexedDB replay persistence is now covered by unit tests (via `fake-indexeddb`) across pruning strategies, batch grouping, mutation serialization, cleanup semantics, and transient open-failure recovery.
73
+ - Real-browser E2E now covers concurrent multi-publisher bursts and full connection re-apply; the architecture docs gained a stability-invariants reference (English and Chinese).
74
+
75
+ ## 0.20.72 delivered scope
76
+
77
+ - Expanded the benchmark matrix across publish batching, wildcard routing, deduplication, replay pruning, bulk persistence, and asynchronous trace sinks.
78
+ - Long-session stability hardening: regression coverage for handoff ACK generation validation, repeated BFCache round-trips, recovery exhaustion reset, storage write backoff recovery, and replay persistence cleanup races; fixed an inverted stale-ACK generation check and a batch-flush resurrection race in replay cleanup.
79
+ - Production capabilities: `getHealthSummary()` readiness verdict, `getPersistenceStats()`, transport status/suspended granularity in diagnostics, and a build-time injected SDK version.
4
80
 
5
81
  ## 0.20.71 delivered scope
6
82
 
@@ -398,10 +474,10 @@
398
474
 
399
475
  ## 0.20.69 candidates
400
476
 
401
- 1. Publish a peer capability matrix and expose SDK/backend/transport identity in diagnostics.
402
- 2. Unify replay, deduplication, trace, recovery, and cluster health counters.
403
- 3. Optimize IndexedDB concurrent append and cleanup paths.
404
- 4. Add performance baselines for adaptive dedup, async trace, pruning, and long-running multi-tab workloads.
477
+ 1. ~~Publish a peer capability matrix and expose SDK/backend/transport identity in diagnostics.~~ Delivered: `getDiagnostics()` carries protocol version, unknown-message stats, peer protocol versions, and transport identity; `getHealthSummary()`/`getMetrics()` now ship live trace metrics and sink state.
478
+ 2. ~~Unify replay, deduplication, trace, recovery, and cluster health counters.~~ Delivered: `getDiagnostics()` + `getMetrics()` + `getHealthSummary()` cover lifecycle, recovery, dedup, replay, persistence, protocol, transport, cluster, trace metrics, and sink back-pressure in single snapshots.
479
+ 3. ~~Optimize IndexedDB concurrent append and cleanup paths.~~ Delivered: adjacent `appendBatch` mutations coalesce into one transaction; clear/clearTopic/clearBefore ordering preserved.
480
+ 4. ~~Add performance baselines for adaptive dedup, async trace, pruning, and long-running multi-tab workloads.~~ Partially delivered: bench covers load-weighting scoring, `getMetrics`, publishBatch batch-size sensitivity, and the existing dedup/async-sink/persistence cases.
405
481
 
406
482
  ## 0.13.0 candidates
407
483
 
package/docs/zh/README.md CHANGED
@@ -13,8 +13,9 @@
13
13
  | [架构说明](./architecture.md) | Worker 集群、路由、存储、迁移和降级设计 |
14
14
  | [能力矩阵](./capabilities.md) | 已实现、未实现和计划待实现的能力矩阵 |
15
15
  | [路线图](./roadmap.md) | 面向版本的优先级与验证清单 |
16
+ | [基准趋势](./benchmarks.md) | 基于 `bench-results/` 自动生成的浏览器基准历史 |
16
17
  | [发布检查清单](./release-checklist.md) | 本地门禁、打 tag、发布和发布后验证 |
17
- | [../..//examples/demo](../../examples/demo) | 可运行的多标签浏览器演示 |
18
+ | [../../examples/demo](../../examples/demo) | 可运行的多标签浏览器演示 |
18
19
  | [../../CHANGELOG.md](../../CHANGELOG.md) | 版本变更记录 |
19
20
 
20
21
  ## 阅读顺序
package/docs/zh/api.md CHANGED
@@ -56,7 +56,7 @@ new CrossTabDataBus<TConfig, TData>(options)
56
56
  start(config: TConfig): Promise<void>
57
57
  ```
58
58
 
59
- 启动集群协调和 transport。首次调用真正启动 transport;启动过程中并发调用共享同一个启动 Promise,不重复创建 transport。启动成功或失败后,内部 gate 会重置:之后再次调用是已启动的空操作(立即 resolve),不会重复启动;`stop()` 之后可重新调用再次启动。
59
+ 启动集群协调和 transport。首次调用真正启动 transport;打开过程尚未结束时,并发调用共享同一个启动 Promise,不重复创建 transport。对健康且已启动的实例调用是立即 resolve 的空操作。若 transport 已断开,`start()` 作为显式手动恢复:保留 cluster、订阅和 replay 缓冲区,重置失败/恢复账本并重新打开 transport。若显式 `stop()` 尚未完成,`start()` 会在清理之后排队一次全新启动,并返回随重启完成而 settle 的 Promise。该排队重启归属于最近一次生命周期意图:若它在真正执行前又收到 `stop()`,则会被取消(排队 start 的 Promise resolve,但不会打开 transport);取消之后再调用 `start()` 会以更高令牌重新排队。`stop()` 完成后也可正常再次调用 `start()` 重启。
60
60
 
61
61
  ### `ready()`
62
62
 
@@ -66,6 +66,12 @@ ready(): Promise<void>
66
66
 
67
67
  等待当前 transport 的 `start` 完成。自动启动失败时 Promise 会 reject;再次调用可以触发基于 `initialConfig` 的重试。
68
68
 
69
+ 显式 `stop()` 尚未 settle 时,`ready()` 会 reject,除非此前已有 `start()` 在该 stop 之后排队重启;它绝不会针对正在拆除的 transport 报告 ready。调用方应等待 `stop()` settle,再调用 `start()` 后重新 await `ready()`。
70
+
71
+ 若后续 `stop()` 取消了该排队重启,排队 `start()` Promise 仍按既定语义 resolve 且不会打开 transport,但 `ready()` 会以生命周期错误 reject,而不会把已停止的 bus 报告为 ready。
72
+
73
+ 若该排队重启在 transport 启动阶段失败,即使未传入 `initialConfig`,`ready()` 也会以底层启动错误 reject。该失败会保留给显式恢复,而不会被通用的「缺少配置」错误掩盖。
74
+
69
75
  未传入 `initialConfig` 且未显式调用 `start(config)` 时,`ready()` 返回 rejected Promise 而不是同步抛出,调用方可以统一通过 `.catch` 处理并决定是否显式启动。
70
76
 
71
77
  `ready()` 不等价于服务端已连接,协议连接状态通过 `onStatus` 获取。
@@ -85,8 +91,9 @@ subscribe(
85
91
  - 当前 Tab 第一个 handler 会登记集群订阅。
86
92
  - 最后一个 handler 释放后,当前 Tab 才退出该 Topic。
87
93
  - transport 尚未 ready 时订阅自动排队。
94
+ - 显式 `stop()` 尚未 settle 时发起的订阅不会登记:`subscribe()` 通过 `onError` 上报并返回 no-op 释放函数。调用方应等待 `stop()` settle,再调用 `start()` 后重新订阅。
88
95
  - 通配符订阅:以 `.*` 结尾的 Topic(如 `chat.*`)匹配任意后缀,`*` 匹配全部。pattern 以字面量参与路由、归属与传输订阅;携带匹配的具体 topic(或 pattern 本身)的发布都会投递给通配 handler。匹配规则见下方 `topicMatchesPattern`。
89
- - 重放(可选):构造 bus 时传 `replay: { maxPerTopic }` 开启缓冲,`maxPerTopic` 必须是正安全整数;`subscribe()` 第三个参数传 `{ replay: true | n }` 后,新 handler 会立即收到缓冲历史(最多 `n` 条,受 `maxPerTopic` 上限约束,默认 100),消息带 `message.replayed: true` 标记——晚加入的 handler 不会错过更早的发布。只有被分发过的消息才入缓冲(无本地订阅者的 topic 会被 owner 丢弃);缓冲仅存内存,该 topic 最后一个 handler 退订时清空。通配订阅会对所有匹配 pattern 的已缓冲 topic 做回放。需要跨 reload/BFCache 持久化时,可传入 `createIndexedDbReplayPersistence({ maxPerTopic })` 创建的 `persistence`;持久化为异步操作,失败会通过 `onError` 报告,不影响实时投递。设置 `retentionMs` 后,如果 adapter 支持 `clearBefore`,会在 hydrate 和追加后自动清理过期历史。设置 `persistenceRetry: { maxAttempts, backoffMs }` 可重试瞬时持久化失败;默认仍保持单次尝试。
96
+ - 重放(可选):构造 bus 时传 `replay: { maxPerTopic }` 开启缓冲,`maxPerTopic` 必须是正安全整数;`subscribe()` 第三个参数传 `{ replay: true | n }` 后,新 handler 会立即收到缓冲历史(最多 `n` 条,受 `maxPerTopic` 上限约束,默认 100),消息带 `message.replayed: true` 标记——晚加入的 handler 不会错过更早的发布。只有被分发过的消息才入缓冲(无本地订阅者的 topic 会被 owner 丢弃);缓冲仅存内存,该 topic 最后一个 handler 退订时清空。通配订阅会对所有匹配 pattern 的已缓冲 topic 做回放。需要跨 reload/BFCache 持久化时,可传入 `createIndexedDbReplayPersistence({ maxPerTopic })` 创建的 `persistence`;持久化为异步操作,失败会通过 `onError` 报告,不影响实时投递。设置 `retentionMs` 后会清理内存中过期的 producer-timestamped 历史,并通过实现 `clearBefore` 的 adapter 在 hydrate 和追加后清理 durable 历史。设置 `persistenceRetry: { maxAttempts, backoffMs }` 可重试瞬时持久化失败;默认仍保持单次尝试。设置 `pruneStrategy` 为 `'count'`(默认)、`'age'` 或 `'both'`,分别表示按 `maxPerTopic` 截断、按 `retentionMs` 清理带时间戳历史,或两者都应用。`age` 下无时间戳的 legacy 条目会保留,但受 `maxPerTopic` 限制;带时间戳条目由 retention 窗口约束。
90
97
  启用 trace 后,重试会发出 `reliability` 事件,包含 `operation: 'persistence_retry'`、有界的 `persistenceOperation` 和 `attempt`。
91
98
 
92
99
  WebSocket transport 支持以 `ArrayBuffer` 或浏览器 `Blob` 帧接收二进制 publication。
@@ -118,6 +125,8 @@ publish(
118
125
 
119
126
  当 owner 是远端 Tab、且发布控制消息无法投递时(例如 BroadcastChannel 无法克隆 payload),`publish()` 会通过 `onError` 上报失败,而不是静默丢弃。
120
127
 
128
+ 在 `stop()` 尚未 settle 时调用 `publish()` 会通过 `onError` 上报且不路由任何消息;消息不会延迟到之后的 start。更早发出、仍排队等待 transport open 的发布会被 stop 取消。
129
+
121
130
  传入 `options.messageId` 和 `options.timestamp` 后,元数据会穿过跨 Tab 路由、Worker 边界和支持的 transport。服务端必须回显或以其他方式保留它们,入站去重和 replay retention 才能使用。
122
131
 
123
132
  `DataBusMessage` 与 `DataBusPublication` 暴露相同的可选元数据。
@@ -130,6 +139,21 @@ publish(
130
139
  }
131
140
  ```
132
141
 
142
+ ### `publishBatch(topic, items)`
143
+
144
+ ```ts
145
+ publishBatch(
146
+ topic: string,
147
+ items: Array<{ data: unknown; messageId?: string; timestamp?: number }>
148
+ ): void
149
+ ```
150
+
151
+ 把多条 item 作为一个工作单元发布到同一个 topic。内置 WebSocket transport 会把整个 batch 打包成**一帧**(`publishBatch` op),而不是逐条一帧;未实现可选钩子 `DataBusTransport.publishBatch` 的 transport 会自动回退逐条 `publish()`,因此两种情况下调用都安全。
152
+
153
+ 每条 item 的 `messageId` 与 `timestamp` 在传输后保留,dedup、replay 与顺序都按 item 维度、以源顺序生效。空 batch 为 no-op;单 item batch 直接委托给 `publish()`。直接操作协调层的调用方可用 `WorkerClusterRuntime` 上的同名方法。
154
+
155
+ 在 `stop()` 尚未 settle 时提交非空 batch 会通过 `onError` 上报且不发送任何内容;空 batch 仍为 no-op。
156
+
133
157
  ### `clearReplay()`
134
158
 
135
159
  ```ts
@@ -170,6 +194,65 @@ getStatus(): WorkerStatus
170
194
 
171
195
  返回当前状态:`connecting`、`connected`、`disconnected` 或 `error`。
172
196
 
197
+ ### `getHealthSummary()`
198
+
199
+ ```ts
200
+ getHealthSummary(): DataBusHealthSummary
201
+ ```
202
+
203
+ 面向仪表盘、就绪探针与支持包的紧凑健康判定。先回答「总线当前是否可用」,再附带解释该结论的失败与恢复上下文:
204
+
205
+ ```ts
206
+ interface DataBusHealthSummary {
207
+ healthy: boolean; // 已启动、未挂起、transport 就绪
208
+ state: 'stopped' | 'starting' | 'healthy' | 'recovering' | 'suspended' | 'degraded';
209
+ status: WorkerStatus;
210
+ sdkVersion: string;
211
+ started: boolean;
212
+ suspended: boolean; // Tab 隐藏(BFCache)期间为 true
213
+ transport: { name; backend; ready; status };
214
+ recovery: { attempt; exhausted; maxAttempts; generation; lastSuccessAt; hasError; errorMessage; errorAt };
215
+ lastFailure: { source: 'transport' | 'persistence' | 'dispatch'; message: string; at: number } | null;
216
+ persistence: { failures: number; lastFailureAt: number | null; lastErrorMessage: string | null };
217
+ metrics: DataBusMetricsSnapshot | null; // 实时 trace 窗口;trace 指标未启用时为 null
218
+ trace: { asyncSink: boolean; pendingEvents: number }; // sink 背压可见性
219
+ }
220
+ ```
221
+
222
+ `state` 语义:`stopped`(未启动)、`starting`(首次连接进行中)、`recovering`(transport 自动恢复进行中)、`suspended`(Tab 隐藏,pageshow 后自动恢复)、`degraded`(自动恢复已耗尽,需要手动 `start()` 或重新 subscribe 触发恢复)、`healthy`。处于 degraded 时再次调用 `start()` 会保留 cluster、订阅和 replay 缓冲区,重置失败/恢复账本后重新打开 transport;subscribe 与 publish 也走同一恢复路径。`lastFailure` 是覆盖全部失败来源的统一账本,每次显式 `start()` 后重置。
223
+
224
+ ### `getMetrics()`
225
+
226
+ ```ts
227
+ getMetrics(): DataBusMetricsSnapshot | null
228
+ ```
229
+
230
+ 对当前 trace 指标窗口的**同步、非破坏性**快照——与周期性 `message_metrics` 事件相同的派生计数(received、dispatched、topics,分发延迟 avg/p50/p95/max,dedup accepted/suppressed),无需 sink 或间隔 flush 即可按需读取。trace 指标未启用(禁用或 events-only 模式)时返回 `null`。
231
+
232
+ `getDiagnostics().replay` 输出 `{ enabled, topics, messages, bytes }` —— `bytes` 是缓冲 replay 环的近似内存 payload 占用(与自适应负载加权相同的 string/binary/number 尺寸启发式),按需计算,热路径 append 不为此付任何成本。
233
+
234
+ ### `getRecoveryStats()` / `getPersistenceStats()`
235
+
236
+ ```ts
237
+ getRecoveryStats(): { attempt; exhausted; maxAttempts; hasError; errorMessage; errorAt; generation; lastSuccessAt }
238
+ getPersistenceStats(): { failures; lastFailureAt; lastErrorMessage }
239
+ ```
240
+
241
+ `recovery.generation` 在每次 transport 成功打开时递增(首次启动与每次恢复);`lastSuccessAt` 是该次成功的时间戳(首次成功前为 `null`)。持久化计数仅覆盖可选的 replay 持久化后端。
242
+
243
+ ### `getDiagnostics()`
244
+
245
+ ```ts
246
+ getDiagnostics(): DataBusDiagnostics
247
+ ```
248
+
249
+ 完整诊断快照,合并生命周期、transport 身份(`name`、`backend`、实时 `status`、`suspended`)、恢复、dedup、replay、持久化、协议(`version`、`unknownMessages`、`peers`)与集群快照,外加两个仅用于诊断的字段:
250
+
251
+ - `metrics: DataBusMetricsSnapshot | null` — 当前 trace 指标窗口(吞吐、分发延迟百分位、dedup 结果);trace 指标未启用(禁用或 events-only 模式)时为 `null`。
252
+ - `trace: { asyncSink: boolean; pendingEvents: number }` — sink 投递模式与排队事件深度;`asyncSink: true` 下 `pendingEvents` 持续增长是 sink 背压的第一个信号。
253
+
254
+ `sdkVersion` 构建时从 `package.json` 注入。只需要就绪结论的调用方应优先使用 `getHealthSummary()`。
255
+
173
256
  ### `getClusterSnapshot()`
174
257
 
175
258
  返回诊断快照:
@@ -206,7 +289,9 @@ trace: {
206
289
  }
207
290
  ```
208
291
 
209
- 低频事件类型包括 `lifecycle`、`status`、`subscription`、`coordination` 和 `error`;高频数据按窗口输出 `message_metrics`,包含接收/分发计数、活跃 Topic 数量和分发延迟聚合(`dispatchSamples`、`dispatchAvgMs`、`dispatchP50Ms`、`dispatchP95Ms`、`dispatchMaxMs`),以及去重结果(`dedupAccepted`、`dedupSuppressed`)。所有公开事件都使用固定结构,不包含原始 Topic、消息 payload、连接地址或错误正文。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,便于定位诊断配置问题。sink 应尽量避免抛出异常——预期中的错误条件应通过事件数据表达,而不是通过异常上报。
292
+ 低频事件类型包括 `lifecycle`、`status`、`subscription`、`coordination`、`reliability` 和 `error`;高频数据按窗口输出 `message_metrics`,包含接收/分发计数、活跃 Topic 数量和分发延迟聚合(`dispatchSamples`、`dispatchAvgMs`、`dispatchP50Ms`、`dispatchP95Ms`、`dispatchMaxMs`),以及去重结果(`dedupAccepted`、`dedupSuppressed`)。路由归属变化通过 `reliability` 事件呈现:优雅交接上报 `operation: 'route_migration'`,而恢复悬挂未确认交接的重选上报 `operation: 'route_migration_recovery'`(前任 owner 已消失且 ACK 始终未到达),便于 trace 消费者区分恢复与常规交接。`coordination` 事件在每次 transport 打开后发出,携带 `coordinated`、`activeWorkers`、`workers`(格式化后的 worker 记录)与 `routes`(`topicKey@workerId|confirmed=…`,反映已收敛的路由列表)。所有公开事件都使用固定结构,不包含原始 Topic、消息 payload、连接地址或错误正文。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,便于定位诊断配置问题。sink 应尽量避免抛出异常——预期中的错误条件应通过事件数据表达,而不是通过异常上报。
293
+
294
+ **`asyncSink: true` 的投递语义。** 默认(`false`)下,每条事件同步调用 sink。开启 `asyncSink: true` 后,事件先入内存队列,在**一个微任务批次**中统一投递:任务的第一条事件调度 `queueMicrotask`,在该微任务运行前产生的所有事件(含周期性 `message_metrics` 快照)按 FIFO 顺序一次性送往 sink。这样热路径永远不会因 sink 工作而阻塞。错误隔离与同步模式一致——sink 抛错被捕获并记入 `console.warn`,既不会中断分发,也不会中断批次内其余事件。顺序保证边界:**批次内**顺序是确定的,但投递被推迟到下一个微任务,因此事件不再保证在你的下一行语句执行前可见;批次 flush 之后新产生的事件会落入后续批次。当需要"每条事件在下一行代码前可见"时,请使用默认的同步 sink(或自行合并计数)。
210
295
 
211
296
  ### `stop()`
212
297
 
@@ -214,7 +299,7 @@ trace: {
214
299
  stop(): Promise<void>
215
300
  ```
216
301
 
217
- 永久销毁当前实例:清理 handler、集群注册、路由、Worker 和 transport。普通页面隐藏和恢复不需要调用。
302
+ 永久销毁当前实例:清理 handler、集群注册、路由、Worker 和 transport。若 transport open/reopen 仍在收敛,`stop()` 会等待它结束并使该结果失效,确保它不会在 stop 后变为 ready。普通页面隐藏和恢复不需要调用。
218
303
 
219
304
  ## `DataBusTransport<TConfig, TData>`
220
305
 
@@ -224,6 +309,8 @@ interface DataBusTransport<TConfig, TData> {
224
309
  subscribe(topic): void | Promise<void>;
225
310
  unsubscribe(topic): void | Promise<void>;
226
311
  publish(topic, data): void | Promise<void>;
312
+ /** 可选:将多条消息合并为一帧发送。未提供时 DataBus 回退为逐条 `publish`。 */
313
+ publishBatch?(topic, items): void | Promise<void>;
227
314
  stop(): void | Promise<void>;
228
315
  }
229
316
  ```
@@ -271,6 +358,18 @@ const transport = new CentrifugeWorkerTransport({
271
358
  - `workerFactory`:自定义 Dedicated Worker 加载方式
272
359
  - `sharedWorkerFactory`:自定义 SharedWorker 加载方式
273
360
 
361
+ ## `createStorageEventChannel(options)`
362
+
363
+ ```ts
364
+ createStorageEventChannel(options: {
365
+ name: string;
366
+ storage: StorageLike | null;
367
+ win: StorageEventWindow | null;
368
+ }): ClusterChannel | null
369
+ ```
370
+
371
+ 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。投递语义与 BroadcastChannel 一致(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入);载荷信封内的单调序列号保证连续相同消息仍可投递。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
372
+
274
373
  ## WebSocket 传输后端
275
374
 
276
375
  基于原生 WebSocket 的零依赖传输。任何实现下列 JSON 帧协议的服务器都能驱动与 Centrifuge 后端相同的跨 Tab 集群栈(owner 去重、粘性路由、故障转移)。
@@ -334,6 +433,10 @@ React(>= 18)是可选 peer 依赖;独立入口保证非 React 消费者不
334
433
 
335
434
  把 `bus.onStatus()` 镜像为 React 状态,bus 身份变化时同步读取当前值。返回 `'connecting' | 'connected' | 'disconnected' | 'error'`。
336
435
 
436
+ ### `useCrossTabHealth(bus, options?)`
437
+
438
+ 将 `bus.getHealthSummary()` 镜像为 React 状态(`DataBusHealthSummary | null`)。由于健康摘要是快照而非事件流,该 hook 按间隔轮询(默认 1000 ms;传 `{ intervalMs: 0 }` 可仅依赖事件驱动刷新),并在状态变化与错误发生时立即刷新。bus 创建前返回 `null`。
439
+
337
440
  ## Vue Composables(`cross-tab-worker-databus/vue`)
338
441
 
339
442
  Vue 3.3+ 是可选 peer 依赖;独立入口不会影响核心包。
@@ -346,6 +449,10 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
346
449
 
347
450
  `useCrossTabDataBus` 返回 Vue `Ref`,在组件挂载时创建 bus、卸载时停止。`useCrossTabSubscription` 接受字符串或 `Ref<string>` topic,在 bus/topic 变化时自动重绑。`useCrossTabStatus` 返回与 `bus.onStatus()` 同步的 `Ref<WorkerStatus>`。
348
451
 
452
+ ### `useVueCrossTabHealth(bus, options?)`
453
+
454
+ `useCrossTabHealth` 的 Vue 绑定:将 `bus.getHealthSummary()` 镜像为 Vue `Ref<DataBusHealthSummary | null>`。健康摘要是快照而非事件流,因此该组合式函数按间隔轮询(默认 1000 ms;传 `{ intervalMs: 0 }` 可仅依赖事件驱动刷新),并在状态变化与错误发生时立即刷新。bus 创建前返回 `null`。
455
+
349
456
  ## `WorkerClusterRuntime`
350
457
 
351
458
  高级 API,负责 Worker 注册、心跳、可见性、路由、BroadcastChannel 协议和迁移。业务模块不应直接操作它。
@@ -356,6 +463,7 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
356
463
  - `setStatus(status)`
357
464
  - `subscribe(topic)` / `unsubscribe(topic)`
358
465
  - `publish(topic, data)`
466
+ - `publishBatch(topic, items)`
359
467
  - `broadcastEvent(eventType, payload)`
360
468
  - `isAssigned(topic)`
361
469
  - `isActiveWorker()`
@@ -372,6 +480,12 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
372
480
 
373
481
  创建默认浏览器环境适配器,包含 storage、BroadcastChannel、定时器和页面生命周期事件。
374
482
 
483
+ ### `getOrCreateTabId(environment, key?)`
484
+
485
+ 返回当前页面/标签页实例的稳定标识:优先从 sessionStorage 读取,首次调用时创建(`tab-<random>`)并写回,因此刷新后的标签页会认领原有 route,而不是被当成一个全新标签页。
486
+
487
+ 存在 `window.opener` 时**刻意不**复用已存储的值:`window.open()` 会把 opener 的 `sessionStorage` 克隆给子页面,盲目复用会让两个存活的标签页共用一个身份。storage 不可用或抛错时改为生成新 ID。
488
+
375
489
  ### `selectWorkerBackend(mode, availability?)`
376
490
 
377
491
  按 `WorkerMode` 和能力检测选择实际后端,返回 `'shared' | 'dedicated' | 'local'`:
@@ -381,6 +495,22 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
381
495
 
382
496
  `availability` 可显式传入 `worker` / `sharedWorker` 能力标记,用于 SSR、测试或嵌入环境,避免访问不存在的全局对象。
383
497
 
498
+ ### `effectiveWorkerLoad(worker, options?)`
499
+
500
+ 最少负载 owner 选择背后的纯打分函数:`worker.load`(拥有的 Topic 数)加上 `options` 中 `loadWeighting` 权重对应的流量与调度滞后项。
501
+
502
+ 打分结果始终是有限值。没有吞吐采样、权重全为 0(未启用)、采样窗口非正或非有限、或加权和为非有限时,都退回原始 Topic 数——非有限分数永远无法正确比较,会让 owner 选择取决于 Worker 数组顺序而非负载。权重含义见 [configuration.md](./configuration.md#自适应-owner-加权)。
503
+
504
+ ### `approximatePayloadBytes(payload)`
505
+
506
+ 低成本、零分配的 payload 线长估算,用于自适应负载采样的字节侧。仅在启用自适应路由时运行,因此近似值足够——目标是跨 Worker 的稳定比较,而非精确字节数。
507
+
508
+ 尺寸规则:`null`/`undefined` 与 symbol/function 为 `0`,boolean 为 `4`,number 与 bigint 为 `8`,字符串为长度,`ArrayBuffer` 与 TypedArray 视图为 `byteLength`,数组为 8 字节头部加各元素之和,普通对象为各值之和。
509
+
510
+ ### `DEFAULT_MAX_ACTIVE_WORKERS`
511
+
512
+ 同时拥有 Topic 的 Worker 数上限默认值(`3`)。它限制 fan-out 广度:只有这么多 Worker 有资格成为新 route 的 owner,因此二十个标签页的集群仍会把所有权集中在少数几个上,而不是摊薄。可通过集群选项 `maxActiveWorkers` 覆盖。
513
+
384
514
  ### 路由选择函数
385
515
 
386
516
  - `selectActiveWorkers`