@colyseus/core
Version:
Multiplayer Framework for Node.js.
489 lines (488 loc) • 19 kB
TypeScript
import { ClockTimer as Clock } from '@colyseus/timer';
import type { Presence } from './presence/Presence.ts';
import type { Serializer } from './serializer/Serializer.ts';
import { type Type, Deferred } from './utils/Utils.ts';
import { type AuthContext, type Client, ClientArray, type ISendOptions, type MessageArgs } from './Transport.ts';
import { type RoomMethodName, type RoomException } from './errors/RoomExceptions.ts';
import { type StandardSchemaV1 } from './utils/StandardSchema.ts';
import { type MessageHandlerWithFormat as SharedMessageHandlerWithFormat, type MessageHandler as SharedMessageHandler, type Messages as SharedMessages } from '@colyseus/shared-types';
export declare const DEFAULT_SEAT_RESERVATION_TIME: number;
export type SimulationCallback = (deltaTime: number) => void;
export interface RoomOptions {
state?: object;
metadata?: any;
client?: Client;
}
export type ExtractRoomState<T> = T extends {
state?: infer S extends object;
} ? S : any;
export type ExtractRoomMetadata<T> = T extends {
metadata?: infer M;
} ? M : any;
export type ExtractRoomClient<T> = T extends {
client?: infer C extends Client;
} ? C : Client;
export interface IBroadcastOptions extends ISendOptions {
except?: Client | Client[];
}
/**
* Message handler with automatic type inference from format schema.
* When a format is provided, the message type is automatically inferred from the schema.
*/
export type MessageHandlerWithFormat<T extends StandardSchemaV1 = any, This = any> = SharedMessageHandlerWithFormat<T, Client, This>;
export type MessageHandler<This = any> = SharedMessageHandler<Client, This>;
/**
* A map of message types to message handlers.
*/
export type Messages<This extends Room> = SharedMessages<This, Client>;
/**
* Helper function to create a validated message handler with automatic type inference.
*
* @example
* ```typescript
* messages = {
* move: validate(z.object({ x: z.number(), y: z.number() }), (client, message) => {
* // message.x and message.y are automatically typed as numbers
* console.log(message.x, message.y);
* })
* }
* ```
*/
export declare function validate<T extends StandardSchemaV1, This = any>(format: T, handler: (this: This, client: Client, message: StandardSchemaV1.InferOutput<T>) => void): MessageHandlerWithFormat<T, This>;
export declare const RoomInternalState: {
readonly CREATING: 0;
readonly CREATED: 1;
readonly DISPOSING: 2;
};
export type RoomInternalState = (typeof RoomInternalState)[keyof typeof RoomInternalState];
export type OnCreateOptions<T extends Type<Room>> = Parameters<NonNullable<InstanceType<T>['onCreate']>>[0];
/**
* A Room class is meant to implement a game session, and/or serve as the communication channel
* between a group of clients.
*
* - Rooms are created on demand during matchmaking by default
* - Room classes must be exposed using `.define()`
*
* @example
* ```typescript
* class MyRoom extends Room<{
* state: MyState,
* metadata: { difficulty: string },
* client: MyClient
* }> {
* // ...
* }
* ```
*/
export declare class Room<T extends RoomOptions = RoomOptions> {
#private;
'~client': ExtractRoomClient<T>;
'~state': ExtractRoomState<T>;
'~metadata': ExtractRoomMetadata<T>;
/**
* This property will change on these situations:
* - The maximum number of allowed clients has been reached (`maxClients`)
* - You manually locked, or unlocked the room using lock() or `unlock()`.
*
* @readonly
*/
get locked(): boolean;
/**
* Get the room's matchmaking metadata.
*/
get metadata(): ExtractRoomMetadata<T>;
/**
* Set the room's matchmaking metadata.
*
* **Note**: This setter does NOT automatically persist. Use `setMatchmaking()` for automatic persistence.
*
* @example
* ```typescript
* class MyRoom extends Room<{ metadata: { difficulty: string; rating: number } }> {
* async onCreate() {
* this.metadata = { difficulty: "hard", rating: 1500 };
* }
* }
* ```
*/
set metadata(meta: ExtractRoomMetadata<T>);
/**
* The room listing cache for matchmaking.
* @internal
*/
private _listing;
/**
* Timing events tied to the room instance.
* Intervals and timeouts are cleared when the room is disposed.
*/
clock: Clock;
/**
* Maximum number of clients allowed to connect into the room. When room reaches this limit,
* it is locked automatically. Unless the room was explicitly locked by you via `lock()` method,
* the room will be unlocked as soon as a client disconnects from it.
*/
maxClients: number;
/**
* Automatically dispose the room when last client disconnects.
*
* @default true
*/
autoDispose: boolean;
/**
* Frequency to send the room state to connected clients, in milliseconds.
*
* @default 50ms (20fps)
*/
patchRate: number | null;
/**
* Maximum number of messages a client can send to the server per second.
* If a client sends more messages than this, it will be disconnected.
*
* @default Infinity
*/
maxMessagesPerSecond: number;
/**
* The state instance you provided to `setState()`.
*/
state: ExtractRoomState<T>;
/**
* The presence instance. Check Presence API for more details.
*
* @see [Presence API](https://docs.colyseus.io/server/presence)
*/
presence: Presence;
/**
* The array of connected clients.
*
* @see [Client instance](https://docs.colyseus.io/room#client)
*/
clients: ClientArray<ExtractRoomClient<T>>;
/**
* Set the number of seconds a room can wait for a client to effectively join the room.
* You should consider how long your `onAuth()` will have to wait for setting a different seat reservation time.
* The default value is 15 seconds. You may set the `COLYSEUS_SEAT_RESERVATION_TIME`
* environment variable if you'd like to change the seat reservation time globally.
*
* @default 15 seconds
*/
seatReservationTimeout: number;
private _events;
private _reservedSeats;
private _reservedSeatTimeouts;
private _reconnections;
private _reconnectionAttempts;
messages?: Messages<any>;
private onMessageEvents;
private onMessageValidators;
private onMessageFallbacks;
private _serializer;
private _afterNextPatchQueue;
private _simulationInterval;
private _internalState;
private _lockedExplicitly;
private _autoDisposeTimeout;
constructor();
/**
* This method is called by the MatchMaker before onCreate()
* @internal
*/
private __init;
/**
* The name of the room you provided as first argument for `gameServer.define()`.
*
* @returns roomName string
*/
get roomName(): string;
/**
* Setting the name of the room. Overwriting this property is restricted.
*
* @param roomName
*/
set roomName(roomName: string);
/**
* A unique, auto-generated, 9-character-long id of the room.
* You may replace `this.roomId` during `onCreate()`.
*
* @returns roomId string
*/
get roomId(): string;
/**
* Setting the roomId, is restricted in room lifetime except upon room creation.
*
* @param roomId
* @returns roomId string
*/
set roomId(roomId: string);
/**
* This method is called before the latest version of the room's state is broadcasted to all clients.
*/
onBeforePatch?(state: ExtractRoomState<T>): void | Promise<any>;
/**
* This method is called when the room is created.
* @param options - The options passed to the room when it is created.
*/
onCreate?(options: any): void | Promise<any>;
/**
* This method is called when a client joins the room.
* @param client - The client that joined the room.
* @param options - The options passed to the client when it joined the room.
* @param auth - The data returned by the `onAuth` method - (Deprecated: use `client.auth` instead)
*/
onJoin?(client: ExtractRoomClient<T>, options?: any, auth?: any): void | Promise<any>;
/**
* This method is called when a client leaves the room without consent.
* You may allow the client to reconnect by calling `allowReconnection` within this method.
*
* @param client - The client that was dropped from the room.
* @param code - The close code of the leave event.
*/
onDrop?(client: ExtractRoomClient<T>, code?: number): void | Promise<any>;
/**
* This method is called when a client reconnects to the room.
* @param client - The client that reconnected to the room.
*/
onReconnect?(client: ExtractRoomClient<T>): void | Promise<any>;
/**
* This method is called when a client effectively leaves the room.
* @param client - The client that left the room.
* @param code - The close code of the leave event.
*/
onLeave?(client: ExtractRoomClient<T>, code?: number): void | Promise<any>;
/**
* This method is called when the room is disposed.
*/
onDispose?(): void | Promise<any>;
/**
* Define a custom exception handler.
* If defined, all lifecycle hooks will be wrapped by try/catch, and the exception will be forwarded to this method.
*
* These methods will be wrapped by try/catch:
* - `onMessage`
* - `onAuth` / `onJoin` / `onLeave` / `onCreate` / `onDispose`
* - `clock.setTimeout` / `clock.setInterval`
* - `setSimulationInterval`
*
* (Experimental: this feature is subject to change in the future - we're currently getting feedback to improve it)
*/
onUncaughtException?(error: RoomException, methodName: RoomMethodName): void;
/**
* This method is called before onJoin() - this is where you should authenticate the client
* @param client - The client that is authenticating.
* @param options - The options passed to the client when it is authenticating.
* @param context - The authentication context, including the token and the client's IP address.
* @returns The authentication result.
*
* @example
* ```typescript
* return {
* userId: 123,
* username: "John Doe",
* email: "john.doe@example.com",
* };
* ```
*/
onAuth(client: Client, options: any, context: AuthContext): any | Promise<any>;
static onAuth(token: string, options: any, context: AuthContext): Promise<unknown>;
/**
* This method is called during graceful shutdown of the server process
* You may override this method to dispose the room in your own way.
*
* Once process reaches room count of 0, the room process will be terminated.
*/
onBeforeShutdown(): void;
/**
* devMode: When `devMode` is enabled, `onCacheRoom` method is called during
* graceful shutdown.
*
* Implement this method to return custom data to be cached. `onRestoreRoom`
* will be called with the data returned by `onCacheRoom`
*/
onCacheRoom?(): any;
/**
* devMode: When `devMode` is enabled, `onRestoreRoom` method is called during
* process startup, with the data returned by the `onCacheRoom` method.
*/
onRestoreRoom?(cached?: any): void;
/**
* Returns whether the sum of connected clients and reserved seats exceeds maximum number of clients.
*
* @returns boolean
*/
hasReachedMaxClients(): boolean;
/**
* @deprecated Use `seatReservationTimeout=` instead.
*/
setSeatReservationTime(seconds: number): this;
hasReservedSeat(sessionId: string, reconnectionToken?: string): boolean;
checkReconnectionToken(reconnectionToken: string): string;
/**
* (Optional) Set a simulation interval that can change the state of the game.
* The simulation interval is your game loop.
*
* @default 16.6ms (60fps)
*
* @param onTickCallback - You can implement your physics or world updates here!
* This is a good place to update the room state.
* @param delay - Interval delay on executing `onTickCallback` in milliseconds.
*/
setSimulationInterval(onTickCallback?: SimulationCallback, delay?: number): void;
/**
* @deprecated Use `.patchRate=` instead.
*/
setPatchRate(milliseconds: number | null): void;
/**
* @deprecated Use `.state =` instead.
*/
setState(newState: ExtractRoomState<T>): void;
setSerializer(serializer: Serializer<ExtractRoomState<T>>): void;
setMetadata(meta: Partial<ExtractRoomMetadata<T>>, persist?: boolean): Promise<void>;
setPrivate(bool?: boolean, persist?: boolean): Promise<void>;
/**
* Update multiple matchmaking/listing properties at once with a single persist operation.
* This is the recommended way to update room listing properties.
*
* @param updates - Object containing the properties to update
*
* @example
* ```typescript
* // Update multiple properties at once
* await this.setMatchmaking({
* metadata: { difficulty: "hard", rating: 1500 },
* private: true,
* locked: true,
* maxClients: 10
* });
* ```
*
* @example
* ```typescript
* // Update only metadata
* await this.setMatchmaking({
* metadata: { status: "in_progress" }
* });
* ```
*
* @example
* ```typescript
* // Partial metadata update (merges with existing)
* await this.setMatchmaking({
* metadata: { ...this.metadata, round: this.metadata.round + 1 }
* });
* ```
*/
setMatchmaking(updates: {
metadata?: ExtractRoomMetadata<T>;
private?: boolean;
locked?: boolean;
maxClients?: number;
unlisted?: boolean;
[key: string]: any;
}): Promise<void>;
/**
* Lock the room. This prevents new clients from joining this room.
*/
lock(): Promise<void>;
/**
* Unlock the room. This allows new clients to join this room, if maxClients is not reached.
*/
unlock(): Promise<void>;
/**
* @deprecated Use `client.send(...)` instead.
*/
send(client: Client, type: string | number, message: any, options?: ISendOptions): void;
/**
* Broadcast a message to all connected clients.
* @param type - The type of the message.
* @param message - The message to broadcast.
* @param options - The options for the broadcast.
*
* @example
* ```typescript
* this.broadcast('message', { message: 'Hello, world!' });
* ```
*/
broadcast<K extends keyof ExtractRoomClient<T>['~messages'] & string | number>(type: K, ...args: MessageArgs<ExtractRoomClient<T>['~messages'][K], IBroadcastOptions>): void;
/**
* Broadcast bytes (UInt8Arrays) to a particular room
*/
broadcastBytes(type: string | number, message: Uint8Array, options: IBroadcastOptions): void;
/**
* Checks whether mutations have occurred in the state, and broadcast them to all connected clients.
*/
broadcastPatch(): boolean;
/**
* Register a message handler for a specific message type.
* This method is used to handle messages sent by clients to the room.
* @param messageType - The type of the message.
* @param callback - The callback to call when the message is received.
* @returns A function to unbind the callback.
*
* @example
* ```typescript
* this.onMessage('message', (client, message) => {
* console.log(message);
* });
* ```
*
* @example
* ```typescript
* const unbind = this.onMessage('message', (client, message) => {
* console.log(message);
* });
*
* // Unbind the callback when no longer needed
* unbind();
* ```
*/
onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: '*', callback: (client: C, type: string | number, message: T) => void): any;
onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, callback: (client: C, message: T) => void): any;
onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, validationSchema: StandardSchemaV1<T>, callback: (client: C, message: T) => void): any;
onMessageBytes<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, callback: (client: C, message: T) => void): any;
onMessageBytes<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, validationSchema: StandardSchemaV1<T>, callback: (client: C, message: T) => void): any;
/**
* Disconnect all connected clients, and then dispose the room.
*
* @param closeCode WebSocket close code (default = 4000, which is a "consented leave")
* @returns Promise<void>
*/
disconnect(closeCode?: number): Promise<any>;
private _rejectPendingReconnections;
private _onJoin;
/**
* Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method.
* If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds.
*
* @param client - The client that is allowed to reconnect into the room.
* @param seconds - The time in seconds that the client is allowed to reconnect into the room.
*
* @returns Deferred<Client> - The differed is a promise like type.
* This type can forcibly reject the promise by calling `.reject()`.
*
* @example
* ```typescript
* onDrop(client: Client, code: CloseCode) {
* // Allow the client to reconnect into the room with a 15 seconds timeout.
* this.allowReconnection(client, 15);
* }
* ```
*/
allowReconnection(previousClient: Client, seconds: number | "manual"): Deferred<Client>;
private resetAutoDisposeTimeout;
private broadcastMessageType;
private sendFullState;
private _dequeueAfterPatchMessages;
private _reserveSeat;
private _reserveMultipleSeats;
private _onMessage;
private _onLeave;
}
/**
* (WIP) Alternative, method-based room definition.
* We should be able to define
*/
type RoomLifecycleMethods = 'messages' | 'onCreate' | 'onJoin' | 'onLeave' | 'onDispose' | 'onCacheRoom' | 'onRestoreRoom' | 'onDrop' | 'onReconnect' | 'onUncaughtException' | 'onAuth' | 'onBeforeShutdown' | 'onBeforePatch';
type DefineRoomOptions<T extends RoomOptions = RoomOptions> = Partial<Pick<Room<T>, RoomLifecycleMethods>> & {
state?: ExtractRoomState<T> | (() => ExtractRoomState<T>);
} & ThisType<Exclude<Room<T>, RoomLifecycleMethods>> & ThisType<Room<T>>;
export declare function room<T>(options: DefineRoomOptions<T>): typeof Room<T>;
export {};