import { nanoid } from 'nanoid'; import { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts'; import { debugAndPrintError, debugMatchMaking } from '../Debug.ts'; export type Type = new (...args: any[]) => T; export type MethodName = string & { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never }[keyof T]; /** * Utility type that extracts the return type of a method or the type of a property * from a given class/object type. * * - If the key is a method, returns the awaited return type of that method * - If the key is a property, returns the type of that property */ export type ExtractMethodOrPropertyType< TClass, TKey extends keyof TClass > = TClass[TKey] extends (...args: any[]) => infer R ? Awaited : TClass[TKey]; // remote room call timeouts export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000); export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5); export function generateId(length: number = 9) { return nanoid(length); } export function getBearerToken(authHeader: string) { return (authHeader && authHeader.startsWith("Bearer ") && authHeader.substring(7, authHeader.length)) || undefined; } // nodemon sends SIGUSR2 before reloading // (https://github.com/remy/nodemon#controlling-shutdown-of-your-script) // const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2']; export function registerGracefulShutdown(callback: (err?: Error) => void) { /** * Gracefully shutdown on uncaught errors */ process.on('uncaughtException', (err) => { debugAndPrintError(err); callback(err); }); signals.forEach((signal) => process.once(signal, () => callback())); } export function retry( cb: Function, maxRetries: number = 3, errorWhiteList: any[] = [], retries: number = 0, ) { return new Promise((resolve, reject) => { cb() .then(resolve) .catch((e: any) => { if ( errorWhiteList.indexOf(e.constructor) !== -1 && retries++ < maxRetries ) { setTimeout(() => { debugMatchMaking("retrying due to error (error: %s, retries: %s, maxRetries: %s)", e.message, retries, maxRetries); retry(cb, maxRetries, errorWhiteList, retries). then(resolve). catch((e2) => reject(e2)); }, Math.floor(Math.random() * Math.pow(2, retries) * 400)); } else { reject(e); } }); }); } export function spliceOne(arr: any[], index: number): boolean { // manually splice availableRooms array // http://jsperf.com/manual-splice if (index === -1 || index >= arr.length) { return false; } const len = arr.length - 1; for (let i = index; i < len; i++) { arr[i] = arr[i + 1]; } arr.length = len; return true; } export class Deferred { public promise: Promise; public resolve: Function; public reject: Function; constructor(promise?: Promise) { this.promise = promise ?? new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) { return this.promise.then(onFulfilled, onRejected); } public catch(func: (value: any) => any) { return this.promise.catch(func); } static reject (reason?: any) { return new Deferred(Promise.reject(reason)); } static resolve(value?: T) { return new Deferred(Promise.resolve(value)); } } export function merge(a: any, ...objs: any[]): any { for (let i = 0, len = objs.length; i < len; i++) { const b = objs[i]; for (const key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } } return a; } export function wrapTryCatch( method: Function, onError: (error: RoomException, methodName: RoomMethodName) => void, exceptionClass: Type, methodName: RoomMethodName, rethrow: boolean = false, ...additionalErrorArgs: any[] ) { return (...args: any[]) => { try { const result = method(...args); if (typeof (result?.catch) === "function") { return result.catch((e: Error) => { onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName); if (rethrow) { throw e; } }); } return result; } catch (e: any) { onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName); if (rethrow) { throw e; } } }; } /** * Dynamically import a module using either require() or import() * based on the current module system (CJS vs ESM). * * This avoids double-loading packages when running in mixed ESM/CJS environments. * Errors are silently caught - await the promise and handle errors at usage site. */ export function dynamicImport(moduleName: string): Promise { // __dirname exists in CJS but not in ESM if ( typeof __dirname !== 'undefined' && // @ts-ignore typeof (Bun) === 'undefined' // prevent bun from loading CJS modules ) { // CJS context - use require() try { return Promise.resolve(require(moduleName)); } catch (e: any) { // If the error is not a MODULE_NOT_FOUND error, reject with the error. if (e.code !== 'MODULE_NOT_FOUND') { return Promise.reject(e); } return Promise.resolve(undefined); } } else { // ESM context - use import() const promise = import(/* @vite-ignore */ moduleName); promise.catch(() => {}); // prevent unhandled rejection warnings return promise; } }