mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
+41
-13
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
+1076
-30
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user