cross-tab-worker-databus 0.21.33 → 0.21.34

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,25 @@
1
+ ## [0.21.34] - 2026-09-25
2
+
3
+ A release gate that can now refuse to tag, one library behavior that got the test it never had, and three shipped sentences about caps that each described a protection the code does not provide. No public API, export, option, default, frame or storage key changed.
4
+
5
+ ### Added
6
+
7
+ - **The release record is checked against the registry before a tag moves.** `scripts/verify-release-version.mjs` now reads the packument's `time` map beside the version/tag/CHANGELOG checks it already made, and fails the tag when the CHANGELOG names a release at or above `firstGatedRelease = 0.20.85` that npm never recorded. The release in flight is exempt by necessity (the step runs before the publish), a duplicated section reports its hole once, and a failed registry read **warns and skips** rather than redding the release — the deliberate half, because this repository has several gates that have already turned a good release red over their own plumbing. Why a floor rather than the whole file: 40 of the CHANGELOG's 164 `## [x.y.z]` sections name versions with no registry `time` entry at all — 41 counting the release in flight, which is exactly why the gate exempts it — and they are `0.9.0`, each minor's first patch from `0.12.0` to `0.18.0`, **all ten** `0.19.x`, `0.20.0`–`0.20.5`, `0.20.54`, `0.20.55`, `0.20.69` and `0.20.72`–`0.20.84`, which is 1 + 7 + 10 + 6 + 2 + 1 + 13 = 40, every one of them below `0.20.85`, while from there on the record is in lockstep: all 47 sections from `0.20.85` through `0.21.33` are in the registry's `time` map (that count is this tag's own reading — take it from the line the script prints, which rises by one with every release). So the gate is strict about the era where strictness is the norm and cannot fire on the archaeology before it. Re-derive with `npm view cross-tab-worker-databus time --json`; the script prints its own count rather than letting a reader trust this one.
8
+
9
+ ### Tests
10
+
11
+ - **`rememberTopic()`'s eviction loop skips two candidates, and only one of them could decide anything a test saw.** Deleting `candidate === topicKey ||` left all 944 tests green, so the term was live and unpinned rather than dominated. The reachable state is narrow: a fresh `Map.set` lands at the **back** of the insertion-ordered Map, so the front-scan arrives at the key being remembered only once every older entry is owned — and there the mutant discards the very mapping the call exists to install. `tests/cluster.test.ts`'s new case builds exactly that (500 adopted peer `CONTROL/SUBSCRIBE` frames, which are owned by definition, then one `CONTROL/PUBLISH` for a new topic); with the term removed it fails at `expected [ 'owned.0', 'owned.1', …(498) ] to include 'evict.extra'`, and the pre-existing `never evicts a topic the worker still owns from knownTopics` is what attributes the *other* skip, so both terms now have their own kill set. Two candidate designs were tried and discarded before this one, and the reason is recorded in the test's comment: a peer SUBSCRIBE frame cannot produce an unowned entry, and re-remembering a key already present cannot overflow the cap.
12
+
13
+ ### Documentation
14
+
15
+ - **The `knownTopics` cap does not bound an adoption flood — in either language, and in a table cell nobody re-read.** `docs/architecture.md` claimed `MAX_KNOWN_TOPICS = 500` "prevents a misbehaving or malicious peer from exhausting memory by referencing arbitrary topics in control messages". Measured, it does not: each such frame is *adopted*, adoption makes its key owned, and the eviction loop declines to drop an owned key, so 1,200 peer `CONTROL/SUBSCRIBE` frames grew `knownTopics` and `assignedTopics` to 1,200 (marks at 500 / 600 / 800 / 1,200) and one reconcile tick took both back to `0`. The bound on that flood is the sweep; the cap bounds entries this worker does not own. The same section's identifier table still carried the older short form of the rule ("FIFO-capped at 500; never evicts owned keys") and the row beside it named `CONTROL/UNSUBSCRIBE` and handoff as the only release paths for `assignedTopics`, omitting `reconcileAssignedTopics` — the leg that actually ends the flood. All three surfaces now say which structure bounds which, in both languages.
16
+ - **`MAX_PENDING_TOPICS` is an admission gate, not a per-topic bound.** `src/core/trace.ts` credited both of its caps with protecting against "a single misbehaving topic"; only `MAX_PENDING_MESSAGES_PER_TOPIC` (256) is about one topic. The 1,000-entry cap refuses the *next distinct* topic — measured 1,001 topics leaving 1,000 queues while an admitted topic still grows to 256 — and the `topics` Set carries neither cap on purpose (10,001 touched beside 1,000 sampled), being bounded by the window flush instead. Both caps turned out to have had named tests all along: two new cases written for this cycle were mutation-probed, found to duplicate `caps the number of topics tracked for pending receives` and `caps the pending receive queue per topic`, and deleted, so the shipped comment cites the existing names and the release adds no test and removes none.
17
+ - **The publication-lag samples were extended, and the ceiling's derivation did not move.** `0.21.33`'s tag run is the seventh ack→`time[<version>]` reading and the eighth green step duration: ack `00:31:20Z`, registry record `00:32:35.519Z` (**75.5 s**), `Verify published npm consumers` **304 s** — one 300 s lifetime plus command time, measured from that runner's own first fetch. Both lists are written out in `docs/release-checklist.md` (en + zh), the workflow comment and `tests/workflows.test.ts`, each with the `gh api` lookup that re-derives it. 75.5 s does not beat the 310 s that sizes the 720 s ceiling, so `maxMeasuredAckToRecordMs` stays put and the floor passes unmodified — widening the sample set without moving the derivation is the result being looked for.
18
+
19
+ ### Compatibility
20
+
21
+ No library code, export, option, default, frame or storage key changed; `verify:compat`, `verify:types` and `verify:pack` run against `v0.21.33`. The suite is 37 files / 950 tests (944 before `0.21.34`'s cycle, plus the five release-gate cases and the eviction pin). The only new blocking behavior is in the release workflow's own version gate, which skips itself when the registry read fails.
22
+
1
23
  ## [0.21.33] - 2026-09-25
2
24
 
3
25
  A sentence in the release checklist was wrong in a way only a new measurement could show, and the release it described was the one that produced it: `0.21.32`'s published-consumer step took 304 s while its registry record existed 76.1 s in. No library code changed.
@@ -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;IAgEjB;;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"}
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;IAmEjB;;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-FEWEXHZA.js";
10
+ } from "./chunk-A3UG46FI.js";
11
11
  import {
12
12
  CENTRIFUGE_INPUT_TYPE,
13
13
  CENTRIFUGE_OUTPUT_TYPE,
@@ -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 (`build/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`. The line number this used to cite \u2014 762\n // \u2014 matches neither the pinned copy, where the guard sits at 763-764, nor the\n // 5.7.0 copy in the store, where it is at 748; whether it drifted or was\n // mis-copied is not recoverable from here. That is the argument for quoting\n // the dependency's own comment as the locator instead of a number), and the\n // bundled emitter throws on an `error` emit with no listener\n // (`throw er; // Unhandled 'error' event` in the same file).\n // 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 three 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` (its class body is lines 667-2673 of that file, and\n // the twelve were counted in it) 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 the whole suite 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 // the whole suite 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 (`build/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: a no-op\n // `error` listener is installed on every subscription precisely\n // \"to avoid unhandled exception in EventEmitter for non-set error handler\",\n // and not by `Subscription` itself \u2014 that class body contains no `on('error')`\n // call at all (measured on the pinned copy), so the guard runs in the\n // `BaseSubscription` constructor every subscription reaches through `super`.\n // (centrifuge 5.7.4, `build/index.js`. The line number this used to cite \u2014 762\n // \u2014 matches neither the pinned copy, where the guard sits at 763-764, nor the\n // 5.7.0 copy in the store, where it is at 748; whether it drifted or was\n // mis-copied is not recoverable from here. That is the argument for quoting\n // the dependency's own comment as the locator instead of a number), and the\n // bundled emitter throws on an `error` emit with no listener\n // (`throw er; // Unhandled 'error' event` in the same file).\n // 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 three 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` (its class body is lines 667-2673 of that file, and\n // the twelve were counted in it) 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 the whole suite 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 // the whole suite 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
  }