cross-tab-worker-databus 0.21.17 → 0.21.18
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/dist/centrifuge.js +1 -1
- package/dist/{chunk-EZYCSJRS.js → chunk-KRSD6KHE.js} +2 -2
- package/dist/cjs/centrifuge.cjs +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/roadmap.md +9 -1
- package/docs/zh/roadmap.md +9 -1
- package/package.json +1 -1
- /package/dist/{chunk-EZYCSJRS.js.map → chunk-KRSD6KHE.js.map} +0 -0
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/core/replay-persistence.ts", "../src/websocket.ts"],
|
|
4
|
-
"sourcesContent": ["import type { DataBusMessage } from './types';\nimport { pruneReplayHistory } from './replay-pruning';\nimport { DEFAULT_STORAGE_PREFIX, PRUNE_STRATEGY } from '../utils/constants';\nimport { assertPositiveFiniteNumber, assertPositiveSafeInteger, assertPruneStrategy } from '../utils/validation';\n\n/** Optional persistence backend for replay history. */\nexport interface DataBusReplayPersistence<TData = unknown> {\n load(): Promise<ReadonlyArray<DataBusMessage<TData>>>;\n append(message: DataBusMessage<TData>): Promise<void>;\n /** Optional bulk append used to amortize IndexedDB transaction overhead. */\n appendBatch?(messages: ReadonlyArray<DataBusMessage<TData>>): Promise<void>;\n /** Remove all persisted replay history. */\n clear?(): Promise<void>;\n /** Remove persisted replay history for one exact topic. */\n clearTopic?(topic: string): Promise<void>;\n /** Remove persisted messages older than the given epoch-millisecond cutoff. */\n clearBefore?(timestamp: number): Promise<void>;\n}\n\nexport interface IndexedDbReplayPersistenceOptions {\n dbName?: string;\n maxPerTopic: number;\n pruneStrategy?: (typeof PRUNE_STRATEGY)[keyof typeof PRUNE_STRATEGY];\n retentionMs?: number;\n}\n\n/** Create a browser IndexedDB-backed replay store. */\nexport function createIndexedDbReplayPersistence<TData = unknown>(\n options: IndexedDbReplayPersistenceOptions\n): DataBusReplayPersistence<TData> {\n const indexedDb = globalThis.indexedDB;\n if (!indexedDb) throw new Error('IndexedDB is unavailable in this environment.');\n const dbName = options.dbName ?? DEFAULT_STORAGE_PREFIX;\n const storeName = 'replay';\n const maxPerTopic = options.maxPerTopic;\n const pruneStrategy = options.pruneStrategy ?? PRUNE_STRATEGY.COUNT;\n const retentionMs = options.retentionMs;\n assertPruneStrategy(pruneStrategy);\n if (retentionMs !== undefined) assertPositiveFiniteNumber(retentionMs, 'retentionMs');\n assertPositiveSafeInteger(maxPerTopic, 'maxPerTopic');\n let dbPromise: Promise<IDBDatabase> | null = null;\n const invalidate = (db: IDBDatabase): void => {\n if (dbPromise) {\n // The rejection arm is what keeps this total: `invalidate` can chain onto a\n // *replacement* open that is still in flight, and `.then(current => \u2026)`\n // alone would derive a rejected promise nobody observes, turning that\n // open's failure into an unhandled rejection on top of the caller's. No\n // test reaches it \u2014 it needs a scheduled failure racing a rejecting reopen\n // \u2014 and it is not the `open()` catch below, which is registered on the\n // promise it guards and only clears the cache.\n void dbPromise.then(current => {\n if (current === db) {\n current.close();\n dbPromise = null;\n }\n }, () => undefined);\n }\n };\n // IndexedDB transactions are atomic, but a read-modify-write append can\n // still lose updates when callers start several appends concurrently.\n // Serialize all mutations per adapter instance while keeping reads free.\n // Consecutive append/appendBatch entries are coalesced into a single\n // transaction at the head of the queue, so a burst spanning many microtask\n // flushes issues one transaction instead of one per flush. Ordering against\n // clears is preserved: coalescing only merges adjacent batch entries and\n // never reorders them relative to a clear.\n type QueuedMutation =\n | { kind: 'batch'; messages: ReadonlyArray<DataBusMessage<TData>> }\n | { kind: 'run'; run: () => Promise<void> };\n const pending: Array<{\n mutation: QueuedMutation;\n resolve: () => void;\n reject: (error: unknown) => void;\n }> = [];\n let draining = false;\n\n const drain = async (): Promise<void> => {\n if (draining) return;\n draining = true;\n try {\n // Let a burst of synchronous enqueues accumulate into `pending` before\n // the first coalescing pass, so a single flush cycle's batches merge\n // into one transaction instead of two.\n await Promise.resolve();\n while (pending.length > 0) {\n const head = pending[0]!.mutation;\n if (head.kind === 'batch') {\n // Merge every adjacent batch entry into one transaction.\n const merged: DataBusMessage<TData>[] = [];\n const entries: Array<{ resolve: () => void; reject: (error: unknown) => void }> = [];\n while (pending.length > 0) {\n const next = pending[0]!.mutation;\n if (next.kind !== 'batch') break;\n merged.push(...next.messages);\n entries.push({ resolve: pending[0]!.resolve, reject: pending[0]!.reject });\n pending.shift();\n }\n try {\n await appendTransaction(merged);\n for (const entry of entries) entry.resolve();\n } catch (error) {\n for (const entry of entries) entry.reject(error);\n }\n } else {\n const entry = pending.shift()!;\n try {\n await head.run();\n entry.resolve();\n } catch (error) {\n entry.reject(error);\n }\n }\n }\n } finally {\n draining = false;\n }\n };\n\n const enqueue = (mutation: QueuedMutation): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n pending.push({ mutation, resolve, reject });\n void drain();\n });\n\n /** Read-modify-write one topic-batched append inside a single transaction. */\n const appendTransaction = (messages: ReadonlyArray<DataBusMessage<TData>>): Promise<void> =>\n (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try {\n transaction = db.transaction(storeName, 'readwrite');\n } catch (error) {\n invalidate(db);\n reject(error);\n return;\n }\n const store = transaction.objectStore(storeName);\n const grouped = new Map<string, DataBusMessage<TData>[]>();\n for (const message of messages) {\n grouped.set(message.topic, [...(grouped.get(message.topic) ?? []), message]);\n }\n let hasError = false;\n const fail = (error: unknown): void => {\n if (hasError) return;\n hasError = true;\n invalidate(db);\n reject(error);\n };\n for (const [topic, topicMessages] of grouped) {\n const request = store.get(topic);\n request.onsuccess = () => {\n if (hasError) return;\n const history = pruneReplayHistory(\n ((request.result?.messages ?? []) as DataBusMessage<TData>[]).concat(topicMessages),\n { maxPerTopic, pruneStrategy, retentionMs, now: Date.now() }\n );\n store.put({ topic, messages: history });\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to read replay history.'));\n }\n transaction.oncomplete = () => {\n if (!hasError) resolve();\n };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to persist replay history.'));\n // A connection loss can abort a transaction without first dispatching a\n // request error. Without this path, the serialized mutation queue would\n // stay blocked forever after the promise never settles.\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to persist replay history.'));\n });\n })();\n const open = (): Promise<IDBDatabase> => {\n // The two `if (dbPromise \u2026)` guards inside have never had their false arm\n // execute. Reaching either needs a connection that is live but no longer\n // cached, so a versionchange could land on an empty slot, or a slot replaced\n // before its own rejection catch runs \u2014 and both are excluded here because a\n // replacement can only start from a cleared slot, which is what the clearing\n // path itself performs. Defensive, so recorded rather than pinned.\n if (dbPromise) return dbPromise;\n const pending = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDb.open(dbName, 1);\n request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: 'topic' });\n request.onsuccess = () => {\n const db = request.result;\n // A schema upgrade in another tab invalidates this connection. Close\n // it and clear the cached promise so the next operation reopens a\n // usable connection instead of repeatedly targeting a dead database.\n db.onversionchange = () => {\n db.close();\n if (dbPromise) dbPromise = null;\n };\n resolve(db);\n };\n request.onerror = () => reject(request.error ?? new Error('Failed to open replay database.'));\n });\n dbPromise = pending;\n // Do not permanently cache a rejected open promise. IndexedDB can fail\n // transiently (quota, private-mode initialization, a closing connection,\n // or a browser shutdown); the next operation must be able to retry.\n void pending.catch(() => {\n if (dbPromise === pending) dbPromise = null;\n });\n return pending;\n };\n return {\n async load() {\n const db = await open();\n return new Promise((resolve, reject) => {\n let transaction: IDBTransaction;\n let request: IDBRequest;\n try {\n transaction = db.transaction(storeName, 'readonly');\n request = transaction.objectStore(storeName).getAll();\n } catch (error) {\n invalidate(db);\n reject(error);\n return;\n }\n // One-shot settlement, the same latch in all four `invalidate`+`reject`\n // closures here. Entering it twice is reachable \u2014 one aborting\n // transaction dispatches `error` and then `abort` \u2014 but measured\n // harmless: `reject` on a settled promise is a no-op, and the extra\n // `invalidate` cannot see a replaced cache because both signals are\n // dispatched inside one microtask batch, so their callbacks run before\n // the serialized queue reopens. Deleting all four latches leaves the\n // suite green and the connection count unchanged; they stay because that\n // proof rests on dispatch timing outside this file. The stale-signal case\n // that is inside this file is pinned by tests/replay-persistence.test.ts's\n // 'keeps the connection a later operation reopened when an older signal\n // lands late'.\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n let records: Array<{ messages: DataBusMessage<TData>[] }> = [];\n request.onsuccess = () => {\n records = request.result as Array<{ messages: DataBusMessage<TData>[] }>;\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to load replay history.'));\n transaction.oncomplete = () => {\n if (settled) return;\n settled = true;\n resolve(records.flatMap(record => record.messages));\n };\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to load replay history.'));\n });\n },\n append(message) {\n return enqueue({ kind: 'batch', messages: [message] });\n },\n appendBatch(messages) {\n if (messages.length === 0) return Promise.resolve();\n return enqueue({ kind: 'batch', messages });\n },\n clear() {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n transaction.objectStore(storeName).clear();\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to clear replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to clear replay history.'));\n });\n })()\n });\n },\n clearTopic(topic) {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n transaction.objectStore(storeName).delete(topic);\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to clear topic replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to clear topic replay history.'));\n });\n })()\n });\n },\n clearBefore(timestamp) {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n const store = transaction.objectStore(storeName);\n const request = store.getAll();\n request.onsuccess = () => {\n for (const record of request.result as Array<{ topic: string; messages: DataBusMessage<TData>[] }>) {\n const messages = record.messages.filter(message => message.timestamp === undefined || message.timestamp >= timestamp);\n if (messages.length === 0) store.delete(record.topic);\n else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });\n }\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to read replay history.'));\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to prune replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to prune replay history.'));\n });\n })()\n });\n }\n };\n}\n", "/**\n * WebSocketTransport \u2014 a dependency-free transport over a plain WebSocket.\n *\n * Validates the `DataBusTransport` abstraction with a second, minimal backend:\n * any WebSocket server that speaks the tiny JSON protocol below can back the\n * same cross-tab clustering stack (owner dedup, sticky routes, EVENT fan-out)\n * that the Centrifuge backend uses.\n *\n * Wire protocol \u2014 two frame formats, both over text/binary WebSocket messages:\n * - client \u2192 server: `{\"op\":\"subscribe\"|\"unsubscribe\"|\"publish\",\"topic\":...,\"data\":...}`\n * - client \u2192 server (batched): `{\"op\":\"publishBatch\",\"topic\":...,\"items\":[{data,...}]}`\n * - server \u2192 client: `{\"topic\":...,\"data\":...}` for publications; anything\n * without a string `topic` field is ignored (forward-compatible).\n * - binary publications use a *separate* tagged frame, not JSON:\n * `0xc7 | uint16 topicLength | topic UTF-8 | raw payload`, written by\n * `sendBinaryFrame` when `publish()` receives an `ArrayBuffer` with no\n * `messageId`/`timestamp` (with metadata it degrades to a JSON PUBLISH whose\n * `data` is a number array, so an id is never lost to the compact shape), and\n * parsed by the matching reader. A server that implements only the JSON arm\n * above silently drops those \u2014 see `scripts/demo-ws-server.mjs`, which\n * implements both directions.\n */\nimport { CrossTabDataBus } from './core/data-bus';\nimport { parseDataBusPublication } from './core/publication';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport { WS_OP, WORKER_STATUS } from './utils/constants';\nimport type {\n DataBusTransport,\n DataBusTransportHandlers,\n DataBusPublishOptions,\n DataBusPublicationItem,\n DataBusMessage,\n MaybePromise,\n WorkerStatus\n} from './core/types';\n\n/** Minimal WebSocket surface used by the transport. Matches the browser\n * `WebSocket` subset the transport touches; injectable for tests and runtimes. */\nexport interface WebSocketLike {\n /** Current connection state; 1 (OPEN) means frames may be sent. */\n readonly readyState?: number;\n send(data: string | ArrayBuffer): void;\n close(code?: number, reason?: string): void;\n onopen: (() => void) | null;\n onclose: (() => void) | null;\n onerror: (() => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n}\n\n/** Connection configuration for {@link WebSocketTransport}. */\nexport interface WebSocketDataBusConfig {\n /** WebSocket endpoint, e.g. `wss://example.test/ws`. */\n url: string;\n /** Subprotocol(s) passed to the WebSocket handshake. */\n protocols?: string | string[];\n /** Custom socket factory. Defaults to the global `WebSocket`; injectable\n * for tests and non-browser runtimes. */\n webSocketFactory?: (url: string, protocols?: string | string[]) => WebSocketLike;\n /** Milliseconds to wait for the handshake before reporting `error` and\n * failing the start. Defaults to 30000 ms; pass\n * `0` or `Infinity` to wait indefinitely. The timeout exists because\n * `start()` resolves on connect, so a socket that never opens and never\n * errors would otherwise leave the DataBus start gate (and every operation\n * queued behind it) pending forever. */\n connectTimeoutMs?: number;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a WebSocket transport. */\nexport interface CreateWebSocketDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<WebSocketDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n > {\n /** WebSocket connection configuration. */\n connection: WebSocketDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/** Default handshake budget. A socket that never opens and never fires\n * error/close would otherwise keep a started transport stuck in `connecting`\n * forever, with every queued operation parked behind an unsettled `start()`. */\nconst DEFAULT_CONNECT_TIMEOUT_MS = 30_000;\n\nconst WS_OPEN = 1;\n\n/** Transport that talks a minimal JSON protocol over a plain WebSocket.\n * Connection lifecycle maps directly to the DataBus status vocabulary:\n * open \u2192 `connected`, close \u2192 `disconnected`, error \u2192 `error` (which the\n * DataBus treats as its auto-recovery trigger). The transport holds no\n * reconnection logic of its own \u2014 reopening is the DataBus's job. */\nexport class WebSocketTransport<TData = unknown>\n implements DataBusTransport<WebSocketDataBusConfig, TData>\n{\n readonly diagnosticsName = 'websocket';\n readonly diagnosticsBackend = 'native-websocket';\n private socket: WebSocketLike | null = null;\n private socketActive = false;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n private readonly subscribedTopics = new Set<string>();\n // Handshake gate for the current start(). Resolves once the socket opens,\n // rejects when the attempt fails, so the DataBus start Promise \u2014 and every\n // operation parked behind it \u2014 settles at the real connection boundary.\n private connectPromise: Promise<void> | null = null;\n private connectResolve: (() => void) | null = null;\n private connectReject: ((error: unknown) => void) | null = null;\n private connectTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private readonly connection: WebSocketDataBusConfig) {}\n\n /** Open the WebSocket and wire lifecycle listeners. Resolves once the\n * handshake completes and rejects when the attempt fails, matching the\n * `DataBusTransport.start` contract (\"resolves on connect or rejects on\n * failure\"). A factory failure is reported through `onStatus('error')` so\n * the DataBus can recover. */\n start(config: WebSocketDataBusConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void> {\n if (this.socket && this.socketActive) {\n // Reuse the live socket instead of orphaning it. While the first\n // attempt is still connecting, share its handshake gate so a duplicate\n // start() cannot report readiness before the socket is usable.\n return this.connectPromise ?? undefined;\n }\n // A failed or closed socket is one-shot; retain its object only long\n // enough for a transparent same-object reopen to fire, but replace it\n // whenever start() is called again. Clearing the reference here also\n // makes every late callback from the old socket a no-op.\n this.socket = null;\n this.socketActive = false;\n this.handlers = handlers;\n // The factory may live on the constructor connection (createWebSocketDataBus\n // path) or on the runtime config (direct transport use) \u2014 accept both.\n const factory = config.webSocketFactory ?? this.connection.webSocketFactory ?? defaultWebSocketFactory;\n const protocols = config.protocols ?? this.connection.protocols;\n let socket: WebSocketLike;\n try {\n socket = factory(config.url, protocols);\n } catch (error) {\n handlers.onStatus(WORKER_STATUS.ERROR);\n handlers.onError(error);\n return;\n }\n const opening = new Promise<void>((resolve, reject) => {\n this.connectResolve = resolve;\n this.connectReject = reject;\n // Per-attempt handshake state. A socket that opens, then closes and\n // re-opens in place (a protocol-level recovery) may reuse the same\n // attempt; a timeout or a close/error before the first open permanently\n // invalidates it so a late onopen cannot report readiness.\n let handshakeCompleted = false;\n let handshakeFailed = false;\n const timeoutMs =\n config.connectTimeoutMs ?? this.connection.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n if (Number.isFinite(timeoutMs) && timeoutMs > 0) {\n this.connectTimer = setTimeout(() => {\n // The `handshakeCompleted` term cannot be reached today: `onopen`\n // cancels this timer directly and again through `settleConnect()`, so a\n // completed attempt has no live timer left to fire. Measured \u2014 deleting\n // only that term leaves all 880 tests green, while deleting it *and*\n // both cancels makes the transport report a phantom\n // `onStatus('error')` and abort a socket that had already connected.\n // The term stays because that is the asymmetric cost, and because\n // tests/websocket.test.ts's 'never lets the connect timer tear down a\n // handshake that already completed' now holds the pair.\n if (this.socket !== socket || this.handlers !== handlers || handshakeCompleted) return;\n handshakeFailed = true;\n this.connectTimer = null;\n this.socketActive = false;\n const error = new Error(`WebSocket did not open within ${timeoutMs}ms.`);\n handlers.onStatus(WORKER_STATUS.ERROR);\n handlers.onError(error);\n this.failConnect(error);\n // Abort the half-open handshake so the timed-out attempt cannot\n // linger in CONNECTING or deliver a late onopen.\n this.abortSocket(socket);\n }, timeoutMs);\n }\n socket.onopen = () => {\n if (this.socket !== socket || this.handlers !== handlers || handshakeFailed) return;\n this.socketActive = true;\n this.clearConnectTimer();\n // Re-assert every topic so a reopened socket (recovery path) restores\n // the server-side subscriptions without DataBus involvement.\n for (const topic of this.subscribedTopics) {\n this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });\n }\n handlers.onStatus(WORKER_STATUS.CONNECTED);\n if (!handshakeCompleted) {\n handshakeCompleted = true;\n this.settleConnect();\n }\n };\n socket.onclose = () => {\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n this.socketActive = false;\n handlers.onStatus(WORKER_STATUS.DISCONNECTED);\n if (!handshakeCompleted) {\n handshakeFailed = true;\n this.failConnect(new Error('WebSocket closed before the handshake completed.'));\n }\n };\n socket.onerror = () => {\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n this.socketActive = false;\n handlers.onStatus(WORKER_STATUS.ERROR);\n if (!handshakeCompleted) {\n handshakeFailed = true;\n this.failConnect(new Error('WebSocket failed to open.'));\n }\n // `error` is not guaranteed to be followed by `close` (especially for\n // injected WebSocket implementations). Abort here so DataBus recovery\n // cannot orphan a socket that this transport has already marked dead.\n this.abortSocket(socket);\n };\n socket.onmessage = event => {\n if (this.socket === socket && this.handlers === handlers && this.socketActive) {\n void this.handleMessage(event.data);\n }\n };\n this.socket = socket;\n this.socketActive = true;\n });\n this.connectPromise = opening;\n return opening;\n }\n\n /** Idempotent: re-subscribing an active topic re-sends the frame but does\n * not duplicate the local tracking entry. */\n subscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.add(topic);\n this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });\n }\n\n /** Idempotent for local tracking: an unknown topic has nothing to delete, but\n * the UNSUBSCRIBE frame is still sent, exactly as SUBSCRIBE is re-sent above. */\n unsubscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.delete(topic);\n this.sendFrame({ op: WS_OP.UNSUBSCRIBE, topic });\n }\n\n /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void> {\n if (data instanceof ArrayBuffer) {\n this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);\n return;\n }\n this.sendFrame({\n op: WS_OP.PUBLISH,\n topic,\n data,\n ...(options?.messageId === undefined ? {} : { messageId: options.messageId }),\n ...(options?.timestamp === undefined ? {} : { timestamp: options.timestamp })\n });\n }\n\n /** Publish many items for one topic as a single wire frame. One-item\n * batches delegate to `publish` so the legacy single-publication frame\n * shape (including binary framing) is preserved. */\n publishBatch(topic: string, items: ReadonlyArray<DataBusPublicationItem>): MaybePromise<void> {\n if (items.length === 0) return;\n if (items.length === 1) {\n const single = items[0]!;\n return this.publish(topic, single.data, {\n ...(single.messageId === undefined ? {} : { messageId: single.messageId }),\n ...(single.timestamp === undefined ? {} : { timestamp: single.timestamp })\n });\n }\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error('WebSocket is not open; dropped \"publishBatch\" frame.'));\n return;\n }\n // Binary payloads are embedded as byte arrays so the whole batch stays in\n // one JSON frame; the server re-fans them out as individual publications.\n this.socket.send(JSON.stringify({\n op: WS_OP.PUBLISH_BATCH,\n topic,\n items: items.map(item => ({\n data: item.data instanceof ArrayBuffer ? Array.from(new Uint8Array(item.data)) : item.data,\n ...(item.messageId === undefined ? {} : { messageId: item.messageId }),\n ...(item.timestamp === undefined ? {} : { timestamp: item.timestamp })\n }))\n }));\n }\n\n /** Close the socket and drop all state. Safe to call multiple times. */\n stop(): MaybePromise<void> {\n const socket = this.socket;\n const shouldClose = this.socketActive;\n this.socket = null;\n this.socketActive = false;\n this.handlers = null;\n this.subscribedTopics.clear();\n // Settle an in-flight handshake gate: a DataBus stop() awaits the start\n // Promise, so leaving it pending would hang teardown. Resolving (rather\n // than rejecting) keeps an intentional stop from surfacing as an error.\n this.settleConnect();\n this.connectPromise = null;\n if (shouldClose) socket?.close();\n }\n\n /** Resolve the in-flight handshake gate. Idempotent: once the socket has\n * opened (or a newer attempt replaced it) later calls are no-ops. */\n private settleConnect(): void {\n this.clearConnectTimer();\n const resolve = this.connectResolve;\n this.connectResolve = null;\n this.connectReject = null;\n resolve?.();\n }\n\n /** Reject the in-flight handshake gate. Idempotent on the same terms as\n * {@link settleConnect}. */\n private failConnect(error: unknown): void {\n this.clearConnectTimer();\n const reject = this.connectReject;\n this.connectResolve = null;\n this.connectReject = null;\n reject?.(error);\n }\n\n /** Best-effort close for a socket that can no longer carry transport data.\n * The error that invalidated it has already been reported by the caller. */\n private abortSocket(socket: WebSocketLike): void {\n try {\n socket.close();\n } catch {\n // Some injected implementations throw when close races a failed\n // handshake. The original failure remains the actionable error.\n }\n }\n\n private clearConnectTimer(): void {\n if (this.connectTimer !== null) {\n clearTimeout(this.connectTimer);\n this.connectTimer = null;\n }\n }\n\n /** Send one JSON frame. Frames are dropped with an `onError` report when\n * the socket is not open. A dropped SUBSCRIBE is recovered \u2014 `onopen`\n * re-asserts every topic in `subscribedTopics` \u2014 but the other two are not:\n * a dropped PUBLISH is the documented at-most-once loss, and a dropped\n * UNSUBSCRIBE is the quieter one, because `unsubscribe()` has already deleted\n * the topic from that set, so no later re-assert resends it and the server\n * keeps streaming a channel the app left. Through the DataBus neither of the\n * two is reachable while closed \u2014 `runTransport()` queues both operations\n * behind recovery \u2014 so this reads as a direct-transport-contract note. */\n private sendFrame(payload: { op: string; topic: string; data?: unknown; messageId?: string; timestamp?: number }): void {\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error(`WebSocket is not open; dropped \"${payload.op}\" frame.`));\n return;\n }\n this.socket.send(JSON.stringify(payload));\n }\n\n private sendBinaryFrame(topic: string, data: ArrayBuffer, messageId?: string, timestamp?: number): void {\n if (messageId !== undefined || timestamp !== undefined) {\n // Binary frames retain their compact legacy shape; metadata is sent as a\n // JSON envelope so IDs are never silently lost.\n this.sendFrame({\n op: WS_OP.PUBLISH,\n topic,\n data: Array.from(new Uint8Array(data)),\n ...(messageId === undefined ? {} : { messageId }),\n ...(timestamp === undefined ? {} : { timestamp })\n });\n return;\n }\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error('WebSocket is not open; dropped \"publish\" frame.'));\n return;\n }\n const topicBytes = new TextEncoder().encode(topic);\n if (topicBytes.length > 0xffff) {\n this.handlers?.onError(new Error('WebSocket topic is too long for a binary frame.'));\n return;\n }\n const frame = new Uint8Array(3 + topicBytes.length + data.byteLength);\n frame[0] = 0xc7;\n new DataView(frame.buffer).setUint16(1, topicBytes.length);\n frame.set(topicBytes, 3);\n frame.set(new Uint8Array(data), 3 + topicBytes.length);\n this.socket.send(frame.buffer);\n }\n\n /** Parse a server frame. Only objects carrying a string `topic` are\n * publications, and unknown shapes are dropped silently so a chatty server\n * cannot crash the message path. Malformed JSON is *not* dropped silently: it\n * goes to `handlers.onError` as \"WebSocket server sent a non-JSON frame.\",\n * which also records it in the bus's last-failure diagnostics. Neither case\n * throws out of the socket listener. */\n private async handleMessage(raw: unknown): Promise<void> {\n // Browser WebSockets may deliver binary frames as Blob unless\n // `binaryType = 'arraybuffer'` is explicitly configured by the host.\n // Normalize Blob asynchronously and reuse the exact ArrayBuffer parser.\n if (typeof Blob !== 'undefined' && raw instanceof Blob) {\n // Blob conversion is asynchronous. Capture the socket/handler pair so a\n // stop/start that replaces the connection while arrayBuffer() is pending\n // cannot let the stale frame leak into the new connection's handlers.\n const socket = this.socket;\n const handlers = this.handlers;\n try {\n const buffer = await raw.arrayBuffer();\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n await this.handleMessage(buffer);\n } catch (error) {\n if (this.socket === socket && this.handlers === handlers && this.socketActive) {\n this.handlers?.onError(error);\n }\n }\n return;\n }\n let parsed: unknown;\n if (raw instanceof ArrayBuffer) {\n const bytes = new Uint8Array(raw);\n if (bytes[0] !== 0xc7 || bytes.length < 3) return;\n const topicLength = new DataView(raw).getUint16(1);\n if (bytes.length < 3 + topicLength) return;\n const topic = new TextDecoder().decode(bytes.subarray(3, 3 + topicLength));\n const data = bytes.slice(3 + topicLength).buffer;\n this.handlers?.onMessage({ topic, data: data as TData });\n return;\n }\n if (typeof raw !== 'string') return;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.handlers?.onError(new Error('WebSocket server sent a non-JSON frame.'));\n return;\n }\n if (!parsed || typeof parsed !== 'object') return;\n const publication = parseDataBusPublication<TData>(parsed);\n if (publication) this.handlers?.onMessage(publication as DataBusMessage<TData>);\n }\n}\n\n/** Resolve the platform WebSocket. Throws in a runtime without one (SSR/Node);\n * it has no null arm, and `start()`'s catch is what turns that throw into an\n * `onStatus('error')` + `onError` report. */\nfunction defaultWebSocketFactory(url: string, protocols?: string | string[]): WebSocketLike {\n if (typeof WebSocket === 'undefined') {\n throw new Error('WebSocketTransport requires a WebSocket implementation.');\n }\n return new WebSocket(url, protocols) as unknown as WebSocketLike;\n}\n\n/** Create a CrossTabDataBus backed by a plain WebSocket transport.\n * Cross-tab clustering (owner dedup, sticky routes, failover) works identically\n * to the Centrifuge backend \u2014 only the transport I/O differs. */\nexport function createWebSocketDataBus<TData = unknown>(\n options: CreateWebSocketDataBusOptions<TData>\n): CrossTabDataBus<WebSocketDataBusConfig, TData> {\n const { clusterKey, connection, ...dataBusOptions } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new WebSocketTransport<TData>(connection)\n });\n}\n\n/** Re-export for convenience: the status type used by the transport. */\nexport type { WorkerStatus };\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { DataBusMessage } from './types';\nimport { pruneReplayHistory } from './replay-pruning';\nimport { DEFAULT_STORAGE_PREFIX, PRUNE_STRATEGY } from '../utils/constants';\nimport { assertPositiveFiniteNumber, assertPositiveSafeInteger, assertPruneStrategy } from '../utils/validation';\n\n/** Optional persistence backend for replay history. */\nexport interface DataBusReplayPersistence<TData = unknown> {\n load(): Promise<ReadonlyArray<DataBusMessage<TData>>>;\n append(message: DataBusMessage<TData>): Promise<void>;\n /** Optional bulk append used to amortize IndexedDB transaction overhead. */\n appendBatch?(messages: ReadonlyArray<DataBusMessage<TData>>): Promise<void>;\n /** Remove all persisted replay history. */\n clear?(): Promise<void>;\n /** Remove persisted replay history for one exact topic. */\n clearTopic?(topic: string): Promise<void>;\n /** Remove persisted messages older than the given epoch-millisecond cutoff. */\n clearBefore?(timestamp: number): Promise<void>;\n}\n\nexport interface IndexedDbReplayPersistenceOptions {\n dbName?: string;\n maxPerTopic: number;\n pruneStrategy?: (typeof PRUNE_STRATEGY)[keyof typeof PRUNE_STRATEGY];\n retentionMs?: number;\n}\n\n/** Create a browser IndexedDB-backed replay store. */\nexport function createIndexedDbReplayPersistence<TData = unknown>(\n options: IndexedDbReplayPersistenceOptions\n): DataBusReplayPersistence<TData> {\n const indexedDb = globalThis.indexedDB;\n if (!indexedDb) throw new Error('IndexedDB is unavailable in this environment.');\n const dbName = options.dbName ?? DEFAULT_STORAGE_PREFIX;\n const storeName = 'replay';\n const maxPerTopic = options.maxPerTopic;\n const pruneStrategy = options.pruneStrategy ?? PRUNE_STRATEGY.COUNT;\n const retentionMs = options.retentionMs;\n assertPruneStrategy(pruneStrategy);\n if (retentionMs !== undefined) assertPositiveFiniteNumber(retentionMs, 'retentionMs');\n assertPositiveSafeInteger(maxPerTopic, 'maxPerTopic');\n let dbPromise: Promise<IDBDatabase> | null = null;\n const invalidate = (db: IDBDatabase): void => {\n if (dbPromise) {\n // The rejection arm is what keeps this total: `invalidate` can chain onto a\n // *replacement* open that is still in flight, and `.then(current => \u2026)`\n // alone would derive a rejected promise nobody observes, turning that\n // open's failure into an unhandled rejection on top of the caller's. No\n // test reaches it \u2014 it needs a scheduled failure racing a rejecting reopen\n // \u2014 and it is not the `open()` catch below, which is registered on the\n // promise it guards and only clears the cache.\n void dbPromise.then(current => {\n if (current === db) {\n current.close();\n dbPromise = null;\n }\n }, () => undefined);\n }\n };\n // IndexedDB transactions are atomic, but a read-modify-write append can\n // still lose updates when callers start several appends concurrently.\n // Serialize all mutations per adapter instance while keeping reads free.\n // Consecutive append/appendBatch entries are coalesced into a single\n // transaction at the head of the queue, so a burst spanning many microtask\n // flushes issues one transaction instead of one per flush. Ordering against\n // clears is preserved: coalescing only merges adjacent batch entries and\n // never reorders them relative to a clear.\n type QueuedMutation =\n | { kind: 'batch'; messages: ReadonlyArray<DataBusMessage<TData>> }\n | { kind: 'run'; run: () => Promise<void> };\n const pending: Array<{\n mutation: QueuedMutation;\n resolve: () => void;\n reject: (error: unknown) => void;\n }> = [];\n let draining = false;\n\n const drain = async (): Promise<void> => {\n if (draining) return;\n draining = true;\n try {\n // Let a burst of synchronous enqueues accumulate into `pending` before\n // the first coalescing pass, so a single flush cycle's batches merge\n // into one transaction instead of two.\n await Promise.resolve();\n while (pending.length > 0) {\n const head = pending[0]!.mutation;\n if (head.kind === 'batch') {\n // Merge every adjacent batch entry into one transaction.\n const merged: DataBusMessage<TData>[] = [];\n const entries: Array<{ resolve: () => void; reject: (error: unknown) => void }> = [];\n while (pending.length > 0) {\n const next = pending[0]!.mutation;\n if (next.kind !== 'batch') break;\n merged.push(...next.messages);\n entries.push({ resolve: pending[0]!.resolve, reject: pending[0]!.reject });\n pending.shift();\n }\n try {\n await appendTransaction(merged);\n for (const entry of entries) entry.resolve();\n } catch (error) {\n for (const entry of entries) entry.reject(error);\n }\n } else {\n const entry = pending.shift()!;\n try {\n await head.run();\n entry.resolve();\n } catch (error) {\n entry.reject(error);\n }\n }\n }\n } finally {\n draining = false;\n }\n };\n\n const enqueue = (mutation: QueuedMutation): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n pending.push({ mutation, resolve, reject });\n void drain();\n });\n\n /** Read-modify-write one topic-batched append inside a single transaction. */\n const appendTransaction = (messages: ReadonlyArray<DataBusMessage<TData>>): Promise<void> =>\n (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try {\n transaction = db.transaction(storeName, 'readwrite');\n } catch (error) {\n invalidate(db);\n reject(error);\n return;\n }\n const store = transaction.objectStore(storeName);\n const grouped = new Map<string, DataBusMessage<TData>[]>();\n for (const message of messages) {\n grouped.set(message.topic, [...(grouped.get(message.topic) ?? []), message]);\n }\n let hasError = false;\n const fail = (error: unknown): void => {\n if (hasError) return;\n hasError = true;\n invalidate(db);\n reject(error);\n };\n for (const [topic, topicMessages] of grouped) {\n const request = store.get(topic);\n request.onsuccess = () => {\n if (hasError) return;\n const history = pruneReplayHistory(\n ((request.result?.messages ?? []) as DataBusMessage<TData>[]).concat(topicMessages),\n { maxPerTopic, pruneStrategy, retentionMs, now: Date.now() }\n );\n store.put({ topic, messages: history });\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to read replay history.'));\n }\n transaction.oncomplete = () => {\n if (!hasError) resolve();\n };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to persist replay history.'));\n // A connection loss can abort a transaction without first dispatching a\n // request error. Without this path, the serialized mutation queue would\n // stay blocked forever after the promise never settles.\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to persist replay history.'));\n });\n })();\n const open = (): Promise<IDBDatabase> => {\n // The two `if (dbPromise \u2026)` guards inside have never had their false arm\n // execute. Reaching either needs a connection that is live but no longer\n // cached, so a versionchange could land on an empty slot, or a slot replaced\n // before its own rejection catch runs \u2014 and both are excluded here because a\n // replacement can only start from a cleared slot, which is what the clearing\n // path itself performs. Defensive, so recorded rather than pinned.\n if (dbPromise) return dbPromise;\n const pending = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDb.open(dbName, 1);\n request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: 'topic' });\n request.onsuccess = () => {\n const db = request.result;\n // A schema upgrade in another tab invalidates this connection. Close\n // it and clear the cached promise so the next operation reopens a\n // usable connection instead of repeatedly targeting a dead database.\n db.onversionchange = () => {\n db.close();\n if (dbPromise) dbPromise = null;\n };\n resolve(db);\n };\n request.onerror = () => reject(request.error ?? new Error('Failed to open replay database.'));\n });\n dbPromise = pending;\n // Do not permanently cache a rejected open promise. IndexedDB can fail\n // transiently (quota, private-mode initialization, a closing connection,\n // or a browser shutdown); the next operation must be able to retry.\n void pending.catch(() => {\n if (dbPromise === pending) dbPromise = null;\n });\n return pending;\n };\n return {\n async load() {\n const db = await open();\n return new Promise((resolve, reject) => {\n let transaction: IDBTransaction;\n let request: IDBRequest;\n try {\n transaction = db.transaction(storeName, 'readonly');\n request = transaction.objectStore(storeName).getAll();\n } catch (error) {\n invalidate(db);\n reject(error);\n return;\n }\n // One-shot settlement, the same latch in all four `invalidate`+`reject`\n // closures here. Entering it twice is reachable \u2014 one aborting\n // transaction dispatches `error` and then `abort` \u2014 but measured\n // harmless: `reject` on a settled promise is a no-op, and the extra\n // `invalidate` cannot see a replaced cache because both signals are\n // dispatched inside one microtask batch, so their callbacks run before\n // the serialized queue reopens. Deleting all four latches leaves the\n // suite green and the connection count unchanged; they stay because that\n // proof rests on dispatch timing outside this file. The stale-signal case\n // that is inside this file is pinned by tests/replay-persistence.test.ts's\n // 'keeps the connection a later operation reopened when an older signal\n // lands late'.\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n let records: Array<{ messages: DataBusMessage<TData>[] }> = [];\n request.onsuccess = () => {\n records = request.result as Array<{ messages: DataBusMessage<TData>[] }>;\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to load replay history.'));\n transaction.oncomplete = () => {\n if (settled) return;\n settled = true;\n resolve(records.flatMap(record => record.messages));\n };\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to load replay history.'));\n });\n },\n append(message) {\n return enqueue({ kind: 'batch', messages: [message] });\n },\n appendBatch(messages) {\n if (messages.length === 0) return Promise.resolve();\n return enqueue({ kind: 'batch', messages });\n },\n clear() {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n transaction.objectStore(storeName).clear();\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to clear replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to clear replay history.'));\n });\n })()\n });\n },\n clearTopic(topic) {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n transaction.objectStore(storeName).delete(topic);\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to clear topic replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to clear topic replay history.'));\n });\n })()\n });\n },\n clearBefore(timestamp) {\n return enqueue({\n kind: 'run',\n run: () => (async () => {\n const db = await open();\n return new Promise<void>((resolve, reject) => {\n let transaction: IDBTransaction;\n try { transaction = db.transaction(storeName, 'readwrite'); }\n catch (error) { invalidate(db); reject(error); return; }\n let settled = false;\n const fail = (error: unknown): void => {\n if (settled) return;\n settled = true;\n invalidate(db);\n reject(error);\n };\n const store = transaction.objectStore(storeName);\n const request = store.getAll();\n request.onsuccess = () => {\n for (const record of request.result as Array<{ topic: string; messages: DataBusMessage<TData>[] }>) {\n const messages = record.messages.filter(message => message.timestamp === undefined || message.timestamp >= timestamp);\n if (messages.length === 0) store.delete(record.topic);\n else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });\n }\n };\n request.onerror = () => fail(request.error ?? new Error('Failed to read replay history.'));\n transaction.oncomplete = () => { settled = true; resolve(); };\n transaction.onerror = () => fail(transaction.error ?? new Error('Failed to prune replay history.'));\n transaction.onabort = () => fail(transaction.error ?? new Error('Failed to prune replay history.'));\n });\n })()\n });\n }\n };\n}\n", "/**\n * WebSocketTransport \u2014 a dependency-free transport over a plain WebSocket.\n *\n * Validates the `DataBusTransport` abstraction with a second, minimal backend:\n * any WebSocket server that speaks the tiny JSON protocol below can back the\n * same cross-tab clustering stack (owner dedup, sticky routes, EVENT fan-out)\n * that the Centrifuge backend uses.\n *\n * Wire protocol \u2014 two frame formats, both over text/binary WebSocket messages:\n * - client \u2192 server: `{\"op\":\"subscribe\"|\"unsubscribe\"|\"publish\",\"topic\":...,\"data\":...}`\n * - client \u2192 server (batched): `{\"op\":\"publishBatch\",\"topic\":...,\"items\":[{data,...}]}`\n * - server \u2192 client: `{\"topic\":...,\"data\":...}` for publications; anything\n * without a string `topic` field is ignored (forward-compatible).\n * - binary publications use a *separate* tagged frame, not JSON:\n * `0xc7 | uint16 topicLength | topic UTF-8 | raw payload`, written by\n * `sendBinaryFrame` when `publish()` receives an `ArrayBuffer` with no\n * `messageId`/`timestamp` (with metadata it degrades to a JSON PUBLISH whose\n * `data` is a number array, so an id is never lost to the compact shape), and\n * parsed by the matching reader. A server that implements only the JSON arm\n * above silently drops those \u2014 see `scripts/demo-ws-server.mjs`, which\n * implements both directions.\n */\nimport { CrossTabDataBus } from './core/data-bus';\nimport { parseDataBusPublication } from './core/publication';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport { WS_OP, WORKER_STATUS } from './utils/constants';\nimport type {\n DataBusTransport,\n DataBusTransportHandlers,\n DataBusPublishOptions,\n DataBusPublicationItem,\n DataBusMessage,\n MaybePromise,\n WorkerStatus\n} from './core/types';\n\n/** Minimal WebSocket surface used by the transport. Matches the browser\n * `WebSocket` subset the transport touches; injectable for tests and runtimes. */\nexport interface WebSocketLike {\n /** Current connection state; 1 (OPEN) means frames may be sent. */\n readonly readyState?: number;\n send(data: string | ArrayBuffer): void;\n close(code?: number, reason?: string): void;\n onopen: (() => void) | null;\n onclose: (() => void) | null;\n onerror: (() => void) | null;\n onmessage: ((event: { data: unknown }) => void) | null;\n}\n\n/** Connection configuration for {@link WebSocketTransport}. */\nexport interface WebSocketDataBusConfig {\n /** WebSocket endpoint, e.g. `wss://example.test/ws`. */\n url: string;\n /** Subprotocol(s) passed to the WebSocket handshake. */\n protocols?: string | string[];\n /** Custom socket factory. Defaults to the global `WebSocket`; injectable\n * for tests and non-browser runtimes. */\n webSocketFactory?: (url: string, protocols?: string | string[]) => WebSocketLike;\n /** Milliseconds to wait for the handshake before reporting `error` and\n * failing the start. Defaults to 30000 ms; pass\n * `0` or `Infinity` to wait indefinitely. The timeout exists because\n * `start()` resolves on connect, so a socket that never opens and never\n * errors would otherwise leave the DataBus start gate (and every operation\n * queued behind it) pending forever. */\n connectTimeoutMs?: number;\n}\n\n/** Options for creating a fully-configured CrossTabDataBus with a WebSocket transport. */\nexport interface CreateWebSocketDataBusOptions<TData = unknown>\n extends Omit<\n CrossTabDataBusOptions<WebSocketDataBusConfig, TData>,\n 'autoStart' | 'clusterKey' | 'initialConfig' | 'transport'\n > {\n /** WebSocket connection configuration. */\n connection: WebSocketDataBusConfig;\n /** Cluster key for cross-tab coordination. Defaults to the connection URL. */\n clusterKey?: string;\n}\n\n/** Default handshake budget. A socket that never opens and never fires\n * error/close would otherwise keep a started transport stuck in `connecting`\n * forever, with every queued operation parked behind an unsettled `start()`. */\nconst DEFAULT_CONNECT_TIMEOUT_MS = 30_000;\n\nconst WS_OPEN = 1;\n\n/** Transport that talks a minimal JSON protocol over a plain WebSocket.\n * Connection lifecycle maps directly to the DataBus status vocabulary:\n * open \u2192 `connected`, close \u2192 `disconnected`, error \u2192 `error` (which the\n * DataBus treats as its auto-recovery trigger). The transport holds no\n * reconnection logic of its own \u2014 reopening is the DataBus's job. */\nexport class WebSocketTransport<TData = unknown>\n implements DataBusTransport<WebSocketDataBusConfig, TData>\n{\n readonly diagnosticsName = 'websocket';\n readonly diagnosticsBackend = 'native-websocket';\n private socket: WebSocketLike | null = null;\n private socketActive = false;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n private readonly subscribedTopics = new Set<string>();\n // Handshake gate for the current start(). Resolves once the socket opens,\n // rejects when the attempt fails, so the DataBus start Promise \u2014 and every\n // operation parked behind it \u2014 settles at the real connection boundary.\n private connectPromise: Promise<void> | null = null;\n private connectResolve: (() => void) | null = null;\n private connectReject: ((error: unknown) => void) | null = null;\n private connectTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private readonly connection: WebSocketDataBusConfig) {}\n\n /** Open the WebSocket and wire lifecycle listeners. Resolves once the\n * handshake completes and rejects when the attempt fails, matching the\n * `DataBusTransport.start` contract (\"resolves on connect or rejects on\n * failure\"). A factory failure is reported through `onStatus('error')` so\n * the DataBus can recover. */\n start(config: WebSocketDataBusConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void> {\n if (this.socket && this.socketActive) {\n // Reuse the live socket instead of orphaning it. While the first\n // attempt is still connecting, share its handshake gate so a duplicate\n // start() cannot report readiness before the socket is usable.\n return this.connectPromise ?? undefined;\n }\n // A failed or closed socket is one-shot; retain its object only long\n // enough for a transparent same-object reopen to fire, but replace it\n // whenever start() is called again. Clearing the reference here also\n // makes every late callback from the old socket a no-op.\n this.socket = null;\n this.socketActive = false;\n this.handlers = handlers;\n // The factory may live on the constructor connection (createWebSocketDataBus\n // path) or on the runtime config (direct transport use) \u2014 accept both.\n const factory = config.webSocketFactory ?? this.connection.webSocketFactory ?? defaultWebSocketFactory;\n const protocols = config.protocols ?? this.connection.protocols;\n let socket: WebSocketLike;\n try {\n socket = factory(config.url, protocols);\n } catch (error) {\n handlers.onStatus(WORKER_STATUS.ERROR);\n handlers.onError(error);\n return;\n }\n const opening = new Promise<void>((resolve, reject) => {\n this.connectResolve = resolve;\n this.connectReject = reject;\n // Per-attempt handshake state. A socket that opens, then closes and\n // re-opens in place (a protocol-level recovery) may reuse the same\n // attempt; a timeout or a close/error before the first open permanently\n // invalidates it so a late onopen cannot report readiness.\n let handshakeCompleted = false;\n let handshakeFailed = false;\n const timeoutMs =\n config.connectTimeoutMs ?? this.connection.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;\n if (Number.isFinite(timeoutMs) && timeoutMs > 0) {\n this.connectTimer = setTimeout(() => {\n // This early return has never executed in any run of the suite, and the\n // reason is the same for all three terms: no attempt can leave a timer\n // armed behind it. Term 3 (`handshakeCompleted`) because `onopen` cancels\n // here directly and again through `settleConnect()`; terms 1 and 2 because\n // `this.socket` and `this.handlers` are reassigned only in `start()` and\n // `stop()`, reaching either assignment in `start()` needs `socketActive`\n // already false, and every path that lowers `socketActive` clears the\n // armed timer first \u2014 `onclose` and `onerror` through `failConnect()`,\n // `stop()` through `settleConnect()`, and this callback by nulling the\n // handle as it fires. So the guard cannot distinguish anything, which is\n // what its coverage arm records.\n //\n // It stays as the transport's statement that a superseded attempt must\n // never act, because the domination is a property of those cancels rather\n // than of this condition, and the asymmetry is measured: deleting only the\n // `handshakeCompleted` term leaves the suite green, deleting only `failConnect()`'s cancel lets a dead attempt's\n // timeout report a second failure for a connection the application was\n // already told had failed, and the first pin's own comment records the\n // third case \u2014 deleting the term *and* both cancels makes a healthy socket\n // report a phantom `onStatus('error')` and get aborted. The two pins name\n // the legs they hold: tests/websocket.test.ts's 'never lets the connect\n // timer tear down a handshake that already completed', and 'leaves no\n // connect timer armed when the handshake fails on a close'.\n if (this.socket !== socket || this.handlers !== handlers || handshakeCompleted) return;\n handshakeFailed = true;\n this.connectTimer = null;\n this.socketActive = false;\n const error = new Error(`WebSocket did not open within ${timeoutMs}ms.`);\n handlers.onStatus(WORKER_STATUS.ERROR);\n handlers.onError(error);\n this.failConnect(error);\n // Abort the half-open handshake so the timed-out attempt cannot\n // linger in CONNECTING or deliver a late onopen.\n this.abortSocket(socket);\n }, timeoutMs);\n }\n socket.onopen = () => {\n if (this.socket !== socket || this.handlers !== handlers || handshakeFailed) return;\n this.socketActive = true;\n this.clearConnectTimer();\n // Re-assert every topic so a reopened socket (recovery path) restores\n // the server-side subscriptions without DataBus involvement.\n for (const topic of this.subscribedTopics) {\n this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });\n }\n handlers.onStatus(WORKER_STATUS.CONNECTED);\n if (!handshakeCompleted) {\n handshakeCompleted = true;\n this.settleConnect();\n }\n };\n socket.onclose = () => {\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n this.socketActive = false;\n handlers.onStatus(WORKER_STATUS.DISCONNECTED);\n if (!handshakeCompleted) {\n handshakeFailed = true;\n this.failConnect(new Error('WebSocket closed before the handshake completed.'));\n }\n };\n socket.onerror = () => {\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n this.socketActive = false;\n handlers.onStatus(WORKER_STATUS.ERROR);\n if (!handshakeCompleted) {\n handshakeFailed = true;\n this.failConnect(new Error('WebSocket failed to open.'));\n }\n // `error` is not guaranteed to be followed by `close` (especially for\n // injected WebSocket implementations). Abort here so DataBus recovery\n // cannot orphan a socket that this transport has already marked dead.\n this.abortSocket(socket);\n };\n socket.onmessage = event => {\n if (this.socket === socket && this.handlers === handlers && this.socketActive) {\n void this.handleMessage(event.data);\n }\n };\n this.socket = socket;\n this.socketActive = true;\n });\n this.connectPromise = opening;\n return opening;\n }\n\n /** Idempotent: re-subscribing an active topic re-sends the frame but does\n * not duplicate the local tracking entry. */\n subscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.add(topic);\n this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });\n }\n\n /** Idempotent for local tracking: an unknown topic has nothing to delete, but\n * the UNSUBSCRIBE frame is still sent, exactly as SUBSCRIBE is re-sent above. */\n unsubscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.delete(topic);\n this.sendFrame({ op: WS_OP.UNSUBSCRIBE, topic });\n }\n\n /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */\n publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void> {\n if (data instanceof ArrayBuffer) {\n this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);\n return;\n }\n this.sendFrame({\n op: WS_OP.PUBLISH,\n topic,\n data,\n ...(options?.messageId === undefined ? {} : { messageId: options.messageId }),\n ...(options?.timestamp === undefined ? {} : { timestamp: options.timestamp })\n });\n }\n\n /** Publish many items for one topic as a single wire frame. One-item\n * batches delegate to `publish` so the legacy single-publication frame\n * shape (including binary framing) is preserved. */\n publishBatch(topic: string, items: ReadonlyArray<DataBusPublicationItem>): MaybePromise<void> {\n if (items.length === 0) return;\n if (items.length === 1) {\n const single = items[0]!;\n return this.publish(topic, single.data, {\n ...(single.messageId === undefined ? {} : { messageId: single.messageId }),\n ...(single.timestamp === undefined ? {} : { timestamp: single.timestamp })\n });\n }\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error('WebSocket is not open; dropped \"publishBatch\" frame.'));\n return;\n }\n // Binary payloads are embedded as byte arrays so the whole batch stays in\n // one JSON frame; the server re-fans them out as individual publications.\n this.socket.send(JSON.stringify({\n op: WS_OP.PUBLISH_BATCH,\n topic,\n items: items.map(item => ({\n data: item.data instanceof ArrayBuffer ? Array.from(new Uint8Array(item.data)) : item.data,\n ...(item.messageId === undefined ? {} : { messageId: item.messageId }),\n ...(item.timestamp === undefined ? {} : { timestamp: item.timestamp })\n }))\n }));\n }\n\n /** Close the socket and drop all state. Safe to call multiple times. */\n stop(): MaybePromise<void> {\n const socket = this.socket;\n const shouldClose = this.socketActive;\n this.socket = null;\n this.socketActive = false;\n this.handlers = null;\n this.subscribedTopics.clear();\n // Settle an in-flight handshake gate: a DataBus stop() awaits the start\n // Promise, so leaving it pending would hang teardown. Resolving (rather\n // than rejecting) keeps an intentional stop from surfacing as an error.\n this.settleConnect();\n this.connectPromise = null;\n if (shouldClose) socket?.close();\n }\n\n /** Resolve the in-flight handshake gate. Idempotent: once the socket has\n * opened (or a newer attempt replaced it) later calls are no-ops. */\n private settleConnect(): void {\n this.clearConnectTimer();\n const resolve = this.connectResolve;\n this.connectResolve = null;\n this.connectReject = null;\n resolve?.();\n }\n\n /** Reject the in-flight handshake gate. Idempotent on the same terms as\n * {@link settleConnect}. */\n private failConnect(error: unknown): void {\n this.clearConnectTimer();\n const reject = this.connectReject;\n this.connectResolve = null;\n this.connectReject = null;\n reject?.(error);\n }\n\n /** Best-effort close for a socket that can no longer carry transport data.\n * The error that invalidated it has already been reported by the caller. */\n private abortSocket(socket: WebSocketLike): void {\n try {\n socket.close();\n } catch {\n // Some injected implementations throw when close races a failed\n // handshake. The original failure remains the actionable error.\n }\n }\n\n private clearConnectTimer(): void {\n if (this.connectTimer !== null) {\n clearTimeout(this.connectTimer);\n this.connectTimer = null;\n }\n }\n\n /** Send one JSON frame. Frames are dropped with an `onError` report when\n * the socket is not open. A dropped SUBSCRIBE is recovered \u2014 `onopen`\n * re-asserts every topic in `subscribedTopics` \u2014 but the other two are not:\n * a dropped PUBLISH is the documented at-most-once loss, and a dropped\n * UNSUBSCRIBE is the quieter one, because `unsubscribe()` has already deleted\n * the topic from that set, so no later re-assert resends it and the server\n * keeps streaming a channel the app left. Through the DataBus neither of the\n * two is reachable while closed \u2014 `runTransport()` queues both operations\n * behind recovery \u2014 so this reads as a direct-transport-contract note. */\n private sendFrame(payload: { op: string; topic: string; data?: unknown; messageId?: string; timestamp?: number }): void {\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error(`WebSocket is not open; dropped \"${payload.op}\" frame.`));\n return;\n }\n this.socket.send(JSON.stringify(payload));\n }\n\n private sendBinaryFrame(topic: string, data: ArrayBuffer, messageId?: string, timestamp?: number): void {\n if (messageId !== undefined || timestamp !== undefined) {\n // Binary frames retain their compact legacy shape; metadata is sent as a\n // JSON envelope so IDs are never silently lost.\n this.sendFrame({\n op: WS_OP.PUBLISH,\n topic,\n data: Array.from(new Uint8Array(data)),\n ...(messageId === undefined ? {} : { messageId }),\n ...(timestamp === undefined ? {} : { timestamp })\n });\n return;\n }\n if (this.socket?.readyState !== WS_OPEN) {\n this.handlers?.onError(new Error('WebSocket is not open; dropped \"publish\" frame.'));\n return;\n }\n const topicBytes = new TextEncoder().encode(topic);\n if (topicBytes.length > 0xffff) {\n this.handlers?.onError(new Error('WebSocket topic is too long for a binary frame.'));\n return;\n }\n const frame = new Uint8Array(3 + topicBytes.length + data.byteLength);\n frame[0] = 0xc7;\n new DataView(frame.buffer).setUint16(1, topicBytes.length);\n frame.set(topicBytes, 3);\n frame.set(new Uint8Array(data), 3 + topicBytes.length);\n this.socket.send(frame.buffer);\n }\n\n /** Parse a server frame. Only objects carrying a string `topic` are\n * publications, and unknown shapes are dropped silently so a chatty server\n * cannot crash the message path. Malformed JSON is *not* dropped silently: it\n * goes to `handlers.onError` as \"WebSocket server sent a non-JSON frame.\",\n * which also records it in the bus's last-failure diagnostics. Neither case\n * throws out of the socket listener. */\n private async handleMessage(raw: unknown): Promise<void> {\n // Browser WebSockets may deliver binary frames as Blob unless\n // `binaryType = 'arraybuffer'` is explicitly configured by the host.\n // Normalize Blob asynchronously and reuse the exact ArrayBuffer parser.\n if (typeof Blob !== 'undefined' && raw instanceof Blob) {\n // Blob conversion is asynchronous. Capture the socket/handler pair so a\n // stop/start that replaces the connection while arrayBuffer() is pending\n // cannot let the stale frame leak into the new connection's handlers.\n const socket = this.socket;\n const handlers = this.handlers;\n try {\n const buffer = await raw.arrayBuffer();\n if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;\n await this.handleMessage(buffer);\n } catch (error) {\n if (this.socket === socket && this.handlers === handlers && this.socketActive) {\n this.handlers?.onError(error);\n }\n }\n return;\n }\n let parsed: unknown;\n if (raw instanceof ArrayBuffer) {\n const bytes = new Uint8Array(raw);\n if (bytes[0] !== 0xc7 || bytes.length < 3) return;\n const topicLength = new DataView(raw).getUint16(1);\n if (bytes.length < 3 + topicLength) return;\n const topic = new TextDecoder().decode(bytes.subarray(3, 3 + topicLength));\n const data = bytes.slice(3 + topicLength).buffer;\n this.handlers?.onMessage({ topic, data: data as TData });\n return;\n }\n if (typeof raw !== 'string') return;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.handlers?.onError(new Error('WebSocket server sent a non-JSON frame.'));\n return;\n }\n if (!parsed || typeof parsed !== 'object') return;\n const publication = parseDataBusPublication<TData>(parsed);\n if (publication) this.handlers?.onMessage(publication as DataBusMessage<TData>);\n }\n}\n\n/** Resolve the platform WebSocket. Throws in a runtime without one (SSR/Node);\n * it has no null arm, and `start()`'s catch is what turns that throw into an\n * `onStatus('error')` + `onError` report. */\nfunction defaultWebSocketFactory(url: string, protocols?: string | string[]): WebSocketLike {\n if (typeof WebSocket === 'undefined') {\n throw new Error('WebSocketTransport requires a WebSocket implementation.');\n }\n return new WebSocket(url, protocols) as unknown as WebSocketLike;\n}\n\n/** Create a CrossTabDataBus backed by a plain WebSocket transport.\n * Cross-tab clustering (owner dedup, sticky routes, failover) works identically\n * to the Centrifuge backend \u2014 only the transport I/O differs. */\nexport function createWebSocketDataBus<TData = unknown>(\n options: CreateWebSocketDataBusOptions<TData>\n): CrossTabDataBus<WebSocketDataBusConfig, TData> {\n const { clusterKey, connection, ...dataBusOptions } = options;\n return new CrossTabDataBus({\n ...dataBusOptions,\n autoStart: true,\n clusterKey: clusterKey ?? connection.url,\n initialConfig: connection,\n transport: new WebSocketTransport<TData>(connection)\n });\n}\n\n/** Re-export for convenience: the status type used by the transport. */\nexport type { WorkerStatus };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["pending"]
|
|
7
7
|
}
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,KAAK,EACV,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EAEtB,YAAY,EACZ,YAAY,EACb,MAAM,cAAc,CAAC;AAEtB;kFACkF;AAClF,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED,+DAA+D;AAC/D,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B;6CACyC;IACzC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,aAAa,CAAC;IACjF;;;;;4CAKwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,0FAA0F;AAC1F,MAAM,WAAW,6BAA6B,CAAC,KAAK,GAAG,OAAO,CAC5D,SAAQ,IAAI,CACV,sBAAsB,CAAC,sBAAsB,EAAE,KAAK,CAAC,EACrD,WAAW,GAAG,YAAY,GAAG,eAAe,GAAG,WAAW,CAC3D;IACD,0CAA0C;IAC1C,UAAU,EAAE,sBAAsB,CAAC;IACnC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD;;;;qEAIqE;AACrE,qBAAa,kBAAkB,CAAC,KAAK,GAAG,OAAO,CAC7C,YAAW,gBAAgB,CAAC,sBAAsB,EAAE,KAAK,CAAC;IAgB9C,OAAO,CAAC,QAAQ,CAAC,UAAU;IAdvC,QAAQ,CAAC,eAAe,eAAe;IACvC,QAAQ,CAAC,kBAAkB,sBAAsB;IACjD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAgD;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IAItD,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,YAAY,CAA8C;gBAErC,UAAU,EAAE,sBAAsB;IAE/D;;;;kCAI8B;IAC9B,KAAK,CAAC,MAAM,EAAE,sBAAsB,EAAE,QAAQ,EAAE,wBAAwB,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
|
package/docs/roadmap.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.18 was released on September 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
|
|
4
|
+
|
|
5
|
+
## 0.21.18 delivered scope
|
|
6
|
+
|
|
7
|
+
- **Five legs that no test named, each pinned with its mutation attribution.** A non-string frame `type` normalized to `null` before it reaches `getDiagnostics().protocol.lastUnknownMessageType` (and `getUnknownMessageStats()`, which had no reader at all); a `replay.persistenceRetry` block that names one field and must inherit the documented default for the other; the connect-timer cancel inside `failConnect()`; a `channelToken` credential request that names no channel; and a `Blob` frame whose conversion fails after its connection was replaced. The records say which assertion each mutant reaches and name the three legs that have **no mutant of their own**.
|
|
8
|
+
- **The lesson about partial option objects.** Both defaulting sites read *hot* in coverage — a bus with no policy takes the fallback, a bus with both fields takes the supplied value — and the shape in between, one field given, had never been read. A counter that moves does not mean the configuration space is covered; the pin has to name which default it can see, which is why each of the two defaults needed its own case.
|
|
9
|
+
- **The lesson about async completions.** Two of the five legs are the same shape in different files: a guard that discards a result from a superseded attempt, where the reachability of the guard is owned by a *cancel somewhere else* — and that cancel was the untested line. The connect-timer guard is now documented as unreachable-by-construction, with the construction it depends on pinned by name.
|
|
10
|
+
- **One shipped comment rewritten, and one numeric claim retired.** `src/websocket.ts`'s timeout-callback comment now enumerates why all three of its terms cannot fire rather than explaining only one, and drops its "all 880 tests green" count instead of refreshing a number that rots. Comment text ships: it reaches consumers through the bundles as well as the declarations.
|
|
11
|
+
- **What did not move.** No behavior, public API, protocol frame, default, storage key, or export changed, so mixed-version peers see nothing new. `src/` holds 56 zero-count branch slots of 1960 (down four), and `validation.ts` reports full branch coverage for the first time.
|
|
4
12
|
|
|
5
13
|
## 0.21.17 delivered scope
|
|
6
14
|
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
3
|
+
0.21.18 已于 2026 年 9 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
|
|
4
|
+
|
|
5
|
+
## 0.21.18 已完成范围
|
|
6
|
+
|
|
7
|
+
- **五条没有任何测试点名的分支,各自带着变异归因被固定下来。** 分别是:非字符串的帧 `type` 在进入 `getDiagnostics().protocol.lastUnknownMessageType` 之前被归一为 `null`(以及从来没有读取方的 `getUnknownMessageStats()`);只写了一个字段的 `replay.persistenceRetry` 必须让另一个字段继承文档里的默认值;`failConnect()` 内部取消连接超时定时器的那一行;没有给出 channel 的 `channelToken` 凭据请求;以及在连接被替换之后才失败的 `Blob` 转换。记录里写明了每个变异会被哪一条断言捕获,也点名了那三条**没有任何变异专门死于它**的分支。
|
|
8
|
+
- **关于部分填写的配置对象的教训。** 两处取默认值的位置在覆盖率里都是*热的*——完全不配策略的总线走兜底分支,两个字段都给的走传入值——而介于两者之间的形态,只给一个字段,从来没有被读过。计数会变动不等于配置空间被覆盖;固定测试必须说清它看得见的是哪个默认值,这就是两个默认值各需一个用例的原因。
|
|
9
|
+
- **关于异步完成的教训。** 五条里有两条在不同文件里是同一个形状:一个丢弃“已被取代的那次尝试”的结果的守卫,而它是否可达由**别处的某次取消**决定——而那次取消正是没有测试的一行。连接超时守卫现在被写明是“按构造不可达”,并且把它所依赖的那个构造按名字固定了下来。
|
|
10
|
+
- **一条随包发布的注释被重写,一个数字声明被撤下。** `src/websocket.ts` 超时回调的注释现在列出了三个条件项为何都无法触达,而不再只解释其中一个;并且删掉了它那句“880 个测试全绿”,选择删除而不是刷新一个会在原地腐坏的计数。注释文本是会发布的:它同时通过打包产物和类型声明到达使用者。
|
|
11
|
+
- **没有变动的部分。** 行为、公开 API、协议帧、默认值、存储键和导出全部原样,混版本对端感知不到任何变化。`src/` 的零计数分支槽为 1960 中的 56(减少四个),`validation.ts` 首次达到分支全覆盖。
|
|
4
12
|
|
|
5
13
|
## 0.21.17 已完成范围
|
|
6
14
|
|
package/package.json
CHANGED
|
File without changes
|