From a5a3588d3d59fdab27683a7b053296bbd084ab98 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 15 Oct 2026 15:59:17 -0500 Subject: [PATCH 1/2] Add net socket address fields, write/end/setTimeout callbacks, close(hadError) and server connection limits - socket.remotePort, remoteFamily, localAddress, localPort, localFamily, bytesRead, readyState, connecting and pending. The peer's fields are cached at first read as remoteAddress is (undefined while connecting), and the local ones are undefined while connecting and after close. - write(data, cb) runs cb with null once the data is flushed, and end(cb) / end(data, cb) once the socket finishes. - 'close' listeners receive hadError, 'ready' fires after 'connect', and 'data' listeners may take the string chunks setEncoding gives. - setTimeout(ms, cb) adds cb as a once('timeout') listener (0 removes it), and setTimeout throws ERR_OUT_OF_RANGE for a negative, NaN or infinite msecs where it disabled the timer before. setKeepAlive chains. - server.maxConnections closes connections past the cap and emits 'drop' with their addresses; server.getConnections(cb) answers the count on the next tick. - net.isIP, isIPv4 and isIPv6 follow Node's address patterns. - A callback adapted to a void return keeps the identity of the function it wraps, so removing it by that function finds it. Corpus 5644-5647 cover each case. --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 22 +- .../generated/node-v24-backlog.json | 49 +- .../generated/node-v24-internal.json | 28 +- .../ambient/scriptc-node-fallback.d.ts | 60 ++- .../compiler/src/backend/llvm/lib-network.ts | 18 + .../compiler/src/backend/llvm/lib-shared.ts | 18 +- .../src/frontend/lowering/lower-server.ts | 330 +++++++++++- .../src/frontend/lowering/lower-stmts.ts | 11 + .../src/frontend/lowering/surfaces.ts | 4 + packages/compiler/src/frontend/type-mapper.ts | 22 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 32 ++ packages/compiler/surface-manifest.json | 18 + .../corpus/5644-net-socket-addresses.ts.json | 6 + .../corpus/5645-net-socket-callbacks.ts.json | 6 + .../5646-net-server-connections.ts.json | 6 + .../tests/corpus/5647-net-is-ip.ts.json | 6 + packages/runtime/src/scr_net.c | 499 +++++++++++++++++- packages/runtime/src/scr_net.h | 24 + tests/corpus/5644-net-socket-addresses.ts | 69 +++ tests/corpus/5645-net-socket-callbacks.ts | 65 +++ tests/corpus/5646-net-server-connections.ts | 44 ++ tests/corpus/5647-net-is-ip.ts | 34 ++ 24 files changed, 1309 insertions(+), 67 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json create mode 100644 tests/corpus/5644-net-socket-addresses.ts create mode 100644 tests/corpus/5645-net-socket-callbacks.ts create mode 100644 tests/corpus/5646-net-server-connections.ts create mode 100644 tests/corpus/5647-net-is-ip.ts diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index b15b32aff..3515e0011 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "a38d2d004b348e926af4", + "artifactVersion": "1dd6d714f44893573704", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c007fdbe5..ba2cd261c 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -494,9 +494,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52122,9 +52122,9 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52140,9 +52140,9 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52158,9 +52158,9 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 58e08f0e6..af3ca3822 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 37, - "verify-gap": 2395, - "audit-partial": 953, + "verify-gap": 2392, + "audit-partial": 956, "classify": 2, "implement": 85 } @@ -51298,12 +51298,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51318,12 +51321,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51338,12 +51344,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index f3ea536e8..3b474aafe 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -521,9 +521,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -73516,8 +73516,12 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73534,8 +73538,12 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73552,8 +73560,12 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 15e4d6569..c70759364 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3415,6 +3415,17 @@ declare module "net" { ref(): void; unref(): void; readonly remoteAddress: string | undefined; + /* The peer's fields are cached at first read, like remoteAddress; the + * local ones are undefined while connecting and after close. */ + readonly remotePort: number | undefined; + readonly remoteFamily: string | undefined; + readonly localAddress: string | undefined; + readonly localPort: number | undefined; + readonly localFamily: string | undefined; + readonly bytesRead: number; + readonly connecting: boolean; + readonly pending: boolean; + readonly readyState: "opening" | "open" | "readOnly" | "writeOnly" | "closed"; /* true once the fd is gone (destroy() or full close) — Node's flag. */ readonly destroyed: boolean; /* the write half is open: no end() yet, no FIN sent, fd alive. */ @@ -3424,8 +3435,11 @@ declare module "net" { /* every byte the write paths accepted (buffered included — Node * counts those too; plaintext on TLS sockets). */ readonly bytesWritten: number; - write(data: string | Uint8Array): void; - end(data?: string | Uint8Array): void; + /* write's callback runs once the data is flushed, with null; end's + * once the socket finishes. */ + write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void; + end(callback?: () => void): void; + end(data: string | Uint8Array, callback?: () => void): void; destroy(): void; /* Flow control: pause holds reads off (kernel/TCP backpressure is * the buffer); resume flows — and discards without listeners, so @@ -3434,6 +3448,8 @@ declare module "net" { resume(): Socket; /* TCP_NODELAY on the live fd; missing means true. Chains. */ setNoDelay(noDelay?: boolean): Socket; + /* SO_KEEPALIVE with the initial delay in ms. Chains. */ + setKeepAlive(enable?: boolean, initialDelay?: number): Socket; /* end() now, destroy once the FIN actually flushed. */ destroySoon(): void; /* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage @@ -3444,7 +3460,8 @@ declare module "net" { pipe( destination: Socket | import("http").ServerResponse | import("http2").Http2ServerResponse, ): void; - setTimeout(ms: number): void; + /* The callback is a once('timeout') listener (removed again by 0). */ + setTimeout(ms: number, callback?: () => void): void; /* The paused-mode demux surface: once('readable') + read(1) + * unshift — read answers exactly n buffered bytes or null. */ read(size?: number): Buffer | null; @@ -3455,25 +3472,34 @@ declare module "net" { * registration never fires — Node's exact split. 'session' fires * once with the serialized session (the received-ticket event). */ on(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + on(event: "data", listener: (chunk: string) => void): void; on( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + on(event: "close", listener: (hadError: boolean) => void): void; on(event: "error", listener: (err: Error) => void): void; on(event: "session", listener: (session: Buffer) => void): void; /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + addListener(event: "data", listener: (chunk: string) => void): void; addListener( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + addListener(event: "close", listener: (hadError: boolean) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener(event: "session", listener: (session: Buffer) => void): void; once(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + once(event: "data", listener: (chunk: string) => void): void; once( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + once(event: "close", listener: (hadError: boolean) => void): void; once(event: "error", listener: (err: Error) => void): void; once(event: "session", listener: (session: Buffer) => void): void; } @@ -3482,6 +3508,11 @@ declare module "net" { ref(): void; unref(): void; readonly listening: boolean; + /* Connections past this many are closed with a 'drop' event; + * undefined (the default) or null lifts the cap. */ + maxConnections: number | undefined; + /* cb(null, count) on the next tick. */ + getConnections(callback: (error: Error | null, count: number) => void): Server; /* Node answers the server itself (`return this` chaining). */ listen(port: number, callback?: () => void): Server; /* The positional bind address (the options form's host in argument @@ -3503,6 +3534,7 @@ declare module "net" { emit(event: "connection", socket: Socket): boolean; on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; on(event: "close" | "listening", listener: () => void): void; + on(event: "drop", listener: (data?: DropArgument) => void): void; on(event: "timeout", listener: (socket: Socket) => void): void; on(event: "error", listener: (err: Error) => void): void; /* The WebSocket handover: fires INSTEAD of 'request' for @@ -3520,6 +3552,7 @@ declare module "net" { /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; addListener(event: "close" | "listening", listener: () => void): void; + addListener(event: "drop", listener: (data?: DropArgument) => void): void; addListener(event: "timeout", listener: (socket: Socket) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener( @@ -3532,6 +3565,7 @@ declare module "net" { ): void; once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; once(event: "close" | "listening", listener: () => void): void; + once(event: "drop", listener: (data?: DropArgument) => void): void; once(event: "timeout", listener: (socket: Socket) => void): void; once(event: "error", listener: (err: Error) => void): void; once( @@ -3543,6 +3577,20 @@ declare module "net" { listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, ): void; } + /* The 'drop' payload: the turned-away connection's addresses. */ + export interface DropArgument { + localAddress?: string; + localPort?: number; + localFamily?: string; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + } + export type AddressInfo = import("node:dgram").AddressInfo; + /* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */ + export function isIP(input: string): number; + export function isIPv4(input: string): boolean; + export function isIPv6(input: string): boolean; /* The Socket VALUE — the `x instanceof net.Socket` narrowing target * (the h2 compat 'connect' listener's test). Constructing bare sockets * has no lowering; the construct signature exists for instanceof. */ diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 19e9d1c13..5da0c4d3d 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -250,6 +250,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) e.fn === "net.sockOnConnect" || e.fn === "net.sockOnTimeout" || e.fn === "net.sockOnReadable" || + e.fn === "net.sockOnReady" || e.fn === "http.reqOnEnd" || e.fn === "http.reqOnClose" || e.fn === "http.reqOnAborted" || @@ -268,6 +269,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "net.sockOnConnect": "scr_net_sock_on_connect", "net.sockOnTimeout": "scr_net_sock_on_timeout", "net.sockOnReadable": "scr_net_sock_on_readable", + "net.sockOnReady": "scr_net_sock_on_ready", "http.reqOnEnd": "scr_http_req_on_end", "http.reqOnClose": "scr_http_req_on_close", "http.reqOnAborted": "scr_http_req_on_aborted", @@ -640,5 +642,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } + if (e.fn === "net.sockOnWriteFlush") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[1]!); + host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`); + B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); + return { name: "", type: e.type }; + } + if (e.fn === "net.sockSetTimeoutCb") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[2]!); + host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)"); + B.line( + `call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, + ); + return { name: "", type: e.type }; + } return host.emitGenericLibCall(e); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index ce44e2ce4..84f039f2c 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -973,13 +973,29 @@ export const LIB_FN_SYMS: Record = { "net.sockDestroyed": "scr_net_sock_destroyed", "net.sockWritable": "scr_net_sock_writable", "net.sockSetEncoding": "scr_net_sock_set_encoding", - "net.sockSetTimeout": "scr_net_sock_set_timeout", + "net.sockSetTimeout": "scr_net_sock_set_timeout_checked", "net.sockUnshift": "scr_net_sock_unshift_bytes", "net.sockPause": "scr_net_sock_pause", "net.sockResume": "scr_net_sock_resume", "net.sockSetNoDelay": "scr_net_sock_set_nodelay", "net.sockDestroySoon": "scr_net_sock_destroy_soon", "net.sockBytesWritten": "scr_net_sock_bytes_written", + "net.sockAddrField": "scr_net_sock_addr_field", + "net.sockBytesRead": "scr_net_sock_bytes_read", + "net.sockConnecting": "scr_net_sock_connecting", + "net.sockPending": "scr_net_sock_pending", + "net.sockReadyState": "scr_net_sock_ready_state", + "net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret", + "net.sockOnCloseDyn": "scr_net_sock_on_close_dyn", + "net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn", + "net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn", + "net.isIP": "scr_net_is_ip", + "net.isIPv4": "scr_net_is_ipv4", + "net.isIPv6": "scr_net_is_ipv6", + "net.serverMaxConnectionsGet": "scr_net_server_max_connections_get", + "net.serverMaxConnectionsSet": "scr_net_server_max_connections_set", + "net.serverOnDrop": "scr_net_server_on_drop", + "net.serverGetConnections": "scr_net_server_get_connections", "net.sockReadable": "scr_net_sock_readable", "net.sockPipeRes": "scr_http_sock_pipe_res", "net.serverEmitConnection": "scr_net_server_emit_connection", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 050fe9be3..757020407 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -224,7 +224,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE body: [ { kind: "return", - value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc }, + value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, adapts: true, loc }, loc, }, ], @@ -234,6 +234,30 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE return { kind: "call", callee: name, args: [cb], type: toT, loc }; } +/** A socket or server listener with parameters that the runtime calls + * through the checked-dynamic boundary (close(hadError), write's + * cb(err), 'drop', getConnections): a dyn function value as is, or a + * typed function boxed with per-argument checks. */ +function lowerSocketDynListener(lowerer: Lowerer, node: ts.Expression, what: string): IrExpr { + const cb = lowerer.lowerExpr(node); + if (cb.type.kind === "dyn") return cb; + if ( + cb.type.kind === "func" && + canBoxFuncIntoDyn( + cb.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { kind: "dynFrom", value: cb, type: DYN, loc: locOf(node) }; + } + lowerer.noLowering( + `${what} that cannot cross the checked-dynamic boundary`, + node, + "the callback's parameters and return value must have checked-dynamic conversions", + ); +} + /** A registration/side-effect libCall re-shaped to answer its RECEIVER — * Node's `return this` chaining (server.listen(...), res.writeHead(...)): * an interned helper runs the void call and returns its first argument. @@ -1283,6 +1307,33 @@ export function lowerHttpServerTimeoutAssignment( }; } +/** `server.maxConnections = n`: the value crosses as a dyn (undefined + * and null unset the cap). */ +export function lowerNetServerMaxConnectionsAssignment( + lowerer: Lowerer, + left: ts.Expression, + right: ts.Expression, + loc: SrcLoc, +): IrStmt | null { + if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return null; + if (left.name.text !== "maxConnections") return null; + if (lowerer.mapTypeOf(lowerer.typeOf(left.expression))?.kind !== "netServer") return null; + if (!lowerer.isStdlibMember(left)) return null; + const receiver = coerceToHandle(lowerer, left.expression, NETSERVER_T); + const value = lowerer.coerceToExpected(lowerer.lowerExpr(right), DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "net.serverMaxConnectionsSet", + args: [receiver, value], + type: VOID, + loc, + }, + loc, + }; +} + /** Method calls on net.Server receivers: listen(port[, cb]), close([cb]), * on/once("connection" | "error" | "close", cb). address() alone fences * toward the composed address().port read. Null for other receivers. */ @@ -1296,6 +1347,34 @@ function lowerNetServerMethodCall( const name = access.name.text; const loc = locOf(call); const args = call.arguments; + if (name === "getConnections") { + // cb(null, count) on the next tick; answers the server. + if (args.length !== 1) { + lowerer.noLowering( + `getConnections with ${args.length} arguments`, + call, + "the form is getConnections(callback)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + const cb = lowerSocketDynListener(lowerer, args[0]!, "getConnections callbacks"); + if (resultIsDiscarded(call)) { + return { + kind: "libCall", + fn: "net.serverGetConnections", + args: [receiver, cb], + type: VOID, + loc, + }; + } + return receiverReturningCall( + lowerer, + "net.serverGetConnections", + [receiver, cb], + NETSERVER_T, + loc, + ); + } if (name === "setTimeout") { if (args.length < 1 || args.length > 2) lowerer.noLowering( @@ -1744,6 +1823,17 @@ function lowerNetServerMethodCall( loc, }; } + if (event === "drop") { + // A connection past maxConnections, with its local and remote + // addresses. + return { + kind: "libCall", + fn: "net.serverOnDrop", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "drop listeners"), once], + type: VOID, + loc, + }; + } if (event === "session") { // The h2c server's per-connection session event: (session). The // result is voidized; the handle passes by reference. @@ -1885,7 +1975,7 @@ function lowerNetServerMethodCall( lowerer.noLowering( `server.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"connection", "request", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', + '"connection", "request", "drop", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', ); } if (name === "emit") { @@ -2019,6 +2109,85 @@ function lowerNetSocketMethodCall( } } } + // A trailing callback: write's runs once the data is flushed (with + // null for the error), end's once the socket finishes ('finish'). + const last = args.length > 0 ? args[args.length - 1]! : null; + if ( + last !== null && + args.length <= 2 && + (name === "end" || args.length === 2) && + lowerer.checker.getCallSignatures(lowerer.typeOf(last)).length > 0 + ) { + const sockLocal = lowerer.declareHiddenLocal("%sockCb", NETSOCKET_T); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: sockLocal.id, + init: coerceToHandle(lowerer, access.expression, NETSOCKET_T), + loc, + }, + ]; + const receiver = (): IrExpr => ({ + kind: "varRef", + localId: sockLocal.id, + type: NETSOCKET_T, + loc, + }); + const dataNode = args.length === 2 ? args[0]! : null; + let main: IrExpr = { + kind: "libCall", + fn: "net.sockEnd", + args: [receiver()], + type: VOID, + loc, + }; + if (dataNode !== null) { + const data = lowerer.lowerExpr(dataNode); + const fn: IrLibFn | null = + data.type.kind === "string" + ? name === "write" + ? "net.sockWrite" + : "net.sockEndStr" + : data.type.kind === "bytes" && data.type.elem === "u8" + ? name === "write" + ? "net.sockWriteBytes" + : "net.sockEndBytes" + : data.type.kind === "dyn" + ? name === "write" + ? "net.sockWriteDyn" + : "net.sockEndDyn" + : null; + if (fn === null) { + lowerer.noLowering( + `${name} of '${lowerer.fmt(data.type)}' data`, + dataNode, + NARROW_DATA_HINT, + ); + } + main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc }; + } + const cb = lowerer.lowerExpr(last); + let register: IrExpr; + if (cb.type.kind === "func" && cb.type.params.length === 0 && cb.type.ret.kind === "void") { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlush" : "net.sockOnFinish", + args: [receiver(), cb], + type: VOID, + loc, + }; + } else { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlushDyn" : "net.sockOnFinishDyn", + args: [receiver(), lowerSocketDynListener(lowerer, last, `${name} callbacks`)], + type: VOID, + loc, + }; + } + stmts.push({ kind: "exprStmt", expr: main, loc }); + return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + } const maxArgs = name === "write" ? 1 : 1; const minArgs = name === "write" ? 1 : 0; if (args.length < minArgs || args.length > maxArgs) { @@ -2083,17 +2252,62 @@ function lowerNetSocketMethodCall( } if (name === "setTimeout") { requireStatementPosition(lowerer, call, "socket.setTimeout(...)"); - if (args.length !== 1) { + if (args.length !== 1 && args.length !== 2) { lowerer.noLowering( `setTimeout with ${args.length} arguments`, call, - "the supported form is setTimeout(ms) — register the callback separately: socket.once('timeout', ...)", + "the supported forms are setTimeout(ms) and setTimeout(ms, callback)", ); } const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); const ms = lowerer.lowerExprExpecting(args[0]!, F64); + if (args.length === 2) { + // Node adds the callback as a once('timeout') listener (removes it + // for 0); msecs is checked first either way. + const { cb } = lowerCallbackArg( + lowerer, + args[1]!, + "setTimeout callbacks", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "net.sockSetTimeoutCb", + args: [receiver, ms, cb], + type: VOID, + loc, + }; + } return { kind: "libCall", fn: "net.sockSetTimeout", args: [receiver, ms], type: VOID, loc }; } + if (name === "setKeepAlive") { + // SO_KEEPALIVE plus the initial delay (whole seconds, at least 1); + // Node's defaults are false and 0. Answers the socket. + if (args.length > 2) { + lowerer.noLowering( + `setKeepAlive with ${args.length} arguments`, + call, + "the form is setKeepAlive(enable?, initialDelay?)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); + const enable: IrExpr = + args.length >= 1 ? lowerer.lowerExprExpecting(args[0]!, BOOL) : boolLit(false, loc); + const delay: IrExpr = + args.length === 2 + ? lowerer.lowerExprExpecting(args[1]!, F64) + : { kind: "numLit", value: 0, type: F64, loc }; + return { + kind: "libCall", + fn: "net.sockSetKeepAlive", + args: [receiver, enable, delay], + type: NETSOCKET_T, + loc, + }; + } if (name === "pause" || name === "resume") { // Flow control (the struct's flag comments in scr_net.c): pause holds // reads off — kernel/TCP backpressure is the buffer; resume flows @@ -2212,6 +2426,31 @@ function lowerNetSocketMethodCall( const event = evT.isStringLiteralType() ? evT.value : null; const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); if (event === "data") { + const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!)); + const firstParam = sigs.length === 1 ? sigs[0]!.getParameters()[0] : undefined; + const param = + firstParam !== undefined + ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(firstParam)) + : null; + if ( + param !== null && + param.kind !== "dyn" && + !(param.kind === "bytes" && param.elem === "u8") + ) { + // A chunk typed string (or string | Buffer): the listener takes + // the chunk through a checked boundary, so a string arrives after + // setEncoding and a Buffer without it fails the check instead of + // being decoded. + const boxed = lowerSocketDynListener(lowerer, args[1]!, "data listeners"); + const cb: IrExpr = { kind: "dynCheck", value: boxed, type: funcOf([DYN], VOID), loc }; + return { + kind: "libCall", + fn: "net.sockOnData", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } const { cb } = lowerCallbackArg( lowerer, args[1]!, @@ -2241,10 +2480,26 @@ function lowerNetSocketMethodCall( loc, }; } + if ( + event === "close" && + lowerer.checker + .getCallSignatures(lowerer.typeOf(args[1]!)) + .some((sig) => sig.getParameters().length > 0) + ) { + // close(hadError): true when the socket closed because of an error. + return { + kind: "libCall", + fn: "net.sockOnCloseDyn", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "close listeners"), once], + type: VOID, + loc, + }; + } if ( event === "end" || event === "close" || event === "connect" || + event === "ready" || event === "timeout" || event === "readable" ) { @@ -2266,7 +2521,9 @@ function lowerNetSocketMethodCall( ? "net.sockOnTimeout" : event === "readable" ? "net.sockOnReadable" - : "net.sockOnConnect"; + : event === "ready" + ? "net.sockOnReady" + : "net.sockOnConnect"; return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } if (event === "finish") { @@ -2328,13 +2585,13 @@ function lowerNetSocketMethodCall( lowerer.noLowering( `socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"data", "end", "close", "error", "connect", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', + '"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', ); } lowerer.noLowering( `Socket.${name}`, call, - "write, end, destroy, pipe, setTimeout, remoteAddress, read, unshift, and on/once of data/end/close/error/connect/timeout/readable are the supported Socket members", + "write, end, destroy, pipe, setTimeout, setKeepAlive, setNoDelay, the address fields, read, unshift, and on/once of data/end/close/error/connect/ready/timeout/readable are the supported Socket members", lowerer.checker.getSymbolAtLocation(access.name), ); } @@ -2978,6 +3235,25 @@ export function lowerServerProperty( const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); return { kind: "libCall", fn: "net.serverListening", args: [receiver], type: BOOL, loc }; } + if (expr.name.text === "maxConnections") { + const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.serverMaxConnectionsGet", + args: [receiver], + type: DYN, + loc, + }; + // The property's declared type: an earlier `= undefined` narrows + // the read's own type to undefined. + const sym = lowerer.checker.getSymbolAtLocation(expr.name); + const declared = lowerer.mapTypeOf( + sym ? lowerer.checker.getTypeOfSymbol(sym) : lowerer.typeOf(expr), + ); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } const field = httpServerTimeoutField(expr.name.text); if (field !== null) { const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); @@ -3205,6 +3481,46 @@ export function lowerServerProperty( loc, }; } + if (recvKind === "netSocket" && lowerer.isStdlibMember(expr)) { + const name = expr.name.text; + // The other address fields: the peer's cached like remoteAddress, the + // local ones read live (undefined while connecting and after close). + const field = [ + "remotePort", + "remoteFamily", + "localAddress", + "localPort", + "localFamily", + ].indexOf(name); + if (field >= 0) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.sockAddrField", + args: [receiver, { kind: "numLit", value: field, type: F64, loc }], + type: DYN, + loc, + }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } + const scalar: { fn: IrLibFn; type: IrType } | null = + name === "bytesRead" + ? { fn: "net.sockBytesRead", type: F64 } + : name === "connecting" + ? { fn: "net.sockConnecting", type: BOOL } + : name === "pending" + ? { fn: "net.sockPending", type: BOOL } + : name === "readyState" + ? { fn: "net.sockReadyState", type: STRING } + : null; + if (scalar !== null) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + return { kind: "libCall", fn: scalar.fn, args: [receiver], type: scalar.type, loc }; + } + } if ( recvKind === "httpClientReq" && lowerer.isStdlibMember(expr) && diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 5d6305aa8..5ae5bef1a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -126,6 +126,7 @@ import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-strea import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, + lowerNetServerMaxConnectionsAssignment, lowerServerCloseOverrideAssignment, } from "./lower-server.js"; import { lowerUrlAssignment } from "./builtins/url.js"; @@ -6166,6 +6167,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); if (timeoutProp) return timeoutProp; } + // server.maxConnections — net.Server's connection cap. + { + const capProp = lowerNetServerMaxConnectionsAssignment( + lowerer, + expr.left, + expr.right, + locOf(expr), + ); + if (capProp) return capProp; + } // `N.x = v` — a namespace-qualified write: exported `let` // members are module globals, so the qualified spelling assigns // exactly like the bare one inside the namespace body. diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 939b12776..a4cd79bfe 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1763,6 +1763,10 @@ export const BUILTIN_MODULE_FNS: Record< params: [F64], result: VOID, }, + // Node's IPv4Reg / IPv6Reg tests over String(input). + isIP: { fn: "net.isIP", params: [DYN], result: F64 }, + isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, + isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, }, http: {}, // node:tls and node:https ride the same spoke (tls.createServer's diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 555ba9efc..c18e5a990 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3031,6 +3031,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ), }; } + // net's 'drop' payload: the record the runtime's address object checks + // into, each optional field with its declared type. + if (isStdlibInterface("DropArgument")) { + const known = [ + "localAddress", + "localPort", + "localFamily", + "remoteAddress", + "remotePort", + "remoteFamily", + ]; + const fields: { name: string; type: IrType }[] = []; + for (const property of checker.getPropertiesOfType(widened)) { + if (!known.includes(property.name)) return null; + const type = mapType(checker.getTypeOfSymbol(property), ctx); + if (!type) return null; + fields.push({ name: property.name, type }); + } + const order = fields.map((field) => field.name); + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; + } // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index f73633f01..20b896112 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -182,6 +182,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.nextChunkDyn", // The checked-dynamic chunk forms throw Node's ERR_INVALID_ARG_TYPE // chunk TypeError on a non-string/Buffer dyn kind. + // socket.setTimeout's msecs check (ERR_OUT_OF_RANGE). + "net.sockSetTimeout", + "net.sockSetTimeoutCb", "net.sockWriteDyn", "net.sockEndDyn", "http.resWriteDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index dc2aeb7b6..efd568238 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1686,6 +1686,38 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), + * localFamily (4): dyn values, undefined when unknown. */ + "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, + "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, + "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, + "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, + "net.sockOnReady": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** Listeners with a parameter, as dyn functions: close(hadError), + * write's cb(err) and end's cb. */ + "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + "net.sockOnWriteFlush": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockSetTimeoutCb": { + argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.isIP": { argTypes: [DYN], result: F64 }, + "net.isIPv4": { argTypes: [DYN], result: BOOL }, + "net.isIPv6": { argTypes: [DYN], result: BOOL }, + "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, + "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out * (sweep-deferred, never the registering stack). */ diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index b4fcc0a07..a27748984 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1394,6 +1394,24 @@ "name": "net.getDefaultAutoSelectFamilyAttemptTimeout", "status": "static" }, + { + "id": "node-builtin.net.isIP", + "kind": "node-builtin", + "name": "net.isIP", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv4", + "kind": "node-builtin", + "name": "net.isIPv4", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv6", + "kind": "node-builtin", + "name": "net.isIPv6", + "status": "static" + }, { "id": "node-builtin.net.setDefaultAutoSelectFamilyAttemptTimeout", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json new file mode 100644 index 000000000..0f0c242e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5644-net-socket-addresses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json new file mode 100644 index 000000000..b4277d87a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5645-net-socket-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json new file mode 100644 index 000000000..f7618cd63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5646-net-server-connections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json new file mode 100644 index 000000000..2664352c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5647-net-is-ip.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 461acbedc..8293759ee 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -330,6 +331,62 @@ void scr_net_fire0_this(ScrNetLs *l, void *self, ScrDynHandleTag tag) { void scr_net_fire0(ScrNetLs *l) { scr_net_fire0_this(l, NULL, 0); } +/* A list mixing zero-argument closures (fn NULL) with dyn listeners + * (fn = a ScrNetArgFn) — the close(hadError) and write(chunk, cb(err)) + * shapes: the dyn ones receive `arg`. */ +typedef void (*ScrNetArgFn)(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_fire_arg_this(ScrNetLs *l, const ScrDyn *arg, void *self, ScrDynHandleTag tag) { + ScrNetL *snap; + size_t n = scr_net_ls_snapshot(l, &snap); + scr_dyn_this_push(self, tag); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) { + if (snap[i].fn != NULL) ((ScrNetArgFn)snap[i].fn)(snap[i].cb, arg); + else ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + } + scr_closure_release(snap[i].cb); + } + scr_dyn_this_pop(); + free(snap); +} + +/* The dyn listener's call with one argument (none for NULL). */ +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg) { + ScrDyn *fn = scr_dyn_listener_fn(cb); + ScrDyn *args[1] = { (ScrDyn *)arg }; + scr_dyn_release(scr_dyn_call(fn, args, arg != NULL ? 1 : 0, "listener")); + scr_dyn_release(fn); +} + +/* The JS function a listener entry stands for: the boxed function of a + * dyn listener closure, otherwise the closure's own identity. */ +static ScrClosure *scr_net_listener_root(const ScrNetL *e) { + ScrClosure *c = e->cb; + if (e->fn == (void *)&scr_net_dyn_fire_arg || c->fn == (void *)&scr_dyn_listener_fire0) { + ScrDyn *boxed = scr_dyn_listener_fn(c); + ScrClosure *inner = boxed->kind == SCR_DYN_FUNC ? boxed->v.fn.clo : NULL; + scr_dyn_release(boxed); + if (inner != NULL) return inner->identity ? inner->identity : inner; + } + return c->identity ? c->identity : c; +} + +/* The listener standing for the same function as `root` (through any + * adapter) leaves the list: removeListener's first match. */ +static void scr_net_ls_remove_fn(ScrNetLs *l, ScrClosure *root) { + root = root->identity ? root->identity : root; + for (size_t i = 0; i < l->n; i++) { + ScrClosure *c = l->ls[i].cb; + if (scr_net_listener_root(&l->ls[i]) != root) continue; + scr_closure_release(c); + scr_net_once_release(l->ls[i].shared_once); + memmove(&l->ls[i], &l->ls[i + 1], (l->n - i - 1) * sizeof *l->ls); + l->n--; + return; + } +} + /* Fire an error list through the ScrChildErrFn adapters (msg borrowed), * the emitting handle bound as the ambient receiver. An 'error' with NO * listener is fatal, like Node's unhandled 'error' EventEmitter throw: @@ -387,6 +444,10 @@ struct ScrNetServer { ScrStr *pending_err; /* deferred listen failure */ size_t nconns; ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls; + /* server.maxConnections (unset = undefined) and its 'drop' listeners */ + bool max_conns_set; + double max_conns; + ScrNetLs drop_ls; /* protocol layer (scr_http.c): a C-level connection consumer + its * context, freed with the handle */ ScrNetNativeConnFn native_conn; @@ -448,6 +509,8 @@ struct ScrNetSocket { * value read while connected survives destroy; never-read sockets * answer undefined after close */ ScrStr *remote_cache; + int remote_port_cache; + bool remote_v6_cache; ScrStr *pending_err; /* deferred connect failure */ /* the caller-lookup dial (net.connect with a lookup option): true while * the lookup decides; dial_ips holds its answered addresses (+1 each), @@ -483,6 +546,8 @@ struct ScrNetSocket { * buffer to drain (this surface's flush moment). */ ScrNetLs finish_ls, wcb_ls; bool finish_pending; /* wr_done seen: fire finish_ls at the next sweep */ + ScrNetLs ready_ls; /* 'ready': right after the client's 'connect' listeners */ + size_t bytes_read; /* socket.bytesRead: every byte delivered from the peer */ /* a server whose 'connection' waits for this socket's handshake — the * TLS defer target (+1; the socket's own `server` backref stays the * ACCEPTING server, which may be the demux wrapper) */ @@ -549,6 +614,7 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->close_ls); scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); + scr_net_ls_drop(&s->drop_ls); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_str_release(s->bound_host); @@ -583,6 +649,7 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->readable_ls); scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); + scr_net_ls_drop(&s->ready_ls); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -1164,6 +1231,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { return; } scr_net_sock_touch(s); + s->bytes_read += (size_t)n; if (flowing) { if (!scr_net_sock_deliver(s, buf, (size_t)n)) return; } else if (s->readable_ls.n == 0) { @@ -1260,6 +1328,8 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); } @@ -1312,6 +1382,8 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); scr_net_sock_update_read(s); scr_net_sock_update_write(s); @@ -1679,6 +1751,51 @@ void scr_net_server_close_idle_connections(ScrNetServer *s) { scr_net_server_close_http_connections(s, true); } +/* server.maxConnections: undefined until set; a set number turns away + * connections once that many are open ('drop'). Setting undefined or + * null unsets it. +1 for the getter. */ +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s) { + return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : scr_dyn_retain(scr_dyn_undefined()); +} + +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { + s->max_conns_set = false; + return; + } + double n = NAN; + scr_dyn_number_coerce_js(value, &n); + s->max_conns_set = true; + s->max_conns = n; +} + +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->drop_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +/* server.getConnections(cb): cb(null, count) on the next tick, with the + * count at the call, as Node's process.nextTick(end, null, count). */ +static void scr_net_get_connections_fire(ScrClosure *self) { + ScrDyn *fn = (ScrDyn *)scr_box_get_ref(self->caps[0]); + ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_num(scr_box_get_f64(self->caps[1])) }; + scr_dyn_release(scr_dyn_call(fn, args, 2, "callback")); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); +} + +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/) { + ScrClosure *clo = scr_dyn_listener_closure_fn(callback, (void *)&scr_net_get_connections_fire); + ScrClosure *job = scr_closure_new((void *)&scr_net_get_connections_fire, 2); + job->caps[0] = scr_box_retain(clo->caps[0]); + job->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(job->caps[1], (double)s->nconns); + scr_closure_release(clo); + scr_next_tick(job); +} + /* wrapper.close = fn: the override MOVES in (a compiler-emitted zero-arg * wrapper — see the struct comment); reassignment releases the old one, * matching JS's last-write-wins. */ @@ -1748,6 +1865,48 @@ void scr_net_server_on_timeout(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNet scr_net_ls_add(&s->timeout_ls, cb, (void *)fn, once); } +/* One end of a dropped connection as Node's 'drop' data reports it. */ +static void scr_net_drop_side(ScrDyn *data, const char *prefix, const struct sockaddr_storage *ss) { + char addr[INET6_ADDRSTRLEN] = ""; + int port = 0; + bool v6 = ss->ss_family == AF_INET6; + if (v6) { + inet_ntop(AF_INET6, &((const struct sockaddr_in6 *)ss)->sin6_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in6 *)ss)->sin6_port); + } else { + inet_ntop(AF_INET, &((const struct sockaddr_in *)ss)->sin_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in *)ss)->sin_port); + } + char key[24]; + int n = snprintf(key, sizeof key, "%sAddress", prefix); + ScrStr *a = scr_str_new(addr, strlen(addr)); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(a)); + scr_str_release(a); + n = snprintf(key, sizeof key, "%sPort", prefix); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_num(port)); + n = snprintf(key, sizeof key, "%sFamily", prefix); + ScrStr *f = scr_str_new(v6 ? "IPv6" : "IPv4", 4); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(f)); + scr_str_release(f); +} + +/* A connection past server.maxConnections: 'drop' with the local and + * remote addresses (a null-prototype object in Node), then the close. */ +static void scr_net_server_drop_conn(ScrNetServer *srv, int fd) { + if (srv->drop_ls.n > 0) { + ScrDyn *data = scr_dyn_new_obj(); + data->null_proto = true; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "local", &ss); + len = sizeof ss; + if (getpeername(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "remote", &ss); + scr_net_fire_arg_this(&srv->drop_ls, data, srv, SCR_DYNH_NET_SERVER); + scr_dyn_release(data); + } + scr_net_close_fd_raw(fd); +} + /* Accept until EAGAIN; each connection fires the 'connection' listeners * directly (the accepted socket passes +1 per listener via the adapter). */ static void scr_net_server_accept(ScrNetServer *srv) { @@ -1761,6 +1920,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { scr_net_nonblock(fd); int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + if (srv->max_conns_set && (double)srv->nconns >= srv->max_conns) { + /* server.maxConnections: Node closes the connection and emits + * 'drop' with the addresses instead of 'connection' */ + scr_net_server_drop_conn(srv, fd); + if (scr_exc_pending()) return; + continue; + } ScrNetSocket *sock = scr_net_sock_new(); sock->fd = fd; sock->server = scr_net_server_retain(srv); @@ -2307,6 +2473,19 @@ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms) { #endif } +/* socket.setKeepAlive(enable, initialDelay) as the program calls it: + * answers the socket (+1), Node's chaining. */ +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms) { + scr_net_sock_set_keepalive(s, enable, delay_ms); + return scr_net_sock_retain(s); +} + +/* end(callback) with a dyn callback: once 'finish', called without + * arguments. */ +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + scr_net_sock_on_finish(s, scr_dyn_listener_closure0(listener)); +} + /* socket.destroySoon(): end the write half now, destroy once the FIN is * actually out (buffered bytes flush first — Node's 'finish'-then-destroy). */ void scr_net_sock_destroy_soon(ScrNetSocket *s) { @@ -2377,7 +2556,8 @@ bool scr_net_sock_encrypted(ScrNetSocket *s) { return s->tops != NULL; } ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { if (s->remote_cache) return scr_str_retain(s->remote_cache); - if (s->fd < 0) return NULL; + /* Node's _getpeername answers {} while connecting */ + if (s->fd < 0 || scr_net_sock_connecting(s)) return NULL; struct sockaddr_storage ss; socklen_t len = sizeof ss; if (getpeername(s->fd, (struct sockaddr *)&ss, &len) != 0) return NULL; @@ -2390,9 +2570,251 @@ ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { } if (!out) return NULL; s->remote_cache = scr_str_new(out, strlen(out)); + s->remote_v6_cache = ss.ss_family == AF_INET6; + s->remote_port_cache = ntohs(ss.ss_family == AF_INET6 ? ((struct sockaddr_in6 *)&ss)->sin6_port + : ((struct sockaddr_in *)&ss)->sin_port); return scr_str_retain(s->remote_cache); } +/* socket.remotePort / remoteFamily: the same cached peer read as + * remoteAddress; NaN / NULL answer undefined. */ +double scr_net_sock_remote_port(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + return s->remote_cache ? (double)s->remote_port_cache : NAN; +} + +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + if (!s->remote_cache) return NULL; + return s->remote_v6_cache ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* socket.localAddress / localPort / localFamily: read from the open + * socket (undefined while connecting and after close, as in Node). */ +static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) { + if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(s->fd, (struct sockaddr *)&ss, &len) != 0) return false; + if (ss.ss_family == AF_INET) { + if (!inet_ntop(AF_INET, &((struct sockaddr_in *)&ss)->sin_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in *)&ss)->sin_port); + *v6 = false; + return true; + } + if (ss.ss_family == AF_INET6) { + if (!inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&ss)->sin6_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port); + *v6 = true; + return true; + } + return false; +} + +ScrStr *scr_net_sock_local_address(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? scr_str_new(addr, strlen(addr)) : NULL; +} + +double scr_net_sock_local_port(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? (double)port : NAN; +} + +ScrStr *scr_net_sock_local_family(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + if (!scr_net_sock_local(s, addr, sizeof addr, &port, &v6)) return NULL; + return v6 ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* The address fields as dyn values (+1): remotePort (0), remoteFamily + * (1), localAddress (2), localPort (3) and localFamily (4), each + * undefined when unknown. */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field) { + int f = (int)field; + if (f == 0 || f == 3) { + double port = f == 0 ? scr_net_sock_remote_port(s) : scr_net_sock_local_port(s); + return isnan(port) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(port); + } + ScrStr *v = f == 1 ? scr_net_sock_remote_family(s) + : f == 2 ? scr_net_sock_local_address(s) + : scr_net_sock_local_family(s); + if (v == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *d = scr_dyn_new_str(v); + scr_str_release(v); + return d; +} + +double scr_net_sock_bytes_read(ScrNetSocket *s) { return (double)s->bytes_read; } + +/* socket.connecting: a dial in progress (deferred, looking up, or + * awaiting the TCP handshake). */ +bool scr_net_sock_connecting(ScrNetSocket *s) { + return s->fd >= 0 ? s->connecting : (s->dial_deferred || s->lookup_wait); +} + +/* socket.pending: no open handle yet (or anymore), or still connecting. */ +bool scr_net_sock_pending(ScrNetSocket *s) { return s->fd < 0 || scr_net_sock_connecting(s); } + +/* socket.readyState, Node's getter: opening while connecting, then by + * which halves are still readable and writable. +1. */ +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s) { + const char *state; + if (scr_net_sock_connecting(s)) state = "opening"; + else { + bool r = scr_net_sock_readable(s), w = scr_net_sock_writable(s); + state = r && w ? "open" : r ? "readOnly" : w ? "writeOnly" : "closed"; + } + return scr_str_new(state, strlen(state)); +} + +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->ready_ls, cb, NULL, once); +} + +/* close(hadError) and write(chunk, cb(err)) listeners as dyn functions, + * called with the hadError boolean and null. */ +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->close_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + if (s->close_emitted) return; + scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, true); +} + +/* socket.setTimeout(ms[, callback]) as the program calls it: Node's + * msecs check (a non-negative finite number), then the timer; a callback + * becomes a once('timeout') listener, and with 0 it is removed. */ +static bool scr_net_timeout_msecs_valid(double ms) { + if (isfinite(ms) && ms >= 0) return true; + char recv[48], msg[160]; + scr_num_received(ms, recv); + int len = snprintf(msg, sizeof msg, + "The value of \"msecs\" is out of range. It must be a non-negative finite number. " + "Received %s", + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms) { + if (!scr_net_timeout_msecs_valid(ms)) return; + scr_net_sock_set_timeout(s, ms); +} + +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/) { + if (!scr_net_timeout_msecs_valid(ms)) { + scr_closure_release(cb); + return; + } + scr_net_sock_set_timeout(s, ms); + if (ms == 0) { + scr_net_ls_remove_fn(&s->timeout_ls, cb); + scr_closure_release(cb); + return; + } + scr_net_sock_on_timeout(s, cb, true); +} + +/* net.isIP / isIPv4 / isIPv6, as Node's IPv4Reg and IPv6Reg match: dotted + * decimal without leading zeros, and the RFC 4291 text forms (one "::" + * standing for at least one group, an IPv4 tail as two groups) with an + * optional %zone. */ +static bool scr_net_ipv4_text(const char *p, size_t n) { + size_t i = 0; + for (int part = 0; part < 4; part++) { + if (part > 0) { + if (i >= n || p[i] != '.') return false; + i++; + } + size_t start = i; + int value = 0; + while (i < n && p[i] >= '0' && p[i] <= '9' && i - start < 3) value = value * 10 + (p[i++] - '0'); + size_t digits = i - start; + if (digits == 0 || (digits > 1 && p[start] == '0') || value > 255) return false; + } + return i == n; +} + +static bool scr_net_ipv6_text(const char *p, size_t n) { + /* an optional zone: '%' and at least one of [0-9a-zA-Z-.:] */ + for (size_t i = 0; i < n; i++) { + if (p[i] != '%') continue; + if (i + 1 == n) return false; + for (size_t j = i + 1; j < n; j++) { + char c = p[j]; + if (!(isalnum((unsigned char)c) || c == '-' || c == '.' || c == ':')) return false; + } + n = i; + break; + } + int groups = 0; + bool compressed = false; + size_t i = 0; + if (n >= 2 && p[0] == ':' && p[1] == ':') { + compressed = true; + i = 2; + if (i == n) return true; + } else if (n >= 1 && p[0] == ':') { + return false; + } + while (i < n) { + size_t start = i; + while (i < n && isxdigit((unsigned char)p[i]) && i - start < 5) i++; + size_t digits = i - start; + if (i < n && p[i] == '.') { + /* the IPv4 tail ends the address */ + if (!scr_net_ipv4_text(p + start, n - start)) return false; + groups += 2; + i = n; + break; + } + if (digits == 0 || digits > 4) return false; + groups++; + if (i == n) break; + if (p[i] != ':') return false; + i++; + if (i < n && p[i] == ':') { + if (compressed) return false; + compressed = true; + i++; + if (i == n) break; + } else if (i == n) { + return false; /* a single trailing ':' */ + } + } + return compressed ? groups <= 7 : groups == 8; +} + +static ScrStr *scr_net_ip_input(const ScrDyn *input) { + return input->kind == SCR_DYN_STR ? scr_str_retain(input->v.str) : scr_dyn_string_coerce_js(input); +} + +double scr_net_is_ip(const ScrDyn *input) { + ScrStr *t = scr_net_ip_input(input); + if (!t) return 0; + double r = scr_net_ipv4_text(t->data, t->len) ? 4 : scr_net_ipv6_text(t->data, t->len) ? 6 : 0; + scr_str_release(t); + return r; +} + +bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; } +bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; } + void scr_net_sock_destroy(ScrNetSocket *s) { if (s->fd < 0 && !s->in_registry) return; s->rd_eof = true; @@ -2898,7 +3320,9 @@ static void scr_net_sweep(void) { * stack. A dead socket fires them too (the buffered bytes are * gone either way; Node errors them — documented divergence, * the no-backpressure stance). */ - scr_net_fire0_this(&sock->wcb_ls, sock, SCR_DYNH_NET_SOCKET); + ScrDyn *no_error = scr_dyn_new_null(); + scr_net_fire_arg_this(&sock->wcb_ls, no_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(no_error); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -2954,7 +3378,11 @@ static void scr_net_sweep(void) { return; } } - scr_net_fire0_this(&sock->close_ls, sock, SCR_DYNH_NET_SOCKET); + { + ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); + scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(had_error); + } /* settle: listeners drop (cycle story) */ scr_net_ls_drop(&sock->data_ls); scr_net_ls_drop(&sock->end_ls); @@ -2965,6 +3393,7 @@ static void scr_net_sweep(void) { scr_net_ls_drop(&sock->readable_ls); scr_net_ls_drop(&sock->finish_ls); scr_net_ls_drop(&sock->wcb_ls); + scr_net_ls_drop(&sock->ready_ls); if (sock->pipe_dst) { scr_net_sock_release(sock->pipe_dst); sock->pipe_dst = NULL; @@ -3359,13 +3788,23 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho scr_dyn_arg_type_fail("msecs", "of type number", ms); return NULL; } - scr_net_sock_set_timeout(s, ms->v.num); - if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { - scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); - } else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF && args[1]->kind != SCR_DYN_FUNC) { scr_dyn_check_listener(args[1], "callback"); return NULL; } + if (!scr_net_timeout_msecs_valid(ms->v.num)) return NULL; + scr_net_sock_set_timeout(s, ms->v.num); + if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { + if (ms->v.num == 0) scr_net_ls_remove_fn(&s->timeout_ls, args[1]->v.fn.clo); + else scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); + } + return scr_dyn_retain(self); + } + if (strcmp(method, "setKeepAlive") == 0) { + bool enable = argc > 0 && scr_dyn_truthy(args[0]); + double delay = 0; + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) scr_dyn_number_coerce_js(args[1], &delay); + scr_net_sock_set_keepalive(s, enable, delay); return scr_dyn_retain(self); } if (strcmp(method, "setEncoding") == 0) { @@ -3410,7 +3849,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho if (scr_net_dynh_tls->invoke(s, method, args, argc, &out)) return out; } { - static const char *const known[] = { "connect", "setKeepAlive", + static const char *const known[] = { "connect", "address", "cork", "uncork", "read", "unpipe", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL }; @@ -3447,6 +3886,28 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { return scr_net_sock_encrypted(s) ? scr_dyn_new_bool(true) : NULL; } if (strcmp(key, "bytesWritten") == 0) return scr_dyn_new_num(scr_net_sock_bytes_written(s)); + if (strcmp(key, "bytesRead") == 0) return scr_dyn_new_num(scr_net_sock_bytes_read(s)); + if (strcmp(key, "connecting") == 0) return scr_dyn_new_bool(scr_net_sock_connecting(s)); + if (strcmp(key, "pending") == 0) return scr_dyn_new_bool(scr_net_sock_pending(s)); + if (strcmp(key, "readyState") == 0) { + ScrStr *state = scr_net_sock_ready_state(s); + ScrDyn *d = scr_dyn_new_str(state); + scr_str_release(state); + return d; + } + { + static const char *const fields[] = { "remotePort", "remoteFamily", "localAddress", "localPort", + "localFamily", NULL }; + for (size_t i = 0; fields[i]; i++) { + if (strcmp(key, fields[i]) != 0) continue; + ScrDyn *d = scr_net_sock_addr_field(s, (double)i); + if (d->kind == SCR_DYN_UNDEF) { + scr_dyn_release(d); + return NULL; + } + return d; + } + } if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s)); if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) { /* Node's default stream highWaterMark — a constant here (backpressure @@ -3458,9 +3919,7 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { if (scr_net_dynh_tls->get(s, key, &out)) return out; } { - static const char *const known[] = { "remotePort", "remoteFamily", "localAddress", - "localPort", "localFamily", "bytesRead", "connecting", "pending", - "readyState", "bufferSize", "timeout", "closed", "errored", NULL }; + static const char *const known[] = { "bufferSize", "timeout", "closed", "errored", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_unsupported(key, NULL); @@ -3643,6 +4102,15 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method scr_net_server_set_ref((ScrNetServer *)h, method[0] == 'r'); return scr_dyn_retain(self); } + if (strcmp(method, "getConnections") == 0) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + scr_dyn_check_listener(cb, "callback"); + return NULL; + } + scr_net_server_get_connections((ScrNetServer *)h, cb); + return scr_dyn_retain(self); + } if (strcmp(method, "toString") == 0) { ScrStr *str = scr_str_new("[object Object]", 15); ScrDyn *d = scr_dyn_new_str(str); @@ -3650,7 +4118,7 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method return d; } { - static const char *const known[] = { "setTimeout", "getConnections", + static const char *const known[] = { "setTimeout", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "closeAllConnections", "closeIdleConnections", NULL }; @@ -3690,7 +4158,8 @@ static ScrDyn *scr_net_dynh_srv_get(void *h, const char *key, size_t key_len) { return scr_dyn_new_num(scr_net_server_timeout_get(s, (double)timeout_field)); } { - static const char *const known[] = { "maxConnections", "connections", "maxHeadersCount", NULL }; + if (strcmp(key, "maxConnections") == 0) return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : NULL; + static const char *const known[] = { "connections", "maxHeadersCount", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_srv_unsupported(key, NULL); @@ -3714,6 +4183,10 @@ static bool scr_net_dynh_srv_set(void *h, const char *key, size_t key_len, const } return true; } + if (strcmp(key, "maxConnections") == 0) { + scr_net_server_max_connections_set(s, value); + return true; + } return false; } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 117a242c7..ad81ac200 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -162,6 +162,10 @@ ScrStr *scr_net_server_addr_family(ScrNetServer *s); /* +1 — address().family void scr_net_server_close(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/); void scr_net_server_close_all_connections(ScrNetServer *s); void scr_net_server_close_idle_connections(ScrNetServer *s); +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s); /* +1 */ +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/); +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/); /* The REAL close behind `wrapper.close.bind(wrapper)` — never consults * the override (the proxy-through idiom cannot recurse). */ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/); @@ -233,6 +237,26 @@ void scr_net_sock_dial_start(ScrNetSocket *s); void scr_net_sock_set_timeout(ScrNetSocket *s, double ms); void scr_net_sock_on_timeout(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); ScrStr *scr_net_sock_remote_address(ScrNetSocket *s); /* +1 or NULL (undefined arm) */ +double scr_net_sock_remote_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s); /* +1 or NULL */ +ScrStr *scr_net_sock_local_address(ScrNetSocket *s); /* +1 or NULL */ +double scr_net_sock_local_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_local_family(ScrNetSocket *s); /* +1 or NULL */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field); /* +1 */ +double scr_net_sock_bytes_read(ScrNetSocket *s); +bool scr_net_sock_connecting(ScrNetSocket *s); +bool scr_net_sock_pending(ScrNetSocket *s); +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s); /* +1 */ +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms); /* +1 */ +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms); +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/); +double scr_net_is_ip(const ScrDyn *input); +bool scr_net_is_ipv4(const ScrDyn *input); +bool scr_net_is_ipv6(const ScrDyn *input); bool scr_net_sock_encrypted(ScrNetSocket *s); /* true = TLS transport (false maps to the undefined arm) */ void scr_net_sock_pipe(ScrNetSocket *src, ScrNetSocket *dst); void scr_net_sock_on_data(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once); diff --git a/tests/corpus/5644-net-socket-addresses.ts b/tests/corpus/5644-net-socket-addresses.ts new file mode 100644 index 000000000..0053105cd --- /dev/null +++ b/tests/corpus/5644-net-socket-addresses.ts @@ -0,0 +1,69 @@ +// A socket's address fields, byte counts and state: the remote fields +// are cached at first read and stay readable after close, the local ones +// are undefined while connecting and after close, and readyState follows +// the open halves. +import * as net from "node:net"; + +const server = net.createServer((sock) => { + const port = (server.address() as net.AddressInfo).port; + console.log( + "server side", + sock.remoteAddress, + sock.remoteFamily, + typeof sock.remotePort, + sock.localAddress, + sock.localPort === port, + sock.localFamily, + ); + sock.on("data", (d: Buffer) => { + console.log("server got", d.toString(), sock.bytesRead); + sock.end("bye"); + }); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + console.log( + "before connect", + c.readyState, + c.connecting, + c.pending, + c.remoteAddress, + c.remotePort, + c.localAddress, + c.localPort, + c.bytesRead, + ); + c.on("connect", () => { + console.log( + "connected", + c.readyState, + c.connecting, + c.pending, + c.remotePort === port, + c.remoteFamily, + c.localAddress, + typeof c.localPort, + c.localFamily, + ); + c.write("hello"); + }); + c.on("data", (d: Buffer) => console.log("client got", d.toString(), c.bytesRead)); + c.on("end", () => console.log("end", c.readyState, c.bytesRead, c.bytesWritten)); + c.on("close", () => { + console.log( + "closed", + c.readyState, + c.pending, + c.remoteAddress, + c.remotePort === port, + c.localAddress, + c.localPort, + ); + const d = net.connect(port, "127.0.0.1"); + d.destroy(); + console.log("destroyed before connect", d.readyState, d.connecting, d.pending, d.remoteAddress); + d.on("close", () => server.close()); + }); +}); diff --git a/tests/corpus/5645-net-socket-callbacks.ts b/tests/corpus/5645-net-socket-callbacks.ts new file mode 100644 index 000000000..9bc670dc5 --- /dev/null +++ b/tests/corpus/5645-net-socket-callbacks.ts @@ -0,0 +1,65 @@ +// write and end callbacks, close listeners with hadError, the 'ready' +// event, string 'data' chunks after setEncoding, setTimeout with a callback (and its msecs checks) and +// setKeepAlive's chaining. +import * as net from "node:net"; + +const events: string[] = []; +let serverSockets = 0; +const server = net.createServer((sock) => { + sock.setEncoding("utf8"); + sock.on("data", (d: string) => { + events.push(`server data ${typeof d} ${d}`); + sock.end("pong", () => events.push("server end callback")); + }); + // the first connection's server side (the timeout connection's close + // order against the client's depends on the event loop phase) + if (serverSockets++ === 0) { + sock.on("close", (hadError: boolean) => events.push(`server close ${hadError}`)); + } +}); + +function attempt(label: string, f: () => void): void { + try { + f(); + events.push(`${label} ok`); + } catch (err) { + const e = err as NodeJS.ErrnoException; + events.push(`${label} ${e.name} ${e.code} ${e.message}`); + } +} + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + c.on("connect", () => { + events.push("connect"); + c.write("ping", (err?: Error | null) => events.push(`write callback ${err === null}`)); + events.push(`setKeepAlive chains ${c.setKeepAlive(true, 1000) === c}`); + }); + c.on("ready", () => events.push("ready")); + c.on("data", (d: Buffer) => events.push(`client data ${d.toString()}`)); + c.on("end", () => { + events.push("client end"); + c.end(() => events.push("client end callback")); + }); + c.on("close", (hadError: boolean) => { + events.push(`client close ${hadError}`); + const t = net.connect(port, "127.0.0.1", () => { + attempt("negative", () => t.setTimeout(-1)); + attempt("NaN", () => t.setTimeout(NaN)); + attempt("Infinity", () => t.setTimeout(Infinity)); + const never = () => events.push("removed callback ran"); + t.setTimeout(20, never); + t.setTimeout(0, never); + t.setTimeout(30, () => { + events.push("timeout callback"); + t.destroy(); + }); + }); + t.on("timeout", () => events.push("timeout event")); + t.on("close", (hadError: boolean) => { + events.push(`t close ${hadError}`); + server.close(() => console.log(events.join("\n"))); + }); + }); +}); diff --git a/tests/corpus/5646-net-server-connections.ts b/tests/corpus/5646-net-server-connections.ts new file mode 100644 index 000000000..9f2e01aec --- /dev/null +++ b/tests/corpus/5646-net-server-connections.ts @@ -0,0 +1,44 @@ +// server.maxConnections turns away connections past the cap with a +// 'drop' event carrying their addresses, and getConnections reports the +// open count on the next tick. +import * as net from "node:net"; + +const server = net.createServer((sock) => { + sock.on("data", () => {}); +}); +console.log("default", server.maxConnections); +server.maxConnections = 1; +console.log("set", server.maxConnections); +server.on("drop", (data?: net.DropArgument) => { + console.log( + "drop", + data?.localAddress, + typeof data?.localPort, + data?.localFamily, + data?.remoteAddress, + typeof data?.remotePort, + data?.remoteFamily, + ); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const first = net.connect(port, "127.0.0.1", () => { + const second = net.connect(port, "127.0.0.1"); + second.resume(); + second.on("close", () => { + console.log("second closed"); + process.nextTick(() => console.log("tick before")); + const same = server.getConnections((err: Error | null, count: number) => { + console.log("connections", err, count); + }); + console.log("getConnections chains", same === server); + process.nextTick(() => console.log("tick after")); + Promise.resolve().then(() => console.log("microtask")); + server.maxConnections = undefined; + console.log("unset", server.maxConnections); + first.destroy(); + server.close(() => console.log("server closed")); + }); + }); +}); diff --git a/tests/corpus/5647-net-is-ip.ts b/tests/corpus/5647-net-is-ip.ts new file mode 100644 index 000000000..4db2cc0cc --- /dev/null +++ b/tests/corpus/5647-net-is-ip.ts @@ -0,0 +1,34 @@ +// net.isIP, isIPv4 and isIPv6 follow Node's address patterns: dotted +// decimal without leading zeros, and IPv6 text forms with one "::", an +// IPv4 tail and an optional %zone. +import * as net from "node:net"; + +const inputs = [ + "1.2.3.4", + "255.255.255.255", + "01.2.3.4", + "256.1.1.1", + "1.2.3", + "1.2.3.4.5", + "::", + "::1", + "fe80::1%eth0", + "fe80::1%", + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7::", + "::1:2:3:4:5:6:7", + "1:2:3:4:5:6:7:8:9", + "1::2::3", + ":1::", + "1:", + "::ffff:1.2.3.4", + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5:6:7:1.2.3.4", + "12345::1", + "abcd:EF01::", + "", + "localhost", +]; +for (const input of inputs) { + console.log(JSON.stringify(input), net.isIP(input), net.isIPv4(input), net.isIPv6(input)); +} From bfb0ca8ffc3986a79dedfadfd88fac7db8928203 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 15 Oct 2026 16:42:32 -0500 Subject: [PATCH 2/2] Print the net corpus's server and client lines in a fixed order The server's 'connection' and the client's 'connect' race, and macOS runs them in the other order from Linux. 5644 now prints both sides once both have run, and 5645 keeps the server's and the client's events in separate lists printed one after the other. --- tests/corpus/5644-net-socket-addresses.ts | 43 ++++++++++++----------- tests/corpus/5645-net-socket-callbacks.ts | 15 +++++--- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/tests/corpus/5644-net-socket-addresses.ts b/tests/corpus/5644-net-socket-addresses.ts index 0053105cd..fff2f7aee 100644 --- a/tests/corpus/5644-net-socket-addresses.ts +++ b/tests/corpus/5644-net-socket-addresses.ts @@ -4,17 +4,25 @@ // the open halves. import * as net from "node:net"; +// The server's 'connection' and the client's 'connect' race (either can +// run first), so each side records its line and both print once both +// have run. +let serverLine: string | null = null; +let clientLine: string | null = null; +let startClient: (() => void) | null = null; +function bothConnected(): void { + if (serverLine === null || clientLine === null || startClient === null) return; + console.log(serverLine); + console.log(clientLine); + startClient(); +} + const server = net.createServer((sock) => { const port = (server.address() as net.AddressInfo).port; - console.log( - "server side", - sock.remoteAddress, - sock.remoteFamily, - typeof sock.remotePort, - sock.localAddress, - sock.localPort === port, - sock.localFamily, - ); + serverLine = + `server side ${sock.remoteAddress} ${sock.remoteFamily} ${typeof sock.remotePort} ` + + `${sock.localAddress} ${sock.localPort === port} ${sock.localFamily}`; + bothConnected(); sock.on("data", (d: Buffer) => { console.log("server got", d.toString(), sock.bytesRead); sock.end("bye"); @@ -36,18 +44,11 @@ server.listen(0, "127.0.0.1", () => { c.bytesRead, ); c.on("connect", () => { - console.log( - "connected", - c.readyState, - c.connecting, - c.pending, - c.remotePort === port, - c.remoteFamily, - c.localAddress, - typeof c.localPort, - c.localFamily, - ); - c.write("hello"); + clientLine = + `connected ${c.readyState} ${c.connecting} ${c.pending} ${c.remotePort === port} ` + + `${c.remoteFamily} ${c.localAddress} ${typeof c.localPort} ${c.localFamily}`; + startClient = () => c.write("hello"); + bothConnected(); }); c.on("data", (d: Buffer) => console.log("client got", d.toString(), c.bytesRead)); c.on("end", () => console.log("end", c.readyState, c.bytesRead, c.bytesWritten)); diff --git a/tests/corpus/5645-net-socket-callbacks.ts b/tests/corpus/5645-net-socket-callbacks.ts index 9bc670dc5..c80704828 100644 --- a/tests/corpus/5645-net-socket-callbacks.ts +++ b/tests/corpus/5645-net-socket-callbacks.ts @@ -3,18 +3,22 @@ // setKeepAlive's chaining. import * as net from "node:net"; +// Each side keeps its own list: the client's callbacks and the server's +// events can interleave either way, so the lists print one after the +// other at the end. const events: string[] = []; +const serverEvents: string[] = []; let serverSockets = 0; const server = net.createServer((sock) => { sock.setEncoding("utf8"); sock.on("data", (d: string) => { - events.push(`server data ${typeof d} ${d}`); - sock.end("pong", () => events.push("server end callback")); + serverEvents.push(`server data ${typeof d} ${d}`); + sock.end("pong", () => serverEvents.push("server end callback")); }); // the first connection's server side (the timeout connection's close // order against the client's depends on the event loop phase) if (serverSockets++ === 0) { - sock.on("close", (hadError: boolean) => events.push(`server close ${hadError}`)); + sock.on("close", (hadError: boolean) => serverEvents.push(`server close ${hadError}`)); } }); @@ -59,7 +63,10 @@ server.listen(0, "127.0.0.1", () => { t.on("timeout", () => events.push("timeout event")); t.on("close", (hadError: boolean) => { events.push(`t close ${hadError}`); - server.close(() => console.log(events.join("\n"))); + server.close(() => { + console.log(serverEvents.join("\n")); + console.log(events.join("\n")); + }); }); }); });