@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.mjs CHANGED
@@ -1,57 +1,96 @@
1
- import { unpack } from "msgpackr";
1
+ // packages/core/src/Room.ts
2
+ import { unpack } from "@colyseus/msgpackr";
2
3
  import { decode, $changes } from "@colyseus/schema";
3
- import Clock from "@gamestdio/timer";
4
+ import Clock from "@colyseus/timer";
4
5
  import { EventEmitter } from "events";
5
- import { logger } from "./Logger";
6
- import { NoneSerializer } from "./serializer/NoneSerializer";
7
- import { SchemaSerializer } from "./serializer/SchemaSerializer";
8
- import { ErrorCode, getMessageBytes, Protocol } from "./Protocol";
9
- import { Deferred, generateId } from "./utils/Utils";
10
- import { isDevMode } from "./utils/DevMode";
11
- import { debugAndPrintError, debugMessage } from "./Debug";
12
- import { ServerError } from "./errors/ServerError";
13
- import { ClientArray, ClientState } from "./Transport";
14
- const DEFAULT_PATCH_RATE = 1e3 / 20;
15
- const DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
16
- const noneSerializer = new NoneSerializer();
17
- const DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
6
+ import { logger } from "./Logger.mjs";
7
+ import { NoneSerializer } from "./serializer/NoneSerializer.mjs";
8
+ import { SchemaSerializer } from "./serializer/SchemaSerializer.mjs";
9
+ import { ErrorCode, getMessageBytes, Protocol } from "./Protocol.mjs";
10
+ import { Deferred, generateId } from "./utils/Utils.mjs";
11
+ import { isDevMode } from "./utils/DevMode.mjs";
12
+ import { debugAndPrintError, debugMessage } from "./Debug.mjs";
13
+ import { ServerError } from "./errors/ServerError.mjs";
14
+ import { ClientArray, ClientState } from "./Transport.mjs";
15
+ var DEFAULT_PATCH_RATE = 1e3 / 20;
16
+ var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
17
+ var noneSerializer = new NoneSerializer();
18
+ var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
18
19
  var RoomInternalState = /* @__PURE__ */ ((RoomInternalState2) => {
19
20
  RoomInternalState2[RoomInternalState2["CREATING"] = 0] = "CREATING";
20
21
  RoomInternalState2[RoomInternalState2["CREATED"] = 1] = "CREATED";
21
22
  RoomInternalState2[RoomInternalState2["DISPOSING"] = 2] = "DISPOSING";
22
23
  return RoomInternalState2;
23
24
  })(RoomInternalState || {});
24
- class Room {
25
+ var Room = class _Room {
25
26
  constructor() {
27
+ /**
28
+ * Timing events tied to the room instance.
29
+ * Intervals and timeouts are cleared when the room is disposed.
30
+ */
26
31
  this.clock = new Clock();
27
- this.#_autoDispose = true;
32
+ /**
33
+ * Maximum number of clients allowed to connect into the room. When room reaches this limit,
34
+ * it is locked automatically. Unless the room was explicitly locked by you via `lock()` method,
35
+ * the room will be unlocked as soon as a client disconnects from it.
36
+ */
28
37
  this.maxClients = Infinity;
38
+ /**
39
+ * Automatically dispose the room when last client disconnects.
40
+ *
41
+ * @default true
42
+ */
43
+ this.autoDispose = true;
44
+ /**
45
+ * Frequency to send the room state to connected clients, in milliseconds.
46
+ *
47
+ * @default 50ms (20fps)
48
+ */
29
49
  this.patchRate = DEFAULT_PATCH_RATE;
50
+ /**
51
+ * The array of connected clients.
52
+ *
53
+ * @see {@link https://docs.colyseus.io/colyseus/server/room/#client|Client instance}
54
+ */
30
55
  this.clients = new ClientArray();
56
+ /** @internal */
31
57
  this._events = new EventEmitter();
58
+ // seat reservation & reconnection
32
59
  this.seatReservationTime = DEFAULT_SEAT_RESERVATION_TIME;
33
60
  this.reservedSeats = {};
34
61
  this.reservedSeatTimeouts = {};
35
62
  this._reconnections = {};
36
63
  this._reconnectingSessionId = /* @__PURE__ */ new Map();
37
- this.onMessageHandlers = {};
64
+ this.onMessageHandlers = {
65
+ "__no_message_handler": {
66
+ callback: (client, messageType, _) => {
67
+ const errorMessage = `onMessage for "${messageType}" not registered.`;
68
+ debugAndPrintError(errorMessage);
69
+ if (isDevMode) {
70
+ client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
71
+ } else {
72
+ client.leave(Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
73
+ }
74
+ }
75
+ }
76
+ };
38
77
  this._serializer = noneSerializer;
39
78
  this._afterNextPatchQueue = [];
40
79
  this._internalState = 0 /* CREATING */;
41
80
  this._locked = false;
42
81
  this._lockedExplicitly = false;
43
82
  this._maxClientsReached = false;
44
- this._events.once("dispose", async () => {
45
- try {
46
- await this._dispose();
47
- } catch (e) {
48
- debugAndPrintError(`onDispose error: ${e && e.message || e || "promise rejected"}`);
49
- }
50
- this._events.emit("disconnect");
83
+ this._events.once("dispose", () => {
84
+ this._dispose().catch((e) => debugAndPrintError(`onDispose error: ${e && e.message || e || "promise rejected"}`)).finally(() => this._events.emit("disconnect"));
51
85
  });
52
- this.setPatchRate(this.patchRate);
53
- this.resetAutoDisposeTimeout(this.seatReservationTime);
54
86
  }
87
+ /**
88
+ * This property will change on these situations:
89
+ * - The maximum number of allowed clients has been reached (`maxClients`)
90
+ * - You manually locked, or unlocked the room using lock() or `unlock()`.
91
+ *
92
+ * @readonly
93
+ */
55
94
  get locked() {
56
95
  return this._locked;
57
96
  }
@@ -61,64 +100,149 @@ class Room {
61
100
  #_roomId;
62
101
  #_roomName;
63
102
  #_autoDispose;
64
- get autoDispose() {
65
- return this.#_autoDispose;
66
- }
67
- set autoDispose(value) {
68
- if (value !== this.#_autoDispose && this._internalState !== 2 /* DISPOSING */) {
69
- this.#_autoDispose = value;
70
- this.resetAutoDisposeTimeout();
71
- }
103
+ #_patchRate;
104
+ #_patchInterval;
105
+ __init() {
106
+ if (this.state) {
107
+ this.setState(this.state);
108
+ }
109
+ this.#_autoDispose = this.autoDispose;
110
+ this.#_patchRate = this.patchRate;
111
+ Object.defineProperties(this, {
112
+ autoDispose: {
113
+ enumerable: true,
114
+ get: () => this.#_autoDispose,
115
+ set: (value) => {
116
+ if (value !== this.#_autoDispose && this._internalState !== 2 /* DISPOSING */) {
117
+ this.#_autoDispose = value;
118
+ this.resetAutoDisposeTimeout();
119
+ }
120
+ }
121
+ },
122
+ patchRate: {
123
+ enumerable: true,
124
+ get: () => this.#_patchRate,
125
+ set: (milliseconds) => {
126
+ this.#_patchRate = milliseconds;
127
+ if (this.#_patchInterval) {
128
+ clearInterval(this.#_patchInterval);
129
+ this.#_patchInterval = void 0;
130
+ }
131
+ if (milliseconds !== null && milliseconds !== 0) {
132
+ this.#_patchInterval = setInterval(() => this.broadcastPatch(), milliseconds);
133
+ }
134
+ }
135
+ }
136
+ });
137
+ this.patchRate = this.#_patchRate;
138
+ this.resetAutoDisposeTimeout(this.seatReservationTime);
72
139
  }
140
+ /**
141
+ * The name of the room you provided as first argument for `gameServer.define()`.
142
+ *
143
+ * @returns roomName string
144
+ */
73
145
  get roomName() {
74
146
  return this.#_roomName;
75
147
  }
148
+ /**
149
+ * Setting the name of the room. Overwriting this property is restricted.
150
+ *
151
+ * @param roomName
152
+ */
76
153
  set roomName(roomName) {
77
154
  if (this.#_roomName) {
78
155
  throw new ServerError(ErrorCode.APPLICATION_ERROR, "'roomName' cannot be overwritten.");
79
156
  }
80
157
  this.#_roomName = roomName;
81
158
  }
159
+ /**
160
+ * A unique, auto-generated, 9-character-long id of the room.
161
+ * You may replace `this.roomId` during `onCreate()`.
162
+ *
163
+ * @returns roomId string
164
+ */
82
165
  get roomId() {
83
166
  return this.#_roomId;
84
167
  }
168
+ /**
169
+ * Setting the roomId, is restricted in room lifetime except upon room creation.
170
+ *
171
+ * @param roomId
172
+ * @returns roomId string
173
+ */
85
174
  set roomId(roomId) {
86
175
  if (this._internalState !== 0 /* CREATING */ && !isDevMode) {
87
176
  throw new ServerError(ErrorCode.APPLICATION_ERROR, "'roomId' can only be overridden upon room creation.");
88
177
  }
89
178
  this.#_roomId = roomId;
90
179
  }
180
+ // TODO: flag as @deprecated on v0.16
181
+ // TOOD: remove instance level `onAuth` on 1.0
182
+ /**
183
+ * onAuth at the instance level will be deprecated in the future.
184
+ * Please use "static onAuth(token, req) instead
185
+ */
91
186
  onAuth(client, options, request) {
92
187
  return true;
93
188
  }
94
189
  static async onAuth(token, req) {
95
190
  return true;
96
191
  }
192
+ /**
193
+ * Returns whether the sum of connected clients and reserved seats exceeds maximum number of clients.
194
+ *
195
+ * @returns boolean
196
+ */
97
197
  hasReachedMaxClients() {
98
198
  return this.clients.length + Object.keys(this.reservedSeats).length >= this.maxClients || this._internalState === 2 /* DISPOSING */;
99
199
  }
200
+ /**
201
+ * Set the number of seconds a room can wait for a client to effectively join the room.
202
+ * You should consider how long your `onAuth()` will have to wait for setting a different seat reservation time.
203
+ * The default value is 15 seconds. You may set the `COLYSEUS_SEAT_RESERVATION_TIME`
204
+ * environment variable if you'd like to change the seat reservation time globally.
205
+ *
206
+ * @default 15 seconds
207
+ *
208
+ * @param seconds - number of seconds.
209
+ * @returns The modified Room object.
210
+ */
100
211
  setSeatReservationTime(seconds) {
101
212
  this.seatReservationTime = seconds;
102
213
  return this;
103
214
  }
104
215
  hasReservedSeat(sessionId, reconnectionToken) {
105
- if (reconnectionToken) {
106
- const reconnection = this._reconnections[reconnectionToken];
107
- return reconnection && reconnection[0] === sessionId && this.reservedSeats[sessionId] !== void 0 && this._reconnectingSessionId.has(sessionId);
216
+ const reservedSeat = this.reservedSeats[sessionId];
217
+ if (reservedSeat === void 0) {
218
+ return false;
219
+ }
220
+ if (reservedSeat[3]) {
221
+ return reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId && this._reconnectingSessionId.has(sessionId);
108
222
  } else {
109
- return this.reservedSeats[sessionId] !== void 0 && (!this._reconnectingSessionId.has(sessionId) || this._reconnectingSessionId.get(sessionId) === sessionId);
223
+ return reservedSeat[2] === false;
110
224
  }
111
225
  }
112
226
  checkReconnectionToken(reconnectionToken) {
113
- const reconnection = this._reconnections[reconnectionToken];
114
- const sessionId = reconnection && reconnection[0];
115
- if (this.hasReservedSeat(sessionId)) {
227
+ const sessionId = this._reconnections[reconnectionToken]?.[0];
228
+ const reservedSeat = this.reservedSeats[sessionId];
229
+ if (reservedSeat && reservedSeat[3]) {
116
230
  this._reconnectingSessionId.set(sessionId, reconnectionToken);
117
231
  return sessionId;
118
232
  } else {
119
233
  return void 0;
120
234
  }
121
235
  }
236
+ /**
237
+ * (Optional) Set a simulation interval that can change the state of the game.
238
+ * The simulation interval is your game loop.
239
+ *
240
+ * @default 16.6ms (60fps)
241
+ *
242
+ * @param onTickCallback - You can implement your physics or world updates here!
243
+ * This is a good place to update the room state.
244
+ * @param delay - Interval delay on executing `onTickCallback` in milliseconds.
245
+ */
122
246
  setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
123
247
  if (this._simulationInterval) {
124
248
  clearInterval(this._simulationInterval);
@@ -130,15 +254,11 @@ class Room {
130
254
  }, delay);
131
255
  }
132
256
  }
257
+ /**
258
+ * @deprecated Use `.patchRate=` instead.
259
+ */
133
260
  setPatchRate(milliseconds) {
134
261
  this.patchRate = milliseconds;
135
- if (this._patchInterval) {
136
- clearInterval(this._patchInterval);
137
- this._patchInterval = void 0;
138
- }
139
- if (milliseconds !== null && milliseconds !== 0) {
140
- this._patchInterval = setInterval(() => this.broadcastPatch(), milliseconds);
141
- }
142
262
  }
143
263
  setState(newState) {
144
264
  this.clock.start();
@@ -172,14 +292,16 @@ class Room {
172
292
  }
173
293
  }
174
294
  async setPrivate(bool = true) {
175
- if (this.listing.private === bool)
176
- return;
295
+ if (this.listing.private === bool) return;
177
296
  this.listing.private = bool;
178
297
  if (this._internalState === 1 /* CREATED */) {
179
298
  await this.listing.save();
180
299
  }
181
300
  this._events.emit("visibility-change", bool);
182
301
  }
302
+ /**
303
+ * Locking the room will remove it from the pool of available rooms for new clients to connect to.
304
+ */
183
305
  async lock() {
184
306
  this._lockedExplicitly = arguments[0] === void 0;
185
307
  if (this._locked) {
@@ -191,6 +313,9 @@ class Room {
191
313
  });
192
314
  this._events.emit("lock");
193
315
  }
316
+ /**
317
+ * Unlocking the room returns it to the pool of available rooms for new clients to connect to.
318
+ */
194
319
  async unlock() {
195
320
  if (arguments[0] === void 0) {
196
321
  this._lockedExplicitly = false;
@@ -208,16 +333,28 @@ class Room {
208
333
  logger.warn("DEPRECATION WARNING: use client.send(...) instead of this.send(client, ...)");
209
334
  client.send(messageOrType, messageOrOptions, options);
210
335
  }
211
- broadcast(typeOrSchema, messageOrOptions, options) {
212
- const isSchema = typeof typeOrSchema === "object";
213
- const opts = isSchema ? messageOrOptions : options;
214
- if (opts && opts.afterNextPatch) {
215
- delete opts.afterNextPatch;
336
+ broadcast(type, message, options) {
337
+ if (options && options.afterNextPatch) {
338
+ delete options.afterNextPatch;
216
339
  this._afterNextPatchQueue.push(["broadcast", arguments]);
217
340
  return;
218
341
  }
219
- this.broadcastMessageType(typeOrSchema, messageOrOptions, opts);
342
+ this.broadcastMessageType(type, message, options);
343
+ }
344
+ /**
345
+ * Broadcast bytes (UInt8Arrays) to a particular room
346
+ */
347
+ broadcastBytes(type, message, options) {
348
+ if (options && options.afterNextPatch) {
349
+ delete options.afterNextPatch;
350
+ this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
351
+ return;
352
+ }
353
+ this.broadcastMessageType(type, message, options);
220
354
  }
355
+ /**
356
+ * Checks whether mutations have occurred in the state, and broadcast them to all connected clients.
357
+ */
221
358
  broadcastPatch() {
222
359
  if (this.onBeforePatch) {
223
360
  this.onBeforePatch(this.state);
@@ -232,10 +369,16 @@ class Room {
232
369
  this._dequeueAfterPatchMessages();
233
370
  return hasChanges;
234
371
  }
235
- onMessage(messageType, callback) {
236
- this.onMessageHandlers[messageType] = callback;
372
+ onMessage(messageType, callback, validate) {
373
+ this.onMessageHandlers[messageType] = { callback, validate };
237
374
  return () => delete this.onMessageHandlers[messageType];
238
375
  }
376
+ /**
377
+ * Disconnect all connected clients, and then dispose the room.
378
+ *
379
+ * @param closeCode WebSocket close code (default = 4000, which is a "consented leave")
380
+ * @returns Promise<void>
381
+ */
239
382
  disconnect(closeCode = Protocol.WS_CLOSE_CONSENTED) {
240
383
  if (this._internalState === 2 /* DISPOSING */) {
241
384
  return Promise.resolve(`disconnect() ignored: room (${this.roomId}) is already disposing.`);
@@ -261,7 +404,7 @@ class Room {
261
404
  }
262
405
  async ["_onJoin"](client, req) {
263
406
  const sessionId = client.sessionId;
264
- client._reconnectionToken = generateId();
407
+ client.reconnectionToken = generateId();
265
408
  if (this.reservedSeatTimeouts[sessionId]) {
266
409
  clearTimeout(this.reservedSeatTimeouts[sessionId]);
267
410
  delete this.reservedSeatTimeouts[sessionId];
@@ -270,23 +413,28 @@ class Room {
270
413
  clearTimeout(this._autoDisposeTimeout);
271
414
  this._autoDisposeTimeout = void 0;
272
415
  }
273
- const [joinOptions, authData] = this.reservedSeats[sessionId];
274
- if (this.reservedSeats[sessionId].length > 2) {
416
+ const [joinOptions, authData, isConsumed, isWaitingReconnection] = this.reservedSeats[sessionId];
417
+ if (isConsumed) {
275
418
  throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, "already consumed");
276
419
  }
277
- this.reservedSeats[sessionId].push(true);
420
+ this.reservedSeats[sessionId][2] = true;
278
421
  client._afterNextPatchQueue = this._afterNextPatchQueue;
279
422
  client.ref["onleave"] = (_) => client.state = ClientState.LEAVING;
280
423
  client.ref.once("close", client.ref["onleave"]);
281
- const previousReconnectionToken = this._reconnectingSessionId.get(sessionId);
282
- if (previousReconnectionToken) {
283
- this.clients.push(client);
284
- this._reconnections[previousReconnectionToken]?.[1].resolve(client);
424
+ if (isWaitingReconnection) {
425
+ const previousReconnectionToken = this._reconnectingSessionId.get(sessionId);
426
+ if (previousReconnectionToken) {
427
+ this.clients.push(client);
428
+ await this._reconnections[previousReconnectionToken]?.[1].resolve(client);
429
+ } else {
430
+ const errorMessage = process.env.NODE_ENV === "production" ? "already consumed" : "bad reconnection token";
431
+ throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, errorMessage);
432
+ }
285
433
  } else {
286
434
  try {
287
435
  if (authData) {
288
436
  client.auth = authData;
289
- } else if (this.onAuth !== Room.prototype.onAuth) {
437
+ } else if (this.onAuth !== _Room.prototype.onAuth) {
290
438
  client.auth = await this.onAuth(client, joinOptions, req);
291
439
  if (!client.auth) {
292
440
  throw new ServerError(ErrorCode.AUTH_FAILED, "onAuth failed");
@@ -324,15 +472,25 @@ class Room {
324
472
  client.ref.once("close", client.ref["onleave"]);
325
473
  client.ref.on("message", this._onMessage.bind(this, client));
326
474
  client.raw(getMessageBytes[Protocol.JOIN_ROOM](
327
- client._reconnectionToken,
475
+ client.reconnectionToken,
328
476
  this._serializer.id,
329
477
  this._serializer.handshake && this._serializer.handshake()
330
478
  ));
331
479
  }
332
480
  }
481
+ /**
482
+ * Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method.
483
+ * If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds.
484
+ *
485
+ * @param previousClient - The client which is to be waiting until re-connection happens.
486
+ * @param seconds - Timeout period on re-connection in seconds.
487
+ *
488
+ * @returns Deferred<Client> - The differed is a promise like type.
489
+ * This type can forcibly reject the promise by calling `.reject()`.
490
+ */
333
491
  allowReconnection(previousClient, seconds) {
334
492
  if (previousClient._enqueuedMessages !== void 0) {
335
- return;
493
+ return Deferred.reject("client not joined");
336
494
  }
337
495
  if (seconds === void 0) {
338
496
  console.warn('DEPRECATED: allowReconnection() requires a second argument. Using "manual" mode.');
@@ -343,10 +501,10 @@ class Room {
343
501
  }
344
502
  if (this._internalState === 2 /* DISPOSING */) {
345
503
  this._disposeIfEmpty();
346
- throw new Error("disconnecting");
504
+ return Deferred.reject("disconnecting");
347
505
  }
348
506
  const sessionId = previousClient.sessionId;
349
- const reconnectionToken = previousClient._reconnectionToken;
507
+ const reconnectionToken = previousClient.reconnectionToken;
350
508
  this._reserveSeat(sessionId, true, previousClient.auth, seconds, true);
351
509
  const reconnection = new Deferred();
352
510
  this._reconnections[reconnectionToken] = [sessionId, reconnection];
@@ -384,7 +542,7 @@ class Room {
384
542
  }
385
543
  broadcastMessageType(type, message, options = {}) {
386
544
  debugMessage("broadcast: %O", message);
387
- const encodedMessage = getMessageBytes.raw(Protocol.ROOM_DATA, type, message);
545
+ const encodedMessage = message instanceof Uint8Array ? getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, void 0, message) : getMessageBytes.raw(Protocol.ROOM_DATA, type, message);
388
546
  const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0;
389
547
  let numClients = this.clients.length;
390
548
  while (numClients--) {
@@ -395,7 +553,7 @@ class Room {
395
553
  }
396
554
  }
397
555
  sendFullState(client) {
398
- client.enqueueRaw(this._serializer.getFullState(client));
556
+ client.raw(this._serializer.getFullState(client));
399
557
  }
400
558
  _dequeueAfterPatchMessages() {
401
559
  const length = this._afterNextPatchQueue.length;
@@ -415,7 +573,7 @@ class Room {
415
573
  if (!allowReconnection && this.hasReachedMaxClients()) {
416
574
  return false;
417
575
  }
418
- this.reservedSeats[sessionId] = [joinOptions, authData];
576
+ this.reservedSeats[sessionId] = [joinOptions, authData, false, allowReconnection];
419
577
  if (!allowReconnection) {
420
578
  await this._incrementClientCount();
421
579
  this.reservedSeatTimeouts[sessionId] = setTimeout(async () => {
@@ -439,14 +597,14 @@ class Room {
439
597
  }
440
598
  async _dispose() {
441
599
  this._internalState = 2 /* DISPOSING */;
442
- await this.listing.remove();
600
+ this.listing.remove();
443
601
  let userReturnData;
444
602
  if (this.onDispose) {
445
603
  userReturnData = this.onDispose();
446
604
  }
447
- if (this._patchInterval) {
448
- clearInterval(this._patchInterval);
449
- this._patchInterval = void 0;
605
+ if (this.#_patchInterval) {
606
+ clearInterval(this.#_patchInterval);
607
+ this.#_patchInterval = void 0;
450
608
  }
451
609
  if (this._simulationInterval) {
452
610
  clearInterval(this._simulationInterval);
@@ -460,59 +618,52 @@ class Room {
460
618
  this.clock.stop();
461
619
  return await (userReturnData || Promise.resolve());
462
620
  }
463
- _onMessage(client, bytes) {
621
+ _onMessage(client, buffer) {
464
622
  if (client.state === ClientState.LEAVING) {
465
623
  return;
466
624
  }
467
- const it = { offset: 0 };
468
- const code = decode.uint8(bytes, it);
469
- if (!bytes) {
470
- debugAndPrintError(`${this.roomName} (${this.roomId}), couldn't decode message: ${bytes}`);
625
+ const it = { offset: 1 };
626
+ const code = buffer[0];
627
+ if (!buffer) {
628
+ debugAndPrintError(`${this.roomName} (${this.roomId}), couldn't decode message: ${buffer}`);
471
629
  return;
472
630
  }
473
631
  if (code === Protocol.ROOM_DATA) {
474
- const messageType = decode.stringCheck(bytes, it) ? decode.string(bytes, it) : decode.number(bytes, it);
632
+ const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
633
+ const messageTypeHandler = this.onMessageHandlers[messageType];
475
634
  let message;
476
635
  try {
477
- message = bytes.length > it.offset ? unpack(new Uint8Array(bytes.slice(it.offset, bytes.length))) : void 0;
636
+ message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
478
637
  debugMessage("received: '%s' -> %j", messageType, message);
638
+ if (messageTypeHandler?.validate !== void 0) {
639
+ message = messageTypeHandler.validate(message);
640
+ }
479
641
  } catch (e) {
480
642
  debugAndPrintError(e);
481
643
  client.leave(Protocol.WS_CLOSE_WITH_ERROR);
482
644
  return;
483
645
  }
484
- if (this.onMessageHandlers[messageType]) {
485
- this.onMessageHandlers[messageType](client, message);
486
- } else if (this.onMessageHandlers["*"]) {
487
- this.onMessageHandlers["*"](client, messageType, message);
646
+ if (messageTypeHandler) {
647
+ messageTypeHandler.callback(client, message);
488
648
  } else {
489
- const errorMessage = `onMessage for "${messageType}" not registered.`;
490
- debugAndPrintError(errorMessage);
491
- if (isDevMode) {
492
- client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
493
- } else {
494
- client.leave(Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
495
- }
649
+ (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message);
496
650
  }
497
651
  } else if (code === Protocol.ROOM_DATA_BYTES) {
498
- const messageType = decode.stringCheck(bytes, it) ? decode.string(bytes, it) : decode.number(bytes, it);
499
- const message = bytes.slice(it.offset, bytes.length);
652
+ const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
653
+ const messageTypeHandler = this.onMessageHandlers[messageType];
654
+ let message = buffer.subarray(it.offset, buffer.byteLength);
500
655
  debugMessage("received: '%s' -> %j", messageType, message);
501
- if (this.onMessageHandlers[messageType]) {
502
- this.onMessageHandlers[messageType](client, message);
503
- } else if (this.onMessageHandlers["*"]) {
504
- this.onMessageHandlers["*"](client, messageType, message);
656
+ if (messageTypeHandler?.validate !== void 0) {
657
+ message = messageTypeHandler.validate(message);
658
+ }
659
+ if (messageTypeHandler) {
660
+ messageTypeHandler.callback(client, message);
505
661
  } else {
506
- const errorMessage = `onMessage for "${messageType}" not registered.`;
507
- debugAndPrintError(errorMessage);
508
- if (isDevMode) {
509
- client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
510
- } else {
511
- client.leave(Protocol.WS_CLOSE_WITH_ERROR, errorMessage);
512
- }
662
+ (this.onMessageHandlers["*"] || this.onMessageHandlers["__no_message_handler"]).callback(client, messageType, message);
513
663
  }
514
664
  } else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {
515
665
  client.state = ClientState.JOINED;
666
+ client._joinedAt = this.clock.elapsedTime;
516
667
  if (this.state) {
517
668
  this.sendFullState(client);
518
669
  }
@@ -541,11 +692,18 @@ class Room {
541
692
  }
542
693
  }
543
694
  }
544
- if (client.state !== ClientState.RECONNECTED) {
545
- const willDispose = await this._decrementClientCount();
546
- if (this.reservedSeats[client.sessionId] === void 0) {
547
- this._events.emit("leave", client, willDispose);
548
- }
695
+ if (this._reconnections[client.reconnectionToken]) {
696
+ this._reconnections[client.reconnectionToken][1].catch(async () => {
697
+ await this._onAfterLeave(client);
698
+ });
699
+ } else if (client.state !== ClientState.RECONNECTED) {
700
+ await this._onAfterLeave(client);
701
+ }
702
+ }
703
+ async _onAfterLeave(client) {
704
+ const willDispose = await this._decrementClientCount();
705
+ if (this.reservedSeats[client.sessionId] === void 0) {
706
+ this._events.emit("leave", client, willDispose);
549
707
  }
550
708
  }
551
709
  async _incrementClientCount() {
@@ -575,7 +733,7 @@ class Room {
575
733
  }
576
734
  return willDispose;
577
735
  }
578
- }
736
+ };
579
737
  export {
580
738
  DEFAULT_SEAT_RESERVATION_TIME,
581
739
  Room,