refactor: harden log stream writes

This commit is contained in:
Peter Steinberger
2026-01-21 03:03:23 +00:00
parent a0cd295c0f
commit 438a41f91f
5 changed files with 291 additions and 68 deletions

View File

@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from "vitest";
import { createSafeStreamWriter } from "./stream-writer.js";
describe("createSafeStreamWriter", () => {
it("signals broken pipes and closes the writer", () => {
const onBrokenPipe = vi.fn();
const writer = createSafeStreamWriter({ onBrokenPipe });
const stream = {
write: vi.fn(() => {
const err = new Error("EPIPE") as NodeJS.ErrnoException;
err.code = "EPIPE";
throw err;
}),
} as unknown as NodeJS.WriteStream;
expect(writer.writeLine(stream, "hello")).toBe(false);
expect(writer.isClosed()).toBe(true);
expect(onBrokenPipe).toHaveBeenCalledTimes(1);
onBrokenPipe.mockClear();
expect(writer.writeLine(stream, "again")).toBe(false);
expect(onBrokenPipe).toHaveBeenCalledTimes(0);
});
it("treats broken pipes from beforeWrite as closed", () => {
const onBrokenPipe = vi.fn();
const writer = createSafeStreamWriter({
onBrokenPipe,
beforeWrite: () => {
const err = new Error("EIO") as NodeJS.ErrnoException;
err.code = "EIO";
throw err;
},
});
const stream = { write: vi.fn(() => true) } as unknown as NodeJS.WriteStream;
expect(writer.write(stream, "hi")).toBe(false);
expect(writer.isClosed()).toBe(true);
expect(onBrokenPipe).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,64 @@
export type SafeStreamWriterOptions = {
beforeWrite?: () => void;
onBrokenPipe?: (err: NodeJS.ErrnoException, stream: NodeJS.WriteStream) => void;
};
export type SafeStreamWriter = {
write: (stream: NodeJS.WriteStream, text: string) => boolean;
writeLine: (stream: NodeJS.WriteStream, text: string) => boolean;
reset: () => void;
isClosed: () => boolean;
};
function isBrokenPipeError(err: unknown): err is NodeJS.ErrnoException {
const code = (err as NodeJS.ErrnoException)?.code;
return code === "EPIPE" || code === "EIO";
}
export function createSafeStreamWriter(options: SafeStreamWriterOptions = {}): SafeStreamWriter {
let closed = false;
let notified = false;
const noteBrokenPipe = (err: NodeJS.ErrnoException, stream: NodeJS.WriteStream) => {
if (notified) return;
notified = true;
options.onBrokenPipe?.(err, stream);
};
const handleError = (err: unknown, stream: NodeJS.WriteStream): boolean => {
if (!isBrokenPipeError(err)) {
throw err;
}
closed = true;
noteBrokenPipe(err, stream);
return false;
};
const write = (stream: NodeJS.WriteStream, text: string): boolean => {
if (closed) return false;
try {
options.beforeWrite?.();
} catch (err) {
return handleError(err, process.stderr);
}
try {
stream.write(text);
return !closed;
} catch (err) {
return handleError(err, stream);
}
};
const writeLine = (stream: NodeJS.WriteStream, text: string): boolean =>
write(stream, `${text}\n`);
return {
write,
writeLine,
reset: () => {
closed = false;
notified = false;
},
isClosed: () => closed,
};
}