cross-tab-worker-databus 0.1.1 → 0.2.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +11 -0
  3. package/README.zh.md +9 -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 +14 -4
  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 +76 -28
  11. package/dist/centrifuge.js.map +2 -2
  12. package/dist/centrifuge.shared.worker.js +84 -28
  13. package/dist/centrifuge.shared.worker.js.map +2 -2
  14. package/dist/centrifuge.worker.js +48 -14
  15. package/dist/centrifuge.worker.js.map +2 -2
  16. package/dist/{chunk-GABYBK7I.js → chunk-LBXREMZA.js} +287 -162
  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 +14 -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 +23 -6
  37. package/dist/workers/port-reaper.d.ts.map +1 -1
  38. package/docs/architecture.md +20 -8
  39. package/docs/configuration.md +2 -0
  40. package/docs/transports.md +144 -0
  41. package/docs/zh/architecture.md +20 -8
  42. package/docs/zh/configuration.md +2 -0
  43. package/docs/zh/transports.md +132 -0
  44. package/package.json +7 -2
  45. package/dist/chunk-GABYBK7I.js.map +0 -7
package/CHANGELOG.md CHANGED
@@ -4,6 +4,65 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] - 2026-08-27
8
+
9
+ ### Changed
10
+
11
+ - `centrifuge` 从 `dependencies` 改为 optional `peerDependencies`:仅在使用内置 Centrifuge 后端时需要安装,核心包零运行时依赖。
12
+ - `CentrifugeSession.handle()` 由连续 `if` 改为 `switch` + `default`,未知消息类型静默忽略,为未来协议扩展留出兼容余地。
13
+ - `routing.ts` 的 workerId 平局处理由 `localeCompare` 改为数值比较,消除宿主 locale 对路由确定性的影响。
14
+ - `port-reaper.ts` 提取 `computeMinHeartbeat()` 方法,reap 迭代改用 `Array.from` 快照,reaper 节奏计算与计时器逻辑解耦。
15
+ - `centrifuge-protocol.ts` 新增 `DEFAULT_SESSION_TIMEOUT_MS` 派生常量,消除 `port-reaper` 中重复的字面量计算。
16
+ - `CentrifugeWorkerTransport` 提取 `buildInitInput()` 与 `postToPortLike()` helper,消除 `start()`/`post()` 中的重复构造逻辑。
17
+ - `cluster.ts` 提取 `readAllByPrefix()` helper,统一 `readWorkers`/`cleanupOrphanedRoutes`/`cleanupOrphanedSubscribers`/`getSnapshot` 的 listKeys+readJson 循环;删除无调用点的 `listKeysSafe`。
18
+ - `cluster.ts` 提取 `buildLocalRoute()`,将 `readRoute` 的无 storage 降级分支独立命名并补文档;`readSubscriberTabIds` 同类分支拆为多行可读形式。
19
+ - `cluster.ts` 提取 `sendRouteReleased()` helper,消除三处 `ROUTE_RELEASED` 消息构造的重复;`handoffAssignedTopics` 的 generation 计算复用单次求值;`reconcileAssignedTopics` 去除同一 topicKey 的二次 `readRoute` 调用。
20
+ - `cluster.ts` `releaseSubscription` 返回 topicKey,`unsubscribe` 复用而非重新哈希;`handleMessage`/`handleControlMessage`/`sendControl` 连续 `if` 改 `switch`;`getSnapshot` 的序列化改用 `Array.from`。
21
+ - `data-bus.ts` `onControl` 连续 `if` 改 `switch`;提取 `invokeHandlers()` 统一 dispatch/status/error 三处 handler 遍历的 try-catch 隔离;提取 `formatWorkerTrace`/`formatRouteTrace` 格式化函数。
22
+ - `trace.ts` 提取 `metricsActive` getter,统一四个 record/flush 方法的守卫表达式。
23
+ - `cluster.ts` 提取 `routeOwnerIsLive()` 与 `isActiveAmong()`,消除 subscribe/publish 和 isActiveWorker/refreshRole 中的重复 route+workers.some / selectActiveWorkers+some 模式。
24
+ - `cluster.ts` 提取 `isStaleRouteRelease()`,命名 handleRouteReleasedMessage 的守卫条件。
25
+ - `cluster.ts` `activate()` 合并两个分支的 rememberTopic 调用为单循环;`handoffAssignedTopics` 去掉同一 topicKey 的二次 readRoute。
26
+ - `cluster.ts` `readSubscriberTabIds` 改用 `readAllByPrefix` + `Array.from`;`getSnapshot` 序列化改用 `Array.from` + mapping callback。
27
+ - `data-bus.ts` 提取 `formatWorkerTrace`/`formatRouteTrace` 格式化函数;`invokeHandlers` 的 label 参数改为联合类型字面量而非自由字符串。
28
+ - 多处 JSDoc/注释补全:writeRecord notify 参数、activate channel 判空、reconcileWorkers cleanup 顺序、rememberTopic eviction break、suspendTransport pendingStop 守卫、reopenTransport startPromise 检查、trace flushNow 条件、storage-batch flush break / scheduleFlush 降级 / scheduleRetry、port-reaper 各方法守卫、centrifuge assertHeartbeatInterval/assertStructuredCloneable/deserializeWorkerError、environment canUseStorage。
29
+
30
+ ### Documentation (JSDoc 全覆盖)
31
+
32
+ - `hash.ts` 提取 seed/prime/avalanche 常量与 `avalancheMix()` 函数,补非 ASCII 字符处理注释。
33
+ - `types.ts` 全部类型(WorkerRecord/WorkerRoute/WorkerClusterMessage/DataBusTransport/DataBusMessage/handler 别名)字段级 JSDoc 补全。
34
+ - `centrifuge-protocol.ts` 全部协议变体(WorkerUnsafeOption/CentrifugeWorkerConfig/CentrifugeWorkerInput 各变体/CentrifugeWorkerOutput 各变体/SerializedWorkerError)JSDoc 补全。
35
+ - `worker-mode.ts` WorkerMode/WorkerBackend/WorkerAvailability/selectWorkerBackend JSDoc 补全。
36
+ - `routing.ts` DEFAULT_MAX_ACTIVE_WORKERS/selectLeastLoadedWorker JSDoc 增强。
37
+ - `storage-batch.ts` INITIAL/MAX_RETRY 常量 + BatchingStorageWriter 类 + pendingSize getter JSDoc 补全。
38
+ - `trace.ts` normalizeInterval/roundMs JSDoc 补全。
39
+ - `environment.ts` ClusterEnvironment 各字段 + canUseStorage JSDoc 补全。
40
+ - `cluster.ts` readJson/writeJson/WorkerClusterOptions 各字段 JSDoc 补全。
41
+ - `data-bus.ts` PUBLICATION_EVENT/getStatus/getClusterSnapshot/CrossTabDataBusOptions JSDoc 补全。
42
+ - `centrifuge.ts` handleOutput/onWorkerFailed JSDoc 补全。
43
+
44
+ ### Documentation
45
+
46
+ - 架构运行时图(中英)补全 `BatchingStorageWriter`、`PortReaper`、`CentrifugeSession`、`CentrifugeWorkerTransport` 节点,与代码结构对齐。
47
+ - `AGENTS.md` 目录注释明确列出每个测试文件职责。
48
+ - `README`(中英)补充 `centrifuge` 为可选 peer 依赖的安装说明。
49
+ - `trace.ts`、`environment.ts` 补充 JSDoc:trace 模式与选项字段含义、SSR 场景的 storage/ BroadcastChannel 检测说明。
50
+
51
+ ## [0.1.2] - 2026-08-24
52
+
53
+ ### Fixed
54
+
55
+ - `BatchingStorageWriter` 在结构性写失败(如某 key 让底层 storage 持续抛错)时不再无限重试:每个 key 最多重试 5 次后丢弃并 `console.warn`,避免协调平面永久卡住。
56
+ - `PortReaper.setTimeout` 校验 `heartbeatIntervalMs` 为有限正数,`0`/`NaN`/负数回退默认值,防止 reaper 退化为忙循环或沉睡。
57
+ - `CentrifugeSession.unsubscribe` 先移除 subscription 监听器再退订,避免迟到的 `unsubscribed` 事件删除重订阅后的 subscription(幽灵订阅竞态)。
58
+ - `rememberTopic` FIFO 淘汰改为向前扫描首个非 own 条目,`knownTopics` 在有非 own 条目时真正不超 `MAX_KNOWN_TOPICS` 上限。
59
+ - `DataBusTraceReporter.pause()` 清零 `intervalStartedAt`,避免 stop 后手动 flush 算出异常大的 `durationMs`。
60
+
61
+ ### Documentation
62
+
63
+ - 架构文档(中英)补充 transport 层 client 级与 subscription 级 publication 去重说明。
64
+ - 配置文档(中英)补充 SharedWorker reaper 的失败隔离与关闭清理行为。
65
+
7
66
  ## [0.1.1] - 2026-08-24
8
67
 
9
68
  ### Fixed
package/README.md CHANGED
@@ -31,6 +31,17 @@ See the [Capabilities Matrix](./docs/capabilities.md) for the full list of imple
31
31
  pnpm add cross-tab-worker-databus
32
32
  ```
33
33
 
34
+ The core package has zero runtime dependencies. The Centrifuge transport
35
+ (`cross-tab-worker-databus/centrifuge`) declares `centrifuge` as an optional
36
+ peer dependency — install it only when you use the built-in Centrifuge backend:
37
+
38
+ ```bash
39
+ pnpm add cross-tab-worker-databus centrifuge
40
+ ```
41
+
42
+ Tabs that only use the local BroadcastChannel data bus (no WebSocket server)
43
+ do not need to install `centrifuge` at all.
44
+
34
45
  ## Getting Started
35
46
 
36
47
  ```ts
package/README.zh.md CHANGED
@@ -31,6 +31,15 @@
31
31
  pnpm add cross-tab-worker-databus
32
32
  ```
33
33
 
34
+ 核心包零运行时依赖。Centrifuge transport(`cross-tab-worker-databus/centrifuge`)
35
+ 将 `centrifuge` 声明为可选 peer 依赖——仅在使用内置 Centrifuge 后端时安装:
36
+
37
+ ```bash
38
+ pnpm add cross-tab-worker-databus centrifuge
39
+ ```
40
+
41
+ 仅使用本地 BroadcastChannel 数据总线(不连 WebSocket 服务)的 Tab 无需安装 `centrifuge`。
42
+
34
43
  ## 快速接入
35
44
 
36
45
  ```ts
@@ -19,58 +19,94 @@ export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 10000;
19
19
  * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of
20
20
  * several intervals tolerates throttled tabs without reaping healthy sessions. */
21
21
  export declare const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;
22
+ /** Default per-port session timeout in ms, derived from the heartbeat interval
23
+ * and the multiplier. Used as the fallback when a port sends no INIT config. */
24
+ export declare const DEFAULT_SESSION_TIMEOUT_MS: number;
22
25
  type WorkerUnsafeOption = 'eventsource' | 'fetch' | 'getData' | 'getToken' | 'networkEventTarget' | 'readableStream' | 'sockjs' | 'websocket';
23
- /** Centrifuge options safe to pass into a Worker; unsafe options are explicitly excluded. */
26
+ /** Centrifuge options safe to pass into a Worker; unsafe options are explicitly
27
+ * excluded and set to `never` so the compiler rejects them at the call site. */
24
28
  export type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {
25
29
  [Key in WorkerUnsafeOption]?: never;
26
30
  };
27
- /** Messages sent from the main thread to the Worker. */
28
- export type CentrifugeWorkerInput = {
31
+ /** Messages sent from the main thread to the Worker. All variants are
32
+ * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */
33
+ export type CentrifugeWorkerInput =
34
+ /** Initial connection: URL + config. Sent once per backend creation.
35
+ * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.
36
+ * `heartbeatIntervalMs` overrides the SharedWorker PING cadence. */
37
+ {
29
38
  type: 'INIT';
30
39
  url: string;
31
40
  config: CentrifugeWorkerConfig;
32
41
  transferable?: boolean;
33
42
  heartbeatIntervalMs?: number;
34
- } | {
43
+ }
44
+ /** Subscribe to a channel. Idempotent — re-subscribing is a no-op. */
45
+ | {
35
46
  type: 'SUBSCRIBE';
36
47
  topic: string;
37
- } | {
48
+ }
49
+ /** Unsubscribe from a channel. Idempotent. */
50
+ | {
38
51
  type: 'UNSUBSCRIBE';
39
52
  topic: string;
40
- } | {
53
+ }
54
+ /** Publish a structured-cloneable payload to a channel. */
55
+ | {
41
56
  type: 'PUBLISH';
42
57
  topic: string;
43
58
  data: unknown;
44
- } | {
59
+ }
60
+ /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */
61
+ | {
45
62
  type: 'PUBLISH_BIN';
46
63
  topic: string;
47
64
  data: ArrayBuffer;
48
- } | {
65
+ }
66
+ /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */
67
+ | {
49
68
  type: 'PING';
50
- } | {
69
+ }
70
+ /** Disconnect the client and clear all subscriptions. */
71
+ | {
51
72
  type: 'STOP';
52
73
  };
53
- /** Messages sent from the Worker back to the main thread. */
54
- export type CentrifugeWorkerOutput<TData = unknown> = {
74
+ /** Messages sent from the Worker back to the main thread. The main thread
75
+ * routes these to the DataBusTransportHandlers via handleOutput(). */
76
+ export type CentrifugeWorkerOutput<TData = unknown> =
77
+ /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */
78
+ {
55
79
  type: 'STATUS';
56
80
  status: WorkerStatus;
57
- } | {
81
+ }
82
+ /** A JSON publication arrived. Routed to onMessage via handleOutput. */
83
+ | {
58
84
  type: 'MESSAGE';
59
85
  topic: string;
60
86
  data: TData;
61
- } | {
87
+ }
88
+ /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */
89
+ | {
62
90
  type: 'MESSAGE_BIN';
63
91
  topic: string;
64
92
  data: ArrayBuffer;
65
- } | {
93
+ }
94
+ /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */
95
+ | {
66
96
  type: 'ERROR';
67
97
  error: SerializedWorkerError;
68
98
  };
69
- /** Error object serialized for cross-thread transfer (Error instances cannot be cloned via postMessage). */
99
+ /** Error object serialized for cross-thread transfer. Error instances cannot
100
+ * be structured-cloned via postMessage, so the Worker converts them to this
101
+ * shape and the main thread rebuilds an Error via deserializeWorkerError(). */
70
102
  export interface SerializedWorkerError {
103
+ /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */
71
104
  name: string;
105
+ /** The Error's `message`. */
72
106
  message: string;
107
+ /** The Error's `stack` if available (for debugging). */
73
108
  stack?: string;
109
+ /** Arbitrary context attached by the Worker (e.g. the failing operation). */
74
110
  context?: unknown;
75
111
  }
76
112
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"centrifuge-protocol.d.ts","sourceRoot":"","sources":["../src/centrifuge-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD;;kFAEkF;AAClF,eAAO,MAAM,kCAAkC,IAAI,CAAC;AAIpD,KAAK,kBAAkB,GACnB,aAAa,GACb,OAAO,GACP,SAAS,GACT,UAAU,GACV,oBAAoB,GACpB,gBAAgB,GAChB,QAAQ,GACR,WAAW,CAAC;AAEhB,6FAA6F;AAC7F,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,GAAG;KAC/E,GAAG,IAAI,kBAAkB,CAAC,CAAC,EAAE,KAAK;CACpC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE,GACnH;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,6DAA6D;AAC7D,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,OAAO,IAC9C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAEpD,4GAA4G;AAC5G,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
1
+ {"version":3,"file":"centrifuge-protocol.d.ts","sourceRoot":"","sources":["../src/centrifuge-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD;;kFAEkF;AAClF,eAAO,MAAM,kCAAkC,IAAI,CAAC;AACpD;gFACgF;AAChF,eAAO,MAAM,0BAA0B,QAC6B,CAAC;AAOrE,KAAK,kBAAkB,GACnB,aAAa,GACb,OAAO,GACP,SAAS,GACT,UAAU,GACV,oBAAoB,GACpB,gBAAgB,GAChB,QAAQ,GACR,WAAW,CAAC;AAEhB;gFACgF;AAChF,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,GAAG;KAC/E,GAAG,IAAI,kBAAkB,CAAC,CAAC,EAAE,KAAK;CACpC,CAAC;AAEF;gFACgF;AAChF,MAAM,MAAM,qBAAqB;AAC/B;;oEAEoE;AAClE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE;AACrH,sEAAsE;GACpE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACtC,8CAA8C;GAC5C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACxC,2DAA2D;GACzD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE;AACnD,qFAAqF;GACnF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE;AAC3D,2EAA2E;GACzE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,yDAAyD;GACvD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB;sEACsE;AACtE,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,OAAO;AAChD,mFAAmF;AACjF;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE;AAC1C,wEAAwE;GACtE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AACjD,6FAA6F;GAC3F;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE;AAC3D,gGAAgG;GAC9F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAEpD;;+EAE+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
@@ -13,17 +13,27 @@ export declare class CentrifugeSession<TData = unknown> {
13
13
  private readonly subscriptions;
14
14
  private transferable;
15
15
  constructor(sink: CentrifugeSessionSink<TData>);
16
- /** Dispatch an incoming Worker message to the matching operation. */
16
+ /** Dispatch an incoming Worker message to the matching operation.
17
+ * Unknown message types are ignored rather than thrown, so a future protocol
18
+ * extension adding a new variant cannot crash an older session. */
17
19
  handle(message: CentrifugeWorkerInput): void;
18
20
  /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */
19
21
  private initialize;
20
- /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists. */
22
+ /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.
23
+ * Listeners are only registered once per subscription object — a repeated
24
+ * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,
25
+ * avoiding the removeAllListeners + re-on churn on every duplicate message. */
21
26
  private subscribe;
22
- /** Unsubscribe from a Centrifuge channel and clean up the local reference. */
27
+ /** Unsubscribe from a Centrifuge channel and clean up the local reference.
28
+ * Listeners are removed before unsubscribing so a late `unsubscribed` event
29
+ * cannot delete a subscription that a subsequent `subscribe()` re-added. */
23
30
  private unsubscribe;
24
31
  /** Publish a message to the Centrifuge channel. */
25
32
  private publish;
26
- /** Forward a publication to the transport, using Transferable for binary data when enabled. */
33
+ /** Forward a publication to the transport. Binary payloads take the
34
+ * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
35
+ * else is structured-cloned via `MESSAGE`. An empty topic means the
36
+ * publication carried no channel info and is silently dropped. */
27
37
  private postPublication;
28
38
  /** Disconnect the client and clear all subscriptions. */
29
39
  private stop;
@@ -1 +1 @@
1
- {"version":3,"file":"centrifuge-session.d.ts","sourceRoot":"","sources":["../src/centrifuge-session.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAEV,qBAAqB,EACrB,sBAAsB,EAEvB,MAAM,uBAAuB,CAAC;AAE/B,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB,CAAC,KAAK,GAAG,OAAO;IACpD,IAAI,CAAC,OAAO,EAAE,sBAAsB,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;CAC9E;AAED;;;GAGG;AACH,qBAAa,iBAAiB,CAAC,KAAK,GAAG,OAAO;IAKhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,YAAY,CAAS;gBAEA,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC;IAE/D,qEAAqE;IACrE,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAS5C,8EAA8E;IAC9E,OAAO,CAAC,UAAU;IAuBlB,wFAAwF;IACxF,OAAO,CAAC,SAAS;IAmBjB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;IAOnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAKf,+FAA+F;IAC/F,OAAO,CAAC,eAAe;IASvB,yDAAyD;IACzD,OAAO,CAAC,IAAI;IAOZ,2DAA2D;IAC3D,OAAO,CAAC,IAAI;IAIZ;;;;0DAIsD;IACtD,OAAO,CAAC,SAAS;CAGlB"}
1
+ {"version":3,"file":"centrifuge-session.d.ts","sourceRoot":"","sources":["../src/centrifuge-session.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAEV,qBAAqB,EACrB,sBAAsB,EAEvB,MAAM,uBAAuB,CAAC;AAE/B,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB,CAAC,KAAK,GAAG,OAAO;IACpD,IAAI,CAAC,OAAO,EAAE,sBAAsB,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;CAC9E;AAED;;;GAGG;AACH,qBAAa,iBAAiB,CAAC,KAAK,GAAG,OAAO;IAKhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,YAAY,CAAS;gBAEA,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC;IAE/D;;uEAEmE;IACnE,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAyB5C,8EAA8E;IAC9E,OAAO,CAAC,UAAU;IAuBlB;;;mFAG+E;IAC/E,OAAO,CAAC,SAAS;IA2BjB;;gFAE4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAKf;;;sEAGkE;IAClE,OAAO,CAAC,eAAe;IASvB,yDAAyD;IACzD,OAAO,CAAC,IAAI;IAOZ,2DAA2D;IAC3D,OAAO,CAAC,IAAI;IAIZ;;;;0DAIsD;IACtD,OAAO,CAAC,SAAS;CAGlB"}
@@ -88,6 +88,10 @@ export declare class CentrifugeWorkerTransport<TData = unknown> implements DataB
88
88
  * terminate the Worker (or close the SharedWorker port).
89
89
  */
90
90
  stop(): void;
91
+ /** Build the INIT payload sent to the Worker / local session. Optional fields
92
+ * are only included when they deviate from the defaults, so the Worker's own
93
+ * default-resolution logic kicks in for the common case. */
94
+ private buildInitInput;
91
95
  /** Create and initialise a dedicated Worker, then send the INIT message. */
92
96
  private startDedicatedWorker;
93
97
  /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */
@@ -96,11 +100,15 @@ export declare class CentrifugeWorkerTransport<TData = unknown> implements DataB
96
100
  private readonly handleMessage;
97
101
  /** Handle a message from the in-process CentrifugeSession (local fallback). */
98
102
  private readonly handleSessionOutput;
99
- /** Route a Worker output message to the appropriate handler callback. */
103
+ /** Route a Worker output message to the appropriate handler callback.
104
+ * Shared by the Worker message listener, the SharedWorker port listener,
105
+ * and the local-session sink — all three feed into this single dispatcher. */
100
106
  private handleOutput;
101
107
  /** Handle a Worker-level failure (crash, message decode error). Discards the
102
108
  * dead backend so a later start()/reopen can rebuild from scratch, and
103
- * signals an error status so the DataBus can trigger recovery. */
109
+ * signals an error status so the DataBus can trigger recovery.
110
+ * Only invoked when the generation guard confirms the failing backend is
111
+ * still current — late errors from a superseded Worker are silently dropped. */
104
112
  private onWorkerFailed;
105
113
  /** Remove every listener attached to the current SharedWorker and its port. */
106
114
  private detachSharedWorkerListeners;
@@ -1 +1 @@
1
- {"version":3,"file":"centrifuge.d.ts","sourceRoot":"","sources":["../src/centrifuge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"centrifuge.d.ts","sourceRoot":"","sources":["../src/centrifuge.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CrossTabDataBus,
3
3
  selectWorkerBackend
4
- } from "./chunk-GABYBK7I.js";
4
+ } from "./chunk-LBXREMZA.js";
5
5
 
6
6
  // src/centrifuge-session.ts
7
7
  import { Centrifuge } from "centrifuge";
@@ -12,14 +12,30 @@ var CentrifugeSession = class {
12
12
  client = null;
13
13
  subscriptions = /* @__PURE__ */ new Map();
14
14
  transferable = false;
15
- /** Dispatch an incoming Worker message to the matching operation. */
15
+ /** Dispatch an incoming Worker message to the matching operation.
16
+ * Unknown message types are ignored rather than thrown, so a future protocol
17
+ * extension adding a new variant cannot crash an older session. */
16
18
  handle(message) {
17
- if (message.type === "INIT") this.initialize(message.url, message.config, message.transferable === true);
18
- if (message.type === "SUBSCRIBE") this.subscribe(message.topic);
19
- if (message.type === "UNSUBSCRIBE") this.unsubscribe(message.topic);
20
- if (message.type === "PUBLISH") this.publish(message.topic, message.data);
21
- if (message.type === "PUBLISH_BIN") this.publish(message.topic, message.data);
22
- if (message.type === "STOP") this.stop();
19
+ switch (message.type) {
20
+ case "INIT":
21
+ this.initialize(message.url, message.config, message.transferable === true);
22
+ return;
23
+ case "SUBSCRIBE":
24
+ this.subscribe(message.topic);
25
+ return;
26
+ case "UNSUBSCRIBE":
27
+ this.unsubscribe(message.topic);
28
+ return;
29
+ case "PUBLISH":
30
+ case "PUBLISH_BIN":
31
+ this.publish(message.topic, message.data);
32
+ return;
33
+ case "STOP":
34
+ this.stop();
35
+ return;
36
+ default:
37
+ return;
38
+ }
23
39
  }
24
40
  /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */
25
41
  initialize(url, config, transferable) {
@@ -40,9 +56,17 @@ var CentrifugeSession = class {
40
56
  });
41
57
  client.connect();
42
58
  }
43
- /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists. */
59
+ /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.
60
+ * Listeners are only registered once per subscription object — a repeated
61
+ * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,
62
+ * avoiding the removeAllListeners + re-on churn on every duplicate message. */
44
63
  subscribe(topic) {
45
64
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
65
+ const existing = this.subscriptions.get(topic);
66
+ if (existing) {
67
+ existing.subscribe();
68
+ return;
69
+ }
46
70
  let subscription = this.client.getSubscription(topic);
47
71
  if (!subscription) subscription = this.client.newSubscription(topic);
48
72
  subscription.removeAllListeners("publication");
@@ -56,19 +80,27 @@ var CentrifugeSession = class {
56
80
  subscription.on("unsubscribed", () => this.subscriptions.delete(topic));
57
81
  subscription.subscribe();
58
82
  }
59
- /** Unsubscribe from a Centrifuge channel and clean up the local reference. */
83
+ /** Unsubscribe from a Centrifuge channel and clean up the local reference.
84
+ * Listeners are removed before unsubscribing so a late `unsubscribed` event
85
+ * cannot delete a subscription that a subsequent `subscribe()` re-added. */
60
86
  unsubscribe(topic) {
61
87
  const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);
62
88
  if (!subscription) return;
63
- subscription.unsubscribe();
89
+ subscription.removeAllListeners("publication");
90
+ subscription.removeAllListeners("error");
91
+ subscription.removeAllListeners("unsubscribed");
64
92
  this.subscriptions.delete(topic);
93
+ subscription.unsubscribe();
65
94
  }
66
95
  /** Publish a message to the Centrifuge channel. */
67
96
  publish(topic, data) {
68
97
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
69
98
  void this.client.publish(topic, data).catch((error) => this.postError(error));
70
99
  }
71
- /** Forward a publication to the transport, using Transferable for binary data when enabled. */
100
+ /** Forward a publication to the transport. Binary payloads take the
101
+ * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
102
+ * else is structured-cloned via `MESSAGE`. An empty topic means the
103
+ * publication carried no channel info and is silently dropped. */
72
104
  postPublication(topic, data) {
73
105
  if (!topic) return;
74
106
  if (this.transferable && data instanceof ArrayBuffer) {
@@ -100,12 +132,14 @@ var CentrifugeSession = class {
100
132
  function getPayloadTopic(data) {
101
133
  if (!data || typeof data !== "object") return "";
102
134
  const payload = data;
103
- const topic = payload.push?.channel ?? payload.channel;
135
+ const push = payload.push;
136
+ const nested = typeof push === "object" && push !== null ? push.channel : void 0;
137
+ const topic = nested ?? payload.channel;
104
138
  return typeof topic === "string" ? topic : "";
105
139
  }
140
+ var LIVE_STATES = /* @__PURE__ */ new Set(["connecting", "connected"]);
106
141
  function normalizeStatus(status) {
107
- if (status === "connecting" || status === "connected") return status;
108
- return "disconnected";
142
+ return LIVE_STATES.has(status) ? status : "disconnected";
109
143
  }
110
144
  function serializeError(error) {
111
145
  if (error instanceof Error) {
@@ -124,6 +158,8 @@ function serializeError(error) {
124
158
 
125
159
  // src/centrifuge-protocol.ts
126
160
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
161
+ var DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;
162
+ var DEFAULT_SESSION_TIMEOUT_MS = DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;
127
163
 
128
164
  // src/centrifuge.ts
129
165
  var CentrifugeWorkerTransport = class {
@@ -165,13 +201,7 @@ var CentrifugeWorkerTransport = class {
165
201
  worker: this.workerFactory !== void 0,
166
202
  sharedWorker: this.sharedWorkerFactory !== void 0
167
203
  });
168
- const input = {
169
- type: "INIT",
170
- url: config.url,
171
- config: config.options ?? {},
172
- ...this.transferable ? { transferable: true } : {},
173
- ...this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS ? { heartbeatIntervalMs: this.heartbeatIntervalMs } : {}
174
- };
204
+ const input = this.buildInitInput(config);
175
205
  if (backend === "shared") {
176
206
  this.startSharedWorker(input);
177
207
  this.backend = "shared";
@@ -224,6 +254,18 @@ var CentrifugeWorkerTransport = class {
224
254
  this.resetBackend();
225
255
  this.handlers = null;
226
256
  }
257
+ /** Build the INIT payload sent to the Worker / local session. Optional fields
258
+ * are only included when they deviate from the defaults, so the Worker's own
259
+ * default-resolution logic kicks in for the common case. */
260
+ buildInitInput(config) {
261
+ return {
262
+ type: "INIT",
263
+ url: config.url,
264
+ config: config.options ?? {},
265
+ ...this.transferable ? { transferable: true } : {},
266
+ ...this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS ? { heartbeatIntervalMs: this.heartbeatIntervalMs } : {}
267
+ };
268
+ }
227
269
  /** Create and initialise a dedicated Worker, then send the INIT message. */
228
270
  startDedicatedWorker(input) {
229
271
  this.backendGeneration = ++this.generation;
@@ -255,7 +297,9 @@ var CentrifugeWorkerTransport = class {
255
297
  handleSessionOutput = (message) => {
256
298
  this.handleOutput(message);
257
299
  };
258
- /** Route a Worker output message to the appropriate handler callback. */
300
+ /** Route a Worker output message to the appropriate handler callback.
301
+ * Shared by the Worker message listener, the SharedWorker port listener,
302
+ * and the local-session sink — all three feed into this single dispatcher. */
259
303
  handleOutput(message) {
260
304
  if (message.type === "STATUS") this.handlers?.onStatus(message.status);
261
305
  if (message.type === "MESSAGE") this.handlers?.onMessage({ topic: message.topic, data: message.data });
@@ -264,7 +308,9 @@ var CentrifugeWorkerTransport = class {
264
308
  }
265
309
  /** Handle a Worker-level failure (crash, message decode error). Discards the
266
310
  * dead backend so a later start()/reopen can rebuild from scratch, and
267
- * signals an error status so the DataBus can trigger recovery. */
311
+ * signals an error status so the DataBus can trigger recovery.
312
+ * Only invoked when the generation guard confirms the failing backend is
313
+ * still current — late errors from a superseded Worker are silently dropped. */
268
314
  onWorkerFailed(message) {
269
315
  this.worker?.removeEventListener("message", this.handleMessage);
270
316
  this.worker?.removeEventListener("error", this.handleWorkerError);
@@ -320,13 +366,11 @@ var CentrifugeWorkerTransport = class {
320
366
  */
321
367
  post(message, transfer) {
322
368
  if (this.worker) {
323
- if (transfer) this.worker.postMessage(message, transfer);
324
- else this.worker.postMessage(message);
369
+ postToPortLike(this.worker, message, transfer);
325
370
  return;
326
371
  }
327
372
  if (this.port) {
328
- if (transfer) this.port.postMessage(message, transfer);
329
- else this.port.postMessage(message);
373
+ postToPortLike(this.port, message, transfer);
330
374
  return;
331
375
  }
332
376
  if (this.localSession) {
@@ -397,6 +441,10 @@ function assertStructuredCloneable(value) {
397
441
  );
398
442
  }
399
443
  }
444
+ function postToPortLike(target, message, transfer) {
445
+ if (transfer) target.postMessage(message, transfer);
446
+ else target.postMessage(message);
447
+ }
400
448
  function deserializeWorkerError(error) {
401
449
  const result = new Error(error.message);
402
450
  result.name = error.name;