@kronos-ts/postgres 0.5.0 → 0.7.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.
Files changed (48) hide show
  1. package/README.md +13 -0
  2. package/dist/adapters/bun-sql.d.ts +2 -1
  3. package/dist/adapters/bun-sql.d.ts.map +1 -1
  4. package/dist/adapters/bun-sql.js +5 -0
  5. package/dist/adapters/bun-sql.js.map +1 -1
  6. package/dist/adapters/pg.d.ts +2 -1
  7. package/dist/adapters/pg.d.ts.map +1 -1
  8. package/dist/adapters/pg.js +5 -0
  9. package/dist/adapters/pg.js.map +1 -1
  10. package/dist/adapters/postgres.d.ts +2 -1
  11. package/dist/adapters/postgres.d.ts.map +1 -1
  12. package/dist/adapters/postgres.js +5 -0
  13. package/dist/adapters/postgres.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/postgres-event-scheduler.js +3 -3
  19. package/dist/postgres-event-scheduler.js.map +1 -1
  20. package/dist/postgres-event-store.js +5 -5
  21. package/dist/postgres-event-store.js.map +1 -1
  22. package/dist/postgres-transaction-manager.d.ts +1 -20
  23. package/dist/postgres-transaction-manager.d.ts.map +1 -1
  24. package/dist/postgres-transaction-manager.js +4 -26
  25. package/dist/postgres-transaction-manager.js.map +1 -1
  26. package/dist/postgres.d.ts +0 -8
  27. package/dist/postgres.d.ts.map +1 -1
  28. package/dist/postgres.js +5 -1
  29. package/dist/postgres.js.map +1 -1
  30. package/dist/schema.d.ts +9 -7
  31. package/dist/schema.d.ts.map +1 -1
  32. package/dist/schema.js +17 -15
  33. package/dist/schema.js.map +1 -1
  34. package/dist/session-timeouts.d.ts +47 -0
  35. package/dist/session-timeouts.d.ts.map +1 -0
  36. package/dist/session-timeouts.js +44 -0
  37. package/dist/session-timeouts.js.map +1 -0
  38. package/package.json +4 -4
  39. package/src/adapters/bun-sql.ts +10 -1
  40. package/src/adapters/pg.ts +10 -1
  41. package/src/adapters/postgres.ts +10 -1
  42. package/src/index.ts +10 -0
  43. package/src/postgres-event-scheduler.ts +4 -4
  44. package/src/postgres-event-store.ts +8 -8
  45. package/src/postgres-transaction-manager.ts +3 -51
  46. package/src/postgres.ts +5 -9
  47. package/src/schema.ts +17 -15
  48. package/src/session-timeouts.ts +77 -0
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,mBAAmB,EAAE,UAIjC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAgB,CAAA;AAErD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CA+B9D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAMhE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAUjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAcvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAIzE;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACzD;AAED,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CACjC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAmBf"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,mBAAmB,EAAE,UAIjC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAgB,CAAA;AAErD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CA+B9D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAMhE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAUjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAcvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAIzE;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACzD;AAED,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CACjC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAmBf"}
package/dist/schema.js CHANGED
@@ -34,11 +34,12 @@ export function buildEventsTableDDL(tables) {
34
34
  // UNIQUE auto-creates a btree; v7's time-ordered prefix keeps it compact under
35
35
  // append load (a v4 random UUID would fragment the leaf pages over time).
36
36
  //
37
- // version + message_timestamp persist the EventMessage's own `version` and authored
38
- // `timestamp` (epoch ms) so source()/open() reconstruct the full EventMessage contract,
39
- // matching the in-memory, axon-server, and kronosdb engines. message_timestamp is the
40
- // authored time distinct from recorded_at (DB insert time). This mirrors the
41
- // scheduled-events table, which already carries both columns.
37
+ // version + timestamp persist the EventMessage's own `version` and authored `timestamp`
38
+ // (epoch ms) so source()/open() reconstruct the full EventMessage contract, matching the
39
+ // in-memory, axon-server, and kronosdb engines. `timestamp` is a BIGINT (epoch ms), fully
40
+ // btree/BRIN-indexable; `timestamp` is a non-reserved keyword in Postgres and works
41
+ // unquoted in every position we use (the only conflict is the `TIMESTAMP 'literal'` cast,
42
+ // which we never write).
42
43
  //
43
44
  // MIGRATION: this is CREATE-only. `CREATE TABLE IF NOT EXISTS` does NOT add columns to a
44
45
  // pre-existing table, and the columns are NOT NULL, so an events table created before
@@ -53,8 +54,7 @@ export function buildEventsTableDDL(tables) {
53
54
  payload JSONB NOT NULL,
54
55
  metadata JSONB NOT NULL DEFAULT '{}',
55
56
  version TEXT NOT NULL,
56
- message_timestamp BIGINT NOT NULL,
57
- recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
57
+ timestamp BIGINT NOT NULL
58
58
  ) WITH (
59
59
  autovacuum_freeze_min_age = 10000000,
60
60
  autovacuum_freeze_table_age = 100000000,
@@ -103,13 +103,15 @@ export function buildSnapshotsTableDDL(tables) {
103
103
  * # Payload columns
104
104
  *
105
105
  * The whole EventMessage shape is captured inline (event_id, type, tags,
106
- * payload, metadata, version, message_timestamp) so the fire-time worker
107
- * can reconstruct it from a single row read. `message_timestamp` is the
108
- * EventMessage's authored timestamp (epoch ms) — distinct from
109
- * `created_at` (when the row was inserted) and `fire_at` (when it should
110
- * fire). At append-time, the worker MAY overwrite message_timestamp with
111
- * `now()` so consumers see the actual append time; that is an
112
- * implementation decision left to the scheduler.
106
+ * payload, metadata, version, timestamp) so the fire-time worker can
107
+ * reconstruct it from a single row read. `timestamp` is the EventMessage's
108
+ * authored timestamp (epoch ms) — distinct from `created_at` (when the row
109
+ * was inserted) and `fire_at` (when it should fire). At append-time, the
110
+ * worker MAY overwrite `timestamp` with `now()` so consumers see the actual
111
+ * append time; that is an implementation decision left to the scheduler.
112
+ *
113
+ * Column names mirror the events table (`version`, `timestamp`) so a schedule
114
+ * row and the event it materialises into share the same vocabulary.
113
115
  */
114
116
  export function buildScheduledEventsTableDDL(tables) {
115
117
  return `CREATE TABLE IF NOT EXISTS ${tables.scheduled} (
@@ -122,7 +124,7 @@ export function buildScheduledEventsTableDDL(tables) {
122
124
  payload JSONB NOT NULL,
123
125
  metadata JSONB NOT NULL DEFAULT '{}',
124
126
  version TEXT NOT NULL,
125
- message_timestamp BIGINT NOT NULL,
127
+ timestamp BIGINT NOT NULL,
126
128
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
127
129
  );`;
128
130
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,MAAM,EAAE,eAAe;IACvB,SAAS,EAAE,kBAAkB;IAC7B,SAAS,EAAE,yBAAyB;CACrC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAW,CAAC,MAAM,CAAA;AAErD,MAAM,UAAU,mBAAmB,CAAC,MAAkB;IACpD,mFAAmF;IACnF,+EAA+E;IAC/E,0EAA0E;IAC1E,EAAE;IACF,oFAAoF;IACpF,wFAAwF;IACxF,sFAAsF;IACtF,+EAA+E;IAC/E,8DAA8D;IAC9D,EAAE;IACF,yFAAyF;IACzF,sFAAsF;IACtF,oFAAoF;IACpF,+DAA+D;IAC/D,OAAO,8BAA8B,MAAM,CAAC,MAAM;;;;;;;;;;;;;;;GAejD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAkB;IACtD,OAAO,qCAAqC,MAAM,CAAC,MAAM;OACpD,MAAM,CAAC,MAAM;;6BAES,MAAM,CAAC,MAAM;OACnC,MAAM,CAAC,MAAM,2CAA2C,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAkB;IACvD,OAAO,8BAA8B,MAAM,CAAC,SAAS;;;;;;;;GAQpD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAkB;IAC7D,OAAO,8BAA8B,MAAM,CAAC,SAAS;;;;;;;;;;;;GAYpD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,8BAA8B,CAAC,MAAkB;IAC/D,OAAO,8BAA8B,MAAM,CAAC,SAAS;OAChD,MAAM,CAAC,SAAS;4BACK,CAAA;AAC5B,CAAC;AAiBD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B,EAC/B,UAAkC,EAAE;IAEpC,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAA;IAExD,uEAAuE;IACvE,qEAAqE;IACrE,sBAAsB;IACtB,MAAM,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAA;IAE5E,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAA;QAChD,MAAM,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAA;QACnD,MAAM,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,MAAM,CAAC,CAAC,CAAA;QACzD,MAAM,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7D,CAAC;YAAS,CAAC;QACT,qEAAqE;QACrE,+CAA+C;QAC/C,MAAM,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAA;IAChF,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,MAAM,EAAE,eAAe;IACvB,SAAS,EAAE,kBAAkB;IAC7B,SAAS,EAAE,yBAAyB;CACrC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAW,CAAC,MAAM,CAAA;AAErD,MAAM,UAAU,mBAAmB,CAAC,MAAkB;IACpD,mFAAmF;IACnF,+EAA+E;IAC/E,0EAA0E;IAC1E,EAAE;IACF,wFAAwF;IACxF,yFAAyF;IACzF,0FAA0F;IAC1F,oFAAoF;IACpF,0FAA0F;IAC1F,yBAAyB;IACzB,EAAE;IACF,yFAAyF;IACzF,sFAAsF;IACtF,oFAAoF;IACpF,+DAA+D;IAC/D,OAAO,8BAA8B,MAAM,CAAC,MAAM;;;;;;;;;;;;;;GAcjD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAkB;IACtD,OAAO,qCAAqC,MAAM,CAAC,MAAM;OACpD,MAAM,CAAC,MAAM;;6BAES,MAAM,CAAC,MAAM;OACnC,MAAM,CAAC,MAAM,2CAA2C,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAkB;IACvD,OAAO,8BAA8B,MAAM,CAAC,SAAS;;;;;;;;GAQpD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAkB;IAC7D,OAAO,8BAA8B,MAAM,CAAC,SAAS;;;;;;;;;;;;GAYpD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,8BAA8B,CAAC,MAAkB;IAC/D,OAAO,8BAA8B,MAAM,CAAC,SAAS;OAChD,MAAM,CAAC,SAAS;4BACK,CAAA;AAC5B,CAAC;AAiBD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B,EAC/B,UAAkC,EAAE;IAEpC,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAA;IAExD,uEAAuE;IACvE,qEAAqE;IACrE,sBAAsB;IACtB,MAAM,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAA;IAE5E,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAA;QAChD,MAAM,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAA;QACnD,MAAM,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,MAAM,CAAC,CAAC,CAAA;QACzD,MAAM,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7D,CAAC;YAAS,CAAC;QACT,qEAAqE;QACrE,+CAA+C;QAC/C,MAAM,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAA;IAChF,CAAC;AACH,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Per-transaction safety timeouts, armed via `SET LOCAL` by each adapter's
3
+ * `transaction()` at BEGIN. Living on the adapter (not the transaction
4
+ * manager) means EVERY postgres transaction is bounded — UoW-scoped commits,
5
+ * the event store's own-tx append/publish, and the scheduler worker tick alike
6
+ * — and each adapter instance carries its own settings, so two adapters
7
+ * pointed at two different databases stay fully decoupled.
8
+ *
9
+ * A non-postgres adapter (e.g. a future sqlite one) arms its own
10
+ * dialect-appropriate settings, or none.
11
+ */
12
+ import type { PostgresAdapterTransaction } from "./adapter.js";
13
+ export interface SessionTimeoutOptions {
14
+ /**
15
+ * `idle_in_transaction_session_timeout` (ms) applied via `SET LOCAL` on every
16
+ * transaction. A transaction that begins but stalls before commit/rollback
17
+ * would otherwise hold its connection — and pin `pg_snapshot_xmin`, which
18
+ * gates the gap-free tailing query in the event store — open indefinitely,
19
+ * stalling all streaming processors until the process restarts. This bounds
20
+ * that window: postgres aborts the idle transaction and the connection (and
21
+ * xmin) is freed. Default 30000 (30s). Set 0 to disable (postgres default).
22
+ */
23
+ readonly idleInTransactionTimeoutMs?: number;
24
+ /**
25
+ * `statement_timeout` (ms) applied via `SET LOCAL` on every transaction.
26
+ * Bounds a single hung statement inside the tx. Default 0 (disabled) — large
27
+ * appends / replays can legitimately run long, so opt in per deployment.
28
+ */
29
+ readonly statementTimeoutMs?: number;
30
+ }
31
+ export interface ResolvedSessionTimeouts {
32
+ readonly idleInTransactionTimeoutMs: number;
33
+ readonly statementTimeoutMs: number;
34
+ }
35
+ /** Resolve config to concrete, normalized timeouts (applying defaults). */
36
+ export declare function resolveSessionTimeouts(opts?: SessionTimeoutOptions): ResolvedSessionTimeouts;
37
+ /**
38
+ * Arm the resolved timeouts on a freshly-opened transaction. No-op for any
39
+ * timeout resolved to 0.
40
+ *
41
+ * GUCs cannot be parameterized ($1) — the value is a config-supplied integer,
42
+ * normalized to a non-negative whole number, so inlining is injection-safe.
43
+ * `SET LOCAL` auto-resets at COMMIT/ROLLBACK, so it never leaks onto pooled
44
+ * connections.
45
+ */
46
+ export declare function applySessionTimeouts(tx: PostgresAdapterTransaction, timeouts: ResolvedSessionTimeouts): Promise<void>;
47
+ //# sourceMappingURL=session-timeouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-timeouts.d.ts","sourceRoot":"","sources":["../src/session-timeouts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAE9D,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CACrC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAA;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;CACpC;AAID,2EAA2E;AAC3E,wBAAgB,sBAAsB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,uBAAuB,CAO5F;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,0BAA0B,EAC9B,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Per-transaction safety timeouts, armed via `SET LOCAL` by each adapter's
3
+ * `transaction()` at BEGIN. Living on the adapter (not the transaction
4
+ * manager) means EVERY postgres transaction is bounded — UoW-scoped commits,
5
+ * the event store's own-tx append/publish, and the scheduler worker tick alike
6
+ * — and each adapter instance carries its own settings, so two adapters
7
+ * pointed at two different databases stay fully decoupled.
8
+ *
9
+ * A non-postgres adapter (e.g. a future sqlite one) arms its own
10
+ * dialect-appropriate settings, or none.
11
+ */
12
+ const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS = 30_000;
13
+ /** Resolve config to concrete, normalized timeouts (applying defaults). */
14
+ export function resolveSessionTimeouts(opts) {
15
+ return {
16
+ idleInTransactionTimeoutMs: normalizeTimeoutMs(opts?.idleInTransactionTimeoutMs ?? DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS),
17
+ statementTimeoutMs: normalizeTimeoutMs(opts?.statementTimeoutMs ?? 0),
18
+ };
19
+ }
20
+ /**
21
+ * Arm the resolved timeouts on a freshly-opened transaction. No-op for any
22
+ * timeout resolved to 0.
23
+ *
24
+ * GUCs cannot be parameterized ($1) — the value is a config-supplied integer,
25
+ * normalized to a non-negative whole number, so inlining is injection-safe.
26
+ * `SET LOCAL` auto-resets at COMMIT/ROLLBACK, so it never leaks onto pooled
27
+ * connections.
28
+ */
29
+ export async function applySessionTimeouts(tx, timeouts) {
30
+ if (timeouts.idleInTransactionTimeoutMs > 0) {
31
+ await tx.query(`SET LOCAL idle_in_transaction_session_timeout = ${timeouts.idleInTransactionTimeoutMs}`);
32
+ }
33
+ if (timeouts.statementTimeoutMs > 0) {
34
+ await tx.query(`SET LOCAL statement_timeout = ${timeouts.statementTimeoutMs}`);
35
+ }
36
+ }
37
+ /** Coerce a config timeout to a non-negative whole number of milliseconds.
38
+ * Non-finite or negative values disable the timeout (treated as 0). */
39
+ function normalizeTimeoutMs(value) {
40
+ if (!Number.isFinite(value) || value <= 0)
41
+ return 0;
42
+ return Math.floor(value);
43
+ }
44
+ //# sourceMappingURL=session-timeouts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-timeouts.js","sourceRoot":"","sources":["../src/session-timeouts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4BH,MAAM,sCAAsC,GAAG,MAAM,CAAA;AAErD,2EAA2E;AAC3E,MAAM,UAAU,sBAAsB,CAAC,IAA4B;IACjE,OAAO;QACL,0BAA0B,EAAE,kBAAkB,CAC5C,IAAI,EAAE,0BAA0B,IAAI,sCAAsC,CAC3E;QACD,kBAAkB,EAAE,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,IAAI,CAAC,CAAC;KACtE,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,EAA8B,EAC9B,QAAiC;IAEjC,IAAI,QAAQ,CAAC,0BAA0B,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,mDAAmD,QAAQ,CAAC,0BAA0B,EAAE,CAAC,CAAA;IAC1G,CAAC;IACD,IAAI,QAAQ,CAAC,kBAAkB,GAAG,CAAC,EAAE,CAAC;QACpC,MAAM,EAAE,CAAC,KAAK,CAAC,iCAAiC,QAAQ,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAChF,CAAC;AACH,CAAC;AAED;wEACwE;AACxE,SAAS,kBAAkB,CAAC,KAAa;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kronos-ts/postgres",
3
- "version": "0.5.0",
3
+ "version": "0.7.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.4.0",
72
- "@kronos-ts/eventsourcing": "0.2.2",
73
- "@kronos-ts/messaging": "0.6.0"
71
+ "@kronos-ts/app": "0.5.0",
72
+ "@kronos-ts/eventsourcing": "0.3.0",
73
+ "@kronos-ts/messaging": "0.8.0"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "pg": ">=8.0.0",
@@ -23,8 +23,13 @@ import type {
23
23
  QueryRow,
24
24
  } from "../adapter.js"
25
25
  import { IsolationLevel } from "../adapter.js"
26
+ import {
27
+ type SessionTimeoutOptions,
28
+ applySessionTimeouts,
29
+ resolveSessionTimeouts,
30
+ } from "../session-timeouts.js"
26
31
 
27
- export interface BunSqlAdapterConfig {
32
+ export interface BunSqlAdapterConfig extends SessionTimeoutOptions {
28
33
  readonly connectionString: string
29
34
  }
30
35
 
@@ -106,6 +111,7 @@ function getBunSql(): BunSqlConstructor {
106
111
  export function bunSqlAdapter(config: BunSqlAdapterConfig): PostgresAdapter {
107
112
  let sql: BunSqlInstance | undefined
108
113
  let disconnected = false
114
+ const timeouts = resolveSessionTimeouts(config)
109
115
 
110
116
  function getInstance(): BunSqlInstance {
111
117
  if (!sql) {
@@ -183,6 +189,9 @@ export function bunSqlAdapter(config: BunSqlAdapterConfig): PostgresAdapter {
183
189
  return txSql.unsafe(text, normalizeParams(params)).catch(normalizeBunSqlError) as Promise<R[]>
184
190
  },
185
191
  }
192
+ // Arm the per-transaction safety timeouts before handing the tx to the
193
+ // caller, so even the very first awaited statement is bounded.
194
+ await applySessionTimeouts(tx, timeouts)
186
195
  return fn(tx)
187
196
  }).catch(normalizeBunSqlError)
188
197
  },
@@ -21,8 +21,13 @@ import type {
21
21
  QueryRow,
22
22
  } from "../adapter.js"
23
23
  import { IsolationLevel } from "../adapter.js"
24
+ import {
25
+ type SessionTimeoutOptions,
26
+ applySessionTimeouts,
27
+ resolveSessionTimeouts,
28
+ } from "../session-timeouts.js"
24
29
 
25
- export interface PgAdapterConfig {
30
+ export interface PgAdapterConfig extends SessionTimeoutOptions {
26
31
  /** Standard libpq URI: postgresql://user:pass@host:port/db */
27
32
  readonly connectionString: string
28
33
  /** Optional pg.Pool config overrides (max connections, idleTimeoutMillis, etc.). */
@@ -39,6 +44,7 @@ export function pgAdapter(config: PgAdapterConfig): PostgresAdapter {
39
44
  let listenClient: PoolClient | undefined
40
45
  const listenSlots = new Map<string, Set<ListenerSlot>>()
41
46
  let disconnected = false
47
+ const timeouts = resolveSessionTimeouts(config)
42
48
 
43
49
  function getPool(): Pool {
44
50
  if (!pool) {
@@ -132,6 +138,9 @@ export function pgAdapter(config: PgAdapterConfig): PostgresAdapter {
132
138
  }
133
139
  let result: T
134
140
  try {
141
+ // Arm the per-transaction safety timeouts before handing the tx to
142
+ // the caller, so even the very first awaited statement is bounded.
143
+ await applySessionTimeouts(tx, timeouts)
135
144
  result = await fn(tx)
136
145
  } catch (err) {
137
146
  // ROLLBACK best-effort; preserve the ORIGINAL error.
@@ -26,8 +26,13 @@ import type {
26
26
  QueryRow,
27
27
  } from "../adapter.js"
28
28
  import { IsolationLevel } from "../adapter.js"
29
+ import {
30
+ type SessionTimeoutOptions,
31
+ applySessionTimeouts,
32
+ resolveSessionTimeouts,
33
+ } from "../session-timeouts.js"
29
34
 
30
- export interface PostgresAdapterConfig {
35
+ export interface PostgresAdapterConfig extends SessionTimeoutOptions {
31
36
  readonly connectionString: string
32
37
  /** Additional postgres.js options. `transform.column.from` is forced off regardless. */
33
38
  readonly clientOptions?: Parameters<typeof postgresClient>[1]
@@ -36,6 +41,7 @@ export interface PostgresAdapterConfig {
36
41
  export function postgresAdapter(config: PostgresAdapterConfig): PostgresAdapter {
37
42
  let sql: Sql | undefined
38
43
  let disconnected = false
44
+ const timeouts = resolveSessionTimeouts(config)
39
45
 
40
46
  function getSql(): Sql {
41
47
  if (!sql) {
@@ -112,6 +118,9 @@ export function postgresAdapter(config: PostgresAdapterConfig): PostgresAdapter
112
118
  return rows
113
119
  },
114
120
  }
121
+ // Arm the per-transaction safety timeouts before handing the tx to the
122
+ // caller, so even the very first awaited statement is bounded.
123
+ await applySessionTimeouts(tx, timeouts)
115
124
  return fn(tx)
116
125
  })) as T
117
126
  },
package/src/index.ts CHANGED
@@ -49,6 +49,16 @@ export { postgres, type PostgresConfig } from "./postgres.js"
49
49
  // hand.
50
50
  export { postgresTransactionManager } from "./postgres-transaction-manager.js"
51
51
 
52
+ // Per-transaction safety timeouts (idle-in-transaction / statement), armed by
53
+ // each adapter's transaction() at BEGIN. The options are spread onto every
54
+ // adapter config; the helpers are exported for authors of custom adapters.
55
+ export {
56
+ type SessionTimeoutOptions,
57
+ type ResolvedSessionTimeouts,
58
+ resolveSessionTimeouts,
59
+ applySessionTimeouts,
60
+ } from "./session-timeouts.js"
61
+
52
62
  // Postgres event scheduler — durable schedule() + cancel() + polling worker
53
63
  // that fires due schedules into the event store. Wired into postgres()
54
64
  // automatically when a uowFactory with the lazy postgres tx is in place;
@@ -109,7 +109,7 @@ interface ScheduleRow {
109
109
  payload: unknown
110
110
  metadata: unknown
111
111
  version: string
112
- message_timestamp: string | number
112
+ timestamp: string | number
113
113
  [key: string]: unknown
114
114
  }
115
115
 
@@ -132,7 +132,7 @@ export function createPostgresEventScheduler(
132
132
  .map((t) => encodeTag(t.key, t.value))
133
133
  await tx.query(
134
134
  `INSERT INTO ${tables.scheduled}
135
- (schedule_id, fire_at, status, type, tags, payload, metadata, version, message_timestamp)
135
+ (schedule_id, fire_at, status, type, tags, payload, metadata, version, timestamp)
136
136
  VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, $8)`,
137
137
  [
138
138
  scheduleId,
@@ -196,7 +196,7 @@ export function createPostgresEventScheduler(
196
196
  name: qualifiedNameFromString(row.type),
197
197
  payload: decodeJsonbValue(row.payload),
198
198
  metadata: decodeJsonbValue(row.metadata) as EventMessage["metadata"],
199
- timestamp: Number(row.message_timestamp),
199
+ timestamp: Number(row.timestamp),
200
200
  version: row.version,
201
201
  tags: decodeTags(row.tags),
202
202
  }
@@ -223,7 +223,7 @@ export function createPostgresEventScheduler(
223
223
  }
224
224
 
225
225
  const rows = await tx.query<ScheduleRow>(
226
- `SELECT schedule_id, type, tags, payload, metadata, version, message_timestamp
226
+ `SELECT schedule_id, type, tags, payload, metadata, version, timestamp
227
227
  FROM ${tables.scheduled}
228
228
  WHERE status = 'pending' AND fire_at <= now()
229
229
  ORDER BY fire_at
@@ -163,7 +163,7 @@ export function createPostgresEventStore(
163
163
  const metadata = e.metadata ?? {}
164
164
 
165
165
  const rows = await tx.query<{ sequence_position: string; transaction_id: string }>(
166
- `INSERT INTO ${tables.events} (event_id, type, tags, payload, metadata, version, message_timestamp)
166
+ `INSERT INTO ${tables.events} (event_id, type, tags, payload, metadata, version, timestamp)
167
167
  VALUES ($1, $2, $3, $4, $5, $6, $7)
168
168
  RETURNING sequence_position, transaction_id`,
169
169
  [
@@ -193,7 +193,7 @@ export function createPostgresEventStore(
193
193
  const start = condition.start ?? 0n
194
194
  const built = buildCriteriaWhere(condition.criteria, 2) // $1 = start
195
195
  const sql = `
196
- SELECT sequence_position, event_id, type, tags, payload, metadata, version, message_timestamp
196
+ SELECT sequence_position, event_id, type, tags, payload, metadata, version, timestamp
197
197
  FROM ${tables.events}
198
198
  WHERE sequence_position >= $1 AND (${built.where})
199
199
  ORDER BY sequence_position ASC
@@ -206,7 +206,7 @@ export function createPostgresEventStore(
206
206
  payload: unknown
207
207
  metadata: unknown
208
208
  version: string
209
- message_timestamp: string | number
209
+ timestamp: string | number
210
210
  }>(sql, [start, ...built.params])
211
211
 
212
212
  const events: EventMessage[] = rows.map((r) => decodeEvent(r))
@@ -482,7 +482,7 @@ export function createPostgresEventStore(
482
482
  sql = `
483
483
  SELECT sequence_position::text AS sequence_position,
484
484
  transaction_id::text AS transaction_id,
485
- event_id, type, tags, payload, metadata, version, message_timestamp
485
+ event_id, type, tags, payload, metadata, version, timestamp
486
486
  FROM ${tables.events}
487
487
  WHERE sequence_position > $1::bigint
488
488
  AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
@@ -501,7 +501,7 @@ export function createPostgresEventStore(
501
501
  sql = `
502
502
  SELECT sequence_position::text AS sequence_position,
503
503
  transaction_id::text AS transaction_id,
504
- event_id, type, tags, payload, metadata, version, message_timestamp
504
+ event_id, type, tags, payload, metadata, version, timestamp
505
505
  FROM ${tables.events}
506
506
  WHERE (transaction_id, sequence_position) > ($1::xid8, $2::bigint)
507
507
  AND transaction_id < pg_snapshot_xmin(pg_current_snapshot())
@@ -521,7 +521,7 @@ export function createPostgresEventStore(
521
521
  payload: unknown
522
522
  metadata: unknown
523
523
  version: string
524
- message_timestamp: string | number
524
+ timestamp: string | number
525
525
  }>(sql, queryParams)
526
526
 
527
527
  for (const r of rows) {
@@ -614,7 +614,7 @@ function decodeEvent(row: {
614
614
  sequence_position: string
615
615
  event_id: string
616
616
  version: string
617
- message_timestamp: string | number
617
+ timestamp: string | number
618
618
  }): EventMessage {
619
619
  const qn = qualifiedNameFromString(row.type)
620
620
  const tags = row.tags.map((t) => {
@@ -631,7 +631,7 @@ function decodeEvent(row: {
631
631
  tags,
632
632
  payload: decodeJsonb(row.payload),
633
633
  metadata: decodeJsonb(row.metadata) as EventMessage["metadata"],
634
- timestamp: Number(row.message_timestamp),
634
+ timestamp: Number(row.timestamp),
635
635
  }
636
636
  }
637
637
 
@@ -36,51 +36,10 @@ 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
-
61
39
  export function postgresTransactionManager(
62
40
  adapter: PostgresAdapter,
63
41
  isolationLevel: IsolationLevel = IsolationLevel.READ_COMMITTED,
64
- options: PostgresTransactionManagerOptions = {},
65
42
  ): 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
-
84
43
  return {
85
44
  async begin(): Promise<PostgresAdapterTransaction> {
86
45
  let captureTx!: (tx: PostgresAdapterTransaction) => void
@@ -97,9 +56,9 @@ export function postgresTransactionManager(
97
56
 
98
57
  const txPromise = adapter
99
58
  .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
+ // Per-transaction safety timeouts are armed by the adapter's
60
+ // transaction() at BEGIN (see session-timeouts.ts), so they cover
61
+ // this UoW-scoped tx and every ad-hoc adapter.transaction() alike.
103
62
  captureTx(tx)
104
63
  await completion
105
64
  })
@@ -153,13 +112,6 @@ export function postgresTransactionManager(
153
112
  }
154
113
  }
155
114
 
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
-
163
115
  /**
164
116
  * Run `fn` inside a postgres tx, joining a UoW-scoped tx if one is active
165
117
  * (or installed lazily), otherwise opening an ad-hoc tx via `adapter.transaction`.
package/src/postgres.ts CHANGED
@@ -60,14 +60,6 @@ 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
- }
71
63
  }
72
64
 
73
65
  export function postgres(config: PostgresConfig): (app: App) => void {
@@ -75,7 +67,11 @@ export function postgres(config: PostgresConfig): (app: App) => void {
75
67
  const bootstrap = config.bootstrap ?? true
76
68
  const tables = config.tableNames ?? DEFAULT_TABLE_NAMES
77
69
 
78
- const txManager = postgresTransactionManager(adapter, undefined, config.transaction)
70
+ // Safety timeouts (idle-in-transaction / statement) are armed by the adapter
71
+ // itself — configure them on the adapter (e.g. `pgAdapter({ connectionString,
72
+ // idleInTransactionTimeoutMs })`), so every transaction through it is bounded
73
+ // and two adapters on two databases stay independently configured.
74
+ const txManager = postgresTransactionManager(adapter)
79
75
 
80
76
  return (app: App) => {
81
77
  app.set("eventStore", ({ serializer, tagResolver }) =>
package/src/schema.ts CHANGED
@@ -43,11 +43,12 @@ export function buildEventsTableDDL(tables: TableNames): string {
43
43
  // UNIQUE auto-creates a btree; v7's time-ordered prefix keeps it compact under
44
44
  // append load (a v4 random UUID would fragment the leaf pages over time).
45
45
  //
46
- // version + message_timestamp persist the EventMessage's own `version` and authored
47
- // `timestamp` (epoch ms) so source()/open() reconstruct the full EventMessage contract,
48
- // matching the in-memory, axon-server, and kronosdb engines. message_timestamp is the
49
- // authored time distinct from recorded_at (DB insert time). This mirrors the
50
- // scheduled-events table, which already carries both columns.
46
+ // version + timestamp persist the EventMessage's own `version` and authored `timestamp`
47
+ // (epoch ms) so source()/open() reconstruct the full EventMessage contract, matching the
48
+ // in-memory, axon-server, and kronosdb engines. `timestamp` is a BIGINT (epoch ms), fully
49
+ // btree/BRIN-indexable; `timestamp` is a non-reserved keyword in Postgres and works
50
+ // unquoted in every position we use (the only conflict is the `TIMESTAMP 'literal'` cast,
51
+ // which we never write).
51
52
  //
52
53
  // MIGRATION: this is CREATE-only. `CREATE TABLE IF NOT EXISTS` does NOT add columns to a
53
54
  // pre-existing table, and the columns are NOT NULL, so an events table created before
@@ -62,8 +63,7 @@ export function buildEventsTableDDL(tables: TableNames): string {
62
63
  payload JSONB NOT NULL,
63
64
  metadata JSONB NOT NULL DEFAULT '{}',
64
65
  version TEXT NOT NULL,
65
- message_timestamp BIGINT NOT NULL,
66
- recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
66
+ timestamp BIGINT NOT NULL
67
67
  ) WITH (
68
68
  autovacuum_freeze_min_age = 10000000,
69
69
  autovacuum_freeze_table_age = 100000000,
@@ -115,13 +115,15 @@ export function buildSnapshotsTableDDL(tables: TableNames): string {
115
115
  * # Payload columns
116
116
  *
117
117
  * The whole EventMessage shape is captured inline (event_id, type, tags,
118
- * payload, metadata, version, message_timestamp) so the fire-time worker
119
- * can reconstruct it from a single row read. `message_timestamp` is the
120
- * EventMessage's authored timestamp (epoch ms) — distinct from
121
- * `created_at` (when the row was inserted) and `fire_at` (when it should
122
- * fire). At append-time, the worker MAY overwrite message_timestamp with
123
- * `now()` so consumers see the actual append time; that is an
124
- * implementation decision left to the scheduler.
118
+ * payload, metadata, version, timestamp) so the fire-time worker can
119
+ * reconstruct it from a single row read. `timestamp` is the EventMessage's
120
+ * authored timestamp (epoch ms) — distinct from `created_at` (when the row
121
+ * was inserted) and `fire_at` (when it should fire). At append-time, the
122
+ * worker MAY overwrite `timestamp` with `now()` so consumers see the actual
123
+ * append time; that is an implementation decision left to the scheduler.
124
+ *
125
+ * Column names mirror the events table (`version`, `timestamp`) so a schedule
126
+ * row and the event it materialises into share the same vocabulary.
125
127
  */
126
128
  export function buildScheduledEventsTableDDL(tables: TableNames): string {
127
129
  return `CREATE TABLE IF NOT EXISTS ${tables.scheduled} (
@@ -134,7 +136,7 @@ export function buildScheduledEventsTableDDL(tables: TableNames): string {
134
136
  payload JSONB NOT NULL,
135
137
  metadata JSONB NOT NULL DEFAULT '{}',
136
138
  version TEXT NOT NULL,
137
- message_timestamp BIGINT NOT NULL,
139
+ timestamp BIGINT NOT NULL,
138
140
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
139
141
  );`
140
142
  }