@esengine/server 4.4.0 → 4.5.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,2045 @@
1
+ import { createLogger } from './chunk-I4QQSQ72.js';
2
+ import { __name, __publicField } from './chunk-T626JPC7.js';
3
+ import * as path from 'path';
4
+ import { createServer as createServer$1 } from 'http';
5
+ import { serve } from '@esengine/rpc/server';
6
+ import { rpc } from '@esengine/rpc';
7
+ import * as fs from 'fs';
8
+ import { pathToFileURL } from 'url';
9
+
10
+ // src/room/RoomManager.ts
11
+ var logger = createLogger("Room");
12
+ var _RoomManager = class _RoomManager {
13
+ constructor(sendFn, sendBinaryFn) {
14
+ /**
15
+ * @zh 房间类型定义映射
16
+ * @en Room type definitions map
17
+ */
18
+ __publicField(this, "_definitions", /* @__PURE__ */ new Map());
19
+ /**
20
+ * @zh 房间实例映射
21
+ * @en Room instances map
22
+ */
23
+ __publicField(this, "_rooms", /* @__PURE__ */ new Map());
24
+ /**
25
+ * @zh 玩家到房间的映射
26
+ * @en Player to room mapping
27
+ */
28
+ __publicField(this, "_playerToRoom", /* @__PURE__ */ new Map());
29
+ /**
30
+ * @zh 下一个房间 ID 计数器
31
+ * @en Next room ID counter
32
+ */
33
+ __publicField(this, "_nextRoomId", 1);
34
+ /**
35
+ * @zh 消息发送函数
36
+ * @en Message send function
37
+ */
38
+ __publicField(this, "_sendFn");
39
+ /**
40
+ * @zh 二进制发送函数
41
+ * @en Binary send function
42
+ */
43
+ __publicField(this, "_sendBinaryFn");
44
+ this._sendFn = sendFn;
45
+ this._sendBinaryFn = sendBinaryFn;
46
+ }
47
+ /**
48
+ * @zh 注册房间类型
49
+ * @en Define room type
50
+ */
51
+ define(name, roomClass) {
52
+ this._definitions.set(name, {
53
+ roomClass
54
+ });
55
+ }
56
+ /**
57
+ * @zh 创建房间
58
+ * @en Create room
59
+ *
60
+ * @param name - 房间类型名称 | Room type name
61
+ * @param options - 房间配置 | Room options
62
+ * @returns 房间实例或 null | Room instance or null
63
+ */
64
+ async create(name, options) {
65
+ const room = await this._createRoomInstance(name, options);
66
+ if (room) {
67
+ await this._onRoomCreated(name, room);
68
+ logger.info(`Created: ${name} (${room.id})`);
69
+ }
70
+ return room;
71
+ }
72
+ /**
73
+ * @zh 房间创建后的回调
74
+ * @en Callback after room is created
75
+ *
76
+ * @param _name - 房间类型名称 | Room type name
77
+ * @param _room - 房间实例 | Room instance
78
+ */
79
+ async _onRoomCreated(_name, _room) {
80
+ }
81
+ /**
82
+ * @zh 加入或创建房间
83
+ * @en Join or create room
84
+ *
85
+ * @param name - 房间类型名称 | Room type name
86
+ * @param playerId - 玩家 ID | Player ID
87
+ * @param conn - 玩家连接 | Player connection
88
+ * @param options - 房间配置 | Room options
89
+ * @returns 房间和玩家实例或 null | Room and player instance or null
90
+ */
91
+ async joinOrCreate(name, playerId, conn, options) {
92
+ let room = this._findAvailableRoom(name);
93
+ if (!room) {
94
+ room = await this.create(name, options);
95
+ if (!room) return null;
96
+ }
97
+ const player = await room._addPlayer(playerId, conn);
98
+ if (!player) return null;
99
+ this._onPlayerJoined(playerId, room.id, player);
100
+ logger.info(`Player ${playerId} joined ${room.id}`);
101
+ return {
102
+ room,
103
+ player
104
+ };
105
+ }
106
+ /**
107
+ * @zh 加入指定房间
108
+ * @en Join specific room
109
+ *
110
+ * @param roomId - 房间 ID | Room ID
111
+ * @param playerId - 玩家 ID | Player ID
112
+ * @param conn - 玩家连接 | Player connection
113
+ * @returns 房间和玩家实例或 null | Room and player instance or null
114
+ */
115
+ async joinById(roomId, playerId, conn) {
116
+ const room = this._rooms.get(roomId);
117
+ if (!room) return null;
118
+ const player = await room._addPlayer(playerId, conn);
119
+ if (!player) return null;
120
+ this._onPlayerJoined(playerId, room.id, player);
121
+ logger.info(`Player ${playerId} joined ${room.id}`);
122
+ return {
123
+ room,
124
+ player
125
+ };
126
+ }
127
+ /**
128
+ * @zh 玩家离开
129
+ * @en Player leave
130
+ *
131
+ * @param playerId - 玩家 ID | Player ID
132
+ * @param reason - 离开原因 | Leave reason
133
+ */
134
+ async leave(playerId, reason) {
135
+ const roomId = this._playerToRoom.get(playerId);
136
+ if (!roomId) return;
137
+ const room = this._rooms.get(roomId);
138
+ if (room) {
139
+ await room._removePlayer(playerId, reason);
140
+ }
141
+ this._onPlayerLeft(playerId, roomId);
142
+ logger.info(`Player ${playerId} left ${roomId}`);
143
+ }
144
+ /**
145
+ * @zh 处理消息
146
+ * @en Handle message
147
+ */
148
+ handleMessage(playerId, type, data) {
149
+ const roomId = this._playerToRoom.get(playerId);
150
+ if (!roomId) return;
151
+ const room = this._rooms.get(roomId);
152
+ if (room) {
153
+ room._handleMessage(type, data, playerId);
154
+ }
155
+ }
156
+ /**
157
+ * @zh 获取房间
158
+ * @en Get room
159
+ */
160
+ getRoom(roomId) {
161
+ return this._rooms.get(roomId);
162
+ }
163
+ /**
164
+ * @zh 获取玩家所在房间
165
+ * @en Get player's room
166
+ */
167
+ getPlayerRoom(playerId) {
168
+ const roomId = this._playerToRoom.get(playerId);
169
+ return roomId ? this._rooms.get(roomId) : void 0;
170
+ }
171
+ /**
172
+ * @zh 获取所有房间
173
+ * @en Get all rooms
174
+ */
175
+ getRooms() {
176
+ return Array.from(this._rooms.values());
177
+ }
178
+ /**
179
+ * @zh 获取指定类型的所有房间
180
+ * @en Get all rooms of a type
181
+ */
182
+ getRoomsByType(name) {
183
+ const def = this._definitions.get(name);
184
+ if (!def) return [];
185
+ return Array.from(this._rooms.values()).filter((room) => room instanceof def.roomClass);
186
+ }
187
+ /**
188
+ * @zh 查找可用房间
189
+ * @en Find available room
190
+ *
191
+ * @param name - 房间类型名称 | Room type name
192
+ * @returns 可用房间或 null | Available room or null
193
+ */
194
+ _findAvailableRoom(name) {
195
+ const def = this._definitions.get(name);
196
+ if (!def) return null;
197
+ for (const room of this._rooms.values()) {
198
+ if (room instanceof def.roomClass && !room.isFull && !room.isLocked && !room.isDisposed) {
199
+ return room;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * @zh 生成房间 ID
206
+ * @en Generate room ID
207
+ *
208
+ * @returns 新的房间 ID | New room ID
209
+ */
210
+ _generateRoomId() {
211
+ return `room_${this._nextRoomId++}`;
212
+ }
213
+ /**
214
+ * @zh 获取房间定义
215
+ * @en Get room definition
216
+ *
217
+ * @param name - 房间类型名称 | Room type name
218
+ * @returns 房间定义或 undefined | Room definition or undefined
219
+ */
220
+ _getDefinition(name) {
221
+ return this._definitions.get(name);
222
+ }
223
+ /**
224
+ * @zh 内部创建房间实例
225
+ * @en Internal create room instance
226
+ *
227
+ * @param name - 房间类型名称 | Room type name
228
+ * @param options - 房间配置 | Room options
229
+ * @param roomId - 可选的房间 ID(用于分布式恢复) | Optional room ID (for distributed recovery)
230
+ * @returns 房间实例或 null | Room instance or null
231
+ */
232
+ async _createRoomInstance(name, options, roomId) {
233
+ const def = this._definitions.get(name);
234
+ if (!def) {
235
+ logger.warn(`Room type not found: ${name}`);
236
+ return null;
237
+ }
238
+ const finalRoomId = roomId ?? this._generateRoomId();
239
+ const room = new def.roomClass();
240
+ room._init({
241
+ id: finalRoomId,
242
+ sendFn: this._sendFn,
243
+ sendBinaryFn: this._sendBinaryFn,
244
+ broadcastFn: /* @__PURE__ */ __name((type, data) => {
245
+ for (const player of room.players) {
246
+ player.send(type, data);
247
+ }
248
+ }, "broadcastFn"),
249
+ disposeFn: /* @__PURE__ */ __name(() => {
250
+ this._onRoomDisposed(finalRoomId);
251
+ }, "disposeFn")
252
+ });
253
+ this._rooms.set(finalRoomId, room);
254
+ await room._create(options);
255
+ return room;
256
+ }
257
+ /**
258
+ * @zh 房间销毁回调
259
+ * @en Room disposed callback
260
+ *
261
+ * @param roomId - 房间 ID | Room ID
262
+ */
263
+ _onRoomDisposed(roomId) {
264
+ this._rooms.delete(roomId);
265
+ }
266
+ /**
267
+ * @zh 玩家加入房间后的回调
268
+ * @en Callback after player joins room
269
+ *
270
+ * @param playerId - 玩家 ID | Player ID
271
+ * @param roomId - 房间 ID | Room ID
272
+ * @param player - 玩家实例 | Player instance
273
+ */
274
+ _onPlayerJoined(playerId, roomId, _player) {
275
+ this._playerToRoom.set(playerId, roomId);
276
+ }
277
+ /**
278
+ * @zh 玩家离开房间后的回调
279
+ * @en Callback after player leaves room
280
+ *
281
+ * @param playerId - 玩家 ID | Player ID
282
+ * @param _roomId - 房间 ID | Room ID
283
+ */
284
+ _onPlayerLeft(playerId, _roomId) {
285
+ this._playerToRoom.delete(playerId);
286
+ }
287
+ };
288
+ __name(_RoomManager, "RoomManager");
289
+ var RoomManager = _RoomManager;
290
+
291
+ // src/http/router.ts
292
+ var logger2 = createLogger("HTTP");
293
+ function parseRoutePath(path3) {
294
+ const paramNames = [];
295
+ const isStatic = !path3.includes(":");
296
+ if (isStatic) {
297
+ return {
298
+ pattern: new RegExp(`^${escapeRegex(path3)}$`),
299
+ paramNames,
300
+ isStatic: true
301
+ };
302
+ }
303
+ const segments = path3.split("/").map((segment) => {
304
+ if (segment.startsWith(":")) {
305
+ const paramName = segment.slice(1);
306
+ paramNames.push(paramName);
307
+ return "([^/]+)";
308
+ }
309
+ return escapeRegex(segment);
310
+ });
311
+ return {
312
+ pattern: new RegExp(`^${segments.join("/")}$`),
313
+ paramNames,
314
+ isStatic: false
315
+ };
316
+ }
317
+ __name(parseRoutePath, "parseRoutePath");
318
+ function escapeRegex(str) {
319
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
320
+ }
321
+ __name(escapeRegex, "escapeRegex");
322
+ function matchRoute(routes, path3, method) {
323
+ for (const route of routes) {
324
+ if (!route.isStatic) continue;
325
+ if (route.method !== "*" && route.method !== method) continue;
326
+ if (route.pattern.test(path3)) {
327
+ return {
328
+ route,
329
+ params: {}
330
+ };
331
+ }
332
+ }
333
+ for (const route of routes) {
334
+ if (route.isStatic) continue;
335
+ if (route.method !== "*" && route.method !== method) continue;
336
+ const match = path3.match(route.pattern);
337
+ if (match) {
338
+ const params = {};
339
+ route.paramNames.forEach((name, index) => {
340
+ params[name] = decodeURIComponent(match[index + 1]);
341
+ });
342
+ return {
343
+ route,
344
+ params
345
+ };
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+ __name(matchRoute, "matchRoute");
351
+ async function createRequest(req, params = {}) {
352
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
353
+ const query = {};
354
+ url.searchParams.forEach((value, key) => {
355
+ query[key] = value;
356
+ });
357
+ let body = null;
358
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
359
+ body = await parseBody(req);
360
+ }
361
+ const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
362
+ return {
363
+ raw: req,
364
+ method: req.method ?? "GET",
365
+ path: url.pathname,
366
+ params,
367
+ query,
368
+ headers: req.headers,
369
+ body,
370
+ ip
371
+ };
372
+ }
373
+ __name(createRequest, "createRequest");
374
+ function parseBody(req) {
375
+ return new Promise((resolve2) => {
376
+ const chunks = [];
377
+ req.on("data", (chunk) => {
378
+ chunks.push(chunk);
379
+ });
380
+ req.on("end", () => {
381
+ const rawBody = Buffer.concat(chunks).toString("utf-8");
382
+ if (!rawBody) {
383
+ resolve2(null);
384
+ return;
385
+ }
386
+ const contentType = req.headers["content-type"] ?? "";
387
+ if (contentType.includes("application/json")) {
388
+ try {
389
+ resolve2(JSON.parse(rawBody));
390
+ } catch {
391
+ resolve2(rawBody);
392
+ }
393
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
394
+ const params = new URLSearchParams(rawBody);
395
+ const result = {};
396
+ params.forEach((value, key) => {
397
+ result[key] = value;
398
+ });
399
+ resolve2(result);
400
+ } else {
401
+ resolve2(rawBody);
402
+ }
403
+ });
404
+ req.on("error", () => {
405
+ resolve2(null);
406
+ });
407
+ });
408
+ }
409
+ __name(parseBody, "parseBody");
410
+ function createResponse(res) {
411
+ let statusCode = 200;
412
+ let ended = false;
413
+ const response = {
414
+ raw: res,
415
+ status(code) {
416
+ statusCode = code;
417
+ return response;
418
+ },
419
+ header(name, value) {
420
+ if (!ended) {
421
+ res.setHeader(name, value);
422
+ }
423
+ return response;
424
+ },
425
+ json(data) {
426
+ if (ended) return;
427
+ ended = true;
428
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
429
+ res.statusCode = statusCode;
430
+ res.end(JSON.stringify(data));
431
+ },
432
+ text(data) {
433
+ if (ended) return;
434
+ ended = true;
435
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
436
+ res.statusCode = statusCode;
437
+ res.end(data);
438
+ },
439
+ error(code, message) {
440
+ if (ended) return;
441
+ ended = true;
442
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
443
+ res.statusCode = code;
444
+ res.end(JSON.stringify({
445
+ error: message
446
+ }));
447
+ }
448
+ };
449
+ return response;
450
+ }
451
+ __name(createResponse, "createResponse");
452
+ function createOriginWhitelist(origins) {
453
+ const whitelist = {};
454
+ for (const origin of origins) {
455
+ whitelist[origin] = true;
456
+ }
457
+ return whitelist;
458
+ }
459
+ __name(createOriginWhitelist, "createOriginWhitelist");
460
+ function applyCors(res, req, cors) {
461
+ const credentials = cors.credentials ?? false;
462
+ if (typeof cors.origin === "string" && cors.origin !== "*") {
463
+ res.setHeader("Access-Control-Allow-Origin", cors.origin);
464
+ if (credentials) {
465
+ res.setHeader("Access-Control-Allow-Credentials", "true");
466
+ }
467
+ } else if (Array.isArray(cors.origin)) {
468
+ const requestOrigin = req.headers.origin;
469
+ if (typeof requestOrigin === "string") {
470
+ const whitelist = createOriginWhitelist(cors.origin);
471
+ if (requestOrigin in whitelist) {
472
+ res.setHeader("Access-Control-Allow-Origin", requestOrigin);
473
+ if (credentials) {
474
+ res.setHeader("Access-Control-Allow-Credentials", "true");
475
+ }
476
+ }
477
+ }
478
+ } else if (!credentials) {
479
+ if (cors.origin === "*" || cors.origin === true) {
480
+ res.setHeader("Access-Control-Allow-Origin", "*");
481
+ }
482
+ }
483
+ res.setHeader("Access-Control-Allow-Methods", cors.methods?.join(", ") ?? "GET, POST, PUT, DELETE, PATCH, OPTIONS");
484
+ res.setHeader("Access-Control-Allow-Headers", cors.allowedHeaders?.join(", ") ?? "Content-Type, Authorization");
485
+ if (cors.maxAge) {
486
+ res.setHeader("Access-Control-Max-Age", String(cors.maxAge));
487
+ }
488
+ }
489
+ __name(applyCors, "applyCors");
490
+ async function executeMiddlewares(middlewares, req, res, finalHandler) {
491
+ let index = 0;
492
+ const next = /* @__PURE__ */ __name(async () => {
493
+ if (index < middlewares.length) {
494
+ const middleware = middlewares[index++];
495
+ await middleware(req, res, next);
496
+ } else {
497
+ await finalHandler();
498
+ }
499
+ }, "next");
500
+ await next();
501
+ }
502
+ __name(executeMiddlewares, "executeMiddlewares");
503
+ async function executeWithTimeout(handler, timeoutMs, res) {
504
+ let resolved = false;
505
+ const timeoutPromise = new Promise((_, reject) => {
506
+ setTimeout(() => {
507
+ if (!resolved) {
508
+ reject(new Error("Request timeout"));
509
+ }
510
+ }, timeoutMs);
511
+ });
512
+ try {
513
+ await Promise.race([
514
+ handler().then(() => {
515
+ resolved = true;
516
+ }),
517
+ timeoutPromise
518
+ ]);
519
+ } catch (error) {
520
+ if (error instanceof Error && error.message === "Request timeout") {
521
+ if (!res.writableEnded) {
522
+ res.statusCode = 408;
523
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
524
+ res.end(JSON.stringify({
525
+ error: "Request Timeout"
526
+ }));
527
+ }
528
+ } else {
529
+ throw error;
530
+ }
531
+ }
532
+ }
533
+ __name(executeWithTimeout, "executeWithTimeout");
534
+ function isHandlerDefinition(value) {
535
+ return typeof value === "object" && value !== null && "handler" in value && typeof value.handler === "function";
536
+ }
537
+ __name(isHandlerDefinition, "isHandlerDefinition");
538
+ function isRouteMethods(value) {
539
+ if (typeof value !== "object" || value === null) return false;
540
+ const methods = [
541
+ "GET",
542
+ "POST",
543
+ "PUT",
544
+ "DELETE",
545
+ "PATCH",
546
+ "OPTIONS"
547
+ ];
548
+ return Object.keys(value).some((key) => methods.includes(key));
549
+ }
550
+ __name(isRouteMethods, "isRouteMethods");
551
+ function extractHandler(methodHandler) {
552
+ if (isHandlerDefinition(methodHandler)) {
553
+ return {
554
+ handler: methodHandler.handler,
555
+ middlewares: methodHandler.middlewares ?? [],
556
+ timeout: methodHandler.timeout
557
+ };
558
+ }
559
+ return {
560
+ handler: methodHandler,
561
+ middlewares: [],
562
+ timeout: void 0
563
+ };
564
+ }
565
+ __name(extractHandler, "extractHandler");
566
+ function createHttpRouter(routes, options = {}) {
567
+ const globalMiddlewares = options.middlewares ?? [];
568
+ const globalTimeout = options.timeout;
569
+ const parsedRoutes = [];
570
+ for (const [path3, handlerOrMethods] of Object.entries(routes)) {
571
+ const { pattern, paramNames, isStatic } = parseRoutePath(path3);
572
+ if (typeof handlerOrMethods === "function") {
573
+ parsedRoutes.push({
574
+ method: "*",
575
+ path: path3,
576
+ handler: handlerOrMethods,
577
+ pattern,
578
+ paramNames,
579
+ middlewares: [],
580
+ timeout: void 0,
581
+ isStatic
582
+ });
583
+ } else if (isRouteMethods(handlerOrMethods)) {
584
+ for (const [method, methodHandler] of Object.entries(handlerOrMethods)) {
585
+ if (methodHandler !== void 0) {
586
+ const { handler, middlewares, timeout } = extractHandler(methodHandler);
587
+ parsedRoutes.push({
588
+ method,
589
+ path: path3,
590
+ handler,
591
+ pattern,
592
+ paramNames,
593
+ middlewares,
594
+ timeout,
595
+ isStatic
596
+ });
597
+ }
598
+ }
599
+ } else if (isHandlerDefinition(handlerOrMethods)) {
600
+ const { handler, middlewares, timeout } = extractHandler(handlerOrMethods);
601
+ parsedRoutes.push({
602
+ method: "*",
603
+ path: path3,
604
+ handler,
605
+ pattern,
606
+ paramNames,
607
+ middlewares,
608
+ timeout,
609
+ isStatic
610
+ });
611
+ }
612
+ }
613
+ const corsOptions = options.cors === true ? {
614
+ origin: "*"
615
+ } : options.cors === false ? null : options.cors ?? null;
616
+ return /* @__PURE__ */ __name(async function handleRequest(req, res) {
617
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
618
+ const path3 = url.pathname;
619
+ const method = req.method ?? "GET";
620
+ if (corsOptions) {
621
+ applyCors(res, req, corsOptions);
622
+ if (method === "OPTIONS") {
623
+ res.statusCode = 204;
624
+ res.end();
625
+ return true;
626
+ }
627
+ }
628
+ const match = matchRoute(parsedRoutes, path3, method);
629
+ if (!match) {
630
+ return false;
631
+ }
632
+ const { route, params } = match;
633
+ try {
634
+ const httpReq = await createRequest(req, params);
635
+ const httpRes = createResponse(res);
636
+ const allMiddlewares = [
637
+ ...globalMiddlewares,
638
+ ...route.middlewares
639
+ ];
640
+ const timeout = route.timeout ?? globalTimeout;
641
+ const finalHandler = /* @__PURE__ */ __name(async () => {
642
+ await route.handler(httpReq, httpRes);
643
+ }, "finalHandler");
644
+ const executeHandler = /* @__PURE__ */ __name(async () => {
645
+ if (allMiddlewares.length > 0) {
646
+ await executeMiddlewares(allMiddlewares, httpReq, httpRes, finalHandler);
647
+ } else {
648
+ await finalHandler();
649
+ }
650
+ }, "executeHandler");
651
+ if (timeout && timeout > 0) {
652
+ await executeWithTimeout(executeHandler, timeout, res);
653
+ } else {
654
+ await executeHandler();
655
+ }
656
+ return true;
657
+ } catch (error) {
658
+ logger2.error("Route handler error:", error);
659
+ if (!res.writableEnded) {
660
+ res.statusCode = 500;
661
+ res.setHeader("Content-Type", "application/json");
662
+ res.end(JSON.stringify({
663
+ error: "Internal Server Error"
664
+ }));
665
+ }
666
+ return true;
667
+ }
668
+ }, "handleRequest");
669
+ }
670
+ __name(createHttpRouter, "createHttpRouter");
671
+
672
+ // src/distributed/DistributedRoomManager.ts
673
+ var logger3 = createLogger("DistributedRoom");
674
+ var _DistributedRoomManager = class _DistributedRoomManager extends RoomManager {
675
+ /**
676
+ * @zh 创建分布式房间管理器
677
+ * @en Create distributed room manager
678
+ *
679
+ * @param adapter - 分布式适配器 | Distributed adapter
680
+ * @param config - 配置 | Configuration
681
+ * @param sendFn - 消息发送函数 | Message send function
682
+ * @param sendBinaryFn - 二进制发送函数 | Binary send function
683
+ */
684
+ constructor(adapter, config, sendFn, sendBinaryFn) {
685
+ super(sendFn, sendBinaryFn);
686
+ __publicField(this, "_adapter");
687
+ __publicField(this, "_config");
688
+ __publicField(this, "_serverId");
689
+ __publicField(this, "_heartbeatTimer", null);
690
+ __publicField(this, "_snapshotTimer", null);
691
+ __publicField(this, "_subscriptions", []);
692
+ __publicField(this, "_isShuttingDown", false);
693
+ this._adapter = adapter;
694
+ this._serverId = config.serverId;
695
+ this._config = {
696
+ serverId: config.serverId,
697
+ serverAddress: config.serverAddress,
698
+ serverPort: config.serverPort,
699
+ heartbeatInterval: config.heartbeatInterval ?? 5e3,
700
+ snapshotInterval: config.snapshotInterval ?? 3e4,
701
+ migrationTimeout: config.migrationTimeout ?? 1e4,
702
+ enableFailover: config.enableFailover ?? true,
703
+ capacity: config.capacity ?? 100,
704
+ metadata: config.metadata ?? {}
705
+ };
706
+ }
707
+ /**
708
+ * @zh 获取服务器 ID
709
+ * @en Get server ID
710
+ */
711
+ get serverId() {
712
+ return this._serverId;
713
+ }
714
+ /**
715
+ * @zh 获取分布式适配器
716
+ * @en Get distributed adapter
717
+ */
718
+ get adapter() {
719
+ return this._adapter;
720
+ }
721
+ /**
722
+ * @zh 获取配置
723
+ * @en Get configuration
724
+ */
725
+ get config() {
726
+ return this._config;
727
+ }
728
+ // =========================================================================
729
+ // 生命周期 | Lifecycle
730
+ // =========================================================================
731
+ /**
732
+ * @zh 启动分布式房间管理器
733
+ * @en Start distributed room manager
734
+ */
735
+ async start() {
736
+ if (!this._adapter.isConnected()) {
737
+ await this._adapter.connect();
738
+ }
739
+ await this._registerServer();
740
+ await this._subscribeToEvents();
741
+ this._startHeartbeat();
742
+ if (this._config.snapshotInterval > 0) {
743
+ this._startSnapshotTimer();
744
+ }
745
+ logger3.info(`Distributed room manager started: ${this._serverId}`);
746
+ }
747
+ /**
748
+ * @zh 停止分布式房间管理器
749
+ * @en Stop distributed room manager
750
+ *
751
+ * @param graceful - 是否优雅关闭(等待玩家退出)| Whether to gracefully shutdown (wait for players)
752
+ */
753
+ async stop(graceful = true) {
754
+ this._isShuttingDown = true;
755
+ if (this._heartbeatTimer) {
756
+ clearInterval(this._heartbeatTimer);
757
+ this._heartbeatTimer = null;
758
+ }
759
+ if (this._snapshotTimer) {
760
+ clearInterval(this._snapshotTimer);
761
+ this._snapshotTimer = null;
762
+ }
763
+ for (const unsub of this._subscriptions) {
764
+ unsub();
765
+ }
766
+ this._subscriptions = [];
767
+ if (graceful) {
768
+ await this._adapter.updateServer(this._serverId, {
769
+ status: "draining"
770
+ });
771
+ await this._saveAllSnapshots();
772
+ }
773
+ await this._adapter.unregisterServer(this._serverId);
774
+ logger3.info(`Distributed room manager stopped: ${this._serverId}`);
775
+ }
776
+ // =========================================================================
777
+ // 房间操作覆盖 | Room Operation Overrides
778
+ // =========================================================================
779
+ /**
780
+ * @zh 房间创建后注册到分布式系统
781
+ * @en Register room to distributed system after creation
782
+ */
783
+ async _onRoomCreated(name, room) {
784
+ const registration = {
785
+ roomId: room.id,
786
+ roomType: name,
787
+ serverId: this._serverId,
788
+ serverAddress: `${this._config.serverAddress}:${this._config.serverPort}`,
789
+ playerCount: room.players.length,
790
+ maxPlayers: room.maxPlayers,
791
+ isLocked: room.isLocked,
792
+ metadata: {},
793
+ createdAt: Date.now(),
794
+ updatedAt: Date.now()
795
+ };
796
+ await this._adapter.registerRoom(registration);
797
+ logger3.debug(`Registered room: ${room.id}`);
798
+ }
799
+ /**
800
+ * @zh 房间销毁时从分布式系统注销
801
+ * @en Unregister room from distributed system when disposed
802
+ */
803
+ _onRoomDisposed(roomId) {
804
+ super._onRoomDisposed(roomId);
805
+ this._adapter.unregisterRoom(roomId).catch((err) => {
806
+ logger3.error(`Failed to unregister room ${roomId}:`, err);
807
+ });
808
+ this._adapter.deleteSnapshot(roomId).catch((err) => {
809
+ logger3.error(`Failed to delete snapshot for ${roomId}:`, err);
810
+ });
811
+ }
812
+ /**
813
+ * @zh 玩家加入后更新分布式房间信息
814
+ * @en Update distributed room info after player joins
815
+ */
816
+ _onPlayerJoined(playerId, roomId, player) {
817
+ super._onPlayerJoined(playerId, roomId, player);
818
+ const room = this._rooms.get(roomId);
819
+ if (room) {
820
+ this._adapter.updateRoom(roomId, {
821
+ playerCount: room.players.length,
822
+ updatedAt: Date.now()
823
+ }).catch((err) => {
824
+ logger3.error(`Failed to update room ${roomId}:`, err);
825
+ });
826
+ }
827
+ }
828
+ /**
829
+ * @zh 玩家离开后更新分布式房间信息
830
+ * @en Update distributed room info after player leaves
831
+ */
832
+ _onPlayerLeft(playerId, roomId) {
833
+ super._onPlayerLeft(playerId, roomId);
834
+ const room = this._rooms.get(roomId);
835
+ if (room) {
836
+ this._adapter.updateRoom(roomId, {
837
+ playerCount: room.players.length,
838
+ updatedAt: Date.now()
839
+ }).catch((err) => {
840
+ logger3.error(`Failed to update room ${roomId}:`, err);
841
+ });
842
+ }
843
+ }
844
+ // =========================================================================
845
+ // 分布式路由 | Distributed Routing
846
+ // =========================================================================
847
+ /**
848
+ * @zh 路由玩家到合适的房间/服务器
849
+ * @en Route player to appropriate room/server
850
+ *
851
+ * @param request - 路由请求 | Routing request
852
+ * @returns 路由结果 | Routing result
853
+ */
854
+ async route(request) {
855
+ if (request.roomId) {
856
+ return this._routeToRoom(request.roomId);
857
+ }
858
+ if (request.roomType) {
859
+ return this._routeByType(request.roomType, request.query);
860
+ }
861
+ return {
862
+ type: "unavailable",
863
+ reason: "No room type or room ID specified"
864
+ };
865
+ }
866
+ /**
867
+ * @zh 加入或创建房间(分布式版本)
868
+ * @en Join or create room (distributed version)
869
+ *
870
+ * @zh 此方法会:
871
+ * 1. 先在分布式注册表中查找可用房间
872
+ * 2. 如果找到其他服务器的房间,返回重定向
873
+ * 3. 如果找到本地房间或需要创建,在本地处理
874
+ * @en This method will:
875
+ * 1. First search for available room in distributed registry
876
+ * 2. If room found on another server, return redirect
877
+ * 3. If local room found or creation needed, handle locally
878
+ */
879
+ async joinOrCreateDistributed(name, playerId, conn, options) {
880
+ const lockKey = `joinOrCreate:${name}`;
881
+ const locked = await this._adapter.acquireLock(lockKey, 5e3);
882
+ if (!locked) {
883
+ await this._sleep(100);
884
+ return this.joinOrCreateDistributed(name, playerId, conn, options);
885
+ }
886
+ try {
887
+ const availableRoom = await this._adapter.findAvailableRoom(name);
888
+ if (availableRoom) {
889
+ if (availableRoom.serverId === this._serverId) {
890
+ return super.joinOrCreate(name, playerId, conn, options);
891
+ } else {
892
+ return {
893
+ redirect: availableRoom.serverAddress
894
+ };
895
+ }
896
+ }
897
+ return super.joinOrCreate(name, playerId, conn, options);
898
+ } finally {
899
+ await this._adapter.releaseLock(lockKey);
900
+ }
901
+ }
902
+ // =========================================================================
903
+ // 状态管理 | State Management
904
+ // =========================================================================
905
+ /**
906
+ * @zh 保存房间状态快照
907
+ * @en Save room state snapshot
908
+ *
909
+ * @param roomId - 房间 ID | Room ID
910
+ */
911
+ async saveSnapshot(roomId) {
912
+ const room = this._rooms.get(roomId);
913
+ if (!room) return;
914
+ const def = this._getDefinitionByRoom(room);
915
+ if (!def) return;
916
+ const snapshot = {
917
+ roomId: room.id,
918
+ roomType: def.name,
919
+ state: room.state ?? {},
920
+ players: room.players.map((p) => ({
921
+ id: p.id,
922
+ data: p.data ?? {}
923
+ })),
924
+ version: Date.now(),
925
+ timestamp: Date.now()
926
+ };
927
+ await this._adapter.saveSnapshot(snapshot);
928
+ logger3.debug(`Saved snapshot for room: ${roomId}`);
929
+ }
930
+ /**
931
+ * @zh 从快照恢复房间
932
+ * @en Restore room from snapshot
933
+ *
934
+ * @param roomId - 房间 ID | Room ID
935
+ * @returns 是否成功恢复 | Whether restore was successful
936
+ */
937
+ async restoreFromSnapshot(roomId) {
938
+ const snapshot = await this._adapter.loadSnapshot(roomId);
939
+ if (!snapshot) return false;
940
+ const room = await this._createRoomInstance(snapshot.roomType, {
941
+ state: snapshot.state
942
+ }, snapshot.roomId);
943
+ if (!room) return false;
944
+ await this._onRoomCreated(snapshot.roomType, room);
945
+ logger3.info(`Restored room from snapshot: ${roomId}`);
946
+ return true;
947
+ }
948
+ // =========================================================================
949
+ // 私有方法 | Private Methods
950
+ // =========================================================================
951
+ /**
952
+ * @zh 注册服务器到分布式系统
953
+ * @en Register server to distributed system
954
+ */
955
+ async _registerServer() {
956
+ const registration = {
957
+ serverId: this._serverId,
958
+ address: this._config.serverAddress,
959
+ port: this._config.serverPort,
960
+ roomCount: this._rooms.size,
961
+ playerCount: this._countTotalPlayers(),
962
+ capacity: this._config.capacity,
963
+ status: "online",
964
+ lastHeartbeat: Date.now(),
965
+ metadata: this._config.metadata
966
+ };
967
+ await this._adapter.registerServer(registration);
968
+ }
969
+ /**
970
+ * @zh 订阅分布式事件
971
+ * @en Subscribe to distributed events
972
+ */
973
+ async _subscribeToEvents() {
974
+ if (this._config.enableFailover) {
975
+ const unsub = await this._adapter.subscribe("server:offline", (event) => {
976
+ this._handleServerOffline(event);
977
+ });
978
+ this._subscriptions.push(unsub);
979
+ }
980
+ const roomMsgUnsub = await this._adapter.subscribe("room:message", (event) => {
981
+ this._handleRoomMessage(event);
982
+ });
983
+ this._subscriptions.push(roomMsgUnsub);
984
+ }
985
+ /**
986
+ * @zh 启动心跳定时器
987
+ * @en Start heartbeat timer
988
+ */
989
+ _startHeartbeat() {
990
+ this._heartbeatTimer = setInterval(async () => {
991
+ try {
992
+ await this._adapter.heartbeat(this._serverId);
993
+ await this._adapter.updateServer(this._serverId, {
994
+ roomCount: this._rooms.size,
995
+ playerCount: this._countTotalPlayers()
996
+ });
997
+ } catch (err) {
998
+ logger3.error("Heartbeat failed:", err);
999
+ }
1000
+ }, this._config.heartbeatInterval);
1001
+ }
1002
+ /**
1003
+ * @zh 启动快照定时器
1004
+ * @en Start snapshot timer
1005
+ */
1006
+ _startSnapshotTimer() {
1007
+ this._snapshotTimer = setInterval(async () => {
1008
+ await this._saveAllSnapshots();
1009
+ }, this._config.snapshotInterval);
1010
+ }
1011
+ /**
1012
+ * @zh 保存所有房间快照
1013
+ * @en Save all room snapshots
1014
+ */
1015
+ async _saveAllSnapshots() {
1016
+ const promises = [];
1017
+ for (const roomId of this._rooms.keys()) {
1018
+ promises.push(this.saveSnapshot(roomId));
1019
+ }
1020
+ await Promise.allSettled(promises);
1021
+ }
1022
+ /**
1023
+ * @zh 路由到指定房间
1024
+ * @en Route to specific room
1025
+ */
1026
+ async _routeToRoom(roomId) {
1027
+ if (this._rooms.has(roomId)) {
1028
+ return {
1029
+ type: "local",
1030
+ roomId
1031
+ };
1032
+ }
1033
+ const registration = await this._adapter.getRoom(roomId);
1034
+ if (!registration) {
1035
+ return {
1036
+ type: "unavailable",
1037
+ reason: "Room not found"
1038
+ };
1039
+ }
1040
+ if (registration.serverId === this._serverId) {
1041
+ return {
1042
+ type: "local",
1043
+ roomId
1044
+ };
1045
+ }
1046
+ return {
1047
+ type: "redirect",
1048
+ serverAddress: registration.serverAddress,
1049
+ roomId
1050
+ };
1051
+ }
1052
+ /**
1053
+ * @zh 按类型路由
1054
+ * @en Route by type
1055
+ */
1056
+ async _routeByType(roomType, _query) {
1057
+ const availableRoom = await this._adapter.findAvailableRoom(roomType);
1058
+ if (!availableRoom) {
1059
+ return {
1060
+ type: "create",
1061
+ roomId: void 0
1062
+ };
1063
+ }
1064
+ if (availableRoom.serverId === this._serverId) {
1065
+ return {
1066
+ type: "local",
1067
+ roomId: availableRoom.roomId
1068
+ };
1069
+ }
1070
+ return {
1071
+ type: "redirect",
1072
+ serverAddress: availableRoom.serverAddress,
1073
+ roomId: availableRoom.roomId
1074
+ };
1075
+ }
1076
+ /**
1077
+ * @zh 处理服务器离线事件
1078
+ * @en Handle server offline event
1079
+ */
1080
+ _handleServerOffline(event) {
1081
+ if (this._isShuttingDown) return;
1082
+ if (!this._config.enableFailover) return;
1083
+ const offlineServerId = event.serverId;
1084
+ if (offlineServerId === this._serverId) return;
1085
+ logger3.info(`Server offline detected: ${offlineServerId}`);
1086
+ this._tryRecoverRoomsFromServer(offlineServerId).catch((err) => {
1087
+ logger3.error(`Failed to recover rooms from ${offlineServerId}:`, err);
1088
+ });
1089
+ }
1090
+ /**
1091
+ * @zh 尝试从离线服务器恢复房间
1092
+ * @en Try to recover rooms from offline server
1093
+ */
1094
+ async _tryRecoverRoomsFromServer(offlineServerId) {
1095
+ if (this._rooms.size >= this._config.capacity) {
1096
+ logger3.warn(`Cannot recover rooms: server at capacity (${this._rooms.size}/${this._config.capacity})`);
1097
+ return;
1098
+ }
1099
+ const rooms = await this._adapter.queryRooms({
1100
+ serverId: offlineServerId
1101
+ });
1102
+ if (rooms.length === 0) {
1103
+ logger3.info(`No rooms to recover from ${offlineServerId}`);
1104
+ return;
1105
+ }
1106
+ logger3.info(`Attempting to recover ${rooms.length} rooms from ${offlineServerId}`);
1107
+ for (const roomReg of rooms) {
1108
+ if (this._rooms.size >= this._config.capacity) {
1109
+ logger3.warn("Reached capacity during recovery, stopping");
1110
+ break;
1111
+ }
1112
+ const lockKey = `failover:${roomReg.roomId}`;
1113
+ const acquired = await this._adapter.acquireLock(lockKey, this._config.migrationTimeout);
1114
+ if (!acquired) {
1115
+ continue;
1116
+ }
1117
+ try {
1118
+ const success = await this.restoreFromSnapshot(roomReg.roomId);
1119
+ if (success) {
1120
+ logger3.info(`Successfully recovered room ${roomReg.roomId}`);
1121
+ await this._adapter.publish({
1122
+ type: "room:migrated",
1123
+ serverId: this._serverId,
1124
+ roomId: roomReg.roomId,
1125
+ payload: {
1126
+ fromServer: offlineServerId,
1127
+ toServer: this._serverId
1128
+ },
1129
+ timestamp: Date.now()
1130
+ });
1131
+ }
1132
+ } finally {
1133
+ await this._adapter.releaseLock(lockKey);
1134
+ }
1135
+ }
1136
+ }
1137
+ /**
1138
+ * @zh 处理跨服务器房间消息
1139
+ * @en Handle cross-server room message
1140
+ */
1141
+ _handleRoomMessage(event) {
1142
+ if (!event.roomId) return;
1143
+ const room = this._rooms.get(event.roomId);
1144
+ if (!room) return;
1145
+ const payload = event.payload;
1146
+ if (payload.playerId) {
1147
+ room._handleMessage(payload.messageType, payload.data, payload.playerId);
1148
+ }
1149
+ }
1150
+ /**
1151
+ * @zh 统计总玩家数
1152
+ * @en Count total players
1153
+ */
1154
+ _countTotalPlayers() {
1155
+ let count = 0;
1156
+ for (const room of this._rooms.values()) {
1157
+ count += room.players.length;
1158
+ }
1159
+ return count;
1160
+ }
1161
+ /**
1162
+ * @zh 根据房间实例获取定义
1163
+ * @en Get definition by room instance
1164
+ */
1165
+ _getDefinitionByRoom(room) {
1166
+ for (const [name, def] of this._definitions) {
1167
+ if (room instanceof def.roomClass) {
1168
+ return {
1169
+ name
1170
+ };
1171
+ }
1172
+ }
1173
+ return null;
1174
+ }
1175
+ /**
1176
+ * @zh 休眠指定时间
1177
+ * @en Sleep for specified time
1178
+ */
1179
+ _sleep(ms) {
1180
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1181
+ }
1182
+ /**
1183
+ * @zh 向其他服务器的房间发送消息
1184
+ * @en Send message to room on another server
1185
+ *
1186
+ * @param roomId - 房间 ID | Room ID
1187
+ * @param messageType - 消息类型 | Message type
1188
+ * @param data - 消息数据 | Message data
1189
+ * @param playerId - 发送者玩家 ID(可选)| Sender player ID (optional)
1190
+ */
1191
+ async sendToRemoteRoom(roomId, messageType, data, playerId) {
1192
+ await this._adapter.sendToRoom(roomId, messageType, data, playerId);
1193
+ }
1194
+ /**
1195
+ * @zh 获取所有在线服务器
1196
+ * @en Get all online servers
1197
+ */
1198
+ async getServers() {
1199
+ return this._adapter.getServers();
1200
+ }
1201
+ /**
1202
+ * @zh 查询分布式房间
1203
+ * @en Query distributed rooms
1204
+ */
1205
+ async queryDistributedRooms(query) {
1206
+ return this._adapter.queryRooms(query);
1207
+ }
1208
+ };
1209
+ __name(_DistributedRoomManager, "DistributedRoomManager");
1210
+ var DistributedRoomManager = _DistributedRoomManager;
1211
+
1212
+ // src/distributed/adapters/MemoryAdapter.ts
1213
+ var _MemoryAdapter = class _MemoryAdapter {
1214
+ constructor(config = {}) {
1215
+ __publicField(this, "_config");
1216
+ __publicField(this, "_connected", false);
1217
+ // 存储
1218
+ __publicField(this, "_servers", /* @__PURE__ */ new Map());
1219
+ __publicField(this, "_rooms", /* @__PURE__ */ new Map());
1220
+ __publicField(this, "_snapshots", /* @__PURE__ */ new Map());
1221
+ __publicField(this, "_locks", /* @__PURE__ */ new Map());
1222
+ // 事件订阅
1223
+ __publicField(this, "_subscribers", /* @__PURE__ */ new Map());
1224
+ __publicField(this, "_subscriberId", 0);
1225
+ // TTL 检查定时器
1226
+ __publicField(this, "_ttlCheckTimer", null);
1227
+ this._config = {
1228
+ serverTtl: 15e3,
1229
+ enableTtlCheck: true,
1230
+ ttlCheckInterval: 5e3,
1231
+ ...config
1232
+ };
1233
+ }
1234
+ // =========================================================================
1235
+ // 生命周期 | Lifecycle
1236
+ // =========================================================================
1237
+ async connect() {
1238
+ if (this._connected) return;
1239
+ this._connected = true;
1240
+ if (this._config.enableTtlCheck) {
1241
+ this._ttlCheckTimer = setInterval(() => this._checkServerTtl(), this._config.ttlCheckInterval);
1242
+ }
1243
+ }
1244
+ async disconnect() {
1245
+ if (!this._connected) return;
1246
+ if (this._ttlCheckTimer) {
1247
+ clearInterval(this._ttlCheckTimer);
1248
+ this._ttlCheckTimer = null;
1249
+ }
1250
+ this._connected = false;
1251
+ this._servers.clear();
1252
+ this._rooms.clear();
1253
+ this._snapshots.clear();
1254
+ this._locks.clear();
1255
+ this._subscribers.clear();
1256
+ }
1257
+ isConnected() {
1258
+ return this._connected;
1259
+ }
1260
+ // =========================================================================
1261
+ // 服务器注册 | Server Registry
1262
+ // =========================================================================
1263
+ async registerServer(server) {
1264
+ this._ensureConnected();
1265
+ this._servers.set(server.serverId, {
1266
+ ...server,
1267
+ lastHeartbeat: Date.now()
1268
+ });
1269
+ await this.publish({
1270
+ type: "server:online",
1271
+ serverId: server.serverId,
1272
+ payload: server,
1273
+ timestamp: Date.now()
1274
+ });
1275
+ }
1276
+ async unregisterServer(serverId) {
1277
+ this._ensureConnected();
1278
+ const server = this._servers.get(serverId);
1279
+ if (!server) return;
1280
+ this._servers.delete(serverId);
1281
+ for (const [roomId, room] of this._rooms) {
1282
+ if (room.serverId === serverId) {
1283
+ this._rooms.delete(roomId);
1284
+ }
1285
+ }
1286
+ await this.publish({
1287
+ type: "server:offline",
1288
+ serverId,
1289
+ payload: {
1290
+ serverId
1291
+ },
1292
+ timestamp: Date.now()
1293
+ });
1294
+ }
1295
+ async heartbeat(serverId) {
1296
+ this._ensureConnected();
1297
+ const server = this._servers.get(serverId);
1298
+ if (server) {
1299
+ server.lastHeartbeat = Date.now();
1300
+ }
1301
+ }
1302
+ async getServers() {
1303
+ this._ensureConnected();
1304
+ return Array.from(this._servers.values()).filter((s) => s.status === "online");
1305
+ }
1306
+ async getServer(serverId) {
1307
+ this._ensureConnected();
1308
+ return this._servers.get(serverId) ?? null;
1309
+ }
1310
+ async updateServer(serverId, updates) {
1311
+ this._ensureConnected();
1312
+ const server = this._servers.get(serverId);
1313
+ if (server) {
1314
+ Object.assign(server, updates);
1315
+ }
1316
+ }
1317
+ // =========================================================================
1318
+ // 房间注册 | Room Registry
1319
+ // =========================================================================
1320
+ async registerRoom(room) {
1321
+ this._ensureConnected();
1322
+ this._rooms.set(room.roomId, {
1323
+ ...room
1324
+ });
1325
+ const server = this._servers.get(room.serverId);
1326
+ if (server) {
1327
+ server.roomCount = this._countRoomsByServer(room.serverId);
1328
+ }
1329
+ await this.publish({
1330
+ type: "room:created",
1331
+ serverId: room.serverId,
1332
+ roomId: room.roomId,
1333
+ payload: {
1334
+ roomType: room.roomType
1335
+ },
1336
+ timestamp: Date.now()
1337
+ });
1338
+ }
1339
+ async unregisterRoom(roomId) {
1340
+ this._ensureConnected();
1341
+ const room = this._rooms.get(roomId);
1342
+ if (!room) return;
1343
+ this._rooms.delete(roomId);
1344
+ this._snapshots.delete(roomId);
1345
+ const server = this._servers.get(room.serverId);
1346
+ if (server) {
1347
+ server.roomCount = this._countRoomsByServer(room.serverId);
1348
+ }
1349
+ await this.publish({
1350
+ type: "room:disposed",
1351
+ serverId: room.serverId,
1352
+ roomId,
1353
+ payload: {},
1354
+ timestamp: Date.now()
1355
+ });
1356
+ }
1357
+ async updateRoom(roomId, updates) {
1358
+ this._ensureConnected();
1359
+ const room = this._rooms.get(roomId);
1360
+ if (!room) return;
1361
+ Object.assign(room, updates, {
1362
+ updatedAt: Date.now()
1363
+ });
1364
+ await this.publish({
1365
+ type: "room:updated",
1366
+ serverId: room.serverId,
1367
+ roomId,
1368
+ payload: updates,
1369
+ timestamp: Date.now()
1370
+ });
1371
+ }
1372
+ async getRoom(roomId) {
1373
+ this._ensureConnected();
1374
+ return this._rooms.get(roomId) ?? null;
1375
+ }
1376
+ async queryRooms(query) {
1377
+ this._ensureConnected();
1378
+ let results = Array.from(this._rooms.values());
1379
+ if (query.roomType) {
1380
+ results = results.filter((r) => r.roomType === query.roomType);
1381
+ }
1382
+ if (query.hasSpace) {
1383
+ results = results.filter((r) => r.playerCount < r.maxPlayers);
1384
+ }
1385
+ if (query.notLocked) {
1386
+ results = results.filter((r) => !r.isLocked);
1387
+ }
1388
+ if (query.metadata) {
1389
+ results = results.filter((r) => {
1390
+ for (const [key, value] of Object.entries(query.metadata)) {
1391
+ if (r.metadata[key] !== value) {
1392
+ return false;
1393
+ }
1394
+ }
1395
+ return true;
1396
+ });
1397
+ }
1398
+ if (query.offset) {
1399
+ results = results.slice(query.offset);
1400
+ }
1401
+ if (query.limit) {
1402
+ results = results.slice(0, query.limit);
1403
+ }
1404
+ return results;
1405
+ }
1406
+ async findAvailableRoom(roomType) {
1407
+ const rooms = await this.queryRooms({
1408
+ roomType,
1409
+ hasSpace: true,
1410
+ notLocked: true,
1411
+ limit: 1
1412
+ });
1413
+ return rooms[0] ?? null;
1414
+ }
1415
+ async getRoomsByServer(serverId) {
1416
+ this._ensureConnected();
1417
+ return Array.from(this._rooms.values()).filter((r) => r.serverId === serverId);
1418
+ }
1419
+ // =========================================================================
1420
+ // 房间状态 | Room State
1421
+ // =========================================================================
1422
+ async saveSnapshot(snapshot) {
1423
+ this._ensureConnected();
1424
+ this._snapshots.set(snapshot.roomId, {
1425
+ ...snapshot
1426
+ });
1427
+ }
1428
+ async loadSnapshot(roomId) {
1429
+ this._ensureConnected();
1430
+ return this._snapshots.get(roomId) ?? null;
1431
+ }
1432
+ async deleteSnapshot(roomId) {
1433
+ this._ensureConnected();
1434
+ this._snapshots.delete(roomId);
1435
+ }
1436
+ // =========================================================================
1437
+ // 发布/订阅 | Pub/Sub
1438
+ // =========================================================================
1439
+ async publish(event) {
1440
+ this._ensureConnected();
1441
+ const wildcardHandlers = this._subscribers.get("*") ?? /* @__PURE__ */ new Set();
1442
+ const typeHandlers = this._subscribers.get(event.type) ?? /* @__PURE__ */ new Set();
1443
+ for (const handler of wildcardHandlers) {
1444
+ try {
1445
+ handler(event);
1446
+ } catch (error) {
1447
+ console.error("Event handler error:", error);
1448
+ }
1449
+ }
1450
+ for (const handler of typeHandlers) {
1451
+ try {
1452
+ handler(event);
1453
+ } catch (error) {
1454
+ console.error("Event handler error:", error);
1455
+ }
1456
+ }
1457
+ }
1458
+ async subscribe(pattern, handler) {
1459
+ this._ensureConnected();
1460
+ if (!this._subscribers.has(pattern)) {
1461
+ this._subscribers.set(pattern, /* @__PURE__ */ new Set());
1462
+ }
1463
+ this._subscribers.get(pattern).add(handler);
1464
+ return () => {
1465
+ const handlers = this._subscribers.get(pattern);
1466
+ if (handlers) {
1467
+ handlers.delete(handler);
1468
+ if (handlers.size === 0) {
1469
+ this._subscribers.delete(pattern);
1470
+ }
1471
+ }
1472
+ };
1473
+ }
1474
+ async sendToRoom(roomId, messageType, data, playerId) {
1475
+ this._ensureConnected();
1476
+ const room = this._rooms.get(roomId);
1477
+ if (!room) return;
1478
+ await this.publish({
1479
+ type: "room:message",
1480
+ serverId: room.serverId,
1481
+ roomId,
1482
+ payload: {
1483
+ messageType,
1484
+ data,
1485
+ playerId
1486
+ },
1487
+ timestamp: Date.now()
1488
+ });
1489
+ }
1490
+ // =========================================================================
1491
+ // 分布式锁 | Distributed Lock
1492
+ // =========================================================================
1493
+ async acquireLock(key, ttlMs) {
1494
+ this._ensureConnected();
1495
+ const now = Date.now();
1496
+ const existing = this._locks.get(key);
1497
+ if (existing && existing.expireAt > now) {
1498
+ return false;
1499
+ }
1500
+ const owner = `lock_${++this._subscriberId}`;
1501
+ this._locks.set(key, {
1502
+ owner,
1503
+ expireAt: now + ttlMs
1504
+ });
1505
+ return true;
1506
+ }
1507
+ async releaseLock(key) {
1508
+ this._ensureConnected();
1509
+ this._locks.delete(key);
1510
+ }
1511
+ async extendLock(key, ttlMs) {
1512
+ this._ensureConnected();
1513
+ const lock = this._locks.get(key);
1514
+ if (!lock) return false;
1515
+ lock.expireAt = Date.now() + ttlMs;
1516
+ return true;
1517
+ }
1518
+ // =========================================================================
1519
+ // 私有方法 | Private Methods
1520
+ // =========================================================================
1521
+ _ensureConnected() {
1522
+ if (!this._connected) {
1523
+ throw new Error("MemoryAdapter is not connected");
1524
+ }
1525
+ }
1526
+ _countRoomsByServer(serverId) {
1527
+ let count = 0;
1528
+ for (const room of this._rooms.values()) {
1529
+ if (room.serverId === serverId) {
1530
+ count++;
1531
+ }
1532
+ }
1533
+ return count;
1534
+ }
1535
+ async _checkServerTtl() {
1536
+ const now = Date.now();
1537
+ const expiredServers = [];
1538
+ for (const [serverId, server] of this._servers) {
1539
+ if (server.status === "online" && now - server.lastHeartbeat > this._config.serverTtl) {
1540
+ server.status = "offline";
1541
+ expiredServers.push(serverId);
1542
+ }
1543
+ }
1544
+ for (const serverId of expiredServers) {
1545
+ await this.publish({
1546
+ type: "server:offline",
1547
+ serverId,
1548
+ payload: {
1549
+ serverId,
1550
+ reason: "heartbeat_timeout"
1551
+ },
1552
+ timestamp: now
1553
+ });
1554
+ }
1555
+ }
1556
+ // =========================================================================
1557
+ // 测试辅助方法 | Test Helper Methods
1558
+ // =========================================================================
1559
+ /**
1560
+ * @zh 清除所有数据(仅用于测试)
1561
+ * @en Clear all data (for testing only)
1562
+ */
1563
+ _clear() {
1564
+ this._servers.clear();
1565
+ this._rooms.clear();
1566
+ this._snapshots.clear();
1567
+ this._locks.clear();
1568
+ }
1569
+ /**
1570
+ * @zh 获取内部状态(仅用于测试)
1571
+ * @en Get internal state (for testing only)
1572
+ */
1573
+ _getState() {
1574
+ return {
1575
+ servers: this._servers,
1576
+ rooms: this._rooms,
1577
+ snapshots: this._snapshots
1578
+ };
1579
+ }
1580
+ };
1581
+ __name(_MemoryAdapter, "MemoryAdapter");
1582
+ var MemoryAdapter = _MemoryAdapter;
1583
+ var logger4 = createLogger("Server");
1584
+ function fileNameToHandlerName(fileName) {
1585
+ const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, "");
1586
+ return baseName.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1587
+ }
1588
+ __name(fileNameToHandlerName, "fileNameToHandlerName");
1589
+ function scanDirectory(dir) {
1590
+ if (!fs.existsSync(dir)) {
1591
+ return [];
1592
+ }
1593
+ const files = [];
1594
+ const entries = fs.readdirSync(dir, {
1595
+ withFileTypes: true
1596
+ });
1597
+ for (const entry of entries) {
1598
+ if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
1599
+ if (entry.name.startsWith("_") || entry.name.startsWith("index.")) {
1600
+ continue;
1601
+ }
1602
+ files.push(path.join(dir, entry.name));
1603
+ }
1604
+ }
1605
+ return files;
1606
+ }
1607
+ __name(scanDirectory, "scanDirectory");
1608
+ async function loadApiHandlers(apiDir) {
1609
+ const files = scanDirectory(apiDir);
1610
+ const handlers = [];
1611
+ for (const filePath of files) {
1612
+ try {
1613
+ const fileUrl = pathToFileURL(filePath).href;
1614
+ const module = await import(fileUrl);
1615
+ const definition = module.default;
1616
+ if (definition && typeof definition.handler === "function") {
1617
+ const name = fileNameToHandlerName(path.basename(filePath));
1618
+ handlers.push({
1619
+ name,
1620
+ path: filePath,
1621
+ definition
1622
+ });
1623
+ }
1624
+ } catch (err) {
1625
+ logger4.warn(`Failed to load API handler: ${filePath}`, err);
1626
+ }
1627
+ }
1628
+ return handlers;
1629
+ }
1630
+ __name(loadApiHandlers, "loadApiHandlers");
1631
+ async function loadMsgHandlers(msgDir) {
1632
+ const files = scanDirectory(msgDir);
1633
+ const handlers = [];
1634
+ for (const filePath of files) {
1635
+ try {
1636
+ const fileUrl = pathToFileURL(filePath).href;
1637
+ const module = await import(fileUrl);
1638
+ const definition = module.default;
1639
+ if (definition && typeof definition.handler === "function") {
1640
+ const name = fileNameToHandlerName(path.basename(filePath));
1641
+ handlers.push({
1642
+ name,
1643
+ path: filePath,
1644
+ definition
1645
+ });
1646
+ }
1647
+ } catch (err) {
1648
+ logger4.warn(`Failed to load msg handler: ${filePath}`, err);
1649
+ }
1650
+ }
1651
+ return handlers;
1652
+ }
1653
+ __name(loadMsgHandlers, "loadMsgHandlers");
1654
+ function scanDirectoryRecursive(dir, baseDir = dir) {
1655
+ if (!fs.existsSync(dir)) {
1656
+ return [];
1657
+ }
1658
+ const files = [];
1659
+ const entries = fs.readdirSync(dir, {
1660
+ withFileTypes: true
1661
+ });
1662
+ for (const entry of entries) {
1663
+ const fullPath = path.join(dir, entry.name);
1664
+ if (entry.isDirectory()) {
1665
+ files.push(...scanDirectoryRecursive(fullPath, baseDir));
1666
+ } else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
1667
+ if (entry.name.startsWith("_") || entry.name.startsWith("index.")) {
1668
+ continue;
1669
+ }
1670
+ const relativePath = path.relative(baseDir, fullPath);
1671
+ files.push({
1672
+ filePath: fullPath,
1673
+ relativePath
1674
+ });
1675
+ }
1676
+ }
1677
+ return files;
1678
+ }
1679
+ __name(scanDirectoryRecursive, "scanDirectoryRecursive");
1680
+ function filePathToRoute(relativePath, prefix) {
1681
+ let route = relativePath.replace(/\.(ts|js|mts|mjs)$/, "").replace(/\\/g, "/").replace(/\[(\w+)\]/g, ":$1");
1682
+ if (!route.startsWith("/")) {
1683
+ route = "/" + route;
1684
+ }
1685
+ const fullRoute = prefix.endsWith("/") ? prefix.slice(0, -1) + route : prefix + route;
1686
+ return fullRoute;
1687
+ }
1688
+ __name(filePathToRoute, "filePathToRoute");
1689
+ async function loadHttpHandlers(httpDir, prefix = "/api") {
1690
+ const files = scanDirectoryRecursive(httpDir);
1691
+ const handlers = [];
1692
+ for (const { filePath, relativePath } of files) {
1693
+ try {
1694
+ const fileUrl = pathToFileURL(filePath).href;
1695
+ const module = await import(fileUrl);
1696
+ const definition = module.default;
1697
+ if (definition && typeof definition.handler === "function") {
1698
+ const route = filePathToRoute(relativePath, prefix);
1699
+ const method = definition.method ?? "POST";
1700
+ handlers.push({
1701
+ route,
1702
+ method,
1703
+ path: filePath,
1704
+ definition
1705
+ });
1706
+ }
1707
+ } catch (err) {
1708
+ logger4.warn(`Failed to load HTTP handler: ${filePath}`, err);
1709
+ }
1710
+ }
1711
+ return handlers;
1712
+ }
1713
+ __name(loadHttpHandlers, "loadHttpHandlers");
1714
+
1715
+ // src/core/server.ts
1716
+ var DEFAULT_CONFIG = {
1717
+ port: 3e3,
1718
+ apiDir: "src/api",
1719
+ msgDir: "src/msg",
1720
+ httpDir: "src/http",
1721
+ httpPrefix: "/api",
1722
+ tickRate: 20
1723
+ };
1724
+ async function createServer(config = {}) {
1725
+ const opts = {
1726
+ ...DEFAULT_CONFIG,
1727
+ ...config
1728
+ };
1729
+ const cwd = process.cwd();
1730
+ const logger5 = createLogger("Server");
1731
+ const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir));
1732
+ const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir));
1733
+ const httpDir = config.httpDir ?? opts.httpDir;
1734
+ const httpPrefix = config.httpPrefix ?? opts.httpPrefix;
1735
+ const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix);
1736
+ if (apiHandlers.length > 0) {
1737
+ logger5.info(`Loaded ${apiHandlers.length} API handlers`);
1738
+ }
1739
+ if (msgHandlers.length > 0) {
1740
+ logger5.info(`Loaded ${msgHandlers.length} message handlers`);
1741
+ }
1742
+ if (httpHandlers.length > 0) {
1743
+ logger5.info(`Loaded ${httpHandlers.length} HTTP handlers`);
1744
+ }
1745
+ const mergedHttpRoutes = {};
1746
+ for (const handler of httpHandlers) {
1747
+ const existingRoute = mergedHttpRoutes[handler.route];
1748
+ if (existingRoute && typeof existingRoute !== "function") {
1749
+ existingRoute[handler.method] = handler.definition.handler;
1750
+ } else {
1751
+ mergedHttpRoutes[handler.route] = {
1752
+ [handler.method]: handler.definition.handler
1753
+ };
1754
+ }
1755
+ }
1756
+ if (config.http) {
1757
+ for (const [route, handlerOrMethods] of Object.entries(config.http)) {
1758
+ if (typeof handlerOrMethods === "function") {
1759
+ mergedHttpRoutes[route] = handlerOrMethods;
1760
+ } else {
1761
+ const existing = mergedHttpRoutes[route];
1762
+ if (existing && typeof existing !== "function") {
1763
+ Object.assign(existing, handlerOrMethods);
1764
+ } else {
1765
+ mergedHttpRoutes[route] = handlerOrMethods;
1766
+ }
1767
+ }
1768
+ }
1769
+ }
1770
+ const hasHttpRoutes = Object.keys(mergedHttpRoutes).length > 0;
1771
+ const distributedConfig = config.distributed;
1772
+ const isDistributed = distributedConfig?.enabled ?? false;
1773
+ const apiDefs = {
1774
+ // 内置 API
1775
+ JoinRoom: rpc.api(),
1776
+ LeaveRoom: rpc.api()
1777
+ };
1778
+ const msgDefs = {
1779
+ // 内置消息(房间消息透传)
1780
+ RoomMessage: rpc.msg(),
1781
+ // 分布式重定向消息
1782
+ $redirect: rpc.msg()
1783
+ };
1784
+ for (const handler of apiHandlers) {
1785
+ apiDefs[handler.name] = rpc.api();
1786
+ }
1787
+ for (const handler of msgHandlers) {
1788
+ msgDefs[handler.name] = rpc.msg();
1789
+ }
1790
+ const protocol = rpc.define({
1791
+ api: apiDefs,
1792
+ msg: msgDefs
1793
+ });
1794
+ let currentTick = 0;
1795
+ let tickInterval = null;
1796
+ let rpcServer = null;
1797
+ let httpServer = null;
1798
+ const sendFn = /* @__PURE__ */ __name((conn, type, data) => {
1799
+ rpcServer?.send(conn, "RoomMessage", {
1800
+ type,
1801
+ data
1802
+ });
1803
+ }, "sendFn");
1804
+ const sendBinaryFn = /* @__PURE__ */ __name((conn, data) => {
1805
+ if (conn && typeof conn.sendBinary === "function") {
1806
+ conn.sendBinary(data);
1807
+ }
1808
+ }, "sendBinaryFn");
1809
+ let roomManager;
1810
+ let distributedManager = null;
1811
+ if (isDistributed && distributedConfig) {
1812
+ const adapter = distributedConfig.adapter ?? new MemoryAdapter();
1813
+ distributedManager = new DistributedRoomManager(adapter, {
1814
+ serverId: distributedConfig.serverId,
1815
+ serverAddress: distributedConfig.serverAddress,
1816
+ serverPort: distributedConfig.serverPort ?? opts.port,
1817
+ heartbeatInterval: distributedConfig.heartbeatInterval,
1818
+ snapshotInterval: distributedConfig.snapshotInterval,
1819
+ enableFailover: distributedConfig.enableFailover,
1820
+ capacity: distributedConfig.capacity
1821
+ }, sendFn, sendBinaryFn);
1822
+ roomManager = distributedManager;
1823
+ logger5.info(`Distributed mode enabled (serverId: ${distributedConfig.serverId})`);
1824
+ } else {
1825
+ roomManager = new RoomManager(sendFn, sendBinaryFn);
1826
+ }
1827
+ const apiMap = {};
1828
+ for (const handler of apiHandlers) {
1829
+ apiMap[handler.name] = handler;
1830
+ }
1831
+ const msgMap = {};
1832
+ for (const handler of msgHandlers) {
1833
+ msgMap[handler.name] = handler;
1834
+ }
1835
+ const gameServer = {
1836
+ get connections() {
1837
+ return rpcServer?.connections ?? [];
1838
+ },
1839
+ get tick() {
1840
+ return currentTick;
1841
+ },
1842
+ get rooms() {
1843
+ return roomManager;
1844
+ },
1845
+ /**
1846
+ * @zh 注册房间类型
1847
+ * @en Define room type
1848
+ */
1849
+ define(name, roomClass) {
1850
+ roomManager.define(name, roomClass);
1851
+ },
1852
+ async start() {
1853
+ const apiHandlersObj = {};
1854
+ apiHandlersObj["JoinRoom"] = async (input, conn) => {
1855
+ const { roomType, roomId, options } = input;
1856
+ if (roomId) {
1857
+ const result = await roomManager.joinById(roomId, conn.id, conn);
1858
+ if (!result) {
1859
+ throw new Error("Failed to join room");
1860
+ }
1861
+ return {
1862
+ roomId: result.room.id,
1863
+ playerId: result.player.id
1864
+ };
1865
+ }
1866
+ if (roomType) {
1867
+ if (distributedManager) {
1868
+ const result2 = await distributedManager.joinOrCreateDistributed(roomType, conn.id, conn, options);
1869
+ if (!result2) {
1870
+ throw new Error("Failed to join or create room");
1871
+ }
1872
+ if ("redirect" in result2) {
1873
+ rpcServer?.send(conn, "$redirect", {
1874
+ address: result2.redirect,
1875
+ roomType
1876
+ });
1877
+ return {
1878
+ redirect: result2.redirect
1879
+ };
1880
+ }
1881
+ return {
1882
+ roomId: result2.room.id,
1883
+ playerId: result2.player.id
1884
+ };
1885
+ }
1886
+ const result = await roomManager.joinOrCreate(roomType, conn.id, conn, options);
1887
+ if (!result) {
1888
+ throw new Error("Failed to join or create room");
1889
+ }
1890
+ return {
1891
+ roomId: result.room.id,
1892
+ playerId: result.player.id
1893
+ };
1894
+ }
1895
+ throw new Error("roomType or roomId required");
1896
+ };
1897
+ apiHandlersObj["LeaveRoom"] = async (_input, conn) => {
1898
+ await roomManager.leave(conn.id);
1899
+ return {
1900
+ success: true
1901
+ };
1902
+ };
1903
+ for (const [name, handler] of Object.entries(apiMap)) {
1904
+ apiHandlersObj[name] = async (input, conn) => {
1905
+ const ctx = {
1906
+ conn,
1907
+ server: gameServer
1908
+ };
1909
+ const definition = handler.definition;
1910
+ if (definition.schema) {
1911
+ const result = definition.schema.validate(input);
1912
+ if (!result.success) {
1913
+ const pathStr = result.error.path.length > 0 ? ` at "${result.error.path.join(".")}"` : "";
1914
+ throw new Error(`Validation failed${pathStr}: ${result.error.message}`);
1915
+ }
1916
+ return handler.definition.handler(result.data, ctx);
1917
+ }
1918
+ return handler.definition.handler(input, ctx);
1919
+ };
1920
+ }
1921
+ const msgHandlersObj = {};
1922
+ msgHandlersObj["RoomMessage"] = async (data, conn) => {
1923
+ const { type, data: payload } = data;
1924
+ roomManager.handleMessage(conn.id, type, payload);
1925
+ };
1926
+ for (const [name, handler] of Object.entries(msgMap)) {
1927
+ msgHandlersObj[name] = async (data, conn) => {
1928
+ const ctx = {
1929
+ conn,
1930
+ server: gameServer
1931
+ };
1932
+ const definition = handler.definition;
1933
+ if (definition.schema) {
1934
+ const result = definition.schema.validate(data);
1935
+ if (!result.success) {
1936
+ const pathStr = result.error.path.length > 0 ? ` at "${result.error.path.join(".")}"` : "";
1937
+ logger5.warn(`Message validation failed for ${name}${pathStr}: ${result.error.message}`);
1938
+ return;
1939
+ }
1940
+ await handler.definition.handler(result.data, ctx);
1941
+ return;
1942
+ }
1943
+ await handler.definition.handler(data, ctx);
1944
+ };
1945
+ }
1946
+ if (hasHttpRoutes) {
1947
+ const httpRouter = createHttpRouter(mergedHttpRoutes, {
1948
+ cors: config.cors ?? true
1949
+ });
1950
+ httpServer = createServer$1(async (req, res) => {
1951
+ const handled = await httpRouter(req, res);
1952
+ if (!handled) {
1953
+ res.statusCode = 404;
1954
+ res.setHeader("Content-Type", "application/json");
1955
+ res.end(JSON.stringify({
1956
+ error: "Not Found"
1957
+ }));
1958
+ }
1959
+ });
1960
+ rpcServer = serve(protocol, {
1961
+ server: httpServer,
1962
+ createConnData: /* @__PURE__ */ __name(() => ({}), "createConnData"),
1963
+ onStart: /* @__PURE__ */ __name(() => {
1964
+ logger5.info(`Started on http://localhost:${opts.port}`);
1965
+ opts.onStart?.(opts.port);
1966
+ }, "onStart"),
1967
+ onConnect: /* @__PURE__ */ __name(async (conn) => {
1968
+ await config.onConnect?.(conn);
1969
+ }, "onConnect"),
1970
+ onDisconnect: /* @__PURE__ */ __name(async (conn) => {
1971
+ await roomManager?.leave(conn.id, "disconnected");
1972
+ await config.onDisconnect?.(conn);
1973
+ }, "onDisconnect"),
1974
+ api: apiHandlersObj,
1975
+ msg: msgHandlersObj
1976
+ });
1977
+ await rpcServer.start();
1978
+ await new Promise((resolve2) => {
1979
+ httpServer.listen(opts.port, () => resolve2());
1980
+ });
1981
+ } else {
1982
+ rpcServer = serve(protocol, {
1983
+ port: opts.port,
1984
+ createConnData: /* @__PURE__ */ __name(() => ({}), "createConnData"),
1985
+ onStart: /* @__PURE__ */ __name((p) => {
1986
+ logger5.info(`Started on ws://localhost:${p}`);
1987
+ opts.onStart?.(p);
1988
+ }, "onStart"),
1989
+ onConnect: /* @__PURE__ */ __name(async (conn) => {
1990
+ await config.onConnect?.(conn);
1991
+ }, "onConnect"),
1992
+ onDisconnect: /* @__PURE__ */ __name(async (conn) => {
1993
+ await roomManager?.leave(conn.id, "disconnected");
1994
+ await config.onDisconnect?.(conn);
1995
+ }, "onDisconnect"),
1996
+ api: apiHandlersObj,
1997
+ msg: msgHandlersObj
1998
+ });
1999
+ await rpcServer.start();
2000
+ }
2001
+ if (distributedManager) {
2002
+ await distributedManager.start();
2003
+ }
2004
+ if (opts.tickRate > 0) {
2005
+ tickInterval = setInterval(() => {
2006
+ currentTick++;
2007
+ }, 1e3 / opts.tickRate);
2008
+ }
2009
+ },
2010
+ async stop() {
2011
+ if (tickInterval) {
2012
+ clearInterval(tickInterval);
2013
+ tickInterval = null;
2014
+ }
2015
+ if (distributedManager) {
2016
+ await distributedManager.stop(true);
2017
+ }
2018
+ if (rpcServer) {
2019
+ await rpcServer.stop();
2020
+ rpcServer = null;
2021
+ }
2022
+ if (httpServer) {
2023
+ await new Promise((resolve2, reject) => {
2024
+ httpServer.close((err) => {
2025
+ if (err) reject(err);
2026
+ else resolve2();
2027
+ });
2028
+ });
2029
+ httpServer = null;
2030
+ }
2031
+ },
2032
+ broadcast(name, data) {
2033
+ rpcServer?.broadcast(name, data);
2034
+ },
2035
+ send(conn, name, data) {
2036
+ rpcServer?.send(conn, name, data);
2037
+ }
2038
+ };
2039
+ return gameServer;
2040
+ }
2041
+ __name(createServer, "createServer");
2042
+
2043
+ export { DistributedRoomManager, MemoryAdapter, RoomManager, createHttpRouter, createServer };
2044
+ //# sourceMappingURL=chunk-NWZLKNGV.js.map
2045
+ //# sourceMappingURL=chunk-NWZLKNGV.js.map