import { test, expect } from "bun:test"; import { defineRoom, isRoomDefinition, createRealtimeRegistry, type RawSocket, } from "../src/index.ts"; interface MockSocket extends RawSocket { sent: Record[]; } function mockSocket(): MockSocket { const sent: Record[] = []; return { sent, send(data: string) { sent.push(JSON.parse(data) as Record); }, close() {}, }; } test("defineRoom marks a room definition", () => { expect(isRoomDefinition(defineRoom({}))).toBe(true); expect(isRoomDefinition({})).toBe(false); expect(isRoomDefinition(null)).toBe(false); }); test("lifecycle hooks fire; broadcast reaches the whole room", async () => { const events: string[] = []; const def = defineRoom({ onConnect(c) { events.push("connect"); c.broadcast({ type: "join" }); // others only }, onMessage(c, m) { events.push("message"); c.room.broadcast({ type: "echo", text: m.text }); // everyone incl. sender }, onLeave(c) { events.push("leave"); c.broadcast({ type: "left" }); }, }); const reg = createRealtimeRegistry(); const a = mockSocket(); const b = mockSocket(); await reg.open(a, { room: "/r/x", def }); await reg.open(b, { room: "/r/x", def }); expect(a.sent.some((m) => m.type === "join")).toBe(true); // A saw B join expect(b.sent.some((m) => m.type === "join")).toBe(false); // B didn't see its own join await reg.message(a, JSON.stringify({ text: "hi" })); expect(a.sent.some((m) => m.type === "echo" && m.text === "hi")).toBe(true); // sender sees own expect(b.sent.some((m) => m.type === "echo" && m.text === "hi")).toBe(true); await reg.close(b); expect(a.sent.some((m) => m.type === "left")).toBe(true); expect(reg.size()).toBe(1); expect(events).toEqual(["connect", "connect", "message", "leave"]); }); test("to(connectionId) and toUser(user|users) target precisely", async () => { const ids: Record = {}; const def = defineRoom({ onConnect(c) { c.user = c.query.as; // identify by ?as= ids[c.query.as!] = c.id; }, onMessage(c, m) { if (m.toUser) c.toUser(m.toUser).send({ type: "dm", text: m.text }); if (m.toId) c.to(m.toId).send({ type: "direct", text: m.text }); }, }); const reg = createRealtimeRegistry(); const alice = mockSocket(); const bob = mockSocket(); const carol = mockSocket(); await reg.open(alice, { room: "/r", def, query: { as: "alice" } }); await reg.open(bob, { room: "/r", def, query: { as: "bob" } }); await reg.open(carol, { room: "/r", def, query: { as: "carol" } }); // single user await reg.message(alice, JSON.stringify({ toUser: "bob", text: "hey bob" })); expect(bob.sent.some((m) => m.type === "dm" && m.text === "hey bob")).toBe(true); expect(carol.sent.some((m) => m.type === "dm")).toBe(false); // selected users await reg.message(alice, JSON.stringify({ toUser: ["bob", "carol"], text: "both" })); expect(bob.sent.filter((m) => m.type === "dm").length).toBe(2); expect(carol.sent.some((m) => m.text === "both")).toBe(true); // by connection id await reg.message(alice, JSON.stringify({ toId: ids.carol, text: "by-id" })); expect(carol.sent.some((m) => m.type === "direct" && m.text === "by-id")).toBe(true); }); test("rooms are isolated from each other", async () => { const def = defineRoom({ onMessage(c, m) { c.room.broadcast({ type: "x", text: m.text }); }, }); const reg = createRealtimeRegistry(); const a = mockSocket(); const b = mockSocket(); await reg.open(a, { room: "/room/1", def }); // dynamic room instances, one handler await reg.open(b, { room: "/room/2", def }); await reg.message(a, JSON.stringify({ text: "one" })); expect(a.sent.some((m) => m.text === "one")).toBe(true); expect(b.sent.length).toBe(0); // different room, untouched }); test("bridge relays broadcasts + toUser across registries (horizontal scaling)", async () => { const regA = createRealtimeRegistry(); const regB = createRealtimeRegistry(); // A shared bus: each instance delivers the other's published envelopes. regA.setBridge({ publish: (env) => regB.deliver(env) }); regB.setBridge({ publish: (env) => regA.deliver(env) }); const def = defineRoom({ onConnect(c) { c.user = c.query.as; }, onMessage(c, m) { if (m.toUser) c.toUser(m.toUser).send({ type: "dm", text: m.text }); else c.room.broadcast({ type: "x", text: m.text }); }, }); const a = mockSocket(); const b = mockSocket(); await regA.open(a, { room: "/r", def, query: { as: "alice" } }); await regB.open(b, { room: "/r", def, query: { as: "bob" } }); // b is on the OTHER instance // broadcast from A reaches B through the bridge await regA.message(a, JSON.stringify({ text: "cross-instance" })); expect(a.sent.some((m) => m.text === "cross-instance")).toBe(true); expect(b.sent.some((m) => m.text === "cross-instance")).toBe(true); // toUser bob (on instance B) from A reaches him via the bridge; alice doesn't await regA.message(a, JSON.stringify({ toUser: "bob", text: "hi bob" })); expect(b.sent.some((m) => m.type === "dm" && m.text === "hi bob")).toBe(true); const aliceDms = a.sent.filter((m) => m.type === "dm").length; expect(aliceDms).toBe(0); // not looped back / not delivered to the wrong user }); test("room.state and count() track the live room", async () => { const def = defineRoom({ onConnect(c) { c.room.state.hits = ((c.room.state.hits as number) ?? 0) + 1; c.send({ type: "welcome", online: c.room.count(), hits: c.room.state.hits }); }, }); const reg = createRealtimeRegistry(); const a = mockSocket(); const b = mockSocket(); await reg.open(a, { room: "/r", def }); await reg.open(b, { room: "/r", def }); expect(a.sent[0]).toMatchObject({ online: 1, hits: 1 }); expect(b.sent[0]).toMatchObject({ online: 2, hits: 2 }); }); test("realtime security enforces authentication, message size, rate, and safe shapes", async () => { const violations: string[] = []; const def = defineRoom({ security: { requireUser: true, maxMessageBytes: 64, maxMessagesPerSecond: 1, onViolation(reason) { violations.push(reason); }, }, }); const now = { value: 0 }; const reg = createRealtimeRegistry({ now: () => now.value }); const anonymousClosures: Array<[number | undefined, string | undefined]> = []; const anonymous: RawSocket = { send() {}, close(code, reason) { anonymousClosures.push([code, reason]); }, }; await reg.open(anonymous, { room: "/secure", def }); expect(anonymousClosures[0]).toEqual([1008, "Authentication required"]); const closures: Array<[number | undefined, string | undefined]> = []; const socket: RawSocket = { send() {}, close(code, reason) { closures.push([code, reason]); }, }; await reg.open(socket, { room: "/secure", def, user: "u1" }); await reg.message(socket, JSON.stringify({ ok: true })); await reg.message(socket, JSON.stringify({ ok: true })); expect(closures.at(-1)).toEqual([1008, "Message rate exceeded"]); expect(violations).toContain("message-rate-limit"); now.value = 2_000; const unsafe = '{"constructor":{"prototype":{"admin":true}}}'; await reg.message(socket, unsafe); expect(violations).toContain("invalid-message-shape"); });