@colyseus/core 0.18.0 → 0.18.1

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 (125) hide show
  1. package/build/MatchMaker.cjs +3 -7
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.mjs +4 -8
  4. package/build/MatchMaker.mjs.map +2 -2
  5. package/build/Protocol.cjs +3 -3
  6. package/build/Protocol.cjs.map +1 -1
  7. package/build/Protocol.d.ts +3 -3
  8. package/build/Protocol.mjs +3 -3
  9. package/build/Protocol.mjs.map +1 -1
  10. package/build/Rewind.cjs +385 -0
  11. package/build/Rewind.cjs.map +7 -0
  12. package/build/Rewind.d.ts +291 -0
  13. package/build/Rewind.mjs +359 -0
  14. package/build/Rewind.mjs.map +7 -0
  15. package/build/Room.cjs +280 -281
  16. package/build/Room.cjs.map +3 -3
  17. package/build/Room.d.ts +185 -81
  18. package/build/Room.mjs +286 -286
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +265 -0
  21. package/build/RoomMessages.cjs.map +7 -0
  22. package/build/RoomMessages.d.ts +49 -0
  23. package/build/RoomMessages.mjs +240 -0
  24. package/build/RoomMessages.mjs.map +7 -0
  25. package/build/Server.cjs +88 -31
  26. package/build/Server.cjs.map +2 -2
  27. package/build/Server.d.ts +36 -0
  28. package/build/Server.mjs +88 -31
  29. package/build/Server.mjs.map +2 -2
  30. package/build/Transport.cjs +26 -0
  31. package/build/Transport.cjs.map +2 -2
  32. package/build/Transport.d.ts +69 -2
  33. package/build/Transport.mjs +25 -0
  34. package/build/Transport.mjs.map +2 -2
  35. package/build/errors/RoomExceptions.cjs +7 -4
  36. package/build/errors/RoomExceptions.cjs.map +2 -2
  37. package/build/errors/RoomExceptions.d.ts +15 -3
  38. package/build/errors/RoomExceptions.mjs +5 -3
  39. package/build/errors/RoomExceptions.mjs.map +2 -2
  40. package/build/index.cjs +19 -2
  41. package/build/index.cjs.map +2 -2
  42. package/build/index.d.ts +7 -4
  43. package/build/index.mjs +17 -4
  44. package/build/index.mjs.map +2 -2
  45. package/build/input/InputBuffer.cjs +439 -16
  46. package/build/input/InputBuffer.cjs.map +2 -2
  47. package/build/input/InputBuffer.d.ts +186 -96
  48. package/build/input/InputBuffer.mjs +436 -15
  49. package/build/input/InputBuffer.mjs.map +2 -2
  50. package/build/input/RoomInput.cjs +306 -0
  51. package/build/input/RoomInput.cjs.map +7 -0
  52. package/build/input/RoomInput.d.ts +91 -0
  53. package/build/input/RoomInput.mjs +287 -0
  54. package/build/input/RoomInput.mjs.map +7 -0
  55. package/build/input/types.cjs +18 -0
  56. package/build/input/types.cjs.map +7 -0
  57. package/build/input/types.d.ts +476 -0
  58. package/build/input/types.mjs +0 -0
  59. package/build/input/types.mjs.map +7 -0
  60. package/build/rooms/QueueRoom.cjs +1 -1
  61. package/build/rooms/QueueRoom.cjs.map +2 -2
  62. package/build/rooms/QueueRoom.mjs +1 -1
  63. package/build/rooms/QueueRoom.mjs.map +2 -2
  64. package/build/router/default_routes.cjs +4 -1
  65. package/build/router/default_routes.cjs.map +2 -2
  66. package/build/router/default_routes.mjs +4 -1
  67. package/build/router/default_routes.mjs.map +2 -2
  68. package/build/router/index.cjs +6 -1
  69. package/build/router/index.cjs.map +2 -2
  70. package/build/router/index.mjs +6 -1
  71. package/build/router/index.mjs.map +2 -2
  72. package/build/router/node.cjs +37 -2
  73. package/build/router/node.cjs.map +2 -2
  74. package/build/router/node.d.ts +10 -0
  75. package/build/router/node.mjs +35 -1
  76. package/build/router/node.mjs.map +2 -2
  77. package/build/serializer/NoneSerializer.cjs +2 -2
  78. package/build/serializer/NoneSerializer.cjs.map +2 -2
  79. package/build/serializer/NoneSerializer.d.ts +3 -3
  80. package/build/serializer/NoneSerializer.mjs +2 -2
  81. package/build/serializer/NoneSerializer.mjs.map +2 -2
  82. package/build/serializer/SchemaSerializer.cjs +44 -14
  83. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  84. package/build/serializer/SchemaSerializer.d.ts +3 -3
  85. package/build/serializer/SchemaSerializer.mjs +46 -16
  86. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  87. package/build/serializer/Serializer.cjs.map +1 -1
  88. package/build/serializer/Serializer.d.ts +12 -2
  89. package/build/utils/DevMode.cjs +14 -14
  90. package/build/utils/DevMode.cjs.map +2 -2
  91. package/build/utils/DevMode.d.ts +0 -2
  92. package/build/utils/DevMode.mjs +14 -12
  93. package/build/utils/DevMode.mjs.map +2 -2
  94. package/build/utils/Env.cjs +8 -4
  95. package/build/utils/Env.cjs.map +3 -3
  96. package/build/utils/Env.mjs +8 -4
  97. package/build/utils/Env.mjs.map +2 -2
  98. package/build/utils/Utils.cjs +3 -0
  99. package/build/utils/Utils.cjs.map +2 -2
  100. package/build/utils/Utils.d.ts +8 -0
  101. package/build/utils/Utils.mjs +2 -0
  102. package/build/utils/Utils.mjs.map +2 -2
  103. package/package.json +11 -11
  104. package/src/MatchMaker.ts +7 -9
  105. package/src/Protocol.ts +3 -3
  106. package/src/Rewind.ts +572 -0
  107. package/src/Room.ts +409 -448
  108. package/src/RoomMessages.ts +342 -0
  109. package/src/Server.ts +112 -38
  110. package/src/Transport.ts +97 -2
  111. package/src/errors/RoomExceptions.ts +18 -4
  112. package/src/index.ts +13 -2
  113. package/src/input/InputBuffer.ts +464 -107
  114. package/src/input/RoomInput.ts +337 -0
  115. package/src/input/types.ts +503 -0
  116. package/src/rooms/QueueRoom.ts +1 -1
  117. package/src/router/default_routes.ts +6 -1
  118. package/src/router/index.ts +6 -1
  119. package/src/router/node.ts +44 -0
  120. package/src/serializer/NoneSerializer.ts +3 -3
  121. package/src/serializer/SchemaSerializer.ts +110 -19
  122. package/src/serializer/Serializer.ts +13 -2
  123. package/src/utils/DevMode.ts +18 -13
  124. package/src/utils/Env.ts +12 -4
  125. package/src/utils/Utils.ts +9 -0
@@ -1,11 +1,85 @@
1
- import { Protocol } from '@colyseus/shared-types';
1
+ import { Protocol, ProtocolModifier } from '@colyseus/shared-types';
2
2
 
3
- import type { Serializer } from './Serializer.ts';
4
- import { type Client, ClientState } from '../Transport.ts';
3
+ import type { PatchTimingContext, Serializer } from './Serializer.ts';
4
+ import { type Client, type ClientPrivate, ClientState } from '../Transport.ts';
5
5
 
6
- import { type Iterator, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
6
+ import { type Iterator, encode, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
7
7
  import { debugPatch } from '../Debug.ts';
8
8
 
9
+ /**
10
+ * Size of the {@link ProtocolModifier.TIMED} prefix that follows the
11
+ * protocol byte:
12
+ * `[uint32 sNow][uint32 inputSeq]`
13
+ *
14
+ * - `sNow` — server clock as **milliseconds since room start**
15
+ * (`room.clock.elapsedTime`), NOT raw `performance.now()`. A portable,
16
+ * language-agnostic integer (C/C#/Lua have no `performance.now()`); the
17
+ * server's own time-keyed logic uses the SAME `clock.elapsedTime` so the
18
+ * client's reconstructed `serverNow()` stays in phase. Wraps at u32
19
+ * (~49.7 days uptime) — irrelevant per session. Drives the client clock
20
+ * offset / `serverNow()`.
21
+ * - `inputSeq` — seq VALUE of the last input CONSUMED into the authoritative
22
+ * state (the input buffer's `ackSeq`, i.e. last-PROCESSED input). Reliable:
23
+ * equals the consumed count (inputs are sequenced implicitly by receive
24
+ * order). Unreliable: the framework wire seq, so a fully-dropped input
25
+ * doesn't make the ack lag the client's sent seq by the lost count. This
26
+ * single number is the canonical reconciliation ack ("the sequence number
27
+ * of the last input the server processed"); the client prunes its
28
+ * pending-input buffer against it AND derives RTT from its round-trip
29
+ * (`now − sendTime(inputSeq)`). No separate received-seq / received-time —
30
+ * RTT-from-the-processed-ack is round-trip-inclusive, which is the standard.
31
+ */
32
+ const TIMED_PREFIX_SIZE = 8;
33
+
34
+ /**
35
+ * Construct a per-recipient TIMED buffer:
36
+ * `[code|TIMED][sNow][inputSeq][...schema bytes]`
37
+ *
38
+ * `encodedBody` is the shared encoded buffer whose byte 0 already holds the
39
+ * base protocol code (ROOM_STATE or ROOM_STATE_PATCH); bytes 1.. are the
40
+ * schema payload. We construct a fresh per-client Buffer so the transport
41
+ * never sees a buffer that might mutate between queue and send.
42
+ *
43
+ * Uses the schema `encode.*` helpers to keep the wire layout symmetric with
44
+ * the SDK's `decode.*`-driven reader.
45
+ */
46
+ function buildTimedFrame(
47
+ encodedBody: Uint8Array,
48
+ sNow: number,
49
+ inputSeq: number,
50
+ ): Buffer {
51
+ const schemaPayload = encodedBody.subarray(1);
52
+ const out = Buffer.allocUnsafe(1 + TIMED_PREFIX_SIZE + schemaPayload.length);
53
+ out[0] = encodedBody[0] | ProtocolModifier.TIMED;
54
+ const it: Iterator = { offset: 1 };
55
+ encode.uint32(out, sNow >>> 0, it); // ms since room start (clock.elapsedTime)
56
+ encode.uint32(out, inputSeq >>> 0, it);
57
+ out.set(schemaPayload, it.offset);
58
+ return out;
59
+ }
60
+
61
+ /** Seq VALUE of the last input consumed from the client's buffer — the
62
+ * reconciliation ack (and RTT correlation key). Reliable: equals the consumed
63
+ * count; unreliable: the framework wire seq (so packet loss doesn't skew it).
64
+ * 0 when the room doesn't buffer input. */
65
+ function clientProcessedInputSeq(client: Client): number {
66
+ return (client as Client & ClientPrivate)._inputBuffer?.ackSeq ?? 0;
67
+ }
68
+
69
+ /**
70
+ * Send a per-recipient patch frame: a TIMED-prefixed buffer when `timing` is set,
71
+ * else the shared `encodedBody` passed straight through (no per-client copy). Any
72
+ * `afterNextPatch` frames staged on `client._pendingFrames` are NOT coalesced here
73
+ * — they go out standalone right after the patch via `Room._flushPendingClientFrames`.
74
+ */
75
+ function sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: PatchTimingContext): void {
76
+ if (timing) {
77
+ client.raw(buildTimedFrame(encodedBody, timing.sNow, clientProcessedInputSeq(client)));
78
+ } else {
79
+ client.raw(encodedBody);
80
+ }
81
+ }
82
+
9
83
  const SHARED_VIEW = {};
10
84
 
11
85
  export class SchemaSerializer<T extends Schema> implements Serializer<T> {
@@ -38,27 +112,29 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
38
112
  }
39
113
  }
40
114
 
41
- public getFullState(client?: Client) {
115
+ public getFullState(client?: Client, timing?: PatchTimingContext) {
42
116
  if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
43
117
  this.sharedOffsetCache = { offset: 1 };
44
118
  this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
45
119
  this.needFullEncode = false;
46
120
  }
47
121
 
48
- if (this.hasFilters && client?.view) {
49
- return this.encoder.encodeAllView(
50
- client.view,
51
- this.sharedOffsetCache.offset,
52
- { ...this.sharedOffsetCache },
53
- this.fullEncodeBuffer
54
- );
55
-
56
- } else {
57
- return this.fullEncodeCache;
122
+ const body = (this.hasFilters && client?.view)
123
+ ? this.encoder.encodeAllView(
124
+ client.view,
125
+ this.sharedOffsetCache.offset,
126
+ { ...this.sharedOffsetCache },
127
+ this.fullEncodeBuffer
128
+ )
129
+ : this.fullEncodeCache;
130
+
131
+ if (timing && client) {
132
+ return buildTimedFrame(body, timing.sNow, clientProcessedInputSeq(client));
58
133
  }
134
+ return body;
59
135
  }
60
136
 
61
- public applyPatches(clients: Client[]) {
137
+ public applyPatches(clients: Client[], _state?: unknown, timing?: PatchTimingContext) {
62
138
  let numClients = clients.length;
63
139
 
64
140
  if (numClients === 0) {
@@ -92,11 +168,26 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
92
168
  this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
93
169
 
94
170
  clientsWithViewChange.forEach((client) => {
95
- client.raw(this.encoder.encodeView(client.view, sharedOffset, it));
171
+ const encodedView = this.encoder.encodeView(client.view, sharedOffset, it);
172
+ sendClientFrame(client, encodedView, timing);
96
173
  });
97
174
  }
98
175
  }
99
176
 
177
+ // Heartbeat: clients that advertised CLIENT_TIMING still need
178
+ // periodic timing samples to keep their clock offset / RTT estimate
179
+ // fresh during quiet ticks. Emit a TIMED-prefixed ROOM_STATE_PATCH
180
+ // with an empty schema body — 1 byte protocol + 12 bytes prefix.
181
+ // No-op for rooms without `defineInput()` (timing is undefined).
182
+ if (timing) {
183
+ const heartbeatBody = Buffer.from([Protocol.ROOM_STATE_PATCH]);
184
+ for (let i = 0; i < clients.length; i++) {
185
+ const client = clients[i];
186
+ if (client.state !== ClientState.JOINED) continue;
187
+ sendClientFrame(client, heartbeatBody, timing);
188
+ }
189
+ }
190
+
100
191
  // skip patching state if:
101
192
  // - no clients are connected
102
193
  // - no changes were made
@@ -129,7 +220,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
129
220
  continue;
130
221
  }
131
222
 
132
- client.raw(encodedChanges);
223
+ sendClientFrame(client, encodedChanges, timing);
133
224
  }
134
225
 
135
226
  } else {
@@ -159,7 +250,7 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
159
250
  this.encodedViews.set(view, encodedView);
160
251
  }
161
252
 
162
- client.raw(encodedView);
253
+ sendClientFrame(client, encodedView, timing);
163
254
  }
164
255
 
165
256
  // clear views
@@ -1,9 +1,20 @@
1
1
  import type { Client } from '../Transport.ts';
2
2
 
3
+ /**
4
+ * Per-tick timing context, populated by the Room when
5
+ * {@link Room.defineInput} was called. The serializer uses it to emit a
6
+ * {@link ProtocolModifier.TIMED} prefix for clients that advertised
7
+ * the {@link HandshakeSection.CLIENT_TIMING} capability.
8
+ */
9
+ export interface PatchTimingContext {
10
+ /** `performance.now()` at the moment the patch is being applied. */
11
+ sNow: number;
12
+ }
13
+
3
14
  export interface Serializer<T> {
4
15
  id: string;
5
16
  reset(data: any): void;
6
- getFullState(client?: Client): Uint8Array;
7
- applyPatches(clients: Client[], state: T): boolean;
17
+ getFullState(client?: Client, timing?: PatchTimingContext): Uint8Array;
18
+ applyPatches(clients: Client[], state: T, timing?: PatchTimingContext): boolean;
8
19
  handshake?(): Uint8Array;
9
20
  }
@@ -31,11 +31,24 @@ export async function reloadFromCache() {
31
31
  debugDevMode("rooms to restore: %i", roomHistoryList.length);
32
32
 
33
33
  for (const [roomId, value] of roomHistoryList) {
34
- const roomHistory = JSON.parse(value);
35
- debugDevMode("restoring room %s (%s)", roomHistory.roomName, roomId);
36
-
37
- const recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);
38
- const recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);
34
+ let roomHistory: any;
35
+ let recreatedRoom: Room;
36
+ let recreatedRoomListing: any;
37
+
38
+ try {
39
+ roomHistory = JSON.parse(value);
40
+ debugDevMode("restoring room %s (%s)", roomHistory.roomName, roomId);
41
+
42
+ recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);
43
+ recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);
44
+
45
+ } catch (e: any) {
46
+ // unrestorable entry (e.g. room type renamed/removed) — drop it
47
+ // instead of wedging the boot
48
+ debugAndPrintError(`❌ couldn't restore room '${roomId}':\n${e.stack}`);
49
+ await presence.hdel(getRoomRestoreListKey(), roomId);
50
+ continue;
51
+ }
39
52
 
40
53
  // Restore previous state
41
54
  if (roomHistory.hasOwnProperty("state")) {
@@ -144,14 +157,6 @@ export async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {
144
157
  }
145
158
  }
146
159
 
147
- export async function getPreviousProcessId(hostname: string = '') {
148
- return await presence.hget(getProcessRestoreKey(), hostname);
149
- }
150
-
151
160
  export function getRoomRestoreListKey() {
152
161
  return 'roomhistory';
153
162
  }
154
-
155
- export function getProcessRestoreKey() {
156
- return 'processhistory';
157
- }
package/src/utils/Env.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import os from 'os';
1
2
  import { LocalPresence } from '../presence/LocalPresence.ts';
2
3
  import { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';
3
4
  import { logger } from '../Logger.ts';
@@ -7,16 +8,23 @@ export function isColyseusCloud(): boolean {
7
8
  return process.env.COLYSEUS_CLOUD !== undefined;
8
9
  }
9
10
 
11
+ // On Colyseus Cloud, only opt into Redis when there's more than one CPU
12
+ // (multi-process deploys) or when REDIS_URI is explicitly provided.
13
+ // Single-CPU instances without REDIS_URI keep using Local presence/driver.
14
+ function shouldUseRedisOnCloud(): boolean {
15
+ return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;
16
+ }
17
+
10
18
  export async function getDefaultPresence(): Promise<Presence> {
11
- if (isColyseusCloud()) {
19
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
12
20
  try {
13
21
  const RedisPresence = await import('@colyseus/redis-presence');
14
22
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
15
23
  } catch (e) {
16
24
  console.error(e);
17
25
  logger.warn("");
18
- logger.warn("❌ could not initialize RedisDriver.");
19
- logger.warn("👉 npm install --save @colyseus/redis-driver");
26
+ logger.warn("❌ could not initialize RedisPresence.");
27
+ logger.warn("👉 npm install --save @colyseus/redis-presence");
20
28
  logger.warn("");
21
29
  }
22
30
  }
@@ -24,7 +32,7 @@ export async function getDefaultPresence(): Promise<Presence> {
24
32
  }
25
33
 
26
34
  export async function getDefaultDriver(): Promise<MatchMakerDriver> {
27
- if (isColyseusCloud()) {
35
+ if (isColyseusCloud() && shouldUseRedisOnCloud()) {
28
36
  try {
29
37
  const RedisDriver = await import('@colyseus/redis-driver');
30
38
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);
@@ -26,6 +26,15 @@ export type ExtractMethodOrPropertyType<
26
26
  export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);
27
27
  export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
28
28
 
29
+ /**
30
+ * The well-known `Symbol.metadata` (TC39 decorator metadata), falling back to the
31
+ * registered symbol on runtimes that don't expose it globally yet. `@colyseus/schema`
32
+ * stores each class's field metadata under this key — read it via
33
+ * `instance.constructor[$METADATA]`. Shared so every reader resolves the IDENTICAL
34
+ * symbol (InputBuffer's field-name walk, Rewind's field-index lookup).
35
+ */
36
+ export const $METADATA: symbol = (Symbol as { metadata?: symbol }).metadata ?? Symbol.for("Symbol.metadata");
37
+
29
38
  export function generateId(length: number = 9) {
30
39
  return nanoid(length);
31
40
  }