cross-tab-worker-databus 0.21.19 → 0.21.21

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,3 +1,41 @@
1
+ ## [0.21.21] - 2026-09-24
2
+
3
+ One branch arm closed by a call that had never been made, three shipped comment claims retracted against the code they describe, and a repository rule about the difference between a guard nobody can pin and a guard nobody has pinned.
4
+
5
+ ### Tests
6
+
7
+ - **`queueStartAfterStop()`'s no-gate fallback had never run.** `this.stopPromise ?? Promise.resolve()` is reached with `stopPromise` null only by a *failed initial open*, whose teardown belongs to `pendingStop` instead — and nothing reachable through a browser re-enters `start()` inside that window, because the only synchronous application-code seam there is the caller's own `ClusterEnvironment` port. The new case drives that seam (a storage write that calls back into the bus) and closes one of `src/`'s 48 zero-count branch arms; `data-bus.ts` goes 12 → 11, and aggregate branches 97.55 → 97.60. What gives the test teeth is not the expression it was written for: routing `start()` on the gate instead of the flag (`if (this.stopping)` → `if (this.stopPromise)`) reddens it, while the fallback operand itself is provably consequence-free, because `start()` chains the reopen behind `pendingStop` regardless.
8
+ - **A second test was written, measured, and deleted.** A case that queued a restart behind a transport whose `stop()` was held open passed under all four single-leg mutations tried, so it pinned nothing: a bounded number of awaited microtasks cannot observe a wait whose wakeup needs the timer queue. Its zero kill rate is recorded at the site instead, next to the measurement of what the guard actually does — deleting the read turns one deferral into a microtask busy-wait that starves its own wakeup, which the suite reports as an out-of-memory worker abort in the lifecycle fuzz rather than as a failed assertion.
9
+
10
+ ### Documentation
11
+
12
+ - **Three comment claims re-read against the code, and all three were wrong.** `subscribe()`'s note described the `?? this.currentRecord` fallbacks as "the identical pair" where the file holds three (the third — the stale-handoff re-election — carried no site text at all, so it had been quietly re-hunted by every coverage pass); `reopenTransport()`'s note quoted a mutation result ("fails exactly one test, with the guard present or deleted") that reproduces under none of four experiments now run in its place; and of the eleven `:NNN` citations pointing into this repository's own files, **six were wrong and one imprecise**, including two that had drifted *inside another comment*. Every one now names a symbol or a condition instead of a position, and the numbers that were measured — three, two and seven `tsc` rejections for the three fallbacks, 194/194 and 51-of-194 for the absorb — are what the text carries.
13
+ - **The verification that a prose sweep touched no code.** `npx esbuild --loader=ts` over each changed file at both revisions, then `cmp`: identical emitted JavaScript for all five files. That is a stronger statement than counting which diff lines begin with `//`, and it costs one loop.
14
+ - **Two `AGENTS.md` rules.** A guard whose deletion changes *when* work happens rather than *what* it produces cannot be pinned by a bounded assertion — name the fuzz as the witness at the site instead of adding a test that looks like coverage. And a line-number citation in a comment decays on every edit above it, silently, so cite the symbol; keep a number only where no name exists, and pin it to a dependency version there.
15
+
16
+ ### Compatibility
17
+
18
+ Mixed-version peers are unaffected: no frame, default, storage key, export or behavior moved, and the only consumer-visible difference is comment text, which reaches them through both the declarations and the bundles. `verify:compat`, `verify:types` and `verify:pack` pass against `v0.21.20`.
19
+
20
+ ## [0.21.20] - 2026-09-24
21
+
22
+ Six uncovered branch arms closed by calls no test had ever made, one shipped sentence corrected, and the coverage ledger given a third tier — the kind a compiler, not a test, owns.
23
+
24
+ ### Tests
25
+
26
+ - **The string overload of `publish()` had never been called.** `WorkerClusterRuntime.publish` declares `(topic, data, messageId: string)` alongside the metadata form, and the normalization at the top of the method is the only thing that turns it into the object every downstream reader expects. `CrossTabDataBus.publish` always hands over an object, so nothing reachable *through the bus* can take that arm — and no test had called the runtime directly with a string, even though `docs/api.md` points callers who coordinate straight at the cluster there.
27
+ - **A single-item `publishBatch` whose item carries partial or no metadata.** Every existing case supplied both `messageId` and `timestamp`, so the `… || …` test, its `: undefined` fallback, and both conditional spreads had each only ever been evaluated one way. The absent-metadata case is the one with a consequence: `sendControl` calls the handler with **three** arguments when the metadata is falsy and five otherwise, and that arity is what separates "arrived without an id" from "arrived with an id of `undefined`". The two partial shapes are now covered and have no assertion of their own — recorded as such, because the receiving side re-normalizes with `metadata?.x === undefined ? {} : …`, so an omitted key and an `undefined` value produce the same wire frame.
28
+ - **`clusterKey: ''` is a namespace alias, and it is now pinned on both sides.** The namespace is `createOpaqueKey(options.clusterKey || '__default__')`, so an empty key hashes the literal `'__default__'` rather than naming its own cluster. Two runtimes on one storage and channel, keyed `''` and `'__default__'`, now have to leave exactly one owner for a shared topic, and a third runtime on another key owns its own copy — which is the half that fails if the fallback ever widens past the empty string.
29
+
30
+ ### Documentation
31
+
32
+ - **`clusterKey`'s shipped JSDoc said different keys "operate in isolation" without naming that pair** — an absolute about behavior, reaching consumers through both the declarations and the bundles. The exception is now stated where the option is declared, and in `AGENTS.md`.
33
+ - **A third tier in the uncovered-arm ledger, in `AGENTS.md`.** With `noUncheckedIndexedAccess` on, an index read is `T | undefined` whether or not a hole is reachable, so a `?? fallback` that satisfies an assignment can neither run nor be deleted; `exactOptionalPropertyTypes` produces the same tier from the other side, where the always-present form of a conditional spread is a `TS2769` while the whole suite stays green. Both were established by **deleting the fallback and running `tsc`** per arm, which is the procedure the rule prescribes: in the route-write block it separated five compiler-held legs from one (`(previous?.generation ?? 0)`) that compiles bare and is therefore dominated by a filter three lines above rather than by its own expression.
34
+
35
+ ### Compatibility
36
+
37
+ Mixed-version peers are unaffected: no frame, default, storage key, or export moved, and no behavior changed — the empty-`clusterKey` alias is documented as it already was, because moving `''` into its own namespace would silently relocate storage for anyone passing an empty key today. `verify:compat`, `verify:types` and `verify:pack` pass against `v0.21.19`.
38
+
1
39
  ## [0.21.19] - 2026-09-24
2
40
 
3
41
  One behavior fix in the trace reporter, two test pins that each close a leg no test named, and the last unclassified uncovered-branch legs outside the two large modules written up at their sites.
@@ -45,7 +45,8 @@ export declare class CentrifugeSession<TData = unknown> {
45
45
  * Measured — deleting this line leaves all 37 test files green, and the frame
46
46
  * it withholds has nowhere to land anyway: the only handler registry is the
47
47
  * DataBus's `topicHandlers`, and every write to it sits behind
48
- * `assertPublicTopic` (`data-bus.ts:929`, `:1009`, `:1033`), so no `''` key can
48
+ * `assertPublicTopic` (at the head of `data-bus.ts`'s `subscribe()`, `publish()`
49
+ * and `publishBatch()`), so no `''` key can
49
50
  * exist — while `topicMatchesPattern` answers false for an empty topic against
50
51
  * both `*` and `prefix.*`, so a wildcard handler cannot see it either. Neither
51
52
  * of those is a reason to delete this guard: it is the only one *at this layer*,
@@ -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;AAuBD;;;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;IAsDjB;;gFAE4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAmBf;;;;;;;;;;;;;;;;;;;0DAmBsD;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
+ {"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;AAuBD;;;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;IAyDjB;;gFAE4E;IAC5E,OAAO,CAAC,WAAW;IAUnB,mDAAmD;IACnD,OAAO,CAAC,OAAO;IAmBf;;;;;;;;;;;;;;;;;;;;0DAoBsD;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"}
@@ -7,7 +7,7 @@ import {
7
7
  publicationMetadata,
8
8
  selectWorkerBackend,
9
9
  serializeError
10
- } from "./chunk-Q2INCAFO.js";
10
+ } from "./chunk-37NTHK22.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -173,7 +173,8 @@ var CentrifugeSession = class {
173
173
  * Measured — deleting this line leaves all 37 test files green, and the frame
174
174
  * it withholds has nowhere to land anyway: the only handler registry is the
175
175
  * DataBus's `topicHandlers`, and every write to it sits behind
176
- * `assertPublicTopic` (`data-bus.ts:929`, `:1009`, `:1033`), so no `''` key can
176
+ * `assertPublicTopic` (at the head of `data-bus.ts`'s `subscribe()`, `publish()`
177
+ * and `publishBatch()`), so no `''` key can
177
178
  * exist — while `topicMatchesPattern` answers false for an empty topic against
178
179
  * both `*` and `prefix.*`, so a wildcard handler cannot see it either. Neither
179
180
  * of those is a reason to delete this guard: it is the only one *at this layer*,
@@ -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 function-valued credential hooks this session installs.\n * Against the pinned SDK (5.7.4) `getToken` is the only one that is a real\n * client option (`types.d.ts:126`, and the same hook serves renewal);\n * `getChannelToken` appears nowhere in the package's types or build, and\n * subscriptions are created with `newSubscription(topic)` and no per-\n * subscription options \u2014 so that hook is never invoked, and the `'channelToken'`\n * request kind is produced only by the test fake. It stays because the Worker\n * protocol carries the kind: an SDK that later names it would need no change on\n * this side. 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 // Scoped to the three events this method wires, so the client's own `state`\n // wiring is untouched \u2014 but `error` is NOT internal-listener-free: the\n // Subscription constructor installs its own no-op `error` listener precisely\n // \"to avoid unhandled exception in EventEmitter for non-set error handler\"\n // (centrifuge 5.7.4, build/index.js:762), and the bundled emitter throws on\n // an `error` emit with no listener (`:162`). This call removes that guard,\n // and the `.on('error', \u2026)` two statements below replaces it for as long as\n // this session holds the subscription. `unsubscribe()` removes both and adds\n // nothing back, which is settled rather than open. All twelve `emit('error')`\n // sites in `BaseSubscription` (`:667-2674`) sit behind a\n // `_isSubscribing()`/`_isSubscribed()` test except one, the\n // `badConfiguration` emit in `_getSubscriptionToken`; and after\n // `_setUnsubscribed` has moved `state` to `Unsubscribed` no path re-enters it\n // \u2014 the resubscribe timer's callback re-tests `_isSubscribing()` before it\n // does anything, the client's reconnect pass (`_sendSubscribeCommands`)\n // drives only subscriptions whose state is already `Subscribing`, and\n // `_refresh()`, the one entry into `_getSubscriptionToken` with no state test\n // of its own, is reachable only through `_refreshTimeout`, which\n // `_setUnsubscribed` clears via `_clearSubscribedState()`. So a throw would\n // need the listeners gone while the object is still live, and\n // `unsubscribe()` does not leave it live. `MapSubscription` and\n // `SharedPollSubscription` have their own emit sites but never carry this\n // session's listeners \u2014 `newSubscription` constructs a plain `Subscription`.\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 has nowhere to land anyway: the only handler registry is the\n * DataBus's `topicHandlers`, and every write to it sits behind\n * `assertPublicTopic` (`data-bus.ts:929`, `:1009`, `:1033`), so no `''` key can\n * exist \u2014 while `topicMatchesPattern` answers false for an empty topic against\n * both `*` and `prefix.*`, so a wildcard handler cannot see it either. Neither\n * of those is a reason to delete this guard: it is the only one *at this layer*,\n * because neither the session nor the transport calls `assertPublicTopic` \u2014 a\n * SUBSCRIBE frame naming `''` creates a real Centrifuge subscription\n * (`newSubscription` does not validate a channel), so the undeliverable\n * publication would otherwise be posted back to the main thread. 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// These options reference browser APIs (WebSocket, EventSource, fetch, \u2026) that\n// do not exist inside a Worker \u2014 the worker imports its own WebSocket. Nothing\n// strips them at runtime: `buildInitInput` forwards `config.options` verbatim,\n// so this exclusion is compile-time only. A value forced through with a cast is\n// caught by the runtime gate that does exist, `assertStructuredCloneable` on the\n// same options, which throws a TypeError from the transport before any backend\n// is created \u2014 a function-valued option would otherwise die at the structured-\n// clone boundary with a DataCloneError instead.\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 /** The SharedWorker's reaper is about to close this port, so its session and\n * WebSocket are going away with it. Sent as the last message on the port: a tab\n * whose heartbeat was merely starved (a long task, background throttling) is\n * alive and can rebuild, but only if it learns that its backend is gone. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.SESSION_REAPED };\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends. Which one is tried first depends on `workerMode`:\n * - `dedicated` (the default): Dedicated Worker \u2192 SharedWorker \u2192 in-process\n * - `shared` / `auto`: SharedWorker \u2192 Dedicated Worker \u2192 in-process\n *\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. On the two Worker backends\n * the Worker is isolated from the main thread, so WebSocket lifecycle, token\n * refresh, and binary data handling stay off the UI thread. On the `local`\n * fallback there is no Worker: the same work runs on the main thread, which is\n * the point of the fallback (it keeps the transport usable where no Worker API\n * exists) rather than a property it preserves.\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 when a *Worker* backend is created\n // (`startDedicatedWorker`, `startSharedWorker`) and when the transport stops.\n // The in-process `localSession` creation does not bump it, which is why\n // `isCurrentBackend()` compares worker/port/localSession identity as well.\n // Only the asynchronous credential bridge reads the counter: a provider may\n // settle after the backend it was answering is gone. Worker error events need\n // no such check, but not for the reason this note used to give \u2014 the top of\n // `stop()` bumps the counter and only then removes the listeners, so the\n // generation moves on *before* the removal, not after, while `onWorkerFailed()`\n // removes them without touching the counter at all. The containment in both\n // cases is that they happen in one synchronous task, together with\n // `handlers = null`, and no message or error event can be delivered to this\n // object mid-task.\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 // The SharedWorker is closing this port because our PING heartbeat went silent\n // for longer than its session timeout \u2014 a long synchronous task, or the\n // throttling a backgrounded tab gets. Nothing else can tell us: a MessagePort\n // has no close event, and posting into the closed end succeeds and is\n // discarded. Treating it as any other backend loss is what makes the bus report\n // it and lets the existing cooldown recovery rebuild the port; measured in a\n // real browser before this existed, a 34 s stall left a shared-mode tab owning\n // its topic with zero server-side subscribers while reporting\n // `state: healthy` / `status: connected` / `transportReady: true`.\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.SESSION_REAPED) {\n this.onWorkerFailed(\"The SharedWorker ended this tab's session after its port heartbeat went silent.\");\n }\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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",
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 function-valued credential hooks this session installs.\n * Against the pinned SDK (5.7.4) `getToken` is the only one that is a real\n * client option (`types.d.ts:126`, and the same hook serves renewal);\n * `getChannelToken` appears nowhere in the package's types or build, and\n * subscriptions are created with `newSubscription(topic)` and no per-\n * subscription options \u2014 so that hook is never invoked, and the `'channelToken'`\n * request kind is produced only by the test fake. It stays because the Worker\n * protocol carries the kind: an SDK that later names it would need no change on\n * this side. 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 // Scoped to the three events this method wires, so the client's own `state`\n // wiring is untouched \u2014 but `error` is NOT internal-listener-free: the\n // Subscription constructor installs its own no-op `error` listener precisely\n // \"to avoid unhandled exception in EventEmitter for non-set error handler\"\n // (centrifuge 5.7.4, build/index.js:762), and the bundled emitter throws on\n // an `error` emit with no listener (`:162`). This call removes that guard,\n // and the `subscription.on('error', \u2026)` re-installation below puts one back for\n // as long as this session holds the subscription \u2014 four statements later (the\n // `unsubscribed` removal, the `subscriptions` set, the `publication` wiring), all\n // of them inside this same synchronous task, so no emit can land in between.\n // `unsubscribe()` removes both \u2014 this handler and the constructor's no-op \u2014 and\n // adds nothing back, which is settled rather than open. All twelve `emit('error')`\n // sites in `BaseSubscription` (`:667-2674`) sit behind a\n // `_isSubscribing()`/`_isSubscribed()` test except one, the\n // `badConfiguration` emit in `_getSubscriptionToken`; and after\n // `_setUnsubscribed` has moved `state` to `Unsubscribed` no path re-enters it\n // \u2014 the resubscribe timer's callback re-tests `_isSubscribing()` before it\n // does anything, the client's reconnect pass (`_sendSubscribeCommands`)\n // drives only subscriptions whose state is already `Subscribing`, and\n // `_refresh()`, the one entry into `_getSubscriptionToken` with no state test\n // of its own, is reachable only through `_refreshTimeout`, which\n // `_setUnsubscribed` clears via `_clearSubscribedState()`. So a throw would\n // need the listeners gone while the object is still live, and\n // `unsubscribe()` does not leave it live. `MapSubscription` and\n // `SharedPollSubscription` have their own emit sites but never carry this\n // session's listeners \u2014 `newSubscription` constructs a plain `Subscription`.\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 has nowhere to land anyway: the only handler registry is the\n * DataBus's `topicHandlers`, and every write to it sits behind\n * `assertPublicTopic` (at the head of `data-bus.ts`'s `subscribe()`, `publish()`\n * and `publishBatch()`), so no `''` key can\n * exist \u2014 while `topicMatchesPattern` answers false for an empty topic against\n * both `*` and `prefix.*`, so a wildcard handler cannot see it either. Neither\n * of those is a reason to delete this guard: it is the only one *at this layer*,\n * because neither the session nor the transport calls `assertPublicTopic` \u2014 a\n * SUBSCRIBE frame naming `''` creates a real Centrifuge subscription\n * (`newSubscription` does not validate a channel), so the undeliverable\n * publication would otherwise be posted back to the main thread. 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// These options reference browser APIs (WebSocket, EventSource, fetch, \u2026) that\n// do not exist inside a Worker \u2014 the worker imports its own WebSocket. Nothing\n// strips them at runtime: `buildInitInput` forwards `config.options` verbatim,\n// so this exclusion is compile-time only. A value forced through with a cast is\n// caught by the runtime gate that does exist, `assertStructuredCloneable` on the\n// same options, which throws a TypeError from the transport before any backend\n// is created \u2014 a function-valued option would otherwise die at the structured-\n// clone boundary with a DataCloneError instead.\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 /** The SharedWorker's reaper is about to close this port, so its session and\n * WebSocket are going away with it. Sent as the last message on the port: a tab\n * whose heartbeat was merely starved (a long task, background throttling) is\n * alive and can rebuild, but only if it learns that its backend is gone. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.SESSION_REAPED };\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends. Which one is tried first depends on `workerMode`:\n * - `dedicated` (the default): Dedicated Worker \u2192 SharedWorker \u2192 in-process\n * - `shared` / `auto`: SharedWorker \u2192 Dedicated Worker \u2192 in-process\n *\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. On the two Worker backends\n * the Worker is isolated from the main thread, so WebSocket lifecycle, token\n * refresh, and binary data handling stay off the UI thread. On the `local`\n * fallback there is no Worker: the same work runs on the main thread, which is\n * the point of the fallback (it keeps the transport usable where no Worker API\n * exists) rather than a property it preserves.\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 when a *Worker* backend is created\n // (`startDedicatedWorker`, `startSharedWorker`) and when the transport stops.\n // The in-process `localSession` creation does not bump it, which is why\n // `isCurrentBackend()` compares worker/port/localSession identity as well.\n // Only the asynchronous credential bridge reads the counter: a provider may\n // settle after the backend it was answering is gone. Worker error events need\n // no such check, but not for the reason this note used to give \u2014 the top of\n // `stop()` bumps the counter and only then removes the listeners, so the\n // generation moves on *before* the removal, not after, while `onWorkerFailed()`\n // removes them without touching the counter at all. The containment in both\n // cases is that they happen in one synchronous task, together with\n // `handlers = null`, and no message or error event can be delivered to this\n // object mid-task.\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 // The SharedWorker is closing this port because our PING heartbeat went silent\n // for longer than its session timeout \u2014 a long synchronous task, or the\n // throttling a backgrounded tab gets. Nothing else can tell us: a MessagePort\n // has no close event, and posting into the closed end succeeds and is\n // discarded. Treating it as any other backend loss is what makes the bus report\n // it and lets the existing cooldown recovery rebuild the port; measured in a\n // real browser before this existed, a 34 s stall left a shared-mode tab owning\n // its topic with zero server-side subscribers while reporting\n // `state: healthy` / `status: connected` / `transportReady: true`.\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.SESSION_REAPED) {\n this.onWorkerFailed(\"The SharedWorker ended this tab's session after its port heartbeat went silent.\");\n }\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
  }
@@ -5253,7 +5253,8 @@ var CentrifugeSession = class {
5253
5253
  * Measured — deleting this line leaves all 37 test files green, and the frame
5254
5254
  * it withholds has nowhere to land anyway: the only handler registry is the
5255
5255
  * DataBus's `topicHandlers`, and every write to it sits behind
5256
- * `assertPublicTopic` (`data-bus.ts:929`, `:1009`, `:1033`), so no `''` key can
5256
+ * `assertPublicTopic` (at the head of `data-bus.ts`'s `subscribe()`, `publish()`
5257
+ * and `publishBatch()`), so no `''` key can
5257
5258
  * exist — while `topicMatchesPattern` answers false for an empty topic against
5258
5259
  * both `*` and `prefix.*`, so a wildcard handler cannot see it either. Neither
5259
5260
  * of those is a reason to delete this guard: it is the only one *at this layer*,