cross-tab-worker-databus 0.1.0
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/README.zh.md +98 -0
- package/dist/centrifuge-protocol.d.ts +77 -0
- package/dist/centrifuge-protocol.d.ts.map +1 -0
- package/dist/centrifuge-session.d.ts +39 -0
- package/dist/centrifuge-session.d.ts.map +1 -0
- package/dist/centrifuge.d.ts +135 -0
- package/dist/centrifuge.d.ts.map +1 -0
- package/dist/centrifuge.js +407 -0
- package/dist/centrifuge.js.map +7 -0
- package/dist/centrifuge.shared.worker.js +5220 -0
- package/dist/centrifuge.shared.worker.js.map +7 -0
- package/dist/centrifuge.worker.js +5109 -0
- package/dist/centrifuge.worker.js.map +7 -0
- package/dist/chunk-GABYBK7I.js +1527 -0
- package/dist/chunk-GABYBK7I.js.map +7 -0
- package/dist/core/cluster.d.ts +219 -0
- package/dist/core/cluster.d.ts.map +1 -0
- package/dist/core/data-bus.d.ts +133 -0
- package/dist/core/data-bus.d.ts.map +1 -0
- package/dist/core/environment.d.ts +67 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/hash.d.ts +11 -0
- package/dist/core/hash.d.ts.map +1 -0
- package/dist/core/routing.d.ts +42 -0
- package/dist/core/routing.d.ts.map +1 -0
- package/dist/core/storage-batch.d.ts +35 -0
- package/dist/core/storage-batch.d.ts.map +1 -0
- package/dist/core/trace.d.ts +126 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/types.d.ts +112 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +7 -0
- package/dist/worker-mode.d.ts +25 -0
- package/dist/worker-mode.d.ts.map +1 -0
- package/dist/workers/centrifuge.shared.worker.d.ts +2 -0
- package/dist/workers/centrifuge.shared.worker.d.ts.map +1 -0
- package/dist/workers/centrifuge.worker.d.ts +2 -0
- package/dist/workers/centrifuge.worker.d.ts.map +1 -0
- package/dist/workers/port-reaper.d.ts +52 -0
- package/dist/workers/port-reaper.d.ts.map +1 -0
- package/docs/README.md +21 -0
- package/docs/api.md +261 -0
- package/docs/architecture.md +514 -0
- package/docs/capabilities.md +41 -0
- package/docs/configuration.md +211 -0
- package/docs/getting-started.md +161 -0
- package/docs/zh/README.md +23 -0
- package/docs/zh/api.md +261 -0
- package/docs/zh/architecture.md +515 -0
- package/docs/zh/capabilities.md +41 -0
- package/docs/zh/configuration.md +211 -0
- package/docs/zh/getting-started.md +161 -0
- package/package.json +71 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
> [中文](./zh/configuration.md) | English
|
|
2
|
+
|
|
3
|
+
# Configuration
|
|
4
|
+
|
|
5
|
+
## Core DataBus Configuration
|
|
6
|
+
|
|
7
|
+
`CrossTabDataBus<TConfig, TData>` accepts `CrossTabDataBusOptions<TConfig, TData>`.
|
|
8
|
+
|
|
9
|
+
| Config | Type | Default | Description |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `clusterKey` | `string` | Required | Isolates different connection contexts; not written to storage in plaintext |
|
|
12
|
+
| `transport` | `DataBusTransport<TConfig, TData>` | Required | Actual connection and subscription implementation |
|
|
13
|
+
| `initialConfig` | `TConfig` | None | Passed to transport on auto-start |
|
|
14
|
+
| `autoStart` | `boolean` | `true` when `initialConfig` is provided | Whether to auto-start after instance creation |
|
|
15
|
+
| `storagePrefix` | `string` | `cross-tab-worker-databus` | Namespace for storage keys and BroadcastChannel |
|
|
16
|
+
| `maxActiveWorkers` | `number` | `3` | Maximum number of Workers that can be Topic owners |
|
|
17
|
+
| `heartbeatIntervalMs` | `number` | `3000` | Worker heartbeat interval |
|
|
18
|
+
| `workerTtlMs` | `number` | `10000` | Worker expiry threshold |
|
|
19
|
+
| `environment` | `ClusterEnvironment` | Browser native environment | Used for testing, embedded environments, or capability replacement |
|
|
20
|
+
| `tabId` | `string` | Auto-generated | Advanced debugging and test injection; not recommended for production use |
|
|
21
|
+
| `workerId` | `string` | Auto-generated | Advanced debugging and test injection; not recommended for production use |
|
|
22
|
+
| `trace` | `DataBusTraceOptions` | Disabled | Optional diagnostic events, message throughput, and distribution latency aggregates; does not affect data transfer |
|
|
23
|
+
|
|
24
|
+
## Diagnostics & Throughput Metrics
|
|
25
|
+
|
|
26
|
+
Trace is disabled by default. When enabled, lifecycle, connection status, coordination mode, and subscription count changes are output immediately; high-frequency messages are aggregated by count rather than printed individually.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const bus = new CrossTabDataBus({
|
|
30
|
+
clusterKey: 'realtime-feed',
|
|
31
|
+
initialConfig: {},
|
|
32
|
+
transport,
|
|
33
|
+
trace: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
mode: 'all',
|
|
36
|
+
metricsIntervalMs: 5000,
|
|
37
|
+
sink: event => console.info('[DataBus]', event)
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Config | Type | Default | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `enabled` | `boolean` | `false` | Master switch |
|
|
45
|
+
| `mode` | `'events' \| 'metrics' \| 'all'` | `'all'` | Low-frequency events only, aggregated metrics only, or both |
|
|
46
|
+
| `metricsIntervalMs` | `number` | `5000` | Aggregation window; must be a finite value greater than 0 |
|
|
47
|
+
| `sink` | `(event) => void` | Required | Determined by the integrator: console, monitoring SDK, or other output |
|
|
48
|
+
|
|
49
|
+
`message_metrics` contains window duration, received count, dispatched count, active Topic count, and receive-to-dispatch latency sample count, average, P50, P95, and maximum. Latency is aggregated in 50ms buckets and never contains an individual message payload. Subscription events include their Topic so an integrator can correlate ownership changes. They are emitted only when the owner transport subscription set changes; idempotent `CONTROL` retries do not produce duplicate subscription events. Treat trace sinks as diagnostic surfaces: redact sensitive Topic conventions before writing to a console or external telemetry. Trace events do not include URLs, credentials, payloads, or error bodies. Errors thrown in the sink are isolated and will not interrupt message distribution, but will output to `console.warn` to help integrators discover diagnostic configuration issues.
|
|
50
|
+
|
|
51
|
+
On `pagehide`, the aggregation timer stops and discards the incomplete window; on `pageshow`, it resumes with a new window. A permanent `stop()` clears the timer. Only diagnostics output is throttled; actual message reception and distribution are never rate-limited.
|
|
52
|
+
|
|
53
|
+
### Timing Parameter Constraints
|
|
54
|
+
|
|
55
|
+
- `workerTtlMs` should be at least twice `heartbeatIntervalMs`.
|
|
56
|
+
- A TTL that is too short may cause false migrations during background scheduling jitter.
|
|
57
|
+
- A TTL that is too long delays recovery of abnormal tabs.
|
|
58
|
+
- The default `3000/10000` is suitable for general desktop browser real-time scenarios.
|
|
59
|
+
|
|
60
|
+
### TTL Message-Loss Window
|
|
61
|
+
|
|
62
|
+
When a worker dies abnormally (e.g., tab crash):
|
|
63
|
+
|
|
64
|
+
- Other workers detect the death only after `workerTtlMs` (default 10 000 ms) — the stale record is pruned during the next reconciliation cycle.
|
|
65
|
+
- The periodic heartbeat writes to localStorage without a BroadcastChannel notification, so a full heartbeat interval may pass before the stale record is discovered.
|
|
66
|
+
- **Worst-case window**: up to `heartbeatIntervalMs + workerTtlMs` (~13 s by default). During this window, topics owned by the dead worker receive no service — publications to those topics are lost.
|
|
67
|
+
- **Mitigation**: reduce `heartbeatIntervalMs` and `workerTtlMs` proportionally (e.g. 1 s / 4 s). This increases storage write frequency and raises the risk of false migrations during scheduling jitter.
|
|
68
|
+
|
|
69
|
+
The default 3 s / 10 s values are suitable for general desktop browser real-time scenarios. Tune based on your tolerance for missed publications vs. false migration rate.
|
|
70
|
+
|
|
71
|
+
### Active Worker Count
|
|
72
|
+
|
|
73
|
+
`maxActiveWorkers` limits the number of Workers that can become Topic owners, not the number of connections established by transport. In Dedicated Worker mode, each Tab can still create its own Worker; in SharedWorker mode, same-origin Tabs reuse the same Worker.
|
|
74
|
+
|
|
75
|
+
The active set is used only when a Topic needs a new owner. An existing live owner keeps its established routes even if visibility changes later mark that Worker as standby or place it outside the current candidate set.
|
|
76
|
+
|
|
77
|
+
- `1`: Minimum connections and subscriptions, but the single owner bears a concentrated load.
|
|
78
|
+
- `2-3`: Balances resource reuse and fault recovery.
|
|
79
|
+
- Larger values: Suitable for scenarios with many Topics and where a single connection faces server-side limits.
|
|
80
|
+
|
|
81
|
+
## Centrifuge Configuration
|
|
82
|
+
|
|
83
|
+
Main configuration for `createCentrifugeDataBus<TData>(options)`:
|
|
84
|
+
|
|
85
|
+
| Config | Type | Default | Description |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `connection.url` | `string` | Required | Centrifuge connection URL |
|
|
88
|
+
| `connection.options` | `CentrifugeWorkerConfig` | `{}` | Client configuration sent to the Worker |
|
|
89
|
+
| `clusterKey` | `string` | `connection.url` | Manually isolate logical clusters |
|
|
90
|
+
| `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 |
|
|
91
|
+
| `transferable` | `boolean` | `false` | When enabled, `publish(topic, ArrayBuffer)` uses Transferable transport; ArrayBuffer publications on the receiving side also follow the transfer path |
|
|
92
|
+
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely. Distinct from the Core cluster heartbeat (default 3000 ms) which tracks worker liveness via localStorage |
|
|
93
|
+
| `workerFactory` | `() => Worker` | Built-in Worker | For testing or custom Worker loading |
|
|
94
|
+
| `sharedWorkerFactory` | `() => SharedWorker` | Built-in SharedWorker | For testing or custom SharedWorker loading |
|
|
95
|
+
| Other Core config | Corresponding type | Core defaults | `storagePrefix`, heartbeat, TTL, etc. |
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const bus = createCentrifugeDataBus({
|
|
99
|
+
connection: {
|
|
100
|
+
url: getConnectionUrl(),
|
|
101
|
+
options: {
|
|
102
|
+
token: getConnectionCredential(),
|
|
103
|
+
timeout: 5000,
|
|
104
|
+
maxServerPingDelay: 10000
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
maxActiveWorkers: 3,
|
|
108
|
+
heartbeatIntervalMs: 3000,
|
|
109
|
+
workerTtlMs: 10000
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Worker Mode & Degradation
|
|
114
|
+
|
|
115
|
+
`workerMode` controls how the Centrifuge transport operates:
|
|
116
|
+
|
|
117
|
+
- `dedicated` (default): Each Tab creates its own Dedicated Worker; best compatibility.
|
|
118
|
+
- `shared`: Same-origin Tabs reuse the same SharedWorker; within the Worker, each connection port maintains its own independent `CentrifugeSession`. Stopping or refreshing one Tab does not affect other Tabs' connections.
|
|
119
|
+
- `auto`: Selects SharedWorker at runtime, degrades to Dedicated Worker if unsupported, and finally degrades to main-thread local mode.
|
|
120
|
+
|
|
121
|
+
The full chain for `auto` is **SharedWorker -> Dedicated Worker -> main-thread local mode**; the chain for explicit `shared` is identical to `auto` (**SharedWorker -> Dedicated Worker -> main-thread local mode**); the chain for explicit `dedicated` is **Dedicated Worker -> SharedWorker -> main-thread local mode**.
|
|
122
|
+
|
|
123
|
+
`shared` does not refuse to degrade: if the browser lacks `SharedWorker` support, it falls back through the same chain as `auto`. The only difference between `shared` and `auto` is the preference order's first choice — `shared` always prefers the SharedWorker, while `auto` performs the same selection but is the mode used when the caller has no strong preference.
|
|
124
|
+
|
|
125
|
+
When neither `sharedWorkerFactory` nor `workerFactory` is provided, the transport detects global `SharedWorker` / `Worker` capability at startup. When a custom factory is provided, the corresponding backend is considered available, avoiding false negatives from global capability detection in Node, SSR, or embedded environments. All modes perform the same structured clone validation; config and `publish` data must be structured-clonable.
|
|
126
|
+
|
|
127
|
+
## SharedWorker Session Reaper
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
- **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely — use this only when the SharedWorker reaper is not needed (e.g. the SharedWorker is guaranteed to be torn down with the tab).
|
|
132
|
+
- **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. This is distinct from the Core cluster heartbeat (default `3000` ms) which tracks worker liveness via localStorage — see the note below.
|
|
133
|
+
- **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.
|
|
134
|
+
- **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.
|
|
135
|
+
|
|
136
|
+
This is the mechanism that recovers sessions for tabs that crash without sending `STOP`. Lower `heartbeatIntervalMs` to reap dead sessions faster, at the cost of more frequent PING messages on the port.
|
|
137
|
+
|
|
138
|
+
`heartbeatIntervalMs` must be a positive number or `Infinity`; a value of `0`, a negative number, or `NaN` causes the transport constructor to throw a `TypeError` immediately (such a value would otherwise degenerate `setInterval` into a 0ms busy loop).
|
|
139
|
+
|
|
140
|
+
> **Two heartbeats, don't confuse them.** The Core `heartbeatIntervalMs` (default `3000` ms) is the cluster heartbeat — the WorkerClusterRuntime writes its liveness record to localStorage on this cadence. The Centrifuge `heartbeatIntervalMs` (default `10000` ms) is the SharedWorker PING heartbeat documented here. They are independent and both appear in the config surface; the Centrifuge one is only meaningful in `shared` mode.
|
|
141
|
+
|
|
142
|
+
## Binary Message Transfer
|
|
143
|
+
|
|
144
|
+
`transferable: true` enables the ArrayBuffer optimization path without changing the external API:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const bus = createCentrifugeDataBus({
|
|
148
|
+
connection: { url: 'wss://example.test/connection/websocket' },
|
|
149
|
+
transferable: true
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
bus.publish('resource.command', binaryBuffer);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When enabled, `publish(topic, ArrayBuffer)` uses `PUBLISH_BIN` for Worker transport and adds the buffer to the transfer list, avoiding structured clone copying. ArrayBuffer publications returned by the Worker are transferred back to the main thread via `MESSAGE_BIN`. The business layer still only sees `DataBusMessage<TData>`; object, string, and number payloads continue along the existing object message path. When disabled, ArrayBuffers are copied via structured clone like ordinary objects.
|
|
156
|
+
|
|
157
|
+
## Worker Structured Clone Constraints
|
|
158
|
+
|
|
159
|
+
In Worker mode, config is sent via `Worker` / `SharedWorker` `postMessage`; local mode also performs the same validation. Values must be structured-clonable. The following config items must not be passed directly:
|
|
160
|
+
|
|
161
|
+
- `getToken`
|
|
162
|
+
- `getData`
|
|
163
|
+
- Custom `websocket`
|
|
164
|
+
- Custom `fetch`
|
|
165
|
+
- `eventsource`
|
|
166
|
+
- `sockjs`
|
|
167
|
+
- `networkEventTarget`
|
|
168
|
+
- `ReadableStream` and other runtime objects
|
|
169
|
+
|
|
170
|
+
When non-clonable data is passed, `CentrifugeWorkerTransport` will throw a clear `TypeError`.
|
|
171
|
+
|
|
172
|
+
## Storage Data Boundaries
|
|
173
|
+
|
|
174
|
+
Storage only holds:
|
|
175
|
+
|
|
176
|
+
- Worker ID, Tab ID, status, visibility, load, and heartbeat
|
|
177
|
+
- Opaque Topic key, owner Worker, and last update time
|
|
178
|
+
- Topic subscriber's Tab ID
|
|
179
|
+
|
|
180
|
+
Storage does NOT hold:
|
|
181
|
+
|
|
182
|
+
- Connection URL plaintext
|
|
183
|
+
- Topic names
|
|
184
|
+
- Connection credentials
|
|
185
|
+
- Publication data
|
|
186
|
+
- Publish data
|
|
187
|
+
|
|
188
|
+
Note: BroadcastChannel coordination messages carry topic names, event types, and publication payloads in plaintext (in-memory only). Only localStorage metadata is hashed via `createOpaqueKey()`.
|
|
189
|
+
|
|
190
|
+
## Security & Trust Model
|
|
191
|
+
|
|
192
|
+
The coordination plane has **no authentication**. Only use this library on pages where every same-origin script is trusted:
|
|
193
|
+
|
|
194
|
+
- `BroadcastChannel` messages are delivered to **every same-origin tab**, unencrypted, and any script in that origin can send or receive them. `localStorage` coordination records can likewise be read and written by any same-origin script.
|
|
195
|
+
- A malicious or buggy same-origin script can forge Worker records, hijack topic ownership, read Topic names and publication payloads from the BroadcastChannel, inject publications, or impersonate subscribers. Topics and payloads traverse the BroadcastChannel **in plaintext** (in memory only) — do not place credentials, tokens, or PII in Topic names or in coordinated message payloads beyond what your server would send anyway.
|
|
196
|
+
- `clusterKey` provides **isolation, not security**: it only prevents *accidental* cross-talk between logical clusters. It does not stop a script that can read `localStorage` or listen on the BroadcastChannel, because the opaque keys and channel name are derived from the same origin and can be recomputed. It also does not protect against scripts that read your page's own runtime state.
|
|
197
|
+
- `clusterKey` is hashed via `createOpaqueKey` (a non-cryptographic 128-bit hash) to derive the storage prefix and BroadcastChannel name. In practice `clusterKey` is always a connection URL or a developer-controlled namespace, so a hash collision between two different clusterKeys (~2⁻⁶⁴ birthday bound) is not a practical concern.
|
|
198
|
+
- Mitigations: keep the page free of untrusted third-party scripts; load coordination on an isolated origin; treat the origin's `localStorage` and BroadcastChannel namespaces as public. CSP cannot restrict BroadcastChannel or localStorage access from same-origin scripts.
|
|
199
|
+
|
|
200
|
+
The transport plane (e.g. the Centrifuge WebSocket) has its own security model — tokens, TLS, and server-side permissions — and is unaffected by the above. The cluster only routes which tab owns the transport subscription; it never proxies the payload through `localStorage` (payloads travel via BroadcastChannel in memory or via the server directly).
|
|
201
|
+
|
|
202
|
+
## Cluster Isolation Recommendations
|
|
203
|
+
|
|
204
|
+
The following contexts must use different `clusterKey`:
|
|
205
|
+
|
|
206
|
+
- Different server connections
|
|
207
|
+
- Different authentication identities
|
|
208
|
+
- Different data permission scopes
|
|
209
|
+
- Different protocol versions
|
|
210
|
+
|
|
211
|
+
When using the Centrifuge factory, the default connection URL usually provides sufficient isolation. When identity or permissions change under the same URL, an explicit `clusterKey` should be provided that includes the context version but not the credential plaintext.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
> [中文](./zh/getting-started.md) | English
|
|
2
|
+
|
|
3
|
+
# Getting Started
|
|
4
|
+
|
|
5
|
+
## 1. Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add cross-tab-worker-databus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package provides the following entry points:
|
|
12
|
+
|
|
13
|
+
- `cross-tab-worker-databus`: the core DataBus and transport interfaces
|
|
14
|
+
- `cross-tab-worker-databus/centrifuge`: the built-in Centrifuge Worker transport
|
|
15
|
+
- `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
|
|
16
|
+
|
|
17
|
+
## 2. Creating an Instance
|
|
18
|
+
|
|
19
|
+
It is recommended to create an instance in the application's infrastructure layer and have other modules import it directly. This way, business modules within the same Tab share the Worker, connection, and Topic references.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createCentrifugeDataBus } from 'cross-tab-worker-databus/centrifuge';
|
|
23
|
+
|
|
24
|
+
export interface ResourceEvent {
|
|
25
|
+
id: string;
|
|
26
|
+
version: number;
|
|
27
|
+
content: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const dataBus = createCentrifugeDataBus<ResourceEvent>({
|
|
31
|
+
connection: {
|
|
32
|
+
url: getConnectionUrl(),
|
|
33
|
+
options: getConnectionOptions()
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`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.
|
|
39
|
+
|
|
40
|
+
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'`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export const dataBus = createCentrifugeDataBus<ResourceEvent>({
|
|
44
|
+
connection: {
|
|
45
|
+
url: getConnectionUrl(),
|
|
46
|
+
options: getConnectionOptions()
|
|
47
|
+
},
|
|
48
|
+
workerMode: 'auto'
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`auto` prefers SharedWorker when available, otherwise falls back to Dedicated Worker, and finally to the main-thread local mode.
|
|
53
|
+
|
|
54
|
+
## 3. Subscribing
|
|
55
|
+
|
|
56
|
+
You can subscribe immediately after the instance is created, without waiting for the connection to complete.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const unsubscribe = dataBus.subscribe('resource.changed', message => {
|
|
60
|
+
applyResourceEvent(message.data);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When the connection is not yet ready, the SDK saves the subscription intent and executes it after the transport is ready. Multiple handlers for the same Topic use local reference counting, producing only a single cluster subscription.
|
|
65
|
+
|
|
66
|
+
## 4. Waiting for Connection
|
|
67
|
+
|
|
68
|
+
Most business logic does not need to call `ready()`. Only wait when subsequent steps must confirm that the Worker has been created and transport's `start` has completed:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await dataBus.ready();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`ready()` does not guarantee that the server has finished authentication; the connection state is determined by the `onStatus` callback.
|
|
75
|
+
|
|
76
|
+
If the instance was created without `initialConfig` and you call `ready()` before an explicit `start(config)`, the returned Promise rejects instead of throwing; attach `.catch` and retry with `start(config)` when appropriate.
|
|
77
|
+
|
|
78
|
+
## 5. Publishing
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
dataBus.publish('resource.command', {
|
|
82
|
+
action: 'refresh',
|
|
83
|
+
targetId: 'resource-id'
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Only use `publish` when the server protocol allows the client to publish. The SDK does not automatically replay publish operations that were not executed due to page suspension, to avoid side effects from stale commands.
|
|
88
|
+
|
|
89
|
+
## 6. Status and Errors
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const removeStatusListener = dataBus.onStatus(status => {
|
|
93
|
+
updateConnectionIndicator(status);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const removeErrorListener = dataBus.onError(error => {
|
|
97
|
+
reportDataBusError(error);
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Status values:
|
|
102
|
+
|
|
103
|
+
- `connecting`
|
|
104
|
+
- `connected`
|
|
105
|
+
- `disconnected`
|
|
106
|
+
- `error`
|
|
107
|
+
|
|
108
|
+
## 7. Cleanup
|
|
109
|
+
|
|
110
|
+
Release a single module subscription:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
unsubscribe();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Fully destroy an instance:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
removeStatusListener();
|
|
120
|
+
removeErrorListener();
|
|
121
|
+
await dataBus.stop();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Normal Tab hiding, entering BFCache, and restoring do not require business logic to call `stop()` or re-subscribe; the SDK handles this automatically.
|
|
125
|
+
|
|
126
|
+
## 8. Running the Multi-Tab Demo
|
|
127
|
+
|
|
128
|
+
The repository's `examples/demo` provides a browser demo page where you can see the full flow of messages across Tabs, clusters, Worker sessions, and the server:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pnpm install
|
|
132
|
+
pnpm build
|
|
133
|
+
pnpm examples
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
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:
|
|
137
|
+
|
|
138
|
+
- Connecting to the public Centrifugo demo address `wss://faye.centrifugal.dev/connection/websocket` by default
|
|
139
|
+
- Modifying the WSS address, `workerMode`, Topic, and `transferable` configuration in the page
|
|
140
|
+
- Switching to "Local Broadcast" mode, which does not depend on an external server and demonstrates multi-Tab collaboration using only BroadcastChannel
|
|
141
|
+
- Data flow animations, event stream, distribution latency metrics, and cluster Worker routing status
|
|
142
|
+
- SDK capability, transport configuration, active/standby Worker, and visible/hidden Tab state
|
|
143
|
+
|
|
144
|
+
When consuming the repository directly through a Git dependency, use a pinned commit. The repository ships `dist` so consumers do not need to build the SDK during installation.
|
|
145
|
+
|
|
146
|
+
## 9. Explicit Start
|
|
147
|
+
|
|
148
|
+
When a custom transport's configuration needs to be fetched asynchronously, you can omit `initialConfig` and explicitly start once preparation is complete:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { CrossTabDataBus } from 'cross-tab-worker-databus';
|
|
152
|
+
|
|
153
|
+
const bus = new CrossTabDataBus({
|
|
154
|
+
clusterKey: 'shared-resource-stream',
|
|
155
|
+
transport
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const config = await loadTransportConfig();
|
|
159
|
+
await bus.start(config);
|
|
160
|
+
bus.subscribe('resource.changed', handleResourceEvent);
|
|
161
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
> 中文 | [English](../README.md)
|
|
2
|
+
|
|
3
|
+
# 文档索引(中文版)
|
|
4
|
+
|
|
5
|
+
本目录为中文版本地化文档。英文原版位于 [docs/](../README.md)。
|
|
6
|
+
|
|
7
|
+
| 文档 | 内容 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [快速接入](./getting-started.md) | 安装、创建实例、订阅、发布和销毁 |
|
|
10
|
+
| [配置说明](./configuration.md) | 核心配置、Centrifuge 配置、默认值和约束 |
|
|
11
|
+
| [API 参考](./api.md) | 公共入口、类型、方法、返回值和行为 |
|
|
12
|
+
| [架构说明](./architecture.md) | Worker 集群、路由、存储、迁移和降级设计 |
|
|
13
|
+
| [能力矩阵](./capabilities.md) | 已实现、未实现和计划待实现的能力矩阵 |
|
|
14
|
+
| [../..//examples/demo](../../examples/demo) | 可运行的多标签浏览器演示 |
|
|
15
|
+
| [../../CHANGELOG.md](../../CHANGELOG.md) | 版本变更记录 |
|
|
16
|
+
|
|
17
|
+
## 阅读顺序
|
|
18
|
+
|
|
19
|
+
1. 首次接入阅读 [快速接入](./getting-started.md)。
|
|
20
|
+
2. 生产配置阅读 [配置说明](./configuration.md)。
|
|
21
|
+
3. 开发封装或自定义 transport 时阅读 [API 参考](./api.md)。
|
|
22
|
+
4. 排查跨 Tab 行为时阅读 [架构说明](./architecture.md)。
|
|
23
|
+
5. 评估当前边界和后续计划时阅读 [能力矩阵](./capabilities.md)。
|
package/docs/zh/api.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
> 中文 | [English](../api.md)
|
|
2
|
+
|
|
3
|
+
# API 参考
|
|
4
|
+
|
|
5
|
+
## 包入口
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
CrossTabDataBus,
|
|
10
|
+
type DataBusTraceEvent,
|
|
11
|
+
WorkerClusterRuntime,
|
|
12
|
+
createBrowserEnvironment,
|
|
13
|
+
createOpaqueKey,
|
|
14
|
+
selectWorkerBackend,
|
|
15
|
+
type WorkerMode
|
|
16
|
+
} from 'cross-tab-worker-databus';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
CentrifugeWorkerTransport,
|
|
20
|
+
createCentrifugeDataBus
|
|
21
|
+
} from 'cross-tab-worker-databus/centrifuge';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
业务接入优先使用 `CrossTabDataBus` 或 `createCentrifugeDataBus`。`WorkerClusterRuntime` 属于高级协调 API。
|
|
25
|
+
|
|
26
|
+
## `CrossTabDataBus<TConfig, TData>`
|
|
27
|
+
|
|
28
|
+
### constructor
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
new CrossTabDataBus<TConfig, TData>(options)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
创建 DataBus。传入 `initialConfig` 时默认自动启动。
|
|
35
|
+
|
|
36
|
+
### `start(config)`
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
start(config: TConfig): Promise<void>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
启动集群协调和 transport。首次调用真正启动 transport;启动过程中并发调用共享同一个启动 Promise,不重复创建 transport。启动成功或失败后,内部 gate 会重置:之后再次调用是已启动的空操作(立即 resolve),不会重复启动;`stop()` 之后可重新调用再次启动。
|
|
43
|
+
|
|
44
|
+
### `ready()`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
ready(): Promise<void>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
等待当前 transport 的 `start` 完成。自动启动失败时 Promise 会 reject;再次调用可以触发基于 `initialConfig` 的重试。
|
|
51
|
+
|
|
52
|
+
未传入 `initialConfig` 且未显式调用 `start(config)` 时,`ready()` 返回 rejected Promise 而不是同步抛出,调用方可以统一通过 `.catch` 处理并决定是否显式启动。
|
|
53
|
+
|
|
54
|
+
`ready()` 不等价于服务端已连接,协议连接状态通过 `onStatus` 获取。
|
|
55
|
+
|
|
56
|
+
### `subscribe(topic, handler)`
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
subscribe(
|
|
60
|
+
topic: string,
|
|
61
|
+
handler: DataBusMessageHandler<TData>
|
|
62
|
+
): () => void
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
登记本地订阅并返回释放函数。
|
|
66
|
+
|
|
67
|
+
- 同一 Topic 的多个 handler 使用引用计数。
|
|
68
|
+
- 当前 Tab 第一个 handler 会登记集群订阅。
|
|
69
|
+
- 最后一个 handler 释放后,当前 Tab 才退出该 Topic。
|
|
70
|
+
- transport 尚未 ready 时订阅自动排队。
|
|
71
|
+
|
|
72
|
+
### `unsubscribe(topic, handler?)`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
unsubscribe(topic: string, handler?: DataBusMessageHandler<TData>): void
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
传入 handler 时只释放对应回调;省略 handler 时释放当前实例中该 Topic 的全部 handler。
|
|
79
|
+
|
|
80
|
+
优先使用 `subscribe` 返回的释放函数,避免误删其他模块的回调。
|
|
81
|
+
|
|
82
|
+
### `publish(topic, data)`
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
publish(topic: string, data: unknown): void
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
将发布操作路由到当前 Topic owner;没有有效路由时使用当前 Worker。
|
|
89
|
+
|
|
90
|
+
发布数据必须满足底层 transport 的序列化约束。SDK 不会在页面暂停期间持久化或延迟重放发布命令。
|
|
91
|
+
|
|
92
|
+
当 owner 是远端 Tab、且发布控制消息无法投递时(例如 BroadcastChannel 无法克隆 payload),`publish()` 会通过 `onError` 上报失败,而不是静默丢弃。
|
|
93
|
+
|
|
94
|
+
### `onStatus(handler)`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
onStatus(handler: DataBusStatusHandler): () => void
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
监听 transport 状态。注册后立即收到当前状态。
|
|
101
|
+
|
|
102
|
+
### `onError(handler)`
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
onError(handler: DataBusErrorHandler): () => void
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
监听启动、订阅、退订、发布和 Worker 错误。
|
|
109
|
+
|
|
110
|
+
### `getStatus()`
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
getStatus(): WorkerStatus
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
返回当前状态:`connecting`、`connected`、`disconnected` 或 `error`。
|
|
117
|
+
|
|
118
|
+
### `getClusterSnapshot()`
|
|
119
|
+
|
|
120
|
+
返回诊断快照:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface WorkerClusterSnapshot {
|
|
124
|
+
coordinated: boolean;
|
|
125
|
+
suspended: boolean;
|
|
126
|
+
currentWorker: WorkerRecord;
|
|
127
|
+
workers: WorkerRecord[];
|
|
128
|
+
/** 路由记录,从 knownTopics 缓存注入明文 topic。 */
|
|
129
|
+
routes: Array<WorkerRoute & { topic: string | null }>;
|
|
130
|
+
subscribedTopics: string[];
|
|
131
|
+
assignedTopics: string[];
|
|
132
|
+
/** 不透明 key → 明文 topic 的映射,用于调试。 */
|
|
133
|
+
knownTopics: Array<{ topicKey: string; topic: string }>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
快照用于诊断和测试,不应作为业务状态源。
|
|
138
|
+
|
|
139
|
+
使用 `console.table(snapshot.routes)` 查看所有路由及其明文 topic,或 `snapshot.knownTopics` 关联不透明 key 与 topic。
|
|
140
|
+
|
|
141
|
+
### `trace`
|
|
142
|
+
|
|
143
|
+
通过构造配置启用可选诊断:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
trace: {
|
|
147
|
+
enabled: true,
|
|
148
|
+
mode: 'all',
|
|
149
|
+
metricsIntervalMs: 5000,
|
|
150
|
+
sink: (event: DataBusTraceEvent) => report(event)
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
低频事件类型包括 `lifecycle`、`status`、`subscription`、`coordination` 和 `error`;高频数据按窗口输出 `message_metrics`,包含接收/分发计数、活跃 Topic 数量和分发延迟聚合(`dispatchSamples`、`dispatchAvgMs`、`dispatchP50Ms`、`dispatchP95Ms`、`dispatchMaxMs`)。所有公开事件都使用固定结构,不包含原始 Topic、消息 payload、连接地址或错误正文。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,便于定位诊断配置问题。sink 应尽量避免抛出异常——预期中的错误条件应通过事件数据表达,而不是通过异常上报。
|
|
155
|
+
|
|
156
|
+
### `stop()`
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
stop(): Promise<void>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
永久销毁当前实例:清理 handler、集群注册、路由、Worker 和 transport。普通页面隐藏和恢复不需要调用。
|
|
163
|
+
|
|
164
|
+
## `DataBusTransport<TConfig, TData>`
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface DataBusTransport<TConfig, TData> {
|
|
168
|
+
start(config, handlers): void | Promise<void>;
|
|
169
|
+
subscribe(topic): void | Promise<void>;
|
|
170
|
+
unsubscribe(topic): void | Promise<void>;
|
|
171
|
+
publish(topic, data): void | Promise<void>;
|
|
172
|
+
stop(): void | Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
实现要求:
|
|
177
|
+
|
|
178
|
+
- `subscribe` 和 `unsubscribe` 必须幂等。
|
|
179
|
+
- `stop` 后必须允许再次 `start`,用于 BFCache 恢复。
|
|
180
|
+
- 收到数据时调用 `handlers.onMessage({ topic, data })`。
|
|
181
|
+
- 状态变化时调用 `handlers.onStatus(status)`。
|
|
182
|
+
- 异步错误通过 reject 或 `handlers.onError(error)` 上报。
|
|
183
|
+
|
|
184
|
+
## `createCentrifugeDataBus<TData>(options)`
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
createCentrifugeDataBus<TData>(options): CrossTabDataBus<CentrifugeDataBusConfig, TData>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
创建自动启动的 Centrifuge DataBus。默认:
|
|
191
|
+
|
|
192
|
+
- `clusterKey = connection.url`
|
|
193
|
+
- `workerMode = 'dedicated'`,每个 Tab 使用独立 Dedicated Worker
|
|
194
|
+
- 使用包内 `centrifuge.worker.js`
|
|
195
|
+
- Worker 名称为 `cross-tab-worker-databus`
|
|
196
|
+
|
|
197
|
+
SharedWorker 模式使用包内 `centrifuge.shared.worker.js`。`workerMode: 'auto'` 时按 SharedWorker → Dedicated Worker → 本地模式降级。完整配置见 [configuration.md](./configuration.md)。
|
|
198
|
+
|
|
199
|
+
## `CentrifugeWorkerTransport<TData>`
|
|
200
|
+
|
|
201
|
+
低层 Centrifuge transport。只有需要自定义 DataBus 组装时才直接创建:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const transport = new CentrifugeWorkerTransport({
|
|
205
|
+
workerMode: 'auto',
|
|
206
|
+
workerFactory: () => new Worker(customWorkerUrl, { type: 'module' }),
|
|
207
|
+
sharedWorkerFactory: () => new SharedWorker(customSharedWorkerUrl, { type: 'module' })
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
可用选项:
|
|
212
|
+
|
|
213
|
+
- `workerMode`:`'dedicated'`(默认)、`'shared'` 或 `'auto'`;`auto` 的降级链路为 SharedWorker → Dedicated Worker → 本地模式
|
|
214
|
+
- `transferable`:`boolean`,默认 `false`;开启后 ArrayBuffer payload 使用 Transferable 传输,对象消息 API 不变
|
|
215
|
+
- `heartbeatIntervalMs`:`number`,默认 `10000`;SharedWorker PING 心跳间隔(毫秒)。传 `Infinity` 完全禁用心跳。详见 [配置](./configuration.md#sharedworker-会话回收)
|
|
216
|
+
- `workerFactory`:自定义 Dedicated Worker 加载方式
|
|
217
|
+
- `sharedWorkerFactory`:自定义 SharedWorker 加载方式
|
|
218
|
+
|
|
219
|
+
## `WorkerClusterRuntime`
|
|
220
|
+
|
|
221
|
+
高级 API,负责 Worker 注册、心跳、可见性、路由、BroadcastChannel 协议和迁移。业务模块不应直接操作它。
|
|
222
|
+
|
|
223
|
+
主要方法:
|
|
224
|
+
|
|
225
|
+
- `start()` / `stop()`
|
|
226
|
+
- `setStatus(status)`
|
|
227
|
+
- `subscribe(topic)` / `unsubscribe(topic)`
|
|
228
|
+
- `publish(topic, data)`
|
|
229
|
+
- `broadcastEvent(eventType, payload)`
|
|
230
|
+
- `isAssigned(topic)`
|
|
231
|
+
- `isActiveWorker()`
|
|
232
|
+
- `hasLocalSubscriber(topic)`
|
|
233
|
+
- `getSnapshot()`
|
|
234
|
+
|
|
235
|
+
## 工具函数
|
|
236
|
+
|
|
237
|
+
### `createOpaqueKey(value)`
|
|
238
|
+
|
|
239
|
+
生成稳定的 128-bit 十六进制不透明 key。用于避免把连接或 Topic 原文写入协调元数据;它不是密码学摘要,不应用于密码存储或安全签名。
|
|
240
|
+
|
|
241
|
+
### `createBrowserEnvironment()`
|
|
242
|
+
|
|
243
|
+
创建默认浏览器环境适配器,包含 storage、BroadcastChannel、定时器和页面生命周期事件。
|
|
244
|
+
|
|
245
|
+
### `selectWorkerBackend(mode, availability?)`
|
|
246
|
+
|
|
247
|
+
按 `WorkerMode` 和能力检测选择实际后端,返回 `'shared' | 'dedicated' | 'local'`:
|
|
248
|
+
|
|
249
|
+
- `shared` / `auto`:SharedWorker → Dedicated Worker → 本地模式
|
|
250
|
+
- `dedicated`(默认):Dedicated Worker → SharedWorker → 本地模式
|
|
251
|
+
|
|
252
|
+
`availability` 可显式传入 `worker` / `sharedWorker` 能力标记,用于 SSR、测试或嵌入环境,避免访问不存在的全局对象。
|
|
253
|
+
|
|
254
|
+
### 路由选择函数
|
|
255
|
+
|
|
256
|
+
- `selectActiveWorkers`
|
|
257
|
+
- `selectLeastLoadedWorker`
|
|
258
|
+
- `selectRebalanceTarget`
|
|
259
|
+
- `hasActiveOwner`
|
|
260
|
+
|
|
261
|
+
这些纯函数主要用于测试、诊断和自定义协调策略。
|