import type { StateView } from '@colyseus/schema'; export interface CompatClient { readonly id: string; readonly sessionId: string; state: number; userData?: unknown; auth?: unknown; reconnectionToken: string; /** * Per-client visibility filter for `@view()`-gated state — identical to real * Colyseus. A game sets `client.view = new StateView()` in `onJoin` and adds * the refs this client may see; the host runtime encodes each viewed client * separately so gated fields never reach a client whose view excludes them. */ view?: StateView; ref: { on(event: string, handler: (...args: unknown[]) => void): void; off(event: string, handler: (...args: unknown[]) => void): void; emit(event: string, ...args: unknown[]): void; }; raw(data: Uint8Array | Buffer, options?: unknown, cb?: (err?: Error) => void): void; enqueueRaw(data: Uint8Array | Buffer, options?: unknown): void; send(type: string | number, payload?: unknown): void; sendBytes(type: string | number, bytes: Uint8Array | Buffer): void; leave(code?: number, data?: string): void; close(code?: number, data?: string): void; error(code: number, message?: string): void; } export type Client = CompatClient; export type MessageHandler = (client: CompatClient, payload: unknown) => void; export type WildcardMessageHandler = ( client: CompatClient, type: string | number, payload: unknown, ) => void; export class ClientArray extends Array { getById(sessionId: string): C | undefined { return this.find((client) => client.sessionId === sessionId); } delete(client: C): boolean { const index = this.indexOf(client); if (index < 0) return false; this.splice(index, 1); return true; } } export interface BroadcastOptions { except?: CompatClient | CompatClient[]; afterNextPatch?: boolean; } export interface RoomRuntimeBridge { broadcastMessage(type: string | number, payload?: unknown, options?: BroadcastOptions): void; broadcastState(): boolean; disconnect(code?: number): Promise; send(client: CompatClient, type: string | number, payload?: unknown): void; removeClient(client: CompatClient, code?: number, reason?: string): void; allowReconnection( client: CompatClient, seconds: number | 'manual', ): Promise & { reject(reason?: unknown): void; }; } export abstract class Room { maxClients = Infinity; autoDispose = true; patchRate: number | null = 50; maxMessagesPerSecond = Infinity; seatReservationTimeout = 15; state!: State; readonly clients = new ClientArray(); private explicitlyLocked = false; private matchmakingPrivate = false; private roomMetadata: unknown = {}; private readonly messageHandlers = new Map>(); private readonly wildcardMessageHandlers = new Set(); private readonly simulationTimers = new Set>(); private readonly timeoutTimers = new Set>(); private runtime: RoomRuntimeBridge | null = null; get locked(): boolean { return this.explicitlyLocked || this.hasReachedMaxClients(); } get metadata(): unknown { return this.roomMetadata; } set metadata(meta: unknown) { this.roomMetadata = meta; } get isPrivate(): boolean { return this.matchmakingPrivate; } setState(state: State): void { this.state = state; this.runtime?.broadcastState(); } onMessage(type: '*', handler: WildcardMessageHandler): () => void; onMessage(type: string | number, handler: MessageHandler): () => void; onMessage(type: string | number, _validationSchema: unknown, handler: MessageHandler): () => void; onMessage( type: string | number, handlerOrSchema: MessageHandler | WildcardMessageHandler | unknown, maybeHandler?: MessageHandler, ): () => void { const handler = (maybeHandler ?? handlerOrSchema) as MessageHandler | WildcardMessageHandler; if (type === '*') { this.wildcardMessageHandlers.add(handler as WildcardMessageHandler); return () => this.wildcardMessageHandlers.delete(handler as WildcardMessageHandler); } const key = String(type); let handlers = this.messageHandlers.get(key); if (!handlers) { handlers = new Set(); this.messageHandlers.set(key, handlers); } handlers.add(handler as MessageHandler); return () => handlers?.delete(handler as MessageHandler); } onMessageBytes(type: string | number, handler: MessageHandler): () => void; onMessageBytes( type: string | number, _validationSchema: unknown, handler: MessageHandler, ): () => void; onMessageBytes( type: string | number, handlerOrSchema: MessageHandler | unknown, maybeHandler?: MessageHandler, ): () => void { return this.onMessage(type, (maybeHandler ?? handlerOrSchema) as MessageHandler); } send(client: CompatClient, type: string | number, payload?: unknown): void { this.runtime?.send(client, type, payload); } broadcast(type: string | number, payload?: unknown, options?: BroadcastOptions): void { this.runtime?.broadcastMessage(type, payload, options); } broadcastBytes(type: string | number, payload: Uint8Array, options?: BroadcastOptions): void { this.broadcast(type, payload, options); } broadcastPatch(): boolean { return this.runtime?.broadcastState() ?? false; } setSimulationInterval(callback: (dt: number) => void, ms = 16): void { let last = Date.now(); const timer = setInterval(() => { const now = Date.now(); callback(now - last); last = now; this.runtime?.broadcastState(); }, ms); this.simulationTimers.add(timer); (timer as unknown as { unref?: () => void }).unref?.(); } setPatchRate(milliseconds: number | null): void { this.patchRate = milliseconds; } async setMetadata(meta: unknown): Promise { this.metadata = meta; } async setPrivate(bool = true): Promise { this.matchmakingPrivate = bool; } async setMatchmaking(updates: { metadata?: unknown; private?: boolean; locked?: boolean; maxClients?: number; [key: string]: unknown; }): Promise { if ('metadata' in updates) this.metadata = updates.metadata; if (typeof updates.private === 'boolean') this.matchmakingPrivate = updates.private; if (typeof updates.locked === 'boolean') this.explicitlyLocked = updates.locked; if (typeof updates.maxClients === 'number') this.maxClients = updates.maxClients; } async lock(): Promise { this.explicitlyLocked = true; } async unlock(): Promise { this.explicitlyLocked = false; } hasReachedMaxClients(): boolean { return this.clients.length >= this.maxClients; } setSeatReservationTime(seconds: number): this { this.seatReservationTimeout = seconds; return this; } hasReservedSeat(_sessionId: string): boolean { return false; } checkReconnectionToken(reconnectionToken: string): string { return reconnectionToken.split(':')[0] ?? reconnectionToken; } async disconnect(closeCode = 4000): Promise { await this.runtime?.disconnect(closeCode); } allowReconnection( previousClient: CompatClient, seconds: number | 'manual', ): Promise & { reject(reason?: unknown): void } { if (!this.runtime) throw new Error('Room runtime is not attached'); return this.runtime.allowReconnection(previousClient, seconds); } onBeforeShutdown(): void {} onCreate?(_options?: unknown): void; onAuth?( _client: CompatClient, _options?: unknown, ): boolean | unknown | Promise; onJoin?(_client: CompatClient, _options?: unknown): void; onDrop?(_client: CompatClient, _code?: number): void; onReconnect?(_client: CompatClient): void; onLeave?(_client: CompatClient, _code?: number): void; onDispose?(): void; _attachRuntime(runtime: RoomRuntimeBridge): void { this.runtime = runtime; } _dispatchMessage(client: CompatClient, type: string | number, payload: unknown): void { for (const handler of this.messageHandlers.get(String(type)) ?? []) handler(client, payload); for (const handler of this.wildcardMessageHandlers) handler(client, type, payload); this.runtime?.broadcastState(); } _disposeRuntime(): void { for (const timer of this.simulationTimers) clearInterval(timer); for (const timer of this.timeoutTimers) clearTimeout(timer); this.simulationTimers.clear(); this.timeoutTimers.clear(); this.runtime = null; } } export function validate(_format: unknown, handler: MessageHandler): MessageHandler { return handler; } export function room(options: Partial & ThisType): new () => T { return class extends Room { constructor() { super(); Object.assign(this, options); if ('state' in options && options.state !== undefined) { this.state = typeof options.state === 'function' ? (options.state as () => unknown)() : options.state; } } } as unknown as new () => T; }