Fix chalk builder field calls and hot-shots send failures and tcp completion

This commit is contained in:
Chris Tate
2026-10-22 10:30:56 -05:00
parent 2e297caf99
commit ccfca67aec
14 changed files with 118 additions and 5 deletions
+19 -4
View File
@@ -179,6 +179,8 @@ interface Transport {
close(): void;
onError(listener: (err: Error) => void): void;
offError(listener: (err: Error) => void): void;
/** Emits 'error' on the socket, as hot-shots' `socket.emit('error')`. */
emitError(err: Error): void;
onClose(listener: () => void): void;
}
@@ -261,6 +263,9 @@ class UdpTransport implements Transport {
offError(listener: (err: Error) => void): void {
this.errors.remove(listener);
}
emitError(err: Error): void {
this.errors.emit(err);
}
onClose(listener: () => void): void {
this.socket.on("close", listener);
}
@@ -281,10 +286,10 @@ class TcpTransport implements Transport {
this.socket.on("error", (err) => errors.emit(err));
}
send(buf: Buffer, callback: SendCallback): void {
// The typed socket's write takes the data alone; the completion runs
// on the next tick.
this.socket.write(addEol(buf));
process.nextTick(() => callback(null));
// hot-shots writes the line with the 'ascii' encoding, which encodes
// strings as 'latin1' does. The callback runs once the data is written,
// so a message held back by a full socket buffer stays in flight.
this.socket.write(Buffer.from(addEol(buf), "latin1"), (err) => callback(err ?? null));
}
close(): void {
this.socket.destroy();
@@ -295,6 +300,9 @@ class TcpTransport implements Transport {
offError(listener: (err: Error) => void): void {
this.errors.remove(listener);
}
emitError(err: Error): void {
this.errors.emit(err);
}
onClose(listener: () => void): void {
this.socket.on("close", () => listener());
}
@@ -376,6 +384,9 @@ class UdsTransport implements Transport {
offError(listener: (err: Error) => void): void {
this.errors.remove(listener);
}
emitError(err: Error): void {
this.errors.emit(err);
}
onClose(listener: () => void): void {
this.closeListeners.push(listener);
}
@@ -999,6 +1010,10 @@ export class StatsD {
handler(errFormatted);
} else {
console.error(String(errFormatted));
// hot-shots also emits the error on the current socket, where the
// uds reconnection handler listens; with no listener it throws.
const current = this.transport;
if (current !== null) current.emitError(errFormatted);
}
}
};
@@ -9250,6 +9250,10 @@ export function lowerRecordFieldCall(
if (callee.type.kind === "object" && !init) {
const direct = callableInstanceStringCall(lowerer, call, callee, loc);
if (direct) return direct;
// Other argument counts call the %call closure through its value, as
// a call on the builder itself does (`theme.error("a", "b")`).
const closure = lowerer.hybridCallUnwrap(callee);
if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc);
}
if (callee.type.kind === "record" || callee.type.kind === "object")
callee = lowerer.hybridCallUnwrap(callee);
@@ -1,5 +1,6 @@
{
"order": [
"<repo>/packages/compiler/shims/chalk/v5.ts",
"<repo>/tests/fixtures/npm/cases/chalk5-builder/main.ts"
],
"diags": []
@@ -1,5 +1,6 @@
{
"order": [
"<repo>/packages/compiler/shims/chalk/v5.ts",
"<repo>/tests/fixtures/npm/cases/chalk5/main.ts"
],
"diags": []
@@ -1,5 +1,7 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-buffered/main.ts"
],
"diags": []
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-send-error/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-tcp/main.ts"
],
"diags": []
}
@@ -1,5 +1,7 @@
{
"order": [
"<repo>/packages/compiler/shims/hot-shots/client.ts",
"<repo>/packages/compiler/shims/hot-shots/v8.ts",
"<repo>/tests/fixtures/npm/cases/hot-shots-udp/main.ts"
],
"diags": []
+7
View File
@@ -55,6 +55,13 @@ double scr_dgram_addr_port(ScrDgramSocket *s);
void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */
void scr_dgram_unref(ScrDgramSocket *s);
void scr_dgram_ref(ScrDgramSocket *s);
/* Unix datagram sockets for the hot-shots package shim (the unix-dgram
* addon's surface): each answers its result (fd, 0) or a negative errno;
* send answers 1 when the socket would block. */
double scr_dgram_unix_socket(void);
double scr_dgram_unix_connect(double fd, ScrStr *path /*borrowed*/);
double scr_dgram_unix_send(double fd, ScrBytes *data /*borrowed*/);
double scr_dgram_unix_close(double fd);
/* Socket options: Node's checks and errors (ErrnoException "<syscall>
* <CODE>", ERR_SOCKET_BAD_BUFFER_SIZE, ERR_SOCKET_BUFFER_SIZE, the
* TypeError after close()). The TTL setters and setMulticastLoopback
+4 -1
View File
@@ -1,6 +1,7 @@
// chalk 5 builders used as values (the package compiles from scriptc's
// built-in shim): builders in function slots and arrays, styles picked at
// run time, visible at level 0, chalkStderr, and the root's own call.
// run time, builders in record fields called with any number of arguments,
// visible at level 0, chalkStderr, and the root's own call.
import chalk, { Chalk, chalkStderr, type ChalkInstance, type ForegroundColorName } from "chalk";
const show = (label: string, value: string): void => {
@@ -36,6 +37,8 @@ const off = new Chalk({ level: 0 });
show("visible off", off.visible("v") + "|" + off.red.visible("v") + "|" + off.red("plain"));
show("root call", c("a", 1, true) + "|" + c() + "|" + c(undefined) + "|" + c(null, "n"));
show("builder args", c.red(undefined) + "|" + c.red(null) + "|" + c.red(1, 2) + "|" + c.red());
const theme = { error: c.red, note: c.bold.blue };
show("field args", theme.error("a", "b") + "|" + theme.note("x", 1, true) + "|" + theme.error("one") + "|" + theme.error());
show("multiline", c.cyan("one\r\ntwo\nthree"));
show("stderr level", String(chalkStderr.level >= 0));
show("rgb levels", [1, 2, 3].map((level) => new Chalk({ level: level as 1 | 2 | 3 }).rgb(255, 136, 0)("o")).join("|"));
+27
View File
@@ -0,0 +1,27 @@
// The hot-shots StatsD client (compiled from scriptc's built-in shim) when a
// send fails: with a callback or an errorHandler the error goes there; with
// neither, the client logs it and emits it on the socket, where an 'error'
// event nobody listens for ends the process with status 1.
import { StatsD } from "hot-shots";
// A datagram larger than udp allows fails with EMSGSIZE before it leaves
// the host; port 9 keeps the message the same on every run.
const big = "x".repeat(70000);
const withCallback = new StatsD({ host: "127.0.0.1", port: 9 });
withCallback.increment(big, 1, 1, [], (err: Error | null | undefined) => {
console.log("callback:", err ? err.message : "no error");
withCallback.close();
const withHandler = new StatsD({
host: "127.0.0.1",
port: 9,
errorHandler: (error: Error) => {
console.log("errorHandler:", error.message);
withHandler.close();
console.log("without a callback or errorHandler");
new StatsD({ host: "127.0.0.1", port: 9 }).increment(big);
},
});
withHandler.increment(big);
});
@@ -0,0 +1 @@
{ "type": "module" }
+33
View File
@@ -0,0 +1,33 @@
// The hot-shots StatsD client over tcp (compiled from scriptc's built-in
// shim): a send completes once the socket has written its line, so close()
// waits for lines that a full socket buffer holds back, and the receiver
// gets every line before the connection ends.
import * as net from "node:net";
import { StatsD } from "hot-shots";
const LINES = 20000;
const name = "m".repeat(1000);
let bytes = 0;
let ended: () => void = () => {};
const serverDone = new Promise<void>((resolve) => {
ended = resolve;
});
const server = net.createServer((socket) => {
socket.pause();
setTimeout(() => socket.resume(), 150);
socket.on("data", (chunk: Buffer) => {
bytes += chunk.length;
});
socket.on("end", () => ended());
});
server.listen(0, "127.0.0.1", () => {
const port = (server.address() as net.AddressInfo).port;
const client = new StatsD({ host: "127.0.0.1", port, protocol: "tcp" });
for (let i = 0; i < LINES; i++) client.increment(name);
const closed = new Promise<void>((resolve) => client.close(() => resolve()));
void Promise.all([closed, serverDone]).then(() => {
console.log("closed; received", bytes, "of", LINES * (name.length + 5), "bytes");
server.close(() => console.log("server closed"));
});
});
+1
View File
@@ -0,0 +1 @@
{ "type": "module" }