first commit
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import {
|
||||
defineRoom,
|
||||
isRoomDefinition,
|
||||
createRealtimeRegistry,
|
||||
type RawSocket,
|
||||
} from "../src/index.ts";
|
||||
|
||||
interface MockSocket extends RawSocket {
|
||||
sent: Record<string, unknown>[];
|
||||
}
|
||||
function mockSocket(): MockSocket {
|
||||
const sent: Record<string, unknown>[] = [];
|
||||
return {
|
||||
sent,
|
||||
send(data: string) {
|
||||
sent.push(JSON.parse(data) as Record<string, unknown>);
|
||||
},
|
||||
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<string, string> = {};
|
||||
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 });
|
||||
});
|
||||
Reference in New Issue
Block a user