Skip to content

Commit 911808f

Browse files
Copilotmikeharder
andauthored
[.github/shared] Add async backpressure-aware log() helper to console.js (#40568)
* Initial plan * Add async backpressure-aware log() helper to .github/shared/src/console.js Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> * make arg types exactly match * simplify, improve typings * improve backpressure test * comments * comment * Apply suggestion from @mikeharder * Remove duplicate exports in package.json Removed duplicate entries for console and error-reporting in exports. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> Co-authored-by: Mike Harder <mharder@microsoft.com>
1 parent 48b3faa commit 911808f

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

.github/shared/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"./array": "./src/array.js",
77
"./breaking-change": "./src/breaking-change.js",
88
"./changed-files": "./src/changed-files.js",
9+
"./console": "./src/console.js",
910
"./error-reporting": "./src/error-reporting.js",
1011
"./eslint-base-config": "./eslint.base.config.js",
1112
"./exec": "./src/exec.js",

.github/shared/src/console.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { once } from "node:events";
2+
import { format } from "node:util";
3+
4+
/**
5+
* Async, backpressure-aware console.log() replacement. console.log() can silently drop messages under backpressure.
6+
*
7+
* Use when you need to log a lot of text, and the console reader may be applying backpressure.
8+
*
9+
* Examples: `node app.js | tee out.txt`, GitHub Actions console log reader
10+
*
11+
* @type {(...args: Parameters<typeof console.log>) => Promise<void>}
12+
*/
13+
export async function log(...args) {
14+
const line = format(...args) + "\n";
15+
16+
if (!process.stdout.write(line)) {
17+
await once(process.stdout, "drain");
18+
}
19+
}
20+
21+
// ## Future Improvement
22+
//
23+
// The log() function currently handles backpressure per call, but concurrent callers
24+
// can still invoke `process.stdout.write()` before a prior `drain` completes
25+
// (eg callers using Promise.all()). If we ever need strict global backpressure control,
26+
// add a shared write queue (promise chain/mutex) to serialize all writes across calls.
27+
//
28+
// /** @type {Promise<void>} */
29+
// let writeQueue = Promise.resolve();
30+
//
31+
// /**
32+
// * Async, backpressure-aware console.log replacement.
33+
// *
34+
// * @type {(...args: Parameters<typeof console.log>) => Promise<void>}
35+
// */
36+
// export function log(...args) {
37+
// const line = format(...args) + "\n";
38+
//
39+
// const writeOnce = async () => {
40+
// if (!process.stdout.write(line)) {
41+
// await once(process.stdout, "drain");
42+
// }
43+
// };
44+
//
45+
// const next = writeQueue.then(writeOnce, writeOnce);
46+
// writeQueue = next.catch(() => {});
47+
// return next;
48+
// }
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { log } from "../src/console.js";
3+
4+
describe("console", () => {
5+
describe("log", () => {
6+
/** @type {import("vitest").MockInstance<typeof process.stdout.write>} */
7+
let writeSpy;
8+
9+
beforeEach(() => {
10+
writeSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
11+
});
12+
13+
afterEach(() => {
14+
writeSpy.mockRestore();
15+
});
16+
17+
it("writes a formatted line to stdout", async () => {
18+
await log("hello %s", "world");
19+
expect(writeSpy).toBeCalledWith("hello world\n");
20+
});
21+
22+
it("formats multiple arguments like console.log", async () => {
23+
await log("a", "b", "c");
24+
expect(writeSpy).toBeCalledWith("a b c\n");
25+
});
26+
27+
it("works with no arguments", async () => {
28+
await log();
29+
expect(writeSpy).toBeCalledWith("\n");
30+
});
31+
32+
it("awaits drain when stdout has backpressure", async () => {
33+
writeSpy.mockReturnValueOnce(false);
34+
35+
let resolved = false;
36+
const promise = log("backpressure test").then(() => {
37+
resolved = true;
38+
});
39+
40+
// Should still be pending before drain
41+
expect(resolved).toBe(false);
42+
expect(writeSpy).toBeCalledWith("backpressure test\n");
43+
44+
// Unblock backpressure
45+
process.stdout.emit("drain");
46+
await promise;
47+
48+
expect(resolved).toBe(true);
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)