@colyseus/sdk 0.17.41 → 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 (73) hide show
  1. package/build/3rd_party/discord.cjs +1 -1
  2. package/build/3rd_party/discord.mjs +1 -1
  3. package/build/Auth.cjs +16 -2
  4. package/build/Auth.cjs.map +1 -1
  5. package/build/Auth.mjs +16 -2
  6. package/build/Auth.mjs.map +1 -1
  7. package/build/Client.cjs +1 -1
  8. package/build/Client.mjs +1 -1
  9. package/build/Connection.cjs +1 -1
  10. package/build/Connection.mjs +1 -1
  11. package/build/HTTP.cjs +1 -1
  12. package/build/HTTP.mjs +1 -1
  13. package/build/Room.cjs +231 -46
  14. package/build/Room.cjs.map +1 -1
  15. package/build/Room.d.ts +62 -2
  16. package/build/Room.mjs +229 -44
  17. package/build/Room.mjs.map +1 -1
  18. package/build/Storage.cjs +1 -1
  19. package/build/Storage.mjs +1 -1
  20. package/build/core/nanoevents.cjs +1 -1
  21. package/build/core/nanoevents.mjs +1 -1
  22. package/build/core/signal.cjs +1 -1
  23. package/build/core/signal.cjs.map +1 -1
  24. package/build/core/signal.mjs +1 -1
  25. package/build/core/signal.mjs.map +1 -1
  26. package/build/core/utils.cjs +1 -1
  27. package/build/core/utils.mjs +1 -1
  28. package/build/debug.cjs +1 -1
  29. package/build/debug.cjs.map +1 -1
  30. package/build/debug.mjs +1 -1
  31. package/build/debug.mjs.map +1 -1
  32. package/build/errors/Errors.cjs +1 -1
  33. package/build/errors/Errors.mjs +1 -1
  34. package/build/fetchXHR.cjs +1 -1
  35. package/build/fetchXHR.mjs +1 -1
  36. package/build/index.cjs +1 -1
  37. package/build/index.cjs.map +1 -1
  38. package/build/index.d.ts +1 -1
  39. package/build/index.mjs +1 -1
  40. package/build/index.mjs.map +1 -1
  41. package/build/input/InputHandle.cjs +47 -0
  42. package/build/input/InputHandle.cjs.map +1 -0
  43. package/build/input/InputHandle.d.ts +79 -0
  44. package/build/input/InputHandle.mjs +48 -0
  45. package/build/input/InputHandle.mjs.map +1 -0
  46. package/build/legacy.cjs +1 -1
  47. package/build/legacy.mjs +1 -1
  48. package/build/serializer/NoneSerializer.cjs +1 -1
  49. package/build/serializer/NoneSerializer.mjs +1 -1
  50. package/build/serializer/SchemaSerializer.cjs +1 -1
  51. package/build/serializer/SchemaSerializer.mjs +1 -1
  52. package/build/serializer/Serializer.cjs +1 -1
  53. package/build/serializer/Serializer.mjs +1 -1
  54. package/build/transport/H3Transport.cjs +1 -1
  55. package/build/transport/H3Transport.cjs.map +1 -1
  56. package/build/transport/H3Transport.mjs +1 -1
  57. package/build/transport/H3Transport.mjs.map +1 -1
  58. package/build/transport/WebSocketTransport.cjs +1 -1
  59. package/build/transport/WebSocketTransport.mjs +1 -1
  60. package/dist/colyseus.js +13152 -1885
  61. package/dist/colyseus.js.map +1 -1
  62. package/dist/debug.js +1 -1
  63. package/dist/debug.js.map +1 -1
  64. package/package.json +11 -8
  65. package/src/Auth.ts +11 -1
  66. package/src/Room.ts +294 -48
  67. package/src/core/signal.ts +1 -1
  68. package/src/debug.ts +8 -8
  69. package/src/index.ts +1 -1
  70. package/src/input/InputHandle.ts +115 -0
  71. package/src/transport/H3Transport.ts +2 -2
  72. package/build/serializer/FossilDeltaSerializer.d.ts +0 -0
  73. package/src/serializer/FossilDeltaSerializer.ts +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/sdk",
3
- "version": "0.17.41",
3
+ "version": "0.18.0",
4
4
  "description": "Colyseus Multiplayer SDK for JavaScript/TypeScript",
5
5
  "author": "Endel Dreyer",
6
6
  "license": "MIT",
@@ -51,12 +51,12 @@
51
51
  "node": ">= 12.x"
52
52
  },
53
53
  "dependencies": {
54
- "@colyseus/msgpackr": "^1.11.2",
55
- "@colyseus/schema": "^4.0.7",
54
+ "@colyseus/schema": "^5.0.3",
55
+ "msgpackr": "^2.0.1",
56
56
  "tslib": "^2.1.0",
57
57
  "ws": "^8.13.0",
58
- "@colyseus/shared-types": "^0.17.6",
59
- "@colyseus/better-call": "^1.3.1"
58
+ "@colyseus/better-call": "^1.3.1",
59
+ "@colyseus/shared-types": "^0.18.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@rollup/plugin-alias": "^5.1.1",
@@ -79,13 +79,13 @@
79
79
  "ts-loader": "^6.2.1",
80
80
  "ts-node": "^6.0.3",
81
81
  "tslint": "^5.9.1",
82
- "typescript": "^5.9.3",
82
+ "typescript": "^6.0.3",
83
83
  "vite": "^5.0.11",
84
84
  "vitest": "^2.1.1",
85
- "@colyseus/core": "^0.17.41"
85
+ "@colyseus/core": "^0.18.0"
86
86
  },
87
87
  "peerDependencies": {
88
- "@colyseus/core": "0.17.x"
88
+ "@colyseus/core": "0.18.x"
89
89
  },
90
90
  "peerDependenciesMeta": {
91
91
  "@colyseus/core": {
@@ -95,6 +95,9 @@
95
95
  "optional": true
96
96
  }
97
97
  },
98
+ "publishConfig": {
99
+ "tag": "next"
100
+ },
98
101
  "scripts": {
99
102
  "test": "vitest run --dir test --reporter verbose",
100
103
  "start": "vite --config example/vite.config.ts",
package/src/Auth.ts CHANGED
@@ -165,7 +165,17 @@ export class Auth<UserData = any> {
165
165
  window.removeEventListener("message", onMessage);
166
166
 
167
167
  if (event.data.error !== undefined) {
168
- reject(event.data.error);
168
+ // Reject with an Error so consumers' `catch (e) {
169
+ // ... e.message }` reads as expected. Attach the
170
+ // structured payload (reason / until / etc.) as
171
+ // properties so callers that want to render a
172
+ // richer message (e.g. "banned until X for Y")
173
+ // can pull them off without re-parsing.
174
+ const err: any = new Error(String(event.data.error));
175
+ err.code = event.data.error;
176
+ if (event.data.reason !== undefined) { err.reason = event.data.reason; }
177
+ if (event.data.until !== undefined) { err.until = event.data.until; }
178
+ reject(err);
169
179
 
170
180
  } else {
171
181
  resolve(event.data);
package/src/Room.ts CHANGED
@@ -1,7 +1,11 @@
1
- import { CloseCode, Protocol, type InferState, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType } from '@colyseus/shared-types';
2
- import { decode, Decoder, encode, Iterator, Schema } from '@colyseus/schema';
1
+ import { CloseCode, HandshakeSection, Protocol, ResponseStatus, type InferState, type InferInput, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType, type ExtractResponseType } from '@colyseus/shared-types';
2
+ import { decode, Decoder, encode, Iterator, Reflection, Schema } from '@colyseus/schema';
3
+ import { InputEncoder } from '@colyseus/schema/input';
3
4
 
4
- import { Packr, unpack } from '@colyseus/msgpackr';
5
+ import { ClientInputHandleImpl, type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
6
+ export { type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
7
+
8
+ import { Packr, unpack, RESERVE_START_SPACE } from 'msgpackr';
5
9
 
6
10
  import { Connection } from './Connection.ts';
7
11
  import { getSerializer, Serializer } from './serializer/Serializer.ts';
@@ -135,17 +139,48 @@ export class Room<
135
139
  protected onMessageHandlers = createNanoEvents();
136
140
 
137
141
  protected packr: Packr;
142
+ protected sharedBuffer: Uint8Array;
138
143
 
139
144
  #lastPingTime: number = 0;
140
145
  #pingCallback?: (ms: number) => void = undefined;
141
146
 
147
+ /**
148
+ * Default time (ms) a `room.request()` / `room.send(..., callback)` waits
149
+ * for a reply before rejecting. Override per-call with the `timeout`
150
+ * option. Tune globally by assigning to this field after joining.
151
+ */
152
+ public requestTimeout: number = 10000;
153
+
154
+ /** Monotonic id correlating a {@link Protocol.ROOM_REQUEST} with its reply. @internal */
155
+ #nextRequestId: number = 0;
156
+
157
+ /** In-flight requests awaiting a {@link Protocol.ROOM_RESPONSE}. @internal */
158
+ #pendingRequests = new Map<number, {
159
+ resolve: (value: any) => void;
160
+ reject: (reason: any) => void;
161
+ timer: ReturnType<typeof setTimeout>;
162
+ }>();
163
+
164
+ #inputHandle?: ClientInputHandle<any>;
165
+ /**
166
+ * Schema constructor recovered via Reflection from the server's
167
+ * handshake (the `INPUT_REFLECTION` tagged section). Populated on JOIN
168
+ * when the server room called `defineInput()`; falls back to `undefined`
169
+ * otherwise. Survives reconnects that skip the handshake — the field is
170
+ * set on the original join and never cleared.
171
+ *
172
+ * Typed as `new () => any` (not `Schema`) on purpose — pinning to this
173
+ * SDK's Schema type would clash with user instances coming from a
174
+ * different copy of `@colyseus/schema` under multi-version installs.
175
+ * @internal
176
+ */
177
+ #inputCtorFromReflection?: new () => any;
178
+
142
179
  constructor(name: string, rootSchema?: SchemaConstructor<State>) {
143
180
  this.name = name;
144
181
 
145
182
  this.packr = new Packr();
146
-
147
- // msgpackr workaround: force buffer to be created.
148
- this.packr.encode(undefined);
183
+ this.sharedBuffer = new Uint8Array(8192);
149
184
 
150
185
  if (rootSchema) {
151
186
  const serializer: SchemaSerializer = new (getSerializer("schema"));
@@ -166,6 +201,9 @@ export class Room<
166
201
  this.connection = new Connection(options.protocol);
167
202
  this.connection.events.onmessage = this.onMessageCallback.bind(this);
168
203
  this.connection.events.onclose = (e: CloseEvent) => {
204
+ // the in-flight requests can't be answered on a closed socket
205
+ this.#rejectAllPending("connection closed before a response was received.");
206
+
169
207
  if (this.joinedAtTime === 0) {
170
208
  console.warn?.(`Room connection was closed unexpectedly (${e.code}): ${e.reason}`);
171
209
  this.onError.invoke(e.code, e.reason);
@@ -213,8 +251,8 @@ export class Room<
213
251
 
214
252
  if (this.connection) {
215
253
  if (consented) {
216
- this.packr.buffer[0] = Protocol.LEAVE_ROOM;
217
- this.connection.send(this.packr.buffer.subarray(0, 1));
254
+ this.sharedBuffer[0] = Protocol.LEAVE_ROOM;
255
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
218
256
 
219
257
  } else {
220
258
  this.connection.close();
@@ -248,36 +286,63 @@ export class Room<
248
286
 
249
287
  this.#lastPingTime = now();
250
288
  this.#pingCallback = callback;
251
- this.packr.buffer[0] = Protocol.PING;
252
- this.connection.send(this.packr.buffer.subarray(0, 1));
289
+ this.sharedBuffer[0] = Protocol.PING;
290
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
253
291
  }
254
292
 
255
293
  public send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(
256
294
  messageType: MessageType,
257
295
  payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>
258
296
  ): void
297
+ // Request overload: passing a callback turns this into a request/response —
298
+ // the callback receives the value the server handler returns (or an Error).
299
+ public send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(
300
+ messageType: MessageType,
301
+ payload: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>,
302
+ callback: (response: ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, error?: Error) => void
303
+ ): void
259
304
  // Fallback overload: only available when no typed messages are defined
260
305
  public send<Payload = any>(
261
306
  messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never,
262
307
  payload?: Payload
263
308
  ): void
264
- public send(messageType: string | number, payload?: any): void {
309
+ // Fallback request overload
310
+ public send<Payload = any, Response = any>(
311
+ messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never,
312
+ payload: Payload,
313
+ callback: (response: Response, error?: Error) => void
314
+ ): void
315
+ public send(messageType: string | number, payload?: any, callback?: (response: any, error?: Error) => void): void {
316
+ // Request/response form: defer to `request()` and adapt to a
317
+ // (response, error) callback.
318
+ if (callback !== undefined) {
319
+ this.#request(messageType, payload, this.requestTimeout).then(
320
+ (response) => callback(response, undefined),
321
+ (error) => callback(undefined, error),
322
+ );
323
+ return;
324
+ }
325
+
265
326
  const it: Iterator = { offset: 1 };
266
- this.packr.buffer[0] = Protocol.ROOM_DATA;
327
+ this.sharedBuffer[0] = Protocol.ROOM_DATA;
267
328
 
268
329
  if (typeof(messageType) === "string") {
269
- encode.string(this.packr.buffer as Buffer, messageType, it);
330
+ encode.string(this.sharedBuffer, messageType, it);
270
331
 
271
332
  } else {
272
- encode.number(this.packr.buffer as Buffer, messageType, it);
333
+ encode.number(this.sharedBuffer, messageType, it);
334
+ }
335
+ const headerLength = it.offset;
336
+
337
+ let data: Uint8Array;
338
+ if (payload !== undefined) {
339
+ // Reserve `headerLength` writable bytes at the front of msgpackr's
340
+ // output and prepend the protocol header into them.
341
+ data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
342
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
343
+ } else {
344
+ data = this.sharedBuffer.subarray(0, headerLength);
273
345
  }
274
-
275
- // force packr to use beginning of the buffer
276
- this.packr.position = 0;
277
-
278
- const data = (payload !== undefined)
279
- ? this.packr.pack(payload, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
280
- : this.packr.buffer.subarray(0, it.offset);
281
346
 
282
347
  // If connection is not open, buffer the message
283
348
  if (!this.connection.isOpen) {
@@ -287,58 +352,186 @@ export class Room<
287
352
  }
288
353
  }
289
354
 
355
+ /**
356
+ * Send a message and await the server's reply. The server answers by
357
+ * returning a value from its matching `onMessage(type, ...)` handler.
358
+ *
359
+ * Rejects if the handler throws, if no handler is registered, if the
360
+ * connection closes first, or if no reply arrives within `timeout`
361
+ * (defaults to {@link Room.requestTimeout}).
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * const profile = await room.request("get-profile", { id: 42 });
366
+ * ```
367
+ */
368
+ public request<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(
369
+ messageType: MessageType,
370
+ payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>,
371
+ options?: { timeout?: number }
372
+ ): Promise<ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>>
373
+ public request<Payload = any, Response = any>(
374
+ messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never,
375
+ payload?: Payload,
376
+ options?: { timeout?: number }
377
+ ): Promise<Response>
378
+ public request(messageType: string | number, payload?: any, options?: { timeout?: number }): Promise<any> {
379
+ return this.#request(messageType, payload, options?.timeout ?? this.requestTimeout);
380
+ }
381
+
382
+ #request(messageType: string | number, payload: any, timeoutMs: number): Promise<any> {
383
+ return new Promise((resolve, reject) => {
384
+ if (!this.connection.isOpen) {
385
+ reject(new Error(`cannot send request "${messageType}": connection is not open.`));
386
+ return;
387
+ }
388
+
389
+ const requestId = this.#nextRequestId;
390
+ this.#nextRequestId = (this.#nextRequestId + 1) >>> 0; // keep within uint32
391
+
392
+ const it: Iterator = { offset: 1 };
393
+ this.sharedBuffer[0] = Protocol.ROOM_REQUEST;
394
+ encode.number(this.sharedBuffer, requestId, it);
395
+
396
+ if (typeof(messageType) === "string") {
397
+ encode.string(this.sharedBuffer, messageType, it);
398
+ } else {
399
+ encode.number(this.sharedBuffer, messageType, it);
400
+ }
401
+ const headerLength = it.offset;
402
+
403
+ let data: Uint8Array;
404
+ if (payload !== undefined) {
405
+ data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
406
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
407
+ } else {
408
+ data = this.sharedBuffer.subarray(0, headerLength);
409
+ }
410
+
411
+ const timer = setTimeout(() => {
412
+ this.#pendingRequests.delete(requestId);
413
+ reject(new Error(`request "${messageType}" timed out after ${timeoutMs}ms.`));
414
+ }, timeoutMs);
415
+
416
+ this.#pendingRequests.set(requestId, { resolve, reject, timer });
417
+ this.connection.send(data);
418
+ });
419
+ }
420
+
421
+ #rejectAllPending(reason: string) {
422
+ if (this.#pendingRequests.size === 0) { return; }
423
+ const error = new Error(reason);
424
+ for (const pending of this.#pendingRequests.values()) {
425
+ clearTimeout(pending.timer);
426
+ pending.reject(error);
427
+ }
428
+ this.#pendingRequests.clear();
429
+ }
430
+
290
431
  public sendUnreliable<T = any>(type: string | number, message?: T): void {
291
432
  // If connection is not open, skip
292
433
  if (!this.connection.isOpen) { return; }
293
434
 
294
435
  const it: Iterator = { offset: 1 };
295
- this.packr.buffer[0] = Protocol.ROOM_DATA;
436
+ this.sharedBuffer[0] = Protocol.ROOM_DATA;
296
437
 
297
438
  if (typeof(type) === "string") {
298
- encode.string(this.packr.buffer as Buffer, type, it);
439
+ encode.string(this.sharedBuffer, type, it);
299
440
 
300
441
  } else {
301
- encode.number(this.packr.buffer as Buffer, type, it);
442
+ encode.number(this.sharedBuffer, type, it);
302
443
  }
444
+ const headerLength = it.offset;
303
445
 
304
- // force packr to use beginning of the buffer
305
- this.packr.position = 0;
306
-
307
- const data = (message !== undefined)
308
- ? this.packr.pack(message, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
309
- : this.packr.buffer.subarray(0, it.offset);
446
+ let data: Uint8Array;
447
+ if (message !== undefined) {
448
+ data = this.packr.pack(message, RESERVE_START_SPACE | headerLength);
449
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
450
+ } else {
451
+ data = this.sharedBuffer.subarray(0, headerLength);
452
+ }
310
453
 
311
454
  this.connection.sendUnreliable(data);
312
455
  }
313
456
 
314
457
  public sendBytes(type: string | number, bytes: Uint8Array) {
315
458
  const it: Iterator = { offset: 1 };
316
- this.packr.buffer[0] = Protocol.ROOM_DATA_BYTES;
459
+ this.sharedBuffer[0] = Protocol.ROOM_DATA_BYTES;
317
460
 
318
461
  if (typeof(type) === "string") {
319
- encode.string(this.packr.buffer as Buffer, type, it);
462
+ encode.string(this.sharedBuffer, type, it);
320
463
 
321
464
  } else {
322
- encode.number(this.packr.buffer as Buffer, type, it);
465
+ encode.number(this.sharedBuffer, type, it);
323
466
  }
467
+ const headerLength = it.offset;
324
468
 
325
- // check if buffer needs to be resized
326
- // TODO: can we avoid this?
327
- if (bytes.byteLength + it.offset > this.packr.buffer.byteLength) {
328
- const newBuffer = new Uint8Array(it.offset + bytes.byteLength);
329
- newBuffer.set(this.packr.buffer);
330
- this.packr.useBuffer(newBuffer);
469
+ // grow the scratch buffer if needed, preserving the header bytes
470
+ if (headerLength + bytes.byteLength > this.sharedBuffer.byteLength) {
471
+ const newBuffer = new Uint8Array(headerLength + bytes.byteLength);
472
+ newBuffer.set(this.sharedBuffer.subarray(0, headerLength));
473
+ this.sharedBuffer = newBuffer;
331
474
  }
332
475
 
333
- this.packr.buffer.set(bytes, it.offset);
476
+ this.sharedBuffer.set(bytes, headerLength);
334
477
 
335
478
  // If connection is not open, buffer the message
336
479
  if (!this.connection.isOpen) {
337
- enqueueMessage(this, this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
480
+ enqueueMessage(this, this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
338
481
  } else {
339
- this.connection.send(this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
482
+ this.connection.send(this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
483
+ }
484
+
485
+ }
486
+
487
+ /**
488
+ * Get the per-room input handle. Lazily created on first call and cached;
489
+ * subsequent calls return the same handle (options on later calls are
490
+ * ignored).
491
+ *
492
+ * Schema discovery, in order:
493
+ * 1. `options.type` — explicit constructor (overrides everything).
494
+ * 2. Server-sent reflection from the JOIN handshake — populated when the
495
+ * server room called `defineInput()`. The synthesized class has the
496
+ * same fields as the server's input schema; `instanceof YourInput`
497
+ * won't pass on it.
498
+ *
499
+ * Throws if neither source has produced a constructor.
500
+ *
501
+ * For rollback netcode, prefer `{ mode: "unreliable", delta: true,
502
+ * historySize: 4 }`: tiny per-tick payloads, redundancy across drops,
503
+ * idempotent under reordering.
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const conn = await client.joinOrCreate<typeof FpsRoom>("fps");
508
+ * const input = conn.input({ mode: "unreliable" }); // type from server
509
+ * // each simulation tick:
510
+ * input.data.seq++;
511
+ * input.data.vx = vx;
512
+ * input.data.vy = vy;
513
+ * input.send();
514
+ * ```
515
+ */
516
+ public input<
517
+ I = ([InferInput<T>] extends [never] ? any : InferInput<T>),
518
+ >(options?: ClientInputOptions<I>): ClientInputHandle<I> {
519
+ if (this.#inputHandle) {
520
+ return this.#inputHandle as ClientInputHandle<I>;
340
521
  }
341
522
 
523
+ const Ctor = (options?.type ?? this.#inputCtorFromReflection) as (new () => I) | undefined;
524
+ if (!Ctor) {
525
+ throw new Error(
526
+ "conn.input(): no input schema available. The server room must call " +
527
+ "`defineInput(YourInput)`, or you can pass `{ type: YourInput }` explicitly."
528
+ );
529
+ }
530
+
531
+ const instance = new Ctor();
532
+ const encoder = new InputEncoder(instance as any, options);
533
+ this.#inputHandle = new ClientInputHandleImpl(this, instance, encoder);
534
+ return this.#inputHandle as ClientInputHandle<I>;
342
535
  }
343
536
 
344
537
  public get state (): State {
@@ -376,9 +569,34 @@ export class Room<
376
569
  this.serializer = new serializer();
377
570
  }
378
571
 
379
- // apply handshake on first join (no need to do this on reconnect)
380
- if (buffer.byteLength > it.offset && this.serializer.handshake) {
381
- this.serializer.handshake(buffer, it);
572
+ // State reflection is length-prefixed (varint). The schema decoder
573
+ // runs `while (offset < bytes.byteLength)` so without a boundary
574
+ // it would read past the state reflection into the trailing
575
+ // tagged-section bytes — see Protocol.ts for the wire layout.
576
+ const stateReflectionLen = decode.number(buffer as Buffer, it);
577
+ if (stateReflectionLen > 0 && this.serializer.handshake) {
578
+ const stateReflectionEnd = it.offset + stateReflectionLen;
579
+ this.serializer.handshake(buffer.subarray(0, stateReflectionEnd), it);
580
+ it.offset = stateReflectionEnd;
581
+ }
582
+
583
+ // Parse trailing tagged sections (forward-compatible: unknown tags
584
+ // are skipped via length). See HandshakeSection in shared-types.
585
+ while (it.offset < buffer.byteLength) {
586
+ const tag = buffer[it.offset++];
587
+ const sectionLen = decode.number(buffer as Buffer, it);
588
+ const sectionEnd = it.offset + sectionLen;
589
+
590
+ if (tag === HandshakeSection.INPUT_REFLECTION) {
591
+ const inputDecoder = Reflection.decode(buffer.subarray(0, sectionEnd) as any, it);
592
+ // Install schema-builder field descriptors on the
593
+ // reconstructed class so `InputEncoder` can read its
594
+ // `$values` and emit non-empty packets.
595
+ Reflection.makeEncodable(inputDecoder.state.constructor as any);
596
+ this.#inputCtorFromReflection = inputDecoder.state.constructor as new () => any;
597
+ }
598
+
599
+ it.offset = sectionEnd;
382
600
  }
383
601
 
384
602
  if (this.joinedAtTime === 0) {
@@ -394,8 +612,8 @@ export class Room<
394
612
  this.reconnectionToken = `${this.roomId}:${reconnectionToken}`;
395
613
 
396
614
  // acknowledge successfull JOIN_ROOM
397
- this.packr.buffer[0] = Protocol.JOIN_ROOM;
398
- this.connection.send(this.packr.buffer.subarray(0, 1));
615
+ this.sharedBuffer[0] = Protocol.JOIN_ROOM;
616
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
399
617
 
400
618
  // Send any enqueued messages that were buffered while disconnected
401
619
  if (this.reconnection.enqueuedMessages.length > 0) {
@@ -441,6 +659,32 @@ export class Room<
441
659
 
442
660
  this.dispatchMessage(type, buffer.subarray(it.offset));
443
661
 
662
+ } else if (code === Protocol.ROOM_RESPONSE) {
663
+ // reply to a pending `request()` / `send(..., callback)`
664
+ const requestId = decode.number(buffer as Buffer, it);
665
+ const status = buffer[it.offset++];
666
+
667
+ const pending = this.#pendingRequests.get(requestId);
668
+ // already settled (e.g. timed out) or unknown id — ignore
669
+ if (pending !== undefined) {
670
+ this.#pendingRequests.delete(requestId);
671
+ clearTimeout(pending.timer);
672
+
673
+ const payload = (buffer.byteLength > it.offset)
674
+ ? unpack(buffer as Buffer, { start: it.offset })
675
+ : undefined;
676
+
677
+ if (status === ResponseStatus.OK) {
678
+ pending.resolve(payload);
679
+ } else {
680
+ // payload carries { name, message, code } from the server
681
+ const error: any = new Error(payload?.message ?? "request failed");
682
+ if (payload?.name) { error.name = payload.name; }
683
+ if (payload?.code !== undefined) { error.code = payload.code; }
684
+ pending.reject(error);
685
+ }
686
+ }
687
+
444
688
  } else if (code === Protocol.PING) {
445
689
  this.#pingCallback?.(Math.round(now() - this.#lastPingTime));
446
690
  this.#pingCallback = undefined;
@@ -538,4 +782,6 @@ function enqueueMessage(room: Room, message: Uint8Array) {
538
782
  if (room.reconnection.enqueuedMessages.length > room.reconnection.maxEnqueuedMessages) {
539
783
  room.reconnection.enqueuedMessages.shift();
540
784
  }
541
- }
785
+ }
786
+
787
+
@@ -47,7 +47,7 @@ export function createSignal<CallbackSignature extends (...args: any[]) => void
47
47
  };
48
48
 
49
49
  register.once = (cb: CallbackSignature) => {
50
- const callback: any = function (...args: any[]) {
50
+ const callback: any = function (this: any, ...args: any[]) {
51
51
  cb.apply(this, args);
52
52
  emitter.remove(callback);
53
53
  }
package/src/debug.ts CHANGED
@@ -1434,7 +1434,7 @@ function openSendMessagesModal(uniquePanelId) {
1434
1434
  sendButton.style.cursor = 'pointer';
1435
1435
  }, 800);
1436
1436
 
1437
- } catch (e) {
1437
+ } catch (e: any) {
1438
1438
  errorContainer.textContent = 'Error: ' + e.message;
1439
1439
  errorContainer.style.display = 'block';
1440
1440
  }
@@ -1740,7 +1740,7 @@ function openStateInspectorModal(uniquePanelId) {
1740
1740
  html += renderKeyValue(key, value, depth + 1, currentPath, keyStr, typeof key === 'string');
1741
1741
  }
1742
1742
  });
1743
- } catch (e) {
1743
+ } catch (e: any) {
1744
1744
  var errorIndent = (depth + 1) * 6;
1745
1745
  html += '<div style="margin-left: ' + errorIndent + 'px; color: #e74856;">Error iterating: ' + escapeHtml(e.message) + '</div>';
1746
1746
  }
@@ -1843,7 +1843,7 @@ function openStateInspectorModal(uniquePanelId) {
1843
1843
  contentContainer.innerHTML = '<div style="font-family: \'Consolas\', \'Monaco\', \'Courier New\', monospace; font-size: 12px; line-height: 1.5; color: #d4d4d4; padding: 8px;">' + renderState(state) + '</div>';
1844
1844
 
1845
1845
  // Event delegation: single click listener handles all expand buttons
1846
- } catch (e) {
1846
+ } catch (e: any) {
1847
1847
  contentContainer.innerHTML = '<div style="color: #e74856; padding: 20px;">Error accessing room state: ' + escapeHtml(e.message) + '</div>';
1848
1848
  }
1849
1849
  }
@@ -1853,7 +1853,7 @@ function openStateInspectorModal(uniquePanelId) {
1853
1853
  function throttle(func, wait) {
1854
1854
  var timeout;
1855
1855
  var previous = 0;
1856
- return function executedFunction() {
1856
+ return function executedFunction(this: any) {
1857
1857
  var context = this;
1858
1858
  var args = arguments;
1859
1859
  var now = Date.now();
@@ -2804,7 +2804,7 @@ function applyMonkeyPatches() {
2804
2804
  originalOnMessage.call(event.target, syntheticEvent);
2805
2805
  }, preferences.latencySimulation.delay);
2806
2806
  } else {
2807
- return originalOnMessage.apply(this, arguments);
2807
+ return originalOnMessage.apply(this, arguments as any);
2808
2808
  }
2809
2809
  };
2810
2810
 
@@ -2815,11 +2815,11 @@ function applyMonkeyPatches() {
2815
2815
  const originalOnClose = transport.events.onclose;
2816
2816
  transport.events.onclose = function(event) {
2817
2817
  if (preferences.latencySimulation.enabled && preferences.latencySimulation.delay > 0) {
2818
- setTimeout(function() {
2818
+ setTimeout(function(this: any) {
2819
2819
  if (originalOnClose) originalOnClose.call(this, event);
2820
2820
  }, preferences.latencySimulation.delay + 1);
2821
2821
  } else {
2822
- if (originalOnClose) return originalOnClose.apply(this, arguments);
2822
+ if (originalOnClose) return originalOnClose.apply(this, arguments as any);
2823
2823
  }
2824
2824
  };
2825
2825
 
@@ -2879,7 +2879,7 @@ function applyMonkeyPatches() {
2879
2879
  // Patch consumeSeatReservation to intercept all room connections
2880
2880
  var originalConsumeSeatReservation = Client.prototype.consumeSeatReservation;
2881
2881
  Client.prototype.consumeSeatReservation = function() {
2882
- var promise = originalConsumeSeatReservation.apply(this, arguments);
2882
+ var promise = originalConsumeSeatReservation.apply(this, arguments as any);
2883
2883
  return promise.then((room) => patchRoom(room));
2884
2884
  };
2885
2885
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import './legacy';
1
+ import './legacy.ts';
2
2
 
3
3
  export { ColyseusSDK, Client, type JoinOptions, type EndpointSettings, type ClientOptions, type ISeatReservation as SeatReservation } from './Client.ts';
4
4
  export { type FetchFn } from './HTTP.ts';