first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
import { test, expect } from "bun:test";
import {
createContext,
hashPassword,
verifyPassword,
logIn,
logOut,
getUser,
sessionAuth,
requireAuth,
} from "../src/index.ts";
function ctx(method = "GET", path = "/", accept?: string) {
const headers: Record<string, string> = {};
if (accept) headers.accept = accept;
const url = new URL(`http://x${path}`);
const req = new Request(url, { method, headers });
return createContext(req, url);
}
test("hashPassword / verifyPassword round-trip", async () => {
const hash = await hashPassword("correct horse battery staple");
expect(hash).toBeTruthy();
expect(hash).not.toBe("correct horse battery staple");
expect(await verifyPassword("correct horse battery staple", hash)).toBe(true);
expect(await verifyPassword("wrong", hash)).toBe(false);
});
test("verifyPassword tolerates empty/garbage hashes", async () => {
expect(await verifyPassword("x", "")).toBe(false);
expect(await verifyPassword("x", "not-a-real-hash")).toBe(false);
});
test("logIn stores the user; getUser reads it; logOut clears it", () => {
const c = ctx();
expect(getUser(c)).toBeNull();
logIn(c, { id: 1, email: "a@b.com" });
expect(getUser<{ id: number }>(c)?.id).toBe(1);
expect(c.session.get<{ id: number; email: string }>("user")).toEqual({ id: 1, email: "a@b.com" });
logOut(c);
expect(getUser(c)).toBeNull();
expect(c.user).toBeNull();
});
test("logIn regenerates the session id (fixation defense) but keeps data", () => {
const c = ctx();
c.session.set("cart", [1, 2]);
const before = c.session.id();
logIn(c, { id: 1, email: "a@b.com" });
const after = c.session.id();
expect(after).not.toBe(before); // fresh id issued on login
expect(after.length).toBeGreaterThanOrEqual(32);
expect(c.session.get<number[]>("cart")).toEqual([1, 2]); // data preserved
expect(getUser<{ id: number }>(c)?.id).toBe(1);
});
test("sessionAuth hydrates ctx.user from the session", async () => {
const c = ctx();
c.session.set("user", { id: 7 });
let seen: unknown = "unset";
await sessionAuth()(c, () => {
seen = c.user;
return new Response("ok");
});
expect(seen).toEqual({ id: 7 });
});
test("requireAuth: passes through when authenticated", async () => {
const c = ctx();
logIn(c, { id: 1 });
const res = await requireAuth()(c, () => new Response("secret"));
expect(await res.text()).toBe("secret");
});
test("requireAuth: 401 JSON for API paths when anonymous", async () => {
const c = ctx("GET", "/api/me");
const res = await requireAuth()(c, () => new Response("secret"));
expect(res.status).toBe(401);
expect(await res.json()).toEqual({ ok: false, error: "Unauthorized" });
});
test("requireAuth: 302 redirect for page navigations when anonymous", async () => {
const c = ctx("GET", "/dashboard?tab=1", "text/html");
const res = await requireAuth()(c, () => new Response("secret"));
expect(res.status).toBe(302);
expect(res.headers.get("location")).toBe("/login?next=%2Fdashboard%3Ftab%3D1");
});
test("requireAuth: custom loginPath", async () => {
const c = ctx("GET", "/dashboard", "text/html");
const res = await requireAuth({ loginPath: "/signin" })(c, () => new Response("x"));
expect(res.headers.get("location")).toBe("/signin?next=%2Fdashboard");
});
+28
View File
@@ -0,0 +1,28 @@
import { test, expect } from "bun:test";
import { createContext, csrfToken, verifyCsrf, CSRF_COOKIE } from "../src/index.ts";
function ctx(method: string, cookie?: string, header?: string) {
const headers: Record<string, string> = {};
if (cookie) headers.cookie = `${CSRF_COOKIE}=${cookie}`;
if (header) headers["x-csrf-token"] = header;
const req = new Request("http://x/api", { method, headers });
return createContext(req, new URL(req.url));
}
test("csrfToken issues a token", () => {
const token = csrfToken(ctx("GET"));
expect(token).toBeTruthy();
expect(token.length).toBeGreaterThan(16);
});
test("verifyCsrf: safe methods always pass", () => {
expect(verifyCsrf(ctx("GET"))).toBe(true);
expect(verifyCsrf(ctx("HEAD"))).toBe(true);
});
test("verifyCsrf: unsafe methods need matching cookie + header", () => {
expect(verifyCsrf(ctx("POST", "abc", "abc"))).toBe(true);
expect(verifyCsrf(ctx("POST", "abc", "xyz"))).toBe(false); // mismatch
expect(verifyCsrf(ctx("POST", "abc"))).toBe(false); // no header
expect(verifyCsrf(ctx("POST", undefined, "abc"))).toBe(false); // no cookie
});
+66
View File
@@ -0,0 +1,66 @@
import { test, expect } from "bun:test";
import { withSecurityHeaders, isWebSocketOriginAllowed } from "../src/index.ts";
const req = (headers: Record<string, string> = {}) => new Request("https://x/", { headers });
function scriptSrc(csp: string): string {
return csp
.split(";")
.map((s) => s.trim())
.find((s) => s.startsWith("script-src"))!;
}
test("CSP nonce is added to script-src and drops unsafe-inline", () => {
const res = withSecurityHeaders(req(), new Response("x"), "development", undefined, "ABC123");
const directive = scriptSrc(res.headers.get("content-security-policy")!);
expect(directive).toContain("'nonce-ABC123'");
expect(directive).not.toContain("'unsafe-inline'");
});
test("without a nonce, dev script-src keeps unsafe-inline (for HMR)", () => {
const res = withSecurityHeaders(req(), new Response("x"), "development");
expect(scriptSrc(res.headers.get("content-security-policy")!)).toContain("'unsafe-inline'");
});
test("CORS credentials + origin:* is refused (credentials dropped)", () => {
const res = withSecurityHeaders(
req({ origin: "https://evil.test" }),
new Response("x"),
"production",
{
cors: { enabled: true, origin: "*", credentials: true },
},
);
expect(res.headers.get("access-control-allow-credentials")).toBeNull();
});
test("production sets HSTS + strict CSP", () => {
const res = withSecurityHeaders(req(), new Response("x"), "production");
expect(res.headers.get("strict-transport-security")).toContain("max-age=");
expect(res.headers.get("content-security-policy")).toContain("default-src 'self'");
});
test("permissions policy overrides merge with restrictive defaults", () => {
const res = withSecurityHeaders(req(), new Response("x"), "development", {
permissionsPolicy: { camera: ["self"] },
});
const policy = res.headers.get("permissions-policy")!;
expect(policy).toContain("camera=(self)");
expect(policy).toContain("microphone=()");
});
test("isWebSocketOriginAllowed blocks cross-site WS (CSWSH), allows same-origin", () => {
const wsReq = (origin: string | null, host: string) =>
new Request("http://x/realtime/c", {
headers: origin ? { origin, host } : { host },
});
expect(isWebSocketOriginAllowed(wsReq("http://app.test", "app.test"))).toBe(true); // same-origin
expect(isWebSocketOriginAllowed(wsReq("http://evil.test", "app.test"))).toBe(false); // cross-site
expect(isWebSocketOriginAllowed(wsReq(null, "app.test"))).toBe(true); // native client, no cookies
// Explicit CORS allowlist opens a cross-origin WS.
expect(
isWebSocketOriginAllowed(wsReq("http://other.test", "app.test"), {
cors: { enabled: true, origin: "http://other.test" },
}),
).toBe(true);
});
+13
View File
@@ -0,0 +1,13 @@
import { expect, test } from "bun:test";
import { jsx } from "../src/jsx-runtime.ts";
test("JSX rejects dynamic tag-name injection", () => {
expect(() => jsx("div><script>alert(1)</script><div" as "div", {})).toThrow(
"Invalid JSX tag name",
);
});
test("JSX skips invalid spread attribute names", () => {
const html = jsx("div", { 'title" onmouseover="alert(1)': "x", title: "safe" }).toString();
expect(html).toBe('<div title="safe"></div>');
});
+292
View File
@@ -0,0 +1,292 @@
import { test, expect } from "bun:test";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { existsSync } from "node:fs";
import {
createContext,
rateLimit,
requestLogger,
TTLCache,
cacheControl,
withCacheControl,
etag,
notModified,
saveUpload,
collectUploads,
sanitizeFilename,
UploadError,
setSessionBackend,
loadSession,
type SessionBackend,
type SessionEntry,
type AsyncSessionBackend,
type RateLimitStore,
} from "../src/index.ts";
function memoryBackend(): SessionBackend {
const map = new Map<string, SessionEntry>();
return {
get: (id) => map.get(id),
set: (id, e) => void map.set(id, e),
delete: (id) => void map.delete(id),
_map: map,
} as SessionBackend & { _map: Map<string, SessionEntry> };
}
function ctx(path = "/", headers: Record<string, string> = {}) {
const url = new URL(`http://x${path}`);
return createContext(new Request(url, { headers }), url);
}
// --- rate limiting ---------------------------------------------------------
test("rateLimit (trustProxy) allows up to max then 429 with headers", async () => {
const mw = rateLimit({ max: 2, windowMs: 60_000, trustProxy: true });
const ok = () => new Response("ok");
const key = { "x-forwarded-for": "1.1.1.1" };
const r1 = await mw(ctx("/", key), ok);
const r2 = await mw(ctx("/", key), ok);
const r3 = await mw(ctx("/", key), ok);
expect(r1.status).toBe(200);
expect(r1.headers.get("RateLimit-Remaining")).toBe("1");
expect(r2.status).toBe(200);
expect(r2.headers.get("RateLimit-Remaining")).toBe("0");
expect(r3.status).toBe(429);
expect(r3.headers.get("retry-after")).toBeTruthy();
});
test("rateLimit (trustProxy) buckets are independent per key", async () => {
const mw = rateLimit({ max: 1, windowMs: 60_000, trustProxy: true });
const ok = () => new Response("ok");
const a = await mw(ctx("/", { "x-forwarded-for": "2.2.2.2" }), ok);
const b = await mw(ctx("/", { "x-forwarded-for": "3.3.3.3" }), ok);
expect(a.status).toBe(200);
expect(b.status).toBe(200);
});
test("setSessionBackend routes session data through a custom backend", () => {
const backend = memoryBackend() as SessionBackend & { _map: Map<string, SessionEntry> };
setSessionBackend(backend);
try {
const c = ctx("/");
c.session.set("k", "v");
const id = c.session.id();
expect(backend._map.has(id)).toBe(true);
expect(backend._map.get(id)!.data).toEqual({ k: "v" });
} finally {
setSessionBackend(memoryBackend()); // restore an equivalent for other tests
}
});
test("rateLimit accepts a custom (shared) store", async () => {
const hits: string[] = [];
const store: RateLimitStore = {
hit(key, windowMs, now) {
hits.push(key);
return { count: hits.filter((k) => k === key).length, resetAt: now + windowMs };
},
};
const mw = rateLimit({ max: 1, windowMs: 1000, store, trustProxy: true });
const ok = () => new Response("ok");
const key = { "x-forwarded-for": "5.5.5.5" };
expect((await mw(ctx("/", key), ok)).status).toBe(200);
expect((await mw(ctx("/", key), ok)).status).toBe(429);
expect(hits.length).toBe(2); // both requests went through the injected store
});
test("rateLimit awaits an ASYNC store (e.g. Redis)", async () => {
const counts = new Map<string, number>();
const store: RateLimitStore = {
async hit(key, windowMs, now) {
const n = (counts.get(key) ?? 0) + 1;
counts.set(key, n);
return { count: n, resetAt: now + windowMs };
},
};
const mw = rateLimit({ max: 1, windowMs: 1000, store, trustProxy: true });
const ok = () => new Response("ok");
const key = { "x-forwarded-for": "7.7.7.7" };
expect((await mw(ctx("/", key), ok)).status).toBe(200);
expect((await mw(ctx("/", key), ok)).status).toBe(429);
});
test("loadSession persists a session through an ASYNC backend across requests", async () => {
const kv = new Map<string, SessionEntry>();
const backend: AsyncSessionBackend = {
load: async (id) => kv.get(id),
save: async (id, entry) => void kv.set(id, entry),
destroy: async (id) => void kv.delete(id),
};
const mw = loadSession(backend);
// Request 1: write a value, capture the issued session id.
const c1 = ctx("/");
await mw(c1, () => {
c1.session.set("hits", 1);
return new Response("ok");
});
const sid = c1.cookies.get("wrnexus.sid")!;
expect(sid).toBeTruthy();
expect(kv.has(sid)).toBe(true); // saved to the async backend
// Request 2: same cookie → session loads from the backend.
const c2 = ctx("/", { cookie: `wrnexus.sid=${sid}` });
let seen: unknown;
await mw(c2, () => {
seen = c2.session.get("hits");
return new Response("ok");
});
expect(seen).toBe(1);
});
test("rateLimit default keys on the non-spoofable peer IP, not XFF headers", async () => {
const mw = rateLimit({ max: 1, windowMs: 60_000 });
const ok = () => new Response("ok");
// Same peer IP, different spoofed XFF → still one bucket (XFF ignored).
const c1 = ctx("/", { "x-forwarded-for": "9.9.9.9" });
c1.ip = "10.0.0.1";
const c2 = ctx("/", { "x-forwarded-for": "8.8.8.8" });
c2.ip = "10.0.0.1";
expect((await mw(c1, ok)).status).toBe(200);
expect((await mw(c2, ok)).status).toBe(429);
});
test("rateLimit validates its in-memory key bound", () => {
expect(() => rateLimit({ maxKeys: 0 })).toThrow("maxKeys");
});
// --- request logging -------------------------------------------------------
test("requestLogger emits a structured record with duration and id", async () => {
const records: string[] = [];
let t = 1000;
const mw = requestLogger({
format: "json",
sink: (line) => records.push(line),
now: () => (t += 5),
});
const res = await mw(ctx("/api/users"), () => new Response("x", { status: 201 }));
expect(res.status).toBe(201);
const rec = JSON.parse(records[0]!);
expect(rec.method).toBe("GET");
expect(rec.path).toBe("/api/users");
expect(rec.status).toBe(201);
expect(rec.durationMs).toBeGreaterThanOrEqual(0);
expect(rec.id).toBeTruthy();
});
test("requestLogger logs status 500 when the handler throws", async () => {
const records: string[] = [];
const mw = requestLogger({ format: "json", sink: (l) => records.push(l) });
await expect(
mw(ctx("/boom"), () => {
throw new Error("nope");
}),
).rejects.toThrow("nope");
expect(JSON.parse(records[0]!).status).toBe(500);
});
// --- caching ---------------------------------------------------------------
test("TTLCache getOrLoad caches until expiry", async () => {
const cache = new TTLCache<number>(60_000);
let calls = 0;
const load = () => {
calls++;
return 42;
};
expect(await cache.getOrLoad("k", load)).toBe(42);
expect(await cache.getOrLoad("k", load)).toBe(42);
expect(calls).toBe(1);
cache.delete("k");
expect(await cache.getOrLoad("k", load)).toBe(42);
expect(calls).toBe(2);
});
test("TTLCache coalesces concurrent loads for the same key", async () => {
const cache = new TTLCache<number>();
let calls = 0;
const loader = async () => {
calls++;
await Promise.resolve();
return 7;
};
expect(await Promise.all([cache.getOrLoad("x", loader), cache.getOrLoad("x", loader)])).toEqual([
7, 7,
]);
expect(calls).toBe(1);
});
test("TTLCache does not let an old in-flight load overwrite set, delete, or clear", async () => {
const cache = new TTLCache<number>();
let release!: (value: number) => void;
const loading = cache.getOrLoad("x", () => new Promise<number>((resolve) => (release = resolve)));
await Promise.resolve();
cache.set("x", 9);
release(1);
expect(await loading).toBe(1);
expect(cache.get("x")).toBe(9);
let releaseClear!: (value: number) => void;
const clearing = cache.getOrLoad(
"y",
() => new Promise<number>((resolve) => (releaseClear = resolve)),
);
await Promise.resolve();
cache.clear();
releaseClear(2);
await clearing;
expect(cache.get("y")).toBeUndefined();
});
test("cacheControl builds directives; no-store wins", () => {
expect(cacheControl({ maxAge: 60, sMaxAge: 120 })).toBe("public, max-age=60, s-maxage=120");
expect(cacheControl({ private: true, noCache: true })).toBe("private, no-cache");
expect(cacheControl({ noStore: true, maxAge: 99 })).toBe("no-store");
const res = withCacheControl(new Response("x"), { maxAge: 30, immutable: true });
expect(res.headers.get("Cache-Control")).toBe("public, max-age=30, immutable");
});
test("etag + notModified drive conditional requests", () => {
const tag = etag("hello world");
expect(tag).toMatch(/^W\/"/);
expect(etag("hello world")).toBe(tag); // stable
expect(etag("different")).not.toBe(tag);
const req = new Request("http://x", { headers: { "if-none-match": tag } });
expect(notModified(req, tag)).toBe(true);
expect(notModified(new Request("http://x"), tag)).toBe(false);
});
// --- uploads ---------------------------------------------------------------
test("sanitizeFilename strips traversal and separators", () => {
const s = sanitizeFilename("../../etc/passwd");
expect(s).not.toContain("/");
expect(s).not.toContain("..");
expect(s).toContain("passwd");
expect(sanitizeFilename("a/b\\c.png")).toBe("a_b_c.png");
expect(sanitizeFilename("")).toBe("upload");
});
test("saveUpload writes a validated file and enforces limits", async () => {
const dir = join(tmpdir(), "wire-upload-test");
const file = new File(["hello upload"], "note.txt", { type: "text/plain" });
const saved = await saveUpload(file, { dir, allowedTypes: ["text/plain", ".txt"] });
expect(saved.filename).toBe("note.txt");
expect(saved.size).toBe(12);
expect(existsSync(saved.path)).toBe(true);
await expect(saveUpload(file, { dir, maxBytes: 4 })).rejects.toThrow(UploadError);
await expect(saveUpload(file, { dir, allowedTypes: ["image/png"] })).rejects.toThrow(UploadError);
});
test("collectUploads returns only non-empty File fields", async () => {
const form = new FormData();
form.append("name", "ada");
form.append("avatar", new File(["img"], "a.png", { type: "image/png" }));
const uploads = collectUploads(form);
expect(uploads.length).toBe(1);
expect(uploads[0]!.field).toBe("avatar");
});
@@ -0,0 +1,63 @@
import { test, expect } from "bun:test";
import {
createRealtimeRegistry,
bridgeRealtime,
defineRoom,
type RawSocket,
} from "../src/index.ts";
import { createPubSub } from "../../pubsub/src/index.ts";
/** A fake socket that records what the server sends to it. */
function fakeSocket(): RawSocket & { received: string[] } {
const received: string[] = [];
return { received, send: (d: string) => received.push(d), close: () => {} };
}
test("bridgeRealtime: a room broadcast on one registry reaches connections on another", async () => {
// One shared bus stands in for Redis across two 'processes' (registries).
const bus = createPubSub();
const room = defineRoom({
onMessage(client, msg) {
client.room.broadcast({ echo: msg }); // everyone in the room, on every process
},
});
const rA = createRealtimeRegistry();
const rB = createRealtimeRegistry();
bridgeRealtime(rA, bus);
bridgeRealtime(rB, bus);
// A client connected to registry B, in room "chat".
const sB = fakeSocket();
await rB.open(sB, { room: "chat", def: room });
// A client connected to registry A triggers a broadcast.
const sA = fakeSocket();
await rA.open(sA, { room: "chat", def: room });
await rA.message(sA, JSON.stringify({ hi: 1 }));
// The broadcast crossed the bus: B's client received it even though the
// broadcast happened on registry A.
const gotOnB = sB.received.find((p) => p.includes("echo"));
expect(gotOnB).toBeTruthy();
expect(JSON.parse(gotOnB!)).toEqual({ echo: { hi: 1 } });
});
test("bridgeRealtime: no bus means broadcasts stay local", async () => {
const room = defineRoom({
onMessage(client, msg) {
client.room.broadcast({ echo: msg });
},
});
const rA = createRealtimeRegistry();
const rB = createRealtimeRegistry(); // NOT bridged to A
const sB = fakeSocket();
await rB.open(sB, { room: "chat", def: room });
const sA = fakeSocket();
await rA.open(sA, { room: "chat", def: room });
await rA.message(sA, JSON.stringify({ hi: 1 }));
expect(sB.received.length).toBe(0); // isolated — nothing crossed
});
+161
View File
@@ -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 });
});
+38
View File
@@ -0,0 +1,38 @@
import { test, expect } from "bun:test";
import { streamResponse, sse } from "../src/index.ts";
test("streamResponse streams a sync iterable of strings as HTML", async () => {
const res = streamResponse(["<h1>", "Hello", "</h1>"]);
expect(res.headers.get("content-type")).toBe("text/html; charset=utf-8");
expect(await res.text()).toBe("<h1>Hello</h1>");
});
test("streamResponse streams an async generator (streaming SSR shell + body)", async () => {
async function* page() {
yield '<!doctype html><body><div id="app">';
yield "<p>content</p>";
yield "</div></body>";
}
const res = streamResponse(page(), { status: 200 });
const text = await res.text();
expect(text).toContain('<div id="app">');
expect(text).toContain("<p>content</p>");
});
test("streamResponse honours custom content-type and status", async () => {
const res = streamResponse(["plain"], { contentType: "text/plain", status: 201 });
expect(res.status).toBe(201);
expect(res.headers.get("content-type")).toBe("text/plain");
});
test("sse formats Server-Sent Events frames", async () => {
async function* events() {
yield { data: "hello", event: "greeting", id: "1" };
yield { data: "line1\nline2", retry: 3000 };
}
const res = sse(events());
expect(res.headers.get("content-type")).toBe("text/event-stream");
const text = await res.text();
expect(text).toContain("event: greeting\nid: 1\ndata: hello\n\n");
expect(text).toContain("retry: 3000\ndata: line1\ndata: line2\n\n");
});