/** * hosting/webSocketConversation — one upgraded socket, presented as a * {@link HostConversation}. * * This is the whole adapter side of the conversation port: it takes the * `'upgrade'` event a `node:http` server hands it, answers the handshake, and * turns the frames flowing over the socket into the six-member port a handler * sees. Everything protocol-shaped lives in `webSocketFrames.ts`; everything * port-shaped lives in `types.ts`; this file is the join. * * ── The laws it keeps, all of them pinned by tests ─────────────────────────── * - **A path this door does not own is not touched.** `node:http` calls EVERY * `'upgrade'` listener for every upgrade, exactly as it calls every * `'request'` listener — so a caller's own protocol lives beside this one on * the same socket. Whether an unclaimed path gets an answer is the caller's * business on a server they own, and this door's only on a server it owns. * - **Frames that arrive before the handler subscribes are held, up to a * declared bound.** An `async` handler that awaits before calling `onFrame` * would otherwise lose the far side's opening frame. The bound is in BYTES * and overflow ends the conversation with a stated reason, because an * unbounded queue somebody else fills is a way to kill this process, and an * undeclared ceiling is the exact thing the declared-ceilings rule exists to * forbid. * - **`close()` ends every live conversation before the socket is released.** * Measured, not assumed: an upgraded socket keeps `server.close()` waiting * forever, so a door that let go of its conversations would hang the whole * shutdown. * - **The port's frame is the whole message.** A message the transport * delivered in fragments counts against `maxFrameBytes` in total, so * fragmentation cannot be used to walk around the ceiling. * - **Nothing in a conversation's lifecycle is ever the PROCESS's failure.** * Node calls a socket's listeners from its own stack, so a throw inside one * is uncaught and ends the container. Every listener body here that computes * is wrapped: a surprise costs THIS conversation, with a reason, and the * door keeps carrying everybody else's. * * Pattern: Adapter. Role: outer ring, one transport. */ /// /// import type { IncomingMessage } from 'node:http'; import type { Duplex } from 'node:stream'; import type { ConversationHandler, ConversationLimits } from './types.js'; /** * What a wire read out of one handshake — the conversation half of a * deployment's dialect. * * There is no body to read here, which is the whole difference from a request: * a handshake is a URL and some headers, so a dialect that wants a session id * or a credential has to find it in those. */ export interface ConversationHandshake { /** The session this conversation claims. Caller data; never identity. */ readonly sessionId?: string; /** * Headers to merge OVER the raw lower-cased ones the transport delivered. * * This is how an adapter maps its own spelling into the port's vocabulary — * a credential a browser could only send as a subprotocol becomes an ordinary * `authorization` header here. The raw headers are never removed, so nothing * a mapping did not understand is lost. */ readonly headers?: Readonly>; /** * The subprotocol to echo in the 101, when this dialect selects one. * * Selecting is the wire's job because only the wire read the offer. Echoing * something the client did not offer makes the client fail the connection. */ readonly protocol?: string; } /** Everything the door needs to answer one upgrade. */ export interface ConversationDoorOptions { /** The adapter's name — every refusal carries it. */ readonly hostName: string; /** The path this door owns. Anything else is not ours. */ readonly path: string; /** The ceilings this door declares, already defaulted. */ readonly limits: ConversationLimits; /** This deployment's handshake dialect. Absent ⇒ raw headers and no session. */ readonly readConversation?: (facts: HandshakeFacts) => ConversationHandshake; /** Where a new conversation goes. */ readonly handler: ConversationHandler; /** Whether the host is still taking conversations — false after `close()`. */ readonly accepting: () => boolean; } /** What a handshake dialect may read. Deliberately the same shape a request wire gets, minus the body. */ export interface HandshakeFacts { /** Header names lower-cased. */ readonly headers: Readonly>; /** The query string, already parsed — where a browser has to put things it cannot header. */ readonly query: URLSearchParams; } /** A door: hand it upgrades, close it when you are done. */ export interface ConversationDoor { /** * Take one upgrade, or decline it. Returns whether this door claimed the * path — the caller decides what an unclaimed upgrade deserves. */ handleUpgrade(request: IncomingMessage, socket: Duplex): boolean; /** End every live conversation politely, then resolve. */ closeAll(reason: string): Promise; /** How many conversations are open right now. */ readonly liveCount: number; } export declare function conversationDoor(options: ConversationDoorOptions): ConversationDoor; //# sourceMappingURL=webSocketConversation.d.ts.map