@colyseus/core 0.18.16 → 0.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/MatchMaker.cjs +93 -10
- package/build/MatchMaker.cjs.map +3 -3
- package/build/MatchMaker.d.ts +20 -1
- package/build/MatchMaker.mjs +93 -11
- package/build/MatchMaker.mjs.map +3 -3
- package/build/Room.cjs +69 -68
- package/build/Room.cjs.map +2 -2
- package/build/Room.mjs +69 -68
- package/build/Room.mjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +14 -1
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +2 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +14 -1
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +11 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.cjs +2 -0
- package/build/presence/LocalPresence.cjs.map +2 -2
- package/build/presence/LocalPresence.mjs +2 -0
- package/build/presence/LocalPresence.mjs.map +2 -2
- package/build/utils/DevMode.cjs +3 -0
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.mjs +4 -1
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/Utils.cjs +6 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +2 -0
- package/build/utils/Utils.mjs +4 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/MatchMaker.ts +174 -16
- package/src/Room.ts +72 -30
- package/src/matchmaker/LocalDriver/LocalDriver.ts +15 -7
- package/src/matchmaker/driver.ts +12 -0
- package/src/presence/LocalPresence.ts +2 -0
- package/src/utils/DevMode.ts +4 -1
- package/src/utils/Utils.ts +6 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
459
|
-
*
|
|
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
|
-
|
|
462
|
-
this
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
511
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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;
|
|
@@ -2187,7 +2228,8 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
2187
2228
|
async #_dispose(): Promise<any> {
|
|
2188
2229
|
this._internalState = RoomInternalState.DISPOSING;
|
|
2189
2230
|
|
|
2190
|
-
//
|
|
2231
|
+
// no row to remove: roomId is unset if onCreate() failed, and cleared when the
|
|
2232
|
+
// row belongs to another process (see `recordRoom`)
|
|
2191
2233
|
if (this._listing?.roomId !== undefined) {
|
|
2192
2234
|
await matchMaker.driver.remove(this._listing.roomId);
|
|
2193
2235
|
}
|
|
@@ -2284,7 +2326,7 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
2284
2326
|
if (client._enqueuedMessages.length > 0) {
|
|
2285
2327
|
client._enqueuedMessages.forEach((enqueued) => client.raw(enqueued));
|
|
2286
2328
|
}
|
|
2287
|
-
|
|
2329
|
+
client._enqueuedMessages = undefined; // not `delete`: keeps the client in V8 fast mode (see Room's static block)
|
|
2288
2330
|
|
|
2289
2331
|
} else if (code === Protocol.PING) {
|
|
2290
2332
|
client.raw(getMessageBytes[Protocol.PING]());
|
|
@@ -8,8 +8,11 @@ export type { IRoomCache, SortOptions, MatchMakerDriver };
|
|
|
8
8
|
export class LocalDriver implements MatchMakerDriver {
|
|
9
9
|
public rooms: IRoomCache[] = [];
|
|
10
10
|
|
|
11
|
+
// the roomIds in `rooms`: keeps has() and insert() O(1) however many rooms there are
|
|
12
|
+
private roomIds = new Set<string>();
|
|
13
|
+
|
|
11
14
|
public has(roomId: string) {
|
|
12
|
-
return this.
|
|
15
|
+
return this.roomIds.has(roomId);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
|
|
@@ -70,15 +73,18 @@ export class LocalDriver implements MatchMakerDriver {
|
|
|
70
73
|
return true;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
public
|
|
74
|
-
|
|
75
|
-
// // already in the list
|
|
76
|
-
// return true;
|
|
77
|
-
// }
|
|
76
|
+
public async insert(room: IRoomCache) {
|
|
77
|
+
if (this.has(room.roomId)) { return false; }
|
|
78
78
|
|
|
79
|
+
this.roomIds.add(room.roomId);
|
|
80
|
+
this.rooms.push(room);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public persist(room: IRoomCache, create: boolean = false) {
|
|
79
85
|
if (!create) { return false; }
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
this.roomIds.add(room.roomId);
|
|
82
88
|
this.rooms.push(room);
|
|
83
89
|
|
|
84
90
|
return true;
|
|
@@ -88,6 +94,7 @@ export class LocalDriver implements MatchMakerDriver {
|
|
|
88
94
|
const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
|
|
89
95
|
if (roomIndex !== -1) {
|
|
90
96
|
this.rooms.splice(roomIndex, 1);
|
|
97
|
+
this.roomIds.delete(roomId);
|
|
91
98
|
return true;
|
|
92
99
|
}
|
|
93
100
|
return false;
|
|
@@ -95,6 +102,7 @@ export class LocalDriver implements MatchMakerDriver {
|
|
|
95
102
|
|
|
96
103
|
public clear() {
|
|
97
104
|
this.rooms = [];
|
|
105
|
+
this.roomIds.clear();
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
public shutdown() {
|
package/src/matchmaker/driver.ts
CHANGED
|
@@ -186,6 +186,18 @@ export interface MatchMakerDriver {
|
|
|
186
186
|
*/
|
|
187
187
|
persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Record a room, unless its `roomId` is already recorded. Must be atomic, and
|
|
191
|
+
* must return false rather than throwing when the `roomId` is taken.
|
|
192
|
+
*
|
|
193
|
+
* Matchmaking uses this to settle which process owns a room when one create
|
|
194
|
+
* request is answered twice. Drivers that don't implement it fall back to
|
|
195
|
+
* `persist(room, true)`, where the second write silently wins.
|
|
196
|
+
*
|
|
197
|
+
* TODO: required on 1.0, replacing `persist()`'s `create` flag.
|
|
198
|
+
*/
|
|
199
|
+
insert?(room: IRoomCache): Promise<boolean>;
|
|
200
|
+
|
|
189
201
|
/**
|
|
190
202
|
* Empty the room cache. Used for testing purposes only.
|
|
191
203
|
* May be asynchronous — callers must await it.
|
|
@@ -107,6 +107,8 @@ export class LocalPresence implements Presence {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
public del(key: string) {
|
|
110
|
+
clearTimeout(this.timeouts[key]); // a pending expiry would wipe whatever is set next
|
|
111
|
+
delete this.timeouts[key];
|
|
110
112
|
delete this.keys[key];
|
|
111
113
|
delete this.data[key];
|
|
112
114
|
delete this.hash[key];
|
package/src/utils/DevMode.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType, $changes } from '@colyseus/schema';
|
|
4
4
|
import { logger } from '../Logger.ts';
|
|
5
5
|
import { debugAndPrintError, debugDevMode } from '../Debug.ts';
|
|
6
|
-
import { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';
|
|
6
|
+
import { getLocalRoomById, handleCreateRoom, MatchMakerState, presence, remoteRoomCall, state } from '../MatchMaker.ts';
|
|
7
7
|
import type { Room } from '../Room.ts';
|
|
8
8
|
|
|
9
9
|
const DEVMODE_CACHE_FILE_PATH = path.resolve(".devmode.json");
|
|
@@ -49,6 +49,9 @@ export async function reloadFromCache() {
|
|
|
49
49
|
recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);
|
|
50
50
|
|
|
51
51
|
} catch (e: any) {
|
|
52
|
+
// shutdown interrupted the restore: keep the remaining entries for the next boot
|
|
53
|
+
if (state === MatchMakerState.SHUTTING_DOWN) { break; }
|
|
54
|
+
|
|
52
55
|
// unrestorable entry (e.g. room type renamed/removed) — drop it
|
|
53
56
|
// instead of wedging the boot
|
|
54
57
|
debugAndPrintError(`❌ couldn't restore room '${roomId}':\n${e.stack}`);
|
package/src/utils/Utils.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type RemoteRoomCallReturn<
|
|
|
44
44
|
export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);
|
|
45
45
|
export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
|
|
46
46
|
|
|
47
|
+
// a peer that removes this process's room listings leaves a receipt. this
|
|
48
|
+
// process looks for it every INTERVAL (ms) and restores them. the TTL (seconds)
|
|
49
|
+
// only bounds what dead processes leave behind.
|
|
50
|
+
export const SWEPT_CHECK_INTERVAL = Number(process.env.COLYSEUS_SWEPT_CHECK_INTERVAL || 5000);
|
|
51
|
+
export const SWEPT_RECEIPT_TTL = Number(process.env.COLYSEUS_SWEPT_RECEIPT_TTL || 600);
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the
|
|
49
55
|
* registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`
|