@dangao/bun-server 1.9.0 → 1.12.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.
Files changed (209) hide show
  1. package/README.md +79 -6
  2. package/dist/cache/cache-module.d.ts +6 -0
  3. package/dist/cache/cache-module.d.ts.map +1 -1
  4. package/dist/client/generator.d.ts +16 -0
  5. package/dist/client/generator.d.ts.map +1 -0
  6. package/dist/client/index.d.ts +4 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/runtime.d.ts +15 -0
  9. package/dist/client/runtime.d.ts.map +1 -0
  10. package/dist/client/types.d.ts +36 -0
  11. package/dist/client/types.d.ts.map +1 -0
  12. package/dist/config/config-module.d.ts +7 -0
  13. package/dist/config/config-module.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +1 -1
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/service.d.ts +13 -0
  17. package/dist/config/service.d.ts.map +1 -1
  18. package/dist/config/types.d.ts +10 -0
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/core/application.d.ts +7 -0
  21. package/dist/core/application.d.ts.map +1 -1
  22. package/dist/core/apply-decorators.d.ts +6 -0
  23. package/dist/core/apply-decorators.d.ts.map +1 -0
  24. package/dist/core/cluster.d.ts +47 -0
  25. package/dist/core/cluster.d.ts.map +1 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/server.d.ts +8 -0
  29. package/dist/core/server.d.ts.map +1 -1
  30. package/dist/dashboard/controller.d.ts +55 -0
  31. package/dist/dashboard/controller.d.ts.map +1 -0
  32. package/dist/dashboard/dashboard-extension.d.ts +20 -0
  33. package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
  34. package/dist/dashboard/dashboard-module.d.ts +13 -0
  35. package/dist/dashboard/dashboard-module.d.ts.map +1 -0
  36. package/dist/dashboard/index.d.ts +4 -0
  37. package/dist/dashboard/index.d.ts.map +1 -0
  38. package/dist/dashboard/types.d.ts +16 -0
  39. package/dist/dashboard/types.d.ts.map +1 -0
  40. package/dist/dashboard/ui.d.ts +7 -0
  41. package/dist/dashboard/ui.d.ts.map +1 -0
  42. package/dist/database/database-module.d.ts +7 -0
  43. package/dist/database/database-module.d.ts.map +1 -1
  44. package/dist/debug/debug-module.d.ts +13 -0
  45. package/dist/debug/debug-module.d.ts.map +1 -0
  46. package/dist/debug/debug-ui-middleware.d.ts +8 -0
  47. package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
  48. package/dist/debug/index.d.ts +5 -0
  49. package/dist/debug/index.d.ts.map +1 -0
  50. package/dist/debug/middleware.d.ts +12 -0
  51. package/dist/debug/middleware.d.ts.map +1 -0
  52. package/dist/debug/recorder.d.ts +61 -0
  53. package/dist/debug/recorder.d.ts.map +1 -0
  54. package/dist/debug/types.d.ts +48 -0
  55. package/dist/debug/types.d.ts.map +1 -0
  56. package/dist/debug/ui.d.ts +6 -0
  57. package/dist/debug/ui.d.ts.map +1 -0
  58. package/dist/di/async-module.d.ts +49 -0
  59. package/dist/di/async-module.d.ts.map +1 -0
  60. package/dist/di/lifecycle.d.ts +49 -0
  61. package/dist/di/lifecycle.d.ts.map +1 -0
  62. package/dist/di/module-registry.d.ts +24 -0
  63. package/dist/di/module-registry.d.ts.map +1 -1
  64. package/dist/index.d.ts +9 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1887 -35
  67. package/dist/router/route.d.ts +5 -7
  68. package/dist/router/route.d.ts.map +1 -1
  69. package/dist/swagger/generator.d.ts +10 -0
  70. package/dist/swagger/generator.d.ts.map +1 -1
  71. package/dist/testing/test-client.d.ts +49 -0
  72. package/dist/testing/test-client.d.ts.map +1 -0
  73. package/dist/testing/testing-module.d.ts +90 -0
  74. package/dist/testing/testing-module.d.ts.map +1 -0
  75. package/dist/websocket/registry.d.ts +1 -6
  76. package/dist/websocket/registry.d.ts.map +1 -1
  77. package/docs/async-module.md +59 -0
  78. package/docs/client-generation.md +100 -0
  79. package/docs/cluster.md +81 -0
  80. package/docs/custom-decorators.md +1 -7
  81. package/docs/dashboard.md +54 -0
  82. package/docs/debug.md +58 -0
  83. package/docs/extensions.md +0 -2
  84. package/docs/guide.md +0 -1
  85. package/docs/lifecycle.md +72 -0
  86. package/docs/testing.md +110 -0
  87. package/docs/zh/async-module.md +98 -0
  88. package/docs/zh/client-generation.md +92 -0
  89. package/docs/zh/cluster.md +74 -0
  90. package/docs/zh/custom-decorators.md +1 -7
  91. package/docs/zh/dashboard.md +69 -0
  92. package/docs/zh/debug.md +81 -0
  93. package/docs/zh/extensions.md +0 -2
  94. package/docs/zh/guide.md +0 -1
  95. package/docs/zh/lifecycle.md +87 -0
  96. package/docs/zh/migration.md +0 -5
  97. package/docs/zh/testing.md +119 -0
  98. package/package.json +4 -4
  99. package/src/cache/cache-module.ts +25 -0
  100. package/src/client/generator.ts +36 -0
  101. package/src/client/index.ts +8 -0
  102. package/src/client/runtime.ts +101 -0
  103. package/src/client/types.ts +38 -0
  104. package/src/config/config-module.ts +44 -4
  105. package/src/config/index.ts +1 -0
  106. package/src/config/service.ts +50 -0
  107. package/src/config/types.ts +12 -0
  108. package/src/core/application.ts +37 -0
  109. package/src/core/apply-decorators.ts +31 -0
  110. package/src/core/cluster.ts +143 -0
  111. package/src/core/index.ts +1 -0
  112. package/src/core/server.ts +14 -1
  113. package/src/dashboard/controller.ts +227 -0
  114. package/src/dashboard/dashboard-extension.ts +26 -0
  115. package/src/dashboard/dashboard-module.ts +38 -0
  116. package/src/dashboard/index.ts +3 -0
  117. package/src/dashboard/types.ts +16 -0
  118. package/src/dashboard/ui.ts +219 -0
  119. package/src/database/database-module.ts +20 -0
  120. package/src/debug/debug-module.ts +70 -0
  121. package/src/debug/debug-ui-middleware.ts +110 -0
  122. package/src/debug/index.ts +9 -0
  123. package/src/debug/middleware.ts +126 -0
  124. package/src/debug/recorder.ts +141 -0
  125. package/src/debug/types.ts +49 -0
  126. package/src/debug/ui.ts +393 -0
  127. package/src/di/async-module.ts +141 -0
  128. package/src/di/lifecycle.ts +117 -0
  129. package/src/di/module-registry.ts +75 -0
  130. package/src/index.ts +35 -0
  131. package/src/router/route.ts +20 -20
  132. package/src/swagger/generator.ts +100 -0
  133. package/src/testing/test-client.ts +112 -0
  134. package/src/testing/testing-module.ts +238 -0
  135. package/src/websocket/registry.ts +3 -16
  136. package/tests/auth/auth-decorators.test.ts +0 -1
  137. package/tests/auth/oauth2-service.test.ts +0 -1
  138. package/tests/cache/cache-decorators-extended.test.ts +0 -1
  139. package/tests/cache/cache-decorators.test.ts +0 -1
  140. package/tests/cache/cache-interceptors.test.ts +0 -1
  141. package/tests/cache/cache-module.test.ts +0 -1
  142. package/tests/cache/cache-service-proxy.test.ts +0 -1
  143. package/tests/client/client-generator.test.ts +142 -0
  144. package/tests/config/config-center-integration.test.ts +0 -1
  145. package/tests/config/config-module-extended.test.ts +0 -1
  146. package/tests/config/config-module.test.ts +0 -1
  147. package/tests/controller/controller.test.ts +0 -1
  148. package/tests/controller/param-binder.test.ts +0 -1
  149. package/tests/controller/path-combination.test.ts +0 -1
  150. package/tests/core/application.test.ts +34 -0
  151. package/tests/core/apply-decorators.test.ts +109 -0
  152. package/tests/core/cluster.test.ts +32 -0
  153. package/tests/dashboard/dashboard-module.test.ts +85 -0
  154. package/tests/database/database-module.test.ts +0 -1
  155. package/tests/database/orm.test.ts +0 -1
  156. package/tests/database/postgres-mysql-integration.test.ts +0 -1
  157. package/tests/database/transaction.test.ts +0 -1
  158. package/tests/debug/debug-module.test.ts +141 -0
  159. package/tests/di/async-module.test.ts +125 -0
  160. package/tests/di/container.test.ts +0 -1
  161. package/tests/di/lifecycle.test.ts +140 -0
  162. package/tests/error/error-handler.test.ts +0 -1
  163. package/tests/events/event-decorators.test.ts +0 -1
  164. package/tests/events/event-listener-scanner.test.ts +0 -1
  165. package/tests/events/event-module.test.ts +0 -1
  166. package/tests/extensions/logger-module.test.ts +0 -1
  167. package/tests/health/health-module.test.ts +0 -1
  168. package/tests/integration/oauth2-e2e.test.ts +0 -1
  169. package/tests/integration/session-e2e.test.ts +0 -1
  170. package/tests/interceptor/base-interceptor.test.ts +0 -1
  171. package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
  172. package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
  173. package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
  174. package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
  175. package/tests/interceptor/interceptor-chain.test.ts +0 -1
  176. package/tests/interceptor/interceptor-integration.test.ts +0 -1
  177. package/tests/interceptor/interceptor-metadata.test.ts +0 -1
  178. package/tests/interceptor/interceptor-registry.test.ts +0 -1
  179. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
  180. package/tests/metrics/metrics-module.test.ts +0 -1
  181. package/tests/microservice/config-center.test.ts +0 -1
  182. package/tests/microservice/service-client-decorators.test.ts +0 -1
  183. package/tests/microservice/service-registry-decorators.test.ts +0 -1
  184. package/tests/microservice/service-registry.test.ts +0 -1
  185. package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
  186. package/tests/middleware/builtin/rate-limit.test.ts +0 -1
  187. package/tests/middleware/middleware-decorators.test.ts +0 -1
  188. package/tests/middleware/middleware-pipeline.test.ts +0 -1
  189. package/tests/middleware/middleware.test.ts +0 -1
  190. package/tests/perf/optimization.test.ts +0 -1
  191. package/tests/queue/queue-decorators.test.ts +0 -1
  192. package/tests/queue/queue-module.test.ts +0 -1
  193. package/tests/queue/queue-service.test.ts +0 -1
  194. package/tests/router/router-decorators.test.ts +0 -1
  195. package/tests/router/router-extended.test.ts +0 -1
  196. package/tests/security/guards/guards-integration.test.ts +0 -1
  197. package/tests/security/guards/guards.test.ts +0 -1
  198. package/tests/security/guards/reflector.test.ts +0 -1
  199. package/tests/security/security-filter.test.ts +0 -1
  200. package/tests/security/security-module-extended.test.ts +0 -1
  201. package/tests/security/security-module.test.ts +0 -1
  202. package/tests/session/session-decorators.test.ts +0 -1
  203. package/tests/session/session-module.test.ts +0 -1
  204. package/tests/swagger/decorators.test.ts +0 -1
  205. package/tests/swagger/swagger-module.test.ts +0 -1
  206. package/tests/swagger/ui.test.ts +0 -1
  207. package/tests/testing/testing-module.test.ts +129 -0
  208. package/tests/validation/class-validator.test.ts +0 -1
  209. package/tests/validation/controller-validation.test.ts +0 -1
@@ -0,0 +1,109 @@
1
+ import 'reflect-metadata';
2
+ import { describe, expect, test } from 'bun:test';
3
+ import { applyDecorators } from '../../src/core/apply-decorators';
4
+
5
+ const TEST_KEY_A = Symbol('test:a');
6
+ const TEST_KEY_B = Symbol('test:b');
7
+ const TEST_KEY_C = Symbol('test:c');
8
+
9
+ function MarkA(value: string): MethodDecorator {
10
+ return (target, propertyKey) => {
11
+ Reflect.defineMetadata(TEST_KEY_A, value, target, propertyKey);
12
+ };
13
+ }
14
+
15
+ function MarkB(value: number): MethodDecorator {
16
+ return (target, propertyKey) => {
17
+ Reflect.defineMetadata(TEST_KEY_B, value, target, propertyKey);
18
+ };
19
+ }
20
+
21
+ function ClassMark(value: string): ClassDecorator {
22
+ return (target) => {
23
+ Reflect.defineMetadata(TEST_KEY_C, value, target);
24
+ };
25
+ }
26
+
27
+ describe('applyDecorators', () => {
28
+ test('should compose method decorators', () => {
29
+ const Combined = applyDecorators(MarkA('hello'), MarkB(42));
30
+
31
+ class TestClass {
32
+ @Combined
33
+ public myMethod(): void {}
34
+ }
35
+
36
+ const a = Reflect.getMetadata(TEST_KEY_A, TestClass.prototype, 'myMethod');
37
+ const b = Reflect.getMetadata(TEST_KEY_B, TestClass.prototype, 'myMethod');
38
+ expect(a).toBe('hello');
39
+ expect(b).toBe(42);
40
+ });
41
+
42
+ test('should compose class decorators', () => {
43
+ const Combined = applyDecorators(
44
+ ClassMark('cls-value'),
45
+ );
46
+
47
+ @Combined
48
+ class TestClass {}
49
+
50
+ const c = Reflect.getMetadata(TEST_KEY_C, TestClass);
51
+ expect(c).toBe('cls-value');
52
+ });
53
+
54
+ test('should apply decorators in correct order (last applied first)', () => {
55
+ const order: string[] = [];
56
+
57
+ function Track(label: string): MethodDecorator {
58
+ return (target, propertyKey) => {
59
+ order.push(label);
60
+ };
61
+ }
62
+
63
+ const Combined = applyDecorators(Track('first'), Track('second'));
64
+
65
+ class TestClass {
66
+ @Combined
67
+ public myMethod(): void {}
68
+ }
69
+
70
+ // decorators.reverse() means 'second' is applied first, then 'first'
71
+ expect(order).toEqual(['second', 'first']);
72
+ });
73
+
74
+ test('should work with single decorator', () => {
75
+ const Combined = applyDecorators(MarkA('solo'));
76
+
77
+ class TestClass {
78
+ @Combined
79
+ public myMethod(): void {}
80
+ }
81
+
82
+ const a = Reflect.getMetadata(TEST_KEY_A, TestClass.prototype, 'myMethod');
83
+ expect(a).toBe('solo');
84
+ });
85
+
86
+ test('should work with many decorators', () => {
87
+ const keys: symbol[] = [];
88
+ const decorators: MethodDecorator[] = [];
89
+ for (let i = 0; i < 10; i++) {
90
+ const key = Symbol(`test:${i}`);
91
+ keys.push(key);
92
+ decorators.push((target, propertyKey) => {
93
+ Reflect.defineMetadata(key, i, target, propertyKey);
94
+ });
95
+ }
96
+
97
+ const Combined = applyDecorators(...decorators);
98
+
99
+ class TestClass {
100
+ @Combined
101
+ public myMethod(): void {}
102
+ }
103
+
104
+ for (let i = 0; i < 10; i++) {
105
+ const val = Reflect.getMetadata(keys[i]!, TestClass.prototype, 'myMethod');
106
+ expect(val).toBe(i);
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,32 @@
1
+ import 'reflect-metadata';
2
+ import { describe, expect, test } from 'bun:test';
3
+ import { ClusterManager } from '../../src/core/cluster';
4
+
5
+ describe('ClusterManager', () => {
6
+ test('isWorker should return false in test environment', () => {
7
+ expect(ClusterManager.isWorker()).toBe(false);
8
+ });
9
+
10
+ test('getWorkerId should return -1 when not in worker', () => {
11
+ expect(ClusterManager.getWorkerId()).toBe(-1);
12
+ });
13
+
14
+ test('should calculate worker count from auto', () => {
15
+ const manager = new ClusterManager({
16
+ workers: 'auto',
17
+ scriptPath: '/tmp/test.ts',
18
+ port: 3000,
19
+ });
20
+ // auto should resolve to CPU core count
21
+ expect(manager).toBeDefined();
22
+ });
23
+
24
+ test('should accept explicit worker count', () => {
25
+ const manager = new ClusterManager({
26
+ workers: 4,
27
+ scriptPath: '/tmp/test.ts',
28
+ port: 3000,
29
+ });
30
+ expect(manager).toBeDefined();
31
+ });
32
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { Application } from '../../src/core/application';
3
+ import { DashboardModule } from '../../src/dashboard';
4
+ import { RouteRegistry } from '../../src/router/registry';
5
+ import { MODULE_METADATA_KEY } from '../../src/di/module';
6
+
7
+ describe('DashboardModule', () => {
8
+ beforeEach(() => {
9
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, DashboardModule);
10
+ });
11
+
12
+ test('should register dashboard extension in forRoot', () => {
13
+ DashboardModule.forRoot({ path: '/_dashboard' });
14
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, DashboardModule);
15
+ expect(metadata).toBeDefined();
16
+ expect(metadata.extensions).toHaveLength(1);
17
+ });
18
+
19
+ test('should use default path when not specified', () => {
20
+ DashboardModule.forRoot({});
21
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, DashboardModule);
22
+ expect(metadata).toBeDefined();
23
+ expect(metadata.extensions).toHaveLength(1);
24
+ });
25
+
26
+ test('should register routes when extension is used', async () => {
27
+ const app = new Application();
28
+ app.registerModule(DashboardModule.forRoot({ path: '/_dashboard' }));
29
+ await app.listen(0);
30
+ try {
31
+ const port = app.getServer()?.getPort() ?? 0;
32
+ const baseRes = await fetch(`http://127.0.0.1:${port}/_dashboard`);
33
+ expect(baseRes.status).toBe(200);
34
+ expect(baseRes.headers.get('Content-Type')).toContain('text/html');
35
+
36
+ const systemRes = await fetch(`http://127.0.0.1:${port}/_dashboard/api/system`);
37
+ expect(systemRes.status).toBe(200);
38
+ const systemData = await systemRes.json();
39
+ expect(systemData).toHaveProperty('uptime');
40
+ expect(systemData).toHaveProperty('memory');
41
+ expect(systemData.memory).toHaveProperty('rss');
42
+ expect(systemData.memory).toHaveProperty('heapUsed');
43
+ expect(systemData).toHaveProperty('platform');
44
+ expect(systemData).toHaveProperty('bunVersion');
45
+
46
+ const routesRes = await fetch(`http://127.0.0.1:${port}/_dashboard/api/routes`);
47
+ expect(routesRes.status).toBe(200);
48
+ const routesData = await routesRes.json();
49
+ expect(Array.isArray(routesData)).toBe(true);
50
+
51
+ const healthRes = await fetch(`http://127.0.0.1:${port}/_dashboard/api/health`);
52
+ expect(healthRes.status).toBe(200);
53
+ const healthData = await healthRes.json();
54
+ expect(healthData).toHaveProperty('status');
55
+ expect(healthData).toHaveProperty('timestamp');
56
+ } finally {
57
+ await app.stop();
58
+ }
59
+ });
60
+
61
+ test('should require Basic Auth when configured', async () => {
62
+ const app = new Application();
63
+ app.registerModule(
64
+ DashboardModule.forRoot({
65
+ path: '/_dashboard',
66
+ auth: { username: 'admin', password: 'secret' },
67
+ }),
68
+ );
69
+ await app.listen(0);
70
+ try {
71
+ const port = app.getServer()?.getPort() ?? 0;
72
+ const res = await fetch(`http://127.0.0.1:${port}/_dashboard/api/system`);
73
+ expect(res.status).toBe(401);
74
+
75
+ const authRes = await fetch(`http://127.0.0.1:${port}/_dashboard/api/system`, {
76
+ headers: {
77
+ Authorization: 'Basic ' + btoa('admin:secret'),
78
+ },
79
+ });
80
+ expect(authRes.status).toBe(200);
81
+ } finally {
82
+ await app.stop();
83
+ }
84
+ });
85
+ });
@@ -3,7 +3,6 @@ import { DatabaseModule, DatabaseService, DATABASE_SERVICE_TOKEN } from '../../s
3
3
  import { Container } from '../../src/di/container';
4
4
  import { ModuleRegistry } from '../../src/di/module-registry';
5
5
  import { MODULE_METADATA_KEY } from '../../src/di/module';
6
- import 'reflect-metadata';
7
6
 
8
7
  describe('DatabaseModule', () => {
9
8
  let container: Container;
@@ -11,7 +11,6 @@ import {
11
11
  } from '../../src/database/orm';
12
12
  import { DatabaseService, DATABASE_SERVICE_TOKEN } from '../../src/database';
13
13
  import { Container } from '../../src/di/container';
14
- import 'reflect-metadata';
15
14
 
16
15
  // 测试实体
17
16
  @Entity('test_users')
@@ -7,7 +7,6 @@ import {
7
7
  import { Container } from '../../src/di/container';
8
8
  import { ModuleRegistry } from '../../src/di/module-registry';
9
9
  import { MODULE_METADATA_KEY } from '../../src/di/module';
10
- import 'reflect-metadata';
11
10
 
12
11
  /**
13
12
  * PostgreSQL/MySQL 集成测试
@@ -9,7 +9,6 @@ import {
9
9
  } from '../../src/database/orm';
10
10
  import { DatabaseService, DATABASE_SERVICE_TOKEN } from '../../src/database';
11
11
  import { Container } from '../../src/di/container';
12
- import 'reflect-metadata';
13
12
 
14
13
  describe('TransactionManager', () => {
15
14
  let container: Container;
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
+ import { Application } from '../../src/core/application';
3
+ import { DebugModule, RequestRecorder } from '../../src/debug';
4
+ import { MODULE_METADATA_KEY } from '../../src/di/module';
5
+ import { getTestPort } from '../utils/test-port';
6
+
7
+ describe('RequestRecorder', () => {
8
+ let recorder: RequestRecorder;
9
+
10
+ beforeEach(() => {
11
+ recorder = new RequestRecorder(10);
12
+ });
13
+
14
+ test('should record and retrieve requests', () => {
15
+ recorder.record({
16
+ timestamp: 1000,
17
+ request: { method: 'GET', path: '/test', headers: {} },
18
+ response: { status: 200, headers: {}, bodySize: 0 },
19
+ timing: { total: 5 },
20
+ metadata: {},
21
+ });
22
+
23
+ const all = recorder.getAll();
24
+ expect(all).toHaveLength(1);
25
+ expect(all[0].request.method).toBe('GET');
26
+ expect(all[0].request.path).toBe('/test');
27
+ expect(all[0].id).toBeDefined();
28
+ });
29
+
30
+ test('should get record by id', () => {
31
+ recorder.record({
32
+ timestamp: 1000,
33
+ request: { method: 'POST', path: '/api', headers: {} },
34
+ response: { status: 201, headers: {}, bodySize: 10 },
35
+ timing: { total: 10 },
36
+ metadata: {},
37
+ });
38
+
39
+ const all = recorder.getAll();
40
+ const id = all[0].id;
41
+ const found = recorder.getById(id);
42
+ expect(found).toBeDefined();
43
+ expect(found?.request.method).toBe('POST');
44
+ });
45
+
46
+ test('should use ring buffer when maxRecords exceeded', () => {
47
+ for (let i = 0; i < 15; i++) {
48
+ recorder.record({
49
+ timestamp: 1000 + i,
50
+ request: { method: 'GET', path: `/test/${i}`, headers: {} },
51
+ response: { status: 200, headers: {}, bodySize: 0 },
52
+ timing: { total: 1 },
53
+ metadata: {},
54
+ });
55
+ }
56
+
57
+ expect(recorder.getCount()).toBe(10);
58
+ expect(recorder.getAll()).toHaveLength(10);
59
+ });
60
+
61
+ test('should clear all records', () => {
62
+ recorder.record({
63
+ timestamp: 1000,
64
+ request: { method: 'GET', path: '/test', headers: {} },
65
+ response: { status: 200, headers: {}, bodySize: 0 },
66
+ timing: { total: 1 },
67
+ metadata: {},
68
+ });
69
+
70
+ recorder.clear();
71
+ expect(recorder.getCount()).toBe(0);
72
+ expect(recorder.getAll()).toHaveLength(0);
73
+ });
74
+ });
75
+
76
+ describe('DebugModule', () => {
77
+ beforeEach(() => {
78
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, DebugModule);
79
+ });
80
+
81
+ test('should register providers and middlewares via forRoot', () => {
82
+ DebugModule.forRoot({ maxRecords: 100, path: '/_debug' });
83
+
84
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, DebugModule);
85
+ expect(metadata.providers).toBeDefined();
86
+ expect(metadata.providers.length).toBeGreaterThanOrEqual(2);
87
+ expect(metadata.middlewares).toBeDefined();
88
+ expect(metadata.middlewares.length).toBe(2);
89
+ });
90
+
91
+ test('should not add middlewares when disabled', () => {
92
+ DebugModule.forRoot({ enabled: false });
93
+
94
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, DebugModule);
95
+ expect(metadata.middlewares).toHaveLength(0);
96
+ });
97
+ });
98
+
99
+ describe('DebugModule Integration', () => {
100
+ let app: Application;
101
+ let port: number;
102
+
103
+ beforeEach(() => {
104
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, DebugModule);
105
+ port = getTestPort();
106
+ app = new Application();
107
+ app.registerModule(DebugModule.forRoot({ path: '/_debug', maxRecords: 50 }));
108
+ });
109
+
110
+ afterEach(async () => {
111
+ await app.stop();
112
+ });
113
+
114
+ test('should serve debug UI at configured path', async () => {
115
+ await app.listen(port);
116
+ const res = await fetch(`http://localhost:${port}/_debug`);
117
+ expect(res.status).toBe(200);
118
+ expect(res.headers.get('content-type')).toContain('text/html');
119
+ const html = await res.text();
120
+ expect(html).toContain('Debug - Request Replay');
121
+ });
122
+
123
+ test('should list records via API', async () => {
124
+ await app.listen(port);
125
+ await fetch(`http://localhost:${port}/api/test`);
126
+ const res = await fetch(`http://localhost:${port}/_debug/api/records`);
127
+ expect(res.status).toBe(200);
128
+ const data = await res.json();
129
+ expect(Array.isArray(data)).toBe(true);
130
+ });
131
+
132
+ test('should clear records via API', async () => {
133
+ await app.listen(port);
134
+ const res = await fetch(`http://localhost:${port}/_debug/api/records`, {
135
+ method: 'DELETE',
136
+ });
137
+ expect(res.status).toBe(200);
138
+ const data = await res.json();
139
+ expect(data.ok).toBe(true);
140
+ });
141
+ });
@@ -0,0 +1,125 @@
1
+ import 'reflect-metadata';
2
+ import { describe, expect, test, beforeEach } from 'bun:test';
3
+ import { AsyncProviderRegistry, registerAsyncProviders } from '../../src/di/async-module';
4
+ import { Module, MODULE_METADATA_KEY } from '../../src/di/module';
5
+ import { Container } from '../../src/di/container';
6
+
7
+ const TEST_TOKEN_A = Symbol('test:a');
8
+ const TEST_TOKEN_B = Symbol('test:b');
9
+ const DEP_TOKEN = Symbol('test:dep');
10
+
11
+ describe('AsyncProviderRegistry', () => {
12
+ beforeEach(() => {
13
+ AsyncProviderRegistry.getInstance().clear();
14
+ });
15
+
16
+ test('should register and initialize async providers', async () => {
17
+ const registry = AsyncProviderRegistry.getInstance();
18
+ const container = new Container();
19
+
20
+ registry.register(TEST_TOKEN_A, async () => {
21
+ return { value: 'async-value' };
22
+ });
23
+
24
+ expect(registry.hasPending()).toBe(true);
25
+
26
+ await registry.initializeAll(container);
27
+
28
+ expect(registry.hasPending()).toBe(false);
29
+ const resolved = container.resolve<{ value: string }>(TEST_TOKEN_A);
30
+ expect(resolved.value).toBe('async-value');
31
+ });
32
+
33
+ test('should resolve inject dependencies from container', async () => {
34
+ const registry = AsyncProviderRegistry.getInstance();
35
+ const container = new Container();
36
+
37
+ container.registerInstance(DEP_TOKEN, { url: 'postgres://localhost' });
38
+
39
+ registry.register(TEST_TOKEN_A, async (c) => {
40
+ const dep = c.resolve<{ url: string }>(DEP_TOKEN);
41
+ return { connectionUrl: dep.url };
42
+ });
43
+
44
+ await registry.initializeAll(container);
45
+
46
+ const resolved = container.resolve<{ connectionUrl: string }>(TEST_TOKEN_A);
47
+ expect(resolved.connectionUrl).toBe('postgres://localhost');
48
+ });
49
+
50
+ test('should initialize multiple async providers in order', async () => {
51
+ const registry = AsyncProviderRegistry.getInstance();
52
+ const container = new Container();
53
+ const order: string[] = [];
54
+
55
+ registry.register(TEST_TOKEN_A, async () => {
56
+ order.push('a');
57
+ return 'value-a';
58
+ });
59
+
60
+ registry.register(TEST_TOKEN_B, async () => {
61
+ order.push('b');
62
+ return 'value-b';
63
+ });
64
+
65
+ await registry.initializeAll(container);
66
+
67
+ expect(order).toEqual(['a', 'b']);
68
+ expect(container.resolve(TEST_TOKEN_A)).toBe('value-a');
69
+ expect(container.resolve(TEST_TOKEN_B)).toBe('value-b');
70
+ });
71
+ });
72
+
73
+ describe('registerAsyncProviders', () => {
74
+ beforeEach(() => {
75
+ AsyncProviderRegistry.getInstance().clear();
76
+ });
77
+
78
+ test('should register async tokens on module metadata', () => {
79
+ @Module({ providers: [] })
80
+ class TestModule {}
81
+
82
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, TestModule);
83
+
84
+ const tokenMap = new Map<symbol, (config: { name: string }) => unknown>();
85
+ tokenMap.set(TEST_TOKEN_A, (config) => config.name);
86
+
87
+ registerAsyncProviders(
88
+ TestModule,
89
+ {
90
+ useFactory: () => ({ name: 'hello' }),
91
+ },
92
+ tokenMap,
93
+ );
94
+
95
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, TestModule);
96
+ expect(metadata).toBeDefined();
97
+ expect(metadata.exports).toContain(TEST_TOKEN_A);
98
+ });
99
+
100
+ test('should work end-to-end with AsyncProviderRegistry', async () => {
101
+ const registry = AsyncProviderRegistry.getInstance();
102
+ const container = new Container();
103
+
104
+ @Module({ providers: [] })
105
+ class TestModule {}
106
+
107
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, TestModule);
108
+
109
+ const tokenMap = new Map<symbol, (config: { port: number }) => unknown>();
110
+ tokenMap.set(TEST_TOKEN_A, (config) => ({ port: config.port }));
111
+
112
+ registerAsyncProviders(
113
+ TestModule,
114
+ {
115
+ useFactory: async () => ({ port: 5432 }),
116
+ },
117
+ tokenMap,
118
+ );
119
+
120
+ await registry.initializeAll(container);
121
+
122
+ const resolved = container.resolve<{ port: number }>(TEST_TOKEN_A);
123
+ expect(resolved.port).toBe(5432);
124
+ });
125
+ });
@@ -1,4 +1,3 @@
1
- import 'reflect-metadata';
2
1
  import { describe, expect, test, beforeEach } from 'bun:test';
3
2
  import { Container } from '../../src/di/container';
4
3
  import { Lifecycle } from '../../src/di/types';
@@ -0,0 +1,140 @@
1
+ import 'reflect-metadata';
2
+ import { describe, expect, test, beforeEach } from 'bun:test';
3
+ import { Application } from '../../src/core/application';
4
+ import { Module } from '../../src/di/module';
5
+ import { Injectable, Inject } from '../../src/di/decorators';
6
+ import { Controller } from '../../src/controller';
7
+ import { GET } from '../../src/router/decorators';
8
+ import type {
9
+ OnModuleInit,
10
+ OnModuleDestroy,
11
+ OnApplicationBootstrap,
12
+ OnApplicationShutdown,
13
+ } from '../../src/di/lifecycle';
14
+ import {
15
+ callOnModuleInit,
16
+ callOnModuleDestroy,
17
+ callOnApplicationBootstrap,
18
+ callOnApplicationShutdown,
19
+ } from '../../src/di/lifecycle';
20
+
21
+ describe('Lifecycle Hooks', () => {
22
+ describe('type guards and runners', () => {
23
+ test('callOnModuleInit should invoke onModuleInit', async () => {
24
+ const calls: string[] = [];
25
+ const instances = [
26
+ { onModuleInit: () => { calls.push('a'); } },
27
+ { onModuleInit: async () => { calls.push('b'); } },
28
+ { notAHook: true },
29
+ ];
30
+
31
+ await callOnModuleInit(instances);
32
+ expect(calls).toEqual(['a', 'b']);
33
+ });
34
+
35
+ test('callOnModuleDestroy should invoke in reverse order', async () => {
36
+ const calls: string[] = [];
37
+ const instances = [
38
+ { onModuleDestroy: () => { calls.push('first'); } },
39
+ { onModuleDestroy: () => { calls.push('second'); } },
40
+ { onModuleDestroy: () => { calls.push('third'); } },
41
+ ];
42
+
43
+ await callOnModuleDestroy(instances);
44
+ expect(calls).toEqual(['third', 'second', 'first']);
45
+ });
46
+
47
+ test('callOnApplicationBootstrap should invoke onApplicationBootstrap', async () => {
48
+ const calls: string[] = [];
49
+ const instances = [
50
+ { onApplicationBootstrap: () => { calls.push('boot'); } },
51
+ ];
52
+
53
+ await callOnApplicationBootstrap(instances);
54
+ expect(calls).toEqual(['boot']);
55
+ });
56
+
57
+ test('callOnApplicationShutdown should pass signal and call in reverse', async () => {
58
+ const calls: Array<{ name: string; signal?: string }> = [];
59
+ const instances = [
60
+ { onApplicationShutdown: (signal?: string) => { calls.push({ name: 'a', signal }); } },
61
+ { onApplicationShutdown: (signal?: string) => { calls.push({ name: 'b', signal }); } },
62
+ ];
63
+
64
+ await callOnApplicationShutdown(instances, 'SIGTERM');
65
+ expect(calls).toEqual([
66
+ { name: 'b', signal: 'SIGTERM' },
67
+ { name: 'a', signal: 'SIGTERM' },
68
+ ]);
69
+ });
70
+
71
+ test('should skip non-hook instances', async () => {
72
+ const calls: string[] = [];
73
+ const instances = [
74
+ null,
75
+ undefined,
76
+ 42,
77
+ 'string',
78
+ { someOther: true },
79
+ { onModuleInit: () => { calls.push('ok'); } },
80
+ ];
81
+
82
+ await callOnModuleInit(instances as unknown[]);
83
+ expect(calls).toEqual(['ok']);
84
+ });
85
+ });
86
+
87
+ describe('integration with Application', () => {
88
+ test('should call lifecycle hooks during start and stop', async () => {
89
+ const calls: string[] = [];
90
+
91
+ @Injectable()
92
+ class LifecycleService implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown {
93
+ public onModuleInit(): void {
94
+ calls.push('onModuleInit');
95
+ }
96
+ public onModuleDestroy(): void {
97
+ calls.push('onModuleDestroy');
98
+ }
99
+ public onApplicationBootstrap(): void {
100
+ calls.push('onApplicationBootstrap');
101
+ }
102
+ public onApplicationShutdown(signal?: string): void {
103
+ calls.push(`onApplicationShutdown:${signal ?? 'none'}`);
104
+ }
105
+ }
106
+
107
+ @Controller('/test')
108
+ class TestController {
109
+ @GET('/')
110
+ public get(): string {
111
+ return 'ok';
112
+ }
113
+ }
114
+
115
+ @Module({
116
+ controllers: [TestController],
117
+ providers: [LifecycleService],
118
+ })
119
+ class TestModule {}
120
+
121
+ const app = new Application({ port: 0, enableSignalHandlers: false });
122
+ app.registerModule(TestModule);
123
+ await app.listen(0);
124
+
125
+ expect(calls).toContain('onModuleInit');
126
+ expect(calls).toContain('onApplicationBootstrap');
127
+
128
+ const initIdx = calls.indexOf('onModuleInit');
129
+ const bootIdx = calls.indexOf('onApplicationBootstrap');
130
+ expect(initIdx).toBeLessThan(bootIdx);
131
+
132
+ await app.stop();
133
+
134
+ expect(calls).toContain('onModuleDestroy');
135
+ // onApplicationShutdown with no signal when using stop() directly
136
+ const shutdownEntry = calls.find((c) => c.startsWith('onApplicationShutdown'));
137
+ expect(shutdownEntry).toBeDefined();
138
+ });
139
+ });
140
+ });
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test, beforeEach } from 'bun:test';
2
- import 'reflect-metadata';
3
2
 
4
3
  import { handleError } from '../../src/error/handler';
5
4
  import { HttpException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException } from '../../src/error';
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test, beforeEach } from 'bun:test';
2
- import 'reflect-metadata';
3
2
  import {
4
3
  OnEvent,
5
4
  getOnEventMetadata,