@kronos-ts/postgres 0.3.2 → 0.4.0
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/dist/postgres-transaction-manager.d.ts +20 -1
- package/dist/postgres-transaction-manager.d.ts.map +1 -1
- package/dist/postgres-transaction-manager.js +38 -2
- package/dist/postgres-transaction-manager.js.map +1 -1
- package/dist/postgres.d.ts +8 -0
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +1 -1
- package/dist/postgres.js.map +1 -1
- package/package.json +4 -4
- package/src/postgres-transaction-manager.ts +67 -1
- package/src/postgres.ts +9 -1
|
@@ -14,7 +14,26 @@
|
|
|
14
14
|
import { type TransactionManager } from "@kronos-ts/messaging";
|
|
15
15
|
import type { PostgresAdapter, PostgresAdapterTransaction } from "./adapter.js";
|
|
16
16
|
import { IsolationLevel } from "./adapter.js";
|
|
17
|
-
|
|
17
|
+
/** Tuning for the safety timeouts applied to every UoW-scoped transaction. */
|
|
18
|
+
export interface PostgresTransactionManagerOptions {
|
|
19
|
+
/**
|
|
20
|
+
* `idle_in_transaction_session_timeout` (ms) applied via `SET LOCAL` on every
|
|
21
|
+
* transaction. A UoW that begins a tx but stalls before commit/rollback would
|
|
22
|
+
* otherwise hold its connection — and pin `pg_snapshot_xmin`, which gates the
|
|
23
|
+
* gap-free tailing query in the event store — open indefinitely, stalling all
|
|
24
|
+
* streaming processors until the process restarts. This bounds that window:
|
|
25
|
+
* postgres aborts the idle transaction and the connection (and xmin) is freed.
|
|
26
|
+
* Default 30000 (30s). Set 0 to disable (postgres default — no timeout).
|
|
27
|
+
*/
|
|
28
|
+
readonly idleInTransactionTimeoutMs?: number;
|
|
29
|
+
/**
|
|
30
|
+
* `statement_timeout` (ms) applied via `SET LOCAL` on every transaction.
|
|
31
|
+
* Bounds a single hung statement inside the tx. Default 0 (disabled) — large
|
|
32
|
+
* appends / replays can legitimately run long, so opt in per deployment.
|
|
33
|
+
*/
|
|
34
|
+
readonly statementTimeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function postgresTransactionManager(adapter: PostgresAdapter, isolationLevel?: IsolationLevel, options?: PostgresTransactionManagerOptions): TransactionManager<PostgresAdapterTransaction>;
|
|
18
37
|
/**
|
|
19
38
|
* Run `fn` inside a postgres tx, joining a UoW-scoped tx if one is active
|
|
20
39
|
* (or installed lazily), otherwise opening an ad-hoc tx via `adapter.transaction`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-transaction-manager.d.ts","sourceRoot":"","sources":["../src/postgres-transaction-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA+B,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAsB7C,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,cAAc,GAAE,cAA8C,
|
|
1
|
+
{"version":3,"file":"postgres-transaction-manager.d.ts","sourceRoot":"","sources":["../src/postgres-transaction-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA+B,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAsB7C,8EAA8E;AAC9E,MAAM,WAAW,iCAAiC;IAChD;;;;;;;;OAQG;IACH,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CACrC;AAID,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,cAAc,GAAE,cAA8C,EAC9D,OAAO,GAAE,iCAAsC,GAC9C,kBAAkB,CAAC,0BAA0B,CAAC,CAyFhD;AASD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,eAAe,EACxB,EAAE,EAAE,CAAC,EAAE,EAAE,0BAA0B,KAAK,OAAO,CAAC,CAAC,CAAC,EAClD,cAAc,GAAE,cAA8C,GAC7D,OAAO,CAAC,CAAC,CAAC,CAIZ"}
|
|
@@ -21,7 +21,22 @@ import { IsolationLevel } from "./adapter.js";
|
|
|
21
21
|
const TX_CONTROL = Symbol("kronos.postgresTxControl");
|
|
22
22
|
/** Marker error: signals an intentional rollback so the .catch can suppress it. */
|
|
23
23
|
const ROLLBACK_MARKER = "__kronos_postgres_tx_rollback__";
|
|
24
|
-
|
|
24
|
+
const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS = 30_000;
|
|
25
|
+
export function postgresTransactionManager(adapter, isolationLevel = IsolationLevel.READ_COMMITTED, options = {}) {
|
|
26
|
+
const idleTimeoutMs = normalizeTimeoutMs(options.idleInTransactionTimeoutMs ?? DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS);
|
|
27
|
+
const statementTimeoutMs = normalizeTimeoutMs(options.statementTimeoutMs ?? 0);
|
|
28
|
+
// GUCs cannot be parameterized ($1) — the value is a config-supplied integer,
|
|
29
|
+
// normalized to a non-negative whole number, so inlining is injection-safe.
|
|
30
|
+
// SET LOCAL auto-resets at COMMIT/ROLLBACK, so it never leaks onto pooled
|
|
31
|
+
// connections.
|
|
32
|
+
async function applyTimeouts(tx) {
|
|
33
|
+
if (idleTimeoutMs > 0) {
|
|
34
|
+
await tx.query(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeoutMs}`);
|
|
35
|
+
}
|
|
36
|
+
if (statementTimeoutMs > 0) {
|
|
37
|
+
await tx.query(`SET LOCAL statement_timeout = ${statementTimeoutMs}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
25
40
|
return {
|
|
26
41
|
async begin() {
|
|
27
42
|
let captureTx;
|
|
@@ -36,6 +51,9 @@ export function postgresTransactionManager(adapter, isolationLevel = IsolationLe
|
|
|
36
51
|
});
|
|
37
52
|
const txPromise = adapter
|
|
38
53
|
.transaction(isolationLevel, async (tx) => {
|
|
54
|
+
// Arm the per-transaction safety timeouts before handing the tx to
|
|
55
|
+
// the UoW, so even the very first awaited statement is bounded.
|
|
56
|
+
await applyTimeouts(tx);
|
|
39
57
|
captureTx(tx);
|
|
40
58
|
await completion;
|
|
41
59
|
})
|
|
@@ -45,7 +63,18 @@ export function postgresTransactionManager(adapter, isolationLevel = IsolationLe
|
|
|
45
63
|
return;
|
|
46
64
|
throw err;
|
|
47
65
|
});
|
|
48
|
-
|
|
66
|
+
// If the transaction callback fails before it hands back the tx — BEGIN
|
|
67
|
+
// itself failing, or arming the safety timeouts throwing — `captureTx`
|
|
68
|
+
// never runs and `txReady` would never resolve. Race it against
|
|
69
|
+
// `txPromise` so an early failure rejects begin() instead of hanging it
|
|
70
|
+
// forever. In the happy path `txPromise` stays pending (parked on
|
|
71
|
+
// `completion` until commit/rollback), so `txReady` always wins.
|
|
72
|
+
const tx = (await Promise.race([txReady, txPromise]));
|
|
73
|
+
if (tx === undefined) {
|
|
74
|
+
// txPromise settled first by resolving — the tx ended before begin()
|
|
75
|
+
// returned, so the handle is unusable. Surface rather than return it.
|
|
76
|
+
throw new Error("postgresTransactionManager: transaction ended before begin() completed");
|
|
77
|
+
}
|
|
49
78
|
tx[TX_CONTROL] = { resolveCommit, rejectRollback, txPromise };
|
|
50
79
|
return tx;
|
|
51
80
|
},
|
|
@@ -69,6 +98,13 @@ export function postgresTransactionManager(adapter, isolationLevel = IsolationLe
|
|
|
69
98
|
},
|
|
70
99
|
};
|
|
71
100
|
}
|
|
101
|
+
/** Coerce a config timeout to a non-negative whole number of milliseconds.
|
|
102
|
+
* Non-finite or negative values disable the timeout (treated as 0). */
|
|
103
|
+
function normalizeTimeoutMs(value) {
|
|
104
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
105
|
+
return 0;
|
|
106
|
+
return Math.floor(value);
|
|
107
|
+
}
|
|
72
108
|
/**
|
|
73
109
|
* Run `fn` inside a postgres tx, joining a UoW-scoped tx if one is active
|
|
74
110
|
* (or installed lazily), otherwise opening an ad-hoc tx via `adapter.transaction`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-transaction-manager.js","sourceRoot":"","sources":["../src/postgres-transaction-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,2BAA2B,EAA2B,MAAM,sBAAsB,CAAA;AAE3F,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C;;;;GAIG;AACH,MAAM,UAAU,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAA;AAYrD,mFAAmF;AACnF,MAAM,eAAe,GAAG,iCAAiC,CAAA;
|
|
1
|
+
{"version":3,"file":"postgres-transaction-manager.js","sourceRoot":"","sources":["../src/postgres-transaction-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/postgres.d.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface PostgresConfig {
|
|
|
46
46
|
readonly pollIntervalMs?: number;
|
|
47
47
|
readonly batchSize?: number;
|
|
48
48
|
};
|
|
49
|
+
/** Safety timeouts applied via `SET LOCAL` to every UoW-scoped transaction.
|
|
50
|
+
* Guards against a stalled UoW holding a connection — and pinning
|
|
51
|
+
* `pg_snapshot_xmin`, which would stall all streaming tailing — open until
|
|
52
|
+
* restart. Defaults: 30s idle-in-transaction, statement timeout disabled. */
|
|
53
|
+
readonly transaction?: {
|
|
54
|
+
readonly idleInTransactionTimeoutMs?: number;
|
|
55
|
+
readonly statementTimeoutMs?: number;
|
|
56
|
+
};
|
|
49
57
|
}
|
|
50
58
|
export declare function postgres(config: PostgresConfig): (app: App) => void;
|
|
51
59
|
//# sourceMappingURL=postgres.d.ts.map
|
package/dist/postgres.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAMzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAQnD,OAAO,EAAwC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAEnF,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;IACjC;uCACmC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;IAChC;4CACwC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC/C,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;QAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAC5B,CAAA;CACF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAgEnE"}
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAMzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAQnD,OAAO,EAAwC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAEnF,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;IACjC;uCACmC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;IAChC;4CACwC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC/C,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;QAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAC5B,CAAA;IACD;;;kFAG8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;QAC5C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KACrC,CAAA;CACF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAgEnE"}
|
package/dist/postgres.js
CHANGED
|
@@ -37,7 +37,7 @@ export function postgres(config) {
|
|
|
37
37
|
const { adapter, resilience } = config;
|
|
38
38
|
const bootstrap = config.bootstrap ?? true;
|
|
39
39
|
const tables = config.tableNames ?? DEFAULT_TABLE_NAMES;
|
|
40
|
-
const txManager = postgresTransactionManager(adapter);
|
|
40
|
+
const txManager = postgresTransactionManager(adapter, undefined, config.transaction);
|
|
41
41
|
return (app) => {
|
|
42
42
|
app.set("eventStore", ({ serializer, tagResolver }) => createPostgresEventStore({ adapter, serializer, tagResolver, tableNames: tables }));
|
|
43
43
|
app.set("snapshotStore", ({ serializer }) => createPostgresSnapshotStore({ adapter, serializer, tableNames: tables }));
|
package/dist/postgres.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EACL,kCAAkC,EAClC,WAAW,GACZ,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AAC9E,OAAO,EACL,4BAA4B,GAE7B,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kronos-ts/postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "PostgreSQL extension for Kronos — event store and snapshot store adapters.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@kronos-ts/common": "0.1.1",
|
|
71
|
-
"@kronos-ts/app": "0.3.
|
|
72
|
-
"@kronos-ts/eventsourcing": "0.1
|
|
73
|
-
"@kronos-ts/messaging": "0.
|
|
71
|
+
"@kronos-ts/app": "0.3.4",
|
|
72
|
+
"@kronos-ts/eventsourcing": "0.2.1",
|
|
73
|
+
"@kronos-ts/messaging": "0.5.1"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"pg": ">=8.0.0",
|
|
@@ -36,10 +36,51 @@ interface ManagedPostgresTransaction extends PostgresAdapterTransaction {
|
|
|
36
36
|
/** Marker error: signals an intentional rollback so the .catch can suppress it. */
|
|
37
37
|
const ROLLBACK_MARKER = "__kronos_postgres_tx_rollback__"
|
|
38
38
|
|
|
39
|
+
/** Tuning for the safety timeouts applied to every UoW-scoped transaction. */
|
|
40
|
+
export interface PostgresTransactionManagerOptions {
|
|
41
|
+
/**
|
|
42
|
+
* `idle_in_transaction_session_timeout` (ms) applied via `SET LOCAL` on every
|
|
43
|
+
* transaction. A UoW that begins a tx but stalls before commit/rollback would
|
|
44
|
+
* otherwise hold its connection — and pin `pg_snapshot_xmin`, which gates the
|
|
45
|
+
* gap-free tailing query in the event store — open indefinitely, stalling all
|
|
46
|
+
* streaming processors until the process restarts. This bounds that window:
|
|
47
|
+
* postgres aborts the idle transaction and the connection (and xmin) is freed.
|
|
48
|
+
* Default 30000 (30s). Set 0 to disable (postgres default — no timeout).
|
|
49
|
+
*/
|
|
50
|
+
readonly idleInTransactionTimeoutMs?: number
|
|
51
|
+
/**
|
|
52
|
+
* `statement_timeout` (ms) applied via `SET LOCAL` on every transaction.
|
|
53
|
+
* Bounds a single hung statement inside the tx. Default 0 (disabled) — large
|
|
54
|
+
* appends / replays can legitimately run long, so opt in per deployment.
|
|
55
|
+
*/
|
|
56
|
+
readonly statementTimeoutMs?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS = 30_000
|
|
60
|
+
|
|
39
61
|
export function postgresTransactionManager(
|
|
40
62
|
adapter: PostgresAdapter,
|
|
41
63
|
isolationLevel: IsolationLevel = IsolationLevel.READ_COMMITTED,
|
|
64
|
+
options: PostgresTransactionManagerOptions = {},
|
|
42
65
|
): TransactionManager<PostgresAdapterTransaction> {
|
|
66
|
+
const idleTimeoutMs = normalizeTimeoutMs(
|
|
67
|
+
options.idleInTransactionTimeoutMs ?? DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS,
|
|
68
|
+
)
|
|
69
|
+
const statementTimeoutMs = normalizeTimeoutMs(options.statementTimeoutMs ?? 0)
|
|
70
|
+
|
|
71
|
+
// GUCs cannot be parameterized ($1) — the value is a config-supplied integer,
|
|
72
|
+
// normalized to a non-negative whole number, so inlining is injection-safe.
|
|
73
|
+
// SET LOCAL auto-resets at COMMIT/ROLLBACK, so it never leaks onto pooled
|
|
74
|
+
// connections.
|
|
75
|
+
async function applyTimeouts(tx: PostgresAdapterTransaction): Promise<void> {
|
|
76
|
+
if (idleTimeoutMs > 0) {
|
|
77
|
+
await tx.query(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeoutMs}`)
|
|
78
|
+
}
|
|
79
|
+
if (statementTimeoutMs > 0) {
|
|
80
|
+
await tx.query(`SET LOCAL statement_timeout = ${statementTimeoutMs}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
return {
|
|
44
85
|
async begin(): Promise<PostgresAdapterTransaction> {
|
|
45
86
|
let captureTx!: (tx: PostgresAdapterTransaction) => void
|
|
@@ -56,6 +97,9 @@ export function postgresTransactionManager(
|
|
|
56
97
|
|
|
57
98
|
const txPromise = adapter
|
|
58
99
|
.transaction(isolationLevel, async (tx) => {
|
|
100
|
+
// Arm the per-transaction safety timeouts before handing the tx to
|
|
101
|
+
// the UoW, so even the very first awaited statement is bounded.
|
|
102
|
+
await applyTimeouts(tx)
|
|
59
103
|
captureTx(tx)
|
|
60
104
|
await completion
|
|
61
105
|
})
|
|
@@ -68,7 +112,22 @@ export function postgresTransactionManager(
|
|
|
68
112
|
},
|
|
69
113
|
)
|
|
70
114
|
|
|
71
|
-
|
|
115
|
+
// If the transaction callback fails before it hands back the tx — BEGIN
|
|
116
|
+
// itself failing, or arming the safety timeouts throwing — `captureTx`
|
|
117
|
+
// never runs and `txReady` would never resolve. Race it against
|
|
118
|
+
// `txPromise` so an early failure rejects begin() instead of hanging it
|
|
119
|
+
// forever. In the happy path `txPromise` stays pending (parked on
|
|
120
|
+
// `completion` until commit/rollback), so `txReady` always wins.
|
|
121
|
+
const tx = (await Promise.race([txReady, txPromise])) as
|
|
122
|
+
| ManagedPostgresTransaction
|
|
123
|
+
| undefined
|
|
124
|
+
if (tx === undefined) {
|
|
125
|
+
// txPromise settled first by resolving — the tx ended before begin()
|
|
126
|
+
// returned, so the handle is unusable. Surface rather than return it.
|
|
127
|
+
throw new Error(
|
|
128
|
+
"postgresTransactionManager: transaction ended before begin() completed",
|
|
129
|
+
)
|
|
130
|
+
}
|
|
72
131
|
tx[TX_CONTROL] = { resolveCommit, rejectRollback, txPromise }
|
|
73
132
|
return tx
|
|
74
133
|
},
|
|
@@ -94,6 +153,13 @@ export function postgresTransactionManager(
|
|
|
94
153
|
}
|
|
95
154
|
}
|
|
96
155
|
|
|
156
|
+
/** Coerce a config timeout to a non-negative whole number of milliseconds.
|
|
157
|
+
* Non-finite or negative values disable the timeout (treated as 0). */
|
|
158
|
+
function normalizeTimeoutMs(value: number): number {
|
|
159
|
+
if (!Number.isFinite(value) || value <= 0) return 0
|
|
160
|
+
return Math.floor(value)
|
|
161
|
+
}
|
|
162
|
+
|
|
97
163
|
/**
|
|
98
164
|
* Run `fn` inside a postgres tx, joining a UoW-scoped tx if one is active
|
|
99
165
|
* (or installed lazily), otherwise opening an ad-hoc tx via `adapter.transaction`.
|
package/src/postgres.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface PostgresConfig {
|
|
|
60
60
|
readonly pollIntervalMs?: number
|
|
61
61
|
readonly batchSize?: number
|
|
62
62
|
}
|
|
63
|
+
/** Safety timeouts applied via `SET LOCAL` to every UoW-scoped transaction.
|
|
64
|
+
* Guards against a stalled UoW holding a connection — and pinning
|
|
65
|
+
* `pg_snapshot_xmin`, which would stall all streaming tailing — open until
|
|
66
|
+
* restart. Defaults: 30s idle-in-transaction, statement timeout disabled. */
|
|
67
|
+
readonly transaction?: {
|
|
68
|
+
readonly idleInTransactionTimeoutMs?: number
|
|
69
|
+
readonly statementTimeoutMs?: number
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
export function postgres(config: PostgresConfig): (app: App) => void {
|
|
@@ -67,7 +75,7 @@ export function postgres(config: PostgresConfig): (app: App) => void {
|
|
|
67
75
|
const bootstrap = config.bootstrap ?? true
|
|
68
76
|
const tables = config.tableNames ?? DEFAULT_TABLE_NAMES
|
|
69
77
|
|
|
70
|
-
const txManager = postgresTransactionManager(adapter)
|
|
78
|
+
const txManager = postgresTransactionManager(adapter, undefined, config.transaction)
|
|
71
79
|
|
|
72
80
|
return (app: App) => {
|
|
73
81
|
app.set("eventStore", ({ serializer, tagResolver }) =>
|