@getstrata/core 2.2.8 → 2.2.10

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,17 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.10
4
+
5
+ - Add bounded OpenTelemetry SDK health observations and selected native transaction-acquisition histograms through explicit metrics options.
6
+ - Add bounded Postgres failed-job counts using ORM ID projections, with schema guards, native acquisition deadlines and read-only collector draining.
7
+ - Observe Streams retry promotion lateness using Redis time; cancellation cleanup sentinels omit unavailable age instead of reporting age since epoch.
8
+ - Generate typed runtimeMetrics opt-in and provider-owned SQL collector draining, including startup cleanup; HTTP-only defaults and borrowed tracing ownership remain intact.
9
+ - Keep additional SQL observation dialects, sustained operational qualification and encryption rotation explicit follow-up work.
10
+
11
+ ## 2.2.9
12
+
13
+ - Support native cancellable transaction acquisition
14
+
3
15
  ## 2.2.8
4
16
 
5
17
  - Add bounded read-only Postgres outbox metrics
@@ -0,0 +1,23 @@
1
+ declare const outcomes: readonly ["acquired", "failed", "aborted"];
2
+ type AcquisitionOutcome = (typeof outcomes)[number];
3
+ interface AcquisitionHistogram {
4
+ outcome: AcquisitionOutcome;
5
+ count: number;
6
+ sumSeconds: number;
7
+ buckets: readonly number[];
8
+ }
9
+ interface TransactionAcquisitionSnapshot {
10
+ inflight: number;
11
+ outcomes: readonly AcquisitionHistogram[];
12
+ }
13
+ declare const metricsBrand: unique symbol;
14
+ interface TransactionAcquisitionMetrics {
15
+ readonly [metricsBrand]: true;
16
+ snapshot(): TransactionAcquisitionSnapshot;
17
+ }
18
+ declare function createTransactionAcquisitionMetrics(): TransactionAcquisitionMetrics;
19
+ /** Internal native-reservation boundary, never a business/BEGIN/savepoint timer. */
20
+ declare function observeTransactionAcquisition<T>(metrics: TransactionAcquisitionMetrics, acquire: () => Promise<T>, signal?: AbortSignal): Promise<T>;
21
+ declare function renderTransactionAcquisitionMetrics(snapshot: TransactionAcquisitionSnapshot): string;
22
+ export type { TransactionAcquisitionMetrics, TransactionAcquisitionSnapshot };
23
+ export { createTransactionAcquisitionMetrics, observeTransactionAcquisition, renderTransactionAcquisitionMetrics, };
@@ -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
  }
@@ -0,0 +1,7 @@
1
+ interface ReadOnlyCollector<T> {
2
+ collect(): Promise<T>;
3
+ close(): Promise<void>;
4
+ }
5
+ /** Internal shared observation lifecycle. No bypass, payload or tenant enumeration. */
6
+ declare function createPostgresReadOnlyCollector<T>(name: string, timeoutMs: number, collect: (remaining: () => number) => Promise<T>): ReadOnlyCollector<T>;
7
+ export { createPostgresReadOnlyCollector };
@@ -1,4 +1,14 @@
1
+ import { type TransactionAcquisitionMetrics } from "./acquisitionMetrics.ts";
1
2
  import type { DatabaseConnection } from "./baseRepository.ts";
2
3
  import { hasActiveTransaction, requestTransactionRollback } from "./transactionControl.ts";
3
- declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>): Promise<TValue>;
4
+ interface TransactionOptions {
5
+ /** Cancel admission/pool checkout only. Does not cancel SQL or an admitted business callback. */
6
+ acquisitionSignal?: AbortSignal;
7
+ /** Observe native checkout only; requires a reserve-capable adapter. */
8
+ acquisitionMetrics?: TransactionAcquisitionMetrics;
9
+ }
10
+ declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>, options?: TransactionOptions): Promise<TValue>;
11
+ export type { TransactionAcquisitionMetrics, TransactionAcquisitionSnapshot, } from "./acquisitionMetrics.ts";
12
+ export { createTransactionAcquisitionMetrics, renderTransactionAcquisitionMetrics, } from "./acquisitionMetrics.ts";
13
+ export type { TransactionOptions };
4
14
  export { hasActiveTransaction, requestTransactionRollback, runInTransaction };
@@ -0,0 +1,17 @@
1
+ interface FailedJobMetricsSnapshot {
2
+ sampleLimit: number;
3
+ count: number;
4
+ capped: boolean;
5
+ }
6
+ interface FailedJobMetricsOptions {
7
+ timeoutMs?: number;
8
+ sampleLimit?: number;
9
+ }
10
+ interface FailedJobMetricsCollector {
11
+ collect(): Promise<FailedJobMetricsSnapshot>;
12
+ close(): Promise<void>;
13
+ }
14
+ declare function createFailedJobMetricsCollector(options?: FailedJobMetricsOptions): FailedJobMetricsCollector;
15
+ declare function renderFailedJobMetrics(snapshot: FailedJobMetricsSnapshot): string;
16
+ export type { FailedJobMetricsCollector, FailedJobMetricsOptions, FailedJobMetricsSnapshot };
17
+ export { createFailedJobMetricsCollector, renderFailedJobMetrics };
@@ -26,6 +26,8 @@ interface QueuePrioritySnapshot {
26
26
  inflightCapped: boolean;
27
27
  retryDue: number;
28
28
  retryWaiting: number;
29
+ /** Oldest positive due-score lateness; unavailable for lists or cancellation sentinels. */
30
+ retryActionableLatenessSeconds?: number | null;
29
31
  quarantined: number;
30
32
  /** Residence age of the oldest current stream entry, not original job age across retries. */
31
33
  oldestStreamEntryAgeSeconds: number | null;
@@ -39,5 +41,7 @@ declare const INFLIGHT_SAMPLE_LIMIT = 500;
39
41
  declare function readRedisQueueSnapshot(redisUrl: string, options?: RedisQueueSnapshotOptions): Promise<RedisQueueSnapshot>;
40
42
  /** Fixed metric/label vocabulary; no job IDs, tenant IDs, payloads or credentials. */
41
43
  declare function renderRedisQueueMetrics(snapshot: RedisQueueSnapshot): string;
44
+ export type { FailedJobMetricsCollector, FailedJobMetricsOptions, FailedJobMetricsSnapshot, } from "./failedJobMetrics";
45
+ export { createFailedJobMetricsCollector, renderFailedJobMetrics } from "./failedJobMetrics";
42
46
  export type { QueuePrioritySnapshot, RedisQueueSnapshot, RedisQueueSnapshotOptions };
43
47
  export { INFLIGHT_SAMPLE_LIMIT, readRedisQueueSnapshot, renderRedisQueueMetrics };
@@ -0,0 +1,27 @@
1
+ import { type MeterProvider } from "@opentelemetry/api";
2
+ import type { SpanExporter } from "@opentelemetry/sdk-trace-base";
3
+ interface TracingMetricsSnapshot {
4
+ enabled: boolean;
5
+ exporterConfigured: boolean;
6
+ active: boolean;
7
+ queueSize: number | null;
8
+ queueCapacity: number | null;
9
+ droppedSpans: number;
10
+ exportedBatches: number;
11
+ failedBatches: number;
12
+ exportedSpans: number;
13
+ failedSpans: number;
14
+ exportDeadlineExceededBatches: number;
15
+ flushFailures: number;
16
+ shutdownFailures: number;
17
+ }
18
+ /** A fixed projection of SDK self-observations, not a general metric registry. */
19
+ declare function createTracingHealth(enabled: boolean, exporterConfigured: boolean, timeoutMillis: number): {
20
+ state: TracingMetricsSnapshot;
21
+ meterProvider: MeterProvider;
22
+ snapshot(): TracingMetricsSnapshot;
23
+ wrap(exporter: SpanExporter): SpanExporter;
24
+ };
25
+ declare function renderTracingMetrics(snapshot: TracingMetricsSnapshot): string;
26
+ export type { TracingMetricsSnapshot };
27
+ export { createTracingHealth, renderTracingMetrics };
@@ -1,6 +1,7 @@
1
1
  import { type Context, type Tracer } from "@opentelemetry/api";
2
2
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
3
3
  import { type SpanExporter } from "@opentelemetry/sdk-trace-base";
4
+ import { type TracingMetricsSnapshot } from "./health";
4
5
  type TracingOptions = {
5
6
  exporter?: SpanExporter;
6
7
  endpoint?: string;
@@ -15,6 +16,7 @@ type TracingRuntime = {
15
16
  tracer: Tracer;
16
17
  propagator: W3CTraceContextPropagator;
17
18
  run<T>(parent: Context, callback: () => T): T;
19
+ metrics(): TracingMetricsSnapshot;
18
20
  forceFlush(): Promise<void>;
19
21
  shutdown(): Promise<void>;
20
22
  };
@@ -1,6 +1,8 @@
1
1
  import type { Middleware } from "@getstrata/core/http/middleware";
2
2
  import { type TracingRuntime } from "./otel";
3
3
  declare function createTracingMiddleware(runtime?: TracingRuntime): Middleware;
4
+ export type { TracingMetricsSnapshot } from "./health";
5
+ export { renderTracingMetrics } from "./health";
4
6
  export type { TracingOptions, TracingRuntime } from "./otel";
5
7
  export { acquireTracingRuntime, createTracingRuntime, getTracingRuntime } from "./otel";
6
8
  export { createTracingMiddleware };
@@ -129,6 +129,9 @@ function unwrapTransactionRollback(error, depth = 0) {
129
129
  }
130
130
  return unwrapTransactionRollback(error.cause, depth + 1);
131
131
  }
132
+ function hasActiveTransaction() {
133
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
134
+ }
132
135
  function requestTransactionRollback() {
133
136
  const state = transactionScopes.getStore();
134
137
  const index = state ? state.rollback.length - 1 : -1;
@@ -344,6 +347,55 @@ var init_deferredModelEvents = __esm(() => {
344
347
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
345
348
  });
346
349
 
350
+ // ../../src/core/database/acquisitionMetrics.ts
351
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
352
+ const state = states.get(metrics);
353
+ if (!state)
354
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
355
+ const started = performance.now();
356
+ state.inflight++;
357
+ let outcome = "acquired";
358
+ try {
359
+ return await acquire();
360
+ } catch (error) {
361
+ outcome = signal?.aborted ? "aborted" : "failed";
362
+ throw error;
363
+ } finally {
364
+ state.inflight--;
365
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
366
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
367
+ if (histogram) {
368
+ histogram.count++;
369
+ histogram.sumSeconds += elapsed;
370
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
371
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
372
+ if (bound !== undefined && elapsed <= bound)
373
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
379
+ var init_acquisitionMetrics = __esm(() => {
380
+ ACQUISITION_BUCKETS_SECONDS = [
381
+ 0.001,
382
+ 0.005,
383
+ 0.01,
384
+ 0.025,
385
+ 0.05,
386
+ 0.1,
387
+ 0.25,
388
+ 0.5,
389
+ 1,
390
+ 2.5,
391
+ 5,
392
+ 10
393
+ ];
394
+ outcomes = ["acquired", "failed", "aborted"];
395
+ metricsBrand = Symbol("transaction acquisition metrics");
396
+ states = new WeakMap;
397
+ });
398
+
347
399
  // ../../src/core/database/connection.ts
348
400
  function createDatabaseConnection(source) {
349
401
  return {
@@ -697,28 +749,51 @@ async function runInSavepoint(connection, operation) {
697
749
  function supportsTransactions(connection) {
698
750
  return typeof connection.begin === "function";
699
751
  }
700
- async function runInTransaction(operation) {
752
+ async function runInTransaction(operation, options = {}) {
753
+ const signal = options.acquisitionSignal;
754
+ signal?.throwIfAborted();
755
+ const admittedOperation = (connection) => {
756
+ signal?.throwIfAborted();
757
+ return operation(connection);
758
+ };
701
759
  const pool = resolveRepositoryConnection();
702
760
  if (hasActiveDatabaseConnection()) {
703
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
761
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
704
762
  }
705
763
  if (!supportsTransactions(pool)) {
706
764
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
707
765
  }
708
766
  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
- });
767
+ let reserved;
768
+ if (signal || options.acquisitionMetrics) {
769
+ if (!pool.reserve)
770
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
771
+ const reserve = pool.reserve.bind(pool);
772
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
773
+ }
774
+ const connection = reserved ?? pool;
775
+ try {
776
+ signal?.throwIfAborted();
777
+ if (!supportsTransactions(connection))
778
+ throw new Error("Reserved database connection does not support transactions.");
779
+ return await connection.begin(async (transaction) => {
780
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
781
+ });
782
+ } finally {
783
+ await reserved?.release();
784
+ }
712
785
  }));
713
786
  }
714
787
  var transactionFrame, outerFrames;
715
788
  var init_transaction = __esm(() => {
716
789
  init_deferredModelEvents();
717
790
  init_asyncContextStore();
791
+ init_acquisitionMetrics();
718
792
  init_connection();
719
793
  init_connectionContext();
720
794
  init_repositoryConnection();
721
795
  init_transactionControl();
796
+ init_acquisitionMetrics();
722
797
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
723
798
  outerFrames = new WeakMap;
724
799
  });
@@ -74,6 +74,9 @@ function unwrapTransactionRollback(error, depth = 0) {
74
74
  }
75
75
  return unwrapTransactionRollback(error.cause, depth + 1);
76
76
  }
77
+ function hasActiveTransaction() {
78
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
79
+ }
77
80
  function requestTransactionRollback() {
78
81
  const state = transactionScopes.getStore();
79
82
  const index = state ? state.rollback.length - 1 : -1;
@@ -300,6 +303,55 @@ var init_deferredModelEvents = __esm(() => {
300
303
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
301
304
  });
302
305
 
306
+ // ../../src/core/database/acquisitionMetrics.ts
307
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
308
+ const state = states.get(metrics);
309
+ if (!state)
310
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
311
+ const started = performance.now();
312
+ state.inflight++;
313
+ let outcome = "acquired";
314
+ try {
315
+ return await acquire();
316
+ } catch (error) {
317
+ outcome = signal?.aborted ? "aborted" : "failed";
318
+ throw error;
319
+ } finally {
320
+ state.inflight--;
321
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
322
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
323
+ if (histogram) {
324
+ histogram.count++;
325
+ histogram.sumSeconds += elapsed;
326
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
327
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
328
+ if (bound !== undefined && elapsed <= bound)
329
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
330
+ }
331
+ }
332
+ }
333
+ }
334
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
335
+ var init_acquisitionMetrics = __esm(() => {
336
+ ACQUISITION_BUCKETS_SECONDS = [
337
+ 0.001,
338
+ 0.005,
339
+ 0.01,
340
+ 0.025,
341
+ 0.05,
342
+ 0.1,
343
+ 0.25,
344
+ 0.5,
345
+ 1,
346
+ 2.5,
347
+ 5,
348
+ 10
349
+ ];
350
+ outcomes = ["acquired", "failed", "aborted"];
351
+ metricsBrand = Symbol("transaction acquisition metrics");
352
+ states = new WeakMap;
353
+ });
354
+
303
355
  // ../../src/core/database/connection.ts
304
356
  function createDatabaseConnection(source) {
305
357
  return {
@@ -653,28 +705,51 @@ async function runInSavepoint(connection, operation) {
653
705
  function supportsTransactions(connection) {
654
706
  return typeof connection.begin === "function";
655
707
  }
656
- async function runInTransaction(operation) {
708
+ async function runInTransaction(operation, options = {}) {
709
+ const signal = options.acquisitionSignal;
710
+ signal?.throwIfAborted();
711
+ const admittedOperation = (connection) => {
712
+ signal?.throwIfAborted();
713
+ return operation(connection);
714
+ };
657
715
  const pool = resolveRepositoryConnection();
658
716
  if (hasActiveDatabaseConnection()) {
659
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
717
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
660
718
  }
661
719
  if (!supportsTransactions(pool)) {
662
720
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
663
721
  }
664
722
  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
- });
723
+ let reserved;
724
+ if (signal || options.acquisitionMetrics) {
725
+ if (!pool.reserve)
726
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
727
+ const reserve = pool.reserve.bind(pool);
728
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
729
+ }
730
+ const connection = reserved ?? pool;
731
+ try {
732
+ signal?.throwIfAborted();
733
+ if (!supportsTransactions(connection))
734
+ throw new Error("Reserved database connection does not support transactions.");
735
+ return await connection.begin(async (transaction) => {
736
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
737
+ });
738
+ } finally {
739
+ await reserved?.release();
740
+ }
668
741
  }));
669
742
  }
670
743
  var transactionFrame, outerFrames;
671
744
  var init_transaction = __esm(() => {
672
745
  init_deferredModelEvents();
673
746
  init_asyncContextStore();
747
+ init_acquisitionMetrics();
674
748
  init_connection();
675
749
  init_connectionContext();
676
750
  init_repositoryConnection();
677
751
  init_transactionControl();
752
+ init_acquisitionMetrics();
678
753
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
679
754
  outerFrames = new WeakMap;
680
755
  });
@@ -52,6 +52,9 @@ function unwrapTransactionRollback(error, depth = 0) {
52
52
  }
53
53
  return unwrapTransactionRollback(error.cause, depth + 1);
54
54
  }
55
+ function hasActiveTransaction() {
56
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
57
+ }
55
58
  function requestTransactionRollback() {
56
59
  const state = transactionScopes.getStore();
57
60
  const index = state ? state.rollback.length - 1 : -1;
@@ -129,6 +129,9 @@ function unwrapTransactionRollback(error, depth = 0) {
129
129
  }
130
130
  return unwrapTransactionRollback(error.cause, depth + 1);
131
131
  }
132
+ function hasActiveTransaction() {
133
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
134
+ }
132
135
  function requestTransactionRollback() {
133
136
  const state = transactionScopes.getStore();
134
137
  const index = state ? state.rollback.length - 1 : -1;
@@ -344,6 +347,55 @@ var init_deferredModelEvents = __esm(() => {
344
347
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
345
348
  });
346
349
 
350
+ // ../../src/core/database/acquisitionMetrics.ts
351
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
352
+ const state = states.get(metrics);
353
+ if (!state)
354
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
355
+ const started = performance.now();
356
+ state.inflight++;
357
+ let outcome = "acquired";
358
+ try {
359
+ return await acquire();
360
+ } catch (error) {
361
+ outcome = signal?.aborted ? "aborted" : "failed";
362
+ throw error;
363
+ } finally {
364
+ state.inflight--;
365
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
366
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
367
+ if (histogram) {
368
+ histogram.count++;
369
+ histogram.sumSeconds += elapsed;
370
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
371
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
372
+ if (bound !== undefined && elapsed <= bound)
373
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
379
+ var init_acquisitionMetrics = __esm(() => {
380
+ ACQUISITION_BUCKETS_SECONDS = [
381
+ 0.001,
382
+ 0.005,
383
+ 0.01,
384
+ 0.025,
385
+ 0.05,
386
+ 0.1,
387
+ 0.25,
388
+ 0.5,
389
+ 1,
390
+ 2.5,
391
+ 5,
392
+ 10
393
+ ];
394
+ outcomes = ["acquired", "failed", "aborted"];
395
+ metricsBrand = Symbol("transaction acquisition metrics");
396
+ states = new WeakMap;
397
+ });
398
+
347
399
  // ../../src/core/database/connection.ts
348
400
  function createDatabaseConnection(source) {
349
401
  return {
@@ -697,28 +749,51 @@ async function runInSavepoint(connection, operation) {
697
749
  function supportsTransactions(connection) {
698
750
  return typeof connection.begin === "function";
699
751
  }
700
- async function runInTransaction(operation) {
752
+ async function runInTransaction(operation, options = {}) {
753
+ const signal = options.acquisitionSignal;
754
+ signal?.throwIfAborted();
755
+ const admittedOperation = (connection) => {
756
+ signal?.throwIfAborted();
757
+ return operation(connection);
758
+ };
701
759
  const pool = resolveRepositoryConnection();
702
760
  if (hasActiveDatabaseConnection()) {
703
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
761
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
704
762
  }
705
763
  if (!supportsTransactions(pool)) {
706
764
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
707
765
  }
708
766
  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
- });
767
+ let reserved;
768
+ if (signal || options.acquisitionMetrics) {
769
+ if (!pool.reserve)
770
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
771
+ const reserve = pool.reserve.bind(pool);
772
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
773
+ }
774
+ const connection = reserved ?? pool;
775
+ try {
776
+ signal?.throwIfAborted();
777
+ if (!supportsTransactions(connection))
778
+ throw new Error("Reserved database connection does not support transactions.");
779
+ return await connection.begin(async (transaction) => {
780
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
781
+ });
782
+ } finally {
783
+ await reserved?.release();
784
+ }
712
785
  }));
713
786
  }
714
787
  var transactionFrame, outerFrames;
715
788
  var init_transaction = __esm(() => {
716
789
  init_deferredModelEvents();
717
790
  init_asyncContextStore();
791
+ init_acquisitionMetrics();
718
792
  init_connection();
719
793
  init_connectionContext();
720
794
  init_repositoryConnection();
721
795
  init_transactionControl();
796
+ init_acquisitionMetrics();
722
797
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
723
798
  outerFrames = new WeakMap;
724
799
  });