@kronos-ts/postgres 0.3.3 → 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.
@@ -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
- export declare function postgresTransactionManager(adapter: PostgresAdapter, isolationLevel?: IsolationLevel): TransactionManager<PostgresAdapterTransaction>;
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,GAC7D,kBAAkB,CAAC,0BAA0B,CAAC,CAqDhD;AAED;;;;;;;;;;;;;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"}
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
- export function postgresTransactionManager(adapter, isolationLevel = IsolationLevel.READ_COMMITTED) {
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
- const tx = (await txReady);
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;AAEzD,MAAM,UAAU,0BAA0B,CACxC,OAAwB,EACxB,iBAAiC,cAAc,CAAC,cAAc;IAE9D,OAAO;QACL,KAAK,CAAC,KAAK;YACT,IAAI,SAAoD,CAAA;YACxD,MAAM,OAAO,GAAG,IAAI,OAAO,CAA6B,CAAC,GAAG,EAAE,EAAE;gBAC9D,SAAS,GAAG,GAAG,CAAA;YACjB,CAAC,CAAC,CAAA;YAEF,IAAI,aAA0B,CAAA;YAC9B,IAAI,cAAuC,CAAA;YAC3C,MAAM,UAAU,GAAG,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;gBAChD,aAAa,GAAG,GAAG,CAAA;gBACnB,cAAc,GAAG,GAAG,CAAA;YACtB,CAAC,CAAC,CAAA;YAEF,MAAM,SAAS,GAAG,OAAO;iBACtB,WAAW,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;gBACxC,SAAS,CAAC,EAAE,CAAC,CAAA;gBACb,MAAM,UAAU,CAAA;YAClB,CAAC,CAAC;iBACD,IAAI,CACH,GAAG,EAAE,CAAC,SAAS,EACf,CAAC,GAAG,EAAE,EAAE;gBACN,yDAAyD;gBACzD,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,OAAO,KAAK,eAAe;oBAAE,OAAM;gBACnE,MAAM,GAAG,CAAA;YACX,CAAC,CACF,CAAA;YAEH,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAA+B,CAAA;YACxD,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,CAAA;YAC7D,OAAO,EAAE,CAAA;QACX,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,EAA8B;YACzC,MAAM,IAAI,GAAI,EAAiC,CAAC,UAAU,CAAC,CAAA;YAC3D,IAAI,CAAC,aAAa,EAAE,CAAA;YACpB,MAAM,IAAI,CAAC,SAAS,CAAA;QACtB,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,EAA8B;YAC3C,MAAM,IAAI,GAAI,EAAiC,CAAC,UAAU,CAAC,CAAA;YAC3D,IAAI,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAA;YAC/C,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,CAAA;YACtB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,gEAAgE;gBAChE,8DAA8D;gBAC9D,8CAA8C;gBAC9C,OAAO,CAAC,IAAI,CAAC,kDAAkD,EAAE,GAAG,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAwB,EACxB,EAAkD,EAClD,iBAAiC,cAAc,CAAC,cAAc;IAE9D,MAAM,MAAM,GAAG,MAAM,2BAA2B,EAA8B,CAAA;IAC9E,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC,MAAM,CAAC,CAAA;IAC3C,OAAO,OAAO,CAAC,WAAW,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;AAChD,CAAC"}
1
+ {"version":3,"file":"postgres-transaction-manager.js","sourceRoot":"","sources":["../src/postgres-transaction-manager.ts"],"names":[],"mappings":"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"}
@@ -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
@@ -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 }));
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../src/postgres.ts"],"names":[],"mappings":"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"}
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.3",
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.3",
72
- "@kronos-ts/eventsourcing": "0.2.0",
73
- "@kronos-ts/messaging": "0.5.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
- const tx = (await txReady) as ManagedPostgresTransaction
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 }) =>