import { ArrayIsArray, getter, NumberIsFinite, ObjectCreate, ObjectDefineProperty as define, ObjectFreeze as freeze, RangeError, TypedArrayPrototypeGetBuffer as bufferOf, TypedArrayPrototypeGetByteLength as length, TypedArrayPrototypeGetByteOffset as offsetOf, TypedArrayPrototypeGetTag as tag, TypeError, Uint8Array, Uint8ArrayPrototypeSet as setBytes, uncurryThis, WeakMap, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { promiseResolve, promiseThen as then, safePromise, } from "ext:core/promises"; import { op_net_accept, op_net_close, op_net_connect_start, op_net_connected, op_net_datagram_broadcast, op_net_datagram_listen, op_net_datagram_receive, op_net_datagram_send, op_net_listen, op_net_read, op_net_shutdown, op_net_write, op_net_writev, } from "ext:core/ops"; import type { NetAddress, NetConnection, NetListener, NetListenerId, NetUdpAddress, } from "ext:core/ops"; import { array, dictionary, state, usv } from "ext:web/idl.js"; import { addListener, removeListener, signals } from "ext:web/events.js"; import { registerUncloneable } from "ext:web/serialization.js"; import { byobRequest, enqueue, errorStream, finish, ReadableStream, ReadableStreamBYOBRequest, WritableStream, } from "ext:webstreams/streams.js"; import type { ReadableByteStreamController, WritableStreamDefaultController, } from "ext:webstreams/streams.js"; import { failure, result } from "ext:file/errors.js"; interface SocketData { id: number | null; owner: object; reader?: ReadableByteStreamController | null; writer?: WritableStreamDefaultController | null; } interface ConnectionData extends SocketData { writing: Promise; local: A; remote: A; readable: ReadableStream> | null; writable: WritableStream> | null; reader: ReadableByteStreamController | null; writer: WritableStreamDefaultController | null; } interface ListenerData extends SocketData { id: NetListenerId | null; local: A; } interface DatagramData extends SocketData { writing: Promise; local: Readonly; } type Datagram = [ bytes: Uint8Array, remote: Readonly, ]; const connections = new WeakMap(), listeners = new WeakMap(), datagrams = new WeakMap(); registerUncloneable(connections); registerUncloneable(listeners); registerUncloneable(datagrams); const FinalizationRegistry = globalThis.FinalizationRegistry; const register = uncurryThis( (FinalizationRegistry.prototype as FinalizationRegistry).register, ); const unregister = uncurryThis(FinalizationRegistry.prototype.unregister); const registry = new FinalizationRegistry((id: number) => op_net_close(id)); const getView = getter(ReadableStreamBYOBRequest.prototype, "view"); const respond = uncurryThis(ReadableStreamBYOBRequest.prototype.respond); const writableError = uncurryThis( WritableStreamDefaultController.prototype.error, ); const dispose: typeof Symbol.dispose = Symbol.dispose, asyncIterator: typeof Symbol.asyncIterator = Symbol.asyncIterator, empty = new Uint8Array(); const safeInteger = Number.isSafeInteger; function args(...items: T): T { const value = array(); for (let i = 0; i < items.length; i++) value[i] = items[i]; return value as T; } function address(value: A): A { const copy: (NetAddress | NetUdpAddress) & { __proto__?: null } = value.transport === "unix" ? { __proto__: null, transport: "unix", path: value.path } : { __proto__: null, transport: value.transport, hostname: value.hostname, port: value.port, }; return freeze(copy) as A; } function options( value: unknown, connecting = false, ): NetAddress & { __proto__?: null } { const options = dictionary(value); const transport = options.transport, host = options.hostname, inputPort = options.port, path = options.path; if (transport === "unix") { if (host !== undefined || inputPort !== undefined) { throw new TypeError("Unix sockets cannot specify hostname or port"); } if (path === undefined) throw new TypeError("Unix sockets require a path"); const label = usv(path); if (!label) throw new TypeError("Unix sockets require a path"); return { __proto__: null, transport: "unix", path: label }; } if (path !== undefined) { throw new TypeError("Socket paths require Unix transport"); } const port = +(inputPort as number); if (transport !== undefined && transport !== "tcp") { throw new TypeError("Expected TCP transport"); } if ( !NumberIsFinite(port) || port < (connecting ? 1 : 0) || port > 65535 || port % 1 !== 0 ) throw new RangeError("Invalid socket port"); return { __proto__: null, transport: "tcp" as const, hostname: host === undefined ? "127.0.0.1" : usv(host), port, }; } function id(data: { id: I | null }): I { if (data.id === null) throw failure("BadResource", "Socket is closed"); return data.id; } function bytes(value: unknown): Uint8Array { if (tag(value) !== "Uint8Array") { throw new TypeError("Socket I/O requires a Uint8Array"); } setBytes(value as Uint8Array, empty); return value as Uint8Array; } function segment( value: Uint8Array, offset = 0, ): Uint8Array { const count = length(value) - offset; return new Uint8Array( bufferOf(value), offsetOf(value) + offset, count < 65536 ? count : 65536, ); } function vectors(buffers: unknown): Uint8Array[] { if (!ArrayIsArray(buffers)) { throw new TypeError("Socket I/O requires an array of Uint8Array"); } // Snapshot before queueing, without rereading caller-controlled elements. const count = buffers.length, pieces: Uint8Array[] = args(); if (!safeInteger(count) || count < 0 || count > 4294967295) { throw new RangeError("Invalid vector length"); } let total = 0; for (let i = 0; i < count; i++) { const view: unknown = buffers[i]; if (tag(view) !== "Uint8Array") { throw new TypeError("Socket I/O requires an array of Uint8Array"); } setBytes(view as Uint8Array, empty); const size = length(view); if (size === 0) continue; if (!safeInteger(total + size)) { throw new RangeError("Vector exceeds safe integer range"); } total += size; // Fixed snapshot for the native call, which needs unshared storage. pieces[pieces.length] = new Uint8Array( new Uint8Array( bufferOf(view), offsetOf(view), size, ), ); } return pieces; } function close( data: SocketData, quiet = false, source: "read" | "write" | null = null, ): void { if (data.id === null) { if (!quiet) id(data); return; } const current = data.id; data.id = null; unregister(registry, data.owner); const error = failure("BadResource", "Socket is closed"); if (data.reader && source !== "read") errorStream(data.reader, error); if (data.writer && source !== "write") writableError(data.writer, error); const reply = op_net_close(current); if (!quiet) result(reply); } function queue( data: { writing: Promise }, operation: () => T | PromiseLike, ): Promise { const promise = then(data.writing, operation, operation); data.writing = then(promise, () => {}, () => {}); return promise; } const socketKey = Symbol(); type TcpAddr = Extract; type UnixAddr = Extract; type TcpOptions = { transport?: "tcp"; hostname?: string; port: number; signal?: AbortSignal; }; type UnixOptions = { transport: "unix"; path: string; hostname?: never; port?: never; signal?: AbortSignal; }; class Conn { declare static readonly [Symbol.hasInstance]: (value: unknown) => value is Conn; #data: ConnectionData; constructor(...args: [typeof socketKey, NetConnection]) { const key = args[0], row = args[1]; if (key !== socketKey) throw new TypeError("Illegal constructor"); this.#data = { id: row[0], owner: this, local: address(row[1]), remote: address(row[2]), readable: null, writable: null, reader: null, writer: null, writing: promiseResolve(undefined), }; set(connections, this, this.#data); register(registry, this, row[0], this); } get localAddr(): A { state(connections, this); return this.#data.local; } get remoteAddr(): A { state(connections, this); return this.#data.remote; } async read(buffer: Uint8Array): Promise { const data = state(connections, this), current = id(data); bytes(buffer); const chunk = result( await safePromise(op_net_read(args(current, length(buffer)))), ); setBytes(buffer, empty); if (chunk === null) return null; setBytes(buffer, chunk); return length(chunk); } write(buffer: Uint8Array): Promise { const data = state(connections, this); id(data); const snapshot = new Uint8Array(segment(bytes(buffer))); return queue( data, () => then(op_net_write(args(id(data), snapshot)), result), ); } writev(buffers: unknown): Promise { const data = state(connections, this); id(data); const snapshots = vectors(buffers); return queue(data, async () => { if (snapshots.length === 0) { return result( await safePromise(op_net_writev(args(id(data), snapshots))), ); } let total = 0, elem = 0, inner = 0; while (elem < snapshots.length) { const batch: Uint8Array[] = args(); let budget = 65536, scan = elem, scanInner = inner; while (scan < snapshots.length && batch.length < 16 && budget > 0) { const piece = snapshots[scan], size = length(piece) - scanInner; if (size === 0) { scan++; scanInner = 0; continue; } const take = size < budget ? size : budget; batch[batch.length] = new Uint8Array( bufferOf(piece), offsetOf(piece) + scanInner, take, ); budget -= take; scanInner += take; if (scanInner >= length(piece)) { scan++; scanInner = 0; } } const count = result( await safePromise(op_net_writev(args(id(data), batch))), ); if (!safeInteger(count) || count <= 0 || count > 65536 - budget) { throw failure("IoError", "Invalid partial socket write"); } total += count; let skip = count; while (skip > 0) { const remaining = length(snapshots[elem]) - inner; if (remaining === 0) { elem++; inner = 0; continue; } if (skip < remaining) { inner += skip; break; } skip -= remaining; elem++; inner = 0; } } return total; }); } closeWrite(): Promise { const data = state(connections, this); id(data); return queue(data, () => then(op_net_shutdown(id(data)), (reply) => { result(reply); })); } close() { close(state(connections, this)); } [dispose]() { close(state(connections, this), true); } get readable() { const data = state(connections, this); if (data.readable) return data.readable; id(data); data.readable = new ReadableStream({ type: "bytes", start(controller) { data.reader = controller; }, async pull(controller) { try { const request = byobRequest(controller), view = request && getView(request); const chunk = result( await safePromise( op_net_read(args(id(data), view ? length(view) : 65536)), ), ); if (data.id === null) return; if (chunk === null) finish(controller); else if (request) { setBytes(view!, chunk); respond(request, length(chunk)); } else enqueue(controller, chunk); } catch (error) { errorStream(controller, error); close(data, true, "read"); } }, cancel() { close(data, true, "read"); }, }, { highWaterMark: 0 }); return data.readable; } get writable() { const data = state(connections, this); if (data.writable) return data.writable; id(data); data.writable = new WritableStream({ start(controller) { data.writer = controller; }, async write(value) { try { const snapshot = new Uint8Array(bytes(value)); await safePromise(queue(data, async () => { let offset = 0; while (offset < length(snapshot)) { const count = result( await safePromise( op_net_write( args( id(data), segment(snapshot, offset), ), ), ), ); if (count <= 0 || count > length(snapshot) - offset) { throw failure("IoError", "Invalid partial socket write"); } offset += count; } })); } catch (error) { close(data, true, "write"); throw error; } }, close() { return queue(data, () => then(op_net_shutdown(id(data)), result)); }, abort() { close(data, true, "write"); }, }, { highWaterMark: 65536, size(value) { return tag(value) === "Uint8Array" ? length(value) : 1; }, }); return data.writable; } } function connection(row: NetConnection): Conn { return new Conn(socketKey, row); } class Listener { declare static readonly [Symbol.hasInstance]: (value: unknown) => value is Listener; #data: ListenerData; constructor(...args: [typeof socketKey, NetListener]) { const key = args[0], row = args[1]; if (key !== socketKey) throw new TypeError("Illegal constructor"); this.#data = { id: row[0], owner: this, local: address(row[1]) }; set(listeners, this, this.#data); register(registry, this, row[0], this); } get addr(): A { state(listeners, this); return this.#data.local; } async accept(): Promise> { state(listeners, this); return connection( result(await safePromise(op_net_accept(id(this.#data)))), ); } close() { close(state(listeners, this)); } [dispose]() { close(state(listeners, this), true); } [asyncIterator](): AsyncIterableIterator, undefined> { state(listeners, this); const data = this.#data; return { async next() { if (data.id === null) return { done: true, value: undefined }; try { return { done: false, value: connection( result(await safePromise(op_net_accept(id(data)))), ), }; } catch (error) { if (data.id === null) return { done: true, value: undefined }; throw error; } }, return() { close(data, true); return promiseResolve({ done: true, value: undefined }); }, [asyncIterator]() { return this; }, }; } } function listen(value: TcpOptions): Listener; function listen(value: UnixOptions): Listener; function listen(value: unknown): Listener { const row = result(op_net_listen(options(value))); return new Listener(socketKey, row); } function connect(value: TcpOptions): Promise>; function connect(value: UnixOptions): Promise>; async function connect(value: unknown): Promise { const settings = dictionary(value); const target = options(settings, true), signal = settings.signal; if (signal !== undefined && state(signals, signal).aborted) { throw state(signals, signal).reason; } const current = result(op_net_connect_start(target)); const cancel = () => op_net_close(current); if (signal !== undefined) { addListener(signal, "abort", cancel, { once: true }); } try { const row = result(await safePromise(op_net_connected(current))); if (signal !== undefined && state(signals, signal).aborted) { cancel(); throw state(signals, signal).reason; } return connection(row); } catch (error) { cancel(); if (signal !== undefined && state(signals, signal).aborted) { throw state(signals, signal).reason; } throw error; } finally { if (signal !== undefined) removeListener(signal, "abort", cancel); } } function datagramOptions( value: unknown, sending = false, ): NetUdpAddress & { __proto__?: null } { const options = dictionary(value); const transport = options.transport, inputHost = options.hostname, inputPort = options.port, path = options.path; if (transport !== undefined && transport !== "udp") { throw new TypeError("Expected UDP transport"); } if (path !== undefined) { throw new TypeError("UDP sockets cannot specify a path"); } const port = +(inputPort as number); if ( !NumberIsFinite(port) || port < (sending ? 1 : 0) || port > 65535 || port % 1 !== 0 ) throw new RangeError("Invalid UDP port"); return { __proto__: null, transport: "udp" as const, hostname: inputHost === undefined ? "127.0.0.1" : usv(inputHost), port, }; } async function receiveDatagram( data: DatagramData, buffer: Uint8Array | undefined, ): Promise { const current = id(data); if (buffer !== undefined) bytes(buffer); const row = result( await safePromise( op_net_datagram_receive( args(current, buffer === undefined ? 65535 : length(buffer)), ), ), ); let message: Uint8Array = row[0]; if (buffer !== undefined) { setBytes(buffer, empty); setBytes(buffer, message); message = new Uint8Array( bufferOf(buffer), offsetOf(buffer), length(message), ); } return [message, address(row[1])]; } class DatagramConn { declare static readonly [Symbol.hasInstance]: (value: unknown) => value is DatagramConn; private constructor() { throw new TypeError("Illegal constructor"); } get addr() { return state(datagrams, this).local; } receive( buffer: Uint8Array | undefined = undefined, ): Promise { return receiveDatagram(state(datagrams, this), buffer); } send(buffer: Uint8Array, target: unknown): Promise { const data = state(datagrams, this); id(data); bytes(buffer); if (length(buffer) > 65507) { throw new RangeError("Datagram exceeds 65507 bytes"); } const snapshot: Uint8Array = new Uint8Array(buffer), destination = datagramOptions(target, true); return queue( data, () => then( op_net_datagram_send(args(id(data), snapshot, destination)), result, ), ); } setBroadcast(enabled = true): void { result( op_net_datagram_broadcast(args(id(state(datagrams, this)), !!enabled)), ); } close() { close(state(datagrams, this)); } [dispose]() { close(state(datagrams, this), true); } [asyncIterator](): AsyncIterableIterator { const data = state(datagrams, this); return { async next() { if (data.id === null) return { done: true, value: undefined }; try { return { done: false, value: await safePromise(receiveDatagram(data, undefined)), }; } catch (error) { if (data.id === null) return { done: true, value: undefined }; throw error; } }, return() { close(data, true); return promiseResolve({ done: true, value: undefined }); }, [asyncIterator]() { return this; }, }; } } function listenDatagram(value: unknown): DatagramConn { const row = result(op_net_datagram_listen(datagramOptions(value))); const socket = ObjectCreate(DatagramConn.prototype) as DatagramConn; set(datagrams, socket, { id: row[0], owner: socket, local: address(row[1]), writing: promiseResolve(undefined), }); register(registry, socket, row[0], socket); return socket; } for ( const pair of [[Conn, "Conn"], [Listener, "Listener"], [ DatagramConn, "DatagramConn", ]] as const ) { define(pair[0].prototype, Symbol.toStringTag, { value: pair[1], configurable: true, }); } export { Conn, connect, DatagramConn, listen, listenDatagram, Listener };