@esengine/server 1.1.4 → 1.3.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,629 @@
1
+ import { onMessage, createServer, Room } from '../chunk-7C6JZO4O.js';
2
+ import { __name, __publicField } from '../chunk-T626JPC7.js';
3
+ import WebSocket from 'ws';
4
+ import { json } from '@esengine/rpc/codec';
5
+
6
+ var PacketType = {
7
+ ApiRequest: 0,
8
+ ApiResponse: 1,
9
+ ApiError: 2,
10
+ Message: 3
11
+ };
12
+ var _TestClient = class _TestClient {
13
+ constructor(port, options = {}) {
14
+ __publicField(this, "_port");
15
+ __publicField(this, "_codec");
16
+ __publicField(this, "_timeout");
17
+ __publicField(this, "_connectTimeout");
18
+ __publicField(this, "_ws", null);
19
+ __publicField(this, "_callIdCounter", 0);
20
+ __publicField(this, "_connected", false);
21
+ __publicField(this, "_currentRoomId", null);
22
+ __publicField(this, "_currentPlayerId", null);
23
+ __publicField(this, "_pendingCalls", /* @__PURE__ */ new Map());
24
+ __publicField(this, "_msgHandlers", /* @__PURE__ */ new Map());
25
+ __publicField(this, "_receivedMessages", []);
26
+ this._port = port;
27
+ this._codec = options.codec ?? json();
28
+ this._timeout = options.timeout ?? 5e3;
29
+ this._connectTimeout = options.connectTimeout ?? 5e3;
30
+ }
31
+ // ========================================================================
32
+ // Properties | 属性
33
+ // ========================================================================
34
+ /**
35
+ * @zh 是否已连接
36
+ * @en Whether connected
37
+ */
38
+ get isConnected() {
39
+ return this._connected;
40
+ }
41
+ /**
42
+ * @zh 当前房间 ID
43
+ * @en Current room ID
44
+ */
45
+ get roomId() {
46
+ return this._currentRoomId;
47
+ }
48
+ /**
49
+ * @zh 当前玩家 ID
50
+ * @en Current player ID
51
+ */
52
+ get playerId() {
53
+ return this._currentPlayerId;
54
+ }
55
+ /**
56
+ * @zh 收到的所有消息
57
+ * @en All received messages
58
+ */
59
+ get receivedMessages() {
60
+ return this._receivedMessages;
61
+ }
62
+ // ========================================================================
63
+ // Connection | 连接管理
64
+ // ========================================================================
65
+ /**
66
+ * @zh 连接到服务器
67
+ * @en Connect to server
68
+ */
69
+ connect() {
70
+ return new Promise((resolve, reject) => {
71
+ const url = `ws://localhost:${this._port}`;
72
+ this._ws = new WebSocket(url);
73
+ const timeout = setTimeout(() => {
74
+ this._ws?.close();
75
+ reject(new Error(`Connection timeout after ${this._connectTimeout}ms`));
76
+ }, this._connectTimeout);
77
+ this._ws.on("open", () => {
78
+ clearTimeout(timeout);
79
+ this._connected = true;
80
+ resolve(this);
81
+ });
82
+ this._ws.on("close", () => {
83
+ this._connected = false;
84
+ this._rejectAllPending("Connection closed");
85
+ });
86
+ this._ws.on("error", (err) => {
87
+ clearTimeout(timeout);
88
+ if (!this._connected) {
89
+ reject(err);
90
+ }
91
+ });
92
+ this._ws.on("message", (data) => {
93
+ this._handleMessage(data);
94
+ });
95
+ });
96
+ }
97
+ /**
98
+ * @zh 断开连接
99
+ * @en Disconnect from server
100
+ */
101
+ async disconnect() {
102
+ return new Promise((resolve) => {
103
+ if (!this._ws || this._ws.readyState === WebSocket.CLOSED) {
104
+ resolve();
105
+ return;
106
+ }
107
+ this._ws.once("close", () => {
108
+ this._connected = false;
109
+ this._ws = null;
110
+ resolve();
111
+ });
112
+ this._ws.close();
113
+ });
114
+ }
115
+ // ========================================================================
116
+ // Room Operations | 房间操作
117
+ // ========================================================================
118
+ /**
119
+ * @zh 加入房间
120
+ * @en Join a room
121
+ */
122
+ async joinRoom(roomType, options) {
123
+ const result = await this.call("JoinRoom", {
124
+ roomType,
125
+ options
126
+ });
127
+ this._currentRoomId = result.roomId;
128
+ this._currentPlayerId = result.playerId;
129
+ return result;
130
+ }
131
+ /**
132
+ * @zh 通过 ID 加入房间
133
+ * @en Join a room by ID
134
+ */
135
+ async joinRoomById(roomId) {
136
+ const result = await this.call("JoinRoom", {
137
+ roomId
138
+ });
139
+ this._currentRoomId = result.roomId;
140
+ this._currentPlayerId = result.playerId;
141
+ return result;
142
+ }
143
+ /**
144
+ * @zh 离开房间
145
+ * @en Leave room
146
+ */
147
+ async leaveRoom() {
148
+ await this.call("LeaveRoom", {});
149
+ this._currentRoomId = null;
150
+ this._currentPlayerId = null;
151
+ }
152
+ /**
153
+ * @zh 发送消息到房间
154
+ * @en Send message to room
155
+ */
156
+ sendToRoom(type, data) {
157
+ this.send("RoomMessage", {
158
+ type,
159
+ data
160
+ });
161
+ }
162
+ // ========================================================================
163
+ // API Calls | API 调用
164
+ // ========================================================================
165
+ /**
166
+ * @zh 调用 API
167
+ * @en Call API
168
+ */
169
+ call(name, input) {
170
+ return new Promise((resolve, reject) => {
171
+ if (!this._connected || !this._ws) {
172
+ reject(new Error("Not connected"));
173
+ return;
174
+ }
175
+ const id = ++this._callIdCounter;
176
+ const timer = setTimeout(() => {
177
+ this._pendingCalls.delete(id);
178
+ reject(new Error(`API call '${name}' timeout after ${this._timeout}ms`));
179
+ }, this._timeout);
180
+ this._pendingCalls.set(id, {
181
+ resolve,
182
+ reject,
183
+ timer
184
+ });
185
+ const packet = [
186
+ PacketType.ApiRequest,
187
+ id,
188
+ name,
189
+ input
190
+ ];
191
+ this._ws.send(this._codec.encode(packet));
192
+ });
193
+ }
194
+ /**
195
+ * @zh 发送消息
196
+ * @en Send message
197
+ */
198
+ send(name, data) {
199
+ if (!this._connected || !this._ws) return;
200
+ const packet = [
201
+ PacketType.Message,
202
+ name,
203
+ data
204
+ ];
205
+ this._ws.send(this._codec.encode(packet));
206
+ }
207
+ // ========================================================================
208
+ // Message Handling | 消息处理
209
+ // ========================================================================
210
+ /**
211
+ * @zh 监听消息
212
+ * @en Listen for message
213
+ */
214
+ on(name, handler) {
215
+ let handlers = this._msgHandlers.get(name);
216
+ if (!handlers) {
217
+ handlers = /* @__PURE__ */ new Set();
218
+ this._msgHandlers.set(name, handlers);
219
+ }
220
+ handlers.add(handler);
221
+ return this;
222
+ }
223
+ /**
224
+ * @zh 取消监听消息
225
+ * @en Remove message listener
226
+ */
227
+ off(name, handler) {
228
+ if (handler) {
229
+ this._msgHandlers.get(name)?.delete(handler);
230
+ } else {
231
+ this._msgHandlers.delete(name);
232
+ }
233
+ return this;
234
+ }
235
+ /**
236
+ * @zh 等待收到指定消息
237
+ * @en Wait for a specific message
238
+ */
239
+ waitForMessage(type, timeout) {
240
+ return new Promise((resolve, reject) => {
241
+ const timeoutMs = timeout ?? this._timeout;
242
+ const timer = setTimeout(() => {
243
+ this.off(type, handler);
244
+ reject(new Error(`Timeout waiting for message '${type}' after ${timeoutMs}ms`));
245
+ }, timeoutMs);
246
+ const handler = /* @__PURE__ */ __name((data) => {
247
+ clearTimeout(timer);
248
+ this.off(type, handler);
249
+ resolve(data);
250
+ }, "handler");
251
+ this.on(type, handler);
252
+ });
253
+ }
254
+ /**
255
+ * @zh 等待收到指定房间消息
256
+ * @en Wait for a specific room message
257
+ */
258
+ waitForRoomMessage(type, timeout) {
259
+ return new Promise((resolve, reject) => {
260
+ const timeoutMs = timeout ?? this._timeout;
261
+ const timer = setTimeout(() => {
262
+ this.off("RoomMessage", handler);
263
+ reject(new Error(`Timeout waiting for room message '${type}' after ${timeoutMs}ms`));
264
+ }, timeoutMs);
265
+ const handler = /* @__PURE__ */ __name((data) => {
266
+ const msg = data;
267
+ if (msg.type === type) {
268
+ clearTimeout(timer);
269
+ this.off("RoomMessage", handler);
270
+ resolve(msg.data);
271
+ }
272
+ }, "handler");
273
+ this.on("RoomMessage", handler);
274
+ });
275
+ }
276
+ // ========================================================================
277
+ // Assertions | 断言辅助
278
+ // ========================================================================
279
+ /**
280
+ * @zh 是否收到过指定消息
281
+ * @en Whether received a specific message
282
+ */
283
+ hasReceivedMessage(type) {
284
+ return this._receivedMessages.some((m) => m.type === type);
285
+ }
286
+ /**
287
+ * @zh 获取指定类型的所有消息
288
+ * @en Get all messages of a specific type
289
+ */
290
+ getMessagesOfType(type) {
291
+ return this._receivedMessages.filter((m) => m.type === type).map((m) => m.data);
292
+ }
293
+ /**
294
+ * @zh 获取最后收到的指定类型消息
295
+ * @en Get the last received message of a specific type
296
+ */
297
+ getLastMessage(type) {
298
+ for (let i = this._receivedMessages.length - 1; i >= 0; i--) {
299
+ if (this._receivedMessages[i].type === type) {
300
+ return this._receivedMessages[i].data;
301
+ }
302
+ }
303
+ return void 0;
304
+ }
305
+ /**
306
+ * @zh 清空消息记录
307
+ * @en Clear message records
308
+ */
309
+ clearMessages() {
310
+ this._receivedMessages.length = 0;
311
+ }
312
+ /**
313
+ * @zh 获取收到的消息数量
314
+ * @en Get received message count
315
+ */
316
+ getMessageCount(type) {
317
+ if (type) {
318
+ return this._receivedMessages.filter((m) => m.type === type).length;
319
+ }
320
+ return this._receivedMessages.length;
321
+ }
322
+ // ========================================================================
323
+ // Private Methods | 私有方法
324
+ // ========================================================================
325
+ _handleMessage(raw) {
326
+ try {
327
+ const packet = this._codec.decode(raw);
328
+ const type = packet[0];
329
+ switch (type) {
330
+ case PacketType.ApiResponse:
331
+ this._handleApiResponse([
332
+ packet[0],
333
+ packet[1],
334
+ packet[2]
335
+ ]);
336
+ break;
337
+ case PacketType.ApiError:
338
+ this._handleApiError([
339
+ packet[0],
340
+ packet[1],
341
+ packet[2],
342
+ packet[3]
343
+ ]);
344
+ break;
345
+ case PacketType.Message:
346
+ this._handleMsg([
347
+ packet[0],
348
+ packet[1],
349
+ packet[2]
350
+ ]);
351
+ break;
352
+ }
353
+ } catch (err) {
354
+ console.error("[TestClient] Failed to handle message:", err);
355
+ }
356
+ }
357
+ _handleApiResponse([, id, result]) {
358
+ const pending = this._pendingCalls.get(id);
359
+ if (pending) {
360
+ clearTimeout(pending.timer);
361
+ this._pendingCalls.delete(id);
362
+ pending.resolve(result);
363
+ }
364
+ }
365
+ _handleApiError([, id, code, message]) {
366
+ const pending = this._pendingCalls.get(id);
367
+ if (pending) {
368
+ clearTimeout(pending.timer);
369
+ this._pendingCalls.delete(id);
370
+ pending.reject(new Error(`[${code}] ${message}`));
371
+ }
372
+ }
373
+ _handleMsg([, name, data]) {
374
+ this._receivedMessages.push({
375
+ type: name,
376
+ data,
377
+ timestamp: Date.now()
378
+ });
379
+ const handlers = this._msgHandlers.get(name);
380
+ if (handlers) {
381
+ for (const handler of handlers) {
382
+ try {
383
+ handler(data);
384
+ } catch (err) {
385
+ console.error("[TestClient] Handler error:", err);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ _rejectAllPending(reason) {
391
+ for (const [, pending] of this._pendingCalls) {
392
+ clearTimeout(pending.timer);
393
+ pending.reject(new Error(reason));
394
+ }
395
+ this._pendingCalls.clear();
396
+ }
397
+ };
398
+ __name(_TestClient, "TestClient");
399
+ var TestClient = _TestClient;
400
+
401
+ // src/testing/TestServer.ts
402
+ async function getRandomPort() {
403
+ const net = await import('net');
404
+ return new Promise((resolve, reject) => {
405
+ const server = net.createServer();
406
+ server.listen(0, () => {
407
+ const address = server.address();
408
+ if (address && typeof address === "object") {
409
+ const port = address.port;
410
+ server.close(() => resolve(port));
411
+ } else {
412
+ server.close(() => reject(new Error("Failed to get port")));
413
+ }
414
+ });
415
+ server.on("error", reject);
416
+ });
417
+ }
418
+ __name(getRandomPort, "getRandomPort");
419
+ async function createTestServer(options = {}) {
420
+ const port = options.port || await getRandomPort();
421
+ const silent = options.silent ?? true;
422
+ const originalLog = console.log;
423
+ if (silent) {
424
+ console.log = () => {
425
+ };
426
+ }
427
+ const server = await createServer({
428
+ port,
429
+ tickRate: options.tickRate ?? 0,
430
+ apiDir: "__non_existent_api__",
431
+ msgDir: "__non_existent_msg__"
432
+ });
433
+ await server.start();
434
+ if (silent) {
435
+ console.log = originalLog;
436
+ }
437
+ return {
438
+ server,
439
+ port,
440
+ cleanup: /* @__PURE__ */ __name(async () => {
441
+ await server.stop();
442
+ }, "cleanup")
443
+ };
444
+ }
445
+ __name(createTestServer, "createTestServer");
446
+ async function createTestEnv(options = {}) {
447
+ const { server, port, cleanup: serverCleanup } = await createTestServer(options);
448
+ const clients = [];
449
+ return {
450
+ server,
451
+ port,
452
+ clients,
453
+ async createClient(clientOptions) {
454
+ const client = new TestClient(port, clientOptions);
455
+ await client.connect();
456
+ clients.push(client);
457
+ return client;
458
+ },
459
+ async createClients(count, clientOptions) {
460
+ const newClients = [];
461
+ for (let i = 0; i < count; i++) {
462
+ const client = new TestClient(port, clientOptions);
463
+ await client.connect();
464
+ clients.push(client);
465
+ newClients.push(client);
466
+ }
467
+ return newClients;
468
+ },
469
+ async cleanup() {
470
+ await Promise.all(clients.map((c) => c.disconnect().catch(() => {
471
+ })));
472
+ clients.length = 0;
473
+ await serverCleanup();
474
+ }
475
+ };
476
+ }
477
+ __name(createTestEnv, "createTestEnv");
478
+
479
+ // src/testing/MockRoom.ts
480
+ function _ts_decorate(decorators, target, key, desc) {
481
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
482
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
483
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
484
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
485
+ }
486
+ __name(_ts_decorate, "_ts_decorate");
487
+ function _ts_metadata(k, v) {
488
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
489
+ }
490
+ __name(_ts_metadata, "_ts_metadata");
491
+ var _MockRoom = class _MockRoom extends Room {
492
+ constructor() {
493
+ super(...arguments);
494
+ __publicField(this, "state", {
495
+ messages: [],
496
+ joinCount: 0,
497
+ leaveCount: 0
498
+ });
499
+ }
500
+ onCreate() {
501
+ }
502
+ onJoin(player) {
503
+ this.state.joinCount++;
504
+ this.broadcast("PlayerJoined", {
505
+ playerId: player.id,
506
+ joinCount: this.state.joinCount
507
+ });
508
+ }
509
+ onLeave(player) {
510
+ this.state.leaveCount++;
511
+ this.broadcast("PlayerLeft", {
512
+ playerId: player.id,
513
+ leaveCount: this.state.leaveCount
514
+ });
515
+ }
516
+ handleAnyMessage(data, player, type) {
517
+ this.state.messages.push({
518
+ type,
519
+ data,
520
+ playerId: player.id
521
+ });
522
+ this.broadcast("MessageReceived", {
523
+ type,
524
+ data,
525
+ from: player.id
526
+ });
527
+ }
528
+ handleEcho(data, player) {
529
+ player.send("EchoReply", data);
530
+ }
531
+ handleBroadcast(data, _player) {
532
+ this.broadcast("BroadcastMessage", data);
533
+ }
534
+ handlePing(_data, player) {
535
+ player.send("Pong", {
536
+ timestamp: Date.now()
537
+ });
538
+ }
539
+ };
540
+ __name(_MockRoom, "MockRoom");
541
+ var MockRoom = _MockRoom;
542
+ _ts_decorate([
543
+ onMessage("*"),
544
+ _ts_metadata("design:type", Function),
545
+ _ts_metadata("design:paramtypes", [
546
+ Object,
547
+ typeof Player === "undefined" ? Object : Player,
548
+ String
549
+ ]),
550
+ _ts_metadata("design:returntype", void 0)
551
+ ], MockRoom.prototype, "handleAnyMessage", null);
552
+ _ts_decorate([
553
+ onMessage("Echo"),
554
+ _ts_metadata("design:type", Function),
555
+ _ts_metadata("design:paramtypes", [
556
+ Object,
557
+ typeof Player === "undefined" ? Object : Player
558
+ ]),
559
+ _ts_metadata("design:returntype", void 0)
560
+ ], MockRoom.prototype, "handleEcho", null);
561
+ _ts_decorate([
562
+ onMessage("Broadcast"),
563
+ _ts_metadata("design:type", Function),
564
+ _ts_metadata("design:paramtypes", [
565
+ Object,
566
+ typeof Player === "undefined" ? Object : Player
567
+ ]),
568
+ _ts_metadata("design:returntype", void 0)
569
+ ], MockRoom.prototype, "handleBroadcast", null);
570
+ _ts_decorate([
571
+ onMessage("Ping"),
572
+ _ts_metadata("design:type", Function),
573
+ _ts_metadata("design:paramtypes", [
574
+ Object,
575
+ typeof Player === "undefined" ? Object : Player
576
+ ]),
577
+ _ts_metadata("design:returntype", void 0)
578
+ ], MockRoom.prototype, "handlePing", null);
579
+ var _EchoRoom = class _EchoRoom extends Room {
580
+ handleAnyMessage(data, player, type) {
581
+ player.send(type, data);
582
+ }
583
+ };
584
+ __name(_EchoRoom, "EchoRoom");
585
+ var EchoRoom = _EchoRoom;
586
+ _ts_decorate([
587
+ onMessage("*"),
588
+ _ts_metadata("design:type", Function),
589
+ _ts_metadata("design:paramtypes", [
590
+ Object,
591
+ typeof Player === "undefined" ? Object : Player,
592
+ String
593
+ ]),
594
+ _ts_metadata("design:returntype", void 0)
595
+ ], EchoRoom.prototype, "handleAnyMessage", null);
596
+ var _BroadcastRoom = class _BroadcastRoom extends Room {
597
+ onJoin(player) {
598
+ this.broadcast("PlayerJoined", {
599
+ id: player.id
600
+ });
601
+ }
602
+ onLeave(player) {
603
+ this.broadcast("PlayerLeft", {
604
+ id: player.id
605
+ });
606
+ }
607
+ handleAnyMessage(data, player, type) {
608
+ this.broadcast(type, {
609
+ from: player.id,
610
+ data
611
+ });
612
+ }
613
+ };
614
+ __name(_BroadcastRoom, "BroadcastRoom");
615
+ var BroadcastRoom = _BroadcastRoom;
616
+ _ts_decorate([
617
+ onMessage("*"),
618
+ _ts_metadata("design:type", Function),
619
+ _ts_metadata("design:paramtypes", [
620
+ Object,
621
+ typeof Player === "undefined" ? Object : Player,
622
+ String
623
+ ]),
624
+ _ts_metadata("design:returntype", void 0)
625
+ ], BroadcastRoom.prototype, "handleAnyMessage", null);
626
+
627
+ export { MockRoom, TestClient, createTestEnv, createTestServer };
628
+ //# sourceMappingURL=index.js.map
629
+ //# sourceMappingURL=index.js.map