cross-tab-worker-databus 0.21.9 → 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.
- package/CHANGELOG.md +29 -0
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.shared.worker.js +24 -9
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/{chunk-75ZDU7VG.js → chunk-UO57SLQD.js} +2 -2
- package/dist/chunk-UO57SLQD.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +1 -1
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/workers/port-reaper.d.ts +23 -8
- package/dist/workers/port-reaper.d.ts.map +1 -1
- package/docs/api.md +2 -2
- package/docs/architecture.md +28 -8
- package/docs/benchmarks.md +7 -7
- package/docs/capabilities.md +5 -4
- package/docs/configuration.md +7 -7
- package/docs/getting-started.md +4 -3
- package/docs/roadmap.md +60 -1
- package/docs/transports.md +30 -8
- package/docs/zh/api.md +2 -2
- package/docs/zh/architecture.md +13 -8
- package/docs/zh/benchmarks.md +7 -7
- package/docs/zh/capabilities.md +4 -3
- package/docs/zh/configuration.md +7 -7
- package/docs/zh/getting-started.md +4 -3
- package/docs/zh/roadmap.md +42 -1
- package/docs/zh/transports.md +16 -6
- package/package.json +1 -1
- package/dist/chunk-75ZDU7VG.js.map +0 -7
|
@@ -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
|
@@ -39,14 +39,24 @@ 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).
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
51
|
+
* `Infinity` is the documented way to disable the PING heartbeat, and it means
|
|
52
|
+
* this port can never be judged silent: `startHeartbeat()` sends nothing at all
|
|
53
|
+
* once configured that way, so a timeout would be a death sentence with no
|
|
54
|
+
* possible appeal. A non-finite-or-non-positive *other* value falls back to the
|
|
55
|
+
* default so a bad payload cannot degenerate the reaper into a busy loop or
|
|
56
|
+
* silence it — those three (NaN, 0, negative) are rejected by
|
|
57
|
+
* `assertHeartbeatInterval` at the transport constructor and so can only arrive
|
|
58
|
+
* from a main thread that is not this library. No-op for untracked ports (e.g.
|
|
59
|
+
* setTimeout arrives after remove/STOP). */
|
|
50
60
|
setTimeout(port: MessagePort, heartbeatIntervalMs: number): void;
|
|
51
61
|
/** Remove a port (STOP message). The caller is responsible for closing the
|
|
52
62
|
* port and stopping the session. No-op for untracked ports so a duplicate
|
|
@@ -65,8 +75,13 @@ export declare class PortReaper {
|
|
|
65
75
|
* be unit-tested in isolation from the timer plumbing. */
|
|
66
76
|
private computeMinHeartbeat;
|
|
67
77
|
/** Close sessions whose port has been silent longer than its timeout.
|
|
68
|
-
* Iterates a snapshot
|
|
69
|
-
*
|
|
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. */
|
|
70
85
|
private reap;
|
|
71
86
|
}
|
|
72
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
|
|
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
|
@@ -362,7 +362,7 @@ Available options:
|
|
|
362
362
|
|
|
363
363
|
- `workerMode`: `'dedicated'` (default), `'shared'`, or `'auto'`; the `auto` degradation chain is SharedWorker -> Dedicated Worker -> local mode
|
|
364
364
|
- `transferable`: `boolean`, default `false`; when enabled, ArrayBuffer payloads use Transferable transport, while the object message API remains unchanged
|
|
365
|
-
- `heartbeatIntervalMs`: `number`, default `10000`; SharedWorker PING heartbeat interval in ms. `Infinity` disables heartbeats. Must be a positive number or `Infinity` — `0`, a negative number, or `NaN` throws a `TypeError` in the transport constructor. See [configuration](./configuration.md#sharedworker-session-reaper) for details
|
|
365
|
+
- `heartbeatIntervalMs`: `number`, default `10000`; SharedWorker PING heartbeat interval in ms. `Infinity` disables heartbeats and exempts that port from reaping. Must be a positive number or `Infinity` — `0`, a negative number, or `NaN` throws a `TypeError` in the transport constructor. See [configuration](./configuration.md#sharedworker-session-reaper) for details
|
|
366
366
|
- `workerFactory`: custom Dedicated Worker loading method
|
|
367
367
|
- `sharedWorkerFactory`: custom SharedWorker loading method
|
|
368
368
|
|
|
@@ -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
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/docs/benchmarks.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
# Browser Benchmark Trend
|
|
4
4
|
|
|
5
|
-
> Data through 2026-09-
|
|
5
|
+
> Data through 2026-09-23, from the 71 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
|
|
6
6
|
|
|
7
7
|
The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50`, which compares the newest report against the median of the same metric in the preceding reports (50% ceiling absorbs shared-runner noise; a single previous report was a fragile baseline because the in-page hot-path metrics alternate between a fast and a slow mode on identical code). This doc records the long-run picture: values are per-metric latencies where lower is better, and the last column is the best run inside the most recent 5 reports — not an all-time record, because the in-page matrix changed measurement semantics in early September 2026 and older reports are not comparable.
|
|
8
8
|
|
|
9
9
|
<!-- BENCH-TREND:BEGIN (machine-generated table) -->
|
|
10
10
|
| Metric | Previous (ms) | Latest (ms) | Δ | Best of last 5 runs (ms) |
|
|
11
11
|
|---|---|---|---|---|
|
|
12
|
-
| publish per-message (ms, lower is better) — dedicated |
|
|
13
|
-
| publish per-message (ms, lower is better) — shared |
|
|
14
|
-
| wildcard dispatch ×1000 (ms, lower is better) |
|
|
15
|
-
| publishBatch ×1000 (ms, lower is better) |
|
|
16
|
-
| dedup ×1000 (ms, lower is better) |
|
|
17
|
-
| trace + publish ×1000 (ms, lower is better) |
|
|
12
|
+
| publish per-message (ms, lower is better) — dedicated | 55.4416 | 50.6133 | -4.83 | 50.6133 |
|
|
13
|
+
| publish per-message (ms, lower is better) — shared | 58.2441 | 33.8955 | -24.35 | 33.8955 |
|
|
14
|
+
| wildcard dispatch ×1000 (ms, lower is better) | 5.2 | 5.7 | +0.50 | 5.2 |
|
|
15
|
+
| publishBatch ×1000 (ms, lower is better) | 3.5 | 3.7 | +0.20 | 3.5 |
|
|
16
|
+
| dedup ×1000 (ms, lower is better) | 13 | 11 | -2.00 | 11 |
|
|
17
|
+
| trace + publish ×1000 (ms, lower is better) | 4.6 | 4.7 | +0.10 | 4.6 |
|
|
18
18
|
| first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
|
|
19
19
|
<!-- BENCH-TREND:END -->
|
|
20
20
|
|
package/docs/capabilities.md
CHANGED
|
@@ -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 |
|
|
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 |
|
|
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
|
|
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`)
|
|
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 |
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
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;
|
|
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
|
|
|
@@ -162,7 +162,7 @@ Main configuration for `createCentrifugeDataBus<TData>(options)`:
|
|
|
162
162
|
| `clusterKey` | `string` | `connection.url` | Manually isolate logical clusters |
|
|
163
163
|
| `workerMode` | `'dedicated' \| 'shared' \| 'auto'` | `'dedicated'` | Worker transport runtime mode; `auto` degrades via SharedWorker -> Dedicated Worker -> local mode, explicit `dedicated` degrades via Dedicated Worker -> SharedWorker -> local mode |
|
|
164
164
|
| `transferable` | `boolean` | `false` | When enabled, `publish(topic, ArrayBuffer)` uses Transferable transport; ArrayBuffer publications on the receiving side also follow the transfer path |
|
|
165
|
-
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely. Distinct from the Core cluster heartbeat (default 3000
|
|
165
|
+
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely and exempts that port from reaping. Distinct from the Core cluster heartbeat (default 3000 ms) which tracks worker liveness via localStorage |
|
|
166
166
|
| `workerFactory` | `() => Worker` | Built-in Worker | For testing or custom Worker loading |
|
|
167
167
|
| `sharedWorkerFactory` | `() => SharedWorker` | Built-in SharedWorker | For testing or custom SharedWorker loading |
|
|
168
168
|
| `credentialProvider` | `{ getToken?, getChannelToken? }` | `undefined` | Async credential refresh bridge: the Worker asks the main thread for each fresh token (`getToken` / `getChannelToken`) and this provider supplies it from application context. Required because function-valued Centrifuge options cannot be structured-cloned into the Worker |
|
|
@@ -210,13 +210,13 @@ Storage events are delivered to other documents only. Two bus runtimes in the *s
|
|
|
210
210
|
|
|
211
211
|
A `MessagePort` has no `close` event, so the SharedWorker cannot know when a tab has crashed or been closed without sending a `STOP` message. To avoid leaking a `CentrifugeSession` (and its WebSocket) for a dead tab, the transport sends a periodic **PING heartbeat** to the SharedWorker, and the SharedWorker runs a **reaper** that closes any session whose port has been silent for longer than its timeout.
|
|
212
212
|
|
|
213
|
-
- **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely —
|
|
214
|
-
- **Session timeout**: `3 × heartbeatIntervalMs` (default `30000`
|
|
213
|
+
- **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely: the transport sends no `PING` at all, and it tells the Worker so by carrying that same `Infinity` in its `INIT`, which **exempts the port from reaping** — nothing on that port can ever be judged silent. Use this only when the SharedWorker reaper is not needed (e.g. the tab always stops the bus itself, so every port leaves through `STOP`). The cost is symmetrical and worth stating: a tab configured this way and then *crashed* leaves its `CentrifugeSession` and its WebSocket in place, because the one thing that would have noticed was the heartbeat that was switched off.
|
|
214
|
+
- **Session timeout**: `3 × heartbeatIntervalMs` (default `30000` ms). A port silent for longer than its timeout is reaped: its session is stopped and its WebSocket closed. A port whose `INIT` carried `heartbeatIntervalMs: Infinity` has no timeout and is never reaped. This is distinct from the Core cluster heartbeat (default `3000` ms) which tracks worker liveness via localStorage — see the note below.
|
|
215
215
|
- **Adaptive cadence**: the reaper runs at the smallest configured heartbeat interval across active ports, so a port with a short heartbeat is reaped promptly. When the last port disconnects, the reaper interval is cleared so a long-lived SharedWorker does not run a perpetual no-op interval between connection bursts.
|
|
216
216
|
- **The tab is told before the port goes**: a port that is being reclaimed receives one final `SESSION_REAPED` message, then is closed, then its session is stopped. The order is the point — a `MessagePort` has no close event, and posting into a port the other side closed succeeds while delivering nothing, so this is the only thing a starved tab can ever learn from. Without it a tab that was merely stalled rather than gone (a long synchronous task, or the throttling a backgrounded tab gets) kept its cluster role and its topic routes while every publication it sent vanished into the dead port, and `getHealthSummary()` went on reporting `healthy` / `connected`. On receiving the message the transport discards the backend and reports the failure through `onError` and an `error` status, which is what lets the normal recovery path rebuild the session on a fresh port. Measured in a real browser: after a 34 s stall the tab re-established its subscription on its own — no user gesture, no lifecycle event — and the replacement connection was under a second old when it was observed.
|
|
217
217
|
- **Port closed before session stop**: when a port is reaped, the port is closed first and the session stopped after. Closing the port discards the session's `disconnected` status post (so it never reaches a live-but-slow main thread) and guarantees a closed port can never deliver a later message that would resurrect the session outside the reaper's tracking.
|
|
218
|
-
- **Failure isolation**:
|
|
219
|
-
-
|
|
218
|
+
- **Failure isolation**: reaping wraps `target.notify()`/`close()`/`stop()` in try-catch and `dispose()` wraps `close()`/`stop()`, so a single failing port cannot abort the reaping pass or leave subsequent dead tabs uncollected. A step that throws does skip the steps after it — for that one port only.
|
|
219
|
+
- **`dispose()` has no Worker-side caller**: it clears the reaper interval and closes/stops **every** still-tracked session, and the unit tests exercise it, but the shipped SharedWorker never invokes it — a `MessagePort` has no close event, so there is no "the SharedWorker is shutting down" signal for such a hook to hang on. Every port leaves tracking through its own `STOP` handler or through the reaper, and the interval the reaper runs on is cleared once the last tracked port is gone.
|
|
220
220
|
|
|
221
221
|
This is the mechanism that recovers sessions for tabs that crash without sending `STOP`, and the announcement it makes is what lets a tab that did *not* crash notice and reconnect. Lower `heartbeatIntervalMs` to reap dead sessions faster, at the cost of more frequent PING messages on the port — and note that a value below roughly a third of the longest task your page runs will start reaping live tabs, each of which recovers by rebuilding its session.
|
|
222
222
|
|
package/docs/getting-started.md
CHANGED
|
@@ -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.
|
|
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
|
|
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,65 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
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.
|
|
37
|
+
|
|
38
|
+
## 0.21.10 delivered scope
|
|
39
|
+
|
|
40
|
+
- `heartbeatIntervalMs: Infinity` — the documented way to switch the SharedWorker PING
|
|
41
|
+
heartbeat off — was being read by the reaper as a malformed number. It fell through the
|
|
42
|
+
same `Number.isFinite` guard as `NaN`, `0` and negatives and took the default 10 s, so
|
|
43
|
+
the port was given a 30 s session timeout on the strength of being the one port that would
|
|
44
|
+
never send another message: the option whose stated purpose is "the reaper is not needed"
|
|
45
|
+
guaranteed a reap 30 s after connecting. `Infinity` now exempts that port from reaping,
|
|
46
|
+
while the three values the transport constructor rejects (and which therefore cannot come
|
|
47
|
+
from this library at all) keep the fallback. Pinned by one test that kills three mutants at
|
|
48
|
+
three different assertions, plus an assertion that the `INIT` actually carries the value —
|
|
49
|
+
the two halves of this defect were each covered by a green test, which is how it survived.
|
|
50
|
+
- The shipped reaper documentation stopped promising things the code does not do, in both
|
|
51
|
+
languages: no `PortReaper.dispose()` shutdown hook runs "when the SharedWorker shuts down"
|
|
52
|
+
(nothing calls it — a `MessagePort` has no close event, so there is no such signal to hang a
|
|
53
|
+
hook on), and what disabling the heartbeat costs is stated next to what it disables (a tab
|
|
54
|
+
configured that way and then crashed keeps its session and its WebSocket).
|
|
55
|
+
- A test that passed while breaking the gate: an IndexedDB replay assertion attached its
|
|
56
|
+
rejection handler after an await that a real 40 ms timer could overtake, so on a loaded
|
|
57
|
+
runner `pnpm test:coverage` exited 1 with `Unhandled Rejection` and every test green. The
|
|
58
|
+
handlers are created next to their promises now, measured in both directions by forcing the
|
|
59
|
+
round trip to 200 ms.
|
|
60
|
+
- No wire-format, storage-layout or cluster-protocol change. The behaviour change is confined
|
|
61
|
+
to shared mode configured with `heartbeatIntervalMs: Infinity`, which could not work as
|
|
62
|
+
documented before.
|
|
4
63
|
|
|
5
64
|
## 0.21.9 delivered scope
|
|
6
65
|
|
package/docs/transports.md
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
36
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
@@ -361,7 +361,7 @@ const transport = new CentrifugeWorkerTransport({
|
|
|
361
361
|
|
|
362
362
|
- `workerMode`:`'dedicated'`(默认)、`'shared'` 或 `'auto'`;`auto` 的降级链路为 SharedWorker → Dedicated Worker → 本地模式
|
|
363
363
|
- `transferable`:`boolean`,默认 `false`;开启后 ArrayBuffer payload 使用 Transferable 传输,对象消息 API 不变
|
|
364
|
-
- `heartbeatIntervalMs`:`number`,默认 `10000`;SharedWorker PING 心跳间隔(毫秒)。传 `Infinity`
|
|
364
|
+
- `heartbeatIntervalMs`:`number`,默认 `10000`;SharedWorker PING 心跳间隔(毫秒)。传 `Infinity` 完全禁用心跳,该端口也因此豁免于回收。详见 [配置](./configuration.md#sharedworker-会话回收)
|
|
365
365
|
- `workerFactory`:自定义 Dedicated Worker 加载方式
|
|
366
366
|
- `sharedWorkerFactory`:自定义 SharedWorker 加载方式
|
|
367
367
|
|
|
@@ -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
|
|
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
|
|