Files
bun.sh/test/regression/issue/26358.test.ts
robobun c4f6874960 fix(ws): allow ws.once() to work multiple times (#26359)
## Summary
- Fixes `ws.once()` only working on the first call for each event type
- The bug was in the `#onOrOnce` method which tracked native listeners
via a bitset but didn't account for `once` listeners auto-removing after
firing
- Now only persistent `on()` listeners set the bitset; `once()`
listeners always add new native handlers unless a persistent listener
already exists

## Test plan
- [x] Added regression test `test/regression/issue/26358.test.ts`
- [x] Test verifies `once('message')` works multiple times
- [x] Test verifies `once('pong')` works multiple times  
- [x] Test verifies `on()` still works correctly
- [x] Test verifies mixing `on()` and `once()` works correctly
- [x] Verified test fails with `USE_SYSTEM_BUN=1` (bug exists)
- [x] Verified test passes with `bun bd test` (fix works)

Fixes #26358

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-23 00:22:36 -08:00

216 lines
5.3 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { WebSocket } from "ws";
describe("ws.once() multiple calls", () => {
let server: Bun.Server;
let port: number;
beforeAll(() => {
server = Bun.serve({
port: 0,
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Not Found", { status: 404 });
},
websocket: {
message(ws, message) {
ws.send(message);
},
ping(ws, data) {
// Bun automatically responds with pong
},
},
});
port = server.port;
});
afterAll(() => {
server.stop(true);
});
test("ws.once('message') works multiple times", async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
await new Promise<void>(resolve => ws.once("open", resolve));
const messages: string[] = [];
// First once() listener
const p1 = new Promise<void>(resolve => {
ws.once("message", data => {
messages.push(data.toString());
resolve();
});
});
ws.send("message1");
await p1;
// Second once() listener - this should also work
const p2 = new Promise<void>(resolve => {
ws.once("message", data => {
messages.push(data.toString());
resolve();
});
});
ws.send("message2");
await p2;
// Third once() listener - this should also work
const p3 = new Promise<void>(resolve => {
ws.once("message", data => {
messages.push(data.toString());
resolve();
});
});
ws.send("message3");
await p3;
expect(messages).toEqual(["message1", "message2", "message3"]);
ws.close();
});
test("ws.once('pong') works multiple times", async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
await new Promise<void>(resolve => ws.once("open", resolve));
let pongCount = 0;
// First ping/pong
const p1 = new Promise<void>(resolve => {
ws.once("pong", () => {
pongCount++;
resolve();
});
});
ws.ping();
await p1;
// Second ping/pong - this should also work
const p2 = new Promise<void>(resolve => {
ws.once("pong", () => {
pongCount++;
resolve();
});
});
ws.ping();
await p2;
// Third ping/pong - this should also work
const p3 = new Promise<void>(resolve => {
ws.once("pong", () => {
pongCount++;
resolve();
});
});
ws.ping();
await p3;
expect(pongCount).toBe(3);
ws.close();
});
test("ws.on() still works correctly (only one native listener)", async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
await new Promise<void>(resolve => ws.once("open", resolve));
const messages: string[] = [];
let messageWaiter: { count: number; resolve: () => void } | null = null;
const checkWaiter = () => {
if (messageWaiter && messages.length >= messageWaiter.count) {
messageWaiter.resolve();
messageWaiter = null;
}
};
// Add multiple on() listeners - they should all receive every message
ws.on("message", data => {
messages.push(`listener1:${data.toString()}`);
checkWaiter();
});
ws.on("message", data => {
messages.push(`listener2:${data.toString()}`);
checkWaiter();
});
const waitForMessages = (count: number) =>
new Promise<void>(resolve => {
if (messages.length >= count) {
resolve();
} else {
messageWaiter = { count, resolve };
}
});
ws.send("test1");
await waitForMessages(2);
ws.send("test2");
await waitForMessages(4);
// Both listeners should receive both messages
expect(messages).toContain("listener1:test1");
expect(messages).toContain("listener2:test1");
expect(messages).toContain("listener1:test2");
expect(messages).toContain("listener2:test2");
ws.close();
});
test("mixing on() and once() works correctly", async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
await new Promise<void>(resolve => ws.once("open", resolve));
const messages: string[] = [];
let messageWaiter: { count: number; resolve: () => void } | null = null;
const checkWaiter = () => {
if (messageWaiter && messages.length >= messageWaiter.count) {
messageWaiter.resolve();
messageWaiter = null;
}
};
// Add a persistent on() listener
ws.on("message", data => {
messages.push(`persistent:${data.toString()}`);
checkWaiter();
});
// Add a once() listener
ws.once("message", data => {
messages.push(`once:${data.toString()}`);
checkWaiter();
});
const waitForMessages = (count: number) =>
new Promise<void>(resolve => {
if (messages.length >= count) {
resolve();
} else {
messageWaiter = { count, resolve };
}
});
ws.send("test1");
await waitForMessages(2); // Both listeners fire
ws.send("test2");
await waitForMessages(3); // Only persistent listener fires
expect(messages).toContain("persistent:test1");
expect(messages).toContain("once:test1");
expect(messages).toContain("persistent:test2");
expect(messages).not.toContain("once:test2");
ws.close();
});
});