@colyseus/core 0.18.17 → 0.18.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.17",
3
+ "version": "0.18.18",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -61,9 +61,9 @@
61
61
  "@colyseus/schema": "^5.0.8",
62
62
  "express": "^5.0.0",
63
63
  "vitest": "^3.1.1",
64
- "@colyseus/redis-driver": "^0.18.4",
65
64
  "@colyseus/redis-presence": "^0.18.5",
66
- "@colyseus/tools": "^0.18.4"
65
+ "@colyseus/tools": "^0.18.4",
66
+ "@colyseus/redis-driver": "^0.18.4"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@colyseus/schema": "^5.0.8",
package/src/Room.ts CHANGED
@@ -65,6 +65,9 @@ const noneSerializer = new NoneSerializer();
65
65
  * next patch. Frozen + shared (read-only) → zero per-call allocation. */
66
66
  const AFTER_PATCH_OPTS = Object.freeze({ afterNextPatch: true });
67
67
 
68
+ /** Room properties backed by `Room.prototype` accessors. */
69
+ const ROOM_ACCESSORS = ['state', 'maxClients', 'autoDispose', 'patchRate', 'unreliablePatchRate'] as const;
70
+
68
71
  export const DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
69
72
 
70
73
  export type SimulationCallback = (deltaTime: number) => void;
@@ -258,25 +261,25 @@ export class Room<T extends RoomOptions = RoomOptions> {
258
261
  * it is locked automatically. Unless the room was explicitly locked by you via `lock()` method,
259
262
  * the room will be unlocked as soon as a client disconnects from it.
260
263
  */
261
- public maxClients: number = Infinity;
264
+ declare public maxClients: number;
262
265
  #_maxClientsReached: boolean = false;
263
- #_maxClients: number;
266
+ #_maxClients: number = Infinity;
264
267
 
265
268
  /**
266
269
  * Automatically dispose the room when last client disconnects.
267
270
  *
268
271
  * @default true
269
272
  */
270
- public autoDispose: boolean = true;
271
- #_autoDispose: boolean;
273
+ declare public autoDispose: boolean;
274
+ #_autoDispose: boolean = true;
272
275
 
273
276
  /**
274
277
  * Frequency to send the room state to connected clients, in milliseconds.
275
278
  *
276
279
  * @default 50ms (20fps)
277
280
  */
278
- public patchRate: number | null = DEFAULT_PATCH_RATE;
279
- #_patchRate: number;
281
+ declare public patchRate: number | null;
282
+ #_patchRate: number = DEFAULT_PATCH_RATE;
280
283
  #_patchInterval: NodeJS.Timeout;
281
284
 
282
285
  /**
@@ -305,7 +308,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
305
308
  * @default null — flush alongside every {@link broadcastPatch}, and only when
306
309
  * the state actually declares an `@unreliable` field.
307
310
  */
308
- public unreliablePatchRate: number | null = null;
311
+ declare public unreliablePatchRate: number | null;
309
312
  #_unreliablePatchRate: number | null = null;
310
313
  #_unreliablePatchInterval: NodeJS.Timeout;
311
314
 
@@ -329,9 +332,12 @@ export class Room<T extends RoomOptions = RoomOptions> {
329
332
  /**
330
333
  * The state instance you provided to `setState()`.
331
334
  */
332
- public state: ExtractRoomState<T>;
335
+ declare public state: ExtractRoomState<T>;
333
336
  #_state: ExtractRoomState<T>;
334
337
 
338
+ /** Until `__init()`, the prototype accessor setters only store the value. */
339
+ #_ready: boolean = false;
340
+
335
341
  /**
336
342
  * The presence instance. Check Presence API for more details.
337
343
  *
@@ -455,21 +461,30 @@ export class Room<T extends RoomOptions = RoomOptions> {
455
461
  }
456
462
 
457
463
  /**
458
- * This method is called by the MatchMaker before onCreate()
459
- * @internal
464
+ * These accessors are defined once, on the prototype, to keep rooms in V8's
465
+ * fast-properties mode.
466
+ *
467
+ * V8 reads a property of an ordinary object at a fixed offset. A few
468
+ * operations switch the object to "dictionary mode" instead, where every
469
+ * read is a hash lookup, and it stays there:
470
+ * - `delete obj.prop`, unless `prop` was the last property added;
471
+ * - redefining an existing data property as an accessor;
472
+ * - `Object.defineProperty(instance, ...)` with fresh getter/setter
473
+ * closures per instance (every room after the first).
474
+ *
475
+ * Room and Client properties are read on every message and broadcast, so
476
+ * never `delete` their properties (assign `undefined`) and never define
477
+ * accessors per instance. Check with `node --allow-natives-syntax` and
478
+ * `%HasFastProperties(room)`.
460
479
  */
461
- private __init() {
462
- this.#_state = this.state;
463
- this.#_autoDispose = this.autoDispose;
464
- this.#_patchRate = this.patchRate;
465
- this.#_unreliablePatchRate = this.unreliablePatchRate;
466
- this.#_maxClients = this.maxClients;
467
-
468
- Object.defineProperties(this, {
480
+ static {
481
+ Object.defineProperties(this.prototype, {
469
482
  state: {
470
483
  enumerable: true,
471
- get: () => this.#_state,
472
- set: (newState: ExtractRoomState<T>) => {
484
+ configurable: true,
485
+ get(this: Room<any>) { return this.#_state; },
486
+ set(this: Room<any>, newState: any) {
487
+ if (!this.#_ready) { this.#_state = newState; return; }
473
488
  if (newState?.constructor[Symbol.metadata] !== undefined || newState[$changes] !== undefined) {
474
489
  this.setSerializer(new SchemaSerializer());
475
490
  } else if ('_definition' in newState) {
@@ -485,16 +500,20 @@ export class Room<T extends RoomOptions = RoomOptions> {
485
500
 
486
501
  maxClients: {
487
502
  enumerable: true,
488
- get: () => this.#_maxClients,
489
- set: (value: number) => {
503
+ configurable: true,
504
+ get(this: Room<any>) { return this.#_maxClients; },
505
+ set(this: Room<any>, value: number) {
506
+ if (!this.#_ready) { this.#_maxClients = value; return; }
490
507
  this.setMatchmaking({ maxClients: value });
491
508
  },
492
509
  },
493
510
 
494
511
  autoDispose: {
495
512
  enumerable: true,
496
- get: () => this.#_autoDispose,
497
- set: (value: boolean) => {
513
+ configurable: true,
514
+ get(this: Room<any>) { return this.#_autoDispose; },
515
+ set(this: Room<any>, value: boolean) {
516
+ if (!this.#_ready) { this.#_autoDispose = value; return; }
498
517
  if (
499
518
  value !== this.#_autoDispose &&
500
519
  this._internalState !== RoomInternalState.DISPOSING
@@ -507,9 +526,11 @@ export class Room<T extends RoomOptions = RoomOptions> {
507
526
 
508
527
  patchRate: {
509
528
  enumerable: true,
510
- get: () => this.#_patchRate,
511
- set: (milliseconds: number) => {
529
+ configurable: true,
530
+ get(this: Room<any>) { return this.#_patchRate; },
531
+ set(this: Room<any>, milliseconds: number) {
512
532
  this.#_patchRate = milliseconds;
533
+ if (!this.#_ready) { return; }
513
534
  // clear previous interval in case called setPatchRate more than once
514
535
  if (this.#_patchInterval) {
515
536
  clearInterval(this.#_patchInterval);
@@ -526,13 +547,33 @@ export class Room<T extends RoomOptions = RoomOptions> {
526
547
 
527
548
  unreliablePatchRate: {
528
549
  enumerable: true,
529
- get: () => this.#_unreliablePatchRate,
530
- set: (milliseconds: number | null) => {
550
+ configurable: true,
551
+ get(this: Room<any>) { return this.#_unreliablePatchRate; },
552
+ set(this: Room<any>, milliseconds: number | null) {
531
553
  this.#_unreliablePatchRate = milliseconds;
554
+ if (!this.#_ready) { return; }
532
555
  this._armUnreliablePatches();
533
556
  },
534
557
  },
535
558
  });
559
+ }
560
+
561
+ /**
562
+ * This method is called by the MatchMaker before onCreate()
563
+ * @internal
564
+ */
565
+ private __init() {
566
+ // Native class fields (plain JS, type-stripped TS, `useDefineForClassFields`) shadow
567
+ // the prototype accessors: re-install them here, at the cost of this room's fast mode.
568
+ for (let i = 0; i < ROOM_ACCESSORS.length; i++) {
569
+ const key = ROOM_ACCESSORS[i];
570
+ if (!Object.hasOwn(this, key)) { continue; }
571
+ const value = this[key];
572
+ Object.defineProperty(this, key, Object.getOwnPropertyDescriptor(Room.prototype, key));
573
+ (this as any)[key] = value; // not ready yet: only stores
574
+ }
575
+
576
+ this.#_ready = true;
536
577
 
537
578
  // set patch interval, now with the setter
538
579
  this.patchRate = this.#_patchRate;
@@ -2285,7 +2326,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
2285
2326
  if (client._enqueuedMessages.length > 0) {
2286
2327
  client._enqueuedMessages.forEach((enqueued) => client.raw(enqueued));
2287
2328
  }
2288
- delete client._enqueuedMessages;
2329
+ client._enqueuedMessages = undefined; // not `delete`: keeps the client in V8 fast mode (see Room's static block)
2289
2330
 
2290
2331
  } else if (code === Protocol.PING) {
2291
2332
  client.raw(getMessageBytes[Protocol.PING]());