@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.cjs","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.cjs","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/server.d.cts CHANGED
@@ -2,17 +2,16 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
2
2
 
3
3
  //#region src/server.d.ts
4
4
  /**
5
- * Runtime-agnostic WebSocket server.
5
+ * Runtime-agnostic WebSocket server built entirely on Node.js built-ins
6
+ * (`node:crypto`, `node:stream`) — no third-party dependencies.
6
7
  *
7
8
  * @remarks
8
- * Internally backed by the [`ws`](https://github.com/websockets/ws) package
9
- * which runs on Bun, Node.js and Deno (via `npm:` specifiers). The class
10
- * does **not** create or own an HTTP server — callers feed it raw upgrade
11
- * tuples coming from any HTTP runtime they prefer.
9
+ * The class does **not** create or own an HTTP server. Callers feed it raw
10
+ * upgrade tuples coming from any HTTP runtime (Node.js `http`, Bun, Deno,
11
+ * Hono, Fastify's upgrade hook, etc.).
12
12
  *
13
- * It implements topic-based pub/sub on top of the per-connection
14
- * `subscribe` / `unsubscribe` / `publish` API expected by Hedystia,
15
- * matching the shape of `Bun.ServerWebSocket`.
13
+ * Topic-based pub/sub is implemented in user-space, matching the shape of
14
+ * `Bun.ServerWebSocket` so the same handler code runs on every runtime.
16
15
  *
17
16
  * @typeParam Data - Shape of the user-attached `data` field
18
17
  *
@@ -22,8 +21,9 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
22
21
  * import { WebSocketServer } from "@hedystia/ws/server";
23
22
  *
24
23
  * const wss = new WebSocketServer({
25
- * open: (ws) => ws.send("welcome"),
24
+ * open: (ws) => ws.send("welcome"),
26
25
  * message: (ws, msg) => ws.publish("room", msg),
26
+ * close: (ws, code) => console.log("closed", code),
27
27
  * });
28
28
  *
29
29
  * const http = createServer((_req, res) => res.end("ok"));
@@ -35,7 +35,8 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
35
35
  */
36
36
  declare class WebSocketServer<Data extends WSData = WSData> {
37
37
  private readonly handlers;
38
- private readonly wss;
38
+ private readonly maxPayload;
39
+ private readonly resolveData;
39
40
  private readonly topics;
40
41
  private readonly socketTopics;
41
42
  private readonly allSockets;
@@ -43,7 +44,7 @@ declare class WebSocketServer<Data extends WSData = WSData> {
43
44
  * Build a new WebSocket server.
44
45
  *
45
46
  * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})
46
- * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})
47
+ * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})
47
48
  *
48
49
  * @example
49
50
  * ```ts
@@ -58,14 +59,16 @@ declare class WebSocketServer<Data extends WSData = WSData> {
58
59
  * Upgrade a raw HTTP upgrade tuple to a WebSocket connection.
59
60
  *
60
61
  * @remarks
61
- * The returned promise resolves to the connected {@link ServerWebSocket}
62
- * once the handshake completes; rejection means the handshake failed.
62
+ * Performs the RFC 6455 handshake synchronously on the duplex socket,
63
+ * then wires up frame parsing and lifecycle handlers. The returned
64
+ * promise resolves to the {@link ServerWebSocket} wrapper immediately
65
+ * after the handshake bytes are written.
63
66
  *
64
- * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event
67
+ * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event
65
68
  * @param options - Optional initial `data` for the new connection
66
69
  * @returns Promise that resolves with the established socket wrapper
67
70
  *
68
- * @throws {Error} When the underlying handshake throws synchronously
71
+ * @throws {Error} When `Sec-WebSocket-Key` is absent from the request headers
69
72
  *
70
73
  * @example
71
74
  * ```ts
@@ -83,10 +86,10 @@ declare class WebSocketServer<Data extends WSData = WSData> {
83
86
  /**
84
87
  * Publish a message to all sockets currently subscribed to `topic`.
85
88
  *
86
- * @param topic - Topic name
87
- * @param message - Payload to broadcast
88
- * @param _compress - Reserved for future use; ignored under the `ws` adapter
89
- * @returns Number of sockets that received the message.
89
+ * @param topic - Topic name
90
+ * @param message - Payload to broadcast
91
+ * @param _compress - Reserved for future use
92
+ * @returns Number of sockets that received the message
90
93
  *
91
94
  * @example
92
95
  * ```ts
@@ -97,23 +100,100 @@ declare class WebSocketServer<Data extends WSData = WSData> {
97
100
  /**
98
101
  * Close the server and optionally terminate all live sockets.
99
102
  *
100
- * @param closeActiveConnections - When `true`, calls `socket.terminate()`
101
- * on every live connection before shutting down.
103
+ * @param closeActiveConnections - When `true`, immediately terminates
104
+ * every live connection before clearing internal state
102
105
  */
103
106
  close(closeActiveConnections?: boolean): void;
104
107
  /**
105
- * Attach the per-socket lifecycle listeners (`message`, `close`, `error`).
108
+ * Wire up the duplex transport listeners (`data`, `close`, `error`) and
109
+ * invoke the user's `open` handler.
110
+ *
111
+ * @param native - Wrapped transport socket
112
+ * @param wrapped - Public {@link ServerWebSocket} wrapper
113
+ * @param head - Buffered bytes captured by the HTTP parser (re-fed into the parser)
106
114
  *
107
115
  * @internal
108
116
  */
109
117
  private bind;
110
118
  /**
111
- * Build the {@link ServerWebSocket} wrapper exposed to user handlers.
119
+ * Remove a socket from every topic it had joined and from the global
120
+ * tracking set.
121
+ *
122
+ * @param native - Socket being cleaned up
123
+ *
124
+ * @internal
125
+ */
126
+ private cleanup;
127
+ /**
128
+ * Build the {@link ServerWebSocket} wrapper exposed to user handlers,
129
+ * embedding topic-based pub/sub backed by the server's internal maps.
130
+ *
131
+ * @param native - Wrapped transport socket
132
+ * @param data - User-supplied `data` payload attached on upgrade
133
+ * @param rawReq - Raw incoming message used to extract the remote address
134
+ * @returns The fully-featured public wrapper
112
135
  *
113
136
  * @internal
114
137
  */
115
138
  private wrap;
116
139
  }
140
+ /**
141
+ * Return value of {@link serve}.
142
+ */
143
+ interface ServeInfo {
144
+ /** Port the HTTP server is listening on. */
145
+ port: number;
146
+ /** Hostname the HTTP server bound to. */
147
+ hostname: string;
148
+ /** Full URL of the listening server (uses `http://` scheme). */
149
+ url: URL;
150
+ /**
151
+ * Publish a message to all sockets subscribed to `topic`.
152
+ *
153
+ * @param topic - Topic name
154
+ * @param message - Payload to broadcast
155
+ * @param compress - Whether to compress (honoured on Bun, ignored on Node)
156
+ * @returns Number of sockets that received the message
157
+ */
158
+ publish: (topic: string, message: WSMessage, compress?: boolean) => number;
159
+ /**
160
+ * Stop the server and optionally close active connections.
161
+ *
162
+ * @param closeActiveConnections - When `true`, terminates all live sockets
163
+ */
164
+ stop: (closeActiveConnections?: boolean) => void | Promise<void>;
165
+ }
166
+ /**
167
+ * Start a standalone WebSocket server on the given port.
168
+ *
169
+ * @remarks
170
+ * Auto-detects the runtime and uses the native implementation:
171
+ * - **Bun:** delegates to `Bun.serve()` with native WebSocket support
172
+ * - **Node/Deno:** creates a `node:http` server with the built-in
173
+ * {@link WebSocketServer} upgrade handler
174
+ *
175
+ * @typeParam Data - Shape of the user-attached `data` field
176
+ *
177
+ * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})
178
+ * @param options - Server options including `port` and `hostname`
179
+ * @returns A promise resolving to {@link ServeInfo}
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * import { serve } from "@hedystia/ws";
184
+ *
185
+ * const server = await serve({
186
+ * open: (ws) => ws.subscribe("global"),
187
+ * message: (ws, msg) => ws.publish("global", msg),
188
+ * });
189
+ *
190
+ * console.log(`Listening on ${server.url}`);
191
+ * ```
192
+ */
193
+ declare function serve<Data extends WSData = WSData>(handlers: WebSocketHandlers<Data>, options?: WebSocketServerOptions & {
194
+ port?: number;
195
+ hostname?: string;
196
+ }): Promise<ServeInfo>;
117
197
  //#endregion
118
- export { type ServerWebSocket, type UpgradeOptions, type UpgradeRequest, type WSData, type WSMessage, type WebSocketHandlers, WebSocketServer, type WebSocketServerOptions };
198
+ export { ServeInfo, type ServerWebSocket, type UpgradeOptions, type UpgradeRequest, type WSData, type WSMessage, type WebSocketHandlers, WebSocketServer, type WebSocketServerOptions, serve };
119
199
  //# sourceMappingURL=server.d.cts.map
package/dist/server.d.mts CHANGED
@@ -2,17 +2,16 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
2
2
 
3
3
  //#region src/server.d.ts
4
4
  /**
5
- * Runtime-agnostic WebSocket server.
5
+ * Runtime-agnostic WebSocket server built entirely on Node.js built-ins
6
+ * (`node:crypto`, `node:stream`) — no third-party dependencies.
6
7
  *
7
8
  * @remarks
8
- * Internally backed by the [`ws`](https://github.com/websockets/ws) package
9
- * which runs on Bun, Node.js and Deno (via `npm:` specifiers). The class
10
- * does **not** create or own an HTTP server — callers feed it raw upgrade
11
- * tuples coming from any HTTP runtime they prefer.
9
+ * The class does **not** create or own an HTTP server. Callers feed it raw
10
+ * upgrade tuples coming from any HTTP runtime (Node.js `http`, Bun, Deno,
11
+ * Hono, Fastify's upgrade hook, etc.).
12
12
  *
13
- * It implements topic-based pub/sub on top of the per-connection
14
- * `subscribe` / `unsubscribe` / `publish` API expected by Hedystia,
15
- * matching the shape of `Bun.ServerWebSocket`.
13
+ * Topic-based pub/sub is implemented in user-space, matching the shape of
14
+ * `Bun.ServerWebSocket` so the same handler code runs on every runtime.
16
15
  *
17
16
  * @typeParam Data - Shape of the user-attached `data` field
18
17
  *
@@ -22,8 +21,9 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
22
21
  * import { WebSocketServer } from "@hedystia/ws/server";
23
22
  *
24
23
  * const wss = new WebSocketServer({
25
- * open: (ws) => ws.send("welcome"),
24
+ * open: (ws) => ws.send("welcome"),
26
25
  * message: (ws, msg) => ws.publish("room", msg),
26
+ * close: (ws, code) => console.log("closed", code),
27
27
  * });
28
28
  *
29
29
  * const http = createServer((_req, res) => res.end("ok"));
@@ -35,7 +35,8 @@ import { ServerWebSocket, UpgradeOptions, UpgradeRequest, WSData, WSMessage, Web
35
35
  */
36
36
  declare class WebSocketServer<Data extends WSData = WSData> {
37
37
  private readonly handlers;
38
- private readonly wss;
38
+ private readonly maxPayload;
39
+ private readonly resolveData;
39
40
  private readonly topics;
40
41
  private readonly socketTopics;
41
42
  private readonly allSockets;
@@ -43,7 +44,7 @@ declare class WebSocketServer<Data extends WSData = WSData> {
43
44
  * Build a new WebSocket server.
44
45
  *
45
46
  * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})
46
- * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})
47
+ * @param options - Optional behavioural overrides ({@link WebSocketServerOptions})
47
48
  *
48
49
  * @example
49
50
  * ```ts
@@ -58,14 +59,16 @@ declare class WebSocketServer<Data extends WSData = WSData> {
58
59
  * Upgrade a raw HTTP upgrade tuple to a WebSocket connection.
59
60
  *
60
61
  * @remarks
61
- * The returned promise resolves to the connected {@link ServerWebSocket}
62
- * once the handshake completes; rejection means the handshake failed.
62
+ * Performs the RFC 6455 handshake synchronously on the duplex socket,
63
+ * then wires up frame parsing and lifecycle handlers. The returned
64
+ * promise resolves to the {@link ServerWebSocket} wrapper immediately
65
+ * after the handshake bytes are written.
63
66
  *
64
- * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event
67
+ * @param req - Upgrade tuple emitted by `node:http`'s `'upgrade'` event
65
68
  * @param options - Optional initial `data` for the new connection
66
69
  * @returns Promise that resolves with the established socket wrapper
67
70
  *
68
- * @throws {Error} When the underlying handshake throws synchronously
71
+ * @throws {Error} When `Sec-WebSocket-Key` is absent from the request headers
69
72
  *
70
73
  * @example
71
74
  * ```ts
@@ -83,10 +86,10 @@ declare class WebSocketServer<Data extends WSData = WSData> {
83
86
  /**
84
87
  * Publish a message to all sockets currently subscribed to `topic`.
85
88
  *
86
- * @param topic - Topic name
87
- * @param message - Payload to broadcast
88
- * @param _compress - Reserved for future use; ignored under the `ws` adapter
89
- * @returns Number of sockets that received the message.
89
+ * @param topic - Topic name
90
+ * @param message - Payload to broadcast
91
+ * @param _compress - Reserved for future use
92
+ * @returns Number of sockets that received the message
90
93
  *
91
94
  * @example
92
95
  * ```ts
@@ -97,23 +100,100 @@ declare class WebSocketServer<Data extends WSData = WSData> {
97
100
  /**
98
101
  * Close the server and optionally terminate all live sockets.
99
102
  *
100
- * @param closeActiveConnections - When `true`, calls `socket.terminate()`
101
- * on every live connection before shutting down.
103
+ * @param closeActiveConnections - When `true`, immediately terminates
104
+ * every live connection before clearing internal state
102
105
  */
103
106
  close(closeActiveConnections?: boolean): void;
104
107
  /**
105
- * Attach the per-socket lifecycle listeners (`message`, `close`, `error`).
108
+ * Wire up the duplex transport listeners (`data`, `close`, `error`) and
109
+ * invoke the user's `open` handler.
110
+ *
111
+ * @param native - Wrapped transport socket
112
+ * @param wrapped - Public {@link ServerWebSocket} wrapper
113
+ * @param head - Buffered bytes captured by the HTTP parser (re-fed into the parser)
106
114
  *
107
115
  * @internal
108
116
  */
109
117
  private bind;
110
118
  /**
111
- * Build the {@link ServerWebSocket} wrapper exposed to user handlers.
119
+ * Remove a socket from every topic it had joined and from the global
120
+ * tracking set.
121
+ *
122
+ * @param native - Socket being cleaned up
123
+ *
124
+ * @internal
125
+ */
126
+ private cleanup;
127
+ /**
128
+ * Build the {@link ServerWebSocket} wrapper exposed to user handlers,
129
+ * embedding topic-based pub/sub backed by the server's internal maps.
130
+ *
131
+ * @param native - Wrapped transport socket
132
+ * @param data - User-supplied `data` payload attached on upgrade
133
+ * @param rawReq - Raw incoming message used to extract the remote address
134
+ * @returns The fully-featured public wrapper
112
135
  *
113
136
  * @internal
114
137
  */
115
138
  private wrap;
116
139
  }
140
+ /**
141
+ * Return value of {@link serve}.
142
+ */
143
+ interface ServeInfo {
144
+ /** Port the HTTP server is listening on. */
145
+ port: number;
146
+ /** Hostname the HTTP server bound to. */
147
+ hostname: string;
148
+ /** Full URL of the listening server (uses `http://` scheme). */
149
+ url: URL;
150
+ /**
151
+ * Publish a message to all sockets subscribed to `topic`.
152
+ *
153
+ * @param topic - Topic name
154
+ * @param message - Payload to broadcast
155
+ * @param compress - Whether to compress (honoured on Bun, ignored on Node)
156
+ * @returns Number of sockets that received the message
157
+ */
158
+ publish: (topic: string, message: WSMessage, compress?: boolean) => number;
159
+ /**
160
+ * Stop the server and optionally close active connections.
161
+ *
162
+ * @param closeActiveConnections - When `true`, terminates all live sockets
163
+ */
164
+ stop: (closeActiveConnections?: boolean) => void | Promise<void>;
165
+ }
166
+ /**
167
+ * Start a standalone WebSocket server on the given port.
168
+ *
169
+ * @remarks
170
+ * Auto-detects the runtime and uses the native implementation:
171
+ * - **Bun:** delegates to `Bun.serve()` with native WebSocket support
172
+ * - **Node/Deno:** creates a `node:http` server with the built-in
173
+ * {@link WebSocketServer} upgrade handler
174
+ *
175
+ * @typeParam Data - Shape of the user-attached `data` field
176
+ *
177
+ * @param handlers - Lifecycle handlers ({@link WebSocketHandlers})
178
+ * @param options - Server options including `port` and `hostname`
179
+ * @returns A promise resolving to {@link ServeInfo}
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * import { serve } from "@hedystia/ws";
184
+ *
185
+ * const server = await serve({
186
+ * open: (ws) => ws.subscribe("global"),
187
+ * message: (ws, msg) => ws.publish("global", msg),
188
+ * });
189
+ *
190
+ * console.log(`Listening on ${server.url}`);
191
+ * ```
192
+ */
193
+ declare function serve<Data extends WSData = WSData>(handlers: WebSocketHandlers<Data>, options?: WebSocketServerOptions & {
194
+ port?: number;
195
+ hostname?: string;
196
+ }): Promise<ServeInfo>;
117
197
  //#endregion
118
- export { type ServerWebSocket, type UpgradeOptions, type UpgradeRequest, type WSData, type WSMessage, type WebSocketHandlers, WebSocketServer, type WebSocketServerOptions };
198
+ export { ServeInfo, type ServerWebSocket, type UpgradeOptions, type UpgradeRequest, type WSData, type WSMessage, type WebSocketHandlers, WebSocketServer, type WebSocketServerOptions, serve };
119
199
  //# sourceMappingURL=server.d.mts.map