@colyseus/core 0.18.23 → 0.18.25

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 (42) hide show
  1. package/build/Room.cjs.map +2 -2
  2. package/build/Room.d.ts +3 -0
  3. package/build/Room.mjs.map +2 -2
  4. package/build/RoomMessages.cjs +23 -4
  5. package/build/RoomMessages.cjs.map +2 -2
  6. package/build/RoomMessages.mjs +23 -4
  7. package/build/RoomMessages.mjs.map +2 -2
  8. package/build/index.cjs +2 -0
  9. package/build/index.cjs.map +2 -2
  10. package/build/index.d.ts +1 -1
  11. package/build/index.mjs +2 -1
  12. package/build/index.mjs.map +2 -2
  13. package/build/input/InputBuffer.cjs +52 -1
  14. package/build/input/InputBuffer.cjs.map +3 -3
  15. package/build/input/InputBuffer.d.ts +13 -0
  16. package/build/input/InputBuffer.mjs +51 -1
  17. package/build/input/InputBuffer.mjs.map +3 -3
  18. package/build/input/RoomInput.cjs +3 -2
  19. package/build/input/RoomInput.cjs.map +2 -2
  20. package/build/input/RoomInput.d.ts +1 -1
  21. package/build/input/RoomInput.mjs +4 -2
  22. package/build/input/RoomInput.mjs.map +2 -2
  23. package/build/input/types.cjs.map +1 -1
  24. package/build/input/types.d.ts +6 -1
  25. package/build/matchmaker/Lobby.cjs +15 -10
  26. package/build/matchmaker/Lobby.cjs.map +2 -2
  27. package/build/matchmaker/Lobby.mjs +15 -10
  28. package/build/matchmaker/Lobby.mjs.map +2 -2
  29. package/build/rooms/LobbyRoom.cjs +22 -9
  30. package/build/rooms/LobbyRoom.cjs.map +2 -2
  31. package/build/rooms/LobbyRoom.d.ts +4 -1
  32. package/build/rooms/LobbyRoom.mjs +22 -9
  33. package/build/rooms/LobbyRoom.mjs.map +2 -2
  34. package/package.json +3 -3
  35. package/src/Room.ts +3 -0
  36. package/src/RoomMessages.ts +24 -4
  37. package/src/index.ts +1 -1
  38. package/src/input/InputBuffer.ts +58 -1
  39. package/src/input/RoomInput.ts +6 -5
  40. package/src/input/types.ts +7 -1
  41. package/src/matchmaker/Lobby.ts +22 -15
  42. package/src/rooms/LobbyRoom.ts +26 -10
@@ -35,6 +35,7 @@ __export(LobbyRoom_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(LobbyRoom_exports);
37
37
  var matchMaker = __toESM(require("../MatchMaker.cjs"), 1);
38
+ var import_Debug = require("../Debug.cjs");
38
39
  var import_Lobby = require("../matchmaker/Lobby.cjs");
39
40
  var import_Room = require("../Room.cjs");
40
41
  var LobbyRoom = class extends import_Room.Room {
@@ -55,30 +56,29 @@ var LobbyRoom = class extends import_Room.Room {
55
56
  });
56
57
  this.unsubscribeLobby = await (0, import_Lobby.subscribeLobby)((roomId, data) => {
57
58
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
58
- const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);
59
59
  if (!data) {
60
60
  if (roomIndex !== -1) {
61
61
  const previousData = this.rooms[roomIndex];
62
62
  this.rooms.splice(roomIndex, 1);
63
- clients.forEach((client) => {
64
- if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {
63
+ this.#notifyClients((client, filter) => {
64
+ if (this.filterItemForClient(previousData, filter)) {
65
65
  client.send("-", roomId);
66
66
  }
67
67
  });
68
68
  }
69
69
  } else if (roomIndex === -1) {
70
70
  this.rooms.push(data);
71
- clients.forEach((client) => {
72
- if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {
71
+ this.#notifyClients((client, filter) => {
72
+ if (this.filterItemForClient(data, filter)) {
73
73
  client.send("+", [roomId, data]);
74
74
  }
75
75
  });
76
76
  } else {
77
77
  const previousData = this.rooms[roomIndex];
78
78
  this.rooms[roomIndex] = data;
79
- clients.forEach((client) => {
80
- const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);
81
- const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);
79
+ this.#notifyClients((client, filter) => {
80
+ const hadData = this.filterItemForClient(previousData, filter);
81
+ const hasData = this.filterItemForClient(data, filter);
82
82
  if (hadData && !hasData) {
83
83
  client.send("-", roomId);
84
84
  } else if (hasData) {
@@ -102,6 +102,19 @@ var LobbyRoom = class extends import_Room.Room {
102
102
  this.unsubscribeLobby();
103
103
  }
104
104
  }
105
+ #notifyClients(notify) {
106
+ this.clients.forEach((client) => {
107
+ const options = this.clientOptions[client.sessionId];
108
+ if (!options) {
109
+ return;
110
+ }
111
+ try {
112
+ notify(client, options.filter);
113
+ } catch (e) {
114
+ (0, import_Debug.debugAndPrintError)(e);
115
+ }
116
+ });
117
+ }
105
118
  filterItemsForClient(options) {
106
119
  const filter = options.filter;
107
120
  return filter ? this.rooms.filter((room) => this.filterItemForClient(room, filter)) : this.rooms;
@@ -116,7 +129,7 @@ var LobbyRoom = class extends import_Room.Room {
116
129
  }
117
130
  if (filter.metadata) {
118
131
  for (const field in filter.metadata) {
119
- if (room.metadata[field] !== filter.metadata[field]) {
132
+ if (room.metadata?.[field] !== filter.metadata[field]) {
120
133
  isAllowed = false;
121
134
  break;
122
135
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/LobbyRoom.ts"],
4
- "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport { debugAndPrintError } from '../Debug.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n /** Room name to list. Required: a filter without it matches nothing. */\n name: string;\n\n /** Narrows the listing to rooms whose metadata has these exact values. */\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n this.#notifyClients((client, filter) => {\n if (this.filterItemForClient(previousData, filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n this.#notifyClients((client, filter) => {\n if (this.filterItemForClient(data, filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n this.#notifyClients((client, filter) => {\n const hadData = this.filterItemForClient(previousData, filter);\n const hasData = this.filterItemForClient(data, filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n #notifyClients(notify: (client: LobbyClient, filter: FilterInput) => void) {\n this.clients.forEach((client) => {\n const options = this.clientOptions[client.sessionId];\n if (!options) { return; }\n\n try {\n notify(client, options.filter);\n } catch (e: any) {\n debugAndPrintError(e); // one client's failure must not skip the rest\n }\n });\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata?.[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,7 +9,9 @@ type LobbyClient = Client<{
9
9
  };
10
10
  }>;
11
11
  export interface FilterInput {
12
- name?: string;
12
+ /** Room name to list. Required: a filter without it matches nothing. */
13
+ name: string;
14
+ /** Narrows the listing to rooms whose metadata has these exact values. */
13
15
  metadata?: any;
14
16
  }
15
17
  export interface LobbyOptions {
@@ -19,6 +21,7 @@ export type LobbyMessages = {
19
21
  filter: (client: LobbyClient, filter: FilterInput) => void;
20
22
  };
21
23
  export declare class LobbyRoom<Metadata = any> extends Room {
24
+ #private;
22
25
  rooms: IRoomCache<Metadata>[];
23
26
  unsubscribeLobby: () => void;
24
27
  clientOptions: {
@@ -1,5 +1,6 @@
1
1
  // packages/core/src/rooms/LobbyRoom.ts
2
2
  import * as matchMaker from "../MatchMaker.mjs";
3
+ import { debugAndPrintError } from "../Debug.mjs";
3
4
  import { subscribeLobby } from "../matchmaker/Lobby.mjs";
4
5
  import { Room } from "../Room.mjs";
5
6
  var LobbyRoom = class extends Room {
@@ -20,30 +21,29 @@ var LobbyRoom = class extends Room {
20
21
  });
21
22
  this.unsubscribeLobby = await subscribeLobby((roomId, data) => {
22
23
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
23
- const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);
24
24
  if (!data) {
25
25
  if (roomIndex !== -1) {
26
26
  const previousData = this.rooms[roomIndex];
27
27
  this.rooms.splice(roomIndex, 1);
28
- clients.forEach((client) => {
29
- if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {
28
+ this.#notifyClients((client, filter) => {
29
+ if (this.filterItemForClient(previousData, filter)) {
30
30
  client.send("-", roomId);
31
31
  }
32
32
  });
33
33
  }
34
34
  } else if (roomIndex === -1) {
35
35
  this.rooms.push(data);
36
- clients.forEach((client) => {
37
- if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {
36
+ this.#notifyClients((client, filter) => {
37
+ if (this.filterItemForClient(data, filter)) {
38
38
  client.send("+", [roomId, data]);
39
39
  }
40
40
  });
41
41
  } else {
42
42
  const previousData = this.rooms[roomIndex];
43
43
  this.rooms[roomIndex] = data;
44
- clients.forEach((client) => {
45
- const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);
46
- const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);
44
+ this.#notifyClients((client, filter) => {
45
+ const hadData = this.filterItemForClient(previousData, filter);
46
+ const hasData = this.filterItemForClient(data, filter);
47
47
  if (hadData && !hasData) {
48
48
  client.send("-", roomId);
49
49
  } else if (hasData) {
@@ -67,6 +67,19 @@ var LobbyRoom = class extends Room {
67
67
  this.unsubscribeLobby();
68
68
  }
69
69
  }
70
+ #notifyClients(notify) {
71
+ this.clients.forEach((client) => {
72
+ const options = this.clientOptions[client.sessionId];
73
+ if (!options) {
74
+ return;
75
+ }
76
+ try {
77
+ notify(client, options.filter);
78
+ } catch (e) {
79
+ debugAndPrintError(e);
80
+ }
81
+ });
82
+ }
70
83
  filterItemsForClient(options) {
71
84
  const filter = options.filter;
72
85
  return filter ? this.rooms.filter((room) => this.filterItemForClient(room, filter)) : this.rooms;
@@ -81,7 +94,7 @@ var LobbyRoom = class extends Room {
81
94
  }
82
95
  if (filter.metadata) {
83
96
  for (const field in filter.metadata) {
84
- if (room.metadata[field] !== filter.metadata[field]) {
97
+ if (room.metadata?.[field] !== filter.metadata[field]) {
85
98
  isAllowed = false;
86
99
  break;
87
100
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/LobbyRoom.ts"],
4
- "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport { debugAndPrintError } from '../Debug.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n /** Room name to list. Required: a filter without it matches nothing. */\n name: string;\n\n /** Narrows the listing to rooms whose metadata has these exact values. */\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n this.#notifyClients((client, filter) => {\n if (this.filterItemForClient(previousData, filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n this.#notifyClients((client, filter) => {\n if (this.filterItemForClient(data, filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n this.#notifyClients((client, filter) => {\n const hadData = this.filterItemForClient(previousData, filter);\n const hasData = this.filterItemForClient(data, filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n #notifyClients(notify: (client: LobbyClient, filter: FilterInput) => void) {\n this.clients.forEach((client) => {\n const options = this.clientOptions[client.sessionId];\n if (!options) { return; }\n\n try {\n notify(client, options.filter);\n } catch (e: any) {\n debugAndPrintError(e); // one client's failure must not skip the rest\n }\n });\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata?.[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.23",
3
+ "version": "0.18.25",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -61,9 +61,9 @@
61
61
  "@colyseus/schema": "^5.0.8",
62
62
  "express": "^5.0.0",
63
63
  "vitest": "^3.1.1",
64
- "@colyseus/tools": "^0.18.10",
65
64
  "@colyseus/redis-driver": "^0.18.7",
66
- "@colyseus/redis-presence": "^0.18.6"
65
+ "@colyseus/redis-presence": "^0.18.6",
66
+ "@colyseus/tools": "^0.18.11"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@colyseus/auth": "0.18.x",
package/src/Room.ts CHANGED
@@ -797,6 +797,9 @@ export class Room<T extends RoomOptions = RoomOptions> {
797
797
  * - `clock.setTimeout` / `clock.setInterval`
798
798
  * - `setTimestep` / `setFixedTimestep`
799
799
  *
800
+ * Without this method, an error thrown by an `onMessage` handler is logged and the
801
+ * room keeps running, while an error thrown by a timer shuts the server down.
802
+ *
800
803
  * (Experimental: this feature is subject to change in the future - we're currently getting feedback to improve it)
801
804
  */
802
805
  public onUncaughtException?(error: RoomException, methodName: RoomMethodName): void;
@@ -197,16 +197,36 @@ export class RoomMessages {
197
197
  }
198
198
 
199
199
  if (this.events.events[messageType]) {
200
- this.events.emit(messageType as string, client, message, SEND_CONTEXT);
200
+ this.#dispatch(this.events.events[messageType], client, message, SEND_CONTEXT);
201
201
 
202
202
  } else if (this.events.events['*']) {
203
- this.events.emit('*', client, messageType, message);
203
+ this.#dispatch(this.events.events['*'], client, messageType, message);
204
204
 
205
205
  } else {
206
206
  this.#noHandler(client, messageType, message);
207
207
  }
208
208
  }
209
209
 
210
+ /**
211
+ * Call the handlers of a fire-and-forget frame. With no reply to carry an error
212
+ * and nothing above this to catch one, a throw would reach the socket's event
213
+ * listener and shut the process down, along with every other room in it. So
214
+ * errors are logged, and the remaining handlers still run. Handlers wrapped for
215
+ * `onUncaughtException` report their own errors and never throw here.
216
+ */
217
+ #dispatch(handlers: Array<(...args: any[]) => any>, ...args: any[]): void {
218
+ for (let i = 0, length = handlers.length; i < length; i++) {
219
+ try {
220
+ const result = handlers[i](...args);
221
+ if (typeof result?.catch === 'function') {
222
+ result.catch(debugAndPrintError);
223
+ }
224
+ } catch (e: any) {
225
+ debugAndPrintError(e);
226
+ }
227
+ }
228
+ }
229
+
210
230
  /** Dispatch a `ROOM_REQUEST` frame: same handlers as ROOM_DATA, but the client
211
231
  * opted into a reply, so echo the `requestId` with the outcome. The handler's
212
232
  * reply is decided by what it returns / does to `ctx`: `ctx.reject(reason)` →
@@ -314,10 +334,10 @@ export class RoomMessages {
314
334
  }
315
335
 
316
336
  if (this.events.events[bytesMessageType]) {
317
- this.events.emit(bytesMessageType, client, message);
337
+ this.#dispatch(this.events.events[bytesMessageType], client, message);
318
338
 
319
339
  } else if (this.events.events['*']) {
320
- this.events.emit('*', client, messageType, message);
340
+ this.#dispatch(this.events.events['*'], client, messageType, message);
321
341
 
322
342
  } else {
323
343
  this.#noHandler(client, messageType, message);
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@ export {
20
20
  // Core classes
21
21
  export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
22
22
  export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
23
- export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
23
+ export { InputBufferImpl, compileSanitizer, compileSnapshot } from './input/InputBuffer.ts';
24
24
  export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
25
25
  export { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';
26
26
  export {
@@ -1,3 +1,5 @@
1
+ import { $values } from '@colyseus/schema';
2
+
1
3
  import type { ClientPrivate } from '../Transport.ts';
2
4
  import { $METADATA } from '../utils/Utils.ts';
3
5
 
@@ -43,7 +45,9 @@ export function compileSanitizer<I>(spec: SanitizeInput<I>): (input: I) => void
43
45
  // never assigned; the range floor turned "no input" into "held stick").
44
46
  if (v === undefined) { continue; }
45
47
  // NaN-safe: NaN fails both comparisons → clamp floor.
46
- inst[names[i]] = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
48
+ const clamped = v >= mins[i] ? (v <= maxs[i] ? v : maxs[i]) : mins[i];
49
+ // Honest frames clamp nothing: a keyed write through the setter costs more than the walk.
50
+ if (clamped !== v) { inst[names[i]] = clamped; }
47
51
  }
48
52
  };
49
53
  }
@@ -63,6 +67,59 @@ function fieldNamesOf(ctor: new () => any): string[] {
63
67
  return names;
64
68
  }
65
69
 
70
+ /** One per input schema, so every room's frames share a hidden class. */
71
+ const _snapshots = new WeakMap<new () => any, (input: any) => any>();
72
+
73
+ /**
74
+ * @internal Compile how a decoded input is copied into the buffer. `clone()`
75
+ * builds a Schema and its ChangeTree per input, for a frame that's never
76
+ * encoded, so a schema of plain values gets a lighter frame: its own array of
77
+ * values behind the ctor's prototype. Methods, getters, `instanceof`, `clone()`
78
+ * and `toJSON()` all hold, and a field write lands on the copy, untracked. A
79
+ * nested schema or collection keeps `clone()`, whose copy is deep.
80
+ *
81
+ * Fields are read by name, as `clone()` reads them, never by copying `$values`:
82
+ * native class fields (`useDefineForClassFields`) shadow the schema's accessors,
83
+ * so the decoder's values sit on the instance and `$values` stays empty.
84
+ */
85
+ export function compileSnapshot<I>(ctor: new () => I): (input: I) => I {
86
+ let snapshot = _snapshots.get(ctor);
87
+ if (snapshot !== undefined) { return snapshot; }
88
+
89
+ const md = (ctor as any)[$METADATA] as Record<number, { type?: unknown }> | undefined;
90
+ const names = fieldNamesOf(ctor);
91
+ const plain = md !== undefined && names.every((_, i) => {
92
+ const type = md[i].type;
93
+ return typeof type === "string" || (type !== null && typeof type === "object" && "quantized" in type);
94
+ });
95
+
96
+ if (plain) {
97
+ const fields: PropertyDescriptorMap = {};
98
+ names.forEach((name, i) => {
99
+ fields[name] = {
100
+ get(this: any) { return this[$values][i]; },
101
+ set(this: any, value: unknown) { this[$values][i] = value; },
102
+ enumerable: true,
103
+ configurable: true,
104
+ };
105
+ });
106
+ // A function, not a class: `constructor` must stay the schema's (clone/toJSON read its metadata).
107
+ function Frame(this: any, values: unknown[]) { this[$values] = values; }
108
+ Frame.prototype = Object.create(ctor.prototype, fields);
109
+ const count = names.length;
110
+ snapshot = (input) => {
111
+ const values = new Array(count);
112
+ for (let i = 0; i < count; i++) { values[i] = input[names[i]]; }
113
+ return new (Frame as any)(values);
114
+ };
115
+ } else {
116
+ snapshot = (input) => input.clone();
117
+ }
118
+
119
+ _snapshots.set(ctor, snapshot!);
120
+ return snapshot!;
121
+ }
122
+
66
123
  /**
67
124
  * @internal Seed every declared field that has no construction default with
68
125
  * its wire-neutral zero value (0 / false / ""; quantized: 0 clamped into the
@@ -4,7 +4,7 @@ import { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared
4
4
 
5
5
  import {
6
6
  InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,
7
- compileSanitizer, seedInputZeroValues, validateSubSteps,
7
+ compileSanitizer, compileSnapshot, seedInputZeroValues, validateSubSteps,
8
8
  } from './InputBuffer.ts';
9
9
  import type {
10
10
  InputAccessor, InputAPI, NormalizedInputOptions,
@@ -148,6 +148,7 @@ export class RoomInput {
148
148
  idle: opts?.idle,
149
149
  // Compiled once (map → dense min/max walk); applied per decoded frame.
150
150
  sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,
151
+ snapshot: compileSnapshot(type),
151
152
  tickRate,
152
153
  subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),
153
154
  };
@@ -220,15 +221,15 @@ export class RoomInput {
220
221
 
221
222
  /**
222
223
  * Sanitize the freshly-decoded `client._input`, then (when buffering is on)
223
- * push a clone into the per-client buffer. Honors the framework seq
224
+ * push a snapshot into the per-client buffer. Honors the framework seq
224
225
  * (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
225
226
  * redundancy ring.
226
227
  */
227
228
  capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {
228
- // Sanitize before anything reads it (latest, the clone below, the idle ctx).
229
+ // Sanitize before anything reads it (latest, the snapshot below, the idle ctx).
229
230
  this.options.sanitize?.(client._input);
230
231
  const buf = client._inputBuffer;
231
- if (!buf) { return; } // no consumer registered — skip the clone allocation
232
+ if (!buf) { return; } // no consumer registered — skip the snapshot allocation
232
233
  const inst = client._input!;
233
234
  if (seq !== undefined) {
234
235
  // Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).
@@ -241,7 +242,7 @@ export class RoomInput {
241
242
  if (typeof value === 'number' && !buf.accept(value)) { return; }
242
243
  }
243
244
  }
244
- buf.push(inst.clone() as any, renderTime, reckonTime, seq);
245
+ buf.push(this.options.snapshot(inst), renderTime, reckonTime, seq);
245
246
  }
246
247
 
247
248
  /** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
@@ -193,7 +193,7 @@ export interface NormalizedInputOptions {
193
193
  seqField?: string;
194
194
 
195
195
  /**
196
- * > 0 enables per-client buffering of cloned snapshots — required for
196
+ * > 0 enables per-client buffering of input snapshots — required for
197
197
  * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
198
198
  * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
199
199
  * works).
@@ -229,6 +229,12 @@ export interface NormalizedInputOptions {
229
229
  * `latest` / the buffer / the idle ctx.
230
230
  */
231
231
  sanitize?: (instance: any) => void;
232
+
233
+ /**
234
+ * COMPILED copy of a decoded frame for the buffer (see `compileSnapshot`).
235
+ * Reads and writes like the schema, without its change tracking.
236
+ */
237
+ snapshot: (instance: any) => any;
232
238
  }
233
239
 
234
240
  /**
@@ -1,4 +1,5 @@
1
1
  import * as matchMaker from '../MatchMaker.ts';
2
+ import { debugAndPrintError } from '../Debug.ts';
2
3
 
3
4
  import type { Room } from '../Room.ts';
4
5
  import type { IRoomCache } from './driver.ts';
@@ -38,27 +39,33 @@ export async function subscribeLobby(callback: (roomId: string, roomListing: IRo
38
39
  const cb = async (message: string) => {
39
40
  const [roomId, isRemove] = message.split(',');
40
41
 
41
- if (isRemove === '1') {
42
- // Mark as removed and process immediately
43
- removedRoomIds.add(roomId);
44
- callback(roomId, null);
42
+ try {
43
+ if (isRemove === '1') {
44
+ // Mark as removed and process immediately
45
+ removedRoomIds.add(roomId);
45
46
 
46
- // Clean up after a short timeout to prevent memory leaks
47
- setTimeout(() => removedRoomIds.delete(roomId), 2000);
47
+ // Clean up after a short timeout to prevent memory leaks
48
+ setTimeout(() => removedRoomIds.delete(roomId), 2000);
48
49
 
49
- } else {
50
- // Clear removed status - room might be coming back (e.g., visibility change)
51
- removedRoomIds.delete(roomId);
50
+ callback(roomId, null);
52
51
 
53
- const room = (await matchMaker.query({ roomId }))[0];
52
+ } else {
53
+ // Clear removed status - room might be coming back (e.g., visibility change)
54
+ removedRoomIds.delete(roomId);
54
55
 
55
- // Check if room was removed while we were querying
56
- // See "updating metadata should not cause race condition" test in LobbyRoom.test.ts
57
- if (removedRoomIds.has(roomId)) {
58
- return;
56
+ const room = (await matchMaker.query({ roomId }))[0];
57
+
58
+ // Check if room was removed while we were querying
59
+ // See "updating metadata should not cause race condition" test in LobbyRoom.test.ts
60
+ if (removedRoomIds.has(roomId)) {
61
+ return;
62
+ }
63
+
64
+ callback(roomId, room);
59
65
  }
60
66
 
61
- callback(roomId, room);
67
+ } catch (e: any) {
68
+ debugAndPrintError(e); // presence listener: nothing above this can catch it
62
69
  }
63
70
  };
64
71
 
@@ -1,4 +1,5 @@
1
1
  import * as matchMaker from '../MatchMaker.ts';
2
+ import { debugAndPrintError } from '../Debug.ts';
2
3
  import type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';
3
4
  import type { Client } from '../Transport.ts';
4
5
  import { subscribeLobby } from '../matchmaker/Lobby.ts';
@@ -23,7 +24,10 @@ type LobbyClient = Client<{
23
24
  }>;
24
25
 
25
26
  export interface FilterInput {
26
- name?: string;
27
+ /** Room name to list. Required: a filter without it matches nothing. */
28
+ name: string;
29
+
30
+ /** Narrows the listing to rooms whose metadata has these exact values. */
27
31
  metadata?: any;
28
32
  }
29
33
 
@@ -57,7 +61,6 @@ export class LobbyRoom<Metadata = any> extends Room {
57
61
 
58
62
  this.unsubscribeLobby = await subscribeLobby((roomId, data) => {
59
63
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
60
- const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);
61
64
 
62
65
  if (!data) {
63
66
  // remove room listing data
@@ -66,8 +69,8 @@ export class LobbyRoom<Metadata = any> extends Room {
66
69
 
67
70
  this.rooms.splice(roomIndex, 1);
68
71
 
69
- clients.forEach((client) => {
70
- if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {
72
+ this.#notifyClients((client, filter) => {
73
+ if (this.filterItemForClient(previousData, filter)) {
71
74
  client.send('-', roomId);
72
75
  }
73
76
  });
@@ -77,8 +80,8 @@ export class LobbyRoom<Metadata = any> extends Room {
77
80
  // append room listing data
78
81
  this.rooms.push(data);
79
82
 
80
- clients.forEach((client) => {
81
- if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {
83
+ this.#notifyClients((client, filter) => {
84
+ if (this.filterItemForClient(data, filter)) {
82
85
  client.send('+', [roomId, data]);
83
86
  }
84
87
  });
@@ -89,9 +92,9 @@ export class LobbyRoom<Metadata = any> extends Room {
89
92
  // replace room listing data
90
93
  this.rooms[roomIndex] = data;
91
94
 
92
- clients.forEach((client) => {
93
- const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);
94
- const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);
95
+ this.#notifyClients((client, filter) => {
96
+ const hadData = this.filterItemForClient(previousData, filter);
97
+ const hasData = this.filterItemForClient(data, filter);
95
98
 
96
99
  if (hadData && !hasData) {
97
100
  client.send('-', roomId);
@@ -124,6 +127,19 @@ export class LobbyRoom<Metadata = any> extends Room {
124
127
  }
125
128
  }
126
129
 
130
+ #notifyClients(notify: (client: LobbyClient, filter: FilterInput) => void) {
131
+ this.clients.forEach((client) => {
132
+ const options = this.clientOptions[client.sessionId];
133
+ if (!options) { return; }
134
+
135
+ try {
136
+ notify(client, options.filter);
137
+ } catch (e: any) {
138
+ debugAndPrintError(e); // one client's failure must not skip the rest
139
+ }
140
+ });
141
+ }
142
+
127
143
  protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {
128
144
  const filter = options.filter;
129
145
 
@@ -145,7 +161,7 @@ export class LobbyRoom<Metadata = any> extends Room {
145
161
 
146
162
  if (filter.metadata) {
147
163
  for (const field in filter.metadata) {
148
- if (room.metadata[field] !== filter.metadata[field]) {
164
+ if (room.metadata?.[field] !== filter.metadata[field]) {
149
165
  isAllowed = false;
150
166
  break;
151
167
  }