Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/js/ahead-of-time/node/_http_outgoing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// What the compiler reads in place of src/js/node/_http_outgoing.ts, which `bun run` evaluates. The two do the same.
// Written against that file with the SHA-256 17a5f190f17251c9b4bea8ce90880918a91fffc6329dddc4d5ee859370eb9709.
// Written against that file with the SHA-256 88cc609e812d2ff8933707581fee5dd0d2bb35619f0dbcfe268a07ec05c0e9f4.
// When that file changes, bring the change here and write the new hash: test/internal/second-texts-of-modules.test.ts.

// Hardcoded module "node:_http_outgoing"
Expand Down Expand Up @@ -147,7 +147,7 @@ function isContentDispositionField(s: string) {

declare global {
namespace $Node {
interface OutgoingMessageOptions extends EventEmitterOptions {
interface OutgoingMessageOptions {
highWaterMark?: number;
rejectNonStandardBodyWrites?: boolean;
}
Expand Down Expand Up @@ -282,12 +282,11 @@ function OutgoingMessage(
rejectNonStandardBodyWrites: boolean | undefined = undefined,
keepAliveTimeout: number | undefined = undefined,
) {
Stream.$call(this, undefined);
if (options === kHandle) {
Stream.$call(this, undefined);
highWaterMark ??= getDefaultHighWaterMark();
if (rejectNonStandardBodyWrites === undefined) rejectNonStandardBodyWrites = false;
} else {
Stream.$call(this, options);
keepAliveTimeout = 0;
}

Expand Down
3 changes: 2 additions & 1 deletion src/js/node/_http_outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ function isContentDispositionField(s) {
}

function OutgoingMessage(options) {
Stream.$call(this, options);
// As in node, Stream gets no options. `undefined` and not a missing argument: that call needs no arity fix-up.
Stream.$call(this, undefined);

// Queue that holds all currently pending data, until the response will be
// assigned to the socket (until it will its turn in the HTTP pipeline).
Expand Down
5 changes: 5 additions & 0 deletions src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,11 @@ class ChildProcess extends EventEmitter {
channel;
killed = false;

// Not the default constructor, which hands its arguments to EventEmitter. Node's takes none.
constructor() {
super();
}

[Symbol.dispose]() {
if (!this.killed) {
this.kill();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// `captureRejections` in the options of an OutgoingMessage or a ServerResponse does nothing, as in node: EventEmitter is handed no options.
import { EventEmitter } from "node:events";
import http from "node:http";

// An emitter that captures rejections calls then() of the promise that a listener returns.
function captures(emitter) {
let called = false;
const promise = Promise.resolve();
promise.then = () => {
called = true;
};
emitter.on("asked", () => promise);
emitter.emit("asked");
return called;
}
const request = () => ({ method: "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
class Message extends http.OutgoingMessage {}
class Response extends http.ServerResponse {}
function Called(req, options) {
http.ServerResponse.call(this, req, options);
}
Object.setPrototypeOf(Called.prototype, http.ServerResponse.prototype);

for (const [label, make] of [
["new OutgoingMessage", options => new http.OutgoingMessage(options)],
["a class that extends OutgoingMessage", options => new Message(options)],
["new ServerResponse", options => new http.ServerResponse(request(), options)],
["a class that extends ServerResponse", options => new Response(request(), options)],
["ServerResponse called on an object", options => new Called(request(), options)],
]) {
for (const captureRejections of [true, "yes"]) {
let answer;
try {
answer = captures(make({ captureRejections }));
} catch (error) {
answer = error.code;
}
console.log(`${label}, captureRejections ${JSON.stringify(captureRejections)}: ${answer}`);
}
EventEmitter.captureRejections = true;
try {
console.log(`${label}, while EventEmitter.captureRejections is true: ${captures(make())}`);
} finally {
EventEmitter.captureRejections = false;
}
}
47 changes: 47 additions & 0 deletions test/js/node/child_process/child_process-node.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { bunEnv, bunExe, isWindows } from "harness";
import { createTest } from "node-harness";
import { ChildProcess, exec, fork, spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { tmpdir } from "node:os";
import path from "node:path";
import util from "node:util";
Expand Down Expand Up @@ -58,6 +59,52 @@ describe("ChildProcess.constructor", () => {
it("should be a function", () => {
strictEqual(typeof ChildProcess, "function");
});

// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/child_process.js#L255-L256
it("hands its arguments to no EventEmitter, like Node.js", () => {
// An emitter that captures rejections calls then() of the promise that a listener returns.
const captures = emitter => {
let calls = 0;
const promise = Promise.resolve();
promise.then = () => void calls++;
emitter.on("asked", () => promise);
emitter.emit("asked");
return calls > 0;
};
class Heir extends ChildProcess {}
const answers = {};
for (const [way, make] of [
["new ChildProcess", options => new ChildProcess(options)],
["a class that extends ChildProcess", options => new Heir(options)],
]) {
const answer = options => {
try {
return captures(make(options));
} catch (error) {
return error.code;
}
};
answers[way] = {
"captureRejections: true": answer({ captureRejections: true }),
'captureRejections: "yes"': answer({ captureRejections: "yes" }),
};
EventEmitter.captureRejections = true;
try {
answers[way]["while EventEmitter.captureRejections is true"] = answer();
} finally {
EventEmitter.captureRejections = false;
}
}
const asInNode = {
"captureRejections: true": false,
'captureRejections: "yes"': false,
"while EventEmitter.captureRejections is true": true,
};
expect(answers).toEqual({
"new ChildProcess": asInNode,
"a class that extends ChildProcess": asInNode,
});
});
});

describe("ChildProcess.spawn()", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,35 @@ test("the setters of a class that extends ServerResponse get every store of its
expect(stdout.split("\n")).toEqual(expected.split("\n"));
expect(exitCode).toBe(0);
});

test("OutgoingMessage and ServerResponse ignore captureRejections in their options, like Node.js", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
join(import.meta.dir, "../../../aot/parallel/library-modules/http-messages-hand-no-options-to-the-emitter.mjs"),
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// What node v26.3.0 prints: https://github.com/nodejs/node/blob/v26.3.0/lib/_http_outgoing.js#L107
expect({ stdout: stdout.split("\n"), stderr, exitCode }).toEqual({
stdout: [
...[
"new OutgoingMessage",
"a class that extends OutgoingMessage",
"new ServerResponse",
"a class that extends ServerResponse",
"ServerResponse called on an object",
].flatMap(way => [
`${way}, captureRejections true: false`,
`${way}, captureRejections "yes": false`,
`${way}, while EventEmitter.captureRejections is true: true`,
]),
"",
],
stderr: "",
exitCode: 0,
});
});
Loading