@colyseus/core 0.17.0 → 0.17.1
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/package.json +3 -4
- package/src/Debug.ts +37 -0
- package/src/IPC.ts +124 -0
- package/src/Logger.ts +30 -0
- package/src/MatchMaker.ts +1119 -0
- package/src/Protocol.ts +160 -0
- package/src/Room.ts +1797 -0
- package/src/Server.ts +325 -0
- package/src/Stats.ts +107 -0
- package/src/Transport.ts +207 -0
- package/src/errors/RoomExceptions.ts +141 -0
- package/src/errors/SeatReservationError.ts +5 -0
- package/src/errors/ServerError.ts +17 -0
- package/src/index.ts +81 -0
- package/src/matchmaker/Lobby.ts +68 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +92 -0
- package/src/matchmaker/LocalDriver/Query.ts +94 -0
- package/src/matchmaker/RegisteredHandler.ts +172 -0
- package/src/matchmaker/controller.ts +64 -0
- package/src/matchmaker/driver.ts +191 -0
- package/src/presence/LocalPresence.ts +331 -0
- package/src/presence/Presence.ts +263 -0
- package/src/rooms/LobbyRoom.ts +135 -0
- package/src/rooms/RankedQueueRoom.ts +425 -0
- package/src/rooms/RelayRoom.ts +90 -0
- package/src/router/default_routes.ts +58 -0
- package/src/router/index.ts +43 -0
- package/src/serializer/NoneSerializer.ts +16 -0
- package/src/serializer/SchemaSerializer.ts +194 -0
- package/src/serializer/SchemaSerializerDebug.ts +148 -0
- package/src/serializer/Serializer.ts +9 -0
- package/src/utils/DevMode.ts +133 -0
- package/src/utils/StandardSchema.ts +20 -0
- package/src/utils/Utils.ts +169 -0
- package/src/utils/nanoevents.ts +20 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ExtractMessageType, Room } from '../Room.ts';
|
|
2
|
+
|
|
3
|
+
export type RoomMethodName = 'onCreate'
|
|
4
|
+
| 'onAuth'
|
|
5
|
+
| 'onJoin'
|
|
6
|
+
| 'onLeave'
|
|
7
|
+
| 'onDispose'
|
|
8
|
+
| 'onMessage'
|
|
9
|
+
| 'setSimulationInterval'
|
|
10
|
+
| 'setInterval'
|
|
11
|
+
| 'setTimeout';
|
|
12
|
+
|
|
13
|
+
export type RoomException<R extends Room = Room> =
|
|
14
|
+
OnCreateException<R> |
|
|
15
|
+
OnAuthException<R> |
|
|
16
|
+
OnJoinException<R> |
|
|
17
|
+
OnLeaveException<R> |
|
|
18
|
+
OnDisposeException |
|
|
19
|
+
OnMessageException<R> |
|
|
20
|
+
SimulationIntervalException |
|
|
21
|
+
TimedEventException;
|
|
22
|
+
|
|
23
|
+
export class OnCreateException<R extends Room = Room> extends Error {
|
|
24
|
+
options: Parameters<R['onCreate']>[0];
|
|
25
|
+
constructor(
|
|
26
|
+
cause: Error,
|
|
27
|
+
message: string,
|
|
28
|
+
options: Parameters<R['onCreate']>[0],
|
|
29
|
+
) {
|
|
30
|
+
super(message, { cause });
|
|
31
|
+
this.name = 'OnCreateException';
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class OnAuthException<R extends Room = Room> extends Error {
|
|
37
|
+
client: Parameters<R['onAuth']>[0];
|
|
38
|
+
options: Parameters<R['onAuth']>[1];
|
|
39
|
+
constructor(
|
|
40
|
+
cause: Error,
|
|
41
|
+
message: string,
|
|
42
|
+
client: Parameters<R['onAuth']>[0],
|
|
43
|
+
options: Parameters<R['onAuth']>[1],
|
|
44
|
+
) {
|
|
45
|
+
super(message, { cause });
|
|
46
|
+
this.name = 'OnAuthException';
|
|
47
|
+
this.client = client;
|
|
48
|
+
this.options = options;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class OnJoinException<R extends Room = Room> extends Error {
|
|
53
|
+
client: Parameters<R['onJoin']>[0];
|
|
54
|
+
options: Parameters<R['onJoin']>[1];
|
|
55
|
+
auth: Parameters<R['onJoin']>[2];
|
|
56
|
+
constructor(
|
|
57
|
+
cause: Error,
|
|
58
|
+
message: string,
|
|
59
|
+
client: Parameters<R['onJoin']>[0],
|
|
60
|
+
options: Parameters<R['onJoin']>[1],
|
|
61
|
+
auth: Parameters<R['onJoin']>[2],
|
|
62
|
+
) {
|
|
63
|
+
super(message, { cause });
|
|
64
|
+
this.name = 'OnJoinException';
|
|
65
|
+
this.client = client;
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.auth = auth;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class OnLeaveException<R extends Room = Room> extends Error {
|
|
72
|
+
client: Parameters<R['onLeave']>[0];
|
|
73
|
+
consented: Parameters<R['onLeave']>[1];
|
|
74
|
+
constructor(
|
|
75
|
+
cause: Error,
|
|
76
|
+
message: string,
|
|
77
|
+
client: Parameters<R['onLeave']>[0],
|
|
78
|
+
consented: Parameters<R['onLeave']>[1],
|
|
79
|
+
) {
|
|
80
|
+
super(message, { cause });
|
|
81
|
+
this.name = 'OnLeaveException';
|
|
82
|
+
this.client = client;
|
|
83
|
+
this.consented = consented;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class OnDisposeException extends Error {
|
|
88
|
+
constructor(
|
|
89
|
+
cause: Error,
|
|
90
|
+
message: string,
|
|
91
|
+
) {
|
|
92
|
+
super(message, { cause });
|
|
93
|
+
this.name = 'OnDisposeException';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class OnMessageException<R extends Room, MessageType extends keyof R['messages'] = keyof R['messages']> extends Error {
|
|
98
|
+
client: R['~client'];
|
|
99
|
+
payload: ExtractMessageType<R['messages'][MessageType]>;
|
|
100
|
+
type: MessageType;
|
|
101
|
+
constructor(
|
|
102
|
+
cause: Error,
|
|
103
|
+
message: string,
|
|
104
|
+
client: R['~client'],
|
|
105
|
+
payload: ExtractMessageType<R['messages'][MessageType]>,
|
|
106
|
+
type: MessageType,
|
|
107
|
+
) {
|
|
108
|
+
super(message, { cause });
|
|
109
|
+
this.name = 'OnMessageException';
|
|
110
|
+
this.client = client;
|
|
111
|
+
this.payload = payload;
|
|
112
|
+
this.type = type;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public isType<T extends keyof R['messages']>(type: T): this is OnMessageException<R, T> {
|
|
116
|
+
return (this.type as string) === type;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class SimulationIntervalException extends Error {
|
|
121
|
+
constructor(
|
|
122
|
+
cause: Error,
|
|
123
|
+
message: string,
|
|
124
|
+
) {
|
|
125
|
+
super(message, { cause });
|
|
126
|
+
this.name = 'SimulationIntervalException';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class TimedEventException extends Error {
|
|
131
|
+
public args: any[];
|
|
132
|
+
constructor(
|
|
133
|
+
cause: Error,
|
|
134
|
+
message: string,
|
|
135
|
+
...args: any[]
|
|
136
|
+
) {
|
|
137
|
+
super(message, { cause });
|
|
138
|
+
this.name = 'TimedEventException';
|
|
139
|
+
this.args = args;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ErrorCode } from '../Protocol.ts';
|
|
2
|
+
|
|
3
|
+
export class ServerError extends Error {
|
|
4
|
+
public code: number;
|
|
5
|
+
|
|
6
|
+
constructor(code: number = ErrorCode.MATCHMAKE_UNHANDLED, message?: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
|
|
9
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
10
|
+
if (Error.captureStackTrace) {
|
|
11
|
+
Error.captureStackTrace(this, ServerError);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.name = 'ServerError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ClockTimer as Clock, Delayed } from '@colyseus/timer';
|
|
2
|
+
|
|
3
|
+
// Core classes
|
|
4
|
+
export { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';
|
|
5
|
+
export { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type ExtractMessageType, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
|
|
6
|
+
export { Protocol, ErrorCode, getMessageBytes, CloseCode } from './Protocol.ts';
|
|
7
|
+
export { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
|
|
8
|
+
export { ServerError } from './errors/ServerError.ts';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
type RoomException,
|
|
12
|
+
type RoomMethodName,
|
|
13
|
+
OnCreateException,
|
|
14
|
+
OnAuthException,
|
|
15
|
+
OnJoinException,
|
|
16
|
+
OnLeaveException,
|
|
17
|
+
OnDisposeException,
|
|
18
|
+
OnMessageException,
|
|
19
|
+
SimulationIntervalException,
|
|
20
|
+
TimedEventException,
|
|
21
|
+
} from './errors/RoomExceptions.ts';
|
|
22
|
+
|
|
23
|
+
// MatchMaker
|
|
24
|
+
import * as matchMaker from './MatchMaker.ts';
|
|
25
|
+
export { matchMaker };
|
|
26
|
+
export { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';
|
|
27
|
+
|
|
28
|
+
// Driver
|
|
29
|
+
export * from './matchmaker/LocalDriver/LocalDriver.ts';
|
|
30
|
+
export { initializeRoomCache } from './matchmaker/driver.ts';
|
|
31
|
+
|
|
32
|
+
// Transport
|
|
33
|
+
export { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, connectClientToRoom } from './Transport.ts';
|
|
34
|
+
|
|
35
|
+
// Presence
|
|
36
|
+
export { type Presence } from './presence/Presence.ts';
|
|
37
|
+
export { LocalPresence } from './presence/LocalPresence.ts';
|
|
38
|
+
|
|
39
|
+
// Serializers
|
|
40
|
+
export { type Serializer } from './serializer/Serializer.ts';
|
|
41
|
+
export { SchemaSerializer } from './serializer/SchemaSerializer.ts';
|
|
42
|
+
// export { SchemaSerializerDebug } from './serializer/SchemaSerializerDebug.ts';
|
|
43
|
+
|
|
44
|
+
// Utilities
|
|
45
|
+
export { Clock, Delayed };
|
|
46
|
+
export { generateId, Deferred, HttpServerMock, spliceOne, getBearerToken } from './utils/Utils.ts';
|
|
47
|
+
export { isDevMode } from './utils/DevMode.ts';
|
|
48
|
+
|
|
49
|
+
// IPC
|
|
50
|
+
export { subscribeIPC, requestFromIPC } from './IPC.ts';
|
|
51
|
+
|
|
52
|
+
// Debug
|
|
53
|
+
export {
|
|
54
|
+
debugMatchMaking,
|
|
55
|
+
debugMessage,
|
|
56
|
+
debugPatch,
|
|
57
|
+
debugError,
|
|
58
|
+
debugConnection,
|
|
59
|
+
debugDriver,
|
|
60
|
+
debugPresence,
|
|
61
|
+
debugAndPrintError,
|
|
62
|
+
} from './Debug.ts';
|
|
63
|
+
|
|
64
|
+
// Default rooms
|
|
65
|
+
export { LobbyRoom } from './rooms/LobbyRoom.ts';
|
|
66
|
+
export { RelayRoom } from './rooms/RelayRoom.ts';
|
|
67
|
+
export { RankedQueueRoom, type RankedQueueOptions, type MatchGroup, type MatchTeam, type ClientQueueData } from './rooms/RankedQueueRoom.ts';
|
|
68
|
+
|
|
69
|
+
// Router / Endpoints
|
|
70
|
+
export {
|
|
71
|
+
createEndpoint,
|
|
72
|
+
createInternalContext,
|
|
73
|
+
createMiddleware,
|
|
74
|
+
createRouter,
|
|
75
|
+
toNodeHandler,
|
|
76
|
+
__globalEndpoints,
|
|
77
|
+
type Router,
|
|
78
|
+
} from './router/index.ts';
|
|
79
|
+
|
|
80
|
+
// Abstract logging support
|
|
81
|
+
export { logger } from './Logger.ts';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as matchMaker from '../MatchMaker.ts';
|
|
2
|
+
|
|
3
|
+
import type { Room } from '../Room.ts';
|
|
4
|
+
import type { IRoomCache } from './driver.ts';
|
|
5
|
+
|
|
6
|
+
const LOBBY_CHANNEL = '$lobby';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* TODO: refactor this on 1.0
|
|
10
|
+
*
|
|
11
|
+
* Some users might be relying on "1" = "removed" from the lobby due to this workaround: https://github.com/colyseus/colyseus/issues/617
|
|
12
|
+
* Though, for consistency, we should invert as "0" = "invisible" and "1" = "visible".
|
|
13
|
+
*
|
|
14
|
+
* - rename "removed" to "isVisible" and swap the logic
|
|
15
|
+
* - emit "visibility-change" with inverted value (isVisible)
|
|
16
|
+
* - update "subscribeLobby" to check "1" as "isVisible" instead of "removed"
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export function updateLobby<T extends Room>(room: T, removed: boolean = false) {
|
|
20
|
+
const listing = room['_listing'];
|
|
21
|
+
|
|
22
|
+
if (listing.unlisted || !listing.roomId) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (removed) {
|
|
27
|
+
matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},1`);
|
|
28
|
+
} else if (!listing.private) {
|
|
29
|
+
matchMaker.presence.publish(LOBBY_CHANNEL, `${listing.roomId},0`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function subscribeLobby(callback: (roomId: string, roomListing: IRoomCache) => void) {
|
|
34
|
+
// Track removed roomIds to prevent race conditions where pending queries
|
|
35
|
+
// complete after a room has been removed
|
|
36
|
+
const removedRoomIds = new Set<string>();
|
|
37
|
+
|
|
38
|
+
const cb = async (message: string) => {
|
|
39
|
+
const [roomId, isRemove] = message.split(',');
|
|
40
|
+
|
|
41
|
+
if (isRemove === '1') {
|
|
42
|
+
// Mark as removed and process immediately
|
|
43
|
+
removedRoomIds.add(roomId);
|
|
44
|
+
callback(roomId, null);
|
|
45
|
+
|
|
46
|
+
// Clean up after a short timeout to prevent memory leaks
|
|
47
|
+
setTimeout(() => removedRoomIds.delete(roomId), 2000);
|
|
48
|
+
|
|
49
|
+
} else {
|
|
50
|
+
// Clear removed status - room might be coming back (e.g., visibility change)
|
|
51
|
+
removedRoomIds.delete(roomId);
|
|
52
|
+
|
|
53
|
+
const room = (await matchMaker.query({ roomId }))[0];
|
|
54
|
+
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
callback(roomId, room);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await matchMaker.presence.subscribe(LOBBY_CHANNEL, cb);
|
|
66
|
+
|
|
67
|
+
return () => matchMaker.presence.unsubscribe(LOBBY_CHANNEL, cb);
|
|
68
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { debugMatchMaking } from '../../Debug.ts';
|
|
2
|
+
import type { IRoomCache, SortOptions, MatchMakerDriver } from '../driver.ts';
|
|
3
|
+
import { Query } from './Query.ts';
|
|
4
|
+
|
|
5
|
+
// re-export
|
|
6
|
+
export type { IRoomCache, SortOptions, MatchMakerDriver };
|
|
7
|
+
|
|
8
|
+
export class LocalDriver implements MatchMakerDriver {
|
|
9
|
+
public rooms: IRoomCache[] = [];
|
|
10
|
+
|
|
11
|
+
public has(roomId: string) {
|
|
12
|
+
return this.rooms.some((room) => room.roomId === roomId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
|
|
16
|
+
const query = new Query<IRoomCache>(this.rooms, conditions);
|
|
17
|
+
|
|
18
|
+
if (sortOptions) {
|
|
19
|
+
query.sort(sortOptions);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return query.filter(conditions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public cleanup(processId: string) {
|
|
26
|
+
const cachedRooms = this.query({ processId });
|
|
27
|
+
debugMatchMaking("removing stale rooms by processId %s (%s rooms found)", processId, cachedRooms.length);
|
|
28
|
+
|
|
29
|
+
cachedRooms.forEach((room) => this.remove(room.roomId));
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
|
|
34
|
+
const query = new Query<IRoomCache>(this.rooms, conditions);
|
|
35
|
+
|
|
36
|
+
if (sortOptions) {
|
|
37
|
+
query.sort(sortOptions);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return query as unknown as Promise<IRoomCache>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {
|
|
44
|
+
if (operations.$set) {
|
|
45
|
+
for (const field in operations.$set) {
|
|
46
|
+
if (operations.$set.hasOwnProperty(field)) {
|
|
47
|
+
room[field] = operations.$set[field];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (operations.$inc) {
|
|
53
|
+
for (const field in operations.$inc) {
|
|
54
|
+
if (operations.$inc.hasOwnProperty(field)) {
|
|
55
|
+
room[field] += operations.$inc[field];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public persist(room: IRoomCache, create: boolean = false) {
|
|
64
|
+
// if (this.rooms.indexOf(room) !== -1) {
|
|
65
|
+
// // already in the list
|
|
66
|
+
// return true;
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
if (!create) { return false; }
|
|
70
|
+
|
|
71
|
+
// add to the list
|
|
72
|
+
this.rooms.push(room);
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public remove(roomId: string) {
|
|
78
|
+
const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
|
|
79
|
+
if (roomIndex !== -1) {
|
|
80
|
+
this.rooms.splice(roomIndex, 1);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public clear() {
|
|
87
|
+
this.rooms = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public shutdown() {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { IRoomCache, SortOptions } from '../driver.ts';
|
|
2
|
+
|
|
3
|
+
export class Query<T extends IRoomCache> {
|
|
4
|
+
private $rooms: T[];
|
|
5
|
+
private conditions: any;
|
|
6
|
+
private sortOptions?: SortOptions;
|
|
7
|
+
|
|
8
|
+
constructor(rooms: any[], conditions) {
|
|
9
|
+
this.$rooms = rooms.slice(0);
|
|
10
|
+
this.conditions = conditions;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public sort(options: SortOptions) {
|
|
14
|
+
// Store sort options instead of sorting immediately
|
|
15
|
+
// This allows filtering first, then sorting fewer items
|
|
16
|
+
this.sortOptions = options;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private applySort(rooms: T[]): T[] {
|
|
21
|
+
if (!this.sortOptions) {
|
|
22
|
+
return rooms;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return rooms.sort((room1: T, room2: T) => {
|
|
26
|
+
for (const field in this.sortOptions) {
|
|
27
|
+
const direction = this.sortOptions[field];
|
|
28
|
+
if (room1.hasOwnProperty(field)) {
|
|
29
|
+
/**
|
|
30
|
+
* IRoomCache field
|
|
31
|
+
*/
|
|
32
|
+
if (direction === 1 || direction === 'asc' || direction === 'ascending') {
|
|
33
|
+
if (room1[field] > room2[field]) { return 1; }
|
|
34
|
+
if (room1[field] < room2[field]) { return -1; }
|
|
35
|
+
|
|
36
|
+
} else {
|
|
37
|
+
if (room1[field] > room2[field]) { return -1; }
|
|
38
|
+
if (room1[field] < room2[field]) { return 1; }
|
|
39
|
+
}
|
|
40
|
+
} else if (room1.metadata?.hasOwnProperty(field)) {
|
|
41
|
+
/**
|
|
42
|
+
* metadata field
|
|
43
|
+
*/
|
|
44
|
+
if (direction === 1 || direction === 'asc' || direction === 'ascending') {
|
|
45
|
+
if (room1.metadata[field] > room2.metadata[field]) { return 1; }
|
|
46
|
+
if (room1.metadata[field] < room2.metadata[field]) { return -1; }
|
|
47
|
+
} else {
|
|
48
|
+
if (room1.metadata[field] > room2.metadata[field]) { return -1; }
|
|
49
|
+
if (room1.metadata[field] < room2.metadata[field]) { return 1; }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private applyFilter(rooms: T[], conditions: any): T[] {
|
|
58
|
+
return rooms.filter(((room) => {
|
|
59
|
+
for (const field in conditions) {
|
|
60
|
+
if (conditions.hasOwnProperty(field)) {
|
|
61
|
+
// Check if field exists in room (IRoomCache base fields)
|
|
62
|
+
if (room.hasOwnProperty(field)) {
|
|
63
|
+
if (room[field] !== conditions[field]) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
} else if (room.metadata?.hasOwnProperty(field)) {
|
|
67
|
+
// Check if field exists in metadata
|
|
68
|
+
if (room.metadata[field] !== conditions[field]) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Field doesn't exist in room or metadata
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public filter(conditions: any) {
|
|
82
|
+
// Filter first to reduce the number of items to sort
|
|
83
|
+
const filtered = this.applyFilter(this.$rooms, conditions);
|
|
84
|
+
return this.applySort(filtered);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public then(resolve, reject) {
|
|
88
|
+
// Filter first to reduce the number of items to sort
|
|
89
|
+
const filtered = this.applyFilter(this.$rooms, this.conditions);
|
|
90
|
+
const sorted = this.applySort(filtered);
|
|
91
|
+
const result: any = sorted[0];
|
|
92
|
+
return resolve(result);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { logger } from '../Logger.ts';
|
|
3
|
+
import { Room } from './../Room.ts';
|
|
4
|
+
import { updateLobby } from './Lobby.ts';
|
|
5
|
+
|
|
6
|
+
import type { IRoomCache, SortOptions, IRoomCacheFilterByKeys, IRoomCacheSortByKeys, ExtractMetadata } from './driver.ts';
|
|
7
|
+
import type { Client } from '../Transport.ts';
|
|
8
|
+
import type { Type } from "../utils/Utils.ts";
|
|
9
|
+
|
|
10
|
+
export const INVALID_OPTION_KEYS: Array<keyof IRoomCache> = [
|
|
11
|
+
'clients',
|
|
12
|
+
'locked',
|
|
13
|
+
'private',
|
|
14
|
+
// 'maxClients', - maxClients can be useful as filter options
|
|
15
|
+
'metadata',
|
|
16
|
+
'name',
|
|
17
|
+
'processId',
|
|
18
|
+
'roomId',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type for filterBy that supports both onCreate options and metadata fields
|
|
23
|
+
*/
|
|
24
|
+
type FilterByKeys<RoomType extends Room> =
|
|
25
|
+
| IRoomCacheFilterByKeys
|
|
26
|
+
| (ExtractMetadata<RoomType> extends object
|
|
27
|
+
? keyof ExtractMetadata<RoomType> & string
|
|
28
|
+
: never)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type for sortBy that supports room cache fields and metadata fields
|
|
32
|
+
*/
|
|
33
|
+
type SortByKeys<RoomType extends Room> =
|
|
34
|
+
| IRoomCacheSortByKeys
|
|
35
|
+
| (ExtractMetadata<RoomType> extends object
|
|
36
|
+
? keyof ExtractMetadata<RoomType> & string
|
|
37
|
+
: never);
|
|
38
|
+
|
|
39
|
+
export interface RegisteredHandlerEvents<RoomType extends Type<Room> = any> {
|
|
40
|
+
create: [room: InstanceType<RoomType>];
|
|
41
|
+
lock: [room: InstanceType<RoomType>];
|
|
42
|
+
unlock: [room: InstanceType<RoomType>];
|
|
43
|
+
join: [room: InstanceType<RoomType>, client: Client];
|
|
44
|
+
leave: [room: InstanceType<RoomType>, client: Client, willDispose: boolean];
|
|
45
|
+
dispose: [room: InstanceType<RoomType>];
|
|
46
|
+
'visibility-change': [room: InstanceType<RoomType>, isVisible: boolean];
|
|
47
|
+
'metadata-change': [room: InstanceType<RoomType>];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class RegisteredHandler<
|
|
51
|
+
RoomType extends Type<Room> = any
|
|
52
|
+
> extends EventEmitter<RegisteredHandlerEvents<RoomType>> {
|
|
53
|
+
'~room': RoomType;
|
|
54
|
+
|
|
55
|
+
public klass: RoomType;
|
|
56
|
+
public options: any;
|
|
57
|
+
|
|
58
|
+
public name: string;
|
|
59
|
+
public filterOptions: Array<FilterByKeys<InstanceType<RoomType>>> = [];
|
|
60
|
+
public sortOptions?: SortOptions;
|
|
61
|
+
|
|
62
|
+
public realtimeListingEnabled: boolean = false;
|
|
63
|
+
|
|
64
|
+
constructor(klass: RoomType, options?: any) {
|
|
65
|
+
super();
|
|
66
|
+
|
|
67
|
+
this.klass = klass;
|
|
68
|
+
this.options = options;
|
|
69
|
+
|
|
70
|
+
if (typeof(klass) !== 'function') {
|
|
71
|
+
logger.debug('You are likely not importing your room class correctly.');
|
|
72
|
+
throw new Error(`class is expected but ${typeof(klass)} was provided.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public enableRealtimeListing() {
|
|
77
|
+
this.realtimeListingEnabled = true;
|
|
78
|
+
this.on('create', (room) => updateLobby(room));
|
|
79
|
+
this.on('lock', (room) => updateLobby(room));
|
|
80
|
+
this.on('unlock', (room) => updateLobby(room));
|
|
81
|
+
this.on('join', (room) => updateLobby(room));
|
|
82
|
+
this.on('leave', (room, _, willDispose) => {
|
|
83
|
+
if (!willDispose) {
|
|
84
|
+
updateLobby(room);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.on('visibility-change', (room, isVisible) => updateLobby(room, isVisible));
|
|
88
|
+
this.on('metadata-change', (room) => updateLobby(room));
|
|
89
|
+
this.on('dispose', (room) => updateLobby(room, true));
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Define which fields should be used for filtering rooms.
|
|
95
|
+
* Supports both onCreate options and metadata fields using dot notation.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Filter by IRoomCache fields
|
|
99
|
+
* .filterBy(['maxClients'])
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* // Filter by metadata fields
|
|
103
|
+
* .filterBy(['difficulty', 'metadata.region'])
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // Mix both
|
|
107
|
+
* .filterBy(['mode', 'difficulty', 'maxClients'])
|
|
108
|
+
*/
|
|
109
|
+
public filterBy<T extends FilterByKeys<InstanceType<RoomType>>>(
|
|
110
|
+
options: T[]
|
|
111
|
+
) {
|
|
112
|
+
this.filterOptions = options;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Define how rooms should be sorted when querying.
|
|
118
|
+
* Supports both room cache fields and metadata fields using dot notation.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Sort by number of clients (descending)
|
|
122
|
+
* .sortBy({ clients: -1 })
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Sort by metadata field
|
|
126
|
+
* .sortBy({ 'metadata.rating': -1 })
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // Multiple sort criteria
|
|
130
|
+
* .sortBy({ 'metadata.skillLevel': 1, clients: -1 })
|
|
131
|
+
*/
|
|
132
|
+
public sortBy<T extends SortByKeys<InstanceType<RoomType>>>(
|
|
133
|
+
options: { [K in T]: SortOptions[string] }
|
|
134
|
+
): this {
|
|
135
|
+
this.sortOptions = options as unknown as SortOptions;
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public getMetadataFromOptions(options: any) {
|
|
140
|
+
const metadata = this.getFilterOptions(options);
|
|
141
|
+
|
|
142
|
+
if (this.sortOptions) {
|
|
143
|
+
for (const field in this.sortOptions) {
|
|
144
|
+
if (field in options && !(field in metadata)) {
|
|
145
|
+
metadata[field] = options[field];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Object.keys(metadata).length > 0 ? { metadata } : {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract filter options from client options.
|
|
155
|
+
*/
|
|
156
|
+
public getFilterOptions(options: any) {
|
|
157
|
+
return this.filterOptions.reduce((prev, curr, i, arr) => {
|
|
158
|
+
const field = String(arr[i]);
|
|
159
|
+
|
|
160
|
+
// Handle regular (non-metadata) fields
|
|
161
|
+
if (options.hasOwnProperty(field)) {
|
|
162
|
+
if (INVALID_OPTION_KEYS.indexOf(field as any) !== -1) {
|
|
163
|
+
logger.warn(`option "${field}" has internal usage and is going to be ignored.`);
|
|
164
|
+
} else {
|
|
165
|
+
prev[field] = options[field];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return prev;
|
|
170
|
+
}, {});
|
|
171
|
+
}
|
|
172
|
+
}
|