cross-tab-worker-databus 0.21.10 → 0.21.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/core/cluster.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/core/cluster.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  selectRebalanceTarget,
21
21
  selectWorkerBackend,
22
22
  topicMatchesPattern
23
- } from "./chunk-L5P66THO.js";
23
+ } from "./chunk-UO57SLQD.js";
24
24
  import {
25
25
  DEFAULT_STORAGE_PREFIX,
26
26
  PRUNE_STRATEGY,
@@ -39,9 +39,13 @@ export declare class PortReaper {
39
39
  * setTimeout() overrides it. register() always calls schedule() so the
40
40
  * reaper interval starts as soon as the first port connects. */
41
41
  register(port: MessagePort, target: ReapTarget): void;
42
- /** Record activity on a port (any incoming message). No-op for untracked
43
- * ports — a late PING from a port that was already removed/STOP'd must not
44
- * resurrect it in the reaper's tracking maps. */
42
+ /** Record activity on a port (any incoming message). The early return is what
43
+ * keeps a late PING from a removed port out of `lastSeenAt`: `reap()` iterates
44
+ * `targets`, so a row here would be unreachable rather than dangerous — measured,
45
+ * deleting the guard passes every test in the suite, because the only difference is
46
+ * one stale map entry per dead port. It stays as the cheap invariant that the three
47
+ * maps hold exactly the tracked ports, which is what `reap()`'s `??` fallbacks and
48
+ * this file's tracking contract both lean on. */
45
49
  touch(port: MessagePort): void;
46
50
  /** Override a port's session timeout from its INIT heartbeat config.
47
51
  * `Infinity` is the documented way to disable the PING heartbeat, and it means
@@ -71,8 +75,13 @@ export declare class PortReaper {
71
75
  * be unit-tested in isolation from the timer plumbing. */
72
76
  private computeMinHeartbeat;
73
77
  /** Close sessions whose port has been silent longer than its timeout.
74
- * Iterates a snapshot so closing a target (which mutates `targets`) during
75
- * the loop cannot skip a subsequent entry or visit one twice. */
78
+ * Iterates a snapshot. That is defensive rather than load-bearing: `Map`
79
+ * iteration in JS is deletion-safe, so removing the current entry cannot skip the
80
+ * next one, and the only case a snapshot changes is a re-entrant `register()` from
81
+ * inside a target's own teardown — which a fresh port would survive anyway, because
82
+ * `register()` stamps `lastSeenAt` with the current time and a zero-age port is
83
+ * never past its timeout. Measured: iterating the live map instead passes every
84
+ * test, so do not read this line as pinned. */
76
85
  private reap;
77
86
  }
78
87
  export {};
@@ -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;;;;;;;;;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"}
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;;;;;;qDAMiD;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;;;;;;;mDAO+C;IAC/C,OAAO,CAAC,IAAI;CAoDb"}
package/docs/api.md CHANGED
@@ -376,7 +376,7 @@ createStorageEventChannel(options: {
376
376
  }): ClusterChannel | null
377
377
  ```
378
378
 
379
- Creates a `ClusterChannel` backed by localStorage `storage` events — the coordination fallback for environments without BroadcastChannel. Returns `null` when storage or a storage-event source is unavailable. Delivery semantics mirror BroadcastChannel (no echo to the sender, JSON-serializable messages, closed channels refuse further posts); a monotonic sequence in the payload envelope keeps consecutive identical messages deliverable. Opt in via `createBrowserEnvironment({ channelFallback: 'storage-event' })`; see the security note in [configuration.md](./configuration.md#coordination-channel-fallback-broadcastchannel-unavailable).
379
+ Creates a `ClusterChannel` backed by localStorage `storage` events — the coordination fallback for environments without BroadcastChannel. Returns `null` when storage or a storage-event source is unavailable. Delivery semantics mirror BroadcastChannel (no echo to the sender, JSON-serializable messages, closed channels refuse further posts); the payload envelope carries a per-channel sender nonce **and** a monotonic sequence, which is what keeps consecutive identical messages deliverable — including two tabs whose first frame would otherwise store the same `seq=1` value and be silently suppressed. Opt in via `createBrowserEnvironment({ channelFallback: 'storage-event' })`; see the security note in [configuration.md](./configuration.md#coordination-channel-fallback-broadcastchannel-unavailable).
380
380
 
381
381
  ## WebSocket Transport Backend
382
382
 
@@ -85,7 +85,7 @@ Terms are explained in plain language; the code and the rest of this document us
85
85
  | Term | Short name in code | Plain-language meaning |
86
86
  |---|---|---|
87
87
  | **Topic** | `topic` | A named channel (e.g. `price.feed`) that applications subscribe to or publish on. |
88
- | **Topic key** | `topicKey` | An opaque 128-bit hash of the Topic name. The Topic name itself is never persisted in coordination storage. |
88
+ | **Topic key** | `topicKey` | An opaque 128-bit hash of the Topic name. The Topic name never appears in the worker, route or subscriber records; the two opt-in exceptions that do write plaintext are `replay.persistence` (IndexedDB) and `channelFallback: 'storage-event'`. |
89
89
  | **Tab** | `tabId` | One browser page instance. `tabId` survives refresh so a tab keeps its identity across the page lifecycle. |
90
90
  | **Worker** | `workerId` | One runtime instance inside a Tab. Each Worker publishes its own heartbeat and can own Topics. A Tab can briefly run two Workers during a restart/handoff. |
91
91
  | **Topic owner** | — | The Worker responsible for the real transport subscription of a Topic. "Owner" is a hat a Worker wears, not a permanent role: it receives the Topic's publications from the server and fans them out to other Tabs. |
@@ -117,9 +117,13 @@ Unlike the old single-JSON route table, subscribers use per-Tab independent keys
117
117
 
118
118
  ```ts
119
119
  interface WorkerRecord {
120
+ /** Cluster protocol version advertised by this worker; absent from a legacy peer. */
121
+ protocolVersion?: number;
120
122
  workerId: string;
121
123
  tabId: string;
122
124
  load: number;
125
+ /** Rolling traffic sample, present only when `loadWeighting` is enabled. */
126
+ throughput?: WorkerThroughputSample;
123
127
  role: 'active' | 'standby';
124
128
  status: 'connecting' | 'connected' | 'disconnected' | 'error';
125
129
  visibilityState: 'visible' | 'hidden';
@@ -146,7 +150,7 @@ interface WorkerRoute {
146
150
 
147
151
  `generation` increments on every re-assignment and must match across the handoff handshake; `handoffFromWorkerId` records the previous owner during a graceful handoff. The interface above matches the current protocol — see [Failover](#failover) for how these two fields drive takeover.
148
152
 
149
- Routes do not store the original topic string or payload. When the actual owner receives `CONTROL/SUBSCRIBE`, the original topic string is only passed through the BroadcastChannel in-memory message. The receiver accepts that control frame only when the durable route currently names it; a delayed frame from an earlier assignment round is dropped, and a route awaiting `ROUTE_RELEASED` can be confirmed only by the matching handoff ACK. `confirmedAt` is written after the owner processes the control message; before the route is confirmed, the subscriber Runtime holding the original topic string will resend `SUBSCRIBE` to recover from BroadcastChannel message loss that results in "a route without a real subscription".
153
+ Routes do not store the original topic string or payload. When the actual owner receives `CONTROL/SUBSCRIBE`, the topic plaintext reached it only through the coordination channel (in-memory on BroadcastChannel; part of the stored frame under `channelFallback: 'storage-event'`). The receiver drops that control frame when the durable route names a *different* worker, so a delayed frame from an earlier assignment round cannot make a non-owner subscribe, and a route awaiting `ROUTE_RELEASED` can be confirmed only by the matching handoff ACK. A topic whose route cannot be read is accepted rather than dropped — `confirmRoute` writes nothing without a route to stamp, so the frame mints no durable ownership, and the next reconcile withdraws the assignment; the tolerance exists so coordination still works when storage is absent or the record has expired. `confirmedAt` is stamped as the frame is accepted, **before** the control action is handed to the transport, so it records "this Worker took the assignment", not "the server subscription is live" — which is exactly why an unconfirmed route is re-sent: it is how "a route without a real subscription" is recovered from BroadcastChannel message loss.
150
154
 
151
155
  ### Receiver-side frame validation
152
156
 
@@ -166,7 +170,10 @@ The second row is what makes ownership authorization meaningful. `topicKey` is a
166
170
  substitution, not a variant — and because ownership is authorized by the route stored under `topicKey`
167
171
  while the transport subscription is named by `topic`, an unchecked mismatch lets a frame borrow one
168
172
  channel's authorization to name another. The durable route record stores only `topicKey`, never the
169
- plaintext, so the key → name mapping exists only in memory and cannot be injected through localStorage.
173
+ plaintext, so the key → name mapping exists only in memory and cannot be injected through the
174
+ coordination records. The storage-event fallback channel is the one path where a frame — and with it
175
+ a key/plaintext pair — does arrive through localStorage; the pairing rule is what makes a frame
176
+ written by any other tenant of that storage just as invalid as one posted into the channel.
170
177
 
171
178
  The third row exists because the batch path iterates its payload: a value with a `length` but no
172
179
  iterator threw out of the message listener, and an iterable non-array (a string) walked the batch as
@@ -195,6 +202,7 @@ These identifiers represent different layers:
195
202
  | `tabId` | Stable identity of a browser Tab | Identifies which Tab subscribes to a `topicKey` | Yes, in subscriber keys |
196
203
  | `workerId` | Identity of the current Runtime/Worker instance | Identifies the Worker that owns the transport subscription | Yes, in worker/route records |
197
204
  | `BroadcastChannel` | Same-origin, in-memory real-time channel | Carries control actions, publication events, and reconciliation signals | No |
205
+ | storage-event channel | The same channel interface over localStorage `storage` events, used only when `channelFallback: 'storage-event'` is set and BroadcastChannel is unavailable | Carries exactly the frames above | **Yes** — each frame is written whole under `cross-tab-worker-databus:channel:*`, so plaintext topics and `PUBLISH` payloads persist until the channel closes (indefinitely if the tab dies first) |
198
206
 
199
207
  ```text
200
208
  topic
@@ -211,7 +219,7 @@ BroadcastChannel CONTROL
211
219
 
212
220
  ### In-memory topic key cache (`knownTopics`)
213
221
 
214
- Each Runtime maintains a `Map<topicKey, topic>` called `knownTopics` that serves as the reverse-lookup cache from opaque key to plaintext topic. It is populated by `rememberTopic()`, which is called on every `subscribe`, `publish`, `unsubscribe`, and inbound `CONTROL` message.
222
+ Each Runtime maintains a `Map<topicKey, topic>` called `knownTopics` that serves as the reverse-lookup cache from opaque key to plaintext topic. It is populated by `rememberTopic()`, which is called on every `subscribe`, `publish`, and `unsubscribe`, and on every inbound `CONTROL` message that survives the `topicKey` pairing check — a frame dropped for disagreeing about that pair is dropped before its plaintext is ever read, so invalid pairs cannot grow this map. Legitimate calls still can, through any API a same-origin script may reach on the page's own bus, which is what the cap below bounds.
215
223
 
216
224
  The cache exists for two reasons:
217
225
 
@@ -302,7 +310,10 @@ This decentralized structure is a trade-off for concurrency correctness, not for
302
310
  | Single large JSON for Worker/route/subscriber | High | Only whole read/write | Multiple Tabs doing read-modify-write concurrently can easily overwrite each other, losing subscribers |
303
311
  | Independent key per entity | Low | Can clean up per Worker, per route, per Topic+Tab precisely | More keys, requires TTL-based garbage collection |
304
312
 
305
- The SDK does not rely on `storage` events to drive coordination; control notifications use BroadcastChannel. Although Worker heartbeats update their own independent key, this does not trigger repeated business callbacks or message dispatch within the SDK. The core benefit of separate keys is that different Tabs write different records, avoiding overwrite contention on a shared large object.
313
+ By default the SDK does not rely on `storage` events to drive coordination; control notifications use
314
+ BroadcastChannel. That is a default, not a guarantee: `channelFallback: 'storage-event'` selects a
315
+ channel implemented entirely on `storage` events, which is the one configuration where coordination
316
+ notifications do arrive that way. Although Worker heartbeats update their own independent key, this does not trigger repeated business callbacks or message dispatch within the SDK. The core benefit of separate keys is that different Tabs write different records, avoiding overwrite contention on a shared large object.
306
317
 
307
318
  Under normal conditions, the number of keys is approximately: `Number of Workers + Number of Topic routes + Number of Topic/Tab subscription relationships`. The Runtime cleans up timed-out Workers, orphaned subscribers without active Tabs, and orphaned routes that have exceeded the Worker TTL and no longer have subscribers. Other naming structures left over from older versions do not belong to the current SDK protocol and do not participate in current route resolution.
308
319
 
@@ -348,7 +359,11 @@ The two `topicKey → topic` maps (`assignedTopics`, `knownTopics`) deliberately
348
359
 
349
360
  ## BroadcastChannel Protocol
350
361
 
351
- All real-time coordination flows through one BroadcastChannel per cluster, whose name is derived from `clusterKey`. Messages on it exist only in memory: they never touch localStorage and never pass through the transport server. Four message types are exchanged:
362
+ All real-time coordination flows through one channel per cluster, whose name is derived from
363
+ `clusterKey`. BroadcastChannel — the implementation unless `channelFallback` is set — keeps its
364
+ messages in memory only; they never touch localStorage. The storage-event fallback is the exception
365
+ by construction: it *is* localStorage, so every frame on it persists as long as the key does. Neither
366
+ implementation passes through the transport server. Four message types are exchanged:
352
367
 
353
368
  | Type | Direction | Purpose |
354
369
  |---|---|---|
@@ -513,7 +528,12 @@ sequenceDiagram
513
528
 
514
529
  BroadcastChannel does not echo to its sender, so the owner receives no `EVENT` back for its own broadcast — its local dispatch is the only local delivery.
515
530
 
516
- Publications are not written to localStorage. Message data and publication metadata only exist in the BroadcastChannel in-memory event and within the transport; batch writes only cover coordination metadata.
531
+ Publications are not written to localStorage by the coordination layer or the transport; message data
532
+ and publication metadata exist in the BroadcastChannel in-memory event and in the transport, and batch
533
+ writes cover coordination metadata only. Two opt-ins do put payloads in browser storage, and each is
534
+ named where it is documented: the `channelFallback: 'storage-event'` channel writes every frame it
535
+ carries — including `PUBLISH` payloads — under its channel key, and `replay.persistence` stores the
536
+ publications themselves in IndexedDB, keyed by the plaintext topic.
517
537
 
518
538
  ### Service Worker boundary
519
539
 
@@ -588,7 +608,7 @@ These invariants are pinned by regression tests (see `tests/stability.test.ts` a
588
608
  - **Last-subscriber release.** An owning Worker learns a topic is unserved from the last subscriber's `CONTROL/UNSUBSCRIBE`: it drops the assignment *and* dispatches the release to its transport. Consuming the message inside the handoff short-circuit alone would leave the server subscription — and its inbound fan-out — alive after every tab has left.
589
609
  - **Assignment drift repair.** Every reconcile tick drops any topic this Worker holds in `assignedTopics` once the durable route no longer names it, releasing the transport subscription as it goes. Ownership thus follows the storage record rather than whichever message arrived last: a stale `CONTROL/SUBSCRIBE`, a lost route write, or a tab that claimed a route it does not own all converge back to exactly one owner within a heartbeat. `tests/coordination-invariants.test.ts` fuzzes three tabs through those corruptions and asserts the quiescent result (pinned by regression: emptying this sweep makes the fuzzed end state fail).
590
610
  - **Stranded-handoff recovery.** If the previous owner is gone and its `ROUTE_RELEASED` never arrives (dropped channel message under load, or a crash between the route write and the ACK), the reconcile loop re-elects a live owner once the unconfirmed handoff has been stuck longer than a worker TTL (10 s default): the route is rewritten with a fresh generation and the handoff marker cleared, so the normal confirmation path completes (pinned by regression). The age gate matters — a fresh unconfirmed route may simply be waiting out its confirmation flush — and while the previous owner is still alive the new owner keeps waiting, so the strict handoff keeps its no-overlap guarantee.
591
- - **Loss and recovery matrix.** Each coordination message has a bounded recovery path: a lost `CONTROL/SUBSCRIBE` is re-sent by the heartbeat reconcile for any route still lacking `confirmedAt`; a lost `REGISTRY` nudge costs at most one heartbeat interval (3 s default) because every tick reconciles anyway; a lost `ROUTE_RELEASED` is recovered by reconcile re-election once the previous owner is gone and the handoff has been stuck longer than a worker TTL (see the stranded-handoff invariant above, pinned by regression); publications dropped during a transport disconnect window are the one documented unrecoverable loss (transport contract). The storage-event fallback channel guarantees value-change delivery via a monotonic sequence in the envelope, and a dropped dispatch recovers through the same reconcile loop.
611
+ - **Loss and recovery matrix.** Each coordination message has a bounded recovery path: a lost `CONTROL/SUBSCRIBE` is re-sent by the heartbeat reconcile for any route still lacking `confirmedAt`; a lost `REGISTRY` nudge costs at most one heartbeat interval (3 s default) because every tick reconciles anyway; a lost `ROUTE_RELEASED` is recovered by reconcile re-election once the previous owner is gone and the handoff has been stuck longer than a worker TTL (see the stranded-handoff invariant above, pinned by regression); publications dropped during a transport disconnect window are the one documented unrecoverable loss (transport contract). The storage-event fallback channel makes every write a distinct value — a per-channel sender nonce *and* a monotonic sequence in the envelope — because a browser suppresses the `storage` event when `setItem` leaves the stored value unchanged, and the nonce is what separates two tabs whose first identical frame would each be `seq=1`; a dropped dispatch still recovers through the same reconcile loop.
592
612
  - **Recovery diagnostics.** `getHealthSummary()` derives a single readiness verdict from the lifecycle flags (`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`); the unified `lastFailure` ledger and persistence counters reset on every explicit `start()`.
593
613
 
594
614
  ## Transport Reconnection
@@ -22,7 +22,7 @@ Status Legend: `✅ Implemented` means the current version has code and test cov
22
22
  | Degradation | Runs locally when localStorage or BroadcastChannel is unavailable | ✅ Implemented | Preserves the current Tab's connection and subscription capabilities |
23
23
  | Degradation | Opt-in localStorage storage-event coordination channel when BroadcastChannel is unavailable | ✅ Implemented | `createBrowserEnvironment({ channelFallback: 'storage-event' })`; coordination payloads (plaintext topic names) persist to localStorage — documented trade-off |
24
24
  | Centrifuge | Built-in Dedicated / Shared Worker transport | ✅ Implemented | Supports subscribe, unsubscribe, publish, connection status, and error reporting; `auto` degrades from SharedWorker → Dedicated Worker → main-thread WebSocket |
25
- | Security Boundary | localStorage uses opaque keys derived from connection and Topic; BroadcastChannel coordination messages carry plaintext topic names | ✅ Implemented | Does not persist URLs, raw Topic names, credentials, or publication payloads. BroadcastChannel coordination messages are in-memory only and carry plaintext topic names — they are not persisted. |
25
+ | Security Boundary | localStorage uses opaque keys derived from connection and Topic; BroadcastChannel coordination messages carry plaintext topic names | ✅ Implemented | The coordination records themselves persist no URLs, credentials, raw Topic names, or publication payloads, and BroadcastChannel coordination messages are in-memory only (they carry plaintext topic names but are never written). Two opt-ins persist what the default path does not, and both are documented trade-offs: `replay.persistence` (IndexedDB) keys its object store by the plaintext topic and stores publication payloads in it, and `channelFallback: 'storage-event'` writes every coordination frame — plaintext Topic names, and payloads on `PUBLISH` frames — to localStorage under the channel key until that channel closes. |
26
26
  | Diagnostics | Aggregates lifecycle events, throughput, delivery latency, dedup outcomes, recovery retries, route acknowledgments, and migrations | ✅ Implemented | Disabled by default; metrics are emitted every 5 seconds by default, with bounded reliability events for recovery and route coordination. On-demand `getMetrics()` / `getDiagnostics().metrics` expose the current window synchronously without a sink |
27
27
  | Diagnostics | `getHealthSummary()` single-object readiness verdict and unified failure ledger | ✅ Implemented | `healthy`/`state` covers stopped, starting, recovering, BFCache-suspended, and recovery-exhausted degraded; `lastFailure` unifies transport/persistence/dispatch sources and resets on `start()` |
28
28
  | Performance | Batched writes of coordination metadata with backoff retry | ✅ Implemented | Heartbeat, route, and subscriber writes are merged and flushed in a microtask; failures use exponential backoff; `pagehide` / `stop()` flush synchronously |
@@ -35,7 +35,8 @@ Status Legend: `✅ Implemented` means the current version has code and test cov
35
35
  | Observability | Metrics/events for owner acknowledgments, migrations, and recovery attempts | ✅ Implemented | `DataBusReliabilityTraceEvent` reports bounded route ack/migration and transport recovery events; exact server-side ack remains transport-specific |
36
36
  | Runtime Model | SharedWorker / Dedicated Worker transport | ✅ Implemented | `workerMode` supports `dedicated`, `shared`, and `auto`, defaulting to `dedicated` |
37
37
  | Runtime Model | Service Worker transport | Not Implemented | Deliberately deferred; lifetime and long-lived connection constraints are documented in `docs/architecture.md` |
38
- | Durable Messages | Persisting publications or publish commands across page close | Not Implemented | The SDK does not persist business payloads, nor does it replay publish commands after restoration |
38
+ | Durable Messages | Replaying published payloads to a tab that loads later | ✅ Implemented | Opt-in: `createIndexedDbReplayPersistence` stores the publications themselves — business payloads included, under the plaintext topic as the object store's key — and they are re-hydrated on the next load, so history survives a reload or a page close. This is inbound replay for subscribers, not a delivery guarantee |
39
+ | Durable Messages | Re-sending outbound publish commands after a page closes | Not Implemented | No publish command is persisted; a publication that was already dropped (a transport disconnect window, or the local-mode degradation) is the one documented unrecoverable loss |
39
40
 
40
41
  ## Verified Guarantees (pinned by the regression suite)
41
42
 
@@ -46,10 +47,10 @@ and regression-locked:
46
47
 
47
48
  | Invariant | Area | Regressed guarantee |
48
49
  |---|---|---|
49
- | SUBSCRIBE route binding | coordination | An inbound `CONTROL/SUBSCRIBE` is honored only when the durable route currently names the receiver and is not awaiting `ROUTE_RELEASED`; delayed frames from earlier assignment rounds cannot create ownership, subscribe the transport, or confirm a pending handoff |
50
+ | SUBSCRIBE route binding | coordination | An inbound `CONTROL/SUBSCRIBE` is dropped when the durable route names a different worker, and a route awaiting `ROUTE_RELEASED` is confirmed only by the matching handoff ACK — so delayed frames from earlier assignment rounds cannot create lasting ownership, subscribe the transport, or confirm a pending handoff. A topic with no readable route is accepted rather than dropped, and that tolerance is bounded: `confirmRoute` writes nothing without a route to stamp, and the next reconcile takes the assignment back. It is what keeps coordination working when storage is unavailable or the record has expired |
50
51
  | Handoff ACK validity | coordination | `ROUTE_RELEASED` is honored only when the route still points to the receiver, the release matches `handoffFromWorkerId`, and the ACK generation exactly matches the stored route — delayed ACKs from another a↔b ping-pong round are dropped |
51
52
  | Replay persistence cleanup ordering | durability | Queued batch flushes are filtered against the winning cleanup (`unsubscribe`/`clearReplayTopic` drop the topic's pending entries, `clearReplayBefore` drops entries older than the cutoff, and `suspend()`/`stop()` discard the queued batch); cleared or stopped-session history is never re-appended by an in-flight flush |
52
- | Storage write recovery | coordination | Coalesced writes retry with exponential backoff (50 ms → 1.6 s cap); a structurally failing key is dropped after 5 attempts (with `console.warn`) without blocking other queued keys; backoff resets once the queue drains or `clear()` cancels the retries |
53
+ | Storage write recovery | coordination | Coalesced writes retry with exponential backoff (50 ms → 1.6 s cap); a structurally failing key is dropped after 5 attempts (with `console.warn`); the pass stops at the first failure, so keys queued behind it wait for that key's retry (four backoff passes, 50+100+200+400 ms) and then go through the same pass that drops the poisoned key — delayed, never permanently blocked; backoff resets once the queue drains or `clear()` cancels the retries |
53
54
  | Transport recovery budget | lifecycle | Auto-recovery is paced by a cooldown, bounded by `recovery.maxAttempts`, and reports `exhausted` when the budget is spent; a successful reopen resets attempt + exhausted, and an explicit `subscribe` on a down transport can still recover manually |
54
55
  | Recovery waiter lifetime | lifecycle | A recovery operation parked behind the transport gate captures the recovery cancellation generation; `stop()` / page-hide invalidates it, so an immediate explicit `start()` cannot be followed by a stale duplicate operation after the replacement transport re-establishes subscriptions. By contrast, an explicit `start()` that supersedes the automatic recovery timer preserves the gate and its parked operations if the manual open also fails, allowing a later automatic or demand-driven reopen to flush them |
55
56
  | BFCache suspension | lifecycle | Hiding the tab stops the transport and silently cancels in-flight persistence retries; pageshow reopens the transport and re-establishes subscriptions exactly once per cycle |
@@ -54,7 +54,7 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
54
54
 
55
55
  `replay.retentionSweepMs` optionally schedules the same cleanup on a periodic interval. It is useful for quiet topics whose old durable records should still expire; it requires `retentionMs` and a persistence adapter with `clearBefore()`. The timer follows visibility and lifecycle transitions and is disabled by default.
56
56
 
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. A batch flush still queued in a microtask when the bus suspends or stops is discarded with its lifecycle generation rather than starting a durable append after teardown.
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 are capped only where they grow: after a failed attempt the delay doubles and the doubled value is capped at 1600 ms, while `backoffMs` itself is waited verbatim as the first delay — so `backoffMs: 5000` waits 5000 ms, then 1600 ms, rather than being clamped to the ceiling; final failures retain the existing `onError` and reliability behavior. A batch flush still queued in a microtask when the bus suspends or stops is discarded with its lifecycle generation rather than starting a durable append after teardown.
58
58
 
59
59
  ### Replay Options
60
60
 
@@ -65,7 +65,7 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
65
65
  | `retentionMs` | `number` | — | Producer-timestamp retention window; history older than the cutoff is pruned through the adapter's `clearBefore` |
66
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
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 |
68
+ | `persistenceRetry` | `{ maxAttempts, backoffMs }` | `1` / `50` | Bounded retry for transient persistence failures; the delay doubles after each attempt and the doubled value is capped at 1600 ms, `backoffMs` being waited verbatim first |
69
69
 
70
70
  ### Deduplication Options
71
71
 
@@ -14,7 +14,8 @@ The package provides the following entry points:
14
14
  - `cross-tab-worker-databus/centrifuge`: the built-in Centrifuge Worker transport
15
15
  - `cross-tab-worker-databus/centrifuge.worker`: the Dedicated Worker build artifact, loaded by default by the built-in factory; typically no need to reference it directly
16
16
  - `cross-tab-worker-databus/centrifuge.shared.worker`: the SharedWorker build artifact, loaded by default by the built-in factory; typically no need to reference it directly
17
- - `cross-tab-worker-databus/hooks`: optional React hooks adapter (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`); React (>= 18) is an optional peer dependency
17
+ - `cross-tab-worker-databus/hooks`: optional React hooks adapter (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`, `useCrossTabHealth`); React (>= 18) is an optional peer dependency
18
+ - `cross-tab-worker-databus/vue`: optional Vue composables with the same four names (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`, `useCrossTabHealth`); Vue (>= 3.3) is an optional peer dependency
18
19
  - `cross-tab-worker-databus` also exports a zero-dependency native `WebSocketTransport` / `createWebSocketDataBus` for servers that speak plain WebSockets
19
20
 
20
21
  The `cross-tab-worker-databus/centrifuge` entry point relies on the optional peer dependency `centrifuge` (^5.5.3). Install it alongside this package when using the built-in Centrifuge transport: `pnpm add centrifuge`.
@@ -46,7 +47,7 @@ export const dataBus = createCentrifugeDataBus<ResourceEvent>({
46
47
 
47
48
  `clusterKey` is derived from the connection URL by default. It is only used for cluster isolation and is converted to an opaque key before entering localStorage and as the BroadcastChannel channel name. Note that topic names and event types sent over the BroadcastChannel coordination channel are transmitted in plaintext; only localStorage metadata is obfuscated via hashing.
48
49
 
49
- By default, a Dedicated Worker is used, one Worker per Tab. To have same-origin Tabs reuse a single connection, set `workerMode: 'shared'` or `'auto'`:
50
+ By default, a Dedicated Worker is used, one Worker per Tab. `workerMode: 'shared'` (or `'auto'`, which tries shared first) puts same-origin Tabs into **one** SharedWorker process — but it does not merge their connections. Each connecting port gets its own `CentrifugeSession` and so its own WebSocket, which is why closing or refreshing one tab leaves the others subscribed: N tabs still mean N server-side connections and N channel subscriptions. Shared mode saves the Worker process, not the socket:
50
51
 
51
52
  ```ts
52
53
  export const dataBus = createCentrifugeDataBus<ResourceEvent>({
@@ -144,7 +145,7 @@ pnpm examples
144
145
 
145
146
  Open `http://localhost:4173/examples/demo/` and open it in multiple browser Tabs at the same time to observe cross-Tab data flow. The page supports:
146
147
 
147
- - Connecting to the public Centrifugo demo address `wss://faye.centrifugal.dev/connection/websocket` by default
148
+ - Connecting to the demo's own local Centrifugo endpoint by default (`<scheme>://<host>/centrifuge/demo/connection/websocket`, seeded into the address box on load); the public `wss://faye.centrifugal.dev/connection/websocket` is one of the selectable presets, not the default
148
149
  - Modifying the WSS address, `workerMode`, Topic, and `transferable` configuration in the page
149
150
  - Switching to "Local Broadcast" mode, which does not depend on an external server and demonstrates multi-Tab collaboration using only BroadcastChannel
150
151
  - Data flow animations, event stream, distribution latency metrics, and cluster Worker routing status
package/docs/roadmap.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # Roadmap
2
2
 
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.
3
+ 0.21.11 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.11 delivered scope
6
+
7
+ - A prose audit of the four docs files no earlier sweep had covered — `architecture.md`,
8
+ `capabilities.md`, `getting-started.md`, `transports.md`, plus the `api.md` entry they lean on —
9
+ produced roughly fifteen sentences the code does not support, corrected in English and Chinese. The
10
+ load-bearing ones are all of the "never persists" family. `architecture.md` promised in six sentences
11
+ that coordination traffic never touches localStorage, which `channelFallback: 'storage-event'`
12
+ contradicts by construction: the envelope *is* the frame, so plaintext topics and publication
13
+ payloads live under the channel key until the channel closes and indefinitely after a tab crash. The
14
+ matrix said the SDK does not persist business payloads, which its own exported
15
+ `createIndexedDbReplayPersistence` disproves (its object store is keyed by the plaintext topic), and
16
+ it sold `workerMode: 'shared'` as reusing one connection when every port gets its own
17
+ `CentrifugeSession` and its own WebSocket — shared mode saves the process, never the socket.
18
+ - Two instructions written for people implementing a transport pointed at the wrong callback and the
19
+ wrong guard. Auto-recovery is started by `onStatus('error')` and paced by the recovery cooldown;
20
+ `onError` records and notifies and does nothing else, so a backend that reports a dead socket only
21
+ there is never reopened. The generation counter is compared only by the asynchronous credential
22
+ bridge — a superseded Worker cannot reach the transport, because its listeners are removed first.
23
+ The same file's interface block, Worker unions and wire sketch each omitted members that ship
24
+ (`publish`'s metadata argument, optional `publishBatch`, the diagnostics labels, the `TOKEN_*` and
25
+ `SESSION_REAPED` types, the batch frame).
26
+ - One over-absolute claim sat in three places, a `cluster.ts` comment among them: the route check
27
+ drops a `CONTROL/SUBSCRIBE` naming a *different* worker, and a topic whose route cannot be read is
28
+ accepted. The tolerance is bounded — `confirmRoute` writes nothing without a route to stamp and the
29
+ next reconcile withdraws the assignment — and is now pinned by a test. Measured, the stricter rule
30
+ the prose described fails that test *and* two load-weighting tests that already relied on it.
31
+ - Also in this release, from the same "measure the claim rather than re-read it" pass: the
32
+ persistence-retry sentence in `configuration.md` now says which half the 1600 ms ceiling bounds, two
33
+ tests carry the full doubling schedule and the verbatim first wait, and two `PortReaper` comments
34
+ describe what their legs actually protect.
35
+ - No behaviour, wire-format, storage-layout or cluster-protocol change. Whole-tree coverage is
36
+ unchanged at 99.01 / 96.90 / 99.26 / 99.69 over **895** tests.
4
37
 
5
38
  ## 0.21.10 delivered scope
6
39
 
@@ -15,10 +15,20 @@ idempotent — the DataBus may call them repeatedly and replays them on reconnec
15
15
 
16
16
  ```ts
17
17
  interface DataBusTransport<TConfig = unknown, TData = unknown> {
18
+ /** Optional identity/backend labels surfaced through diagnostics. */
19
+ readonly diagnosticsName?: string;
20
+ readonly diagnosticsBackend?: string;
18
21
  start(config: TConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void>;
19
22
  subscribe(topic: string): MaybePromise<void>;
20
23
  unsubscribe(topic: string): MaybePromise<void>;
21
- publish(topic: string, data: unknown): MaybePromise<void>;
24
+ /** `options` carries the caller's `{ messageId, timestamp }`; a backend that
25
+ * drops it silently strips dedup identity and producer timestamps from peers. */
26
+ publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
27
+ /** Optional: one frame for a burst. Without it the DataBus loops `publish`.
28
+ * Each item is `{ data, messageId?, timestamp? }` — the same metadata `publish`
29
+ * takes, per entry. (That entry type is internal: name it inline, or widen the
30
+ * `ReadonlyArray` to whatever your backend accepts.) */
31
+ publishBatch?(topic: string, items: ReadonlyArray<{ data: unknown; messageId?: string; timestamp?: number }>): MaybePromise<void>;
22
32
  stop(): MaybePromise<void>;
23
33
  }
24
34
 
@@ -32,8 +42,12 @@ interface DataBusTransportHandlers<TData = unknown> {
32
42
  `start()` receives the user-supplied connection config (untyped `TConfig` — the
33
43
  backend owns its shape) and the three callbacks. Call `onStatus` whenever the
34
44
  connection state changes; call `onMessage` for each inbound publication; call
35
- `onError` for non-fatal errors (the DataBus applies a recovery cooldown so a
36
- flapping connection does not retry-loop).
45
+ `onError` for non-fatal errors. Only `onStatus('error')` starts auto-recovery —
46
+ the DataBus paces that reopen with a recovery cooldown and bounds it with
47
+ `recovery.maxAttempts`, so a flapping connection does not retry-loop. `onError`
48
+ records the failure for `getHealthSummary().lastFailure` and notifies subscribers,
49
+ and nothing else: a backend that reports a dead socket through `onError` alone
50
+ is never reopened.
37
51
 
38
52
  `start()` MUST settle its returned promise only once the backend is connected,
39
53
  and reject it when the attempt fails. The DataBus uses that settlement as its
@@ -65,8 +79,11 @@ transport only owns the I/O path: connect, subscribe, publish, disconnect.
65
79
 
66
80
  Mirror the Centrifuge backend's `centrifuge-protocol.ts`: a discriminated union
67
81
  of messages the main thread sends to the Worker (`INIT` / `SUBSCRIBE` /
68
- `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` / `PING` / `STOP`) and a union the
69
- Worker posts back (`STATUS` / `MESSAGE` / `MESSAGE_BIN` / `ERROR`). Keep it
82
+ `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` / `PING` / `STOP`, plus
83
+ `TOKEN_RESPONSE` / `TOKEN_ERROR` answering a credential request) and a union the
84
+ Worker posts back (`STATUS` / `MESSAGE` / `MESSAGE_BIN` / `ERROR`, plus
85
+ `TOKEN_REQUEST` for the credential bridge and `SESSION_REAPED` when a SharedWorker
86
+ port is reclaimed). Keep it
70
87
  structured-cloneable (no functions,
71
88
  no class instances — `Error` must be serialised).
72
89
 
@@ -92,9 +109,13 @@ reference shape, including:
92
109
 
93
110
  - **Backend selection**: reuse `selectWorkerBackend` from `worker-mode.ts` so
94
111
  your backend degrades consistently with the rest of the SDK.
95
- - **Generation guard**: bump a monotonic counter when a backend is created;
96
- error handlers check it so late errors from a superseded Worker cannot
97
- corrupt the fresh session.
112
+ - **Generation guard**: bump a monotonic counter when a backend is created or the
113
+ transport stops. Only the asynchronous credential bridge compares it, because a
114
+ provider can settle after the Worker it was answering is gone. Worker error
115
+ handlers read no such counter in the reference implementation — `stop()` and
116
+ `onWorkerFailed()` remove those listeners before the generation moves on, so a
117
+ superseded backend cannot reach the transport at all. If your backend keeps a
118
+ listener alive across a swap, that is when you need the check yourself.
98
119
  - **SharedWorker heartbeat**: if you use a SharedWorker, send periodic PINGs
99
120
  so a `PortReaper` can reclaim dead-tab sessions, and handle the
100
121
  `SESSION_REAPED` message the reaper posts before it closes a port. That
@@ -153,6 +174,7 @@ const bus = createWebSocketDataBus({
153
174
  Wire protocol (JSON text frames):
154
175
 
155
176
  - client → server: `{"op":"subscribe"|"unsubscribe"|"publish","topic":"...","data":...,"messageId"?:"...","timestamp"?:123}`
177
+ - client → server (batched): `{"op":"publishBatch","topic":"...","items":[{data,...,"messageId"?,"timestamp"?}]}` — one frame whose entries the server re-fans out as individual publications. Binary payloads ride inside it as byte arrays. A one-item batch is sent as a plain `publish` frame instead, so a server that implements only `publish` still sees the legacy shape; an empty batch is dropped before it reaches the socket. A batch frame on a closed socket is reported through `handlers.onError` and dropped.
156
178
  - server → client: the canonical publication is `{"op":"publication","publication":{"topic":"...","data":...,"messageId"?:"...","timestamp"?:123}}`. The legacy flat `{"topic":"...","data":...}` frame remains accepted. Frames without a string topic are ignored; malformed JSON is reported through `handlers.onError` without throwing.
157
179
  - A publication that carries its own string `topic` is addressed by **that** value rather than by the channel it arrived on — that is how a server delivering through a wildcard channel (`chat.*`) names the concrete topic. A payload whose top level happens to contain a `topic` string is therefore re-addressed, and dropped when no tab owns the resulting topic. On Centrifuge the channel normally arrives out of band, which is why the rule needs stating here: it is the one transport where "the topic was already in the frame" is not otherwise visible.
158
180
 
package/docs/zh/api.md CHANGED
@@ -375,7 +375,7 @@ createStorageEventChannel(options: {
375
375
  }): ClusterChannel | null
376
376
  ```
377
377
 
378
- 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。投递语义与 BroadcastChannel 一致(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入);载荷信封内的单调序列号保证连续相同消息仍可投递。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
378
+ 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。投递语义与 BroadcastChannel 一致(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入);载荷信封里同时带一个**每通道 sender nonce** 和一个单调序列号,正是这两者共同保证连续相同的消息仍可投递——包括两个 Tab 各自的第一帧都会写入相同的 `seq=1`、否则会被静默抑制的情况。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
379
379
 
380
380
  ## WebSocket 传输后端
381
381
 
@@ -75,7 +75,7 @@ graph TB
75
75
  | 术语 | 代码中的简称 | 通俗含义 |
76
76
  |---|---|---|
77
77
  | **Topic**(主题) | `topic` | 一个有名字的频道(如 `price.feed`),应用订阅它或向它发布消息。 |
78
- | **Topic key**(主题键) | `topicKey` | Topic 名称的 128-bit 不透明哈希。Topic 名称本身从不写入协调存储。 |
78
+ | **Topic key**(主题键) | `topicKey` | Topic 名称的 128-bit 不透明哈希。worker / route / subscriber 三类协调记录里都不出现 Topic 名称;会写明文的只有两个需显式开启的选项:`replay.persistence`(IndexedDB)与 `channelFallback: 'storage-event'`。 |
79
79
  | **Tab**(标签页) | `tabId` | 一个浏览器页面实例。`tabId` 在刷新后保持稳定,让标签页在页面生命周期内保留身份。 |
80
80
  | **Worker**(工作器) | `workerId` | Tab 内的一个运行时实例。每个 Worker 发布自己的心跳,也可以拥有 Topic。重启/交接时一个 Tab 可能短暂存在两个 Worker。 |
81
81
  | **Topic owner**(主题持有者) | — | 负责某个 Topic 真实 transport 订阅的 Worker。"owner"是 Worker 戴的一顶帽子,不是永久角色:它从服务器接收该 Topic 的 publication 并扇出给其他 Tab。 |
@@ -107,9 +107,13 @@ cross-tab-worker-databus:{clusterHash}:subscriber:{topicKey}:{tabId}
107
107
 
108
108
  ```ts
109
109
  interface WorkerRecord {
110
+ /** 该 Worker 声明的集群协议版本;legacy 对端不写这个字段。 */
111
+ protocolVersion?: number;
110
112
  workerId: string;
111
113
  tabId: string;
112
114
  load: number;
115
+ /** 滚动流量采样,仅在开启 `loadWeighting` 时出现。 */
116
+ throughput?: WorkerThroughputSample;
113
117
  role: 'active' | 'standby';
114
118
  status: 'connecting' | 'connected' | 'disconnected' | 'error';
115
119
  visibilityState: 'visible' | 'hidden';
@@ -136,7 +140,7 @@ interface WorkerRoute {
136
140
 
137
141
  `generation` 在每次重新分配时递增,交接握手必须匹配该值;`handoffFromWorkerId` 记录优雅迁移时的前任 owner。上面的接口与当前协议一致——这两个字段如何驱动接管见 [故障转移](#故障转移)。
138
142
 
139
- 路由不保存原始 Topic 字符串或 payload。真实 owner 收到 `CONTROL/SUBSCRIBE` 时,原始 Topic 字符串只通过 BroadcastChannel 内存消息传递。接收方只有在持久化 route 当前指向自己时才接受该控制帧;来自较早分配轮的迟到帧会被丢弃,等待 `ROUTE_RELEASED` 的交接也只能由精确匹配的 ACK 确认。`confirmedAt` 在 owner 处理控制消息后写入;在路由确认之前,持有原始 Topic 字符串的订阅方 Runtime 会重发 `SUBSCRIBE`,以从"有路由但无真实订阅"的 BroadcastChannel 消息丢失中恢复。
143
+ 路由不保存原始 Topic 字符串或 payload。真实 owner 收到 `CONTROL/SUBSCRIBE` 时,Topic 明文只经由协调通道到达它(BroadcastChannel 上存在于内存;`channelFallback: 'storage-event'` 下它本身就是被写入 localStorage 的那帧的一部分)。当持久路由指向**另一个** Worker 时接收方才丢弃该控制帧,因此较早分配轮的迟到帧不能让非 owner 完成订阅;等待 `ROUTE_RELEASED` 的交接也只能由精确匹配的 ACK 确认。读不到路由的 topic 是被接受而非丢弃——没有路由可盖章时 `confirmRoute` 什么都不写,所以这一帧凭空造不出持久所有权,下一次 reconcile 会收回这条 assignment;保留这个容忍是为了在存储不可用、或记录已过期时协调仍能工作。`confirmedAt` 是在接受该帧时就写入,**早于**把控制动作交给 transport,所以它记录的是"这个 Worker 认领了任务",而不是"服务端订阅已建立"——这也正是未确认路由要被重发的原因:它是从"有路由但无真实订阅"的 BroadcastChannel 消息丢失中恢复的手段。
140
144
 
141
145
  ### 接收侧的帧校验
142
146
 
@@ -148,7 +152,7 @@ interface WorkerRoute {
148
152
  | `topicKey === createOpaqueKey(topic)` | `CONTROL`(任意 action)与 `ROUTE_RELEASED` | 帧被忽略 |
149
153
  | 携带 `items` 的 `CONTROL/PUBLISH` 帧必须给出非空**数组** | `CONTROL/PUBLISH` | 帧被忽略 |
150
154
 
151
- 第二行是所有权的授权前提。`topicKey` 是 `topic` 的纯函数,所以本库构造的每个帧两者必然一致;不一致就是替换而非变体。由于所有权依据 `topicKey` 对应的持久路由判定,而传输层订阅的名字来自 `topic`,不校验就允许一帧借用某个频道的授权去命名另一个频道。持久路由记录只保存 `topicKey`、从不保存明文,因此 key → 明文的映射只存在于内存中,无法通过 localStorage 注入。
155
+ 第二行是所有权的授权前提。`topicKey` 是 `topic` 的纯函数,所以本库构造的每个帧两者必然一致;不一致就是替换而非变体。由于所有权依据 `topicKey` 对应的持久路由判定,而传输层订阅的名字来自 `topic`,不校验就允许一帧借用某个频道的授权去命名另一个频道。持久路由记录只保存 `topicKey`、从不保存明文,因此 key → 明文的映射只存在于内存中,无法通过协调记录注入。storage-event 降级通道是唯一一帧"经由 localStorage 到达"的路径;配对规则的作用不变——其他租户写进这块存储的帧,和直接 post 进通道一样无效。
152
156
 
153
157
  第三行存在是因为批量路径会迭代负载:只有 `length` 而没有迭代器的值会从消息监听器里抛出异常;可迭代的非数组(字符串)会被当成多个单字符条目,其 `data` 为 `undefined`,随后 owner 会以自己的会话把它们发布出去。
154
158
 
@@ -167,6 +171,7 @@ interface WorkerRoute {
167
171
  | `tabId` | 一个浏览器 Tab 的稳定身份 | 标识哪个 Tab 订阅了某个 `topicKey` | 是,体现在 subscriber key 中 |
168
172
  | `workerId` | 当前 Runtime/Worker 实例身份 | 标识哪个 Worker 负责实际 transport 订阅 | 是,体现在 worker/route 记录中 |
169
173
  | `BroadcastChannel` | 同源 Tab 间的实时内存通道 | 传递控制动作、publication 事件和重协调通知 | 否,不持久化消息 |
174
+ | storage-event 通道 | 建立在 localStorage `storage` 事件上的同一套 channel 接口,仅在设置了 `channelFallback: 'storage-event'` 且 BroadcastChannel 不可用时启用 | 传递与上面完全相同的帧 | **是**——每帧整体写到 `cross-tab-worker-databus:channel:*` 之下,因此明文 Topic 与 `PUBLISH` payload 会保留到该 channel 关闭(tab 先退出则无限期保留) |
170
175
 
171
176
  它们通过以下 key 和消息字段关联:
172
177
 
@@ -185,7 +190,7 @@ BroadcastChannel CONTROL
185
190
 
186
191
  ### 内存 Topic key 缓存 (`knownTopics`)
187
192
 
188
- 每个 Runtime 维护一个 `Map<topicKey, topic>` 称为 `knownTopics`,作为不透明 key 到原始 topic 的反向查找缓存。它由 `rememberTopic()` 填充,`subscribe`、`publish`、`unsubscribe` 和入站 `CONTROL` 消息都会调用它。
193
+ 每个 Runtime 维护一个 `Map<topicKey, topic>` 称为 `knownTopics`,作为不透明 key 到原始 topic 的反向查找缓存。它由 `rememberTopic()` 填充:`subscribe`、`publish`、`unsubscribe` 都会调用它,入站 `CONTROL` 消息则只在通过 `topicKey` 配对校验后才走到这一步——一对不相符的帧在读到明文之前就被丢弃,因此无效配对无法撑大这张表。合法调用仍然可以(同源脚本能通过页面上自己的 bus 触达这些 API),约束它们的正是下面的上限。
189
194
 
190
195
  该缓存存在两个原因:
191
196
 
@@ -277,7 +282,7 @@ console.table(__bus.getClusterSnapshot().routes)
277
282
  | 单个大 JSON 存放 Worker/路由/subscriber | 高 | 只能整体读写 | 多个 Tab 并发读-改-写容易互相覆盖,丢失 subscriber |
278
283
  | 每个实体独立 key | 低 | 可精确按 Worker、路由、Topic+Tab 清理 | key 更多,需要基于 TTL 的垃圾回收 |
279
284
 
280
- SDK 不依赖 `storage` 事件驱动协调,控制通知使用 BroadcastChannel。虽然 Worker 心跳会更新自己的独立 key,但这不会在 SDK 内部触发重复的业务回调或消息分发。独立 key 的核心好处是不同 Tab 写不同的记录,避免对共享大对象产生覆盖竞争。
285
+ 默认情况下 SDK 不依赖 `storage` 事件驱动协调,控制通知使用 BroadcastChannel。这是默认值而非保证:`channelFallback: 'storage-event'` 选的正是一条完全建立在 `storage` 事件上的通道,那也是协调通知唯一经由这种事件到达的配置。虽然 Worker 心跳会更新自己的独立 key,但这不会在 SDK 内部触发重复的业务回调或消息分发。独立 key 的核心好处是不同 Tab 写不同的记录,避免对共享大对象产生覆盖竞争。
281
286
 
282
287
  正常条件下 key 数量约为:`Worker 数量 + Topic 路由数量 + Topic/Tab 订阅关系数量`。Runtime 会清理超时的 Worker、无活跃 Tab 的孤儿 subscriber,以及超过 Worker TTL 且不再有 subscriber 的孤儿路由。旧版本遗留的其他命名结构不属于当前 SDK 协议,不参与当前路由解析。
283
288
 
@@ -323,7 +328,7 @@ SDK 不依赖 `storage` 事件驱动协调,控制通知使用 BroadcastChannel
323
328
 
324
329
  ## BroadcastChannel 通信协议
325
330
 
326
- 所有实时协调都经由每个集群唯一的一条 BroadcastChannel(名称由 `clusterKey` 派生)传递。通道上的消息只存在于内存:不写入 localStorage,也不经过 transport 服务器。共四类消息:
331
+ 所有实时协调都经由每个集群唯一的一条通道传递(名称由 `clusterKey` 派生)。BroadcastChannel——未设置 `channelFallback` 时的实现——其消息只存在于内存:不写入 localStorage。storage-event 降级通道是构造上的例外:它本身就是 localStorage,所以帧在 key 存续期间一直留在盘上。两种实现都不经过 transport 服务器。共四类消息:
327
332
 
328
333
  | 类型 | 方向 | 用途 |
329
334
  |---|---|---|
@@ -467,7 +472,7 @@ sequenceDiagram
467
472
 
468
473
  BroadcastChannel 不会把消息回传给发送者,因此 owner 收不到自己广播的 `EVENT`——本地分发是唯一一次本地投递。
469
474
 
470
- publication 不写入 localStorage。消息数据与 publication 元数据只存在于 BroadcastChannel 内存事件和 transport 内;批量写入只覆盖协调元数据。
475
+ 协调层与 transport 都不会把 publication 写入 localStorage;消息数据与 publication 元数据只存在于 BroadcastChannel 内存事件和 transport 内,批量写入只覆盖协调元数据。两个需要显式开启的选项确实会把 payload 放进浏览器存储,并且各自在文档里点名:`channelFallback: 'storage-event'` 通道把它承载的每一帧(含 `PUBLISH` payload)写到自己的 channel key 下;`replay.persistence` 则把 publication 本体存进 IndexedDB,并以明文 Topic 作为 key。
471
476
 
472
477
  ### Service Worker 边界
473
478
 
@@ -541,7 +546,7 @@ Transport 消息 → isAssigned(topic)? → 是 → broadcastEvent(EVENT)
541
546
  - **最后一个订阅者的释放。** owner Worker 通过最后一个订阅者发来的 `CONTROL/UNSUBSCRIBE` 得知某 Topic 已无人订阅:它会同时丢弃本地 ownership 并把释放动作派发给自己的 transport。若只在交接短路里消费该消息,server 订阅(以及它的入站扇出)会在所有 Tab 离开后依然存活。
542
547
  - **归属漂移的修复。** 每一轮 reconcile 都会丢弃那些 `assignedTopics` 里仍持有、但持久路由已不再指向本 Worker 的 Topic,并顺带释放对应的 transport 订阅。于是 ownership 跟随存储记录,而不是最后到达的那条消息:陈旧的 `CONTROL/SUBSCRIBE`、丢失的路由写入、或某个抢到了不属于自己的路由的 Tab,都会在一个 heartbeat 内收敛回“恰好一个 owner”。`tests/coordination-invariants.test.ts` 会针对三个 Tab 随机制造这些破坏并断言静默后的结果(由回归测试固定:把这一轮清扫掏空,模糊测试的终态就会失败)。
543
548
  - **悬挂交接恢复。** 若前任 owner 已消失而其 `ROUTE_RELEASED` 始终未到达(高负载下通道消息丢失,或 route 写入与 ACK 发送之间崩溃),reconcile 循环会在该未确认交接悬挂超过一个 worker TTL(默认 10 秒)后重新选举存活 owner:路由以全新 generation 重写并清除交接标记,使常规确认路径得以完成(已有回归固化)。年龄门限很关键——刚写入的未确认路由可能只是在等确认落盘,不能误判为悬挂;而只要前任 owner 仍然存活,新 owner 会继续等待,因此严格交接的无重叠保证不受影响。
544
- - **丢失与恢复矩阵。** 每类协调消息都有有界恢复路径:丢失的 `CONTROL/SUBSCRIBE` 由心跳 reconcile 对未确认路由重发;丢失的 `REGISTRY` 通知最多损失一个心跳间隔(默认 3 秒),因为每次 tick 都会 reconcile;丢失的 `ROUTE_RELEASED` 由 reconcile 在前任 owner 消失且交接悬挂超过一个 worker TTL 后重新选举恢复(见上文悬挂交接不变量,已有回归固化);transport 断连窗口内被丢弃的 publication 是唯一文档化的不可恢复丢失(transport 契约)。storage-event 降级通道通过信封内的单调序列号保证变值投递,丢失的派发由同一 reconcile 循环恢复。
549
+ - **丢失与恢复矩阵。** 每类协调消息都有有界恢复路径:丢失的 `CONTROL/SUBSCRIBE` 由心跳 reconcile 对未确认路由重发;丢失的 `REGISTRY` 通知最多损失一个心跳间隔(默认 3 秒),因为每次 tick 都会 reconcile;丢失的 `ROUTE_RELEASED` 由 reconcile 在前任 owner 消失且交接悬挂超过一个 worker TTL 后重新选举恢复(见上文悬挂交接不变量,已有回归固化);transport 断连窗口内被丢弃的 publication 是唯一文档化的不可恢复丢失(transport 契约)。storage-event 降级通道让每次写入都是一个不同的值——信封里同时带一个**每通道 sender nonce** 和一个单调递增的序列号——因为浏览器在 `setItem` 没有改变存储值时会抑制 `storage` 事件,而两个 Tab 各自的第一帧同样都是 `seq=1`,把它们区分开靠的是那个 nonce;丢失的派发仍由同一 reconcile 循环恢复。
545
550
  - **恢复诊断。** `getHealthSummary()` 从生命周期标志推导单一就绪判定(`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`);统一的 `lastFailure` 账本与持久化计数在每次显式 `start()` 后重置。
546
551
 
547
552
  ## Transport 重连
@@ -22,7 +22,7 @@
22
22
  | 降级 | localStorage 或 BroadcastChannel 不可用时本地运行 | ✅ 已实现 | 保留当前 Tab 的连接和订阅能力 |
23
23
  | 降级 | BroadcastChannel 不可用时 opt-in 启用 localStorage storage-event 协调通道 | ✅ 已实现 | `createBrowserEnvironment({ channelFallback: 'storage-event' })`;协调载荷(明文 Topic 名称)会写入 localStorage,已在文档中标注权衡 |
24
24
  | Centrifuge | 内置 Dedicated / Shared Worker transport | ✅ 已实现 | 支持 subscribe、unsubscribe、publish、连接状态和错误上报;`auto` 从 SharedWorker → Dedicated Worker → 主线程 WebSocket 降级 |
25
- | 安全边界 | localStorage 使用连接和 Topic 派生不透明 key;BroadcastChannel 协调消息以明文传输 Topic 名称 | ✅ 已实现 | 不持久化 URL、原始 Topic 名称、凭证或 publication payload。BroadcastChannel 协调消息仅存在于内存中,以明文传输 Topic 名称——不会被持久化。 |
25
+ | 安全边界 | localStorage 使用连接和 Topic 派生不透明 key;BroadcastChannel 协调消息以明文传输 Topic 名称 | ✅ 已实现 | 协调记录本身不持久化 URL、凭证、原始 Topic 名称或 publication payload;BroadcastChannel 协调消息仅存在于内存中(带明文 Topic 名称,但从不落盘)。两个需要显式开启的选项会写入默认路径不写的数据,且都是文档化的取舍:`replay.persistence`(IndexedDB)以明文 Topic 作为对象仓的 key,并把 publication payload 存进去;`channelFallback: 'storage-event'` 会把每一帧协调消息——明文 Topic 名称,以及 `PUBLISH` 帧的 payload——写到 localStorage 的 channel key 下,直到该 channel 关闭。 |
26
26
  | 诊断 | 聚合生命周期、吞吐量、分发延迟、去重结果、恢复重试、路由确认和迁移 | ✅ 已实现 | 默认关闭;默认每 5 秒输出指标,并以有界 reliability 事件记录恢复和路由协调。按需 `getMetrics()` / `getDiagnostics().metrics` 无需 sink 即可同步暴露当前窗口 |
27
27
  | 诊断 | `getHealthSummary()` 单对象健康判定与统一失败账本 | ✅ 已实现 | `healthy`/`state` 覆盖未启动、启动中、恢复中、BFCache 挂起与恢复耗尽降级;`lastFailure` 统一 transport/persistence/dispatch 来源,`start()` 时重置 |
28
28
  | 性能 | 协调元数据批量写入 + 退避重试 | ✅ 已实现 | 心跳、路由和 subscriber 写入合并后在微任务中 flush;失败时指数退避;`pagehide` / `stop()` 同步 flush |
@@ -35,7 +35,8 @@
35
35
  | 可观测性 | owner 确认、迁移和恢复尝试的指标/事件 | ✅ 已实现 | `DataBusReliabilityTraceEvent` 记录有界的 route ack/migration 与 transport recovery;服务端最终确认仍由 transport 决定 |
36
36
  | 运行时模型 | SharedWorker / Dedicated Worker transport | ✅ 已实现 | `workerMode` 支持 `dedicated`、`shared` 和 `auto`,默认 `dedicated` |
37
37
  | 运行时模型 | Service Worker transport | 未实现 | 刻意延后;生命周期与长连接约束见 `docs/zh/architecture.md` |
38
- | 持久消息 | 跨页面关闭持久化 publication 或发布命令 | 未实现 | SDK 不持久化业务 payload,也不在恢复后重放发布命令 |
38
+ | 持久消息 | 向稍后加载的 Tab 回放已发布的 payload | ✅ 已实现 | 需要显式开启:`createIndexedDbReplayPersistence` 保存 publication 自身——包含业务 payload,并以明文 Topic 作为对象仓的 key——下次加载时重新水合,因此历史可以跨越 reload 或页面关闭。这是给订阅方的入站回放,不是投递保证 |
39
+ | 持久消息 | 页面关闭后重发外发发布命令 | 未实现 | 发布命令不会被持久化;已经被丢弃的 publication(transport 断连窗口,或 local-mode 降级)是唯一文档化的不可恢复丢失 |
39
40
 
40
41
  ## 已验证保证(由回归套件钉住)
41
42
 
@@ -43,7 +44,7 @@
43
44
 
44
45
  | 不变量 | 领域 | 被回归固化的保证 |
45
46
  |---|---|---|
46
- | SUBSCRIBE 路由绑定 | 协调 | 仅当持久化 route 当前指向接收方、且不在等待 `ROUTE_RELEASED` 时才执行入站 `CONTROL/SUBSCRIBE`;较早分配轮的迟到帧不能创建 ownership、订阅 transport 或确认悬挂交接 |
47
+ | SUBSCRIBE 路由绑定 | 协调 | 当持久化 route 指向另一个 Worker 时,入站 `CONTROL/SUBSCRIBE` 会被丢弃;等待 `ROUTE_RELEASED` 的 route 只被匹配的交接 ACK 确认——因此较早分配轮的迟到帧不能创建持久 ownership、订阅 transport 或确认悬挂交接。topic 读不到 route 时该帧是被接受而非丢弃,而这个容忍是有界的:没有 route 可盖章时 `confirmRoute` 什么都不写,下一次 reconcile 会收回这条 assignment。它换来的是存储不可用、或记录已过期时协调仍能工作 |
47
48
  | 交接 ACK 有效性 | 协调 | 仅当 route 仍指向接收方、释放方匹配 `handoffFromWorkerId`、且 ACK 代数与存储 route 代数精确相等时才接受 `ROUTE_RELEASED`——其他交接轮次的过期 ACK(如 a↔b 乒乓)会被丢弃 |
48
49
  | 回放持久化清理顺序 | 持久性 | 排队中的批量 flush 会按先到清理过滤(`unsubscribe`/`clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore` 丢弃早于截止时间的条目,`suspend()`/`stop()` 丢弃整个待写批次);已清或已停止会话的历史不会被进行中的 flush 重新追加 |
49
50
  | 存储写入恢复 | 协调 | 合并写以指数退避重试(50 ms → 1.6 s 上限);结构性失败键在 5 次后丢弃(并 `console.warn`)而不阻塞其他排队键;队列清空或 `clear()` 取消后退避重置 |