@colyseus/core 0.16.0-preview.3 → 0.16.0-preview.31

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.
Files changed (141) hide show
  1. package/build/Debug.js +6 -2
  2. package/build/Debug.js.map +2 -2
  3. package/build/Debug.mjs +11 -10
  4. package/build/Debug.mjs.map +2 -2
  5. package/build/IPC.d.ts +1 -1
  6. package/build/IPC.js +3 -3
  7. package/build/IPC.js.map +2 -2
  8. package/build/IPC.mjs +4 -3
  9. package/build/IPC.mjs.map +2 -2
  10. package/build/Logger.mjs +4 -3
  11. package/build/Logger.mjs.map +1 -1
  12. package/build/MatchMaker.d.ts +30 -22
  13. package/build/MatchMaker.js +110 -87
  14. package/build/MatchMaker.js.map +2 -2
  15. package/build/MatchMaker.mjs +114 -94
  16. package/build/MatchMaker.mjs.map +2 -2
  17. package/build/Protocol.d.ts +3 -4
  18. package/build/Protocol.js +28 -19
  19. package/build/Protocol.js.map +2 -2
  20. package/build/Protocol.mjs +31 -21
  21. package/build/Protocol.mjs.map +2 -2
  22. package/build/Room.d.ts +36 -35
  23. package/build/Room.js +270 -109
  24. package/build/Room.js.map +2 -2
  25. package/build/Room.mjs +274 -116
  26. package/build/Room.mjs.map +2 -2
  27. package/build/Server.d.ts +6 -7
  28. package/build/Server.js +40 -12
  29. package/build/Server.js.map +2 -2
  30. package/build/Server.mjs +40 -15
  31. package/build/Server.mjs.map +3 -3
  32. package/build/Stats.js +1 -1
  33. package/build/Stats.js.map +2 -2
  34. package/build/Stats.mjs +6 -5
  35. package/build/Stats.mjs.map +2 -2
  36. package/build/Transport.d.ts +24 -11
  37. package/build/Transport.js +1 -1
  38. package/build/Transport.js.map +2 -2
  39. package/build/Transport.mjs +6 -5
  40. package/build/Transport.mjs.map +2 -2
  41. package/build/discovery/index.d.ts +1 -1
  42. package/build/discovery/index.js.map +2 -2
  43. package/build/discovery/index.mjs +3 -2
  44. package/build/discovery/index.mjs.map +2 -2
  45. package/build/errors/SeatReservationError.mjs +3 -2
  46. package/build/errors/SeatReservationError.mjs.map +1 -1
  47. package/build/errors/ServerError.js +1 -1
  48. package/build/errors/ServerError.js.map +1 -1
  49. package/build/errors/ServerError.mjs +5 -4
  50. package/build/errors/ServerError.mjs.map +2 -2
  51. package/build/index.d.ts +20 -19
  52. package/build/index.js +28 -20
  53. package/build/index.js.map +2 -2
  54. package/build/index.mjs +21 -19
  55. package/build/index.mjs.map +2 -2
  56. package/build/matchmaker/Lobby.d.ts +3 -3
  57. package/build/matchmaker/Lobby.js +6 -3
  58. package/build/matchmaker/Lobby.js.map +2 -2
  59. package/build/matchmaker/Lobby.mjs +4 -4
  60. package/build/matchmaker/Lobby.mjs.map +2 -2
  61. package/build/matchmaker/RegisteredHandler.d.ts +6 -7
  62. package/build/matchmaker/RegisteredHandler.js +7 -10
  63. package/build/matchmaker/RegisteredHandler.js.map +2 -2
  64. package/build/matchmaker/RegisteredHandler.mjs +11 -13
  65. package/build/matchmaker/RegisteredHandler.mjs.map +2 -2
  66. package/build/matchmaker/controller.d.ts +3 -4
  67. package/build/matchmaker/controller.js +22 -5
  68. package/build/matchmaker/controller.js.map +2 -2
  69. package/build/matchmaker/controller.mjs +19 -3
  70. package/build/matchmaker/controller.mjs.map +2 -2
  71. package/build/matchmaker/driver/RoomData.d.ts +3 -3
  72. package/build/matchmaker/driver/RoomData.js +3 -3
  73. package/build/matchmaker/driver/RoomData.js.map +2 -2
  74. package/build/matchmaker/driver/RoomData.mjs +2 -2
  75. package/build/matchmaker/driver/RoomData.mjs.map +2 -2
  76. package/build/matchmaker/driver/api.d.ts +104 -0
  77. package/build/matchmaker/driver/api.js +29 -0
  78. package/build/matchmaker/driver/api.js.map +7 -0
  79. package/build/matchmaker/driver/api.mjs +7 -0
  80. package/build/matchmaker/driver/api.mjs.map +7 -0
  81. package/build/matchmaker/driver/index.d.ts +7 -7
  82. package/build/matchmaker/driver/index.js +1 -1
  83. package/build/matchmaker/driver/index.js.map +2 -2
  84. package/build/matchmaker/driver/index.mjs +2 -2
  85. package/build/matchmaker/driver/index.mjs.map +2 -2
  86. package/build/matchmaker/driver/interfaces.d.ts +7 -11
  87. package/build/matchmaker/driver/interfaces.js.map +1 -1
  88. package/build/matchmaker/driver/local/LocalDriver.d.ts +13 -0
  89. package/build/matchmaker/driver/local/LocalDriver.js +65 -0
  90. package/build/matchmaker/driver/local/LocalDriver.js.map +7 -0
  91. package/build/matchmaker/driver/local/LocalDriver.mjs +43 -0
  92. package/build/matchmaker/driver/local/LocalDriver.mjs.map +7 -0
  93. package/build/matchmaker/driver/local/Query.d.ts +9 -0
  94. package/build/matchmaker/driver/local/Query.js +78 -0
  95. package/build/matchmaker/driver/local/Query.js.map +7 -0
  96. package/build/matchmaker/driver/local/Query.mjs +56 -0
  97. package/build/matchmaker/driver/local/Query.mjs.map +7 -0
  98. package/build/matchmaker/driver/local/RoomData.d.ts +19 -0
  99. package/build/matchmaker/driver/local/RoomData.js +79 -0
  100. package/build/matchmaker/driver/local/RoomData.js.map +7 -0
  101. package/build/matchmaker/driver/local/RoomData.mjs +57 -0
  102. package/build/matchmaker/driver/local/RoomData.mjs.map +7 -0
  103. package/build/presence/LocalPresence.d.ts +10 -3
  104. package/build/presence/LocalPresence.js +83 -5
  105. package/build/presence/LocalPresence.js.map +3 -3
  106. package/build/presence/LocalPresence.mjs +83 -8
  107. package/build/presence/LocalPresence.mjs.map +3 -3
  108. package/build/presence/Presence.d.ts +38 -2
  109. package/build/presence/Presence.js.map +1 -1
  110. package/build/rooms/LobbyRoom.d.ts +6 -6
  111. package/build/rooms/LobbyRoom.js +8 -3
  112. package/build/rooms/LobbyRoom.js.map +2 -2
  113. package/build/rooms/LobbyRoom.mjs +7 -5
  114. package/build/rooms/LobbyRoom.mjs.map +2 -2
  115. package/build/rooms/RelayRoom.d.ts +2 -2
  116. package/build/rooms/RelayRoom.js +3 -1
  117. package/build/rooms/RelayRoom.js.map +2 -2
  118. package/build/rooms/RelayRoom.mjs +10 -7
  119. package/build/rooms/RelayRoom.mjs.map +2 -2
  120. package/build/serializer/NoneSerializer.d.ts +2 -2
  121. package/build/serializer/NoneSerializer.js.map +1 -1
  122. package/build/serializer/NoneSerializer.mjs +3 -2
  123. package/build/serializer/NoneSerializer.mjs.map +2 -2
  124. package/build/serializer/SchemaSerializer.d.ts +13 -11
  125. package/build/serializer/SchemaSerializer.js +13 -10
  126. package/build/serializer/SchemaSerializer.js.map +2 -2
  127. package/build/serializer/SchemaSerializer.mjs +17 -13
  128. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  129. package/build/serializer/Serializer.d.ts +1 -2
  130. package/build/serializer/Serializer.js.map +1 -1
  131. package/build/utils/DevMode.d.ts +2 -2
  132. package/build/utils/DevMode.js +8 -4
  133. package/build/utils/DevMode.js.map +2 -2
  134. package/build/utils/DevMode.mjs +7 -6
  135. package/build/utils/DevMode.mjs.map +2 -2
  136. package/build/utils/Utils.d.ts +4 -2
  137. package/build/utils/Utils.js +16 -15
  138. package/build/utils/Utils.js.map +2 -2
  139. package/build/utils/Utils.mjs +18 -21
  140. package/build/utils/Utils.mjs.map +2 -2
  141. package/package.json +9 -3
package/build/Room.js CHANGED
@@ -17,6 +17,10 @@ var __copyProps = (to, from, except, desc) => {
17
17
  return to;
18
18
  };
19
19
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
20
24
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
21
25
  mod
22
26
  ));
@@ -28,19 +32,19 @@ __export(Room_exports, {
28
32
  RoomInternalState: () => RoomInternalState
29
33
  });
30
34
  module.exports = __toCommonJS(Room_exports);
31
- var import_msgpackr = require("msgpackr");
35
+ var import_msgpackr = require("@colyseus/msgpackr");
32
36
  var import_schema = require("@colyseus/schema");
33
- var import_timer = __toESM(require("@gamestdio/timer"));
37
+ var import_timer = __toESM(require("@colyseus/timer"));
34
38
  var import_events = require("events");
35
- var import_Logger = require("./Logger");
36
- var import_NoneSerializer = require("./serializer/NoneSerializer");
37
- var import_SchemaSerializer = require("./serializer/SchemaSerializer");
38
- var import_Protocol = require("./Protocol");
39
- var import_Utils = require("./utils/Utils");
40
- var import_DevMode = require("./utils/DevMode");
41
- var import_Debug = require("./Debug");
42
- var import_ServerError = require("./errors/ServerError");
43
- var import_Transport = require("./Transport");
39
+ var import_Logger = require("./Logger.js");
40
+ var import_NoneSerializer = require("./serializer/NoneSerializer.js");
41
+ var import_SchemaSerializer = require("./serializer/SchemaSerializer.js");
42
+ var import_Protocol = require("./Protocol.js");
43
+ var import_Utils = require("./utils/Utils.js");
44
+ var import_DevMode = require("./utils/DevMode.js");
45
+ var import_Debug = require("./Debug.js");
46
+ var import_ServerError = require("./errors/ServerError.js");
47
+ var import_Transport = require("./Transport.js");
44
48
  const DEFAULT_PATCH_RATE = 1e3 / 20;
45
49
  const DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
46
50
  const noneSerializer = new import_NoneSerializer.NoneSerializer();
@@ -53,35 +57,73 @@ var RoomInternalState = /* @__PURE__ */ ((RoomInternalState2) => {
53
57
  })(RoomInternalState || {});
54
58
  class Room {
55
59
  constructor() {
60
+ /**
61
+ * Timing events tied to the room instance.
62
+ * Intervals and timeouts are cleared when the room is disposed.
63
+ */
56
64
  this.clock = new import_timer.default();
57
- this.#_autoDispose = true;
65
+ /**
66
+ * Maximum number of clients allowed to connect into the room. When room reaches this limit,
67
+ * it is locked automatically. Unless the room was explicitly locked by you via `lock()` method,
68
+ * the room will be unlocked as soon as a client disconnects from it.
69
+ */
58
70
  this.maxClients = Infinity;
71
+ /**
72
+ * Automatically dispose the room when last client disconnects.
73
+ *
74
+ * @default true
75
+ */
76
+ this.autoDispose = true;
77
+ /**
78
+ * Frequency to send the room state to connected clients, in milliseconds.
79
+ *
80
+ * @default 50ms (20fps)
81
+ */
59
82
  this.patchRate = DEFAULT_PATCH_RATE;
83
+ /**
84
+ * The array of connected clients.
85
+ *
86
+ * @see {@link https://docs.colyseus.io/colyseus/server/room/#client|Client instance}
87
+ */
60
88
  this.clients = new import_Transport.ClientArray();
89
+ /** @internal */
61
90
  this._events = new import_events.EventEmitter();
91
+ // seat reservation & reconnection
62
92
  this.seatReservationTime = DEFAULT_SEAT_RESERVATION_TIME;
63
93
  this.reservedSeats = {};
64
94
  this.reservedSeatTimeouts = {};
65
95
  this._reconnections = {};
66
96
  this._reconnectingSessionId = /* @__PURE__ */ new Map();
67
- this.onMessageHandlers = {};
97
+ this.onMessageHandlers = {
98
+ "__no_message_handler": {
99
+ callback: (client, messageType, _) => {
100
+ const errorMessage = `onMessage for "${messageType}" not registered.`;
101
+ (0, import_Debug.debugAndPrintError)(errorMessage);
102
+ if (import_DevMode.isDevMode) {
103
+ client.error(import_Protocol.ErrorCode.INVALID_PAYLOAD, errorMessage);
104
+ } else {
105
+ client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
106
+ }
107
+ }
108
+ }
109
+ };
68
110
  this._serializer = noneSerializer;
69
111
  this._afterNextPatchQueue = [];
70
112
  this._internalState = 0 /* CREATING */;
71
113
  this._locked = false;
72
114
  this._lockedExplicitly = false;
73
115
  this._maxClientsReached = false;
74
- this._events.once("dispose", async () => {
75
- try {
76
- await this._dispose();
77
- } catch (e) {
78
- (0, import_Debug.debugAndPrintError)(`onDispose error: ${e && e.message || e || "promise rejected"}`);
79
- }
80
- this._events.emit("disconnect");
116
+ this._events.once("dispose", () => {
117
+ this._dispose().catch((e) => (0, import_Debug.debugAndPrintError)(`onDispose error: ${e && e.message || e || "promise rejected"}`)).finally(() => this._events.emit("disconnect"));
81
118
  });
82
- this.setPatchRate(this.patchRate);
83
- this.resetAutoDisposeTimeout(this.seatReservationTime);
84
119
  }
120
+ /**
121
+ * This property will change on these situations:
122
+ * - The maximum number of allowed clients has been reached (`maxClients`)
123
+ * - You manually locked, or unlocked the room using lock() or `unlock()`.
124
+ *
125
+ * @readonly
126
+ */
85
127
  get locked() {
86
128
  return this._locked;
87
129
  }
@@ -91,64 +133,149 @@ class Room {
91
133
  #_roomId;
92
134
  #_roomName;
93
135
  #_autoDispose;
94
- get autoDispose() {
95
- return this.#_autoDispose;
96
- }
97
- set autoDispose(value) {
98
- if (value !== this.#_autoDispose && this._internalState !== 2 /* DISPOSING */) {
99
- this.#_autoDispose = value;
100
- this.resetAutoDisposeTimeout();
101
- }
136
+ #_patchRate;
137
+ #_patchInterval;
138
+ __init() {
139
+ if (this.state) {
140
+ this.setState(this.state);
141
+ }
142
+ this.#_autoDispose = this.autoDispose;
143
+ this.#_patchRate = this.patchRate;
144
+ Object.defineProperties(this, {
145
+ autoDispose: {
146
+ enumerable: true,
147
+ get: () => this.#_autoDispose,
148
+ set: (value) => {
149
+ if (value !== this.#_autoDispose && this._internalState !== 2 /* DISPOSING */) {
150
+ this.#_autoDispose = value;
151
+ this.resetAutoDisposeTimeout();
152
+ }
153
+ }
154
+ },
155
+ patchRate: {
156
+ enumerable: true,
157
+ get: () => this.#_patchRate,
158
+ set: (milliseconds) => {
159
+ this.#_patchRate = milliseconds;
160
+ if (this.#_patchInterval) {
161
+ clearInterval(this.#_patchInterval);
162
+ this.#_patchInterval = void 0;
163
+ }
164
+ if (milliseconds !== null && milliseconds !== 0) {
165
+ this.#_patchInterval = setInterval(() => this.broadcastPatch(), milliseconds);
166
+ }
167
+ }
168
+ }
169
+ });
170
+ this.patchRate = this.#_patchRate;
171
+ this.resetAutoDisposeTimeout(this.seatReservationTime);
102
172
  }
173
+ /**
174
+ * The name of the room you provided as first argument for `gameServer.define()`.
175
+ *
176
+ * @returns roomName string
177
+ */
103
178
  get roomName() {
104
179
  return this.#_roomName;
105
180
  }
181
+ /**
182
+ * Setting the name of the room. Overwriting this property is restricted.
183
+ *
184
+ * @param roomName
185
+ */
106
186
  set roomName(roomName) {
107
187
  if (this.#_roomName) {
108
188
  throw new import_ServerError.ServerError(import_Protocol.ErrorCode.APPLICATION_ERROR, "'roomName' cannot be overwritten.");
109
189
  }
110
190
  this.#_roomName = roomName;
111
191
  }
192
+ /**
193
+ * A unique, auto-generated, 9-character-long id of the room.
194
+ * You may replace `this.roomId` during `onCreate()`.
195
+ *
196
+ * @returns roomId string
197
+ */
112
198
  get roomId() {
113
199
  return this.#_roomId;
114
200
  }
201
+ /**
202
+ * Setting the roomId, is restricted in room lifetime except upon room creation.
203
+ *
204
+ * @param roomId
205
+ * @returns roomId string
206
+ */
115
207
  set roomId(roomId) {
116
208
  if (this._internalState !== 0 /* CREATING */ && !import_DevMode.isDevMode) {
117
209
  throw new import_ServerError.ServerError(import_Protocol.ErrorCode.APPLICATION_ERROR, "'roomId' can only be overridden upon room creation.");
118
210
  }
119
211
  this.#_roomId = roomId;
120
212
  }
213
+ // TODO: flag as @deprecated on v0.16
214
+ // TOOD: remove instance level `onAuth` on 1.0
215
+ /**
216
+ * onAuth at the instance level will be deprecated in the future.
217
+ * Please use "static onAuth(token, req) instead
218
+ */
121
219
  onAuth(client, options, request) {
122
220
  return true;
123
221
  }
124
222
  static async onAuth(token, req) {
125
223
  return true;
126
224
  }
225
+ /**
226
+ * Returns whether the sum of connected clients and reserved seats exceeds maximum number of clients.
227
+ *
228
+ * @returns boolean
229
+ */
127
230
  hasReachedMaxClients() {
128
231
  return this.clients.length + Object.keys(this.reservedSeats).length >= this.maxClients || this._internalState === 2 /* DISPOSING */;
129
232
  }
233
+ /**
234
+ * Set the number of seconds a room can wait for a client to effectively join the room.
235
+ * You should consider how long your `onAuth()` will have to wait for setting a different seat reservation time.
236
+ * The default value is 15 seconds. You may set the `COLYSEUS_SEAT_RESERVATION_TIME`
237
+ * environment variable if you'd like to change the seat reservation time globally.
238
+ *
239
+ * @default 15 seconds
240
+ *
241
+ * @param seconds - number of seconds.
242
+ * @returns The modified Room object.
243
+ */
130
244
  setSeatReservationTime(seconds) {
131
245
  this.seatReservationTime = seconds;
132
246
  return this;
133
247
  }
134
248
  hasReservedSeat(sessionId, reconnectionToken) {
135
- if (reconnectionToken) {
136
- const reconnection = this._reconnections[reconnectionToken];
137
- return reconnection && reconnection[0] === sessionId && this.reservedSeats[sessionId] !== void 0 && this._reconnectingSessionId.has(sessionId);
249
+ const reservedSeat = this.reservedSeats[sessionId];
250
+ if (reservedSeat === void 0) {
251
+ return false;
252
+ }
253
+ if (reservedSeat[3]) {
254
+ return reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId && this._reconnectingSessionId.has(sessionId);
138
255
  } else {
139
- return this.reservedSeats[sessionId] !== void 0 && (!this._reconnectingSessionId.has(sessionId) || this._reconnectingSessionId.get(sessionId) === sessionId);
256
+ return reservedSeat[2] === false;
140
257
  }
141
258
  }
142
259
  checkReconnectionToken(reconnectionToken) {
143
- const reconnection = this._reconnections[reconnectionToken];
144
- const sessionId = reconnection && reconnection[0];
145
- if (this.hasReservedSeat(sessionId)) {
260
+ const sessionId = this._reconnections[reconnectionToken]?.[0];
261
+ const reservedSeat = this.reservedSeats[sessionId];
262
+ if (reservedSeat && reservedSeat[3]) {
146
263
  this._reconnectingSessionId.set(sessionId, reconnectionToken);
147
264
  return sessionId;
148
265
  } else {
149
266
  return void 0;
150
267
  }
151
268
  }
269
+ /**
270
+ * (Optional) Set a simulation interval that can change the state of the game.
271
+ * The simulation interval is your game loop.
272
+ *
273
+ * @default 16.6ms (60fps)
274
+ *
275
+ * @param onTickCallback - You can implement your physics or world updates here!
276
+ * This is a good place to update the room state.
277
+ * @param delay - Interval delay on executing `onTickCallback` in milliseconds.
278
+ */
152
279
  setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
153
280
  if (this._simulationInterval) {
154
281
  clearInterval(this._simulationInterval);
@@ -160,15 +287,11 @@ class Room {
160
287
  }, delay);
161
288
  }
162
289
  }
290
+ /**
291
+ * @deprecated Use `.patchRate=` instead.
292
+ */
163
293
  setPatchRate(milliseconds) {
164
294
  this.patchRate = milliseconds;
165
- if (this._patchInterval) {
166
- clearInterval(this._patchInterval);
167
- this._patchInterval = void 0;
168
- }
169
- if (milliseconds !== null && milliseconds !== 0) {
170
- this._patchInterval = setInterval(() => this.broadcastPatch(), milliseconds);
171
- }
172
295
  }
173
296
  setState(newState) {
174
297
  this.clock.start();
@@ -202,14 +325,16 @@ class Room {
202
325
  }
203
326
  }
204
327
  async setPrivate(bool = true) {
205
- if (this.listing.private === bool)
206
- return;
328
+ if (this.listing.private === bool) return;
207
329
  this.listing.private = bool;
208
330
  if (this._internalState === 1 /* CREATED */) {
209
331
  await this.listing.save();
210
332
  }
211
333
  this._events.emit("visibility-change", bool);
212
334
  }
335
+ /**
336
+ * Locking the room will remove it from the pool of available rooms for new clients to connect to.
337
+ */
213
338
  async lock() {
214
339
  this._lockedExplicitly = arguments[0] === void 0;
215
340
  if (this._locked) {
@@ -221,6 +346,9 @@ class Room {
221
346
  });
222
347
  this._events.emit("lock");
223
348
  }
349
+ /**
350
+ * Unlocking the room returns it to the pool of available rooms for new clients to connect to.
351
+ */
224
352
  async unlock() {
225
353
  if (arguments[0] === void 0) {
226
354
  this._lockedExplicitly = false;
@@ -238,16 +366,28 @@ class Room {
238
366
  import_Logger.logger.warn("DEPRECATION WARNING: use client.send(...) instead of this.send(client, ...)");
239
367
  client.send(messageOrType, messageOrOptions, options);
240
368
  }
241
- broadcast(typeOrSchema, messageOrOptions, options) {
242
- const isSchema = typeof typeOrSchema === "object";
243
- const opts = isSchema ? messageOrOptions : options;
244
- if (opts && opts.afterNextPatch) {
245
- delete opts.afterNextPatch;
369
+ broadcast(type, message, options) {
370
+ if (options && options.afterNextPatch) {
371
+ delete options.afterNextPatch;
246
372
  this._afterNextPatchQueue.push(["broadcast", arguments]);
247
373
  return;
248
374
  }
249
- this.broadcastMessageType(typeOrSchema, messageOrOptions, opts);
375
+ this.broadcastMessageType(type, message, options);
376
+ }
377
+ /**
378
+ * Broadcast bytes (UInt8Arrays) to a particular room
379
+ */
380
+ broadcastBytes(type, message, options) {
381
+ if (options && options.afterNextPatch) {
382
+ delete options.afterNextPatch;
383
+ this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
384
+ return;
385
+ }
386
+ this.broadcastMessageType(type, message, options);
250
387
  }
388
+ /**
389
+ * Checks whether mutations have occurred in the state, and broadcast them to all connected clients.
390
+ */
251
391
  broadcastPatch() {
252
392
  if (this.onBeforePatch) {
253
393
  this.onBeforePatch(this.state);
@@ -262,10 +402,16 @@ class Room {
262
402
  this._dequeueAfterPatchMessages();
263
403
  return hasChanges;
264
404
  }
265
- onMessage(messageType, callback) {
266
- this.onMessageHandlers[messageType] = callback;
405
+ onMessage(messageType, callback, validate) {
406
+ this.onMessageHandlers[messageType] = { callback, validate };
267
407
  return () => delete this.onMessageHandlers[messageType];
268
408
  }
409
+ /**
410
+ * Disconnect all connected clients, and then dispose the room.
411
+ *
412
+ * @param closeCode WebSocket close code (default = 4000, which is a "consented leave")
413
+ * @returns Promise<void>
414
+ */
269
415
  disconnect(closeCode = import_Protocol.Protocol.WS_CLOSE_CONSENTED) {
270
416
  if (this._internalState === 2 /* DISPOSING */) {
271
417
  return Promise.resolve(`disconnect() ignored: room (${this.roomId}) is already disposing.`);
@@ -291,7 +437,7 @@ class Room {
291
437
  }
292
438
  async ["_onJoin"](client, req) {
293
439
  const sessionId = client.sessionId;
294
- client._reconnectionToken = (0, import_Utils.generateId)();
440
+ client.reconnectionToken = (0, import_Utils.generateId)();
295
441
  if (this.reservedSeatTimeouts[sessionId]) {
296
442
  clearTimeout(this.reservedSeatTimeouts[sessionId]);
297
443
  delete this.reservedSeatTimeouts[sessionId];
@@ -300,18 +446,23 @@ class Room {
300
446
  clearTimeout(this._autoDisposeTimeout);
301
447
  this._autoDisposeTimeout = void 0;
302
448
  }
303
- const [joinOptions, authData] = this.reservedSeats[sessionId];
304
- if (this.reservedSeats[sessionId].length > 2) {
449
+ const [joinOptions, authData, isConsumed, isWaitingReconnection] = this.reservedSeats[sessionId];
450
+ if (isConsumed) {
305
451
  throw new import_ServerError.ServerError(import_Protocol.ErrorCode.MATCHMAKE_EXPIRED, "already consumed");
306
452
  }
307
- this.reservedSeats[sessionId].push(true);
453
+ this.reservedSeats[sessionId][2] = true;
308
454
  client._afterNextPatchQueue = this._afterNextPatchQueue;
309
455
  client.ref["onleave"] = (_) => client.state = import_Transport.ClientState.LEAVING;
310
456
  client.ref.once("close", client.ref["onleave"]);
311
- const previousReconnectionToken = this._reconnectingSessionId.get(sessionId);
312
- if (previousReconnectionToken) {
313
- this.clients.push(client);
314
- this._reconnections[previousReconnectionToken]?.[1].resolve(client);
457
+ if (isWaitingReconnection) {
458
+ const previousReconnectionToken = this._reconnectingSessionId.get(sessionId);
459
+ if (previousReconnectionToken) {
460
+ this.clients.push(client);
461
+ await this._reconnections[previousReconnectionToken]?.[1].resolve(client);
462
+ } else {
463
+ const errorMessage = process.env.NODE_ENV === "production" ? "already consumed" : "bad reconnection token";
464
+ throw new import_ServerError.ServerError(import_Protocol.ErrorCode.MATCHMAKE_EXPIRED, errorMessage);
465
+ }
315
466
  } else {
316
467
  try {
317
468
  if (authData) {
@@ -354,15 +505,25 @@ class Room {
354
505
  client.ref.once("close", client.ref["onleave"]);
355
506
  client.ref.on("message", this._onMessage.bind(this, client));
356
507
  client.raw(import_Protocol.getMessageBytes[import_Protocol.Protocol.JOIN_ROOM](
357
- client._reconnectionToken,
508
+ client.reconnectionToken,
358
509
  this._serializer.id,
359
510
  this._serializer.handshake && this._serializer.handshake()
360
511
  ));
361
512
  }
362
513
  }
514
+ /**
515
+ * Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method.
516
+ * If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds.
517
+ *
518
+ * @param previousClient - The client which is to be waiting until re-connection happens.
519
+ * @param seconds - Timeout period on re-connection in seconds.
520
+ *
521
+ * @returns Deferred<Client> - The differed is a promise like type.
522
+ * This type can forcibly reject the promise by calling `.reject()`.
523
+ */
363
524
  allowReconnection(previousClient, seconds) {
364
525
  if (previousClient._enqueuedMessages !== void 0) {
365
- return;
526
+ return import_Utils.Deferred.reject("client not joined");
366
527
  }
367
528
  if (seconds === void 0) {
368
529
  console.warn('DEPRECATED: allowReconnection() requires a second argument. Using "manual" mode.');
@@ -373,10 +534,10 @@ class Room {
373
534
  }
374
535
  if (this._internalState === 2 /* DISPOSING */) {
375
536
  this._disposeIfEmpty();
376
- throw new Error("disconnecting");
537
+ return import_Utils.Deferred.reject("disconnecting");
377
538
  }
378
539
  const sessionId = previousClient.sessionId;
379
- const reconnectionToken = previousClient._reconnectionToken;
540
+ const reconnectionToken = previousClient.reconnectionToken;
380
541
  this._reserveSeat(sessionId, true, previousClient.auth, seconds, true);
381
542
  const reconnection = new import_Utils.Deferred();
382
543
  this._reconnections[reconnectionToken] = [sessionId, reconnection];
@@ -414,7 +575,7 @@ class Room {
414
575
  }
415
576
  broadcastMessageType(type, message, options = {}) {
416
577
  (0, import_Debug.debugMessage)("broadcast: %O", message);
417
- const encodedMessage = import_Protocol.getMessageBytes.raw(import_Protocol.Protocol.ROOM_DATA, type, message);
578
+ const encodedMessage = message instanceof Uint8Array ? import_Protocol.getMessageBytes.raw(import_Protocol.Protocol.ROOM_DATA_BYTES, type, void 0, message) : import_Protocol.getMessageBytes.raw(import_Protocol.Protocol.ROOM_DATA, type, message);
418
579
  const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0;
419
580
  let numClients = this.clients.length;
420
581
  while (numClients--) {
@@ -425,7 +586,7 @@ class Room {
425
586
  }
426
587
  }
427
588
  sendFullState(client) {
428
- client.enqueueRaw(this._serializer.getFullState(client));
589
+ client.raw(this._serializer.getFullState(client));
429
590
  }
430
591
  _dequeueAfterPatchMessages() {
431
592
  const length = this._afterNextPatchQueue.length;
@@ -445,7 +606,7 @@ class Room {
445
606
  if (!allowReconnection && this.hasReachedMaxClients()) {
446
607
  return false;
447
608
  }
448
- this.reservedSeats[sessionId] = [joinOptions, authData];
609
+ this.reservedSeats[sessionId] = [joinOptions, authData, false, allowReconnection];
449
610
  if (!allowReconnection) {
450
611
  await this._incrementClientCount();
451
612
  this.reservedSeatTimeouts[sessionId] = setTimeout(async () => {
@@ -469,14 +630,14 @@ class Room {
469
630
  }
470
631
  async _dispose() {
471
632
  this._internalState = 2 /* DISPOSING */;
472
- await this.listing.remove();
633
+ this.listing.remove();
473
634
  let userReturnData;
474
635
  if (this.onDispose) {
475
636
  userReturnData = this.onDispose();
476
637
  }
477
- if (this._patchInterval) {
478
- clearInterval(this._patchInterval);
479
- this._patchInterval = void 0;
638
+ if (this.#_patchInterval) {
639
+ clearInterval(this.#_patchInterval);
640
+ this.#_patchInterval = void 0;
480
641
  }
481
642
  if (this._simulationInterval) {
482
643
  clearInterval(this._simulationInterval);
@@ -490,59 +651,52 @@ class Room {
490
651
  this.clock.stop();
491
652
  return await (userReturnData || Promise.resolve());
492
653
  }
493
- _onMessage(client, bytes) {
654
+ _onMessage(client, buffer) {
494
655
  if (client.state === import_Transport.ClientState.LEAVING) {
495
656
  return;
496
657
  }
497
- const it = { offset: 0 };
498
- const code = import_schema.decode.uint8(bytes, it);
499
- if (!bytes) {
500
- (0, import_Debug.debugAndPrintError)(`${this.roomName} (${this.roomId}), couldn't decode message: ${bytes}`);
658
+ const it = { offset: 1 };
659
+ const code = buffer[0];
660
+ if (!buffer) {
661
+ (0, import_Debug.debugAndPrintError)(`${this.roomName} (${this.roomId}), couldn't decode message: ${buffer}`);
501
662
  return;
502
663
  }
503
664
  if (code === import_Protocol.Protocol.ROOM_DATA) {
504
- const messageType = import_schema.decode.stringCheck(bytes, it) ? import_schema.decode.string(bytes, it) : import_schema.decode.number(bytes, it);
665
+ const messageType = import_schema.decode.stringCheck(buffer, it) ? import_schema.decode.string(buffer, it) : import_schema.decode.number(buffer, it);
666
+ const messageTypeHandler = this.onMessageHandlers[messageType];
505
667
  let message;
506
668
  try {
507
- message = bytes.length > it.offset ? (0, import_msgpackr.unpack)(new Uint8Array(bytes.slice(it.offset, bytes.length))) : void 0;
669
+ message = buffer.byteLength > it.offset ? (0, import_msgpackr.unpack)(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
508
670
  (0, import_Debug.debugMessage)("received: '%s' -> %j", messageType, message);
671
+ if (messageTypeHandler?.validate !== void 0) {
672
+ message = messageTypeHandler.validate(message);
673
+ }
509
674
  } catch (e) {
510
675
  (0, import_Debug.debugAndPrintError)(e);
511
676
  client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR);
512
677
  return;
513
678
  }
514
- if (this.onMessageHandlers[messageType]) {
515
- this.onMessageHandlers[messageType](client, message);
516
- } else if (this.onMessageHandlers["*"]) {
517
- this.onMessageHandlers["*"](client, messageType, message);
679
+ if (messageTypeHandler) {
680
+ messageTypeHandler.callback(client, message);
518
681
  } else {
519
- const errorMessage = `onMessage for "${messageType}" not registered.`;
520
- (0, import_Debug.debugAndPrintError)(errorMessage);
521
- if (import_DevMode.isDevMode) {
522
- client.error(import_Protocol.ErrorCode.INVALID_PAYLOAD, errorMessage);
523
- } else {
524
- client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
525
- }
682
+ (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message);
526
683
  }
527
684
  } else if (code === import_Protocol.Protocol.ROOM_DATA_BYTES) {
528
- const messageType = import_schema.decode.stringCheck(bytes, it) ? import_schema.decode.string(bytes, it) : import_schema.decode.number(bytes, it);
529
- const message = bytes.slice(it.offset, bytes.length);
685
+ const messageType = import_schema.decode.stringCheck(buffer, it) ? import_schema.decode.string(buffer, it) : import_schema.decode.number(buffer, it);
686
+ const messageTypeHandler = this.onMessageHandlers[messageType];
687
+ let message = buffer.subarray(it.offset, buffer.byteLength);
530
688
  (0, import_Debug.debugMessage)("received: '%s' -> %j", messageType, message);
531
- if (this.onMessageHandlers[messageType]) {
532
- this.onMessageHandlers[messageType](client, message);
533
- } else if (this.onMessageHandlers["*"]) {
534
- this.onMessageHandlers["*"](client, messageType, message);
689
+ if (messageTypeHandler?.validate !== void 0) {
690
+ message = messageTypeHandler.validate(message);
691
+ }
692
+ if (messageTypeHandler) {
693
+ messageTypeHandler.callback(client, message);
535
694
  } else {
536
- const errorMessage = `onMessage for "${messageType}" not registered.`;
537
- (0, import_Debug.debugAndPrintError)(errorMessage);
538
- if (import_DevMode.isDevMode) {
539
- client.error(import_Protocol.ErrorCode.INVALID_PAYLOAD, errorMessage);
540
- } else {
541
- client.leave(import_Protocol.Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
542
- }
695
+ (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message);
543
696
  }
544
697
  } else if (code === import_Protocol.Protocol.JOIN_ROOM && client.state === import_Transport.ClientState.JOINING) {
545
698
  client.state = import_Transport.ClientState.JOINED;
699
+ client._joinedAt = this.clock.elapsedTime;
546
700
  if (this.state) {
547
701
  this.sendFullState(client);
548
702
  }
@@ -571,11 +725,18 @@ class Room {
571
725
  }
572
726
  }
573
727
  }
574
- if (client.state !== import_Transport.ClientState.RECONNECTED) {
575
- const willDispose = await this._decrementClientCount();
576
- if (this.reservedSeats[client.sessionId] === void 0) {
577
- this._events.emit("leave", client, willDispose);
578
- }
728
+ if (this._reconnections[client.reconnectionToken]) {
729
+ this._reconnections[client.reconnectionToken][1].catch(async () => {
730
+ await this._onAfterLeave(client);
731
+ });
732
+ } else if (client.state !== import_Transport.ClientState.RECONNECTED) {
733
+ await this._onAfterLeave(client);
734
+ }
735
+ }
736
+ async _onAfterLeave(client) {
737
+ const willDispose = await this._decrementClientCount();
738
+ if (this.reservedSeats[client.sessionId] === void 0) {
739
+ this._events.emit("leave", client, willDispose);
579
740
  }
580
741
  }
581
742
  async _incrementClientCount() {