import type { AbstractCrdtDocFactory, InferStorage, NoopCrdtDocFactory } from "@pluv/crdt"; import type { InferIOCrdtKind, InputZodLike, IOLike, JsonObject } from "@pluv/types"; import { MAX_PRESENCE_SIZE_BYTES } from "./constants"; import type { InferCallback } from "./infer"; import { PluvProcedure } from "./PluvProcedure"; import type { AuthEndpoint, PluvRoomAddon, PluvRoomDebug, ReconnectTimeoutMs, RoomConfig, RoomEndpoints, WsEndpoint, } from "./PluvRoom"; import { PluvRoom } from "./PluvRoom"; import type { PluvRouterEventConfig } from "./PluvRouter"; import { PluvRouter } from "./PluvRouter"; import type { PluvClientLimits, PublicKey, WithMetadata } from "./types"; export type PluvClientOptions< TIO extends IOLike, TPresence extends Record, TCrdt extends InferIOCrdtKind, TMetadata extends JsonObject, > = RoomEndpoints & { debug?: boolean; /** * @description Configurable limits defined for client-side validation. You should only set * this if you control the server and have changed the limits there. */ limits?: PluvClientLimits; metadata?: InputZodLike; presence?: InputZodLike; publicKey?: PublicKey; types: InferCallback; } & (InferIOCrdtKind extends NoopCrdtDocFactory ? { initialStorage?: "[ERROR]: Must provide crdt to createIO to use storage" } : { initialStorage?: TCrdt }); export type CreateRoomOptions< TIO extends IOLike, TPresence extends Record, TCrdt extends AbstractCrdtDocFactory, TMetadata extends JsonObject, TEvents extends PluvRouterEventConfig> = {}, > = { addons?: readonly PluvRoomAddon>[]; debug?: boolean | PluvRoomDebug; initialPresence?: TPresence; initialStorage?: TCrdt; onAuthorizationFail?: (error: Error) => void; reconnectTimeoutMs?: ReconnectTimeoutMs; router?: PluvRouter, TEvents>; }; export type EnterRoomParams = keyof TMetadata extends never ? [] : [WithMetadata]; export class PluvClient< TIO extends IOLike, TPresence extends Record = {}, TCrdt extends InferIOCrdtKind = InferIOCrdtKind, TMetadata extends JsonObject = {}, > { public readonly metadata?: InputZodLike; private readonly _authEndpoint: AuthEndpoint | undefined; private readonly _debug: boolean; private readonly _initialStorage?: TCrdt; private readonly _limits: PluvClientLimits; private readonly _presence?: InputZodLike; private readonly _publicKey: PublicKey | null = null; private readonly _rooms = new Map>(); private readonly _wsEndpoint: WsEndpoint | undefined; public get _defs() { return { initialStorage: this._initialStorage, }; } public get procedure(): PluvProcedure { return new PluvProcedure(); } constructor(options: PluvClientOptions) { const { authEndpoint, debug = false, initialStorage, limits, metadata, presence, publicKey, wsEndpoint, } = options; this.metadata = metadata; this._authEndpoint = authEndpoint as AuthEndpoint; this._debug = debug; this._initialStorage = initialStorage as TCrdt; this._limits = { presenceMaxSize: MAX_PRESENCE_SIZE_BYTES, ...limits, }; this._presence = presence; this._wsEndpoint = wsEndpoint; if (!!publicKey) this._publicKey = publicKey; } public createRoom = < TEvents extends PluvRouterEventConfig> = {}, >( room: string, options: CreateRoomOptions = {}, ): PluvRoom => { const oldRoom = this.getRoom(room); if (oldRoom) return oldRoom; const newRoom = new PluvRoom(room, { addons: options.addons, authEndpoint: this._authEndpoint, debug: options.debug, initialPresence: options.initialPresence, initialStorage: options.initialStorage ?? this._initialStorage, limits: this._limits, metadata: this.metadata, onAuthorizationFail: options.onAuthorizationFail, presence: this._presence, publicKey: this._publicKey ?? undefined, reconnectTimeoutMs: options.reconnectTimeoutMs, router: options.router, wsEndpoint: this._wsEndpoint, } as RoomConfig); this._rooms.set(room, newRoom); this._logDebug(`New room was created: ${room}`); return newRoom; }; public enter = async ( room: string | PluvRoom, ...args: EnterRoomParams ): Promise> => { const toEnter = typeof room === "string" ? this.getRoom(room) : room; if (!toEnter) throw new Error(`Could not find room: ${room}.`); this._rooms.set(toEnter.id, toEnter); await toEnter.connect(...args); this._logDebug(`Entered room: ${room}`); return toEnter; }; public getRoom = (room: string): PluvRoom | null => { const found = this._rooms.get(room) as | PluvRoom | undefined; return found ?? null; }; public getRooms = (): readonly PluvRoom[] => { return Array.from(this._rooms.values()); }; public leave = async (room: string | PluvRoom): Promise => { const toLeave = typeof room === "string" ? this.getRoom(room) : room; if (!toLeave) return; await toLeave.disconnect(); this._rooms.delete(toLeave.id); this._logDebug(`Left and deleted room: ${room}`); }; public router> = {}>( events: TEvents, ): PluvRouter, TEvents> { const invalidName = Object.keys(events).find((name) => name.includes("$")); if (typeof invalidName === "string") { throw new Error(`Invalid event name. Event names must not contain $: "${invalidName}"`); } return new PluvRouter, TEvents>(events); } private _logDebug(...data: any[]): void { if (typeof process === "undefined") return; if (process.env?.NODE_ENV === "production") return; if (this._debug) console.log(...data); } }