@dangao/bun-server 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +72 -3
  2. package/dist/cache/cache-module.d.ts +18 -0
  3. package/dist/cache/cache-module.d.ts.map +1 -1
  4. package/dist/cache/index.d.ts +3 -1
  5. package/dist/cache/index.d.ts.map +1 -1
  6. package/dist/cache/interceptors.d.ts +41 -0
  7. package/dist/cache/interceptors.d.ts.map +1 -0
  8. package/dist/cache/service-proxy.d.ts +62 -0
  9. package/dist/cache/service-proxy.d.ts.map +1 -0
  10. package/dist/controller/controller.d.ts +8 -0
  11. package/dist/controller/controller.d.ts.map +1 -1
  12. package/dist/core/application.d.ts +5 -0
  13. package/dist/core/application.d.ts.map +1 -1
  14. package/dist/core/server.d.ts.map +1 -1
  15. package/dist/di/container.d.ts +18 -1
  16. package/dist/di/container.d.ts.map +1 -1
  17. package/dist/di/index.d.ts +1 -1
  18. package/dist/di/index.d.ts.map +1 -1
  19. package/dist/di/types.d.ts +22 -0
  20. package/dist/di/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3235 -2875
  24. package/dist/websocket/registry.d.ts +19 -0
  25. package/dist/websocket/registry.d.ts.map +1 -1
  26. package/docs/symbol-interface-pattern.md +431 -0
  27. package/docs/zh/symbol-interface-pattern.md +431 -0
  28. package/package.json +1 -1
  29. package/src/cache/cache-module.ts +37 -0
  30. package/src/cache/index.ts +16 -1
  31. package/src/cache/interceptors.ts +295 -0
  32. package/src/cache/service-proxy.ts +219 -0
  33. package/src/controller/controller.ts +30 -6
  34. package/src/core/application.ts +25 -1
  35. package/src/core/server.ts +1 -0
  36. package/src/di/container.ts +57 -7
  37. package/src/di/index.ts +7 -1
  38. package/src/di/types.ts +29 -0
  39. package/src/index.ts +7 -0
  40. package/src/websocket/registry.ts +114 -14
  41. package/tests/cache/cache-decorators.test.ts +284 -0
  42. package/tests/controller/path-combination.test.ts +353 -0
  43. package/tests/websocket/gateway.test.ts +207 -1
@@ -203,5 +203,358 @@ describe('Controller Path Combination', () => {
203
203
  const rootResponse = await fetch(`http://localhost:${port}/`);
204
204
  expect(rootResponse.status).toBe(404);
205
205
  });
206
+
207
+ test('should correctly combine root controller "/" with method path "/health"', async () => {
208
+ // 这是 metrics-rate-limit-app.ts 示例中的场景
209
+ // @Controller('/') + @GET('/health') 应该映射到 /health,而不是 //health
210
+ @Controller('/')
211
+ class HealthController {
212
+ @GET('/health')
213
+ public health() {
214
+ return { status: 'ok' };
215
+ }
216
+
217
+ @GET('/')
218
+ public index() {
219
+ return { message: 'index' };
220
+ }
221
+ }
222
+
223
+ app.registerController(HealthController);
224
+ await app.listen();
225
+
226
+ // /health 应该正常访问(修复前会得到 404,因为注册的路径是 //health)
227
+ const healthResponse = await fetch(`http://localhost:${port}/health`);
228
+ expect(healthResponse.status).toBe(200);
229
+ const healthData = await healthResponse.json();
230
+ expect(healthData.status).toBe('ok');
231
+
232
+ // / 应该正常访问
233
+ const rootResponse = await fetch(`http://localhost:${port}/`);
234
+ expect(rootResponse.status).toBe(200);
235
+ const rootData = await rootResponse.json();
236
+ expect(rootData.message).toBe('index');
237
+
238
+ // //health 不应该被访问到(或者应该重定向/返回 404)
239
+ // 注:大多数 HTTP 服务器会规范化 //health 为 /health,所以这里可能返回 200
240
+ });
241
+
242
+ test('should correctly combine root controller "/" with multiple method paths', async () => {
243
+ @Controller('/')
244
+ class RootController {
245
+ @GET('/api/status')
246
+ public status() {
247
+ return { status: 'running' };
248
+ }
249
+
250
+ @GET('/api/info')
251
+ public info() {
252
+ return { info: 'test' };
253
+ }
254
+
255
+ @POST('/api/data')
256
+ public data() {
257
+ return { received: true };
258
+ }
259
+ }
260
+
261
+ app.registerController(RootController);
262
+ await app.listen();
263
+
264
+ // 所有路径都应该正常工作
265
+ const statusResponse = await fetch(`http://localhost:${port}/api/status`);
266
+ expect(statusResponse.status).toBe(200);
267
+ expect((await statusResponse.json()).status).toBe('running');
268
+
269
+ const infoResponse = await fetch(`http://localhost:${port}/api/info`);
270
+ expect(infoResponse.status).toBe(200);
271
+ expect((await infoResponse.json()).info).toBe('test');
272
+
273
+ const dataResponse = await fetch(`http://localhost:${port}/api/data`, {
274
+ method: 'POST',
275
+ });
276
+ expect(dataResponse.status).toBe(200);
277
+ expect((await dataResponse.json()).received).toBe(true);
278
+ });
279
+ });
280
+
281
+ /**
282
+ * 路径规范化测试
283
+ * 根据图片中的测试矩阵,验证各种路径组合是否都能正确映射到 /api/base
284
+ *
285
+ * 配置组合 | 拼接结果 | 规范化后路径 | 是否命中 /api/base
286
+ * [/ + /api/base] | //api/base | /api/base | 是
287
+ * [// + /api/base] | ///api/base | /api/base | 是
288
+ * [/api + /base] | /api/base | /api/base | 是
289
+ * [/api/ + base] | /api/base | /api/base | 是
290
+ * [/api/base + ""] | /api/base | /api/base | 是
291
+ */
292
+ describe('Controller Path Normalization', () => {
293
+ let app: Application;
294
+ let port: number;
295
+
296
+ beforeEach(() => {
297
+ port = getTestPort();
298
+ app = new Application({ port });
299
+ });
300
+
301
+ afterEach(async () => {
302
+ if (app) {
303
+ await app.stop();
304
+ }
305
+ RouteRegistry.getInstance().clear();
306
+ ControllerRegistry.getInstance().clear();
307
+ });
308
+
309
+ // 场景 1: [/ + /api/base] -> //api/base -> /api/base
310
+ test('should normalize "/" + "/api/base" to "/api/base"', async () => {
311
+ @Controller('/')
312
+ class TestController {
313
+ @GET('/api/base')
314
+ public handler() {
315
+ return { success: true, scenario: 1 };
316
+ }
317
+ }
318
+
319
+ app.registerController(TestController);
320
+ await app.listen();
321
+
322
+ const response = await fetch(`http://localhost:${port}/api/base`);
323
+ expect(response.status).toBe(200);
324
+ const data = await response.json();
325
+ expect(data.success).toBe(true);
326
+ expect(data.scenario).toBe(1);
327
+ });
328
+
329
+ // 场景 2: [// + /api/base] -> ///api/base -> /api/base
330
+ test('should normalize "//" + "/api/base" to "/api/base"', async () => {
331
+ @Controller('//')
332
+ class TestController {
333
+ @GET('/api/base')
334
+ public handler() {
335
+ return { success: true, scenario: 2 };
336
+ }
337
+ }
338
+
339
+ app.registerController(TestController);
340
+ await app.listen();
341
+
342
+ const response = await fetch(`http://localhost:${port}/api/base`);
343
+ expect(response.status).toBe(200);
344
+ const data = await response.json();
345
+ expect(data.success).toBe(true);
346
+ expect(data.scenario).toBe(2);
347
+ });
348
+
349
+ // 场景 3: [/api + /base] -> /api/base -> /api/base
350
+ test('should normalize "/api" + "/base" to "/api/base"', async () => {
351
+ @Controller('/api')
352
+ class TestController {
353
+ @GET('/base')
354
+ public handler() {
355
+ return { success: true, scenario: 3 };
356
+ }
357
+ }
358
+
359
+ app.registerController(TestController);
360
+ await app.listen();
361
+
362
+ const response = await fetch(`http://localhost:${port}/api/base`);
363
+ expect(response.status).toBe(200);
364
+ const data = await response.json();
365
+ expect(data.success).toBe(true);
366
+ expect(data.scenario).toBe(3);
367
+ });
368
+
369
+ // 场景 4: [/api/ + base] -> /api/base -> /api/base
370
+ test('should normalize "/api/" + "base" to "/api/base"', async () => {
371
+ @Controller('/api/')
372
+ class TestController {
373
+ @GET('base')
374
+ public handler() {
375
+ return { success: true, scenario: 4 };
376
+ }
377
+ }
378
+
379
+ app.registerController(TestController);
380
+ await app.listen();
381
+
382
+ const response = await fetch(`http://localhost:${port}/api/base`);
383
+ expect(response.status).toBe(200);
384
+ const data = await response.json();
385
+ expect(data.success).toBe(true);
386
+ expect(data.scenario).toBe(4);
387
+ });
388
+
389
+ // 场景 5: [/api/base + ""] -> /api/base -> /api/base
390
+ test('should normalize "/api/base" + "" to "/api/base"', async () => {
391
+ @Controller('/api/base')
392
+ class TestController {
393
+ @GET('')
394
+ public handler() {
395
+ return { success: true, scenario: 5 };
396
+ }
397
+ }
398
+
399
+ app.registerController(TestController);
400
+ await app.listen();
401
+
402
+ const response = await fetch(`http://localhost:${port}/api/base`);
403
+ expect(response.status).toBe(200);
404
+ const data = await response.json();
405
+ expect(data.success).toBe(true);
406
+ expect(data.scenario).toBe(5);
407
+ });
408
+
409
+ // 额外场景: [/api/base + /] -> /api/base/ 或 /api/base
410
+ test('should normalize "/api/base" + "/" to "/api/base"', async () => {
411
+ @Controller('/api/base')
412
+ class TestController {
413
+ @GET('/')
414
+ public handler() {
415
+ return { success: true, scenario: 6 };
416
+ }
417
+ }
418
+
419
+ app.registerController(TestController);
420
+ await app.listen();
421
+
422
+ const response = await fetch(`http://localhost:${port}/api/base`);
423
+ expect(response.status).toBe(200);
424
+ const data = await response.json();
425
+ expect(data.success).toBe(true);
426
+ expect(data.scenario).toBe(6);
427
+ });
428
+
429
+ // 场景: [/// + /api/base] -> /api/base (多个前导斜杠)
430
+ test('should normalize "///" + "/api/base" to "/api/base"', async () => {
431
+ @Controller('///')
432
+ class TestController {
433
+ @GET('/api/base')
434
+ public handler() {
435
+ return { success: true, scenario: 'triple-slash' };
436
+ }
437
+ }
438
+
439
+ app.registerController(TestController);
440
+ await app.listen();
441
+
442
+ const response = await fetch(`http://localhost:${port}/api/base`);
443
+ expect(response.status).toBe(200);
444
+ const data = await response.json();
445
+ expect(data.success).toBe(true);
446
+ });
447
+
448
+ // 场景: [/api// + //base] -> /api/base (多个连续斜杠)
449
+ test('should normalize "/api//" + "//base" to "/api/base"', async () => {
450
+ @Controller('/api//')
451
+ class TestController {
452
+ @GET('//base')
453
+ public handler() {
454
+ return { success: true, scenario: 'double-slashes' };
455
+ }
456
+ }
457
+
458
+ app.registerController(TestController);
459
+ await app.listen();
460
+
461
+ const response = await fetch(`http://localhost:${port}/api/base`);
462
+ expect(response.status).toBe(200);
463
+ const data = await response.json();
464
+ expect(data.success).toBe(true);
465
+ });
466
+
467
+ // 场景: ["" + /api/base] -> /api/base (空基础路径)
468
+ test('should normalize "" + "/api/base" to "/api/base"', async () => {
469
+ @Controller('')
470
+ class TestController {
471
+ @GET('/api/base')
472
+ public handler() {
473
+ return { success: true, scenario: 'empty-base' };
474
+ }
475
+ }
476
+
477
+ app.registerController(TestController);
478
+ await app.listen();
479
+
480
+ const response = await fetch(`http://localhost:${port}/api/base`);
481
+ expect(response.status).toBe(200);
482
+ const data = await response.json();
483
+ expect(data.success).toBe(true);
484
+ });
485
+
486
+ // 测试所有场景在同一应用中同时工作
487
+ test('should handle all path normalization scenarios simultaneously', async () => {
488
+ @Controller('/')
489
+ class RootController {
490
+ @GET('/health')
491
+ public health() {
492
+ return { endpoint: 'health' };
493
+ }
494
+ }
495
+
496
+ @Controller('/api')
497
+ class ApiController {
498
+ @GET('/users')
499
+ public users() {
500
+ return { endpoint: 'users' };
501
+ }
502
+
503
+ @GET('products')
504
+ public products() {
505
+ return { endpoint: 'products' };
506
+ }
507
+ }
508
+
509
+ @Controller('/api/')
510
+ class ApiSlashController {
511
+ @GET('orders')
512
+ public orders() {
513
+ return { endpoint: 'orders' };
514
+ }
515
+ }
516
+
517
+ @Controller('/v1/api')
518
+ class V1Controller {
519
+ @GET('')
520
+ public root() {
521
+ return { endpoint: 'v1-root' };
522
+ }
523
+
524
+ @GET('/')
525
+ public index() {
526
+ return { endpoint: 'v1-index' };
527
+ }
528
+ }
529
+
530
+ app.registerController(RootController);
531
+ app.registerController(ApiController);
532
+ app.registerController(ApiSlashController);
533
+ app.registerController(V1Controller);
534
+ await app.listen();
535
+
536
+ // 验证所有端点
537
+ const healthRes = await fetch(`http://localhost:${port}/health`);
538
+ expect(healthRes.status).toBe(200);
539
+ expect((await healthRes.json()).endpoint).toBe('health');
540
+
541
+ const usersRes = await fetch(`http://localhost:${port}/api/users`);
542
+ expect(usersRes.status).toBe(200);
543
+ expect((await usersRes.json()).endpoint).toBe('users');
544
+
545
+ const productsRes = await fetch(`http://localhost:${port}/api/products`);
546
+ expect(productsRes.status).toBe(200);
547
+ expect((await productsRes.json()).endpoint).toBe('products');
548
+
549
+ const ordersRes = await fetch(`http://localhost:${port}/api/orders`);
550
+ expect(ordersRes.status).toBe(200);
551
+ expect((await ordersRes.json()).endpoint).toBe('orders');
552
+
553
+ const v1Res = await fetch(`http://localhost:${port}/v1/api`);
554
+ expect(v1Res.status).toBe(200);
555
+ // v1-root 或 v1-index,取决于哪个先注册
556
+ const v1Data = await v1Res.json();
557
+ expect(['v1-root', 'v1-index']).toContain(v1Data.endpoint);
558
+ });
206
559
  });
207
560
 
@@ -5,7 +5,7 @@ import { WebSocketGateway, OnOpen, OnMessage, OnClose } from '../../src/websocke
5
5
  import { WebSocketGatewayRegistry } from '../../src/websocket/registry';
6
6
  import { ControllerRegistry } from '../../src/controller/controller';
7
7
  import { Application } from '../../src/core/application';
8
- import { Query, Context } from '../../src/controller/decorators';
8
+ import { Query, Context, Param } from '../../src/controller/decorators';
9
9
  import { getTestPort } from '../utils/test-port';
10
10
  import type { WebSocketConnectionData } from '../../src/websocket/registry';
11
11
  import type { Context as RequestContext } from '../../src/core/context';
@@ -1087,6 +1087,212 @@ describe('WebSocket Gateway Integration', () => {
1087
1087
 
1088
1088
  ws.close();
1089
1089
  });
1090
+
1091
+ test('should support dynamic path matching with path parameters', async () => {
1092
+ @WebSocketGateway('/ws/rooms/:roomId')
1093
+ class DynamicPathGateway {
1094
+ public static connected = false;
1095
+ public static roomIds: string[] = [];
1096
+ public static params: Record<string, string>[] = [];
1097
+
1098
+ @OnOpen
1099
+ public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
1100
+ DynamicPathGateway.connected = true;
1101
+ const roomId = ws.data?.params?.roomId;
1102
+ if (roomId) {
1103
+ DynamicPathGateway.roomIds.push(roomId);
1104
+ DynamicPathGateway.params.push(ws.data.params || {});
1105
+ }
1106
+ }
1107
+
1108
+ @OnMessage
1109
+ public handleMessage(
1110
+ ws: ServerWebSocket<WebSocketConnectionData>,
1111
+ message: string,
1112
+ ): void {
1113
+ const roomId = ws.data?.params?.roomId;
1114
+ if (roomId) {
1115
+ ws.send(`Room ${roomId}: ${message}`);
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ app.registerWebSocketGateway(DynamicPathGateway);
1121
+ await app.listen();
1122
+
1123
+ const registry = WebSocketGatewayRegistry.getInstance();
1124
+
1125
+ // 验证动态路径匹配
1126
+ expect(registry.hasGateway('/ws/rooms/123')).toBe(true);
1127
+ expect(registry.hasGateway('/ws/rooms/abc')).toBe(true);
1128
+ expect(registry.hasGateway('/ws/rooms/room-xyz')).toBe(true);
1129
+ // 不匹配的路径
1130
+ expect(registry.hasGateway('/ws/rooms')).toBe(false);
1131
+ expect(registry.hasGateway('/ws/rooms/123/users')).toBe(false);
1132
+
1133
+ // 测试访问动态路径
1134
+ const roomId1 = 'room-123';
1135
+ const ws1 = new WebSocket(`ws://localhost:${port}/ws/rooms/${roomId1}`);
1136
+
1137
+ await new Promise<void>((resolve, reject) => {
1138
+ const timeout = setTimeout(() => {
1139
+ reject(new Error('WebSocket connection timeout'));
1140
+ }, 2000);
1141
+
1142
+ ws1.onopen = () => {
1143
+ clearTimeout(timeout);
1144
+ resolve();
1145
+ };
1146
+
1147
+ ws1.onerror = (error) => {
1148
+ clearTimeout(timeout);
1149
+ reject(error);
1150
+ };
1151
+ });
1152
+
1153
+ await new Promise((resolve) => setTimeout(resolve, 200));
1154
+ expect(DynamicPathGateway.connected).toBe(true);
1155
+ expect(DynamicPathGateway.roomIds).toContain(roomId1);
1156
+ expect(DynamicPathGateway.params[0]?.roomId).toBe(roomId1);
1157
+
1158
+ // 测试另一个房间 ID
1159
+ const roomId2 = 'room-456';
1160
+ const ws2 = new WebSocket(`ws://localhost:${port}/ws/rooms/${roomId2}`);
1161
+
1162
+ await new Promise<void>((resolve, reject) => {
1163
+ const timeout = setTimeout(() => {
1164
+ reject(new Error('WebSocket connection timeout'));
1165
+ }, 2000);
1166
+
1167
+ ws2.onopen = () => {
1168
+ clearTimeout(timeout);
1169
+ resolve();
1170
+ };
1171
+
1172
+ ws2.onerror = (error) => {
1173
+ clearTimeout(timeout);
1174
+ reject(error);
1175
+ };
1176
+ });
1177
+
1178
+ await new Promise((resolve) => setTimeout(resolve, 200));
1179
+ expect(DynamicPathGateway.roomIds).toContain(roomId2);
1180
+ expect(DynamicPathGateway.params.some((p) => p.roomId === roomId2)).toBe(true);
1181
+
1182
+ ws1.close();
1183
+ ws2.close();
1184
+ });
1185
+
1186
+ test('should support multiple path parameters', async () => {
1187
+ @WebSocketGateway('/ws/users/:userId/rooms/:roomId')
1188
+ class MultiParamGateway {
1189
+ public static connections: Array<{
1190
+ userId: string;
1191
+ roomId: string;
1192
+ }> = [];
1193
+
1194
+ @OnOpen
1195
+ public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
1196
+ const params = ws.data?.params || {};
1197
+ MultiParamGateway.connections.push({
1198
+ userId: params.userId || '',
1199
+ roomId: params.roomId || '',
1200
+ });
1201
+ }
1202
+ }
1203
+
1204
+ app.registerWebSocketGateway(MultiParamGateway);
1205
+ await app.listen();
1206
+
1207
+ const registry = WebSocketGatewayRegistry.getInstance();
1208
+
1209
+ // 验证多参数路径匹配
1210
+ expect(registry.hasGateway('/ws/users/user1/rooms/room1')).toBe(true);
1211
+ expect(registry.hasGateway('/ws/users/user2/rooms/room2')).toBe(true);
1212
+
1213
+ const userId = 'user-123';
1214
+ const roomId = 'room-456';
1215
+ const ws = new WebSocket(
1216
+ `ws://localhost:${port}/ws/users/${userId}/rooms/${roomId}`,
1217
+ );
1218
+
1219
+ await new Promise<void>((resolve, reject) => {
1220
+ const timeout = setTimeout(() => {
1221
+ reject(new Error('WebSocket connection timeout'));
1222
+ }, 2000);
1223
+
1224
+ ws.onopen = () => {
1225
+ clearTimeout(timeout);
1226
+ resolve();
1227
+ };
1228
+
1229
+ ws.onerror = (error) => {
1230
+ clearTimeout(timeout);
1231
+ reject(error);
1232
+ };
1233
+ });
1234
+
1235
+ await new Promise((resolve) => setTimeout(resolve, 200));
1236
+
1237
+ // 验证路径参数被正确提取
1238
+ expect(MultiParamGateway.connections.length).toBeGreaterThan(0);
1239
+ const connection = MultiParamGateway.connections[0];
1240
+ expect(connection.userId).toBe(userId);
1241
+ expect(connection.roomId).toBe(roomId);
1242
+
1243
+ ws.close();
1244
+ });
1245
+
1246
+ test('should support @Param decorator with dynamic path parameters', async () => {
1247
+ @WebSocketGateway('/ws/param-test/:id')
1248
+ class ParamDecoratorGateway {
1249
+ public static receivedIds: string[] = [];
1250
+ public static paramValues: string[] = [];
1251
+
1252
+ @OnMessage
1253
+ public handleMessage(
1254
+ _ws: ServerWebSocket<WebSocketConnectionData>,
1255
+ message: string,
1256
+ @Param('id') id: string,
1257
+ ): void {
1258
+ ParamDecoratorGateway.receivedIds.push(message);
1259
+ ParamDecoratorGateway.paramValues.push(id);
1260
+ }
1261
+ }
1262
+
1263
+ app.registerWebSocketGateway(ParamDecoratorGateway);
1264
+ await app.listen();
1265
+
1266
+ const testId = 'test-123';
1267
+ const ws = new WebSocket(`ws://localhost:${port}/ws/param-test/${testId}`);
1268
+
1269
+ await new Promise<void>((resolve, reject) => {
1270
+ const timeout = setTimeout(() => {
1271
+ reject(new Error('WebSocket connection timeout'));
1272
+ }, 2000);
1273
+
1274
+ ws.onopen = () => {
1275
+ clearTimeout(timeout);
1276
+ resolve();
1277
+ };
1278
+
1279
+ ws.onerror = (error) => {
1280
+ clearTimeout(timeout);
1281
+ reject(error);
1282
+ };
1283
+ });
1284
+
1285
+ await new Promise((resolve) => setTimeout(resolve, 100));
1286
+ ws.send('test-message');
1287
+ await new Promise((resolve) => setTimeout(resolve, 200));
1288
+
1289
+ // 验证消息被接收
1290
+ expect(ParamDecoratorGateway.receivedIds).toContain('test-message');
1291
+ // 验证 @Param 装饰器正确提取了路径参数
1292
+ expect(ParamDecoratorGateway.paramValues).toContain(testId);
1293
+
1294
+ ws.close();
1295
+ });
1090
1296
  });
1091
1297
 
1092
1298