@vgai/p2p-colyseus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,866 @@
1
+ import { ClientArray, type CompatClient, Room, room, validate } from './room';
2
+
3
+ export type Type<T = unknown> = new (...args: unknown[]) => T;
4
+
5
+ class MiniEventEmitter {
6
+ private readonly handlers = new Map<string, Set<(...args: unknown[]) => void>>();
7
+
8
+ on(event: string, handler: (...args: unknown[]) => void): this {
9
+ let eventHandlers = this.handlers.get(event);
10
+ if (!eventHandlers) {
11
+ eventHandlers = new Set();
12
+ this.handlers.set(event, eventHandlers);
13
+ }
14
+ eventHandlers.add(handler);
15
+ return this;
16
+ }
17
+
18
+ once(event: string, handler: (...args: unknown[]) => void): this {
19
+ const wrapped = (...args: unknown[]) => {
20
+ this.off(event, wrapped);
21
+ handler(...args);
22
+ };
23
+ return this.on(event, wrapped);
24
+ }
25
+
26
+ off(event: string, handler: (...args: unknown[]) => void): this {
27
+ this.handlers.get(event)?.delete(handler);
28
+ return this;
29
+ }
30
+
31
+ emit(event: string, ...args: unknown[]): boolean {
32
+ const eventHandlers = this.handlers.get(event);
33
+ if (!eventHandlers || eventHandlers.size === 0) return false;
34
+ for (const handler of [...eventHandlers]) handler(...args);
35
+ return true;
36
+ }
37
+ }
38
+
39
+ export const ClientState = {
40
+ JOINING: 0,
41
+ JOINED: 1,
42
+ RECONNECTING: 2,
43
+ RECONNECTED: 3,
44
+ LEAVING: 4,
45
+ CLOSED: 5,
46
+ } as const;
47
+
48
+ export const Protocol = {
49
+ JOIN_ROOM: 10,
50
+ ERROR: 11,
51
+ LEAVE_ROOM: 12,
52
+ ROOM_DATA: 13,
53
+ ROOM_STATE: 14,
54
+ ROOM_STATE_PATCH: 15,
55
+ ROOM_DATA_BYTES: 16,
56
+ PING: 17,
57
+ PONG: 18,
58
+ } as const;
59
+
60
+ export const ErrorCode = {
61
+ MATCHMAKE_NO_HANDLER: 4210,
62
+ MATCHMAKE_INVALID_CRITERIA: 4211,
63
+ MATCHMAKE_INVALID_ROOM_ID: 4212,
64
+ MATCHMAKE_UNHANDLED: 4213,
65
+ MATCHMAKE_EXPIRED: 4214,
66
+ AUTH_FAILED: 4215,
67
+ APPLICATION_ERROR: 4216,
68
+ } as const;
69
+
70
+ export const CloseCode = {
71
+ CONSENTED: 4000,
72
+ DEVMODE_RESTART: 4010,
73
+ } as const;
74
+
75
+ export const RoomInternalState = {
76
+ CREATING: 0,
77
+ CREATED: 1,
78
+ DISPOSING: 2,
79
+ } as const;
80
+
81
+ export class ServerError extends Error {
82
+ constructor(
83
+ readonly code: number,
84
+ message?: string,
85
+ ) {
86
+ super(message ?? String(code));
87
+ this.name = 'ServerError';
88
+ }
89
+ }
90
+
91
+ export class OnCreateException extends Error {}
92
+ export class OnAuthException extends Error {}
93
+ export class OnJoinException extends Error {}
94
+ export class OnLeaveException extends Error {}
95
+ export class OnDisposeException extends Error {}
96
+ export class OnMessageException extends Error {}
97
+ export class SimulationIntervalException extends Error {}
98
+ export class TimedEventException extends Error {}
99
+
100
+ export class Deferred<T = unknown> implements Promise<T> {
101
+ readonly [Symbol.toStringTag] = 'Promise';
102
+ private resolvePromise!: (value: T | PromiseLike<T>) => void;
103
+ private rejectPromise!: (reason?: unknown) => void;
104
+ private readonly promise = new Promise<T>((resolve, reject) => {
105
+ this.resolvePromise = resolve;
106
+ this.rejectPromise = reject;
107
+ });
108
+
109
+ resolve(value: T | PromiseLike<T>): void {
110
+ this.resolvePromise(value);
111
+ }
112
+
113
+ reject(reason?: unknown): void {
114
+ this.rejectPromise(reason);
115
+ }
116
+
117
+ // biome-ignore lint/suspicious/noThenProperty: Colyseus exposes Deferred as a Promise-compatible thenable.
118
+ then<TResult1 = T, TResult2 = never>(
119
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
120
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
121
+ ): Promise<TResult1 | TResult2> {
122
+ return this.promise.then(onfulfilled, onrejected);
123
+ }
124
+
125
+ catch<TResult = never>(
126
+ onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
127
+ ): Promise<T | TResult> {
128
+ return this.promise.catch(onrejected);
129
+ }
130
+
131
+ finally(onfinally?: (() => void) | null): Promise<T> {
132
+ return this.promise.finally(onfinally);
133
+ }
134
+ }
135
+
136
+ export abstract class Transport {
137
+ protocol?: string;
138
+ server?: unknown;
139
+
140
+ listen(
141
+ _port?: number | string,
142
+ _hostname?: string,
143
+ _backlog?: number,
144
+ _listeningListener?: () => void,
145
+ ): this {
146
+ return this;
147
+ }
148
+
149
+ shutdown(): void {}
150
+
151
+ simulateLatency(_milliseconds?: number): void {}
152
+ }
153
+
154
+ export class LocalPresence {
155
+ private readonly values = new Map<string, string>();
156
+ private readonly subscriptions = new Map<string, Set<(message: unknown) => void>>();
157
+
158
+ async subscribe(topic: string, callback: (message: unknown) => void): Promise<void> {
159
+ let handlers = this.subscriptions.get(topic);
160
+ if (!handlers) {
161
+ handlers = new Set();
162
+ this.subscriptions.set(topic, handlers);
163
+ }
164
+ handlers.add(callback);
165
+ }
166
+
167
+ async unsubscribe(topic: string, callback?: (message: unknown) => void): Promise<void> {
168
+ if (callback) this.subscriptions.get(topic)?.delete(callback);
169
+ else this.subscriptions.delete(topic);
170
+ }
171
+
172
+ publish(topic: string, message: unknown): void {
173
+ for (const callback of this.subscriptions.get(topic) ?? []) callback(message);
174
+ }
175
+
176
+ async set(key: string, value: string): Promise<void> {
177
+ this.values.set(key, value);
178
+ }
179
+
180
+ async get(key: string): Promise<string | undefined> {
181
+ return this.values.get(key);
182
+ }
183
+
184
+ async del(key: string): Promise<void> {
185
+ this.values.delete(key);
186
+ }
187
+
188
+ async exists(key: string): Promise<boolean> {
189
+ return this.values.has(key);
190
+ }
191
+
192
+ async setex(key: string, value: string, _seconds: number): Promise<void> {
193
+ await this.set(key, value);
194
+ }
195
+
196
+ async expire(_key: string, _seconds: number): Promise<boolean> {
197
+ return true;
198
+ }
199
+ }
200
+
201
+ export class Clock {
202
+ setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout> {
203
+ return setTimeout(callback, ms);
204
+ }
205
+
206
+ setInterval(callback: () => void, ms: number): ReturnType<typeof setInterval> {
207
+ return setInterval(callback, ms);
208
+ }
209
+
210
+ clear(): void {}
211
+ }
212
+
213
+ export class Delayed {
214
+ constructor(
215
+ readonly callback: () => void = () => {},
216
+ readonly time = 0,
217
+ ) {}
218
+
219
+ clear(): void {}
220
+ }
221
+
222
+ export class RedisPresence extends LocalPresence {
223
+ shutdown(): void {}
224
+
225
+ setMaxListeners(): void {}
226
+ }
227
+
228
+ export class RedisDriver {
229
+ rooms: RoomCache[] = [];
230
+
231
+ async findOne(conditions: Partial<RoomCache>): Promise<RoomCache | undefined> {
232
+ return this.rooms.find((room) =>
233
+ Object.entries(conditions).every(
234
+ ([key, value]) =>
235
+ value === undefined || (room as unknown as Record<string, unknown>)[key] === value,
236
+ ),
237
+ );
238
+ }
239
+
240
+ query(conditions: Partial<RoomCache> = {}): RoomCache[] {
241
+ return this.rooms.filter((room) =>
242
+ Object.entries(conditions).every(
243
+ ([key, value]) =>
244
+ value === undefined || (room as unknown as Record<string, unknown>)[key] === value,
245
+ ),
246
+ );
247
+ }
248
+
249
+ persist(room: RoomCache): boolean {
250
+ const index = this.rooms.findIndex((candidate) => candidate.roomId === room.roomId);
251
+ if (index >= 0) this.rooms[index] = room;
252
+ else this.rooms.push(room);
253
+ return true;
254
+ }
255
+
256
+ remove(roomId: string): boolean {
257
+ const before = this.rooms.length;
258
+ this.rooms = this.rooms.filter((room) => room.roomId !== roomId);
259
+ return this.rooms.length !== before;
260
+ }
261
+
262
+ clear(): void {
263
+ this.rooms = [];
264
+ }
265
+
266
+ shutdown(): void {
267
+ this.clear();
268
+ }
269
+ }
270
+
271
+ export class LocalDriver extends RedisDriver {}
272
+
273
+ export class SchemaSerializer<T = unknown> {
274
+ state?: T;
275
+
276
+ reset(newState: T): void {
277
+ this.state = newState;
278
+ }
279
+ }
280
+
281
+ export interface RoomCache<Metadata = unknown> {
282
+ name: string;
283
+ roomId: string;
284
+ processId: string;
285
+ clients: number;
286
+ maxClients: number;
287
+ locked: boolean;
288
+ private: boolean;
289
+ metadata?: Metadata;
290
+ }
291
+
292
+ export interface SeatReservation {
293
+ name: string;
294
+ sessionId: string;
295
+ roomId: string;
296
+ publicAddress?: string;
297
+ processId?: string;
298
+ reconnectionToken?: string;
299
+ devMode?: boolean;
300
+ }
301
+
302
+ export class RegisteredHandler<RoomType extends Room = Room> extends MiniEventEmitter {
303
+ '~room'!: RoomType;
304
+ name = '';
305
+ filterOptions: string[] = [];
306
+ sortOptions?: Record<string, 1 | -1>;
307
+ realtimeListingEnabled = false;
308
+
309
+ constructor(
310
+ readonly klass: Type<RoomType>,
311
+ readonly options?: unknown,
312
+ ) {
313
+ super();
314
+ }
315
+
316
+ enableRealtimeListing(): this {
317
+ this.realtimeListingEnabled = true;
318
+ return this;
319
+ }
320
+
321
+ filterBy(options: string[]): this {
322
+ this.filterOptions = options;
323
+ return this;
324
+ }
325
+
326
+ sortBy(options: Record<string, 1 | -1>): this {
327
+ this.sortOptions = options;
328
+ return this;
329
+ }
330
+
331
+ getMetadataFromOptions(options: unknown): { metadata?: Record<string, unknown> } {
332
+ const record = asRecord(options);
333
+ if (!record) return {};
334
+ const metadata: Record<string, unknown> = {};
335
+ for (const key of this.filterOptions) {
336
+ if (key in record) metadata[key] = record[key];
337
+ }
338
+ return Object.keys(metadata).length > 0 ? { metadata } : {};
339
+ }
340
+
341
+ getFilterOptions(options: unknown): Record<string, unknown> {
342
+ const record = asRecord(options);
343
+ if (!record) return {};
344
+ const out: Record<string, unknown> = {};
345
+ for (const key of this.filterOptions) {
346
+ if (key in record) out[key] = record[key];
347
+ }
348
+ return out;
349
+ }
350
+ }
351
+
352
+ const handlers = new Map<string, RegisteredHandler>();
353
+ const rooms = new Map<string, RoomCache>();
354
+
355
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
356
+ return typeof value === 'object' && value !== null
357
+ ? (value as Record<string, unknown>)
358
+ : undefined;
359
+ }
360
+
361
+ export const processId = 'p2p-colyseus-local';
362
+ export let publicAddress = 'localhost';
363
+ export const presence = new LocalPresence();
364
+ export const driver = {
365
+ rooms: [...rooms.values()],
366
+ clear: () => rooms.clear(),
367
+ };
368
+
369
+ export async function setup(
370
+ _presence?: LocalPresence,
371
+ _driver?: unknown,
372
+ address?: string,
373
+ ): Promise<void> {
374
+ if (address) publicAddress = address;
375
+ }
376
+
377
+ export async function accept(): Promise<void> {}
378
+
379
+ export function defineRoomType<T extends Room>(
380
+ roomName: string,
381
+ klass: Type<T>,
382
+ defaultOptions?: Parameters<NonNullable<T['onCreate']>>[0],
383
+ ): RegisteredHandler<T> {
384
+ const handler = new RegisteredHandler(klass, defaultOptions);
385
+ handler.name = roomName;
386
+ handlers.set(roomName, handler);
387
+ return handler;
388
+ }
389
+
390
+ export function addRoomType(handler: RegisteredHandler): void {
391
+ handlers.set(handler.name, handler);
392
+ }
393
+
394
+ export function removeRoomType(roomName: string): void {
395
+ handlers.delete(roomName);
396
+ }
397
+
398
+ export function getAllHandlers(): Record<string, RegisteredHandler> {
399
+ return Object.fromEntries(handlers);
400
+ }
401
+
402
+ export function getHandler(roomName: string): RegisteredHandler {
403
+ const handler = handlers.get(roomName);
404
+ if (!handler) throw new ServerError(404, `Room not defined: ${roomName}`);
405
+ return handler;
406
+ }
407
+
408
+ export function getRoomClass(roomName: string): Type<Room> {
409
+ return getHandler(roomName).klass;
410
+ }
411
+
412
+ export async function createRoom(roomName: string, clientOptions?: unknown): Promise<RoomCache> {
413
+ const handler = getHandler(roomName);
414
+ const roomId = generateId(9);
415
+ const cache: RoomCache = {
416
+ name: roomName,
417
+ roomId,
418
+ processId,
419
+ clients: 0,
420
+ maxClients: Number.POSITIVE_INFINITY,
421
+ locked: false,
422
+ private: false,
423
+ metadata: handler.getMetadataFromOptions(clientOptions).metadata,
424
+ };
425
+ rooms.set(roomId, cache);
426
+ return cache;
427
+ }
428
+
429
+ export const handleCreateRoom = createRoom;
430
+
431
+ export async function getRoomById(roomId: string): Promise<RoomCache> {
432
+ const room = rooms.get(roomId);
433
+ if (!room) throw new ServerError(404, `Room not found: ${roomId}`);
434
+ return room;
435
+ }
436
+
437
+ export function getLocalRoomById(_roomId: string): Room | undefined {
438
+ return undefined;
439
+ }
440
+
441
+ export async function query(conditions: Partial<RoomCache> = {}): Promise<RoomCache[]> {
442
+ return [...rooms.values()].filter((room) =>
443
+ Object.entries(conditions).every(
444
+ ([key, value]) =>
445
+ value === undefined || (room as unknown as Record<string, unknown>)[key] === value,
446
+ ),
447
+ );
448
+ }
449
+
450
+ export async function findOneRoomAvailable(roomName: string): Promise<RoomCache> {
451
+ const room = [...rooms.values()].find(
452
+ (candidate) => candidate.name === roomName && !candidate.locked && !candidate.private,
453
+ );
454
+ if (room) return room;
455
+ return createRoom(roomName);
456
+ }
457
+
458
+ export async function reserveSeatFor(
459
+ room: RoomCache,
460
+ _options?: unknown,
461
+ ): Promise<SeatReservation> {
462
+ const sessionId = generateId();
463
+ room.clients++;
464
+ return buildSeatReservation(room, sessionId);
465
+ }
466
+
467
+ export async function reserveMultipleSeatsFor(
468
+ room: RoomCache,
469
+ clientsData: Array<{ sessionId: string; options: unknown; auth: unknown }>,
470
+ ): Promise<boolean[]> {
471
+ room.clients += clientsData.length;
472
+ return clientsData.map(() => true);
473
+ }
474
+
475
+ export function buildSeatReservation(room: RoomCache, sessionId: string): SeatReservation {
476
+ return {
477
+ name: room.name,
478
+ roomId: room.roomId,
479
+ sessionId,
480
+ processId: room.processId,
481
+ publicAddress,
482
+ };
483
+ }
484
+
485
+ export async function create(roomName: string, clientOptions?: unknown): Promise<SeatReservation> {
486
+ return reserveSeatFor(await createRoom(roomName, clientOptions), clientOptions);
487
+ }
488
+
489
+ export async function joinOrCreate(
490
+ roomName: string,
491
+ clientOptions?: unknown,
492
+ ): Promise<SeatReservation> {
493
+ return reserveSeatFor(await findOneRoomAvailable(roomName), clientOptions);
494
+ }
495
+
496
+ export async function join(roomName: string, clientOptions?: unknown): Promise<SeatReservation> {
497
+ return joinOrCreate(roomName, clientOptions);
498
+ }
499
+
500
+ export async function joinById(roomId: string, clientOptions?: unknown): Promise<SeatReservation> {
501
+ return reserveSeatFor(await getRoomById(roomId), clientOptions);
502
+ }
503
+
504
+ export async function reconnect(roomId: string, clientOptions?: unknown): Promise<SeatReservation> {
505
+ const reservation = await joinById(roomId, clientOptions);
506
+ reservation.reconnectionToken = String(
507
+ asRecord(clientOptions)?.['reconnectionToken'] ?? reservation.sessionId,
508
+ );
509
+ return reservation;
510
+ }
511
+
512
+ export async function remoteRoomCall(_roomId: string, _method: string): Promise<unknown> {
513
+ return undefined;
514
+ }
515
+
516
+ export function disconnectAll(): Promise<void>[] {
517
+ rooms.clear();
518
+ return [];
519
+ }
520
+
521
+ export async function gracefullyShutdown(): Promise<void> {
522
+ rooms.clear();
523
+ handlers.clear();
524
+ }
525
+
526
+ export async function healthCheckAllProcesses(): Promise<void> {}
527
+
528
+ export async function healthCheckProcessId(): Promise<boolean> {
529
+ return true;
530
+ }
531
+
532
+ export const MatchMakerState = {
533
+ INITIALIZING: 0,
534
+ READY: 1,
535
+ SHUTTING_DOWN: 2,
536
+ } as const;
537
+ export const state = MatchMakerState.READY;
538
+ export const onReady = new Deferred<void>();
539
+ onReady.resolve();
540
+
541
+ export function setHealthChecksEnabled(): void {}
542
+
543
+ export const controller = {
544
+ async invokeMethod(method: string, roomName: string, clientOptions?: unknown): Promise<unknown> {
545
+ if (method === 'joinOrCreate') return joinOrCreate(roomName, clientOptions);
546
+ if (method === 'create') return create(roomName, clientOptions);
547
+ if (method === 'join') return join(roomName, clientOptions);
548
+ return undefined;
549
+ },
550
+ };
551
+
552
+ export const stats = {};
553
+
554
+ export const matchMaker = {
555
+ setup,
556
+ accept,
557
+ joinOrCreate,
558
+ create,
559
+ join,
560
+ reconnect,
561
+ joinById,
562
+ query,
563
+ findOneRoomAvailable,
564
+ remoteRoomCall,
565
+ defineRoomType,
566
+ addRoomType,
567
+ removeRoomType,
568
+ getAllHandlers,
569
+ getHandler,
570
+ getRoomClass,
571
+ createRoom,
572
+ handleCreateRoom,
573
+ getRoomById,
574
+ getLocalRoomById,
575
+ disconnectAll,
576
+ gracefullyShutdown,
577
+ reserveSeatFor,
578
+ reserveMultipleSeatsFor,
579
+ buildSeatReservation,
580
+ healthCheckAllProcesses,
581
+ healthCheckProcessId,
582
+ setHealthChecksEnabled,
583
+ controller,
584
+ stats,
585
+ processId,
586
+ presence,
587
+ driver,
588
+ publicAddress,
589
+ MatchMakerState,
590
+ state,
591
+ onReady,
592
+ };
593
+
594
+ export const __globalEndpoints: unknown[] = [];
595
+
596
+ export interface Router {
597
+ routes?: unknown[];
598
+ }
599
+
600
+ export function createRouter(): Router {
601
+ return { routes: [] };
602
+ }
603
+
604
+ export function createEndpoint(
605
+ handler: (...args: unknown[]) => unknown,
606
+ ): (...args: unknown[]) => unknown {
607
+ return handler;
608
+ }
609
+
610
+ export function createInternalContext(context: unknown): unknown {
611
+ return context;
612
+ }
613
+
614
+ export function createMiddleware(
615
+ middleware: (...args: unknown[]) => unknown,
616
+ ): (...args: unknown[]) => unknown {
617
+ return middleware;
618
+ }
619
+
620
+ export function toNodeHandler(
621
+ handler: (...args: unknown[]) => unknown,
622
+ ): (...args: unknown[]) => unknown {
623
+ return handler;
624
+ }
625
+
626
+ export function isDevMode(): boolean {
627
+ return false;
628
+ }
629
+
630
+ export async function subscribeIPC(): Promise<void> {}
631
+
632
+ export async function requestFromIPC<T = unknown>(): Promise<T | undefined> {
633
+ return undefined;
634
+ }
635
+
636
+ export interface ServerOptions {
637
+ publicAddress?: string;
638
+ transport?: Transport;
639
+ presence?: LocalPresence;
640
+ greet?: boolean;
641
+ beforeListen?: () => Promise<void> | void;
642
+ }
643
+
644
+ export class Server<
645
+ RoomTypes extends Record<string, RegisteredHandler> = Record<string, RegisteredHandler>,
646
+ > {
647
+ '~rooms'!: RoomTypes;
648
+ '~routes'!: unknown;
649
+ transport: Transport;
650
+ router = {};
651
+ readonly options: ServerOptions;
652
+ // biome-ignore lint/suspicious/noConfusingVoidType: mirrors Colyseus's onShutdown contract — sync callbacks return nothing, async ones a promise
653
+ private readonly shutdownCallbacks = new Set<() => void | Promise<unknown>>();
654
+ // biome-ignore lint/suspicious/noConfusingVoidType: same contract as shutdownCallbacks
655
+ private readonly beforeShutdownCallbacks = new Set<() => void | Promise<unknown>>();
656
+
657
+ constructor(options: ServerOptions = {}) {
658
+ this.options = options;
659
+ this.transport = options.transport ?? new NoopTransport();
660
+ }
661
+
662
+ async attach(options: ServerOptions): Promise<void> {
663
+ Object.assign(this.options, options);
664
+ if (options.transport) this.transport = options.transport;
665
+ }
666
+
667
+ async listen(
668
+ port: number | string,
669
+ hostname?: string,
670
+ backlog?: number,
671
+ listeningListener?: () => void,
672
+ ): Promise<unknown> {
673
+ await this.options.beforeListen?.();
674
+ this.transport.listen(port, hostname, backlog, listeningListener);
675
+ listeningListener?.();
676
+ return this.transport;
677
+ }
678
+
679
+ define<T extends Type<Room>>(
680
+ roomClass: T,
681
+ defaultOptions?: unknown,
682
+ ): RegisteredHandler<InstanceType<T>>;
683
+ define<T extends Type<Room>>(
684
+ name: string,
685
+ roomClass: T,
686
+ defaultOptions?: unknown,
687
+ ): RegisteredHandler<InstanceType<T>>;
688
+ define<T extends Type<Room>>(
689
+ nameOrClass: string | T,
690
+ roomClassOrOptions?: T | unknown,
691
+ defaultOptions?: unknown,
692
+ ): RegisteredHandler<InstanceType<T>> {
693
+ const name = typeof nameOrClass === 'string' ? nameOrClass : nameOrClass.name;
694
+ const klass = (typeof nameOrClass === 'string' ? roomClassOrOptions : nameOrClass) as T;
695
+ return defineRoomType(
696
+ name,
697
+ klass,
698
+ defaultOptions ?? (typeof nameOrClass === 'string' ? undefined : roomClassOrOptions),
699
+ ) as RegisteredHandler<InstanceType<T>>;
700
+ }
701
+
702
+ removeRoomType(name: string): void {
703
+ removeRoomType(name);
704
+ }
705
+
706
+ simulateLatency(milliseconds: number): void {
707
+ this.transport.simulateLatency(milliseconds);
708
+ }
709
+
710
+ // biome-ignore lint/suspicious/noConfusingVoidType: mirrors Colyseus's onShutdown signature verbatim
711
+ onShutdown(callback: () => void | Promise<unknown>): void {
712
+ this.shutdownCallbacks.add(callback);
713
+ }
714
+
715
+ // biome-ignore lint/suspicious/noConfusingVoidType: mirrors Colyseus's onBeforeShutdown signature verbatim
716
+ onBeforeShutdown(callback: () => void | Promise<unknown>): void {
717
+ this.beforeShutdownCallbacks.add(callback);
718
+ }
719
+
720
+ async gracefullyShutdown(): Promise<void> {
721
+ for (const callback of this.beforeShutdownCallbacks) await callback();
722
+ await gracefullyShutdown();
723
+ for (const callback of this.shutdownCallbacks) await callback();
724
+ this.transport.shutdown();
725
+ }
726
+ }
727
+
728
+ export function defineRoom<T extends Type<Room>>(
729
+ roomKlass: T,
730
+ defaultOptions?: unknown,
731
+ ): RegisteredHandler<InstanceType<T>> {
732
+ return new RegisteredHandler(roomKlass, defaultOptions) as RegisteredHandler<InstanceType<T>>;
733
+ }
734
+
735
+ export function defineServer(
736
+ options: ServerOptions & { rooms?: Record<string, RegisteredHandler> },
737
+ ): Server {
738
+ const server = new Server(options);
739
+ for (const [name, handler] of Object.entries(options.rooms ?? {})) {
740
+ handler.name = name;
741
+ addRoomType(handler);
742
+ }
743
+ return server;
744
+ }
745
+
746
+ export class NoopTransport extends Transport {}
747
+
748
+ export class WebSocketClient {}
749
+
750
+ export interface TransportOptions {
751
+ server?: unknown;
752
+ }
753
+
754
+ export class WebSocketTransport extends Transport {
755
+ constructor(readonly options: TransportOptions = {}) {
756
+ super();
757
+ this.server = options.server;
758
+ }
759
+ }
760
+
761
+ export function generateId(length = 9): string {
762
+ const id = Math.random().toString(36).slice(2);
763
+ return id.length >= length ? id.slice(0, length) : id.padEnd(length, '0');
764
+ }
765
+
766
+ export function spliceOne<T>(array: T[], index: number): boolean {
767
+ if (index < 0 || index >= array.length) return false;
768
+ array.splice(index, 1);
769
+ return true;
770
+ }
771
+
772
+ export function getBearerToken(value?: string): string | undefined {
773
+ return value?.startsWith('Bearer ') ? value.slice('Bearer '.length) : undefined;
774
+ }
775
+
776
+ export async function dynamicImport<T = unknown>(path: string): Promise<T> {
777
+ return import(path) as Promise<T>;
778
+ }
779
+
780
+ export function getMessageBytes(message: Uint8Array): Uint8Array {
781
+ return message;
782
+ }
783
+
784
+ export function updateLobby(): void {}
785
+
786
+ export function subscribeLobby(): () => void {
787
+ return () => {};
788
+ }
789
+
790
+ export function initializeRoomCache(cache: RoomCache): RoomCache {
791
+ return cache;
792
+ }
793
+
794
+ export function connectClientToRoom(): Promise<void> {
795
+ return Promise.resolve();
796
+ }
797
+
798
+ export function debugMatchMaking(): void {}
799
+ export function debugMessage(): void {}
800
+ export function debugPatch(): void {}
801
+ export function debugError(): void {}
802
+ export function debugConnection(): void {}
803
+ export function debugDriver(): void {}
804
+ export function debugPresence(): void {}
805
+ export function debugAndPrintError(error: unknown): void {
806
+ // biome-ignore lint/suspicious/noConsole: printing IS this compat shim's purpose (mirrors colyseus's debugAndPrintError)
807
+ if (error) console.error(error);
808
+ }
809
+
810
+ export const logger = console;
811
+
812
+ export class LobbyRoom extends Room {}
813
+ export class RelayRoom extends Room {}
814
+ export class QueueRoom extends Room {}
815
+
816
+ // biome-ignore lint/complexity/noStaticOnlyClass: mirrors @colyseus/auth's Hash export shape — consumers call Hash.make/Hash.compare statically
817
+ export class Hash {
818
+ static async make(password: string): Promise<string> {
819
+ return password;
820
+ }
821
+
822
+ static async compare(password: string, hash: string): Promise<boolean> {
823
+ return password === hash;
824
+ }
825
+ }
826
+
827
+ export const JWT = {
828
+ async sign(payload: unknown): Promise<string> {
829
+ return btoa(JSON.stringify(payload));
830
+ },
831
+ async verify(token: string): Promise<unknown> {
832
+ return JSON.parse(atob(token));
833
+ },
834
+ };
835
+
836
+ export const auth = {
837
+ settings: {},
838
+ prefix: '/auth',
839
+ routes: {},
840
+ };
841
+
842
+ export function monitor(): { use?: (...args: unknown[]) => unknown } {
843
+ return {};
844
+ }
845
+
846
+ export function playground(): { use?: (...args: unknown[]) => unknown } {
847
+ return {};
848
+ }
849
+
850
+ export async function listen<
851
+ RoomTypes extends Record<string, RegisteredHandler> = Record<string, RegisteredHandler>,
852
+ >(
853
+ optionsOrServer: Server<RoomTypes> | (ServerOptions & { rooms?: RoomTypes }),
854
+ port = Number(process.env['PORT'] ?? 2567),
855
+ ): Promise<Server<RoomTypes>> {
856
+ const server =
857
+ optionsOrServer instanceof Server ? optionsOrServer : defineServer(optionsOrServer);
858
+ await server.listen(port);
859
+ return server as Server<RoomTypes>;
860
+ }
861
+
862
+ export async function getTransport(options: ServerOptions = {}): Promise<Transport> {
863
+ return options.transport ?? new NoopTransport();
864
+ }
865
+
866
+ export { Room, ClientArray, room, validate, type CompatClient as Client };