@bunnykit/orm 0.1.17 → 0.1.19

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);
@@ -20,6 +20,7 @@ export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
20
20
  export { MorphMap } from "./model/MorphMap.js";
21
21
  export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
22
22
  export { BelongsToMany } from "./model/BelongsToMany.js";
23
+ export { IdentityMap } from "./model/IdentityMap.js";
23
24
  export { Migration } from "./migration/Migration.js";
24
25
  export { Migrator } from "./migration/Migrator.js";
25
26
  export type { MigrationEvent, MigrationEventListener, MigrationEventPayload } from "./migration/Migrator.js";
package/dist/src/index.js CHANGED
@@ -15,6 +15,7 @@ export { ObserverRegistry } from "./model/Observer.js";
15
15
  export { MorphMap } from "./model/MorphMap.js";
16
16
  export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
17
17
  export { BelongsToMany } from "./model/BelongsToMany.js";
18
+ export { IdentityMap } from "./model/IdentityMap.js";
18
19
  export { Migration } from "./migration/Migration.js";
19
20
  export { Migrator } from "./migration/Migrator.js";
20
21
  export { MigrationCreator } from "./migration/MigrationCreator.js";
@@ -0,0 +1,8 @@
1
+ import type { Model } from "./Model.js";
2
+ export declare class IdentityMap {
3
+ static current(): Map<string, Model> | undefined;
4
+ static run<T>(callback: () => T | Promise<T>): Promise<T>;
5
+ static get(table: string, key: string | number): Model | undefined;
6
+ static set(table: string, key: string | number, model: Model): void;
7
+ static clear(): void;
8
+ }
@@ -0,0 +1,28 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const storage = new AsyncLocalStorage();
3
+ export class IdentityMap {
4
+ static current() {
5
+ return storage.getStore();
6
+ }
7
+ static async run(callback) {
8
+ return await storage.run(new Map(), callback);
9
+ }
10
+ static get(table, key) {
11
+ const map = this.current();
12
+ if (!map)
13
+ return undefined;
14
+ return map.get(`${table}:${String(key)}`);
15
+ }
16
+ static set(table, key, model) {
17
+ const map = this.current();
18
+ if (!map)
19
+ return;
20
+ map.set(`${table}:${String(key)}`, model);
21
+ }
22
+ static clear() {
23
+ const map = this.current();
24
+ if (!map)
25
+ return;
26
+ map.clear();
27
+ }
28
+ }
@@ -115,6 +115,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
115
115
  static getTable(): string;
116
116
  static getConnection(): Connection;
117
117
  static setConnection(connection: Connection): void;
118
+ static useIdentityMap<T>(callback: () => T | Promise<T>): Promise<T>;
118
119
  static on<M extends ModelConstructor>(this: M, connection: string | Connection): Builder<InstanceType<M>>;
119
120
  static forTenant<M extends ModelConstructor>(this: M, tenantId: string): Builder<InstanceType<M>>;
120
121
  static query<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
@@ -7,6 +7,7 @@ import { Schema } from "../schema/Schema.js";
7
7
  import { ModelNotFoundError } from "./ModelNotFoundError.js";
8
8
  import { ConnectionManager } from "../connection/ConnectionManager.js";
9
9
  import { TenantContext } from "../connection/TenantContext.js";
10
+ import { IdentityMap } from "./IdentityMap.js";
10
11
  const globalScopes = new WeakMap();
11
12
  function getGlobalScopes(model) {
12
13
  const scopes = new Map();
@@ -353,6 +354,9 @@ export class Model {
353
354
  this.connection = connection;
354
355
  ConnectionManager.setDefault(connection);
355
356
  }
357
+ static useIdentityMap(callback) {
358
+ return IdentityMap.run(callback);
359
+ }
356
360
  static on(connection) {
357
361
  const resolved = typeof connection === "string" ? ConnectionManager.require(connection) : connection;
358
362
  const builder = new Builder(resolved, resolved.qualifyTable(this.getTable()));
@@ -813,6 +817,13 @@ export class Model {
813
817
  await ObserverRegistry.dispatch("created", this);
814
818
  await ObserverRegistry.dispatch("saved", this);
815
819
  }
820
+ const identityMap = IdentityMap.current();
821
+ if (identityMap) {
822
+ const pk = this.getAttribute(constructor.primaryKey);
823
+ if (pk !== null && pk !== undefined && pk !== "") {
824
+ IdentityMap.set(constructor.getTable(), pk, this);
825
+ }
826
+ }
816
827
  return this;
817
828
  }
818
829
  updateTimestamps() {
@@ -1,4 +1,5 @@
1
1
  import { ModelNotFoundError } from "../model/ModelNotFoundError.js";
2
+ import { IdentityMap } from "../model/IdentityMap.js";
2
3
  export class Builder {
3
4
  connection;
4
5
  tableName;
@@ -589,13 +590,31 @@ export class Builder {
589
590
  const results = await this.connection.query(sql, this.bindings);
590
591
  const rows = Array.from(results);
591
592
  if (this.model) {
593
+ const identityMap = IdentityMap.current();
594
+ const table = this.model.getTable();
595
+ const primaryKey = this.model.primaryKey || "id";
592
596
  const models = rows.map((row) => {
597
+ if (identityMap) {
598
+ const pk = row[primaryKey];
599
+ if (pk !== null && pk !== undefined) {
600
+ const cached = IdentityMap.get(table, pk);
601
+ if (cached) {
602
+ return cached;
603
+ }
604
+ }
605
+ }
593
606
  const instance = new this.model(row);
594
607
  instance.$exists = true;
595
608
  instance.$original = { ...row };
596
609
  if (typeof instance.setConnection === "function") {
597
610
  instance.setConnection(this.connection);
598
611
  }
612
+ if (identityMap) {
613
+ const pk = row[primaryKey];
614
+ if (pk !== null && pk !== undefined) {
615
+ IdentityMap.set(table, pk, instance);
616
+ }
617
+ }
599
618
  return instance;
600
619
  });
601
620
  if (this.eagerLoads.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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",