@colyseus/core 0.17.51 → 0.17.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/MatchMaker.cjs +54 -3
- package/build/MatchMaker.cjs.map +3 -3
- package/build/MatchMaker.d.ts +9 -0
- package/build/MatchMaker.mjs +54 -4
- package/build/MatchMaker.mjs.map +3 -3
- package/build/presence/LocalPresence.cjs +2 -0
- package/build/presence/LocalPresence.cjs.map +2 -2
- package/build/presence/LocalPresence.mjs +2 -0
- package/build/presence/LocalPresence.mjs.map +2 -2
- package/build/utils/Utils.cjs +6 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +2 -0
- package/build/utils/Utils.mjs +4 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +4 -4
- package/src/MatchMaker.ts +88 -6
- package/src/presence/LocalPresence.ts +2 -0
- package/src/utils/Utils.ts +6 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/Utils.ts"],
|
|
4
|
-
"sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n/**\n * Return type of `remoteRoomCall()`.\n *\n * Resolves to the method's awaited return type (or the property's type) when\n * the method name was captured as a literal type. Falls back to `any` when it\n * wasn't \u2014 e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:\n * TypeScript applies the `TMethod` default instead of inferring the literal\n * once an explicit type argument list is present (microsoft/TypeScript#26242).\n * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a\n * precise return type.\n */\nexport type RemoteRoomCallReturn<\n TRoom,\n TMethod extends keyof TRoom\n> = keyof TRoom extends TMethod\n ? any\n : ExtractMethodOrPropertyType<TRoom, TMethod>;\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;AAAA,SAAS,cAAc;AACvB,OAAwD;AAExD,SAAS,oBAAoB,wBAAwB;AAwC9C,IAAM,4BAA4B,OAAO,QAAQ,IAAI,mCAAmC,GAAI;AAC5F,IAAM,uCAAuC,OAAO,QAAQ,IAAI,iDAAiD,GAAG;
|
|
4
|
+
"sourcesContent": ["import { nanoid } from 'nanoid';\nimport { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';\n\nimport { debugAndPrintError, debugMatchMaking } from '../Debug.ts';\n\nexport type Type<T> = new (...args: any[]) => T;\nexport type MethodName<T> = string & {\n [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never\n}[keyof T];\n\n/**\n * Utility type that extracts the return type of a method or the type of a property\n * from a given class/object type.\n *\n * - If the key is a method, returns the awaited return type of that method\n * - If the key is a property, returns the type of that property\n */\nexport type ExtractMethodOrPropertyType<\n TClass,\n TKey extends keyof TClass\n> = TClass[TKey] extends (...args: any[]) => infer R\n ? Awaited<R>\n : TClass[TKey];\n\n/**\n * Return type of `remoteRoomCall()`.\n *\n * Resolves to the method's awaited return type (or the property's type) when\n * the method name was captured as a literal type. Falls back to `any` when it\n * wasn't \u2014 e.g. `remoteRoomCall<MyRoom>(...)` with only the room type given:\n * TypeScript applies the `TMethod` default instead of inferring the literal\n * once an explicit type argument list is present (microsoft/TypeScript#26242).\n * Pass both type arguments (`remoteRoomCall<MyRoom, 'myMethod'>`) for a\n * precise return type.\n */\nexport type RemoteRoomCallReturn<\n TRoom,\n TMethod extends keyof TRoom\n> = keyof TRoom extends TMethod\n ? any\n : ExtractMethodOrPropertyType<TRoom, TMethod>;\n\n// remote room call timeouts\nexport const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);\nexport const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);\n\n// a peer that removes this process's room listings leaves a receipt. this\n// process looks for it every INTERVAL (ms) and restores them. the TTL (seconds)\n// only bounds what dead processes leave behind.\nexport const SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5000);\nexport const SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);\n\nexport function generateId(length: number = 9) {\n return nanoid(length);\n}\n\nexport function getBearerToken(authHeader: string) {\n return (authHeader && authHeader.startsWith(\"Bearer \") && authHeader.substring(7, authHeader.length)) || undefined;\n}\n\n// nodemon sends SIGUSR2 before reloading\n// (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)\n//\nconst signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];\n\nexport function registerGracefulShutdown(callback: (err?: Error) => void) {\n /**\n * Gracefully shutdown on uncaught errors\n */\n process.on('uncaughtException', (err) => {\n debugAndPrintError(err);\n callback(err);\n });\n\n signals.forEach((signal) =>\n process.once(signal, () => callback()));\n}\n\nexport function retry<T = any>(\n cb: Function,\n maxRetries: number = 3,\n errorWhiteList: any[] = [],\n retries: number = 0,\n) {\n return new Promise<T>((resolve, reject) => {\n cb()\n .then(resolve)\n .catch((e: any) => {\n if (\n errorWhiteList.indexOf(e.constructor) !== -1 &&\n retries++ < maxRetries\n ) {\n setTimeout(() => {\n debugMatchMaking(\"retrying due to error (error: %s, retries: %s, maxRetries: %s)\", e.message, retries, maxRetries);\n retry<T>(cb, maxRetries, errorWhiteList, retries).\n then(resolve).\n catch((e2) => reject(e2));\n }, Math.floor(Math.random() * Math.pow(2, retries) * 400));\n\n } else {\n reject(e);\n }\n });\n });\n}\n\nexport function spliceOne(arr: any[], index: number): boolean {\n // manually splice availableRooms array\n // http://jsperf.com/manual-splice\n if (index === -1 || index >= arr.length) {\n return false;\n }\n\n const len = arr.length - 1;\n for (let i = index; i < len; i++) {\n arr[i] = arr[i + 1];\n }\n\n arr.length = len;\n return true;\n}\n\nexport class Deferred<T = any> {\n public promise: Promise<T>;\n\n public resolve: Function;\n public reject: Function;\n\n constructor(promise?: Promise<T>) {\n this.promise = promise ?? new Promise<T>((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n\n public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {\n return this.promise.then(onFulfilled, onRejected);\n }\n\n public catch(func: (value: any) => any) {\n return this.promise.catch(func);\n }\n\n static reject (reason?: any) {\n return new Deferred(Promise.reject(reason));\n }\n\n static resolve<T = any>(value?: T) {\n return new Deferred<T>(Promise.resolve(value));\n }\n\n}\n\nexport function merge(a: any, ...objs: any[]): any {\n for (let i = 0, len = objs.length; i < len; i++) {\n const b = objs[i];\n for (const key in b) {\n if (b.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n }\n return a;\n}\n\nexport function wrapTryCatch(\n method: Function,\n onError: (error: RoomException, methodName: RoomMethodName) => void,\n exceptionClass: Type<RoomException>,\n methodName: RoomMethodName,\n rethrow: boolean = false,\n ...additionalErrorArgs: any[]\n) {\n return (...args: any[]) => {\n try {\n const result = method(...args);\n if (typeof (result?.catch) === \"function\") {\n return result.catch((e: Error) => {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n });\n }\n return result;\n } catch (e: any) {\n onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);\n if (rethrow) { throw e; }\n }\n };\n}\n\n/**\n * Dynamically import a module using either require() or import()\n * based on the current module system (CJS vs ESM).\n *\n * This avoids double-loading packages when running in mixed ESM/CJS environments.\n * Errors are silently caught - await the promise and handle errors at usage site.\n */\nexport function dynamicImport<T = any>(moduleName: string): Promise<T> {\n // __dirname exists in CJS but not in ESM\n if (\n typeof __dirname !== 'undefined' &&\n // @ts-ignore\n typeof (Bun) === 'undefined' // prevent bun from loading CJS modules\n ) {\n // CJS context - use require()\n try {\n return Promise.resolve(require(moduleName));\n } catch (e: any) {\n // If the error is not a MODULE_NOT_FOUND error, reject with the error.\n if (e.code !== 'MODULE_NOT_FOUND') {\n return Promise.reject(e);\n }\n return Promise.resolve(undefined);\n }\n } else {\n // ESM context - use import()\n const promise = import(/* @vite-ignore */ moduleName);\n promise.catch(() => {}); // prevent unhandled rejection warnings\n return promise;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/core",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.52",
|
|
4
4
|
"description": "Multiplayer Framework for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"input": "./src/index.ts",
|
|
@@ -52,15 +52,15 @@
|
|
|
52
52
|
"debug": "^4.3.4",
|
|
53
53
|
"nanoid": "^3.3.11",
|
|
54
54
|
"@colyseus/shared-types": "^0.17.6",
|
|
55
|
-
"@colyseus/
|
|
56
|
-
"@colyseus/
|
|
55
|
+
"@colyseus/better-call": "^1.3.1",
|
|
56
|
+
"@colyseus/greeting-banner": "^3.0.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@colyseus/schema": "^4.0.7",
|
|
60
60
|
"express": "^5.0.0",
|
|
61
61
|
"vitest": "^3.1.1",
|
|
62
|
-
"@colyseus/redis-presence": "^0.17.8",
|
|
63
62
|
"@colyseus/tools": "^0.17.22",
|
|
63
|
+
"@colyseus/redis-presence": "^0.17.8",
|
|
64
64
|
"@colyseus/redis-driver": "^0.17.7"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
package/src/MatchMaker.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
|
|
3
3
|
import { requestFromIPC, subscribeIPC, subscribeWithTimeout } from './IPC.ts';
|
|
4
4
|
|
|
5
|
-
import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, type MethodName, type RemoteRoomCallReturn } from './utils/Utils.ts';
|
|
5
|
+
import { type Type, Deferred, generateId, merge, retry, MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME, REMOTE_ROOM_SHORT_TIMEOUT, SWEPT_CHECK_INTERVAL, SWEPT_RECEIPT_TTL, type MethodName, type RemoteRoomCallReturn } from './utils/Utils.ts';
|
|
6
6
|
import { isDevMode, cacheRoomHistory, getPreviousProcessId, getRoomRestoreListKey, reloadFromCache } from './utils/DevMode.ts';
|
|
7
7
|
|
|
8
8
|
import { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
|
|
@@ -174,6 +174,8 @@ export async function accept(isStandalone: boolean = false) {
|
|
|
174
174
|
* (`stats.excludeProcess()`) by mistake due to health-check failure
|
|
175
175
|
*/
|
|
176
176
|
stats.setAutoPersistInterval();
|
|
177
|
+
|
|
178
|
+
startSweptCheck();
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
state = MatchMakerState.READY;
|
|
@@ -774,6 +776,9 @@ export async function gracefullyShutdown(): Promise<any> {
|
|
|
774
776
|
// unsubscribe from process id channel
|
|
775
777
|
presence.unsubscribe(getProcessChannel());
|
|
776
778
|
|
|
779
|
+
// only now: rooms may take minutes to drain, and must stay joinable meanwhile
|
|
780
|
+
clearInterval(sweptCheckInterval);
|
|
781
|
+
|
|
777
782
|
// make sure all rooms are disposed
|
|
778
783
|
return Promise.all(disconnectAll(
|
|
779
784
|
(isDevMode)
|
|
@@ -1026,13 +1031,24 @@ export function healthCheckProcessId(processId: string) {
|
|
|
1026
1031
|
resolve(true)
|
|
1027
1032
|
|
|
1028
1033
|
} catch (e) {
|
|
1029
|
-
// process failed to respond - remove it from stats
|
|
1030
1034
|
logger.debug(`❌ Process '${processId}' failed to respond. Cleaning it up.`);
|
|
1031
|
-
await stats.excludeProcess(processId);
|
|
1032
1035
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
// stop placing new rooms on it. if it is still alive, it re-adds itself
|
|
1038
|
+
// the next time it persists its stats.
|
|
1039
|
+
await stats.excludeProcess(processId);
|
|
1040
|
+
|
|
1041
|
+
// clean-up possibly stale room ids
|
|
1042
|
+
if (!isDevMode) {
|
|
1043
|
+
await removeRoomsByProcessId(processId);
|
|
1044
|
+
|
|
1045
|
+
// receipt goes AFTER the removal: written before, a merely stalled
|
|
1046
|
+
// process could restore its listings ahead of the delete
|
|
1047
|
+
await presence.setex(getProcessSweptKey(processId), '1', SWEPT_RECEIPT_TTL);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
debugMatchMaking(e); // presence/driver failing must not leave the caller hanging
|
|
1036
1052
|
}
|
|
1037
1053
|
|
|
1038
1054
|
resolve(false);
|
|
@@ -1056,6 +1072,68 @@ async function removeRoomsByProcessId(processId: string) {
|
|
|
1056
1072
|
await driver.cleanup(processId);
|
|
1057
1073
|
}
|
|
1058
1074
|
|
|
1075
|
+
let sweptCheckInterval: NodeJS.Timeout | undefined;
|
|
1076
|
+
let isCheckingSwept = false;
|
|
1077
|
+
|
|
1078
|
+
function startSweptCheck() {
|
|
1079
|
+
clearInterval(sweptCheckInterval); // accept() may be called more than once
|
|
1080
|
+
|
|
1081
|
+
sweptCheckInterval = setInterval(async () => {
|
|
1082
|
+
if (isCheckingSwept) { return; } // a slow presence would stack them up
|
|
1083
|
+
isCheckingSwept = true;
|
|
1084
|
+
|
|
1085
|
+
try {
|
|
1086
|
+
await restoreIfSwept();
|
|
1087
|
+
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
debugMatchMaking(e); // a failed presence call must not crash the process
|
|
1090
|
+
|
|
1091
|
+
} finally {
|
|
1092
|
+
isCheckingSwept = false;
|
|
1093
|
+
}
|
|
1094
|
+
}, SWEPT_CHECK_INTERVAL);
|
|
1095
|
+
|
|
1096
|
+
sweptCheckInterval.unref(); // don't keep the process running just for this
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* A peer that failed to reach this process removed our room listings, and
|
|
1101
|
+
* left a receipt. If we are reading it, we were only stalled: re-create them.
|
|
1102
|
+
*/
|
|
1103
|
+
async function restoreIfSwept() {
|
|
1104
|
+
if (!await presence.exists(getProcessSweptKey())) { return; }
|
|
1105
|
+
|
|
1106
|
+
// consume before restoring: a removal landing mid-restore leaves a new receipt
|
|
1107
|
+
await presence.del(getProcessSweptKey());
|
|
1108
|
+
await restoreMissingRoomCaches();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Re-create the listings of rooms running on this process that are missing
|
|
1113
|
+
* from the driver.
|
|
1114
|
+
*
|
|
1115
|
+
* Peers remove a process's listings when they believe it is dead. If this
|
|
1116
|
+
* process was only stalled, its rooms are still running, and without their
|
|
1117
|
+
* listings nobody could join them anymore.
|
|
1118
|
+
*/
|
|
1119
|
+
export async function restoreMissingRoomCaches() {
|
|
1120
|
+
const roomIds = Object.keys(rooms);
|
|
1121
|
+
if (roomIds.length === 0) { return; }
|
|
1122
|
+
|
|
1123
|
+
// one lookup per room: 0.17 drivers have no batch lookup, and this only runs after a sweep
|
|
1124
|
+
const listed = await Promise.all(roomIds.map((roomId) => driver.has(roomId)));
|
|
1125
|
+
|
|
1126
|
+
const missing = roomIds
|
|
1127
|
+
.filter((_, i) => !listed[i])
|
|
1128
|
+
.map((roomId) => rooms[roomId])
|
|
1129
|
+
.filter((room) =>
|
|
1130
|
+
room?.['_internalState'] === RoomInternalState.CREATED // still here, not disposing
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
debugMatchMaking('restoring %s missing room listing(s)', missing.length);
|
|
1134
|
+
await Promise.all(missing.map((room) => driver.persist(room['_listing'], true)));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1059
1137
|
async function createRoomReferences(room: Room, init: boolean = false): Promise<boolean> {
|
|
1060
1138
|
rooms[room.roomId] = room;
|
|
1061
1139
|
|
|
@@ -1212,3 +1290,7 @@ function getConcurrencyHashKey(roomName: string) {
|
|
|
1212
1290
|
function getProcessChannel(id: string = processId) {
|
|
1213
1291
|
return `p:${id}`;
|
|
1214
1292
|
}
|
|
1293
|
+
|
|
1294
|
+
function getProcessSweptKey(id: string = processId) {
|
|
1295
|
+
return `swept:${id}`;
|
|
1296
|
+
}
|
|
@@ -106,6 +106,8 @@ export class LocalPresence implements Presence {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
public del(key: string) {
|
|
109
|
+
clearTimeout(this.timeouts[key]); // a pending expiry would wipe whatever is set next
|
|
110
|
+
delete this.timeouts[key];
|
|
109
111
|
delete this.keys[key];
|
|
110
112
|
delete this.data[key];
|
|
111
113
|
delete this.hash[key];
|
package/src/utils/Utils.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type RemoteRoomCallReturn<
|
|
|
44
44
|
export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);
|
|
45
45
|
export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
|
|
46
46
|
|
|
47
|
+
// a peer that removes this process's room listings leaves a receipt. this
|
|
48
|
+
// process looks for it every INTERVAL (ms) and restores them. the TTL (seconds)
|
|
49
|
+
// only bounds what dead processes leave behind.
|
|
50
|
+
export const SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5000);
|
|
51
|
+
export const SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);
|
|
52
|
+
|
|
47
53
|
export function generateId(length: number = 9) {
|
|
48
54
|
return nanoid(length);
|
|
49
55
|
}
|