@gwakko/shared-websocket 0.3.0 → 0.6.2

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/src/types.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export type SocketState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
2
2
  export type TabRole = 'leader' | 'follower';
3
3
  export type Unsubscribe = () => void;
4
- export type EventHandler = (data: any) => void;
4
+ export type EventHandler<T = unknown> = (data: T) => void;
5
+
6
+ /** Type-safe event map. Keys are event names, values are payload types. */
7
+ export type EventMap = Record<string, unknown>;
5
8
 
6
9
  export interface BusMessage {
7
10
  id: string;
@@ -12,7 +15,40 @@ export interface BusMessage {
12
15
  timestamp: number;
13
16
  }
14
17
 
15
- export interface SharedWebSocketOptions {
18
+ /**
19
+ * Logger interface — inject your own logger (console, pino, winston, Sentry).
20
+ *
21
+ * @example
22
+ * // Default: console
23
+ * { logger: console }
24
+ *
25
+ * @example
26
+ * // Sentry breadcrumbs
27
+ * {
28
+ * logger: {
29
+ * debug: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'debug' }),
30
+ * info: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'info' }),
31
+ * warn: (msg, ...args) => Sentry.addBreadcrumb({ message: msg, data: args, level: 'warning' }),
32
+ * error: (msg, ...args) => Sentry.captureException(args[0] ?? new Error(msg)),
33
+ * }
34
+ * }
35
+ *
36
+ * @example
37
+ * // Pino
38
+ * import pino from 'pino';
39
+ * { logger: pino({ name: 'shared-ws' }) }
40
+ */
41
+ export interface Logger {
42
+ debug(message: string, ...args: unknown[]): void;
43
+ info(message: string, ...args: unknown[]): void;
44
+ warn(message: string, ...args: unknown[]): void;
45
+ error(message: string, ...args: unknown[]): void;
46
+ }
47
+
48
+ /** Middleware function — transform or inspect messages. Return null to drop. */
49
+ export type Middleware<T = unknown> = (message: T) => T | null;
50
+
51
+ export interface SharedWebSocketOptions<TEvents extends EventMap = EventMap> {
16
52
  protocols?: string[];
17
53
  reconnect?: boolean;
18
54
  reconnectMaxDelay?: number;
@@ -31,4 +67,45 @@ export interface SharedWebSocketOptions {
31
67
  useWorker?: boolean;
32
68
  /** Custom worker URL (if useWorker is true and you want to provide your own worker file). */
33
69
  workerUrl?: string | URL;
70
+ /** Override event/field names for server protocol compatibility. */
71
+ events?: Partial<EventProtocol>;
72
+ /** Enable debug logging (default: false). */
73
+ debug?: boolean;
74
+ /** Custom logger (default: console). Supports any logger with debug/info/warn/error. */
75
+ logger?: Logger;
76
+ }
77
+
78
+ /** Configurable event names and field mappings for server protocol. */
79
+ export interface EventProtocol {
80
+ /** Field name for event type in messages (default: "event"). */
81
+ eventField: string;
82
+ /** Field name for payload in messages (default: "data"). */
83
+ dataField: string;
84
+ /** Event name sent when joining a channel (default: "$channel:join"). */
85
+ channelJoin: string;
86
+ /** Event name sent when leaving a channel (default: "$channel:leave"). */
87
+ channelLeave: string;
88
+ /** Heartbeat payload sent to keep connection alive (default: { type: "ping" }). */
89
+ ping: unknown;
90
+ /** Fallback event name when message has no event field (default: "message"). */
91
+ defaultEvent: string;
92
+ }
93
+
94
+ /** Lifecycle event handlers for useSocketLifecycle / onConnect / etc. */
95
+ export interface SocketLifecycleHandlers {
96
+ onConnect?: () => void;
97
+ onDisconnect?: () => void;
98
+ onReconnecting?: () => void;
99
+ onLeaderChange?: (isLeader: boolean) => void;
100
+ onError?: (error: unknown) => void;
101
+ }
102
+
103
+ /** Scoped channel handle for private/topic-based subscriptions. */
104
+ export interface Channel {
105
+ readonly name: string;
106
+ on(event: string, handler: EventHandler): Unsubscribe;
107
+ once(event: string, handler: EventHandler): Unsubscribe;
108
+ send(event: string, data: unknown): void;
109
+ stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
110
+ leave(): void;
34
111
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/disposable.ts","../src/utils/id.ts","../src/MessageBus.ts","../src/TabCoordinator.ts","../src/utils/backoff.ts","../src/SharedSocket.ts","../src/WorkerSocket.ts","../src/SubscriptionManager.ts","../src/SharedWebSocket.ts"],"sourcesContent":["/** Polyfill Symbol.dispose if not available. */\nif (typeof Symbol.dispose === 'undefined') {\n (Symbol as any).dispose = Symbol('Symbol.dispose');\n}\n","export function generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n","import './utils/disposable';\nimport { generateId } from './utils/id';\nimport type { BusMessage, Unsubscribe } from './types';\n\ntype Listener = (msg: BusMessage) => void;\n\nexport class MessageBus implements Disposable {\n private channel: BroadcastChannel;\n private listeners = new Map<string, Set<Listener>>();\n private pendingRequests = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();\n\n constructor(\n channelName: string,\n private readonly tabId: string,\n ) {\n this.channel = new BroadcastChannel(channelName);\n this.channel.onmessage = (ev: MessageEvent<BusMessage>) => {\n this.handleMessage(ev.data);\n };\n }\n\n subscribe<T>(topic: string, fn: (data: T) => void): Unsubscribe {\n const wrapper: Listener = (msg) => {\n if (msg.source !== this.tabId) fn(msg.data as T);\n };\n this.addListener(topic, wrapper);\n return () => this.removeListener(topic, wrapper);\n }\n\n publish<T>(topic: string, data: T): void {\n this.postMessage({ topic, type: 'publish', data });\n }\n\n broadcast<T>(topic: string, data: T): void {\n const msg = this.createMessage(topic, 'broadcast', data);\n this.channel.postMessage(msg);\n // Also deliver to self\n this.handleMessage(msg);\n }\n\n async request<T, R>(topic: string, data: T, timeout = 5000): Promise<R> {\n const msg = this.createMessage(topic, 'request', data);\n return new Promise<R>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pendingRequests.delete(msg.id);\n reject(new Error(`MessageBus.request: timeout for topic \"${topic}\"`));\n }, timeout);\n this.pendingRequests.set(msg.id, { resolve: resolve as (v: unknown) => void, reject, timer });\n this.channel.postMessage(msg);\n });\n }\n\n respond<T, R>(topic: string, fn: (data: T) => R | Promise<R>): Unsubscribe {\n const wrapper: Listener = async (msg) => {\n if (msg.type !== 'request' || msg.source === this.tabId) return;\n const result = await fn(msg.data as T);\n this.postMessage({ topic, type: 'response', data: { requestId: msg.id, result } });\n };\n this.addListener(topic, wrapper);\n return () => this.removeListener(topic, wrapper);\n }\n\n private handleMessage(msg: BusMessage): void {\n // Handle response to pending request\n if (msg.type === 'response') {\n const payload = msg.data as { requestId: string; result: unknown };\n const pending = this.pendingRequests.get(payload.requestId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingRequests.delete(payload.requestId);\n pending.resolve(payload.result);\n return;\n }\n }\n\n const listeners = this.listeners.get(msg.topic);\n if (listeners) {\n for (const fn of listeners) fn(msg);\n }\n }\n\n private postMessage(partial: Pick<BusMessage, 'topic' | 'type' | 'data'>): void {\n this.channel.postMessage(this.createMessage(partial.topic, partial.type, partial.data));\n }\n\n private createMessage(topic: string, type: BusMessage['type'], data: unknown): BusMessage {\n return { id: generateId(), source: this.tabId, topic, type, data, timestamp: Date.now() };\n }\n\n private addListener(topic: string, fn: Listener): void {\n let set = this.listeners.get(topic);\n if (!set) {\n set = new Set();\n this.listeners.set(topic, set);\n }\n set.add(fn);\n }\n\n private removeListener(topic: string, fn: Listener): void {\n this.listeners.get(topic)?.delete(fn);\n }\n\n [Symbol.dispose](): void {\n for (const pending of this.pendingRequests.values()) {\n clearTimeout(pending.timer);\n pending.reject(new Error('MessageBus disposed'));\n }\n this.pendingRequests.clear();\n this.listeners.clear();\n this.channel.close();\n }\n}\n","import './utils/disposable';\nimport { MessageBus } from './MessageBus';\nimport type { Unsubscribe } from './types';\n\ninterface CoordinatorOptions {\n electionTimeout?: number; // ms to wait for rejection (default 200)\n heartbeatInterval?: number; // ms between heartbeats (default 2000)\n leaderTimeout?: number; // ms without heartbeat to trigger election (default 5000)\n}\n\nexport class TabCoordinator implements Disposable {\n private _isLeader = false;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private leaderCheckTimer: ReturnType<typeof setInterval> | null = null;\n private lastHeartbeat = 0;\n private disposed = false;\n\n private onBecomeLeaderFns = new Set<() => void>();\n private onLoseLeadershipFns = new Set<() => void>();\n private cleanups: Unsubscribe[] = [];\n\n private readonly electionTimeout: number;\n private readonly heartbeatInterval: number;\n private readonly leaderTimeout: number;\n\n constructor(\n private readonly bus: MessageBus,\n private readonly tabId: string,\n options: CoordinatorOptions = {},\n ) {\n this.electionTimeout = options.electionTimeout ?? 200;\n this.heartbeatInterval = options.heartbeatInterval ?? 2000;\n this.leaderTimeout = options.leaderTimeout ?? 5000;\n\n // Listen for election requests — reject if we are leader\n this.cleanups.push(\n this.bus.subscribe<{ tabId: string }>('coord:election', () => {\n if (this._isLeader) {\n this.bus.publish('coord:reject', { tabId: this.tabId });\n }\n }),\n );\n\n // Listen for heartbeats\n this.cleanups.push(\n this.bus.subscribe<{ tabId: string }>('coord:heartbeat', () => {\n this.lastHeartbeat = Date.now();\n }),\n );\n\n // Listen for abdication\n this.cleanups.push(\n this.bus.subscribe('coord:abdicate', () => {\n if (!this._isLeader && !this.disposed) {\n this.elect();\n }\n }),\n );\n }\n\n get isLeader(): boolean {\n return this._isLeader;\n }\n\n async elect(): Promise<void> {\n if (this.disposed) return;\n\n return new Promise<void>((resolve) => {\n let rejected = false;\n\n const unsub = this.bus.subscribe('coord:reject', () => {\n rejected = true;\n unsub();\n // We are follower — start monitoring leader heartbeat\n this.startLeaderCheck();\n resolve();\n });\n\n this.bus.publish('coord:election', { tabId: this.tabId });\n\n setTimeout(() => {\n unsub();\n if (!rejected && !this.disposed) {\n this.becomeLeader();\n }\n resolve();\n }, this.electionTimeout);\n });\n }\n\n abdicate(): void {\n if (!this._isLeader) return;\n this._isLeader = false;\n this.stopHeartbeat();\n this.bus.publish('coord:abdicate', { tabId: this.tabId });\n for (const fn of this.onLoseLeadershipFns) fn();\n }\n\n onBecomeLeader(fn: () => void): Unsubscribe {\n this.onBecomeLeaderFns.add(fn);\n return () => this.onBecomeLeaderFns.delete(fn);\n }\n\n onLoseLeadership(fn: () => void): Unsubscribe {\n this.onLoseLeadershipFns.add(fn);\n return () => this.onLoseLeadershipFns.delete(fn);\n }\n\n private becomeLeader(): void {\n this._isLeader = true;\n this.stopLeaderCheck();\n this.startHeartbeat();\n for (const fn of this.onBecomeLeaderFns) fn();\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n this.bus.publish('coord:heartbeat', { tabId: this.tabId });\n }, this.heartbeatInterval);\n // Send immediately\n this.bus.publish('coord:heartbeat', { tabId: this.tabId });\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private startLeaderCheck(): void {\n this.stopLeaderCheck();\n this.lastHeartbeat = Date.now();\n this.leaderCheckTimer = setInterval(() => {\n if (Date.now() - this.lastHeartbeat > this.leaderTimeout && !this.disposed) {\n this.stopLeaderCheck();\n this.elect();\n }\n }, 1000);\n }\n\n private stopLeaderCheck(): void {\n if (this.leaderCheckTimer) {\n clearInterval(this.leaderCheckTimer);\n this.leaderCheckTimer = null;\n }\n }\n\n [Symbol.dispose](): void {\n this.disposed = true;\n if (this._isLeader) {\n this.abdicate();\n }\n this.stopHeartbeat();\n this.stopLeaderCheck();\n for (const unsub of this.cleanups) unsub();\n this.cleanups = [];\n this.onBecomeLeaderFns.clear();\n this.onLoseLeadershipFns.clear();\n }\n}\n","/** Exponential backoff generator with jitter. */\nexport function* backoff(base = 1000, max = 30_000): Generator<number> {\n let delay = base;\n while (true) {\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n yield Math.min(delay + jitter, max);\n delay = Math.min(delay * 2, max);\n }\n}\n","import './utils/disposable';\nimport { backoff } from './utils/backoff';\nimport type { SocketState, Unsubscribe, EventHandler } from './types';\n\ninterface SharedSocketOptions {\n protocols?: string[];\n reconnect?: boolean;\n reconnectMaxDelay?: number;\n heartbeatInterval?: number;\n sendBuffer?: number;\n auth?: () => string | Promise<string>;\n authToken?: string;\n authParam?: string;\n}\n\nexport class SharedSocket implements Disposable {\n private ws: WebSocket | null = null;\n private _state: SocketState = 'closed';\n private buffer: unknown[] = [];\n private disposed = false;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n private onMessageFns = new Set<EventHandler>();\n private onStateChangeFns = new Set<(state: SocketState) => void>();\n\n private readonly opts: Required<Omit<SharedSocketOptions, 'auth' | 'authToken' | 'authParam'>> & {\n auth?: () => string | Promise<string>;\n authToken?: string;\n authParam: string;\n };\n\n constructor(\n private url: string,\n options: SharedSocketOptions = {},\n ) {\n this.opts = {\n protocols: options.protocols ?? [],\n reconnect: options.reconnect ?? true,\n reconnectMaxDelay: options.reconnectMaxDelay ?? 30_000,\n heartbeatInterval: options.heartbeatInterval ?? 30_000,\n sendBuffer: options.sendBuffer ?? 100,\n auth: options.auth,\n authToken: options.authToken,\n authParam: options.authParam ?? 'token',\n };\n }\n\n get state(): SocketState {\n return this._state;\n }\n\n async connect(): Promise<void> {\n if (this.disposed) return;\n\n this.setState('connecting');\n\n const connectUrl = await this.buildUrl();\n this.ws = new WebSocket(connectUrl, this.opts.protocols);\n\n this.ws.onopen = () => {\n this.setState('connected');\n this.flushBuffer();\n this.startHeartbeat();\n };\n\n this.ws.onmessage = (ev: MessageEvent) => {\n let data: unknown;\n try {\n data = JSON.parse(ev.data as string);\n } catch {\n data = ev.data;\n }\n for (const fn of this.onMessageFns) fn(data);\n };\n\n this.ws.onclose = () => {\n this.stopHeartbeat();\n if (!this.disposed && this.opts.reconnect) {\n this.reconnect();\n } else {\n this.setState('closed');\n }\n };\n\n this.ws.onerror = () => {\n // onclose will fire after onerror\n };\n }\n\n disconnect(): void {\n this.disposed = true;\n this.stopHeartbeat();\n this.clearReconnect();\n\n if (this.ws) {\n this.ws.onclose = null;\n this.ws.onmessage = null;\n this.ws.onerror = null;\n if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {\n this.ws.close(1000, 'client disconnect');\n }\n this.ws = null;\n }\n\n this.setState('closed');\n }\n\n send(data: unknown): void {\n if (this._state === 'connected' && this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(data));\n } else if (this._state === 'reconnecting' || this._state === 'connecting') {\n if (this.buffer.length < this.opts.sendBuffer) {\n this.buffer.push(data);\n }\n }\n }\n\n onMessage(fn: EventHandler): Unsubscribe {\n this.onMessageFns.add(fn);\n return () => this.onMessageFns.delete(fn);\n }\n\n onStateChange(fn: (state: SocketState) => void): Unsubscribe {\n this.onStateChangeFns.add(fn);\n return () => this.onStateChangeFns.delete(fn);\n }\n\n private reconnect(): void {\n this.setState('reconnecting');\n const gen = backoff(1000, this.opts.reconnectMaxDelay);\n\n const attempt = () => {\n if (this.disposed) return;\n const delay = gen.next().value;\n this.reconnectTimer = setTimeout(() => {\n if (!this.disposed) this.connect();\n }, delay);\n };\n\n attempt();\n }\n\n private flushBuffer(): void {\n const pending = this.buffer.splice(0);\n for (const item of pending) {\n this.send(item);\n }\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({ type: 'ping' }));\n }\n }, this.opts.heartbeatInterval);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private clearReconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private async buildUrl(): Promise<string> {\n // Resolve token: callback > static > none\n let token: string | undefined;\n if (this.opts.auth) {\n token = await this.opts.auth();\n } else if (this.opts.authToken) {\n token = this.opts.authToken;\n }\n\n if (!token) return this.url;\n\n // WebSocket URLs (ws://, wss://) are not fully supported by URL API.\n // Convert to http(s) for parsing, then back to ws(s).\n const httpUrl = this.url.replace(/^ws(s?):\\/\\//, 'http$1://');\n const parsed = new URL(httpUrl);\n parsed.searchParams.set(this.opts.authParam, token);\n\n return parsed.toString().replace(/^http(s?):\\/\\//, 'ws$1://');\n }\n\n private setState(state: SocketState): void {\n this._state = state;\n for (const fn of this.onStateChangeFns) fn(state);\n }\n\n [Symbol.dispose](): void {\n this.disconnect();\n this.onMessageFns.clear();\n this.onStateChangeFns.clear();\n this.buffer = [];\n }\n}\n","import './utils/disposable';\nimport type { SocketState, Unsubscribe, EventHandler } from './types';\n\n/**\n * WorkerSocket — WebSocket running inside a Web Worker.\n *\n * Same interface as SharedSocket, but WebSocket lives off main thread.\n * Benefits: heartbeat timers and JSON parsing don't block UI rendering.\n *\n * Use when:\n * - High message rate (50+ msgs/sec)\n * - Heavy JSON payloads\n * - UI does complex rendering that could block main thread\n *\n * Don't use when:\n * - Low message rate (simple chat, notifications)\n * - Bundle size matters (adds worker file)\n * - Debugging (Worker DevTools is less convenient)\n */\nexport class WorkerSocket implements Disposable {\n private worker: Worker | null = null;\n private _state: SocketState = 'closed';\n\n private onMessageFns = new Set<EventHandler>();\n private onStateChangeFns = new Set<(state: SocketState) => void>();\n\n constructor(\n private url: string,\n private options: {\n protocols?: string[];\n reconnect?: boolean;\n reconnectMaxDelay?: number;\n heartbeatInterval?: number;\n sendBuffer?: number;\n workerUrl?: string | URL;\n } = {},\n ) {}\n\n get state(): SocketState {\n return this._state;\n }\n\n connect(): void {\n // Create worker from inline blob if no workerUrl provided\n const workerUrl = this.options.workerUrl ?? this.createWorkerBlob();\n\n this.worker = new Worker(workerUrl, { type: 'module' });\n\n this.worker.onmessage = (ev: MessageEvent) => {\n const msg = ev.data;\n\n switch (msg.type) {\n case 'state':\n this._state = msg.state;\n for (const fn of this.onStateChangeFns) fn(msg.state);\n break;\n\n case 'message':\n for (const fn of this.onMessageFns) fn(msg.data);\n break;\n\n case 'open':\n // State already set via 'state' message\n break;\n\n case 'close':\n break;\n\n case 'error':\n console.error('WorkerSocket error:', msg.message);\n break;\n }\n };\n\n this.worker.postMessage({\n type: 'connect',\n url: this.url,\n protocols: this.options.protocols ?? [],\n reconnect: this.options.reconnect ?? true,\n reconnectMaxDelay: this.options.reconnectMaxDelay ?? 30_000,\n heartbeatInterval: this.options.heartbeatInterval ?? 30_000,\n bufferSize: this.options.sendBuffer ?? 100,\n });\n }\n\n send(data: unknown): void {\n this.worker?.postMessage({ type: 'send', data });\n }\n\n disconnect(): void {\n this.worker?.postMessage({ type: 'disconnect' });\n setTimeout(() => {\n this.worker?.terminate();\n this.worker = null;\n }, 100);\n this._state = 'closed';\n }\n\n onMessage(fn: EventHandler): Unsubscribe {\n this.onMessageFns.add(fn);\n return () => this.onMessageFns.delete(fn);\n }\n\n onStateChange(fn: (state: SocketState) => void): Unsubscribe {\n this.onStateChangeFns.add(fn);\n return () => this.onStateChangeFns.delete(fn);\n }\n\n private createWorkerBlob(): URL {\n // Inline the worker code as a blob URL\n // In production, use a bundler (Vite, webpack) to handle worker imports\n const code = `\n let ws = null, state = 'closed', buffer = [], disposed = false;\n let heartbeatTimer = null, reconnectTimer = null;\n let url = '', protocols = [], shouldReconnect = true;\n let maxDelay = 30000, hbInterval = 30000, maxBuf = 100, delay = 1000;\n\n function setState(s) { state = s; self.postMessage({ type: 'state', state: s }); }\n function connect() {\n if (disposed) return;\n setState('connecting');\n ws = new WebSocket(url, protocols);\n ws.onopen = () => { setState('connected'); delay = 1000; self.postMessage({ type: 'open' }); flush(); startHB(); };\n ws.onmessage = (e) => { let d; try { d = JSON.parse(e.data); } catch { d = e.data; } self.postMessage({ type: 'message', data: d }); };\n ws.onclose = (e) => { stopHB(); self.postMessage({ type: 'close', code: e.code, reason: e.reason }); if (!disposed && shouldReconnect && e.code !== 1000) reconnect(); else setState('closed'); };\n ws.onerror = () => { self.postMessage({ type: 'error', message: 'error' }); };\n }\n function send(d) { if (state === 'connected' && ws?.readyState === 1) ws.send(JSON.stringify(d)); else if (buffer.length < maxBuf) buffer.push(d); }\n function flush() { const p = buffer.splice(0); p.forEach(send); }\n function startHB() { stopHB(); heartbeatTimer = setInterval(() => { if (ws?.readyState === 1) ws.send('{\"type\":\"ping\"}'); }, hbInterval); }\n function stopHB() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }\n function reconnect() { setState('reconnecting'); const j = delay * 0.25 * (Math.random() * 2 - 1); reconnectTimer = setTimeout(() => { if (!disposed) connect(); }, Math.min(delay + j, maxDelay)); delay = Math.min(delay * 2, maxDelay); }\n self.onmessage = (e) => {\n const c = e.data;\n if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; connect(); }\n if (c.type === 'send') send(c.data);\n if (c.type === 'disconnect') { disposed = true; stopHB(); if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) { ws.onclose = null; if (ws.readyState < 2) ws.close(1000); ws = null; } buffer = []; setState('closed'); }\n };\n `;\n const blob = new Blob([code], { type: 'application/javascript' });\n return new URL(URL.createObjectURL(blob));\n }\n\n [Symbol.dispose](): void {\n this.disconnect();\n this.onMessageFns.clear();\n this.onStateChangeFns.clear();\n }\n}\n","import './utils/disposable';\nimport type { EventHandler, Unsubscribe } from './types';\n\nexport class SubscriptionManager implements Disposable {\n private handlers = new Map<string, Set<EventHandler>>();\n private lastMessages = new Map<string, unknown>();\n\n on(event: string, handler: EventHandler): Unsubscribe {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler);\n return () => set!.delete(handler);\n }\n\n once(event: string, handler: EventHandler): Unsubscribe {\n const wrapper: EventHandler = (data) => {\n unsub();\n handler(data);\n };\n const unsub = this.on(event, wrapper);\n return unsub;\n }\n\n off(event: string, handler?: EventHandler): void {\n if (handler) {\n this.handlers.get(event)?.delete(handler);\n } else {\n this.handlers.delete(event);\n }\n }\n\n emit(event: string, data: unknown): void {\n this.lastMessages.set(event, data);\n const set = this.handlers.get(event);\n if (set) {\n for (const fn of set) fn(data);\n }\n }\n\n getLastMessage(event: string): unknown | undefined {\n return this.lastMessages.get(event);\n }\n\n async *stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown> {\n const queue: unknown[] = [];\n let resolve: (() => void) | null = null;\n let done = false;\n\n const unsub = this.on(event, (data) => {\n queue.push(data);\n resolve?.();\n });\n\n const onAbort = () => {\n done = true;\n resolve?.();\n };\n signal?.addEventListener('abort', onAbort);\n\n try {\n while (!done) {\n if (queue.length > 0) {\n yield queue.shift()!;\n } else {\n await new Promise<void>((r) => { resolve = r; });\n resolve = null;\n }\n }\n } finally {\n unsub();\n signal?.removeEventListener('abort', onAbort);\n }\n }\n\n offAll(): void {\n this.handlers.clear();\n this.lastMessages.clear();\n }\n\n [Symbol.dispose](): void {\n this.offAll();\n }\n}\n","import './utils/disposable';\nimport { generateId } from './utils/id';\nimport { MessageBus } from './MessageBus';\nimport { TabCoordinator } from './TabCoordinator';\nimport { SharedSocket } from './SharedSocket';\nimport { WorkerSocket } from './WorkerSocket';\nimport { SubscriptionManager } from './SubscriptionManager';\nimport type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler } from './types';\n\n/** Common interface for both SharedSocket and WorkerSocket. */\ninterface SocketAdapter {\n readonly state: string;\n connect(): void;\n send(data: unknown): void;\n disconnect(): void;\n onMessage(fn: EventHandler): Unsubscribe;\n onStateChange(fn: (state: string) => void): Unsubscribe;\n [Symbol.dispose](): void;\n}\n\n/**\n * SharedWebSocket — shares ONE WebSocket connection across browser tabs.\n *\n * One tab becomes the \"leader\" and holds the WebSocket.\n * Other tabs are \"followers\" receiving data via BroadcastChannel.\n * If the leader closes, a new leader is elected automatically.\n */\nexport class SharedWebSocket implements Disposable {\n private bus: MessageBus;\n private coordinator: TabCoordinator;\n private socket: SocketAdapter | null = null;\n private subs = new SubscriptionManager();\n private syncStore = new Map<string, unknown>();\n private tabId: string;\n private cleanups: Unsubscribe[] = [];\n private disposed = false;\n\n constructor(\n private readonly url: string,\n private readonly options: SharedWebSocketOptions = {},\n ) {\n this.tabId = generateId();\n this.bus = new MessageBus('shared-ws', this.tabId);\n this.coordinator = new TabCoordinator(this.bus, this.tabId, {\n electionTimeout: options.electionTimeout,\n heartbeatInterval: options.leaderHeartbeat,\n leaderTimeout: options.leaderTimeout,\n });\n\n // When ANY tab receives a WS message via bus → emit to local subscribers\n this.cleanups.push(\n this.bus.subscribe<{ event: string; data: unknown }>('ws:message', (msg) => {\n this.subs.emit(msg.event, msg.data);\n }),\n );\n\n // Leader listens for send requests from followers\n this.cleanups.push(\n this.bus.subscribe<{ event: string; data: unknown }>('ws:send', (msg) => {\n if (this.coordinator.isLeader && this.socket) {\n this.socket.send({ event: msg.event, data: msg.data });\n }\n }),\n );\n\n // Sync across tabs\n this.cleanups.push(\n this.bus.subscribe<{ key: string; value: unknown }>('ws:sync', (msg) => {\n this.syncStore.set(msg.key, msg.value);\n this.subs.emit(`sync:${msg.key}`, msg.value);\n }),\n );\n\n // Leader lifecycle\n this.coordinator.onBecomeLeader(() => this.onBecomeLeader());\n this.coordinator.onLoseLeadership(() => this.onLoseLeadership());\n\n // Cleanup on tab close\n if (typeof window !== 'undefined') {\n const onBeforeUnload = () => this[Symbol.dispose]();\n window.addEventListener('beforeunload', onBeforeUnload);\n this.cleanups.push(() => window.removeEventListener('beforeunload', onBeforeUnload));\n }\n }\n\n get connected(): boolean {\n return this.socket?.state === 'connected' || !this.coordinator.isLeader;\n }\n\n get tabRole(): TabRole {\n return this.coordinator.isLeader ? 'leader' : 'follower';\n }\n\n /** Start leader election and connect. */\n async connect(): Promise<void> {\n await this.coordinator.elect();\n }\n\n /** Subscribe to server events (works in ALL tabs). */\n on(event: string, handler: EventHandler): Unsubscribe {\n return this.subs.on(event, handler);\n }\n\n once(event: string, handler: EventHandler): Unsubscribe {\n return this.subs.once(event, handler);\n }\n\n off(event: string, handler?: EventHandler): void {\n this.subs.off(event, handler);\n }\n\n /** Async generator for consuming events. */\n stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown> {\n return this.subs.stream(event, signal);\n }\n\n /** Send message to server (auto-routed through leader). */\n send(event: string, data: unknown): void {\n if (this.coordinator.isLeader && this.socket) {\n this.socket.send({ event, data });\n } else {\n this.bus.publish('ws:send', { event, data });\n }\n }\n\n /** Request/response through server via leader. */\n async request<T>(event: string, data: unknown, timeout = 5000): Promise<T> {\n return this.bus.request('ws:request', { event, data }, timeout);\n }\n\n /** Sync state across tabs (no server roundtrip). */\n sync<T>(key: string, value: T): void {\n this.syncStore.set(key, value);\n this.bus.broadcast('ws:sync', { key, value });\n }\n\n getSync<T>(key: string): T | undefined {\n return this.syncStore.get(key) as T | undefined;\n }\n\n onSync<T>(key: string, fn: (value: T) => void): Unsubscribe {\n return this.subs.on(`sync:${key}`, fn as EventHandler);\n }\n\n disconnect(): void {\n this[Symbol.dispose]();\n }\n\n private createSocket(): SocketAdapter {\n const socketOptions = {\n protocols: this.options.protocols,\n reconnect: this.options.reconnect,\n reconnectMaxDelay: this.options.reconnectMaxDelay,\n heartbeatInterval: this.options.heartbeatInterval,\n sendBuffer: this.options.sendBuffer,\n };\n\n if (this.options.useWorker) {\n // WebSocket runs in a Web Worker — main thread stays free\n return new WorkerSocket(this.url, {\n ...socketOptions,\n workerUrl: this.options.workerUrl,\n });\n }\n\n // WebSocket runs in main thread (default)\n return new SharedSocket(this.url, {\n ...socketOptions,\n auth: this.options.auth,\n authToken: this.options.authToken,\n authParam: this.options.authParam,\n });\n }\n\n private onBecomeLeader(): void {\n this.socket = this.createSocket();\n\n this.socket.onMessage((data: any) => {\n const event = data?.event ?? 'message';\n const payload = data?.data ?? data;\n // Broadcast to ALL tabs (including self)\n this.bus.broadcast('ws:message', { event, data: payload });\n });\n\n // Handle send requests from followers (request/response pattern)\n this.cleanups.push(\n this.bus.respond<{ event: string; data: unknown }, unknown>('ws:request', async (req) => {\n return new Promise((resolve) => {\n const unsub = this.socket!.onMessage((response: any) => {\n if (response?.event === req.event || response?.requestId) {\n unsub();\n resolve(response?.data ?? response);\n }\n });\n this.socket!.send({ event: req.event, data: req.data });\n });\n }),\n );\n\n this.socket.connect();\n }\n\n private onLoseLeadership(): void {\n if (this.socket) {\n this.socket[Symbol.dispose]();\n this.socket = null;\n }\n }\n\n [Symbol.dispose](): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.coordinator[Symbol.dispose]();\n\n if (this.socket) {\n this.socket[Symbol.dispose]();\n this.socket = null;\n }\n\n for (const unsub of this.cleanups) unsub();\n this.cleanups = [];\n this.subs[Symbol.dispose]();\n this.bus[Symbol.dispose]();\n this.syncStore.clear();\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/gwakko/Projects/shared-websocket/dist/chunk-Q4OKSJX7.cjs","../src/utils/disposable.ts","../src/utils/id.ts","../src/MessageBus.ts","../src/TabCoordinator.ts","../src/utils/backoff.ts","../src/SharedSocket.ts","../src/WorkerSocket.ts","../src/SubscriptionManager.ts","../src/SharedWebSocket.ts"],"names":[],"mappings":"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","file":"/Users/gwakko/Projects/shared-websocket/dist/chunk-Q4OKSJX7.cjs","sourcesContent":[null,"/** Polyfill Symbol.dispose if not available. */\nif (typeof Symbol.dispose === 'undefined') {\n (Symbol as any).dispose = Symbol('Symbol.dispose');\n}\n","export function generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n","import './utils/disposable';\nimport { generateId } from './utils/id';\nimport type { BusMessage, Unsubscribe } from './types';\n\ntype Listener = (msg: BusMessage) => void;\n\nexport class MessageBus implements Disposable {\n private channel: BroadcastChannel;\n private listeners = new Map<string, Set<Listener>>();\n private pendingRequests = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();\n\n constructor(\n channelName: string,\n private readonly tabId: string,\n ) {\n this.channel = new BroadcastChannel(channelName);\n this.channel.onmessage = (ev: MessageEvent<BusMessage>) => {\n this.handleMessage(ev.data);\n };\n }\n\n subscribe<T>(topic: string, fn: (data: T) => void): Unsubscribe {\n const wrapper: Listener = (msg) => {\n if (msg.source !== this.tabId) fn(msg.data as T);\n };\n this.addListener(topic, wrapper);\n return () => this.removeListener(topic, wrapper);\n }\n\n publish<T>(topic: string, data: T): void {\n this.postMessage({ topic, type: 'publish', data });\n }\n\n broadcast<T>(topic: string, data: T): void {\n const msg = this.createMessage(topic, 'broadcast', data);\n this.channel.postMessage(msg);\n // Also deliver to self\n this.handleMessage(msg);\n }\n\n async request<T, R>(topic: string, data: T, timeout = 5000): Promise<R> {\n const msg = this.createMessage(topic, 'request', data);\n return new Promise<R>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pendingRequests.delete(msg.id);\n reject(new Error(`MessageBus.request: timeout for topic \"${topic}\"`));\n }, timeout);\n this.pendingRequests.set(msg.id, { resolve: resolve as (v: unknown) => void, reject, timer });\n this.channel.postMessage(msg);\n });\n }\n\n respond<T, R>(topic: string, fn: (data: T) => R | Promise<R>): Unsubscribe {\n const wrapper: Listener = async (msg) => {\n if (msg.type !== 'request' || msg.source === this.tabId) return;\n const result = await fn(msg.data as T);\n this.postMessage({ topic, type: 'response', data: { requestId: msg.id, result } });\n };\n this.addListener(topic, wrapper);\n return () => this.removeListener(topic, wrapper);\n }\n\n private handleMessage(msg: BusMessage): void {\n // Handle response to pending request\n if (msg.type === 'response') {\n const payload = msg.data as { requestId: string; result: unknown };\n const pending = this.pendingRequests.get(payload.requestId);\n if (pending) {\n clearTimeout(pending.timer);\n this.pendingRequests.delete(payload.requestId);\n pending.resolve(payload.result);\n return;\n }\n }\n\n const listeners = this.listeners.get(msg.topic);\n if (listeners) {\n for (const fn of listeners) fn(msg);\n }\n }\n\n private postMessage(partial: Pick<BusMessage, 'topic' | 'type' | 'data'>): void {\n this.channel.postMessage(this.createMessage(partial.topic, partial.type, partial.data));\n }\n\n private createMessage(topic: string, type: BusMessage['type'], data: unknown): BusMessage {\n return { id: generateId(), source: this.tabId, topic, type, data, timestamp: Date.now() };\n }\n\n private addListener(topic: string, fn: Listener): void {\n let set = this.listeners.get(topic);\n if (!set) {\n set = new Set();\n this.listeners.set(topic, set);\n }\n set.add(fn);\n }\n\n private removeListener(topic: string, fn: Listener): void {\n this.listeners.get(topic)?.delete(fn);\n }\n\n [Symbol.dispose](): void {\n for (const pending of this.pendingRequests.values()) {\n clearTimeout(pending.timer);\n pending.reject(new Error('MessageBus disposed'));\n }\n this.pendingRequests.clear();\n this.listeners.clear();\n this.channel.close();\n }\n}\n","import './utils/disposable';\nimport { MessageBus } from './MessageBus';\nimport type { Unsubscribe } from './types';\n\ninterface CoordinatorOptions {\n electionTimeout?: number; // ms to wait for rejection (default 200)\n heartbeatInterval?: number; // ms between heartbeats (default 2000)\n leaderTimeout?: number; // ms without heartbeat to trigger election (default 5000)\n}\n\nexport class TabCoordinator implements Disposable {\n private _isLeader = false;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private leaderCheckTimer: ReturnType<typeof setInterval> | null = null;\n private lastHeartbeat = 0;\n private disposed = false;\n\n private onBecomeLeaderFns = new Set<() => void>();\n private onLoseLeadershipFns = new Set<() => void>();\n private cleanups: Unsubscribe[] = [];\n\n private readonly electionTimeout: number;\n private readonly heartbeatInterval: number;\n private readonly leaderTimeout: number;\n\n constructor(\n private readonly bus: MessageBus,\n private readonly tabId: string,\n options: CoordinatorOptions = {},\n ) {\n this.electionTimeout = options.electionTimeout ?? 200;\n this.heartbeatInterval = options.heartbeatInterval ?? 2000;\n this.leaderTimeout = options.leaderTimeout ?? 5000;\n\n // Listen for election requests — reject if we are leader\n this.cleanups.push(\n this.bus.subscribe<{ tabId: string }>('coord:election', () => {\n if (this._isLeader) {\n this.bus.publish('coord:reject', { tabId: this.tabId });\n }\n }),\n );\n\n // Listen for heartbeats\n this.cleanups.push(\n this.bus.subscribe<{ tabId: string }>('coord:heartbeat', () => {\n this.lastHeartbeat = Date.now();\n }),\n );\n\n // Listen for abdication\n this.cleanups.push(\n this.bus.subscribe('coord:abdicate', () => {\n if (!this._isLeader && !this.disposed) {\n this.elect();\n }\n }),\n );\n }\n\n get isLeader(): boolean {\n return this._isLeader;\n }\n\n async elect(): Promise<void> {\n if (this.disposed) return;\n\n return new Promise<void>((resolve) => {\n let rejected = false;\n\n const unsub = this.bus.subscribe('coord:reject', () => {\n rejected = true;\n unsub();\n // We are follower — start monitoring leader heartbeat\n this.startLeaderCheck();\n resolve();\n });\n\n this.bus.publish('coord:election', { tabId: this.tabId });\n\n setTimeout(() => {\n unsub();\n if (!rejected && !this.disposed) {\n this.becomeLeader();\n }\n resolve();\n }, this.electionTimeout);\n });\n }\n\n abdicate(): void {\n if (!this._isLeader) return;\n this._isLeader = false;\n this.stopHeartbeat();\n this.bus.publish('coord:abdicate', { tabId: this.tabId });\n for (const fn of this.onLoseLeadershipFns) fn();\n }\n\n onBecomeLeader(fn: () => void): Unsubscribe {\n this.onBecomeLeaderFns.add(fn);\n return () => this.onBecomeLeaderFns.delete(fn);\n }\n\n onLoseLeadership(fn: () => void): Unsubscribe {\n this.onLoseLeadershipFns.add(fn);\n return () => this.onLoseLeadershipFns.delete(fn);\n }\n\n private becomeLeader(): void {\n this._isLeader = true;\n this.stopLeaderCheck();\n this.startHeartbeat();\n for (const fn of this.onBecomeLeaderFns) fn();\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n this.bus.publish('coord:heartbeat', { tabId: this.tabId });\n }, this.heartbeatInterval);\n // Send immediately\n this.bus.publish('coord:heartbeat', { tabId: this.tabId });\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private startLeaderCheck(): void {\n this.stopLeaderCheck();\n this.lastHeartbeat = Date.now();\n this.leaderCheckTimer = setInterval(() => {\n if (Date.now() - this.lastHeartbeat > this.leaderTimeout && !this.disposed) {\n this.stopLeaderCheck();\n this.elect();\n }\n }, 1000);\n }\n\n private stopLeaderCheck(): void {\n if (this.leaderCheckTimer) {\n clearInterval(this.leaderCheckTimer);\n this.leaderCheckTimer = null;\n }\n }\n\n [Symbol.dispose](): void {\n this.disposed = true;\n if (this._isLeader) {\n this.abdicate();\n }\n this.stopHeartbeat();\n this.stopLeaderCheck();\n for (const unsub of this.cleanups) unsub();\n this.cleanups = [];\n this.onBecomeLeaderFns.clear();\n this.onLoseLeadershipFns.clear();\n }\n}\n","/** Exponential backoff generator with jitter. */\nexport function* backoff(base = 1000, max = 30_000): Generator<number> {\n let delay = base;\n while (true) {\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n yield Math.min(delay + jitter, max);\n delay = Math.min(delay * 2, max);\n }\n}\n","import './utils/disposable';\nimport { backoff } from './utils/backoff';\nimport type { SocketState, Unsubscribe, EventHandler } from './types';\n\ninterface SharedSocketOptions {\n protocols?: string[];\n reconnect?: boolean;\n reconnectMaxDelay?: number;\n heartbeatInterval?: number;\n sendBuffer?: number;\n auth?: () => string | Promise<string>;\n authToken?: string;\n authParam?: string;\n}\n\nexport class SharedSocket implements Disposable {\n private ws: WebSocket | null = null;\n private _state: SocketState = 'closed';\n private buffer: unknown[] = [];\n private disposed = false;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n private onMessageFns = new Set<EventHandler>();\n private onStateChangeFns = new Set<(state: SocketState) => void>();\n\n private readonly opts: Required<Omit<SharedSocketOptions, 'auth' | 'authToken' | 'authParam'>> & {\n auth?: () => string | Promise<string>;\n authToken?: string;\n authParam: string;\n };\n\n constructor(\n private url: string,\n options: SharedSocketOptions = {},\n ) {\n this.opts = {\n protocols: options.protocols ?? [],\n reconnect: options.reconnect ?? true,\n reconnectMaxDelay: options.reconnectMaxDelay ?? 30_000,\n heartbeatInterval: options.heartbeatInterval ?? 30_000,\n sendBuffer: options.sendBuffer ?? 100,\n auth: options.auth,\n authToken: options.authToken,\n authParam: options.authParam ?? 'token',\n };\n }\n\n get state(): SocketState {\n return this._state;\n }\n\n async connect(): Promise<void> {\n if (this.disposed) return;\n\n this.setState('connecting');\n\n const connectUrl = await this.buildUrl();\n this.ws = new WebSocket(connectUrl, this.opts.protocols);\n\n this.ws.onopen = () => {\n this.setState('connected');\n this.flushBuffer();\n this.startHeartbeat();\n };\n\n this.ws.onmessage = (ev: MessageEvent) => {\n let data: unknown;\n try {\n data = JSON.parse(ev.data as string);\n } catch {\n data = ev.data;\n }\n for (const fn of this.onMessageFns) fn(data);\n };\n\n this.ws.onclose = () => {\n this.stopHeartbeat();\n if (!this.disposed && this.opts.reconnect) {\n this.reconnect();\n } else {\n this.setState('closed');\n }\n };\n\n this.ws.onerror = () => {\n // onclose will fire after onerror\n };\n }\n\n disconnect(): void {\n this.disposed = true;\n this.stopHeartbeat();\n this.clearReconnect();\n\n if (this.ws) {\n this.ws.onclose = null;\n this.ws.onmessage = null;\n this.ws.onerror = null;\n if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {\n this.ws.close(1000, 'client disconnect');\n }\n this.ws = null;\n }\n\n this.setState('closed');\n }\n\n send(data: unknown): void {\n if (this._state === 'connected' && this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(data));\n } else if (this._state === 'reconnecting' || this._state === 'connecting') {\n if (this.buffer.length < this.opts.sendBuffer) {\n this.buffer.push(data);\n }\n }\n }\n\n onMessage(fn: EventHandler): Unsubscribe {\n this.onMessageFns.add(fn);\n return () => this.onMessageFns.delete(fn);\n }\n\n onStateChange(fn: (state: SocketState) => void): Unsubscribe {\n this.onStateChangeFns.add(fn);\n return () => this.onStateChangeFns.delete(fn);\n }\n\n private reconnect(): void {\n this.setState('reconnecting');\n const gen = backoff(1000, this.opts.reconnectMaxDelay);\n\n const attempt = () => {\n if (this.disposed) return;\n const delay = gen.next().value;\n this.reconnectTimer = setTimeout(() => {\n if (!this.disposed) this.connect();\n }, delay);\n };\n\n attempt();\n }\n\n private flushBuffer(): void {\n const pending = this.buffer.splice(0);\n for (const item of pending) {\n this.send(item);\n }\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({ type: 'ping' }));\n }\n }, this.opts.heartbeatInterval);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private clearReconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private async buildUrl(): Promise<string> {\n // Resolve token: callback > static > none\n let token: string | undefined;\n if (this.opts.auth) {\n token = await this.opts.auth();\n } else if (this.opts.authToken) {\n token = this.opts.authToken;\n }\n\n if (!token) return this.url;\n\n // WebSocket URLs (ws://, wss://) are not fully supported by URL API.\n // Convert to http(s) for parsing, then back to ws(s).\n const httpUrl = this.url.replace(/^ws(s?):\\/\\//, 'http$1://');\n const parsed = new URL(httpUrl);\n parsed.searchParams.set(this.opts.authParam, token);\n\n return parsed.toString().replace(/^http(s?):\\/\\//, 'ws$1://');\n }\n\n private setState(state: SocketState): void {\n this._state = state;\n for (const fn of this.onStateChangeFns) fn(state);\n }\n\n [Symbol.dispose](): void {\n this.disconnect();\n this.onMessageFns.clear();\n this.onStateChangeFns.clear();\n this.buffer = [];\n }\n}\n","import './utils/disposable';\nimport type { SocketState, Unsubscribe, EventHandler } from './types';\n\n/**\n * WorkerSocket — WebSocket running inside a Web Worker.\n *\n * Same interface as SharedSocket, but WebSocket lives off main thread.\n * Benefits: heartbeat timers and JSON parsing don't block UI rendering.\n *\n * Use when:\n * - High message rate (50+ msgs/sec)\n * - Heavy JSON payloads\n * - UI does complex rendering that could block main thread\n *\n * Don't use when:\n * - Low message rate (simple chat, notifications)\n * - Bundle size matters (adds worker file)\n * - Debugging (Worker DevTools is less convenient)\n */\nexport class WorkerSocket implements Disposable {\n private worker: Worker | null = null;\n private _state: SocketState = 'closed';\n\n private onMessageFns = new Set<EventHandler>();\n private onStateChangeFns = new Set<(state: SocketState) => void>();\n\n constructor(\n private url: string,\n private options: {\n protocols?: string[];\n reconnect?: boolean;\n reconnectMaxDelay?: number;\n heartbeatInterval?: number;\n sendBuffer?: number;\n workerUrl?: string | URL;\n } = {},\n ) {}\n\n get state(): SocketState {\n return this._state;\n }\n\n connect(): void {\n // Create worker from inline blob if no workerUrl provided\n const workerUrl = this.options.workerUrl ?? this.createWorkerBlob();\n\n this.worker = new Worker(workerUrl, { type: 'module' });\n\n this.worker.onmessage = (ev: MessageEvent) => {\n const msg = ev.data;\n\n switch (msg.type) {\n case 'state':\n this._state = msg.state;\n for (const fn of this.onStateChangeFns) fn(msg.state);\n break;\n\n case 'message':\n for (const fn of this.onMessageFns) fn(msg.data);\n break;\n\n case 'open':\n // State already set via 'state' message\n break;\n\n case 'close':\n break;\n\n case 'error':\n console.error('WorkerSocket error:', msg.message);\n break;\n }\n };\n\n this.worker.postMessage({\n type: 'connect',\n url: this.url,\n protocols: this.options.protocols ?? [],\n reconnect: this.options.reconnect ?? true,\n reconnectMaxDelay: this.options.reconnectMaxDelay ?? 30_000,\n heartbeatInterval: this.options.heartbeatInterval ?? 30_000,\n bufferSize: this.options.sendBuffer ?? 100,\n });\n }\n\n send(data: unknown): void {\n this.worker?.postMessage({ type: 'send', data });\n }\n\n disconnect(): void {\n this.worker?.postMessage({ type: 'disconnect' });\n setTimeout(() => {\n this.worker?.terminate();\n this.worker = null;\n }, 100);\n this._state = 'closed';\n }\n\n onMessage(fn: EventHandler): Unsubscribe {\n this.onMessageFns.add(fn);\n return () => this.onMessageFns.delete(fn);\n }\n\n onStateChange(fn: (state: SocketState) => void): Unsubscribe {\n this.onStateChangeFns.add(fn);\n return () => this.onStateChangeFns.delete(fn);\n }\n\n private createWorkerBlob(): URL {\n // Inline the worker code as a blob URL\n // In production, use a bundler (Vite, webpack) to handle worker imports\n const code = `\n let ws = null, state = 'closed', buffer = [], disposed = false;\n let heartbeatTimer = null, reconnectTimer = null;\n let url = '', protocols = [], shouldReconnect = true;\n let maxDelay = 30000, hbInterval = 30000, maxBuf = 100, delay = 1000;\n\n function setState(s) { state = s; self.postMessage({ type: 'state', state: s }); }\n function connect() {\n if (disposed) return;\n setState('connecting');\n ws = new WebSocket(url, protocols);\n ws.onopen = () => { setState('connected'); delay = 1000; self.postMessage({ type: 'open' }); flush(); startHB(); };\n ws.onmessage = (e) => { let d; try { d = JSON.parse(e.data); } catch { d = e.data; } self.postMessage({ type: 'message', data: d }); };\n ws.onclose = (e) => { stopHB(); self.postMessage({ type: 'close', code: e.code, reason: e.reason }); if (!disposed && shouldReconnect && e.code !== 1000) reconnect(); else setState('closed'); };\n ws.onerror = () => { self.postMessage({ type: 'error', message: 'error' }); };\n }\n function send(d) { if (state === 'connected' && ws?.readyState === 1) ws.send(JSON.stringify(d)); else if (buffer.length < maxBuf) buffer.push(d); }\n function flush() { const p = buffer.splice(0); p.forEach(send); }\n function startHB() { stopHB(); heartbeatTimer = setInterval(() => { if (ws?.readyState === 1) ws.send('{\"type\":\"ping\"}'); }, hbInterval); }\n function stopHB() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }\n function reconnect() { setState('reconnecting'); const j = delay * 0.25 * (Math.random() * 2 - 1); reconnectTimer = setTimeout(() => { if (!disposed) connect(); }, Math.min(delay + j, maxDelay)); delay = Math.min(delay * 2, maxDelay); }\n self.onmessage = (e) => {\n const c = e.data;\n if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; connect(); }\n if (c.type === 'send') send(c.data);\n if (c.type === 'disconnect') { disposed = true; stopHB(); if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) { ws.onclose = null; if (ws.readyState < 2) ws.close(1000); ws = null; } buffer = []; setState('closed'); }\n };\n `;\n const blob = new Blob([code], { type: 'application/javascript' });\n return new URL(URL.createObjectURL(blob));\n }\n\n [Symbol.dispose](): void {\n this.disconnect();\n this.onMessageFns.clear();\n this.onStateChangeFns.clear();\n }\n}\n","import './utils/disposable';\nimport type { EventHandler, Unsubscribe } from './types';\n\nexport class SubscriptionManager implements Disposable {\n private handlers = new Map<string, Set<EventHandler>>();\n private lastMessages = new Map<string, unknown>();\n\n on(event: string, handler: EventHandler): Unsubscribe {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler);\n return () => set!.delete(handler);\n }\n\n once(event: string, handler: EventHandler): Unsubscribe {\n const wrapper: EventHandler = (data) => {\n unsub();\n handler(data);\n };\n const unsub = this.on(event, wrapper);\n return unsub;\n }\n\n off(event: string, handler?: EventHandler): void {\n if (handler) {\n this.handlers.get(event)?.delete(handler);\n } else {\n this.handlers.delete(event);\n }\n }\n\n emit(event: string, data: unknown): void {\n this.lastMessages.set(event, data);\n const set = this.handlers.get(event);\n if (set) {\n for (const fn of set) fn(data);\n }\n }\n\n getLastMessage(event: string): unknown | undefined {\n return this.lastMessages.get(event);\n }\n\n async *stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown> {\n const queue: unknown[] = [];\n let resolve: (() => void) | null = null;\n let done = false;\n\n const unsub = this.on(event, (data) => {\n queue.push(data);\n resolve?.();\n });\n\n const onAbort = () => {\n done = true;\n resolve?.();\n };\n signal?.addEventListener('abort', onAbort);\n\n try {\n while (!done) {\n if (queue.length > 0) {\n yield queue.shift()!;\n } else {\n await new Promise<void>((r) => { resolve = r; });\n resolve = null;\n }\n }\n } finally {\n unsub();\n signal?.removeEventListener('abort', onAbort);\n }\n }\n\n offAll(): void {\n this.handlers.clear();\n this.lastMessages.clear();\n }\n\n [Symbol.dispose](): void {\n this.offAll();\n }\n}\n","import './utils/disposable';\nimport { generateId } from './utils/id';\nimport { MessageBus } from './MessageBus';\nimport { TabCoordinator } from './TabCoordinator';\nimport { SharedSocket } from './SharedSocket';\nimport { WorkerSocket } from './WorkerSocket';\nimport { SubscriptionManager } from './SubscriptionManager';\nimport type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler } from './types';\n\n/** Common interface for both SharedSocket and WorkerSocket. */\ninterface SocketAdapter {\n readonly state: string;\n connect(): void;\n send(data: unknown): void;\n disconnect(): void;\n onMessage(fn: EventHandler): Unsubscribe;\n onStateChange(fn: (state: string) => void): Unsubscribe;\n [Symbol.dispose](): void;\n}\n\n/**\n * SharedWebSocket — shares ONE WebSocket connection across browser tabs.\n *\n * One tab becomes the \"leader\" and holds the WebSocket.\n * Other tabs are \"followers\" receiving data via BroadcastChannel.\n * If the leader closes, a new leader is elected automatically.\n */\nexport class SharedWebSocket implements Disposable {\n private bus: MessageBus;\n private coordinator: TabCoordinator;\n private socket: SocketAdapter | null = null;\n private subs = new SubscriptionManager();\n private syncStore = new Map<string, unknown>();\n private tabId: string;\n private cleanups: Unsubscribe[] = [];\n private disposed = false;\n\n constructor(\n private readonly url: string,\n private readonly options: SharedWebSocketOptions = {},\n ) {\n this.tabId = generateId();\n this.bus = new MessageBus('shared-ws', this.tabId);\n this.coordinator = new TabCoordinator(this.bus, this.tabId, {\n electionTimeout: options.electionTimeout,\n heartbeatInterval: options.leaderHeartbeat,\n leaderTimeout: options.leaderTimeout,\n });\n\n // When ANY tab receives a WS message via bus → emit to local subscribers\n this.cleanups.push(\n this.bus.subscribe<{ event: string; data: unknown }>('ws:message', (msg) => {\n this.subs.emit(msg.event, msg.data);\n }),\n );\n\n // Leader listens for send requests from followers\n this.cleanups.push(\n this.bus.subscribe<{ event: string; data: unknown }>('ws:send', (msg) => {\n if (this.coordinator.isLeader && this.socket) {\n this.socket.send({ event: msg.event, data: msg.data });\n }\n }),\n );\n\n // Sync across tabs\n this.cleanups.push(\n this.bus.subscribe<{ key: string; value: unknown }>('ws:sync', (msg) => {\n this.syncStore.set(msg.key, msg.value);\n this.subs.emit(`sync:${msg.key}`, msg.value);\n }),\n );\n\n // Leader lifecycle\n this.coordinator.onBecomeLeader(() => this.onBecomeLeader());\n this.coordinator.onLoseLeadership(() => this.onLoseLeadership());\n\n // Cleanup on tab close\n if (typeof window !== 'undefined') {\n const onBeforeUnload = () => this[Symbol.dispose]();\n window.addEventListener('beforeunload', onBeforeUnload);\n this.cleanups.push(() => window.removeEventListener('beforeunload', onBeforeUnload));\n }\n }\n\n get connected(): boolean {\n return this.socket?.state === 'connected' || !this.coordinator.isLeader;\n }\n\n get tabRole(): TabRole {\n return this.coordinator.isLeader ? 'leader' : 'follower';\n }\n\n /** Start leader election and connect. */\n async connect(): Promise<void> {\n await this.coordinator.elect();\n }\n\n /** Subscribe to server events (works in ALL tabs). */\n on(event: string, handler: EventHandler): Unsubscribe {\n return this.subs.on(event, handler);\n }\n\n once(event: string, handler: EventHandler): Unsubscribe {\n return this.subs.once(event, handler);\n }\n\n off(event: string, handler?: EventHandler): void {\n this.subs.off(event, handler);\n }\n\n /** Async generator for consuming events. */\n stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown> {\n return this.subs.stream(event, signal);\n }\n\n /** Send message to server (auto-routed through leader). */\n send(event: string, data: unknown): void {\n if (this.coordinator.isLeader && this.socket) {\n this.socket.send({ event, data });\n } else {\n this.bus.publish('ws:send', { event, data });\n }\n }\n\n /** Request/response through server via leader. */\n async request<T>(event: string, data: unknown, timeout = 5000): Promise<T> {\n return this.bus.request('ws:request', { event, data }, timeout);\n }\n\n /** Sync state across tabs (no server roundtrip). */\n sync<T>(key: string, value: T): void {\n this.syncStore.set(key, value);\n this.bus.broadcast('ws:sync', { key, value });\n }\n\n getSync<T>(key: string): T | undefined {\n return this.syncStore.get(key) as T | undefined;\n }\n\n onSync<T>(key: string, fn: (value: T) => void): Unsubscribe {\n return this.subs.on(`sync:${key}`, fn as EventHandler);\n }\n\n disconnect(): void {\n this[Symbol.dispose]();\n }\n\n private createSocket(): SocketAdapter {\n const socketOptions = {\n protocols: this.options.protocols,\n reconnect: this.options.reconnect,\n reconnectMaxDelay: this.options.reconnectMaxDelay,\n heartbeatInterval: this.options.heartbeatInterval,\n sendBuffer: this.options.sendBuffer,\n };\n\n if (this.options.useWorker) {\n // WebSocket runs in a Web Worker — main thread stays free\n return new WorkerSocket(this.url, {\n ...socketOptions,\n workerUrl: this.options.workerUrl,\n });\n }\n\n // WebSocket runs in main thread (default)\n return new SharedSocket(this.url, {\n ...socketOptions,\n auth: this.options.auth,\n authToken: this.options.authToken,\n authParam: this.options.authParam,\n });\n }\n\n private onBecomeLeader(): void {\n this.socket = this.createSocket();\n\n this.socket.onMessage((data: any) => {\n const event = data?.event ?? 'message';\n const payload = data?.data ?? data;\n // Broadcast to ALL tabs (including self)\n this.bus.broadcast('ws:message', { event, data: payload });\n });\n\n // Handle send requests from followers (request/response pattern)\n this.cleanups.push(\n this.bus.respond<{ event: string; data: unknown }, unknown>('ws:request', async (req) => {\n return new Promise((resolve) => {\n const unsub = this.socket!.onMessage((response: any) => {\n if (response?.event === req.event || response?.requestId) {\n unsub();\n resolve(response?.data ?? response);\n }\n });\n this.socket!.send({ event: req.event, data: req.data });\n });\n }),\n );\n\n this.socket.connect();\n }\n\n private onLoseLeadership(): void {\n if (this.socket) {\n this.socket[Symbol.dispose]();\n this.socket = null;\n }\n }\n\n [Symbol.dispose](): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.coordinator[Symbol.dispose]();\n\n if (this.socket) {\n this.socket[Symbol.dispose]();\n this.socket = null;\n }\n\n for (const unsub of this.cleanups) unsub();\n this.cleanups = [];\n this.subs[Symbol.dispose]();\n this.bus[Symbol.dispose]();\n this.syncStore.clear();\n }\n}\n"]}