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
8 changes: 8 additions & 0 deletions src/jsc/bindings/JSCTaskScheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ void JSCTaskScheduler::onScheduleWorkSoon(WebCore::JSVMClientData* clientData, R
Bun__eventLoop__incrementRefConcurrently(clientData->bunVM, -1);
return;
}
// An AtSomePoint ticket (Atomics.waitAsync) sat in m_pendingTicketsOther
// without an event-loop ref while dormant. The work is now imminent, so
// promote it; otherwise the queued concurrent task is invisible to
// is_event_loop_alive() and the loop can exit before draining it.
if (auto pending = scheduler.m_pendingTicketsOther.take(ticket)) {
Bun__eventLoop__incrementRefConcurrently(clientData->bunVM, 1);
scheduler.m_pendingTicketsKeepingEventLoopAlive.add(pending.releaseNonNull());
}
auto* job = new JSCDeferredWorkTask(WTF::move(ticket), WTF::move(task));
Bun__queueJSCDeferredWorkTaskConcurrently(clientData->bunVM, job);
}
Expand Down
52 changes: 52 additions & 0 deletions test/js/web/atomics.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

describe("Atomics", () => {
describe("basic operations", () => {
Expand Down Expand Up @@ -159,6 +160,57 @@ describe("Atomics", () => {
expect(typeof result.value).toBe("string");
}
});

test("waitAsync promises resolve when notify is the last live handle", async () => {
// The notify() inside the setTimeout callback is the last thing keeping the
// event loop alive. The waiter resolutions it schedules must still run
// before the process exits (Node.js behaviour).
const src = `
const ia = new Int32Array(new SharedArrayBuffer(4));
const got = [];
for (let i = 0; i < 5; i++)
Atomics.waitAsync(ia, 0, 0, 60000).value.then(v => {
got.push("w" + i + ":" + v);
if (got.length === 5) console.log("resolved:" + got.join(","));
});
setTimeout(() => console.log("notify=" + Atomics.notify(ia, 0)), 40);
process.on("exit", () => {
if (got.length !== 5) console.log("exit-with-unsettled:" + got.length);
});
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "notify=5\nresolved:w0:ok,w1:ok,w2:ok,w3:ok,w4:ok\n",
stderr: expect.any(String),
exitCode: 0,
});
});

test("waitAsync alone does not keep the event loop alive", async () => {
// Matches Node.js: a pending waitAsync with no other live handle lets the
// process exit. Only a delivered notify should extend the loop.
const src = `
const ia = new Int32Array(new SharedArrayBuffer(4));
Atomics.waitAsync(ia, 0, 0, 60000).value.then(v => console.log("settled:" + v));
process.on("exit", () => console.log("exit"));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "exit\n",
stderr: expect.any(String),
exitCode: 0,
});
});
});

describe("different TypedArray types", () => {
Expand Down
Loading