@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 +12 -0
- package/dist/core/database/acquisitionMetrics.d.ts +23 -0
- package/dist/core/database/baseRepository.d.ts +6 -0
- package/dist/core/database/postgresReadOnlyCollector.d.ts +7 -0
- package/dist/core/database/transaction.d.ts +11 -1
- package/dist/core/queue/failedJobMetrics.d.ts +17 -0
- package/dist/core/queue/queueMetrics.d.ts +4 -0
- package/dist/core/tracing/health.d.ts +27 -0
- package/dist/core/tracing/otel.d.ts +2 -0
- package/dist/core/tracing/tracingMiddleware.d.ts +2 -0
- package/dist/entries/audit/exportAuditLogs.js +80 -5
- package/dist/entries/auth/scimAuthMiddleware.js +80 -5
- package/dist/entries/http/response.js +3 -0
- package/dist/entries/jobs/exportAuditLogsJob.js +80 -5
- package/dist/entries/queue/queueMetrics.js +1018 -7
- package/dist/entries/tenant/databaseTenantContext.js +80 -5
- package/dist/entries/tenant/tenantDatabaseScope.js +80 -5
- package/dist/entries/tracing/tracingMiddleware.js +188 -5
- package/dist/framework/public-api.d.ts +7 -0
- package/dist/index.js +474 -98
- package/package.json +2 -1
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
|
-
|
|
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),
|
|
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
|
-
|
|
710
|
-
|
|
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),
|
|
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
|
-
|
|
666
|
-
|
|
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),
|
|
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
|
-
|
|
710
|
-
|
|
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
|
});
|