@colyseus/core 0.17.43 → 0.18.0

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 (95) hide show
  1. package/build/MatchMaker.cjs +19 -6
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +18 -6
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Room.cjs +296 -19
  12. package/build/Room.cjs.map +3 -3
  13. package/build/Room.d.ts +186 -3
  14. package/build/Room.mjs +303 -21
  15. package/build/Room.mjs.map +3 -3
  16. package/build/RoomPlugin.cjs +252 -0
  17. package/build/RoomPlugin.cjs.map +7 -0
  18. package/build/RoomPlugin.d.ts +271 -0
  19. package/build/RoomPlugin.mjs +220 -0
  20. package/build/RoomPlugin.mjs.map +7 -0
  21. package/build/Server.cjs +49 -15
  22. package/build/Server.cjs.map +2 -2
  23. package/build/Server.d.ts +25 -0
  24. package/build/Server.mjs +50 -16
  25. package/build/Server.mjs.map +2 -2
  26. package/build/Transport.cjs +38 -2
  27. package/build/Transport.cjs.map +2 -2
  28. package/build/Transport.d.ts +40 -4
  29. package/build/Transport.mjs +38 -2
  30. package/build/Transport.mjs.map +2 -2
  31. package/build/index.cjs +11 -2
  32. package/build/index.cjs.map +2 -2
  33. package/build/index.d.ts +2 -1
  34. package/build/index.mjs +12 -2
  35. package/build/index.mjs.map +2 -2
  36. package/build/input/InputBuffer.cjs +113 -0
  37. package/build/input/InputBuffer.cjs.map +7 -0
  38. package/build/input/InputBuffer.d.ts +136 -0
  39. package/build/input/InputBuffer.mjs +86 -0
  40. package/build/input/InputBuffer.mjs.map +7 -0
  41. package/build/internal.cjs +61 -0
  42. package/build/internal.cjs.map +7 -0
  43. package/build/internal.d.ts +9 -0
  44. package/build/internal.mjs +29 -0
  45. package/build/internal.mjs.map +7 -0
  46. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  47. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  48. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  49. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  50. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  51. package/build/matchmaker/driver.cjs.map +1 -1
  52. package/build/matchmaker/driver.d.ts +12 -0
  53. package/build/matchmaker/driver.mjs.map +1 -1
  54. package/build/presence/LocalPresence.d.ts +1 -1
  55. package/build/rooms/LobbyRoom.cjs +8 -10
  56. package/build/rooms/LobbyRoom.cjs.map +2 -2
  57. package/build/rooms/LobbyRoom.d.ts +4 -3
  58. package/build/rooms/LobbyRoom.mjs +8 -10
  59. package/build/rooms/LobbyRoom.mjs.map +2 -2
  60. package/build/rooms/RelayRoom.cjs +12 -16
  61. package/build/rooms/RelayRoom.cjs.map +2 -2
  62. package/build/rooms/RelayRoom.d.ts +32 -11
  63. package/build/rooms/RelayRoom.mjs +10 -16
  64. package/build/rooms/RelayRoom.mjs.map +2 -2
  65. package/build/router/index.cjs +65 -4
  66. package/build/router/index.cjs.map +2 -2
  67. package/build/router/index.d.ts +30 -6
  68. package/build/router/index.mjs +66 -6
  69. package/build/router/index.mjs.map +3 -3
  70. package/build/utils/Env.cjs +4 -8
  71. package/build/utils/Env.cjs.map +3 -3
  72. package/build/utils/Env.mjs +4 -8
  73. package/build/utils/Env.mjs.map +2 -2
  74. package/build/utils/UserSessionIndex.cjs +162 -0
  75. package/build/utils/UserSessionIndex.cjs.map +7 -0
  76. package/build/utils/UserSessionIndex.d.ts +166 -0
  77. package/build/utils/UserSessionIndex.mjs +130 -0
  78. package/build/utils/UserSessionIndex.mjs.map +7 -0
  79. package/package.json +20 -15
  80. package/src/MatchMaker.ts +40 -6
  81. package/src/Protocol.ts +130 -59
  82. package/src/Room.ts +475 -22
  83. package/src/RoomPlugin.ts +563 -0
  84. package/src/Server.ts +81 -22
  85. package/src/Transport.ts +76 -8
  86. package/src/index.ts +10 -1
  87. package/src/input/InputBuffer.ts +192 -0
  88. package/src/internal.ts +46 -0
  89. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  90. package/src/matchmaker/driver.ts +13 -0
  91. package/src/rooms/LobbyRoom.ts +12 -8
  92. package/src/rooms/RelayRoom.ts +9 -15
  93. package/src/router/index.ts +112 -11
  94. package/src/utils/Env.ts +4 -12
  95. package/src/utils/UserSessionIndex.ts +311 -0
package/src/Protocol.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Packr } from '@colyseus/msgpackr';
1
+ import { Packr, RESERVE_START_SPACE } from 'msgpackr';
2
2
  import { encode, type Iterator } from '@colyseus/schema';
3
3
  import { Protocol } from '@colyseus/shared-types';
4
4
 
@@ -11,101 +11,172 @@ export const IpcProtocol = {
11
11
  export type IpcProtocol = typeof IpcProtocol[keyof typeof IpcProtocol];
12
12
 
13
13
  const packr = new Packr({
14
- useRecords: false, // increased compatibility with decoders other than "msgpackr"
14
+ useRecords: false, // interop with non-msgpackr decoders
15
15
  });
16
16
 
17
- // msgpackr workaround: initialize buffer
18
- packr.encode(undefined);
17
+ // Buffer for assembling outgoing frames; keeps us off msgpackr's internal
18
+ // buffer so `core` can use the upstream package directly.
19
+ let frameBuffer = Buffer.allocUnsafe(8192);
19
20
 
20
- export const getMessageBytes = {
21
- [Protocol.JOIN_ROOM]: (reconnectionToken: string, serializerId: string, handshake?: Uint8Array) => {
22
- const it: Iterator = { offset: 1 };
23
- packr.buffer[0] = Protocol.JOIN_ROOM;
24
-
25
- packr.buffer[it.offset++] = Buffer.byteLength(reconnectionToken, "utf8");
26
- encode.utf8Write(packr.buffer, reconnectionToken, it);
21
+ // Grow to `capacity`, preserving written bytes (`rawMessage` writes the header
22
+ // before the payload size is known).
23
+ function ensureFrameCapacity(capacity: number) {
24
+ if (capacity > frameBuffer.byteLength) {
25
+ const next = Buffer.allocUnsafe(capacity);
26
+ frameBuffer.copy(next);
27
+ frameBuffer = next;
28
+ }
29
+ }
27
30
 
28
- packr.buffer[it.offset++] = Buffer.byteLength(serializerId, "utf8");
29
- encode.utf8Write(packr.buffer, serializerId, it);
31
+ // Shared across calls to avoid a per-call allocation on the hot send path.
32
+ // Safe: encode.* mutate `.offset` in place and never re-enter this module.
33
+ const it: Iterator = { offset: 0 };
30
34
 
35
+ export const getMessageBytes = {
36
+ /**
37
+ * Build the JOIN_ROOM payload.
38
+ *
39
+ * Wire layout:
40
+ * [JOIN_ROOM byte][rt-len][rt][sid-len][sid][stateReflectionLen varint][stateReflection][...sections]
41
+ *
42
+ * The varint length prefix on `stateReflection` is required because the
43
+ * schema decoder on the SDK side runs `while (offset < bytes.byteLength)`
44
+ * — without a boundary it consumes past the state reflection into the
45
+ * trailing tagged-section bytes, producing "definition mismatch" warnings.
46
+ * Length is `0` when no state reflection is present.
47
+ *
48
+ * Each trailing section is `[tag (uint8)][length (varint)][payload]`. The
49
+ * SDK skips unknown tags via `length`, so new sections can be added without
50
+ * breaking older clients. See {@link HandshakeSection} in shared-types.
51
+ */
52
+ [Protocol.JOIN_ROOM]: (
53
+ reconnectionToken: string,
54
+ serializerId: string,
55
+ handshake?: Uint8Array,
56
+ extraSections?: Array<{ tag: number; bytes: Uint8Array }>,
57
+ ) => {
58
+ const reconnectionTokenLength = Buffer.byteLength(reconnectionToken, "utf8");
59
+ const serializerIdLength = Buffer.byteLength(serializerId, "utf8");
31
60
  let handshakeLength = handshake?.byteLength || 0;
32
61
 
33
- // check if buffer needs to be resized
34
- if (handshakeLength > packr.buffer.byteLength - it.offset) {
35
- packr.useBuffer(Buffer.alloc(it.offset + handshakeLength, packr.buffer));
62
+ // per section: 1 tag byte + 9 (max varint) + payload
63
+ let extraLength = 0;
64
+ if (extraSections !== undefined) {
65
+ for (let i = 0; i < extraSections.length; i++) {
66
+ extraLength += 1 + 9 + extraSections[i].bytes.byteLength;
67
+ }
36
68
  }
37
69
 
70
+ // capacity: header + 9 (handshake-len varint) + handshake + sections
71
+ ensureFrameCapacity(1 + 1 + reconnectionTokenLength + 1 + serializerIdLength + 9 + handshakeLength + extraLength);
72
+
73
+ it.offset = 1;
74
+ frameBuffer[0] = Protocol.JOIN_ROOM;
75
+
76
+ frameBuffer[it.offset++] = reconnectionTokenLength;
77
+ encode.utf8Write(frameBuffer, reconnectionToken, it);
78
+
79
+ frameBuffer[it.offset++] = serializerIdLength;
80
+ encode.utf8Write(frameBuffer, serializerId, it);
81
+
82
+ encode.number(frameBuffer, handshakeLength, it);
38
83
  if (handshakeLength > 0) {
39
- packr.buffer.set(handshake, it.offset);
84
+ frameBuffer.set(handshake, it.offset);
85
+ it.offset += handshakeLength;
86
+ }
87
+
88
+ if (extraSections !== undefined) {
89
+ for (let i = 0; i < extraSections.length; i++) {
90
+ const section = extraSections[i];
91
+ frameBuffer[it.offset++] = section.tag;
92
+ encode.number(frameBuffer, section.bytes.byteLength, it);
93
+ frameBuffer.set(section.bytes, it.offset);
94
+ it.offset += section.bytes.byteLength;
95
+ }
40
96
  }
41
97
 
42
- return Buffer.from(packr.buffer.subarray(0, it.offset + handshakeLength));
98
+ return Buffer.from(frameBuffer.subarray(0, it.offset));
43
99
  },
44
100
 
45
101
  [Protocol.ERROR]: (code: number, message: string = '') => {
46
- const it: Iterator = { offset: 1 };
47
- packr.buffer[0] = Protocol.ERROR;
102
+ // capacity: 1 + code varint + length-prefixed message
103
+ ensureFrameCapacity(1 + 9 + 9 + Buffer.byteLength(message, "utf8"));
104
+
105
+ it.offset = 1;
106
+ frameBuffer[0] = Protocol.ERROR;
48
107
 
49
- encode.number(packr.buffer, code, it);
50
- encode.string(packr.buffer, message, it);
108
+ encode.number(frameBuffer, code, it);
109
+ encode.string(frameBuffer, message, it);
51
110
 
52
- return Buffer.from(packr.buffer.subarray(0, it.offset));
111
+ return Buffer.from(frameBuffer.subarray(0, it.offset));
53
112
  },
54
113
 
55
114
  [Protocol.ROOM_STATE]: (bytes: number[]) => {
56
115
  return [Protocol.ROOM_STATE, ...bytes];
57
116
  },
58
117
 
118
+ /**
119
+ * Reply to a client {@link Protocol.ROOM_REQUEST}.
120
+ *
121
+ * Wire layout: `[ROOM_RESPONSE byte][requestId varint][status uint8][msgpack payload?]`
122
+ *
123
+ * `requestId` is opaque — echoed back exactly as the SDK sent it so the
124
+ * pending callback/promise can be correlated. `status` is a
125
+ * {@link ResponseStatus} (0 = OK, 1 = ERROR). The payload is omitted when a
126
+ * handler resolves with `undefined`. Returns a fresh Buffer copy for the
127
+ * same back-pressure reason documented on `raw` below.
128
+ */
129
+ [Protocol.ROOM_RESPONSE]: (requestId: number, status: number, message?: any): Buffer => {
130
+ it.offset = 1;
131
+ frameBuffer[0] = Protocol.ROOM_RESPONSE;
132
+
133
+ encode.number(frameBuffer, requestId, it);
134
+ frameBuffer[it.offset++] = status;
135
+ const headerLength = it.offset;
136
+
137
+ if (message !== undefined) {
138
+ // reserve `headerLength` bytes up front in the pack output for the header
139
+ const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
140
+ packed.set(frameBuffer.subarray(0, headerLength), 0);
141
+ return Buffer.from(packed);
142
+ }
143
+
144
+ return Buffer.from(frameBuffer.subarray(0, headerLength));
145
+ },
146
+
59
147
  [Protocol.PING]: () => {
60
- packr.buffer[0] = Protocol.PING;
61
- return Buffer.from(packr.buffer.subarray(0, 1));
148
+ frameBuffer[0] = Protocol.PING;
149
+ return Buffer.from(frameBuffer.subarray(0, 1));
62
150
  },
63
151
 
64
- raw: (code: Protocol, type: string | number, message?: any, rawMessage?: Uint8Array | Buffer) => {
65
- const it: Iterator = { offset: 1 };
66
- packr.buffer[0] = code;
152
+ // Returns a fresh copy: frameBuffer/packr are reused next call, but callers pass
153
+ // the result to async consumers (Node retains it for libuv writev across ticks).
154
+ // See benchmark/test-buffer-corruption.ts for the back-pressure repro.
155
+ raw: (code: Protocol, type: string | number, message?: any, rawMessage?: Uint8Array | Buffer): Buffer => {
156
+ it.offset = 1;
157
+ frameBuffer[0] = code;
67
158
 
68
159
  if (typeof (type) === 'string') {
69
- encode.string(packr.buffer, type, it);
160
+ encode.string(frameBuffer, type, it);
70
161
 
71
162
  } else {
72
- encode.number(packr.buffer, type, it);
163
+ encode.number(frameBuffer, type, it);
73
164
  }
165
+ const headerLength = it.offset;
74
166
 
75
167
  if (message !== undefined) {
76
- // force to encode from offset
77
- packr.position = 0;
78
-
79
- //
80
- // TODO: remove this after issue is fixed https://github.com/kriszyp/msgpackr/issues/139
81
- //
82
- // - This check is only required when running integration tests.
83
- // (colyseus.js' usage of msgpackr/buffer is conflicting)
84
- //
85
- if (process.env.NODE_ENV !== "production") {
86
- packr.useBuffer(packr.buffer);
87
- }
88
-
89
- // pack message into the same packr.buffer
90
- const endOfBufferOffset = packr.pack(message, 2048 + it.offset).byteLength;
91
- // 2048 = RESERVE_START_SPACE
92
- return Buffer.from(packr.buffer.subarray(0, endOfBufferOffset));
168
+ // reserve `headerLength` bytes up front in the pack output for the header
169
+ const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
170
+ packed.set(frameBuffer.subarray(0, headerLength), 0);
171
+ return Buffer.from(packed);
93
172
 
94
173
  } else if (rawMessage !== undefined) {
95
-
96
- // check if buffer needs to be resized
97
- // TODO: can we avoid this?
98
- if (rawMessage.length + it.offset > packr.buffer.byteLength) {
99
- packr.useBuffer(Buffer.alloc(it.offset + rawMessage.length, packr.buffer));
100
- }
101
-
102
- // copy raw message into packr.buffer
103
- packr.buffer.set(rawMessage, it.offset);
104
-
105
- return Buffer.from(packr.buffer.subarray(0, it.offset + rawMessage.byteLength));
174
+ ensureFrameCapacity(headerLength + rawMessage.byteLength);
175
+ frameBuffer.set(rawMessage, headerLength);
176
+ return Buffer.from(frameBuffer.subarray(0, headerLength + rawMessage.byteLength));
106
177
 
107
178
  } else {
108
- return Buffer.from(packr.buffer.subarray(0, it.offset));
179
+ return Buffer.from(frameBuffer.subarray(0, headerLength));
109
180
  }
110
181
  },
111
182