@dangao/bun-server 1.1.4 → 1.2.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,223 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import { Container } from '../../src/di/container';
3
+ import { Injectable } from '../../src/di/decorators';
4
+ import { Lifecycle } from '../../src/di/types';
5
+ import { contextStore } from '../../src/core/context-service';
6
+ import { Context } from '../../src/core/context';
7
+
8
+ describe('Lifecycle.Scoped', () => {
9
+ let container: Container;
10
+
11
+ beforeEach(() => {
12
+ container = new Container();
13
+ });
14
+
15
+ test('should create new instance for each request', async () => {
16
+ @Injectable({ lifecycle: Lifecycle.Scoped })
17
+ class ScopedService {
18
+ public readonly id: string;
19
+
20
+ public constructor() {
21
+ this.id = Math.random().toString(36).substring(7);
22
+ }
23
+ }
24
+
25
+ container.register(ScopedService);
26
+
27
+ const request1 = new Request('http://localhost:3000/api/test1');
28
+ const request2 = new Request('http://localhost:3000/api/test2');
29
+ const context1 = new Context(request1);
30
+ const context2 = new Context(request2);
31
+
32
+ let instance1: ScopedService;
33
+ let instance2: ScopedService;
34
+
35
+ await contextStore.run(context1, async () => {
36
+ instance1 = container.resolve(ScopedService);
37
+ });
38
+
39
+ await contextStore.run(context2, async () => {
40
+ instance2 = container.resolve(ScopedService);
41
+ });
42
+
43
+ // 每个请求应该有独立的实例
44
+ expect(instance1!.id).not.toBe(instance2!.id);
45
+ });
46
+
47
+ test('should reuse instance within same request', async () => {
48
+ @Injectable({ lifecycle: Lifecycle.Scoped })
49
+ class ScopedService {
50
+ public readonly id: string;
51
+
52
+ public constructor() {
53
+ this.id = Math.random().toString(36).substring(7);
54
+ }
55
+ }
56
+
57
+ container.register(ScopedService);
58
+
59
+ const request = new Request('http://localhost:3000/api/test');
60
+ const context = new Context(request);
61
+
62
+ let instance1: ScopedService;
63
+ let instance2: ScopedService;
64
+
65
+ await contextStore.run(context, async () => {
66
+ instance1 = container.resolve(ScopedService);
67
+ instance2 = container.resolve(ScopedService);
68
+ });
69
+
70
+ // 同一请求内应该复用同一个实例
71
+ expect(instance1!.id).toBe(instance2!.id);
72
+ });
73
+
74
+ test('should handle scoped service with dependencies', async () => {
75
+ @Injectable({ lifecycle: Lifecycle.Singleton })
76
+ class SingletonService {
77
+ public readonly id = 'singleton';
78
+ }
79
+
80
+ @Injectable({ lifecycle: Lifecycle.Scoped })
81
+ class ScopedService {
82
+ public constructor(public readonly singleton: SingletonService) {}
83
+ }
84
+
85
+ container.register(SingletonService);
86
+ container.register(ScopedService);
87
+
88
+ const request = new Request('http://localhost:3000/api/test');
89
+ const context = new Context(request);
90
+
91
+ let scopedInstance: ScopedService;
92
+
93
+ await contextStore.run(context, async () => {
94
+ scopedInstance = container.resolve(ScopedService);
95
+ });
96
+
97
+ expect(scopedInstance!.singleton).toBeDefined();
98
+ expect(scopedInstance!.singleton.id).toBe('singleton');
99
+ });
100
+
101
+ test('should handle scoped service depending on another scoped service', async () => {
102
+ @Injectable({ lifecycle: Lifecycle.Scoped })
103
+ class ScopedServiceA {
104
+ public readonly id = 'A';
105
+ }
106
+
107
+ @Injectable({ lifecycle: Lifecycle.Scoped })
108
+ class ScopedServiceB {
109
+ public constructor(public readonly serviceA: ScopedServiceA) {}
110
+ }
111
+
112
+ container.register(ScopedServiceA);
113
+ container.register(ScopedServiceB);
114
+
115
+ const request = new Request('http://localhost:3000/api/test');
116
+ const context = new Context(request);
117
+
118
+ let serviceB: ScopedServiceB;
119
+ let serviceA: ScopedServiceA;
120
+
121
+ await contextStore.run(context, async () => {
122
+ serviceB = container.resolve(ScopedServiceB);
123
+ serviceA = container.resolve(ScopedServiceA);
124
+ });
125
+
126
+ // 同一请求内,ScopedServiceB 应该注入同一个 ScopedServiceA 实例
127
+ expect(serviceB!.serviceA).toBe(serviceA!);
128
+ });
129
+
130
+ test('should return undefined when resolving scoped service outside request context', () => {
131
+ @Injectable({ lifecycle: Lifecycle.Scoped })
132
+ class ScopedService {}
133
+
134
+ container.register(ScopedService);
135
+
136
+ // 不在请求上下文中,应该能够解析(但实例不会被缓存)
137
+ const instance = container.resolve(ScopedService);
138
+ expect(instance).toBeDefined();
139
+ expect(instance).toBeInstanceOf(ScopedService);
140
+ });
141
+
142
+ test('should handle concurrent requests with scoped services', async () => {
143
+ @Injectable({ lifecycle: Lifecycle.Scoped })
144
+ class ScopedService {
145
+ public readonly requestId: string;
146
+
147
+ public constructor() {
148
+ this.requestId = Math.random().toString(36).substring(7);
149
+ }
150
+ }
151
+
152
+ container.register(ScopedService);
153
+
154
+ const request1 = new Request('http://localhost:3000/api/test1');
155
+ const request2 = new Request('http://localhost:3000/api/test2');
156
+ const context1 = new Context(request1);
157
+ const context2 = new Context(request2);
158
+
159
+ const promises = [
160
+ contextStore.run(context1, async () => {
161
+ const instance1 = container.resolve(ScopedService);
162
+ await new Promise(resolve => setTimeout(resolve, 10));
163
+ const instance2 = container.resolve(ScopedService);
164
+ return { instance1: instance1.requestId, instance2: instance2.requestId };
165
+ }),
166
+ contextStore.run(context2, async () => {
167
+ const instance1 = container.resolve(ScopedService);
168
+ await new Promise(resolve => setTimeout(resolve, 10));
169
+ const instance2 = container.resolve(ScopedService);
170
+ return { instance1: instance1.requestId, instance2: instance2.requestId };
171
+ }),
172
+ ];
173
+
174
+ const results = await Promise.all(promises);
175
+
176
+ // 每个请求内的实例应该相同
177
+ expect(results[0].instance1).toBe(results[0].instance2);
178
+ expect(results[1].instance1).toBe(results[1].instance2);
179
+
180
+ // 不同请求的实例应该不同
181
+ expect(results[0].instance1).not.toBe(results[1].instance1);
182
+ });
183
+
184
+ test('should handle scoped service with factory', async () => {
185
+ let factoryCallCount = 0;
186
+
187
+ container.register('ScopedService', {
188
+ lifecycle: Lifecycle.Scoped,
189
+ factory: () => {
190
+ factoryCallCount++;
191
+ return { id: factoryCallCount };
192
+ },
193
+ });
194
+
195
+ const request1 = new Request('http://localhost:3000/api/test1');
196
+ const request2 = new Request('http://localhost:3000/api/test2');
197
+ const context1 = new Context(request1);
198
+ const context2 = new Context(request2);
199
+
200
+ let instance1: { id: number };
201
+ let instance2: { id: number };
202
+ let instance1Again: { id: number };
203
+
204
+ await contextStore.run(context1, async () => {
205
+ instance1 = container.resolve<{ id: number }>('ScopedService');
206
+ instance1Again = container.resolve<{ id: number }>('ScopedService');
207
+ });
208
+
209
+ await contextStore.run(context2, async () => {
210
+ instance2 = container.resolve<{ id: number }>('ScopedService');
211
+ });
212
+
213
+ // 同一请求内应该复用实例
214
+ expect(instance1!.id).toBe(instance1Again!.id);
215
+
216
+ // 不同请求应该创建新实例
217
+ expect(instance1!.id).not.toBe(instance2!.id);
218
+
219
+ // 工厂函数应该被调用 2 次(每个请求一次)
220
+ expect(factoryCallCount).toBe(2);
221
+ });
222
+ });
223
+
@@ -1,20 +1,25 @@
1
- import { describe, expect, test, beforeEach } from 'bun:test';
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
2
  import { Application } from '../../src/core/application';
3
3
  import { Context } from '../../src/core/context';
4
4
  import { Controller } from '../../src/controller';
5
5
  import { GET } from '../../src/router/decorators';
6
6
  import { createRateLimitMiddleware, MemoryRateLimitStore, type RateLimitOptions } from '../../src/middleware/builtin/rate-limit';
7
7
  import { RateLimit } from '../../src/middleware/decorators';
8
+ import { getTestPort } from '../utils/test-port';
8
9
 
9
10
  describe('Rate Limit Middleware', () => {
10
11
  let app: Application;
11
12
  let port: number;
12
13
 
13
14
  beforeEach(() => {
14
- port = 3000 + Math.floor(Math.random() * 10000);
15
+ port = getTestPort();
15
16
  app = new Application({ port });
16
17
  });
17
18
 
19
+ afterEach(async () => {
20
+ await app.stop();
21
+ });
22
+
18
23
  test('should allow requests within limit', async () => {
19
24
  const middleware = createRateLimitMiddleware({
20
25
  max: 5,
@@ -1,10 +1,29 @@
1
1
  /**
2
2
  * 获取测试专用端口
3
- * 使用随机高位端口,降低并行测试的端口冲突概率
3
+ * 使用随机高位端口,并主动探测端口可用性,降低并行测试/系统占用导致的端口冲突概率
4
4
  * @returns 不同测试之间不冲突的端口号
5
5
  */
6
6
  export function getTestPort(): number {
7
7
  // 选择 30000-59999 之间的随机端口,避免常用端口冲突
8
- return 30000 + Math.floor(Math.random() * 30000);
8
+ // 并通过 Bun.serve 探测端口是否可用(可用则立刻 stop),避免 EADDRINUSE 造成测试雪崩失败
9
+ let lastError: unknown;
10
+ for (let i = 0; i < 50; i++) {
11
+ const port = 30000 + Math.floor(Math.random() * 30000);
12
+ try {
13
+ const probe = Bun.serve({
14
+ port,
15
+ fetch: () => new Response('ok'),
16
+ });
17
+ probe.stop();
18
+ return port;
19
+ } catch (error) {
20
+ lastError = error;
21
+ // 端口占用,继续尝试
22
+ }
23
+ }
24
+
25
+ throw new Error(
26
+ `Unable to find an available test port. Last error: ${String(lastError)}`,
27
+ );
9
28
  }
10
29
 
File without changes