import { type AppPlugin, type MutableRemoteState, rpcOptions, type Service } from "./api.ts"; import type { ServerWireMessage, SessionRequest, StateSnapshot } from "./protocol.ts"; class SessionRemoteState implements MutableRemoteState { private readonly listeners = new Set<(value: T) => void>(); private current: T; constructor(initial: T) { this.current = structuredClone(initial); } get value(): T { return this.current; } set(value: T): void { this.current = structuredClone(value); for (const listener of this.listeners) listener(this.current); } subscribe(listener: (value: T) => void): () => void { this.listeners.add(listener); listener(this.current); return () => this.listeners.delete(listener); } } class ClientHub { private readonly connections = new Map void>(); connect(clientId: string, snapshot: () => StateSnapshot, send: (message: ServerWireMessage) => void): () => void { const pending: ServerWireMessage[] = []; let ready = false; const connection = (message: ServerWireMessage) => { if (ready) send(message); else pending.push(message); }; this.connections.set(clientId, connection); send({ type: "snapshot", states: structuredClone(snapshot()) }); ready = true; for (const message of pending) send(message); return () => { if (this.connections.get(clientId) === connection) this.connections.delete(clientId); }; } broadcast(message: ServerWireMessage): void { for (const send of this.connections.values()) send(structuredClone(message)); } } export interface SessionContext { onActivate(callback: () => void | Promise): void; onClientConnect(callback: (clientId: string) => void): void; onClientDisconnect(callback: (clientId: string) => void): void; onClose(callback: () => void): void; provide(service: Service, implementation: T): void; remoteState(initial: T): MutableRemoteState; use(service: Service): T; } export interface SessionDriver { readonly trace: readonly string[]; connect(clientId: string, send: (message: ServerWireMessage) => void): () => void; request(clientId: string, request: SessionRequest, signal: AbortSignal): Promise; use(service: Service): T; } export class SessionRuntime { readonly driver: SessionDriver; private readonly activateCallbacks: Array<() => void | Promise> = []; private readonly clientConnectCallbacks: Array<(clientId: string) => void> = []; private readonly clientDisconnectCallbacks: Array<(clientId: string) => void> = []; private readonly closeCallbacks: Array<() => void> = []; private readonly hub = new ClientHub(); private readonly plugins: readonly AppPlugin[]; private readonly services = new Map(); private readonly trace: string[] = []; constructor(plugins: readonly AppPlugin[]) { this.plugins = plugins; this.driver = { trace: this.trace, connect: (clientId, send) => { const disconnect = this.hub.connect(clientId, () => this.snapshot(), send); for (const callback of this.clientConnectCallbacks) callback(clientId); let connected = true; return () => { if (!connected) return; connected = false; disconnect(); for (const callback of this.clientDisconnectCallbacks) callback(clientId); }; }, request: async (clientId, request, signal) => { const service = this.services.get(request.service) as Record | undefined; const method = service?.[request.method]; if (typeof method !== "function") throw new Error(`Unknown RPC: ${request.service}.${request.method}`); this.trace.push(`rpc:${request.service}.${request.method}`); const args = request.rpcOptions ? [...request.args, rpcOptions({ clientId, signal })] : request.args; return Reflect.apply(method, service, args); }, use: (service) => this.use(service), }; } async start(): Promise { const context: SessionContext = { onActivate: (callback) => this.activateCallbacks.push(callback), onClientConnect: (callback) => this.clientConnectCallbacks.push(callback), onClientDisconnect: (callback) => this.clientDisconnectCallbacks.push(callback), onClose: (callback) => this.closeCallbacks.push(callback), provide: (service, implementation) => this.provide(service, implementation), remoteState: (initial) => new SessionRemoteState(initial), use: (service) => this.use(service), }; for (const plugin of this.plugins) await plugin.session?.(context); for (const activate of this.activateCallbacks) await activate(); } close(): void { for (const close of this.closeCallbacks.splice(0).reverse()) close(); } private provide(service: Service, implementation: T): void { if (this.services.has(service.id)) throw new Error(`Service already provided: ${service.id}`); this.services.set(service.id, implementation as object); for (const [property, value] of Object.entries(implementation as object)) { if (!(value instanceof SessionRemoteState)) continue; value.subscribe((next) => { this.hub.broadcast({ type: "state_update", service: service.id, property, value: structuredClone(next), }); }); } } private use(service: Service): T { const implementation = this.services.get(service.id); if (!implementation) throw new Error(`Service not provided: ${service.id}`); return implementation as T; } private snapshot(): StateSnapshot { const snapshot: StateSnapshot = {}; for (const [serviceId, implementation] of this.services) { const states: Record = {}; for (const [property, value] of Object.entries(implementation)) { if (value instanceof SessionRemoteState) states[property] = structuredClone(value.value); } if (Object.keys(states).length > 0) snapshot[serviceId] = states; } return snapshot; } }