cross-tab-worker-databus 0.5.0 → 0.7.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 +23 -0
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/dist/centrifuge.js +1 -2
- package/dist/centrifuge.js.map +1 -1
- package/dist/{chunk-W7DAXK4D.js → chunk-SI7N5KNJ.js} +50 -2
- package/dist/chunk-SI7N5KNJ.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +49 -1
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/hooks.cjs +1 -1913
- package/dist/cjs/hooks.cjs.map +4 -4
- package/dist/cjs/index.cjs +98 -1
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/vue.cjs +93 -0
- package/dist/cjs/vue.cjs.map +7 -0
- package/dist/core/cluster.d.ts +5 -0
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +12 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts +4 -0
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/trace.d.ts +19 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/hooks.js +7 -1911
- package/dist/hooks.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -2
- package/dist/index.js.map +2 -2
- package/dist/vue.d.ts +10 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/vue.js +72 -0
- package/dist/vue.js.map +7 -0
- package/dist/websocket.d.ts +2 -1
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +21 -0
- package/docs/capabilities.md +3 -3
- package/docs/roadmap.md +7 -7
- package/docs/transports.md +4 -0
- package/docs/zh/api.md +19 -0
- package/docs/zh/capabilities.md +3 -3
- package/docs/zh/roadmap.md +7 -7
- package/docs/zh/transports.md +4 -0
- package/package.json +13 -3
- package/dist/chunk-5WRI5ZAA.js +0 -31
- package/dist/chunk-5WRI5ZAA.js.map +0 -7
- package/dist/chunk-W7DAXK4D.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,29 @@
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.7.0] - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Replay persistence lifecycle cleanup (`clear` / `clearTopic`) with stale-history removal on unsubscribe.
|
|
27
|
+
- Opt-in reliability diagnostics for recovery retries, owner acknowledgments, and route migrations.
|
|
28
|
+
- Bounded, opt-in publication deduplication with caller-supplied message IDs.
|
|
29
|
+
- React/Vue and custom-transport compatibility fixtures plus browser/package regression coverage.
|
|
30
|
+
|
|
31
|
+
### Implemented in the current 0.7.0 worktree
|
|
32
|
+
|
|
33
|
+
- Replay persistence now supports optional `clear()` and `clearTopic()` lifecycle hooks; `CrossTabDataBus` invokes them on stop and final topic unsubscribe.
|
|
34
|
+
- Trace exports bounded `reliability` events for transport recovery attempts, route acknowledgments, and graceful route migrations.
|
|
35
|
+
- `DataBusMessage.messageId` plus opt-in `dedup: { maxEntries, ttlMs }` suppresses duplicate inbound publications with bounded memory.
|
|
36
|
+
|
|
37
|
+
## [0.6.0] - 2026-08-31
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `publish()` 在当前 Worker 已同步持有 topic owner assignment 时走本地快路径,避免每条消息重复扫描 worker/route storage;跨 Tab 路由仍保留原有存储校验。
|
|
42
|
+
- 新增独立 `./vue` 入口,提供 `useCrossTabDataBus`、`useCrossTabSubscription` 和 `useCrossTabStatus`;Vue 3 作为可选 peer dependency,不影响核心入口。
|
|
43
|
+
- WebSocketTransport 与内置 demo 支持 `ArrayBuffer` 二进制帧往返;JSON payload 协议保持兼容。
|
|
44
|
+
|
|
22
45
|
## [0.5.0] - 2026-08-31
|
|
23
46
|
|
|
24
47
|
### Added
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ By default each tab holds its own Dedicated Worker; when configured with `worker
|
|
|
23
23
|
- Wildcard subscriptions: `chat.*` and `*` patterns match concrete topics at dispatch
|
|
24
24
|
- Built-in zero-dependency native WebSocket transport (`createWebSocketDataBus`) for plain-WebSocket servers
|
|
25
25
|
- Optional React hooks adapter (`cross-tab-worker-databus/hooks`): StrictMode-safe bus lifecycle, auto-cleanup subscriptions
|
|
26
|
+
- Optional Vue 3 composables adapter (`cross-tab-worker-databus/vue`): lifecycle-safe bus, subscription, and status composables
|
|
26
27
|
- `pagehide` releases resources automatically; `pageshow` rebuilds the Worker and connection automatically
|
|
27
28
|
- Transport reconnect automatically restores the current owner's Topics
|
|
28
29
|
- After a tab exits abnormally, automatic migration happens via heartbeat TTL
|
package/README.zh.md
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- 通配符订阅:`chat.*` 与 `*` pattern 在分发侧匹配具体 Topic
|
|
24
24
|
- 内置零依赖的原生 WebSocket 传输(`createWebSocketDataBus`),适配普通 WebSocket 服务器
|
|
25
25
|
- 可选的 React hooks 适配层(`cross-tab-worker-databus/hooks`):StrictMode 安全的 bus 生命周期与自动清理订阅
|
|
26
|
+
- 可选的 Vue 3 composables 适配层(`cross-tab-worker-databus/vue`):安全管理 bus 生命周期、订阅和状态
|
|
26
27
|
- `pagehide` 自动释放资源;`pageshow` 自动重建 Worker 和连接
|
|
27
28
|
- Transport 重连自动恢复当前 owner 的 Topic
|
|
28
29
|
- Tab 异常退出后通过心跳 TTL 自动迁移
|
package/dist/centrifuge.js
CHANGED
package/dist/centrifuge.js.map
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/centrifuge-session.ts", "../src/centrifuge-protocol.ts", "../src/centrifuge.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * CentrifugeSession \u2014 a reusable wrapper around a single Centrifuge client.\n *\n * Provides a structured-clone-safe message protocol (INIT/SUBSCRIBE/UNSUBSCRIBE/\n * PUBLISH/STOP) so the same session class can run inside a Dedicated Worker,\n * a SharedWorker port, or directly on the main thread as a local fallback.\n */\nimport { Centrifuge } from 'centrifuge';\nimport type {\n PublicationContext,\n StateContext,\n Subscription,\n SubscriptionErrorContext\n} from 'centrifuge';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\n\n/** Callback interface for posting messages back to the transport layer. */\nexport interface CentrifugeSessionSink<TData = unknown> {\n post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void;\n}\n\n/**\n * Stateful Centrifuge client wrapper shared by Dedicated Worker, SharedWorker\n * ports and the main-thread local fallback. Each session owns one connection.\n */\nexport class CentrifugeSession<TData = unknown> {\n private client: Centrifuge | null = null;\n private readonly subscriptions = new Map<string, Subscription>();\n private transferable = false;\n\n constructor(private readonly sink: CentrifugeSessionSink<TData>) {}\n\n /** Dispatch an incoming Worker message to the matching operation.\n * Unknown message types are ignored rather than thrown, so a future protocol\n * extension adding a new variant cannot crash an older session. */\n handle(message: CentrifugeWorkerInput): void {\n switch (message.type) {\n case 'INIT':\n this.initialize(message.url, message.config, message.transferable === true);\n return;\n case 'SUBSCRIBE':\n this.subscribe(message.topic);\n return;\n case 'UNSUBSCRIBE':\n this.unsubscribe(message.topic);\n return;\n case 'PUBLISH':\n case 'PUBLISH_BIN':\n // Binary and JSON publish share the same Centrifuge client call; the\n // transport layer decides whether to transfer the ArrayBuffer.\n this.publish(message.topic, message.data);\n return;\n case 'STOP':\n this.stop();\n return;\n default:\n return;\n }\n }\n\n /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */\n private initialize(url: string, config: CentrifugeWorkerConfig, transferable: boolean): void {\n if (this.client) return;\n this.transferable = transferable;\n const client = new Centrifuge(url, config);\n this.client = client;\n client.on('state', (context: StateContext) => {\n this.post({ type: 'STATUS', status: normalizeStatus(context.newState) });\n });\n client.on('connected', () => this.post({ type: 'STATUS', status: 'connected' }));\n client.on('disconnected', () => this.post({ type: 'STATUS', status: 'disconnected' }));\n client.on('error', context => this.postError(context));\n // Client-level publications are only for server-side subscriptions (where\n // no client Subscription object exists). For topics we have an active\n // subscription for, the subscription-level 'publication' listener handles\n // dispatch \u2014 skip here to avoid delivering the same message twice.\n client.on('publication', (context: PublicationContext) => {\n const topic = context.channel || getPayloadTopic(context.data);\n if (!topic || this.subscriptions.has(topic)) return;\n this.postPublication(topic, context.data);\n });\n client.connect();\n }\n\n /** Subscribe to a Centrifuge channel. Reuses an existing subscription if one exists.\n * Listeners are only registered once per subscription object \u2014 a repeated\n * SUBSCRIBE for an already-tracked topic skips the listener wiring entirely,\n * avoiding the removeAllListeners + re-on churn on every duplicate message. */\n private subscribe(topic: string): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n // If we already track this subscription, it already has our listeners \u2014\n // a duplicate SUBSCRIBE is a no-op (idempotent), matching the transport\n // contract. Only a fresh subscription needs listener wiring.\n const existing = this.subscriptions.get(topic);\n if (existing) {\n existing.subscribe();\n return;\n }\n let subscription = this.client.getSubscription(topic);\n if (!subscription) subscription = this.client.newSubscription(topic);\n // Remove only our own listeners so that any Centrifuge internal listeners\n // on the subscription object are preserved. Each subscription event carries\n // a single listener so removeAllListeners(\u2026) is safe here.\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.set(topic, subscription);\n subscription.on('publication', context => {\n this.postPublication(topic, context.data);\n });\n subscription.on('error', (context: SubscriptionErrorContext) => this.postError(context));\n subscription.on('unsubscribed', () => this.subscriptions.delete(topic));\n subscription.subscribe();\n }\n\n /** Unsubscribe from a Centrifuge channel and clean up the local reference.\n * Listeners are removed before unsubscribing so a late `unsubscribed` event\n * cannot delete a subscription that a subsequent `subscribe()` re-added. */\n private unsubscribe(topic: string): void {\n const subscription = this.subscriptions.get(topic) ?? this.client?.getSubscription(topic);\n if (!subscription) return;\n subscription.removeAllListeners('publication');\n subscription.removeAllListeners('error');\n subscription.removeAllListeners('unsubscribed');\n this.subscriptions.delete(topic);\n subscription.unsubscribe();\n }\n\n /** Publish a message to the Centrifuge channel. */\n private publish(topic: string, data: unknown): void {\n if (!this.client) return this.postError(new Error('Centrifuge client is not initialized.'));\n void this.client.publish(topic, data).catch(error => this.postError(error));\n }\n\n /** Forward a publication to the transport. Binary payloads take the\n * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything\n * else is structured-cloned via `MESSAGE`. An empty topic means the\n * publication carried no channel info and is silently dropped. */\n private postPublication(topic: string, data: unknown): void {\n if (!topic) return;\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'MESSAGE_BIN', topic, data }, [data]);\n return;\n }\n this.post({ type: 'MESSAGE', topic, data: data as TData });\n }\n\n /** Disconnect the client and clear all subscriptions. */\n private stop(): void {\n this.client?.disconnect();\n this.subscriptions.clear();\n this.client = null;\n this.post({ type: 'STATUS', status: 'disconnected' });\n }\n\n /** Forward a message to the sink (the transport layer). */\n private post(message: CentrifugeWorkerOutput<TData>, transfer?: ArrayBuffer[]): void {\n this.sink.post(message, transfer);\n }\n\n /** Serialise and report an error. The Centrifuge client handles reconnection\n * internally, so a transient error should not trigger a `STATUS: error` that\n * would cause `selectActiveWorkers()` to exclude this worker from routing.\n * Fatal errors are distinguished by the client eventually emitting\n * `disconnected` without a subsequent `connected`. */\n private postError(error: unknown): void {\n this.post({ type: 'ERROR', error: serializeError(error) });\n }\n}\n\n/** Extract a topic from a Centrifuge publication payload if one is present.\n * Handles both direct `channel` fields and the nested `push.channel` shape\n * that Centrifugo uses for some server-side push types. */\nfunction getPayloadTopic(data: unknown): string {\n if (!data || typeof data !== 'object') return '';\n const payload = data as Record<string, unknown>;\n // Prefer nested push.channel (server-side push) then fall back to top-level channel.\n const push = payload.push;\n const nested = typeof push === 'object' && push !== null ? (push as Record<string, unknown>).channel : undefined;\n const topic = nested ?? payload.channel;\n return typeof topic === 'string' ? topic : '';\n}\n\n/** Map a Centrifuge state string to the DataBus's status vocabulary.\n * 'connecting' and 'connected' pass through; anything else (e.g. 'reconnecting',\n * 'disconnected') maps to 'disconnected'. */\nconst LIVE_STATES = new Set(['connecting', 'connected']);\nfunction normalizeStatus(status: string): 'connecting' | 'connected' | 'disconnected' {\n return LIVE_STATES.has(status) ? (status as 'connecting' | 'connected') : 'disconnected';\n}\n\n/** Convert an arbitrary error into a structured-cloneable form for postMessage. */\nfunction serializeError(error: unknown): SerializedWorkerError {\n if (error instanceof Error) {\n return {\n name: error.name,\n message: error.message,\n ...(error.stack ? { stack: error.stack } : {})\n };\n }\n return {\n name: 'CentrifugeError',\n message: typeof error === 'string' ? error : 'Centrifuge worker operation failed.',\n ...(error === undefined ? {} : { context: error })\n };\n}\n", "/**\n * Worker-thread protocol for Centrifuge WebSocket transport.\n *\n * Defines the message types exchanged between the main thread and a Web Worker\n * (dedicated or shared) that runs a centrifuge client. The worker is isolated\n * from the main thread so that WebSocket lifecycle, token refresh, and binary\n * data handling never block the UI.\n */\nimport type { Options } from 'centrifuge';\nimport type { WorkerStatus } from './core/types';\n\n/**\n * Default interval between main-thread PING heartbeats to a SharedWorker. The\n * SharedWorker reaps a silent port after `SESSION_TIMEOUT_MULTIPLIER` intervals.\n * Shared here so the main thread (heartbeat sender) and the SharedWorker (reaper)\n * always agree on the cadence.\n */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;\n/** SharedWorker per-port session timeout, expressed as a multiple of that port's\n * heartbeat interval. A live port PINGs every heartbeat interval, so a timeout of\n * several intervals tolerates throttled tabs without reaping healthy sessions. */\nexport const DEFAULT_SESSION_TIMEOUT_MULTIPLIER = 3;\n/** Default per-port session timeout in ms, derived from the heartbeat interval\n * and the multiplier. Used as the fallback when a port sends no INIT config. */\nexport const DEFAULT_SESSION_TIMEOUT_MS =\n DEFAULT_HEARTBEAT_INTERVAL_MS * DEFAULT_SESSION_TIMEOUT_MULTIPLIER;\n\n// Centrifuge Options that reference browser APIs (WebSocket, EventSource, etc.)\n// are unavailable inside a Worker \u2014 the worker uses its own WebSocket import.\n// These are stripped from the config sent to the worker so the type system\n// prevents accidentally passing a main-thread-only function (which would fail\n// structured cloning and throw a DataCloneError).\ntype WorkerUnsafeOption =\n | 'eventsource'\n | 'fetch'\n | 'getData'\n | 'getToken'\n | 'networkEventTarget'\n | 'readableStream'\n | 'sockjs'\n | 'websocket';\n\n/** Centrifuge options safe to pass into a Worker; unsafe options are explicitly\n * excluded and set to `never` so the compiler rejects them at the call site. */\nexport type CentrifugeWorkerConfig = Omit<Partial<Options>, WorkerUnsafeOption> & {\n [Key in WorkerUnsafeOption]?: never;\n};\n\n/** Messages sent from the main thread to the Worker. All variants are\n * structured-cloneable; `PUBLISH_BIN` carries an ArrayBuffer (transferable). */\nexport type CentrifugeWorkerInput =\n /** Initial connection: URL + config. Sent once per backend creation.\n * `transferable` enables ArrayBuffer zero-copy for subsequent PUBLISH_BIN.\n * `heartbeatIntervalMs` overrides the SharedWorker PING cadence. */\n | { type: 'INIT'; url: string; config: CentrifugeWorkerConfig; transferable?: boolean; heartbeatIntervalMs?: number }\n /** Subscribe to a channel. Idempotent \u2014 re-subscribing is a no-op. */\n | { type: 'SUBSCRIBE'; topic: string }\n /** Unsubscribe from a channel. Idempotent. */\n | { type: 'UNSUBSCRIBE'; topic: string }\n /** Publish a structured-cloneable payload to a channel. */\n | { type: 'PUBLISH'; topic: string; data: unknown }\n /** Publish an ArrayBuffer via Transferable (zero-copy when `transferable` is on). */\n | { type: 'PUBLISH_BIN'; topic: string; data: ArrayBuffer }\n /** Heartbeat from the main thread; the SharedWorker reaps silent ports. */\n | { type: 'PING' }\n /** Disconnect the client and clear all subscriptions. */\n | { type: 'STOP' };\n\n/** Messages sent from the Worker back to the main thread. The main thread\n * routes these to the DataBusTransportHandlers via handleOutput(). */\nexport type CentrifugeWorkerOutput<TData = unknown> =\n /** Connection status changed. Maps Centrifuge states to the DataBus vocabulary. */\n | { type: 'STATUS'; status: WorkerStatus }\n /** A JSON publication arrived. Routed to onMessage via handleOutput. */\n | { type: 'MESSAGE'; topic: string; data: TData }\n /** A binary publication arrived (Transferable). Routed to onMessage with the ArrayBuffer. */\n | { type: 'MESSAGE_BIN'; topic: string; data: ArrayBuffer }\n /** A non-fatal error occurred. Does not imply disconnection (the client retries internally). */\n | { type: 'ERROR'; error: SerializedWorkerError };\n\n/** Error object serialized for cross-thread transfer. Error instances cannot\n * be structured-cloned via postMessage, so the Worker converts them to this\n * shape and the main thread rebuilds an Error via deserializeWorkerError(). */\nexport interface SerializedWorkerError {\n /** The Error's `name` (e.g. 'TypeError', 'CentrifugeError'). */\n name: string;\n /** The Error's `message`. */\n message: string;\n /** The Error's `stack` if available (for debugging). */\n stack?: string;\n /** Arbitrary context attached by the Worker (e.g. the failing operation). */\n context?: unknown;\n}\n", "/**\n * Centrifuge WebSocket transport that runs inside a Web Worker.\n *\n * Supports three backends, selected in order of preference:\n * 1. SharedWorker \u2014 one WebSocket per tab session, hosted in a shared process\n * 2. Dedicated Worker \u2014 one WebSocket per tab\n * 3. In-process (local) \u2014 Centrifuge runs on the main thread (fallback when\n * neither Worker type is available, e.g. in non-browser environments)\n *\n * Binary data (ArrayBuffer) can be transferred via Transferable when the\n * `transferable` option is enabled, avoiding structured-clone overhead.\n */\n\nimport { CrossTabDataBus } from './core/data-bus';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type { DataBusTransport, DataBusTransportHandlers } from './core/types';\nimport { CentrifugeSession } from './centrifuge-session';\nimport { selectWorkerBackend } from './worker-mode';\nimport type { WorkerBackend, WorkerMode } from './worker-mode';\nimport type {\n CentrifugeWorkerConfig,\n CentrifugeWorkerInput,\n CentrifugeWorkerOutput,\n SerializedWorkerError\n} from './centrifuge-protocol';\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS } from './centrifuge-protocol';\n\nexport type { CentrifugeWorkerConfig, SerializedWorkerError } from './centrifuge-protocol';\nexport type { WorkerBackend, WorkerMode } from './worker-mode';\n\n/** WebSocket connection parameters passed to the Centrifuge Worker. */\nexport interface CentrifugeDataBusConfig {\n /** Centrifuge server WebSocket URL. */\n url: string;\n /** Centrifuge client options (token, channel params, etc.). */\n options?: CentrifugeWorkerConfig;\n}\n\n/** Options for configuring the Worker backend (dedicated, shared, or local). */\nexport interface CentrifugeWorkerTransportOptions {\n /** Custom dedicated Worker factory. Used for testing or bundler integration. */\n workerFactory?: () => Worker;\n /** Custom SharedWorker factory. */\n sharedWorkerFactory?: () => SharedWorker;\n /** Preferred Worker mode: 'dedicated', 'shared', or 'auto'. */\n workerMode?: WorkerMode;\n /** Enable transferable (ArrayBuffer) support for binary data. */\n transferable?: boolean;\n /** Interval (ms) between PING heartbeats sent to the SharedWorker. The\n * SharedWorker reaps a silent port after `DEFAULT_SESSION_TIMEOUT_MULTIPLIER`\n * \u00D7 this interval. Pass `Infinity` to disable heartbeats entirely. Defaults\n * to `DEFAULT_HEARTBEAT_INTERVAL_MS`. */\n heartbeatIntervalMs?: number;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a Centrifuge transport. */\nexport interface CreateCentrifugeDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<CentrifugeDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n >,\n CentrifugeWorkerTransportOptions {\n /** Centrifuge connection configuration. */\n connection: CentrifugeDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/**\n * Transport layer that runs a Centrifuge WebSocket client inside a Web Worker.\n *\n * Delegates the actual WebSocket connection to a Worker (dedicated or shared)\n * or falls back to an in-process CentrifugeSession. The Worker is isolated from\n * the main thread so that WebSocket lifecycle, token refresh, and binary data\n * handling never block the UI.\n */\nexport class CentrifugeWorkerTransport<TData = unknown>\n implements DataBusTransport<CentrifugeDataBusConfig, TData>\n{\n private readonly workerMode: WorkerMode;\n private readonly transferable: boolean;\n private readonly heartbeatIntervalMs: number;\n private readonly workerFactory: (() => Worker) | undefined;\n private readonly sharedWorkerFactory: (() => SharedWorker) | undefined;\n private backend: WorkerBackend | null = null;\n private worker: Worker | null = null;\n private sharedWorker: SharedWorker | null = null;\n private port: MessagePort | null = null;\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n private localSession: CentrifugeSession<TData> | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n // Monotonically increasing counter, bumped each time a backend is created.\n // Used to ignore late error events from a superseded Worker.\n private generation = 0;\n // Generation captured when the current backend was created. Error handlers\n // only act when the backend that registered them is still current.\n private backendGeneration = 0;\n\n constructor(options: CentrifugeWorkerTransportOptions = {}) {\n this.workerMode = options.workerMode ?? 'dedicated';\n this.transferable = options.transferable ?? false;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n assertHeartbeatInterval(this.heartbeatIntervalMs);\n this.workerFactory = options.workerFactory;\n this.sharedWorkerFactory = options.sharedWorkerFactory;\n }\n\n /**\n * Start the transport: select a backend, initialise the Worker (or local\n * session), and send the INIT message with connection parameters.\n */\n start(config: CentrifugeDataBusConfig, handlers: DataBusTransportHandlers<TData>): void {\n if (this.backend) return;\n assertStructuredCloneable(config.options ?? {});\n this.handlers = handlers;\n const backend = selectWorkerBackend(this.workerMode, {\n worker: this.workerFactory !== undefined,\n sharedWorker: this.sharedWorkerFactory !== undefined\n });\n const input = this.buildInitInput(config);\n if (backend === 'shared') {\n this.startSharedWorker(input);\n this.backend = 'shared';\n return;\n }\n if (backend === 'dedicated') {\n this.startDedicatedWorker(input);\n this.backend = 'dedicated';\n return;\n }\n this.localSession = new CentrifugeSession<TData>({ post: this.handleSessionOutput });\n this.localSession.handle(input);\n this.backend = 'local';\n }\n\n subscribe(topic: string): void {\n this.post({ type: 'SUBSCRIBE', topic });\n }\n\n unsubscribe(topic: string): void {\n this.post({ type: 'UNSUBSCRIBE', topic });\n }\n\n /**\n * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable\n * when `transferable` is enabled, avoiding a structured-clone cycle.\n */\n publish(topic: string, data: unknown): void {\n if (this.transferable && data instanceof ArrayBuffer) {\n this.post({ type: 'PUBLISH_BIN', topic, data }, [data]);\n return;\n }\n this.post({ type: 'PUBLISH', topic, data });\n }\n\n /**\n * Gracefully stop the transport: send STOP, clean up event listeners, and\n * terminate the Worker (or close the SharedWorker port).\n */\n stop(): void {\n if (!this.backend) return;\n this.generation++;\n this.post({ type: 'STOP' });\n this.clearHeartbeat();\n if (this.worker) {\n this.worker.removeEventListener('message', this.handleMessage);\n this.worker.removeEventListener('error', this.handleWorkerError);\n this.worker.terminate();\n }\n if (this.sharedWorker) {\n this.detachSharedWorkerListeners();\n this.port?.close();\n }\n this.resetBackend();\n this.handlers = null;\n }\n\n /** Build the INIT payload sent to the Worker / local session. Optional fields\n * are only included when they deviate from the defaults, so the Worker's own\n * default-resolution logic kicks in for the common case. */\n private buildInitInput(config: CentrifugeDataBusConfig): CentrifugeWorkerInput {\n return {\n type: 'INIT',\n url: config.url,\n config: config.options ?? {},\n ...(this.transferable ? { transferable: true } : {}),\n ...(this.heartbeatIntervalMs !== DEFAULT_HEARTBEAT_INTERVAL_MS\n ? { heartbeatIntervalMs: this.heartbeatIntervalMs }\n : {})\n };\n }\n\n /** Create and initialise a dedicated Worker, then send the INIT message. */\n private startDedicatedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const worker = (this.workerFactory ?? createDefaultWorker)();\n this.worker = worker;\n worker.addEventListener('message', this.handleMessage);\n worker.addEventListener('error', this.handleWorkerError);\n worker.postMessage(input);\n }\n\n /** Create and initialise a SharedWorker, open the MessagePort, and send the INIT message. */\n private startSharedWorker(input: CentrifugeWorkerInput): void {\n this.backendGeneration = ++this.generation;\n const shared = (this.sharedWorkerFactory ?? createDefaultSharedWorker)();\n this.sharedWorker = shared;\n const port = shared.port;\n this.port = port;\n port.addEventListener('message', this.handleMessage);\n port.addEventListener('messageerror', this.handlePortError);\n shared.addEventListener('error', this.handleSharedWorkerError);\n port.start();\n port.postMessage(input);\n this.startHeartbeat();\n }\n\n /** Handle a message event from the Worker (dedicated or shared). */\n private readonly handleMessage = (event: MessageEvent<CentrifugeWorkerOutput<TData>>) => {\n this.handleOutput(event.data);\n };\n\n /** Handle a message from the in-process CentrifugeSession (local fallback). */\n private readonly handleSessionOutput = (message: CentrifugeWorkerOutput<TData>) => {\n this.handleOutput(message);\n };\n\n /** Route a Worker output message to the appropriate handler callback.\n * Shared by the Worker message listener, the SharedWorker port listener,\n * and the local-session sink \u2014 all three feed into this single dispatcher. */\n private handleOutput(message: CentrifugeWorkerOutput<TData>): void {\n if (message.type === 'STATUS') this.handlers?.onStatus(message.status);\n if (message.type === 'MESSAGE') this.handlers?.onMessage({ topic: message.topic, data: message.data });\n if (message.type === 'MESSAGE_BIN') this.handlers?.onMessage({ topic: message.topic, data: message.data as TData });\n if (message.type === 'ERROR') this.handlers?.onError(deserializeWorkerError(message.error));\n }\n\n /** Handle a Worker-level failure (crash, message decode error). Discards the\n * dead backend so a later start()/reopen can rebuild from scratch, and\n * signals an error status so the DataBus can trigger recovery.\n * Only invoked when the generation guard confirms the failing backend is\n * still current \u2014 late errors from a superseded Worker are silently dropped. */\n private onWorkerFailed(message: string): void {\n // Remove the message listener and release the old backend before recovery\n // reopens, so late messages from the failed Worker/port cannot be routed\n // into the freshly started session.\n this.worker?.removeEventListener('message', this.handleMessage);\n this.worker?.removeEventListener('error', this.handleWorkerError);\n this.worker?.terminate();\n this.detachSharedWorkerListeners();\n this.port?.close();\n this.clearHeartbeat();\n this.resetBackend();\n this.handlers?.onError(new Error(message));\n this.handlers?.onStatus('error');\n }\n\n /** Remove every listener attached to the current SharedWorker and its port. */\n private detachSharedWorkerListeners(): void {\n this.port?.removeEventListener('message', this.handleMessage);\n this.port?.removeEventListener('messageerror', this.handlePortError);\n this.sharedWorker?.removeEventListener('error', this.handleSharedWorkerError);\n }\n\n /** Periodically ping the SharedWorker so its session reaper can detect a dead tab. */\n private startHeartbeat(): void {\n if (this.heartbeatHandle !== null) return;\n // Infinite disables the heartbeat (e.g. for environment where the\n // SharedWorker reaper is not needed).\n if (this.heartbeatIntervalMs === Infinity) return;\n this.heartbeatHandle = setInterval(() => {\n this.post({ type: 'PING' });\n }, this.heartbeatIntervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatHandle !== null) clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n\n private readonly handleWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge worker failed.');\n };\n\n private readonly handlePortError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker message decoding failed.');\n };\n\n private readonly handleSharedWorkerError = () => {\n if (this.generation !== this.backendGeneration) return;\n this.onWorkerFailed('Centrifuge shared worker failed.');\n };\n\n /** Clear the Worker/port/backend references after a failure or stop. */\n private resetBackend(): void {\n this.worker = null;\n this.sharedWorker = null;\n this.port = null;\n this.backend = null;\n this.localSession = null;\n }\n\n /**\n * Post a message to the active backend. Accepts optional Transferable buffers\n * for zero-copy ArrayBuffer transfer.\n */\n private post(message: CentrifugeWorkerInput, transfer?: ArrayBuffer[]): void {\n if (this.worker) {\n postToPortLike(this.worker, message, transfer);\n return;\n }\n if (this.port) {\n postToPortLike(this.port, message, transfer);\n return;\n }\n if (this.localSession) {\n this.localSession.handle(message);\n return;\n }\n throw new Error('CentrifugeWorkerTransport.start() must be called first.');\n }\n}\n\n/**\n * Create a fully-configured CrossTabDataBus with a Centrifuge WebSocket transport.\n *\n * This is the primary entry point for consumers. It wires up the transport,\n * cluster coordination, and lifecycle management:\n *\n * ```ts\n * const bus = createCentrifugeDataBus({\n * connection: { url: 'wss://example.com/connection/websocket', options: { token: '\u2026' } },\n * trace: { enabled: true, sink: event => console.log(event) },\n * });\n * ```\n */\nexport function createCentrifugeDataBus<TData = unknown>(\n options: CreateCentrifugeDataBusOptions<TData>\n): CrossTabDataBus<CentrifugeDataBusConfig, TData> {\n const {\n clusterKey,\n connection,\n heartbeatIntervalMs,\n sharedWorkerFactory,\n transferable,\n workerFactory,\n workerMode,\n ...dataBusOptions\n } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new CentrifugeWorkerTransport<TData>({\n ...(workerFactory ? { workerFactory } : {}),\n ...(sharedWorkerFactory ? { sharedWorkerFactory } : {}),\n ...(transferable === undefined ? {} : { transferable }),\n ...(workerMode ? { workerMode } : {}),\n ...(heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs })\n })\n });\n}\n\n\n/** Create the default dedicated Worker hosting the Centrifuge client. */\nfunction createDefaultWorker(): Worker {\n if (typeof Worker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser Worker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly.'\n );\n }\n return new Worker(workerUrl, {\n name: 'cross-tab-worker-databus',\n type: 'module'\n });\n}\n\n/** Create the default SharedWorker. Each connecting port within the SharedWorker\n * creates its own CentrifugeSession with an independent WebSocket connection,\n * so refreshing or stopping one tab does not affect the others. */\nfunction createDefaultSharedWorker(): SharedWorker {\n if (typeof SharedWorker === 'undefined') {\n throw new Error('CentrifugeWorkerTransport requires a browser SharedWorker implementation.');\n }\n let workerUrl: URL;\n try {\n workerUrl = new URL('./centrifuge.shared.worker.js', import.meta.url);\n } catch {\n throw new Error(\n 'The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly.'\n );\n }\n return new SharedWorker(workerUrl, {\n name: 'cross-tab-worker-databus-shared',\n type: 'module'\n });\n}\n\n/** Validate the SharedWorker PING heartbeat interval. A value of `0`, a negative\n * number, or `NaN` would otherwise make `setInterval` degenerate into a 0ms busy\n * loop, driving the reaper and the main-thread PING out of control. `Infinity`\n * is allowed and disables heartbeats entirely (for environments where the\n * SharedWorker reaper is not needed, e.g. a single-tab deployment).\n * @throws {TypeError} when `value` is not a positive finite number or Infinity. */\nfunction assertHeartbeatInterval(value: number): void {\n if (value === Infinity) return;\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) return;\n throw new TypeError(\n `Centrifuge heartbeatIntervalMs must be a positive number or Infinity, got ${String(value)}.`\n );\n}\n\n/** Validate that `value` is structured-cloneable. Throws early so config errors\n * surface on the main thread rather than silently failing inside the Worker\n * (where a DataCloneError would be reported as a generic Worker error with no\n * actionable message). Skips validation when `structuredClone` is unavailable\n * (older browsers without the API) \u2014 the Worker will still throw on its own.\n * @throws {TypeError} when `value` contains non-cloneable members (functions,\n * Symbols, DOM nodes, etc.). */\nfunction assertStructuredCloneable(value: unknown): void {\n if (typeof structuredClone !== 'function') return;\n try {\n structuredClone(value);\n } catch (error) {\n throw new TypeError(\n 'Centrifuge Worker configuration and published data must be structured-cloneable.',\n { cause: error }\n );\n }\n}\n\n/** Post `message` to a Worker or MessagePort, forwarding `transfer` buffers\n * when present. Both targets share the `postMessage(message, transfer?)`\n * signature, so a single helper eliminates the duplicated if/else at each\n * call site. */\nfunction postToPortLike(\n target: Pick<Worker, 'postMessage'>,\n message: CentrifugeWorkerInput,\n transfer?: ArrayBuffer[]\n): void {\n if (transfer) target.postMessage(message, transfer as Transferable[]);\n else target.postMessage(message);\n}\n\n/** Reconstruct an Error instance from its serialised form. Error objects cannot\n * be structured-cloned across the Worker boundary, so the Worker serialises them\n * into {@link SerializedWorkerError} and the main thread rebuilds the Error here\n * so the caller's `onError` handler receives a real Error with name/stack. */\nfunction deserializeWorkerError(error: SerializedWorkerError): Error {\n const result = new Error(error.message);\n result.name = error.name;\n if (error.stack) result.stack = error.stack;\n if (error.context !== undefined) Object.assign(result, { context: error.context });\n return result;\n}\n"],
|
|
5
|
-
"mappings": "
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -521,6 +521,7 @@ var WorkerClusterRuntime = class {
|
|
|
521
521
|
projectedLoads.set(owner.workerId, (projectedLoads.get(owner.workerId) ?? owner.load) + 1);
|
|
522
522
|
const generation = (previous?.generation ?? 0) + 1;
|
|
523
523
|
this.writeRoute(topicKey, owner, previous?.workerId, generation);
|
|
524
|
+
this.handlers.onDiagnostic?.({ operation: "route_migration", topic });
|
|
524
525
|
this.flushStorage();
|
|
525
526
|
this.handlers.onControl("UNSUBSCRIBE", topic);
|
|
526
527
|
this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
|
|
@@ -534,6 +535,14 @@ var WorkerClusterRuntime = class {
|
|
|
534
535
|
*/
|
|
535
536
|
publish(topic, data) {
|
|
536
537
|
const topicKey = this.rememberTopic(topic);
|
|
538
|
+
if (this.assignedTopics.has(topicKey)) {
|
|
539
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
540
|
+
}
|
|
541
|
+
for (const pattern of this.assignedTopics.values()) {
|
|
542
|
+
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
543
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
537
546
|
const workers = this.readWorkers();
|
|
538
547
|
const route = this.readRoute(topicKey);
|
|
539
548
|
const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
@@ -897,6 +906,8 @@ var WorkerClusterRuntime = class {
|
|
|
897
906
|
...route,
|
|
898
907
|
confirmedAt: this.environment.now()
|
|
899
908
|
});
|
|
909
|
+
const topic = this.knownTopics.get(topicKey);
|
|
910
|
+
if (topic) this.handlers.onDiagnostic?.({ operation: "route_ack", topic });
|
|
900
911
|
}
|
|
901
912
|
/** Remove routes whose topic has no subscribers and whose TTL has expired. */
|
|
902
913
|
cleanupOrphanedRoutes(workers) {
|
|
@@ -1190,6 +1201,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1190
1201
|
initialConfig;
|
|
1191
1202
|
hasInitialConfig;
|
|
1192
1203
|
trace;
|
|
1204
|
+
dedupMaxEntries;
|
|
1205
|
+
dedupTtlMs;
|
|
1206
|
+
dedupEnabled;
|
|
1207
|
+
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1193
1208
|
activeConfig;
|
|
1194
1209
|
status = "disconnected";
|
|
1195
1210
|
started = false;
|
|
@@ -1228,11 +1243,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1228
1243
|
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1229
1244
|
this.replayPersistence = replay?.persistence ?? null;
|
|
1230
1245
|
this.replayHydration = this.hydrateReplay();
|
|
1231
|
-
const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
|
|
1246
|
+
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1232
1247
|
this.transport = transport;
|
|
1233
1248
|
this.initialConfig = initialConfig;
|
|
1234
1249
|
this.hasInitialConfig = "initialConfig" in options;
|
|
1235
1250
|
this.trace = new DataBusTraceReporter(trace);
|
|
1251
|
+
this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
|
|
1252
|
+
this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
|
|
1253
|
+
this.dedupEnabled = dedup !== void 0;
|
|
1254
|
+
if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
|
|
1255
|
+
throw new TypeError("dedup.maxEntries must be a positive safe integer.");
|
|
1256
|
+
}
|
|
1257
|
+
if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
|
|
1258
|
+
throw new TypeError("dedup.ttlMs must be a positive finite number.");
|
|
1259
|
+
}
|
|
1236
1260
|
this.cluster = new WorkerClusterRuntime({
|
|
1237
1261
|
...clusterOptions,
|
|
1238
1262
|
handlers: {
|
|
@@ -1272,6 +1296,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1272
1296
|
this.trace.event({ type: "lifecycle", action: "resume" });
|
|
1273
1297
|
this.trace.start();
|
|
1274
1298
|
this.resumeTransport();
|
|
1299
|
+
},
|
|
1300
|
+
onDiagnostic: (event) => {
|
|
1301
|
+
this.trace.event({ type: "reliability", ...event });
|
|
1275
1302
|
}
|
|
1276
1303
|
}
|
|
1277
1304
|
});
|
|
@@ -1418,6 +1445,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1418
1445
|
if (handlers.size > 0) return;
|
|
1419
1446
|
this.topicHandlers.delete(topic);
|
|
1420
1447
|
this.replayBuffers?.delete(topic);
|
|
1448
|
+
if (this.replayPersistence?.clearTopic) {
|
|
1449
|
+
void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
|
|
1450
|
+
}
|
|
1421
1451
|
this.cluster.unsubscribe(topic);
|
|
1422
1452
|
}
|
|
1423
1453
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
@@ -1473,6 +1503,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1473
1503
|
else await this.transport.stop();
|
|
1474
1504
|
} finally {
|
|
1475
1505
|
this.transportSubscribedTopics.clear();
|
|
1506
|
+
this.seenMessageIds.clear();
|
|
1476
1507
|
this.started = false;
|
|
1477
1508
|
this.stopping = false;
|
|
1478
1509
|
this.suspended = false;
|
|
@@ -1490,6 +1521,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1490
1521
|
* and dispatches locally.
|
|
1491
1522
|
*/
|
|
1492
1523
|
handleTransportMessage(message) {
|
|
1524
|
+
if (this.isDuplicate(message)) return;
|
|
1493
1525
|
this.trace.recordReceived(message.topic);
|
|
1494
1526
|
if (!this.cluster.isAssigned(message.topic)) {
|
|
1495
1527
|
this.trace.recordDiscarded(message.topic);
|
|
@@ -1502,6 +1534,21 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1502
1534
|
}
|
|
1503
1535
|
this.trace.recordDiscarded(message.topic);
|
|
1504
1536
|
}
|
|
1537
|
+
isDuplicate(message) {
|
|
1538
|
+
if (!this.dedupEnabled || !message.messageId) return false;
|
|
1539
|
+
const now = Date.now();
|
|
1540
|
+
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1541
|
+
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1542
|
+
}
|
|
1543
|
+
if (this.seenMessageIds.has(message.messageId)) return true;
|
|
1544
|
+
this.seenMessageIds.set(message.messageId, now);
|
|
1545
|
+
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1546
|
+
const oldest = this.seenMessageIds.keys().next().value;
|
|
1547
|
+
if (oldest === void 0) break;
|
|
1548
|
+
this.seenMessageIds.delete(oldest);
|
|
1549
|
+
}
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1505
1552
|
/** Deliver a message to every local handler registered for its topic,
|
|
1506
1553
|
* plus every handler registered with a wildcard subscription that matches
|
|
1507
1554
|
* (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
|
|
@@ -1586,6 +1633,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1586
1633
|
const now = Date.now();
|
|
1587
1634
|
if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
|
|
1588
1635
|
this.lastRecoveryAt = now;
|
|
1636
|
+
this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
|
|
1589
1637
|
setTimeout(() => {
|
|
1590
1638
|
if (this.stopping || !this.started || this.suspended) return;
|
|
1591
1639
|
if (this.status !== "error") return;
|
|
@@ -1766,4 +1814,4 @@ export {
|
|
|
1766
1814
|
CrossTabDataBus,
|
|
1767
1815
|
selectWorkerBackend
|
|
1768
1816
|
};
|
|
1769
|
-
//# sourceMappingURL=chunk-
|
|
1817
|
+
//# sourceMappingURL=chunk-SI7N5KNJ.js.map
|