Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 12 additions & 14 deletions src/js/node/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ function lazyGlob() {

const { guardCallback, kCustomPromisifyArgsSymbol } = require("internal/shared");

// Captured at load: a process.nextTick that user code replaces later (fake timers) must not hold back fs callbacks.
const nextTick = process.nextTick;

// guardCallback reroutes a throw inside the user callback to the
// uncaughtException path.
function wrapFsCallback(callback) {
Expand All @@ -44,16 +41,17 @@ function ensureCallback(callback) {
}

// Via nextTick: called from the reaction itself (a microtask), a callback's microtasks would run before its nextTicks.
// process.nextTick is read at each call, not at load: the first read creates the tick queue, and every event loop task then drains it.
function settleCallback(promise, callback) {
promise.then(
value => nextTick(callback, null, value),
err => nextTick(callback, err),
value => process.nextTick(callback, null, value),
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
err => process.nextTick(callback, err),
);
}
function settleCallbackWithNull(promise, callback) {
promise.then(
() => nextTick(callback, null),
err => nextTick(callback, err),
() => process.nextTick(callback, null),
err => process.nextTick(callback, err),
Comment thread
robobun marked this conversation as resolved.
Resolved
);
}

Expand Down Expand Up @@ -393,7 +391,7 @@ var access = function access(path, mode, callback?) {

const signal = options?.signal;
if (signal?.aborted) {
nextTick(callback, $makeAbortError(undefined, { cause: signal.reason }));
process.nextTick(callback, $makeAbortError(undefined, { cause: signal.reason }));
return;
}

Expand Down Expand Up @@ -865,7 +863,7 @@ const realpath: typeof import("node:fs").realpath =
LOOP();
});
} else {
nextTick(LOOP);
process.nextTick(LOOP);
}

// Walk down the path, swapping out linked path parts for their real
Expand Down Expand Up @@ -909,7 +907,7 @@ const realpath: typeof import("node:fs").realpath =
// If not a symlink, skip to the next path part
if (!stats.isSymbolicLink()) {
knownHard.add(base);
return nextTick(LOOP);
return process.nextTick(LOOP);
}

// Stat & read the link if not read before.
Expand Down Expand Up @@ -944,7 +942,7 @@ const realpath: typeof import("node:fs").realpath =
LOOP();
});
} else {
nextTick(LOOP);
process.nextTick(LOOP);
}
}
} as typeof import("node:fs").realpath);
Expand Down Expand Up @@ -1022,13 +1020,13 @@ function _toUnixTimestamp(time: any, name = "time") {

function onOpendirStatFulfilled(callback, path, result, stats) {
if (!stats.isDirectory()) {
nextTick(callback, opendirNotDirError(path));
process.nextTick(callback, opendirNotDirError(path));
return;
}
nextTick(callback, null, result);
process.nextTick(callback, null, result);
}
function onOpendirStatRejected(callback, path, err) {
nextTick(callback, typeof err?.errno === "number" ? opendirStatError(err, path) : err);
process.nextTick(callback, typeof err?.errno === "number" ? opendirStatError(err, path) : err);
}

function opendirSync(path, options) {
Expand Down
39 changes: 39 additions & 0 deletions test/js/node/fs/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7295,6 +7295,45 @@ describe("a process.nextTick queued by an fs callback runs before a microtask it
expect(stdout.trim()).toBe("callback nextTick microtask");
expect(exitCode).toBe(0);
});

// fs.rm is a promise chain in JS. Its reaction calls process.nextTick, and here that call creates the tick queue.
it("when the callback is the first use of process.nextTick in the process", async () => {
using dir = tempDir("fs-callback-order", { "file.txt": "hello" });
const script = `
const fs = require("fs");
const order = [];
process.on("exit", () => console.log(order.join(" ")));
fs.rm(${JSON.stringify(join(String(dir), "file.txt"))}, () => {
order.push("callback");
process.nextTick(() => order.push("nextTick"));
queueMicrotask(() => order.push("microtask"));
});
`;
await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout.trim()).toBe("callback nextTick microtask");
expect(exitCode).toBe(0);
});
});

// The first read of process.nextTick creates the tick queue. From then on every event loop task drains it.
it("loading node:fs does not create the process.nextTick queue", async () => {
const script = `
const { heapStats } = require("bun:jsc");
const queues = () => heapStats().objectTypeCounts.NextTickQueue ?? 0;
require("node:fs");
const afterLoad = queues();
process.nextTick;
console.log(JSON.stringify({ afterLoad, afterRead: queues() }));
`;
await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ afterLoad: 0, afterRead: 1 });
expect(exitCode).toBe(0);
});

// The operation pins the buffer while it runs. Node can transfer it from the callback.
Expand Down
Loading