initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@lxc/live-client",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
|
||||
"scripts": { "build": "tsc -p tsconfig.json", "check": "tsc -p tsconfig.json --noEmit", "test": "node --test test/*.test.mjs" },
|
||||
"dependencies": { "@lxc/protocol": "*" },
|
||||
"devDependencies": { "typescript": "^5.8.3", "vitest": "^3.1.1" }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { LiveClient } from "./index.js";
|
||||
describe("LiveClient", () => it("dispatches validated messages", () => {
|
||||
const socket: any = { close: vi.fn() }; const client = new LiveClient({ url: "ws://test", websocketFactory: () => socket }); const fn = vi.fn(); client.on("live.enter", fn); client.connect();
|
||||
socket.onmessage({ data: JSON.stringify({ version: 1, id: "a0000000-0000-4000-8000-000000000001", occurredAt: new Date().toISOString(), roomId: "1", type: "live.enter", payload: { viewer: { uid: "1", name: "a" } } }) }); expect(fn).toHaveBeenCalledOnce();
|
||||
}));
|
||||
@@ -0,0 +1,22 @@
|
||||
import { liveEventSchema, type EventType, type LiveEvent } from "@lxc/protocol";
|
||||
|
||||
type Handler<T extends EventType = EventType> = (event: Extract<LiveEvent, { type: T }>) => void;
|
||||
export interface LiveClientOptions { url: string; protocols?: string | string[]; minReconnectMs?: number; maxReconnectMs?: number; websocketFactory?: (url: string, protocols?: string | string[]) => WebSocket; }
|
||||
|
||||
export class LiveClient {
|
||||
private ws?: WebSocket; private closed = false; private retry = 0;
|
||||
private readonly typed = new Map<EventType, Set<Handler>>(); private readonly any = new Set<Handler>();
|
||||
constructor(private readonly options: LiveClientOptions) {}
|
||||
connect() { this.closed = false; this.open(); }
|
||||
close() { this.closed = true; this.ws?.close(); }
|
||||
on<T extends EventType>(type: T, handler: Handler<T>) { const set = this.typed.get(type) ?? new Set(); const untyped = handler as unknown as Handler; set.add(untyped); this.typed.set(type, set); return () => set.delete(untyped); }
|
||||
onAny(handler: Handler) { this.any.add(handler); return () => this.any.delete(handler); }
|
||||
private open() {
|
||||
const Factory = this.options.websocketFactory ?? ((url: string, protocols?: string | string[]) => new WebSocket(url, protocols));
|
||||
this.ws = Factory(this.options.url, this.options.protocols);
|
||||
this.ws.onopen = () => { this.retry = 0; };
|
||||
this.ws.onmessage = ({ data }) => { try { const event = liveEventSchema.parse(JSON.parse(String(data))); this.typed.get(event.type)?.forEach(fn => fn(event)); this.any.forEach(fn => fn(event)); } catch { /* reject malformed server data */ } };
|
||||
this.ws.onclose = () => this.scheduleReconnect(); this.ws.onerror = () => this.ws?.close();
|
||||
}
|
||||
private scheduleReconnect() { if (this.closed) return; const min = this.options.minReconnectMs ?? 500; const max = this.options.maxReconnectMs ?? 10_000; const delay = Math.min(max, min * 2 ** this.retry++); setTimeout(() => this.open(), delay); }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { LiveClient } from "../dist/index.js";
|
||||
test("client dispatches a validated server event", () => {
|
||||
const socket = {}; const client = new LiveClient({ url: "ws://invalid", websocketFactory: () => socket }); let received;
|
||||
client.on("live.enter", event => { received = event; }); client.connect();
|
||||
socket.onmessage({ data: JSON.stringify({ version: 1, id: "a0000000-0000-4000-8000-000000000001", occurredAt: new Date().toISOString(), roomId: "1", type: "live.enter", payload: { viewer: { uid: "1", name: "A" } } }) });
|
||||
assert.equal(received.payload.viewer.name, "A");
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "declaration": true, "outDir": "dist", "strict": true, "skipLibCheck": true }, "include": ["src"] }
|
||||
Reference in New Issue
Block a user