@bunnykit/orm 0.1.17 → 0.1.18

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.
@@ -8,6 +8,7 @@ export declare class Connection {
8
8
  private config;
9
9
  private schema?;
10
10
  private ownsDriver;
11
+ private transactionDepth;
11
12
  constructor(config: ConnectionConfig, options?: {
12
13
  driver?: SQL;
13
14
  schema?: string;
@@ -25,6 +26,7 @@ export declare class Connection {
25
26
  beginTransaction(): Promise<void>;
26
27
  commit(): Promise<void>;
27
28
  rollback(): Promise<void>;
29
+ isInTransaction(): boolean;
28
30
  transaction<T>(callback: (connection: Connection) => T | Promise<T>): Promise<T>;
29
31
  withTenant<T>(tenantId: string, callback: (connection: Connection) => T | Promise<T>, setting?: string): Promise<T>;
30
32
  withSearchPath<T>(schema: string, callback: (connection: Connection) => T | Promise<T>): Promise<T>;
@@ -9,6 +9,7 @@ export class Connection {
9
9
  config;
10
10
  schema;
11
11
  ownsDriver;
12
+ transactionDepth = 0;
12
13
  constructor(config, options = {}) {
13
14
  this.config = config;
14
15
  this.schema = options.schema || ("schema" in config ? config.schema : undefined);
@@ -83,12 +84,18 @@ export class Connection {
83
84
  }
84
85
  async beginTransaction() {
85
86
  await this.driver.unsafe("BEGIN");
87
+ this.transactionDepth++;
86
88
  }
87
89
  async commit() {
88
90
  await this.driver.unsafe("COMMIT");
91
+ this.transactionDepth = Math.max(0, this.transactionDepth - 1);
89
92
  }
90
93
  async rollback() {
91
94
  await this.driver.unsafe("ROLLBACK");
95
+ this.transactionDepth = Math.max(0, this.transactionDepth - 1);
96
+ }
97
+ isInTransaction() {
98
+ return this.transactionDepth > 0;
92
99
  }
93
100
  async transaction(callback) {
94
101
  return await this.driver.begin(async (sql) => {
@@ -33,16 +33,18 @@ export declare class ConnectionManager {
33
33
  private static poolConfigs;
34
34
  private static tenantResolver?;
35
35
  private static tenantCache;
36
+ private static waiters;
36
37
  static setDefault(connection: Connection): void;
37
38
  static getDefault(): Connection | undefined;
38
39
  static setPoolConfig(name: string, config: PoolConfig): void;
39
40
  static getPoolConfig(name: string): PoolConfig | undefined;
40
41
  private static getPooledConnection;
42
+ private static removeWaiter;
41
43
  private static releasePooledConnection;
42
44
  static add(name: string, connection: Connection | ConnectionConfig): Connection;
43
45
  static get(name: string): Connection | undefined;
44
46
  static getPooled(name: string, config?: ConnectionConfig): Promise<Connection>;
45
- static release(name: string, connection: Connection): void;
47
+ static release(name: string, connection: Connection): Promise<void>;
46
48
  static require(name: string): Connection;
47
49
  static setTenantResolver(resolver: TenantResolver): void;
48
50
  static resolveTenant(tenantId: string): Promise<ActiveTenantContext>;
@@ -6,6 +6,7 @@ export class ConnectionManager {
6
6
  static poolConfigs = new Map();
7
7
  static tenantResolver;
8
8
  static tenantCache = new Map();
9
+ static waiters = new Map();
9
10
  static setDefault(connection) {
10
11
  this.defaultConnection = connection;
11
12
  }
@@ -34,7 +35,7 @@ export class ConnectionManager {
34
35
  const pooled = pool[idx];
35
36
  pool.splice(idx, 1);
36
37
  try {
37
- pooled.connection.query("SELECT 1").catch(() => null);
38
+ await pooled.connection.query("SELECT 1");
38
39
  pooled.inUse = true;
39
40
  return pooled.connection;
40
41
  }
@@ -48,30 +49,54 @@ export class ConnectionManager {
48
49
  return connection;
49
50
  }
50
51
  return new Promise((resolve, reject) => {
51
- const checkInterval = setInterval(() => {
52
- const available = pool.find((c) => !c.inUse);
53
- if (available) {
54
- clearInterval(checkInterval);
55
- available.inUse = true;
56
- available.lastUsed = Date.now();
57
- resolve(available.connection);
58
- }
59
- }, 50);
60
- setTimeout(() => {
61
- clearInterval(checkInterval);
52
+ const timer = setTimeout(() => {
53
+ this.removeWaiter(name, waiter);
62
54
  reject(new Error(`Connection pool exhausted for "${name}"`));
63
55
  }, 30000);
56
+ const waiter = { resolve, reject, timer };
57
+ let poolWaiters = this.waiters.get(name);
58
+ if (!poolWaiters) {
59
+ poolWaiters = [];
60
+ this.waiters.set(name, poolWaiters);
61
+ }
62
+ poolWaiters.push(waiter);
64
63
  });
65
64
  }
66
- static releasePooledConnection(name, connection) {
65
+ static removeWaiter(name, waiter) {
66
+ const poolWaiters = this.waiters.get(name);
67
+ if (!poolWaiters)
68
+ return;
69
+ const idx = poolWaiters.indexOf(waiter);
70
+ if (idx !== -1)
71
+ poolWaiters.splice(idx, 1);
72
+ }
73
+ static async releasePooledConnection(name, connection) {
67
74
  const pool = this.pools.get(name);
68
75
  if (!pool)
69
76
  return;
70
77
  const pooled = pool.find((p) => p.connection === connection);
71
- if (pooled) {
72
- pooled.inUse = false;
78
+ if (!pooled)
79
+ return;
80
+ // Safety: roll back any leaked transaction before returning to pool
81
+ if (pooled.connection.isInTransaction()) {
82
+ try {
83
+ await pooled.connection.rollback();
84
+ }
85
+ catch {
86
+ // Connection may already be dead; ignore rollback errors
87
+ }
88
+ }
89
+ // Hand off directly to a waiter if one exists
90
+ const poolWaiters = this.waiters.get(name);
91
+ if (poolWaiters && poolWaiters.length > 0) {
92
+ const waiter = poolWaiters.shift();
93
+ clearTimeout(waiter.timer);
73
94
  pooled.lastUsed = Date.now();
95
+ waiter.resolve(connection);
96
+ return;
74
97
  }
98
+ pooled.inUse = false;
99
+ pooled.lastUsed = Date.now();
75
100
  }
76
101
  static add(name, connection) {
77
102
  const resolved = connection instanceof Connection ? connection : new Connection(connection);
@@ -90,8 +115,8 @@ export class ConnectionManager {
90
115
  return existing;
91
116
  throw new Error(`No connection registered for "${name}". Use add() first or provide config.`);
92
117
  }
93
- static release(name, connection) {
94
- this.releasePooledConnection(name, connection);
118
+ static async release(name, connection) {
119
+ await this.releasePooledConnection(name, connection);
95
120
  }
96
121
  static require(name) {
97
122
  const connection = this.get(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",