HTTP Agent and dynamic-handle compatibility

- Add Agent connection accounting and HTTP/socket compatibility surfaces
- Support dynamic Buffer at and slice access
- Pin behavior with differential corpus coverage
This commit is contained in:
Chris Tate
2026-07-24 18:52:19 -05:00
26 changed files with 2277 additions and 172 deletions
+41 -13
View File
@@ -1735,9 +1735,23 @@ declare module "net" {
readonly destroyed: boolean;
/* the write half is open: no end() yet, no FIN sent, fd alive. */
readonly writable: boolean;
/* true until the read half is done (peer FIN / destroy). */
readonly readable: boolean;
/* 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;
destroy(): void;
/* Flow control: pause holds reads off (kernel/TCP backpressure is
* the buffer); resume flows — and discards without listeners, so
* 'end' stays reachable. Both chain, Node's shape. */
pause(): Socket;
resume(): Socket;
/* TCP_NODELAY on the live fd; missing means true. Chains. */
setNoDelay(noDelay?: boolean): Socket;
/* end() now, destroy once the FIN actually flushed. */
destroySoon(): void;
/* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage
* twin's contract. */
setEncoding(encoding: string): void;
@@ -1755,16 +1769,16 @@ 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;
on(event: "end" | "close" | "connect" | "timeout" | "readable" | "secureConnect", listener: () => void): void;
on(event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => 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;
addListener(event: "end" | "close" | "connect" | "timeout" | "readable" | "secureConnect", listener: () => void): void;
addListener(event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => 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;
once(event: "end" | "close" | "connect" | "timeout" | "readable" | "secureConnect", listener: () => void): void;
once(event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "session", listener: (session: Buffer) => void): void;
}
@@ -1949,9 +1963,10 @@ declare module "http" {
/* The caller's own dialer (the proxy's loopback dial): invoked once,
* synchronously; its socket carries the exchange. */
createConnection?: () => Socket;
/* agent: false is lowered (a one-shot dial with Connection: close —
* exactly the compiled client's model); null/undefined are the
* default; Agent INSTANCES fence at the call site (no pooling). */
/* agent: false is a one-shot dial with Connection: close — exactly
* the compiled client's model; null/undefined are the default; an
* Agent VALUE threads through (getName-keyed maxSockets accounting
* over one-dial-per-request connections). */
agent?: Agent | boolean | null;
/* The options-record stance: Node ignores an options record's
* unknown keys, so every key typechecks here — the option WALK in
@@ -1959,10 +1974,13 @@ declare module "http" {
* undocumented keys exactly like Node). */
[option: string]: unknown;
}
/* Declared surface without a lowering: the compiled client dials one
* connection per exchange and closes it with the response (the
* agent: false shape), so pooling Agents cannot be honored —
* constructing one fences at the site. */
/* The Agent lowers to a checked-dynamic HANDLE: option parsing at the
* literal construction site, getName, destroy, the sockets/requests/
* freeSockets snapshots, and REAL maxSockets accounting (over-limit
* requests defer their dial and queue). keepAlive: true throws the
* named pooling fence — this client dials one connection per request
* and closes it with the response, so freeSockets is always
* empty. */
export interface AgentOptions {
keepAlive?: boolean;
keepAliveMsecs?: number;
@@ -1971,16 +1989,26 @@ declare module "http" {
maxTotalSockets?: number;
scheduling?: string;
timeout?: number;
port?: number;
[option: string]: unknown;
}
export class Agent {
constructor(options?: AgentOptions);
destroy(): void;
/* Node's exact key shape: host:port:localAddress[:family][:socketPath]. */
getName(options?: { host?: string; port?: number; localAddress?: string; family?: number; socketPath?: string; [option: string]: unknown }): string;
maxSockets: number;
maxFreeSockets: number;
readonly sockets: { [key: string]: unknown };
readonly freeSockets: { [key: string]: unknown };
readonly requests: { [key: string]: unknown };
keepAliveMsecs: number;
readonly keepAlive: boolean;
readonly protocol: string;
/* Settable — a portless request through this agent dials it (Node's
* option merge). */
defaultPort: number;
readonly totalSocketCount: number;
readonly sockets: { [key: string]: unknown[] };
readonly freeSockets: { [key: string]: unknown[] };
readonly requests: { [key: string]: unknown[] };
}
export const globalAgent: Agent;
export interface ClientRequest {
@@ -2286,6 +2286,10 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
// Object.defineProperties over dyn values: both borrowed,
// result the target (+1); throws catchably (may-throw seed).
return finish(`scr_dyn_define_props(${arg(0)}, ${arg(1)})`);
case "dyn.hasKey":
// `k in v` with a runtime key: the DOM presence answer (both
// borrowed, no allocation, never throws).
return finish(`scr_dyn_has_key(${arg(0)}, ${arg(1)})`);
case "dyn.keySet":
// Keyed write on a dyn receiver: all three borrowed (the
// member retains the value in); throws Node's TypeErrors on
@@ -3231,6 +3235,25 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
case "net.sockDestroy":
E.line(`scr_net_sock_destroy(${arg(0)});${E.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "net.sockPause":
return finish(`scr_net_sock_pause(${arg(0)})`);
case "net.sockResume":
return finish(`scr_net_sock_resume(${arg(0)})`);
case "net.sockSetNoDelay":
return finish(`scr_net_sock_set_nodelay(${arg(0)}, ${arg(1)})`);
case "net.sockDestroySoon":
E.line(`scr_net_sock_destroy_soon(${arg(0)});${E.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "net.sockBytesWritten":
return finish(`scr_net_sock_bytes_written(${arg(0)})`);
case "net.sockReadable":
return finish(`scr_net_sock_readable(${arg(0)})`);
case "net.sockOnFinish": {
const cb = args[1]!;
E.moveTemp(cb);
E.line(`scr_net_sock_on_finish(${arg(0)}, ${cb.name});${E.srcComment(e.loc)}`);
return { name: "", type: e.type };
}
case "net.sockPipe":
E.line(`scr_net_sock_pipe(${arg(0)}, ${arg(1)});${E.srcComment(e.loc)}`);
return { name: "", type: e.type };
@@ -3781,6 +3804,28 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
`scr_http_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${cbExpr}, ${adapter})`,
);
}
case "http.agentNew":
E.usesTimers = true; // queued dials hold the loop open
return finish(
`scr_http_agent_new(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)})`,
);
case "http.requestAgent":
case "http.requestAgentCb": {
E.usesTimers = true;
let cbExpr = "NULL";
let adapter = "NULL";
if (e.fn === "http.requestAgentCb") {
const cbT = e.args[8]!.type;
if (cbT.kind !== "func") throw new Error("emitter bug: http.requestAgentCb callback not a func");
const cb = args[8]!;
E.moveTemp(cb);
cbExpr = cb.name;
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
}
return finish(
`scr_http_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, ${cbExpr}, ${adapter})`,
);
}
case "http.requestUrl":
case "http.requestUrlCb": {
E.usesTimers = true; // an in-flight request holds the loop open
@@ -4317,6 +4362,23 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
`scr_https_request(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${cbExpr}, ${adapter})`,
);
}
case "https.requestAgent":
case "https.requestAgentCb": {
E.usesTimers = true; // an in-flight request holds the loop open
let cbExpr = "NULL";
let adapter = "NULL";
if (e.fn === "https.requestAgentCb") {
const cbT = e.args[10]!.type;
if (cbT.kind !== "func") throw new Error("emitter bug: https.requestAgentCb callback not a func");
const cb = args[10]!;
E.moveTemp(cb);
cbExpr = cb.name;
adapter = cbT.params.length === 0 ? "&scr_http_resp_thunk0" : "&scr_http_resp_thunk_res";
}
return finish(
`scr_https_request_agent(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${arg(5)}, ${arg(6)}, ${arg(7)}, (const char *)${arg(8)}->data, ${arg(8)}->len, ${arg(9)}, ${cbExpr}, ${adapter})`,
);
}
case "https.requestFn":
case "https.requestFnCb": {
// The requestFn binding's runtime dial: arg 0 picks the client
+21 -3
View File
@@ -475,6 +475,7 @@ const LIB_FN_SYMS: Record<string, string> = {
"dyn.iterPack": "scr_dyn_iter_pack",
"dyn.arrLen": "scr_dyn_arr_len",
"dyn.arrAt": "scr_dyn_arr_at",
"dyn.hasKey": "scr_dyn_has_key",
"dyn.defineProps": "scr_dyn_define_props",
"dyn.typeof": "scr_dyn_typeof",
"dyn.toString": "scr_dyn_to_string_method",
@@ -571,6 +572,12 @@ const LIB_FN_SYMS: Record<string, string> = {
"net.sockSetEncoding": "scr_net_sock_set_encoding",
"net.sockSetTimeout": "scr_net_sock_set_timeout",
"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.sockReadable": "scr_net_sock_readable",
"net.sockPipeRes": "scr_http_sock_pipe_res",
"net.serverEmitConnection": "scr_net_server_emit_connection",
"net.getAutoSelTimeout": "scr_net_get_autosel_timeout",
@@ -617,6 +624,7 @@ const LIB_FN_SYMS: Record<string, string> = {
"http.clientDestroy": "scr_http_client_destroy",
"http.clientDestroyed": "scr_http_client_destroyed",
"http2.streamUndefCall": "scr_http2_stream_undef_call",
"http.agentNew": "scr_http_agent_new",
// The island surface (--dynamic): eval/import bridge catchably (the
// may-throw seed's pending check); importDyn answers an engine promise
// and never throws itself. insp.jsval throws on composite island
@@ -760,6 +768,7 @@ const USES_TIMERS_LIB_FNS = new Set<string>([
"http.createServer", "http.createServerEmpty",
"http.request", "http.requestCb", "http.requestUrl", "http.requestUrlCb",
"http.requestConn", "http.requestConnCb",
"http.agentNew", "http.requestAgent", "http.requestAgentCb",
// The dyn-async slice (emit-exprs.ts's markings): fiber parks, the
// microtask/immediate mints, the tracing-promise reaction fiber, and
// the loop-end unhandled-rejection report.
@@ -11288,9 +11297,11 @@ class LlEmitter {
B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr @${adapter}, i1 ${args[2]!.name})`);
return { name: "", type: e.type };
}
if (e.fn === "http.request" || e.fn === "http.requestCb" || e.fn === "http.requestUrl" || e.fn === "http.requestUrlCb") {
if (e.fn === "http.request" || e.fn === "http.requestCb" || e.fn === "http.requestUrl" || e.fn === "http.requestUrlCb" ||
e.fn === "http.requestAgent" || e.fn === "http.requestAgentCb") {
const isUrl = e.fn.startsWith("http.requestUrl");
const cbIdx = isUrl ? 3 : 7;
const isAgent = e.fn.startsWith("http.requestAgent");
const cbIdx = isUrl ? 3 : isAgent ? 8 : 7;
const hasCb = e.fn.endsWith("Cb");
const args = e.args.map((a) => this.emitExpr(a));
let cb = "null";
@@ -11306,7 +11317,7 @@ class LlEmitter {
}
const head = args.slice(0, cbIdx);
const decls = head.map((a) => (this.llType(a.type) === "i1" ? "i1 zeroext" : this.llType(a.type)));
const entry = isUrl ? "scr_http_request_url" : "scr_http_request";
const entry = isUrl ? "scr_http_request_url" : isAgent ? "scr_http_request_agent" : "scr_http_request";
this.declare(`declare ptr @${entry}(${[...decls, "ptr", "ptr"].join(", ")})`);
const t = B.tmp();
B.line(
@@ -11341,6 +11352,13 @@ class LlEmitter {
B.line(`call void @scr_http_res_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`);
return { name: "", type: e.type };
}
if (e.fn === "net.sockOnFinish") {
const args = e.args.map((a) => this.emitExpr(a));
this.moveTemp(args[1]!);
this.declare(`declare void @scr_net_sock_on_finish(ptr, ptr)`);
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 === "island.castFail") {
// The deferred boundary failure: the island value was evaluated
// (its side effects are real), the throw is unconditional
@@ -4213,6 +4213,9 @@ export const DYN_DISPATCH_METHODS = new Set([
"writeContinue", "writeEarlyHints", "cork", "uncork", "addTrailers",
"ref", "unref", "address", "setNoDelay", "setKeepAlive", "connect",
"resetAndDestroy", "destroySoon",
// The Agent handle's own member (no other DOM prototype declares it,
// so the remainder keeps the stored-member answers).
"getName",
// The netServer half of the handle surface (`let server; server =
// createServer(...)` — the handle lives in a dyn binding whose
// closures the checker cannot narrow): listen/close dispatch onto the
@@ -18,7 +18,7 @@ import { STREAM_API_MEMBERS, STREAM_PROP_MEMBERS, UNDERSCORE_METHODS, lowerStrea
import { emitOverrideShapeReason, emitSpecSuperForward, emitterRooted, lowerEmitterSuperCall, type EmitOverrideRec } from "./lower-emitter.js";
import { declSymbolOf } from "./lower-modules.js";
import { uniqueSymbolKeyOf } from "./lower-exprs.js";
import { lowerHttpServerNew } from "./lower-server.js";
import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js";
import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, fenceEarlyAliasUse, fenceEarlyNsMemberRef, nsMemberIdentOf, nsUndefRead } from "./lower-namespaces.js";
import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixins.js";
@@ -4890,6 +4890,13 @@ export function lowerNew(L: Lowerer, expr: ts.NewExpression): IrExpr {
const httpServer = lowerHttpServerNew(L, expr);
if (httpServer) return httpServer;
}
// `new http.Agent(opts?)` / `new https.Agent(opts?)` — the Agent
// handle (lower-server): getName/destroy/counters through the DOM
// handle ops, requests thread it via the agent option.
{
const agent = lowerHttpAgentNew(L, expr);
if (agent) return agent;
}
if (ts.isIdentifier(expr.expression)) {
// `import C = N.C; new C()` — the alias's own source-order guards
// (a no-op for every non-import= binding).
@@ -5621,7 +5628,7 @@ export function lowerNew(L: Lowerer, expr: ts.NewExpression): IrExpr {
{
const STDLIB_CTOR_HINTS: Record<string, string | undefined> = {
Agent:
"constructing an http Agent (compiled clients dial one connection per request and close it with the response — pass agent: false for Node's one-shot semantics, or omit the option)",
"constructing an http Agent through an indirect class binding (spell the construction on the module binding — new http.Agent(...)/new https.Agent(...) or the named Agent import — which lowers to the Agent handle)",
};
const ctorName = ts.isIdentifier(expr.expression)
? expr.expression
@@ -5951,7 +5951,10 @@ export function lowerObjectLiteral(L: Lowerer, expr: ts.ObjectLiteralExpression)
litKey !== null
? ({ kind: "strLit", value: litKey, type: STRING, loc: locOf(keyNode) } satisfies IrExpr)
: L.lowerExpr(keyNode);
if (dk.type.kind === "f64") dk = L.ensureString(dk, keyNode);
// Number AND checked-dynamic keys ride the JS-exact formatter —
// property keys ARE strings (o[k] is o[String(k)] in JS), and a dyn
// key (agent.sockets[agent.getName(...)]) stringifies the same way.
if (dk.type.kind === "f64" || dk.type.kind === "dyn") dk = L.ensureString(dk, keyNode);
if (dk.type.kind !== "string") {
L.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
}
@@ -7522,6 +7525,12 @@ export function lowerBinary(L: Lowerer, expr: ts.BinaryExpression): IrExpr {
) {
return { kind: "bin", op: negated ? "!==" : "===", left: idLeft, right: idRight, type: BOOL, loc };
}
// Runtime HANDLES are objects to === too: one handle per socket/
// request/response, so pointer identity IS JS's object equality
// (`c.pause() === c` — Node's chaining assertions). */
if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) {
return { kind: "bin", op: negated ? "!==" : "===", left: idLeft, right: idRight, type: BOOL, loc };
}
L.unsupported("SC1043", expr);
break;
}
@@ -8634,6 +8643,25 @@ export function lowerBinary(L: Lowerer, expr: ts.BinaryExpression): IrExpr {
// runtime key table to ask.
const rIn = lowerRuntimeKeyIn(L, expr, loc);
if (rIn) return rIn;
// A runtime key over a CHECKED-DYNAMIC receiver (`name in
// agent.sockets` — both sides computed; the checker may type the
// receiver as a Dict while the VALUE lives in the DOM, so the
// LOWERED type decides): the DOM presence answer, with the key
// stringified like every property key (o[k] is o[String(k)] in JS;
// `in` shares the coercion).
{
const probed = probeLower(L, expr.right);
if (probed?.type.kind === "dyn") {
let k = L.lowerExpr(expr.left); // JS order: the key evaluates first
if (k.type.kind === "f64" || k.type.kind === "string" || k.type.kind === "dyn") {
k = L.ensureString(k, expr.left);
const recvD = L.lowerExpr(expr.right);
if (recvD.type.kind === "dyn") {
return { kind: "libCall", fn: "dyn.hasKey", args: [recvD, k], type: BOOL, loc };
}
}
}
}
L.unsupported(
"SC1090",
expr.left,
@@ -14,6 +14,7 @@ import { ladderFenceExpr, nodeThrowExpr } from "./lowerer.js";
import { isJsSourceFile, locOf } from "../program.js";
import { arrayOf, BOOL, BYTES_U8, canBoxFuncIntoDyn, canConvertToDyn, DYN, DYN_HANDLE_KINDS, F64, funcOf, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, IrExpr, IrLibFn, IrStmt, IrType, NETSERVER_T, NETSOCKET_T, NULL_T, SECURECTX_T, STRING, UNDEFINED_T, SrcLoc, typeKey, VOID } from "../../ir/nodes.js";
import {
AGENT_DOCUMENTED_OPTIONS,
builtinFenceHintOf,
builtinModuleFnOf,
fenceOrDropOptionKey,
@@ -1453,6 +1454,40 @@ function lowerNetSocketMethodCall(L: Lowerer, call: ts.CallExpression,
const ms = L.lowerExprExpecting(args[0]!, F64);
return { kind: "libCall", fn: "net.sockSetTimeout", args: [receiver, ms], type: VOID, 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
// (and discards sans listeners, so 'end' is reachable). Both answer
// the socket, Node's chaining.
if (args.length !== 0) {
L.noLowering(`${name} with ${args.length} arguments`, call, `the form is ${name}()`);
}
const receiver = handleReceiver(L, access.expression, NETSOCKET_T);
const fn: IrLibFn = name === "pause" ? "net.sockPause" : "net.sockResume";
return { kind: "libCall", fn, args: [receiver], type: NETSOCKET_T, loc };
}
if (name === "setNoDelay") {
// TCP_NODELAY on the live fd; missing/undefined means true (Node).
// Answers the socket, Node's chaining.
if (args.length > 1) {
L.noLowering(`setNoDelay with ${args.length} arguments`, call, "the form is setNoDelay(noDelay?)");
}
const receiver = handleReceiver(L, access.expression, NETSOCKET_T);
const enable: IrExpr = args.length === 1
? L.lowerExprExpecting(args[0]!, BOOL)
: boolLit(true, loc);
return { kind: "libCall", fn: "net.sockSetNoDelay", args: [receiver, enable], type: NETSOCKET_T, loc };
}
if (name === "destroySoon") {
// end() now, destroy once the FIN actually flushed — Node's
// 'finish'-then-destroy.
requireStatementPosition(L, call, "socket.destroySoon()");
if (args.length !== 0) {
L.noLowering(`destroySoon with ${args.length} arguments`, call, "the form is destroySoon()");
}
const receiver = handleReceiver(L, access.expression, NETSOCKET_T);
return { kind: "libCall", fn: "net.sockDestroySoon", args: [receiver], type: VOID, loc };
}
if (name === "read") {
// socket.read(n?) — the demux peek. Answers the interned
// `Buffer | null` union: exactly n buffered bytes, or null (Node's
@@ -1539,6 +1574,12 @@ function lowerNetSocketMethodCall(L: Lowerer, call: ts.CallExpression,
: "net.sockOnConnect";
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
if (event === "finish") {
// Fires once when the FIN goes out — once either way (the event
// happens at most once per socket).
const { cb } = lowerCallbackArg(L, args[1]!, "finish listeners", 0, () => false, "use ()", []);
return { kind: "libCall", fn: "net.sockOnFinish", args: [receiver, cb], type: VOID, loc };
}
if (event === "secureConnect") {
// The TLS client's post-handshake event: on a TLS socket the
// 'connect' list already fires at establishment (scr_net.c's
@@ -2056,6 +2097,17 @@ export function lowerServerProperty(L: Lowerer, expr: ts.PropertyAccessExpressio
const receiver = handleReceiver(L, expr.expression, NETSOCKET_T);
return { kind: "libCall", fn: "net.sockWritable", args: [receiver], type: BOOL, loc };
}
if (recvKind === "netSocket" && L.isStdlibMember(expr) && expr.name.text === "bytesWritten") {
// Every byte the write paths accepted (buffered included — Node
// counts those too; plaintext on TLS sockets).
const receiver = handleReceiver(L, expr.expression, NETSOCKET_T);
return { kind: "libCall", fn: "net.sockBytesWritten", args: [receiver], type: F64, loc };
}
if (recvKind === "netSocket" && L.isStdlibMember(expr) && expr.name.text === "readable") {
// true until the read half is done (peer FIN / destroy).
const receiver = handleReceiver(L, expr.expression, NETSOCKET_T);
return { kind: "libCall", fn: "net.sockReadable", args: [receiver], type: BOOL, loc };
}
if (recvKind === "netSocket" && L.isStdlibMember(expr) && expr.name.text === "remoteAddress") {
// `string | undefined` — Node's read-time caching: a value read while
// connected survives destroy; never-read sockets answer undefined
@@ -2311,6 +2363,126 @@ function lowerHttpCreateServerForms(L: Lowerer, expr: ts.CallExpression | ts.New
* createServer (Node's Server class IS the factory's product); called
* from lowerNew ahead of the stdlib-constructor fence. Null when the
* callee is not the http module's Server. */
/** `new http.Agent(opts?)` / `new https.Agent(opts?)` (property access or
* named import): the Agent lowers to a checked-dynamic HANDLE —
* getName/destroy and the sockets/requests/freeSockets counters dispatch
* through the DOM handle ops, and the request path threads it (the
* requestAgent rows). Options parse at the LITERAL construction site:
* keepAlive (a boolean value — TRUE throws the runtime's named pooling
* fence: this client dials one connection per request), keepAliveMsecs /
* maxSockets / maxFreeSockets / timeout / port as numbers, scheduling
* DROPS (no free pool exists, so selection order cannot observe), other
* documented keys fence by name, and undocumented keys drop like Node
* drops them. Null when the callee isn't the http/https Agent. */
export function lowerHttpAgentNew(L: Lowerer, expr: ts.NewExpression): IrExpr | null {
const callee = expr.expression;
const bi =
ts.isPropertyAccessExpression(callee) && callee.name.text === "Agent"
? L.builtinMemberOf(callee)
: ts.isIdentifier(callee)
? L.builtinImportOf(callee)
: null;
if (!bi || bi.member !== "Agent" || (bi.module !== "http" && bi.module !== "https")) return null;
const loc = locOf(expr);
const api = `new ${bi.module}.Agent`;
const numLit = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const boolAt = (value: boolean): IrExpr => ({ kind: "boolLit", value, type: BOOL, loc });
let keepAlive: IrExpr = boolAt(false);
let kaMsecs: IrExpr = numLit(-1);
let maxSockets: IrExpr = numLit(-1);
let maxFree: IrExpr = numLit(-1);
let timeout: IrExpr = numLit(-1);
let port: IrExpr = numLit(-1);
const args = expr.arguments ?? [];
if (args.length > 1) {
L.noLowering(`${api} with ${args.length} arguments`, expr, "the supported form is new Agent(options?)");
}
if (args.length === 1) {
const optsNode = stripParensAndCasts(args[0]!);
if (!ts.isObjectLiteralExpression(optsNode)) {
L.noLowering(
`${api} with a non-literal options value`,
args[0]!,
"spell the options as an object literal at the construction site",
);
}
for (const prop of optsNode.properties) {
if (ts.isSpreadAssignment(prop)) {
L.noLowering(`${api} with an options spread`, prop, "write each option inline");
}
let initializer: ts.Expression | null;
if (ts.isPropertyAssignment(prop) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) {
initializer = prop.initializer;
} else if (ts.isShorthandPropertyAssignment(prop)) {
initializer = null;
} else {
L.noLowering(
`${api} options with computed keys`,
prop,
"each option must be a plain `name: value` (or shorthand) entry with a literal key",
);
}
const key = (prop.name as ts.Identifier | ts.StringLiteral).text;
const lowerVal = (want: "bool" | "f64"): IrExpr => {
const v = initializer !== null
? L.lowerExpr(initializer)
: L.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
if (v.type.kind === "dyn") {
return { kind: "dynCheck", value: v, type: want === "bool" ? BOOL : F64, loc: locOf(prop) };
}
if (v.type.kind !== want) {
L.noLowering(
`a ${api} '${key}' option of '${L.fmt(v.type)}' values`,
prop,
want === "bool" ? "the option value must be a boolean" : "the option value must be a number",
);
}
return v;
};
switch (key) {
case "keepAlive":
keepAlive = lowerVal("bool");
break;
case "keepAliveMsecs":
kaMsecs = lowerVal("f64");
break;
case "maxSockets":
maxSockets = lowerVal("f64");
break;
case "maxFreeSockets":
maxFree = lowerVal("f64");
break;
case "timeout":
timeout = lowerVal("f64");
break;
case "port":
// Node merges agent options under portless request options —
// the settable defaultPort carries this one.
port = lowerVal("f64");
break;
case "scheduling":
// Free-socket selection order: no free pool exists here (no
// keep-alive reuse), so the choice cannot observe — dropped.
if (initializer !== null) L.lowerExpr(initializer); // evaluate, like Node
break;
default:
fenceOrDropOptionKey(
L, prop, key, api, AGENT_DOCUMENTED_OPTIONS,
"keepAlive, keepAliveMsecs, maxSockets, maxFreeSockets, timeout, port, and scheduling are the supported options",
);
}
}
}
return {
kind: "libCall",
fn: "http.agentNew",
args: [boolAt(bi.module === "https"), keepAlive, kaMsecs, maxSockets, maxFree, timeout, port],
type: DYN,
loc,
};
}
export function lowerHttpServerNew(L: Lowerer, expr: ts.NewExpression): IrExpr | null {
const callee = expr.expression;
const isHttpServer =
@@ -3347,6 +3519,8 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
let connCb: IrExpr | null = null;
/** agent: false — inject Connection: close into the request head. */
let agentClose: ts.Node | null = null;
/** agent: <Agent value> — the requestAgent rows thread the handle. */
let agentVal: IrExpr | null = null;
for (const prop of optsNode.properties) {
// Shorthand entries ({ port } for { port: port }) are the natural
// spelling at these call sites — the shorthand's VALUE lowering
@@ -3522,8 +3696,11 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
// model, so it LOWERS (the header injects below). null/undefined
// pick the default agent, whose keep-alive request header the
// compiled client already sends (socket REUSE stays the
// documented divergence either way). Agent instances configure
// pooling the runtime does not have — fence.
// documented divergence either way). An Agent VALUE (the lowered
// `new http.Agent(...)` handle — a checked-dynamic value) threads
// through the requestAgent rows: getName-keyed maxSockets
// accounting over one-dial-per-request connections; a runtime
// non-Agent value is Node's ERR_INVALID_ARG_TYPE.
const e = initializer !== null ? stripParensAndCasts(initializer) : null;
if (e !== null && e.kind === ts.SyntaxKind.FalseKeyword) {
agentClose = prop;
@@ -3534,13 +3711,18 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
(ts.isIdentifier(e) && e.text === "undefined"))) {
break; // the default agent: what the agent-free call compiles
}
L.noLowering(
`a ${member} 'agent' option carrying an Agent value`,
prop,
"compiled clients dial one connection per request and close it with the response — " +
"agent: false (lowered: sends Connection: close) and agent: null/undefined (the default) " +
"compile; pooling Agent instances have no lowering",
);
const v = initializer !== null
? L.lowerExpr(initializer)
: L.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
if (v.type.kind !== "dyn") {
L.noLowering(
`a ${member} 'agent' option of '${L.fmt(v.type)}' values`,
prop,
"the agent is the checked-dynamic Agent handle new http.Agent(...) answers " +
"(or false / null / undefined)",
);
}
agentVal = v;
break;
}
default:
@@ -3590,11 +3772,28 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
"the dialer supplies the socket — set the Host header via headers: { host: ... } instead",
);
}
if (agentVal !== null && connCb !== null) {
L.noLowering(
`${member} mixing an agent value with createConnection`,
optsNode,
"both own the dial — pass one or the other",
);
}
if (agentVal !== null && binding !== null) {
L.noLowering(
`${member} through a module-function binding with an agent value`,
optsNode,
"call http.request/https.request directly when passing an Agent",
);
}
host ??= strLit("localhost");
// The binding mode's default port follows the runtime dial: 443 on the
// TLS arm, 80 on the plain one — exactly each client's own default.
// With an AGENT the sentinel -1 says "no port option": the runtime
// consults the agent's (settable) defaultPort first, Node's merge.
port ??= binding !== null
? { kind: "ternary", cond: secureExpr(), then: numLit(443), else_: numLit(80), type: F64, loc }
: agentVal !== null ? numLit(-1)
: numLit(secure === true ? 443 : 80);
path ??= strLit("/");
method ??= strLit("GET");
@@ -3620,9 +3819,12 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
ca ??= strLit(""); /* none: /etc/ssl/cert.pem stands in for Node's roots */
base.push(reject, ca);
}
if (agentVal !== null) base.push(agentVal);
if (binding !== null) base.unshift(secureExpr());
if (args.length === 1) {
const fn: IrLibFn = binding !== null ? "https.requestFn" : secure === true ? "https.request" : "http.request";
const fn: IrLibFn = binding !== null ? "https.requestFn"
: secure === true ? (agentVal !== null ? "https.requestAgent" : "https.request")
: agentVal !== null ? "http.requestAgent" : "http.request";
return { kind: "libCall", fn, args: base, type: HTTPCLIENTREQ_T, loc };
}
const { cb } = lowerCallbackArg(
@@ -3631,7 +3833,9 @@ function lowerHttpClientCall(L: Lowerer, expr: ts.CallExpression, member: "reque
"use (res) or ()",
[HTTPREQ_T],
);
const fn: IrLibFn = binding !== null ? "https.requestFnCb" : secure === true ? "https.requestCb" : "http.requestCb";
const fn: IrLibFn = binding !== null ? "https.requestFnCb"
: secure === true ? (agentVal !== null ? "https.requestAgentCb" : "https.requestCb")
: agentVal !== null ? "http.requestAgentCb" : "http.requestCb";
return { kind: "libCall", fn, args: [...base, cb], type: HTTPCLIENTREQ_T, loc };
}
@@ -172,6 +172,16 @@ export const HTTP_SERVER_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
"requireHostHeader", "uniqueHeaders",
]);
/** new http.Agent(options)'s documented keys (Node v24 — the Agent
* constructor table plus the socket.connect() options it forwards into
* each dial). The lowered set is keepAlive/keepAliveMsecs/maxSockets/
* maxFreeSockets/timeout/port (+ scheduling, dropped: no free pool
* exists); the rest fence by name and unknown keys drop like Node. */
export const AGENT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
"family", "hints", "host", "keepAliveInitialDelay", "localAddress",
"localPort", "lookup", "maxTotalSockets", "noDelay", "path",
]);
/** https.request adds tls.connect's client-side TLS knobs. */
export const HTTPS_CLIENT_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
...HTTP_CLIENT_DOCUMENTED_OPTIONS,
@@ -1205,6 +1215,12 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<string, Record<string, string |
if (!sym || !L.isStdlibSymbol(sym)) return;
const member = access.name.text;
const recv = access.expression;
// A CHECKED-DYNAMIC receiver whose checker type is a concrete stdlib
// class mapped to dyn (the http Agent handle): its members dispatch
// at runtime through the DOM/handle machinery — the keyed-read claim
// below this fence answers, member-or-refusal ladder, so no compile
// fence belongs here.
if (L.mapTypeOf(L.typeOf(recv))?.kind === "dyn") return;
// Name the container the way the source reads: the global's name when
// the receiver IS a stdlib global (Math, process), the dotted path for
// a member of one (process.stdout — its TYPE text would be the useless
+17
View File
@@ -1680,6 +1680,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "object", className: "%Error" };
}
// http.Agent / https.Agent — the Agent VALUE is a checked-dynamic
// handle (new http.Agent lowers to the DOM handle whose members
// dispatch through the handle ops), so the TYPE maps to dyn: a typed
// binding, parameter, or field carrying an Agent stays usable instead
// of fencing at the declaration. Module-checked like the rest (https'
// Agent extends http's; both live in their ambient modules).
if (
psym?.name === "Agent" &&
checker.declarationsOf(psym).some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
ctx.isStdlibFile(d.getSourceFile()) &&
(isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")),
)
) {
return { kind: "dyn" };
}
// http.IncomingMessage / http.ServerResponse — module-checked like
// net.Server (the names repeat nowhere today, but provenance stays the
// rule).
+50
View File
@@ -1605,6 +1605,11 @@ export type IrLibFn =
* throws. */
| "dyn.arrLen"
| "dyn.arrAt"
/** `key in v` with a RUNTIME (string) key on a checked-dynamic
* receiver (args: value dyn, key string; result bool): OBJ answers
* own-member presence, ARR answers 'length'/a valid index — exactly
* the compile-time dynHasKey fold, per value. Never throws. */
| "dyn.hasKey"
/** Object.defineProperties over dyn values (args: target, descriptors —
* both borrowed dyn; result: the target, +1 — JS's return value).
* Value descriptors become plain own properties on OBJ and FUNC targets
@@ -2347,6 +2352,21 @@ export type IrLibFn =
| "net.sockOnReadable"
| "net.sockRead"
| "net.sockUnshift"
/** Socket flow control and the compat surface: pause/resume (reads
* gate off/on — kernel backpressure holds paused bytes; resume flows
* and discards sans listeners) and setNoDelay answer the SOCKET (+1,
* Node's chaining); destroySoon ends now and destroys once the FIN
* flushed; bytesWritten counts accepted bytes; readable is true until
* the read half ends. */
| "net.sockPause"
| "net.sockResume"
| "net.sockSetNoDelay"
| "net.sockDestroySoon"
| "net.sockBytesWritten"
| "net.sockReadable"
/** socket.on('finish', cb) / end(cb): fires once when the FIN goes out
* (sweep-deferred, never the registering stack). */
| "net.sockOnFinish"
| "net.serverEmitConnection"
/** node:http, the CLIENT slice (http.request/http.get over the net
* client machinery): request/requestCb take (host, port, path, method,
@@ -2365,6 +2385,27 @@ export type IrLibFn =
* Content-Length exactly like Node. */
| "http.request"
| "http.requestCb"
/** new http.Agent(opts) / new https.Agent(opts): (secure, keepAlive,
* keepAliveMsecs, maxSockets, maxFreeSockets, timeoutMs, port) — the
* numeric options arrive < 0 for "unset" (Infinity/256/none; port
* seeds the settable defaultPort, Node's option merge). Returns the
* Agent as a checked-dynamic HANDLE (getName/destroy and the
* sockets/requests/freeSockets counters dispatch through the DOM
* handle ops). keepAlive: true THROWS the named construction fence —
* socket POOLING is not modeled (one dial per request); maxSockets
* accounting is real: over-limit requests defer their dial and queue.
* MAY THROW. */
| "http.agentNew"
/** The agent-threaded request rows: the http.request/https.request
* shape with a trailing `agent` dyn argument (an Agent handle, false —
* the one-shot Connection: close dial — or null/undefined for the
* default path). port < 0 means "no port option": the agent's settable
* defaultPort, then the scheme's. MAY THROW (a non-Agent value is
* Node's ERR_INVALID_ARG_TYPE). */
| "http.requestAgent"
| "http.requestAgentCb"
| "https.requestAgent"
| "https.requestAgentCb"
/** The createConnection form (the proxy's own dialer): args are
* (connCb, path, method, timeout, headers, autoEnd[, cb]) — connCb is
* a `() => net.Socket` closure the runtime invokes ONCE, synchronously
@@ -5122,6 +5163,7 @@ export const DYN_HANDLE_KINDS: ReadonlyMap<string, { tag: string; cls: string }>
["netServer", { tag: "SCR_DYNH_NET_SERVER", cls: "Server" }],
["http2Session", { tag: "SCR_DYNH_H2_SESSION", cls: "Http2Session" }],
["http2Stream", { tag: "SCR_DYNH_H2_STREAM", cls: "Http2Stream" }],
["httpClientReq", { tag: "SCR_DYNH_HTTP_CLIENT", cls: "ClientRequest" }],
]);
/** A static type that CONVERTS into a dyn value — the dynFrom domain:
@@ -6444,6 +6486,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"http.requestCb",
"http.requestConn",
"http.requestConnCb",
// The agent rows add the runtime agent-value gates (a non-Agent value
// is Node's ERR_INVALID_ARG_TYPE); agentNew's keepAlive: true throws
// the named pooling fence.
"http.agentNew",
"http.requestAgent",
"http.requestAgentCb",
"https.requestAgent",
"https.requestAgentCb",
"https.request",
"https.requestCb",
"https.requestFn",
+25 -3
View File
@@ -94,6 +94,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
"dyn.arrLen": { argTypes: [DYN], result: F64 },
"dyn.arrAt": { argTypes: [DYN, F64], result: DYN },
"dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL },
"dyn.toString": { argTypes: [DYN, STRING, STRING], result: STRING },
"dyn.defineProps": { argTypes: [DYN, DYN], result: DYN },
"dyn.typeof": { argTypes: [DYN], result: STRING },
@@ -469,6 +470,9 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// program-dependent (checked below); header pairs arrive flat.
"http.request": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], result: HTTPCLIENTREQ_T },
"http.requestCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], result: HTTPCLIENTREQ_T },
"http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64], result: DYN },
"http.requestAgent": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], result: HTTPCLIENTREQ_T },
"http.requestAgentCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], result: HTTPCLIENTREQ_T },
"http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
"net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
@@ -477,6 +481,13 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// placeholder the special case overrides).
"net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID },
"net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
"net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T },
"net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T },
"net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T },
"net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID },
"net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 },
"net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL },
"net.sockOnFinish": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], result: VOID },
"net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID },
// tls/https: cert/key/ca PEM arguments are strings OR Buffers (null =
// both accepted; the emitter passes data+len either way).
@@ -591,6 +602,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"http.requestConnCb": { argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], result: HTTPCLIENTREQ_T },
"https.request": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], result: HTTPCLIENTREQ_T },
"https.requestAgent": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], result: HTTPCLIENTREQ_T },
"https.requestAgentCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], result: HTTPCLIENTREQ_T },
// The requestFn binding's runtime-secure rows: https.request's shape
// with the leading `secure` bool.
"https.requestFn": { argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], result: HTTPCLIENTREQ_T },
@@ -1574,7 +1587,10 @@ function validateFunction(
// Symbol identity IS pointer identity (the frontend's rule).
e.left.type.kind === "symbol" ||
e.left.type.kind === "bytes" ||
e.left.type.kind === "promise")
e.left.type.kind === "promise" ||
// Runtime handles are objects to === (one handle per socket/
// request — pointer identity is JS's object equality).
DYN_HANDLE_KINDS.has(e.left.type.kind))
) {
// Reference identity: both operands must be the same ref type.
if (!typeEquals(e.left.type, e.right.type)) {
@@ -3410,9 +3426,15 @@ function validateFunction(
}
break;
}
if (e.fn === "http.requestCb" || e.fn === "http.requestUrlCb" || e.fn === "http.clientOnResponse") {
if (e.fn === "http.requestCb" || e.fn === "http.requestUrlCb" || e.fn === "http.clientOnResponse" ||
e.fn === "http.requestAgentCb" || e.fn === "https.requestAgentCb") {
// The response listener: void, no params or exactly (res: httpReq).
const cbT = e.args[e.fn === "http.requestCb" ? 7 : e.fn === "http.requestUrlCb" ? 3 : 1]?.type;
const cbT = e.args[
e.fn === "http.requestCb" ? 7
: e.fn === "http.requestUrlCb" ? 3
: e.fn === "http.requestAgentCb" ? 8
: e.fn === "https.requestAgentCb" ? 10
: 1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
ok = cbT.params[0]!.kind === "httpReq";
+27 -3
View File
@@ -671,9 +671,33 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, si
return NULL;
}
if (recv->kind == SCR_DYN_BYTES && dyn_bytes_proto_real(method)) {
dyn_throw_unsupported("Uint8Array", method);
return NULL;
if (recv->kind == SCR_DYN_BYTES) {
ScrBytes *bytes = recv->v.bytes;
size_t blen = bytes->len;
if (dyn_name_is(method, "at")) {
double iD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
double idx = iD < 0 ? (double)blen + iD : iD;
if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined());
return scr_dyn_new_num((double)bytes->data[(size_t)idx]);
}
if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) {
/* Both COPY (no views in this runtime — the static lane's
* documented divergence for subarray/Buffer.slice); the result
* keeps the receiver's Buffer flavor. */
double startD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
double endD = dyn_index_arg(args, argc, 1, (double)blen, what);
if (scr_exc_pending()) return NULL;
ScrBytes *out = scr_bytes_slice(bytes, startD, endD);
ScrDyn *d = recv->buffer ? scr_dyn_new_buffer_copy(out) : scr_dyn_new_bytes_copy(out);
scr_bytes_release(out);
return d;
}
if (dyn_bytes_proto_real(method)) {
dyn_throw_unsupported("Uint8Array", method);
return NULL;
}
}
/* NUM/BOOL/BYTES-remainder: the name is no method of this kind — JS's
File diff suppressed because it is too large Load Diff
+1
View File
@@ -2192,6 +2192,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2
scr_http_h2_req_header(req, name, value);
}
ScrHttpRes *res = scr_http_h2_res_new(s->sock, st);
scr_http_res_set_req(res, req); /* res.req — the compat pair's backref */
st->compat_req = scr_http_req_retain(req);
st->compat_res = scr_http_res_retain(res);
/* the req owns the READ side: the stream's own 'end' bookkeeping is
+21
View File
@@ -1527,6 +1527,27 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
* ignore silently — the loud choice, SEMANTICS.md). Receiver, key, and
* value are all BORROWED (the member retains the value in). */
static const char *scr_dyn_kind_name(const ScrDyn *d);
/* `key in v` with a RUNTIME key (the compile-time dynHasKey fold, per
* value): OBJ answers own-member presence, ARR answers 'length' or a
* valid dense index, every other kind false (tsc admits `in` only on
* object-typed operands). Borrows both; never throws. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
if (v->kind == SCR_DYN_OBJ) return scr_dyn_obj_get(v, key->data, key->len) != NULL;
if (v->kind == SCR_DYN_ARR) {
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true;
if (key->len == 0 || key->len > 15) return false;
size_t idx = 0;
for (size_t i = 0; i < key->len; i++) {
char c = key->data[i];
if (c < '0' || c > '9') return false;
if (i > 0 && idx == 0) return false; /* a leading zero is no canonical index */
idx = idx * 10 + (size_t)(c - '0');
}
return idx < v->v.arr.len;
}
return false;
}
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (recv->kind == SCR_DYN_OBJ) {
scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
+240 -34
View File
@@ -464,12 +464,34 @@ struct ScrNetSocket {
* dial_ips[dial_i]; the LAST failure's message surfaces (Node's
* autoSelectFamily aggregate is the documented divergence) */
bool lookup_wait;
/* a deferred dial (the http agent's maxSockets queue): registered and
* "connecting" (writes buffer), the actual dial starts on
* scr_net_sock_dial_start */
bool dial_deferred;
ScrStr **dial_ips;
size_t dial_n, dial_i;
/* the in-flight dial chain's LAST failure — surfaced (moved into
* pending_err) only when the list exhausts; a later success drops it */
ScrStr *dial_err;
ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls;
/* Flow control (pause()/resume()): user_paused holds reads OFF even
* with consumers (kernel/TCP backpressure is the buffer); flowing
* counts as a consumer even with no 'data' listener
* (resume()'s flow-and-discard drain, how Node reaches 'end' on an
* unconsumed stream). resume() clears user_paused and sets flowing. */
bool user_paused;
bool flowing;
/* destroySoon(): end() now, destroy once the FIN actually went out */
bool destroy_on_finish;
/* socket.bytesWritten: every byte accepted by the write paths (user
* writes, protocol heads, pipe deliveries — plaintext on TLS sockets) */
size_t bytes_written;
/* end(callback)/write(chunk, callback): 'finish'-shaped callbacks fire
* from the SWEEP (never the registering stack — Node defers them too).
* finish_ls waits for the FIN (wr_done); wcb_ls waits for the write
* 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 */
/* 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) */
@@ -564,6 +586,8 @@ void scr_net_sock_release(ScrNetSocket *s) {
scr_net_ls_drop(&s->conn_ls);
scr_net_ls_drop(&s->timeout_ls);
scr_net_ls_drop(&s->readable_ls);
scr_net_ls_drop(&s->finish_ls);
scr_net_ls_drop(&s->wcb_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]);
@@ -751,8 +775,9 @@ static ScrNetSocket *scr_net_sock_new(void) {
/* Consumer-driven read arming (the stdin discipline: never read a byte
* nobody consumes — the socket buffer is the backpressure). */
static void scr_net_sock_update_read(ScrNetSocket *s) {
bool want = s->fd >= 0 && !s->connecting && !s->rd_eof &&
bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused &&
(s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL ||
s->flowing /* resume(): flow (and discard sans listeners) */ ||
s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ ||
s->tops != NULL /* a transport is ALWAYS a consumer: the TLS
* layer must process protocol frames (the
@@ -804,6 +829,13 @@ static void scr_net_sock_maybe_finish_write(ScrNetSocket *s) {
if (s->tops && s->t_est) s->tops->shutdown_write(s->tctx);
shutdown(s->fd, SHUT_WR);
s->wr_done = true;
if (s->finish_ls.n > 0) s->finish_pending = true; /* end(cb): the sweep fires it */
if (s->destroy_on_finish) {
/* destroySoon(): the FIN is out — tear down now (Node's 'finish'-then-
* destroy). The close emits through the ordinary sweep. */
scr_net_sock_close_fd(s);
return;
}
if (s->rd_eof) scr_net_sock_close_fd(s);
else scr_net_sock_update_read(s); /* the drain-to-EOF mode arms here */
}
@@ -833,8 +865,15 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) {
* dead or ended write half (Node fires 'error' there; this slice drops —
* SEMANTICS.md documents the bound). */
static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len) {
if (s->fd < 0 || s->wr_ending || s->close_emitted || len == 0) return;
if (s->connecting || s->wlen > s->whead || (s->tops && !s->t_est)) {
/* A socket whose dial hasn't STARTED yet (the agent's deferred dial,
* the caller-lookup wait) has no fd but is logically connecting —
* bytes buffer and flush at establishment like any mid-connect write. */
bool pre_dial = s->fd < 0 && (s->dial_deferred || s->lookup_wait) &&
!s->wr_ending && !s->close_emitted;
if (!pre_dial && (s->fd < 0 || s->wr_ending || s->close_emitted)) return;
if (len == 0) return;
s->bytes_written += len; /* accepted bytes (Node counts buffered ones too) */
if (pre_dial || s->connecting || s->wlen > s->whead || (s->tops && !s->t_est)) {
/* mid-connect and mid-handshake bytes buffer; the flush after
* 'connect'/establishment sends them */
scr_net_sock_buffer(s, data, len);
@@ -959,8 +998,8 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s);
static void scr_net_sock_read(ScrNetSocket *s) {
if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */
for (;;) {
if (s->fd < 0) return;
bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data;
if (s->fd < 0 || s->user_paused) return;
bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing;
/* buffered (peeked/unshifted) bytes first — they precede the
* kernel's in the stream; a TLS engine drains them through its bio */
if (!s->tops && flowing && s->rlen > s->rhead) {
@@ -1532,18 +1571,13 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) {
return scr_str_new(ip, strlen(ip));
}
ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
ScrClosure *cb /*moves, nullable*/) {
ScrNetSocket *s = scr_net_sock_new();
if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true);
s->peer_port = (int)port;
const char *h = host && host->len > 0 ? host->data : "localhost";
if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
if (!scr_net_poller_init()) {
fputs("scriptc: event poller init failed\n", stderr);
abort();
}
/* The dial itself (peer_ip/peer_port already set): the sockaddr arms,
* socket(), and the nonblocking connect(). Shared by the immediate dial
* (scr_net_connect) and the DEFERRED one (the http agent's maxSockets
* queue — the socket exists, buffers writes, and dials when a slot
* frees). */
static void scr_net_sock_dial_peer(ScrNetSocket *s) {
const char *h = s->peer_ip;
struct sockaddr_in a4;
struct sockaddr_in6 a6;
struct sockaddr *sa = NULL;
@@ -1569,8 +1603,8 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
s->pending_err = scr_str_new(msg, strlen(msg));
s->had_error = true;
s->emit_close = true;
scr_net_sock_register(s);
return s;
s->connecting = false;
return;
}
int fd = socket(sa->sa_family, SOCK_STREAM, 0);
if (fd < 0) {
@@ -1596,8 +1630,24 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
s->peer_port);
s->pending_err = scr_str_new(msg, strlen(msg));
s->had_error = true;
s->connecting = false;
scr_net_sock_close_fd(s);
}
}
ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
ScrClosure *cb /*moves, nullable*/) {
ScrNetSocket *s = scr_net_sock_new();
if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true);
s->peer_port = (int)port;
const char *h = host && host->len > 0 ? host->data : "localhost";
if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
if (!scr_net_poller_init()) {
fputs("scriptc: event poller init failed\n", stderr);
abort();
}
scr_net_sock_dial_peer(s);
scr_net_sock_register(s);
return s;
}
@@ -1713,6 +1763,31 @@ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) {
scr_throw_lowering_fence(fence);
}
/* A dial the caller starts LATER (the http agent's maxSockets queue):
* the socket exists, registers, and buffers writes as "connecting" —
* scr_net_sock_dial_start runs the actual dial when a slot frees. */
ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/) {
ScrNetSocket *s = scr_net_sock_new();
s->peer_port = (int)port;
const char *h = host && host->len > 0 ? host->data : "localhost";
if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
if (!scr_net_poller_init()) {
fputs("scriptc: event poller init failed\n", stderr);
abort();
}
s->connecting = true; /* logically connecting while the dial waits */
s->dial_deferred = true;
scr_net_sock_register(s);
return s;
}
void scr_net_sock_dial_start(ScrNetSocket *s) {
if (!s->dial_deferred || s->close_emitted || s->emit_close || s->fd >= 0) return;
s->dial_deferred = false;
scr_net_sock_dial_peer(s);
}
/* ── the caller-lookup dial (net.connect with a lookup option) ─────────
*
* portless's createLoopbackConnection: connect({ host, port,
@@ -1891,6 +1966,78 @@ void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
scr_net_sock_end(s);
}
/* socket.pause(): reads stay off — the kernel buffer (and TCP flow
* control) holds arrived bytes. Delivery restarts on
* resume(), always from the poller/sweep, never this stack. Answers the
* socket (+1) — Node's chaining. */
ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s) {
s->user_paused = true;
scr_net_sock_update_read(s);
return scr_net_sock_retain(s);
}
/* socket.resume(): flowing mode — a consumer even with no 'data'
* listener (arrived bytes discard, so 'end' can be reached, Node's
* resumed-but-unconsumed stream). Buffered bytes deliver from the next
* sweep (flags_pending sees the flowing consumer), not this stack.
* Answers the socket (+1) — Node's chaining. */
ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s) {
s->user_paused = false;
s->flowing = true;
scr_net_sock_update_read(s);
return scr_net_sock_retain(s);
}
/* socket.setNoDelay(enable): TCP_NODELAY on the live fd (client dials
* already set it — Node's default there is off, a documented divergence
* in the dial path; this call makes the state explicit either way).
* Answers the socket (+1) — Node's chaining. */
ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable) {
if (s->fd >= 0) {
int v = enable ? 1 : 0;
setsockopt(s->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof v);
}
return scr_net_sock_retain(s);
}
/* 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) {
if (s->fd < 0) return;
if (s->wr_done) {
scr_net_sock_close_fd(s);
return;
}
s->destroy_on_finish = true;
s->wr_ending = true;
scr_net_sock_maybe_finish_write(s);
}
/* end(callback): fires once the FIN went out ('finish'), from the sweep. */
void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/) {
if (s->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&s->finish_ls, cb, NULL, true);
if (s->wr_done) s->finish_pending = true; /* already finished: next sweep */
}
/* write(chunk, callback): fires when the write buffer drains (this
* surface's flush moment), from the sweep. */
void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) {
if (s->close_emitted) {
scr_closure_release(cb);
return;
}
scr_net_ls_add(&s->wcb_ls, cb, NULL, true);
}
double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; }
/* socket.readable: true until the read half is done (peer FIN / destroy). */
bool scr_net_sock_readable(ScrNetSocket *s) { return s->fd >= 0 && !s->rd_eof; }
/* socket.setTimeout(ms): ms > 0 arms the idle timer NOW (Node starts the
* clock at the call — connecting time counts), ms <= 0 disables. */
void scr_net_sock_set_timeout(ScrNetSocket *s, double ms) {
@@ -2351,10 +2498,13 @@ static bool scr_net_flags_pending(void) {
if (scr_net_proto_pending()) return true;
for (ScrNetSocket *s = scr_net_socks; s; s = s->next) {
if (s->pending_err || (s->emit_close && !s->close_emitted)) return true;
if (s->finish_pending && s->finish_ls.n > 0) return true;
if (s->wcb_ls.n > 0 && (s->wlen == s->whead || s->fd < 0)) return true;
/* bytes already out of the kernel with a consumer waiting — decrypted
* plaintext inside a transport, or unshifted bytes in the receive
* buffer: the poller can't re-signal them, the sweep must deliver */
bool consumers = s->data_ls.n > 0 || s->pipe_dst || s->native_data;
bool consumers = (s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing) &&
!s->user_paused;
if (consumers && s->fd >= 0) {
if (s->tops && s->t_est && s->tops->pending && s->tops->pending(s->tctx)) return true;
if (!s->tops && s->rlen > s->rhead) return true;
@@ -2393,7 +2543,8 @@ static void scr_net_sweep(void) {
while (sock) {
ScrNetSocket *next = sock->next; /* may unregister below */
scr_net_sock_retain(sock); /* callbacks may drop every other ref */
if (sock->fd >= 0 && (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data) &&
if (sock->fd >= 0 && !sock->user_paused &&
(sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data || sock->flowing) &&
((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) ||
(!sock->tops && sock->rlen > sock->rhead))) {
/* deliver bytes the kernel can't re-signal (see flags_pending) */
@@ -2403,6 +2554,26 @@ static void scr_net_sweep(void) {
return;
}
}
if (sock->wcb_ls.n > 0 && (sock->wlen == sock->whead || sock->fd < 0)) {
/* write(chunk, cb): the buffer drained (this surface's flush
* moment) — the callbacks fire off the sweep, never the writing
* 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);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
return;
}
}
if (sock->finish_pending) {
sock->finish_pending = false;
scr_net_fire0_this(&sock->finish_ls, sock, SCR_DYNH_NET_SOCKET);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
return;
}
}
if (sock->pending_err) {
ScrStr *msg = sock->pending_err;
sock->pending_err = NULL;
@@ -2454,6 +2625,8 @@ static void scr_net_sweep(void) {
scr_net_ls_drop(&sock->conn_ls);
scr_net_ls_drop(&sock->timeout_ls);
scr_net_ls_drop(&sock->readable_ls);
scr_net_ls_drop(&sock->finish_ls);
scr_net_ls_drop(&sock->wcb_ls);
if (sock->pipe_dst) {
scr_net_sock_release(sock->pipe_dst);
sock->pipe_dst = NULL;
@@ -2718,6 +2891,9 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(cb), once);
} else if (scr_net_dynh_name_is(name, "readable")) {
scr_net_sock_on_readable(s, scr_dyn_listener_closure0(cb), once);
} else if (scr_net_dynh_name_is(name, "finish")) {
/* fires once when the FIN goes out — once either way */
scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb));
} else if (scr_net_dynh_name_is(name, "drain")) {
/* This surface never backpressures (write answers true) — an
* accepted, never-fired registration is the consistent answer. */
@@ -2743,28 +2919,51 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
return NULL;
}
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
scr_net_dynh_unsupported("write", "write(chunk, callback) is not modeled");
return NULL;
}
if (chunk->kind == SCR_DYN_STR) scr_net_sock_write_str(s, chunk->v.str);
else scr_net_sock_write_bytes(s, chunk->v.bytes);
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
/* write(chunk, cb): fires when the buffer drains (sweep-deferred) */
scr_net_sock_on_write_flush(s, scr_dyn_listener_closure0(args[1]));
}
return scr_dyn_new_bool(true); /* backpressure is not modeled (SEMANTICS.md) */
}
if (strcmp(method, "end") == 0) {
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *cb = NULL;
if (chunk->kind == SCR_DYN_FUNC) { /* end(callback) */
cb = chunk;
chunk = scr_dyn_undefined();
} else if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
cb = args[1]; /* end(chunk, callback) */
}
if (cb != NULL) scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb));
if (chunk->kind == SCR_DYN_STR) scr_net_sock_end_str(s, chunk->v.str);
else if (chunk->kind == SCR_DYN_BYTES) scr_net_sock_end_bytes(s, chunk->v.bytes);
else if (chunk->kind == SCR_DYN_UNDEF || argc == 0) scr_net_sock_end(s);
else if (chunk->kind == SCR_DYN_FUNC) {
scr_net_dynh_unsupported("end", "end(callback) is not modeled");
return NULL;
} else {
else {
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
return NULL;
}
return scr_dyn_retain(self);
}
if (strcmp(method, "pause") == 0) {
scr_net_sock_release(scr_net_sock_pause(s)); /* the chaining +1; self answers */
return scr_dyn_retain(self);
}
if (strcmp(method, "resume") == 0) {
scr_net_sock_release(scr_net_sock_resume(s));
return scr_dyn_retain(self);
}
if (strcmp(method, "setNoDelay") == 0) {
/* setNoDelay([enable]) — missing/undefined means true, Node */
bool enable = argc == 0 || args[0]->kind == SCR_DYN_UNDEF || scr_dyn_truthy(args[0]);
scr_net_sock_release(scr_net_sock_set_nodelay(s, enable));
return scr_dyn_retain(self);
}
if (strcmp(method, "destroySoon") == 0) {
scr_net_sock_destroy_soon(s);
return scr_dyn_retain(scr_dyn_undefined());
}
if (strcmp(method, "destroy") == 0) {
if (argc > 0 && args[0]->kind != SCR_DYN_UNDEF) {
scr_net_dynh_unsupported("destroy", "destroy(error) carries a payload this surface does not model");
@@ -2826,10 +3025,10 @@ 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", "setNoDelay", "setKeepAlive",
"address", "pause", "resume", "ref", "unref", "cork", "uncork", "read", "unpipe",
static const char *const known[] = { "connect", "setKeepAlive",
"address", "ref", "unref", "cork", "uncork", "read", "unpipe",
"off", "removeListener", "removeAllListeners", "emit", "prependListener",
"prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", "destroySoon", NULL };
"prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL };
for (size_t i = 0; known[i]; i++) {
if (strcmp(method, known[i]) == 0) {
scr_net_dynh_unsupported(method, NULL);
@@ -2862,14 +3061,21 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) {
* undefined — the static lane's false→undefined mapping. */
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, "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
* is not modeled; SEMANTICS.md) */
return scr_dyn_new_num(16384);
}
if (scr_net_dynh_tls != NULL) {
ScrDyn *out = NULL;
if (scr_net_dynh_tls->get(s, key, &out)) return out;
}
{
static const char *const known[] = { "remotePort", "remoteFamily", "localAddress",
"localPort", "localFamily", "bytesRead", "bytesWritten", "connecting", "pending",
"readyState", "bufferSize", "timeout", "readable", "closed", "errored", NULL };
"localPort", "localFamily", "bytesRead", "connecting", "pending",
"readyState", "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);
+70
View File
@@ -2681,6 +2681,8 @@ typedef enum {
SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */
SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */
SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */
SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */
SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */
SCR_DYNH_COUNT,
} ScrDynHandleTag;
@@ -2861,6 +2863,9 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value
* non-object kinds throw Node's catchable TypeErrors (strict-mode
* wording). All three operands BORROWED (the value is retained in). */
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
/* `key in v` with a runtime key — the dynHasKey fold per value (OBJ own
* members, ARR length/valid indices, false elsewhere). Never throws. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key);
/* Bare `typeof v` on a dyn value: the dyn kind's JS answer (+1 string;
* null answers "object"). Never throws. */
ScrStr *scr_dyn_typeof(const ScrDyn *d);
@@ -4819,6 +4824,22 @@ void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
void scr_net_sock_destroy(ScrNetSocket *s);
/* Flow control (pause/resume — see the struct's flag comments), the
* FIN-flushed destroy (destroySoon), TCP_NODELAY, the deferred write/
* finish callbacks (write(chunk, cb) / end(cb) — sweep-fired), and the
* counters/flags the compat surface reads. */
ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */
ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */
ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */
void scr_net_sock_destroy_soon(ScrNetSocket *s);
void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/);
void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/);
double scr_net_sock_bytes_written(ScrNetSocket *s);
bool scr_net_sock_readable(ScrNetSocket *s);
/* The deferred dial (the http agent's maxSockets queue): the socket
* registers "connecting" and buffers writes; dial_start runs the dial. */
ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/); /* +1 */
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) */
@@ -5180,6 +5201,23 @@ void scr_http_upgrade_thunk3(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock
double scr_http_req_status(ScrHttpReq *r); /* < 0 = the undefined arm (server request) */
ScrNetSocket *scr_http_req_socket(ScrHttpReq *r); /* +1 */
void scr_http_req_resume(ScrHttpReq *r);
/* pause()/resume() hold and drain 'data'/'end' delivery (the parser keeps
* consuming; the drain rides the emit queue); setTimeout delegates to the
* socket's idle timer; the flags back req.destroyed/req.readable. */
void scr_http_req_pause(ScrHttpReq *r);
void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/);
bool scr_http_req_destroyed_flag(ScrHttpReq *r);
bool scr_http_req_readable(ScrHttpReq *r);
/* flushHeaders/cork/uncork/writableCorked, the res.req backref, the
* socket-delegated setTimeout, and write(chunk, cb)'s deferred callback. */
void scr_http_res_flush_headers(ScrHttpRes *r);
void scr_http_res_cork(ScrHttpRes *r);
void scr_http_res_uncork(ScrHttpRes *r);
double scr_http_res_writable_corked(ScrHttpRes *r);
bool scr_http_res_destroyed_flag(ScrHttpRes *r);
void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed, nullable*/);
void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/);
void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/);
/* req.setEncoding(enc) — the socket twin's contract; may throw. */
void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/);
void scr_http_req_destroy(ScrHttpReq *r);
@@ -5244,6 +5282,30 @@ ScrHttpClientReq *scr_http_request_ex(ScrStr *host /*borrowed*/, double port,
ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn); /* +1 */
/* ── the http Agent (new http.Agent(opts) — option surface, getName, and
* the maxSockets queue over one-dial-per-request connections; keep-alive
* POOLING is not modeled: keepAlive: true fences at construction).
* agent_new answers the SCR_DYNH_HTTP_AGENT handle (+1 dyn) or throws.
* max_sockets/max_free arrive < 0 for "unset" (Infinity / 256);
* timeout_ms < 0 = no idle timer. request_agent_ex threads the agent dyn
* (undefined/null = the default path; false = one-shot with Connection:
* close; an Agent handle = queue accounting); port < 0 means "no port
* option" — the agent's (settable) defaultPort, then the scheme's. */
ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
double max_sockets, double max_free, double timeout_ms,
double port /* < 0 = unset: the option-merge default */); /* +1 */
ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx); /* +1 */
ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
#ifdef SCR_RC_AUDIT
long scr_http_live_count(void);
#endif
@@ -5394,6 +5456,14 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The agent-threaded twin (the scr_http_request_agent story over TLS). */
ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const struct ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The fetch unit's https leg (defined in scr_tls.c): a client transport
* context (SNI/verify against the URL hostname — the DIALED address may
* be a resolved IP) plus the wrap hook scr_http_request_ex installs on
+12
View File
@@ -983,6 +983,18 @@ ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
fn, 443, &scr_tls_cli_wrap, cli);
}
ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
ScrTlsCli *cli = scr_tls_cli_new(host, reject_unauthorized, ca, ca_len);
return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
agent, cb, fn, 443, &scr_tls_cli_wrap, cli);
}
/* ── RUNTIME options records (the divergence-66 stance) ────────────────
* A non-literal options value — the checked-dynamic JS lane's dyn record
* — reads its members at RUNTIME. Members the literal path lowers take
+87
View File
@@ -0,0 +1,87 @@
// The ServerResponse compat members on CHECKED-DYNAMIC handles (the
// untyped-wrapper listener boxes req/res into the DOM): setHeader
// chaining + getHeaders snapshots, flushHeaders, the raw-array writeHead
// form (per-name override of setHeader state, other names survive),
// cork/uncork with the writableCorked counter (corked writes coalesce and
// flush on the last uncork), write-with-encoding ('hex' decodes the
// string chunk), the deferred write/end callbacks, res.req identity, and
// the destroyed flags (false at work time, true inside 'close').
// Strict ping-pong: one exchange at a time, every line causally ordered.
'use strict';
const http = require('http');
function wrap(fn) {
return function () {
return fn.apply(this, arguments);
};
}
const server = http.createServer();
server.on('request', wrap(function (req, res) {
console.log('srv', req.url, 'req.destroyed:', req.destroyed, 'req.readable:', req.readable);
res.setHeader('a', '1');
console.log('setHeader chains:', res.setHeader('b', '2') === res);
console.log('getHeaders:', JSON.stringify(res.getHeaders()));
if (req.url === '/raw') {
res.writeHead(200, ['test', '9', 'b', 'override']);
res.end('raw');
return;
}
if (req.url === '/cork') {
res.writeHead(200);
console.log('corked0:', res.writableCorked);
res.cork();
res.write('AA');
res.cork();
res.write('BB');
console.log('corked2:', res.writableCorked);
res.uncork();
res.uncork();
console.log('corked-after:', res.writableCorked, 'finished:', res.writableFinished);
res.end('CC', wrap(function () {
console.log('end cb, finished:', res.writableFinished);
}));
return;
}
if (req.url === '/hex') {
res.writeHead(200);
res.write('414243', 'hex', wrap(function () {
console.log('write cb ran');
}));
res.end();
return;
}
console.log('res.req is req:', res.req === req);
res.flushHeaders();
console.log('headersSent after flushHeaders:', res.headersSent);
res.on('close', wrap(function () {
console.log('res close, destroyed:', res.destroyed);
}));
res.end('done');
}));
function get(path, done) {
http.get({ port: server.address().port, path: path }, wrap(function (res) {
let body = '';
res.on('data', wrap(function (c) {
body += c.toString('utf8');
}));
res.on('end', wrap(function () {
console.log(path, '->', res.statusCode, JSON.stringify(body),
'test:', res.headers.test, 'a:', res.headers.a, 'b:', res.headers.b);
done();
}));
}));
}
server.listen(0, function () {
get('/plain', function () {
get('/raw', function () {
get('/cork', function () {
get('/hex', function () {
server.close();
});
});
});
});
});
+51
View File
@@ -0,0 +1,51 @@
// IncomingMessage flow control on checked-dynamic handles: req.pause()
// holds 'data' delivery (the buffered bytes drain on resume(), off the
// resuming stack) AND defers 'end' until the drain; the client response
// reads readable/destroyed (true until 'end', destroyed only at
// teardown). The timings are causal, not raced: the pause happens on the
// first delivered chunk, the resume 30ms later, and the request's final
// chunk lands 60ms in — after the resume, so the body always reassembles
// complete.
'use strict';
const http = require('http');
function wrap(fn) {
return function () {
return fn.apply(this, arguments);
};
}
const server = http.createServer();
server.on('request', wrap(function (req, res) {
let got = '';
let paused = false;
req.on('data', wrap(function (c) {
got += c.toString('utf8');
if (!paused) {
paused = true;
console.log('pausing after first chunk:', got.length >= 4);
req.pause();
setTimeout(wrap(function () {
console.log('resuming');
req.resume();
}), 30);
}
}));
req.on('end', wrap(function () {
console.log('server end, body:', JSON.stringify(got));
res.end('ok');
}));
}));
server.listen(0, function () {
const r = http.request({ port: server.address().port, method: 'POST', path: '/' }, wrap(function (res) {
console.log('res.readable pre:', res.readable, 'destroyed pre:', res.destroyed);
res.resume();
res.on('end', wrap(function () {
console.log('res.readable post:', res.readable);
server.close();
}));
}));
r.write('abcd');
setTimeout(function () { r.end('efghij'); }, 60);
});
@@ -0,0 +1,54 @@
// The net.Socket compat members on checked-dynamic handles: setNoDelay
// chains, pause()/resume() return the socket (and genuinely gate reads),
// write(chunk, cb)/end(chunk, cb) fire their callbacks off the sweep (cb
// after the bytes entered the buffer / the FIN went out), bytesWritten
// counts accepted bytes, readable flips at the read half's end,
// destroySoon tears down after the FIN, and dyn Buffer at()/slice()
// dispatch (slice COPIES — the documented no-views stance — and keeps
// the Buffer flavor). One connection, causally ordered lines; the pause
// guard fires once no matter how the chunks arrive.
'use strict';
const net = require('net');
function wrap(fn) {
return function () {
return fn.apply(this, arguments);
};
}
const server = net.createServer(wrap(function (sock) {
console.log('setNoDelay is sock:', sock.setNoDelay() === sock);
console.log('readable pre:', sock.readable);
sock.write('hello', wrap(function () {
console.log('write cb, bytesWritten:', sock.bytesWritten);
}));
sock.end('!', wrap(function () {
console.log('finish cb');
}));
}));
server.listen(0, wrap(function () {
const c = net.connect(server.address().port);
let got = '';
let pausedOnce = false;
c.on('data', wrap(function (ch) {
got += ch.toString('utf8');
if (!pausedOnce) {
pausedOnce = true;
console.log('pause returns c:', c.pause() === c);
setTimeout(wrap(function () { c.resume(); }), 20);
}
}));
c.on('end', wrap(function () {
console.log('client got:', JSON.stringify(got), 'readable:', c.readable);
const dyn = wrap(function (b) { return b; });
const buf = dyn(Buffer.from('abc\n'));
const piece = buf.slice(0, 3);
console.log('at(-1):', buf.at(-1), 'slice:', piece.length, piece[0], piece[2]);
c.destroySoon();
}));
c.on('close', wrap(function () {
console.log('client closed, destroyed:', c.destroyed);
server.close();
}));
}));
+44
View File
@@ -0,0 +1,44 @@
// The http.Agent surface: construction with options, getName's exact
// string shapes (host/port/localAddress/family/socketPath arms), the
// option properties (maxSockets Infinity default, maxFreeSockets,
// protocol, settable defaultPort), a request THROUGH the agent (the
// sockets/requests tables key by getName; freeSockets stays empty — no
// pooling exists), and destroy(). The default-port merge: an agent
// carrying defaultPort dials it when the request omits port. Causally
// ordered, one exchange at a time.
'use strict';
const http = require('http');
const agent = new http.Agent();
console.log('getName():', agent.getName());
console.log('getName({}):', agent.getName({}));
console.log('getName(full):', agent.getName({ host: '0.0.0.0', port: 80, localAddress: '192.168.1.1' }));
console.log('getName(family4):', agent.getName({ family: 4 }));
console.log('getName(sockPath):', agent.getName({ socketPath: '/tmp/x.sock' }));
console.log('maxSockets:', agent.maxSockets, 'maxFreeSockets:', agent.maxFreeSockets);
console.log('protocol:', agent.protocol, 'keepAlive:', agent.keepAlive);
const server = http.createServer(function (req, res) {
res.end('ok:' + req.url);
});
server.listen(0, function () {
const port = server.address().port;
agent.defaultPort = port;
console.log('defaultPort set:', agent.defaultPort === port);
const name = agent.getName({ port: port });
// No port option: the agent's defaultPort dials (Node's option merge).
http.get({ host: 'localhost', agent: agent, path: '/merged' }, function (res) {
console.log('status:', res.statusCode);
console.log('sockets has name:', name in agent.sockets, 'len:', agent.sockets[name].length);
console.log('requests has name:', name in agent.requests);
console.log('freeSockets empty:', JSON.stringify(agent.freeSockets));
let body = '';
res.on('data', function (c) { body += c.toString('utf8'); });
res.on('end', function () {
console.log('body:', body);
agent.destroy();
server.close();
});
});
});
@@ -0,0 +1,38 @@
// REAL maxSockets accounting through an Agent: with maxSockets 1 the
// second request queues (agent.requests[name] holds it) until the first
// exchange finishes, and the counters read exactly Node's at every
// causally-pinned point — sockets[name].length at response time, the
// queue length, and the drained tables afterwards. Socket REUSE is not
// pinned (this client dials one connection per request — the documented
// divergence); only the counters and the strict first-then-second
// ordering are.
'use strict';
const http = require('http');
const agent = new http.Agent({ maxSockets: 1 });
const server = http.createServer(function (req, res) {
res.end('ok:' + req.url);
});
server.listen(0, function () {
const port = server.address().port;
const name = agent.getName({ port: port });
let done = 0;
const finish = function () {
if (++done === 2) {
console.log('after both: sockets?', name in agent.sockets, 'requests?', name in agent.requests);
agent.destroy();
server.close();
}
};
http.get({ port: port, agent: agent, path: '/a' }, function (res) {
console.log('res1', res.statusCode, 'active:', agent.sockets[name].length, 'queued:', agent.requests[name].length);
res.resume();
res.on('end', finish);
});
http.get({ port: port, agent: agent, path: '/b' }, function (res2) {
console.log('res2', res2.statusCode, 'still queued:', name in agent.requests, 'active:', agent.sockets[name].length);
res2.resume();
res2.on('end', finish);
});
});
+4 -3
View File
@@ -10,9 +10,10 @@ import * as fs from "node:fs";
import * as dns from "node:dns";
import * as readline from "node:readline";
// A pooling Agent instance cannot be honored — pointed fence at the
// construction AND at the option.
const agent = new http.Agent({ keepAlive: true });
// The Agent lowers to a checked-dynamic handle now — but only from a
// LITERAL options object; a runtime options value cannot be walked.
const agentOpts: http.AgentOptions = { maxSockets: 1 };
const agent = new http.Agent(agentOpts);
http.get({ port: 80, agent }, () => {});
// agent: false needs a LITERAL headers object to inject Connection: close
@@ -1,132 +1,125 @@
options-record.ts:15:7 - error SC2020: 'Agent' is part of the standard library types but has no scriptc lowering yet
options-record.ts:15:7 - error SC2020: 'AgentOptions' is part of the standard library types but has no scriptc lowering yet
14 | // construction AND at the option.
15 | const agent = new http.Agent({ keepAlive: true });
| ^~~~~
16 | http.get({ port: 80, agent }, () => {});
14 | // LITERAL options object; a runtime options value cannot be walked.
15 | const agentOpts: http.AgentOptions = { maxSockets: 1 };
| ^~~~~~~~~
16 | const agent = new http.Agent(agentOpts);
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
options-record.ts:15:15 - error SC1090: constructing an http Agent (compiled clients dial one connection per request and close it with the response — pass agent: false for Node's one-shot semantics, or omit the option) is not supported yet
options-record.ts:16:30 - error SC2020: 'new http.Agent with a non-literal options value' is part of the standard library types but has no scriptc lowering yet
14 | // construction AND at the option.
15 | const agent = new http.Agent({ keepAlive: true });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 | http.get({ port: 80, agent }, () => {});
15 | const agentOpts: http.AgentOptions = { maxSockets: 1 };
16 | const agent = new http.Agent(agentOpts);
| ^~~~~~~~~
17 | http.get({ port: 80, agent }, () => {});
options-record.ts:16:22 - error SC2020: 'a get 'agent' option carrying an Agent value' is part of the standard library types but has no scriptc lowering yet
hint: spell the options as an object literal at the construction site
15 | const agent = new http.Agent({ keepAlive: true });
16 | http.get({ port: 80, agent }, () => {});
| ^~~~~
17 |
options-record.ts:22:22 - error SC2020: 'get with agent: false and a non-literal headers record' is part of the standard library types but has no scriptc lowering yet
hint: compiled clients dial one connection per request and close it with the response — agent: false (lowered: sends Connection: close) and agent: null/undefined (the default) compile; pooling Agent instances have no lowering
options-record.ts:21:22 - error SC2020: 'get with agent: false and a non-literal headers record' is part of the standard library types but has no scriptc lowering yet
20 | const dynHeaders: Record<string, string> = { "x-a": "1" };
21 | http.get({ port: 80, agent: false, headers: dynHeaders }, () => {});
21 | const dynHeaders: Record<string, string> = { "x-a": "1" };
22 | http.get({ port: 80, agent: false, headers: dynHeaders }, () => {});
| ^~~~~~~~~~~~
22 |
23 |
hint: agent: false lowers by injecting Connection: close into a LITERAL headers object — add connection: "close" to the record instead, or write the headers inline
options-record.ts:24:22 - error SC2020: 'get option 'socketPath'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:25:22 - error SC2020: 'get option 'socketPath'' is part of the standard library types but has no scriptc lowering yet
23 | // Documented client options with no lowering fence by name.
24 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
24 | // Documented client options with no lowering fence by name.
25 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~~
25 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {});
26 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {});
hint: hostname/host, port, path, method, timeout, headers, and agent are the supported options
options-record.ts:25:22 - error SC2020: 'get option 'localAddress'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:26:22 - error SC2020: 'get option 'localAddress'' is part of the standard library types but has no scriptc lowering yet
24 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
25 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {});
25 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
26 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~~
26 |
27 |
hint: hostname/host, port, path, method, timeout, headers, and agent are the supported options
options-record.ts:33:22 - error SC2020: 'the undocumented get option 'zorp' with an effectful value' is part of the standard library types but has no scriptc lowering yet
options-record.ts:34:22 - error SC2020: 'the undocumented get option 'zorp' with an effectful value' is part of the standard library types but has no scriptc lowering yet
32 | }
33 | http.get({ port: 80, zorp: effect() }, () => {});
33 | }
34 | http.get({ port: 80, zorp: effect() }, () => {});
| ^~~~~~~~~~~~~~
34 |
35 |
hint: Node ignores undocumented option keys and so does this compiler, but Node still evaluates the value — hoist it (const v = ...) or drop the entry
options-record.ts:36:41 - error SC2020: 'tls.createServer option 'ciphers'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:37:41 - error SC2020: 'tls.createServer option 'ciphers'' is part of the standard library types but has no scriptc lowering yet
35 | // Documented TLS server options with no lowering fence by name.
36 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {});
36 | // Documented TLS server options with no lowering fence by name.
37 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {});
38 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {});
hint: cert and key (PEM strings or Buffers) are the supported options
options-record.ts:37:41 - error SC2020: 'tls.createServer option 'ticketKeys'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:38:41 - error SC2020: 'tls.createServer option 'ticketKeys'' is part of the standard library types but has no scriptc lowering yet
36 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {});
37 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {});
37 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {});
38 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38 |
39 |
hint: cert and key (PEM strings or Buffers) are the supported options
options-record.ts:41:20 - error SC2020: 'fs.watch with the recursive option' is part of the standard library types but has no scriptc lowering yet
options-record.ts:42:20 - error SC2020: 'fs.watch with the recursive option' is part of the standard library types but has no scriptc lowering yet
40 | // stated defaults are accepted (corpus 1751).
41 | fs.watch("/tmp", { recursive: true }, () => {});
41 | // stated defaults are accepted (corpus 1751).
42 | fs.watch("/tmp", { recursive: true }, () => {});
| ^~~~~~~~~~~~~~~
42 | fs.watch("/tmp", { persistent: false }, () => {});
43 | fs.watch("/tmp", { persistent: false }, () => {});
hint: recursive watching has no lowering yet — kqueue watches the one opened path; watch each path
options-record.ts:42:20 - error SC2020: 'fs.watch with persistent disabled' is part of the standard library types but has no scriptc lowering yet
options-record.ts:43:20 - error SC2020: 'fs.watch with persistent disabled' is part of the standard library types but has no scriptc lowering yet
41 | fs.watch("/tmp", { recursive: true }, () => {});
42 | fs.watch("/tmp", { persistent: false }, () => {});
42 | fs.watch("/tmp", { recursive: true }, () => {});
43 | fs.watch("/tmp", { persistent: false }, () => {});
| ^~~~~~~~~~~~~~~~~
43 |
44 |
hint: an open watcher keeps the loop alive until close() — that IS the lowering; persistent: false (a watcher the process does not wait for) has no lowering
options-record.ts:45:38 - error SC2020: 'lookup option 'hints'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:46:38 - error SC2020: 'lookup option 'hints'' is part of the standard library types but has no scriptc lowering yet
44 | // dns.lookup: hints/all are documented knobs with no lowering.
45 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {});
45 | // dns.lookup: hints/all are documented knobs with no lowering.
46 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {});
| ^~~~~~~~~
46 | dns.lookup("localhost", { family: 4, all: true }, () => {});
47 | dns.lookup("localhost", { family: 4, all: true }, () => {});
hint: family: 4 is the supported option
options-record.ts:46:38 - error SC2020: 'lookup option 'all'' is part of the standard library types but has no scriptc lowering yet
options-record.ts:47:38 - error SC2020: 'lookup option 'all'' is part of the standard library types but has no scriptc lowering yet
45 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {});
46 | dns.lookup("localhost", { family: 4, all: true }, () => {});
46 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {});
47 | dns.lookup("localhost", { family: 4, all: true }, () => {});
| ^~~~~~~~~
47 |
48 |
hint: the all-addresses callback shape has no lowering — the lowered callback is (err, address, family) over one IPv4 answer
options-record.ts:50:85 - error SC2020: 'createInterface with a finite crlfDelay' is part of the standard library types but has no scriptc lowering yet
options-record.ts:51:85 - error SC2020: 'createInterface with a finite crlfDelay' is part of the standard library types but has no scriptc lowering yet
49 | // needs an interactive terminal.
50 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 });
50 | // needs an interactive terminal.
51 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 });
| ^~~
51 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] });
52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] });
hint: the lowered splitter always joins \r\n across chunks (Node's crlfDelay: Infinity) — Infinity is the accepted value
options-record.ts:51:74 - error SC2020: 'createInterface with a completer' is part of the standard library types but has no scriptc lowering yet
options-record.ts:52:74 - error SC2020: 'createInterface with a completer' is part of the standard library types but has no scriptc lowering yet
50 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 });
51 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] });
51 | readline.createInterface({ input: process.stdin, output: process.stdout, crlfDelay: 100 });
52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
52 |
53 |
hint: tab completion needs an interactive terminal — the lowered interface reads piped lines (terminal: false)
hint: tab completion needs an interactive terminal — the lowered interface reads piped lines (terminal: false)
+4 -2
View File
@@ -640,18 +640,20 @@ server.listen(0, '127.0.0.1', wrap(function() {
`;
test("a real-but-unmodeled member on a dyn handle throws the loud ladder", async () => {
// writeContinue stays unmodeled (cork/uncork/flushHeaders graduated
// to real dispatches — the ladder's exemplar moves with the surface).
const r = await compileAndRun(
"handle-unmodeled-member",
serverPreamble +
`server.on('request', wrap(function(req, res) {
res.cork();
res.writeContinue();
}));
` + listenAndHit,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain(
"'ServerResponse.prototype.cork' on a dynamic value is not supported yet",
"'ServerResponse.prototype.writeContinue' on a dynamic value is not supported yet",
);
});