@colyseus/core 0.17.19 → 0.17.21

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.
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/core/src/rooms/QueueRoom.ts
31
+ var QueueRoom_exports = {};
32
+ __export(QueueRoom_exports, {
33
+ QueueRoom: () => QueueRoom
34
+ });
35
+ module.exports = __toCommonJS(QueueRoom_exports);
36
+ var import_Room = require("../Room.cjs");
37
+ var matchMaker = __toESM(require("../MatchMaker.cjs"), 1);
38
+ var import_Debug = require("../Debug.cjs");
39
+ var import_ServerError = require("../errors/ServerError.cjs");
40
+ var import_shared_types = require("@colyseus/shared-types");
41
+ var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team");
42
+ var DEFAULT_COMPARE = (client, matchGroup) => {
43
+ const diff = Math.abs(client.rank - matchGroup.averageRank);
44
+ const diffRatio = diff / matchGroup.averageRank;
45
+ return diff < 10 || diffRatio <= 2;
46
+ };
47
+ var QueueRoom = class extends import_Room.Room {
48
+ constructor() {
49
+ super(...arguments);
50
+ this.maxPlayers = 4;
51
+ this.allowIncompleteGroups = false;
52
+ this.maxWaitingCycles = 15;
53
+ this.maxWaitingCyclesForPriority = 10;
54
+ /**
55
+ * Evaluate groups for each client at interval
56
+ */
57
+ this.cycleTickInterval = 1e3;
58
+ /**
59
+ * Groups of players per iteration
60
+ */
61
+ this.groups = [];
62
+ this.highPriorityGroups = [];
63
+ this.compare = DEFAULT_COMPARE;
64
+ this.onGroupReady = (group) => matchMaker.createRoom(this.matchRoomName, {});
65
+ this.messages = {
66
+ confirm: (client, _) => {
67
+ const queueData = client.userData;
68
+ if (queueData && queueData.group && typeof queueData.group.confirmed === "number") {
69
+ queueData.confirmed = true;
70
+ queueData.group.confirmed++;
71
+ client.leave();
72
+ }
73
+ }
74
+ };
75
+ }
76
+ onCreate(options) {
77
+ if (typeof options.maxWaitingCycles === "number") {
78
+ this.maxWaitingCycles = options.maxWaitingCycles;
79
+ }
80
+ if (typeof options.maxPlayers === "number") {
81
+ this.maxPlayers = options.maxPlayers;
82
+ }
83
+ if (typeof options.maxTeamSize === "number") {
84
+ this.maxTeamSize = options.maxTeamSize;
85
+ }
86
+ if (typeof options.allowIncompleteGroups !== "undefined") {
87
+ this.allowIncompleteGroups = options.allowIncompleteGroups;
88
+ }
89
+ if (typeof options.compare === "function") {
90
+ this.compare = options.compare;
91
+ }
92
+ if (typeof options.onGroupReady === "function") {
93
+ this.onGroupReady = options.onGroupReady;
94
+ }
95
+ if (options.matchRoomName) {
96
+ this.matchRoomName = options.matchRoomName;
97
+ } else {
98
+ throw new import_ServerError.ServerError(import_shared_types.ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
99
+ }
100
+ (0, import_Debug.debugMatchMaking)("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
101
+ this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
102
+ }
103
+ onJoin(client, options, auth) {
104
+ this.addToQueue(client, {
105
+ rank: options.rank,
106
+ teamId: options.teamId,
107
+ options
108
+ });
109
+ }
110
+ addToQueue(client, queueData) {
111
+ if (queueData.currentCycle === void 0) {
112
+ queueData.currentCycle = 0;
113
+ }
114
+ client.userData = queueData;
115
+ client.send("clients", 1);
116
+ }
117
+ createMatchGroup() {
118
+ const group = { clients: [], averageRank: 0 };
119
+ this.groups.push(group);
120
+ return group;
121
+ }
122
+ reassignMatchGroups() {
123
+ this.groups.length = 0;
124
+ this.highPriorityGroups.length = 0;
125
+ const sortedClients = this.clients.filter((client) => {
126
+ return client.userData && client.userData.group?.ready !== true;
127
+ }).sort((a, b) => {
128
+ return a.userData.rank - b.userData.rank;
129
+ });
130
+ if (typeof this.maxTeamSize === "number") {
131
+ this.redistributeTeams(sortedClients);
132
+ } else {
133
+ this.redistributeClients(sortedClients);
134
+ }
135
+ this.evaluateHighPriorityGroups();
136
+ this.processGroupsReady();
137
+ }
138
+ redistributeTeams(sortedClients) {
139
+ const teamsByID = {};
140
+ sortedClients.forEach((client) => {
141
+ const teamId = client.userData.teamId || DEFAULT_TEAM;
142
+ if (!teamsByID[teamId]) {
143
+ teamsByID[teamId] = { teamId, clients: [], averageRank: 0 };
144
+ }
145
+ teamsByID[teamId].averageRank += client.userData.rank;
146
+ teamsByID[teamId].clients.push(client);
147
+ });
148
+ let teams = Object.values(teamsByID).map((team) => {
149
+ team.averageRank /= team.clients.length;
150
+ return team;
151
+ }).sort((a, b) => {
152
+ return a.averageRank - b.averageRank;
153
+ });
154
+ do {
155
+ let currentGroup = this.createMatchGroup();
156
+ teams = teams.filter((team) => {
157
+ const totalRank = team.averageRank * team.clients.length;
158
+ currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);
159
+ if (team.clients.length >= this.maxTeamSize) {
160
+ return true;
161
+ }
162
+ team.clients.forEach((client) => client.userData.currentCycle++);
163
+ return false;
164
+ });
165
+ } while (teams.length >= 2);
166
+ }
167
+ redistributeClients(sortedClients, currentGroup = this.createMatchGroup(), totalRank = 0) {
168
+ for (let i = 0, l = sortedClients.length; i < l; i++) {
169
+ const client = sortedClients[i];
170
+ const userData = client.userData;
171
+ const currentCycle = userData.currentCycle++;
172
+ if (currentGroup.averageRank > 0) {
173
+ if (!this.compare(userData, currentGroup) && !userData.highPriority) {
174
+ currentGroup = this.createMatchGroup();
175
+ totalRank = 0;
176
+ }
177
+ }
178
+ userData.group = currentGroup;
179
+ currentGroup.clients.push(client);
180
+ totalRank += userData.rank;
181
+ currentGroup.averageRank = totalRank / currentGroup.clients.length;
182
+ if (currentGroup.clients.length === this.maxPlayers) {
183
+ currentGroup.ready = true;
184
+ currentGroup = this.createMatchGroup();
185
+ totalRank = 0;
186
+ continue;
187
+ }
188
+ if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {
189
+ if (this.highPriorityGroups.indexOf(currentGroup) === -1) {
190
+ this.highPriorityGroups.push(currentGroup);
191
+ }
192
+ } else if (this.maxWaitingCyclesForPriority !== void 0 && currentCycle >= this.maxWaitingCyclesForPriority) {
193
+ userData.highPriority = true;
194
+ }
195
+ }
196
+ return currentGroup;
197
+ }
198
+ evaluateHighPriorityGroups() {
199
+ this.highPriorityGroups.forEach((group) => {
200
+ group.ready = group.clients.every((c) => {
201
+ return c.userData?.currentCycle > 1;
202
+ });
203
+ });
204
+ }
205
+ processGroupsReady() {
206
+ this.groups.forEach(async (group) => {
207
+ if (group.ready) {
208
+ group.confirmed = 0;
209
+ try {
210
+ const room = await this.onGroupReady.call(this, group);
211
+ await matchMaker.reserveMultipleSeatsFor(
212
+ room,
213
+ group.clients.map((client) => ({
214
+ sessionId: client.sessionId,
215
+ options: client.userData.options,
216
+ auth: client.auth
217
+ }))
218
+ );
219
+ group.clients.forEach((client, i) => {
220
+ client.send("seat", matchMaker.buildSeatReservation(room, client.sessionId));
221
+ });
222
+ } catch (e) {
223
+ group.clients.forEach((client) => client.leave(1011, e.message));
224
+ }
225
+ } else {
226
+ group.clients.forEach((client) => {
227
+ const queueClientCount = group.clients.length;
228
+ if (client.userData.lastQueueClientCount !== queueClientCount) {
229
+ client.userData.lastQueueClientCount = queueClientCount;
230
+ client.send("clients", queueClientCount);
231
+ }
232
+ });
233
+ }
234
+ });
235
+ }
236
+ };
237
+ // Annotate the CommonJS export names for ESM import in node:
238
+ 0 && (module.exports = {
239
+ QueueRoom
240
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/rooms/QueueRoom.ts"],
4
+ "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,127 @@
1
+ import { Room } from '../Room.ts';
2
+ import type { Client } from '../Transport.ts';
3
+ import type { IRoomCache } from '../matchmaker/driver.ts';
4
+ export interface QueueOptions {
5
+ /**
6
+ * number of players on each match
7
+ */
8
+ maxPlayers?: number;
9
+ /**
10
+ * name of the room to create
11
+ */
12
+ matchRoomName: string;
13
+ /**
14
+ * after these cycles, create a match with a bot
15
+ */
16
+ maxWaitingCycles?: number;
17
+ /**
18
+ * after this time, try to fit this client with a not-so-compatible group
19
+ */
20
+ maxWaitingCyclesForPriority?: number;
21
+ /**
22
+ * If set, teams must have the same size to be matched together
23
+ */
24
+ maxTeamSize?: number;
25
+ /**
26
+ * If `allowIncompleteGroups` is true, players inside an unmatched group (that
27
+ * did not reached `maxPlayers`, and `maxWaitingCycles` has been
28
+ * reached) will be matched together. Your room should fill the remaining
29
+ * spots with "bots" on this case.
30
+ */
31
+ allowIncompleteGroups?: boolean;
32
+ /**
33
+ * Comparison function for matching clients to groups
34
+ * Returns true if the client is compatible with the group
35
+ */
36
+ compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;
37
+ /**
38
+ *
39
+ * When onGroupReady is set, the "roomNameToCreate" option is ignored.
40
+ */
41
+ onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;
42
+ }
43
+ export interface QueueMatchGroup {
44
+ averageRank: number;
45
+ clients: Array<Client<{
46
+ userData: QueueClientData;
47
+ }>>;
48
+ ready?: boolean;
49
+ confirmed?: number;
50
+ }
51
+ export interface QueueMatchTeam {
52
+ averageRank: number;
53
+ clients: Array<Client<{
54
+ userData: QueueClientData;
55
+ }>>;
56
+ teamId: string | symbol;
57
+ }
58
+ export interface QueueClientData {
59
+ /**
60
+ * Rank of the client
61
+ */
62
+ rank: number;
63
+ /**
64
+ * Timestamp of when the client entered the queue
65
+ */
66
+ currentCycle?: number;
67
+ /**
68
+ * Optional: if matching with a team, the team ID
69
+ */
70
+ teamId?: string;
71
+ /**
72
+ * Additional options passed by the client when joining the room
73
+ */
74
+ options?: any;
75
+ /**
76
+ * Match group the client is currently in
77
+ */
78
+ group?: QueueMatchGroup;
79
+ /**
80
+ * Whether the client has confirmed the connection to the room
81
+ */
82
+ confirmed?: boolean;
83
+ /**
84
+ * Whether the client should be prioritized in the queue
85
+ * (e.g. for players that are waiting for a long time)
86
+ */
87
+ highPriority?: boolean;
88
+ /**
89
+ * The last number of clients in the queue sent to the client
90
+ */
91
+ lastQueueClientCount?: number;
92
+ }
93
+ export declare class QueueRoom extends Room {
94
+ maxPlayers: number;
95
+ maxTeamSize: number;
96
+ allowIncompleteGroups: boolean;
97
+ maxWaitingCycles: number;
98
+ maxWaitingCyclesForPriority?: number;
99
+ /**
100
+ * Evaluate groups for each client at interval
101
+ */
102
+ cycleTickInterval: number;
103
+ /**
104
+ * Groups of players per iteration
105
+ */
106
+ groups: QueueMatchGroup[];
107
+ highPriorityGroups: QueueMatchGroup[];
108
+ matchRoomName: string;
109
+ protected compare: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;
110
+ protected onGroupReady: (group: QueueMatchGroup) => Promise<IRoomCache<any>>;
111
+ messages: {
112
+ confirm: (client: Client, _: unknown) => void;
113
+ };
114
+ onCreate(options: QueueOptions): void;
115
+ onJoin(client: Client, options: any, auth?: unknown): void;
116
+ addToQueue(client: Client, queueData: QueueClientData): void;
117
+ createMatchGroup(): QueueMatchGroup;
118
+ reassignMatchGroups(): void;
119
+ redistributeTeams(sortedClients: Client<{
120
+ userData: QueueClientData;
121
+ }>[]): void;
122
+ redistributeClients(sortedClients: Client<{
123
+ userData: QueueClientData;
124
+ }>[], currentGroup?: QueueMatchGroup, totalRank?: number): QueueMatchGroup;
125
+ evaluateHighPriorityGroups(): void;
126
+ processGroupsReady(): void;
127
+ }
@@ -0,0 +1,205 @@
1
+ // packages/core/src/rooms/QueueRoom.ts
2
+ import { Room } from "../Room.mjs";
3
+ import * as matchMaker from "../MatchMaker.mjs";
4
+ import { debugMatchMaking } from "../Debug.mjs";
5
+ import { ServerError } from "../errors/ServerError.mjs";
6
+ import { ErrorCode } from "@colyseus/shared-types";
7
+ var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team");
8
+ var DEFAULT_COMPARE = (client, matchGroup) => {
9
+ const diff = Math.abs(client.rank - matchGroup.averageRank);
10
+ const diffRatio = diff / matchGroup.averageRank;
11
+ return diff < 10 || diffRatio <= 2;
12
+ };
13
+ var QueueRoom = class extends Room {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.maxPlayers = 4;
17
+ this.allowIncompleteGroups = false;
18
+ this.maxWaitingCycles = 15;
19
+ this.maxWaitingCyclesForPriority = 10;
20
+ /**
21
+ * Evaluate groups for each client at interval
22
+ */
23
+ this.cycleTickInterval = 1e3;
24
+ /**
25
+ * Groups of players per iteration
26
+ */
27
+ this.groups = [];
28
+ this.highPriorityGroups = [];
29
+ this.compare = DEFAULT_COMPARE;
30
+ this.onGroupReady = (group) => matchMaker.createRoom(this.matchRoomName, {});
31
+ this.messages = {
32
+ confirm: (client, _) => {
33
+ const queueData = client.userData;
34
+ if (queueData && queueData.group && typeof queueData.group.confirmed === "number") {
35
+ queueData.confirmed = true;
36
+ queueData.group.confirmed++;
37
+ client.leave();
38
+ }
39
+ }
40
+ };
41
+ }
42
+ onCreate(options) {
43
+ if (typeof options.maxWaitingCycles === "number") {
44
+ this.maxWaitingCycles = options.maxWaitingCycles;
45
+ }
46
+ if (typeof options.maxPlayers === "number") {
47
+ this.maxPlayers = options.maxPlayers;
48
+ }
49
+ if (typeof options.maxTeamSize === "number") {
50
+ this.maxTeamSize = options.maxTeamSize;
51
+ }
52
+ if (typeof options.allowIncompleteGroups !== "undefined") {
53
+ this.allowIncompleteGroups = options.allowIncompleteGroups;
54
+ }
55
+ if (typeof options.compare === "function") {
56
+ this.compare = options.compare;
57
+ }
58
+ if (typeof options.onGroupReady === "function") {
59
+ this.onGroupReady = options.onGroupReady;
60
+ }
61
+ if (options.matchRoomName) {
62
+ this.matchRoomName = options.matchRoomName;
63
+ } else {
64
+ throw new ServerError(ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
65
+ }
66
+ debugMatchMaking("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
67
+ this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
68
+ }
69
+ onJoin(client, options, auth) {
70
+ this.addToQueue(client, {
71
+ rank: options.rank,
72
+ teamId: options.teamId,
73
+ options
74
+ });
75
+ }
76
+ addToQueue(client, queueData) {
77
+ if (queueData.currentCycle === void 0) {
78
+ queueData.currentCycle = 0;
79
+ }
80
+ client.userData = queueData;
81
+ client.send("clients", 1);
82
+ }
83
+ createMatchGroup() {
84
+ const group = { clients: [], averageRank: 0 };
85
+ this.groups.push(group);
86
+ return group;
87
+ }
88
+ reassignMatchGroups() {
89
+ this.groups.length = 0;
90
+ this.highPriorityGroups.length = 0;
91
+ const sortedClients = this.clients.filter((client) => {
92
+ return client.userData && client.userData.group?.ready !== true;
93
+ }).sort((a, b) => {
94
+ return a.userData.rank - b.userData.rank;
95
+ });
96
+ if (typeof this.maxTeamSize === "number") {
97
+ this.redistributeTeams(sortedClients);
98
+ } else {
99
+ this.redistributeClients(sortedClients);
100
+ }
101
+ this.evaluateHighPriorityGroups();
102
+ this.processGroupsReady();
103
+ }
104
+ redistributeTeams(sortedClients) {
105
+ const teamsByID = {};
106
+ sortedClients.forEach((client) => {
107
+ const teamId = client.userData.teamId || DEFAULT_TEAM;
108
+ if (!teamsByID[teamId]) {
109
+ teamsByID[teamId] = { teamId, clients: [], averageRank: 0 };
110
+ }
111
+ teamsByID[teamId].averageRank += client.userData.rank;
112
+ teamsByID[teamId].clients.push(client);
113
+ });
114
+ let teams = Object.values(teamsByID).map((team) => {
115
+ team.averageRank /= team.clients.length;
116
+ return team;
117
+ }).sort((a, b) => {
118
+ return a.averageRank - b.averageRank;
119
+ });
120
+ do {
121
+ let currentGroup = this.createMatchGroup();
122
+ teams = teams.filter((team) => {
123
+ const totalRank = team.averageRank * team.clients.length;
124
+ currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);
125
+ if (team.clients.length >= this.maxTeamSize) {
126
+ return true;
127
+ }
128
+ team.clients.forEach((client) => client.userData.currentCycle++);
129
+ return false;
130
+ });
131
+ } while (teams.length >= 2);
132
+ }
133
+ redistributeClients(sortedClients, currentGroup = this.createMatchGroup(), totalRank = 0) {
134
+ for (let i = 0, l = sortedClients.length; i < l; i++) {
135
+ const client = sortedClients[i];
136
+ const userData = client.userData;
137
+ const currentCycle = userData.currentCycle++;
138
+ if (currentGroup.averageRank > 0) {
139
+ if (!this.compare(userData, currentGroup) && !userData.highPriority) {
140
+ currentGroup = this.createMatchGroup();
141
+ totalRank = 0;
142
+ }
143
+ }
144
+ userData.group = currentGroup;
145
+ currentGroup.clients.push(client);
146
+ totalRank += userData.rank;
147
+ currentGroup.averageRank = totalRank / currentGroup.clients.length;
148
+ if (currentGroup.clients.length === this.maxPlayers) {
149
+ currentGroup.ready = true;
150
+ currentGroup = this.createMatchGroup();
151
+ totalRank = 0;
152
+ continue;
153
+ }
154
+ if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {
155
+ if (this.highPriorityGroups.indexOf(currentGroup) === -1) {
156
+ this.highPriorityGroups.push(currentGroup);
157
+ }
158
+ } else if (this.maxWaitingCyclesForPriority !== void 0 && currentCycle >= this.maxWaitingCyclesForPriority) {
159
+ userData.highPriority = true;
160
+ }
161
+ }
162
+ return currentGroup;
163
+ }
164
+ evaluateHighPriorityGroups() {
165
+ this.highPriorityGroups.forEach((group) => {
166
+ group.ready = group.clients.every((c) => {
167
+ return c.userData?.currentCycle > 1;
168
+ });
169
+ });
170
+ }
171
+ processGroupsReady() {
172
+ this.groups.forEach(async (group) => {
173
+ if (group.ready) {
174
+ group.confirmed = 0;
175
+ try {
176
+ const room = await this.onGroupReady.call(this, group);
177
+ await matchMaker.reserveMultipleSeatsFor(
178
+ room,
179
+ group.clients.map((client) => ({
180
+ sessionId: client.sessionId,
181
+ options: client.userData.options,
182
+ auth: client.auth
183
+ }))
184
+ );
185
+ group.clients.forEach((client, i) => {
186
+ client.send("seat", matchMaker.buildSeatReservation(room, client.sessionId));
187
+ });
188
+ } catch (e) {
189
+ group.clients.forEach((client) => client.leave(1011, e.message));
190
+ }
191
+ } else {
192
+ group.clients.forEach((client) => {
193
+ const queueClientCount = group.clients.length;
194
+ if (client.userData.lastQueueClientCount !== queueClientCount) {
195
+ client.userData.lastQueueClientCount = queueClientCount;
196
+ client.send("clients", queueClientCount);
197
+ }
198
+ });
199
+ }
200
+ });
201
+ }
202
+ };
203
+ export {
204
+ QueueRoom
205
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/rooms/QueueRoom.ts"],
4
+ "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -88,9 +88,9 @@ function bindRouterToTransport(transport, router) {
88
88
  });
89
89
  }
90
90
  function expressRootRoute(expressApp) {
91
- const stack = expressApp?.router?.stack ?? expressApp?._router?.stack;
91
+ const stack = expressApp?._router?.stack ?? expressApp?.router?.stack;
92
92
  if (!stack) {
93
- throw new Error("Express app is not initialized");
93
+ return false;
94
94
  }
95
95
  return stack.find((layer) => layer.match("/") && !["query", "expressInit"].includes(layer.name));
96
96
  }