cross-tab-worker-databus 0.20.96 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +1 -7
- package/dist/centrifuge.js.map +2 -2
- package/dist/{chunk-SSSJNWCH.js → chunk-EJDXU4DK.js} +18 -21
- package/dist/chunk-EJDXU4DK.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +17 -26
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +17 -20
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/data-bus.d.ts +7 -7
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/docs/api.md +1 -1
- package/docs/architecture.md +1 -0
- package/docs/benchmarks.md +7 -7
- package/docs/getting-started.md +3 -1
- package/docs/roadmap.md +21 -3
- package/docs/transports.md +1 -0
- package/docs/zh/api.md +1 -1
- package/docs/zh/architecture.md +1 -0
- package/docs/zh/benchmarks.md +7 -7
- package/docs/zh/getting-started.md +3 -1
- package/docs/zh/roadmap.md +21 -3
- package/docs/zh/transports.md +1 -0
- package/package.json +3 -2
- package/dist/chunk-SSSJNWCH.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.21.0] - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
- `subscribe("")`, `publish("")` and `publishBatch("")` now throw a `TypeError` instead of being accepted. This is the removal step of the cycle opened in `0.20.96`, which warned once per instance and changed nothing else; it follows the pre-1.0 policy, and no other behavior of those three calls moved. An empty topic routes as a literal channel that no transport can address, so the subscription it created could never receive anything and a publication to it was dropped without a trace — the call now fails where the mistake is made, the same way an invalid `replay.maxPerTopic` does. The guard runs as the first statement of each method, so a rejected call also starts no transport, registers no handler and writes no route record. `publishBatch("", [])` throws too: the argument is checked ahead of the documented empty-array no-op. **Migration:** pass a real channel name. Where the topic comes from user input or a config field, validate or fall back before calling — `input.trim() || 'demo.flow'`, which is what the bundled example pages do.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- `examples/react` and `examples/vue` now resolve their topic as `topicInput.trim() || <default>`, matching what `examples/demo` already did, because their boxes feed a reactive subscription and would otherwise hand `""` straight to the bus. The failure mode the fallback prevents is measured rather than assumed: with it removed, the Vue page keeps rendering its *previous* topic while a `TypeError` escapes to `pageerror`, and the demo page lands on 错误 with an error-feed row that never mentions the field the user just emptied. Both pages are now driven in real browser tabs (`e2e/adapters.spec.ts`, `e2e/demo.spec.ts`), each arm of the guard killing a different mutant.
|
|
10
|
+
- `docs/getting-started.md` (en + zh) claimed the browser suite covers both adapter pages. It covers the Vue page only — the React page loads React from `esm.sh`, so it cannot run where CI has no network, and it hand-wires the demo page's pattern rather than using the shipped `cross-tab-worker-databus/react` adapter (covered in jsdom by `tests/hooks.test.tsx`). The paragraph now says exactly that.
|
|
11
|
+
|
|
12
|
+
## [0.20.97] - 2026-09-22
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `examples/vue/` — a runnable page for the shipped `cross-tab-worker-databus/vue` entry, served from the local `vue` install (no CDN) and driven by real browser tests in `e2e/adapters.spec.ts`: two tabs publish through `useCrossTabDataBus` / `useCrossTabStatus` / `useCrossTabSubscription`, a reactive topic change is shown to re-attach the subscription and release the old one on the server, and a publication still crosses tabs after the owning tab closes. Until now the Vue adapter was only ever exercised in jsdom, and the existing React example hand-rolls its own effect wiring rather than using the adapter.
|
|
16
|
+
- `docs/transports.md` (en + zh) states the publication addressing rule that the new tests bump into: a publication carrying its own string `topic` is re-addressed to it instead of the channel it arrived on — which is how a wildcard-channel delivery names a concrete topic, and why application data shaped `{ "topic": … }` can silently go nowhere.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Test infrastructure only; no runtime behavior or public surface changed. `tests/coordination-invariants.test.ts` fuzzes three buses over one shared storage registry and BroadcastChannel — subscribes, unsubscribes, publications, hide/show, stop/start, heartbeats, dropped and forged `CONTROL/SUBSCRIBE` frames — then asserts at quiescence that each live topic has exactly one owner holding exactly one transport subscription, that a departed topic leaves no owner, holder or route behind, and that one publication fans out to each live subscriber exactly once. Five separate mutations kill those arms (dropping the transport subscribe/unsubscribe, double dispatch, never pruning orphan routes, emptying the assignment-reconcile sweep); three guards survive it because `reconcileAssignedTopics()` repairs their removal before quiescence, and the harness header records that as the reason those guards belong to the frame-level regressions. `mulberry32()` and `flushMicrotasks()` moved into `tests/fakes.ts` and the two existing fuzzers import them.
|
|
20
|
+
- The Centrifuge transport's synchronous credential-failure guard is pinned by a regression test instead of being removed as dead code. `resolveTokenRequest`'s `catch` compares the captured backend before posting `TOKEN_ERROR`: the credential provider is application code, so it can stop or replace the transport *before* it throws, and the post then addresses a backend that is already gone — `post()` raises `start() must be called first` out of the Worker message listener, which in a page is an uncaught error rather than a closed connection. Deleting the check fails the new test with exactly that error (verified by mutation), so the leg is reclassified from "unreachable by construction" to load-bearing — and it lifts whole-suite branch coverage from 96.02% to 96.07%, the same uplift removing the guard would have produced.
|
|
21
|
+
- `tests/dual-format.test.ts` now pins the CommonJS-only default-Worker failure: esbuild shims `import.meta` to `{}` in the CJS bundle, so the bundled Worker URL cannot resolve there and `start()` has to report "provide workerFactory explicitly" rather than a bare `TypeError`. The case also drives the ESM artifact with a stubbed `Worker`/`SharedWorker` and asserts it resolves and constructs both default Workers, which is what ties the failure to the module *format* rather than to a wrong filename. Deleting either `catch` fails it (verified by mutation).
|
|
22
|
+
- Two unreachable defensive branches removed with it, with no behavior change: the `typeof Worker` and `typeof SharedWorker` throws in the default Worker factories. `start()` builds the selector's availability flags from exactly those globals (`worker: workerFactory !== undefined || typeof Worker !== 'undefined'`) and `selectWorkerBackend` never returns a backend its flags deny, so the factories only ever run where the global exists; the degradation is pinned where it is decided, by `tests/worker-mode.test.ts` and the transport's "falls back to the local session when the platform lacks both Worker APIs" case. This is the counterpart to the credential guard kept above: nothing — not even application code — runs between the check and the call here, so no re-entry can invalidate the fact it asserts.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- The CI `verify` gate no longer times out on the coordination fuzzer added in this range, and the reason turned out not to be runner load. The sweep bounded its depth with `Date.now()`, while this suite fakes `Date` in nearly every file and advances it ~45 simulated seconds per fuzz seed — so a faked clock surviving into the fuzz file's worker made the budget read either instantly exceeded (the same file stopped at its floor after 16.4s in one full run) or never exceeded (539s on CI, still mid-sweep). `tests/setup.ts` now restores real timers after every test globally, and both seeded fuzzers budget on `performance.now()`, which no test here fakes.
|
|
26
|
+
- Both seeded fuzzers also bound depth explicitly: a `MAX_SEEDS` cap, a 60s budget checked per seed, an asserted 100-seed floor so a slow runner cannot "pass" on a handful of interleavings, and a log line when the sweep is truncated. The floor is set from measured mutant kill depth, not preference — the heaviest mutant the coordination harness was proved against (an emptied assignment-reconcile sweep) is caught at seed 12.
|
|
27
|
+
- The hot-path performance gates no longer fail on scheduling. A single wall-clock sample measured the scheduler as much as the code — two of the five gates failed in a full coverage run while the same file passed alone — so they now assert the fastest of three repeats *and* run as their own sequential step (`pnpm test:perf`, part of `pnpm check`, excluded from `pnpm test` and `pnpm test:coverage`), because an absolute-millisecond ceiling is only meaningful on an unscheduled core. A quadratic routing regression still trips its ceiling (13.6s against 1s on the best repeat).
|
|
28
|
+
|
|
29
|
+
### Docs
|
|
30
|
+
- The `0.20.96` entry below undercounts its own test work. Its release range (`v0.20.95..v0.20.96`, PRs #137-#146) also shipped mutation-verified regressions for the WebSocket opt-out handshake budget, the cluster's private-topic handoff rule, the trace reporter's sink-error containment on a runtime without `console.warn`, and the wildcard probe that keeps an unrelated owned pattern from capturing a batch destined for a remote topic — eleven behaviors, not seven. The published `0.20.96` tarball keeps its original wording (npm versions are immutable); this corrects the record for anyone reading between tags, and the `0.20.96` delivered-scope sections in `docs/roadmap.md` and `docs/zh/roadmap.md` now carry the full list.
|
|
31
|
+
|
|
3
32
|
## [0.20.96] - 2026-09-22
|
|
4
33
|
|
|
5
34
|
### Deprecated
|
package/dist/centrifuge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"centrifuge.d.ts","sourceRoot":"","sources":["../src/centrifuge.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"centrifuge.d.ts","sourceRoot":"","sources":["../src/centrifuge.ts"],"names":[],"mappings":"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"}
|
package/dist/centrifuge.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseDataBusPublication,
|
|
6
6
|
publicationMetadata,
|
|
7
7
|
selectWorkerBackend
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-EJDXU4DK.js";
|
|
9
9
|
import {
|
|
10
10
|
CENTRIFUGE_INPUT_TYPE,
|
|
11
11
|
CENTRIFUGE_OUTPUT_TYPE,
|
|
@@ -580,9 +580,6 @@ function createCentrifugeDataBus(options) {
|
|
|
580
580
|
});
|
|
581
581
|
}
|
|
582
582
|
function createDefaultWorker() {
|
|
583
|
-
if (typeof Worker === "undefined") {
|
|
584
|
-
throw new Error("CentrifugeWorkerTransport requires a browser Worker implementation.");
|
|
585
|
-
}
|
|
586
583
|
let workerUrl;
|
|
587
584
|
try {
|
|
588
585
|
workerUrl = new URL("./centrifuge.worker.js", import.meta.url);
|
|
@@ -597,9 +594,6 @@ function createDefaultWorker() {
|
|
|
597
594
|
});
|
|
598
595
|
}
|
|
599
596
|
function createDefaultSharedWorker() {
|
|
600
|
-
if (typeof SharedWorker === "undefined") {
|
|
601
|
-
throw new Error("CentrifugeWorkerTransport requires a browser SharedWorker implementation.");
|
|
602
|
-
}
|
|
603
597
|
let workerUrl;
|
|
604
598
|
try {
|
|
605
599
|
workerUrl = new URL("./centrifuge.shared.worker.js", import.meta.url);
|
package/dist/centrifuge.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/centrifuge-session.ts", "../src/utils/error-utils.ts", "../src/centrifuge-protocol.ts", "../src/centrifuge.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * CentrifugeSession \u2014 a reusable wrapper around a single Centrifuge client.\n *\n * Provides a structured-clone-safe message protocol (INIT/SUBSCRIBE/UNSUBSCRIBE/\n * PUBLISH/STOP) so the same session class can run inside a Dedicated Worker,\n * a SharedWorker port, or directly on the main thread as a local fallback.\n */\nimport { Centrifuge } from 'centrifuge';\nimport { parseDataBusPublication } from './core/publication';\nimport type {\n PublicationContext,\n StateContext,\n Subscription,\n SubscriptionErrorContext\n} from 'centrifuge';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, WORKER_STATUS } from './utils/constants';\nimport { publicationMetadata } from './utils/metadata';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\n\n/** Callback interface for posting messages back to the transport layer. */\nexport interface CentrifugeSessionSink<TData = unknown> {\n post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void;\n}\n\n/** A pending getToken/getChannelToken request awaiting a main-thread response. */\ninterface PendingTokenRequest {\n resolve: (token: string) => void;\n reject: (error: unknown) => void;\n}\n\n/** Centrifuge client options for token-bridge mode: the structured-clone-safe\n * subset plus the two function-valued hooks that Centrifuge invokes for fresh\n * credentials. Used only inside the session, never across the Worker boundary. */\ntype CentrifugeBridgedOptions = Omit<CentrifugeWorkerConfig, 'getToken' | 'getChannelToken'> & {\n getToken: () => Promise<string>;\n getChannelToken: (channel: string) => Promise<string>;\n};\n\n/**\n * Stateful Centrifuge client wrapper shared by Dedicated Worker, SharedWorker\n * ports and the main-thread local fallback. Each session owns one connection.\n */\nexport class CentrifugeSession<TData = unknown> {\n private client: Centrifuge | null = null;\n private readonly subscriptions = new Map<string, Subscription>();\n private transferable = false;\n private tokenBridge = false;\n private nextRequestId = 1;\n // Bumped on every initialize/stop. Async client callbacks capture the value\n // from the connection that created them so a stopped client cannot emit into\n // a later session instance (including the main-thread local fallback).\n private lifecycle = 0;\n private readonly pendingTokenRequests = new Map<number, PendingTokenRequest>();\n\n constructor(private readonly sink: CentrifugeSessionSink<TData>) {}\n\n /** Dispatch an incoming Worker message to the matching operation.\n * Unknown message types are ignored rather than thrown, so a future protocol\n * extension adding a new variant cannot crash an older session. */\n handle(message: CentrifugeWorkerInput): void {\n switch (message.type) {\n case CENTRIFUGE_INPUT_TYPE.INIT:\n this.initialize(message.url, message.config, message.transferable === true, message.tokenBridge === true);\n return;\n case CENTRIFUGE_INPUT_TYPE.SUBSCRIBE:\n this.subscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE:\n this.unsubscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.PUBLISH:\n case CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN:\n // Binary and JSON publish share the same Centrifuge client call; the\n // transport layer decides whether to transfer the ArrayBuffer.\n this.publish(message.topic, message.data, message.messageId, message.timestamp);\n return;\n case CENTRIFUGE_INPUT_TYPE.STOP:\n this.stop();\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE:\n this.resolveToken(message.requestId, message.token);\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR:\n this.rejectToken(message.requestId, deserializeWorkerError(message.error));\n return;\n default:\n return;\n }\n }\n\n /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */\n private initialize(url: string, config: CentrifugeWorkerConfig, transferable: boolean, tokenBridge: boolean): void {\n if (this.client) return;\n const lifecycle = ++this.lifecycle;\n this.transferable = transferable;\n this.tokenBridge = tokenBridge;\n const clientOptions: CentrifugeWorkerConfig | CentrifugeBridgedOptions = tokenBridge\n ? {\n ...config,\n // Route credential fetches back to the main thread, where the\n // application's credentialProvider lives (config must stay\n // structured-cloneable, so functions cannot cross the boundary).\n getToken: () => this.requestToken('token', lifecycle),\n getChannelToken: (channel: string) => this.requestToken('channelToken', lifecycle, channel)\n }\n : config;\n const client = new Centrifuge(url, clientOptions);\n this.client = client;\n client.on('state', (context: StateContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED });\n });\n client.on('disconnected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n });\n client.on('error', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n // Client-level publications are only for server-side subscriptions (where\n // no client Subscription object exists). For topics we have an active\n // subscription for, the subscription-level 'publication' listener handles\n // dispatch \u2014 skip here to avoid delivering the same message twice.\n client.on('publication', (context: PublicationContext) => {\n if (this.lifecycle !== lifecycle) return;\n const topic = context.channel || getPayloadTopic(context.data);\n if (!topic || this.subscriptions.has(topic)) return;\n this.postPublication(topic, context.data);\n });\n client.connect();\n }\n\n /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.\n * Listeners are only registered once per subscription object \u2014 a repeated\n * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,\n * avoiding the removeAllListeners + re-on churn on every duplicate message. */\n private subscribe(topic: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n const lifecycle = this.lifecycle;\n // If we already track this subscription, it already has our listeners \u2014\n // a duplicate SUBSCRIBE is a no-op (idempotent), matching the transport\n // contract. Only a fresh subscription needs listener wiring.\n const existing = this.subscriptions.get(topic);\n if (existing) {\n existing.subscribe();\n return;\n }\n let subscription = this.client.getSubscription(topic);\n if (!subscription) subscription = this.client.newSubscription(topic);\n // Remove only our own listeners so that any Centrifuge internal listeners\n // on the subscription object are preserved. Each subscription event carries\n // a single listener so removeAllListeners(\u2026) is safe here.\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.set(topic, subscription);\n subscription.on('publication', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n subscription.on('unsubscribed', () => {\n if (this.lifecycle === lifecycle) this.subscriptions.delete(topic);\n });\n subscription.subscribe();\n }\n\n /** Unsubscribe from a Centrifuge channel and clean up the local reference.\n * Listeners are removed before unsubscribing so a late `unsubscribed` event\n * cannot delete a subscription that a subsequent `subscribe()` re-added. */\n private unsubscribe(topic: string): void {\n const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);\n if (!subscription) return;\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.delete(topic);\n subscription.unsubscribe();\n }\n\n /** Publish a message to the Centrifuge channel. */\n private publish(topic: string, data: unknown, messageId?: string, timestamp?: number): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n const lifecycle = this.lifecycle;\n // Centrifuge's payload is application-defined. Preserve legacy payloads;\n // when an ID is requested, send a small metadata envelope that compatible\n // servers can echo back for end-to-end deduplication.\n const hasMetadata = messageId !== undefined || timestamp !== undefined;\n const payload = hasMetadata\n ? {\n data,\n ...(messageId === undefined ? {} : { messageId }),\n ...(timestamp === undefined ? {} : { timestamp })\n }\n : data;\n void this.client.publish(topic, payload).catch(error => {\n if (this.lifecycle === lifecycle) this.postError(error);\n });\n }\n\n /** Forward a publication to the transport. Binary payloads take the\n * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything\n * else is structured-cloned via `MESSAGE`. An empty topic means the\n * publication carried no channel info and is silently dropped. */\n private postPublication(topic: string, data: unknown): void {\n if (!topic) return;\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN, topic, data }, [data]);\n return;\n }\n const publication = parseDataBusPublication<TData>(data, topic);\n if (!publication) return;\n this.post({\n type: CENTRIFUGE_OUTPUT_TYPE.MESSAGE,\n topic: publication.topic,\n data: publication.data,\n ...publicationMetadata(publication.messageId, publication.timestamp)\n });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n // Invalidate callbacks from the connection being stopped before disconnect\n // so its synchronous or queued events cannot leak into a later session.\n this.lifecycle += 1;\n // Settle every in-flight credential request so a stop cannot leave the\n // Worker awaiting a main-thread response forever.\n for (const [, pending] of this.pendingTokenRequests) {\n pending.reject(new Error('Centrifuge session stopped before the credential was resolved.'));\n }\n this.pendingTokenRequests.clear();\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n }\n\n /** Issue a credential request to the main thread and await the response.\n * Used as Centrifuge's `getToken` / `getChannelToken` when token bridging is\n * enabled; resolved or rejected by a matching TOKEN_RESPONSE / TOKEN_ERROR. */\n private requestToken(kind: 'token' | 'channelToken', lifecycle: number, channel?: string): Promise<string> {\n // The provider is installed on a specific Centrifuge client. An old client\n // must not be able to route a late credential request into a replacement\n // session merely because it retained the callback closure.\n if (this.lifecycle !== lifecycle) {\n return Promise.reject(new Error('Centrifuge client lifecycle has ended before the credential was requested.'));\n }\n const requestId = this.nextRequestId;\n this.nextRequestId += 1;\n return new Promise<string>((resolve, reject) => {\n this.pendingTokenRequests.set(requestId, { resolve, reject });\n this.post({\n type: CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST,\n requestId,\n kind,\n ...(channel === undefined ? {} : { channel })\n });\n });\n }\n\n private resolveToken(requestId: number, token: string): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.resolve(token);\n }\n\n private rejectToken(requestId: number, error: unknown): void {\n const pending = this.pendingTokenRequests.get(requestId);\n if (!pending) return;\n this.pendingTokenRequests.delete(requestId);\n pending.reject(error);\n }\n\n /** Forward a message to the sink (the transport layer). */\n private post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void {\n this.sink.post(message, transfer);\n }\n\n /** Serialise and report an error. The Centrifuge client handles reconnection\n * internally, so a transient error should not trigger a `STATUS: error` that\n * would cause `selectActiveWorkers()` to exclude this worker from routing.\n * Fatal errors are distinguished by the client eventually emitting\n * `disconnected` without a subsequent `connected`. */\n private postError(error: unknown): void {\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.ERROR, error: serializeError(error) });\n }\n}\n\n/** Extract a topic from a Centrifuge publication payload if one is present.\n * Handles both direct `channel` fields and the nested `push.channel` shape\n * that Centrifugo uses for some server-side push types. */\nfunction getPayloadTopic(data: unknown): string {\n if (!data || typeof data !== 'object') return '';\n const payload = data as Record<string, unknown>;\n // Prefer nested push.channel (server-side push) then fall back to top-level channel.\n const push = payload.push;\n const nested = typeof push === 'object' && push !== null ? (push as Record<string, unknown>).channel : undefined;\n const topic = nested ?? payload.channel;\n return typeof topic === 'string' ? topic : '';\n}\n\n/** Map a Centrifuge state string to the DataBus's status vocabulary.\n * 'connecting' and 'connected' pass through; anything else (e.g. 'reconnecting',\n * 'disconnected') maps to 'disconnected'. */\nconst LIVE_STATES = new Set<string>([WORKER_STATUS.CONNECTING, WORKER_STATUS.CONNECTED]);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\n}\n", "/**\n * \u9519\u8BEF\u5E8F\u5217\u5316\u5DE5\u5177 \u2014\u2014 \u5728 Worker \u8FB9\u754C\u5F80\u8FD4\u4F20\u8F93 Error\u3002\n *\n * Error \u5B9E\u4F8B\u65E0\u6CD5\u901A\u8FC7 postMessage \u7ED3\u6784\u5316\u514B\u9686\uFF0C\u6240\u4EE5 CentrifugeSession \u4FA7\u7528\n * serializeError \u628A\u5B83\u538B\u6210 plain object\uFF0C\u4E3B\u7EBF\u7A0B\u4FA7\u7528 deserializeWorkerError\n * \u8FD8\u539F\u6210\u771F Error\uFF08\u5E26 name/stack\uFF09\u3002\u6B64\u524D\u4E24\u4E2A\u51FD\u6570\u5206\u522B\u5185\u8054\u5728 centrifuge-session.ts\n * \u4E0E centrifuge.ts\uFF0C\u6B64\u5904\u7EDF\u4E00\u5B9A\u4E49\u5E76\u4ECE\u4E24\u5904\u5F15\u7528\u3002\n */\n\n/** Error object serialized for cross-thread transfer. */\nexport interface SerializedWorkerError {\n /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */\n name: string;\n /** The Error's `message`. */\n message: string;\n /** The Error's `stack` if available (for debugging). */\n stack?: string;\n /** Arbitrary context attached by the Worker (e.g. the failing operation). */\n context?: unknown;\n}\n\n/** Convert an arbitrary error into a structured-cloneable form for postMessage.\n * A non-Error value is attached as `context`, but only when it is itself\n * structured-cloneable: a function, symbol, or an object/map holding one would\n * otherwise make the serialised error unserializable, so `postMessage` would\n * throw `DataCloneError` while reporting the original failure. */\nexport function serializeError(error: unknown): SerializedWorkerError {\n if (error instanceof Error) {\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {})\n };\n }\n const context = error === undefined ? undefined : error;\n return {\n name: 'CentrifugeError',\n message: typeof error === 'string' ? error : 'Centrifuge worker operation failed.',\n ...(context === undefined || !isStructuredCloneable(context) ? {} : { context })\n };\n}\n\n/** True when `value` survives a structured clone. When the runtime has no\n * `structuredClone`, cloneability cannot be checked and the value is kept\n * (best effort); the eventual postMessage would fail either way. */\nfunction isStructuredCloneable(value: unknown): boolean {\n if (typeof structuredClone !== 'function') return true;\n try {\n structuredClone(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Reconstruct an Error instance from its serialised form. */\nexport function deserializeWorkerError(error: SerializedWorkerError): Error {\n const result = new Error(error.message);\n result.name = error.name;\n if (error.stack) result.stack = error.stack;\n if (error.context !== undefined) Object.assign(result, { context: error.context });\n return result;\n}\n", "/**\n * Worker-thread protocol for Centrifuge WebSocket transport.\n *\n * Defines the message types exchanged between the main thread and a Web Worker\n * (dedicated or shared) that runs a centrifuge client. The worker is isolated\n * from the main thread so that WebSocket lifecycle, token refresh, and binary\n * data handling never block the UI.\n *\n * The message `type` discriminant values are derived from the constants in\n * `utils/constants.ts` so main-thread and Worker sides reference the same\n * values and can never drift apart.\n */\nimport type { Options } from 'centrifuge';\nimport type { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE } from './utils/constants';\nimport type { WorkerStatus } from './core/types';\nimport type { SerializedWorkerError } from './utils/error-utils';\n\nexport type { SerializedWorkerError } from './utils/error-utils';\n\n/**\n * Default interval between main-thread PING heartbeats to a SharedWorker. The\n * SharedWorker reaps a silent port after `SESSION_TIMEOUT_MULTIPLIER` intervals.\n * Shared here so the main thread (heartbeat sender) and the SharedWorker (reaper)\n * always agree on the cadence.\n */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;\n/** SharedWorker per-port session timeout, expressed as a multiple of that port's\n * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of\n * several intervals tolerates throttled tabs without reaping healthy sessions. */\nexport const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;\n/** Default per-port session timeout in ms, derived from the heartbeat interval\n * and the multiplier. Used as the fallback when a port sends no INIT config. */\nexport const DEFAULT_SESSION_TIMEOUT_MS =\n DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;\n\n// Centrifuge Options that reference browser APIs (WebSocket, EventSource, etc.)\n// are unavailable inside a Worker \u2014 the worker uses its own WebSocket import.\n// These are stripped from the config sent to the worker so the type system\n// prevents accidentally passing a main-thread-only function (which would fail\n// structured cloning and throw a DataCloneError).\ntype WorkerUnsafeOption =\n | 'eventsource'\n | 'fetch'\n | 'getData'\n | 'getToken'\n | 'networkEventTarget'\n | 'readableStream'\n | 'sockjs'\n | 'websocket';\n\n/** Centrifuge options safe to pass into a Worker; unsafe options are explicitly\n * excluded and set to `never` so the compiler rejects them at the call site. */\nexport type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {\n [Key in WorkerUnsafeOption]?: never;\n};\n\n/**\n * Async credential provider run on the main thread. Centrifuge client options\n * are structured-cloned into the Worker, so function-valued `getToken` /\n * `getChannelToken` cannot travel with the config. When a provider is set, the\n * Worker asks the main thread for each fresh token over a TOKEN_REQUEST /\n * TOKEN_RESPONSE exchange and this provider supplies it from application\n * context (async token endpoints, SecureStorage, etc.).\n */\nexport interface CentrifugeCredentialProvider {\n /** Return a fresh top-level connection token. Called on connect and refresh. */\n getToken: () => MaybePromise<string | null | undefined>;\n /** Return a channel token for a channel. Centrifuge falls back to `getToken`\n * when channel tokens are not configured by the server. */\n getChannelToken?: (channel: string) => MaybePromise<string | null | undefined>;\n}\n\n/** A value or a promise of a value. */\nexport type MaybePromise<T> = T | Promise<T>;\n\n/** Messages sent from the main thread to the Worker. All variants are\n * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */\nexport type CentrifugeWorkerInput =\n /** Initial connection: URL + config. Sent once per backend creation.\n * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.\n * `heartbeatIntervalMs` overrides the SharedWorker PING cadence.\n * `tokenBridge` asks the Worker to route getToken/getChannelToken back to\n * the main thread instead of holding function-valued config. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.INIT; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number; tokenBridge?: boolean }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.SUBSCRIBE; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH; topic: string; data: unknown; messageId?: string; timestamp?: number }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PING }\n /** Disconnect the client and clear all subscriptions. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.STOP }\n /** Fresh credential for an earlier TOKEN_REQUEST, resolved by requestId. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE; requestId: number; token: string }\n /** A credential request failed (provider rejected or returned no token). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR; requestId: number; error: SerializedWorkerError };\n\n/** Messages sent from the Worker back to the main thread. The main thread\n * routes these to the DataBusTransportHandlers via handleOutput(). */\nexport type CentrifugeWorkerOutput<TData = unknown> =\n /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.STATUS; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE; topic: string; data: TData; messageId?: string; timestamp?: number }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.ERROR; error: SerializedWorkerError }\n /** The Worker needs a fresh credential. The main thread resolves it via a\n * matching TOKEN_RESPONSE / TOKEN_ERROR carrying the same `requestId`. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST; requestId: number; kind: 'token' | 'channelToken'; channel?: string };\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends, selected in order of preference:\n * 1. SharedWorker \u2014 one WebSocket per tab session, hosted in a shared process\n * 2. Dedicated Worker \u2014 one WebSocket per tab\n * 3. In-process (local) \u2014 Centrifuge runs on the main thread (fallback when\n * neither Worker type is available, e.g. in non-browser environments)\n *\n * Binary data (ArrayBuffer) can be transferred via Transferable when the\n * `transferable` option is enabled, avoiding structured-clone overhead.\n */\n\nimport { CrossTabDataBus } from './core/data-bus';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type { DataBusTransport, DataBusTransportHandlers } from './core/types';\nimport { CentrifugeSession } from './centrifuge-session';\nimport { selectWorkerBackend } from './worker-mode';\nimport type { WorkerBackend, WorkerMode } from './worker-mode';\nimport type {\n CentrifugeCredentialProvider,\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, DEFAULT_STORAGE_PREFIX, WORKER_BACKEND, WORKER_MODE, WORKER_STATUS } from './utils/constants';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\nimport { publicationMetadata } from './utils/metadata';\nimport { assertHeartbeatInterval, assertStructuredCloneable } from './utils/validation';\n\nexport type { CentrifugeCredentialProvider, CentrifugeWorkerConfig, SerializedWorkerError } from './centrifuge-protocol';\nexport type { WorkerBackend, WorkerMode } from './worker-mode';\n\n/** WebSocket connection parameters passed to the Centrifuge Worker. */\nexport interface CentrifugeDataBusConfig {\n /** Centrifuge server WebSocket URL. */\n url: string;\n /** Centrifuge client options (token, channel params, etc.). */\n options?: CentrifugeWorkerConfig;\n}\n\n/** Options for configuring the Worker backend (dedicated, shared, or local). */\nexport interface CentrifugeWorkerTransportOptions {\n /** Custom dedicated Worker factory. Used for testing or bundler integration. */\n workerFactory?: () => Worker;\n /** Custom SharedWorker factory. */\n sharedWorkerFactory?: () => SharedWorker;\n /** Preferred Worker mode: 'dedicated', 'shared', or 'auto'. */\n workerMode?: WorkerMode;\n /** Enable transferable (ArrayBuffer) support for binary data. */\n transferable?: boolean;\n /** Interval (ms) between PING heartbeats sent to the SharedWorker. The\n * SharedWorker reaps a silent port after `DEFAULT_SESSION_TIMEOUT_MULTIPLIER`\n * \u00D7 this interval. Pass `Infinity` to disable heartbeats entirely. Defaults\n * to `DEFAULT_HEARTBEAT_INTERVAL_MS`. */\n heartbeatIntervalMs?: number;\n /** Async credential provider. Runs on the main thread: the Worker cannot\n * hold function-valued Centrifuge options (they are not structured-cloneable),\n * so when a provider is set the Worker asks for each fresh token over a\n * TOKEN_REQUEST / TOKEN_RESPONSE bridge and this provider supplies it. */\n credentialProvider?: CentrifugeCredentialProvider;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a Centrifuge transport. */\nexport interface CreateCentrifugeDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<CentrifugeDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n >,\n CentrifugeWorkerTransportOptions {\n /** Centrifuge connection configuration. */\n connection: CentrifugeDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/**\n * Transport layer that runs a Centrifuge WebSocket client inside a Web Worker.\n *\n * Delegates the actual WebSocket connection to a Worker (dedicated or shared)\n * or falls back to an in-process CentrifugeSession. The Worker is isolated from\n * the main thread so that WebSocket lifecycle, token refresh, and binary data\n * handling never block the UI.\n */\nexport class CentrifugeWorkerTransport<TData = unknown>\n implements DataBusTransport<CentrifugeDataBusConfig, TData>\n{\n readonly diagnosticsName = 'centrifuge';\n private readonly workerMode: WorkerMode;\n private readonly transferable: boolean;\n private readonly heartbeatIntervalMs: number;\n private readonly workerFactory: (() => Worker) | undefined;\n private readonly sharedWorkerFactory: (() => SharedWorker) | undefined;\n private readonly credentialProvider: CentrifugeCredentialProvider | undefined;\n private backend: WorkerBackend | null = null;\n private worker: Worker | null = null;\n private sharedWorker: SharedWorker | null = null;\n private port: MessagePort | null = null;\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n private localSession: CentrifugeSession<TData> | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n // Monotonically increasing counter, bumped each time a backend is created or\n // the transport stops. Only the asynchronous credential bridge compares it:\n // a provider may settle after the Worker it was answering is gone. Worker\n // error events need no such check \u2014 stop() and onWorkerFailed() remove those\n // listeners before the generation moves on, so a superseded backend can no\n // longer reach this object at all.\n private generation = 0;\n\n get diagnosticsBackend(): string {\n return this.backend ?? 'uninitialized';\n }\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? WORKER_MODE.DEDICATED;\n this.transferable = options.transferable ?? false;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n assertHeartbeatInterval(this.heartbeatIntervalMs);\n this.workerFactory = options.workerFactory;\n this.sharedWorkerFactory = options.sharedWorkerFactory;\n this.credentialProvider = options.credentialProvider;\n }\n\n /**\n * Start the transport: select a backend, initialise the Worker (or local\n * session), and send the INIT message with connection parameters.\n */\n start(config: CentrifugeDataBusConfig, handlers: DataBusTransportHandlers<TData>): void {\n if (this.backend) return;\n assertStructuredCloneable(config.options ?? {});\n this.handlers = handlers;\n // Runtime capability OR an injected factory counts as availability; a\n // factory-less browser must use the bundled default Workers instead of\n // silently degrading to the local session.\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined || typeof Worker !== 'undefined',\n sharedWorker: this.sharedWorkerFactory !== undefined || typeof SharedWorker !== 'undefined'\n });\n const input = this.buildInitInput(config);\n if (backend === WORKER_BACKEND.SHARED) {\n this.startSharedWorker(input);\n this.backend = WORKER_BACKEND.SHARED;\n return;\n }\n if (backend === WORKER_BACKEND.DEDICATED) {\n this.startDedicatedWorker(input);\n this.backend = WORKER_BACKEND.DEDICATED;\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = WORKER_BACKEND.LOCAL;\n }\n\n subscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.SUBSCRIBE, topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE, topic });\n }\n\n /**\n * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable\n * when `transferable` is enabled, avoiding a structured-clone cycle.\n */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): void {\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) }, [data]);\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) });\n }\n\n /**\n * Gracefully stop the transport: send STOP, clean up event listeners, and\n * terminate the Worker (or close the SharedWorker port).\n */\n stop(): void {\n if (!this.backend) return;\n this.generation++;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.STOP });\n this.clearHeartbeat();\n if (this.worker) {\n this.worker.removeEventListener('message', this.handleMessage);\n this.worker.removeEventListener('error', this.handleWorkerError);\n this.worker.terminate();\n }\n if (this.sharedWorker) {\n this.detachSharedWorkerListeners();\n this.port?.close();\n }\n this.resetBackend();\n this.handlers = null;\n }\n\n /** Build the INIT payload sent to the Worker / local session. Optional fields\n * are only included when they deviate from the defaults, so the Worker's own\n * default-resolution logic kicks in for the common case. */\n private buildInitInput(config: CentrifugeDataBusConfig): CentrifugeWorkerInput {\n return {\n type: CENTRIFUGE_INPUT_TYPE.INIT,\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.credentialProvider ? { tokenBridge: true } : {}),\n ...(this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS\n ? { heartbeatIntervalMs: this.heartbeatIntervalMs }\n : {})\n };\n }\n\n /** Create and initialise a dedicated Worker, then send the INIT message. */\n private startDedicatedWorker(input: CentrifugeWorkerInput): void {\n this.generation += 1;\n const worker = (this.workerFactory ?? createDefaultWorker)();\n this.worker = worker;\n worker.addEventListener('message', this.handleMessage);\n worker.addEventListener('error', this.handleWorkerError);\n worker.postMessage(input);\n }\n\n /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */\n private startSharedWorker(input: CentrifugeWorkerInput): void {\n this.generation += 1;\n const shared = (this.sharedWorkerFactory ?? createDefaultSharedWorker)();\n this.sharedWorker = shared;\n const port = shared.port;\n this.port = port;\n port.addEventListener('message', this.handleMessage);\n port.addEventListener('messageerror', this.handlePortError);\n shared.addEventListener('error', this.handleSharedWorkerError);\n port.start();\n port.postMessage(input);\n this.startHeartbeat();\n }\n\n /** Handle a message event from the Worker (dedicated or shared). */\n private readonly handleMessage = (event: MessageEvent<CentrifugeWorkerOutput<TData>>) => {\n this.handleOutput(event.data);\n };\n\n /** Handle a message from the in-process CentrifugeSession (local fallback). */\n private readonly handleSessionOutput = (message: CentrifugeWorkerOutput<TData>) => {\n this.handleOutput(message);\n };\n\n /** Route a Worker output message to the appropriate handler callback.\n * Shared by the Worker message listener, the SharedWorker port listener,\n * and the local-session sink \u2014 all three feed into this single dispatcher. */\n private handleOutput(message: CentrifugeWorkerOutput<TData>): void {\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.STATUS) this.handlers?.onStatus(message.status);\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE) this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN) this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.ERROR) this.handlers?.onError(deserializeWorkerError(message.error));\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST) this.resolveTokenRequest(message.requestId, message.kind, message.channel);\n }\n\n /** Resolve a Worker credential request from the main thread: call the\n * credentialProvider, then post the fresh token (or a serialized failure)\n * back to the session that issued the request. */\n private resolveTokenRequest(requestId: number, kind: 'token' | 'channelToken', channel: string | undefined): void {\n // Credential providers may be asynchronous. Capture the backend identity\n // before awaiting so a stop/start that replaces the Worker cannot receive\n // a late token (or error) belonging to the superseded session. Request IDs\n // restart at 1 for a fresh CentrifugeSession, so without this guard a stale\n // reply can incorrectly satisfy the replacement session's request.\n const generation = this.generation;\n const worker = this.worker;\n const port = this.port;\n const localSession = this.localSession;\n const isCurrentBackend = () =>\n this.generation === generation &&\n this.worker === worker &&\n this.port === port &&\n this.localSession === localSession;\n const provider = this.credentialProvider;\n let value: string | null | Promise<string | null | undefined> | undefined;\n try {\n value = kind === 'channelToken' && provider?.getChannelToken\n ? provider.getChannelToken(channel ?? '')\n : provider?.getToken?.();\n } catch (error) {\n if (isCurrentBackend()) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n return;\n }\n Promise.resolve(value).then(\n token => {\n if (!isCurrentBackend()) return;\n if (typeof token !== 'string' || token.length === 0) {\n this.post({\n type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR,\n requestId,\n error: serializeError(new Error(`The credential provider returned no ${kind}.`))\n });\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE, requestId, token });\n },\n error => {\n if (!isCurrentBackend()) return;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n );\n }\n\n /** Handle a Worker-level failure (crash, message decode error). Discards the\n * dead backend so a later start()/reopen can rebuild from scratch, and\n * signals an error status so the DataBus can trigger recovery. A superseded\n * backend cannot reach this: its listeners are removed before the transport\n * moves on, which is what keeps late Worker errors from tearing down a fresh\n * session (pinned by 'ignores error events from a superseded worker'). */\n private onWorkerFailed(message: string): void {\n // Remove the message listener and release the old backend before recovery\n // reopens, so late messages from the failed Worker/port cannot be routed\n // into the freshly started session.\n this.worker?.removeEventListener('message', this.handleMessage);\n this.worker?.removeEventListener('error', this.handleWorkerError);\n this.worker?.terminate();\n this.detachSharedWorkerListeners();\n this.port?.close();\n this.clearHeartbeat();\n this.resetBackend();\n this.handlers?.onError(new Error(message));\n this.handlers?.onStatus(WORKER_STATUS.ERROR);\n }\n\n /** Remove every listener attached to the current SharedWorker and its port. */\n private detachSharedWorkerListeners(): void {\n this.port?.removeEventListener('message', this.handleMessage);\n this.port?.removeEventListener('messageerror', this.handlePortError);\n this.sharedWorker?.removeEventListener('error', this.handleSharedWorkerError);\n }\n\n /** Periodically ping the SharedWorker so its session reaper can detect a dead tab. */\n private startHeartbeat(): void {\n if (this.heartbeatHandle !== null) return;\n // Infinite disables the heartbeat (e.g. for environment where the\n // SharedWorker reaper is not needed).\n if (this.heartbeatIntervalMs === Infinity) return;\n this.heartbeatHandle = setInterval(() => {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PING });\n }, this.heartbeatIntervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatHandle !== null) clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n\n private readonly handleWorkerError = () => {\n this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\n this.onWorkerFailed('Centrifuge shared worker failed.');\n };\n\n /** Clear the Worker/port/backend references after a failure or stop. */\n private resetBackend(): void {\n this.worker = null;\n this.sharedWorker = null;\n this.port = null;\n this.backend = null;\n this.localSession = null;\n }\n\n /**\n * Post a message to the active backend. Accepts optional Transferable buffers\n * for zero-copy ArrayBuffer transfer.\n */\n private post(message: CentrifugeWorkerInput, transfer?: ArrayBuffer[]): void {\n if (this.worker) {\n postToPortLike(this.worker, message, transfer);\n return;\n }\n if (this.port) {\n postToPortLike(this.port, message, transfer);\n return;\n }\n if (this.localSession) {\n this.localSession.handle(message);\n return;\n }\n throw new Error('CentrifugeWorkerTransport.start() must be called first.');\n }\n}\n\n/**\n * Create a fully-configured CrossTabDataBus with a Centrifuge WebSocket transport.\n *\n * This is the primary entry point for consumers. It wires up the transport,\n * cluster coordination, and lifecycle management:\n *\n * ```ts\n * const bus = createCentrifugeDataBus({\n * connection: { url: 'wss://example.com/connection/websocket', options: { token: '\u2026' } },\n * trace: { enabled: true, sink: event => console.log(event) },\n * });\n * ```\n */\nexport function createCentrifugeDataBus<TData = unknown>(\n options: CreateCentrifugeDataBusOptions<TData>\n): CrossTabDataBus<CentrifugeDataBusConfig, TData> {\n const {\n clusterKey,\n connection,\n credentialProvider,\n heartbeatIntervalMs,\n sharedWorkerFactory,\n transferable,\n workerFactory,\n workerMode,\n ...dataBusOptions\n } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new CentrifugeWorkerTransport<TData>({\n ...(workerFactory ? { workerFactory } : {}),\n ...(sharedWorkerFactory ? { sharedWorkerFactory } : {}),\n ...(transferable === undefined ? {} : { transferable }),\n ...(workerMode ? { workerMode } : {}),\n ...(heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs }),\n ...(credentialProvider ? { credentialProvider } : {})\n })\n });\n}\n\n\n/** Create the default dedicated Worker hosting the Centrifuge client. */\nfunction createDefaultWorker(): Worker {\n if (typeof Worker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser Worker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly.'\n );\n }\n return new Worker(workerUrl, {\n name: 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. */\nfunction createDefaultSharedWorker(): SharedWorker {\n if (typeof SharedWorker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser SharedWorker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.shared.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly.'\n );\n }\n return new SharedWorker(workerUrl, {\n name: `${DEFAULT_STORAGE_PREFIX}-shared`,\n type: 'module'\n });\n}\n\n/** Post `message` to a Worker or MessagePort, forwarding `transfer` buffers\n * when present. Both targets share the `postMessage(message, transfer?)`\n * signature, so a single helper eliminates the duplicated if/else at each\n * call site. */\nfunction postToPortLike(\n target: Pick<Worker, 'postMessage'>,\n message: CentrifugeWorkerInput,\n transfer?: ArrayBuffer[]\n): void {\n if (transfer) target.postMessage(message, transfer as Transferable[]);\n else target.postMessage(message);\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * CentrifugeSession \u2014 a reusable wrapper around a single Centrifuge client.\n *\n * Provides a structured-clone-safe message protocol (INIT/SUBSCRIBE/UNSUBSCRIBE/\n * PUBLISH/STOP) so the same session class can run inside a Dedicated Worker,\n * a SharedWorker port, or directly on the main thread as a local fallback.\n */\nimport { Centrifuge } from 'centrifuge';\nimport { parseDataBusPublication } from './core/publication';\nimport type {\n PublicationContext,\n StateContext,\n Subscription,\n SubscriptionErrorContext\n} from 'centrifuge';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, WORKER_STATUS } from './utils/constants';\nimport { publicationMetadata } from './utils/metadata';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\n\n/** Callback interface for posting messages back to the transport layer. */\nexport interface CentrifugeSessionSink<TData = unknown> {\n post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void;\n}\n\n/** A pending getToken/getChannelToken request awaiting a main-thread response. */\ninterface PendingTokenRequest {\n resolve: (token: string) => void;\n reject: (error: unknown) => void;\n}\n\n/** Centrifuge client options for token-bridge mode: the structured-clone-safe\n * subset plus the two function-valued hooks that Centrifuge invokes for fresh\n * credentials. Used only inside the session, never across the Worker boundary. */\ntype CentrifugeBridgedOptions = Omit<CentrifugeWorkerConfig, 'getToken' | 'getChannelToken'> & {\n getToken: () => Promise<string>;\n getChannelToken: (channel: string) => Promise<string>;\n};\n\n/**\n * Stateful Centrifuge client wrapper shared by Dedicated Worker, SharedWorker\n * ports and the main-thread local fallback. Each session owns one connection.\n */\nexport class CentrifugeSession<TData = unknown> {\n private client: Centrifuge | null = null;\n private readonly subscriptions = new Map<string, Subscription>();\n private transferable = false;\n private tokenBridge = false;\n private nextRequestId = 1;\n // Bumped on every initialize/stop. Async client callbacks capture the value\n // from the connection that created them so a stopped client cannot emit into\n // a later session instance (including the main-thread local fallback).\n private lifecycle = 0;\n private readonly pendingTokenRequests = new Map<number, PendingTokenRequest>();\n\n constructor(private readonly sink: CentrifugeSessionSink<TData>) {}\n\n /** Dispatch an incoming Worker message to the matching operation.\n * Unknown message types are ignored rather than thrown, so a future protocol\n * extension adding a new variant cannot crash an older session. */\n handle(message: CentrifugeWorkerInput): void {\n switch (message.type) {\n case CENTRIFUGE_INPUT_TYPE.INIT:\n this.initialize(message.url, message.config, message.transferable === true, message.tokenBridge === true);\n return;\n case CENTRIFUGE_INPUT_TYPE.SUBSCRIBE:\n this.subscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE:\n this.unsubscribe(message.topic);\n return;\n case CENTRIFUGE_INPUT_TYPE.PUBLISH:\n case CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN:\n // Binary and JSON publish share the same Centrifuge client call; the\n // transport layer decides whether to transfer the ArrayBuffer.\n this.publish(message.topic, message.data, message.messageId, message.timestamp);\n return;\n case CENTRIFUGE_INPUT_TYPE.STOP:\n this.stop();\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE:\n this.resolveToken(message.requestId, message.token);\n return;\n case CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR:\n this.rejectToken(message.requestId, deserializeWorkerError(message.error));\n return;\n default:\n return;\n }\n }\n\n /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */\n private initialize(url: string, config: CentrifugeWorkerConfig, transferable: boolean, tokenBridge: boolean): void {\n if (this.client) return;\n const lifecycle = ++this.lifecycle;\n this.transferable = transferable;\n this.tokenBridge = tokenBridge;\n const clientOptions: CentrifugeWorkerConfig | CentrifugeBridgedOptions = tokenBridge\n ? {\n ...config,\n // Route credential fetches back to the main thread, where the\n // application's credentialProvider lives (config must stay\n // structured-cloneable, so functions cannot cross the boundary).\n getToken: () => this.requestToken('token', lifecycle),\n getChannelToken: (channel: string) => this.requestToken('channelToken', lifecycle, channel)\n }\n : config;\n const client = new Centrifuge(url, clientOptions);\n this.client = client;\n client.on('state', (context: StateContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED });\n });\n client.on('disconnected', () => {\n if (this.lifecycle !== lifecycle) return;\n this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });\n });\n client.on('error', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n // Client-level publications are only for server-side subscriptions (where\n // no client Subscription object exists). For topics we have an active\n // subscription for, the subscription-level 'publication' listener handles\n // dispatch \u2014 skip here to avoid delivering the same message twice.\n client.on('publication', (context: PublicationContext) => {\n if (this.lifecycle !== lifecycle) return;\n const topic = context.channel || getPayloadTopic(context.data);\n if (!topic || this.subscriptions.has(topic)) return;\n this.postPublication(topic, context.data);\n });\n client.connect();\n }\n\n /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.\n * Listeners are only registered once per subscription object \u2014 a repeated\n * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,\n * avoiding the removeAllListeners + re-on churn on every duplicate message. */\n private subscribe(topic: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n const lifecycle = this.lifecycle;\n // If we already track this subscription, it already has our listeners \u2014\n // a duplicate SUBSCRIBE is a no-op (idempotent), matching the transport\n // contract. Only a fresh subscription needs listener wiring.\n const existing = this.subscriptions.get(topic);\n if (existing) {\n existing.subscribe();\n return;\n }\n let subscription = this.client.getSubscription(topic);\n if (!subscription) subscription = this.client.newSubscription(topic);\n // Remove only our own listeners so that any Centrifuge internal listeners\n // on the subscription object are preserved. Each subscription event carries\n // a single listener so removeAllListeners(\u2026) is safe here.\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.set(topic, subscription);\n subscription.on('publication', context => {\n if (this.lifecycle !== lifecycle) return;\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => {\n if (this.lifecycle !== lifecycle) return;\n this.postError(context);\n });\n subscription.on('unsubscribed', () => {\n if (this.lifecycle === lifecycle) this.subscriptions.delete(topic);\n });\n subscription.subscribe();\n }\n\n /** Unsubscribe from a Centrifuge channel and clean up the local reference.\n * Listeners are removed before unsubscribing so a late `unsubscribed` event\n * cannot delete a subscription that a subsequent `subscribe()` re-added. */\n private unsubscribe(topic: string): void {\n const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);\n if (!subscription) return;\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.delete(topic);\n subscription.unsubscribe();\n }\n\n /** Publish a message to the Centrifuge channel. */\n private publish(topic: string, data: unknown, messageId?: string, timestamp?: number): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n const lifecycle = this.lifecycle;\n // Centrifuge's payload is application-defined. Preserve legacy payloads;\n // when an ID is requested, send a small metadata envelope that compatible\n // servers can echo back for end-to-end deduplication.\n const hasMetadata = messageId !== undefined || timestamp !== undefined;\n const payload = hasMetadata\n ? {\n data,\n ...(messageId === undefined ? {} : { messageId }),\n ...(timestamp === undefined ? {} : { timestamp })\n }\n : data;\n void this.client.publish(topic, payload).catch(error => {\n if (this.lifecycle === lifecycle) this.postError(error);\n });\n }\n\n /** Forward a publication to the transport. Binary payloads take the\n * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything\n * else is structured-cloned via `MESSAGE`. An empty topic means the\n * publication carried no channel info and is silently dropped. */\n 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 * \u9519\u8BEF\u5E8F\u5217\u5316\u5DE5\u5177 \u2014\u2014 \u5728 Worker \u8FB9\u754C\u5F80\u8FD4\u4F20\u8F93 Error\u3002\n *\n * Error \u5B9E\u4F8B\u65E0\u6CD5\u901A\u8FC7 postMessage \u7ED3\u6784\u5316\u514B\u9686\uFF0C\u6240\u4EE5 CentrifugeSession \u4FA7\u7528\n * serializeError \u628A\u5B83\u538B\u6210 plain object\uFF0C\u4E3B\u7EBF\u7A0B\u4FA7\u7528 deserializeWorkerError\n * \u8FD8\u539F\u6210\u771F Error\uFF08\u5E26 name/stack\uFF09\u3002\u6B64\u524D\u4E24\u4E2A\u51FD\u6570\u5206\u522B\u5185\u8054\u5728 centrifuge-session.ts\n * \u4E0E centrifuge.ts\uFF0C\u6B64\u5904\u7EDF\u4E00\u5B9A\u4E49\u5E76\u4ECE\u4E24\u5904\u5F15\u7528\u3002\n */\n\n/** Error object serialized for cross-thread transfer. */\nexport interface SerializedWorkerError {\n /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */\n name: string;\n /** The Error's `message`. */\n message: string;\n /** The Error's `stack` if available (for debugging). */\n stack?: string;\n /** Arbitrary context attached by the Worker (e.g. the failing operation). */\n context?: unknown;\n}\n\n/** Convert an arbitrary error into a structured-cloneable form for postMessage.\n * A non-Error value is attached as `context`, but only when it is itself\n * structured-cloneable: a function, symbol, or an object/map holding one would\n * otherwise make the serialised error unserializable, so `postMessage` would\n * throw `DataCloneError` while reporting the original failure. */\nexport function serializeError(error: unknown): SerializedWorkerError {\n if (error instanceof Error) {\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {})\n };\n }\n const context = error === undefined ? undefined : error;\n return {\n name: 'CentrifugeError',\n message: typeof error === 'string' ? error : 'Centrifuge worker operation failed.',\n ...(context === undefined || !isStructuredCloneable(context) ? {} : { context })\n };\n}\n\n/** True when `value` survives a structured clone. When the runtime has no\n * `structuredClone`, cloneability cannot be checked and the value is kept\n * (best effort); the eventual postMessage would fail either way. */\nfunction isStructuredCloneable(value: unknown): boolean {\n if (typeof structuredClone !== 'function') return true;\n try {\n structuredClone(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Reconstruct an Error instance from its serialised form. */\nexport function deserializeWorkerError(error: SerializedWorkerError): Error {\n const result = new Error(error.message);\n result.name = error.name;\n if (error.stack) result.stack = error.stack;\n if (error.context !== undefined) Object.assign(result, { context: error.context });\n return result;\n}\n", "/**\n * Worker-thread protocol for Centrifuge WebSocket transport.\n *\n * Defines the message types exchanged between the main thread and a Web Worker\n * (dedicated or shared) that runs a centrifuge client. The worker is isolated\n * from the main thread so that WebSocket lifecycle, token refresh, and binary\n * data handling never block the UI.\n *\n * The message `type` discriminant values are derived from the constants in\n * `utils/constants.ts` so main-thread and Worker sides reference the same\n * values and can never drift apart.\n */\nimport type { Options } from 'centrifuge';\nimport type { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE } from './utils/constants';\nimport type { WorkerStatus } from './core/types';\nimport type { SerializedWorkerError } from './utils/error-utils';\n\nexport type { SerializedWorkerError } from './utils/error-utils';\n\n/**\n * Default interval between main-thread PING heartbeats to a SharedWorker. The\n * SharedWorker reaps a silent port after `SESSION_TIMEOUT_MULTIPLIER` intervals.\n * Shared here so the main thread (heartbeat sender) and the SharedWorker (reaper)\n * always agree on the cadence.\n */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;\n/** SharedWorker per-port session timeout, expressed as a multiple of that port's\n * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of\n * several intervals tolerates throttled tabs without reaping healthy sessions. */\nexport const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;\n/** Default per-port session timeout in ms, derived from the heartbeat interval\n * and the multiplier. Used as the fallback when a port sends no INIT config. */\nexport const DEFAULT_SESSION_TIMEOUT_MS =\n DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;\n\n// Centrifuge Options that reference browser APIs (WebSocket, EventSource, etc.)\n// are unavailable inside a Worker \u2014 the worker uses its own WebSocket import.\n// These are stripped from the config sent to the worker so the type system\n// prevents accidentally passing a main-thread-only function (which would fail\n// structured cloning and throw a DataCloneError).\ntype WorkerUnsafeOption =\n | 'eventsource'\n | 'fetch'\n | 'getData'\n | 'getToken'\n | 'networkEventTarget'\n | 'readableStream'\n | 'sockjs'\n | 'websocket';\n\n/** Centrifuge options safe to pass into a Worker; unsafe options are explicitly\n * excluded and set to `never` so the compiler rejects them at the call site. */\nexport type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {\n [Key in WorkerUnsafeOption]?: never;\n};\n\n/**\n * Async credential provider run on the main thread. Centrifuge client options\n * are structured-cloned into the Worker, so function-valued `getToken` /\n * `getChannelToken` cannot travel with the config. When a provider is set, the\n * Worker asks the main thread for each fresh token over a TOKEN_REQUEST /\n * TOKEN_RESPONSE exchange and this provider supplies it from application\n * context (async token endpoints, SecureStorage, etc.).\n */\nexport interface CentrifugeCredentialProvider {\n /** Return a fresh top-level connection token. Called on connect and refresh. */\n getToken: () => MaybePromise<string | null | undefined>;\n /** Return a channel token for a channel. Centrifuge falls back to `getToken`\n * when channel tokens are not configured by the server. */\n getChannelToken?: (channel: string) => MaybePromise<string | null | undefined>;\n}\n\n/** A value or a promise of a value. */\nexport type MaybePromise<T> = T | Promise<T>;\n\n/** Messages sent from the main thread to the Worker. All variants are\n * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */\nexport type CentrifugeWorkerInput =\n /** Initial connection: URL + config. Sent once per backend creation.\n * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.\n * `heartbeatIntervalMs` overrides the SharedWorker PING cadence.\n * `tokenBridge` asks the Worker to route getToken/getChannelToken back to\n * the main thread instead of holding function-valued config. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.INIT; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number; tokenBridge?: boolean }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.SUBSCRIBE; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH; topic: string; data: unknown; messageId?: string; timestamp?: number }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.PING }\n /** Disconnect the client and clear all subscriptions. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.STOP }\n /** Fresh credential for an earlier TOKEN_REQUEST, resolved by requestId. */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE; requestId: number; token: string }\n /** A credential request failed (provider rejected or returned no token). */\n | { type: typeof CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR; requestId: number; error: SerializedWorkerError };\n\n/** Messages sent from the Worker back to the main thread. The main thread\n * routes these to the DataBusTransportHandlers via handleOutput(). */\nexport type CentrifugeWorkerOutput<TData = unknown> =\n /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.STATUS; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE; topic: string; data: TData; messageId?: string; timestamp?: number }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN; topic: string; data: ArrayBuffer; messageId?: string; timestamp?: number }\n /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.ERROR; error: SerializedWorkerError }\n /** The Worker needs a fresh credential. The main thread resolves it via a\n * matching TOKEN_RESPONSE / TOKEN_ERROR carrying the same `requestId`. */\n | { type: typeof CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST; requestId: number; kind: 'token' | 'channelToken'; channel?: string };\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends, selected in order of preference:\n * 1. SharedWorker \u2014 one WebSocket per tab session, hosted in a shared process\n * 2. Dedicated Worker \u2014 one WebSocket per tab\n * 3. In-process (local) \u2014 Centrifuge runs on the main thread (fallback when\n * neither Worker type is available, e.g. in non-browser environments)\n *\n * Binary data (ArrayBuffer) can be transferred via Transferable when the\n * `transferable` option is enabled, avoiding structured-clone overhead.\n */\n\nimport { CrossTabDataBus } from './core/data-bus';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type { DataBusTransport, DataBusTransportHandlers } from './core/types';\nimport { CentrifugeSession } from './centrifuge-session';\nimport { selectWorkerBackend } from './worker-mode';\nimport type { WorkerBackend, WorkerMode } from './worker-mode';\nimport type {\n CentrifugeCredentialProvider,\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput\n} from './centrifuge-protocol';\nimport type { DataBusPublishOptions } from './core/types';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\nimport { CENTRIFUGE_INPUT_TYPE, CENTRIFUGE_OUTPUT_TYPE, DEFAULT_STORAGE_PREFIX, WORKER_BACKEND, WORKER_MODE, WORKER_STATUS } from './utils/constants';\nimport { deserializeWorkerError, serializeError } from './utils/error-utils';\nimport { publicationMetadata } from './utils/metadata';\nimport { assertHeartbeatInterval, assertStructuredCloneable } from './utils/validation';\n\nexport type { CentrifugeCredentialProvider, CentrifugeWorkerConfig, SerializedWorkerError } from './centrifuge-protocol';\nexport type { WorkerBackend, WorkerMode } from './worker-mode';\n\n/** WebSocket connection parameters passed to the Centrifuge Worker. */\nexport interface CentrifugeDataBusConfig {\n /** Centrifuge server WebSocket URL. */\n url: string;\n /** Centrifuge client options (token, channel params, etc.). */\n options?: CentrifugeWorkerConfig;\n}\n\n/** Options for configuring the Worker backend (dedicated, shared, or local). */\nexport interface CentrifugeWorkerTransportOptions {\n /** Custom dedicated Worker factory. Used for testing or bundler integration. */\n workerFactory?: () => Worker;\n /** Custom SharedWorker factory. */\n sharedWorkerFactory?: () => SharedWorker;\n /** Preferred Worker mode: 'dedicated', 'shared', or 'auto'. */\n workerMode?: WorkerMode;\n /** Enable transferable (ArrayBuffer) support for binary data. */\n transferable?: boolean;\n /** Interval (ms) between PING heartbeats sent to the SharedWorker. The\n * SharedWorker reaps a silent port after `DEFAULT_SESSION_TIMEOUT_MULTIPLIER`\n * \u00D7 this interval. Pass `Infinity` to disable heartbeats entirely. Defaults\n * to `DEFAULT_HEARTBEAT_INTERVAL_MS`. */\n heartbeatIntervalMs?: number;\n /** Async credential provider. Runs on the main thread: the Worker cannot\n * hold function-valued Centrifuge options (they are not structured-cloneable),\n * so when a provider is set the Worker asks for each fresh token over a\n * TOKEN_REQUEST / TOKEN_RESPONSE bridge and this provider supplies it. */\n credentialProvider?: CentrifugeCredentialProvider;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a Centrifuge transport. */\nexport interface CreateCentrifugeDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<CentrifugeDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n >,\n CentrifugeWorkerTransportOptions {\n /** Centrifuge connection configuration. */\n connection: CentrifugeDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/**\n * Transport layer that runs a Centrifuge WebSocket client inside a Web Worker.\n *\n * Delegates the actual WebSocket connection to a Worker (dedicated or shared)\n * or falls back to an in-process CentrifugeSession. The Worker is isolated from\n * the main thread so that WebSocket lifecycle, token refresh, and binary data\n * handling never block the UI.\n */\nexport class CentrifugeWorkerTransport<TData = unknown>\n implements DataBusTransport<CentrifugeDataBusConfig, TData>\n{\n readonly diagnosticsName = 'centrifuge';\n private readonly workerMode: WorkerMode;\n private readonly transferable: boolean;\n private readonly heartbeatIntervalMs: number;\n private readonly workerFactory: (() => Worker) | undefined;\n private readonly sharedWorkerFactory: (() => SharedWorker) | undefined;\n private readonly credentialProvider: CentrifugeCredentialProvider | undefined;\n private backend: WorkerBackend | null = null;\n private worker: Worker | null = null;\n private sharedWorker: SharedWorker | null = null;\n private port: MessagePort | null = null;\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n private localSession: CentrifugeSession<TData> | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n // Monotonically increasing counter, bumped each time a backend is created or\n // the transport stops. Only the asynchronous credential bridge compares it:\n // a provider may settle after the Worker it was answering is gone. Worker\n // error events need no such check \u2014 stop() and onWorkerFailed() remove those\n // listeners before the generation moves on, so a superseded backend can no\n // longer reach this object at all.\n private generation = 0;\n\n get diagnosticsBackend(): string {\n return this.backend ?? 'uninitialized';\n }\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? WORKER_MODE.DEDICATED;\n this.transferable = options.transferable ?? false;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n assertHeartbeatInterval(this.heartbeatIntervalMs);\n this.workerFactory = options.workerFactory;\n this.sharedWorkerFactory = options.sharedWorkerFactory;\n this.credentialProvider = options.credentialProvider;\n }\n\n /**\n * Start the transport: select a backend, initialise the Worker (or local\n * session), and send the INIT message with connection parameters.\n */\n start(config: CentrifugeDataBusConfig, handlers: DataBusTransportHandlers<TData>): void {\n if (this.backend) return;\n assertStructuredCloneable(config.options ?? {});\n this.handlers = handlers;\n // Runtime capability OR an injected factory counts as availability; a\n // factory-less browser must use the bundled default Workers instead of\n // silently degrading to the local session.\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined || typeof Worker !== 'undefined',\n sharedWorker: this.sharedWorkerFactory !== undefined || typeof SharedWorker !== 'undefined'\n });\n const input = this.buildInitInput(config);\n if (backend === WORKER_BACKEND.SHARED) {\n this.startSharedWorker(input);\n this.backend = WORKER_BACKEND.SHARED;\n return;\n }\n if (backend === WORKER_BACKEND.DEDICATED) {\n this.startDedicatedWorker(input);\n this.backend = WORKER_BACKEND.DEDICATED;\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = WORKER_BACKEND.LOCAL;\n }\n\n subscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.SUBSCRIBE, topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.UNSUBSCRIBE, topic });\n }\n\n /**\n * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable\n * when `transferable` is enabled, avoiding a structured-clone cycle.\n */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): void {\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH_BIN, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) }, [data]);\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PUBLISH, topic, data, ...publicationMetadata(options?.messageId, options?.timestamp) });\n }\n\n /**\n * Gracefully stop the transport: send STOP, clean up event listeners, and\n * terminate the Worker (or close the SharedWorker port).\n */\n stop(): void {\n if (!this.backend) return;\n this.generation++;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.STOP });\n this.clearHeartbeat();\n if (this.worker) {\n this.worker.removeEventListener('message', this.handleMessage);\n this.worker.removeEventListener('error', this.handleWorkerError);\n this.worker.terminate();\n }\n if (this.sharedWorker) {\n this.detachSharedWorkerListeners();\n this.port?.close();\n }\n this.resetBackend();\n this.handlers = null;\n }\n\n /** Build the INIT payload sent to the Worker / local session. Optional fields\n * are only included when they deviate from the defaults, so the Worker's own\n * default-resolution logic kicks in for the common case. */\n private buildInitInput(config: CentrifugeDataBusConfig): CentrifugeWorkerInput {\n return {\n type: CENTRIFUGE_INPUT_TYPE.INIT,\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.credentialProvider ? { tokenBridge: true } : {}),\n ...(this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS\n ? { heartbeatIntervalMs: this.heartbeatIntervalMs }\n : {})\n };\n }\n\n /** Create and initialise a dedicated Worker, then send the INIT message. */\n private startDedicatedWorker(input: CentrifugeWorkerInput): void {\n this.generation += 1;\n const worker = (this.workerFactory ?? createDefaultWorker)();\n this.worker = worker;\n worker.addEventListener('message', this.handleMessage);\n worker.addEventListener('error', this.handleWorkerError);\n worker.postMessage(input);\n }\n\n /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */\n private startSharedWorker(input: CentrifugeWorkerInput): void {\n this.generation += 1;\n const shared = (this.sharedWorkerFactory ?? createDefaultSharedWorker)();\n this.sharedWorker = shared;\n const port = shared.port;\n this.port = port;\n port.addEventListener('message', this.handleMessage);\n port.addEventListener('messageerror', this.handlePortError);\n shared.addEventListener('error', this.handleSharedWorkerError);\n port.start();\n port.postMessage(input);\n this.startHeartbeat();\n }\n\n /** Handle a message event from the Worker (dedicated or shared). */\n private readonly handleMessage = (event: MessageEvent<CentrifugeWorkerOutput<TData>>) => {\n this.handleOutput(event.data);\n };\n\n /** Handle a message from the in-process CentrifugeSession (local fallback). */\n private readonly handleSessionOutput = (message: CentrifugeWorkerOutput<TData>) => {\n this.handleOutput(message);\n };\n\n /** Route a Worker output message to the appropriate handler callback.\n * Shared by the Worker message listener, the SharedWorker port listener,\n * and the local-session sink \u2014 all three feed into this single dispatcher. */\n private handleOutput(message: CentrifugeWorkerOutput<TData>): void {\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.STATUS) this.handlers?.onStatus(message.status);\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE) this.handlers?.onMessage({ topic: message.topic, data: message.data, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.MESSAGE_BIN) this.handlers?.onMessage({ topic: message.topic, data: message.data as TData, ...publicationMetadata(message.messageId, message.timestamp) });\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.ERROR) this.handlers?.onError(deserializeWorkerError(message.error));\n if (message.type === CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST) this.resolveTokenRequest(message.requestId, message.kind, message.channel);\n }\n\n /** Resolve a Worker credential request from the main thread: call the\n * credentialProvider, then post the fresh token (or a serialized failure)\n * back to the session that issued the request. */\n private resolveTokenRequest(requestId: number, kind: 'token' | 'channelToken', channel: string | undefined): void {\n // Credential providers may be asynchronous. Capture the backend identity\n // before awaiting so a stop/start that replaces the Worker cannot receive\n // a late token (or error) belonging to the superseded session. Request IDs\n // restart at 1 for a fresh CentrifugeSession, so without this guard a stale\n // reply can incorrectly satisfy the replacement session's request.\n const generation = this.generation;\n const worker = this.worker;\n const port = this.port;\n const localSession = this.localSession;\n const isCurrentBackend = () =>\n this.generation === generation &&\n this.worker === worker &&\n this.port === port &&\n this.localSession === localSession;\n const provider = this.credentialProvider;\n let value: string | null | Promise<string | null | undefined> | undefined;\n try {\n value = kind === 'channelToken' && provider?.getChannelToken\n ? provider.getChannelToken(channel ?? '')\n : provider?.getToken?.();\n } catch (error) {\n // This arm never yields, but the provider is application code and can\n // stop or replace the transport synchronously before throwing. Without\n // the check `post()` then addresses a backend that no longer exists.\n if (isCurrentBackend()) {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n return;\n }\n Promise.resolve(value).then(\n token => {\n if (!isCurrentBackend()) return;\n if (typeof token !== 'string' || token.length === 0) {\n this.post({\n type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR,\n requestId,\n error: serializeError(new Error(`The credential provider returned no ${kind}.`))\n });\n return;\n }\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE, requestId, token });\n },\n error => {\n if (!isCurrentBackend()) return;\n this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });\n }\n );\n }\n\n /** Handle a Worker-level failure (crash, message decode error). Discards the\n * dead backend so a later start()/reopen can rebuild from scratch, and\n * signals an error status so the DataBus can trigger recovery. A superseded\n * backend cannot reach this: its listeners are removed before the transport\n * moves on, which is what keeps late Worker errors from tearing down a fresh\n * session (pinned by 'ignores error events from a superseded worker'). */\n private onWorkerFailed(message: string): void {\n // Remove the message listener and release the old backend before recovery\n // reopens, so late messages from the failed Worker/port cannot be routed\n // into the freshly started session.\n this.worker?.removeEventListener('message', this.handleMessage);\n this.worker?.removeEventListener('error', this.handleWorkerError);\n this.worker?.terminate();\n this.detachSharedWorkerListeners();\n this.port?.close();\n this.clearHeartbeat();\n this.resetBackend();\n this.handlers?.onError(new Error(message));\n this.handlers?.onStatus(WORKER_STATUS.ERROR);\n }\n\n /** Remove every listener attached to the current SharedWorker and its port. */\n private detachSharedWorkerListeners(): void {\n this.port?.removeEventListener('message', this.handleMessage);\n this.port?.removeEventListener('messageerror', this.handlePortError);\n this.sharedWorker?.removeEventListener('error', this.handleSharedWorkerError);\n }\n\n /** Periodically ping the SharedWorker so its session reaper can detect a dead tab. */\n private startHeartbeat(): void {\n if (this.heartbeatHandle !== null) return;\n // Infinite disables the heartbeat (e.g. for environment where the\n // SharedWorker reaper is not needed).\n if (this.heartbeatIntervalMs === Infinity) return;\n this.heartbeatHandle = setInterval(() => {\n this.post({ type: CENTRIFUGE_INPUT_TYPE.PING });\n }, this.heartbeatIntervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatHandle !== null) clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n\n private readonly handleWorkerError = () => {\n this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\n this.onWorkerFailed('Centrifuge shared worker failed.');\n };\n\n /** Clear the Worker/port/backend references after a failure or stop. */\n private resetBackend(): void {\n this.worker = null;\n this.sharedWorker = null;\n this.port = null;\n this.backend = null;\n this.localSession = null;\n }\n\n /**\n * Post a message to the active backend. Accepts optional Transferable buffers\n * for zero-copy ArrayBuffer transfer.\n */\n private post(message: CentrifugeWorkerInput, transfer?: ArrayBuffer[]): void {\n if (this.worker) {\n postToPortLike(this.worker, message, transfer);\n return;\n }\n if (this.port) {\n postToPortLike(this.port, message, transfer);\n return;\n }\n if (this.localSession) {\n this.localSession.handle(message);\n return;\n }\n throw new Error('CentrifugeWorkerTransport.start() must be called first.');\n }\n}\n\n/**\n * Create a fully-configured CrossTabDataBus with a Centrifuge WebSocket transport.\n *\n * This is the primary entry point for consumers. It wires up the transport,\n * cluster coordination, and lifecycle management:\n *\n * ```ts\n * const bus = createCentrifugeDataBus({\n * connection: { url: 'wss://example.com/connection/websocket', options: { token: '\u2026' } },\n * trace: { enabled: true, sink: event => console.log(event) },\n * });\n * ```\n */\nexport function createCentrifugeDataBus<TData = unknown>(\n options: CreateCentrifugeDataBusOptions<TData>\n): CrossTabDataBus<CentrifugeDataBusConfig, TData> {\n const {\n clusterKey,\n connection,\n credentialProvider,\n heartbeatIntervalMs,\n sharedWorkerFactory,\n transferable,\n workerFactory,\n workerMode,\n ...dataBusOptions\n } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new CentrifugeWorkerTransport<TData>({\n ...(workerFactory ? { workerFactory } : {}),\n ...(sharedWorkerFactory ? { sharedWorkerFactory } : {}),\n ...(transferable === undefined ? {} : { transferable }),\n ...(workerMode ? { workerMode } : {}),\n ...(heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs }),\n ...(credentialProvider ? { credentialProvider } : {})\n })\n });\n}\n\n/** Create the default dedicated Worker hosting the Centrifuge client. There is\n * no `typeof Worker` guard: `start()` asks `selectWorkerBackend` with\n * `worker: workerFactory !== undefined || typeof Worker !== 'undefined'`, that\n * function never returns a Worker backend its availability flags deny, and these\n * factories are only reached for a backend it did return. The degradation is\n * pinned where it is decided \u2014 `tests/worker-mode.test.ts` on the selector, and\n * \"falls back to the local session when the platform lacks both Worker APIs\" on\n * the transport \u2014 so a guard here could only ever re-check a fact the caller has\n * already established, and no runtime can reach it with the global missing. */\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. */\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": ";;;;;;;;;;;;;;;;;;AAOA,SAAS,kBAAkB;;;ACmBpB,SAAS,eAAe,OAAuC;AACpE,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,MACf,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AACA,QAAM,UAAU,UAAU,SAAY,SAAY;AAClD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,OAAO,UAAU,WAAW,QAAQ;AAAA,IAC7C,GAAI,YAAY,UAAa,CAAC,sBAAsB,OAAO,IAAI,CAAC,IAAI,EAAE,QAAQ;AAAA,EAChF;AACF;AAKA,SAAS,sBAAsB,OAAyB;AACtD,MAAI,OAAO,oBAAoB,WAAY,QAAO;AAClD,MAAI;AACF,oBAAgB,KAAK;AACrB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,uBAAuB,OAAqC;AAC1E,QAAM,SAAS,IAAI,MAAM,MAAM,OAAO;AACtC,SAAO,OAAO,MAAM;AACpB,MAAI,MAAM,MAAO,QAAO,QAAQ,MAAM;AACtC,MAAI,MAAM,YAAY,OAAW,QAAO,OAAO,QAAQ,EAAE,SAAS,MAAM,QAAQ,CAAC;AACjF,SAAO;AACT;;;ADfO,IAAM,oBAAN,MAAyC;AAAA,EAY9C,YAA6B,MAAoC;AAApC;AAAA,EAAqC;AAAA,EAArC;AAAA,EAXrB,SAA4B;AAAA,EACnB,gBAAgB,oBAAI,IAA0B;AAAA,EACvD,eAAe;AAAA,EACf,cAAc;AAAA,EACd,gBAAgB;AAAA;AAAA;AAAA;AAAA,EAIhB,YAAY;AAAA,EACH,uBAAuB,oBAAI,IAAiC;AAAA;AAAA;AAAA;AAAA,EAO7E,OAAO,SAAsC;AAC3C,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK,sBAAsB;AACzB,aAAK,WAAW,QAAQ,KAAK,QAAQ,QAAQ,QAAQ,iBAAiB,MAAM,QAAQ,gBAAgB,IAAI;AACxG;AAAA,MACF,KAAK,sBAAsB;AACzB,aAAK,UAAU,QAAQ,KAAK;AAC5B;AAAA,MACF,KAAK,sBAAsB;AACzB,aAAK,YAAY,QAAQ,KAAK;AAC9B;AAAA,MACF,KAAK,sBAAsB;AAAA,MAC3B,KAAK,sBAAsB;AAGzB,aAAK,QAAQ,QAAQ,OAAO,QAAQ,MAAM,QAAQ,WAAW,QAAQ,SAAS;AAC9E;AAAA,MACF,KAAK,sBAAsB;AACzB,aAAK,KAAK;AACV;AAAA,MACF,KAAK,sBAAsB;AACzB,aAAK,aAAa,QAAQ,WAAW,QAAQ,KAAK;AAClD;AAAA,MACF,KAAK,sBAAsB;AACzB,aAAK,YAAY,QAAQ,WAAW,uBAAuB,QAAQ,KAAK,CAAC;AACzE;AAAA,MACF;AACE;AAAA,IACJ;AAAA,EACF;AAAA;AAAA,EAGQ,WAAW,KAAa,QAAgC,cAAuB,aAA4B;AACjH,QAAI,KAAK,OAAQ;AACjB,UAAM,YAAY,EAAE,KAAK;AACzB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,UAAM,gBAAmE,cACrE;AAAA,MACE,GAAG;AAAA;AAAA;AAAA;AAAA,MAIH,UAAU,MAAM,KAAK,aAAa,SAAS,SAAS;AAAA,MACpD,iBAAiB,CAAC,YAAoB,KAAK,aAAa,gBAAgB,WAAW,OAAO;AAAA,IAC5F,IACA;AACJ,UAAM,SAAS,IAAI,WAAW,KAAK,aAAa;AAChD,SAAK,SAAS;AACd,WAAO,GAAG,SAAS,CAAC,YAA0B;AAC5C,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,KAAK,EAAE,MAAM,uBAAuB,QAAQ,QAAQ,gBAAgB,QAAQ,QAAQ,EAAE,CAAC;AAAA,IAC9F,CAAC;AACD,WAAO,GAAG,aAAa,MAAM;AAC3B,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,KAAK,EAAE,MAAM,uBAAuB,QAAQ,QAAQ,cAAc,UAAU,CAAC;AAAA,IACpF,CAAC;AACD,WAAO,GAAG,gBAAgB,MAAM;AAC9B,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,KAAK,EAAE,MAAM,uBAAuB,QAAQ,QAAQ,cAAc,aAAa,CAAC;AAAA,IACvF,CAAC;AACD,WAAO,GAAG,SAAS,aAAW;AAC5B,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,UAAU,OAAO;AAAA,IACxB,CAAC;AAKD,WAAO,GAAG,eAAe,CAAC,YAAgC;AACxD,UAAI,KAAK,cAAc,UAAW;AAClC,YAAM,QAAQ,QAAQ,WAAW,gBAAgB,QAAQ,IAAI;AAC7D,UAAI,CAAC,SAAS,KAAK,cAAc,IAAI,KAAK,EAAG;AAC7C,WAAK,gBAAgB,OAAO,QAAQ,IAAI;AAAA,IAC1C,CAAC;AACD,WAAO,QAAQ;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,UAAU,OAAqB;AACrC,QAAI,CAAC,KAAK,OAAQ,QAAO,KAAK,UAAU,IAAI,MAAM,uCAAuC,CAAC;AAC1F,UAAM,YAAY,KAAK;AAIvB,UAAM,WAAW,KAAK,cAAc,IAAI,KAAK;AAC7C,QAAI,UAAU;AACZ,eAAS,UAAU;AACnB;AAAA,IACF;AACA,QAAI,eAAe,KAAK,OAAO,gBAAgB,KAAK;AACpD,QAAI,CAAC,aAAc,gBAAe,KAAK,OAAO,gBAAgB,KAAK;AAInE,iBAAa,mBAAmB,aAAa;AAC7C,iBAAa,mBAAmB,OAAO;AACvC,iBAAa,mBAAmB,cAAc;AAC9C,SAAK,cAAc,IAAI,OAAO,YAAY;AAC1C,iBAAa,GAAG,eAAe,aAAW;AACxC,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,gBAAgB,OAAO,QAAQ,IAAI;AAAA,IAC1C,CAAC;AACD,iBAAa,GAAG,SAAS,CAAC,YAAsC;AAC9D,UAAI,KAAK,cAAc,UAAW;AAClC,WAAK,UAAU,OAAO;AAAA,IACxB,CAAC;AACD,iBAAa,GAAG,gBAAgB,MAAM;AACpC,UAAI,KAAK,cAAc,UAAW,MAAK,cAAc,OAAO,KAAK;AAAA,IACnE,CAAC;AACD,iBAAa,UAAU;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,OAAqB;AACvC,UAAM,eAAe,KAAK,cAAc,IAAI,KAAK,KAAK,KAAK,QAAQ,gBAAgB,KAAK;AACxF,QAAI,CAAC,aAAc;AACnB,iBAAa,mBAAmB,aAAa;AAC7C,iBAAa,mBAAmB,OAAO;AACvC,iBAAa,mBAAmB,cAAc;AAC9C,SAAK,cAAc,OAAO,KAAK;AAC/B,iBAAa,YAAY;AAAA,EAC3B;AAAA;AAAA,EAGQ,QAAQ,OAAe,MAAe,WAAoB,WAA0B;AAC1F,QAAI,CAAC,KAAK,OAAQ,QAAO,KAAK,UAAU,IAAI,MAAM,uCAAuC,CAAC;AAC1F,UAAM,YAAY,KAAK;AAIvB,UAAM,cAAc,cAAc,UAAa,cAAc;AAC7D,UAAM,UAAU,cACZ;AAAA,MACE;AAAA,MACA,GAAI,cAAc,SAAY,CAAC,IAAI,EAAE,UAAU;AAAA,MAC/C,GAAI,cAAc,SAAY,CAAC,IAAI,EAAE,UAAU;AAAA,IACjD,IACA;AACJ,SAAK,KAAK,OAAO,QAAQ,OAAO,OAAO,EAAE,MAAM,WAAS;AACtD,UAAI,KAAK,cAAc,UAAW,MAAK,UAAU,KAAK;AAAA,IACxD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,gBAAgB,OAAe,MAAqB;AAC1D,QAAI,CAAC,MAAO;AACZ,QAAI,KAAK,gBAAgB,gBAAgB,aAAa;AACpD,WAAK,KAAK,EAAE,MAAM,uBAAuB,aAAa,OAAO,KAAK,GAAG,CAAC,IAAI,CAAC;AAC3E;AAAA,IACF;AACA,UAAM,cAAc,wBAA+B,MAAM,KAAK;AAC9D,QAAI,CAAC,YAAa;AAClB,SAAK,KAAK;AAAA,MACR,MAAM,uBAAuB;AAAA,MAC7B,OAAO,YAAY;AAAA,MACnB,MAAM,YAAY;AAAA,MAClB,GAAG,oBAAoB,YAAY,WAAW,YAAY,SAAS;AAAA,IACrE,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,OAAa;AAGnB,SAAK,aAAa;AAGlB,eAAW,CAAC,EAAE,OAAO,KAAK,KAAK,sBAAsB;AACnD,cAAQ,OAAO,IAAI,MAAM,gEAAgE,CAAC;AAAA,IAC5F;AACA,SAAK,qBAAqB,MAAM;AAChC,SAAK,QAAQ,WAAW;AACxB,SAAK,cAAc,MAAM;AACzB,SAAK,SAAS;AACd,SAAK,KAAK,EAAE,MAAM,uBAAuB,QAAQ,QAAQ,cAAc,aAAa,CAAC;AAAA,EACvF;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,MAAgC,WAAmB,SAAmC;AAIzG,QAAI,KAAK,cAAc,WAAW;AAChC,aAAO,QAAQ,OAAO,IAAI,MAAM,4EAA4E,CAAC;AAAA,IAC/G;AACA,UAAM,YAAY,KAAK;AACvB,SAAK,iBAAiB;AACtB,WAAO,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC9C,WAAK,qBAAqB,IAAI,WAAW,EAAE,SAAS,OAAO,CAAC;AAC5D,WAAK,KAAK;AAAA,QACR,MAAM,uBAAuB;AAAA,QAC7B;AAAA,QACA;AAAA,QACA,GAAI,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ;AAAA,MAC7C,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,WAAmB,OAAqB;AAC3D,UAAM,UAAU,KAAK,qBAAqB,IAAI,SAAS;AACvD,QAAI,CAAC,QAAS;AACd,SAAK,qBAAqB,OAAO,SAAS;AAC1C,YAAQ,QAAQ,KAAK;AAAA,EACvB;AAAA,EAEQ,YAAY,WAAmB,OAAsB;AAC3D,UAAM,UAAU,KAAK,qBAAqB,IAAI,SAAS;AACvD,QAAI,CAAC,QAAS;AACd,SAAK,qBAAqB,OAAO,SAAS;AAC1C,YAAQ,OAAO,KAAK;AAAA,EACtB;AAAA;AAAA,EAGQ,KAAK,SAAwC,UAAgC;AACnF,SAAK,KAAK,KAAK,SAAS,QAAQ;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,UAAU,OAAsB;AACtC,SAAK,KAAK,EAAE,MAAM,uBAAuB,OAAO,OAAO,eAAe,KAAK,EAAE,CAAC;AAAA,EAChF;AACF;AAKA,SAAS,gBAAgB,MAAuB;AAC9C,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,UAAU;AAEhB,QAAM,OAAO,QAAQ;AACrB,QAAM,SAAS,OAAO,SAAS,YAAY,SAAS,OAAQ,KAAiC,UAAU;AACvG,QAAM,QAAQ,UAAU,QAAQ;AAChC,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAKA,IAAM,cAAc,oBAAI,IAAY,CAAC,cAAc,YAAY,cAAc,SAAS,CAAC;AACvF,SAAS,gBAAgB,QAA6D;AACpF,SAAO,YAAY,IAAI,MAAM,IAAK,SAAwC;AAC5E;;;AExSO,IAAM,gCAAgC;AAItC,IAAM,qCAAqC;AAG3C,IAAM,6BACX,gCAAgC;;;ACqD3B,IAAM,4BAAN,MAEP;AAAA,EACW,kBAAkB;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,UAAgC;AAAA,EAChC,SAAwB;AAAA,EACxB,eAAoC;AAAA,EACpC,OAA2B;AAAA,EAC3B,kBAAyD;AAAA,EACzD,eAAgD;AAAA,EAChD,WAAmD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnD,aAAa;AAAA,EAErB,IAAI,qBAA6B;AAC/B,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEA,YAAY,UAA4C,CAAC,GAAG;AAC1D,SAAK,aAAa,QAAQ,cAAc,YAAY;AACpD,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,sBAAsB,QAAQ,uBAAuB;AAC1D,4BAAwB,KAAK,mBAAmB;AAChD,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,sBAAsB,QAAQ;AACnC,SAAK,qBAAqB,QAAQ;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAiC,UAAiD;AACtF,QAAI,KAAK,QAAS;AAClB,8BAA0B,OAAO,WAAW,CAAC,CAAC;AAC9C,SAAK,WAAW;AAIhB,UAAM,UAAU,oBAAoB,KAAK,YAAY;AAAA,MACnD,QAAQ,KAAK,kBAAkB,UAAa,OAAO,WAAW;AAAA,MAC9D,cAAc,KAAK,wBAAwB,UAAa,OAAO,iBAAiB;AAAA,IAClF,CAAC;AACD,UAAM,QAAQ,KAAK,eAAe,MAAM;AACxC,QAAI,YAAY,eAAe,QAAQ;AACrC,WAAK,kBAAkB,KAAK;AAC5B,WAAK,UAAU,eAAe;AAC9B;AAAA,IACF;AACA,QAAI,YAAY,eAAe,WAAW;AACxC,WAAK,qBAAqB,KAAK;AAC/B,WAAK,UAAU,eAAe;AAC9B;AAAA,IACF;AACA,SAAK,eAAe,IAAI,kBAAyB,EAAE,MAAM,KAAK,oBAAoB,CAAC;AACnF,SAAK,aAAa,OAAO,KAAK;AAC9B,SAAK,UAAU,eAAe;AAAA,EAChC;AAAA,EAEA,UAAU,OAAqB;AAC7B,SAAK,KAAK,EAAE,MAAM,sBAAsB,WAAW,MAAM,CAAC;AAAA,EAC5D;AAAA,EAEA,YAAY,OAAqB;AAC/B,SAAK,KAAK,EAAE,MAAM,sBAAsB,aAAa,MAAM,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,OAAe,MAAe,SAAuC;AAC3E,QAAI,KAAK,gBAAgB,gBAAgB,aAAa;AACpD,WAAK,KAAK,EAAE,MAAM,sBAAsB,aAAa,OAAO,MAAM,GAAG,oBAAoB,SAAS,WAAW,SAAS,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC;AAC1I;AAAA,IACF;AACA,SAAK,KAAK,EAAE,MAAM,sBAAsB,SAAS,OAAO,MAAM,GAAG,oBAAoB,SAAS,WAAW,SAAS,SAAS,EAAE,CAAC;AAAA,EAChI;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAa;AACX,QAAI,CAAC,KAAK,QAAS;AACnB,SAAK;AACL,SAAK,KAAK,EAAE,MAAM,sBAAsB,KAAK,CAAC;AAC9C,SAAK,eAAe;AACpB,QAAI,KAAK,QAAQ;AACf,WAAK,OAAO,oBAAoB,WAAW,KAAK,aAAa;AAC7D,WAAK,OAAO,oBAAoB,SAAS,KAAK,iBAAiB;AAC/D,WAAK,OAAO,UAAU;AAAA,IACxB;AACA,QAAI,KAAK,cAAc;AACrB,WAAK,4BAA4B;AACjC,WAAK,MAAM,MAAM;AAAA,IACnB;AACA,SAAK,aAAa;AAClB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,QAAwD;AAC7E,WAAO;AAAA,MACL,MAAM,sBAAsB;AAAA,MAC5B,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO,WAAW,CAAC;AAAA,MAC3B,GAAI,KAAK,eAAe,EAAE,cAAc,KAAK,IAAI,CAAC;AAAA,MAClD,GAAI,KAAK,qBAAqB,EAAE,aAAa,KAAK,IAAI,CAAC;AAAA,MACvD,GAAI,KAAK,wBAAwB,gCAC7B,EAAE,qBAAqB,KAAK,oBAAoB,IAChD,CAAC;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGQ,qBAAqB,OAAoC;AAC/D,SAAK,cAAc;AACnB,UAAM,UAAU,KAAK,iBAAiB,qBAAqB;AAC3D,SAAK,SAAS;AACd,WAAO,iBAAiB,WAAW,KAAK,aAAa;AACrD,WAAO,iBAAiB,SAAS,KAAK,iBAAiB;AACvD,WAAO,YAAY,KAAK;AAAA,EAC1B;AAAA;AAAA,EAGQ,kBAAkB,OAAoC;AAC5D,SAAK,cAAc;AACnB,UAAM,UAAU,KAAK,uBAAuB,2BAA2B;AACvE,SAAK,eAAe;AACpB,UAAM,OAAO,OAAO;AACpB,SAAK,OAAO;AACZ,SAAK,iBAAiB,WAAW,KAAK,aAAa;AACnD,SAAK,iBAAiB,gBAAgB,KAAK,eAAe;AAC1D,WAAO,iBAAiB,SAAS,KAAK,uBAAuB;AAC7D,SAAK,MAAM;AACX,SAAK,YAAY,KAAK;AACtB,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA,EAGiB,gBAAgB,CAAC,UAAuD;AACvF,SAAK,aAAa,MAAM,IAAI;AAAA,EAC9B;AAAA;AAAA,EAGiB,sBAAsB,CAAC,YAA2C;AACjF,SAAK,aAAa,OAAO;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,SAA8C;AACjE,QAAI,QAAQ,SAAS,uBAAuB,OAAQ,MAAK,UAAU,SAAS,QAAQ,MAAM;AAC1F,QAAI,QAAQ,SAAS,uBAAuB,QAAS,MAAK,UAAU,UAAU,EAAE,OAAO,QAAQ,OAAO,MAAM,QAAQ,MAAM,GAAG,oBAAoB,QAAQ,WAAW,QAAQ,SAAS,EAAE,CAAC;AACxL,QAAI,QAAQ,SAAS,uBAAuB,YAAa,MAAK,UAAU,UAAU,EAAE,OAAO,QAAQ,OAAO,MAAM,QAAQ,MAAe,GAAG,oBAAoB,QAAQ,WAAW,QAAQ,SAAS,EAAE,CAAC;AACrM,QAAI,QAAQ,SAAS,uBAAuB,MAAO,MAAK,UAAU,QAAQ,uBAAuB,QAAQ,KAAK,CAAC;AAC/G,QAAI,QAAQ,SAAS,uBAAuB,cAAe,MAAK,oBAAoB,QAAQ,WAAW,QAAQ,MAAM,QAAQ,OAAO;AAAA,EACtI;AAAA;AAAA;AAAA;AAAA,EAKQ,oBAAoB,WAAmB,MAAgC,SAAmC;AAMhH,UAAM,aAAa,KAAK;AACxB,UAAM,SAAS,KAAK;AACpB,UAAM,OAAO,KAAK;AAClB,UAAM,eAAe,KAAK;AAC1B,UAAM,mBAAmB,MACvB,KAAK,eAAe,cACpB,KAAK,WAAW,UAChB,KAAK,SAAS,QACd,KAAK,iBAAiB;AACxB,UAAM,WAAW,KAAK;AACtB,QAAI;AACJ,QAAI;AACF,cAAQ,SAAS,kBAAkB,UAAU,kBACzC,SAAS,gBAAgB,WAAW,EAAE,IACtC,UAAU,WAAW;AAAA,IAC3B,SAAS,OAAO;AAId,UAAI,iBAAiB,GAAG;AACtB,aAAK,KAAK,EAAE,MAAM,sBAAsB,aAAa,WAAW,OAAO,eAAe,KAAK,EAAE,CAAC;AAAA,MAChG;AACA;AAAA,IACF;AACA,YAAQ,QAAQ,KAAK,EAAE;AAAA,MACrB,WAAS;AACP,YAAI,CAAC,iBAAiB,EAAG;AACzB,YAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,eAAK,KAAK;AAAA,YACR,MAAM,sBAAsB;AAAA,YAC5B;AAAA,YACA,OAAO,eAAe,IAAI,MAAM,uCAAuC,IAAI,GAAG,CAAC;AAAA,UACjF,CAAC;AACD;AAAA,QACF;AACA,aAAK,KAAK,EAAE,MAAM,sBAAsB,gBAAgB,WAAW,MAAM,CAAC;AAAA,MAC5E;AAAA,MACA,WAAS;AACP,YAAI,CAAC,iBAAiB,EAAG;AACzB,aAAK,KAAK,EAAE,MAAM,sBAAsB,aAAa,WAAW,OAAO,eAAe,KAAK,EAAE,CAAC;AAAA,MAChG;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,eAAe,SAAuB;AAI5C,SAAK,QAAQ,oBAAoB,WAAW,KAAK,aAAa;AAC9D,SAAK,QAAQ,oBAAoB,SAAS,KAAK,iBAAiB;AAChE,SAAK,QAAQ,UAAU;AACvB,SAAK,4BAA4B;AACjC,SAAK,MAAM,MAAM;AACjB,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,SAAK,UAAU,QAAQ,IAAI,MAAM,OAAO,CAAC;AACzC,SAAK,UAAU,SAAS,cAAc,KAAK;AAAA,EAC7C;AAAA;AAAA,EAGQ,8BAAoC;AAC1C,SAAK,MAAM,oBAAoB,WAAW,KAAK,aAAa;AAC5D,SAAK,MAAM,oBAAoB,gBAAgB,KAAK,eAAe;AACnE,SAAK,cAAc,oBAAoB,SAAS,KAAK,uBAAuB;AAAA,EAC9E;AAAA;AAAA,EAGQ,iBAAuB;AAC7B,QAAI,KAAK,oBAAoB,KAAM;AAGnC,QAAI,KAAK,wBAAwB,SAAU;AAC3C,SAAK,kBAAkB,YAAY,MAAM;AACvC,WAAK,KAAK,EAAE,MAAM,sBAAsB,KAAK,CAAC;AAAA,IAChD,GAAG,KAAK,mBAAmB;AAAA,EAC7B;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,oBAAoB,KAAM,eAAc,KAAK,eAAe;AACrE,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEiB,oBAAoB,MAAM;AACzC,SAAK,eAAe,2BAA2B;AAAA,EACjD;AAAA,EAEiB,kBAAkB,MAAM;AACvC,SAAK,eAAe,mDAAmD;AAAA,EACzE;AAAA,EAEiB,0BAA0B,MAAM;AAC/C,SAAK,eAAe,kCAAkC;AAAA,EACxD;AAAA;AAAA,EAGQ,eAAqB;AAC3B,SAAK,SAAS;AACd,SAAK,eAAe;AACpB,SAAK,OAAO;AACZ,SAAK,UAAU;AACf,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,KAAK,SAAgC,UAAgC;AAC3E,QAAI,KAAK,QAAQ;AACf,qBAAe,KAAK,QAAQ,SAAS,QAAQ;AAC7C;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AACb,qBAAe,KAAK,MAAM,SAAS,QAAQ;AAC3C;AAAA,IACF;AACA,QAAI,KAAK,cAAc;AACrB,WAAK,aAAa,OAAO,OAAO;AAChC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;AAeO,SAAS,wBACd,SACiD;AACjD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,SAAO,IAAI,gBAAgB;AAAA,IACzB,GAAG;AAAA,IACH,WAAW;AAAA,IACX,YAAY,cAAc,WAAW;AAAA,IACrC,eAAe;AAAA,IACf,WAAW,IAAI,0BAAiC;AAAA,MAC9C,GAAI,gBAAgB,EAAE,cAAc,IAAI,CAAC;AAAA,MACzC,GAAI,sBAAsB,EAAE,oBAAoB,IAAI,CAAC;AAAA,MACrD,GAAI,iBAAiB,SAAY,CAAC,IAAI,EAAE,aAAa;AAAA,MACrD,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,MACnC,GAAI,wBAAwB,SAAY,CAAC,IAAI,EAAE,oBAAoB;AAAA,MACnE,GAAI,qBAAqB,EAAE,mBAAmB,IAAI,CAAC;AAAA,IACrD,CAAC;AAAA,EACH,CAAC;AACH;AAWA,SAAS,sBAA8B;AACrC,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,IAAI,0BAA0B,YAAY,GAAG;AAAA,EAC/D,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI,OAAO,WAAW;AAAA,IAC3B,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AACH;AAOA,SAAS,4BAA0C;AACjD,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,IAAI,iCAAiC,YAAY,GAAG;AAAA,EACtE,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI,aAAa,WAAW;AAAA,IACjC,MAAM,GAAG,sBAAsB;AAAA,IAC/B,MAAM;AAAA,EACR,CAAC;AACH;AAMA,SAAS,eACP,QACA,SACA,UACM;AACN,MAAI,SAAU,QAAO,YAAY,SAAS,QAA0B;AAAA,MAC/D,QAAO,YAAY,OAAO;AACjC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -403,6 +403,12 @@ function assertHeartbeatInterval(value) {
|
|
|
403
403
|
`Centrifuge heartbeatIntervalMs must be a positive number or Infinity, got ${String(value)}.`
|
|
404
404
|
);
|
|
405
405
|
}
|
|
406
|
+
function assertPublicTopic(operation, topic) {
|
|
407
|
+
if (topic !== "") return;
|
|
408
|
+
throw new TypeError(
|
|
409
|
+
`CrossTabDataBus.${operation}("") addresses a channel no transport can route; use a non-empty topic.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
406
412
|
function assertStructuredCloneable(value) {
|
|
407
413
|
if (typeof structuredClone !== "function") return;
|
|
408
414
|
try {
|
|
@@ -2340,7 +2346,7 @@ var DedupManager = class {
|
|
|
2340
2346
|
};
|
|
2341
2347
|
|
|
2342
2348
|
// src/core/version.ts
|
|
2343
|
-
var SDK_VERSION = true ? "0.
|
|
2349
|
+
var SDK_VERSION = true ? "0.21.0" : "";
|
|
2344
2350
|
|
|
2345
2351
|
// src/core/data-bus.ts
|
|
2346
2352
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2431,10 +2437,6 @@ var CrossTabDataBus = class {
|
|
|
2431
2437
|
// themselves demand: the failure path starts an on-demand reopen instead of
|
|
2432
2438
|
// stranding them until some unrelated future operation arrives.
|
|
2433
2439
|
recoveryWaiters = 0;
|
|
2434
|
-
// Latch for the empty-topic deprecation warning so a hot publish path cannot
|
|
2435
|
-
// fill the console. It is per-bus and never reset: the warning is about the
|
|
2436
|
-
// caller's code, not about a transient runtime condition.
|
|
2437
|
-
emptyTopicWarned = false;
|
|
2438
2440
|
/** Monotonic generation incremented on every successful transport open.
|
|
2439
2441
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2440
2442
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2829,8 +2831,11 @@ var CrossTabDataBus = class {
|
|
|
2829
2831
|
* unsubscribe function for convenience. During an explicit stop() the
|
|
2830
2832
|
* registration is rejected through onError and a no-op cleanup is returned,
|
|
2831
2833
|
* so a late subscriber cannot leak into a future restart.
|
|
2834
|
+
* @throws {TypeError} when `topic` is `''` — no transport can address such a
|
|
2835
|
+
* channel, so the subscription could never receive anything.
|
|
2832
2836
|
*/
|
|
2833
2837
|
subscribe(topic, handler, options) {
|
|
2838
|
+
assertPublicTopic("subscribe", topic);
|
|
2834
2839
|
if (this.stopping) {
|
|
2835
2840
|
this.reportError(new Error(
|
|
2836
2841
|
"CrossTabDataBus is stopping; subscribe() was not registered. Wait for stop() to resolve, then call start() before subscribing again."
|
|
@@ -2839,7 +2844,6 @@ var CrossTabDataBus = class {
|
|
|
2839
2844
|
};
|
|
2840
2845
|
}
|
|
2841
2846
|
this.ensureStarted();
|
|
2842
|
-
if (topic === "") this.warnEmptyTopic("subscribe");
|
|
2843
2847
|
const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
2844
2848
|
const wasUnused = handlers.size === 0;
|
|
2845
2849
|
handlers.add(handler);
|
|
@@ -2889,20 +2893,10 @@ var CrossTabDataBus = class {
|
|
|
2889
2893
|
resetDedup() {
|
|
2890
2894
|
this.dedupManager.reset();
|
|
2891
2895
|
}
|
|
2892
|
-
/**
|
|
2893
|
-
*
|
|
2894
|
-
* subscription it creates can never be addressed by a transport: the message
|
|
2895
|
-
* silently goes nowhere. A future minor rejects it at this boundary. */
|
|
2896
|
-
warnEmptyTopic(operation) {
|
|
2897
|
-
if (this.emptyTopicWarned) return;
|
|
2898
|
-
this.emptyTopicWarned = true;
|
|
2899
|
-
console.warn(
|
|
2900
|
-
`cross-tab-worker-databus: ${operation}("") addresses a channel no transport can route. Use a non-empty topic; passing "" is planned to throw in a future minor.`
|
|
2901
|
-
);
|
|
2902
|
-
}
|
|
2903
|
-
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
2896
|
+
/** Publish a message to `topic`. The owning Worker delivers it to the transport.
|
|
2897
|
+
* @throws {TypeError} when `topic` is `''`; see `subscribe()`. */
|
|
2904
2898
|
publish(topic, data, options) {
|
|
2905
|
-
|
|
2899
|
+
assertPublicTopic("publish", topic);
|
|
2906
2900
|
this.ensureStarted();
|
|
2907
2901
|
if (this.rejectPublishDuringStop("publish")) return;
|
|
2908
2902
|
if (!this.cluster.publish(topic, data, options)) {
|
|
@@ -2917,11 +2911,14 @@ var CrossTabDataBus = class {
|
|
|
2917
2911
|
* in one tick. Per-item dedup / replay / ordering is preserved; each item
|
|
2918
2912
|
* may carry its own `messageId` / `timestamp` via `options`. Empty array is
|
|
2919
2913
|
* a no-op; single-item array delegates to `publish()`.
|
|
2914
|
+
* @throws {TypeError} when `topic` is `''`; see `subscribe()`. Checked before
|
|
2915
|
+
* the empty-array no-op, so `publishBatch('', [])` throws too — the topic is
|
|
2916
|
+
* the caller's bug regardless of whether anything is carried.
|
|
2920
2917
|
*/
|
|
2921
2918
|
publishBatch(topic, items) {
|
|
2919
|
+
assertPublicTopic("publishBatch", topic);
|
|
2922
2920
|
this.ensureStarted();
|
|
2923
2921
|
if (items.length === 0) return;
|
|
2924
|
-
if (topic === "") this.warnEmptyTopic("publishBatch");
|
|
2925
2922
|
if (this.rejectPublishDuringStop("publishBatch")) return;
|
|
2926
2923
|
if (items.length === 1) {
|
|
2927
2924
|
const first = items[0];
|
|
@@ -3550,4 +3547,4 @@ export {
|
|
|
3550
3547
|
parseDataBusPublication,
|
|
3551
3548
|
selectWorkerBackend
|
|
3552
3549
|
};
|
|
3553
|
-
//# sourceMappingURL=chunk-
|
|
3550
|
+
//# sourceMappingURL=chunk-EJDXU4DK.js.map
|