mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Answer net.Socket write() with Node's backpressure boolean, and fire 'drain'
A TCP socket's write() answered nothing in typed code and true through
untyped values, and a 'drain' listener never fired. Node 24 answers
false when the bytes the write leaves unsent reach the socket's
65536-byte high-water mark (writeOrBuffer computes it after the socket
writes), sets needDrain, and emits 'drain' when the write buffer
empties; writes to an ended or destroyed socket answer false.
The runtime's write entry points now return that answer, record
needDrain, and fire the socket's 'drain' listeners from the flush that
empties the buffer, unless the write half is ending or the socket is
gone. writableLength reads the unsent bytes and writableNeedDrain the
flag. on('drain') and once('drain') register on the typed path and
through untyped values, and socket.write() may be used as a value.
The fallback node:net declarations type write() as boolean, add the
'drain' event and both properties.
RUNTIME_ABI_VERSION goes from 43 to 44: the socket write entry points
return a boolean.
This commit is contained in:
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v43");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v44");
|
||||
});
|
||||
});
|
||||
|
||||
+14
-5
@@ -5322,10 +5322,10 @@ declare module "node:zlib" {
|
||||
* with the real port discoverable as address().port (the composed read
|
||||
* is the lowered form — bare address() has no lowering); close([cb])
|
||||
* firing once connections drain; socket write/end/destroy/pipe and the
|
||||
* data/end/close/error/connect events (on and once — listeners take at
|
||||
* most the declared parameter). write/end return void here (Node returns
|
||||
* the backpressure boolean / the socket); on/once return void (chaining
|
||||
* is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* data/end/close/error/connect/drain events (on and once — listeners
|
||||
* take at most the declared parameter). write returns Node's
|
||||
* backpressure boolean; end returns void here (Node returns the socket);
|
||||
* on/once return void (chaining is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* 127.0.0.1 — SEMANTICS.md); every failure is the async 'error' event,
|
||||
* and an 'error' with no listener exits 1 like an unhandled EventEmitter
|
||||
* 'error'. */
|
||||
@@ -5357,7 +5357,13 @@ declare module "net" {
|
||||
readonly bytesWritten: number;
|
||||
/* write's callback runs once the data is flushed, with null; end's
|
||||
* once the socket finishes. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void;
|
||||
/* false when the bytes this write left unsent reach the 65536-byte
|
||||
* high-water mark, as in Node; 'drain' fires when they are all sent. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): boolean;
|
||||
/* the bytes written and not yet sent */
|
||||
readonly writableLength: number;
|
||||
/* a write answered false and 'drain' has not fired yet */
|
||||
readonly writableNeedDrain: boolean;
|
||||
end(callback?: () => void): void;
|
||||
end(data: string | Uint8Array, callback?: () => void): void;
|
||||
destroy(): void;
|
||||
@@ -5405,6 +5411,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5429,6 +5436,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5452,6 +5460,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
|
||||
@@ -309,6 +309,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
e.fn === "net.sockOnClose" ||
|
||||
e.fn === "net.sockOnConnect" ||
|
||||
e.fn === "net.sockOnTimeout" ||
|
||||
e.fn === "net.sockOnDrain" ||
|
||||
e.fn === "net.sockOnReadable" ||
|
||||
e.fn === "net.sockOnReady" ||
|
||||
e.fn === "http.reqOnEnd" ||
|
||||
@@ -329,6 +330,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
"net.sockOnClose": "scr_net_sock_on_close",
|
||||
"net.sockOnConnect": "scr_net_sock_on_connect",
|
||||
"net.sockOnTimeout": "scr_net_sock_on_timeout",
|
||||
"net.sockOnDrain": "scr_net_sock_on_drain",
|
||||
"net.sockOnReadable": "scr_net_sock_on_readable",
|
||||
"net.sockOnReady": "scr_net_sock_on_ready",
|
||||
"http.reqOnEnd": "scr_http_req_on_end",
|
||||
|
||||
@@ -1337,6 +1337,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"net.sockResetAndDestroy": "scr_net_sock_reset_and_destroy",
|
||||
"net.sockAddress": "scr_net_sock_address",
|
||||
"net.sockBytesWritten": "scr_net_sock_bytes_written",
|
||||
"net.sockWritableLength": "scr_net_sock_writable_length",
|
||||
"net.sockWritableNeedDrain": "scr_net_sock_writable_need_drain",
|
||||
"net.sockAddrField": "scr_net_sock_addr_field",
|
||||
"net.sockBytesRead": "scr_net_sock_bytes_read",
|
||||
"net.sockConnecting": "scr_net_sock_connecting",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 43 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v43" as const;
|
||||
export const RUNTIME_ABI_VERSION = 44 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v44" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -129,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -2425,7 +2425,9 @@ function lowerNetSocketMethodCall(
|
||||
};
|
||||
}
|
||||
if (name === "write" || name === "end") {
|
||||
requireStatementPosition(lowerer, call, `socket.${name}(...)`);
|
||||
// write() answers Node's backpressure boolean; end() stays a statement
|
||||
if (name === "end") requireStatementPosition(lowerer, call, `socket.${name}(...)`);
|
||||
const resultT = name === "write" ? BOOL : VOID;
|
||||
// write(chunk, encoding) — the two-argument encoding form: 'buffer'
|
||||
// beside a string chunk is Writable's encoding check (a synchronous
|
||||
// ERR_UNKNOWN_ENCODING TypeError, "Unknown encoding: buffer" — the
|
||||
@@ -2451,7 +2453,7 @@ function lowerNetSocketMethodCall(
|
||||
const socket = coerceToHandle(lowerer, access.expression, NETSOCKET_T);
|
||||
const chunk = lowerer.lowerExpr(args[0]!);
|
||||
const fn: IrLibFn = chunk.type.kind === "string" ? "net.sockWrite" : "net.sockWriteBytes";
|
||||
return { kind: "libCall", fn, args: [socket, chunk], type: VOID, loc };
|
||||
return { kind: "libCall", fn, args: [socket, chunk], type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2510,7 +2512,7 @@ function lowerNetSocketMethodCall(
|
||||
NARROW_DATA_HINT,
|
||||
);
|
||||
}
|
||||
main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc };
|
||||
main = { kind: "libCall", fn, args: [receiver(), data], type: resultT, loc };
|
||||
}
|
||||
const cb = lowerer.lowerExpr(last);
|
||||
let register: IrExpr;
|
||||
@@ -2531,8 +2533,24 @@ function lowerNetSocketMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
stmts.push({ kind: "exprStmt", expr: main, loc });
|
||||
return { kind: "seqExpr", stmts, result: register, type: VOID, loc };
|
||||
if (resultT.kind === "void") {
|
||||
stmts.push({ kind: "exprStmt", expr: main, loc });
|
||||
return { kind: "seqExpr", stmts, result: register, type: VOID, loc };
|
||||
}
|
||||
// write(data, cb): the data is written, the callback registered, and
|
||||
// the write's answer is the value
|
||||
const okLocal = lowerer.declareHiddenLocal("%sockWriteOk", BOOL);
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: okLocal.id, init: main, loc },
|
||||
{ kind: "exprStmt", expr: register, loc },
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "varRef", localId: okLocal.id, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const maxArgs = name === "write" ? 1 : 1;
|
||||
const minArgs = name === "write" ? 1 : 0;
|
||||
@@ -2552,17 +2570,17 @@ function lowerNetSocketMethodCall(
|
||||
const data = lowerer.lowerExpr(args[0]!);
|
||||
if (data.type.kind === "string") {
|
||||
const fn: IrLibFn = name === "write" ? "net.sockWrite" : "net.sockEndStr";
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc };
|
||||
}
|
||||
if (data.type.kind === "bytes" && data.type.elem === "u8") {
|
||||
const fn: IrLibFn = name === "write" ? "net.sockWriteBytes" : "net.sockEndBytes";
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc };
|
||||
}
|
||||
if (data.type.kind === "dyn") {
|
||||
// An untyped JS payload (a helper's parameter): the runtime
|
||||
// dispatches STR/BYTES and throws Node's chunk TypeError otherwise.
|
||||
const fn: IrLibFn = name === "write" ? "net.sockWriteDyn" : "net.sockEndDyn";
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
|
||||
return { kind: "libCall", fn, args: [receiver, data], type: resultT, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`${name} of '${lowerer.fmt(data.type)}' data`,
|
||||
@@ -2944,6 +2962,7 @@ function lowerNetSocketMethodCall(
|
||||
event === "connect" ||
|
||||
event === "ready" ||
|
||||
event === "timeout" ||
|
||||
event === "drain" ||
|
||||
event === "readable"
|
||||
) {
|
||||
const { cb } = lowerCallbackArg(
|
||||
@@ -2962,11 +2981,13 @@ function lowerNetSocketMethodCall(
|
||||
? "net.sockOnClose"
|
||||
: event === "timeout"
|
||||
? "net.sockOnTimeout"
|
||||
: event === "readable"
|
||||
? "net.sockOnReadable"
|
||||
: event === "ready"
|
||||
? "net.sockOnReady"
|
||||
: "net.sockOnConnect";
|
||||
: event === "drain"
|
||||
? "net.sockOnDrain"
|
||||
: event === "readable"
|
||||
? "net.sockOnReadable"
|
||||
: event === "ready"
|
||||
? "net.sockOnReady"
|
||||
: "net.sockOnConnect";
|
||||
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
|
||||
}
|
||||
if (event === "lookup") {
|
||||
@@ -3957,6 +3978,18 @@ export function lowerServerProperty(
|
||||
const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T);
|
||||
return { kind: "libCall", fn: "net.sockBytesWritten", args: [receiver], type: F64, loc };
|
||||
}
|
||||
if (
|
||||
recvKind === "netSocket" &&
|
||||
lowerer.isStdlibMember(expr) &&
|
||||
(expr.name.text === "writableLength" || expr.name.text === "writableNeedDrain")
|
||||
) {
|
||||
// The write buffer's unsent bytes, and whether a write answered false
|
||||
// and 'drain' has not fired yet.
|
||||
const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T);
|
||||
return expr.name.text === "writableLength"
|
||||
? { kind: "libCall", fn: "net.sockWritableLength", args: [receiver], type: F64, loc }
|
||||
: { kind: "libCall", fn: "net.sockWritableNeedDrain", args: [receiver], type: BOOL, loc };
|
||||
}
|
||||
if (recvKind === "netSocket" && lowerer.isStdlibMember(expr) && expr.name.text === "readable") {
|
||||
// true until the read half is done (peer FIN / destroy).
|
||||
const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T);
|
||||
|
||||
@@ -1543,15 +1543,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* true } and a callback taking an address string or list, then dials
|
||||
* the answers in order. MAY THROW (host validation, a lookup throw). */
|
||||
"net.connectLookupDyn": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T },
|
||||
"net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID },
|
||||
"net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
|
||||
/** socket.write: Node's Writable answer — false when the bytes the write
|
||||
* left unsent reach the 65536-byte high-water mark ('drain' follows),
|
||||
* and on an ended or closed socket. */
|
||||
"net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: BOOL },
|
||||
"net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: BOOL },
|
||||
"net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID },
|
||||
"net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID },
|
||||
"net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
|
||||
/** write/end with a CHECKED-DYNAMIC chunk (an untyped JS payload into a
|
||||
* typed socket): the runtime dispatches STR/BYTES and throws Node's
|
||||
* ERR_INVALID_ARG_TYPE chunk TypeError on any other kind. */
|
||||
"net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID },
|
||||
"net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: BOOL },
|
||||
"net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID },
|
||||
"net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID },
|
||||
"net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID },
|
||||
@@ -2009,6 +2012,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL],
|
||||
result: VOID,
|
||||
},
|
||||
/** on('drain') / once('drain'): fires after a write answered false, once
|
||||
* the write buffer empties. */
|
||||
"net.sockOnDrain": {
|
||||
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL],
|
||||
result: VOID,
|
||||
},
|
||||
"net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID },
|
||||
/** socket.encrypted — `boolean | undefined`: the true arm iff the
|
||||
* socket carries a TLS transport (Node types `encrypted: true` on
|
||||
@@ -2135,6 +2144,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** socket.address(): { address, family, port } or {} as a dyn. */
|
||||
"net.sockAddress": { argTypes: [NETSOCKET_T], result: DYN },
|
||||
"net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 },
|
||||
/** socket.writableLength: the bytes written and not yet sent. */
|
||||
"net.sockWritableLength": { argTypes: [NETSOCKET_T], result: F64 },
|
||||
/** socket.writableNeedDrain: a write answered false and 'drain' has not
|
||||
* fired, with the write half open. */
|
||||
"net.sockWritableNeedDrain": { argTypes: [NETSOCKET_T], result: BOOL },
|
||||
/** remotePort (0), remoteFamily (1), localAddress (2), localPort (3),
|
||||
* localFamily (4): dyn values, undefined when unknown. */
|
||||
"net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN },
|
||||
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v43");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v44");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v43");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v44");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7515-net-socket-backpressure.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -366,7 +366,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: RUNTIME_PACK_MATRIX.target,
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -469,7 +469,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: matrix.target,
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_runtime_abi_v43(void) {}
|
||||
void scr_runtime_abi_v44(void) {}
|
||||
|
||||
void scr_init(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
|
||||
@@ -81,7 +81,7 @@ void scr_init(void);
|
||||
/* Program objects emitted by the bundled LLVM helper reference this symbol.
|
||||
* Its versioned spelling makes a mismatched manual runtime link fail before
|
||||
* the program can start. */
|
||||
void scr_runtime_abi_v43(void);
|
||||
void scr_runtime_abi_v44(void);
|
||||
void scr_runtime_workers_v8(void);
|
||||
|
||||
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
|
||||
|
||||
@@ -565,6 +565,11 @@ struct ScrNetSocket {
|
||||
ScrNetLs lookup_ls;
|
||||
struct ScrNetLookupEv *lookup_evs;
|
||||
ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls;
|
||||
/* Writable backpressure: a user write answered false (the bytes it left
|
||||
* unsent reached SCR_NET_WRITE_HWM), so 'drain' fires once the write
|
||||
* buffer empties */
|
||||
ScrNetLs drain_ls;
|
||||
bool need_drain;
|
||||
/* 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
|
||||
@@ -747,6 +752,7 @@ 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->drain_ls);
|
||||
scr_net_ls_drop(&s->finish_ls);
|
||||
scr_net_ls_drop(&s->wcb_ls);
|
||||
scr_net_ls_drop(&s->ready_ls);
|
||||
@@ -1103,6 +1109,40 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) {
|
||||
s->wlen += len;
|
||||
}
|
||||
|
||||
/* Node's net.Socket writableHighWaterMark (the Writable default,
|
||||
* getDefaultHighWaterMark()). */
|
||||
#define SCR_NET_WRITE_HWM 65536
|
||||
|
||||
/* 'drain' once a false write's bytes are all out: Node's afterWrite emits
|
||||
* it when needDrain is set and nothing is pending, unless the stream is
|
||||
* ending or destroyed. */
|
||||
static void scr_net_sock_maybe_drain(ScrNetSocket *s) {
|
||||
if (!s->need_drain || s->wlen > s->whead) return;
|
||||
if (s->wr_ending || s->close_emitted || s->fd < 0) return;
|
||||
s->need_drain = false;
|
||||
scr_net_fire0_this(&s->drain_ls, s, SCR_DYNH_NET_SOCKET);
|
||||
}
|
||||
|
||||
static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len);
|
||||
|
||||
/* A user write: Writable.write's answer, as Node 24's writeOrBuffer
|
||||
* computes it after the socket writes — the bytes the write path left
|
||||
* unsent below the high-water mark — with needDrain set when it is false.
|
||||
* A write the kernel takes at once answers true whatever its size; the
|
||||
* buffered bytes of a false one drain from the flush. A write to an ended
|
||||
* or closed socket answers false. */
|
||||
static bool scr_net_sock_user_write(ScrNetSocket *s, const char *data, size_t len) {
|
||||
ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s;
|
||||
bool pre_dial = w->fd < 0 && (w->dial_deferred || w->lookup_wait) && !w->wr_ending &&
|
||||
!w->close_emitted;
|
||||
bool live = pre_dial || !(w->fd < 0 || w->wr_ending || w->close_emitted);
|
||||
scr_net_sock_write_raw(s, data, len);
|
||||
if (!live) return false;
|
||||
bool ok = w->wlen - w->whead < SCR_NET_WRITE_HWM;
|
||||
if (!ok) w->need_drain = true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* The write path shared by write()/end(data)/pipe delivery. Ignored on a
|
||||
* dead or ended write half (Node fires 'error' there; this slice drops —
|
||||
* SEMANTICS.md documents the bound). */
|
||||
@@ -1252,6 +1292,9 @@ static void scr_net_sock_flush(ScrNetSocket *s) {
|
||||
s->whead = s->wlen = 0;
|
||||
scr_net_sock_update_write(s);
|
||||
scr_net_sock_maybe_finish_write(s);
|
||||
/* Node's afterWrite for a write the socket completed later: 'drain'
|
||||
* now, unless the write half is ending or the socket is gone */
|
||||
scr_net_sock_maybe_drain(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3150,16 +3193,40 @@ void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowe
|
||||
|
||||
/* ── the socket surface ──────────────────────────────────────────────── */
|
||||
|
||||
void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) {
|
||||
bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) {
|
||||
char *owned;
|
||||
const char *bytes = scr_str_utf8(data, &owned);
|
||||
scr_net_sock_write_raw(s, bytes, data->len);
|
||||
bool ok = scr_net_sock_user_write(s, bytes, data->len);
|
||||
scr_str_utf8_end(owned);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
|
||||
bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
|
||||
SCR_BYTES_SNAPSHOT(data);
|
||||
scr_net_sock_write_raw(s, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
|
||||
return scr_net_sock_user_write(s, (const char *)data->data,
|
||||
(data->len * scr_bytes_elem_size(data->elem)));
|
||||
}
|
||||
|
||||
/* socket.writableLength: the bytes written and not yet sent. */
|
||||
double scr_net_sock_writable_length(ScrNetSocket *s) {
|
||||
ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s;
|
||||
return (double)(w->wlen - w->whead);
|
||||
}
|
||||
|
||||
/* socket.writableNeedDrain: a write answered false and 'drain' has not
|
||||
* fired, on a socket whose write half is open. */
|
||||
bool scr_net_sock_writable_need_drain(ScrNetSocket *s) {
|
||||
ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s;
|
||||
return w->need_drain && !w->wr_ending && !w->close_emitted && w->fd >= 0;
|
||||
}
|
||||
|
||||
/* on('drain') / once('drain'). */
|
||||
void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) {
|
||||
if (s->close_emitted) {
|
||||
scr_closure_release(cb);
|
||||
return;
|
||||
}
|
||||
scr_net_ls_add(&s->drain_ls, cb, NULL, once);
|
||||
}
|
||||
|
||||
void scr_net_sock_end(ScrNetSocket *s) {
|
||||
@@ -5860,6 +5927,7 @@ static void scr_net_cleanup_atexit(void) {
|
||||
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->drain_ls);
|
||||
scr_net_ls_drop(&s->lookup_ls);
|
||||
/* Tear down the protocol layer's parser NOW: an in-flight http
|
||||
* exchange (a handler that threw before res.end()) holds req/res,
|
||||
@@ -5984,8 +6052,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
|
||||
/* 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. */
|
||||
scr_net_sock_on_drain(s, scr_dyn_listener_closure0(cb), once);
|
||||
} else if (scr_net_dynh_tls != NULL && name->kind == SCR_DYN_STR &&
|
||||
scr_net_dynh_tls->on(s, name->v.str->data, cb, once)) {
|
||||
/* a TLS event name ('secureConnect'), handled by the TLS unit */
|
||||
@@ -6008,13 +6075,13 @@ 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 (chunk->kind == SCR_DYN_STR) scr_net_sock_write_str(s, chunk->v.str);
|
||||
else scr_net_sock_write_bytes(s, chunk->v.bytes);
|
||||
bool ok = chunk->kind == SCR_DYN_STR ? scr_net_sock_write_str(s, chunk->v.str)
|
||||
: 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) */
|
||||
return scr_dyn_new_bool(ok);
|
||||
}
|
||||
if (strcmp(method, "end") == 0) {
|
||||
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
|
||||
@@ -6500,10 +6567,11 @@ void scr_net_dyn_install(void) {
|
||||
* STR/BYTES dispatch onto the typed entries; undefined/null end()s plain
|
||||
* (Node accepts them); anything else throws Node's ERR_INVALID_ARG_TYPE
|
||||
* chunk TypeError. Both borrowed. */
|
||||
void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_STR) scr_net_sock_write_str(s, d->v.str);
|
||||
else if (d->kind == SCR_DYN_BYTES) scr_net_sock_write_bytes(s, d->v.bytes);
|
||||
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
||||
bool scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_STR) return scr_net_sock_write_str(s, d->v.str);
|
||||
if (d->kind == SCR_DYN_BYTES) return scr_net_sock_write_bytes(s, d->v.bytes);
|
||||
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
||||
return false;
|
||||
}
|
||||
|
||||
void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d) {
|
||||
|
||||
@@ -227,15 +227,21 @@ ScrNetSocket *scr_net_connect_lookup(double port, ScrStr *host /*borrowed*/,
|
||||
ScrClosure *lookup /*moves*/, void *answer_fn); /* +1 */
|
||||
void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowed, nullable*/,
|
||||
ScrArr *ips /*borrowed, nullable*/);
|
||||
void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/);
|
||||
void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
|
||||
/* write(): Node's Writable answer — false when the bytes the write left
|
||||
* unsent reach the 65536-byte high-water mark, after which 'drain' fires
|
||||
* when the write buffer empties; false on an ended or closed socket. */
|
||||
bool scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/);
|
||||
bool scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
|
||||
double scr_net_sock_writable_length(ScrNetSocket *s);
|
||||
bool scr_net_sock_writable_need_drain(ScrNetSocket *s);
|
||||
void scr_net_sock_on_drain(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
|
||||
void scr_net_sock_end(ScrNetSocket *s);
|
||||
void scr_net_sock_end_str(ScrNetSocket *s, ScrStr *data /*borrowed*/);
|
||||
void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
|
||||
/* Checked-dynamic chunk forms (an untyped JS payload into a typed
|
||||
* socket): STR/BYTES dispatch; end() takes undefined/null as no chunk;
|
||||
* anything else throws Node's ERR_INVALID_ARG_TYPE chunk TypeError. */
|
||||
void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
|
||||
bool 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
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// net.Socket write() backpressure, as in Node 24: write answers false when
|
||||
// the bytes it leaves unsent reach the 65536-byte high-water mark, which a
|
||||
// write made before the connection does, writableNeedDrain stays true
|
||||
// until 'drain', and 'drain' fires once the buffered bytes are sent. An
|
||||
// echo client that waits for 'drain' when write answers false moves every
|
||||
// byte.
|
||||
import { connect, createServer, Socket } from "node:net";
|
||||
|
||||
const CHUNK = 65536;
|
||||
const TOTAL = 64 * CHUNK;
|
||||
const chunk = Buffer.alloc(CHUNK, 120);
|
||||
|
||||
const server = createServer((socket: Socket) => {
|
||||
socket.pipe(socket);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
||||
const client = connect(port, "127.0.0.1");
|
||||
console.log("small write before connect", client.write("hi"), client.writableLength);
|
||||
console.log("large write before connect", client.write(Buffer.alloc(70000, 121)), client.writableLength);
|
||||
console.log("need drain", client.writableNeedDrain);
|
||||
let sent = 70002;
|
||||
let received = 0;
|
||||
const pump = (): void => {
|
||||
while (sent < TOTAL) {
|
||||
sent += CHUNK;
|
||||
if (!client.write(chunk)) {
|
||||
client.once("drain", pump);
|
||||
return;
|
||||
}
|
||||
}
|
||||
client.end();
|
||||
};
|
||||
client.once("drain", () => {
|
||||
console.log("drain fired, need drain", client.writableNeedDrain, "pending", client.writableLength);
|
||||
pump();
|
||||
});
|
||||
client.on("data", (data: Buffer) => {
|
||||
received += data.length;
|
||||
});
|
||||
client.on("end", () => {
|
||||
console.log("received", received, "of", sent);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2800, "san": 4200 }
|
||||
@@ -110,7 +110,7 @@ for (const backend of ["llvm"] as const) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
Reference in New Issue
Block a user