@colyseus/core 0.18.6 → 0.18.8

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/src/Server.ts CHANGED
@@ -11,7 +11,7 @@ import { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './
11
11
 
12
12
  import type { Presence } from "./presence/Presence.ts";
13
13
 
14
- import { setTransport, Transport } from './Transport.ts';
14
+ import { getTransport, setTransport, Transport } from './Transport.ts';
15
15
  import { logger, setLogger } from './Logger.ts';
16
16
  import { setDevMode, isDevMode } from './utils/DevMode.ts';
17
17
  import { type Router, bindRouterToTransport, createRouter } from './router/index.ts';
@@ -125,8 +125,6 @@ export class Server<
125
125
 
126
126
  protected _onTransportReady = new Deferred<Transport>();
127
127
 
128
- private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
129
-
130
128
  // Implicit default for callers that omit explicit Server reference — e.g.
131
129
  // `playground()` reads `Server.current.router.endpoints` at request time.
132
130
  // Last-construction wins; multi-server setups should reference instances
@@ -384,7 +382,7 @@ export class Server<
384
382
  // this is going to lock all rooms and wait for them to be disposed
385
383
  await matchMaker.gracefullyShutdown();
386
384
 
387
- this.transport.shutdown();
385
+ this.transport?.shutdown();
388
386
  this.presence?.shutdown();
389
387
  await this.driver?.shutdown();
390
388
 
@@ -403,29 +401,19 @@ export class Server<
403
401
 
404
402
  /**
405
403
  * Add simulated latency between client and server.
404
+ *
405
+ * May be called at any time — before or after `listen()`. Also available
406
+ * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which
407
+ * overrides calls made before the server boots.
408
+ *
406
409
  * @param milliseconds round trip latency in milliseconds.
407
410
  */
408
411
  public simulateLatency(milliseconds: number) {
409
- if (milliseconds > 0) {
410
- logger.warn(`📶️❗ Colyseus latency simulation enabled → ${milliseconds}ms latency for round trip.`);
411
- } else {
412
- logger.warn(`📶️❗ Colyseus latency simulation disabled.`);
413
- }
414
-
415
- const halfwayMS = (milliseconds / 2);
416
- this.transport.simulateLatency(halfwayMS);
417
-
418
- if (this._originalRoomOnMessage == null) {
419
- this._originalRoomOnMessage = Room.prototype['_onMessage'];
420
- }
421
-
422
- const originalOnMessage = this._originalRoomOnMessage;
423
-
424
- Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
425
- // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
426
- const cachedBuffer = Buffer.from(buffer);
427
- setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
428
- };
412
+ // transport may still be resolving (constructor's attach() imports the default one)
413
+ this._onTransportReady.then(
414
+ (transport) => applySimulatedLatency(transport, milliseconds),
415
+ () => {/* missing-transport error already surfaces via attach()/listen() */},
416
+ );
429
417
  }
430
418
 
431
419
  /**
@@ -447,6 +435,11 @@ export class Server<
447
435
  private async _bootServices(): Promise<void> {
448
436
  const { beforeListen, database, express } = this.options;
449
437
 
438
+ // boot runs after module evaluation, so the env var wins over
439
+ // simulateLatency() calls made at the top level of user code
440
+ const envLatency = parseLatencyEnv();
441
+ if (envLatency !== undefined) { this.simulateLatency(envLatency); }
442
+
450
443
  if (beforeListen) { await beforeListen(); }
451
444
  if (database) { await database.boot(); }
452
445
 
@@ -505,6 +498,55 @@ export class Server<
505
498
  () => Promise.resolve()
506
499
  }
507
500
 
501
+ let _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
502
+
503
+ /**
504
+ * Applies both halves of the round-trip latency simulation: delays the
505
+ * transport's outgoing messages and the Room's incoming message handling.
506
+ *
507
+ * Prefer `server.simulateLatency()` — this is the shared core behind it, the
508
+ * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where
509
+ * no `Server` instance (or transport, yet) may exist.
510
+ */
511
+ export function applySimulatedLatency(transport: Transport | undefined, milliseconds: number) {
512
+ if (milliseconds > 0) {
513
+ logger.warn(`📶️❗ Colyseus latency simulation enabled → ${milliseconds}ms latency for round trip.`);
514
+ } else {
515
+ logger.warn(`📶️❗ Colyseus latency simulation disabled.`);
516
+ }
517
+
518
+ const halfwayMS = (milliseconds / 2);
519
+ transport?.simulateLatency(halfwayMS);
520
+
521
+ if (_originalRoomOnMessage == null) {
522
+ _originalRoomOnMessage = Room.prototype['_onMessage'];
523
+ }
524
+
525
+ const originalOnMessage = _originalRoomOnMessage;
526
+
527
+ Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
528
+ // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
529
+ const cachedBuffer = Buffer.from(buffer);
530
+ setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).
536
+ * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.
537
+ */
538
+ export function parseLatencyEnv(): number | undefined {
539
+ const value = process.env.COLYSEUS_LATENCY;
540
+ if (value === undefined || value === '') { return undefined; }
541
+
542
+ const milliseconds = Number(value);
543
+ if (isNaN(milliseconds)) {
544
+ logger.warn(`📶️❗ Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);
545
+ return undefined;
546
+ }
547
+ return milliseconds;
548
+ }
549
+
508
550
  export type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;
509
551
 
510
552
  function isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {
@@ -562,6 +604,8 @@ export function defineServer<
562
604
  options: serverOptions,
563
605
  router: routes,
564
606
  '~rooms': rooms,
607
+ // the vite plugin registers the transport before importing user code
608
+ simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),
565
609
  } as unknown as Server<T, R>;
566
610
  }
567
611
 
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ export {
18
18
  } from '@colyseus/shared-types';
19
19
 
20
20
  // Core classes
21
- export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
21
+ export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
22
22
  export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
23
23
  export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
24
24
  export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
@@ -10,12 +10,13 @@ type Callback = (...args: any[]) => void;
10
10
  export class LocalPresence implements Presence {
11
11
  public subscriptions: EventEmitter = new EventEmitter();
12
12
 
13
- public data: {[roomName: string]: string[]} = {};
14
- public hash: {[roomName: string]: {[key: string]: string}} = {};
13
+ // null-proto: keys are caller-controlled, must never resolve through Object.prototype
14
+ public data: {[roomName: string]: string[]} = Object.create(null);
15
+ public hash: {[roomName: string]: {[key: string]: string}} = Object.create(null);
15
16
 
16
- public keys: {[name: string]: string | number} = {};
17
+ public keys: {[name: string]: string | number} = Object.create(null);
17
18
 
18
- private timeouts: {[name: string]: NodeJS.Timeout} = {};
19
+ private timeouts: {[name: string]: NodeJS.Timeout} = Object.create(null);
19
20
 
20
21
  constructor() {
21
22
  //
@@ -140,7 +141,7 @@ export class LocalPresence implements Presence {
140
141
  }
141
142
 
142
143
  public async sinter(...keys: string[]) {
143
- const intersection: {[value: string]: number} = {};
144
+ const intersection: {[value: string]: number} = Object.create(null);
144
145
 
145
146
  for (let i = 0, l = keys.length; i < l; i++) {
146
147
  (await this.smembers(keys[i])).forEach((member) => {
@@ -161,13 +162,13 @@ export class LocalPresence implements Presence {
161
162
  }
162
163
 
163
164
  public hset(key: string, field: string, value: string) {
164
- if (!this.hash[key]) { this.hash[key] = {}; }
165
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
165
166
  this.hash[key][field] = value;
166
167
  return Promise.resolve(true);
167
168
  }
168
169
 
169
170
  public hincrby(key: string, field: string, incrBy: number) {
170
- if (!this.hash[key]) { this.hash[key] = {}; }
171
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
171
172
  let value = Number(this.hash[key][field] || '0');
172
173
  value += incrBy;
173
174
  this.hash[key][field] = value.toString();
@@ -175,7 +176,7 @@ export class LocalPresence implements Presence {
175
176
  }
176
177
 
177
178
  public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {
178
- if (!this.hash[key]) { this.hash[key] = {}; }
179
+ if (!this.hash[key]) { this.hash[key] = Object.create(null); }
179
180
  let value = Number(this.hash[key][field] || '0');
180
181
  value += incrBy;
181
182
  this.hash[key][field] = value.toString();
@@ -202,7 +203,7 @@ export class LocalPresence implements Presence {
202
203
  }
203
204
 
204
205
  public async hgetall(key: string) {
205
- return this.hash[key] || {};
206
+ return { ...this.hash[key] }; // fresh plain object, like redis
206
207
  }
207
208
 
208
209
  public hdel(key: string, field: any) {
@@ -14,8 +14,14 @@ export function hasDevModeCache() {
14
14
  return fs.existsSync(DEVMODE_CACHE_FILE_PATH);
15
15
  }
16
16
 
17
+ // presence dictionaries are keyed by caller-controlled strings: rebuild them null-proto
18
+ const nullProto = (_: string, value: any) =>
19
+ (value && typeof value === 'object' && !Array.isArray(value))
20
+ ? Object.assign(Object.create(null), value)
21
+ : value;
22
+
17
23
  export function getDevModeCache() {
18
- return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8')) || {};
24
+ return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8'), nullProto) || {};
19
25
  }
20
26
 
21
27
  export function writeDevModeCache(cache: any) {