mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Add net socket address fields, write and end callbacks, and server connection limits (#1052)
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "a38d2d004b348e926af4",
|
||||
"artifactVersion": "1dd6d714f44893573704",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+54
-6
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -973,13 +973,29 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -182,6 +182,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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",
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5644-net-socket-addresses.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5645-net-socket-callbacks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5646-net-server-connections.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5647-net-is-ip.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+486
-13
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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";
|
||||
|
||||
// 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;
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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));
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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";
|
||||
|
||||
// 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) => {
|
||||
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) => serverEvents.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(serverEvents.join("\n"));
|
||||
console.log(events.join("\n"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user