@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/build/Room.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { type InferState, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType } from '@colyseus/shared-types';
1
+ import { type InferState, type InferInput, type NormalizeRoomType, type ExtractRoomMessages, type ExtractRoomClientMessages, type ExtractMessageType, type ExtractResponseType } from '@colyseus/shared-types';
2
2
  import { Schema } from '@colyseus/schema';
3
- import { Packr } from '@colyseus/msgpackr';
3
+ import { type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
4
+ export { type ClientInputHandle, type ClientInputOptions } from './input/InputHandle.ts';
5
+ import { Packr } from 'msgpackr';
4
6
  import { Connection } from './Connection.ts';
5
7
  import { Serializer } from './serializer/Serializer.ts';
6
8
  import { SchemaConstructor, SchemaSerializer } from './serializer/SchemaSerializer.ts';
@@ -121,6 +123,13 @@ export declare class Room<T = any, State = InferState<T, never>> {
121
123
  protected joinedAtTime: number;
122
124
  protected onMessageHandlers: import("./core/nanoevents.ts").Emitter<import("./core/nanoevents.ts").EventsMap>;
123
125
  protected packr: Packr;
126
+ protected sharedBuffer: Uint8Array;
127
+ /**
128
+ * Default time (ms) a `room.request()` / `room.send(..., callback)` waits
129
+ * for a reply before rejecting. Override per-call with the `timeout`
130
+ * option. Tune globally by assigning to this field after joining.
131
+ */
132
+ requestTimeout: number;
124
133
  constructor(name: string, rootSchema?: SchemaConstructor<State>);
125
134
  connect(endpoint: string, options?: any, headers?: any): void;
126
135
  leave(consented?: boolean): Promise<number>;
@@ -129,9 +138,60 @@ export declare class Room<T = any, State = InferState<T, never>> {
129
138
  onMessage<Payload = any>(type: [keyof ExtractRoomClientMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, callback: (payload: Payload) => void): () => void;
130
139
  ping(callback: (ms: number) => void): void;
131
140
  send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>): void;
141
+ send<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, callback: (response: ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, error?: Error) => void): void;
132
142
  send<Payload = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload?: Payload): void;
143
+ send<Payload = any, Response = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload: Payload, callback: (response: Response, error?: Error) => void): void;
144
+ /**
145
+ * Send a message and await the server's reply. The server answers by
146
+ * returning a value from its matching `onMessage(type, ...)` handler.
147
+ *
148
+ * Rejects if the handler throws, if no handler is registered, if the
149
+ * connection closes first, or if no reply arrives within `timeout`
150
+ * (defaults to {@link Room.requestTimeout}).
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const profile = await room.request("get-profile", { id: 42 });
155
+ * ```
156
+ */
157
+ request<MessageType extends keyof ExtractRoomMessages<NormalizeRoomType<T>>>(messageType: MessageType, payload?: ExtractMessageType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>, options?: {
158
+ timeout?: number;
159
+ }): Promise<ExtractResponseType<ExtractRoomMessages<NormalizeRoomType<T>>[MessageType]>>;
160
+ request<Payload = any, Response = any>(messageType: [keyof ExtractRoomMessages<NormalizeRoomType<T>>] extends [never] ? (string | number) : never, payload?: Payload, options?: {
161
+ timeout?: number;
162
+ }): Promise<Response>;
133
163
  sendUnreliable<T = any>(type: string | number, message?: T): void;
134
164
  sendBytes(type: string | number, bytes: Uint8Array): void;
165
+ /**
166
+ * Get the per-room input handle. Lazily created on first call and cached;
167
+ * subsequent calls return the same handle (options on later calls are
168
+ * ignored).
169
+ *
170
+ * Schema discovery, in order:
171
+ * 1. `options.type` — explicit constructor (overrides everything).
172
+ * 2. Server-sent reflection from the JOIN handshake — populated when the
173
+ * server room called `defineInput()`. The synthesized class has the
174
+ * same fields as the server's input schema; `instanceof YourInput`
175
+ * won't pass on it.
176
+ *
177
+ * Throws if neither source has produced a constructor.
178
+ *
179
+ * For rollback netcode, prefer `{ mode: "unreliable", delta: true,
180
+ * historySize: 4 }`: tiny per-tick payloads, redundancy across drops,
181
+ * idempotent under reordering.
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const conn = await client.joinOrCreate<typeof FpsRoom>("fps");
186
+ * const input = conn.input({ mode: "unreliable" }); // type from server
187
+ * // each simulation tick:
188
+ * input.data.seq++;
189
+ * input.data.vx = vx;
190
+ * input.data.vy = vy;
191
+ * input.send();
192
+ * ```
193
+ */
194
+ input<I = ([InferInput<T>] extends [never] ? any : InferInput<T>)>(options?: ClientInputOptions<I>): ClientInputHandle<I>;
135
195
  get state(): State;
136
196
  removeAllListeners(): void;
137
197
  protected onMessageCallback(event: MessageEvent): void;
package/build/Room.mjs CHANGED
@@ -3,10 +3,12 @@
3
3
  // This software is released under the MIT License.
4
4
  // https://opensource.org/license/MIT
5
5
  //
6
- // colyseus.js@0.17.41
7
- import { CloseCode, Protocol } from '@colyseus/shared-types';
8
- import { Decoder, encode, decode } from '@colyseus/schema';
9
- import { Packr, unpack } from '@colyseus/msgpackr';
6
+ // colyseus.js@0.18.0
7
+ import { CloseCode, Protocol, HandshakeSection, ResponseStatus } from '@colyseus/shared-types';
8
+ import { Decoder, encode, decode, Reflection } from '@colyseus/schema';
9
+ import { InputEncoder } from '@colyseus/schema/input';
10
+ import { ClientInputHandleImpl } from './input/InputHandle.mjs';
11
+ import { Packr, RESERVE_START_SPACE, unpack } from 'msgpackr';
10
12
  import { Connection } from './Connection.mjs';
11
13
  import { getSerializer } from './serializer/Serializer.mjs';
12
14
  import { createNanoEvents } from './core/nanoevents.mjs';
@@ -46,13 +48,37 @@ class Room {
46
48
  joinedAtTime = 0;
47
49
  onMessageHandlers = createNanoEvents();
48
50
  packr;
51
+ sharedBuffer;
49
52
  #lastPingTime = 0;
50
53
  #pingCallback = undefined;
54
+ /**
55
+ * Default time (ms) a `room.request()` / `room.send(..., callback)` waits
56
+ * for a reply before rejecting. Override per-call with the `timeout`
57
+ * option. Tune globally by assigning to this field after joining.
58
+ */
59
+ requestTimeout = 10000;
60
+ /** Monotonic id correlating a {@link Protocol.ROOM_REQUEST} with its reply. @internal */
61
+ #nextRequestId = 0;
62
+ /** In-flight requests awaiting a {@link Protocol.ROOM_RESPONSE}. @internal */
63
+ #pendingRequests = new Map();
64
+ #inputHandle;
65
+ /**
66
+ * Schema constructor recovered via Reflection from the server's
67
+ * handshake (the `INPUT_REFLECTION` tagged section). Populated on JOIN
68
+ * when the server room called `defineInput()`; falls back to `undefined`
69
+ * otherwise. Survives reconnects that skip the handshake — the field is
70
+ * set on the original join and never cleared.
71
+ *
72
+ * Typed as `new () => any` (not `Schema`) on purpose — pinning to this
73
+ * SDK's Schema type would clash with user instances coming from a
74
+ * different copy of `@colyseus/schema` under multi-version installs.
75
+ * @internal
76
+ */
77
+ #inputCtorFromReflection;
51
78
  constructor(name, rootSchema) {
52
79
  this.name = name;
53
80
  this.packr = new Packr();
54
- // msgpackr workaround: force buffer to be created.
55
- this.packr.encode(undefined);
81
+ this.sharedBuffer = new Uint8Array(8192);
56
82
  if (rootSchema) {
57
83
  const serializer = new (getSerializer("schema"));
58
84
  this.serializer = serializer;
@@ -69,6 +95,8 @@ class Room {
69
95
  this.connection = new Connection(options.protocol);
70
96
  this.connection.events.onmessage = this.onMessageCallback.bind(this);
71
97
  this.connection.events.onclose = (e) => {
98
+ // the in-flight requests can't be answered on a closed socket
99
+ this.#rejectAllPending("connection closed before a response was received.");
72
100
  if (this.joinedAtTime === 0) {
73
101
  console.warn?.(`Room connection was closed unexpectedly (${e.code}): ${e.reason}`);
74
102
  this.onError.invoke(e.code, e.reason);
@@ -107,8 +135,8 @@ class Room {
107
135
  this.onLeave((code) => resolve(code));
108
136
  if (this.connection) {
109
137
  if (consented) {
110
- this.packr.buffer[0] = Protocol.LEAVE_ROOM;
111
- this.connection.send(this.packr.buffer.subarray(0, 1));
138
+ this.sharedBuffer[0] = Protocol.LEAVE_ROOM;
139
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
112
140
  }
113
141
  else {
114
142
  this.connection.close();
@@ -129,23 +157,35 @@ class Room {
129
157
  }
130
158
  this.#lastPingTime = now();
131
159
  this.#pingCallback = callback;
132
- this.packr.buffer[0] = Protocol.PING;
133
- this.connection.send(this.packr.buffer.subarray(0, 1));
160
+ this.sharedBuffer[0] = Protocol.PING;
161
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
134
162
  }
135
- send(messageType, payload) {
163
+ send(messageType, payload, callback) {
164
+ // Request/response form: defer to `request()` and adapt to a
165
+ // (response, error) callback.
166
+ if (callback !== undefined) {
167
+ this.#request(messageType, payload, this.requestTimeout).then((response) => callback(response, undefined), (error) => callback(undefined, error));
168
+ return;
169
+ }
136
170
  const it = { offset: 1 };
137
- this.packr.buffer[0] = Protocol.ROOM_DATA;
171
+ this.sharedBuffer[0] = Protocol.ROOM_DATA;
138
172
  if (typeof (messageType) === "string") {
139
- encode.string(this.packr.buffer, messageType, it);
173
+ encode.string(this.sharedBuffer, messageType, it);
174
+ }
175
+ else {
176
+ encode.number(this.sharedBuffer, messageType, it);
177
+ }
178
+ const headerLength = it.offset;
179
+ let data;
180
+ if (payload !== undefined) {
181
+ // Reserve `headerLength` writable bytes at the front of msgpackr's
182
+ // output and prepend the protocol header into them.
183
+ data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
184
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
140
185
  }
141
186
  else {
142
- encode.number(this.packr.buffer, messageType, it);
187
+ data = this.sharedBuffer.subarray(0, headerLength);
143
188
  }
144
- // force packr to use beginning of the buffer
145
- this.packr.position = 0;
146
- const data = (payload !== undefined)
147
- ? this.packr.pack(payload, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
148
- : this.packr.buffer.subarray(0, it.offset);
149
189
  // If connection is not open, buffer the message
150
190
  if (!this.connection.isOpen) {
151
191
  enqueueMessage(this, new Uint8Array(data));
@@ -154,51 +194,146 @@ class Room {
154
194
  this.connection.send(data);
155
195
  }
156
196
  }
197
+ request(messageType, payload, options) {
198
+ return this.#request(messageType, payload, options?.timeout ?? this.requestTimeout);
199
+ }
200
+ #request(messageType, payload, timeoutMs) {
201
+ return new Promise((resolve, reject) => {
202
+ if (!this.connection.isOpen) {
203
+ reject(new Error(`cannot send request "${messageType}": connection is not open.`));
204
+ return;
205
+ }
206
+ const requestId = this.#nextRequestId;
207
+ this.#nextRequestId = (this.#nextRequestId + 1) >>> 0; // keep within uint32
208
+ const it = { offset: 1 };
209
+ this.sharedBuffer[0] = Protocol.ROOM_REQUEST;
210
+ encode.number(this.sharedBuffer, requestId, it);
211
+ if (typeof (messageType) === "string") {
212
+ encode.string(this.sharedBuffer, messageType, it);
213
+ }
214
+ else {
215
+ encode.number(this.sharedBuffer, messageType, it);
216
+ }
217
+ const headerLength = it.offset;
218
+ let data;
219
+ if (payload !== undefined) {
220
+ data = this.packr.pack(payload, RESERVE_START_SPACE | headerLength);
221
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
222
+ }
223
+ else {
224
+ data = this.sharedBuffer.subarray(0, headerLength);
225
+ }
226
+ const timer = setTimeout(() => {
227
+ this.#pendingRequests.delete(requestId);
228
+ reject(new Error(`request "${messageType}" timed out after ${timeoutMs}ms.`));
229
+ }, timeoutMs);
230
+ this.#pendingRequests.set(requestId, { resolve, reject, timer });
231
+ this.connection.send(data);
232
+ });
233
+ }
234
+ #rejectAllPending(reason) {
235
+ if (this.#pendingRequests.size === 0) {
236
+ return;
237
+ }
238
+ const error = new Error(reason);
239
+ for (const pending of this.#pendingRequests.values()) {
240
+ clearTimeout(pending.timer);
241
+ pending.reject(error);
242
+ }
243
+ this.#pendingRequests.clear();
244
+ }
157
245
  sendUnreliable(type, message) {
158
246
  // If connection is not open, skip
159
247
  if (!this.connection.isOpen) {
160
248
  return;
161
249
  }
162
250
  const it = { offset: 1 };
163
- this.packr.buffer[0] = Protocol.ROOM_DATA;
251
+ this.sharedBuffer[0] = Protocol.ROOM_DATA;
164
252
  if (typeof (type) === "string") {
165
- encode.string(this.packr.buffer, type, it);
253
+ encode.string(this.sharedBuffer, type, it);
254
+ }
255
+ else {
256
+ encode.number(this.sharedBuffer, type, it);
257
+ }
258
+ const headerLength = it.offset;
259
+ let data;
260
+ if (message !== undefined) {
261
+ data = this.packr.pack(message, RESERVE_START_SPACE | headerLength);
262
+ data.set(this.sharedBuffer.subarray(0, headerLength), 0);
166
263
  }
167
264
  else {
168
- encode.number(this.packr.buffer, type, it);
265
+ data = this.sharedBuffer.subarray(0, headerLength);
169
266
  }
170
- // force packr to use beginning of the buffer
171
- this.packr.position = 0;
172
- const data = (message !== undefined)
173
- ? this.packr.pack(message, 2048 + it.offset) // 2048 = RESERVE_START_SPACE
174
- : this.packr.buffer.subarray(0, it.offset);
175
267
  this.connection.sendUnreliable(data);
176
268
  }
177
269
  sendBytes(type, bytes) {
178
270
  const it = { offset: 1 };
179
- this.packr.buffer[0] = Protocol.ROOM_DATA_BYTES;
271
+ this.sharedBuffer[0] = Protocol.ROOM_DATA_BYTES;
180
272
  if (typeof (type) === "string") {
181
- encode.string(this.packr.buffer, type, it);
273
+ encode.string(this.sharedBuffer, type, it);
182
274
  }
183
275
  else {
184
- encode.number(this.packr.buffer, type, it);
276
+ encode.number(this.sharedBuffer, type, it);
185
277
  }
186
- // check if buffer needs to be resized
187
- // TODO: can we avoid this?
188
- if (bytes.byteLength + it.offset > this.packr.buffer.byteLength) {
189
- const newBuffer = new Uint8Array(it.offset + bytes.byteLength);
190
- newBuffer.set(this.packr.buffer);
191
- this.packr.useBuffer(newBuffer);
278
+ const headerLength = it.offset;
279
+ // grow the scratch buffer if needed, preserving the header bytes
280
+ if (headerLength + bytes.byteLength > this.sharedBuffer.byteLength) {
281
+ const newBuffer = new Uint8Array(headerLength + bytes.byteLength);
282
+ newBuffer.set(this.sharedBuffer.subarray(0, headerLength));
283
+ this.sharedBuffer = newBuffer;
192
284
  }
193
- this.packr.buffer.set(bytes, it.offset);
285
+ this.sharedBuffer.set(bytes, headerLength);
194
286
  // If connection is not open, buffer the message
195
287
  if (!this.connection.isOpen) {
196
- enqueueMessage(this, this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
288
+ enqueueMessage(this, this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
197
289
  }
198
290
  else {
199
- this.connection.send(this.packr.buffer.subarray(0, it.offset + bytes.byteLength));
291
+ this.connection.send(this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
200
292
  }
201
293
  }
294
+ /**
295
+ * Get the per-room input handle. Lazily created on first call and cached;
296
+ * subsequent calls return the same handle (options on later calls are
297
+ * ignored).
298
+ *
299
+ * Schema discovery, in order:
300
+ * 1. `options.type` — explicit constructor (overrides everything).
301
+ * 2. Server-sent reflection from the JOIN handshake — populated when the
302
+ * server room called `defineInput()`. The synthesized class has the
303
+ * same fields as the server's input schema; `instanceof YourInput`
304
+ * won't pass on it.
305
+ *
306
+ * Throws if neither source has produced a constructor.
307
+ *
308
+ * For rollback netcode, prefer `{ mode: "unreliable", delta: true,
309
+ * historySize: 4 }`: tiny per-tick payloads, redundancy across drops,
310
+ * idempotent under reordering.
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const conn = await client.joinOrCreate<typeof FpsRoom>("fps");
315
+ * const input = conn.input({ mode: "unreliable" }); // type from server
316
+ * // each simulation tick:
317
+ * input.data.seq++;
318
+ * input.data.vx = vx;
319
+ * input.data.vy = vy;
320
+ * input.send();
321
+ * ```
322
+ */
323
+ input(options) {
324
+ if (this.#inputHandle) {
325
+ return this.#inputHandle;
326
+ }
327
+ const Ctor = (options?.type ?? this.#inputCtorFromReflection);
328
+ if (!Ctor) {
329
+ throw new Error("conn.input(): no input schema available. The server room must call " +
330
+ "`defineInput(YourInput)`, or you can pass `{ type: YourInput }` explicitly.");
331
+ }
332
+ const instance = new Ctor();
333
+ const encoder = new InputEncoder(instance, options);
334
+ this.#inputHandle = new ClientInputHandleImpl(this, instance, encoder);
335
+ return this.#inputHandle;
336
+ }
202
337
  get state() {
203
338
  return this.serializer.getState();
204
339
  }
@@ -227,9 +362,31 @@ class Room {
227
362
  const serializer = getSerializer(this.serializerId);
228
363
  this.serializer = new serializer();
229
364
  }
230
- // apply handshake on first join (no need to do this on reconnect)
231
- if (buffer.byteLength > it.offset && this.serializer.handshake) {
232
- this.serializer.handshake(buffer, it);
365
+ // State reflection is length-prefixed (varint). The schema decoder
366
+ // runs `while (offset < bytes.byteLength)` so without a boundary
367
+ // it would read past the state reflection into the trailing
368
+ // tagged-section bytes — see Protocol.ts for the wire layout.
369
+ const stateReflectionLen = decode.number(buffer, it);
370
+ if (stateReflectionLen > 0 && this.serializer.handshake) {
371
+ const stateReflectionEnd = it.offset + stateReflectionLen;
372
+ this.serializer.handshake(buffer.subarray(0, stateReflectionEnd), it);
373
+ it.offset = stateReflectionEnd;
374
+ }
375
+ // Parse trailing tagged sections (forward-compatible: unknown tags
376
+ // are skipped via length). See HandshakeSection in shared-types.
377
+ while (it.offset < buffer.byteLength) {
378
+ const tag = buffer[it.offset++];
379
+ const sectionLen = decode.number(buffer, it);
380
+ const sectionEnd = it.offset + sectionLen;
381
+ if (tag === HandshakeSection.INPUT_REFLECTION) {
382
+ const inputDecoder = Reflection.decode(buffer.subarray(0, sectionEnd), it);
383
+ // Install schema-builder field descriptors on the
384
+ // reconstructed class so `InputEncoder` can read its
385
+ // `$values` and emit non-empty packets.
386
+ Reflection.makeEncodable(inputDecoder.state.constructor);
387
+ this.#inputCtorFromReflection = inputDecoder.state.constructor;
388
+ }
389
+ it.offset = sectionEnd;
233
390
  }
234
391
  if (this.joinedAtTime === 0) {
235
392
  this.joinedAtTime = Date.now();
@@ -242,8 +399,8 @@ class Room {
242
399
  }
243
400
  this.reconnectionToken = `${this.roomId}:${reconnectionToken}`;
244
401
  // acknowledge successfull JOIN_ROOM
245
- this.packr.buffer[0] = Protocol.JOIN_ROOM;
246
- this.connection.send(this.packr.buffer.subarray(0, 1));
402
+ this.sharedBuffer[0] = Protocol.JOIN_ROOM;
403
+ this.connection.send(this.sharedBuffer.subarray(0, 1));
247
404
  // Send any enqueued messages that were buffered while disconnected
248
405
  if (this.reconnection.enqueuedMessages.length > 0) {
249
406
  for (const message of this.reconnection.enqueuedMessages) {
@@ -284,6 +441,34 @@ class Room {
284
441
  : decode.number(buffer, it);
285
442
  this.dispatchMessage(type, buffer.subarray(it.offset));
286
443
  }
444
+ else if (code === Protocol.ROOM_RESPONSE) {
445
+ // reply to a pending `request()` / `send(..., callback)`
446
+ const requestId = decode.number(buffer, it);
447
+ const status = buffer[it.offset++];
448
+ const pending = this.#pendingRequests.get(requestId);
449
+ // already settled (e.g. timed out) or unknown id — ignore
450
+ if (pending !== undefined) {
451
+ this.#pendingRequests.delete(requestId);
452
+ clearTimeout(pending.timer);
453
+ const payload = (buffer.byteLength > it.offset)
454
+ ? unpack(buffer, { start: it.offset })
455
+ : undefined;
456
+ if (status === ResponseStatus.OK) {
457
+ pending.resolve(payload);
458
+ }
459
+ else {
460
+ // payload carries { name, message, code } from the server
461
+ const error = new Error(payload?.message ?? "request failed");
462
+ if (payload?.name) {
463
+ error.name = payload.name;
464
+ }
465
+ if (payload?.code !== undefined) {
466
+ error.code = payload.code;
467
+ }
468
+ pending.reject(error);
469
+ }
470
+ }
471
+ }
287
472
  else if (code === Protocol.PING) {
288
473
  this.#pingCallback?.(Math.round(now() - this.#lastPingTime));
289
474
  this.#pingCallback = undefined;