@colyseus/core
Version:
Multiplayer Framework for Node.js.
198 lines (172 loc) • 5.74 kB
text/typescript
import { nanoid } from 'nanoid';
import { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';
import { debugAndPrintError, debugMatchMaking } from '../Debug.ts';
export type Type<T> = new (...args: any[]) => T;
export type MethodName<T> = 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<R>
: 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<T = any>(
cb: Function,
maxRetries: number = 3,
errorWhiteList: any[] = [],
retries: number = 0,
) {
return new Promise<T>((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<T>(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<T = any> {
public promise: Promise<T>;
public resolve: Function;
public reject: Function;
constructor(promise?: Promise<T>) {
this.promise = promise ?? new Promise<T>((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<T = any>(value?: T) {
return new Deferred<T>(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<RoomException>,
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<T = any>(moduleName: string): Promise<T> {
// __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;
}
}