@hedystia/ws 2.3.3 → 2.3.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":["WSServer"],"sources":["../src/server.ts"],"sourcesContent":["import { type WebSocket as NodeWebSocket, WebSocketServer as WSServer } from \"ws\";\n\nimport type {\n ServerWebSocket,\n UpgradeOptions,\n UpgradeRequest,\n WebSocketHandlers,\n WebSocketServerOptions,\n WSData,\n WSMessage,\n} from \"./types\";\n\nexport type {\n ServerWebSocket,\n UpgradeOptions,\n UpgradeRequest,\n WebSocketHandlers,\n WebSocketServerOptions,\n WSData,\n WSMessage,\n} from \"./types\";\n\n/**\n * Runtime-agnostic WebSocket server.\n *\n * @remarks\n * Internally backed by the [`ws`](https://github.com/websockets/ws) package\n * which runs on Bun, Node.js and Deno (via `npm:` specifiers). The class\n * does **not** create or own an HTTP server — callers feed it raw upgrade\n * tuples coming from any HTTP runtime they prefer.\n *\n * It implements topic-based pub/sub on top of the per-connection\n * `subscribe` / `unsubscribe` / `publish` API expected by Hedystia,\n * matching the shape of `Bun.ServerWebSocket`.\n *\n * @typeParam Data - Shape of the user-attached `data` field\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { WebSocketServer } from \"@hedystia/ws/server\";\n *\n * const wss = new WebSocketServer({\n * open: (ws) => ws.send(\"welcome\"),\n * message: (ws, msg) => ws.publish(\"room\", msg),\n * });\n *\n * const http = createServer((_req, res) => res.end(\"ok\"));\n * http.on(\"upgrade\", (req, socket, head) => {\n * wss.upgrade({ rawRequest: req, socket, head }, { data: { user: \"anon\" } });\n * });\n * http.listen(3000);\n * ```\n */\nexport class WebSocketServer<Data extends WSData = WSData> {\n private readonly handlers: WebSocketHandlers<Data>;\n private readonly wss: WSServer;\n private readonly topics = new Map<string, Set<NodeWebSocket>>();\n private readonly socketTopics = new WeakMap<NodeWebSocket, Set<string>>();\n private readonly allSockets = new Set<NodeWebSocket>();\n\n /**\n * Build a new WebSocket server.\n *\n * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})\n * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})\n *\n * @example\n * ```ts\n * const wss = new WebSocketServer(\n * { message: (ws, msg) => ws.send(msg) },\n * { maxPayload: 1024 * 1024 },\n * );\n * ```\n */\n constructor(handlers: WebSocketHandlers<Data>, options: WebSocketServerOptions = {}) {\n this.handlers = handlers;\n this.wss = new WSServer({\n noServer: true,\n maxPayload: options.maxPayload,\n perMessageDeflate: (options.perMessageDeflate ?? false) as any,\n });\n }\n\n /**\n * Upgrade a raw HTTP upgrade tuple to a WebSocket connection.\n *\n * @remarks\n * The returned promise resolves to the connected {@link ServerWebSocket}\n * once the handshake completes; rejection means the handshake failed.\n *\n * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event\n * @param options - Optional initial `data` for the new connection\n * @returns Promise that resolves with the established socket wrapper\n *\n * @throws {Error} When the underlying handshake throws synchronously\n *\n * @example\n * ```ts\n * http.on(\"upgrade\", async (req, socket, head) => {\n * try {\n * await wss.upgrade({ rawRequest: req, socket, head });\n * } catch (err) {\n * console.error(\"Upgrade failed\", err);\n * socket.destroy();\n * }\n * });\n * ```\n */\n upgrade(req: UpgradeRequest, options?: UpgradeOptions<Data>): Promise<ServerWebSocket<Data>> {\n return new Promise((resolve, reject) => {\n try {\n this.wss.handleUpgrade(req.rawRequest, req.socket, req.head as Buffer, (socket) => {\n const data = (options?.data ?? ({} as Data)) as Data;\n const wrapped = this.wrap(socket, data, req.rawRequest);\n this.bind(socket, wrapped);\n resolve(wrapped);\n });\n } catch (err) {\n reject(err);\n }\n });\n }\n\n /**\n * Publish a message to all sockets currently subscribed to `topic`.\n *\n * @param topic - Topic name\n * @param message - Payload to broadcast\n * @param _compress - Reserved for future use; ignored under the `ws` adapter\n * @returns Number of sockets that received the message.\n *\n * @example\n * ```ts\n * wss.publish(\"room\", JSON.stringify({ kind: \"ping\" }));\n * ```\n */\n publish(topic: string, message: WSMessage, _compress?: boolean): number {\n const set = this.topics.get(topic);\n if (!set || set.size === 0) {\n return 0;\n }\n const payload = toSendable(message);\n let count = 0;\n for (const socket of set) {\n if (socket.readyState === 1) {\n socket.send(payload);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Close the server and optionally terminate all live sockets.\n *\n * @param closeActiveConnections - When `true`, calls `socket.terminate()`\n * on every live connection before shutting down.\n */\n close(closeActiveConnections = false): void {\n if (closeActiveConnections) {\n for (const socket of this.allSockets) {\n try {\n socket.terminate();\n } catch {\n /* ignore */\n }\n }\n this.allSockets.clear();\n }\n this.wss.close();\n }\n\n /**\n * Attach the per-socket lifecycle listeners (`message`, `close`, `error`).\n *\n * @internal\n */\n private bind(socket: NodeWebSocket, wrapped: ServerWebSocket<Data>): void {\n this.allSockets.add(socket);\n\n if (this.handlers.open) {\n Promise.resolve(this.handlers.open(wrapped)).catch((err) =>\n console.error(\"[ws] open handler error:\", err),\n );\n }\n\n socket.on(\"message\", (raw, isBinary) => {\n const message: WSMessage = isBinary\n ? raw instanceof ArrayBuffer\n ? new Uint8Array(raw)\n : Array.isArray(raw)\n ? Buffer.concat(raw)\n : (raw as Buffer)\n : raw.toString();\n Promise.resolve(this.handlers.message(wrapped, message)).catch((err) =>\n console.error(\"[ws] message handler error:\", err),\n );\n });\n\n socket.on(\"close\", (code, reason) => {\n const owned = this.socketTopics.get(socket);\n if (owned) {\n for (const topic of owned) {\n this.topics.get(topic)?.delete(socket);\n }\n this.socketTopics.delete(socket);\n }\n this.allSockets.delete(socket);\n if (this.handlers.close) {\n Promise.resolve(this.handlers.close(wrapped, code, reason?.toString() ?? \"\")).catch((err) =>\n console.error(\"[ws] close handler error:\", err),\n );\n }\n });\n\n socket.on(\"error\", (err) => {\n if (this.handlers.error) {\n Promise.resolve(this.handlers.error(wrapped, err)).catch((e) =>\n console.error(\"[ws] error handler error:\", e),\n );\n }\n });\n }\n\n /**\n * Build the {@link ServerWebSocket} wrapper exposed to user handlers.\n *\n * @internal\n */\n private wrap(socket: NodeWebSocket, data: Data, rawReq: any): ServerWebSocket<Data> {\n const remoteAddress: string =\n (rawReq?.socket?.remoteAddress as string) ||\n (rawReq?.headers?.[\"x-forwarded-for\"] as string) ||\n \"\";\n\n const subscribe = (topic: string) => {\n let set = this.topics.get(topic);\n if (!set) {\n set = new Set();\n this.topics.set(topic, set);\n }\n set.add(socket);\n let owned = this.socketTopics.get(socket);\n if (!owned) {\n owned = new Set();\n this.socketTopics.set(socket, owned);\n }\n owned.add(topic);\n };\n\n const unsubscribe = (topic: string) => {\n this.topics.get(topic)?.delete(socket);\n this.socketTopics.get(socket)?.delete(topic);\n };\n\n const publishToPeers = (topic: string, message: WSMessage) => {\n const set = this.topics.get(topic);\n if (!set) {\n return;\n }\n const payload = toSendable(message);\n for (const peer of set) {\n if (peer !== socket && peer.readyState === 1) {\n peer.send(payload);\n }\n }\n };\n\n const wrapper: ServerWebSocket<Data> = {\n data,\n get readyState() {\n return socket.readyState;\n },\n remoteAddress,\n send: (message, _compress) => {\n const payload = toSendable(message);\n socket.send(payload);\n return typeof payload === \"string\"\n ? Buffer.byteLength(payload)\n : (payload as Buffer | Uint8Array).byteLength;\n },\n close: (code, reason) => {\n socket.close(code, reason);\n },\n subscribe,\n unsubscribe,\n publish: publishToPeers,\n isSubscribed: (topic) => !!this.socketTopics.get(socket)?.has(topic),\n cork: (cb) => cb(wrapper),\n };\n\n return wrapper;\n }\n}\n\n/**\n * Coerce a {@link WSMessage} into something the `ws` package can transmit.\n *\n * @param message - User-supplied payload\n * @returns A `string`, `Buffer` or `Uint8Array` ready to be sent\n *\n * @internal\n */\nfunction toSendable(message: WSMessage): string | Buffer | Uint8Array {\n if (typeof message === \"string\") {\n return message;\n }\n if (message instanceof ArrayBuffer) {\n return Buffer.from(message);\n }\n return message;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { Duplex } from \"node:stream\";\n\nimport type {\n ServerWebSocket,\n UpgradeOptions,\n UpgradeRequest,\n WebSocketHandlers,\n WebSocketServerOptions,\n WSData,\n WSMessage,\n} from \"./types\";\n\nexport type {\n ServerWebSocket,\n UpgradeOptions,\n UpgradeRequest,\n WebSocketHandlers,\n WebSocketServerOptions,\n WSData,\n WSMessage,\n} from \"./types\";\n\n/**\n * WebSocket magic GUID defined in RFC 6455 section 4.2.2.\n *\n * @internal\n */\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_BINARY = 0x2;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\n/**\n * Compute the `Sec-WebSocket-Accept` response header per RFC 6455 §4.2.2.\n *\n * @param clientKey - Value of the `Sec-WebSocket-Key` request header\n * @returns Base-64 encoded SHA-1 digest\n *\n * @internal\n */\nfunction computeAccept(clientKey: string): string {\n return createHash(\"sha1\")\n .update(clientKey + WS_GUID)\n .digest(\"base64\");\n}\n\n/**\n * Write a single unmasked (server-to-client) WebSocket data frame to a raw\n * duplex socket per RFC 6455 §5.2.\n *\n * @param socket - Underlying duplex transport\n * @param opcode - WebSocket opcode (text, binary, close, ping, pong …)\n * @param payload - Payload bytes to encapsulate\n *\n * @internal\n */\nfunction writeFrame(socket: Duplex, opcode: number, payload: Buffer): void {\n const len = payload.length;\n let header: Buffer;\n\n if (len < 126) {\n header = Buffer.allocUnsafe(2);\n header[0] = 0x80 | opcode;\n header[1] = len;\n } else if (len < 0x10000) {\n header = Buffer.allocUnsafe(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.allocUnsafe(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeUInt32BE(0, 2);\n header.writeUInt32BE(len >>> 0, 6);\n }\n\n if (payload.length === 0) {\n socket.write(header);\n } else {\n socket.write(Buffer.concat([header, payload]));\n }\n}\n\n/**\n * Incremental streaming RFC 6455 frame parser for masked client→server frames.\n * Handles fragmentation, ping/pong, and close frames internally.\n *\n * @internal\n */\nclass FrameParser {\n private buf: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n private fragments: Buffer[] = [];\n private fragmentOpcode = 0;\n private readonly maxPayload: number;\n\n /** Callback fired when a complete data frame is assembled. */\n onMessage?: (data: Buffer | string, isBinary: boolean) => void;\n /** Callback fired when a close frame is received. */\n onClose?: (code: number, reason: string) => void;\n /** Callback fired when a ping frame is received. */\n onPing?: (data: Buffer) => void;\n /** Callback fired on parse errors or protocol violations. */\n onError?: (err: Error) => void;\n\n /**\n * @param maxPayload - Maximum allowed frame payload in bytes (default 100 MiB)\n */\n constructor(maxPayload = 100 * 1024 * 1024) {\n this.maxPayload = maxPayload;\n }\n\n /**\n * Feed a new chunk of raw socket data into the parser.\n *\n * @param chunk - Incoming bytes from the transport\n */\n push(chunk: Buffer): void {\n this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);\n this.drain();\n }\n\n /**\n * Consume as many complete frames as possible from the internal buffer.\n *\n * @internal\n */\n private drain(): void {\n for (;;) {\n if (this.buf.length < 2) {\n return;\n }\n\n const b0 = this.buf[0]!;\n const b1 = this.buf[1]!;\n const fin = (b0 & 0x80) !== 0;\n const rsv = b0 & 0x70;\n const opcode = b0 & 0x0f;\n const masked = (b1 & 0x80) !== 0;\n let payloadLen = b1 & 0x7f;\n let offset = 2;\n\n if (rsv !== 0) {\n this.onError?.(new Error(\"WebSocket: RSV bits must be 0 (no extensions negotiated)\"));\n return;\n }\n\n if (payloadLen === 126) {\n if (this.buf.length < 4) {\n return;\n }\n payloadLen = this.buf.readUInt16BE(2);\n offset = 4;\n } else if (payloadLen === 127) {\n if (this.buf.length < 10) {\n return;\n }\n const hi = this.buf.readUInt32BE(2);\n const lo = this.buf.readUInt32BE(6);\n payloadLen = hi * 0x1_0000_0000 + lo;\n offset = 10;\n }\n\n if (payloadLen > this.maxPayload) {\n this.onError?.(\n new Error(\n `WebSocket: payload length ${payloadLen} exceeds maxPayload ${this.maxPayload}`,\n ),\n );\n return;\n }\n\n const maskLen = masked ? 4 : 0;\n const frameEnd = offset + maskLen + payloadLen;\n if (this.buf.length < frameEnd) {\n return;\n }\n\n let payload: Buffer;\n if (masked) {\n const mask = this.buf.subarray(offset, offset + 4);\n payload = Buffer.allocUnsafe(payloadLen);\n for (let i = 0; i < payloadLen; i++) {\n payload[i] = this.buf[offset + 4 + i]! ^ mask[i & 3]!;\n }\n } else {\n payload = Buffer.from(this.buf.subarray(offset, frameEnd));\n }\n\n this.buf = this.buf.subarray(frameEnd);\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n /**\n * Route a parsed frame to the appropriate callback based on opcode.\n *\n * @param fin - Whether this is the final fragment\n * @param opcode - WebSocket frame opcode\n * @param payload - Unmasked payload bytes\n *\n * @internal\n */\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n switch (opcode) {\n case OP_PING:\n this.onPing?.(payload);\n return;\n\n case OP_PONG:\n return;\n\n case OP_CLOSE: {\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1000;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf8\") : \"\";\n this.onClose?.(code, reason);\n return;\n }\n\n case OP_CONTINUATION:\n case OP_TEXT:\n case OP_BINARY:\n if (opcode !== OP_CONTINUATION) {\n this.fragmentOpcode = opcode;\n }\n this.fragments.push(payload);\n if (fin) {\n const full = Buffer.concat(this.fragments);\n this.fragments = [];\n const isBinary = this.fragmentOpcode === OP_BINARY;\n this.onMessage?.(isBinary ? full : full.toString(\"utf8\"), isBinary);\n }\n return;\n\n default:\n this.onError?.(new Error(`WebSocket: unknown opcode 0x${opcode.toString(16)}`));\n }\n }\n}\n\n/**\n * Thin wrapper around a raw `Duplex` that exposes a minimal frame-aware\n * WebSocket interface used internally by {@link WebSocketServer}.\n *\n * @internal\n */\nclass NativeSocket {\n /** Underlying duplex transport stream. */\n readonly duplex: Duplex;\n /** Incremental frame parser attached to this transport. */\n readonly parser: FrameParser;\n\n /** WHATWG-compatible ready-state: `1` open · `2` closing · `3` closed. */\n readyState: 1 | 2 | 3 = 1;\n\n /**\n * @param duplex - Raw duplex transport\n * @param maxPayload - Maximum allowed payload in bytes\n */\n constructor(duplex: Duplex, maxPayload?: number) {\n this.duplex = duplex;\n this.parser = new FrameParser(maxPayload);\n }\n\n /**\n * Send a data frame to the peer.\n *\n * @param data - Payload to transmit\n */\n send(data: string | Buffer | Uint8Array): void {\n if (this.readyState !== 1) {\n return;\n }\n const isBuf = Buffer.isBuffer(data);\n const buf =\n typeof data === \"string\"\n ? Buffer.from(data, \"utf8\")\n : isBuf\n ? data\n : Buffer.from(data as Uint8Array);\n writeFrame(this.duplex, typeof data === \"string\" ? OP_TEXT : OP_BINARY, buf);\n }\n\n /**\n * Initiate the close handshake.\n *\n * @param code - Close status code (default `1000`)\n * @param reason - Optional UTF-8 reason phrase\n */\n close(code = 1000, reason = \"\"): void {\n if (this.readyState !== 1) {\n return;\n }\n this.readyState = 2;\n const reasonBuf = Buffer.from(reason, \"utf8\");\n const payload = Buffer.allocUnsafe(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n writeFrame(this.duplex, OP_CLOSE, payload);\n }\n\n /**\n * Hard-terminate the underlying transport without a close handshake.\n */\n terminate(): void {\n this.readyState = 3;\n this.duplex.destroy();\n }\n}\n\n/**\n * Runtime-agnostic WebSocket server built entirely on Node.js built-ins\n * (`node:crypto`, `node:stream`) — no third-party dependencies.\n *\n * @remarks\n * The class does **not** create or own an HTTP server. Callers feed it raw\n * upgrade tuples coming from any HTTP runtime (Node.js `http`, Bun, Deno,\n * Hono, Fastify's upgrade hook, etc.).\n *\n * Topic-based pub/sub is implemented in user-space, matching the shape of\n * `Bun.ServerWebSocket` so the same handler code runs on every runtime.\n *\n * @typeParam Data - Shape of the user-attached `data` field\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { WebSocketServer } from \"@hedystia/ws/server\";\n *\n * const wss = new WebSocketServer({\n * open: (ws) => ws.send(\"welcome\"),\n * message: (ws, msg) => ws.publish(\"room\", msg),\n * close: (ws, code) => console.log(\"closed\", code),\n * });\n *\n * const http = createServer((_req, res) => res.end(\"ok\"));\n * http.on(\"upgrade\", (req, socket, head) => {\n * wss.upgrade({ rawRequest: req, socket, head }, { data: { user: \"anon\" } });\n * });\n * http.listen(3000);\n * ```\n */\nexport class WebSocketServer<Data extends WSData = WSData> {\n private readonly handlers: WebSocketHandlers<Data>;\n private readonly maxPayload: number | undefined;\n private readonly resolveData: ((req: any) => Record<string, any>) | undefined;\n private readonly topics = new Map<string, Set<any>>();\n private readonly socketTopics = new WeakMap<object, Set<string>>();\n private readonly allSockets = new Set<any>();\n\n /**\n * Build a new WebSocket server.\n *\n * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})\n * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})\n *\n * @example\n * ```ts\n * const wss = new WebSocketServer(\n * { message: (ws, msg) => ws.send(msg) },\n * { maxPayload: 1024 * 1024 },\n * );\n * ```\n */\n constructor(handlers: WebSocketHandlers<Data>, options: WebSocketServerOptions = {}) {\n this.handlers = handlers;\n this.maxPayload = options.maxPayload;\n this.resolveData = options.resolveData;\n }\n\n /**\n * Upgrade a raw HTTP upgrade tuple to a WebSocket connection.\n *\n * @remarks\n * Performs the RFC 6455 handshake synchronously on the duplex socket,\n * then wires up frame parsing and lifecycle handlers. The returned\n * promise resolves to the {@link ServerWebSocket} wrapper immediately\n * after the handshake bytes are written.\n *\n * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event\n * @param options - Optional initial `data` for the new connection\n * @returns Promise that resolves with the established socket wrapper\n *\n * @throws {Error} When `Sec-WebSocket-Key` is absent from the request headers\n *\n * @example\n * ```ts\n * http.on(\"upgrade\", async (req, socket, head) => {\n * try {\n * await wss.upgrade({ rawRequest: req, socket, head });\n * } catch (err) {\n * console.error(\"Upgrade failed\", err);\n * socket.destroy();\n * }\n * });\n * ```\n */\n upgrade(req: UpgradeRequest, options?: UpgradeOptions<Data>): Promise<ServerWebSocket<Data>> {\n return new Promise((resolve, reject) => {\n try {\n const rawReq = req.rawRequest;\n const duplex = req.socket as Duplex;\n\n const clientKey = rawReq.headers?.[\"sec-websocket-key\"] as string | undefined;\n if (!clientKey) {\n duplex.destroy();\n return reject(new Error(\"WebSocket upgrade: missing Sec-WebSocket-Key header\"));\n }\n\n const accept = computeAccept(clientKey);\n const rawProtocol = rawReq.headers?.[\"sec-websocket-protocol\"] as string | undefined;\n const protocol = rawProtocol?.split(\",\")[0]?.trim();\n\n let response =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${accept}\\r\\n`;\n\n if (protocol) {\n response += `Sec-WebSocket-Protocol: ${protocol}\\r\\n`;\n }\n response += \"\\r\\n\";\n duplex.write(response);\n\n const native = new NativeSocket(duplex, this.maxPayload);\n const data = (options?.data ?? {}) as Data;\n const wrapped = this.wrap(native, data, rawReq);\n this.bind(native, wrapped, req.head);\n\n resolve(wrapped);\n } catch (err) {\n reject(err);\n }\n });\n }\n\n /**\n * Publish a message to all sockets currently subscribed to `topic`.\n *\n * @param topic - Topic name\n * @param message - Payload to broadcast\n * @param _compress - Reserved for future use\n * @returns Number of sockets that received the message\n *\n * @example\n * ```ts\n * wss.publish(\"room\", JSON.stringify({ kind: \"ping\" }));\n * ```\n */\n publish(topic: string, message: WSMessage, _compress?: boolean): number {\n const set = this.topics.get(topic);\n if (!set || set.size === 0) {\n return 0;\n }\n const payload = toSendable(message);\n let count = 0;\n for (const native of set) {\n if (native.readyState === 1) {\n native.send(payload);\n count++;\n }\n }\n return count;\n }\n\n /**\n * Close the server and optionally terminate all live sockets.\n *\n * @param closeActiveConnections - When `true`, immediately terminates\n * every live connection before clearing internal state\n */\n close(closeActiveConnections = false): void {\n if (closeActiveConnections) {\n for (const native of this.allSockets) {\n try {\n if (typeof (native as any).terminate === \"function\") {\n (native as any).terminate();\n } else if (typeof (native as any).close === \"function\") {\n (native as any).close(1001, \"Server shutdown\");\n }\n } catch {\n /* ignore */\n }\n }\n this.allSockets.clear();\n }\n this.topics.clear();\n }\n\n /**\n * Wire up the duplex transport listeners (`data`, `close`, `error`) and\n * invoke the user's `open` handler.\n *\n * @param native - Wrapped transport socket\n * @param wrapped - Public {@link ServerWebSocket} wrapper\n * @param head - Buffered bytes captured by the HTTP parser (re-fed into the parser)\n *\n * @internal\n */\n private bind(\n native: NativeSocket,\n wrapped: ServerWebSocket<Data>,\n head: Buffer | Uint8Array,\n ): void {\n this.allSockets.add(native);\n\n if (head && head.length > 0) {\n native.parser.push(Buffer.isBuffer(head) ? head : Buffer.from(head));\n }\n\n native.parser.onMessage = (raw, isBinary) => {\n const message: WSMessage = isBinary\n ? raw instanceof Buffer\n ? raw\n : Buffer.from(raw as Uint8Array)\n : (raw as string);\n Promise.resolve(this.handlers.message(wrapped, message)).catch((err) =>\n console.error(\"[ws] message handler error:\", err),\n );\n };\n\n native.parser.onClose = (code, reason) => {\n if (native.readyState === 1) {\n native.close(code, reason);\n }\n this.cleanup(native);\n if (this.handlers.close) {\n Promise.resolve(this.handlers.close(wrapped, code, reason)).catch((err) =>\n console.error(\"[ws] close handler error:\", err),\n );\n }\n };\n\n native.parser.onPing = (data) => {\n if (native.readyState === 1) {\n writeFrame(native.duplex, OP_PONG, data);\n }\n };\n\n native.parser.onError = (err) => {\n native.terminate();\n this.cleanup(native);\n if (this.handlers.error) {\n Promise.resolve(this.handlers.error(wrapped, err)).catch((e) =>\n console.error(\"[ws] error handler error:\", e),\n );\n }\n };\n\n native.duplex.on(\"data\", (chunk: Buffer) => {\n try {\n native.parser.push(chunk);\n } catch (err) {\n console.error(\"[ws] frame parsing error:\", err);\n native.terminate();\n this.cleanup(native);\n }\n });\n\n native.duplex.on(\"close\", () => {\n if (native.readyState !== 3) {\n native.readyState = 3;\n this.cleanup(native);\n if (this.handlers.close) {\n Promise.resolve(this.handlers.close(wrapped, 1006, \"\")).catch((err) =>\n console.error(\"[ws] close handler error:\", err),\n );\n }\n }\n });\n\n native.duplex.on(\"error\", (err: Error) => {\n native.readyState = 3;\n this.cleanup(native);\n if (this.handlers.error) {\n Promise.resolve(this.handlers.error(wrapped, err)).catch((e) =>\n console.error(\"[ws] error handler error:\", e),\n );\n }\n });\n\n if (this.handlers.open) {\n Promise.resolve(this.handlers.open(wrapped)).catch((err) =>\n console.error(\"[ws] open handler error:\", err),\n );\n }\n }\n\n /**\n * Remove a socket from every topic it had joined and from the global\n * tracking set.\n *\n * @param native - Socket being cleaned up\n *\n * @internal\n */\n private cleanup(native: any): void {\n const owned = this.socketTopics.get(native);\n if (owned) {\n for (const topic of owned) {\n this.topics.get(topic)?.delete(native);\n }\n this.socketTopics.delete(native);\n }\n this.allSockets.delete(native);\n }\n\n /**\n * Build the {@link ServerWebSocket} wrapper exposed to user handlers,\n * embedding topic-based pub/sub backed by the server's internal maps.\n *\n * @param native - Wrapped transport socket\n * @param data - User-supplied `data` payload attached on upgrade\n * @param rawReq - Raw incoming message used to extract the remote address\n * @returns The fully-featured public wrapper\n *\n * @internal\n */\n private wrap(native: NativeSocket, data: Data, rawReq: any): ServerWebSocket<Data> {\n const remoteAddress: string =\n (rawReq?.socket?.remoteAddress as string) ||\n (rawReq?.headers?.[\"x-forwarded-for\"] as string) ||\n \"\";\n\n const subscribe = (topic: string): void => {\n let set = this.topics.get(topic);\n if (!set) {\n set = new Set();\n this.topics.set(topic, set);\n }\n set.add(native);\n let owned = this.socketTopics.get(native);\n if (!owned) {\n owned = new Set();\n this.socketTopics.set(native, owned);\n }\n owned.add(topic);\n };\n\n const unsubscribe = (topic: string): void => {\n this.topics.get(topic)?.delete(native);\n this.socketTopics.get(native)?.delete(topic);\n };\n\n const publishToPeers = (topic: string, message: WSMessage): void => {\n const set = this.topics.get(topic);\n if (!set) {\n return;\n }\n const payload = toSendable(message);\n for (const peer of set) {\n if (peer !== native && peer.readyState === 1) {\n peer.send(payload);\n }\n }\n };\n\n const wrapper: ServerWebSocket<Data> = {\n data,\n get readyState() {\n return native.readyState;\n },\n remoteAddress,\n send: (message, _compress) => {\n const payload = toSendable(message);\n native.send(payload);\n return typeof payload === \"string\"\n ? Buffer.byteLength(payload as string)\n : (payload as Buffer | Uint8Array).byteLength;\n },\n close: (code, reason) => {\n native.close(code, reason);\n },\n subscribe,\n unsubscribe,\n publish: publishToPeers,\n isSubscribed: (topic) => !!this.socketTopics.get(native)?.has(topic),\n cork: (cb) => cb(wrapper),\n };\n\n return wrapper;\n }\n}\n\n/**\n * Return value of {@link serve}.\n */\nexport interface ServeInfo {\n /** Port the HTTP server is listening on. */\n port: number;\n /** Hostname the HTTP server bound to. */\n hostname: string;\n /** Full URL of the listening server (uses `http://` scheme). */\n url: URL;\n /**\n * Publish a message to all sockets subscribed to `topic`.\n *\n * @param topic - Topic name\n * @param message - Payload to broadcast\n * @param compress - Whether to compress (honoured on Bun, ignored on Node)\n * @returns Number of sockets that received the message\n */\n publish: (topic: string, message: WSMessage, compress?: boolean) => number;\n /**\n * Stop the server and optionally close active connections.\n *\n * @param closeActiveConnections - When `true`, terminates all live sockets\n */\n stop: (closeActiveConnections?: boolean) => void | Promise<void>;\n}\n\n/**\n * Start a standalone WebSocket server on the given port.\n *\n * @remarks\n * Auto-detects the runtime and uses the native implementation:\n * - **Bun:** delegates to `Bun.serve()` with native WebSocket support\n * - **Node/Deno:** creates a `node:http` server with the built-in\n * {@link WebSocketServer} upgrade handler\n *\n * @typeParam Data - Shape of the user-attached `data` field\n *\n * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})\n * @param options - Server options including `port` and `hostname`\n * @returns A promise resolving to {@link ServeInfo}\n *\n * @example\n * ```ts\n * import { serve } from \"@hedystia/ws\";\n *\n * const server = await serve({\n * open: (ws) => ws.subscribe(\"global\"),\n * message: (ws, msg) => ws.publish(\"global\", msg),\n * });\n *\n * console.log(`Listening on ${server.url}`);\n * ```\n */\nexport async function serve<Data extends WSData = WSData>(\n handlers: WebSocketHandlers<Data>,\n options?: WebSocketServerOptions & { port?: number; hostname?: string },\n): Promise<ServeInfo> {\n const { detectRuntime } = await import(\"./runtime\");\n const runtime = detectRuntime();\n\n if (runtime === \"bun\") {\n return serveBun(handlers, options);\n }\n return serveNode(handlers, options);\n}\n\n/**\n * Start a WebSocket server using Bun's native `Bun.serve()`.\n *\n * @typeParam Data - Shape of the user-attached `data` field\n * @param handlers - Lifecycle handlers\n * @param options - Server options including `port` and `hostname`\n * @returns A promise resolving to {@link ServeInfo}\n *\n * @internal\n */\nasync function serveBun<Data extends WSData = WSData>(\n handlers: WebSocketHandlers<Data>,\n options?: WebSocketServerOptions & { port?: number; hostname?: string },\n): Promise<ServeInfo> {\n const topics = new Map<string, Set<ServerWebSocket<Data>>>();\n const socketTopics = new WeakMap<ServerWebSocket<Data>, Set<string>>();\n const allSockets = new Set<ServerWebSocket<Data>>();\n const nativeToWrapped = new WeakMap<any, ServerWebSocket<Data>>();\n\n function cleanup(ws: ServerWebSocket<Data>): void {\n const owned = socketTopics.get(ws);\n if (owned) {\n for (const topic of owned) {\n topics.get(topic)?.delete(ws);\n }\n socketTopics.delete(ws);\n }\n allSockets.delete(ws);\n }\n\n const server = (globalThis as any).Bun.serve({\n port: options?.port ?? 0,\n hostname: options?.hostname ?? \"0.0.0.0\",\n fetch: (req: Request) => {\n if (req.headers.get(\"upgrade\") === \"websocket\") {\n const data = (options?.resolveData ? options.resolveData(req) : {}) as Data;\n const ok = server.upgrade(req, { data });\n return ok ? undefined : new Response(\"upgrade failed\", { status: 400 });\n }\n return new Response(\"Not found\", { status: 404 });\n },\n websocket: {\n open: (ws: any) => {\n const data = ws.data ?? ({} as Data);\n const wrapped: ServerWebSocket<Data> = createBunWrapper(ws, data, topics, socketTopics);\n nativeToWrapped.set(ws, wrapped);\n allSockets.add(wrapped);\n if (handlers.open) {\n Promise.resolve(handlers.open(wrapped)).catch((err) =>\n console.error(\"[ws] open handler error:\", err),\n );\n }\n },\n message: (ws: any, msg: any) => {\n const wrapped = nativeToWrapped.get(ws);\n if (wrapped) {\n Promise.resolve(handlers.message(wrapped, msg)).catch((err) =>\n console.error(\"[ws] message handler error:\", err),\n );\n }\n },\n close: (ws: any, code: number, reason: string) => {\n const wrapped = nativeToWrapped.get(ws);\n if (wrapped) {\n cleanup(wrapped);\n if (handlers.close) {\n Promise.resolve(handlers.close(wrapped, code, reason)).catch((err) =>\n console.error(\"[ws] close handler error:\", err),\n );\n }\n }\n },\n drain: handlers.drain\n ? (ws: any) => {\n const wrapped = nativeToWrapped.get(ws);\n if (wrapped && handlers.drain) {\n Promise.resolve(handlers.drain(wrapped)).catch((err) =>\n console.error(\"[ws] drain handler error:\", err),\n );\n }\n }\n : undefined,\n },\n });\n\n return {\n port: server.port,\n hostname: server.hostname,\n url: new URL(`http://${server.hostname}:${server.port}/`),\n publish: (topic, message, _compress) => {\n const set = topics.get(topic);\n if (!set || set.size === 0) {\n return 0;\n }\n const payload = toSendable(message);\n let count = 0;\n for (const ws of set) {\n if (ws.readyState === 1) {\n ws.send(payload);\n count++;\n }\n }\n return count;\n },\n stop: (closeActiveConnections) => {\n if (closeActiveConnections) {\n for (const ws of allSockets) {\n try {\n ws.close(1001, \"Server shutdown\");\n } catch {\n /* ignore */\n }\n }\n allSockets.clear();\n }\n server.stop(closeActiveConnections);\n },\n };\n}\n\n/**\n * Wrap a native Bun WebSocket into a {@link ServerWebSocket} compatible with\n * the public handler interface.\n *\n * @typeParam Data - Shape of the user-attached `data` field\n * @param ws - Raw Bun WebSocket\n * @param data - User-supplied data payload\n * @param topics - Global topic → socket set map\n * @param socketTopics - Reverse lookup for per-socket topic membership\n * @returns A {@link ServerWebSocket} wrapper\n *\n * @internal\n */\nfunction createBunWrapper<Data extends WSData = WSData>(\n ws: any,\n data: Data,\n topics: Map<string, Set<ServerWebSocket<Data>>>,\n socketTopics: WeakMap<ServerWebSocket<Data>, Set<string>>,\n): ServerWebSocket<Data> {\n const wrapper: ServerWebSocket<Data> = {\n data,\n get readyState() {\n return ws.readyState;\n },\n get remoteAddress() {\n return ws.remoteAddress || \"\";\n },\n send: (message, _compress) => {\n ws.send(message);\n return typeof message === \"string\"\n ? Buffer.byteLength(message)\n : (message as ArrayBuffer | Uint8Array).byteLength;\n },\n close: (code, reason) => ws.close(code, reason),\n subscribe: (topic: string) => {\n let set = topics.get(topic);\n if (!set) {\n set = new Set();\n topics.set(topic, set);\n }\n set.add(wrapper);\n let owned = socketTopics.get(wrapper);\n if (!owned) {\n owned = new Set();\n socketTopics.set(wrapper, owned);\n }\n owned.add(topic);\n },\n unsubscribe: (topic: string) => {\n topics.get(topic)?.delete(wrapper);\n socketTopics.get(wrapper)?.delete(topic);\n },\n publish: (topic: string, message: WSMessage, _compress?: boolean) => {\n const set = topics.get(topic);\n if (!set) {\n return;\n }\n const payload = toSendable(message);\n for (const peer of set) {\n if (peer !== wrapper && peer.readyState === 1) {\n peer.send(payload);\n }\n }\n },\n isSubscribed: (topic: string) =>\n !!(socketTopics.get(wrapper) as Set<string> | undefined)?.has(topic),\n cork: (cb: (ws: ServerWebSocket<Data>) => void) => cb(wrapper),\n };\n return wrapper;\n}\n\n/**\n * Start a WebSocket server using Node.js `node:http` + the built-in\n * {@link WebSocketServer} upgrade handler.\n *\n * @typeParam Data - Shape of the user-attached `data` field\n * @param handlers - Lifecycle handlers\n * @param options - Server options including `port` and `hostname`\n * @returns A promise resolving to {@link ServeInfo}\n *\n * @internal\n */\nasync function serveNode<Data extends WSData = WSData>(\n handlers: WebSocketHandlers<Data>,\n options?: WebSocketServerOptions & { port?: number; hostname?: string },\n): Promise<ServeInfo> {\n const { createServer: createHttpServer } = await import(\"node:http\");\n const wss = new WebSocketServer<Data>(handlers, options);\n\n const httpServer = createHttpServer((_req: any, res: any) => {\n res.writeHead(404);\n res.end(\"Not found\");\n });\n\n httpServer.on(\"upgrade\", (req: any, socket: any, head: any) => {\n const data = options?.resolveData ? options.resolveData(req) : undefined;\n wss\n .upgrade({ rawRequest: req, socket, head }, data ? { data: data as Data } : undefined)\n .catch(() => socket.destroy());\n });\n\n await new Promise<void>((resolve) =>\n httpServer.listen(options?.port ?? 0, options?.hostname ?? \"0.0.0.0\", resolve),\n );\n\n const addr = httpServer.address() as any;\n const port = addr?.port ?? 0;\n\n return {\n port,\n hostname: options?.hostname ?? \"0.0.0.0\",\n url: new URL(`http://${options?.hostname ?? \"0.0.0.0\"}:${port}/`),\n publish: (topic, message, _compress) => wss.publish(topic, message, _compress),\n stop: (closeActiveConnections) => {\n wss.close(closeActiveConnections);\n httpServer.close();\n },\n };\n}\n\n/**\n * Coerce a {@link WSMessage} into a form that {@link NativeSocket.send} accepts.\n *\n * @param message - User-supplied payload\n * @returns A `string`, `Buffer` or `Uint8Array` ready to be framed\n *\n * @internal\n */\nfunction toSendable(message: WSMessage): string | Buffer | Uint8Array {\n if (typeof message === \"string\") {\n return message;\n }\n if (message instanceof ArrayBuffer) {\n return Buffer.from(message);\n }\n return message;\n}\n"],"mappings":"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"}
package/dist/types.d.cts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @remarks
6
6
  * Matches the WHATWG `WebSocket.send` signature plus the `Uint8Array`
7
- * convenience accepted by Bun and the `ws` package.
7
+ * convenience accepted by Bun and the native Node.js implementation.
8
8
  */
9
9
  type WSMessage = string | ArrayBuffer | Uint8Array;
10
10
  /**
@@ -20,7 +20,7 @@ type WSData = Record<string, any>;
20
20
  *
21
21
  * @remarks
22
22
  * The interface intentionally mirrors `Bun.ServerWebSocket` so that the
23
- * same handler code works on Bun (native) and on Node.js (via
23
+ * same handler code works on Bun (native) and on Node.js / Deno (via
24
24
  * {@link WebSocketServer}). Topic-based pub/sub is implemented in
25
25
  * user-space when running outside Bun.
26
26
  *
@@ -29,7 +29,7 @@ type WSData = Record<string, any>;
29
29
  * @example
30
30
  * ```ts
31
31
  * const handlers: WebSocketHandlers<{ user: string }> = {
32
- * open: (ws) => ws.subscribe(`user:${ws.data.user}`),
32
+ * open: (ws) => ws.subscribe(`user:${ws.data.user}`),
33
33
  * message: (ws, msg) => ws.publish(`user:${ws.data.user}`, msg),
34
34
  * };
35
35
  * ```
@@ -44,7 +44,7 @@ interface ServerWebSocket<Data extends WSData = WSData> {
44
44
  /**
45
45
  * Send a message to this socket only.
46
46
  *
47
- * @param message - Payload to send
47
+ * @param message - Payload to send
48
48
  * @param compress - Whether to compress (honoured on Bun, ignored on Node)
49
49
  * @returns Number of bytes written (best-effort on Node)
50
50
  */
@@ -52,8 +52,8 @@ interface ServerWebSocket<Data extends WSData = WSData> {
52
52
  /**
53
53
  * Close the connection.
54
54
  *
55
- * @param code - Close code (defaults to 1000)
56
- * @param reason - Optional human-readable reason
55
+ * @param code - Close code (defaults to `1000`)
56
+ * @param reason - Optional human-readable reason phrase
57
57
  */
58
58
  close(code?: number, reason?: string): void;
59
59
  /**
@@ -76,8 +76,8 @@ interface ServerWebSocket<Data extends WSData = WSData> {
76
76
  * @remarks
77
77
  * The sender is excluded by default — matching Bun's default behaviour.
78
78
  *
79
- * @param topic - Topic name
80
- * @param message - Payload to broadcast
79
+ * @param topic - Topic name
80
+ * @param message - Payload to broadcast
81
81
  * @param compress - Whether to compress (honoured on Bun, ignored on Node)
82
82
  */
83
83
  publish(topic: string, message: WSMessage, compress?: boolean): void;
@@ -103,10 +103,9 @@ interface ServerWebSocket<Data extends WSData = WSData> {
103
103
  * Bun-style compression dictionary identifier.
104
104
  *
105
105
  * @remarks
106
- * Used by Bun's `perMessageDeflate` configuration. The `@hedystia/ws`
107
- * server forwards the value to the underlying implementation, which only
108
- * Bun interprets natively; Node falls back to defaults when the value is
109
- * not a recognised `ws` shape.
106
+ * Used by Bun's `perMessageDeflate` configuration. Passed through verbatim
107
+ * to Bun when running natively; has no effect on the pure-Node
108
+ * implementation shipped by `@hedystia/ws`.
110
109
  */
111
110
  type Compressor = "disable" | "shared" | "dedicated" | "3KB" | "4KB" | "8KB" | "16KB" | "32KB" | "64KB" | "128KB" | "256KB";
112
111
  /**
@@ -114,11 +113,11 @@ type Compressor = "disable" | "shared" | "dedicated" | "3KB" | "4KB" | "8KB" | "
114
113
  *
115
114
  * @remarks
116
115
  * Accepts either a boolean (`true` enables defaults, `false` disables it)
117
- * or a free-form object whose shape is forwarded verbatim to the underlying
118
- * implementation. Bun's {@link Compressor} strings (`"3KB"`, `"shared"`, …)
119
- * and the [`ws`](https://github.com/websockets/ws) package's
120
- * `PerMessageDeflateOptions` (`zlibDeflateOptions`, `threshold`, …) are
121
- * both supported by simply matching whatever the runtime expects.
116
+ * or a free-form object. Bun's {@link Compressor} strings (`"3KB"`,
117
+ * `"shared"`, …) are supported when running on Bun natively.
118
+ *
119
+ * **Note:** This option has no effect on the built-in Node.js implementation
120
+ * `@hedystia/ws` does not negotiate the `permessage-deflate` extension.
122
121
  */
123
122
  type PerMessageDeflate = boolean | (Record<string, any> & {
124
123
  compress?: boolean | Compressor;
@@ -128,10 +127,27 @@ type PerMessageDeflate = boolean | (Record<string, any> & {
128
127
  * Construction options for {@link WebSocketServer}.
129
128
  */
130
129
  interface WebSocketServerOptions {
131
- /** Maximum allowed payload in bytes. Defaults to the underlying library's default. */
130
+ /** Maximum allowed payload in bytes. Defaults to 100 MiB. */
132
131
  maxPayload?: number;
133
- /** Per-message deflate configuration. */
132
+ /**
133
+ * Per-message deflate configuration.
134
+ *
135
+ * @remarks
136
+ * Has no effect on the built-in Node.js implementation; reserved for
137
+ * future Bun-native integration.
138
+ */
134
139
  perMessageDeflate?: PerMessageDeflate;
140
+ /**
141
+ * Called for each upgrade to determine the per-connection data.
142
+ *
143
+ * @remarks
144
+ * When using {@link WebSocketServer.start}, this is the only way to
145
+ * set custom `ws.data` based on the incoming request (e.g. URL params).
146
+ * On Node.js via `upgrade()`, pass `data` directly in `UpgradeOptions`.
147
+ *
148
+ * Receives the raw HTTP request (IncomingMessage on Node, Request on Bun).
149
+ */
150
+ resolveData?: (req: any) => Record<string, any>;
135
151
  }
136
152
  /**
137
153
  * Lifecycle handlers passed to {@link WebSocketServer}.
@@ -165,9 +181,9 @@ interface ClientWebSocketOptions {
165
181
  * Custom request headers.
166
182
  *
167
183
  * @remarks
168
- * Honoured on Node via the `ws` package; runtimes that ship a WHATWG
169
- * `WebSocket` (Bun, Deno, browsers, Node ≥ 22) ignore them — matching
170
- * standard WebSocket semantics.
184
+ * Only honoured on runtimes that support them via the `WebSocket`
185
+ * constructor (e.g. Bun). WHATWG-strict environments (browsers, Deno,
186
+ * Node.js ≥ 22) ignore this field — matching standard WebSocket semantics.
171
187
  */
172
188
  headers?: Record<string, string>;
173
189
  }
@@ -175,8 +191,7 @@ interface ClientWebSocketOptions {
175
191
  * Raw upgrade tuple consumed by {@link WebSocketServer.upgrade}.
176
192
  *
177
193
  * @remarks
178
- * Mirrors what `node:http`'s `'upgrade'` event emits and what the `ws`
179
- * package's `WebSocketServer.handleUpgrade` consumes.
194
+ * Mirrors what `node:http`'s `'upgrade'` event emits.
180
195
  */
181
196
  interface UpgradeRequest {
182
197
  /** Raw `IncomingMessage`-like object exposing `headers`, `method`, `url`. */
@@ -198,8 +213,8 @@ interface UpgradeOptions<Data extends WSData = WSData> {
198
213
  * Extra response headers.
199
214
  *
200
215
  * @remarks
201
- * Forwarded to the underlying handshake when supported (Bun); ignored on
202
- * Node-backed upgrades, which control headers via the `ws` package.
216
+ * Reserved for future use; currently ignored by the native implementation.
217
+ * On Bun-native integration this would be forwarded to the handshake.
203
218
  */
204
219
  headers?: Record<string, string> | Headers;
205
220
  }
package/dist/types.d.mts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @remarks
6
6
  * Matches the WHATWG `WebSocket.send` signature plus the `Uint8Array`
7
- * convenience accepted by Bun and the `ws` package.
7
+ * convenience accepted by Bun and the native Node.js implementation.
8
8
  */
9
9
  type WSMessage = string | ArrayBuffer | Uint8Array;
10
10
  /**
@@ -20,7 +20,7 @@ type WSData = Record<string, any>;
20
20
  *
21
21
  * @remarks
22
22
  * The interface intentionally mirrors `Bun.ServerWebSocket` so that the
23
- * same handler code works on Bun (native) and on Node.js (via
23
+ * same handler code works on Bun (native) and on Node.js / Deno (via
24
24
  * {@link WebSocketServer}). Topic-based pub/sub is implemented in
25
25
  * user-space when running outside Bun.
26
26
  *
@@ -29,7 +29,7 @@ type WSData = Record<string, any>;
29
29
  * @example
30
30
  * ```ts
31
31
  * const handlers: WebSocketHandlers<{ user: string }> = {
32
- * open: (ws) => ws.subscribe(`user:${ws.data.user}`),
32
+ * open: (ws) => ws.subscribe(`user:${ws.data.user}`),
33
33
  * message: (ws, msg) => ws.publish(`user:${ws.data.user}`, msg),
34
34
  * };
35
35
  * ```
@@ -44,7 +44,7 @@ interface ServerWebSocket<Data extends WSData = WSData> {
44
44
  /**
45
45
  * Send a message to this socket only.
46
46
  *
47
- * @param message - Payload to send
47
+ * @param message - Payload to send
48
48
  * @param compress - Whether to compress (honoured on Bun, ignored on Node)
49
49
  * @returns Number of bytes written (best-effort on Node)
50
50
  */
@@ -52,8 +52,8 @@ interface ServerWebSocket<Data extends WSData = WSData> {
52
52
  /**
53
53
  * Close the connection.
54
54
  *
55
- * @param code - Close code (defaults to 1000)
56
- * @param reason - Optional human-readable reason
55
+ * @param code - Close code (defaults to `1000`)
56
+ * @param reason - Optional human-readable reason phrase
57
57
  */
58
58
  close(code?: number, reason?: string): void;
59
59
  /**
@@ -76,8 +76,8 @@ interface ServerWebSocket<Data extends WSData = WSData> {
76
76
  * @remarks
77
77
  * The sender is excluded by default — matching Bun's default behaviour.
78
78
  *
79
- * @param topic - Topic name
80
- * @param message - Payload to broadcast
79
+ * @param topic - Topic name
80
+ * @param message - Payload to broadcast
81
81
  * @param compress - Whether to compress (honoured on Bun, ignored on Node)
82
82
  */
83
83
  publish(topic: string, message: WSMessage, compress?: boolean): void;
@@ -103,10 +103,9 @@ interface ServerWebSocket<Data extends WSData = WSData> {
103
103
  * Bun-style compression dictionary identifier.
104
104
  *
105
105
  * @remarks
106
- * Used by Bun's `perMessageDeflate` configuration. The `@hedystia/ws`
107
- * server forwards the value to the underlying implementation, which only
108
- * Bun interprets natively; Node falls back to defaults when the value is
109
- * not a recognised `ws` shape.
106
+ * Used by Bun's `perMessageDeflate` configuration. Passed through verbatim
107
+ * to Bun when running natively; has no effect on the pure-Node
108
+ * implementation shipped by `@hedystia/ws`.
110
109
  */
111
110
  type Compressor = "disable" | "shared" | "dedicated" | "3KB" | "4KB" | "8KB" | "16KB" | "32KB" | "64KB" | "128KB" | "256KB";
112
111
  /**
@@ -114,11 +113,11 @@ type Compressor = "disable" | "shared" | "dedicated" | "3KB" | "4KB" | "8KB" | "
114
113
  *
115
114
  * @remarks
116
115
  * Accepts either a boolean (`true` enables defaults, `false` disables it)
117
- * or a free-form object whose shape is forwarded verbatim to the underlying
118
- * implementation. Bun's {@link Compressor} strings (`"3KB"`, `"shared"`, …)
119
- * and the [`ws`](https://github.com/websockets/ws) package's
120
- * `PerMessageDeflateOptions` (`zlibDeflateOptions`, `threshold`, …) are
121
- * both supported by simply matching whatever the runtime expects.
116
+ * or a free-form object. Bun's {@link Compressor} strings (`"3KB"`,
117
+ * `"shared"`, …) are supported when running on Bun natively.
118
+ *
119
+ * **Note:** This option has no effect on the built-in Node.js implementation
120
+ * `@hedystia/ws` does not negotiate the `permessage-deflate` extension.
122
121
  */
123
122
  type PerMessageDeflate = boolean | (Record<string, any> & {
124
123
  compress?: boolean | Compressor;
@@ -128,10 +127,27 @@ type PerMessageDeflate = boolean | (Record<string, any> & {
128
127
  * Construction options for {@link WebSocketServer}.
129
128
  */
130
129
  interface WebSocketServerOptions {
131
- /** Maximum allowed payload in bytes. Defaults to the underlying library's default. */
130
+ /** Maximum allowed payload in bytes. Defaults to 100 MiB. */
132
131
  maxPayload?: number;
133
- /** Per-message deflate configuration. */
132
+ /**
133
+ * Per-message deflate configuration.
134
+ *
135
+ * @remarks
136
+ * Has no effect on the built-in Node.js implementation; reserved for
137
+ * future Bun-native integration.
138
+ */
134
139
  perMessageDeflate?: PerMessageDeflate;
140
+ /**
141
+ * Called for each upgrade to determine the per-connection data.
142
+ *
143
+ * @remarks
144
+ * When using {@link WebSocketServer.start}, this is the only way to
145
+ * set custom `ws.data` based on the incoming request (e.g. URL params).
146
+ * On Node.js via `upgrade()`, pass `data` directly in `UpgradeOptions`.
147
+ *
148
+ * Receives the raw HTTP request (IncomingMessage on Node, Request on Bun).
149
+ */
150
+ resolveData?: (req: any) => Record<string, any>;
135
151
  }
136
152
  /**
137
153
  * Lifecycle handlers passed to {@link WebSocketServer}.
@@ -165,9 +181,9 @@ interface ClientWebSocketOptions {
165
181
  * Custom request headers.
166
182
  *
167
183
  * @remarks
168
- * Honoured on Node via the `ws` package; runtimes that ship a WHATWG
169
- * `WebSocket` (Bun, Deno, browsers, Node ≥ 22) ignore them — matching
170
- * standard WebSocket semantics.
184
+ * Only honoured on runtimes that support them via the `WebSocket`
185
+ * constructor (e.g. Bun). WHATWG-strict environments (browsers, Deno,
186
+ * Node.js ≥ 22) ignore this field — matching standard WebSocket semantics.
171
187
  */
172
188
  headers?: Record<string, string>;
173
189
  }
@@ -175,8 +191,7 @@ interface ClientWebSocketOptions {
175
191
  * Raw upgrade tuple consumed by {@link WebSocketServer.upgrade}.
176
192
  *
177
193
  * @remarks
178
- * Mirrors what `node:http`'s `'upgrade'` event emits and what the `ws`
179
- * package's `WebSocketServer.handleUpgrade` consumes.
194
+ * Mirrors what `node:http`'s `'upgrade'` event emits.
180
195
  */
181
196
  interface UpgradeRequest {
182
197
  /** Raw `IncomingMessage`-like object exposing `headers`, `method`, `url`. */
@@ -198,8 +213,8 @@ interface UpgradeOptions<Data extends WSData = WSData> {
198
213
  * Extra response headers.
199
214
  *
200
215
  * @remarks
201
- * Forwarded to the underlying handshake when supported (Bun); ignored on
202
- * Node-backed upgrades, which control headers via the `ws` package.
216
+ * Reserved for future use; currently ignored by the native implementation.
217
+ * On Bun-native integration this would be forwarded to the handshake.
203
218
  */
204
219
  headers?: Record<string, string> | Headers;
205
220
  }
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@hedystia/ws",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "description": "Universal WebSocket primitives for Hedystia (Bun, Node.js, Deno)",
5
5
  "homepage": "https://docs.hedystia.com/plugins/websocket",
6
6
  "devDependencies": {
7
7
  "@types/bun": "^1.3.11",
8
- "@types/ws": "^8.5.13",
9
8
  "typescript": "6.0.2"
10
9
  },
11
- "dependencies": {
12
- "ws": "^8.18.0"
13
- },
14
10
  "private": false,
15
11
  "keywords": [
16
12
  "hedystia",
package/readme.md CHANGED
@@ -2,16 +2,7 @@
2
2
 
3
3
  Universal WebSocket primitives for [Hedystia](https://docs.hedystia.com).
4
4
 
5
- The package ships **no HTTP server**. It only exposes WebSocket pieces that
6
- work the same way on **Bun**, **Node.js** and **Deno**:
7
-
8
- - A runtime-aware **client constructor**.
9
- - A portable **`WebSocketServer`** that consumes raw HTTP upgrade tuples
10
- and offers topic-based pub/sub, mirroring `Bun.ServerWebSocket`.
11
- - Shared **types** and **runtime detection** helpers.
12
-
13
- The HTTP layer (Bun.serve / `node:http`) is intentionally left to the
14
- caller (`hedystia` server uses it internally).
5
+ Works on **Bun**, **Node.js** and **Deno** with a single API.
15
6
 
16
7
  ## Install
17
8
 
@@ -21,37 +12,33 @@ bun add @hedystia/ws
21
12
  npm install @hedystia/ws
22
13
  ```
23
14
 
24
- ## Client
15
+ ## Server
16
+
17
+ ### `serve()` — standalone (recommended)
18
+
19
+ Auto-detects the runtime and starts a full HTTP+WebSocket server:
25
20
 
26
21
  ```ts
27
- import { createWebSocket } from "@hedystia/ws/client";
22
+ import { serve } from "@hedystia/ws";
28
23
 
29
- const ws = createWebSocket("ws://localhost:3000", {
30
- headers: { authorization: "Bearer ..." },
24
+ const server = await serve({
25
+ open: (ws) => ws.subscribe("global"),
26
+ message: (ws, msg) => ws.publish("global", msg),
31
27
  });
32
28
 
33
- ws.onopen = () => ws.send("hi");
34
- ws.onmessage = (event) => console.log(event.data);
29
+ console.log(`Listening on ${server.url}`);
30
+ // Broadcast from anywhere
31
+ server.publish("global", "hello everyone");
32
+ await server.stop();
35
33
  ```
36
34
 
37
- `createWebSocket()` uses `globalThis.WebSocket` when available (Bun, Deno,
38
- browsers, Node 22) and falls back to the [`ws`](https://github.com/websockets/ws)
39
- package on older Node releases. Custom request headers are honoured on Node
40
- and ignored elsewhere — matching WHATWG semantics.
41
-
42
- A small ergonomic wrapper is also provided:
43
-
44
- ```ts
45
- import { WebSocketClient } from "@hedystia/ws/client";
35
+ - **Bun:** delegates to `Bun.serve()` with native WebSocket.
36
+ - **Node/Deno:** creates a `node:http` server with the built-in upgrade handler.
46
37
 
47
- const client = new WebSocketClient("ws://localhost:3000");
48
- client.onmessage = (e) => console.log(e.data);
49
- ```
50
-
51
- ## Server
38
+ ### `WebSocketServer` low-level
52
39
 
53
- The `WebSocketServer` does **not** open a port. Plug it into any HTTP
54
- runtime that exposes raw upgrade tuples (`req`, `socket`, `head`).
40
+ Does **not** open a port. Plug it into any HTTP runtime that exposes raw
41
+ upgrade tuples (`req`, `socket`, `head`).
55
42
 
56
43
  ```ts
57
44
  import { createServer } from "node:http";
@@ -74,7 +61,7 @@ http.listen(3000);
74
61
  wss.publish("room", "hello world");
75
62
  ```
76
63
 
77
- The wrapper exposed to handlers implements:
64
+ ### Socket API (both modes)
78
65
 
79
66
  | Method | Behaviour |
80
67
  |--------|-----------|
@@ -87,6 +74,28 @@ The wrapper exposed to handlers implements:
87
74
  | `ws.cork(cb)` | No-op alias for batching writes |
88
75
  | `ws.data` | User-supplied state attached on upgrade |
89
76
 
77
+ ## Client
78
+
79
+ ```ts
80
+ import { createWebSocket } from "@hedystia/ws/client";
81
+
82
+ const ws = createWebSocket("ws://localhost:3000", {
83
+ headers: { authorization: "Bearer ..." },
84
+ });
85
+
86
+ ws.onopen = () => ws.send("hi");
87
+ ws.onmessage = (event) => console.log(event.data);
88
+ ```
89
+
90
+ A small ergonomic wrapper is also provided:
91
+
92
+ ```ts
93
+ import { WebSocketClient } from "@hedystia/ws/client";
94
+
95
+ const client = new WebSocketClient("ws://localhost:3000");
96
+ client.onmessage = (e) => console.log(e.data);
97
+ ```
98
+
90
99
  ## Runtime detection
91
100
 
92
101
  ```ts
@@ -1,5 +0,0 @@
1
- import { createRequire } from "node:module";
2
- //#region \0rolldown/runtime.js
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
- //#endregion
5
- export { __require };