@dangao/bun-server 2.0.8 → 2.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.
Files changed (97) 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/container.d.ts +16 -0
  31. package/dist/di/container.d.ts.map +1 -1
  32. package/dist/di/lifecycle.d.ts +48 -0
  33. package/dist/di/lifecycle.d.ts.map +1 -1
  34. package/dist/di/module-registry.d.ts +10 -6
  35. package/dist/di/module-registry.d.ts.map +1 -1
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3267 -2620
  39. package/dist/microservice/service-registry/service-registry-module.d.ts +16 -0
  40. package/dist/microservice/service-registry/service-registry-module.d.ts.map +1 -1
  41. package/dist/router/index.d.ts +1 -0
  42. package/dist/router/index.d.ts.map +1 -1
  43. package/dist/router/registry.d.ts +1 -1
  44. package/dist/router/registry.d.ts.map +1 -1
  45. package/dist/router/route.d.ts +2 -1
  46. package/dist/router/route.d.ts.map +1 -1
  47. package/dist/router/router.d.ts +1 -1
  48. package/dist/router/router.d.ts.map +1 -1
  49. package/dist/router/timeout-decorator.d.ts +6 -0
  50. package/dist/router/timeout-decorator.d.ts.map +1 -0
  51. package/docs/database.md +48 -15
  52. package/docs/idle-timeout.md +42 -0
  53. package/docs/lifecycle.md +80 -4
  54. package/docs/microservice-nacos.md +1 -0
  55. package/docs/microservice-service-registry.md +7 -0
  56. package/docs/zh/database.md +48 -15
  57. package/docs/zh/idle-timeout.md +41 -0
  58. package/docs/zh/lifecycle.md +49 -5
  59. package/docs/zh/microservice-nacos.md +1 -0
  60. package/docs/zh/microservice-service-registry.md +6 -0
  61. package/package.json +1 -1
  62. package/src/controller/controller.ts +11 -1
  63. package/src/core/application.ts +98 -26
  64. package/src/core/server.ts +10 -0
  65. package/src/database/database-context.ts +43 -0
  66. package/src/database/database-extension.ts +12 -45
  67. package/src/database/database-module.ts +254 -11
  68. package/src/database/db-proxy.ts +75 -0
  69. package/src/database/index.ts +29 -0
  70. package/src/database/orm/transaction-interceptor.ts +12 -149
  71. package/src/database/orm/transaction-manager.ts +143 -210
  72. package/src/database/service.ts +28 -2
  73. package/src/database/sql-manager.ts +62 -0
  74. package/src/database/sqlite-adapter.ts +121 -0
  75. package/src/database/strategy-decorator.ts +42 -0
  76. package/src/database/types.ts +133 -1
  77. package/src/di/container.ts +55 -1
  78. package/src/di/lifecycle.ts +114 -0
  79. package/src/di/module-registry.ts +78 -14
  80. package/src/index.ts +31 -1
  81. package/src/microservice/service-registry/service-registry-module.ts +25 -1
  82. package/src/router/index.ts +1 -0
  83. package/src/router/registry.ts +10 -1
  84. package/src/router/route.ts +31 -3
  85. package/src/router/router.ts +10 -1
  86. package/src/router/timeout-decorator.ts +35 -0
  87. package/tests/core/application.test.ts +10 -0
  88. package/tests/database/database-module.test.ts +91 -430
  89. package/tests/database/db-proxy.test.ts +93 -0
  90. package/tests/database/sql-manager.test.ts +43 -0
  91. package/tests/database/sqlite-adapter.test.ts +45 -0
  92. package/tests/database/strategy-decorator.test.ts +29 -0
  93. package/tests/database/transaction.test.ts +84 -222
  94. package/tests/di/lifecycle.test.ts +139 -1
  95. package/tests/di/scoped-lifecycle.test.ts +61 -0
  96. package/tests/microservice/service-registry.test.ts +15 -0
  97. package/tests/router/timeout-decorator.test.ts +48 -0
@@ -1,8 +1,8 @@
1
- import { Injectable, Inject } from '../../di/decorators';
2
- import { DATABASE_SERVICE_TOKEN } from '../types';
3
- import type { DatabaseService } from '../service';
1
+ import { Inject, Injectable } from '../../di/decorators';
2
+ import { getCurrentSession, runWithSession } from '../database-context';
3
+ import { BUN_SQL_MANAGER_TOKEN } from '../types';
4
+ import type { BunSQLManager } from '../sql-manager';
4
5
  import {
5
- Propagation,
6
6
  IsolationLevel,
7
7
  TransactionStatus,
8
8
  type TransactionOptions,
@@ -15,262 +15,195 @@ import {
15
15
  */
16
16
  @Injectable()
17
17
  export class TransactionManager {
18
- private readonly transactions = new Map<string, TransactionContext>();
19
- private readonly connectionTransactions = new Map<unknown, string>(); // connection -> transactionId
20
-
21
18
  public constructor(
22
- @Inject(DATABASE_SERVICE_TOKEN)
23
- private readonly databaseService: DatabaseService,
19
+ @Inject(BUN_SQL_MANAGER_TOKEN)
20
+ private readonly sqlManager: BunSQLManager,
24
21
  ) {}
25
22
 
26
23
  /**
27
- * 开始事务
24
+ * 在当前 session 中执行事务
28
25
  */
29
- public async beginTransaction(
26
+ public async runInTransaction<T>(
27
+ fn: () => Promise<T>,
30
28
  options: TransactionOptions = {},
31
- ): Promise<TransactionContext> {
32
- const transactionId = this.generateTransactionId();
33
- const context: TransactionContext = {
34
- id: transactionId,
35
- status: TransactionStatus.ACTIVE,
36
- startTime: Date.now(),
37
- level: 0,
38
- savepoints: [],
39
- };
40
-
41
- // 获取数据库连接
42
- const connection = await this.databaseService.getConnection();
43
- this.connectionTransactions.set(connection, transactionId);
44
- this.transactions.set(transactionId, context);
45
-
46
- // 开始事务(根据数据库类型)
47
- await this.executeBegin(connection, options);
48
-
49
- return context;
50
- }
51
-
52
- /**
53
- * 提交事务
54
- */
55
- public async commitTransaction(transactionId: string): Promise<void> {
56
- const context = this.transactions.get(transactionId);
57
- if (!context) {
58
- throw new Error(`Transaction ${transactionId} not found`);
29
+ ): Promise<T> {
30
+ const session = getCurrentSession();
31
+ if (!session) {
32
+ throw new Error(
33
+ '[TransactionManager] No database session in current request context',
34
+ );
59
35
  }
60
36
 
61
- if (context.status !== TransactionStatus.ACTIVE) {
62
- throw new Error(`Transaction ${transactionId} is not active`);
37
+ if (session.sqlite) {
38
+ return await this.runInSqliteTransaction(fn);
63
39
  }
64
40
 
65
- // 查找连接
66
- const connection = this.findConnectionByTransactionId(transactionId);
67
- if (!connection) {
68
- throw new Error(`Connection not found for transaction ${transactionId}`);
41
+ let reserved = session.reserved;
42
+ if (!reserved && session.lazyReserve) {
43
+ reserved = await session.lazyReserve();
44
+ }
45
+ if (!reserved) {
46
+ throw new Error(
47
+ '[TransactionManager] No reserved session in current context. Add @Session() or @DbStrategy("session").',
48
+ );
69
49
  }
70
50
 
71
- // 提交事务
72
- await this.executeCommit(connection);
73
-
74
- context.status = TransactionStatus.COMMITTED;
75
- this.cleanupTransaction(transactionId);
76
- }
51
+ const transactionId = this.generateTransactionId();
52
+ session.transaction = {
53
+ id: transactionId,
54
+ status: TransactionStatus.ACTIVE,
55
+ level: 0,
56
+ savepoints: [],
57
+ };
77
58
 
78
- /**
79
- * 回滚事务
80
- */
81
- public async rollbackTransaction(transactionId: string): Promise<void> {
82
- const context = this.transactions.get(transactionId);
83
- if (!context) {
84
- throw new Error(`Transaction ${transactionId} not found`);
59
+ if (options.isolationLevel) {
60
+ const isolationLevel = this.getIsolationLevelSQL(options.isolationLevel);
61
+ if (isolationLevel) {
62
+ await reserved`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`;
63
+ }
85
64
  }
86
65
 
87
- // 查找连接
88
- const connection = this.findConnectionByTransactionId(transactionId);
89
- if (!connection) {
90
- throw new Error(`Connection not found for transaction ${transactionId}`);
66
+ try {
67
+ const result = await reserved.begin(fn);
68
+ if (session.transaction) {
69
+ session.transaction.status = TransactionStatus.COMMITTED;
70
+ }
71
+ return result;
72
+ } catch (error) {
73
+ if (session.transaction) {
74
+ session.transaction.status = TransactionStatus.ROLLED_BACK;
75
+ }
76
+ throw error;
77
+ } finally {
78
+ session.transaction = undefined;
91
79
  }
92
-
93
- // 回滚事务
94
- await this.executeRollback(connection);
95
-
96
- context.status = TransactionStatus.ROLLED_BACK;
97
- this.cleanupTransaction(transactionId);
98
80
  }
99
81
 
100
82
  /**
101
- * 创建保存点(用于嵌套事务)
83
+ * REQUIRES_NEW:使用独立连接开启新事务
102
84
  */
103
- public async createSavepoint(transactionId: string): Promise<string> {
104
- const context = this.transactions.get(transactionId);
105
- if (!context) {
106
- throw new Error(`Transaction ${transactionId} not found`);
107
- }
108
-
109
- const savepointName = `sp_${context.level}_${Date.now()}`;
110
- context.savepoints = context.savepoints || [];
111
- context.savepoints.push(savepointName);
112
- context.level += 1;
113
-
114
- const connection = this.findConnectionByTransactionId(transactionId);
115
- if (connection) {
116
- await this.executeSavepoint(connection, savepointName);
85
+ public async runInNewTransaction<T>(
86
+ fn: () => Promise<T>,
87
+ options: TransactionOptions = {},
88
+ ): Promise<T> {
89
+ const reserved = await this.sqlManager.getDefault().reserve();
90
+ try {
91
+ const tenantId = getCurrentSession()?.tenantId ?? 'default';
92
+ return await runWithSession(
93
+ {
94
+ tenantId,
95
+ reserved: reserved as any,
96
+ },
97
+ () => this.runInTransaction(fn, options),
98
+ );
99
+ } finally {
100
+ await reserved.release();
117
101
  }
118
-
119
- return savepointName;
120
102
  }
121
103
 
122
- /**
123
- * 回滚到保存点
124
- */
125
- public async rollbackToSavepoint(
126
- transactionId: string,
127
- savepointName: string,
128
- ): Promise<void> {
129
- const context = this.transactions.get(transactionId);
130
- if (!context) {
131
- throw new Error(`Transaction ${transactionId} not found`);
104
+ public async runInNestedTransaction<T>(
105
+ fn: () => Promise<T>,
106
+ options: TransactionOptions = {},
107
+ ): Promise<T> {
108
+ const session = getCurrentSession();
109
+ if (!session?.transaction || !session.reserved) {
110
+ throw new Error(
111
+ '[TransactionManager] NESTED propagation requires an active transaction',
112
+ );
132
113
  }
133
114
 
134
- const connection = this.findConnectionByTransactionId(transactionId);
135
- if (connection) {
136
- await this.executeRollbackToSavepoint(connection, savepointName);
137
- }
115
+ const savepointName = `sp_${session.transaction.level}_${Date.now()}`;
116
+ session.transaction.level += 1;
117
+ session.transaction.savepoints.push(savepointName);
138
118
 
139
- // 移除该保存点之后的所有保存点
140
- const index = context.savepoints?.indexOf(savepointName) ?? -1;
141
- if (index >= 0 && context.savepoints) {
142
- context.savepoints = context.savepoints.slice(0, index);
143
- context.level = index;
119
+ await session.reserved`SAVEPOINT ${savepointName}`;
120
+ try {
121
+ return await fn();
122
+ } catch (error) {
123
+ if (this.shouldRollback(error, options)) {
124
+ await session.reserved`ROLLBACK TO SAVEPOINT ${savepointName}`;
125
+ }
126
+ throw error;
127
+ } finally {
128
+ session.transaction.level = Math.max(0, session.transaction.level - 1);
129
+ session.transaction.savepoints = session.transaction.savepoints.filter(
130
+ (item) => item !== savepointName,
131
+ );
144
132
  }
145
133
  }
146
134
 
147
- /**
148
- * 获取当前事务上下文
149
- */
150
135
  public getCurrentTransaction(): TransactionContext | null {
151
- // 简化实现:返回最后一个活动事务
152
- // 实际实现中应该使用 ThreadLocal 或类似机制
153
- for (const context of this.transactions.values()) {
154
- if (context.status === TransactionStatus.ACTIVE) {
155
- return context;
156
- }
136
+ const tx = getCurrentSession()?.transaction;
137
+ if (!tx) {
138
+ return null;
157
139
  }
158
- return null;
140
+ return {
141
+ id: tx.id,
142
+ status: tx.status,
143
+ startTime: 0,
144
+ level: tx.level,
145
+ savepoints: tx.savepoints,
146
+ };
159
147
  }
160
148
 
161
- /**
162
- * 检查是否有活动事务
163
- */
164
149
  public hasActiveTransaction(): boolean {
165
- return this.getCurrentTransaction() !== null;
166
- }
167
-
168
- /**
169
- * 生成事务 ID
170
- */
171
- private generateTransactionId(): string {
172
- return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
173
- }
174
-
175
- /**
176
- * 查找连接对应的事务 ID
177
- */
178
- private findConnectionByTransactionId(transactionId: string): unknown {
179
- for (const [connection, txId] of this.connectionTransactions.entries()) {
180
- if (txId === transactionId) {
181
- return connection;
182
- }
183
- }
184
- return null;
150
+ const tx = this.getCurrentTransaction();
151
+ return tx?.status === TransactionStatus.ACTIVE;
185
152
  }
186
153
 
187
- /**
188
- * 清理事务
189
- */
190
- private cleanupTransaction(transactionId: string): void {
191
- this.transactions.delete(transactionId);
192
- // 清理连接映射
193
- for (const [connection, txId] of this.connectionTransactions.entries()) {
194
- if (txId === transactionId) {
195
- this.connectionTransactions.delete(connection);
196
- break;
197
- }
154
+ public async runInSqliteTransaction<T>(fn: () => Promise<T>): Promise<T> {
155
+ const session = getCurrentSession();
156
+ if (!session?.sqlite) {
157
+ throw new Error(
158
+ '[TransactionManager] No sqlite adapter found in current request context',
159
+ );
198
160
  }
199
- }
200
161
 
201
- /**
202
- * 执行 BEGIN 语句
203
- */
204
- private async executeBegin(connection: unknown, options: TransactionOptions): Promise<void> {
205
- const dbType = this.databaseService['config'].database.type;
206
-
207
- if (dbType === 'sqlite') {
208
- // SQLite 默认自动提交,需要显式开始事务
209
- await this.databaseService.query('BEGIN TRANSACTION');
210
- } else if (dbType === 'postgres' || dbType === 'mysql') {
211
- // PostgreSQL 和 MySQL 使用 Bun.SQL,需要设置隔离级别
212
- let sql = 'START TRANSACTION';
213
- if (options.isolationLevel) {
214
- const isolation = this.getIsolationLevelSQL(options.isolationLevel);
215
- sql += ` ${isolation}`;
216
- }
217
- if (options.readOnly) {
218
- sql += ' READ ONLY';
219
- }
220
- await this.databaseService.query(sql);
162
+ using _lock = await session.sqlite.semaphore.acquire();
163
+ await session.sqlite.execute('BEGIN TRANSACTION');
164
+ try {
165
+ const result = await fn();
166
+ await session.sqlite.execute('COMMIT');
167
+ return result;
168
+ } catch (error) {
169
+ await session.sqlite.execute('ROLLBACK');
170
+ throw error;
221
171
  }
222
172
  }
223
173
 
224
- /**
225
- * 执行 COMMIT 语句
226
- */
227
- private async executeCommit(connection: unknown): Promise<void> {
228
- await this.databaseService.query('COMMIT');
229
- }
230
-
231
- /**
232
- * 执行 ROLLBACK 语句
233
- */
234
- private async executeRollback(connection: unknown): Promise<void> {
235
- await this.databaseService.query('ROLLBACK');
236
- }
237
-
238
- /**
239
- * 执行 SAVEPOINT 语句
240
- */
241
- private async executeSavepoint(connection: unknown, savepointName: string): Promise<void> {
242
- await this.databaseService.query(`SAVEPOINT ${savepointName}`);
243
- }
244
-
245
- /**
246
- * 执行 ROLLBACK TO SAVEPOINT 语句
247
- */
248
- private async executeRollbackToSavepoint(
249
- connection: unknown,
250
- savepointName: string,
251
- ): Promise<void> {
252
- await this.databaseService.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
174
+ private generateTransactionId(): string {
175
+ return `tx_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
253
176
  }
254
177
 
255
- /**
256
- * 获取隔离级别的 SQL
257
- */
258
178
  private getIsolationLevelSQL(level: IsolationLevel): string {
259
- const dbType = this.databaseService['config'].database.type;
260
179
  const levelMap: Record<IsolationLevel, string> = {
261
180
  [IsolationLevel.READ_UNCOMMITTED]: 'READ UNCOMMITTED',
262
181
  [IsolationLevel.READ_COMMITTED]: 'READ COMMITTED',
263
182
  [IsolationLevel.REPEATABLE_READ]: 'REPEATABLE READ',
264
183
  [IsolationLevel.SERIALIZABLE]: 'SERIALIZABLE',
265
184
  };
266
-
267
- if (dbType === 'postgres') {
268
- return `SET TRANSACTION ISOLATION LEVEL ${levelMap[level]}`;
269
- } else if (dbType === 'mysql') {
270
- return `SET TRANSACTION ISOLATION LEVEL ${levelMap[level]}`;
185
+ return levelMap[level];
186
+ }
187
+
188
+ private shouldRollback(
189
+ error: unknown,
190
+ options: Pick<TransactionOptions, 'rollbackFor' | 'noRollbackFor'>,
191
+ ): boolean {
192
+ if (options.noRollbackFor && options.noRollbackFor.length > 0) {
193
+ for (const ErrorClass of options.noRollbackFor) {
194
+ if (error instanceof ErrorClass) {
195
+ return false;
196
+ }
197
+ }
271
198
  }
272
-
273
- // SQLite 不支持隔离级别设置
274
- return '';
199
+ if (options.rollbackFor && options.rollbackFor.length > 0) {
200
+ for (const ErrorClass of options.rollbackFor) {
201
+ if (error instanceof ErrorClass) {
202
+ return true;
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+ return true;
275
208
  }
276
209
  }
@@ -1,6 +1,7 @@
1
1
  import { Injectable } from '../di/decorators';
2
2
 
3
3
  import { DatabaseConnectionManager } from './connection-manager';
4
+ import { getCurrentSession } from './database-context';
4
5
  import type {
5
6
  ConnectionInfo,
6
7
  DatabaseConfig,
@@ -18,8 +19,27 @@ export class DatabaseService {
18
19
 
19
20
  public constructor(options: DatabaseModuleOptions) {
20
21
  this.options = options;
22
+ const databaseConfig: DatabaseConfig =
23
+ options.database ??
24
+ (options.type === 'sqlite'
25
+ ? {
26
+ type: 'sqlite',
27
+ config: {
28
+ path: options.databasePath ?? ':memory:',
29
+ },
30
+ }
31
+ : {
32
+ type: (options.type ?? 'postgres') as 'postgres' | 'mysql',
33
+ config: {
34
+ host: options.host ?? 'localhost',
35
+ port: options.port ?? 5432,
36
+ database: options.databasePath ?? 'default',
37
+ user: options.username ?? 'root',
38
+ password: options.password ?? '',
39
+ },
40
+ });
21
41
  this.connectionManager = new DatabaseConnectionManager(
22
- options.database,
42
+ databaseConfig,
23
43
  options.pool,
24
44
  );
25
45
  }
@@ -96,7 +116,13 @@ export class DatabaseService {
96
116
  * SQLite 返回同步结果,PostgreSQL/MySQL 返回异步结果
97
117
  */
98
118
  public query<T = unknown>(sql: string, params?: unknown[]): T[] | Promise<T[]> {
99
- const connection = this.getConnection();
119
+ const session = getCurrentSession();
120
+ if (session?.sqlite) {
121
+ return session.sqlite.query<T>(sql, (params ?? []) as any);
122
+ }
123
+
124
+ const perRequestConnection = session?.reserved;
125
+ const connection = perRequestConnection ?? this.getConnection();
100
126
  if (!connection) {
101
127
  throw new Error('Database connection is not established');
102
128
  }
@@ -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
+