@dangao/bun-server 2.0.8 → 2.1.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 (89) hide show
  1. package/README.md +4 -0
  2. package/dist/controller/controller.d.ts.map +1 -1
  3. package/dist/core/application.d.ts +6 -1
  4. package/dist/core/application.d.ts.map +1 -1
  5. package/dist/core/server.d.ts +5 -0
  6. package/dist/core/server.d.ts.map +1 -1
  7. package/dist/database/database-context.d.ts +25 -0
  8. package/dist/database/database-context.d.ts.map +1 -0
  9. package/dist/database/database-extension.d.ts +8 -9
  10. package/dist/database/database-extension.d.ts.map +1 -1
  11. package/dist/database/database-module.d.ts +7 -1
  12. package/dist/database/database-module.d.ts.map +1 -1
  13. package/dist/database/db-proxy.d.ts +12 -0
  14. package/dist/database/db-proxy.d.ts.map +1 -0
  15. package/dist/database/index.d.ts +6 -1
  16. package/dist/database/index.d.ts.map +1 -1
  17. package/dist/database/orm/transaction-interceptor.d.ts +0 -16
  18. package/dist/database/orm/transaction-interceptor.d.ts.map +1 -1
  19. package/dist/database/orm/transaction-manager.d.ts +10 -61
  20. package/dist/database/orm/transaction-manager.d.ts.map +1 -1
  21. package/dist/database/service.d.ts.map +1 -1
  22. package/dist/database/sql-manager.d.ts +14 -0
  23. package/dist/database/sql-manager.d.ts.map +1 -0
  24. package/dist/database/sqlite-adapter.d.ts +32 -0
  25. package/dist/database/sqlite-adapter.d.ts.map +1 -0
  26. package/dist/database/strategy-decorator.d.ts +8 -0
  27. package/dist/database/strategy-decorator.d.ts.map +1 -0
  28. package/dist/database/types.d.ts +122 -1
  29. package/dist/database/types.d.ts.map +1 -1
  30. package/dist/di/module-registry.d.ts.map +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3115 -2586
  34. package/dist/microservice/service-registry/service-registry-module.d.ts +16 -0
  35. package/dist/microservice/service-registry/service-registry-module.d.ts.map +1 -1
  36. package/dist/router/index.d.ts +1 -0
  37. package/dist/router/index.d.ts.map +1 -1
  38. package/dist/router/registry.d.ts +1 -1
  39. package/dist/router/registry.d.ts.map +1 -1
  40. package/dist/router/route.d.ts +2 -1
  41. package/dist/router/route.d.ts.map +1 -1
  42. package/dist/router/router.d.ts +1 -1
  43. package/dist/router/router.d.ts.map +1 -1
  44. package/dist/router/timeout-decorator.d.ts +6 -0
  45. package/dist/router/timeout-decorator.d.ts.map +1 -0
  46. package/docs/database.md +48 -15
  47. package/docs/idle-timeout.md +42 -0
  48. package/docs/lifecycle.md +6 -0
  49. package/docs/microservice-nacos.md +1 -0
  50. package/docs/microservice-service-registry.md +7 -0
  51. package/docs/zh/database.md +48 -15
  52. package/docs/zh/idle-timeout.md +41 -0
  53. package/docs/zh/lifecycle.md +5 -0
  54. package/docs/zh/microservice-nacos.md +1 -0
  55. package/docs/zh/microservice-service-registry.md +6 -0
  56. package/package.json +1 -1
  57. package/src/controller/controller.ts +11 -1
  58. package/src/core/application.ts +60 -1
  59. package/src/core/server.ts +10 -0
  60. package/src/database/database-context.ts +43 -0
  61. package/src/database/database-extension.ts +12 -45
  62. package/src/database/database-module.ts +254 -11
  63. package/src/database/db-proxy.ts +75 -0
  64. package/src/database/index.ts +29 -0
  65. package/src/database/orm/transaction-interceptor.ts +12 -149
  66. package/src/database/orm/transaction-manager.ts +143 -210
  67. package/src/database/service.ts +28 -2
  68. package/src/database/sql-manager.ts +62 -0
  69. package/src/database/sqlite-adapter.ts +121 -0
  70. package/src/database/strategy-decorator.ts +42 -0
  71. package/src/database/types.ts +133 -1
  72. package/src/di/module-registry.ts +20 -4
  73. package/src/index.ts +27 -1
  74. package/src/microservice/service-registry/service-registry-module.ts +25 -1
  75. package/src/router/index.ts +1 -0
  76. package/src/router/registry.ts +10 -1
  77. package/src/router/route.ts +31 -3
  78. package/src/router/router.ts +10 -1
  79. package/src/router/timeout-decorator.ts +35 -0
  80. package/tests/core/application.test.ts +10 -0
  81. package/tests/database/database-module.test.ts +91 -430
  82. package/tests/database/db-proxy.test.ts +93 -0
  83. package/tests/database/sql-manager.test.ts +43 -0
  84. package/tests/database/sqlite-adapter.test.ts +45 -0
  85. package/tests/database/strategy-decorator.test.ts +29 -0
  86. package/tests/database/transaction.test.ts +84 -222
  87. package/tests/di/lifecycle.test.ts +37 -0
  88. package/tests/microservice/service-registry.test.ts +15 -0
  89. package/tests/router/timeout-decorator.test.ts +48 -0
@@ -0,0 +1,62 @@
1
+ import { SQL } from 'bun';
2
+
3
+ import type { BunSQLConfig } from './types';
4
+
5
+ export class BunSQLManager {
6
+ private readonly instances = new Map<string, SQL>();
7
+ private defaultTenantId = 'default';
8
+
9
+ public getOrCreate(tenantId: string, config: BunSQLConfig): SQL {
10
+ const existing = this.instances.get(tenantId);
11
+ if (existing) {
12
+ return existing;
13
+ }
14
+
15
+ const pool = config.pool ?? {};
16
+ const sql = new SQL(config.url, {
17
+ max: pool.max ?? 10,
18
+ idleTimeout: pool.idleTimeout ?? 30,
19
+ maxLifetime: pool.maxLifetime ?? 0,
20
+ connectionTimeout: pool.connectionTimeout ?? 30000,
21
+ });
22
+ this.instances.set(tenantId, sql);
23
+ return sql;
24
+ }
25
+
26
+ public hasTenant(tenantId: string): boolean {
27
+ return this.instances.has(tenantId);
28
+ }
29
+
30
+ public get(tenantId: string): SQL | undefined {
31
+ return this.instances.get(tenantId);
32
+ }
33
+
34
+ public setDefaultTenant(tenantId: string): void {
35
+ this.defaultTenantId = tenantId;
36
+ }
37
+
38
+ public getDefault(): SQL {
39
+ const sql = this.instances.get(this.defaultTenantId);
40
+ if (!sql) {
41
+ throw new Error(
42
+ `[BunSQLManager] default tenant '${this.defaultTenantId}' not initialized`,
43
+ );
44
+ }
45
+ return sql;
46
+ }
47
+
48
+ public async destroy(tenantId: string, timeout = 10): Promise<void> {
49
+ const sql = this.instances.get(tenantId);
50
+ if (!sql) {
51
+ return;
52
+ }
53
+ await sql.close({ timeout });
54
+ this.instances.delete(tenantId);
55
+ }
56
+
57
+ public async destroyAll(timeout = 10): Promise<void> {
58
+ const tenantIds = Array.from(this.instances.keys());
59
+ await Promise.all(tenantIds.map((tenantId) => this.destroy(tenantId, timeout)));
60
+ }
61
+ }
62
+
@@ -0,0 +1,121 @@
1
+ import { Database, type SQLQueryBindings } from 'bun:sqlite';
2
+
3
+ import type { SqliteV2Config } from './types';
4
+
5
+ export interface DisposableLock {
6
+ [Symbol.dispose](): void;
7
+ }
8
+
9
+ export class Semaphore {
10
+ private active = 0;
11
+ private readonly queue: Array<() => void> = [];
12
+
13
+ public constructor(private readonly max: number) {}
14
+
15
+ public async acquire(): Promise<DisposableLock> {
16
+ if (this.active < this.max) {
17
+ this.active += 1;
18
+ return this.createDisposable();
19
+ }
20
+
21
+ await new Promise<void>((resolve) => this.queue.push(resolve));
22
+ this.active += 1;
23
+ return this.createDisposable();
24
+ }
25
+
26
+ private createDisposable(): DisposableLock {
27
+ let released = false;
28
+ return {
29
+ [Symbol.dispose]: () => {
30
+ if (released) {
31
+ return;
32
+ }
33
+ released = true;
34
+ this.active = Math.max(0, this.active - 1);
35
+ const next = this.queue.shift();
36
+ next?.();
37
+ },
38
+ };
39
+ }
40
+ }
41
+
42
+ export class SqliteAdapter {
43
+ private readonly db: Database;
44
+ public readonly semaphore: Semaphore;
45
+
46
+ public constructor(config: SqliteV2Config) {
47
+ this.db = new Database(config.database);
48
+ if (config.wal !== false) {
49
+ this.db.exec('PRAGMA journal_mode = WAL;');
50
+ }
51
+ this.semaphore = new Semaphore(config.maxWriteConcurrency ?? 1);
52
+ }
53
+
54
+ public query<T = unknown>(sql: string, params: SQLQueryBindings[] = []): T[] {
55
+ const stmt = this.db.query(sql);
56
+ return stmt.all(...params) as T[];
57
+ }
58
+
59
+ public async execute(sql: string, params: SQLQueryBindings[] = []): Promise<void> {
60
+ const stmt = this.db.query(sql);
61
+ stmt.run(...params);
62
+ }
63
+
64
+ public close(): void {
65
+ this.db.close();
66
+ }
67
+ }
68
+
69
+ export class SqliteManager {
70
+ private readonly instances = new Map<string, SqliteAdapter>();
71
+ private defaultTenantId = 'default';
72
+
73
+ public getOrCreate(tenantId: string, config: SqliteV2Config): SqliteAdapter {
74
+ const existing = this.instances.get(tenantId);
75
+ if (existing) {
76
+ return existing;
77
+ }
78
+ const adapter = new SqliteAdapter(config);
79
+ this.instances.set(tenantId, adapter);
80
+ return adapter;
81
+ }
82
+
83
+ public setDefaultTenant(tenantId: string): void {
84
+ this.defaultTenantId = tenantId;
85
+ }
86
+
87
+ public getDefault(): SqliteAdapter {
88
+ const adapter = this.instances.get(this.defaultTenantId);
89
+ if (!adapter) {
90
+ throw new Error(
91
+ `[SqliteManager] default tenant '${this.defaultTenantId}' not initialized`,
92
+ );
93
+ }
94
+ return adapter;
95
+ }
96
+
97
+ public getAdapter(tenantId: string): SqliteAdapter {
98
+ const adapter = this.instances.get(tenantId);
99
+ if (!adapter) {
100
+ throw new Error(`[SqliteManager] tenant '${tenantId}' not initialized`);
101
+ }
102
+ return adapter;
103
+ }
104
+
105
+ public destroy(tenantId: string): void {
106
+ const adapter = this.instances.get(tenantId);
107
+ if (!adapter) {
108
+ return;
109
+ }
110
+ adapter.close();
111
+ this.instances.delete(tenantId);
112
+ }
113
+
114
+ public destroyAll(): void {
115
+ for (const [tenantId, adapter] of this.instances.entries()) {
116
+ adapter.close();
117
+ this.instances.delete(tenantId);
118
+ }
119
+ }
120
+ }
121
+
@@ -0,0 +1,42 @@
1
+ import 'reflect-metadata';
2
+
3
+ import type { Constructor } from '@/core/types';
4
+
5
+ export const DB_STRATEGY_KEY = Symbol('@dangao/bun-server:database:strategy');
6
+
7
+ export type DbStrategyType = 'pool' | 'session';
8
+
9
+ export function DbStrategy(
10
+ strategy: DbStrategyType,
11
+ ): MethodDecorator & ClassDecorator {
12
+ return (target: object, propertyKey?: string | symbol) => {
13
+ if (propertyKey !== undefined) {
14
+ Reflect.defineMetadata(DB_STRATEGY_KEY, strategy, target, propertyKey);
15
+ return;
16
+ }
17
+ Reflect.defineMetadata(DB_STRATEGY_KEY, strategy, target);
18
+ };
19
+ }
20
+
21
+ export function Session(): MethodDecorator & ClassDecorator {
22
+ return DbStrategy('session');
23
+ }
24
+
25
+ export function getDbStrategy(
26
+ controllerClass: Constructor<unknown>,
27
+ methodName: string,
28
+ ): DbStrategyType | undefined {
29
+ const methodStrategy = Reflect.getMetadata(
30
+ DB_STRATEGY_KEY,
31
+ controllerClass.prototype,
32
+ methodName,
33
+ ) as DbStrategyType | undefined;
34
+ if (methodStrategy) {
35
+ return methodStrategy;
36
+ }
37
+ return Reflect.getMetadata(
38
+ DB_STRATEGY_KEY,
39
+ controllerClass,
40
+ ) as DbStrategyType | undefined;
41
+ }
42
+
@@ -78,6 +78,59 @@ export type DatabaseConfig =
78
78
  | { type: 'postgres'; config: PostgresConfig }
79
79
  | { type: 'mysql'; config: MysqlConfig };
80
80
 
81
+ /**
82
+ * Bun.SQL 连接池配置
83
+ */
84
+ export interface BunSQLPoolOptions {
85
+ /**
86
+ * 最大物理连接数
87
+ * @default 10
88
+ */
89
+ max?: number;
90
+ /**
91
+ * 空闲连接超时(秒)
92
+ * @default 30
93
+ */
94
+ idleTimeout?: number;
95
+ /**
96
+ * 连接最大生存时间(秒)
97
+ * @default 0
98
+ */
99
+ maxLifetime?: number;
100
+ /**
101
+ * 获取连接超时(毫秒)
102
+ * @default 30000
103
+ */
104
+ connectionTimeout?: number;
105
+ }
106
+
107
+ /**
108
+ * Bun.SQL 配置(Postgres/MySQL)
109
+ */
110
+ export interface BunSQLConfig {
111
+ type: 'postgres' | 'mysql';
112
+ url: string;
113
+ pool?: BunSQLPoolOptions;
114
+ }
115
+
116
+ /**
117
+ * SQLite V2 配置
118
+ */
119
+ export interface SqliteV2Config {
120
+ type: 'sqlite';
121
+ database: string;
122
+ wal?: boolean;
123
+ maxWriteConcurrency?: number;
124
+ }
125
+
126
+ /**
127
+ * 多租户配置
128
+ */
129
+ export interface TenantConfig {
130
+ id: string;
131
+ config: BunSQLConfig | SqliteV2Config;
132
+ }
133
+
81
134
  /**
82
135
  * 连接池配置
83
136
  */
@@ -108,12 +161,59 @@ export interface ConnectionPoolOptions {
108
161
  * DatabaseModule 配置选项
109
162
  */
110
163
  export interface DatabaseModuleOptions {
164
+ /**
165
+ * 全局默认连接策略
166
+ * - pool: 每次查询走共享池(默认)
167
+ * - session: 首次查询惰性 reserve,整个请求复用同一连接
168
+ * @default "pool"
169
+ */
170
+ defaultStrategy?: 'pool' | 'session';
171
+
172
+ /**
173
+ * 多租户配置
174
+ */
175
+ tenants?: TenantConfig[];
176
+ /**
177
+ * 默认租户 ID
178
+ * @default "default"
179
+ */
180
+ defaultTenant?: string;
181
+
182
+ /**
183
+ * 单租户:数据库类型(V2)
184
+ */
185
+ type?: DatabaseType;
186
+ /**
187
+ * 单租户:Postgres/MySQL URL(V2)
188
+ */
189
+ url?: string;
190
+ /**
191
+ * 单租户:SQLite 文件路径(V2)
192
+ */
193
+ databasePath?: string;
194
+ /**
195
+ * 单租户:SQLite WAL 模式
196
+ * @default true
197
+ */
198
+ wal?: boolean;
199
+ /**
200
+ * 单租户:SQLite 最大写并发
201
+ * @default 1
202
+ */
203
+ maxWriteConcurrency?: number;
204
+ /**
205
+ * Bun.SQL 连接池参数(V2)
206
+ */
207
+ bunSqlPool?: BunSQLPoolOptions;
208
+
111
209
  /**
112
210
  * 数据库配置
211
+ * @deprecated 使用 V2 字段(type/url/databasePath)或 tenants
113
212
  */
114
- database: DatabaseConfig;
213
+ database?: DatabaseConfig;
115
214
  /**
116
215
  * 连接池配置
216
+ * @deprecated 旧连接池配置,仅用于兼容旧 DatabaseService
117
217
  */
118
218
  pool?: ConnectionPoolOptions;
119
219
  /**
@@ -135,6 +235,23 @@ export interface DatabaseModuleOptions {
135
235
  */
136
236
  drizzle?: unknown;
137
237
  };
238
+
239
+ /**
240
+ * @deprecated 旧字段兼容(用于构造 URL)
241
+ */
242
+ host?: string;
243
+ /**
244
+ * @deprecated 旧字段兼容(用于构造 URL)
245
+ */
246
+ port?: number;
247
+ /**
248
+ * @deprecated 旧字段兼容(用于构造 URL)
249
+ */
250
+ username?: string;
251
+ /**
252
+ * @deprecated 旧字段兼容(用于构造 URL)
253
+ */
254
+ password?: string;
138
255
  }
139
256
 
140
257
  /**
@@ -169,3 +286,18 @@ export const DATABASE_SERVICE_TOKEN = Symbol('@dangao/bun-server:database:servic
169
286
  * DatabaseModule Options Token
170
287
  */
171
288
  export const DATABASE_OPTIONS_TOKEN = Symbol('@dangao/bun-server:database:options');
289
+
290
+ /**
291
+ * BunSQLManager Token
292
+ */
293
+ export const BUN_SQL_MANAGER_TOKEN = Symbol('@dangao/bun-server:database:bun-sql-manager');
294
+
295
+ /**
296
+ * SQLite manager Token
297
+ */
298
+ export const SQLITE_MANAGER_TOKEN = Symbol('@dangao/bun-server:database:sqlite-manager');
299
+
300
+ /**
301
+ * db proxy Token
302
+ */
303
+ export const DB_TOKEN = Symbol('@dangao/bun-server:database:db');
@@ -229,18 +229,34 @@ export class ModuleRegistry {
229
229
  */
230
230
  public resolveAllProviderInstances(): unknown[] {
231
231
  const instances: unknown[] = [];
232
+ const seen = new Set<unknown>();
232
233
  for (const [, ref] of this.moduleRefs) {
233
234
  for (const provider of ref.metadata.providers) {
234
235
  try {
235
236
  if (typeof provider === 'function') {
236
- instances.push(ref.container.resolve(provider));
237
+ const instance = ref.container.resolve(provider);
238
+ if (!seen.has(instance)) {
239
+ seen.add(instance);
240
+ instances.push(instance);
241
+ }
237
242
  } else if ('useClass' in provider) {
238
243
  const token = provider.provide ?? provider.useClass;
239
- instances.push(ref.container.resolve(token as Constructor<unknown>));
244
+ const instance = ref.container.resolve(token as Constructor<unknown>);
245
+ if (!seen.has(instance)) {
246
+ seen.add(instance);
247
+ instances.push(instance);
248
+ }
240
249
  } else if ('useValue' in provider) {
241
- instances.push(provider.useValue);
250
+ if (!seen.has(provider.useValue)) {
251
+ seen.add(provider.useValue);
252
+ instances.push(provider.useValue);
253
+ }
242
254
  } else if ('useFactory' in provider) {
243
- instances.push(ref.container.resolve(provider.provide as Constructor<unknown>));
255
+ const instance = ref.container.resolve(provider.provide as Constructor<unknown>);
256
+ if (!seen.has(instance)) {
257
+ seen.add(instance);
258
+ instances.push(instance);
259
+ }
244
260
  }
245
261
  } catch (_error) {
246
262
  // skip providers that can't be resolved (e.g. pending async providers)
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export { BunServer, type ServerOptions } from './core/server';
4
4
  export { ClusterManager, type ClusterOptions, type ClusterMode } from './core/cluster';
5
5
  export { Context } from './core/context';
6
6
  export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './core/context-service';
7
- export { Route, Router, RouteRegistry } from './router';
7
+ export { Route, Router, RouteRegistry, IdleTimeout } from './router';
8
8
  export { GET, POST, PUT, DELETE, PATCH } from './router/decorators';
9
9
  export type { HttpMethod, RouteHandler, RouteMatch } from './router/types';
10
10
  export { BodyParser, RequestWrapper, ResponseBuilder } from './request';
@@ -307,16 +307,41 @@ export {
307
307
  ConnectionPool,
308
308
  DatabaseHealthIndicator,
309
309
  DatabaseExtension,
310
+ BunSQLManager,
311
+ SqliteAdapter,
312
+ SqliteManager,
313
+ Semaphore,
314
+ db,
315
+ initDbProxy,
316
+ DbStrategy,
317
+ DbSession,
318
+ DB_STRATEGY_KEY,
319
+ getDbStrategy,
320
+ databaseSessionStore,
321
+ getCurrentSession,
322
+ runWithSession,
310
323
  DATABASE_SERVICE_TOKEN,
311
324
  DATABASE_OPTIONS_TOKEN,
325
+ BUN_SQL_MANAGER_TOKEN,
326
+ SQLITE_MANAGER_TOKEN,
327
+ DB_TOKEN,
312
328
  type DatabaseModuleOptions,
313
329
  type DatabaseConfig,
314
330
  type DatabaseType,
315
331
  type ConnectionInfo,
316
332
  type ConnectionPoolOptions,
333
+ type BunSQLConfig,
334
+ type BunSQLPoolOptions,
317
335
  type SqliteConfig,
336
+ type SqliteV2Config,
318
337
  type PostgresConfig,
319
338
  type MysqlConfig,
339
+ type TenantConfig,
340
+ type DbStrategyType,
341
+ type DbProxy,
342
+ type DatabaseSession,
343
+ type TransactionState as DatabaseTransactionState,
344
+ type ReservedSqlSession,
320
345
  // ORM exports
321
346
  Entity,
322
347
  Column,
@@ -472,6 +497,7 @@ export {
472
497
  } from './microservice';
473
498
  export {
474
499
  ServiceRegistryModule,
500
+ ServiceRegistryDecorator,
475
501
  NacosServiceRegistry,
476
502
  SERVICE_REGISTRY_TOKEN,
477
503
  type ServiceRegistryModuleOptions,
@@ -2,7 +2,11 @@ import { Module, MODULE_METADATA_KEY, type ModuleProvider } from '../../di/modul
2
2
  import { NacosClient } from '@dangao/nacos-client';
3
3
  import type { NacosClientOptions } from '@dangao/nacos-client';
4
4
  import { NacosServiceRegistry } from './nacos-service-registry';
5
- import { SERVICE_REGISTRY_TOKEN, type ServiceRegistry } from './types';
5
+ import {
6
+ SERVICE_REGISTRY_TOKEN,
7
+ type ServiceRegistry,
8
+ type ServiceInstance,
9
+ } from './types';
6
10
 
7
11
  /**
8
12
  * 服务注册中心 Provider 类型
@@ -45,6 +49,21 @@ export interface ServiceRegistryModuleOptions {
45
49
  */
46
50
  nacos?: NacosServiceRegistryOptions;
47
51
 
52
+ /**
53
+ * 是否自动在 Application.listen 时注册带 @ServiceRegistry 的服务
54
+ * @default true
55
+ */
56
+ autoRegister?: boolean;
57
+
58
+ /**
59
+ * 通过模块配置自动注册服务(无需 @ServiceRegistry 装饰器)
60
+ * 当 autoRegister=true 时在 Application.listen 阶段自动注册
61
+ */
62
+ autoRegisterService?: Omit<ServiceInstance, 'ip' | 'port'> & {
63
+ ip?: string;
64
+ port?: number;
65
+ };
66
+
48
67
  // 未来可以添加其他 provider 的配置
49
68
  // consul?: ConsulServiceRegistryOptions;
50
69
  // eureka?: EurekaServiceRegistryOptions;
@@ -57,11 +76,16 @@ export interface ServiceRegistryModuleOptions {
57
76
  providers: [],
58
77
  })
59
78
  export class ServiceRegistryModule {
79
+ public static autoRegister = true;
80
+ public static autoRegisterService: ServiceRegistryModuleOptions['autoRegisterService'];
81
+
60
82
  /**
61
83
  * 创建服务注册中心模块
62
84
  * @param options - 模块配置
63
85
  */
64
86
  public static forRoot(options: ServiceRegistryModuleOptions): typeof ServiceRegistryModule {
87
+ ServiceRegistryModule.autoRegister = options.autoRegister ?? true;
88
+ ServiceRegistryModule.autoRegisterService = options.autoRegisterService;
65
89
  const providers: ModuleProvider[] = [];
66
90
 
67
91
  let serviceRegistry: ServiceRegistry;
@@ -2,5 +2,6 @@ export { Route } from './route';
2
2
  export { Router } from './router';
3
3
  export { RouteRegistry } from './registry';
4
4
  export { GET, POST, PUT, DELETE, PATCH } from './decorators';
5
+ export { IdleTimeout, IDLE_TIMEOUT_KEY, getIdleTimeout } from './timeout-decorator';
5
6
  export type { HttpMethod, RouteHandler, RouteMatch } from './types';
6
7
 
@@ -42,8 +42,17 @@ export class RouteRegistry {
42
42
  middlewares: Middleware[] = [],
43
43
  controllerClass?: Constructor<unknown>,
44
44
  methodName?: string,
45
+ timeout?: number,
45
46
  ): void {
46
- this.router.register(method, path, handler, middlewares, controllerClass, methodName);
47
+ this.router.register(
48
+ method,
49
+ path,
50
+ handler,
51
+ middlewares,
52
+ controllerClass,
53
+ methodName,
54
+ timeout,
55
+ );
47
56
  }
48
57
 
49
58
  /**
@@ -3,6 +3,7 @@ import type { Constructor } from '../core/types';
3
3
  import type { HttpMethod, RouteHandler, RouteMatch } from './types';
4
4
  import { MiddlewarePipeline } from '../middleware/pipeline';
5
5
  import type { Middleware } from '../middleware';
6
+ import { HttpException } from '../error';
6
7
 
7
8
  /**
8
9
  * 路由类
@@ -47,6 +48,7 @@ export class Route {
47
48
  private readonly middlewarePipeline: MiddlewarePipeline | null;
48
49
  private readonly staticKey?: string;
49
50
  public readonly isStatic: boolean;
51
+ private readonly timeout?: number;
50
52
 
51
53
  public constructor(
52
54
  method: HttpMethod,
@@ -55,12 +57,14 @@ export class Route {
55
57
  middlewares: Middleware[] = [],
56
58
  controllerClass?: Constructor<unknown>,
57
59
  methodName?: string,
60
+ timeout?: number,
58
61
  ) {
59
62
  this.method = method;
60
63
  this.path = path;
61
64
  this.handler = handler;
62
65
  this.controllerClass = controllerClass;
63
66
  this.methodName = methodName;
67
+ this.timeout = timeout;
64
68
 
65
69
  this.isStatic = !path.includes(':') && !path.includes('*');
66
70
  if (this.isStatic) {
@@ -124,11 +128,35 @@ export class Route {
124
128
  * @returns 响应对象
125
129
  */
126
130
  public async execute(context: Context): Promise<Response> {
127
- if (!this.middlewarePipeline || !this.middlewarePipeline.hasMiddlewares()) {
128
- return await this.handler(context);
131
+ const executeHandler = async (): Promise<Response> => {
132
+ if (!this.middlewarePipeline || !this.middlewarePipeline.hasMiddlewares()) {
133
+ return await this.handler(context);
134
+ }
135
+ return await this.middlewarePipeline.run(
136
+ context,
137
+ async () => await this.handler(context),
138
+ );
139
+ };
140
+
141
+ if (!this.timeout || this.timeout <= 0) {
142
+ return await executeHandler();
129
143
  }
130
144
 
131
- return await this.middlewarePipeline.run(context, async () => this.handler(context));
145
+ let timer: ReturnType<typeof setTimeout> | undefined;
146
+ const timeoutPromise = new Promise<Response>((_, reject) => {
147
+ timer = setTimeout(
148
+ () => reject(new HttpException(408, 'Request Timeout')),
149
+ this.timeout,
150
+ );
151
+ });
152
+
153
+ try {
154
+ return await Promise.race([executeHandler(), timeoutPromise]);
155
+ } finally {
156
+ if (timer) {
157
+ clearTimeout(timer);
158
+ }
159
+ }
132
160
  }
133
161
 
134
162
  /**
@@ -49,10 +49,19 @@ export class Router {
49
49
  middlewares: Middleware[] = [],
50
50
  controllerClass?: Constructor<unknown>,
51
51
  methodName?: string,
52
+ timeout?: number,
52
53
  ): void {
53
54
  // 规范化路径
54
55
  const normalizedPath = this.normalizePath(path);
55
- const route = new Route(method, normalizedPath, handler, middlewares, controllerClass, methodName);
56
+ const route = new Route(
57
+ method,
58
+ normalizedPath,
59
+ handler,
60
+ middlewares,
61
+ controllerClass,
62
+ methodName,
63
+ timeout,
64
+ );
56
65
  this.routes.push(route);
57
66
  const staticKey = route.getStaticKey();
58
67
  if (staticKey) {
@@ -0,0 +1,35 @@
1
+ import 'reflect-metadata';
2
+
3
+ import type { Constructor } from '../core/types';
4
+
5
+ export const IDLE_TIMEOUT_KEY = Symbol('@dangao/bun-server:route:idle-timeout');
6
+
7
+ export function IdleTimeout(ms: number): MethodDecorator & ClassDecorator {
8
+ return (target: object, propertyKey?: string | symbol) => {
9
+ if (propertyKey !== undefined) {
10
+ Reflect.defineMetadata(IDLE_TIMEOUT_KEY, ms, target, propertyKey);
11
+ return;
12
+ }
13
+ Reflect.defineMetadata(IDLE_TIMEOUT_KEY, ms, target);
14
+ };
15
+ }
16
+
17
+ export function getIdleTimeout(
18
+ controllerClass: Constructor<unknown>,
19
+ methodName: string,
20
+ ): number | undefined {
21
+ const methodTimeout = Reflect.getMetadata(
22
+ IDLE_TIMEOUT_KEY,
23
+ controllerClass.prototype,
24
+ methodName,
25
+ ) as number | undefined;
26
+ if (typeof methodTimeout === 'number') {
27
+ return methodTimeout;
28
+ }
29
+ const classTimeout = Reflect.getMetadata(
30
+ IDLE_TIMEOUT_KEY,
31
+ controllerClass,
32
+ ) as number | undefined;
33
+ return typeof classTimeout === 'number' ? classTimeout : undefined;
34
+ }
35
+