cross-tab-worker-databus 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +32 -0
  3. package/README.zh.md +30 -0
  4. package/dist/centrifuge-protocol.d.ts +51 -15
  5. package/dist/centrifuge-protocol.d.ts.map +1 -1
  6. package/dist/centrifuge-session.d.ts +11 -3
  7. package/dist/centrifuge-session.d.ts.map +1 -1
  8. package/dist/centrifuge.d.ts +10 -2
  9. package/dist/centrifuge.d.ts.map +1 -1
  10. package/dist/centrifuge.js +69 -26
  11. package/dist/centrifuge.js.map +2 -2
  12. package/dist/centrifuge.shared.worker.js +73 -25
  13. package/dist/centrifuge.shared.worker.js.map +2 -2
  14. package/dist/centrifuge.worker.js +41 -12
  15. package/dist/centrifuge.worker.js.map +2 -2
  16. package/dist/{chunk-53INHVYO.js → chunk-LBXREMZA.js} +263 -158
  17. package/dist/chunk-LBXREMZA.js.map +7 -0
  18. package/dist/core/cluster.d.ts +49 -4
  19. package/dist/core/cluster.d.ts.map +1 -1
  20. package/dist/core/data-bus.d.ts +19 -2
  21. package/dist/core/data-bus.d.ts.map +1 -1
  22. package/dist/core/environment.d.ts +16 -2
  23. package/dist/core/environment.d.ts.map +1 -1
  24. package/dist/core/hash.d.ts.map +1 -1
  25. package/dist/core/routing.d.ts +5 -2
  26. package/dist/core/routing.d.ts.map +1 -1
  27. package/dist/core/storage-batch.d.ts +12 -0
  28. package/dist/core/storage-batch.d.ts.map +1 -1
  29. package/dist/core/trace.d.ts +16 -1
  30. package/dist/core/trace.d.ts.map +1 -1
  31. package/dist/core/types.d.ts +76 -21
  32. package/dist/core/types.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/worker-mode.d.ts +12 -3
  35. package/dist/worker-mode.d.ts.map +1 -1
  36. package/dist/workers/port-reaper.d.ts +21 -6
  37. package/dist/workers/port-reaper.d.ts.map +1 -1
  38. package/docs/README.md +1 -0
  39. package/docs/architecture.md +18 -8
  40. package/docs/getting-started.md +3 -0
  41. package/docs/transports.md +145 -0
  42. package/docs/zh/README.md +1 -0
  43. package/docs/zh/architecture.md +18 -8
  44. package/docs/zh/getting-started.md +3 -0
  45. package/docs/zh/transports.md +133 -0
  46. package/package.json +29 -6
  47. package/dist/chunk-53INHVYO.js.map +0 -7
@@ -0,0 +1,145 @@
1
+ # Transport Backends
2
+
3
+ > [中文](./zh/transports.md) | English
4
+
5
+ The core package (`cross-tab-worker-databus`) is transport-agnostic. It defines a
6
+ `DataBusTransport` contract; the built-in Centrifuge backend is one implementation
7
+ of that contract, exposed as the optional `./centrifuge` subpath. This document
8
+ describes the contract and how to wire up a third-party backend (native WebSocket,
9
+ socket.io, SSE, etc.).
10
+
11
+ ## The `DataBusTransport` contract
12
+
13
+ Every backend implements five methods. `subscribe` / `unsubscribe` MUST be
14
+ idempotent — the DataBus may call them repeatedly and replays them on reconnect.
15
+
16
+ ```ts
17
+ interface DataBusTransport<TConfig = unknown, TData = unknown> {
18
+ start(config: TConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void>;
19
+ subscribe(topic: string): MaybePromise<void>;
20
+ unsubscribe(topic: string): MaybePromise<void>;
21
+ publish(topic: string, data: unknown): MaybePromise<void>;
22
+ stop(): MaybePromise<void>;
23
+ }
24
+
25
+ interface DataBusTransportHandlers<TData = unknown> {
26
+ onMessage: (message: DataBusMessage<TData>) => void;
27
+ onStatus: (status: WorkerStatus) => void; // 'connecting' | 'connected' | 'disconnected' | 'error'
28
+ onError: (error: unknown) => void;
29
+ }
30
+ ```
31
+
32
+ `start()` receives the user-supplied connection config (untyped `TConfig` — the
33
+ backend owns its shape) and the three callbacks. Call `onStatus` whenever the
34
+ connection state changes; call `onMessage` for each inbound publication; call
35
+ `onError` for non-fatal errors (the DataBus applies a recovery cooldown so a
36
+ flapping connection does not retry-loop).
37
+
38
+ ## Architectural layers
39
+
40
+ ```
41
+ CrossTabDataBus ──► DataBusTransport (your backend)
42
+
43
+ ┌───────┴────────┐
44
+ │ Worker protocol │ (your backend's main-thread ↔ worker messages)
45
+ └───────┬────────┘
46
+
47
+ Session layer (the actual client: WebSocket / centrifuge / …)
48
+
49
+ Server
50
+ ```
51
+
52
+ The DataBus layer handles cross-tab coordination (BroadcastChannel control
53
+ plane, localStorage routes, owner selection, failover, page lifecycle). Your
54
+ transport only owns the I/O path: connect, subscribe, publish, disconnect.
55
+
56
+ ## Implementing a backend
57
+
58
+ ### 1. Define your Worker protocol
59
+
60
+ Mirror the Centrifuge backend's `centrifuge-protocol.ts`: a discriminated union
61
+ of messages the main thread sends to the Worker (`INIT` / `SUBSCRIBE` /
62
+ `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` / `PING` / `STOP`) and a union the
63
+ Worker posts back (`STATUS` / `MESSAGE` / `MESSAGE_BIN` / `ERROR`). Keep it
64
+ structured-cloneable (no functions,
65
+ no class instances — `Error` must be serialised).
66
+
67
+ ### 2. Implement the session
68
+
69
+ A session class owns one connection and lives inside the Worker (or, as a
70
+ fallback, on the main thread). It receives protocol messages via a `handle()`
71
+ method and posts outputs back through a sink. See
72
+ [`centrifuge-session.ts`](../src/centrifuge-session.ts) for the reference shape:
73
+
74
+ - `handle(message)` dispatches by `message.type`.
75
+ - `subscribe(topic)` is idempotent — re-subscribing an existing topic is a no-op.
76
+ - `unsubscribe(topic)` removes listeners before disconnecting, to avoid a late
77
+ event resurrecting a re-subscribed topic.
78
+ - `stop()` disconnects, clears all subscriptions, and emits `disconnected`.
79
+
80
+ ### 3. Implement the transport
81
+
82
+ The transport selects a backend (SharedWorker / Dedicated Worker / local),
83
+ posts protocol messages to it, and routes Worker outputs back to the
84
+ `DataBusTransportHandlers`. See [`centrifuge.ts`](../src/centrifuge.ts) for the
85
+ reference shape, including:
86
+
87
+ - **Backend selection**: reuse `selectWorkerBackend` from `worker-mode.ts` so
88
+ your backend degrades consistently with the rest of the SDK.
89
+ - **Generation guard**: bump a monotonic counter when a backend is created;
90
+ error handlers check it so late errors from a superseded Worker cannot
91
+ corrupt the fresh session.
92
+ - **SharedWorker heartbeat**: if you use a SharedWorker, send periodic PINGs
93
+ so a `PortReaper` can reclaim dead-tab sessions.
94
+
95
+ ### 4. Expose as a subpath
96
+
97
+ Add `exports` entries in `package.json` (one per entry point — the main bundle,
98
+ the dedicated worker, the shared worker):
99
+
100
+ ```json
101
+ {
102
+ "exports": {
103
+ "./your-backend": {
104
+ "types": "./dist/your-backend.d.ts",
105
+ "import": "./dist/your-backend.js"
106
+ },
107
+ "./your-backend.worker": "./dist/your-backend.worker.js",
108
+ "./your-backend.shared.worker": "./dist/your-backend.shared.worker.js"
109
+ }
110
+ }
111
+ ```
112
+
113
+ This keeps the core package zero-dependency: users who do not import
114
+ `./your-backend` never pull your client library into their bundle.
115
+
116
+ ### 5. Register the peer dependency
117
+
118
+ Declare your client library as an optional peer dependency so consumers
119
+ opt in:
120
+
121
+ ```json
122
+ {
123
+ "peerDependencies": { "your-client-lib": "^x.y.z" },
124
+ "peerDependenciesMeta": { "your-client-lib": { "optional": true } }
125
+ }
126
+ ```
127
+
128
+ ## Factory entry point
129
+
130
+ Provide a `create<Backend>DataBus(options)` factory that wires the transport
131
+ into a `CrossTabDataBus`, mirroring `createCentrifugeDataBus`. This is the
132
+ surface most consumers use; it should accept the connection config, cluster
133
+ key (defaulting to the connection URL), and forward trace / worker-mode
134
+ options to the DataBus.
135
+
136
+ ## What the transport does NOT own
137
+
138
+ - **Cross-tab routing**: the `WorkerClusterRuntime` decides which tab owns a
139
+ topic. Your transport just subscribes when told.
140
+ - **Reconnect replay**: the DataBus replays the current owner's topics on
141
+ reconnect; your transport's `subscribe` must be safe to call again.
142
+ - **Publication fan-out**: the owner broadcasts publications over
143
+ BroadcastChannel; your transport only receives and reports them.
144
+ - **Page lifecycle**: the DataBus suspends/resumes the transport on
145
+ `pagehide` / `pageshow`; your transport's `stop()` must be clean.
package/docs/zh/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  |---|---|
9
9
  | [快速接入](./getting-started.md) | 安装、创建实例、订阅、发布和销毁 |
10
10
  | [配置说明](./configuration.md) | 核心配置、Centrifuge 配置、默认值和约束 |
11
+ | [Transport 后端](./transports.md) | `DataBusTransport` 契约、Worker 协议与第三方后端接入指南 |
11
12
  | [API 参考](./api.md) | 公共入口、类型、方法、返回值和行为 |
12
13
  | [架构说明](./architecture.md) | Worker 集群、路由、存储、迁移和降级设计 |
13
14
  | [能力矩阵](./capabilities.md) | 已实现、未实现和计划待实现的能力矩阵 |
@@ -10,23 +10,33 @@ graph TB
10
10
  subgraph TabA["Tab A"]
11
11
  AppA["业务模块"] --> BusA["CrossTabDataBus"]
12
12
  BusA --> RuntimeA["WorkerClusterRuntime"]
13
- BusA --> WorkerA["Dedicated / Shared Worker A"]
13
+ BusA --> TransportA["CentrifugeWorkerTransport"]
14
+ TransportA --> WorkerA["Dedicated / Shared Worker A"]
14
15
  end
15
16
  subgraph TabB["Tab B"]
16
17
  AppB["业务模块"] --> BusB["CrossTabDataBus"]
17
18
  BusB --> RuntimeB["WorkerClusterRuntime"]
18
- BusB --> WorkerB["Dedicated / Shared Worker B"]
19
+ BusB --> TransportB["CentrifugeWorkerTransport"]
20
+ TransportB --> WorkerB["Dedicated / Shared Worker B"]
19
21
  end
20
22
  end
21
23
 
22
24
  RuntimeA <--> Channel["BroadcastChannel 控制面"]
23
25
  RuntimeB <--> Channel
24
- RuntimeA <--> Registry["localStorage Worker 注册表"]
25
- RuntimeB <--> Registry
26
- RuntimeA <--> Routes["localStorage Topic 路由表"]
27
- RuntimeB <--> Routes
28
- WorkerA --> Server["Centrifuge / 实时服务器"]
29
- WorkerB --> Server
26
+ RuntimeA --> BatchA["BatchingStorageWriter"]
27
+ RuntimeB --> BatchB["BatchingStorageWriter"]
28
+ BatchA <--> Registry["localStorage Worker 注册表"]
29
+ BatchB <--> Registry
30
+ BatchA <--> Routes["localStorage Topic 路由表"]
31
+ BatchB <--> Routes
32
+ WorkerA --> SessionA["CentrifugeSession"]
33
+ WorkerB --> SessionB["CentrifugeSession"]
34
+ SessionA --> Server["Centrifuge / 实时服务器"]
35
+ SessionB --> Server
36
+ subgraph SW["SharedWorker 进程(backend = shared 时)"]
37
+ Reaper["PortReaper"] -.-> SessionA
38
+ Reaper -.-> SessionB
39
+ end
30
40
  ```
31
41
 
32
42
  默认 `workerMode: 'dedicated'` 时,每个 Tab 使用独立的 transport Worker。配置为 `shared` 或 `auto` 且浏览器支持 SharedWorker 时,同源 Tab 复用同一个 SharedWorker;SharedWorker 内每个连接 port 各自维护独立的 `CentrifugeSession`,一个 Tab 刷新或停止不会影响其他 Tab。`auto` 模式按 **SharedWorker → Dedicated Worker → 主线程 WebSocket** 降级,`dedicated` 模式按 **Dedicated Worker → SharedWorker → 主线程 WebSocket** 降级。`BroadcastChannel` 只负责控制消息和实时 publication 转发;localStorage 只负责最终一致的协调元数据。
@@ -12,8 +12,11 @@ pnpm add cross-tab-worker-databus
12
12
 
13
13
  - `cross-tab-worker-databus`:核心 DataBus 和 transport 接口
14
14
  - `cross-tab-worker-databus/centrifuge`:内置 Centrifuge Worker transport
15
+ - `cross-tab-worker-databus/centrifuge.worker`:Dedicated Worker 构建产物,默认由内置 factory 加载,通常无需直接引用
15
16
  - `cross-tab-worker-databus/centrifuge.shared.worker`:SharedWorker 构建产物,默认由内置 factory 加载,通常无需直接引用
16
17
 
18
+ `cross-tab-worker-databus/centrifuge` 入口依赖可选 peer dependency `centrifuge`(^5.5.3)。使用内置 Centrifuge transport 时请一并安装:`pnpm add centrifuge`。
19
+
17
20
  ## 2. 创建实例
18
21
 
19
22
  建议在应用基础设施层创建一个实例,其他模块直接导入。这样同一 Tab 内的业务模块会共享 Worker、连接和 Topic 引用。
@@ -0,0 +1,133 @@
1
+ # Transport 后端
2
+
3
+ > 中文 | [English](../transports.md)
4
+
5
+ 核心包(`cross-tab-worker-databus`)与 transport 无关。它定义了一个
6
+ `DataBusTransport` 契约;内置的 Centrifuge 后端是该契约的一个实现,作为可选的
7
+ `./centrifuge` subpath 暴露。本文档描述该契约以及如何接入第三方后端(原生
8
+ WebSocket、socket.io、SSE 等)。
9
+
10
+ ## `DataBusTransport` 契约
11
+
12
+ 每个后端实现 5 个方法。`subscribe` / `unsubscribe` 必须幂等——DataBus 可能重复
13
+ 调用,并在重连时回放。
14
+
15
+ ```ts
16
+ interface DataBusTransport<TConfig = unknown, TData = unknown> {
17
+ start(config: TConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void>;
18
+ subscribe(topic: string): MaybePromise<void>;
19
+ unsubscribe(topic: string): MaybePromise<void>;
20
+ publish(topic: string, data: unknown): MaybePromise<void>;
21
+ stop(): MaybePromise<void>;
22
+ }
23
+
24
+ interface DataBusTransportHandlers<TData = unknown> {
25
+ onMessage: (message: DataBusMessage<TData>) => void;
26
+ onStatus: (status: WorkerStatus) => void; // 'connecting' | 'connected' | 'disconnected' | 'error'
27
+ onError: (error: unknown) => void;
28
+ }
29
+ ```
30
+
31
+ `start()` 接收用户提供的连接配置(无类型 `TConfig`——后端自行定义其形状)和
32
+ 三个回调。连接状态变化时调 `onStatus`;收到 publication 时调 `onMessage`;
33
+ 非致命错误调 `onError`(DataBus 有恢复冷却窗口,避免抖动连接死循环重试)。
34
+
35
+ ## 架构分层
36
+
37
+ ```
38
+ CrossTabDataBus ──► DataBusTransport(你的后端)
39
+
40
+ ┌───────┴────────┐
41
+ │ Worker 协议 │ (你的后端的主线程 ↔ worker 消息)
42
+ └───────┬────────┘
43
+
44
+ Session 层 (真正的客户端:WebSocket / centrifuge / …)
45
+
46
+ 服务端
47
+ ```
48
+
49
+ DataBus 层负责跨 Tab 协调(BroadcastChannel 控制面、localStorage 路由、owner 选举、
50
+ 故障转移、页面生命周期)。你的 transport 只负责 I/O 路径:连接、订阅、发布、断开。
51
+
52
+ ## 实现一个后端
53
+
54
+ ### 1. 定义你的 Worker 协议
55
+
56
+ 参照 Centrifuge 后端的 `centrifuge-protocol.ts`:一个主线程发给 Worker 的
57
+ 判别联合(`INIT` / `SUBSCRIBE` / `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` /
58
+ `PING` / `STOP`)和一个 Worker 回传的联合(`STATUS` / `MESSAGE` / `MESSAGE_BIN` /
59
+ `ERROR`)。保持结构化克隆安全
60
+ (无函数、无类实例——`Error` 必须序列化)。
61
+
62
+ ### 2. 实现 session
63
+
64
+ 一个 session 类持有一个连接,运行在 Worker 内(或作为降级运行在主线程)。
65
+ 它通过 `handle()` 方法接收协议消息,通过 sink 回传输出。参见
66
+ [`centrifuge-session.ts`](../../src/centrifuge-session.ts) 的参考形状:
67
+
68
+ - `handle(message)` 按 `message.type` 分派。
69
+ - `subscribe(topic)` 幂等——对已存在 topic 重复订阅是 no-op。
70
+ - `unsubscribe(topic)` 先移除监听器再断开,避免迟到事件复活已重订阅的 topic。
71
+ - `stop()` 断开、清理所有订阅、emit `disconnected`。
72
+
73
+ ### 3. 实现 transport
74
+
75
+ transport 选择后端(SharedWorker / Dedicated Worker / 本地),向它发送协议
76
+ 消息,并把 Worker 输出路由回 `DataBusTransportHandlers`。参见
77
+ [`centrifuge.ts`](../../src/centrifuge.ts) 的参考形状,包括:
78
+
79
+ - **后端选举**:复用 `worker-mode.ts` 的 `selectWorkerBackend`,使你的后端与
80
+ SDK 其余部分降级行为一致。
81
+ - **generation 守卫**:创建后端时递增单调计数器;错误处理检查它,使被取代的
82
+ Worker 的迟到错误不会污染新 session。
83
+ - **SharedWorker 心跳**:若用 SharedWorker,定期发 PING,让 `PortReaper` 能
84
+ 回收死 tab 的 session。
85
+
86
+ ### 4. 作为 subpath 暴露
87
+
88
+ 在 `package.json` 加 `exports` 条目(每个入口一个——主 bundle、dedicated worker、
89
+ shared worker):
90
+
91
+ ```json
92
+ {
93
+ "exports": {
94
+ "./your-backend": {
95
+ "types": "./dist/your-backend.d.ts",
96
+ "import": "./dist/your-backend.js"
97
+ },
98
+ "./your-backend.worker": "./dist/your-backend.worker.js",
99
+ "./your-backend.shared.worker": "./dist/your-backend.shared.worker.js"
100
+ }
101
+ }
102
+ ```
103
+
104
+ 这保持核心包零依赖:不导入 `./your-backend` 的用户不会把你的客户端库打进 bundle。
105
+
106
+ ### 5. 声明 peer 依赖
107
+
108
+ 将你的客户端库声明为可选 peer 依赖,让消费者自行选择:
109
+
110
+ ```json
111
+ {
112
+ "peerDependencies": { "your-client-lib": "^x.y.z" },
113
+ "peerDependenciesMeta": { "your-client-lib": { "optional": true } }
114
+ }
115
+ ```
116
+
117
+ ## 工厂入口
118
+
119
+ 提供一个 `create<Backend>DataBus(options)` 工厂,把 transport 接入
120
+ `CrossTabDataBus`,与 `createCentrifugeDataBus` 对称。这是大多数消费者使用的
121
+ 界面;它应接受连接配置、cluster key(默认为连接 URL),并把 trace /
122
+ worker-mode 选项转发给 DataBus。
123
+
124
+ ## transport 不负责的事
125
+
126
+ - **跨 Tab 路由**:`WorkerClusterRuntime` 决定哪个 tab 拥有 topic。你的 transport
127
+ 只在被通知时订阅。
128
+ - **重连回放**:DataBus 在重连时回放当前 owner 的 topic;你的 transport 的
129
+ `subscribe` 必须可安全重复调用。
130
+ - **publication 扇出**:owner 通过 BroadcastChannel 广播 publication;你的
131
+ transport 只接收并上报。
132
+ - **页面生命周期**:DataBus 在 `pagehide` / `pageshow` 时挂起/恢复 transport;
133
+ 你的 transport 的 `stop()` 必须干净。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Framework-agnostic cross-tab data bus with Dedicated/Shared Worker clustering and Centrifuge support.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,31 +41,54 @@
41
41
  "types": "./dist/centrifuge.d.ts",
42
42
  "import": "./dist/centrifuge.js"
43
43
  },
44
- "./centrifuge.worker": "./dist/centrifuge.worker.js",
45
- "./centrifuge.shared.worker": "./dist/centrifuge.shared.worker.js",
44
+ "./centrifuge.worker": {
45
+ "types": "./dist/workers/centrifuge.worker.d.ts",
46
+ "default": "./dist/centrifuge.worker.js"
47
+ },
48
+ "./centrifuge.shared.worker": {
49
+ "types": "./dist/workers/centrifuge.shared.worker.d.ts",
50
+ "default": "./dist/centrifuge.shared.worker.js"
51
+ },
46
52
  "./package.json": "./package.json"
47
53
  },
54
+ "sideEffects": [
55
+ "./dist/centrifuge.worker.js",
56
+ "./dist/centrifuge.shared.worker.js"
57
+ ],
48
58
  "scripts": {
49
59
  "build": "node scripts/build.mjs",
50
60
  "check": "pnpm typecheck && pnpm test && pnpm build",
51
61
  "examples": "node scripts/serve-examples.mjs",
62
+ "lint": "eslint .",
52
63
  "test": "vitest run",
53
64
  "test:watch": "vitest",
65
+ "test:coverage": "vitest run --coverage",
54
66
  "test:e2e": "pnpm build && playwright test",
55
- "typecheck": "tsc --noEmit"
67
+ "typecheck": "tsc --noEmit",
68
+ "prepublishOnly": "pnpm check"
56
69
  },
57
- "dependencies": {
70
+ "peerDependencies": {
58
71
  "centrifuge": "^5.5.3"
59
72
  },
73
+ "peerDependenciesMeta": {
74
+ "centrifuge": {
75
+ "optional": true
76
+ }
77
+ },
60
78
  "devDependencies": {
79
+ "@eslint/js": "^9.39.5",
61
80
  "@playwright/test": "^1.62.1",
62
81
  "@types/node": "^24.0.0",
82
+ "@vitest/coverage-v8": "^3.2.7",
63
83
  "esbuild": "^0.25.0",
84
+ "eslint": "^9.39.4",
85
+ "globals": "^17.11.0",
64
86
  "typescript": "^5.9.0",
87
+ "typescript-eslint": "^8.68.0",
65
88
  "vitest": "^3.2.0"
66
89
  },
67
90
  "engines": {
68
91
  "node": ">=18.0.0"
69
92
  },
70
93
  "packageManager": "pnpm@10.14.0"
71
- }
94
+ }