@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.
- package/build/3rd_party/discord.cjs +1 -1
- package/build/3rd_party/discord.mjs +1 -1
- package/build/Auth.cjs +16 -2
- package/build/Auth.cjs.map +1 -1
- package/build/Auth.mjs +16 -2
- package/build/Auth.mjs.map +1 -1
- package/build/Client.cjs +1 -1
- package/build/Client.mjs +1 -1
- package/build/Connection.cjs +1 -1
- package/build/Connection.mjs +1 -1
- package/build/HTTP.cjs +1 -1
- package/build/HTTP.mjs +1 -1
- package/build/Room.cjs +231 -46
- package/build/Room.cjs.map +1 -1
- package/build/Room.d.ts +62 -2
- package/build/Room.mjs +229 -44
- package/build/Room.mjs.map +1 -1
- package/build/Storage.cjs +1 -1
- package/build/Storage.mjs +1 -1
- package/build/core/nanoevents.cjs +1 -1
- package/build/core/nanoevents.mjs +1 -1
- package/build/core/signal.cjs +1 -1
- package/build/core/signal.cjs.map +1 -1
- package/build/core/signal.mjs +1 -1
- package/build/core/signal.mjs.map +1 -1
- package/build/core/utils.cjs +1 -1
- package/build/core/utils.mjs +1 -1
- package/build/debug.cjs +1 -1
- package/build/debug.cjs.map +1 -1
- package/build/debug.mjs +1 -1
- package/build/debug.mjs.map +1 -1
- package/build/errors/Errors.cjs +1 -1
- package/build/errors/Errors.mjs +1 -1
- package/build/fetchXHR.cjs +1 -1
- package/build/fetchXHR.mjs +1 -1
- package/build/index.cjs +1 -1
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.mjs +1 -1
- package/build/index.mjs.map +1 -1
- package/build/input/InputHandle.cjs +47 -0
- package/build/input/InputHandle.cjs.map +1 -0
- package/build/input/InputHandle.d.ts +79 -0
- package/build/input/InputHandle.mjs +48 -0
- package/build/input/InputHandle.mjs.map +1 -0
- package/build/legacy.cjs +1 -1
- package/build/legacy.mjs +1 -1
- package/build/serializer/NoneSerializer.cjs +1 -1
- package/build/serializer/NoneSerializer.mjs +1 -1
- package/build/serializer/SchemaSerializer.cjs +1 -1
- package/build/serializer/SchemaSerializer.mjs +1 -1
- package/build/serializer/Serializer.cjs +1 -1
- package/build/serializer/Serializer.mjs +1 -1
- package/build/transport/H3Transport.cjs +1 -1
- package/build/transport/H3Transport.cjs.map +1 -1
- package/build/transport/H3Transport.mjs +1 -1
- package/build/transport/H3Transport.mjs.map +1 -1
- package/build/transport/WebSocketTransport.cjs +1 -1
- package/build/transport/WebSocketTransport.mjs +1 -1
- package/dist/colyseus.js +13152 -1885
- package/dist/colyseus.js.map +1 -1
- package/dist/debug.js +1 -1
- package/dist/debug.js.map +1 -1
- package/package.json +11 -8
- package/src/Auth.ts +11 -1
- package/src/Room.ts +294 -48
- package/src/core/signal.ts +1 -1
- package/src/debug.ts +8 -8
- package/src/index.ts +1 -1
- package/src/input/InputHandle.ts +115 -0
- package/src/transport/H3Transport.ts +2 -2
- package/build/serializer/FossilDeltaSerializer.d.ts +0 -0
- 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.
|
|
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/
|
|
55
|
-
"
|
|
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/
|
|
59
|
-
"@colyseus/
|
|
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": "^
|
|
82
|
+
"typescript": "^6.0.3",
|
|
83
83
|
"vite": "^5.0.11",
|
|
84
84
|
"vitest": "^2.1.1",
|
|
85
|
-
"@colyseus/core": "^0.
|
|
85
|
+
"@colyseus/core": "^0.18.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
|
-
"@colyseus/core": "0.
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
217
|
-
this.connection.send(this.
|
|
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.
|
|
252
|
-
this.connection.send(this.
|
|
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
|
-
|
|
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.
|
|
327
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA;
|
|
267
328
|
|
|
268
329
|
if (typeof(messageType) === "string") {
|
|
269
|
-
encode.string(this.
|
|
330
|
+
encode.string(this.sharedBuffer, messageType, it);
|
|
270
331
|
|
|
271
332
|
} else {
|
|
272
|
-
encode.number(this.
|
|
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.
|
|
436
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA;
|
|
296
437
|
|
|
297
438
|
if (typeof(type) === "string") {
|
|
298
|
-
encode.string(this.
|
|
439
|
+
encode.string(this.sharedBuffer, type, it);
|
|
299
440
|
|
|
300
441
|
} else {
|
|
301
|
-
encode.number(this.
|
|
442
|
+
encode.number(this.sharedBuffer, type, it);
|
|
302
443
|
}
|
|
444
|
+
const headerLength = it.offset;
|
|
303
445
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
459
|
+
this.sharedBuffer[0] = Protocol.ROOM_DATA_BYTES;
|
|
317
460
|
|
|
318
461
|
if (typeof(type) === "string") {
|
|
319
|
-
encode.string(this.
|
|
462
|
+
encode.string(this.sharedBuffer, type, it);
|
|
320
463
|
|
|
321
464
|
} else {
|
|
322
|
-
encode.number(this.
|
|
465
|
+
encode.number(this.sharedBuffer, type, it);
|
|
323
466
|
}
|
|
467
|
+
const headerLength = it.offset;
|
|
324
468
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
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.
|
|
480
|
+
enqueueMessage(this, this.sharedBuffer.subarray(0, headerLength + bytes.byteLength));
|
|
338
481
|
} else {
|
|
339
|
-
this.connection.send(this.
|
|
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
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
398
|
-
this.connection.send(this.
|
|
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
|
+
|
package/src/core/signal.ts
CHANGED
|
@@ -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