@getstrata/core 2.2.8 → 2.2.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.9
4
+
5
+ - Support native cancellable transaction acquisition
6
+
3
7
  ## 2.2.8
4
8
 
5
9
  - Add bounded read-only Postgres outbox metrics
@@ -11,6 +11,12 @@ type ExtendedQueryOptions<TEntity extends object> = QueryOptions<TEntity> & {
11
11
  interface DatabaseConnection {
12
12
  unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
13
13
  begin?<T>(callback: (transaction: DatabaseConnection) => Promise<T>): Promise<T>;
14
+ /** Optional native cancellable pool checkout; absence must not be silently emulated. */
15
+ reserve?(options: {
16
+ signal?: AbortSignal;
17
+ }): Promise<DatabaseConnection & {
18
+ release(): void | Promise<void>;
19
+ }>;
14
20
  /** SQLite closes synchronously; pooled drivers return a promise. */
15
21
  close?(): void | Promise<void>;
16
22
  }
@@ -1,4 +1,9 @@
1
1
  import type { DatabaseConnection } from "./baseRepository.ts";
2
2
  import { hasActiveTransaction, requestTransactionRollback } from "./transactionControl.ts";
3
- declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>): Promise<TValue>;
3
+ interface TransactionOptions {
4
+ /** Cancel admission/pool checkout only. Does not cancel SQL or an admitted business callback. */
5
+ acquisitionSignal?: AbortSignal;
6
+ }
7
+ declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>, options?: TransactionOptions): Promise<TValue>;
8
+ export type { TransactionOptions };
4
9
  export { hasActiveTransaction, requestTransactionRollback, runInTransaction };
@@ -697,18 +697,35 @@ async function runInSavepoint(connection, operation) {
697
697
  function supportsTransactions(connection) {
698
698
  return typeof connection.begin === "function";
699
699
  }
700
- async function runInTransaction(operation) {
700
+ async function runInTransaction(operation, options = {}) {
701
+ const signal = options.acquisitionSignal;
702
+ signal?.throwIfAborted();
703
+ const admittedOperation = (connection) => {
704
+ signal?.throwIfAborted();
705
+ return operation(connection);
706
+ };
701
707
  const pool = resolveRepositoryConnection();
702
708
  if (hasActiveDatabaseConnection()) {
703
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
709
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
704
710
  }
705
711
  if (!supportsTransactions(pool)) {
706
712
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
707
713
  }
708
714
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
709
- return await pool.begin(async (transaction) => {
710
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
711
- });
715
+ if (signal && !pool.reserve)
716
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
717
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
718
+ const connection = reserved ?? pool;
719
+ try {
720
+ signal?.throwIfAborted();
721
+ if (!supportsTransactions(connection))
722
+ throw new Error("Reserved database connection does not support transactions.");
723
+ return await connection.begin(async (transaction) => {
724
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
725
+ });
726
+ } finally {
727
+ await reserved?.release();
728
+ }
712
729
  }));
713
730
  }
714
731
  var transactionFrame, outerFrames;
@@ -653,18 +653,35 @@ async function runInSavepoint(connection, operation) {
653
653
  function supportsTransactions(connection) {
654
654
  return typeof connection.begin === "function";
655
655
  }
656
- async function runInTransaction(operation) {
656
+ async function runInTransaction(operation, options = {}) {
657
+ const signal = options.acquisitionSignal;
658
+ signal?.throwIfAborted();
659
+ const admittedOperation = (connection) => {
660
+ signal?.throwIfAborted();
661
+ return operation(connection);
662
+ };
657
663
  const pool = resolveRepositoryConnection();
658
664
  if (hasActiveDatabaseConnection()) {
659
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
665
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
660
666
  }
661
667
  if (!supportsTransactions(pool)) {
662
668
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
663
669
  }
664
670
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
665
- return await pool.begin(async (transaction) => {
666
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
667
- });
671
+ if (signal && !pool.reserve)
672
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
673
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
674
+ const connection = reserved ?? pool;
675
+ try {
676
+ signal?.throwIfAborted();
677
+ if (!supportsTransactions(connection))
678
+ throw new Error("Reserved database connection does not support transactions.");
679
+ return await connection.begin(async (transaction) => {
680
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
681
+ });
682
+ } finally {
683
+ await reserved?.release();
684
+ }
668
685
  }));
669
686
  }
670
687
  var transactionFrame, outerFrames;
@@ -697,18 +697,35 @@ async function runInSavepoint(connection, operation) {
697
697
  function supportsTransactions(connection) {
698
698
  return typeof connection.begin === "function";
699
699
  }
700
- async function runInTransaction(operation) {
700
+ async function runInTransaction(operation, options = {}) {
701
+ const signal = options.acquisitionSignal;
702
+ signal?.throwIfAborted();
703
+ const admittedOperation = (connection) => {
704
+ signal?.throwIfAborted();
705
+ return operation(connection);
706
+ };
701
707
  const pool = resolveRepositoryConnection();
702
708
  if (hasActiveDatabaseConnection()) {
703
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
709
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
704
710
  }
705
711
  if (!supportsTransactions(pool)) {
706
712
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
707
713
  }
708
714
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
709
- return await pool.begin(async (transaction) => {
710
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
711
- });
715
+ if (signal && !pool.reserve)
716
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
717
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
718
+ const connection = reserved ?? pool;
719
+ try {
720
+ signal?.throwIfAborted();
721
+ if (!supportsTransactions(connection))
722
+ throw new Error("Reserved database connection does not support transactions.");
723
+ return await connection.begin(async (transaction) => {
724
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
725
+ });
726
+ } finally {
727
+ await reserved?.release();
728
+ }
712
729
  }));
713
730
  }
714
731
  var transactionFrame, outerFrames;
@@ -697,18 +697,35 @@ async function runInSavepoint(connection, operation) {
697
697
  function supportsTransactions(connection) {
698
698
  return typeof connection.begin === "function";
699
699
  }
700
- async function runInTransaction(operation) {
700
+ async function runInTransaction(operation, options = {}) {
701
+ const signal = options.acquisitionSignal;
702
+ signal?.throwIfAborted();
703
+ const admittedOperation = (connection) => {
704
+ signal?.throwIfAborted();
705
+ return operation(connection);
706
+ };
701
707
  const pool = resolveRepositoryConnection();
702
708
  if (hasActiveDatabaseConnection()) {
703
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
709
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
704
710
  }
705
711
  if (!supportsTransactions(pool)) {
706
712
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
707
713
  }
708
714
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
709
- return await pool.begin(async (transaction) => {
710
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
711
- });
715
+ if (signal && !pool.reserve)
716
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
717
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
718
+ const connection = reserved ?? pool;
719
+ try {
720
+ signal?.throwIfAborted();
721
+ if (!supportsTransactions(connection))
722
+ throw new Error("Reserved database connection does not support transactions.");
723
+ return await connection.begin(async (transaction) => {
724
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
725
+ });
726
+ } finally {
727
+ await reserved?.release();
728
+ }
712
729
  }));
713
730
  }
714
731
  var transactionFrame, outerFrames;
@@ -631,18 +631,35 @@ async function runInSavepoint(connection, operation) {
631
631
  function supportsTransactions(connection) {
632
632
  return typeof connection.begin === "function";
633
633
  }
634
- async function runInTransaction(operation) {
634
+ async function runInTransaction(operation, options = {}) {
635
+ const signal = options.acquisitionSignal;
636
+ signal?.throwIfAborted();
637
+ const admittedOperation = (connection) => {
638
+ signal?.throwIfAborted();
639
+ return operation(connection);
640
+ };
635
641
  const pool = resolveRepositoryConnection();
636
642
  if (hasActiveDatabaseConnection()) {
637
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
643
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
638
644
  }
639
645
  if (!supportsTransactions(pool)) {
640
646
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
641
647
  }
642
648
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
643
- return await pool.begin(async (transaction) => {
644
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
645
- });
649
+ if (signal && !pool.reserve)
650
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
651
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
652
+ const connection = reserved ?? pool;
653
+ try {
654
+ signal?.throwIfAborted();
655
+ if (!supportsTransactions(connection))
656
+ throw new Error("Reserved database connection does not support transactions.");
657
+ return await connection.begin(async (transaction) => {
658
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
659
+ });
660
+ } finally {
661
+ await reserved?.release();
662
+ }
646
663
  }));
647
664
  }
648
665
  var transactionFrame, outerFrames;
@@ -64,6 +64,7 @@ export { loadSeedersFromDirectory, runSeedersFromDirectory, } from "../core/data
64
64
  export type { Seeder, SeederDatabase } from "../core/database/seeders/types.ts";
65
65
  export { createSqliteConnection } from "../core/database/sqliteConnection.ts";
66
66
  export { defineTable } from "../core/database/table.ts";
67
+ export type { TransactionOptions } from "../core/database/transaction.ts";
67
68
  export { hasActiveTransaction, requestTransactionRollback, runInTransaction, } from "../core/database/transaction.ts";
68
69
  export type { ModelWhere, ModelWriteValues, QueryJoin, QueryJoinOn, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, } from "../core/database/types.ts";
69
70
  export type { WhereNode } from "../core/database/whereBuilder.ts";
package/dist/index.js CHANGED
@@ -730,18 +730,35 @@ async function runInSavepoint(connection, operation) {
730
730
  function supportsTransactions(connection) {
731
731
  return typeof connection.begin === "function";
732
732
  }
733
- async function runInTransaction(operation) {
733
+ async function runInTransaction(operation, options = {}) {
734
+ const signal = options.acquisitionSignal;
735
+ signal?.throwIfAborted();
736
+ const admittedOperation = (connection) => {
737
+ signal?.throwIfAborted();
738
+ return operation(connection);
739
+ };
734
740
  const pool = resolveRepositoryConnection();
735
741
  if (hasActiveDatabaseConnection()) {
736
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
742
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
737
743
  }
738
744
  if (!supportsTransactions(pool)) {
739
745
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
740
746
  }
741
747
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
742
- return await pool.begin(async (transaction) => {
743
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
744
- });
748
+ if (signal && !pool.reserve)
749
+ throw new Error("Active database connection does not support cancellable transaction acquisition.");
750
+ const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
751
+ const connection = reserved ?? pool;
752
+ try {
753
+ signal?.throwIfAborted();
754
+ if (!supportsTransactions(connection))
755
+ throw new Error("Reserved database connection does not support transactions.");
756
+ return await connection.begin(async (transaction) => {
757
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
758
+ });
759
+ } finally {
760
+ await reserved?.release();
761
+ }
745
762
  }));
746
763
  }
747
764
  var transactionFrame, outerFrames;
@@ -9235,6 +9252,7 @@ function createOutboxMetricsCollector(options = {}) {
9235
9252
  throw new Error("Outbox metrics collection is still settling.");
9236
9253
  const deadline = performance.now() + timeoutMs;
9237
9254
  let expired = false;
9255
+ const acquisition = new AbortController;
9238
9256
  let timer;
9239
9257
  const remaining = () => {
9240
9258
  const value = Math.floor(deadline - performance.now());
@@ -9266,7 +9284,7 @@ function createOutboxMetricsCollector(options = {}) {
9266
9284
  });
9267
9285
  return { sampleLimit, states };
9268
9286
  });
9269
- });
9287
+ }, { acquisitionSignal: acquisition.signal });
9270
9288
  active = work;
9271
9289
  work.finally(() => {
9272
9290
  if (active === work)
@@ -9278,7 +9296,9 @@ function createOutboxMetricsCollector(options = {}) {
9278
9296
  new Promise((_, reject) => {
9279
9297
  timer = setTimeout(() => {
9280
9298
  expired = true;
9281
- reject(new Error("Outbox metrics collection timed out."));
9299
+ const error = new Error("Outbox metrics collection timed out.");
9300
+ acquisition.abort(error);
9301
+ reject(error);
9282
9302
  }, timeoutMs);
9283
9303
  })
9284
9304
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",