@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.
- package/README.md +13 -0
- package/dist/adapters/bun-sql.d.ts +2 -1
- package/dist/adapters/bun-sql.d.ts.map +1 -1
- package/dist/adapters/bun-sql.js +5 -0
- package/dist/adapters/bun-sql.js.map +1 -1
- package/dist/adapters/pg.d.ts +2 -1
- package/dist/adapters/pg.d.ts.map +1 -1
- package/dist/adapters/pg.js +5 -0
- package/dist/adapters/pg.js.map +1 -1
- package/dist/adapters/postgres.d.ts +2 -1
- package/dist/adapters/postgres.d.ts.map +1 -1
- package/dist/adapters/postgres.js +5 -0
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/postgres-event-scheduler.js +3 -3
- package/dist/postgres-event-scheduler.js.map +1 -1
- package/dist/postgres-event-store.js +5 -5
- package/dist/postgres-event-store.js.map +1 -1
- package/dist/postgres-transaction-manager.d.ts +1 -20
- package/dist/postgres-transaction-manager.d.ts.map +1 -1
- package/dist/postgres-transaction-manager.js +4 -26
- package/dist/postgres-transaction-manager.js.map +1 -1
- package/dist/postgres.d.ts +0 -8
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +5 -1
- package/dist/postgres.js.map +1 -1
- package/dist/schema.d.ts +9 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +17 -15
- package/dist/schema.js.map +1 -1
- package/dist/session-timeouts.d.ts +47 -0
- package/dist/session-timeouts.d.ts.map +1 -0
- package/dist/session-timeouts.js +44 -0
- package/dist/session-timeouts.js.map +1 -0
- package/package.json +4 -4
- package/src/adapters/bun-sql.ts +10 -1
- package/src/adapters/pg.ts +10 -1
- package/src/adapters/postgres.ts +10 -1
- package/src/index.ts +10 -0
- package/src/postgres-event-scheduler.ts +4 -4
- package/src/postgres-event-store.ts +8 -8
- package/src/postgres-transaction-manager.ts +3 -51
- package/src/postgres.ts +5 -9
- package/src/schema.ts +17 -15
- package/src/session-timeouts.ts +77 -0
package/dist/schema.d.ts.map
CHANGED
|
@@ -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
|
|
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 +
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
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
|
-
|
|
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,
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
|
|
127
|
+
timestamp BIGINT NOT NULL,
|
|
126
128
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
127
129
|
);`;
|
|
128
130
|
}
|
package/dist/schema.js.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
72
|
-
"@kronos-ts/eventsourcing": "0.
|
|
73
|
-
"@kronos-ts/messaging": "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",
|
package/src/adapters/bun-sql.ts
CHANGED
|
@@ -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
|
},
|
package/src/adapters/pg.ts
CHANGED
|
@@ -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.
|
package/src/adapters/postgres.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
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
|
-
|
|
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 +
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
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
|
-
|
|
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,
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
-
|
|
139
|
+
timestamp BIGINT NOT NULL,
|
|
138
140
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
139
141
|
);`
|
|
140
142
|
}
|