cross-tab-worker-databus 0.8.0 → 0.11.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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -0
  3. package/README.zh.md +2 -0
  4. package/dist/centrifuge-protocol.d.ts +4 -0
  5. package/dist/centrifuge-protocol.d.ts.map +1 -1
  6. package/dist/centrifuge-session.d.ts.map +1 -1
  7. package/dist/centrifuge.js +29 -11
  8. package/dist/centrifuge.js.map +2 -2
  9. package/dist/centrifuge.shared.worker.js +37 -6
  10. package/dist/centrifuge.shared.worker.js.map +3 -3
  11. package/dist/centrifuge.worker.js +37 -6
  12. package/dist/centrifuge.worker.js.map +3 -3
  13. package/dist/{chunk-WWUY2FV2.js → chunk-NX76TAV3.js} +137 -26
  14. package/dist/chunk-NX76TAV3.js.map +7 -0
  15. package/dist/cjs/centrifuge.cjs +164 -35
  16. package/dist/cjs/centrifuge.cjs.map +4 -4
  17. package/dist/cjs/index.cjs +172 -38
  18. package/dist/cjs/index.cjs.map +3 -3
  19. package/dist/core/cluster.d.ts +3 -2
  20. package/dist/core/cluster.d.ts.map +1 -1
  21. package/dist/core/data-bus.d.ts +19 -0
  22. package/dist/core/data-bus.d.ts.map +1 -1
  23. package/dist/core/publication.d.ts +10 -0
  24. package/dist/core/publication.d.ts.map +1 -0
  25. package/dist/core/replay-persistence.d.ts +2 -0
  26. package/dist/core/replay-persistence.d.ts.map +1 -1
  27. package/dist/core/trace.d.ts +7 -0
  28. package/dist/core/trace.d.ts.map +1 -1
  29. package/dist/core/types.d.ts +19 -7
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +39 -14
  34. package/dist/index.js.map +2 -2
  35. package/dist/websocket.d.ts.map +1 -1
  36. package/docs/api.md +27 -6
  37. package/docs/architecture.md +7 -1
  38. package/docs/capabilities.md +2 -2
  39. package/docs/configuration.md +1 -1
  40. package/docs/roadmap.md +15 -1
  41. package/docs/transports.md +9 -5
  42. package/docs/zh/api.md +27 -6
  43. package/docs/zh/architecture.md +7 -1
  44. package/docs/zh/capabilities.md +2 -2
  45. package/docs/zh/configuration.md +1 -1
  46. package/docs/zh/transports.md +8 -4
  47. package/package.json +1 -1
  48. package/dist/chunk-WWUY2FV2.js.map +0 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/);变更记录格式参考 [Keep a Changelog](https://keepachangelog.com/)。
4
4
 
5
+ ## [0.11.0] - 2026-09-01
6
+
7
+ ### Added
8
+
9
+ - Automatic durable replay retention via `replay.retentionMs` when a persistence adapter supports `clearBefore`.
10
+ - Deduplication accepted/suppressed counters in periodic `message_metrics` trace snapshots.
11
+ - Service Worker runtime boundary documented as intentionally deferred pending stable browser lifetime semantics.
12
+
13
+ ### Compatibility
14
+
15
+ - Existing replay adapters remain valid; `retentionMs` is ignored when an adapter does not implement `clearBefore`.
16
+ - Existing trace consumers can continue reading the original metrics fields; dedup counters are additive.
17
+
18
+ ## [0.10.0] - 2026-09-01
19
+
20
+ ### Added
21
+
22
+ - Formal transport-neutral publication metadata types (`DataBusPublication`, `DataBusPublicationMetadata`, and `DataBusPublicationEnvelope`).
23
+ - Publication metadata (`messageId` and `timestamp`) now traverses cluster controls, Centrifuge Worker boundaries, and WebSocket publish frames.
24
+ - Demo WebSocket server emits the canonical nested publication envelope and preserves caller metadata; real-browser contract coverage added.
25
+
26
+ ### Compatibility
27
+
28
+ - Flat WebSocket publication frames remain accepted.
29
+ - Existing payloads without metadata retain their legacy shape.
30
+
31
+ ## [0.9.0] - 2026-09-01
32
+
33
+ ### Added
34
+
35
+ - Dedup runtime statistics (`getDedupStats`) and reset API (`resetDedup`).
36
+ - Replay retention APIs: `clearReplayTopic` and `clearReplayBefore`; IndexedDB supports optional time-based pruning.
37
+ - Forward-compatible WebSocket publication envelope parsing with timestamp and message ID metadata.
38
+
5
39
  ## [0.4.0] - 2026-08-30
6
40
 
7
41
  ### Added
package/README.md CHANGED
@@ -21,6 +21,8 @@ By default each tab holds its own Dedicated Worker; when configured with `worker
21
21
  - Existing Topic owners remain stable while alive; visibility changes do not move established subscriptions
22
22
  - New Topics are assigned to the least-loaded eligible Worker
23
23
  - Wildcard subscriptions: `chat.*` and `*` patterns match concrete topics at dispatch
24
+ - Transport-neutral publication metadata (`messageId`, `timestamp`) with canonical WebSocket/Centrifuge envelopes and legacy frame compatibility
25
+ - Optional durable replay retention (`replay.retentionMs`) and deduplication outcome metrics in trace snapshots
24
26
  - Built-in zero-dependency native WebSocket transport (`createWebSocketDataBus`) for plain-WebSocket servers
25
27
  - Optional React hooks adapter (`cross-tab-worker-databus/hooks`): StrictMode-safe bus lifecycle, auto-cleanup subscriptions
26
28
  - Optional Vue 3 composables adapter (`cross-tab-worker-databus/vue`): lifecycle-safe bus, subscription, and status composables
package/README.zh.md CHANGED
@@ -21,6 +21,8 @@
21
21
  - 已有 Topic 的 owner 存活时保持稳定,前后台切换不迁移已有订阅
22
22
  - 新 Topic 分配给负载最低的候选 Worker
23
23
  - 通配符订阅:`chat.*` 与 `*` pattern 在分发侧匹配具体 Topic
24
+ - 传输无关的 publication 元数据(`messageId`、`timestamp`),支持标准 WebSocket/Centrifuge envelope,并兼容旧帧格式
25
+ - 可选的 durable replay retention(`replay.retentionMs`),以及 trace snapshot 中的去重结果指标
24
26
  - 内置零依赖的原生 WebSocket 传输(`createWebSocketDataBus`),适配普通 WebSocket 服务器
25
27
  - 可选的 React hooks 适配层(`cross-tab-worker-databus/hooks`):StrictMode 安全的 bus 生命周期与自动清理订阅
26
28
  - 可选的 Vue 3 composables 适配层(`cross-tab-worker-databus/vue`):安全管理 bus 生命周期、订阅和状态
@@ -57,6 +57,7 @@ export type CentrifugeWorkerInput =
57
57
  topic: string;
58
58
  data: unknown;
59
59
  messageId?: string;
60
+ timestamp?: number;
60
61
  }
61
62
  /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */
62
63
  | {
@@ -64,6 +65,7 @@ export type CentrifugeWorkerInput =
64
65
  topic: string;
65
66
  data: ArrayBuffer;
66
67
  messageId?: string;
68
+ timestamp?: number;
67
69
  }
68
70
  /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */
69
71
  | {
@@ -87,6 +89,7 @@ export type CentrifugeWorkerOutput<TData = unknown> =
87
89
  topic: string;
88
90
  data: TData;
89
91
  messageId?: string;
92
+ timestamp?: number;
90
93
  }
91
94
  /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */
92
95
  | {
@@ -94,6 +97,7 @@ export type CentrifugeWorkerOutput<TData = unknown> =
94
97
  topic: string;
95
98
  data: ArrayBuffer;
96
99
  messageId?: string;
100
+ timestamp?: number;
97
101
  }
98
102
  /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */
99
103
  | {
@@ -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;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,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACvE,qFAAqF;GACnF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AAC/E,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,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AACrE,6FAA6F;GAC3F;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;AAC/E,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"}
1
+ {"version":3,"file":"centrifuge-protocol.d.ts","sourceRoot":"","sources":["../src/centrifuge-protocol.ts"],"names":[],"mappings":"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"}
@@ -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;;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;IASf;;;sEAGkE;IAClE,OAAO,CAAC,eAAe;IAevB,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":"AAeA,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;IAgBf;;;sEAGkE;IAClE,OAAO,CAAC,eAAe;IAiBvB,yDAAyD;IACzD,OAAO,CAAC,IAAI;IAOZ,2DAA2D;IAC3D,OAAO,CAAC,IAAI;IAIZ;;;;0DAIsD;IACtD,OAAO,CAAC,SAAS;CAGlB"}
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  CrossTabDataBus,
3
+ parseDataBusPublication,
3
4
  selectWorkerBackend
4
- } from "./chunk-WWUY2FV2.js";
5
+ } from "./chunk-NX76TAV3.js";
5
6
 
6
7
  // src/centrifuge-session.ts
7
8
  import { Centrifuge } from "centrifuge";
@@ -28,7 +29,7 @@ var CentrifugeSession = class {
28
29
  return;
29
30
  case "PUBLISH":
30
31
  case "PUBLISH_BIN":
31
- this.publish(message.topic, message.data, message.messageId);
32
+ this.publish(message.topic, message.data, message.messageId, message.timestamp);
32
33
  return;
33
34
  case "STOP":
34
35
  this.stop();
@@ -93,9 +94,14 @@ var CentrifugeSession = class {
93
94
  subscription.unsubscribe();
94
95
  }
95
96
  /** Publish a message to the Centrifuge channel. */
96
- publish(topic, data, messageId) {
97
+ publish(topic, data, messageId, timestamp) {
97
98
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
98
- const payload = messageId ? { data, messageId } : data;
99
+ const hasMetadata = messageId !== void 0 || timestamp !== void 0;
100
+ const payload = hasMetadata ? {
101
+ data,
102
+ ...messageId === void 0 ? {} : { messageId },
103
+ ...timestamp === void 0 ? {} : { timestamp }
104
+ } : data;
99
105
  void this.client.publish(topic, payload).catch((error) => this.postError(error));
100
106
  }
101
107
  /** Forward a publication to the transport. Binary payloads take the
@@ -108,9 +114,15 @@ var CentrifugeSession = class {
108
114
  this.post({ type: "MESSAGE_BIN", topic, data }, [data]);
109
115
  return;
110
116
  }
111
- const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : void 0;
112
- const payload = messageId && Object.prototype.hasOwnProperty.call(data, "data") ? data.data : data;
113
- this.post({ type: "MESSAGE", topic, data: payload, ...messageId ? { messageId } : {} });
117
+ const publication = parseDataBusPublication(data, topic);
118
+ if (!publication) return;
119
+ this.post({
120
+ type: "MESSAGE",
121
+ topic: publication.topic,
122
+ data: publication.data,
123
+ ...publication.messageId === void 0 ? {} : { messageId: publication.messageId },
124
+ ...publication.timestamp === void 0 ? {} : { timestamp: publication.timestamp }
125
+ });
114
126
  }
115
127
  /** Disconnect the client and clear all subscriptions. */
116
128
  stop() {
@@ -231,10 +243,10 @@ var CentrifugeWorkerTransport = class {
231
243
  */
232
244
  publish(topic, data, options) {
233
245
  if (this.transferable && data instanceof ArrayBuffer) {
234
- this.post({ type: "PUBLISH_BIN", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} }, [data]);
246
+ this.post({ type: "PUBLISH_BIN", topic, data, ...publicationMetadata(options) }, [data]);
235
247
  return;
236
248
  }
237
- this.post({ type: "PUBLISH", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} });
249
+ this.post({ type: "PUBLISH", topic, data, ...publicationMetadata(options) });
238
250
  }
239
251
  /**
240
252
  * Gracefully stop the transport: send STOP, clean up event listeners, and
@@ -305,8 +317,8 @@ var CentrifugeWorkerTransport = class {
305
317
  * and the local-session sink — all three feed into this single dispatcher. */
306
318
  handleOutput(message) {
307
319
  if (message.type === "STATUS") this.handlers?.onStatus(message.status);
308
- if (message.type === "MESSAGE") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...message.messageId ? { messageId: message.messageId } : {} });
309
- if (message.type === "MESSAGE_BIN") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...message.messageId ? { messageId: message.messageId } : {} });
320
+ if (message.type === "MESSAGE") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message) });
321
+ if (message.type === "MESSAGE_BIN") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message) });
310
322
  if (message.type === "ERROR") this.handlers?.onError(deserializeWorkerError(message.error));
311
323
  }
312
324
  /** Handle a Worker-level failure (crash, message decode error). Discards the
@@ -471,6 +483,12 @@ function deserializeWorkerError(error) {
471
483
  if (error.context !== void 0) Object.assign(result, { context: error.context });
472
484
  return result;
473
485
  }
486
+ function publicationMetadata(metadata) {
487
+ return {
488
+ ...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
489
+ ...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
490
+ };
491
+ }
474
492
  export {
475
493
  CentrifugeWorkerTransport,
476
494
  createCentrifugeDataBus
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/centrifuge-session.ts", "../src/centrifuge-protocol.ts", "../src/centrifuge.ts"],
4
- "sourcesContent": ["/**\n * CentrifugeSession \u2014 a reusable wrapper around a single Centrifuge client.\n *\n * Provides a structured-clone-safe message protocol (INIT/SUBSCRIBE/UNSUBSCRIBE/\n * PUBLISH/STOP) so the same session class can run inside a Dedicated Worker,\n * a SharedWorker port, or directly on the main thread as a local fallback.\n */\nimport { Centrifuge } from 'centrifuge';\nimport type {\n PublicationContext,\n StateContext,\n Subscription,\n SubscriptionErrorContext\n} from 'centrifuge';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\n\n/** Callback interface for posting messages back to the transport layer. */\nexport interface CentrifugeSessionSink<TData = unknown> {\n post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void;\n}\n\n/**\n * Stateful Centrifuge client wrapper shared by Dedicated Worker, SharedWorker\n * ports and the main-thread local fallback. Each session owns one connection.\n */\nexport class CentrifugeSession<TData = unknown> {\n private client: Centrifuge | null = null;\n private readonly subscriptions = new Map<string, Subscription>();\n private transferable = false;\n\n constructor(private readonly sink: CentrifugeSessionSink<TData>) {}\n\n /** Dispatch an incoming Worker message to the matching operation.\n * Unknown message types are ignored rather than thrown, so a future protocol\n * extension adding a new variant cannot crash an older session. */\n handle(message: CentrifugeWorkerInput): void {\n switch (message.type) {\n case 'INIT':\n this.initialize(message.url, message.config, message.transferable === true);\n return;\n case 'SUBSCRIBE':\n this.subscribe(message.topic);\n return;\n case 'UNSUBSCRIBE':\n this.unsubscribe(message.topic);\n return;\n case 'PUBLISH':\n case 'PUBLISH_BIN':\n // Binary and JSON publish share the same Centrifuge client call; the\n // transport layer decides whether to transfer the ArrayBuffer.\n this.publish(message.topic, message.data, message.messageId);\n return;\n case 'STOP':\n this.stop();\n return;\n default:\n return;\n }\n }\n\n /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */\n private initialize(url: string, config: CentrifugeWorkerConfig, transferable: boolean): void {\n if (this.client) return;\n this.transferable = transferable;\n const client = new Centrifuge(url, config);\n this.client = client;\n client.on('state', (context: StateContext) => {\n this.post({ type: 'STATUS', status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => this.post({ type: 'STATUS', status: 'connected' }));\n client.on('disconnected', () => this.post({ type: 'STATUS', status: 'disconnected' }));\n client.on('error', context => this.postError(context));\n // Client-level publications are only for server-side subscriptions (where\n // no client Subscription object exists). For topics we have an active\n // subscription for, the subscription-level 'publication' listener handles\n // dispatch \u2014 skip here to avoid delivering the same message twice.\n client.on('publication', (context: PublicationContext) => {\n const topic = context.channel || getPayloadTopic(context.data);\n if (!topic || this.subscriptions.has(topic)) return;\n this.postPublication(topic, context.data);\n });\n client.connect();\n }\n\n /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.\n * Listeners are only registered once per subscription object \u2014 a repeated\n * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,\n * avoiding the removeAllListeners + re-on churn on every duplicate message. */\n private subscribe(topic: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n // If we already track this subscription, it already has our listeners \u2014\n // a duplicate SUBSCRIBE is a no-op (idempotent), matching the transport\n // contract. Only a fresh subscription needs listener wiring.\n const existing = this.subscriptions.get(topic);\n if (existing) {\n existing.subscribe();\n return;\n }\n let subscription = this.client.getSubscription(topic);\n if (!subscription) subscription = this.client.newSubscription(topic);\n // Remove only our own listeners so that any Centrifuge internal listeners\n // on the subscription object are preserved. Each subscription event carries\n // a single listener so removeAllListeners(\u2026) is safe here.\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.set(topic, subscription);\n subscription.on('publication', context => {\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => this.postError(context));\n subscription.on('unsubscribed', () => this.subscriptions.delete(topic));\n subscription.subscribe();\n }\n\n /** Unsubscribe from a Centrifuge channel and clean up the local reference.\n * Listeners are removed before unsubscribing so a late `unsubscribed` event\n * cannot delete a subscription that a subsequent `subscribe()` re-added. */\n private unsubscribe(topic: string): void {\n const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);\n if (!subscription) return;\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.delete(topic);\n subscription.unsubscribe();\n }\n\n /** Publish a message to the Centrifuge channel. */\n private publish(topic: string, data: unknown, messageId?: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n // Centrifuge's payload is application-defined. Preserve legacy payloads;\n // when an ID is requested, send a small metadata envelope that compatible\n // servers can echo back for end-to-end deduplication.\n const payload = messageId ? { data, messageId } : data;\n void this.client.publish(topic, payload).catch(error => this.postError(error));\n }\n\n /** Forward a publication to the transport. Binary payloads take the\n * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything\n * else is structured-cloned via `MESSAGE`. An empty topic means the\n * publication carried no channel info and is silently dropped. */\n private postPublication(topic: string, data: unknown): void {\n if (!topic) return;\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'MESSAGE_BIN', topic, data }, [data]);\n return;\n }\n const messageId = data && typeof data === 'object' && typeof (data as Record<string, unknown>).messageId === 'string'\n ? (data as Record<string, unknown>).messageId as string\n : undefined;\n const payload = messageId && Object.prototype.hasOwnProperty.call(data, 'data')\n ? (data as Record<string, unknown>).data\n : data;\n this.post({ type: 'MESSAGE', topic, data: payload as TData, ...(messageId ? { messageId } : {}) });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: 'STATUS', status: 'disconnected' });\n }\n\n /** Forward a message to the sink (the transport layer). */\n private post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void {\n this.sink.post(message, transfer);\n }\n\n /** Serialise and report an error. The Centrifuge client handles reconnection\n * internally, so a transient error should not trigger a `STATUS: error` that\n * would cause `selectActiveWorkers()` to exclude this worker from routing.\n * Fatal errors are distinguished by the client eventually emitting\n * `disconnected` without a subsequent `connected`. */\n private postError(error: unknown): void {\n this.post({ type: 'ERROR', error: serializeError(error) });\n }\n}\n\n/** Extract a topic from a Centrifuge publication payload if one is present.\n * Handles both direct `channel` fields and the nested `push.channel` shape\n * that Centrifugo uses for some server-side push types. */\nfunction getPayloadTopic(data: unknown): string {\n if (!data || typeof data !== 'object') return '';\n const payload = data as Record<string, unknown>;\n // Prefer nested push.channel (server-side push) then fall back to top-level channel.\n const push = payload.push;\n const nested = typeof push === 'object' && push !== null ? (push as Record<string, unknown>).channel : undefined;\n const topic = nested ?? payload.channel;\n return typeof topic === 'string' ? topic : '';\n}\n\n/** Map a Centrifuge state string to the DataBus's status vocabulary.\n * 'connecting' and 'connected' pass through; anything else (e.g. 'reconnecting',\n * 'disconnected') maps to 'disconnected'. */\nconst LIVE_STATES = new Set(['connecting', 'connected']);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\n}\n\n/** Convert an arbitrary error into a structured-cloneable form for postMessage. */\nfunction serializeError(error: unknown): SerializedWorkerError {\n if (error instanceof Error) {\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {})\n };\n }\n return {\n name: 'CentrifugeError',\n message: typeof error === 'string' ? error : 'Centrifuge worker operation failed.',\n ...(error === undefined ? {} : { context: error })\n };\n}\n", "/**\n * Worker-thread protocol for Centrifuge WebSocket transport.\n *\n * Defines the message types exchanged between the main thread and a Web Worker\n * (dedicated or shared) that runs a centrifuge client. The worker is isolated\n * from the main thread so that WebSocket lifecycle, token refresh, and binary\n * data handling never block the UI.\n */\nimport type { Options } from 'centrifuge';\nimport type { WorkerStatus } from './core/types';\n\n/**\n * Default interval between main-thread PING heartbeats to a SharedWorker. The\n * SharedWorker reaps a silent port after `SESSION_TIMEOUT_MULTIPLIER` intervals.\n * Shared here so the main thread (heartbeat sender) and the SharedWorker (reaper)\n * always agree on the cadence.\n */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;\n/** SharedWorker per-port session timeout, expressed as a multiple of that port's\n * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of\n * several intervals tolerates throttled tabs without reaping healthy sessions. */\nexport const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;\n/** Default per-port session timeout in ms, derived from the heartbeat interval\n * and the multiplier. Used as the fallback when a port sends no INIT config. */\nexport const DEFAULT_SESSION_TIMEOUT_MS =\n DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;\n\n// Centrifuge Options that reference browser APIs (WebSocket, EventSource, etc.)\n// are unavailable inside a Worker \u2014 the worker uses its own WebSocket import.\n// These are stripped from the config sent to the worker so the type system\n// prevents accidentally passing a main-thread-only function (which would fail\n// structured cloning and throw a DataCloneError).\ntype WorkerUnsafeOption =\n | 'eventsource'\n | 'fetch'\n | 'getData'\n | 'getToken'\n | 'networkEventTarget'\n | 'readableStream'\n | 'sockjs'\n | 'websocket';\n\n/** Centrifuge options safe to pass into a Worker; unsafe options are explicitly\n * excluded and set to `never` so the compiler rejects them at the call site. */\nexport type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {\n [Key in WorkerUnsafeOption]?: never;\n};\n\n/** Messages sent from the main thread to the Worker. All variants are\n * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */\nexport type CentrifugeWorkerInput =\n /** Initial connection: URL + config. Sent once per backend creation.\n * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.\n * `heartbeatIntervalMs` overrides the SharedWorker PING cadence. */\n | { type: 'INIT'; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: 'SUBSCRIBE'; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: 'UNSUBSCRIBE'; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: 'PUBLISH'; topic: string; data: unknown; messageId?: string }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: 'PUBLISH_BIN'; topic: string; data: ArrayBuffer; messageId?: string }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: 'PING' }\n /** Disconnect the client and clear all subscriptions. */\n | { type: 'STOP' };\n\n/** Messages sent from the Worker back to the main thread. The main thread\n * routes these to the DataBusTransportHandlers via handleOutput(). */\nexport type CentrifugeWorkerOutput<TData = unknown> =\n /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */\n | { type: 'STATUS'; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: 'MESSAGE'; topic: string; data: TData; messageId?: string }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: 'MESSAGE_BIN'; topic: string; data: ArrayBuffer; messageId?: string }\n /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */\n | { type: 'ERROR'; error: SerializedWorkerError };\n\n/** Error object serialized for cross-thread transfer. Error instances cannot\n * be structured-cloned via postMessage, so the Worker converts them to this\n * shape and the main thread rebuilds an Error via deserializeWorkerError(). */\nexport interface SerializedWorkerError {\n /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */\n name: string;\n /** The Error's `message`. */\n message: string;\n /** The Error's `stack` if available (for debugging). */\n stack?: string;\n /** Arbitrary context attached by the Worker (e.g. the failing operation). */\n context?: unknown;\n}\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends, selected in order of preference:\n * 1. SharedWorker \u2014 one WebSocket per tab session, hosted in a shared process\n * 2. Dedicated Worker \u2014 one WebSocket per tab\n * 3. In-process (local) \u2014 Centrifuge runs on the main thread (fallback when\n * neither Worker type is available, e.g. in non-browser environments)\n *\n * Binary data (ArrayBuffer) can be transferred via Transferable when the\n * `transferable` option is enabled, avoiding structured-clone overhead.\n */\n\nimport { CrossTabDataBus } from './core/data-bus';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type { DataBusTransport, DataBusTransportHandlers } from './core/types';\nimport { CentrifugeSession } from './centrifuge-session';\nimport { selectWorkerBackend } from './worker-mode';\nimport type { WorkerBackend, WorkerMode } from './worker-mode';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\n\nexport type { CentrifugeWorkerConfig, SerializedWorkerError } from './centrifuge-protocol';\nexport type { WorkerBackend, WorkerMode } from './worker-mode';\n\n/** WebSocket connection parameters passed to the Centrifuge Worker. */\nexport interface CentrifugeDataBusConfig {\n /** Centrifuge server WebSocket URL. */\n url: string;\n /** Centrifuge client options (token, channel params, etc.). */\n options?: CentrifugeWorkerConfig;\n}\n\n/** Options for configuring the Worker backend (dedicated, shared, or local). */\nexport interface CentrifugeWorkerTransportOptions {\n /** Custom dedicated Worker factory. Used for testing or bundler integration. */\n workerFactory?: () => Worker;\n /** Custom SharedWorker factory. */\n sharedWorkerFactory?: () => SharedWorker;\n /** Preferred Worker mode: 'dedicated', 'shared', or 'auto'. */\n workerMode?: WorkerMode;\n /** Enable transferable (ArrayBuffer) support for binary data. */\n transferable?: boolean;\n /** Interval (ms) between PING heartbeats sent to the SharedWorker. The\n * SharedWorker reaps a silent port after `DEFAULT_SESSION_TIMEOUT_MULTIPLIER`\n * \u00D7 this interval. Pass `Infinity` to disable heartbeats entirely. Defaults\n * to `DEFAULT_HEARTBEAT_INTERVAL_MS`. */\n heartbeatIntervalMs?: number;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a Centrifuge transport. */\nexport interface CreateCentrifugeDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<CentrifugeDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n >,\n CentrifugeWorkerTransportOptions {\n /** Centrifuge connection configuration. */\n connection: CentrifugeDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/**\n * Transport layer that runs a Centrifuge WebSocket client inside a Web Worker.\n *\n * Delegates the actual WebSocket connection to a Worker (dedicated or shared)\n * or falls back to an in-process CentrifugeSession. The Worker is isolated from\n * the main thread so that WebSocket lifecycle, token refresh, and binary data\n * handling never block the UI.\n */\nexport class CentrifugeWorkerTransport<TData = unknown>\n implements DataBusTransport<CentrifugeDataBusConfig, TData>\n{\n private readonly workerMode: WorkerMode;\n private readonly transferable: boolean;\n private readonly heartbeatIntervalMs: number;\n private readonly workerFactory: (() => Worker) | undefined;\n private readonly sharedWorkerFactory: (() => SharedWorker) | undefined;\n private backend: WorkerBackend | null = null;\n private worker: Worker | null = null;\n private sharedWorker: SharedWorker | null = null;\n private port: MessagePort | null = null;\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n private localSession: CentrifugeSession<TData> | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n // Monotonically increasing counter, bumped each time a backend is created.\n // Used to ignore late error events from a superseded Worker.\n private generation = 0;\n // Generation captured when the current backend was created. Error handlers\n // only act when the backend that registered them is still current.\n private backendGeneration = 0;\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? 'dedicated';\n this.transferable = options.transferable ?? false;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n assertHeartbeatInterval(this.heartbeatIntervalMs);\n this.workerFactory = options.workerFactory;\n this.sharedWorkerFactory = options.sharedWorkerFactory;\n }\n\n /**\n * Start the transport: select a backend, initialise the Worker (or local\n * session), and send the INIT message with connection parameters.\n */\n start(config: CentrifugeDataBusConfig, handlers: DataBusTransportHandlers<TData>): void {\n if (this.backend) return;\n assertStructuredCloneable(config.options ?? {});\n this.handlers = handlers;\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined,\n sharedWorker: this.sharedWorkerFactory !== undefined\n });\n const input = this.buildInitInput(config);\n if (backend === 'shared') {\n this.startSharedWorker(input);\n this.backend = 'shared';\n return;\n }\n if (backend === 'dedicated') {\n this.startDedicatedWorker(input);\n this.backend = 'dedicated';\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = 'local';\n }\n\n subscribe(topic: string): void {\n this.post({ type: 'SUBSCRIBE', topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: 'UNSUBSCRIBE', topic });\n }\n\n /**\n * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable\n * when `transferable` is enabled, avoiding a structured-clone cycle.\n */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): void {\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'PUBLISH_BIN', topic, data, ...(options?.messageId ? { messageId: options.messageId } : {}) }, [data]);\n return;\n }\n this.post({ type: 'PUBLISH', topic, data, ...(options?.messageId ? { messageId: options.messageId } : {}) });\n }\n\n /**\n * Gracefully stop the transport: send STOP, clean up event listeners, and\n * terminate the Worker (or close the SharedWorker port).\n */\n stop(): void {\n if (!this.backend) return;\n this.generation++;\n this.post({ type: 'STOP' });\n this.clearHeartbeat();\n if (this.worker) {\n this.worker.removeEventListener('message', this.handleMessage);\n this.worker.removeEventListener('error', this.handleWorkerError);\n this.worker.terminate();\n }\n if (this.sharedWorker) {\n this.detachSharedWorkerListeners();\n this.port?.close();\n }\n this.resetBackend();\n this.handlers = null;\n }\n\n /** Build the INIT payload sent to the Worker / local session. Optional fields\n * are only included when they deviate from the defaults, so the Worker's own\n * default-resolution logic kicks in for the common case. */\n private buildInitInput(config: CentrifugeDataBusConfig): CentrifugeWorkerInput {\n return {\n type: 'INIT',\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS\n ? { heartbeatIntervalMs: this.heartbeatIntervalMs }\n : {})\n };\n }\n\n /** Create and initialise a dedicated Worker, then send the INIT message. */\n private startDedicatedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const worker = (this.workerFactory ?? createDefaultWorker)();\n this.worker = worker;\n worker.addEventListener('message', this.handleMessage);\n worker.addEventListener('error', this.handleWorkerError);\n worker.postMessage(input);\n }\n\n /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */\n private startSharedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const shared = (this.sharedWorkerFactory ?? createDefaultSharedWorker)();\n this.sharedWorker = shared;\n const port = shared.port;\n this.port = port;\n port.addEventListener('message', this.handleMessage);\n port.addEventListener('messageerror', this.handlePortError);\n shared.addEventListener('error', this.handleSharedWorkerError);\n port.start();\n port.postMessage(input);\n this.startHeartbeat();\n }\n\n /** Handle a message event from the Worker (dedicated or shared). */\n private readonly handleMessage = (event: MessageEvent<CentrifugeWorkerOutput<TData>>) => {\n this.handleOutput(event.data);\n };\n\n /** Handle a message from the in-process CentrifugeSession (local fallback). */\n private readonly handleSessionOutput = (message: CentrifugeWorkerOutput<TData>) => {\n this.handleOutput(message);\n };\n\n /** Route a Worker output message to the appropriate handler callback.\n * Shared by the Worker message listener, the SharedWorker port listener,\n * and the local-session sink \u2014 all three feed into this single dispatcher. */\n private handleOutput(message: CentrifugeWorkerOutput<TData>): void {\n if (message.type === 'STATUS') this.handlers?.onStatus(message.status);\n if (message.type === 'MESSAGE') this.handlers?.onMessage({ topic: message.topic, data: message.data, ...(message.messageId ? { messageId: message.messageId } : {}) });\n if (message.type === 'MESSAGE_BIN') this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...(message.messageId ? { messageId: message.messageId } : {}) });\n if (message.type === 'ERROR') this.handlers?.onError(deserializeWorkerError(message.error));\n }\n\n /** Handle a Worker-level failure (crash, message decode error). Discards the\n * dead backend so a later start()/reopen can rebuild from scratch, and\n * signals an error status so the DataBus can trigger recovery.\n * Only invoked when the generation guard confirms the failing backend is\n * still current \u2014 late errors from a superseded Worker are silently dropped. */\n private onWorkerFailed(message: string): void {\n // Remove the message listener and release the old backend before recovery\n // reopens, so late messages from the failed Worker/port cannot be routed\n // into the freshly started session.\n this.worker?.removeEventListener('message', this.handleMessage);\n this.worker?.removeEventListener('error', this.handleWorkerError);\n this.worker?.terminate();\n this.detachSharedWorkerListeners();\n this.port?.close();\n this.clearHeartbeat();\n this.resetBackend();\n this.handlers?.onError(new Error(message));\n this.handlers?.onStatus('error');\n }\n\n /** Remove every listener attached to the current SharedWorker and its port. */\n private detachSharedWorkerListeners(): void {\n this.port?.removeEventListener('message', this.handleMessage);\n this.port?.removeEventListener('messageerror', this.handlePortError);\n this.sharedWorker?.removeEventListener('error', this.handleSharedWorkerError);\n }\n\n /** Periodically ping the SharedWorker so its session reaper can detect a dead tab. */\n private startHeartbeat(): void {\n if (this.heartbeatHandle !== null) return;\n // Infinite disables the heartbeat (e.g. for environment where the\n // SharedWorker reaper is not needed).\n if (this.heartbeatIntervalMs === Infinity) return;\n this.heartbeatHandle = setInterval(() => {\n this.post({ type: 'PING' });\n }, this.heartbeatIntervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatHandle !== null) clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n\n private readonly handleWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker failed.');\n };\n\n /** Clear the Worker/port/backend references after a failure or stop. */\n private resetBackend(): void {\n this.worker = null;\n this.sharedWorker = null;\n this.port = null;\n this.backend = null;\n this.localSession = null;\n }\n\n /**\n * Post a message to the active backend. Accepts optional Transferable buffers\n * for zero-copy ArrayBuffer transfer.\n */\n private post(message: CentrifugeWorkerInput, transfer?: ArrayBuffer[]): void {\n if (this.worker) {\n postToPortLike(this.worker, message, transfer);\n return;\n }\n if (this.port) {\n postToPortLike(this.port, message, transfer);\n return;\n }\n if (this.localSession) {\n this.localSession.handle(message);\n return;\n }\n throw new Error('CentrifugeWorkerTransport.start() must be called first.');\n }\n}\n\n/**\n * Create a fully-configured CrossTabDataBus with a Centrifuge WebSocket transport.\n *\n * This is the primary entry point for consumers. It wires up the transport,\n * cluster coordination, and lifecycle management:\n *\n * ```ts\n * const bus = createCentrifugeDataBus({\n * connection: { url: 'wss://example.com/connection/websocket', options: { token: '\u2026' } },\n * trace: { enabled: true, sink: event => console.log(event) },\n * });\n * ```\n */\nexport function createCentrifugeDataBus<TData = unknown>(\n options: CreateCentrifugeDataBusOptions<TData>\n): CrossTabDataBus<CentrifugeDataBusConfig, TData> {\n const {\n clusterKey,\n connection,\n heartbeatIntervalMs,\n sharedWorkerFactory,\n transferable,\n workerFactory,\n workerMode,\n ...dataBusOptions\n } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new CentrifugeWorkerTransport<TData>({\n ...(workerFactory ? { workerFactory } : {}),\n ...(sharedWorkerFactory ? { sharedWorkerFactory } : {}),\n ...(transferable === undefined ? {} : { transferable }),\n ...(workerMode ? { workerMode } : {}),\n ...(heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs })\n })\n });\n}\n\n\n/** Create the default dedicated Worker hosting the Centrifuge client. */\nfunction createDefaultWorker(): Worker {\n if (typeof Worker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser Worker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly.'\n );\n }\n return new Worker(workerUrl, {\n name: 'cross-tab-worker-databus',\n type: 'module'\n });\n}\n\n/** Create the default SharedWorker. Each connecting port within the SharedWorker\n * creates its own CentrifugeSession with an independent WebSocket connection,\n * so refreshing or stopping one tab does not affect the others. */\nfunction createDefaultSharedWorker(): SharedWorker {\n if (typeof SharedWorker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser SharedWorker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.shared.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly.'\n );\n }\n return new SharedWorker(workerUrl, {\n name: 'cross-tab-worker-databus-shared',\n type: 'module'\n });\n}\n\n/** Validate the SharedWorker PING heartbeat interval. A value of `0`, a negative\n * number, or `NaN` would otherwise make `setInterval` degenerate into a 0ms busy\n * loop, driving the reaper and the main-thread PING out of control. `Infinity`\n * is allowed and disables heartbeats entirely (for environments where the\n * SharedWorker reaper is not needed, e.g. a single-tab deployment).\n * @throws {TypeError} when `value` is not a positive finite number or Infinity. */\nfunction assertHeartbeatInterval(value: number): void {\n if (value === Infinity) return;\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) return;\n throw new TypeError(\n `Centrifuge heartbeatIntervalMs must be a positive number or Infinity, got ${String(value)}.`\n );\n}\n\n/** Validate that `value` is structured-cloneable. Throws early so config errors\n * surface on the main thread rather than silently failing inside the Worker\n * (where a DataCloneError would be reported as a generic Worker error with no\n * actionable message). Skips validation when `structuredClone` is unavailable\n * (older browsers without the API) \u2014 the Worker will still throw on its own.\n * @throws {TypeError} when `value` contains non-cloneable members (functions,\n * Symbols, DOM nodes, etc.). */\nfunction assertStructuredCloneable(value: unknown): void {\n if (typeof structuredClone !== 'function') return;\n try {\n structuredClone(value);\n } catch (error) {\n throw new TypeError(\n 'Centrifuge Worker configuration and published data must be structured-cloneable.',\n { cause: error }\n );\n }\n}\n\n/** Post `message` to a Worker or MessagePort, forwarding `transfer` buffers\n * when present. Both targets share the `postMessage(message, transfer?)`\n * signature, so a single helper eliminates the duplicated if/else at each\n * call site. */\nfunction postToPortLike(\n target: Pick<Worker, 'postMessage'>,\n message: CentrifugeWorkerInput,\n transfer?: ArrayBuffer[]\n): void {\n if (transfer) target.postMessage(message, transfer as Transferable[]);\n else target.postMessage(message);\n}\n\n/** Reconstruct an Error instance from its serialised form. Error objects cannot\n * be structured-cloned across the Worker boundary, so the Worker serialises them\n * into {@link SerializedWorkerError} and the main thread rebuilds the Error here\n * so the caller's `onError` handler receives a real Error with name/stack. */\nfunction deserializeWorkerError(error: SerializedWorkerError): Error {\n const result = new Error(error.message);\n result.name = error.name;\n if (error.stack) result.stack = error.stack;\n if (error.context !== undefined) Object.assign(result, { context: error.context });\n return result;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * CentrifugeSession \u2014 a reusable wrapper around a single Centrifuge client.\n *\n * Provides a structured-clone-safe message protocol (INIT/SUBSCRIBE/UNSUBSCRIBE/\n * PUBLISH/STOP) so the same session class can run inside a Dedicated Worker,\n * a SharedWorker port, or directly on the main thread as a local fallback.\n */\nimport { Centrifuge } from 'centrifuge';\nimport { parseDataBusPublication } from './core/publication';\nimport type {\n PublicationContext,\n StateContext,\n Subscription,\n SubscriptionErrorContext\n} from 'centrifuge';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\n\n/** Callback interface for posting messages back to the transport layer. */\nexport interface CentrifugeSessionSink<TData = unknown> {\n post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void;\n}\n\n/**\n * Stateful Centrifuge client wrapper shared by Dedicated Worker, SharedWorker\n * ports and the main-thread local fallback. Each session owns one connection.\n */\nexport class CentrifugeSession<TData = unknown> {\n private client: Centrifuge | null = null;\n private readonly subscriptions = new Map<string, Subscription>();\n private transferable = false;\n\n constructor(private readonly sink: CentrifugeSessionSink<TData>) {}\n\n /** Dispatch an incoming Worker message to the matching operation.\n * Unknown message types are ignored rather than thrown, so a future protocol\n * extension adding a new variant cannot crash an older session. */\n handle(message: CentrifugeWorkerInput): void {\n switch (message.type) {\n case 'INIT':\n this.initialize(message.url, message.config, message.transferable === true);\n return;\n case 'SUBSCRIBE':\n this.subscribe(message.topic);\n return;\n case 'UNSUBSCRIBE':\n this.unsubscribe(message.topic);\n return;\n case 'PUBLISH':\n case 'PUBLISH_BIN':\n // Binary and JSON publish share the same Centrifuge client call; the\n // transport layer decides whether to transfer the ArrayBuffer.\n this.publish(message.topic, message.data, message.messageId, message.timestamp);\n return;\n case 'STOP':\n this.stop();\n return;\n default:\n return;\n }\n }\n\n /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */\n private initialize(url: string, config: CentrifugeWorkerConfig, transferable: boolean): void {\n if (this.client) return;\n this.transferable = transferable;\n const client = new Centrifuge(url, config);\n this.client = client;\n client.on('state', (context: StateContext) => {\n this.post({ type: 'STATUS', status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => this.post({ type: 'STATUS', status: 'connected' }));\n client.on('disconnected', () => this.post({ type: 'STATUS', status: 'disconnected' }));\n client.on('error', context => this.postError(context));\n // Client-level publications are only for server-side subscriptions (where\n // no client Subscription object exists). For topics we have an active\n // subscription for, the subscription-level 'publication' listener handles\n // dispatch \u2014 skip here to avoid delivering the same message twice.\n client.on('publication', (context: PublicationContext) => {\n const topic = context.channel || getPayloadTopic(context.data);\n if (!topic || this.subscriptions.has(topic)) return;\n this.postPublication(topic, context.data);\n });\n client.connect();\n }\n\n /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.\n * Listeners are only registered once per subscription object \u2014 a repeated\n * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,\n * avoiding the removeAllListeners + re-on churn on every duplicate message. */\n private subscribe(topic: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n // If we already track this subscription, it already has our listeners \u2014\n // a duplicate SUBSCRIBE is a no-op (idempotent), matching the transport\n // contract. Only a fresh subscription needs listener wiring.\n const existing = this.subscriptions.get(topic);\n if (existing) {\n existing.subscribe();\n return;\n }\n let subscription = this.client.getSubscription(topic);\n if (!subscription) subscription = this.client.newSubscription(topic);\n // Remove only our own listeners so that any Centrifuge internal listeners\n // on the subscription object are preserved. Each subscription event carries\n // a single listener so removeAllListeners(\u2026) is safe here.\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.set(topic, subscription);\n subscription.on('publication', context => {\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => this.postError(context));\n subscription.on('unsubscribed', () => this.subscriptions.delete(topic));\n subscription.subscribe();\n }\n\n /** Unsubscribe from a Centrifuge channel and clean up the local reference.\n * Listeners are removed before unsubscribing so a late `unsubscribed` event\n * cannot delete a subscription that a subsequent `subscribe()` re-added. */\n private unsubscribe(topic: string): void {\n const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);\n if (!subscription) return;\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.delete(topic);\n subscription.unsubscribe();\n }\n\n /** Publish a message to the Centrifuge channel. */\n private publish(topic: string, data: unknown, messageId?: string, timestamp?: number): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n // Centrifuge's payload is application-defined. Preserve legacy payloads;\n // when an ID is requested, send a small metadata envelope that compatible\n // servers can echo back for end-to-end deduplication.\n const hasMetadata = messageId !== undefined || timestamp !== undefined;\n const payload = hasMetadata\n ? {\n data,\n ...(messageId === undefined ? {} : { messageId }),\n ...(timestamp === undefined ? {} : { timestamp })\n }\n : data;\n void this.client.publish(topic, payload).catch(error => this.postError(error));\n }\n\n /** Forward a publication to the transport. Binary payloads take the\n * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything\n * else is structured-cloned via `MESSAGE`. An empty topic means the\n * publication carried no channel info and is silently dropped. */\n private postPublication(topic: string, data: unknown): void {\n if (!topic) return;\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'MESSAGE_BIN', topic, data }, [data]);\n return;\n }\n const publication = parseDataBusPublication<TData>(data, topic);\n if (!publication) return;\n this.post({\n type: 'MESSAGE',\n topic: publication.topic,\n data: publication.data,\n ...(publication.messageId === undefined ? {} : { messageId: publication.messageId }),\n ...(publication.timestamp === undefined ? {} : { timestamp: publication.timestamp })\n });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: 'STATUS', status: 'disconnected' });\n }\n\n /** Forward a message to the sink (the transport layer). */\n private post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void {\n this.sink.post(message, transfer);\n }\n\n /** Serialise and report an error. The Centrifuge client handles reconnection\n * internally, so a transient error should not trigger a `STATUS: error` that\n * would cause `selectActiveWorkers()` to exclude this worker from routing.\n * Fatal errors are distinguished by the client eventually emitting\n * `disconnected` without a subsequent `connected`. */\n private postError(error: unknown): void {\n this.post({ type: 'ERROR', error: serializeError(error) });\n }\n}\n\n/** Extract a topic from a Centrifuge publication payload if one is present.\n * Handles both direct `channel` fields and the nested `push.channel` shape\n * that Centrifugo uses for some server-side push types. */\nfunction getPayloadTopic(data: unknown): string {\n if (!data || typeof data !== 'object') return '';\n const payload = data as Record<string, unknown>;\n // Prefer nested push.channel (server-side push) then fall back to top-level channel.\n const push = payload.push;\n const nested = typeof push === 'object' && push !== null ? (push as Record<string, unknown>).channel : undefined;\n const topic = nested ?? payload.channel;\n return typeof topic === 'string' ? topic : '';\n}\n\n/** Map a Centrifuge state string to the DataBus's status vocabulary.\n * 'connecting' and 'connected' pass through; anything else (e.g. 'reconnecting',\n * 'disconnected') maps to 'disconnected'. */\nconst LIVE_STATES = new Set(['connecting', 'connected']);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\n}\n\n/** Convert an arbitrary error into a structured-cloneable form for postMessage. */\nfunction serializeError(error: unknown): SerializedWorkerError {\n if (error instanceof Error) {\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {})\n };\n }\n return {\n name: 'CentrifugeError',\n message: typeof error === 'string' ? error : 'Centrifuge worker operation failed.',\n ...(error === undefined ? {} : { context: error })\n };\n}\n", "/**\n * Worker-thread protocol for Centrifuge WebSocket transport.\n *\n * Defines the message types exchanged between the main thread and a Web Worker\n * (dedicated or shared) that runs a centrifuge client. The worker is isolated\n * from the main thread so that WebSocket lifecycle, token refresh, and binary\n * data handling never block the UI.\n */\nimport type { Options } from 'centrifuge';\nimport type { WorkerStatus } from './core/types';\n\n/**\n * Default interval between main-thread PING heartbeats to a SharedWorker. The\n * SharedWorker reaps a silent port after `SESSION_TIMEOUT_MULTIPLIER` intervals.\n * Shared here so the main thread (heartbeat sender) and the SharedWorker (reaper)\n * always agree on the cadence.\n */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;\n/** SharedWorker per-port session timeout, expressed as a multiple of that port's\n * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of\n * several intervals tolerates throttled tabs without reaping healthy sessions. */\nexport const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;\n/** Default per-port session timeout in ms, derived from the heartbeat interval\n * and the multiplier. Used as the fallback when a port sends no INIT config. */\nexport const DEFAULT_SESSION_TIMEOUT_MS =\n DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;\n\n// Centrifuge Options that reference browser APIs (WebSocket, EventSource, etc.)\n// are unavailable inside a Worker \u2014 the worker uses its own WebSocket import.\n// These are stripped from the config sent to the worker so the type system\n// prevents accidentally passing a main-thread-only function (which would fail\n// structured cloning and throw a DataCloneError).\ntype WorkerUnsafeOption =\n | 'eventsource'\n | 'fetch'\n | 'getData'\n | 'getToken'\n | 'networkEventTarget'\n | 'readableStream'\n | 'sockjs'\n | 'websocket';\n\n/** Centrifuge options safe to pass into a Worker; unsafe options are explicitly\n * excluded and set to `never` so the compiler rejects them at the call site. */\nexport type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {\n [Key in WorkerUnsafeOption]?: never;\n};\n\n/** Messages sent from the main thread to the Worker. All variants are\n * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */\nexport type CentrifugeWorkerInput =\n /** Initial connection: URL + config. Sent once per backend creation.\n * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.\n * `heartbeatIntervalMs` overrides the SharedWorker PING cadence. */\n | { type: 'INIT'; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: 'SUBSCRIBE'; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: 'UNSUBSCRIBE'; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: 'PUBLISH'; topic: string; data: unknown; messageId?: string; timestamp?: number }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: 'PUBLISH_BIN'; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: 'PING' }\n /** Disconnect the client and clear all subscriptions. */\n | { type: 'STOP' };\n\n/** Messages sent from the Worker back to the main thread. The main thread\n * routes these to the DataBusTransportHandlers via handleOutput(). */\nexport type CentrifugeWorkerOutput<TData = unknown> =\n /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */\n | { type: 'STATUS'; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: 'MESSAGE'; topic: string; data: TData; messageId?: string; timestamp?: number }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: 'MESSAGE_BIN'; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */\n | { type: 'ERROR'; error: SerializedWorkerError };\n\n/** Error object serialized for cross-thread transfer. Error instances cannot\n * be structured-cloned via postMessage, so the Worker converts them to this\n * shape and the main thread rebuilds an Error via deserializeWorkerError(). */\nexport interface SerializedWorkerError {\n /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */\n name: string;\n /** The Error's `message`. */\n message: string;\n /** The Error's `stack` if available (for debugging). */\n stack?: string;\n /** Arbitrary context attached by the Worker (e.g. the failing operation). */\n context?: unknown;\n}\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends, selected in order of preference:\n * 1. SharedWorker \u2014 one WebSocket per tab session, hosted in a shared process\n * 2. Dedicated Worker \u2014 one WebSocket per tab\n * 3. In-process (local) \u2014 Centrifuge runs on the main thread (fallback when\n * neither Worker type is available, e.g. in non-browser environments)\n *\n * Binary data (ArrayBuffer) can be transferred via Transferable when the\n * `transferable` option is enabled, avoiding structured-clone overhead.\n */\n\nimport { CrossTabDataBus } from './core/data-bus';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type { DataBusTransport, DataBusTransportHandlers } from './core/types';\nimport { CentrifugeSession } from './centrifuge-session';\nimport { selectWorkerBackend } from './worker-mode';\nimport type { WorkerBackend, WorkerMode } from './worker-mode';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\n\nexport type { CentrifugeWorkerConfig, SerializedWorkerError } from './centrifuge-protocol';\nexport type { WorkerBackend, WorkerMode } from './worker-mode';\n\n/** WebSocket connection parameters passed to the Centrifuge Worker. */\nexport interface CentrifugeDataBusConfig {\n /** Centrifuge server WebSocket URL. */\n url: string;\n /** Centrifuge client options (token, channel params, etc.). */\n options?: CentrifugeWorkerConfig;\n}\n\n/** Options for configuring the Worker backend (dedicated, shared, or local). */\nexport interface CentrifugeWorkerTransportOptions {\n /** Custom dedicated Worker factory. Used for testing or bundler integration. */\n workerFactory?: () => Worker;\n /** Custom SharedWorker factory. */\n sharedWorkerFactory?: () => SharedWorker;\n /** Preferred Worker mode: 'dedicated', 'shared', or 'auto'. */\n workerMode?: WorkerMode;\n /** Enable transferable (ArrayBuffer) support for binary data. */\n transferable?: boolean;\n /** Interval (ms) between PING heartbeats sent to the SharedWorker. The\n * SharedWorker reaps a silent port after `DEFAULT_SESSION_TIMEOUT_MULTIPLIER`\n * \u00D7 this interval. Pass `Infinity` to disable heartbeats entirely. Defaults\n * to `DEFAULT_HEARTBEAT_INTERVAL_MS`. */\n heartbeatIntervalMs?: number;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a Centrifuge transport. */\nexport interface CreateCentrifugeDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<CentrifugeDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n >,\n CentrifugeWorkerTransportOptions {\n /** Centrifuge connection configuration. */\n connection: CentrifugeDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/**\n * Transport layer that runs a Centrifuge WebSocket client inside a Web Worker.\n *\n * Delegates the actual WebSocket connection to a Worker (dedicated or shared)\n * or falls back to an in-process CentrifugeSession. The Worker is isolated from\n * the main thread so that WebSocket lifecycle, token refresh, and binary data\n * handling never block the UI.\n */\nexport class CentrifugeWorkerTransport<TData = unknown>\n implements DataBusTransport<CentrifugeDataBusConfig, TData>\n{\n private readonly workerMode: WorkerMode;\n private readonly transferable: boolean;\n private readonly heartbeatIntervalMs: number;\n private readonly workerFactory: (() => Worker) | undefined;\n private readonly sharedWorkerFactory: (() => SharedWorker) | undefined;\n private backend: WorkerBackend | null = null;\n private worker: Worker | null = null;\n private sharedWorker: SharedWorker | null = null;\n private port: MessagePort | null = null;\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n private localSession: CentrifugeSession<TData> | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n // Monotonically increasing counter, bumped each time a backend is created.\n // Used to ignore late error events from a superseded Worker.\n private generation = 0;\n // Generation captured when the current backend was created. Error handlers\n // only act when the backend that registered them is still current.\n private backendGeneration = 0;\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? 'dedicated';\n this.transferable = options.transferable ?? false;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n assertHeartbeatInterval(this.heartbeatIntervalMs);\n this.workerFactory = options.workerFactory;\n this.sharedWorkerFactory = options.sharedWorkerFactory;\n }\n\n /**\n * Start the transport: select a backend, initialise the Worker (or local\n * session), and send the INIT message with connection parameters.\n */\n start(config: CentrifugeDataBusConfig, handlers: DataBusTransportHandlers<TData>): void {\n if (this.backend) return;\n assertStructuredCloneable(config.options ?? {});\n this.handlers = handlers;\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined,\n sharedWorker: this.sharedWorkerFactory !== undefined\n });\n const input = this.buildInitInput(config);\n if (backend === 'shared') {\n this.startSharedWorker(input);\n this.backend = 'shared';\n return;\n }\n if (backend === 'dedicated') {\n this.startDedicatedWorker(input);\n this.backend = 'dedicated';\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = 'local';\n }\n\n subscribe(topic: string): void {\n this.post({ type: 'SUBSCRIBE', topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: 'UNSUBSCRIBE', topic });\n }\n\n /**\n * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable\n * when `transferable` is enabled, avoiding a structured-clone cycle.\n */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): void {\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'PUBLISH_BIN', topic, data, ...publicationMetadata(options) }, [data]);\n return;\n }\n this.post({ type: 'PUBLISH', topic, data, ...publicationMetadata(options) });\n }\n\n /**\n * Gracefully stop the transport: send STOP, clean up event listeners, and\n * terminate the Worker (or close the SharedWorker port).\n */\n stop(): void {\n if (!this.backend) return;\n this.generation++;\n this.post({ type: 'STOP' });\n this.clearHeartbeat();\n if (this.worker) {\n this.worker.removeEventListener('message', this.handleMessage);\n this.worker.removeEventListener('error', this.handleWorkerError);\n this.worker.terminate();\n }\n if (this.sharedWorker) {\n this.detachSharedWorkerListeners();\n this.port?.close();\n }\n this.resetBackend();\n this.handlers = null;\n }\n\n /** Build the INIT payload sent to the Worker / local session. Optional fields\n * are only included when they deviate from the defaults, so the Worker's own\n * default-resolution logic kicks in for the common case. */\n private buildInitInput(config: CentrifugeDataBusConfig): CentrifugeWorkerInput {\n return {\n type: 'INIT',\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS\n ? { heartbeatIntervalMs: this.heartbeatIntervalMs }\n : {})\n };\n }\n\n /** Create and initialise a dedicated Worker, then send the INIT message. */\n private startDedicatedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const worker = (this.workerFactory ?? createDefaultWorker)();\n this.worker = worker;\n worker.addEventListener('message', this.handleMessage);\n worker.addEventListener('error', this.handleWorkerError);\n worker.postMessage(input);\n }\n\n /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */\n private startSharedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const shared = (this.sharedWorkerFactory ?? createDefaultSharedWorker)();\n this.sharedWorker = shared;\n const port = shared.port;\n this.port = port;\n port.addEventListener('message', this.handleMessage);\n port.addEventListener('messageerror', this.handlePortError);\n shared.addEventListener('error', this.handleSharedWorkerError);\n port.start();\n port.postMessage(input);\n this.startHeartbeat();\n }\n\n /** Handle a message event from the Worker (dedicated or shared). */\n private readonly handleMessage = (event: MessageEvent<CentrifugeWorkerOutput<TData>>) => {\n this.handleOutput(event.data);\n };\n\n /** Handle a message from the in-process CentrifugeSession (local fallback). */\n private readonly handleSessionOutput = (message: CentrifugeWorkerOutput<TData>) => {\n this.handleOutput(message);\n };\n\n /** Route a Worker output message to the appropriate handler callback.\n * Shared by the Worker message listener, the SharedWorker port listener,\n * and the local-session sink \u2014 all three feed into this single dispatcher. */\n private handleOutput(message: CentrifugeWorkerOutput<TData>): void {\n if (message.type === 'STATUS') this.handlers?.onStatus(message.status);\n if (message.type === 'MESSAGE') this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message) });\n if (message.type === 'MESSAGE_BIN') this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...publicationMetadata(message) });\n if (message.type === 'ERROR') this.handlers?.onError(deserializeWorkerError(message.error));\n }\n\n /** Handle a Worker-level failure (crash, message decode error). Discards the\n * dead backend so a later start()/reopen can rebuild from scratch, and\n * signals an error status so the DataBus can trigger recovery.\n * Only invoked when the generation guard confirms the failing backend is\n * still current \u2014 late errors from a superseded Worker are silently dropped. */\n private onWorkerFailed(message: string): void {\n // Remove the message listener and release the old backend before recovery\n // reopens, so late messages from the failed Worker/port cannot be routed\n // into the freshly started session.\n this.worker?.removeEventListener('message', this.handleMessage);\n this.worker?.removeEventListener('error', this.handleWorkerError);\n this.worker?.terminate();\n this.detachSharedWorkerListeners();\n this.port?.close();\n this.clearHeartbeat();\n this.resetBackend();\n this.handlers?.onError(new Error(message));\n this.handlers?.onStatus('error');\n }\n\n /** Remove every listener attached to the current SharedWorker and its port. */\n private detachSharedWorkerListeners(): void {\n this.port?.removeEventListener('message', this.handleMessage);\n this.port?.removeEventListener('messageerror', this.handlePortError);\n this.sharedWorker?.removeEventListener('error', this.handleSharedWorkerError);\n }\n\n /** Periodically ping the SharedWorker so its session reaper can detect a dead tab. */\n private startHeartbeat(): void {\n if (this.heartbeatHandle !== null) return;\n // Infinite disables the heartbeat (e.g. for environment where the\n // SharedWorker reaper is not needed).\n if (this.heartbeatIntervalMs === Infinity) return;\n this.heartbeatHandle = setInterval(() => {\n this.post({ type: 'PING' });\n }, this.heartbeatIntervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatHandle !== null) clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n\n private readonly handleWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker failed.');\n };\n\n /** Clear the Worker/port/backend references after a failure or stop. */\n private resetBackend(): void {\n this.worker = null;\n this.sharedWorker = null;\n this.port = null;\n this.backend = null;\n this.localSession = null;\n }\n\n /**\n * Post a message to the active backend. Accepts optional Transferable buffers\n * for zero-copy ArrayBuffer transfer.\n */\n private post(message: CentrifugeWorkerInput, transfer?: ArrayBuffer[]): void {\n if (this.worker) {\n postToPortLike(this.worker, message, transfer);\n return;\n }\n if (this.port) {\n postToPortLike(this.port, message, transfer);\n return;\n }\n if (this.localSession) {\n this.localSession.handle(message);\n return;\n }\n throw new Error('CentrifugeWorkerTransport.start() must be called first.');\n }\n}\n\n/**\n * Create a fully-configured CrossTabDataBus with a Centrifuge WebSocket transport.\n *\n * This is the primary entry point for consumers. It wires up the transport,\n * cluster coordination, and lifecycle management:\n *\n * ```ts\n * const bus = createCentrifugeDataBus({\n * connection: { url: 'wss://example.com/connection/websocket', options: { token: '\u2026' } },\n * trace: { enabled: true, sink: event => console.log(event) },\n * });\n * ```\n */\nexport function createCentrifugeDataBus<TData = unknown>(\n options: CreateCentrifugeDataBusOptions<TData>\n): CrossTabDataBus<CentrifugeDataBusConfig, TData> {\n const {\n clusterKey,\n connection,\n heartbeatIntervalMs,\n sharedWorkerFactory,\n transferable,\n workerFactory,\n workerMode,\n ...dataBusOptions\n } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new CentrifugeWorkerTransport<TData>({\n ...(workerFactory ? { workerFactory } : {}),\n ...(sharedWorkerFactory ? { sharedWorkerFactory } : {}),\n ...(transferable === undefined ? {} : { transferable }),\n ...(workerMode ? { workerMode } : {}),\n ...(heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs })\n })\n });\n}\n\n\n/** Create the default dedicated Worker hosting the Centrifuge client. */\nfunction createDefaultWorker(): Worker {\n if (typeof Worker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser Worker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly.'\n );\n }\n return new Worker(workerUrl, {\n name: 'cross-tab-worker-databus',\n type: 'module'\n });\n}\n\n/** Create the default SharedWorker. Each connecting port within the SharedWorker\n * creates its own CentrifugeSession with an independent WebSocket connection,\n * so refreshing or stopping one tab does not affect the others. */\nfunction createDefaultSharedWorker(): SharedWorker {\n if (typeof SharedWorker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser SharedWorker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.shared.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly.'\n );\n }\n return new SharedWorker(workerUrl, {\n name: 'cross-tab-worker-databus-shared',\n type: 'module'\n });\n}\n\n/** Validate the SharedWorker PING heartbeat interval. A value of `0`, a negative\n * number, or `NaN` would otherwise make `setInterval` degenerate into a 0ms busy\n * loop, driving the reaper and the main-thread PING out of control. `Infinity`\n * is allowed and disables heartbeats entirely (for environments where the\n * SharedWorker reaper is not needed, e.g. a single-tab deployment).\n * @throws {TypeError} when `value` is not a positive finite number or Infinity. */\nfunction assertHeartbeatInterval(value: number): void {\n if (value === Infinity) return;\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) return;\n throw new TypeError(\n `Centrifuge heartbeatIntervalMs must be a positive number or Infinity, got ${String(value)}.`\n );\n}\n\n/** Validate that `value` is structured-cloneable. Throws early so config errors\n * surface on the main thread rather than silently failing inside the Worker\n * (where a DataCloneError would be reported as a generic Worker error with no\n * actionable message). Skips validation when `structuredClone` is unavailable\n * (older browsers without the API) \u2014 the Worker will still throw on its own.\n * @throws {TypeError} when `value` contains non-cloneable members (functions,\n * Symbols, DOM nodes, etc.). */\nfunction assertStructuredCloneable(value: unknown): void {\n if (typeof structuredClone !== 'function') return;\n try {\n structuredClone(value);\n } catch (error) {\n throw new TypeError(\n 'Centrifuge Worker configuration and published data must be structured-cloneable.',\n { cause: error }\n );\n }\n}\n\n/** Post `message` to a Worker or MessagePort, forwarding `transfer` buffers\n * when present. Both targets share the `postMessage(message, transfer?)`\n * signature, so a single helper eliminates the duplicated if/else at each\n * call site. */\nfunction postToPortLike(\n target: Pick<Worker, 'postMessage'>,\n message: CentrifugeWorkerInput,\n transfer?: ArrayBuffer[]\n): void {\n if (transfer) target.postMessage(message, transfer as Transferable[]);\n else target.postMessage(message);\n}\n\n/** Reconstruct an Error instance from its serialised form. Error objects cannot\n * be structured-cloned across the Worker boundary, so the Worker serialises them\n * into {@link SerializedWorkerError} and the main thread rebuilds the Error here\n * so the caller's `onError` handler receives a real Error with name/stack. */\nfunction deserializeWorkerError(error: SerializedWorkerError): Error {\n const result = new Error(error.message);\n result.name = error.name;\n if (error.stack) result.stack = error.stack;\n if (error.context !== undefined) Object.assign(result, { context: error.context });\n return result;\n}\n\n/** Copy defined publication metadata without adding empty enumerable fields. */\nfunction publicationMetadata(\n metadata?: { messageId?: string; timestamp?: number }\n): { messageId?: string; timestamp?: number } {\n return {\n ...(metadata?.messageId === undefined ? {} : { messageId: metadata.messageId }),\n ...(metadata?.timestamp === undefined ? {} : { timestamp: metadata.timestamp })\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4981,6 +4981,26 @@ Centrifuge.SubscriptionState = SubscriptionState;
4981
4981
  Centrifuge.State = State;
4982
4982
  Centrifuge.UnauthorizedError = UnauthorizedError;
4983
4983
 
4984
+ // src/core/publication.ts
4985
+ function parseDataBusPublication(value, fallbackTopic) {
4986
+ if (!value || typeof value !== "object") {
4987
+ return fallbackTopic ? { topic: fallbackTopic, data: value } : null;
4988
+ }
4989
+ const frame = value;
4990
+ const nested = frame.publication && typeof frame.publication === "object" ? frame.publication : null;
4991
+ const publication = nested ?? frame;
4992
+ const topic = typeof publication.topic === "string" ? publication.topic : fallbackTopic;
4993
+ if (!topic) return null;
4994
+ const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
4995
+ const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
4996
+ return {
4997
+ topic,
4998
+ data,
4999
+ ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
5000
+ ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
5001
+ };
5002
+ }
5003
+
4984
5004
  // src/centrifuge-session.ts
4985
5005
  var CentrifugeSession = class {
4986
5006
  constructor(sink) {
@@ -5005,7 +5025,7 @@ var CentrifugeSession = class {
5005
5025
  return;
5006
5026
  case "PUBLISH":
5007
5027
  case "PUBLISH_BIN":
5008
- this.publish(message.topic, message.data, message.messageId);
5028
+ this.publish(message.topic, message.data, message.messageId, message.timestamp);
5009
5029
  return;
5010
5030
  case "STOP":
5011
5031
  this.stop();
@@ -5070,9 +5090,14 @@ var CentrifugeSession = class {
5070
5090
  subscription.unsubscribe();
5071
5091
  }
5072
5092
  /** Publish a message to the Centrifuge channel. */
5073
- publish(topic, data, messageId) {
5093
+ publish(topic, data, messageId, timestamp) {
5074
5094
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
5075
- const payload = messageId ? { data, messageId } : data;
5095
+ const hasMetadata = messageId !== void 0 || timestamp !== void 0;
5096
+ const payload = hasMetadata ? {
5097
+ data,
5098
+ ...messageId === void 0 ? {} : { messageId },
5099
+ ...timestamp === void 0 ? {} : { timestamp }
5100
+ } : data;
5076
5101
  void this.client.publish(topic, payload).catch((error) => this.postError(error));
5077
5102
  }
5078
5103
  /** Forward a publication to the transport. Binary payloads take the
@@ -5085,9 +5110,15 @@ var CentrifugeSession = class {
5085
5110
  this.post({ type: "MESSAGE_BIN", topic, data }, [data]);
5086
5111
  return;
5087
5112
  }
5088
- const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : void 0;
5089
- const payload = messageId && Object.prototype.hasOwnProperty.call(data, "data") ? data.data : data;
5090
- this.post({ type: "MESSAGE", topic, data: payload, ...messageId ? { messageId } : {} });
5113
+ const publication = parseDataBusPublication(data, topic);
5114
+ if (!publication) return;
5115
+ this.post({
5116
+ type: "MESSAGE",
5117
+ topic: publication.topic,
5118
+ data: publication.data,
5119
+ ...publication.messageId === void 0 ? {} : { messageId: publication.messageId },
5120
+ ...publication.timestamp === void 0 ? {} : { timestamp: publication.timestamp }
5121
+ });
5091
5122
  }
5092
5123
  /** Disconnect the client and clear all subscriptions. */
5093
5124
  stop() {