@colyseus/core 0.17.42 → 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/MatchMaker.cjs +19 -6
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -0
- package/build/MatchMaker.mjs +18 -6
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +102 -37
- package/build/Protocol.cjs.map +2 -2
- package/build/Protocol.d.ts +33 -2
- package/build/Protocol.mjs +102 -37
- package/build/Protocol.mjs.map +2 -2
- package/build/Room.cjs +296 -19
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +186 -3
- package/build/Room.mjs +303 -21
- package/build/Room.mjs.map +3 -3
- package/build/RoomPlugin.cjs +252 -0
- package/build/RoomPlugin.cjs.map +7 -0
- package/build/RoomPlugin.d.ts +271 -0
- package/build/RoomPlugin.mjs +220 -0
- package/build/RoomPlugin.mjs.map +7 -0
- package/build/Server.cjs +40 -7
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +25 -0
- package/build/Server.mjs +41 -8
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +38 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +40 -4
- package/build/Transport.mjs +38 -2
- package/build/Transport.mjs.map +2 -2
- package/build/index.cjs +11 -2
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +2 -1
- package/build/index.mjs +12 -2
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +113 -0
- package/build/input/InputBuffer.cjs.map +7 -0
- package/build/input/InputBuffer.d.ts +136 -0
- package/build/input/InputBuffer.mjs +86 -0
- package/build/input/InputBuffer.mjs.map +7 -0
- package/build/internal.cjs +61 -0
- package/build/internal.cjs.map +7 -0
- package/build/internal.d.ts +9 -0
- package/build/internal.mjs +29 -0
- package/build/internal.mjs.map +7 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +12 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/rooms/LobbyRoom.cjs +8 -10
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.d.ts +4 -3
- package/build/rooms/LobbyRoom.mjs +8 -10
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/RelayRoom.cjs +12 -16
- package/build/rooms/RelayRoom.cjs.map +2 -2
- package/build/rooms/RelayRoom.d.ts +32 -11
- package/build/rooms/RelayRoom.mjs +10 -16
- package/build/rooms/RelayRoom.mjs.map +2 -2
- package/build/router/index.cjs +65 -4
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +30 -6
- package/build/router/index.mjs +66 -6
- package/build/router/index.mjs.map +3 -3
- package/build/utils/UserSessionIndex.cjs +162 -0
- package/build/utils/UserSessionIndex.cjs.map +7 -0
- package/build/utils/UserSessionIndex.d.ts +166 -0
- package/build/utils/UserSessionIndex.mjs +130 -0
- package/build/utils/UserSessionIndex.mjs.map +7 -0
- package/package.json +19 -14
- package/src/MatchMaker.ts +40 -6
- package/src/Protocol.ts +130 -59
- package/src/Room.ts +475 -22
- package/src/RoomPlugin.ts +563 -0
- package/src/Server.ts +72 -11
- package/src/Transport.ts +76 -8
- package/src/index.ts +10 -1
- package/src/input/InputBuffer.ts +192 -0
- package/src/internal.ts +46 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
- package/src/matchmaker/driver.ts +13 -0
- package/src/rooms/LobbyRoom.ts +12 -8
- package/src/rooms/RelayRoom.ts +9 -15
- package/src/router/index.ts +112 -11
- package/src/utils/UserSessionIndex.ts +311 -0
package/build/Room.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// packages/core/src/Room.ts
|
|
2
|
-
import { unpack } from "
|
|
3
|
-
import { decode, $changes } from "@colyseus/schema";
|
|
2
|
+
import { unpack } from "msgpackr";
|
|
3
|
+
import { decode, Encoder, Reflection, $changes } from "@colyseus/schema";
|
|
4
|
+
import { InputDecoder } from "@colyseus/schema/input";
|
|
5
|
+
import { InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR } from "./input/InputBuffer.mjs";
|
|
6
|
+
import {} from "./input/InputBuffer.mjs";
|
|
4
7
|
import { ClockTimer as Clock } from "@colyseus/timer";
|
|
5
8
|
import { EventEmitter } from "events";
|
|
6
9
|
import { logger } from "./Logger.mjs";
|
|
@@ -19,11 +22,29 @@ import * as matchMaker from "./MatchMaker.mjs";
|
|
|
19
22
|
import {
|
|
20
23
|
CloseCode,
|
|
21
24
|
ErrorCode,
|
|
22
|
-
|
|
25
|
+
HandshakeSection,
|
|
26
|
+
Protocol,
|
|
27
|
+
ResponseStatus
|
|
23
28
|
} from "@colyseus/shared-types";
|
|
29
|
+
import {
|
|
30
|
+
setupRoomPlugins
|
|
31
|
+
} from "./RoomPlugin.mjs";
|
|
32
|
+
import {
|
|
33
|
+
RoomPlugin as RoomPlugin2,
|
|
34
|
+
definePlugins,
|
|
35
|
+
attachToTestRoom
|
|
36
|
+
} from "./RoomPlugin.mjs";
|
|
37
|
+
var _inputReflectionCache = /* @__PURE__ */ new WeakMap();
|
|
24
38
|
var DEFAULT_PATCH_RATE = 1e3 / 20;
|
|
25
39
|
var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
|
|
26
40
|
var noneSerializer = new NoneSerializer();
|
|
41
|
+
function toResponseError(e) {
|
|
42
|
+
if (e instanceof Error) {
|
|
43
|
+
const code = e.code;
|
|
44
|
+
return code !== void 0 ? { name: e.name, message: e.message, code } : { name: e.name, message: e.message };
|
|
45
|
+
}
|
|
46
|
+
return { name: "Error", message: String(e) };
|
|
47
|
+
}
|
|
27
48
|
var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
|
|
28
49
|
function validate(format, handler) {
|
|
29
50
|
return { format, handler };
|
|
@@ -106,6 +127,7 @@ var Room = class _Room {
|
|
|
106
127
|
this._internalState = RoomInternalState.CREATING;
|
|
107
128
|
this._lockedExplicitly = false;
|
|
108
129
|
this.#_locked = false;
|
|
130
|
+
this.#_tick = 0;
|
|
109
131
|
this._events.once("dispose", () => {
|
|
110
132
|
this.#_dispose().catch((e) => debugAndPrintError(`onDispose error: ${e && e.stack || e.message || e || "promise rejected"} (roomId: ${this.roomId})`)).finally(() => this._events.emit("disconnect"));
|
|
111
133
|
});
|
|
@@ -222,6 +244,9 @@ var Room = class _Room {
|
|
|
222
244
|
if (this.#_state) {
|
|
223
245
|
this.state = this.#_state;
|
|
224
246
|
}
|
|
247
|
+
if (this.plugins !== void 0) {
|
|
248
|
+
setupRoomPlugins(this);
|
|
249
|
+
}
|
|
225
250
|
if (this.messages !== void 0) {
|
|
226
251
|
if (this.messages["_"]) {
|
|
227
252
|
this.onMessage("*", this.messages["_"].bind(this));
|
|
@@ -278,6 +303,44 @@ var Room = class _Room {
|
|
|
278
303
|
}
|
|
279
304
|
this.#_roomId = roomId;
|
|
280
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Declare the input schema and configuration in a single line. Returns the
|
|
308
|
+
* callable accessor that gets assigned to `this.input` — call
|
|
309
|
+
* `this.input(sessionId)` per tick to consume.
|
|
310
|
+
*
|
|
311
|
+
* ```typescript
|
|
312
|
+
* class FpsRoom extends Room<{ input: MoveInput }> {
|
|
313
|
+
* input = this.defineInput(MoveInput, {
|
|
314
|
+
* seqField: "tick", // typed: only numeric fields of MoveInput
|
|
315
|
+
* bufferMaxSize: 64,
|
|
316
|
+
* });
|
|
317
|
+
*
|
|
318
|
+
* // …or without options — defaults to seqField: "seq", bufferMaxSize: 32:
|
|
319
|
+
* // input = this.defineInput(MoveInput);
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*
|
|
323
|
+
* **Defaults** when `opts` (or individual fields) are omitted:
|
|
324
|
+
* - `seqField`: `"seq"` — framework dedupes by `input.seq` if the schema has
|
|
325
|
+
* such a field. Schemas without it gracefully skip dedupe.
|
|
326
|
+
* - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
|
|
327
|
+
* `room.input(sessionId).drain() / .peek() / .at()`. Set to `0` to disable
|
|
328
|
+
* buffering (the `.latest` read still works).
|
|
329
|
+
*/
|
|
330
|
+
defineInput(type, opts) {
|
|
331
|
+
this.inputOptions = {
|
|
332
|
+
ctor: type,
|
|
333
|
+
seqField: opts?.seqField ?? "seq",
|
|
334
|
+
bufferMaxSize: opts?.bufferMaxSize ?? 32
|
|
335
|
+
};
|
|
336
|
+
if (!_inputReflectionCache.has(type)) {
|
|
337
|
+
_inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));
|
|
338
|
+
}
|
|
339
|
+
return ((sessionId) => {
|
|
340
|
+
const c = this.clients.getById(sessionId);
|
|
341
|
+
return c?._inputAccessor ?? NO_OP_INPUT_ACCESSOR;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
281
344
|
/**
|
|
282
345
|
* This method is called before onJoin() - this is where you should authenticate the client
|
|
283
346
|
* @param client - The client that is authenticating.
|
|
@@ -378,6 +441,47 @@ var Room = class _Room {
|
|
|
378
441
|
}, delay);
|
|
379
442
|
}
|
|
380
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Run a fixed-rate simulation tagged with a monotonic server tick number.
|
|
446
|
+
* Combine with `room.input(sessionId).at(tick)` to retrieve each client's
|
|
447
|
+
* input *for a specific tick* — the building block for lockstep / rollback
|
|
448
|
+
* netcode.
|
|
449
|
+
*
|
|
450
|
+
* Replaces any previous {@link setSimulationInterval}. The current tick is
|
|
451
|
+
* exposed via {@link tick}.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* class LockstepRoom extends Room<{ input: MoveInput }> {
|
|
456
|
+
* input = this.defineInput(MoveInput, { seqField: "tick", bufferMaxSize: 64 });
|
|
457
|
+
*
|
|
458
|
+
* onCreate() {
|
|
459
|
+
* this.setTickedSimulation((tick, dt) => {
|
|
460
|
+
* for (const c of this.clients) {
|
|
461
|
+
* const snapshot = this.input(c.sessionId).at(tick);
|
|
462
|
+
* if (snapshot) this.apply(c, snapshot);
|
|
463
|
+
* // else: predict, freeze, etc. — game-level decision
|
|
464
|
+
* }
|
|
465
|
+
* }, 1000 / 60);
|
|
466
|
+
* }
|
|
467
|
+
* }
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
setTickedSimulation(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL, startTick = 0) {
|
|
471
|
+
this.#_tick = startTick;
|
|
472
|
+
this.setSimulationInterval((dt) => {
|
|
473
|
+
onTickCallback(this.#_tick, dt);
|
|
474
|
+
this.#_tick++;
|
|
475
|
+
}, delay);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Current server tick. Incremented by {@link setTickedSimulation} after each
|
|
479
|
+
* tick callback returns. Returns 0 when no ticked simulation is running.
|
|
480
|
+
*/
|
|
481
|
+
get tick() {
|
|
482
|
+
return this.#_tick;
|
|
483
|
+
}
|
|
484
|
+
#_tick;
|
|
381
485
|
/**
|
|
382
486
|
* @deprecated Use `.patchRate=` instead.
|
|
383
487
|
*/
|
|
@@ -394,19 +498,7 @@ var Room = class _Room {
|
|
|
394
498
|
this._serializer = serializer;
|
|
395
499
|
}
|
|
396
500
|
async setMetadata(meta, persist = true) {
|
|
397
|
-
|
|
398
|
-
this._listing.metadata = meta;
|
|
399
|
-
} else {
|
|
400
|
-
for (const field in meta) {
|
|
401
|
-
if (!meta.hasOwnProperty(field)) {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
this._listing.metadata[field] = meta[field];
|
|
405
|
-
}
|
|
406
|
-
if ("markModified" in this._listing) {
|
|
407
|
-
this._listing.markModified("metadata");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
501
|
+
this._listing.metadata = meta;
|
|
410
502
|
if (persist && this._internalState === RoomInternalState.CREATED) {
|
|
411
503
|
await matchMaker.driver.persist(this._listing);
|
|
412
504
|
this._events.emit("metadata-change");
|
|
@@ -447,7 +539,8 @@ var Room = class _Room {
|
|
|
447
539
|
*
|
|
448
540
|
* @example
|
|
449
541
|
* ```typescript
|
|
450
|
-
* //
|
|
542
|
+
* // Merging with existing metadata: spread `this.metadata` yourself.
|
|
543
|
+
* // `metadata` is always REPLACED (not merged) by setMatchmaking()/setMetadata().
|
|
451
544
|
* await this.setMatchmaking({
|
|
452
545
|
* metadata: { ...this.metadata, round: this.metadata.round + 1 }
|
|
453
546
|
* });
|
|
@@ -621,6 +714,100 @@ var Room = class _Room {
|
|
|
621
714
|
return this.onMessage(messageType, callback);
|
|
622
715
|
}
|
|
623
716
|
}
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// Operator API — used by @colyseus/admin (and monitor in due course)
|
|
719
|
+
// through `remoteRoomCall(roomId, methodName)`. Marked `@internal` because
|
|
720
|
+
// they're framework-tooling primitives, not part of the game-code surface.
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
/**
|
|
723
|
+
* Snapshot the room's live state for an inspector / admin UI. Includes:
|
|
724
|
+
*
|
|
725
|
+
* roomId, name, maxClients, locked, elapsedTime (ms),
|
|
726
|
+
* metadata, clients (sessionId + per-client elapsed + userId when set),
|
|
727
|
+
* state (the schema/json the SDK would see),
|
|
728
|
+
* stateSize (bytes of the encoded full state, or 0 when no serializer).
|
|
729
|
+
*
|
|
730
|
+
* The payload is intentionally plain JSON — `remoteRoomCall` serializes
|
|
731
|
+
* the return value across process boundaries.
|
|
732
|
+
*
|
|
733
|
+
* @internal Operator-only. Game code should not call this.
|
|
734
|
+
*/
|
|
735
|
+
getInspectorView() {
|
|
736
|
+
const elapsed = this.clock.elapsedTime;
|
|
737
|
+
return {
|
|
738
|
+
roomId: this.roomId,
|
|
739
|
+
name: this.roomName,
|
|
740
|
+
clients: this.clients.length,
|
|
741
|
+
maxClients: this.maxClients,
|
|
742
|
+
locked: this.#_locked,
|
|
743
|
+
elapsedTime: elapsed,
|
|
744
|
+
metadata: this.metadata ?? null,
|
|
745
|
+
// Cast through `unknown`: `ExtractRoomClient<T>` vs `Client & ClientPrivate`
|
|
746
|
+
// don't structurally overlap (the user's `client` generic may carry a
|
|
747
|
+
// narrower userData/auth shape), but the runtime objects we walk here
|
|
748
|
+
// always have the private join-time field. The narrow per-property
|
|
749
|
+
// `(c as any)` reads below keep the cast scoped.
|
|
750
|
+
//
|
|
751
|
+
// userId / userEmail read straight off `client.auth` — the JWT
|
|
752
|
+
// payload @colyseus/auth's default onAuth decodes carries both
|
|
753
|
+
// when the user has them on file. Saves the admin a per-client
|
|
754
|
+
// database round-trip; falls back to null when the client signed
|
|
755
|
+
// in anonymously (or with a custom onAuth that returns a shape
|
|
756
|
+
// without those fields).
|
|
757
|
+
clientList: this.clients.map((c) => {
|
|
758
|
+
const auth = c.auth;
|
|
759
|
+
return {
|
|
760
|
+
sessionId: c.sessionId,
|
|
761
|
+
userId: c.userId ?? auth?.id ?? null,
|
|
762
|
+
userEmail: auth?.email ?? null,
|
|
763
|
+
elapsedTime: elapsed - (c._joinedAt ?? elapsed)
|
|
764
|
+
};
|
|
765
|
+
}),
|
|
766
|
+
state: this.state ?? null,
|
|
767
|
+
stateSize: this.#_inspectorStateSize()
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Force-disconnect a single client by sessionId. No-op when the client
|
|
772
|
+
* isn't connected (idempotent — the caller doesn't need to race-check).
|
|
773
|
+
*
|
|
774
|
+
* @internal Operator-only. Game code disconnects clients by calling
|
|
775
|
+
* `.leave()` on the Client object directly.
|
|
776
|
+
*/
|
|
777
|
+
kickClient(sessionId, closeCode = CloseCode.CONSENTED, reason) {
|
|
778
|
+
for (const client of this.clients) {
|
|
779
|
+
if (client.sessionId === sessionId) {
|
|
780
|
+
this.#_forciblyCloseClient(client, closeCode, reason);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Best-effort byte size of the current full state. Falls back to `0`
|
|
787
|
+
* when the room has no serializer or the serializer can't produce a
|
|
788
|
+
* payload (raw rooms, very-early-onCreate, etc.). The serializer
|
|
789
|
+
* detection mirrors `@colyseus/monitor`'s — we read whichever buffer
|
|
790
|
+
* is available across schema v2 and v3.
|
|
791
|
+
*/
|
|
792
|
+
#_inspectorStateSize() {
|
|
793
|
+
const ser = this._serializer;
|
|
794
|
+
if (!ser) {
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
const hasState = ser.encoder || ser.state;
|
|
798
|
+
if (!hasState) {
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const full = ser.getFullState?.();
|
|
803
|
+
if (!full) {
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
return full.byteLength ?? full.length ?? 0;
|
|
807
|
+
} catch {
|
|
808
|
+
return 0;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
624
811
|
/**
|
|
625
812
|
* Disconnect all connected clients, and then dispose the room.
|
|
626
813
|
*
|
|
@@ -658,6 +845,15 @@ var Room = class _Room {
|
|
|
658
845
|
async _onJoin(client, authContext, connectionOptions) {
|
|
659
846
|
const sessionId = client.sessionId;
|
|
660
847
|
client.reconnectionToken = generateId();
|
|
848
|
+
if (this.inputOptions !== void 0) {
|
|
849
|
+
client._input = new this.inputOptions.ctor();
|
|
850
|
+
client._inputDecoder = new InputDecoder(client._input);
|
|
851
|
+
const maxSize = this.inputOptions.bufferMaxSize;
|
|
852
|
+
if (maxSize > 0) {
|
|
853
|
+
client._inputBuffer = new InputBufferImpl(maxSize, this.inputOptions.seqField);
|
|
854
|
+
}
|
|
855
|
+
client._inputAccessor = new InputAccessorImpl(client);
|
|
856
|
+
}
|
|
661
857
|
if (this._reservedSeatTimeouts[sessionId]) {
|
|
662
858
|
clearTimeout(this._reservedSeatTimeouts[sessionId]);
|
|
663
859
|
delete this._reservedSeatTimeouts[sessionId];
|
|
@@ -760,6 +956,13 @@ var Room = class _Room {
|
|
|
760
956
|
client.ref["onleave"] = this._onLeave.bind(this, client);
|
|
761
957
|
client.ref.once("close", client.ref["onleave"]);
|
|
762
958
|
client.ref.on("message", this._onMessage.bind(this, client));
|
|
959
|
+
let extraSections;
|
|
960
|
+
if (!connectionOptions?.skipHandshake && this.inputOptions !== void 0) {
|
|
961
|
+
const inputBytes = _inputReflectionCache.get(this.inputOptions.ctor);
|
|
962
|
+
if (inputBytes !== void 0) {
|
|
963
|
+
extraSections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
|
|
964
|
+
}
|
|
965
|
+
}
|
|
763
966
|
client.raw(getMessageBytes[Protocol.JOIN_ROOM](
|
|
764
967
|
client.reconnectionToken,
|
|
765
968
|
this._serializer.id,
|
|
@@ -767,7 +970,8 @@ var Room = class _Room {
|
|
|
767
970
|
* if skipHandshake is true, we don't need to send the handshake
|
|
768
971
|
* (in case client already has handshake data)
|
|
769
972
|
*/
|
|
770
|
-
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake()
|
|
973
|
+
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake(),
|
|
974
|
+
extraSections
|
|
771
975
|
));
|
|
772
976
|
}
|
|
773
977
|
}
|
|
@@ -834,7 +1038,7 @@ var Room = class _Room {
|
|
|
834
1038
|
}).finally(() => {
|
|
835
1039
|
cleanup();
|
|
836
1040
|
});
|
|
837
|
-
if (this._reconnectionAttempts[reconnectionToken]) {
|
|
1041
|
+
if (this._reconnectionAttempts[reconnectionToken] !== void 0) {
|
|
838
1042
|
debugMatchMaking("resolving reconnection attempt for client - sessionId: '%s', roomId: '%s'", sessionId, this.roomId);
|
|
839
1043
|
this._reconnectionAttempts[reconnectionToken].resolve(true);
|
|
840
1044
|
}
|
|
@@ -862,6 +1066,12 @@ var Room = class _Room {
|
|
|
862
1066
|
}
|
|
863
1067
|
}
|
|
864
1068
|
}
|
|
1069
|
+
// Encode and enqueue a ROOM_RESPONSE for a client request. If the client has
|
|
1070
|
+
// already left, `enqueueRaw` is a no-op — no response is needed.
|
|
1071
|
+
#replyToRequest(client, requestId, status, payload) {
|
|
1072
|
+
debugMessage("response #%d: status=%d -> %j (roomId: %s)", requestId, status, payload, this.roomId);
|
|
1073
|
+
client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));
|
|
1074
|
+
}
|
|
865
1075
|
sendFullState(client) {
|
|
866
1076
|
client.raw(this._serializer.getFullState(client));
|
|
867
1077
|
}
|
|
@@ -959,6 +1169,26 @@ var Room = class _Room {
|
|
|
959
1169
|
this.clock.stop();
|
|
960
1170
|
return await (userReturnData || Promise.resolve());
|
|
961
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* After the decoder has mutated `client._input`, push a clone into the
|
|
1174
|
+
* per-client buffer (when buffering is enabled). Honors
|
|
1175
|
+
* `inputOptions.seqField` for dedupe of redundant frames.
|
|
1176
|
+
*/
|
|
1177
|
+
#captureInput(client) {
|
|
1178
|
+
const buf = client._inputBuffer;
|
|
1179
|
+
if (!buf) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const inst = client._input;
|
|
1183
|
+
const seqField = this.inputOptions?.seqField;
|
|
1184
|
+
if (seqField !== void 0) {
|
|
1185
|
+
const value = inst[seqField];
|
|
1186
|
+
if (typeof value === "number" && !buf.accept(value)) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
buf.push(inst.clone());
|
|
1191
|
+
}
|
|
962
1192
|
_onMessage(client, buffer) {
|
|
963
1193
|
if (client.state === ClientState.LEAVING) {
|
|
964
1194
|
return;
|
|
@@ -997,6 +1227,36 @@ var Room = class _Room {
|
|
|
997
1227
|
} else {
|
|
998
1228
|
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
999
1229
|
}
|
|
1230
|
+
} else if (code === Protocol.ROOM_REQUEST) {
|
|
1231
|
+
const requestId = decode.number(buffer, it);
|
|
1232
|
+
const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
|
|
1233
|
+
let message;
|
|
1234
|
+
try {
|
|
1235
|
+
message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1236
|
+
debugMessage("request #%d: '%s' -> %j (roomId: %s)", requestId, messageType, message, this.roomId);
|
|
1237
|
+
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1238
|
+
message = standardValidate(this.onMessageValidators[messageType], message);
|
|
1239
|
+
}
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
debugAndPrintError(e);
|
|
1242
|
+
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const handler = this.onMessageEvents.events[messageType]?.[0];
|
|
1246
|
+
if (handler === void 0) {
|
|
1247
|
+
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {
|
|
1248
|
+
name: "no_handler",
|
|
1249
|
+
message: `room "${this.roomName}" has no onMessage("${messageType}") handler to answer this request.`
|
|
1250
|
+
});
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
Promise.resolve().then(() => handler(client, message)).then(
|
|
1254
|
+
(response) => this.#replyToRequest(client, requestId, ResponseStatus.OK, response),
|
|
1255
|
+
(e) => {
|
|
1256
|
+
debugAndPrintError(e);
|
|
1257
|
+
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1000
1260
|
} else if (code === Protocol.ROOM_DATA_BYTES) {
|
|
1001
1261
|
const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
|
|
1002
1262
|
let message = buffer.subarray(it.offset, buffer.byteLength);
|
|
@@ -1018,6 +1278,25 @@ var Room = class _Room {
|
|
|
1018
1278
|
} else {
|
|
1019
1279
|
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1020
1280
|
}
|
|
1281
|
+
} else if (code === Protocol.ROOM_INPUT_RELIABLE) {
|
|
1282
|
+
if (client._inputDecoder) {
|
|
1283
|
+
try {
|
|
1284
|
+
client._inputDecoder.decode(buffer.subarray(1));
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
debugAndPrintError(e);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
this.#captureInput(client);
|
|
1290
|
+
}
|
|
1291
|
+
} else if (code === Protocol.ROOM_INPUT_UNRELIABLE) {
|
|
1292
|
+
if (client._inputDecoder) {
|
|
1293
|
+
try {
|
|
1294
|
+
client._inputDecoder.decodeAll(buffer.subarray(1), () => this.#captureInput(client));
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
debugAndPrintError(e);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1021
1300
|
} else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {
|
|
1022
1301
|
client.state = ClientState.JOINED;
|
|
1023
1302
|
client._joinedAt = this.clock.elapsedTime;
|
|
@@ -1034,10 +1313,10 @@ var Room = class _Room {
|
|
|
1034
1313
|
this.#_forciblyCloseClient(client, CloseCode.CONSENTED);
|
|
1035
1314
|
}
|
|
1036
1315
|
}
|
|
1037
|
-
#_forciblyCloseClient(client, closeCode) {
|
|
1316
|
+
#_forciblyCloseClient(client, closeCode, reason) {
|
|
1038
1317
|
client.ref.removeAllListeners("message");
|
|
1039
1318
|
client.ref.removeListener("close", client.ref["onleave"]);
|
|
1040
|
-
this._onLeave(client, closeCode).then(() => client.leave(closeCode));
|
|
1319
|
+
this._onLeave(client, closeCode).then(() => client.leave(closeCode, reason));
|
|
1041
1320
|
}
|
|
1042
1321
|
async _onLeave(client, code) {
|
|
1043
1322
|
const method = code === CloseCode.CONSENTED || client.state === ClientState.RECONNECTING ? this.onLeave : this.onDrop || this.onLeave;
|
|
@@ -1155,6 +1434,9 @@ export {
|
|
|
1155
1434
|
DEFAULT_SEAT_RESERVATION_TIME,
|
|
1156
1435
|
Room,
|
|
1157
1436
|
RoomInternalState,
|
|
1437
|
+
RoomPlugin2 as RoomPlugin,
|
|
1438
|
+
attachToTestRoom,
|
|
1439
|
+
definePlugins,
|
|
1158
1440
|
room,
|
|
1159
1441
|
validate
|
|
1160
1442
|
};
|