@colyseus/core 0.18.4 → 0.18.6

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 (122) hide show
  1. package/build/Debug.cjs +1 -0
  2. package/build/Debug.cjs.map +1 -1
  3. package/build/IPC.cjs +1 -0
  4. package/build/IPC.cjs.map +1 -1
  5. package/build/Logger.cjs +1 -0
  6. package/build/Logger.cjs.map +1 -1
  7. package/build/MatchMaker.cjs +11 -3
  8. package/build/MatchMaker.cjs.map +2 -2
  9. package/build/MatchMaker.mjs +10 -3
  10. package/build/MatchMaker.mjs.map +2 -2
  11. package/build/Protocol.cjs +1 -0
  12. package/build/Protocol.cjs.map +1 -1
  13. package/build/Rewind.cjs +1 -0
  14. package/build/Rewind.cjs.map +1 -1
  15. package/build/Room.cjs +102 -0
  16. package/build/Room.cjs.map +2 -2
  17. package/build/Room.d.ts +57 -0
  18. package/build/Room.mjs +101 -0
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +1 -0
  21. package/build/RoomMessages.cjs.map +1 -1
  22. package/build/RoomPlugin.cjs +1 -0
  23. package/build/RoomPlugin.cjs.map +1 -1
  24. package/build/Server.cjs +1 -0
  25. package/build/Server.cjs.map +1 -1
  26. package/build/Stats.cjs +1 -0
  27. package/build/Stats.cjs.map +1 -1
  28. package/build/Transport.cjs +39 -0
  29. package/build/Transport.cjs.map +2 -2
  30. package/build/Transport.d.ts +66 -1
  31. package/build/Transport.mjs +37 -1
  32. package/build/Transport.mjs.map +2 -2
  33. package/build/errors/RoomExceptions.cjs +1 -0
  34. package/build/errors/RoomExceptions.cjs.map +1 -1
  35. package/build/errors/SeatReservationError.cjs +1 -0
  36. package/build/errors/SeatReservationError.cjs.map +1 -1
  37. package/build/errors/ServerError.cjs +1 -0
  38. package/build/errors/ServerError.cjs.map +1 -1
  39. package/build/index.cjs +5 -0
  40. package/build/index.cjs.map +2 -2
  41. package/build/index.d.ts +1 -1
  42. package/build/index.mjs +3 -1
  43. package/build/index.mjs.map +2 -2
  44. package/build/input/InputBuffer.cjs +1 -0
  45. package/build/input/InputBuffer.cjs.map +1 -1
  46. package/build/input/RoomInput.cjs +64 -3
  47. package/build/input/RoomInput.cjs.map +2 -2
  48. package/build/input/RoomInput.d.ts +16 -2
  49. package/build/input/RoomInput.mjs +63 -3
  50. package/build/input/RoomInput.mjs.map +2 -2
  51. package/build/input/types.cjs +1 -0
  52. package/build/input/types.cjs.map +1 -1
  53. package/build/internal.cjs +1 -0
  54. package/build/internal.cjs.map +1 -1
  55. package/build/matchmaker/Lobby.cjs +1 -0
  56. package/build/matchmaker/Lobby.cjs.map +1 -1
  57. package/build/matchmaker/LocalDriver/LocalDriver.cjs +1 -0
  58. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +1 -1
  59. package/build/matchmaker/LocalDriver/Query.cjs +1 -0
  60. package/build/matchmaker/LocalDriver/Query.cjs.map +1 -1
  61. package/build/matchmaker/RegisteredHandler.cjs +1 -0
  62. package/build/matchmaker/RegisteredHandler.cjs.map +1 -1
  63. package/build/matchmaker/controller.cjs +1 -0
  64. package/build/matchmaker/controller.cjs.map +1 -1
  65. package/build/matchmaker/driver.cjs +1 -0
  66. package/build/matchmaker/driver.cjs.map +2 -2
  67. package/build/matchmaker/driver.d.ts +4 -2
  68. package/build/matchmaker/driver.mjs.map +1 -1
  69. package/build/presence/LocalPresence.cjs +1 -0
  70. package/build/presence/LocalPresence.cjs.map +1 -1
  71. package/build/presence/Presence.cjs +1 -0
  72. package/build/presence/Presence.cjs.map +1 -1
  73. package/build/rooms/LobbyRoom.cjs +1 -0
  74. package/build/rooms/LobbyRoom.cjs.map +1 -1
  75. package/build/rooms/QueueRoom.cjs +1 -0
  76. package/build/rooms/QueueRoom.cjs.map +1 -1
  77. package/build/rooms/RelayRoom.cjs +1 -0
  78. package/build/rooms/RelayRoom.cjs.map +1 -1
  79. package/build/router/default_routes.cjs +2 -7
  80. package/build/router/default_routes.cjs.map +2 -2
  81. package/build/router/default_routes.mjs +2 -8
  82. package/build/router/default_routes.mjs.map +2 -2
  83. package/build/router/index.cjs +48 -1
  84. package/build/router/index.cjs.map +3 -3
  85. package/build/router/index.d.ts +6 -2
  86. package/build/router/index.mjs +47 -1
  87. package/build/router/index.mjs.map +2 -2
  88. package/build/router/node.cjs +1 -0
  89. package/build/router/node.cjs.map +1 -1
  90. package/build/serializer/NoneSerializer.cjs +1 -0
  91. package/build/serializer/NoneSerializer.cjs.map +1 -1
  92. package/build/serializer/SchemaSerializer.cjs +98 -0
  93. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  94. package/build/serializer/SchemaSerializer.d.ts +25 -0
  95. package/build/serializer/SchemaSerializer.mjs +98 -1
  96. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  97. package/build/serializer/Serializer.cjs +1 -0
  98. package/build/serializer/Serializer.cjs.map +2 -2
  99. package/build/serializer/Serializer.d.ts +15 -0
  100. package/build/utils/DevMode.cjs +1 -0
  101. package/build/utils/DevMode.cjs.map +1 -1
  102. package/build/utils/Env.cjs +1 -0
  103. package/build/utils/Env.cjs.map +1 -1
  104. package/build/utils/StandardSchema.cjs +1 -0
  105. package/build/utils/StandardSchema.cjs.map +1 -1
  106. package/build/utils/UserSessionIndex.cjs +1 -0
  107. package/build/utils/UserSessionIndex.cjs.map +1 -1
  108. package/build/utils/Utils.cjs +1 -0
  109. package/build/utils/Utils.cjs.map +1 -1
  110. package/build/utils/nanoevents.cjs +1 -0
  111. package/build/utils/nanoevents.cjs.map +1 -1
  112. package/package.json +5 -5
  113. package/src/MatchMaker.ts +20 -3
  114. package/src/Room.ts +122 -0
  115. package/src/Transport.ts +132 -4
  116. package/src/index.ts +1 -1
  117. package/src/input/RoomInput.ts +76 -3
  118. package/src/matchmaker/driver.ts +4 -2
  119. package/src/router/default_routes.ts +2 -8
  120. package/src/router/index.ts +72 -3
  121. package/src/serializer/SchemaSerializer.ts +146 -1
  122. package/src/serializer/Serializer.ts +17 -0
@@ -3,8 +3,9 @@ import { Protocol, ProtocolModifier } from '@colyseus/shared-types';
3
3
  import type { PatchTimingContext, Serializer } from './Serializer.ts';
4
4
  import { type Client, type ClientPrivate, ClientState } from '../Transport.ts';
5
5
 
6
- import { type Iterator, encode, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
6
+ import { type Iterator, $numFields, encode, Encoder, dumpChanges, Metadata, Reflection, Schema, StateView, TypeContext } from '@colyseus/schema';
7
7
  import { debugPatch } from '../Debug.ts';
8
+ import { logger } from '../Logger.ts';
8
9
 
9
10
  /**
10
11
  * Size of the {@link ProtocolModifier.TIMED} prefix that follows the
@@ -80,6 +81,38 @@ function sendClientFrame(client: Client, encodedBody: Uint8Array, timing?: Patch
80
81
  }
81
82
  }
82
83
 
84
+ /**
85
+ * Size of the {@link ProtocolModifier.UNRELIABLE} header that follows the
86
+ * protocol byte: `[uint16 seq]`. See {@link ProtocolModifier.UNRELIABLE} for
87
+ * the wrap-safe freshness rule the client applies to `seq`.
88
+ */
89
+ const UNRELIABLE_HEADER_SIZE = 2;
90
+
91
+ /**
92
+ * Does any Schema class reachable from the state root declare an `@unreliable`
93
+ * field? Walked once per room, in {@link SchemaSerializer.reset} — the result
94
+ * gates whether the unreliable channel is wired up at all, so rooms that don't
95
+ * use it never reach any of the code below.
96
+ */
97
+ function stateHasUnreliableFields(context: TypeContext): boolean {
98
+ for (const klass of context.schemas.keys()) {
99
+ const metadata = klass[Symbol.metadata];
100
+ if (metadata === undefined) { continue; }
101
+
102
+ // `$numFields` is the last index, inclusive (matches `Metadata.getFields`).
103
+ for (let i = 0; i <= metadata[$numFields]; i++) {
104
+ if (Metadata.hasUnreliableAtIndex(metadata, i)) { return true; }
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+
110
+ /** Can this client receive the unreliable channel right now? Its transport
111
+ * exposing {@link Client.rawUnreliable} IS the capability check. */
112
+ function canReceiveUnreliable(client: Client): boolean {
113
+ return client.state === ClientState.JOINED && client.rawUnreliable !== undefined;
114
+ }
115
+
83
116
  const SHARED_VIEW = {};
84
117
 
85
118
  export class SchemaSerializer<T extends Schema> implements Serializer<T> {
@@ -100,15 +133,34 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
100
133
 
101
134
  protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
102
135
 
136
+ /** @see {@link Serializer.hasUnreliableFields} */
137
+ public hasUnreliableFields: boolean = false;
138
+
139
+ /** Room-wide counter stamped into every unreliable frame so the client can
140
+ * drop reordered datagrams. Wraps at 65536 — see {@link ProtocolModifier.UNRELIABLE}. */
141
+ protected unreliableSeq: number = 0;
142
+
143
+ protected encodedUnreliableViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
144
+
145
+ /** The `@unreliable`-with-no-datagram-transport warning fires once per room. */
146
+ protected warnedNoUnreliableTransport = false;
147
+
103
148
  public reset(newState: T & Schema) {
104
149
  this.encoder = new Encoder(newState);
105
150
  this.hasFilters = this.encoder.context.hasFilters;
151
+ this.hasUnreliableFields = stateHasUnreliableFields(this.encoder.context);
106
152
 
107
153
  // cache ROOM_STATE byte as part of the encoded buffer
108
154
  this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;
109
155
 
110
156
  if (this.hasFilters) {
111
157
  this.encodedViews = new Map();
158
+
159
+ if (this.hasUnreliableFields) {
160
+ // Separate from `encodedViews`: the two channels encode on independent
161
+ // cadences, so a reliable tick's cache must never be served to one.
162
+ this.encodedUnreliableViews = new Map();
163
+ }
112
164
  }
113
165
  }
114
166
 
@@ -273,6 +325,99 @@ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
273
325
  return true;
274
326
  }
275
327
 
328
+ /**
329
+ * Encode and send the UNRELIABLE channel — the `@unreliable` fields, which
330
+ * `applyPatches` never emits. Frames go out via {@link Client.rawUnreliable}
331
+ * (a WebTransport datagram); clients whose transport has no unreliable
332
+ * channel are skipped entirely.
333
+ *
334
+ * Runs on its own cadence, independent of `applyPatches`. Only ever called on
335
+ * rooms where {@link hasUnreliableFields} is true.
336
+ */
337
+ public applyUnreliablePatches(clients: Client[]): boolean {
338
+ if (!this.encoder.hasUnreliableChanges) { return false; }
339
+
340
+ // These mutations are visible to `encodeAll`, so the cached full state is
341
+ // stale — even though `root.changes` (the reliable queue) is empty and
342
+ // `getFullState`'s own guard would therefore miss it.
343
+ this.needFullEncode = true;
344
+
345
+ if (!clients.some(canReceiveUnreliable)) {
346
+ this.warnNoUnreliableTransport(clients);
347
+ // Drain regardless: the queue is not self-clearing, and an un-discarded
348
+ // tick would grow it without bound.
349
+ this.encoder.discardUnreliableChanges();
350
+ return false;
351
+ }
352
+
353
+ this.unreliableSeq = (this.unreliableSeq + 1) & 0xFFFF;
354
+
355
+ // Stamp the header before encoding, as the reliable path does. Safe across
356
+ // a buffer overflow: `Encoder._resizeBuffer` copies the old bytes forward.
357
+ const it: Iterator = { offset: 1 + UNRELIABLE_HEADER_SIZE };
358
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH | ProtocolModifier.UNRELIABLE;
359
+ encode.uint16(this.encoder.sharedBuffer, this.unreliableSeq, { offset: 1 });
360
+
361
+ // encode changes once, for all clients
362
+ const encodedChanges = this.encoder.encodeUnreliable(it);
363
+
364
+ let numClients = clients.length;
365
+
366
+ if (!this.hasFilters) {
367
+ while (numClients--) {
368
+ const client = clients[numClients];
369
+ if (canReceiveUnreliable(client)) { client.rawUnreliable!(encodedChanges); }
370
+ }
371
+
372
+ } else {
373
+ // cache shared offset
374
+ const sharedOffset = it.offset;
375
+
376
+ while (numClients--) {
377
+ const client = clients[numClients];
378
+ if (!canReceiveUnreliable(client)) { continue; }
379
+
380
+ const view = client.view || SHARED_VIEW;
381
+
382
+ let encodedView = this.encodedUnreliableViews.get(view);
383
+
384
+ // allow to pass the same encoded view for multiple clients
385
+ if (encodedView === undefined) {
386
+ encodedView = (view === SHARED_VIEW)
387
+ ? encodedChanges
388
+ : this.encoder.encodeUnreliableView(client.view, sharedOffset, it);
389
+ this.encodedUnreliableViews.set(view, encodedView);
390
+ }
391
+
392
+ client.rawUnreliable!(encodedView);
393
+ }
394
+
395
+ // clear views
396
+ this.encodedUnreliableViews.clear();
397
+ }
398
+
399
+ // discard changes after sending
400
+ this.encoder.discardUnreliableChanges();
401
+
402
+ return true;
403
+ }
404
+
405
+ /**
406
+ * The state declares `@unreliable` fields but not one connected client can
407
+ * receive them — every WebSocket transport lacks a datagram channel. Those
408
+ * fields keep their join-time value and never update, which is silent
409
+ * otherwise. Warns once per room.
410
+ */
411
+ private warnNoUnreliableTransport(clients: Client[]) {
412
+ if (this.warnedNoUnreliableTransport || clients.length === 0) { return; }
413
+ this.warnedNoUnreliableTransport = true;
414
+ logger.warn(
415
+ "@colyseus/core: state has @unreliable fields, but no connected client's transport" +
416
+ " provides an unreliable channel — those fields will not be patched." +
417
+ " Use @colyseus/h3-transport (WebTransport) to enable them."
418
+ );
419
+ }
420
+
276
421
  public handshake() {
277
422
  /**
278
423
  * Cache handshake to avoid encoding it for each client joining
@@ -17,4 +17,21 @@ export interface Serializer<T> {
17
17
  getFullState(client?: Client, timing?: PatchTimingContext): Uint8Array;
18
18
  applyPatches(clients: Client[], state: T, timing?: PatchTimingContext): boolean;
19
19
  handshake?(): Uint8Array;
20
+
21
+ /**
22
+ * Whether the state declares at least one `@unreliable` field. Computed once
23
+ * in {@link Serializer.reset}; the Room reads it once to decide whether to arm
24
+ * the unreliable flush at all. Never consulted on the tick path — a room that
25
+ * doesn't use the channel must not pay for it.
26
+ */
27
+ hasUnreliableFields?: boolean;
28
+
29
+ /**
30
+ * Encode and send the UNRELIABLE channel to every client whose transport
31
+ * exposes {@link Client.rawUnreliable}. Only called on rooms where
32
+ * {@link Serializer.hasUnreliableFields} is true.
33
+ *
34
+ * Runs on its own cadence, independent of {@link Serializer.applyPatches}.
35
+ */
36
+ applyUnreliablePatches?(clients: Client[]): boolean;
20
37
  }