@colyseus/core 0.18.16 → 0.18.18
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 +93 -10
- package/build/MatchMaker.cjs.map +3 -3
- package/build/MatchMaker.d.ts +20 -1
- package/build/MatchMaker.mjs +93 -11
- package/build/MatchMaker.mjs.map +3 -3
- package/build/Room.cjs +69 -68
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +69 -68
- package/build/Room.mjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +14 -1
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +2 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +14 -1
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +11 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- 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/DevMode.cjs +3 -0
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.mjs +4 -1
- package/build/utils/DevMode.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 +3 -3
- package/src/MatchMaker.ts +174 -16
- package/src/Room.ts +72 -30
- package/src/matchmaker/LocalDriver/LocalDriver.ts +15 -7
- package/src/matchmaker/driver.ts +12 -0
- package/src/presence/LocalPresence.ts +2 -0
- package/src/utils/DevMode.ts +4 -1
- package/src/utils/Utils.ts +6 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/DevMode.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\n// presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto\nconst nullProto = (_: string, value: any) =>\n (value && typeof value === 'object' && !Array.isArray(value))\n ? Object.assign(Object.create(null), value)\n : value;\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAuF,gBAAgB;AACvG,SAAS,cAAc;AACvB,SAAS,oBAAoB,oBAAoB;AACjD,SAAS,kBAAkB,kBAAkB,UAAU,
|
|
4
|
+
"sourcesContent": ["import fs from 'fs';\nimport path from 'path';\nimport { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';\nimport { logger } from '../Logger.ts';\nimport { debugAndPrintError, debugDevMode } from '../Debug.ts';\nimport { getLocalRoomById, handleCreateRoom, MatchMakerState, presence, remoteRoomCall, state } from '../MatchMaker.ts';\nimport type { Room } from '../Room.ts';\n\nconst DEVMODE_CACHE_FILE_PATH = path.resolve(\".devmode.json\");\n\nexport let isDevMode: boolean = false;\n\nexport function hasDevModeCache() {\n return fs.existsSync(DEVMODE_CACHE_FILE_PATH);\n}\n\n// presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto\nconst nullProto = (_: string, value: any) =>\n (value && typeof value === 'object' && !Array.isArray(value))\n ? Object.assign(Object.create(null), value)\n : value;\n\nexport function getDevModeCache() {\n return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};\n}\n\nexport function writeDevModeCache(cache: any) {\n fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');\n}\n\nexport function setDevMode(bool: boolean) {\n isDevMode = bool;\n}\n\nexport async function reloadFromCache() {\n const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));\n debugDevMode(\"rooms to restore: %i\", roomHistoryList.length);\n\n for (const [roomId, value] of roomHistoryList) {\n let roomHistory: any;\n let recreatedRoom: Room;\n let recreatedRoomListing: any;\n\n try {\n roomHistory = JSON.parse(value);\n debugDevMode(\"restoring room %s (%s)\", roomHistory.roomName, roomId);\n\n recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);\n recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);\n\n } catch (e: any) {\n // shutdown interrupted the restore: keep the remaining entries for the next boot\n if (state === MatchMakerState.SHUTTING_DOWN) { break; }\n\n // unrestorable entry (e.g. room type renamed/removed) \u2014 drop it\n // instead of wedging the boot\n debugAndPrintError(`\u274C couldn't restore room '${roomId}':\\n${e.stack}`);\n await presence.hdel(getRoomRestoreListKey(), roomId);\n continue;\n }\n\n // Restore previous state\n if (roomHistory.hasOwnProperty(\"state\")) {\n try {\n const rawState = JSON.parse(roomHistory.state);\n logger.debug(`\uD83D\uDCCB room '${roomId}' state =>`, rawState);\n\n (recreatedRoom.state as Schema).restore(rawState);\n\n // Restore the encoder's nextUniqueId so refIds increase\n // monotonically across HMR cycles. Without this, restore()\n // always produces the same refIds (0,1,2,3...) and onJoin()\n // always assigns the same next refIds (4,5...), causing the\n // client decoder to reuse stale instances on the 2nd+ cycle.\n if (roomHistory.nextRefId !== undefined) {\n const encoderRoot = recreatedRoom.state[$changes]?.root;\n if (encoderRoot && roomHistory.nextRefId > encoderRoot['nextUniqueId']) {\n encoderRoot['nextUniqueId'] = roomHistory.nextRefId;\n }\n }\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't restore room '${roomId}' state:\\n${e.stack}`);\n }\n }\n\n // Reserve seats for clients from cached history.\n // Skip entries without a reconnectionToken \u2014 these are stale\n // seats from allowReconnection() where the client already left\n // (e.g. page refresh). Restoring them would block room disposal.\n if (roomHistory.clients) {\n for (const clientData of roomHistory.clients) {\n const { sessionId, reconnectionToken } = clientData;\n if (!reconnectionToken) { continue; }\n // TODO: need to restore each client's StateView as well\n await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, recreatedRoom.seatReservationTimeout, false, reconnectionToken]);\n }\n }\n\n // call `onRestoreRoom` with custom 'cache'd property.\n recreatedRoom.onRestoreRoom?.(roomHistory[\"cache\"]);\n\n logger.debug(`\uD83D\uDD04 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(\", \")}`);\n }\n\n if (roomHistoryList.length > 0) {\n logger.debug(\"\u2705\", roomHistoryList.length, \"room(s) have been restored.\");\n }\n}\n\nexport async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {\n for (const room of Object.values(rooms)) {\n const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);\n if (roomHistoryResult) {\n try {\n const roomHistory = JSON.parse(roomHistoryResult);\n\n // custom cache method\n roomHistory[\"cache\"] = room.onCacheRoom?.();\n\n // encode state\n debugDevMode(\"caching room %s (%s)\", room.roomName, room.roomId);\n\n if (room.state) {\n roomHistory[\"state\"] = JSON.stringify(room.state);\n\n // Cache the encoder's nextUniqueId so it can be restored.\n // This ensures refIds increase monotonically across HMR cycles,\n // preventing the client decoder from reusing stale refs that\n // happen to have the same refId as newly created instances.\n const encoderRoot = room.state[$changes]?.root;\n if (encoderRoot) {\n roomHistory[\"nextRefId\"] = encoderRoot['nextUniqueId'];\n }\n }\n\n // cache active clients with their reconnection tokens\n // TODO: need to cache each client's StateView as well\n const activeClients = room.clients.map((client) => ({\n sessionId: client.sessionId,\n reconnectionToken: client.reconnectionToken,\n }));\n\n // collect active client sessionIds to avoid duplicates\n const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));\n\n // also cache reserved seats (they don't have reconnectionTokens yet)\n // filter out reserved seats that are already active clients (from devMode reconnection)\n const reservedSeats = Object.keys(room['_reservedSeats'])\n .filter((sessionId) => !activeSessionIds.has(sessionId))\n .map((sessionId) => ({\n sessionId,\n reconnectionToken: undefined,\n }));\n\n roomHistory[\"clients\"] = activeClients.concat(reservedSeats);\n\n await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));\n\n // Rewrite updated room history\n logger.debug(`\uD83D\uDCBE caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory[\"state\"] !== undefined ? \"yes\" : \"no\"})`);\n\n } catch (e: any) {\n debugAndPrintError(`\u274C couldn't cache room '${room.roomId}', due to:\\n${e.stack}`);\n }\n }\n }\n}\n\nexport function getRoomRestoreListKey() {\n return 'roomhistory';\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/utils/Utils.cjs
CHANGED
|
@@ -25,6 +25,8 @@ __export(Utils_exports, {
|
|
|
25
25
|
Deferred: () => Deferred,
|
|
26
26
|
MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME: () => MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME,
|
|
27
27
|
REMOTE_ROOM_SHORT_TIMEOUT: () => REMOTE_ROOM_SHORT_TIMEOUT,
|
|
28
|
+
SWEPT_CHECK_INTERVAL: () => SWEPT_CHECK_INTERVAL,
|
|
29
|
+
SWEPT_RECEIPT_TTL: () => SWEPT_RECEIPT_TTL,
|
|
28
30
|
dynamicImport: () => dynamicImport,
|
|
29
31
|
generateId: () => generateId,
|
|
30
32
|
getBearerToken: () => getBearerToken,
|
|
@@ -40,6 +42,8 @@ var import_RoomExceptions = require("../errors/RoomExceptions.cjs");
|
|
|
40
42
|
var import_Debug = require("../Debug.cjs");
|
|
41
43
|
var REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2e3);
|
|
42
44
|
var MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
|
|
45
|
+
var SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5e3);
|
|
46
|
+
var SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);
|
|
43
47
|
var $METADATA = Symbol.metadata ?? /* @__PURE__ */ Symbol.for("Symbol.metadata");
|
|
44
48
|
function generateId(length = 9) {
|
|
45
49
|
return (0, import_nanoid.nanoid)(length);
|
|
@@ -159,6 +163,8 @@ function dynamicImport(moduleName) {
|
|
|
159
163
|
Deferred,
|
|
160
164
|
MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME,
|
|
161
165
|
REMOTE_ROOM_SHORT_TIMEOUT,
|
|
166
|
+
SWEPT_CHECK_INTERVAL,
|
|
167
|
+
SWEPT_RECEIPT_TTL,
|
|
162
168
|
dynamicImport,
|
|
163
169
|
generateId,
|
|
164
170
|
getBearerToken,
|
|
@@ -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\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuB;AACvB,4BAAwD;AAExD,mBAAqD;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\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\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/build/utils/Utils.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type ExtractMethodOrPropertyType<TClass, TKey extends keyof TClass> = TCl
|
|
|
25
25
|
export type RemoteRoomCallReturn<TRoom, TMethod extends keyof TRoom> = keyof TRoom extends TMethod ? any : ExtractMethodOrPropertyType<TRoom, TMethod>;
|
|
26
26
|
export declare const REMOTE_ROOM_SHORT_TIMEOUT: number;
|
|
27
27
|
export declare const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME: number;
|
|
28
|
+
export declare const SWEPT_CHECK_INTERVAL: number;
|
|
29
|
+
export declare const SWEPT_RECEIPT_TTL: number;
|
|
28
30
|
/**
|
|
29
31
|
* The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the
|
|
30
32
|
* registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`
|
package/build/utils/Utils.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import "../errors/RoomExceptions.mjs";
|
|
|
11
11
|
import { debugAndPrintError, debugMatchMaking } from "../Debug.mjs";
|
|
12
12
|
var REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2e3);
|
|
13
13
|
var MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
|
|
14
|
+
var SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5e3);
|
|
15
|
+
var SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);
|
|
14
16
|
var $METADATA = Symbol.metadata ?? /* @__PURE__ */ Symbol.for("Symbol.metadata");
|
|
15
17
|
function generateId(length = 9) {
|
|
16
18
|
return nanoid(length);
|
|
@@ -129,6 +131,8 @@ export {
|
|
|
129
131
|
Deferred,
|
|
130
132
|
MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME,
|
|
131
133
|
REMOTE_ROOM_SHORT_TIMEOUT,
|
|
134
|
+
SWEPT_CHECK_INTERVAL,
|
|
135
|
+
SWEPT_RECEIPT_TTL,
|
|
132
136
|
dynamicImport,
|
|
133
137
|
generateId,
|
|
134
138
|
getBearerToken,
|
|
@@ -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\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\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\n/**\n * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the\n * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`\n * stores each class's field metadata under this key \u2014 read it via\n * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL\n * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).\n */\nexport const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for(\"Symbol.metadata\");\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.18.
|
|
3
|
+
"version": "0.18.18",
|
|
4
4
|
"description": "Multiplayer Framework for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"input": "./src/index.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"vitest": "^3.1.1",
|
|
64
64
|
"@colyseus/redis-presence": "^0.18.5",
|
|
65
65
|
"@colyseus/tools": "^0.18.4",
|
|
66
|
-
"@colyseus/redis-driver": "^0.18.
|
|
66
|
+
"@colyseus/redis-driver": "^0.18.4"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@colyseus/schema": "^5.0.8",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"zod": "^4.1.12",
|
|
73
73
|
"@colyseus/auth": "^0.18.3",
|
|
74
74
|
"@colyseus/better-call": "^1.3.1",
|
|
75
|
-
"@colyseus/ws-transport": "^0.18.
|
|
75
|
+
"@colyseus/ws-transport": "^0.18.4"
|
|
76
76
|
},
|
|
77
77
|
"peerDependenciesMeta": {
|
|
78
78
|
"@colyseus/auth": {
|
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, getRoomRestoreListKey, reloadFromCache } from './utils/DevMode.ts';
|
|
7
7
|
|
|
8
8
|
import { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
|
|
@@ -182,6 +182,8 @@ export async function accept(isStandalone: boolean = false) {
|
|
|
182
182
|
* (`stats.excludeProcess()`) by mistake due to health-check failure
|
|
183
183
|
*/
|
|
184
184
|
stats.setAutoPersistInterval();
|
|
185
|
+
|
|
186
|
+
startSweptCheck();
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
state = MatchMakerState.READY;
|
|
@@ -530,13 +532,27 @@ export async function createRoom(roomName: string, clientOptions: ClientOptions)
|
|
|
530
532
|
room = await handleCreateRoom(roomName, clientOptions);
|
|
531
533
|
|
|
532
534
|
} else {
|
|
535
|
+
// pre-assign the roomId so a late remote answer collides with our fallback room instead of duplicating it
|
|
536
|
+
const roomId = driver.insert ? generateId() : undefined;
|
|
537
|
+
|
|
538
|
+
//
|
|
539
|
+
// TODO: on 1.0, drop the shape that keeps this working across 0.18 versions:
|
|
540
|
+
// - name this request and send one options object, instead of a positional
|
|
541
|
+
// list that 0.18 receivers `.apply()` onto handleCreateRoom()
|
|
542
|
+
// - `presetRoomId` then joins `restoringRoomId` in that object, and the
|
|
543
|
+
// empty slot at index 2 below goes away
|
|
544
|
+
// - require `MatchMakerDriver.insert()` and drop `persist()`'s `create`
|
|
545
|
+
// flag, so the roomId is always pre-assigned (see `recordRoom`)
|
|
546
|
+
//
|
|
547
|
+
|
|
533
548
|
// ask other process to create the room!
|
|
534
549
|
try {
|
|
535
550
|
room = await requestFromIPC<IRoomCache>(
|
|
536
551
|
presence,
|
|
537
552
|
getProcessChannel(selectedProcessId),
|
|
538
553
|
undefined,
|
|
539
|
-
|
|
554
|
+
// index 2 is restoringRoomId (see handleCreateRoom)
|
|
555
|
+
[roomName, clientOptions, undefined, roomId],
|
|
540
556
|
REMOTE_ROOM_SHORT_TIMEOUT,
|
|
541
557
|
);
|
|
542
558
|
|
|
@@ -554,7 +570,7 @@ export async function createRoom(roomName: string, clientOptions: ClientOptions)
|
|
|
554
570
|
}
|
|
555
571
|
|
|
556
572
|
// if other process failed to respond, create the room on this process
|
|
557
|
-
room = await handleCreateRoom(roomName, clientOptions);
|
|
573
|
+
room = await handleCreateRoom(roomName, clientOptions, undefined, roomId);
|
|
558
574
|
|
|
559
575
|
} else {
|
|
560
576
|
// re-throw intentional exception thrown during remote onCreate()
|
|
@@ -574,7 +590,22 @@ export async function createRoom(roomName: string, clientOptions: ClientOptions)
|
|
|
574
590
|
return room;
|
|
575
591
|
}
|
|
576
592
|
|
|
577
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Create a room on this process.
|
|
595
|
+
*
|
|
596
|
+
* @param restoringRoomId - devMode restore: reuse this roomId, dropping any stale cache row.
|
|
597
|
+
* @param presetRoomId - roomId chosen by the process that requested this room. Stays at
|
|
598
|
+
* argument index 3, because receivers `.apply()` the IPC arguments positionally and
|
|
599
|
+
* index 2 is `restoringRoomId`.
|
|
600
|
+
* @returns the room's cache entry, which belongs to another process if it recorded
|
|
601
|
+
* this roomId first.
|
|
602
|
+
*/
|
|
603
|
+
export async function handleCreateRoom(
|
|
604
|
+
roomName: string,
|
|
605
|
+
clientOptions: ClientOptions,
|
|
606
|
+
restoringRoomId?: string,
|
|
607
|
+
presetRoomId?: string,
|
|
608
|
+
): Promise<IRoomCache> {
|
|
578
609
|
const handler = getHandler(roomName);
|
|
579
610
|
const room: Room = new handler.klass();
|
|
580
611
|
|
|
@@ -585,7 +616,7 @@ export async function handleCreateRoom(roomName: string, clientOptions: ClientOp
|
|
|
585
616
|
room.roomId = restoringRoomId;
|
|
586
617
|
|
|
587
618
|
} else {
|
|
588
|
-
room.roomId = generateId();
|
|
619
|
+
room.roomId = presetRoomId || generateId();
|
|
589
620
|
}
|
|
590
621
|
|
|
591
622
|
//
|
|
@@ -619,8 +650,7 @@ export async function handleCreateRoom(roomName: string, clientOptions: ClientOp
|
|
|
619
650
|
} catch (e: any) {
|
|
620
651
|
debugAndPrintError(e);
|
|
621
652
|
|
|
622
|
-
|
|
623
|
-
room['_events'].emit('dispose');
|
|
653
|
+
discardRoom(room);
|
|
624
654
|
|
|
625
655
|
throw new ServerError(
|
|
626
656
|
e.code || ErrorCode.MATCHMAKE_UNHANDLED,
|
|
@@ -634,6 +664,15 @@ export async function handleCreateRoom(roomName: string, clientOptions: ClientOp
|
|
|
634
664
|
room['_listing'].roomId = room.roomId;
|
|
635
665
|
room['_listing'].maxClients = room.maxClients;
|
|
636
666
|
|
|
667
|
+
releaseIfShuttingDown(room); // shutdown began during onCreate()
|
|
668
|
+
|
|
669
|
+
const owner = await recordRoom(room);
|
|
670
|
+
|
|
671
|
+
// another process got this roomId first, and `room` has been discarded
|
|
672
|
+
if (owner) { return owner; }
|
|
673
|
+
|
|
674
|
+
releaseIfShuttingDown(room); // ...or during the write
|
|
675
|
+
|
|
637
676
|
// imediatelly ask client to join the room
|
|
638
677
|
debugMatchMaking('creating room \'%s\', roomId: \'%s\', processId: \'%s\'', roomName, room.roomId, processId);
|
|
639
678
|
|
|
@@ -678,14 +717,55 @@ export async function handleCreateRoom(roomName: string, clientOptions: ClientOp
|
|
|
678
717
|
// room always start unlocked
|
|
679
718
|
await createRoomReferences(room, true);
|
|
680
719
|
|
|
681
|
-
|
|
682
|
-
|
|
720
|
+
handler.emit('create', room);
|
|
721
|
+
|
|
722
|
+
return room['_listing'];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Record a room in the cache. Returns another process's cache entry when it
|
|
727
|
+
* recorded this roomId first — `room` is discarded in that case. Throws if
|
|
728
|
+
* that entry is already gone.
|
|
729
|
+
*/
|
|
730
|
+
async function recordRoom(room: Room): Promise<IRoomCache | undefined> {
|
|
731
|
+
// older drivers overwrite a known roomId, so there is nothing to arbitrate
|
|
732
|
+
// TODO: require insert() on 1.0 and delete this branch
|
|
733
|
+
if (!driver.insert) {
|
|
683
734
|
await driver.persist(room['_listing'], true);
|
|
735
|
+
return;
|
|
684
736
|
}
|
|
685
737
|
|
|
686
|
-
|
|
738
|
+
if (await driver.insert(room['_listing'])) { return; }
|
|
687
739
|
|
|
688
|
-
|
|
740
|
+
debugMatchMaking('discarding duplicate room \'%s\' (%s)', room.roomName, room.roomId);
|
|
741
|
+
room['_listing'].roomId = undefined; // the row is the winner's: keep disposal off it
|
|
742
|
+
discardRoom(room);
|
|
743
|
+
|
|
744
|
+
const owner = await driver.findOne({ roomId: room.roomId });
|
|
745
|
+
if (!owner) {
|
|
746
|
+
throw new SeatReservationError(`room ${room.roomId} is no longer available.`); // the winner is already gone
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return owner;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Shutdown can't see a room that isn't counted or registered yet, so one still
|
|
754
|
+
* being created when shutdown starts would outlive it. Release it instead.
|
|
755
|
+
*/
|
|
756
|
+
function releaseIfShuttingDown(room: Room) {
|
|
757
|
+
if (state !== MatchMakerState.SHUTTING_DOWN) { return; }
|
|
758
|
+
|
|
759
|
+
discardRoom(room); // removes our cache row too, if it was written
|
|
760
|
+
throw new ServerError(ErrorCode.MATCHMAKE_UNHANDLED, `room '${room.roomName}' not created: shutting down`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Release a room that never went live. Only the room's own timers and presence
|
|
765
|
+
* are attached at this point, so there is no matchMaker bookkeeping to undo.
|
|
766
|
+
*/
|
|
767
|
+
function discardRoom(room: Room) {
|
|
768
|
+
room['_events'].emit('dispose');
|
|
689
769
|
}
|
|
690
770
|
|
|
691
771
|
/**
|
|
@@ -801,6 +881,9 @@ export async function gracefullyShutdown(): Promise<any> {
|
|
|
801
881
|
// unsubscribe from process id channel
|
|
802
882
|
presence.unsubscribe(getProcessChannel());
|
|
803
883
|
|
|
884
|
+
// only now: rooms may take minutes to drain, and must stay joinable meanwhile
|
|
885
|
+
clearInterval(sweptCheckInterval);
|
|
886
|
+
|
|
804
887
|
// make sure all rooms are disposed
|
|
805
888
|
return Promise.all(disconnectAll(
|
|
806
889
|
(isDevMode)
|
|
@@ -1073,13 +1156,23 @@ export function healthCheckProcessId(processId: string) {
|
|
|
1073
1156
|
resolve(true)
|
|
1074
1157
|
|
|
1075
1158
|
} catch (e) {
|
|
1076
|
-
// process failed to respond - remove it from stats
|
|
1077
1159
|
logger.debug(`❌ Process '${processId}' failed to respond. Cleaning it up.`);
|
|
1078
|
-
await stats.excludeProcess(processId);
|
|
1079
1160
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1161
|
+
try {
|
|
1162
|
+
// stop placing new rooms on it. if it is still alive, it re-adds itself
|
|
1163
|
+
// the next time it persists its stats.
|
|
1164
|
+
await stats.excludeProcess(processId);
|
|
1165
|
+
|
|
1166
|
+
// devMode restore reads 'roomhistory' (presence), never these listings
|
|
1167
|
+
await removeRoomsByProcessId(processId);
|
|
1168
|
+
|
|
1169
|
+
// receipt goes AFTER the removal: written before, a merely stalled
|
|
1170
|
+
// process could restore its listings ahead of the delete
|
|
1171
|
+
await presence.setex(getProcessSweptKey(processId), '1', SWEPT_RECEIPT_TTL);
|
|
1172
|
+
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
debugMatchMaking(e); // presence/driver failing must not leave the caller hanging
|
|
1175
|
+
}
|
|
1083
1176
|
|
|
1084
1177
|
resolve(false);
|
|
1085
1178
|
} finally {
|
|
@@ -1102,6 +1195,67 @@ async function removeRoomsByProcessId(processId: string) {
|
|
|
1102
1195
|
await driver.cleanup(processId);
|
|
1103
1196
|
}
|
|
1104
1197
|
|
|
1198
|
+
let sweptCheckInterval: NodeJS.Timeout | undefined;
|
|
1199
|
+
let isCheckingSwept = false;
|
|
1200
|
+
|
|
1201
|
+
function startSweptCheck() {
|
|
1202
|
+
clearInterval(sweptCheckInterval); // accept() may be called more than once
|
|
1203
|
+
|
|
1204
|
+
sweptCheckInterval = setInterval(async () => {
|
|
1205
|
+
if (isCheckingSwept) { return; } // a slow presence would stack them up
|
|
1206
|
+
isCheckingSwept = true;
|
|
1207
|
+
|
|
1208
|
+
try {
|
|
1209
|
+
await restoreIfSwept();
|
|
1210
|
+
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
debugMatchMaking(e); // a failed presence call must not crash the process
|
|
1213
|
+
|
|
1214
|
+
} finally {
|
|
1215
|
+
isCheckingSwept = false;
|
|
1216
|
+
}
|
|
1217
|
+
}, SWEPT_CHECK_INTERVAL);
|
|
1218
|
+
|
|
1219
|
+
sweptCheckInterval.unref(); // don't keep the process running just for this
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* A peer that failed to reach this process removed our room listings, and
|
|
1224
|
+
* left a receipt. If we are reading it, we were only stalled: re-create them.
|
|
1225
|
+
*/
|
|
1226
|
+
async function restoreIfSwept() {
|
|
1227
|
+
if (!await presence.exists(getProcessSweptKey())) { return; }
|
|
1228
|
+
|
|
1229
|
+
// consume before restoring: a removal landing mid-restore leaves a new receipt
|
|
1230
|
+
await presence.del(getProcessSweptKey());
|
|
1231
|
+
await restoreMissingRoomCaches();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Re-create the listings of rooms running on this process that are missing
|
|
1236
|
+
* from the driver.
|
|
1237
|
+
*
|
|
1238
|
+
* Peers remove a process's listings when they believe it is dead. If this
|
|
1239
|
+
* process was only stalled, its rooms are still running, and without their
|
|
1240
|
+
* listings nobody could join them anymore.
|
|
1241
|
+
*/
|
|
1242
|
+
export async function restoreMissingRoomCaches() {
|
|
1243
|
+
const roomIds = Object.keys(rooms);
|
|
1244
|
+
if (roomIds.length === 0) { return; }
|
|
1245
|
+
|
|
1246
|
+
const cached = await driver.findByIds(roomIds);
|
|
1247
|
+
|
|
1248
|
+
const missing = roomIds
|
|
1249
|
+
.map((roomId) => rooms[roomId])
|
|
1250
|
+
.filter((room) =>
|
|
1251
|
+
room?.['_internalState'] === RoomInternalState.CREATED && // still here, not disposing
|
|
1252
|
+
!cached.has(room.roomId)
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
debugMatchMaking('restoring %s missing room listing(s)', missing.length);
|
|
1256
|
+
await Promise.all(missing.map((room) => driver.persist(room['_listing'], true)));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1105
1259
|
async function createRoomReferences(room: Room, init: boolean = false): Promise<boolean> {
|
|
1106
1260
|
rooms[room.roomId] = room;
|
|
1107
1261
|
|
|
@@ -1261,3 +1415,7 @@ function getConcurrencyHashKey(roomName: string) {
|
|
|
1261
1415
|
function getProcessChannel(id: string = processId) {
|
|
1262
1416
|
return `p:${id}`;
|
|
1263
1417
|
}
|
|
1418
|
+
|
|
1419
|
+
function getProcessSweptKey(id: string = processId) {
|
|
1420
|
+
return `swept:${id}`;
|
|
1421
|
+
}
|