cross-tab-worker-databus 0.11.0 → 0.20.7

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/centrifuge.js +1 -1
  3. package/dist/centrifuge.shared.worker.js +4 -2
  4. package/dist/centrifuge.shared.worker.js.map +2 -2
  5. package/dist/centrifuge.worker.js +4 -2
  6. package/dist/centrifuge.worker.js.map +2 -2
  7. package/dist/{chunk-NX76TAV3.js → chunk-LPS4XOK4.js} +152 -20
  8. package/dist/{chunk-NX76TAV3.js.map → chunk-LPS4XOK4.js.map} +2 -2
  9. package/dist/cjs/centrifuge.cjs +151 -19
  10. package/dist/cjs/centrifuge.cjs.map +2 -2
  11. package/dist/cjs/hooks.cjs +4 -1
  12. package/dist/cjs/hooks.cjs.map +2 -2
  13. package/dist/cjs/index.cjs +301 -68
  14. package/dist/cjs/index.cjs.map +2 -2
  15. package/dist/cjs/vue.cjs +7 -1
  16. package/dist/cjs/vue.cjs.map +2 -2
  17. package/dist/core/data-bus.d.ts +32 -0
  18. package/dist/core/data-bus.d.ts.map +1 -1
  19. package/dist/core/publication.d.ts.map +1 -1
  20. package/dist/core/replay-persistence.d.ts.map +1 -1
  21. package/dist/core/trace.d.ts +4 -1
  22. package/dist/core/trace.d.ts.map +1 -1
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +4 -1
  25. package/dist/hooks.js.map +2 -2
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +151 -50
  29. package/dist/index.js.map +2 -2
  30. package/dist/vue.d.ts.map +1 -1
  31. package/dist/vue.js +7 -1
  32. package/dist/vue.js.map +2 -2
  33. package/dist/websocket.d.ts.map +1 -1
  34. package/docs/README.md +1 -0
  35. package/docs/api.md +10 -3
  36. package/docs/configuration.md +21 -1
  37. package/docs/release-checklist.md +22 -0
  38. package/docs/roadmap.md +136 -3
  39. package/docs/zh/README.md +1 -0
  40. package/docs/zh/api.md +10 -3
  41. package/docs/zh/configuration.md +21 -1
  42. package/docs/zh/release-checklist.md +22 -0
  43. package/docs/zh/roadmap.md +137 -2
  44. package/package.json +3 -1
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';\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 /** 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}\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 ?? 'cross-tab-worker-databus';\n const storeName = 'replay';\n const maxPerTopic = options.maxPerTopic;\n if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {\n throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);\n }\n let dbPromise: Promise<IDBDatabase> | null = null;\n const open = (): Promise<IDBDatabase> => {\n if (dbPromise) return dbPromise;\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDb.open(dbName, 1);\n request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: 'topic' });\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error('Failed to open replay database.'));\n });\n return dbPromise;\n };\n return {\n async load() {\n const db = await open();\n return new Promise((resolve, reject) => {\n const request = db.transaction(storeName, 'readonly').objectStore(storeName).getAll();\n request.onsuccess = () => resolve(request.result.flatMap(record => record.messages as DataBusMessage<TData>[]));\n request.onerror = () => reject(request.error ?? new Error('Failed to load replay history.'));\n });\n },\n async append(message) {\n const db = await open();\n await new Promise<void>((resolve, reject) => {\n const transaction = db.transaction(storeName, 'readwrite');\n const store = transaction.objectStore(storeName);\n const request = store.get(message.topic);\n request.onsuccess = () => {\n const messages = ((request.result?.messages ?? []) as DataBusMessage<TData>[]).concat(message).slice(-maxPerTopic);\n store.put({ topic: message.topic, messages });\n };\n request.onerror = () => reject(request.error ?? new Error('Failed to read replay history.'));\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => reject(transaction.error ?? new Error('Failed to persist replay history.'));\n });\n },\n async clear() {\n const db = await open();\n await new Promise<void>((resolve, reject) => {\n const transaction = db.transaction(storeName, 'readwrite');\n transaction.objectStore(storeName).clear();\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => reject(transaction.error ?? new Error('Failed to clear replay history.'));\n });\n },\n async clearTopic(topic) {\n const db = await open();\n await new Promise<void>((resolve, reject) => {\n const transaction = db.transaction(storeName, 'readwrite');\n transaction.objectStore(storeName).delete(topic);\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => reject(transaction.error ?? new Error('Failed to clear topic replay history.'));\n });\n },\n async clearBefore(timestamp) {\n const db = await open();\n await new Promise<void>((resolve, reject) => {\n const transaction = db.transaction(storeName, 'readwrite');\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 ?? 0) >= 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 = () => reject(request.error ?? new Error('Failed to read replay history.'));\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => reject(transaction.error ?? new Error('Failed to prune replay history.'));\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 (JSON text frames):\n * - client \u2192 server: `{\"op\":\"subscribe\"|\"unsubscribe\"|\"publish\",\"topic\":...,\"data\":...}`\n * - server \u2192 client: `{\"topic\":...,\"data\":...}` for publications; anything\n * without a string `topic` field is ignored (forward-compatible).\n */\nimport { CrossTabDataBus } from './core/data-bus';\nimport { parseDataBusPublication } from './core/publication';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type {\n DataBusTransport,\n DataBusTransportHandlers,\n DataBusPublishOptions,\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}\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\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 private socket: WebSocketLike | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n private readonly subscribedTopics = new Set<string>();\n\n constructor(private readonly connection: WebSocketDataBusConfig) {}\n\n /** Open the WebSocket and wire lifecycle listeners. A factory failure is\n * reported through `onStatus('error')` so the DataBus can recover. */\n start(config: WebSocketDataBusConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void> {\n if (this.socket) return;\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('error');\n handlers.onError(error);\n return;\n }\n socket.onopen = () => {\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: 'subscribe', topic });\n }\n handlers.onStatus('connected');\n };\n socket.onclose = () => handlers.onStatus('disconnected');\n socket.onerror = () => handlers.onStatus('error');\n socket.onmessage = event => this.handleMessage(event.data);\n this.socket = socket;\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: 'subscribe', topic });\n }\n\n /** Idempotent: unsubscribing an unknown topic is a no-op. */\n unsubscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.delete(topic);\n this.sendFrame({ 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: 'publish',\n topic,\n data,\n ...(options?.messageId === undefined ? {} : { messageId: options.messageId }),\n ...(options?.timestamp === undefined ? {} : { timestamp: options.timestamp })\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 this.socket = null;\n this.handlers = null;\n this.subscribedTopics.clear();\n socket?.close();\n }\n\n /** Send one JSON frame. Frames are dropped with an `onError` report when\n * the socket is not open \u2014 subscribe frames are re-sent on open, so the\n * only real loss is a publish during a disconnect window. */\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: '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; malformed JSON and unknown shapes are ignored so a chatty\n * server cannot crash the message path. */\n private handleMessage(raw: unknown): void {\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, or null in runtimes without one (SSR/Node). */\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';\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 /** 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}\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 ?? 'cross-tab-worker-databus';\n const storeName = 'replay';\n const maxPerTopic = options.maxPerTopic;\n if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {\n throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);\n }\n let dbPromise: Promise<IDBDatabase> | null = null;\n const invalidate = (db: IDBDatabase): void => {\n if (dbPromise) {\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 let mutationQueue: Promise<void> = Promise.resolve();\n const serializeMutation = (mutation: () => Promise<void>): Promise<void> => {\n const next = mutationQueue.then(mutation, mutation);\n mutationQueue = next.catch(() => undefined);\n return next;\n };\n const open = (): Promise<IDBDatabase> => {\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 request: IDBRequest;\n try {\n request = db.transaction(storeName, 'readonly').objectStore(storeName).getAll();\n } catch (error) {\n invalidate(db);\n reject(error);\n return;\n }\n request.onsuccess = () => resolve((request.result as Array<{ messages: DataBusMessage<TData>[] }>).flatMap(record => record.messages));\n request.onerror = () => {\n invalidate(db);\n reject(request.error ?? new Error('Failed to load replay history.'));\n };\n });\n },\n append(message) {\n return serializeMutation(async () => {\n const db = await open();\n await 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 const store = transaction.objectStore(storeName);\n const request = store.get(message.topic);\n request.onsuccess = () => {\n const messages = ((request.result?.messages ?? []) as DataBusMessage<TData>[]).concat(message).slice(-maxPerTopic);\n store.put({ topic: message.topic, messages });\n };\n request.onerror = () => { invalidate(db); reject(request.error ?? new Error('Failed to read replay history.')); };\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => { invalidate(db); reject(transaction.error ?? new Error('Failed to persist replay history.')); };\n });\n });\n },\n clear() {\n return serializeMutation(async () => {\n const db = await open();\n await 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 transaction.objectStore(storeName).clear();\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => { invalidate(db); reject(transaction.error ?? new Error('Failed to clear replay history.')); };\n });\n });\n },\n clearTopic(topic) {\n return serializeMutation(async () => {\n const db = await open();\n await 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 transaction.objectStore(storeName).delete(topic);\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => { invalidate(db); reject(transaction.error ?? new Error('Failed to clear topic replay history.')); };\n });\n });\n },\n clearBefore(timestamp) {\n return serializeMutation(async () => {\n const db = await open();\n await 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 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 = () => { invalidate(db); reject(request.error ?? new Error('Failed to read replay history.')); };\n transaction.oncomplete = () => resolve();\n transaction.onerror = () => { invalidate(db); reject(transaction.error ?? new Error('Failed to prune replay history.')); };\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 (JSON text frames):\n * - client \u2192 server: `{\"op\":\"subscribe\"|\"unsubscribe\"|\"publish\",\"topic\":...,\"data\":...}`\n * - server \u2192 client: `{\"topic\":...,\"data\":...}` for publications; anything\n * without a string `topic` field is ignored (forward-compatible).\n */\nimport { CrossTabDataBus } from './core/data-bus';\nimport { parseDataBusPublication } from './core/publication';\nimport type { CrossTabDataBusOptions } from './core/data-bus';\nimport type {\n DataBusTransport,\n DataBusTransportHandlers,\n DataBusPublishOptions,\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}\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\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 private socket: WebSocketLike | null = null;\n private handlers: DataBusTransportHandlers<TData> | null = null;\n private readonly subscribedTopics = new Set<string>();\n\n constructor(private readonly connection: WebSocketDataBusConfig) {}\n\n /** Open the WebSocket and wire lifecycle listeners. A factory failure is\n * reported through `onStatus('error')` so the DataBus can recover. */\n start(config: WebSocketDataBusConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void> {\n if (this.socket) return;\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('error');\n handlers.onError(error);\n return;\n }\n socket.onopen = () => {\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: 'subscribe', topic });\n }\n handlers.onStatus('connected');\n };\n socket.onclose = () => handlers.onStatus('disconnected');\n socket.onerror = () => handlers.onStatus('error');\n socket.onmessage = event => { void this.handleMessage(event.data); };\n this.socket = socket;\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: 'subscribe', topic });\n }\n\n /** Idempotent: unsubscribing an unknown topic is a no-op. */\n unsubscribe(topic: string): MaybePromise<void> {\n this.subscribedTopics.delete(topic);\n this.sendFrame({ 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: 'publish',\n topic,\n data,\n ...(options?.messageId === undefined ? {} : { messageId: options.messageId }),\n ...(options?.timestamp === undefined ? {} : { timestamp: options.timestamp })\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 this.socket = null;\n this.handlers = null;\n this.subscribedTopics.clear();\n socket?.close();\n }\n\n /** Send one JSON frame. Frames are dropped with an `onError` report when\n * the socket is not open \u2014 subscribe frames are re-sent on open, so the\n * only real loss is a publish during a disconnect window. */\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: '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; malformed JSON and unknown shapes are ignored so a chatty\n * server cannot crash the message path. */\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 try {\n await this.handleMessage(await raw.arrayBuffer());\n } catch (error) {\n this.handlers?.onError(error);\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, or null in runtimes without one (SSR/Node). */\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": []
7
7
  }
package/dist/vue.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../src/vue.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAsD,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,KAAK,EAC/C,MAAM,EAAE,MAAM,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,EAC7C,IAAI,GAAE,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,CAAM,GACvD,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,CAS7C;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,KAAK,EACpD,GAAG,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAC7E,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,IAAI,GAChD,IAAI,CAkBN;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,KAAK,EAC9C,GAAG,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,GAC/C,GAAG,CAAC,YAAY,CAAC,CAUnB"}
1
+ {"version":3,"file":"vue.d.ts","sourceRoot":"","sources":["../src/vue.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAsD,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,KAAK,EAC/C,MAAM,EAAE,MAAM,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,EAC7C,IAAI,GAAE,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,CAAM,GACvD,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,CAmB7C;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,KAAK,EACpD,GAAG,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAC7E,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,IAAI,GAChD,IAAI,CAoBN;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,KAAK,EAC9C,GAAG,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,GAC/C,GAAG,CAAC,YAAY,CAAC,CAUnB"}
package/dist/vue.js CHANGED
@@ -3,6 +3,7 @@ import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
3
3
  function useCrossTabDataBus(create, deps = []) {
4
4
  const bus = shallowRef(null);
5
5
  let instance = null;
6
+ let lifecycleGeneration = 0;
6
7
  const stop = async () => {
7
8
  const current = instance;
8
9
  instance = null;
@@ -10,7 +11,9 @@ function useCrossTabDataBus(create, deps = []) {
10
11
  if (current) await current.stop();
11
12
  };
12
13
  const start = () => {
14
+ const generation = ++lifecycleGeneration;
13
15
  void stop().then(() => {
16
+ if (generation !== lifecycleGeneration) return;
14
17
  const next = create();
15
18
  instance = next;
16
19
  bus.value = next;
@@ -27,20 +30,23 @@ function useCrossTabDataBus(create, deps = []) {
27
30
  }
28
31
  function useCrossTabSubscription(bus, topic, handler) {
29
32
  let currentBus = null;
33
+ let currentTopic = null;
30
34
  let cleanup;
31
35
  let latestHandler = handler;
32
36
  const stop = () => {
33
37
  cleanup?.();
34
38
  cleanup = void 0;
35
39
  currentBus = null;
40
+ currentTopic = null;
36
41
  };
37
42
  const sync = () => {
38
43
  const nextBus = bus.value;
39
44
  const nextTopic = typeof topic === "string" ? topic : topic.value;
40
- if (nextBus === currentBus && cleanup) return;
45
+ if (nextBus === currentBus && currentTopic === nextTopic && cleanup) return;
41
46
  stop();
42
47
  if (!nextBus) return;
43
48
  currentBus = nextBus;
49
+ currentTopic = nextTopic;
44
50
  cleanup = nextBus.subscribe(nextTopic, (message) => latestHandler(message));
45
51
  };
46
52
  watch(bus, sync, { immediate: true });
package/dist/vue.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/vue.ts"],
4
- "sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => { void stop().then(() => { const next = create(); instance = next; bus.value = next; void next.ready().catch(() => {}); }); };\n onMounted(start);\n onBeforeUnmount(() => { void stop(); });\n if (deps.length > 0) watch(deps, start);\n return bus as Ref<CrossTabDataBus<TConfig, TData> | null>;\n}\n\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>, topic: Ref<string> | string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n let currentBus: CrossTabDataBus<TConfig, TData> | null = null;\n let cleanup: (() => void) | undefined;\n let latestHandler = handler;\n const stop = () => { cleanup?.(); cleanup = undefined; currentBus = null; };\n const sync = () => {\n const nextBus = bus.value;\n const nextTopic = typeof topic === 'string' ? topic : topic.value;\n if (nextBus === currentBus && cleanup) return;\n stop();\n if (!nextBus) return;\n currentBus = nextBus;\n cleanup = nextBus.subscribe(nextTopic, message => latestHandler(message));\n };\n watch(bus, sync, { immediate: true });\n if (typeof topic !== 'string') watch(topic, sync);\n watch(() => handler, value => { latestHandler = value; });\n onBeforeUnmount(stop);\n}\n\nexport function useCrossTabStatus<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>\n): Ref<WorkerStatus> {\n const status = ref<WorkerStatus>('connecting');\n let cleanup: (() => void) | undefined;\n watch(bus, next => {\n cleanup?.(); cleanup = undefined;\n status.value = next?.getStatus() ?? 'connecting';\n if (next) cleanup = next.onStatus(value => { status.value = value; });\n }, { immediate: true });\n onBeforeUnmount(() => cleanup?.());\n return status;\n}\n"],
5
- "mappings": ";AAGA,SAAS,iBAAiB,WAAW,KAAK,YAAY,aAAuB;AAItE,SAAS,mBACd,QACA,OAAsD,CAAC,GACV;AAC7C,QAAM,MAAM,WAAmD,IAAI;AACnE,MAAI,WAAmD;AACvD,QAAM,OAAO,YAAY;AAAE,UAAM,UAAU;AAAU,eAAW;AAAM,QAAI,QAAQ;AAAM,QAAI,QAAS,OAAM,QAAQ,KAAK;AAAA,EAAG;AAC3H,QAAM,QAAQ,MAAM;AAAE,SAAK,KAAK,EAAE,KAAK,MAAM;AAAE,YAAM,OAAO,OAAO;AAAG,iBAAW;AAAM,UAAI,QAAQ;AAAM,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAAG,CAAC;AAAA,EAAG;AAChJ,YAAU,KAAK;AACf,kBAAgB,MAAM;AAAE,SAAK,KAAK;AAAA,EAAG,CAAC;AACtC,MAAI,KAAK,SAAS,EAAG,OAAM,MAAM,KAAK;AACtC,SAAO;AACT;AAEO,SAAS,wBACd,KAAkD,OAClD,SACM;AACN,MAAI,aAAqD;AACzD,MAAI;AACJ,MAAI,gBAAgB;AACpB,QAAM,OAAO,MAAM;AAAE,cAAU;AAAG,cAAU;AAAW,iBAAa;AAAA,EAAM;AAC1E,QAAM,OAAO,MAAM;AACjB,UAAM,UAAU,IAAI;AACpB,UAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC5D,QAAI,YAAY,cAAc,QAAS;AACvC,SAAK;AACL,QAAI,CAAC,QAAS;AACd,iBAAa;AACb,cAAU,QAAQ,UAAU,WAAW,aAAW,cAAc,OAAO,CAAC;AAAA,EAC1E;AACA,QAAM,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC;AACpC,MAAI,OAAO,UAAU,SAAU,OAAM,OAAO,IAAI;AAChD,QAAM,MAAM,SAAS,WAAS;AAAE,oBAAgB;AAAA,EAAO,CAAC;AACxD,kBAAgB,IAAI;AACtB;AAEO,SAAS,kBACd,KACmB;AACnB,QAAM,SAAS,IAAkB,YAAY;AAC7C,MAAI;AACJ,QAAM,KAAK,UAAQ;AACjB,cAAU;AAAG,cAAU;AACvB,WAAO,QAAQ,MAAM,UAAU,KAAK;AACpC,QAAI,KAAM,WAAU,KAAK,SAAS,WAAS;AAAE,aAAO,QAAQ;AAAA,IAAO,CAAC;AAAA,EACtE,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kBAAgB,MAAM,UAAU,CAAC;AACjC,SAAO;AACT;",
4
+ "sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n let lifecycleGeneration = 0;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => {\n const generation = ++lifecycleGeneration;\n void stop().then(() => {\n if (generation !== lifecycleGeneration) return;\n const next = create();\n instance = next;\n bus.value = next;\n void next.ready().catch(() => {});\n });\n };\n onMounted(start);\n onBeforeUnmount(() => { void stop(); });\n if (deps.length > 0) watch(deps, start);\n return bus as Ref<CrossTabDataBus<TConfig, TData> | null>;\n}\n\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>, topic: Ref<string> | string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n let currentBus: CrossTabDataBus<TConfig, TData> | null = null;\n let currentTopic: string | null = null;\n let cleanup: (() => void) | undefined;\n let latestHandler = handler;\n const stop = () => { cleanup?.(); cleanup = undefined; currentBus = null; currentTopic = null; };\n const sync = () => {\n const nextBus = bus.value;\n const nextTopic = typeof topic === 'string' ? topic : topic.value;\n if (nextBus === currentBus && currentTopic === nextTopic && cleanup) return;\n stop();\n if (!nextBus) return;\n currentBus = nextBus;\n currentTopic = nextTopic;\n cleanup = nextBus.subscribe(nextTopic, message => latestHandler(message));\n };\n watch(bus, sync, { immediate: true });\n if (typeof topic !== 'string') watch(topic, sync);\n watch(() => handler, value => { latestHandler = value; });\n onBeforeUnmount(stop);\n}\n\nexport function useCrossTabStatus<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>\n): Ref<WorkerStatus> {\n const status = ref<WorkerStatus>('connecting');\n let cleanup: (() => void) | undefined;\n watch(bus, next => {\n cleanup?.(); cleanup = undefined;\n status.value = next?.getStatus() ?? 'connecting';\n if (next) cleanup = next.onStatus(value => { status.value = value; });\n }, { immediate: true });\n onBeforeUnmount(() => cleanup?.());\n return status;\n}\n"],
5
+ "mappings": ";AAGA,SAAS,iBAAiB,WAAW,KAAK,YAAY,aAAuB;AAItE,SAAS,mBACd,QACA,OAAsD,CAAC,GACV;AAC7C,QAAM,MAAM,WAAmD,IAAI;AACnE,MAAI,WAAmD;AACvD,MAAI,sBAAsB;AAC1B,QAAM,OAAO,YAAY;AAAE,UAAM,UAAU;AAAU,eAAW;AAAM,QAAI,QAAQ;AAAM,QAAI,QAAS,OAAM,QAAQ,KAAK;AAAA,EAAG;AAC3H,QAAM,QAAQ,MAAM;AAClB,UAAM,aAAa,EAAE;AACrB,SAAK,KAAK,EAAE,KAAK,MAAM;AACrB,UAAI,eAAe,oBAAqB;AACxC,YAAM,OAAO,OAAO;AACpB,iBAAW;AACX,UAAI,QAAQ;AACZ,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAClC,CAAC;AAAA,EACH;AACA,YAAU,KAAK;AACf,kBAAgB,MAAM;AAAE,SAAK,KAAK;AAAA,EAAG,CAAC;AACtC,MAAI,KAAK,SAAS,EAAG,OAAM,MAAM,KAAK;AACtC,SAAO;AACT;AAEO,SAAS,wBACd,KAAkD,OAClD,SACM;AACN,MAAI,aAAqD;AACzD,MAAI,eAA8B;AAClC,MAAI;AACJ,MAAI,gBAAgB;AACpB,QAAM,OAAO,MAAM;AAAE,cAAU;AAAG,cAAU;AAAW,iBAAa;AAAM,mBAAe;AAAA,EAAM;AAC/F,QAAM,OAAO,MAAM;AACjB,UAAM,UAAU,IAAI;AACpB,UAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC5D,QAAI,YAAY,cAAc,iBAAiB,aAAa,QAAS;AACrE,SAAK;AACL,QAAI,CAAC,QAAS;AACd,iBAAa;AACb,mBAAe;AACf,cAAU,QAAQ,UAAU,WAAW,aAAW,cAAc,OAAO,CAAC;AAAA,EAC1E;AACA,QAAM,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC;AACpC,MAAI,OAAO,UAAU,SAAU,OAAM,OAAO,IAAI;AAChD,QAAM,MAAM,SAAS,WAAS;AAAE,oBAAgB;AAAA,EAAO,CAAC;AACxD,kBAAgB,IAAI;AACtB;AAEO,SAAS,kBACd,KACmB;AACnB,QAAM,SAAS,IAAkB,YAAY;AAC7C,MAAI;AACJ,QAAM,KAAK,UAAQ;AACjB,cAAU;AAAG,cAAU;AACvB,WAAO,QAAQ,MAAM,UAAU,KAAK;AACpC,QAAI,KAAM,WAAU,KAAK,SAAS,WAAS;AAAE,aAAO,QAAQ;AAAA,IAAO,CAAC;AAAA,EACtE,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kBAAgB,MAAM,UAAU,CAAC;AACjC,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  | [Architecture](./architecture.md) | Worker cluster, routing, storage, migration, and degradation design |
12
12
  | [Capabilities Matrix](./capabilities.md) | Implemented, not implemented, and planned capabilities matrix |
13
13
  | [Roadmap](./roadmap.md) | Release-oriented priorities and verification checklist |
14
+ | [Release checklist](./release-checklist.md) | Local gates, tagging, publishing, and post-release verification |
14
15
  | [../examples/demo](../examples/demo) | Runnable multi-tab browser demo |
15
16
  | [../CHANGELOG.md](../CHANGELOG.md) | Version changelog |
16
17
 
package/docs/api.md CHANGED
@@ -86,7 +86,10 @@ Registers a local subscription and returns a cleanup function.
86
86
  - The current tab only leaves the topic after the last handler is released.
87
87
  - Subscriptions are automatically queued when the transport is not yet ready.
88
88
  - Wildcard subscriptions: a topic ending in `.*` (`chat.*`) matches any remainder, and `*` matches everything. The pattern is routed, owned, and transport-subscribed as a literal channel; publications tagged with a matching concrete topic (or with the pattern itself) are delivered to wildcard handlers. See `topicMatchesPattern` below.
89
- - Replay (opt-in): construct the bus with `replay: { maxPerTopic }` and pass `{ replay: true | n }` as the third `subscribe()` argument. `maxPerTopic` must be a positive safe integer. The new handler immediately receives the buffered history (up to `n`, capped by `maxPerTopic`, default 100) with `message.replayed: true`, so late joiners do not miss earlier publications. Only dispatched publications are buffered (a topic with no local subscriber drops them as unowned); buffers are in-memory and cleared when the last handler for the topic unsubscribes. Wildcard subscriptions replay across every buffered topic matching the pattern. For reload/BFCache persistence, pass an optional `persistence` created by `createIndexedDbReplayPersistence({ maxPerTopic })`; persistence is asynchronous and failures are reported through `onError` without breaking live delivery. Set `retentionMs` to automatically prune durable history via `clearBefore` during hydration and after appends.
89
+ - Replay (opt-in): construct the bus with `replay: { maxPerTopic }` and pass `{ replay: true | n }` as the third `subscribe()` argument. `maxPerTopic` must be a positive safe integer. The new handler immediately receives the buffered history (up to `n`, capped by `maxPerTopic`, default 100) with `message.replayed: true`, so late joiners do not miss earlier publications. Only dispatched publications are buffered (a topic with no local subscriber drops them as unowned); buffers are in-memory and cleared when the last handler for the topic unsubscribes. Wildcard subscriptions replay across every buffered topic matching the pattern. For reload/BFCache persistence, pass an optional `persistence` created by `createIndexedDbReplayPersistence({ maxPerTopic })`; persistence is asynchronous and failures are reported through `onError` without breaking live delivery. Set `retentionMs` to automatically prune durable history via `clearBefore` during hydration and after appends. Set `persistenceRetry: { maxAttempts, backoffMs }` to retry transient persistence failures; defaults preserve one-attempt behavior.
90
+ When tracing is enabled, retries emit `reliability` events with `operation: 'persistence_retry'`, a bounded `persistenceOperation`, and `attempt`.
91
+
92
+ The WebSocket transport accepts binary publications delivered as either `ArrayBuffer` or browser `Blob` frames.
90
93
  - Persistent replay stores may also implement `clearTopic(topic)`; the bus calls it on final topic unsubscribe. A store may expose `clear()` for application-controlled retention cleanup; `stop()` deliberately preserves durable history for reload/BFCache recovery.
91
94
 
92
95
  ### `unsubscribe(topic, handler?)`
@@ -115,7 +118,7 @@ Published data must satisfy the serialization constraints of the underlying tran
115
118
 
116
119
  When the owning Worker is a remote Tab and the publish control message cannot be posted (for example the BroadcastChannel fails to clone the payload), `publish()` reports the failure through `onError` instead of silently dropping it.
117
120
 
118
- Incoming messages may include a caller/server supplied `messageId`. Enable bounded duplicate suppression with `dedup: { maxEntries, ttlMs }`; repeated IDs within the window are ignored. This is disabled by default and does not provide an exactly-once server guarantee.
121
+ Incoming messages may include a caller/server supplied `messageId`. Enable bounded duplicate suppression with `dedup: { maxEntries, ttlMs }`; repeated IDs within the window are ignored. This is disabled by default and does not provide an exactly-once server guarantee. Tests and hosts with a custom time source may provide `dedup.now`. A full `stop()` clears the remembered ID window; a later `start()` begins a fresh dedup session.
119
122
 
120
123
  When supplied, `options.messageId` and `options.timestamp` are propagated through cross-tab routing, Worker boundaries, and supported transports. The server must echo or otherwise preserve them for inbound deduplication and replay retention.
121
124
 
@@ -139,7 +142,11 @@ Clears in-memory replay buffers and invokes the persistence adapter's optional `
139
142
 
140
143
  `clearReplayTopic(topic)` applies the same cleanup to one exact topic. `getDedupStats()` returns bounded counters (`enabled`, `tracked`, `accepted`, `suppressed`), and `resetDedup()` clears remembered IDs and counters without changing configuration.
141
144
 
142
- `clearReplayBefore(timestamp)` removes entries older than an epoch-millisecond cutoff from memory and from adapters that implement optional `clearBefore(timestamp)`. Incoming messages receive a bus timestamp when the transport does not provide one.
145
+ `clearReplayBefore(timestamp)` removes entries with an explicit producer timestamp older than an epoch-millisecond cutoff from memory and from adapters that implement optional `clearBefore(timestamp)`. Legacy messages without a producer timestamp are preserved for compatibility. Incoming messages receive a bus timestamp when the transport does not provide one; that bus timestamp is not treated as producer metadata for retention cleanup.
146
+
147
+ When automatic retention is enabled, repeated cleanup requests are coalesced while a persistence cleanup is in flight; the newest cutoff is applied next.
148
+
149
+ Set `replay.retentionSweepMs` to periodically apply the retention cutoff even when no new publications arrive. The sweep is active only while the bus is started and visible; it pauses during pagehide and stops permanently on `stop()`.
143
150
 
144
151
  ### `onStatus(handler)`
145
152
 
@@ -46,7 +46,27 @@ const bus = new CrossTabDataBus({
46
46
  | `metricsIntervalMs` | `number` | `5000` | Aggregation window; must be a finite value greater than 0 |
47
47
  | `sink` | `(event) => void` | Required | Determined by the integrator: console, monitoring SDK, or other output |
48
48
 
49
- `message_metrics` contains window duration, received count, dispatched count, active Topic count, and receive-to-dispatch latency sample count, average, P50, P95, and maximum, plus `dedupAccepted` and `dedupSuppressed` counters for the same window. Latency is aggregated in 50ms buckets and never contains an individual message payload. Subscription events include their Topic so an integrator can correlate ownership changes. They are emitted only when the owner transport subscription set changes; idempotent `CONTROL` retries do not produce duplicate subscription events. Treat trace sinks as diagnostic surfaces: redact sensitive Topic conventions before writing to a console or external telemetry. Trace events do not include URLs, credentials, payloads, or error bodies. Errors thrown in the sink are isolated and will not interrupt message distribution, but will output to `console.warn` to help integrators discover diagnostic configuration issues.
49
+ `message_metrics` contains window duration, received count, dispatched count, active Topic count, and receive-to-dispatch latency sample count, average, P50, P95, and maximum, plus `dedupAccepted` and `dedupSuppressed` counters for the same window. Latency is aggregated in 50ms buckets and never contains an individual message payload. Persistence failures also emit a bounded `reliability` event with `operation: persistence_cleanup` before reaching the error handlers. `dedup.now` and `trace.now` can be injected for deterministic TTL, event-timestamp, and metrics-window tests. Subscription events include their Topic so an integrator can correlate ownership changes. They are emitted only when the owner transport subscription set changes; idempotent `CONTROL` retries do not produce duplicate subscription events. Treat trace sinks as diagnostic surfaces: redact sensitive Topic conventions before writing to a console or external telemetry. Trace events do not include URLs, credentials, payloads, or error bodies. Errors thrown in the sink are isolated and will not interrupt message distribution, but will output to `console.warn` to help integrators discover diagnostic configuration issues.
50
+
51
+ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced during bursts: an in-flight cleanup is reused and the newest cutoff is applied after it settles. This bounds IndexedDB cleanup work without changing the retention boundary.
52
+
53
+ `replay.retentionSweepMs` optionally schedules the same cleanup on a periodic interval. It is useful for quiet topics whose old durable records should still expire; it requires `retentionMs` and a persistence adapter with `clearBefore()`. The timer follows visibility and lifecycle transitions and is disabled by default.
54
+
55
+ `replay.persistenceRetry` optionally controls transient persistence recovery. `maxAttempts` is the total number of attempts (default `1`), and `backoffMs` is the initial delay before retry (default `50`). Delays grow exponentially and are capped; final failures retain the existing `onError` and reliability behavior.
56
+
57
+ When tracing is enabled, each retry before the final attempt emits a bounded `reliability` event with `operation: persistence_retry`, `persistenceOperation` (`load`, `append`, `clear`, `clearTopic`, or `clearBefore`), and the failed attempt number. No payload, URL, credential, or error body is included.
58
+
59
+ WebSocket binary frames may arrive as either `ArrayBuffer` or browser `Blob`; both use the same compact binary publication format. Blob conversion is asynchronous and conversion failures are reported through the transport error handler.
60
+
61
+ Retry waits are lifecycle-aware: `stop()` and pagehide suspension cancel pending retry attempts. A later `start()`/pageshow begins new work under a fresh lifecycle generation.
62
+
63
+ `dedup.sweepMs` optionally runs TTL cleanup while the bus is started and visible. It is disabled by default; on-message cleanup and `maxEntries` bounds remain in effect regardless.
64
+
65
+ The Vue adapter serializes bus replacement and ignores stale stop completions when reactive dependencies change rapidly, preserving the newest component lifecycle.
66
+
67
+ The React adapter applies the same generation guard across StrictMode and dependency-driven recreation, so stale effect cleanup cannot clear a newer bus.
68
+
69
+ IndexedDB replay persistence closes connections receiving `versionchange` and lazily reopens them for the next operation, which keeps multi-tab schema upgrades recoverable.
50
70
 
51
71
  On `pagehide`, the aggregation timer stops and discards the incomplete window; on `pageshow`, it resumes with a new window. A permanent `stop()` clears the timer. Only diagnostics output is throttled; actual message reception and distribution are never rate-limited.
52
72
 
@@ -0,0 +1,22 @@
1
+ # Release checklist
2
+
3
+ Use this checklist for each pre-1.0 release. The repository does not publish from the assistant; run the final npm command manually after reviewing the packed artifact.
4
+
5
+ ## Before tagging
6
+
7
+ 1. Update `package.json`, `CHANGELOG.md`, and both roadmap files.
8
+ 2. Run `pnpm check`, `pnpm lint`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, and `git diff --check`.
9
+ 3. Confirm the package contains only intended files with `npm pack --dry-run --json`.
10
+ 4. Commit, tag the exact version, and push `main --tags`.
11
+
12
+ ## Publishing
13
+
14
+ Run `npm publish --access public` from the tagged checkout. A version already present on npm cannot be published again. Historical versions missing from npm must be rebuilt from their exact git tags and reviewed individually; never publish the current tree under an old version.
15
+
16
+ ## After publishing
17
+
18
+ 1. Verify the version appears in `npm view cross-tab-worker-databus versions --json`.
19
+ 2. Install the published tarball or version in a clean consumer and import the root plus every public subpath.
20
+ 3. Record the result in the release notes. Do not advance to `1.0.0` until the public API and protocol deprecation policy are explicitly frozen.
21
+
22
+ Run `PUBLISHED_VERSION=0.20.7 pnpm verify:published` after npm publication.
package/docs/roadmap.md CHANGED
@@ -1,6 +1,139 @@
1
1
  # Roadmap
2
2
 
3
- 0.11.0 is the current development line. The next work is organized as 1.0.0 candidates, with protocol stability and operational guarantees taking priority over adding another browser runtime.
3
+ 0.20.7 is the current development line. The project is intentionally continuing through reliability-focused minor releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.20.7 delivered scope
6
+
7
+ - Added `pnpm verify:published` to download the npm package and verify root, hooks, Vue, and Centrifuge ESM/CJS consumers in a clean temporary directory.
8
+ - Published verification accepts `PUBLISHED_VERSION` for release-specific checks and otherwise follows npm's current version.
9
+
10
+ ## 0.20.6 delivered scope
11
+
12
+ - Added bilingual release checklists covering local validation, packed consumers, tagging, manual npm publication, and post-release verification.
13
+ - Documented immutable npm history and the requirement to rebuild missing historical versions from their exact git tags.
14
+
15
+ ## 0.20.5 delivered scope
16
+
17
+ - Public-consumer freeze coverage now exercises root, hooks, Vue, and Centrifuge subpaths in both ESM and CommonJS builds.
18
+ - Shipped declaration files are checked for existence and key replay, deduplication, and publication metadata types.
19
+
20
+ ## 0.20.4 delivered scope
21
+
22
+ - Added a real Chromium multi-tab soak scenario covering repeated fan-out, owner migration, BFCache round trips, reload recovery, and duplicate-free delivery.
23
+ - Browser lifecycle transitions are now exercised as one continuous session, catching timer and route cleanup regressions that isolated checks can miss.
24
+
25
+ ## 0.20.3 delivered scope
26
+
27
+ - Added React lifecycle coverage for dynamic topic changes.
28
+ - Topic replacement verifies old subscriptions are removed before the new topic is delivered through WebSocket hook wiring.
29
+
30
+ ## 0.20.2 delivered scope
31
+
32
+ - Added protocol recovery coverage proving valid WebSocket publications continue after malformed binary and text frames.
33
+ - Binary truncation, JSON parse failures, nested envelopes, and error isolation are exercised as one compatibility sequence.
34
+
35
+ ## 0.20.1 delivered scope
36
+
37
+ - Added persistence mutation-sequence soak coverage spanning hydration, retry recovery, topic cleanup, subsequent append, and full cleanup.
38
+ - Serialized persistence operations remain usable after transient failures.
39
+
40
+ ## 0.20.0 delivered scope
41
+
42
+ - Publication envelope compatibility is covered for legacy, nested, fallback-topic, primitive payload, metadata, and unknown-field frames.
43
+ - Empty or missing topics are rejected consistently while transport-supplied fallback channels remain supported.
44
+
45
+ ## 0.19.9 delivered scope
46
+
47
+ - Added combined deduplication and replay/persistence regression coverage.
48
+ - Dedup-suppressed publications cannot pollute replay history, while TTL expiry permits the same message ID to be accepted again.
49
+
50
+ ## 0.19.8 delivered scope
51
+
52
+ - IndexedDB replay adapters invalidate cached connections after transaction or request failures.
53
+ - Closed connections can recover through the existing persistence retry path without recreating the adapter.
54
+
55
+ ## 0.19.7 delivered scope
56
+
57
+ - IndexedDB replay adapters discard rejected open promises so transient open failures can recover on the next operation.
58
+ - Recovery remains compatible with the existing cross-tab `versionchange` connection reset behavior.
59
+
60
+ ## 0.19.6 delivered scope
61
+
62
+ - IndexedDB replay adapters recover from cross-tab `versionchange` events by reopening on the next operation.
63
+ - Stale database connections are closed instead of being reused after schema changes.
64
+
65
+ ## 0.19.5 delivered scope
66
+
67
+ - React bus effects are generation-guarded across StrictMode and rapid dependency changes.
68
+ - Superseded asynchronous cleanup cannot overwrite the newest active bus.
69
+
70
+ ## 0.19.4 delivered scope
71
+
72
+ - Vue bus recreation is generation-guarded across rapid reactive dependency changes.
73
+ - Stale asynchronous lifecycle completions cannot resurrect an obsolete bus instance.
74
+
75
+ ## 0.19.3 delivered scope
76
+
77
+ - Added optional `dedup.sweepMs` to remove expired message IDs during quiet periods.
78
+ - Sweep timers follow DataBus lifecycle and remain disabled by default.
79
+
80
+ ## 0.19.2 delivered scope
81
+
82
+ - Persistence retries are cancelled across `stop()` and pagehide suspension transitions.
83
+ - Cancelled retry waits do not trigger another adapter call or surface as persistence failures.
84
+
85
+ ## 0.19.1 delivered scope
86
+
87
+ - WebSocket binary publication handling accepts browser `Blob` frames in addition to `ArrayBuffer` frames.
88
+ - Blob conversion failures remain isolated through the transport error callback.
89
+
90
+ ## 0.19.0 delivered scope
91
+
92
+ - Persistence retries emit bounded, opt-in `persistence_retry` reliability events with the operation and attempt number.
93
+ - Diagnostics cover hydration, append, full/topic cleanup, and retention cleanup without exposing payloads or error bodies.
94
+ - Existing retry timing, default single-attempt behavior, adapter contracts, and final error handling remain compatible.
95
+
96
+ ## 0.18.0 delivered scope
97
+
98
+ - Opt-in replay persistence retry policy with bounded attempts and exponential backoff.
99
+ - Append, hydration, manual cleanup, topic cleanup, and retention cleanup share one recovery path.
100
+ - Public retry option type is exported while persistence adapter contracts remain source-compatible.
101
+
102
+ ## 0.17.0 delivered scope
103
+
104
+ - Optional periodic replay retention sweeps run without requiring a new publication.
105
+ - Sweep timers follow start/resume and pagehide/stop lifecycle boundaries.
106
+ - Fake-timer coverage protects cleanup scheduling, teardown, and invalid configuration behavior.
107
+
108
+ ## 0.16.0 delivered scope
109
+
110
+ - Retention cleanup is coalesced during publication bursts and executes serialized mutations using the newest requested cutoff.
111
+ - WebSocket binary protocol boundaries now have regression coverage for truncated and invalid frames.
112
+ - Existing legacy replay, JSON metadata, and manual cleanup compatibility guarantees remain documented.
113
+
114
+ ## 0.15.0 delivered scope
115
+
116
+ - Replay retention preserves legacy timestamp-less messages while pruning only explicitly timestamped records older than the cutoff.
117
+ - Trace timestamps accept an injectable `trace.now` clock, aligned with the existing dedup clock injection.
118
+ - Compatibility and lifecycle regression coverage protects replay cleanup, diagnostics, and adapter behavior.
119
+
120
+ ## 0.14.0 delivered scope
121
+
122
+ - Vue subscription parity for reactive topic changes on a stable bus.
123
+ - Cross-page replay mutation ordering and lifecycle guarantees are documented and regression-tested.
124
+
125
+ ## 0.13.0 delivered scope
126
+
127
+ - IndexedDB replay mutation serialization prevents concurrent append loss.
128
+ - Dedup state is reset on full stop/restart boundaries.
129
+ - Persistence failure and lifecycle regression coverage is included.
130
+
131
+ ## 0.12.0 delivered scope
132
+
133
+ - Injectable deduplication TTL clock for deterministic lifecycle and expiry tests.
134
+ - Structured persistence cleanup diagnostics for append, hydration, unsubscribe, and retention failures.
135
+ - Strict-but-compatible publication metadata normalization (non-empty IDs and finite timestamps only).
136
+ - Protocol compatibility fixtures and expanded package-consumption coverage.
4
137
 
5
138
  ## 0.11.0 delivered scope
6
139
 
@@ -9,14 +142,14 @@
9
142
  - Publication metadata compatibility coverage across WebSocket, Centrifuge, Worker boundaries, and browser E2E.
10
143
  - Service Worker transport decision: remain deliberately unimplemented until a stable connection-lifetime contract exists across target browsers.
11
144
 
12
- ## 1.0.0 candidates
145
+ ## 0.13.0 candidates
13
146
 
14
147
  1. Freeze the public export surface and transport-neutral publication envelope.
15
148
  2. Document at-least-once delivery and deduplication guarantees precisely.
16
149
  3. Add long-running browser soak coverage for replay retention, reconnect, BFCache, and owner migration.
17
150
  4. Publish a migration guide and deprecation policy for any pre-1.0 protocol aliases.
18
151
 
19
- ## 0.7.0 candidates
152
+ ## Longer-term candidates
20
153
 
21
154
  1. **Replay lifecycle and retention** — add explicit persistence cleanup (`clear`, `clearTopic`), make unsubscribe/replacement remove stale history, and surface persistence failures through trace and error handlers.
22
155
  2. **Reliability diagnostics** — emit structured recovery/retry, owner-acknowledgment, and route-migration events with bounded metadata while keeping tracing opt-in.
package/docs/zh/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  | [架构说明](./architecture.md) | Worker 集群、路由、存储、迁移和降级设计 |
14
14
  | [能力矩阵](./capabilities.md) | 已实现、未实现和计划待实现的能力矩阵 |
15
15
  | [路线图](./roadmap.md) | 面向版本的优先级与验证清单 |
16
+ | [发布检查清单](./release-checklist.md) | 本地门禁、打 tag、发布和发布后验证 |
16
17
  | [../..//examples/demo](../../examples/demo) | 可运行的多标签浏览器演示 |
17
18
  | [../../CHANGELOG.md](../../CHANGELOG.md) | 版本变更记录 |
18
19
 
package/docs/zh/api.md CHANGED
@@ -86,7 +86,10 @@ subscribe(
86
86
  - 最后一个 handler 释放后,当前 Tab 才退出该 Topic。
87
87
  - transport 尚未 ready 时订阅自动排队。
88
88
  - 通配符订阅:以 `.*` 结尾的 Topic(如 `chat.*`)匹配任意后缀,`*` 匹配全部。pattern 以字面量参与路由、归属与传输订阅;携带匹配的具体 topic(或 pattern 本身)的发布都会投递给通配 handler。匹配规则见下方 `topicMatchesPattern`。
89
- - 重放(可选):构造 bus 时传 `replay: { maxPerTopic }` 开启缓冲,`maxPerTopic` 必须是正安全整数;`subscribe()` 第三个参数传 `{ replay: true | n }` 后,新 handler 会立即收到缓冲历史(最多 `n` 条,受 `maxPerTopic` 上限约束,默认 100),消息带 `message.replayed: true` 标记——晚加入的 handler 不会错过更早的发布。只有被分发过的消息才入缓冲(无本地订阅者的 topic 会被 owner 丢弃);缓冲仅存内存,该 topic 最后一个 handler 退订时清空。通配订阅会对所有匹配 pattern 的已缓冲 topic 做回放。需要跨 reload/BFCache 持久化时,可传入 `createIndexedDbReplayPersistence({ maxPerTopic })` 创建的 `persistence`;持久化为异步操作,失败会通过 `onError` 报告,不影响实时投递。设置 `retentionMs` 后,如果 adapter 支持 `clearBefore`,会在 hydrate 和追加后自动清理过期历史。
89
+ - 重放(可选):构造 bus 时传 `replay: { maxPerTopic }` 开启缓冲,`maxPerTopic` 必须是正安全整数;`subscribe()` 第三个参数传 `{ replay: true | n }` 后,新 handler 会立即收到缓冲历史(最多 `n` 条,受 `maxPerTopic` 上限约束,默认 100),消息带 `message.replayed: true` 标记——晚加入的 handler 不会错过更早的发布。只有被分发过的消息才入缓冲(无本地订阅者的 topic 会被 owner 丢弃);缓冲仅存内存,该 topic 最后一个 handler 退订时清空。通配订阅会对所有匹配 pattern 的已缓冲 topic 做回放。需要跨 reload/BFCache 持久化时,可传入 `createIndexedDbReplayPersistence({ maxPerTopic })` 创建的 `persistence`;持久化为异步操作,失败会通过 `onError` 报告,不影响实时投递。设置 `retentionMs` 后,如果 adapter 支持 `clearBefore`,会在 hydrate 和追加后自动清理过期历史。设置 `persistenceRetry: { maxAttempts, backoffMs }` 可重试瞬时持久化失败;默认仍保持单次尝试。
90
+ 启用 trace 后,重试会发出 `reliability` 事件,包含 `operation: 'persistence_retry'`、有界的 `persistenceOperation` 和 `attempt`。
91
+
92
+ WebSocket transport 支持以 `ArrayBuffer` 或浏览器 `Blob` 帧接收二进制 publication。
90
93
  - 持久化 replay store 还可实现 `clearTopic(topic)`;bus 会在最后一个 handler 退订时调用。应用可自行保留 `clear()` 做全量留存清理;`stop()` 会保留 durable history,以支持 reload/BFCache 恢复。
91
94
 
92
95
  ### `unsubscribe(topic, handler?)`
@@ -135,9 +138,13 @@ clearReplay(): Promise<void>
135
138
 
136
139
  清空内存 replay 缓冲,并调用持久化适配器可选的 `clear()`。适合留存策略、退出登录或租户切换;普通 `stop()` 仍会保留 durable history。
137
140
 
138
- `clearReplayTopic(topic)` 只清理一个精确 topic。`getDedupStats()` 返回 `enabled`、`tracked`、`accepted`、`suppressed` 四项有界统计;`resetDedup()` 清除已记忆 ID 和计数,不改变 dedup 配置。
141
+ `clearReplayTopic(topic)` 只清理一个精确 topic。`getDedupStats()` 返回 `enabled`、`tracked`、`accepted`、`suppressed` 四项有界统计;`resetDedup()` 清除已记忆 ID 和计数,不改变 dedup 配置。为测试或非墙上时钟宿主,可额外提供 `dedup.now`。完整 `stop()` 会清空已记忆的 ID 窗口,之后 `start()` 会开启新的 dedup 会话。
142
+
143
+ `clearReplayBefore(timestamp)` 按毫秒时间戳清理带显式 producer timestamp 且早于 cutoff 的记录;实现可选 `clearBefore()` 的持久化适配器会同步执行该清理。没有 producer timestamp 的 legacy 消息会为兼容性保留。transport 未提供时间戳时,系统仍会补充 bus timestamp,但该时间戳不会被当作 producer metadata 用于 retention 清理。
144
+
145
+ 启用自动 retention 时,如果持久化清理正在进行,后续清理请求会合并,完成后再应用最新 cutoff。
139
146
 
140
- `clearReplayBefore(timestamp)` 按毫秒时间戳清理旧记录;实现可选 `clearBefore()` 的持久化适配器会同步执行该清理。
147
+ 设置 `replay.retentionSweepMs` 后,即使没有新 publication 也会周期性应用 retention cutoff。sweep 只在 bus started 且页面可见时运行;pagehide 时暂停,`stop()` 后永久停止。
141
148
 
142
149
  ### `onStatus(handler)`
143
150
 
@@ -46,7 +46,27 @@ const bus = new CrossTabDataBus({
46
46
  | `metricsIntervalMs` | `number` | `5000` | 聚合窗口,必须是大于 0 的有限数值 |
47
47
  | `sink` | `(event) => void` | 必填 | 由接入方决定 console、监控 SDK 或其他出口 |
48
48
 
49
- `message_metrics` 包含窗口时长、接收数、分发数、活跃 Topic 数量,以及接收 → 分发延迟的样本数、平均值、P50、P95 和最大值,并包含同一窗口的 `dedupAccepted` 与 `dedupSuppressed` 计数。延迟按 50ms 桶聚合,不包含单条消息 payload。订阅事件会带 Topic,便于接入方关联 owner 变化;仅 owner transport 的订阅集合实际变化时才输出,幂等 `CONTROL` 重试不会产生重复订阅事件。将 trace 写入 console 或外部监控前,应按业务 Topic 约定进行脱敏。Trace 不包含 URL、凭证、payload 或错误正文。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,方便接入方发现诊断配置问题。
49
+ `message_metrics` 包含窗口时长、接收数、分发数、活跃 Topic 数量,以及接收 → 分发延迟的样本数、平均值、P50、P95 和最大值,并包含同一窗口的 `dedupAccepted` 与 `dedupSuppressed` 计数。延迟按 50ms 桶聚合,不包含单条消息 payload。持久化失败会先输出带 `operation: persistence_cleanup` 的有界 `reliability` 事件,再交给 error handler;`dedup.now` 与 `trace.now` 均可注入,以便确定性测试 TTL、事件时间戳和 metrics 窗口。订阅事件会带 Topic,便于接入方关联 owner 变化;仅 owner transport 的订阅集合实际变化时才输出,幂等 `CONTROL` 重试不会产生重复订阅事件。将 trace 写入 console 或外部监控前,应按业务 Topic 约定进行脱敏。Trace 不包含 URL、凭证、payload 或错误正文。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,方便接入方发现诊断配置问题。
50
+
51
+ 启用 `replay.retentionMs` 时,自动 durable cleanup 会在 publication burst 期间合并:复用进行中的清理,并在其完成后应用最新 cutoff。在不改变 retention 边界的前提下,这会限制 IndexedDB 清理事务数量。
52
+
53
+ `replay.retentionSweepMs` 可选地按周期触发同一清理逻辑。它适合安静 topic 的 durable 旧记录也需要过期的场景;需要同时配置 `retentionMs` 和实现 `clearBefore()` 的持久化适配器。定时器遵循页面可见性和生命周期切换,默认关闭。
54
+
55
+ `replay.persistenceRetry` 可选地控制瞬时持久化失败的恢复。`maxAttempts` 是总尝试次数(默认 `1`),`backoffMs` 是首次重试前的延迟(默认 `50`);延迟会指数增长并封顶。最终失败仍沿用现有 `onError` 和 reliability 行为。
56
+
57
+ 启用 trace 后,每次在最终尝试之前发生的重试都会发出有界 `reliability` 事件:`operation: persistence_retry`,并包含 `persistenceOperation`(`load`、`append`、`clear`、`clearTopic` 或 `clearBefore`)和失败的尝试次数。不包含 payload、URL、凭证或错误正文。
58
+
59
+ WebSocket 二进制帧可能以 `ArrayBuffer` 或浏览器 `Blob` 到达;两者使用相同的紧凑二进制 publication 格式。Blob 转换是异步的,转换失败会通过 transport error handler 报告。
60
+
61
+ Retry 等待遵循生命周期:`stop()` 和 pagehide 挂起会取消待执行的 retry;之后的 `start()`/pageshow 会在新的生命周期代际中开始新工作。
62
+
63
+ `dedup.sweepMs` 可选地在 bus started 且可见时执行 TTL 清理,默认关闭;无论是否开启,消息到达时清理和 `maxEntries` 上限仍然生效。
64
+
65
+ Vue 适配层会串行化 bus 替换,并在 reactive 依赖快速变化时忽略过期的 stop 完成,确保组件始终绑定最新生命周期。
66
+
67
+ React 适配层同样在 StrictMode 和依赖驱动的重建过程中使用 generation 保护,过期 effect 清理不会清除更新后的 bus。
68
+
69
+ IndexedDB replay persistence 收到 `versionchange` 时会关闭连接,并在下一次操作时惰性重新打开,从而支持多 tab schema 升级后的恢复。
50
70
 
51
71
  `pagehide` 时聚合定时器会停止并丢弃未完成窗口,`pageshow` 后以新窗口恢复;永久 `stop()` 会清理定时器。这里节流的只是诊断输出,真实消息接收与分发不会被限速。
52
72
 
@@ -0,0 +1,22 @@
1
+ # 发布检查清单
2
+
3
+ 每个 1.0.0 之前的版本都按此清单执行。仓库不由助手执行发布;完成打包验证并人工审阅后,再手动运行 npm 命令。
4
+
5
+ ## 打 tag 前
6
+
7
+ 1. 更新 `package.json`、`CHANGELOG.md` 和中英文 roadmap。
8
+ 2. 运行 `pnpm check`、`pnpm lint`、`pnpm bench`、`pnpm test:e2e`、`pnpm bench:browser`、`pnpm verify:pack` 以及 `git diff --check`。
9
+ 3. 用 `npm pack --dry-run --json` 确认发布包只包含预期文件。
10
+ 4. 提交、给精确版本打 tag,并推送 `main --tags`。
11
+
12
+ ## 发布
13
+
14
+ 从对应 tag 的工作树运行 `npm publish --access public`。已经存在于 npm 的版本不能重复发布;npm 缺失的历史版本必须从对应 git tag 重建并逐个审阅,不能把当前工作树伪装成旧版本发布。
15
+
16
+ ## 发布后
17
+
18
+ 1. 用 `npm view cross-tab-worker-databus versions --json` 确认版本已出现。
19
+ 2. 在干净消费者中安装已发布版本或 tarball,并导入主入口及所有公开子路径。
20
+ 3. 将结果记录到发布说明。在公开 API 和协议弃用策略明确冻结前,不进入 `1.0.0`。
21
+
22
+ npm 发布后运行 `PUBLISHED_VERSION=0.20.7 pnpm verify:published`。