/**
* 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;