@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.
@@ -0,0 +1,194 @@
1
+ import type { Serializer } from './Serializer.ts';
2
+ import { type Client, ClientState } from '../Transport.ts';
3
+
4
+ import { type Iterator, Encoder, dumpChanges, Reflection, Schema, StateView } from '@colyseus/schema';
5
+ import { debugPatch } from '../Debug.ts';
6
+ import { Protocol } from '../Protocol.ts';
7
+
8
+ const SHARED_VIEW = {};
9
+
10
+ export class SchemaSerializer<T extends Schema> implements Serializer<T> {
11
+ public id = 'schema';
12
+
13
+ protected encoder: Encoder<T>;
14
+ protected hasFilters: boolean = false;
15
+
16
+ protected handshakeCache: Uint8Array;
17
+
18
+ // flag to avoid re-encoding full state if no changes were made
19
+ protected needFullEncode: boolean = true;
20
+
21
+ // TODO: make this optional. allocating a new buffer for each room may not be always necessary.
22
+ protected fullEncodeBuffer: Uint8Array = new Uint8Array(Encoder.BUFFER_SIZE);
23
+ protected fullEncodeCache: Uint8Array;
24
+ protected sharedOffsetCache: Iterator = { offset: 0 };
25
+
26
+ protected encodedViews: Map<StateView | typeof SHARED_VIEW, Uint8Array>;
27
+
28
+ public reset(newState: T & Schema) {
29
+ this.encoder = new Encoder(newState);
30
+ this.hasFilters = this.encoder.context.hasFilters;
31
+
32
+ // cache ROOM_STATE byte as part of the encoded buffer
33
+ this.fullEncodeBuffer[0] = Protocol.ROOM_STATE;
34
+
35
+ if (this.hasFilters) {
36
+ this.encodedViews = new Map();
37
+ }
38
+ }
39
+
40
+ public getFullState(client?: Client) {
41
+ if (this.needFullEncode || this.encoder.root.changes.next !== undefined) {
42
+ this.sharedOffsetCache = { offset: 1 };
43
+ this.fullEncodeCache = this.encoder.encodeAll(this.sharedOffsetCache, this.fullEncodeBuffer);
44
+ this.needFullEncode = false;
45
+ }
46
+
47
+ if (this.hasFilters && client?.view) {
48
+ return this.encoder.encodeAllView(
49
+ client.view,
50
+ this.sharedOffsetCache.offset,
51
+ { ...this.sharedOffsetCache },
52
+ this.fullEncodeBuffer
53
+ );
54
+
55
+ } else {
56
+ return this.fullEncodeCache;
57
+ }
58
+ }
59
+
60
+ public applyPatches(clients: Client[]) {
61
+ let numClients = clients.length;
62
+
63
+ if (numClients === 0) {
64
+ // skip patching and clear changes
65
+ this.encoder.discardChanges();
66
+ return false;
67
+ }
68
+
69
+ if (!this.encoder.hasChanges) {
70
+
71
+ // check if views have changes (manual add() or remove() items)
72
+ if (this.hasFilters) {
73
+ //
74
+ // FIXME: refactor this to avoid duplicating code.
75
+ //
76
+ // it's probably better to have 2 different 'applyPatches' methods.
77
+ // (one for handling state with filters, and another for handling state without filters)
78
+ //
79
+ const clientsWithViewChange = clients.filter((client) => {
80
+ return client.state === ClientState.JOINED && client.view?.changes.size > 0
81
+ });
82
+
83
+ if (clientsWithViewChange.length > 0) {
84
+ const it: Iterator = { offset: 1 };
85
+
86
+ const sharedOffset = it.offset;
87
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
88
+
89
+ clientsWithViewChange.forEach((client) => {
90
+ client.raw(this.encoder.encodeView(client.view, sharedOffset, it));
91
+ });
92
+ }
93
+ }
94
+
95
+ // skip patching state if:
96
+ // - no clients are connected
97
+ // - no changes were made
98
+ // - no "filtered changes" were made when using filters
99
+ return false;
100
+ }
101
+
102
+ this.needFullEncode = true;
103
+
104
+ // dump changes for patch debugging
105
+ if (debugPatch.enabled) {
106
+ (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);
107
+ }
108
+
109
+ // get patch bytes
110
+ const it: Iterator = { offset: 1 };
111
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
112
+
113
+ // encode changes once, for all clients
114
+ const encodedChanges = this.encoder.encode(it);
115
+
116
+ if (!this.hasFilters) {
117
+ while (numClients--) {
118
+ const client = clients[numClients];
119
+
120
+ //
121
+ // FIXME: avoid this check for each client
122
+ //
123
+ if (client.state !== ClientState.JOINED) {
124
+ continue;
125
+ }
126
+
127
+ client.raw(encodedChanges);
128
+ }
129
+
130
+ } else {
131
+ // cache shared offset
132
+ const sharedOffset = it.offset;
133
+
134
+ // encode state multiple times, for each client
135
+ while (numClients--) {
136
+ const client = clients[numClients];
137
+
138
+ //
139
+ // FIXME: avoid this check for each client
140
+ //
141
+ if (client.state !== ClientState.JOINED) {
142
+ continue;
143
+ }
144
+
145
+ const view = client.view || SHARED_VIEW;
146
+
147
+ let encodedView = this.encodedViews.get(view);
148
+
149
+ // allow to pass the same encoded view for multiple clients
150
+ if (encodedView === undefined) {
151
+ encodedView = (view === SHARED_VIEW)
152
+ ? encodedChanges
153
+ : this.encoder.encodeView(client.view, sharedOffset, it);
154
+ this.encodedViews.set(view, encodedView);
155
+ }
156
+
157
+ client.raw(encodedView);
158
+ }
159
+
160
+ // clear views
161
+ this.encodedViews.clear();
162
+ }
163
+
164
+ // discard changes after sending
165
+ this.encoder.discardChanges();
166
+
167
+ // debug patches
168
+ if (debugPatch.enabled) {
169
+ debugPatch(
170
+ '%d bytes sent to %d clients, %j',
171
+ encodedChanges.length,
172
+ clients.length,
173
+ (debugPatch as any).dumpChanges,
174
+ );
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ public handshake() {
181
+ /**
182
+ * Cache handshake to avoid encoding it for each client joining
183
+ */
184
+ if (!this.handshakeCache) {
185
+ //
186
+ // TODO: re-use handshake buffer for all rooms of same type (?)
187
+ //
188
+ this.handshakeCache = (this.encoder.state && Reflection.encode(this.encoder));
189
+ }
190
+
191
+ return this.handshakeCache;
192
+ }
193
+
194
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * This serializer is a copy of SchemaSerializer,
3
+ * but it writes debug information to a file.
4
+ *
5
+ * This script must be used
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import { Schema, dumpChanges, Iterator } from '@colyseus/schema';
10
+ import { SchemaSerializer } from './SchemaSerializer.ts';
11
+ import { Client, ClientState } from "../Transport.ts";
12
+ import { Protocol} from '../Protocol.ts';
13
+ import { debugPatch } from '../Debug.ts';
14
+
15
+ /*
16
+ const SHARED_VIEW = {};
17
+
18
+ export class SchemaSerializerDebug<T> extends SchemaSerializer<T> {
19
+ protected debugStream: fs.WriteStream;
20
+
21
+ constructor(fileName: string = "schema-debug.txt") {
22
+ super();
23
+
24
+ try { fs.unlinkSync(fileName); } catch (e) {}
25
+ this.debugStream = fs.createWriteStream(fileName, { flags: "a" });
26
+ }
27
+
28
+ public getFullState(client?: Client): Buffer {
29
+ const buf = super.getFullState(client);
30
+ this.debugStream.write(`state:${client.sessionId}:${Array.from(buf).slice(1).join(",")}\n`);
31
+ return buf;
32
+ }
33
+
34
+ public applyPatches(clients: Client[]) {
35
+ let numClients = clients.length;
36
+
37
+ const debugChangesDeep = Schema.debugChangesDeep(this.encoder.state);
38
+
39
+ if (
40
+ numClients == 0 ||
41
+ (
42
+ this.encoder.root.changes.size === 0 &&
43
+ (!this.hasFilters || this.encoder.root.filteredChanges.size === 0)
44
+ )
45
+ ) {
46
+ // skip patching state if:
47
+ // - no clients are connected
48
+ // - no changes were made
49
+ // - no "filtered changes" were made when using filters
50
+ return false;
51
+ }
52
+
53
+ this.needFullEncode = true;
54
+
55
+ // dump changes for patch debugging
56
+ if (debugPatch.enabled) {
57
+ (debugPatch as any).dumpChanges = dumpChanges(this.encoder.state);
58
+ }
59
+
60
+ // get patch bytes
61
+ const it: Iterator = { offset: 1 };
62
+ this.encoder.sharedBuffer[0] = Protocol.ROOM_STATE_PATCH;
63
+
64
+ // encode changes once, for all clients
65
+ const encodedChanges = this.encoder.encode(it);
66
+
67
+ if (!this.hasFilters) {
68
+ while (numClients--) {
69
+ const client = clients[numClients];
70
+
71
+ //
72
+ // FIXME: avoid this check for each client
73
+ //
74
+ if (client.state !== ClientState.JOINED) {
75
+ continue;
76
+ }
77
+
78
+ debugChangesDeep.split("\n").forEach((line) => {
79
+ this.debugStream.write(`#${client.sessionId}:${line}\n`);
80
+ });
81
+ this.debugStream.write(`patch:${client.sessionId}:${Array.from(encodedChanges).slice(1).join(",")}\n`);
82
+
83
+ client.raw(encodedChanges);
84
+ }
85
+
86
+ } else {
87
+ // cache shared offset
88
+ const sharedOffset = it.offset;
89
+
90
+ // encode state multiple times, for each client
91
+ while (numClients--) {
92
+ const client = clients[numClients];
93
+
94
+ //
95
+ // FIXME: avoid this check for each client
96
+ //
97
+ if (client.state !== ClientState.JOINED) {
98
+ continue;
99
+ }
100
+
101
+ const view = client.view || SHARED_VIEW;
102
+
103
+ let encodedView = this.views.get(view);
104
+
105
+ // allow to pass the same encoded view for multiple clients
106
+ if (encodedView === undefined) {
107
+ encodedView = (view === SHARED_VIEW)
108
+ ? encodedChanges
109
+ : this.encoder.encodeView(client.view, sharedOffset, it);
110
+ this.views.set(view, encodedView);
111
+ }
112
+
113
+ debugChangesDeep.split("\n").forEach((line) => {
114
+ this.debugStream.write(`#${client.sessionId}:${line}\n`);
115
+ });
116
+ this.debugStream.write(`patch:${client.sessionId}:${Array.from(encodedView).slice(1).join(",")}\n`);
117
+
118
+ client.raw(encodedView);
119
+ }
120
+
121
+ // clear views
122
+ this.views.clear();
123
+ }
124
+
125
+ // discard changes after sending
126
+ this.encoder.discardChanges();
127
+
128
+ // debug patches
129
+ if (debugPatch.enabled) {
130
+ debugPatch(
131
+ '%d bytes sent to %d clients, %j',
132
+ encodedChanges.length,
133
+ clients.length,
134
+ (debugPatch as any).dumpChanges,
135
+ );
136
+ }
137
+
138
+ return true;
139
+ }
140
+
141
+ public handshake(): Buffer {
142
+ const buf = super.handshake();
143
+ this.debugStream.write(`handshake:${Array.from(buf).join(",")}\n`);
144
+ return buf;
145
+ }
146
+ }
147
+
148
+ */
@@ -0,0 +1,9 @@
1
+ import type { Client } from '../Transport.ts';
2
+
3
+ export interface Serializer<T> {
4
+ id: string;
5
+ reset(data: any): void;
6
+ getFullState(client?: Client): Uint8Array;
7
+ applyPatches(clients: Client[], state: T): boolean;
8
+ handshake?(): Uint8Array;
9
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { type Schema, MapSchema, ArraySchema, SetSchema, CollectionSchema, $childType } from '@colyseus/schema';
4
+ import { logger } from '../Logger.ts';
5
+ import { debugAndPrintError, debugDevMode } from '../Debug.ts';
6
+ import { getLocalRoomById, handleCreateRoom, presence, remoteRoomCall } from '../MatchMaker.ts';
7
+ import type { Room } from '../Room.ts';
8
+
9
+ const DEVMODE_CACHE_FILE_PATH = path.resolve(".devmode.json");
10
+
11
+ export let isDevMode: boolean = false;
12
+
13
+ export function hasDevModeCache() {
14
+ return fs.existsSync(DEVMODE_CACHE_FILE_PATH);
15
+ }
16
+
17
+ export function getDevModeCache() {
18
+ return JSON.parse(fs.readFileSync(DEVMODE_CACHE_FILE_PATH, 'utf8')) || {};
19
+ }
20
+
21
+ export function writeDevModeCache(cache: any) {
22
+ fs.writeFileSync(DEVMODE_CACHE_FILE_PATH, JSON.stringify(cache, null, 2), 'utf8');
23
+ }
24
+
25
+ export function setDevMode(bool: boolean) {
26
+ isDevMode = bool;
27
+ }
28
+
29
+ export async function reloadFromCache() {
30
+ const roomHistoryList = Object.entries(await presence.hgetall(getRoomRestoreListKey()));
31
+ debugDevMode("rooms to restore: %i", roomHistoryList.length);
32
+
33
+ for (const [roomId, value] of roomHistoryList) {
34
+ const roomHistory = JSON.parse(value);
35
+ debugDevMode("restoring room %s (%s)", roomHistory.roomName, roomId);
36
+
37
+ const recreatedRoomListing = await handleCreateRoom(roomHistory.roomName, roomHistory.clientOptions, roomId);
38
+ const recreatedRoom = getLocalRoomById(recreatedRoomListing.roomId);
39
+
40
+ // Restore previous state
41
+ if (roomHistory.hasOwnProperty("state")) {
42
+ try {
43
+ const rawState = JSON.parse(roomHistory.state);
44
+ logger.debug(`📋 room '${roomId}' state =>`, rawState);
45
+
46
+ (recreatedRoom.state as Schema).restore(rawState);
47
+ } catch (e: any) {
48
+ debugAndPrintError(`❌ couldn't restore room '${roomId}' state:\n${e.stack}`);
49
+ }
50
+ }
51
+
52
+ // Reserve seats for clients from cached history
53
+ if (roomHistory.clients) {
54
+ for (const clientData of roomHistory.clients) {
55
+ // TODO: need to restore each client's StateView as well
56
+ // reserve seat for 20 seconds
57
+ const { sessionId, reconnectionToken } = clientData;
58
+ await remoteRoomCall(recreatedRoomListing.roomId, '_reserveSeat', [sessionId, {}, {}, 20, false, reconnectionToken]);
59
+ }
60
+ }
61
+
62
+ // call `onRestoreRoom` with custom 'cache'd property.
63
+ recreatedRoom.onRestoreRoom?.(roomHistory["cache"]);
64
+
65
+ logger.debug(`🔄 room '${roomId}' has been restored with ${roomHistory.clients?.length || 0} reserved seats: ${roomHistory.clients?.map((c: any) => c.sessionId).join(", ")}`);
66
+ }
67
+
68
+ if (roomHistoryList.length > 0) {
69
+ logger.debug("✅", roomHistoryList.length, "room(s) have been restored.");
70
+ }
71
+ }
72
+
73
+ export async function cacheRoomHistory(rooms: { [roomId: string]: Room }) {
74
+ for (const room of Object.values(rooms)) {
75
+ const roomHistoryResult = await presence.hget(getRoomRestoreListKey(), room.roomId);
76
+ if (roomHistoryResult) {
77
+ try {
78
+ const roomHistory = JSON.parse(roomHistoryResult);
79
+
80
+ // custom cache method
81
+ roomHistory["cache"] = room.onCacheRoom?.();
82
+
83
+ // encode state
84
+ debugDevMode("caching room %s (%s)", room.roomName, room.roomId);
85
+
86
+ if (room.state) {
87
+ roomHistory["state"] = JSON.stringify(room.state);
88
+ }
89
+
90
+ // cache active clients with their reconnection tokens
91
+ // TODO: need to cache each client's StateView as well
92
+ const activeClients = room.clients.map((client) => ({
93
+ sessionId: client.sessionId,
94
+ reconnectionToken: client.reconnectionToken,
95
+ }));
96
+
97
+ // collect active client sessionIds to avoid duplicates
98
+ const activeSessionIds = new Set(activeClients.map((c) => c.sessionId));
99
+
100
+ // also cache reserved seats (they don't have reconnectionTokens yet)
101
+ // filter out reserved seats that are already active clients (from devMode reconnection)
102
+ const reservedSeats = Object.keys(room['_reservedSeats'])
103
+ .filter((sessionId) => !activeSessionIds.has(sessionId))
104
+ .map((sessionId) => ({
105
+ sessionId,
106
+ reconnectionToken: undefined,
107
+ }));
108
+
109
+ roomHistory["clients"] = activeClients.concat(reservedSeats);
110
+
111
+ await presence.hset(getRoomRestoreListKey(), room.roomId, JSON.stringify(roomHistory));
112
+
113
+ // Rewrite updated room history
114
+ logger.debug(`💾 caching room '${room.roomId}' (clients: ${room.clients.length}, has state: ${roomHistory["state"] !== undefined ? "yes" : "no"})`);
115
+
116
+ } catch (e: any) {
117
+ debugAndPrintError(`❌ couldn't cache room '${room.roomId}', due to:\n${e.stack}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ export async function getPreviousProcessId(hostname: string = '') {
124
+ return await presence.hget(getProcessRestoreKey(), hostname);
125
+ }
126
+
127
+ export function getRoomRestoreListKey() {
128
+ return 'roomhistory';
129
+ }
130
+
131
+ export function getProcessRestoreKey() {
132
+ return 'processhistory';
133
+ }
@@ -0,0 +1,20 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ export type { StandardSchemaV1 };
3
+
4
+ export function standardValidate<T extends StandardSchemaV1>(
5
+ schema: T,
6
+ input: StandardSchemaV1.InferInput<T>
7
+ ): StandardSchemaV1.InferOutput<T> {
8
+ let result = schema['~standard'].validate(input);
9
+
10
+ if (result instanceof Promise) {
11
+ throw new Error('Schema validation must be synchronous');
12
+ }
13
+
14
+ // if the `issues` field exists, the validation failed
15
+ if (result.issues) {
16
+ throw new Error(JSON.stringify(result.issues, null, 2));
17
+ }
18
+
19
+ return (result as StandardSchemaV1.SuccessResult<StandardSchemaV1.InferOutput<T>>).value;
20
+ }
@@ -0,0 +1,169 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ import { EventEmitter } from "events";
4
+ import { type RoomException, type RoomMethodName } from '../errors/RoomExceptions.ts';
5
+
6
+ import { debugAndPrintError, debugMatchMaking } from '../Debug.ts';
7
+
8
+ export type Type<T> = new (...args: any[]) => T;
9
+ export type MethodName<T> = string & {
10
+ [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
11
+ }[keyof T];
12
+
13
+ /**
14
+ * Utility type that extracts the return type of a method or the type of a property
15
+ * from a given class/object type.
16
+ *
17
+ * - If the key is a method, returns the awaited return type of that method
18
+ * - If the key is a property, returns the type of that property
19
+ */
20
+ export type ExtractMethodOrPropertyType<
21
+ TClass,
22
+ TKey extends keyof TClass
23
+ > = TClass[TKey] extends (...args: any[]) => infer R
24
+ ? Awaited<R>
25
+ : TClass[TKey];
26
+
27
+ // remote room call timeouts
28
+ export const REMOTE_ROOM_SHORT_TIMEOUT = Number(process.env.COLYSEUS_PRESENCE_SHORT_TIMEOUT || 2000);
29
+ export const MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME = Number(process.env.COLYSEUS_MAX_CONCURRENT_CREATE_ROOM_WAIT_TIME || 0.5);
30
+
31
+ export function generateId(length: number = 9) {
32
+ return nanoid(length);
33
+ }
34
+
35
+ export function getBearerToken(authHeader: string) {
36
+ return (authHeader && authHeader.startsWith("Bearer ") && authHeader.substring(7, authHeader.length)) || undefined;
37
+ }
38
+
39
+ // nodemon sends SIGUSR2 before reloading
40
+ // (https://github.com/remy/nodemon#controlling-shutdown-of-your-script)
41
+ //
42
+ const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGUSR2'];
43
+
44
+ export function registerGracefulShutdown(callback: (err?: Error) => void) {
45
+ /**
46
+ * Gracefully shutdown on uncaught errors
47
+ */
48
+ process.on('uncaughtException', (err) => {
49
+ debugAndPrintError(err);
50
+ callback(err);
51
+ });
52
+
53
+ signals.forEach((signal) =>
54
+ process.once(signal, () => callback()));
55
+ }
56
+
57
+ export function retry<T = any>(
58
+ cb: Function,
59
+ maxRetries: number = 3,
60
+ errorWhiteList: any[] = [],
61
+ retries: number = 0,
62
+ ) {
63
+ return new Promise<T>((resolve, reject) => {
64
+ cb()
65
+ .then(resolve)
66
+ .catch((e: any) => {
67
+ if (
68
+ errorWhiteList.indexOf(e.constructor) !== -1 &&
69
+ retries++ < maxRetries
70
+ ) {
71
+ setTimeout(() => {
72
+ debugMatchMaking("retrying due to error (error: %s, retries: %s, maxRetries: %s)", e.message, retries, maxRetries);
73
+ retry<T>(cb, maxRetries, errorWhiteList, retries).
74
+ then(resolve).
75
+ catch((e2) => reject(e2));
76
+ }, Math.floor(Math.random() * Math.pow(2, retries) * 400));
77
+
78
+ } else {
79
+ reject(e);
80
+ }
81
+ });
82
+ });
83
+ }
84
+
85
+ export function spliceOne(arr: any[], index: number): boolean {
86
+ // manually splice availableRooms array
87
+ // http://jsperf.com/manual-splice
88
+ if (index === -1 || index >= arr.length) {
89
+ return false;
90
+ }
91
+
92
+ const len = arr.length - 1;
93
+ for (let i = index; i < len; i++) {
94
+ arr[i] = arr[i + 1];
95
+ }
96
+
97
+ arr.length = len;
98
+ return true;
99
+ }
100
+
101
+ export class Deferred<T = any> {
102
+ public promise: Promise<T>;
103
+
104
+ public resolve: Function;
105
+ public reject: Function;
106
+
107
+ constructor(promise?: Promise<T>) {
108
+ this.promise = promise ?? new Promise<T>((resolve, reject) => {
109
+ this.resolve = resolve;
110
+ this.reject = reject;
111
+ });
112
+ }
113
+
114
+ public then(onFulfilled?: (value: T) => any, onRejected?: (reason: any) => any) {
115
+ return this.promise.then(onFulfilled, onRejected);
116
+ }
117
+
118
+ public catch(func: (value: any) => any) {
119
+ return this.promise.catch(func);
120
+ }
121
+
122
+ static reject (reason?: any) {
123
+ return new Deferred(Promise.reject(reason));
124
+ }
125
+
126
+ static resolve<T = any>(value?: T) {
127
+ return new Deferred<T>(Promise.resolve(value));
128
+ }
129
+
130
+ }
131
+
132
+ export function merge(a: any, ...objs: any[]): any {
133
+ for (let i = 0, len = objs.length; i < len; i++) {
134
+ const b = objs[i];
135
+ for (const key in b) {
136
+ if (b.hasOwnProperty(key)) {
137
+ a[key] = b[key];
138
+ }
139
+ }
140
+ }
141
+ return a;
142
+ }
143
+
144
+ export function wrapTryCatch(
145
+ method: Function,
146
+ onError: (error: RoomException, methodName: RoomMethodName) => void,
147
+ exceptionClass: Type<RoomException>,
148
+ methodName: RoomMethodName,
149
+ rethrow: boolean = false,
150
+ ...additionalErrorArgs: any[]
151
+ ) {
152
+ return (...args: any[]) => {
153
+ try {
154
+ const result = method(...args);
155
+ if (typeof (result?.catch) === "function") {
156
+ return result.catch((e: Error) => {
157
+ onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);
158
+ if (rethrow) { throw e; }
159
+ });
160
+ }
161
+ return result;
162
+ } catch (e: any) {
163
+ onError(new exceptionClass(e, e.message, ...args, ...additionalErrorArgs), methodName);
164
+ if (rethrow) { throw e; }
165
+ }
166
+ };
167
+ }
168
+
169
+ export class HttpServerMock extends EventEmitter {}
@@ -0,0 +1,20 @@
1
+ export const createNanoEvents = () => ({
2
+ emit(event: string, ...args: any[]) {
3
+ for (
4
+ let callbacks = this.events[event] || [],
5
+ i = 0,
6
+ length = callbacks.length;
7
+ i < length;
8
+ i++
9
+ ) {
10
+ callbacks[i](...args)
11
+ }
12
+ },
13
+ events: {},
14
+ on(event: string, cb: (...args: any[]) => void) {
15
+ ;(this.events[event] ||= []).push(cb)
16
+ return () => {
17
+ this.events[event] = this.events[event]?.filter(i => cb !== i)
18
+ }
19
+ }
20
+ })