tina4-nodejs 3.11.19 → 3.11.32

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.11.19",
6
+ "version": "3.11.32",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult } from "./types.js";
2
3
  import { DatabaseResult } from "./databaseResult.js";
3
4
 
@@ -256,6 +257,23 @@ export class Database {
256
257
  /** Database engine type (sqlite, postgres, mysql, mssql, firebird) */
257
258
  private dbType: string = "sqlite";
258
259
 
260
+ /**
261
+ * Async-local storage for the adapter pinned to the current transaction.
262
+ *
263
+ * With pooling enabled, ordinary calls round-robin through the pool. Inside
264
+ * a transaction, however, all calls must land on the SAME adapter — otherwise
265
+ * startTransaction(), execute() and commit() each rotate to a different
266
+ * connection and the transaction is meaningless (executes autocommit on
267
+ * whatever adapter they hit; the final commit lands on yet another adapter
268
+ * that has nothing to commit; rollback() is silently no-op'd).
269
+ *
270
+ * AsyncLocalStorage is the Node analog of Python's threading.local. It pins
271
+ * the adapter to the current async task tree so concurrent transactions on
272
+ * the same Database don't clobber each other. startTransaction() sets the
273
+ * pin via .enterWith(); commit()/rollback() clear it.
274
+ */
275
+ private txStore: AsyncLocalStorage<{ adapter: DatabaseAdapter | null }> = new AsyncLocalStorage();
276
+
259
277
  /**
260
278
  * Create a Database wrapping an existing adapter.
261
279
  * For creating a Database from a URL, use the async static factories:
@@ -320,8 +338,15 @@ export class Database {
320
338
 
321
339
  /**
322
340
  * Get the next adapter — from pool (round-robin) or single connection.
341
+ *
342
+ * If a transaction is active (an adapter is pinned in async-local storage),
343
+ * that adapter is returned for every call so the whole transaction is
344
+ * atomic on one connection. Otherwise pooled mode round-robins.
323
345
  */
324
346
  private getNextAdapter(): DatabaseAdapter {
347
+ const pinned = this.txStore.getStore()?.adapter;
348
+ if (pinned) return pinned;
349
+
325
350
  if (this._poolSize > 0) {
326
351
  const idx = this.poolIndex;
327
352
  this.poolIndex = (this.poolIndex + 1) % this._poolSize;
@@ -457,19 +482,37 @@ export class Database {
457
482
  }
458
483
  }
459
484
 
460
- /** Start a transaction. */
485
+ /**
486
+ * Start a transaction. Pins the adapter to the current async context for
487
+ * the whole transaction so executes and the final commit/rollback all run
488
+ * on the same connection (critical when pool > 0).
489
+ */
461
490
  startTransaction(): void {
462
- this.getNextAdapter().startTransaction();
491
+ // Pick an adapter using the normal selection logic, then pin it.
492
+ const adapter = this.getNextAdapter();
493
+ let store = this.txStore.getStore();
494
+ if (store) {
495
+ store.adapter = adapter;
496
+ } else {
497
+ this.txStore.enterWith({ adapter });
498
+ }
499
+ adapter.startTransaction();
463
500
  }
464
501
 
465
- /** Commit the current transaction. */
502
+ /** Commit the current transaction and release the adapter pin. */
466
503
  commit(): void {
467
- this.getNextAdapter().commit();
504
+ const adapter = this.getNextAdapter();
505
+ adapter.commit();
506
+ const store = this.txStore.getStore();
507
+ if (store) store.adapter = null;
468
508
  }
469
509
 
470
- /** Rollback the current transaction. */
510
+ /** Rollback the current transaction and release the adapter pin. */
471
511
  rollback(): void {
472
- this.getNextAdapter().rollback();
512
+ const adapter = this.getNextAdapter();
513
+ adapter.rollback();
514
+ const store = this.txStore.getStore();
515
+ if (store) store.adapter = null;
473
516
  }
474
517
 
475
518
  /** Check if a table exists. */