cross-tab-worker-databus 0.21.6 → 0.21.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.21.7] - 2026-09-23
4
+
5
+ ### Changed
6
+ - Four more zero-count legs in three modules are now labelled with the enumeration that covers them, each measured by deleting it and running the suite: `PortReaper.reap()`'s two `??` fallbacks (the three maps gain a port only in `register()` and lose one only together, so a port in `targets` is in the other two; the fallback stays because a half-registered port would read as silent since the epoch and be closed on the next tick), `CentrifugeWorkerTransport.startHeartbeat()`'s already-armed guard (`start()` returns early while a backend exists, and the two routes to `backend === null` both clear the heartbeat first; the guard is the only thing preventing a double-armed interval that `clearHeartbeat()` — which holds one handle — could never cancel), and `CentrifugeSession.postPublication()`'s empty-topic drop (its two callers test emptiness or key a map this session only fills from a SUBSCRIBE frame, and an empty topic is undeliverable downstream anyway: `topicMatchesPattern` answers false for both `*` and `prefix.*` against `''`, measured, and `subscribe('')` throws). All four survive deletion with all 37 test files green, so none is a gap; none is counted closed, and there is no coverage movement. The TypeScript 7 re-check landed the same way — newest release 8.70.1 and newest canary 8.70.2-alpha.4 both still peer on `<6.1.0`, recorded in the roadmap with the date and the two versions that were queried.
7
+ - The WebSocket transport's connect-timeout guard now has a test, and now says what kind of leg it is. The callback opens with `if (this.socket !== socket || this.handlers !== handlers || handshakeCompleted) return;` and the third term had never executed, because a completed handshake has no live timer to fire: `onopen` cancels it directly and `settleConnect()` cancels it again. Measured — deleting only that term leaves the whole suite green, so it is dominated rather than untested; deleting the term *and* both cancellations makes the transport report `onStatus('error')`, call `onError`, and abort a socket that had already connected, which with the 30-second default budget is a healthy connection killed seconds after it opened. The term therefore stays, and `never lets the connect timer tear down a handshake that already completed` in `tests/websocket.test.ts` now pins the pair: measured, it passes with the term deleted, with the `onopen` cancellation deleted, and with both cancellations deleted, and fails only when the term goes with them. No behaviour change and no coverage movement.
8
+ - The re-subscribe loop's supersession `break` in `start()` now carries its own measurement, in the same spirit as the leg below: two re-entrant callbacks were built to reach it and neither made it observable. A `stop()` issued from the transport's own `subscribe()` leaves the subscribed set at `['first']` whether the `break` is live or disabled, because a stopped cluster's `subscribe()` is inert; a re-entrant `start()` leaves it at `['first', 'second']` either way, because the newer lifecycle replays the remaining topic itself and `subscribeTransport()` absorbs the duplicate the superseded loop would have issued. Verdict recorded: executing and redundant in the reachable scenarios, not dominated — kept because it is the only thing between a superseded opening and writing subscriptions into whichever transport is installed now. A test was written for the first scenario, passed, and was deleted once the same mutant passed it too, because pinning a sequence that does not depend on the leg would leave the next reader trusting a coverage win that is not there. No behaviour change and no coverage movement; `data-bus.ts` still reports twelve branch arms with a zero-count slot, unchanged by this pass and by the one below it — two of the twelve are the legs those passes measured, and the rest are recorded in earlier entries as dominated-by-enumeration or kept-for-asymmetric-failure.
9
+ - The `startDemandRecovery()` status guard now carries its measurement instead of silence. It is the one arm of the recovery ledger that resisted construction, and the reason matters for anyone tempted to delete it: `suspended` and `stopping` are excluded at both call sites by their own precondition checks, so only `status !== error` can take it, and that needs an `updateStatus()` away from error which neither releases the gate nor consumes the demand token. Two sequences were built against that description and both measured false — a transport-level reconnect after a failed automatic attempt (the token had already been consumed, so the earlier guard returned and the write went straight out), and the same with a `disconnected` status (a later automatic attempt reopened, so the reopen-count assertion failed). The first attempt also produced a test that passed while proving nothing, which is why the entry records the measurements rather than the arm being relabelled "dominated". It stays an open zero-count leg, kept because deleting it would let any of those futures reopen a connection that is reported up. No behaviour change, no coverage movement.
10
+
11
+ ### Tests
12
+ - The coverage floors had drifted into advisory territory: `vitest.config.ts` required 96 / 92 / 96 / 97 while the tree measures 99.01 / 96.74 / 99.26 / 99.69, so a change could lose more than four points of branch coverage — most of one module — and still pass the release gate. Re-measured before tightening: three consecutive local runs agreed to the last decimal, and CI's own "Coverage thresholds" step reported the identical four numbers for the same tree, so the margin is not there for measurement noise. The floors are now 98 / 96 / 98 / 99, with the remaining headroom reserved for the one genuinely runner-dependent input (the seeded fuzzers bound depth by wall clock, so a loaded box explores fewer interleavings than this one). Teeth verified rather than assumed: raising `branches` to 99 fails the run with `ERROR: Coverage for branches (96.74%) does not meet global threshold (99%)` and a non-zero exit, so losing ~1.3 points of branch coverage now stops a build instead of sailing through. `docs/release-checklist.md` and its Chinese counterpart quote the new figures.
13
+
14
+ ### Documentation
15
+ - `docs/architecture.md` and its Chinese counterpart gained a "Receiver-side frame validation" section: the three checks that are now part of the wire contract (`targetWorkerId` names this worker, `topicKey === createOpaqueKey(topic)` on both point-to-point frame types, and a batch frame must carry a non-empty array), what each one prevents, and — equally important for anyone debugging a silently ignored frame — the two things deliberately *not* checked (`EVENT` payload fields beyond shape, and batch item content). The rules existed only in code comments and `AGENTS.md`, which is the maintainer-facing document; an integrator running a non-JS peer or a custom `ClusterEnvironment` needs to know that a mismatched key/topic pair is dropped without any response, and that a batch with zero items is not a no-op but an ignored frame.
16
+ - `docs/api.md` (and its Chinese counterpart) promised that replay buffers are "cleared when the last handler for the topic unsubscribes", which is true of an exact topic and false of a wildcard: the rings a pattern fills are keyed by the concrete publication topics, so `unsubscribe("chat.*")` releases delivery only — the durable cleanup is called with `chat.*`, a key no row lives under, and the in-memory rings stay with whatever still owns them. Measured before rewriting the text: two concrete topics from three publications under one pattern stayed at `{ topics: 2, messages: 3 }` across the unsubscribe and were redelivered in full by a later `replay: true` subscribe. The docs now say which key each cleanup addresses, that `maxPerTopic` bounds ring *depth* and never the number of rings a wide pattern creates, and what to call instead (`clearReplay()` / `clearReplayTopic()` / `retentionMs`). No behaviour change: pattern-aware pruning would evict topics another live subscription still owns, which is a product decision rather than a doc fix. Pinned by `leaves the concrete topics a pattern filled when the pattern is unsubscribed` in `tests/data-bus.test.ts`.
17
+
18
+ ### Coverage
19
+ - The hydration catch in `replay-manager.ts` had an arm that no test reached: `if (epoch !== this.hydrationEpoch) return;` inside the `catch`, which is what stops a load rejection belonging to a *superseded* snapshot from speaking for the session that replaced it. `resetBuffers()` bumps the epoch but not the retry generation, so the rejection arrives with the generation still current and the only thing between it and `onPersistenceError()` is that line — and the same arm also owns `hydrationComplete`/`hydrationFailed`, whose latch is what `requestHydration()` short-circuits on. Dropping it therefore reported a phantom persistence error *and* left durable history unloaded for the rest of the instance's life. Now pinned by `does not let a superseded hydration failure speak for the replacement session`, which fails against the deleted guard as `expected [ Error: stale load failed ] to deeply equal []`; `replay-manager.ts` branch coverage 95.15% → 95.75%, whole suite 98.98 / 96.69 → 99.01 / 96.74 over 881 tests.
20
+ - Four zero-count legs in the same module are now labelled with the condition that covers them instead of left as an open question, none of them deleted and none of them counted as closed: `hydrate()`'s own `!this.buffers || !this.persistence` (the single caller tests the identical pair immediately above, and it stays because the body reads both through `!` assertions); the post-`load()` generation check (the retry wrapper re-tests the same thing before returning, and the only bump site also raises the epoch, so the next line would catch it — the difference is a reported cancellation vs a silent drop); the `: new PersistenceRetryCancelledError()` wrap in the cancellation catch (the wrapper converts any error that arrives after a bump into that error first, so the value is already one); and `scheduleRetentionCleanup()`'s capability check (both callers test the same capability before calling). `withPersistenceRetry()`'s loop-top generation check is dominated on every pass by construction — iteration 1 captures the generation two statements earlier with no await between, and later iterations arrive only through the identical check at the end of the catch — and is kept as the loop's own statement of its contract.
21
+ - Re-derived the whole-repo zero-count ledger from `coverage/coverage-final.json` rather than from the per-module notes, which had drifted: `cluster.ts` 22 arms (the 0.21.4-era note said 19), `data-bus.ts` 12 (recorded), `replay-manager.ts` 8 → 7, `replay-persistence.ts` 8, `websocket.ts` 3, `centrifuge.ts` 2, `trace.ts` 3, `port-reaper.ts` 3, `centrifuge-session.ts` 1, `version.ts` 1, `validation.ts` 1. `version.ts`'s is the `SDK_VERSION` fallback and is statically dead in every artifact that ships — both bundles inject the `define`, so the `typeof` test can only read `'string'`; its `''` arm exists for a consumer that bundles `src/` with no define, which no in-tree build does.
22
+
23
+
3
24
  ## [0.21.6] - 2026-09-22
4
25
 
5
26
  ### Fixed
@@ -37,7 +37,16 @@ export declare class CentrifugeSession<TData = unknown> {
37
37
  /** Forward a publication to the transport. Binary payloads take the
38
38
  * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
39
39
  * else is structured-cloned via `MESSAGE`. An empty topic means the
40
- * publication carried no channel info and is silently dropped. */
40
+ * publication carried no channel info and is silently dropped.
41
+ *
42
+ * That drop is uncovered and dominated: the connection-level caller tests the
43
+ * same emptiness before calling, and the subscription-level caller reads the
44
+ * topic out of a key this session only ever populates from a SUBSCRIBE frame.
45
+ * Measured — deleting this line leaves all 37 test files green, and the frame
46
+ * it withholds could not be delivered anyway, because `topicMatchesPattern`
47
+ * answers false for an empty topic against both `*` and `prefix.*` (so no
48
+ * wildcard handler sees it either) and `subscribe('')` throws. Kept as the
49
+ * boundary statement, not counted as a closed leg. */
41
50
  private postPublication;
42
51
  /** Disconnect the client and clear all subscriptions. */
43
52
  private stop;
@@ -1 +1 @@
1
- {"version":3,"file":"centrifuge-session.d.ts","sourceRoot":"","sources":["../src/centrifuge-session.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAEV,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAK/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;AAgBD;;;GAGG;AACH,qBAAa,iBAAiB,CAAC,KAAK,GAAG,OAAO;IAYhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAI1B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA0C;gBAElD,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC;IAE/D;;uEAEmE;IACnE,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA+B5C,8EAA8E;IAC9E,OAAO,CAAC,UAAU;IA8ClB;;;mFAG+E;IAC/E,OAAO,CAAC,SAAS;IAkCjB;;gFAE4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAmBf;;;sEAGkE;IAClE,OAAO,CAAC,eAAe;IAgBvB,yDAAyD;IACzD,OAAO,CAAC,IAAI;IAgBZ;;mFAE+E;IAC/E,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAOnB,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,EACvB,MAAM,uBAAuB,CAAC;AAK/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;AAgBD;;;GAGG;AACH,qBAAa,iBAAiB,CAAC,KAAK,GAAG,OAAO;IAYhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAI1B,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA0C;gBAElD,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC;IAE/D;;uEAEmE;IACnE,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IA+B5C,8EAA8E;IAC9E,OAAO,CAAC,UAAU;IA8ClB;;;mFAG+E;IAC/E,OAAO,CAAC,SAAS;IAkCjB;;gFAE4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAmBf;;;;;;;;;;;;0DAYsD;IACtD,OAAO,CAAC,eAAe;IAgBvB,yDAAyD;IACzD,OAAO,CAAC,IAAI;IAgBZ;;mFAE+E;IAC/E,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAOnB,2DAA2D;IAC3D,OAAO,CAAC,IAAI;IAIZ;;;;0DAIsD;IACtD,OAAO,CAAC,SAAS;CAGlB"}
@@ -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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"}
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-LXAHMO7L.js";
10
+ } from "./chunk-JQEKC5WW.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -165,7 +165,16 @@ var CentrifugeSession = class {
165
165
  /** Forward a publication to the transport. Binary payloads take the
166
166
  * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
167
167
  * else is structured-cloned via `MESSAGE`. An empty topic means the
168
- * publication carried no channel info and is silently dropped. */
168
+ * publication carried no channel info and is silently dropped.
169
+ *
170
+ * That drop is uncovered and dominated: the connection-level caller tests the
171
+ * same emptiness before calling, and the subscription-level caller reads the
172
+ * topic out of a key this session only ever populates from a SUBSCRIBE frame.
173
+ * Measured — deleting this line leaves all 37 test files green, and the frame
174
+ * it withholds could not be delivered anyway, because `topicMatchesPattern`
175
+ * answers false for an empty topic against both `*` and `prefix.*` (so no
176
+ * wildcard handler sees it either) and `subscribe('')` throws. Kept as the
177
+ * boundary statement, not counted as a closed leg. */
169
178
  postPublication(topic, data) {
170
179
  if (!topic) return;
171
180
  if (this.transferable && data instanceof ArrayBuffer) {
@@ -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 { 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} from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, WORKER_STATUS } from './utils/constants';\nimport { publicationMetadata } from './utils/metadata';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\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/** A pending getToken/getChannelToken request awaiting a main-thread response. */\ninterface PendingTokenRequest {\n resolve: (token: string) => void;\n reject: (error: unknown) => void;\n}\n\n/** Centrifuge client options for token-bridge mode: the structured-clone-safe\n * subset plus the two function-valued hooks that Centrifuge invokes for fresh\n * credentials. Used only inside the session, never across the Worker boundary. */\ntype CentrifugeBridgedOptions = Omit<CentrifugeWorkerConfig, 'getToken' | 'getChannelToken'> & {\n getToken: () => Promise<string>;\n getChannelToken: (channel: string) => Promise<string>;\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 private tokenBridge = false;\n private nextRequestId = 1;\n // Bumped on every initialize/stop. Async client callbacks capture the value\n // from the connection that created them so a stopped client cannot emit into\n // a later session instance (including the main-thread local fallback).\n private lifecycle = 0;\n private readonly pendingTokenRequests = new Map<number, PendingTokenRequest>();\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 CENTRIFUGE_INPUT_TYPE.INIT:\n this.initialize(message.url, message.config, message.transferable === true, message.tokenBridge === true);\n return;\n case CENTRIFUGE_INPUT_TYPE.SUBSCRIBE:\n this.subscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE:\n this.unsubscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.PUBLISH:\n case CENTRIFUGE_INPUT_TYPE.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 CENTRIFUGE_INPUT_TYPE.STOP:\n this.stop();\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE:\n this.resolveToken(message.requestId, message.token);\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR:\n this.rejectToken(message.requestId, deserializeWorkerError(message.error));\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, tokenBridge: boolean): void {\n if (this.client) return;\n const lifecycle = ++this.lifecycle;\n this.transferable = transferable;\n this.tokenBridge = tokenBridge;\n const clientOptions: CentrifugeWorkerConfig | CentrifugeBridgedOptions = tokenBridge\n ? {\n ...config,\n // Route credential fetches back to the main thread, where the\n // application's credentialProvider lives (config must stay\n // structured-cloneable, so functions cannot cross the boundary).\n getToken: () => this.requestToken('token', lifecycle),\n getChannelToken: (channel: string) => this.requestToken('channelToken', lifecycle, channel)\n }\n : config;\n const client = new Centrifuge(url, clientOptions);\n this.client = client;\n client.on('state', (context: StateContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED });\n });\n client.on('disconnected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n });\n client.on('error', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\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 if (this.lifecycle !== lifecycle) return;\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 const lifecycle = this.lifecycle;\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 if (this.lifecycle !== lifecycle) return;\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n subscription.on('unsubscribed', () => {\n if (this.lifecycle === lifecycle) this.subscriptions.delete(topic);\n });\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 const lifecycle = this.lifecycle;\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 => {\n if (this.lifecycle === lifecycle) this.postError(error);\n });\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: CENTRIFUGE_OUTPUT_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: CENTRIFUGE_OUTPUT_TYPE.MESSAGE,\n topic: publication.topic,\n data: publication.data,\n ...publicationMetadata(publication.messageId, publication.timestamp)\n });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n // Invalidate callbacks from the connection being stopped before disconnect\n // so its synchronous or queued events cannot leak into a later session.\n this.lifecycle += 1;\n // Settle every in-flight credential request so a stop cannot leave the\n // Worker awaiting a main-thread response forever.\n for (const [, pending] of this.pendingTokenRequests) {\n pending.reject(new Error('Centrifuge session stopped before the credential was resolved.'));\n }\n this.pendingTokenRequests.clear();\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n }\n\n /** Issue a credential request to the main thread and await the response.\n * Used as Centrifuge's `getToken` / `getChannelToken` when token bridging is\n * enabled; resolved or rejected by a matching TOKEN_RESPONSE / TOKEN_ERROR. */\n private requestToken(kind: 'token' | 'channelToken', lifecycle: number, channel?: string): Promise<string> {\n // The provider is installed on a specific Centrifuge client. An old client\n // must not be able to route a late credential request into a replacement\n // session merely because it retained the callback closure.\n if (this.lifecycle !== lifecycle) {\n return Promise.reject(new Error('Centrifuge client lifecycle has ended before the credential was requested.'));\n }\n const requestId = this.nextRequestId;\n this.nextRequestId += 1;\n return new Promise<string>((resolve, reject) => {\n this.pendingTokenRequests.set(requestId, { resolve, reject });\n this.post({\n type: CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST,\n requestId,\n kind,\n ...(channel === undefined ? {} : { channel })\n });\n });\n }\n\n private resolveToken(requestId: number, token: string): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.resolve(token);\n }\n\n private rejectToken(requestId: number, error: unknown): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.reject(error);\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: CENTRIFUGE_OUTPUT_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<string>([WORKER_STATUS.CONNECTING, WORKER_STATUS.CONNECTED]);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\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 *\n * The message `type` discriminant values are derived from the constants in\n * `utils/constants.ts` so main-thread and Worker sides reference the same\n * values and can never drift apart.\n */\nimport type { Options } from 'centrifuge';\nimport type { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE } from './utils/constants';\nimport type { WorkerStatus } from './core/types';\nimport type { SerializedWorkerError } from './utils/error-utils';\n\nexport type { SerializedWorkerError } from './utils/error-utils';\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/**\n * Async credential provider run on the main thread. Centrifuge client options\n * are structured-cloned into the Worker, so function-valued `getToken` /\n * `getChannelToken` cannot travel with the config. When a provider is set, the\n * Worker asks the main thread for each fresh token over a TOKEN_REQUEST /\n * TOKEN_RESPONSE exchange and this provider supplies it from application\n * context (async token endpoints, SecureStorage, etc.).\n */\nexport interface CentrifugeCredentialProvider {\n /** Return a fresh top-level connection token. Called on connect and refresh. */\n getToken: () => MaybePromise<string | null | undefined>;\n /** Return a channel token for a channel. Centrifuge falls back to `getToken`\n * when channel tokens are not configured by the server. */\n getChannelToken?: (channel: string) => MaybePromise<string | null | undefined>;\n}\n\n/** A value or a promise of a value. */\nexport type MaybePromise<T> = T | Promise<T>;\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 * `tokenBridge` asks the Worker to route getToken/getChannelToken back to\n * the main thread instead of holding function-valued config. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.INIT; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number; tokenBridge?: boolean }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.SUBSCRIBE; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH; topic: string; data: unknown; messageId?: string; timestamp?: number }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PING }\n /** Disconnect the client and clear all subscriptions. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.STOP }\n /** Fresh credential for an earlier TOKEN_REQUEST, resolved by requestId. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE; requestId: number; token: string }\n /** A credential request failed (provider rejected or returned no token). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR; requestId: number; error: SerializedWorkerError };\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: typeof CENTRIFUGE_OUTPUT_TYPE.STATUS; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE; topic: string; data: TData; messageId?: string; timestamp?: number }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: typeof CENTRIFUGE_OUTPUT_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: typeof CENTRIFUGE_OUTPUT_TYPE.ERROR; error: SerializedWorkerError }\n /** The Worker needs a fresh credential. The main thread resolves it via a\n * matching TOKEN_RESPONSE / TOKEN_ERROR carrying the same `requestId`. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST; requestId: number; kind: 'token' | 'channelToken'; channel?: string };\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 CentrifugeCredentialProvider,\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, DEFAULT_STORAGE_PREFIX, WORKER_BACKEND, WORKER_MODE, WORKER_STATUS } from './utils/constants';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\nimport { publicationMetadata } from './utils/metadata';\nimport { assertHeartbeatInterval, assertStructuredCloneable } from './utils/validation';\n\nexport type { CentrifugeCredentialProvider, 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 /** Async credential provider. Runs on the main thread: the Worker cannot\n * hold function-valued Centrifuge options (they are not structured-cloneable),\n * so when a provider is set the Worker asks for each fresh token over a\n * TOKEN_REQUEST / TOKEN_RESPONSE bridge and this provider supplies it. */\n credentialProvider?: CentrifugeCredentialProvider;\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 readonly diagnosticsName = 'centrifuge';\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 readonly credentialProvider: CentrifugeCredentialProvider | 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 or\n // the transport stops. Only the asynchronous credential bridge compares it:\n // a provider may settle after the Worker it was answering is gone. Worker\n // error events need no such check \u2014 stop() and onWorkerFailed() remove those\n // listeners before the generation moves on, so a superseded backend can no\n // longer reach this object at all.\n private generation = 0;\n\n get diagnosticsBackend(): string {\n return this.backend ?? 'uninitialized';\n }\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? WORKER_MODE.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 this.credentialProvider = options.credentialProvider;\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 // Runtime capability OR an injected factory counts as availability; a\n // factory-less browser must use the bundled default Workers instead of\n // silently degrading to the local session.\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined || typeof Worker !== 'undefined',\n sharedWorker: this.sharedWorkerFactory !== undefined || typeof SharedWorker !== 'undefined'\n });\n const input = this.buildInitInput(config);\n if (backend === WORKER_BACKEND.SHARED) {\n this.startSharedWorker(input);\n this.backend = WORKER_BACKEND.SHARED;\n return;\n }\n if (backend === WORKER_BACKEND.DEDICATED) {\n this.startDedicatedWorker(input);\n this.backend = WORKER_BACKEND.DEDICATED;\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = WORKER_BACKEND.LOCAL;\n }\n\n subscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.SUBSCRIBE, topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_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: CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) }, [data]);\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) });\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: CENTRIFUGE_INPUT_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: CENTRIFUGE_INPUT_TYPE.INIT,\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.credentialProvider ? { tokenBridge: 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.generation += 1;\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.generation += 1;\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 === CENTRIFUGE_OUTPUT_TYPE.STATUS) this.handlers?.onStatus(message.status);\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE) this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN) this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.ERROR) this.handlers?.onError(deserializeWorkerError(message.error));\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST) this.resolveTokenRequest(message.requestId, message.kind, message.channel);\n }\n\n /** Resolve a Worker credential request from the main thread: call the\n * credentialProvider, then post the fresh token (or a serialized failure)\n * back to the session that issued the request. */\n private resolveTokenRequest(requestId: number, kind: 'token' | 'channelToken', channel: string | undefined): void {\n // Credential providers may be asynchronous. Capture the backend identity\n // before awaiting so a stop/start that replaces the Worker cannot receive\n // a late token (or error) belonging to the superseded session. Request IDs\n // restart at 1 for a fresh CentrifugeSession, so without this guard a stale\n // reply can incorrectly satisfy the replacement session's request.\n const generation = this.generation;\n const worker = this.worker;\n const port = this.port;\n const localSession = this.localSession;\n const isCurrentBackend = () =>\n this.generation === generation &&\n this.worker === worker &&\n this.port === port &&\n this.localSession === localSession;\n const provider = this.credentialProvider;\n let value: string | null | Promise<string | null | undefined> | undefined;\n try {\n value = kind === 'channelToken' && provider?.getChannelToken\n ? provider.getChannelToken(channel ?? '')\n : provider?.getToken?.();\n } catch (error) {\n // This arm never yields, but the provider is application code and can\n // stop or replace the transport synchronously before throwing. Without\n // the check `post()` then addresses a backend that no longer exists.\n if (isCurrentBackend()) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n return;\n }\n Promise.resolve(value).then(\n token => {\n if (!isCurrentBackend()) return;\n if (typeof token !== 'string' || token.length === 0) {\n this.post({\n type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR,\n requestId,\n error: serializeError(new Error(`The credential provider returned no ${kind}.`))\n });\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE, requestId, token });\n },\n error => {\n if (!isCurrentBackend()) return;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n );\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. A superseded\n * backend cannot reach this: its listeners are removed before the transport\n * moves on, which is what keeps late Worker errors from tearing down a fresh\n * session (pinned by 'ignores error events from a superseded worker'). */\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(WORKER_STATUS.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: CENTRIFUGE_INPUT_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 this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\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 credentialProvider,\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 ...(credentialProvider ? { credentialProvider } : {})\n })\n });\n}\n\n/** Create the default dedicated Worker hosting the Centrifuge client. There is\n * no `typeof Worker` guard: `start()` asks `selectWorkerBackend` with\n * `worker: workerFactory !== undefined || typeof Worker !== 'undefined'`, that\n * function never returns a Worker backend its availability flags deny, and these\n * factories are only reached for a backend it did return. The degradation is\n * pinned where it is decided \u2014 `tests/worker-mode.test.ts` on the selector, and\n * \"falls back to the local session when the platform lacks both Worker APIs\" on\n * the transport \u2014 so a guard here could only ever re-check a fact the caller has\n * already established, and no runtime can reach it with the global missing.\n *\n * The `catch` below reads as uncovered in `pnpm test:coverage` and is not: it is\n * the CommonJS half of the module-format pair, and the report's `include` is\n * `src/**`, while the artifact that actually has no usable `import.meta.url` is\n * `dist/cjs/centrifuge.cjs`. `tests/dual-format.test.ts` asserts both sides \u2014\n * this message under CJS and a constructed Worker under ESM \u2014 so the leg is\n * pinned through the built output rather than missing. Treat that test as its\n * coverage. */\nfunction createDefaultWorker(): Worker {\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: DEFAULT_STORAGE_PREFIX,\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. Like its\n * dedicated twin it carries no `typeof SharedWorker` guard \u2014 the availability\n * flag in `start()` is what decides this is reachable \u2014 and like its twin the\n * `catch` below is pinned through the built artifacts (the CJS half of\n * `tests/dual-format.test.ts`) rather than being uncovered. */\nfunction createDefaultSharedWorker(): SharedWorker {\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: `${DEFAULT_STORAGE_PREFIX}-shared`,\n type: 'module'\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"],
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} from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, WORKER_STATUS } from './utils/constants';\nimport { publicationMetadata } from './utils/metadata';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\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/** A pending getToken/getChannelToken request awaiting a main-thread response. */\ninterface PendingTokenRequest {\n resolve: (token: string) => void;\n reject: (error: unknown) => void;\n}\n\n/** Centrifuge client options for token-bridge mode: the structured-clone-safe\n * subset plus the two function-valued hooks that Centrifuge invokes for fresh\n * credentials. Used only inside the session, never across the Worker boundary. */\ntype CentrifugeBridgedOptions = Omit<CentrifugeWorkerConfig, 'getToken' | 'getChannelToken'> & {\n getToken: () => Promise<string>;\n getChannelToken: (channel: string) => Promise<string>;\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 private tokenBridge = false;\n private nextRequestId = 1;\n // Bumped on every initialize/stop. Async client callbacks capture the value\n // from the connection that created them so a stopped client cannot emit into\n // a later session instance (including the main-thread local fallback).\n private lifecycle = 0;\n private readonly pendingTokenRequests = new Map<number, PendingTokenRequest>();\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 CENTRIFUGE_INPUT_TYPE.INIT:\n this.initialize(message.url, message.config, message.transferable === true, message.tokenBridge === true);\n return;\n case CENTRIFUGE_INPUT_TYPE.SUBSCRIBE:\n this.subscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE:\n this.unsubscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.PUBLISH:\n case CENTRIFUGE_INPUT_TYPE.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 CENTRIFUGE_INPUT_TYPE.STOP:\n this.stop();\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE:\n this.resolveToken(message.requestId, message.token);\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR:\n this.rejectToken(message.requestId, deserializeWorkerError(message.error));\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, tokenBridge: boolean): void {\n if (this.client) return;\n const lifecycle = ++this.lifecycle;\n this.transferable = transferable;\n this.tokenBridge = tokenBridge;\n const clientOptions: CentrifugeWorkerConfig | CentrifugeBridgedOptions = tokenBridge\n ? {\n ...config,\n // Route credential fetches back to the main thread, where the\n // application's credentialProvider lives (config must stay\n // structured-cloneable, so functions cannot cross the boundary).\n getToken: () => this.requestToken('token', lifecycle),\n getChannelToken: (channel: string) => this.requestToken('channelToken', lifecycle, channel)\n }\n : config;\n const client = new Centrifuge(url, clientOptions);\n this.client = client;\n client.on('state', (context: StateContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED });\n });\n client.on('disconnected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n });\n client.on('error', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\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 if (this.lifecycle !== lifecycle) return;\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 const lifecycle = this.lifecycle;\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 if (this.lifecycle !== lifecycle) return;\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n subscription.on('unsubscribed', () => {\n if (this.lifecycle === lifecycle) this.subscriptions.delete(topic);\n });\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 const lifecycle = this.lifecycle;\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 => {\n if (this.lifecycle === lifecycle) this.postError(error);\n });\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 *\n * That drop is uncovered and dominated: the connection-level caller tests the\n * same emptiness before calling, and the subscription-level caller reads the\n * topic out of a key this session only ever populates from a SUBSCRIBE frame.\n * Measured \u2014 deleting this line leaves all 37 test files green, and the frame\n * it withholds could not be delivered anyway, because `topicMatchesPattern`\n * answers false for an empty topic against both `*` and `prefix.*` (so no\n * wildcard handler sees it either) and `subscribe('')` throws. Kept as the\n * boundary statement, not counted as a closed leg. */\n private postPublication(topic: string, data: unknown): void {\n if (!topic) return;\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: CENTRIFUGE_OUTPUT_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: CENTRIFUGE_OUTPUT_TYPE.MESSAGE,\n topic: publication.topic,\n data: publication.data,\n ...publicationMetadata(publication.messageId, publication.timestamp)\n });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n // Invalidate callbacks from the connection being stopped before disconnect\n // so its synchronous or queued events cannot leak into a later session.\n this.lifecycle += 1;\n // Settle every in-flight credential request so a stop cannot leave the\n // Worker awaiting a main-thread response forever.\n for (const [, pending] of this.pendingTokenRequests) {\n pending.reject(new Error('Centrifuge session stopped before the credential was resolved.'));\n }\n this.pendingTokenRequests.clear();\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n }\n\n /** Issue a credential request to the main thread and await the response.\n * Used as Centrifuge's `getToken` / `getChannelToken` when token bridging is\n * enabled; resolved or rejected by a matching TOKEN_RESPONSE / TOKEN_ERROR. */\n private requestToken(kind: 'token' | 'channelToken', lifecycle: number, channel?: string): Promise<string> {\n // The provider is installed on a specific Centrifuge client. An old client\n // must not be able to route a late credential request into a replacement\n // session merely because it retained the callback closure.\n if (this.lifecycle !== lifecycle) {\n return Promise.reject(new Error('Centrifuge client lifecycle has ended before the credential was requested.'));\n }\n const requestId = this.nextRequestId;\n this.nextRequestId += 1;\n return new Promise<string>((resolve, reject) => {\n this.pendingTokenRequests.set(requestId, { resolve, reject });\n this.post({\n type: CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST,\n requestId,\n kind,\n ...(channel === undefined ? {} : { channel })\n });\n });\n }\n\n private resolveToken(requestId: number, token: string): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.resolve(token);\n }\n\n private rejectToken(requestId: number, error: unknown): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.reject(error);\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: CENTRIFUGE_OUTPUT_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<string>([WORKER_STATUS.CONNECTING, WORKER_STATUS.CONNECTED]);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\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 *\n * The message `type` discriminant values are derived from the constants in\n * `utils/constants.ts` so main-thread and Worker sides reference the same\n * values and can never drift apart.\n */\nimport type { Options } from 'centrifuge';\nimport type { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE } from './utils/constants';\nimport type { WorkerStatus } from './core/types';\nimport type { SerializedWorkerError } from './utils/error-utils';\n\nexport type { SerializedWorkerError } from './utils/error-utils';\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/**\n * Async credential provider run on the main thread. Centrifuge client options\n * are structured-cloned into the Worker, so function-valued `getToken` /\n * `getChannelToken` cannot travel with the config. When a provider is set, the\n * Worker asks the main thread for each fresh token over a TOKEN_REQUEST /\n * TOKEN_RESPONSE exchange and this provider supplies it from application\n * context (async token endpoints, SecureStorage, etc.).\n */\nexport interface CentrifugeCredentialProvider {\n /** Return a fresh top-level connection token. Called on connect and refresh. */\n getToken: () => MaybePromise<string | null | undefined>;\n /** Return a channel token for a channel. Centrifuge falls back to `getToken`\n * when channel tokens are not configured by the server. */\n getChannelToken?: (channel: string) => MaybePromise<string | null | undefined>;\n}\n\n/** A value or a promise of a value. */\nexport type MaybePromise<T> = T | Promise<T>;\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 * `tokenBridge` asks the Worker to route getToken/getChannelToken back to\n * the main thread instead of holding function-valued config. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.INIT; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number; tokenBridge?: boolean }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.SUBSCRIBE; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH; topic: string; data: unknown; messageId?: string; timestamp?: number }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PING }\n /** Disconnect the client and clear all subscriptions. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.STOP }\n /** Fresh credential for an earlier TOKEN_REQUEST, resolved by requestId. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE; requestId: number; token: string }\n /** A credential request failed (provider rejected or returned no token). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR; requestId: number; error: SerializedWorkerError };\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: typeof CENTRIFUGE_OUTPUT_TYPE.STATUS; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE; topic: string; data: TData; messageId?: string; timestamp?: number }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: typeof CENTRIFUGE_OUTPUT_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: typeof CENTRIFUGE_OUTPUT_TYPE.ERROR; error: SerializedWorkerError }\n /** The Worker needs a fresh credential. The main thread resolves it via a\n * matching TOKEN_RESPONSE / TOKEN_ERROR carrying the same `requestId`. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST; requestId: number; kind: 'token' | 'channelToken'; channel?: string };\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 CentrifugeCredentialProvider,\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, DEFAULT_STORAGE_PREFIX, WORKER_BACKEND, WORKER_MODE, WORKER_STATUS } from './utils/constants';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\nimport { publicationMetadata } from './utils/metadata';\nimport { assertHeartbeatInterval, assertStructuredCloneable } from './utils/validation';\n\nexport type { CentrifugeCredentialProvider, 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 /** Async credential provider. Runs on the main thread: the Worker cannot\n * hold function-valued Centrifuge options (they are not structured-cloneable),\n * so when a provider is set the Worker asks for each fresh token over a\n * TOKEN_REQUEST / TOKEN_RESPONSE bridge and this provider supplies it. */\n credentialProvider?: CentrifugeCredentialProvider;\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 readonly diagnosticsName = 'centrifuge';\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 readonly credentialProvider: CentrifugeCredentialProvider | 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 or\n // the transport stops. Only the asynchronous credential bridge compares it:\n // a provider may settle after the Worker it was answering is gone. Worker\n // error events need no such check \u2014 stop() and onWorkerFailed() remove those\n // listeners before the generation moves on, so a superseded backend can no\n // longer reach this object at all.\n private generation = 0;\n\n get diagnosticsBackend(): string {\n return this.backend ?? 'uninitialized';\n }\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? WORKER_MODE.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 this.credentialProvider = options.credentialProvider;\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 // Runtime capability OR an injected factory counts as availability; a\n // factory-less browser must use the bundled default Workers instead of\n // silently degrading to the local session.\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined || typeof Worker !== 'undefined',\n sharedWorker: this.sharedWorkerFactory !== undefined || typeof SharedWorker !== 'undefined'\n });\n const input = this.buildInitInput(config);\n if (backend === WORKER_BACKEND.SHARED) {\n this.startSharedWorker(input);\n this.backend = WORKER_BACKEND.SHARED;\n return;\n }\n if (backend === WORKER_BACKEND.DEDICATED) {\n this.startDedicatedWorker(input);\n this.backend = WORKER_BACKEND.DEDICATED;\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = WORKER_BACKEND.LOCAL;\n }\n\n subscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.SUBSCRIBE, topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_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: CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) }, [data]);\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) });\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: CENTRIFUGE_INPUT_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: CENTRIFUGE_INPUT_TYPE.INIT,\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.credentialProvider ? { tokenBridge: 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.generation += 1;\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.generation += 1;\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 === CENTRIFUGE_OUTPUT_TYPE.STATUS) this.handlers?.onStatus(message.status);\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE) this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN) this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.ERROR) this.handlers?.onError(deserializeWorkerError(message.error));\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST) this.resolveTokenRequest(message.requestId, message.kind, message.channel);\n }\n\n /** Resolve a Worker credential request from the main thread: call the\n * credentialProvider, then post the fresh token (or a serialized failure)\n * back to the session that issued the request. */\n private resolveTokenRequest(requestId: number, kind: 'token' | 'channelToken', channel: string | undefined): void {\n // Credential providers may be asynchronous. Capture the backend identity\n // before awaiting so a stop/start that replaces the Worker cannot receive\n // a late token (or error) belonging to the superseded session. Request IDs\n // restart at 1 for a fresh CentrifugeSession, so without this guard a stale\n // reply can incorrectly satisfy the replacement session's request.\n const generation = this.generation;\n const worker = this.worker;\n const port = this.port;\n const localSession = this.localSession;\n const isCurrentBackend = () =>\n this.generation === generation &&\n this.worker === worker &&\n this.port === port &&\n this.localSession === localSession;\n const provider = this.credentialProvider;\n let value: string | null | Promise<string | null | undefined> | undefined;\n try {\n value = kind === 'channelToken' && provider?.getChannelToken\n ? provider.getChannelToken(channel ?? '')\n : provider?.getToken?.();\n } catch (error) {\n // This arm never yields, but the provider is application code and can\n // stop or replace the transport synchronously before throwing. Without\n // the check `post()` then addresses a backend that no longer exists.\n if (isCurrentBackend()) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n return;\n }\n Promise.resolve(value).then(\n token => {\n if (!isCurrentBackend()) return;\n if (typeof token !== 'string' || token.length === 0) {\n this.post({\n type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR,\n requestId,\n error: serializeError(new Error(`The credential provider returned no ${kind}.`))\n });\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE, requestId, token });\n },\n error => {\n if (!isCurrentBackend()) return;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n );\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. A superseded\n * backend cannot reach this: its listeners are removed before the transport\n * moves on, which is what keeps late Worker errors from tearing down a fresh\n * session (pinned by 'ignores error events from a superseded worker'). */\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(WORKER_STATUS.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 // Uncovered and dominated. The only caller is `startSharedWorker()`, which\n // `start()` reaches past its own `if (this.backend) return` guard, and\n // `backend` becomes null only through `stop()` and `onWorkerFailed()` \u2014 both\n // of which call `clearHeartbeat()` first. Measured: deleting this line leaves\n // all 37 test files green. It stays because `clearHeartbeat()` can only ever\n // drop the one handle it holds, so a double-arm would leak an interval that\n // nothing can clear afterwards \u2014 doubled PINGs at the SharedWorker for the\n // rest of the page, which reads to the reaper as a tab that never goes away.\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: CENTRIFUGE_INPUT_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 this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\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 credentialProvider,\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 ...(credentialProvider ? { credentialProvider } : {})\n })\n });\n}\n\n/** Create the default dedicated Worker hosting the Centrifuge client. There is\n * no `typeof Worker` guard: `start()` asks `selectWorkerBackend` with\n * `worker: workerFactory !== undefined || typeof Worker !== 'undefined'`, that\n * function never returns a Worker backend its availability flags deny, and these\n * factories are only reached for a backend it did return. The degradation is\n * pinned where it is decided \u2014 `tests/worker-mode.test.ts` on the selector, and\n * \"falls back to the local session when the platform lacks both Worker APIs\" on\n * the transport \u2014 so a guard here could only ever re-check a fact the caller has\n * already established, and no runtime can reach it with the global missing.\n *\n * The `catch` below reads as uncovered in `pnpm test:coverage` and is not: it is\n * the CommonJS half of the module-format pair, and the report's `include` is\n * `src/**`, while the artifact that actually has no usable `import.meta.url` is\n * `dist/cjs/centrifuge.cjs`. `tests/dual-format.test.ts` asserts both sides \u2014\n * this message under CJS and a constructed Worker under ESM \u2014 so the leg is\n * pinned through the built output rather than missing. Treat that test as its\n * coverage. */\nfunction createDefaultWorker(): Worker {\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: DEFAULT_STORAGE_PREFIX,\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. Like its\n * dedicated twin it carries no `typeof SharedWorker` guard \u2014 the availability\n * flag in `start()` is what decides this is reachable \u2014 and like its twin the\n * `catch` below is pinned through the built artifacts (the CJS half of\n * `tests/dual-format.test.ts`) rather than being uncovered. */\nfunction createDefaultSharedWorker(): SharedWorker {\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: `${DEFAULT_STORAGE_PREFIX}-shared`,\n type: 'module'\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"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5244,7 +5244,16 @@ var CentrifugeSession = class {
5244
5244
  /** Forward a publication to the transport. Binary payloads take the
5245
5245
  * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
5246
5246
  * else is structured-cloned via `MESSAGE`. An empty topic means the
5247
- * publication carried no channel info and is silently dropped. */
5247
+ * publication carried no channel info and is silently dropped.
5248
+ *
5249
+ * That drop is uncovered and dominated: the connection-level caller tests the
5250
+ * same emptiness before calling, and the subscription-level caller reads the
5251
+ * topic out of a key this session only ever populates from a SUBSCRIBE frame.
5252
+ * Measured — deleting this line leaves all 37 test files green, and the frame
5253
+ * it withholds could not be delivered anyway, because `topicMatchesPattern`
5254
+ * answers false for an empty topic against both `*` and `prefix.*` (so no
5255
+ * wildcard handler sees it either) and `subscribe('')` throws. Kept as the
5256
+ * boundary statement, not counted as a closed leg. */
5248
5257
  postPublication(topic, data) {
5249
5258
  if (!topic) return;
5250
5259
  if (this.transferable && data instanceof ArrayBuffer) {