// The socket a guest server gets for a raw connection (a page WebSocket upgrading): // a net.Socket-shaped Duplex over the connection's MessagePort, whose other end is the // page itself. Bytes arrive as loop completions; writes post at once. import type { ConnectionData } from "../shared/protocol"; import type { Loop } from "./loop"; import { runInFrame, type Context } from "./async_hooks"; import { Buffer } from "./buffer"; import { Duplex } from "./stream"; let nextRemotePort = 49152; export class PortSocket extends Duplex { readonly remoteAddress = "127.0.0.1"; readonly remoteFamily = "IPv4"; readonly remotePort: number; readonly localAddress = "127.0.0.1"; readonly localPort: number; readonly server: unknown; readonly connecting = false; readonly pending = false; bytesRead = 0; #bytesWritten = 0; readonly #wire: MessagePort; readonly #loop: Loop; readonly #frame: Context; #finSent = false; #ref = true; /** `onClosed` runs once the socket has closed. Until then it holds the loop, as an * open TCP handle does, unless unref()'d. `frame` is the frame this socket's * server was listening in (http.ts's connect() passes it: the socket itself is * constructed from the page's raw MessagePort arriving, a native callback with no * frame of its own), so every byte it delivers lands in that frame, as a real * accepted connection's data would (DESIGN's stage 1 point 2). */ constructor(server: unknown, port: number, wire: MessagePort, loop: Loop, onClosed: () => void, frame: Context) { // Node's http server sockets are half-open: the peer's end leaves writes open. super({ allowHalfOpen: true }); this.server = server; this.localPort = port; this.remotePort = nextRemotePort; nextRemotePort = nextRemotePort === 65535 ? 49152 : nextRemotePort + 1; this.#wire = wire; this.#loop = loop; this.#frame = frame; loop.hold(); wire.onmessage = (ev: MessageEvent) => this.receive(ev.data); this.once("close", () => { if (this.#ref) loop.release(); onClosed(); }); } /** Bytes from the page, or null for its end; delivered as a loop completion. */ receive(data: ConnectionData): void { this.#loop.enqueueCompletion(() => runInFrame(this.#frame, () => { if (this.destroyed) return; if (data === null) { this.push(null); return; } this.bytesRead += data.length; this.push(new Buffer(data.buffer as ArrayBuffer, data.byteOffset, data.byteLength)); })); } get bytesWritten(): number { return this.#bytesWritten; } override _read(): void {} override _write(chunk: unknown, _encoding: string, callback: (error?: Error | null) => void): void { const bytes = new Uint8Array(chunk as Uint8Array); this.#bytesWritten += bytes.length; this.#wire.postMessage(bytes satisfies ConnectionData, [bytes.buffer]); callback(); } override _final(callback: (error?: Error | null) => void): void { this.#fin(); callback(); } override _destroy(error: Error | null, callback: (error?: Error | null) => void): void { this.#fin(); this.#wire.close(); callback(error); } /** net.Socket's 'close' says whether an error closed it. Every emit (not only the * completions receive() already wraps) runs in the handle's frame: writable-side * events like 'finish' and 'close' are driven by the guest calling end()/destroy() * from inside a 'data'/'request' handler already in that frame, but Duplex's own * internal scheduling (nextTick) recaptures whatever is current at that point, and * this keeps it pinned regardless. */ override emit(event: string | symbol, ...args: unknown[]): boolean { return runInFrame(this.#frame, () => { if (event === "close" && args.length === 0) return super.emit(event, this.errored !== null); return super.emit(event, ...args); }); } #fin(): void { if (this.#finSent) return; this.#finSent = true; this.#wire.postMessage(null satisfies ConnectionData); } address(): { address: string; family: string; port: number } { return { address: this.localAddress, family: "IPv4", port: this.localPort }; } destroySoon(): void { if (this.writableFinished) this.destroy(); else this.end(() => this.destroy()); } /** There is no idle clock on the bridge, so a timeout never fires. */ setTimeout(_ms: number, cb?: () => void): this { if (cb) this.once("timeout", cb); return this; } setNoDelay(): this { return this; } setKeepAlive(): this { return this; } ref(): this { if (!this.#ref && !this.closed) this.#loop.hold(); this.#ref = true; return this; } unref(): this { if (this.#ref && !this.closed) this.#loop.release(); this.#ref = false; return this; } }