@kronos-ts/postgres 0.1.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 +176 -0
- package/dist/adapter.d.ts +89 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +29 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters/bun-sql.d.ts +23 -0
- package/dist/adapters/bun-sql.d.ts.map +1 -0
- package/dist/adapters/bun-sql.js +175 -0
- package/dist/adapters/bun-sql.js.map +1 -0
- package/dist/adapters/pg.d.ts +24 -0
- package/dist/adapters/pg.d.ts.map +1 -0
- package/dist/adapters/pg.js +156 -0
- package/dist/adapters/pg.js.map +1 -0
- package/dist/adapters/postgres.d.ts +27 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +99 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/advisory-locks.d.ts +56 -0
- package/dist/advisory-locks.d.ts.map +1 -0
- package/dist/advisory-locks.js +112 -0
- package/dist/advisory-locks.js.map +1 -0
- package/dist/criteria-sql.d.ts +29 -0
- package/dist/criteria-sql.d.ts.map +1 -0
- package/dist/criteria-sql.js +69 -0
- package/dist/criteria-sql.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres-event-store.d.ts +52 -0
- package/dist/postgres-event-store.d.ts.map +1 -0
- package/dist/postgres-event-store.js +496 -0
- package/dist/postgres-event-store.js.map +1 -0
- package/dist/postgres-snapshot-store.d.ts +34 -0
- package/dist/postgres-snapshot-store.d.ts.map +1 -0
- package/dist/postgres-snapshot-store.js +122 -0
- package/dist/postgres-snapshot-store.js.map +1 -0
- package/dist/postgres.d.ts +34 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +42 -0
- package/dist/postgres.js.map +1 -0
- package/dist/schema.d.ts +96 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +174 -0
- package/dist/schema.js.map +1 -0
- package/package.json +93 -0
- package/src/adapter.ts +104 -0
- package/src/adapters/bun-sql.ts +228 -0
- package/src/adapters/pg.ts +189 -0
- package/src/adapters/postgres.ts +134 -0
- package/src/advisory-locks.ts +139 -0
- package/src/criteria-sql.ts +89 -0
- package/src/errors.ts +47 -0
- package/src/index.ts +56 -0
- package/src/postgres-event-store.ts +593 -0
- package/src/postgres-snapshot-store.ts +153 -0
- package/src/postgres.ts +66 -0
- package/src/schema.ts +204 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg (node-postgres 8.20+) reference adapter for @kronos-ts/postgres.
|
|
3
|
+
*
|
|
4
|
+
* Import via the sub-path:
|
|
5
|
+
* import { pgAdapter } from "@kronos-ts/postgres/adapters/pg"
|
|
6
|
+
*
|
|
7
|
+
* Implements every PostgresAdapter method per the contract in
|
|
8
|
+
* ../adapter.ts. Pool sizing follows pg defaults; override via the
|
|
9
|
+
* `poolConfig` field if needed.
|
|
10
|
+
*
|
|
11
|
+
* LISTEN uses a dedicated long-lived PoolClient pinned outside the pool
|
|
12
|
+
* (never released) so notification delivery is not racing pool eviction.
|
|
13
|
+
* That client is closed by `disconnect()`.
|
|
14
|
+
*/
|
|
15
|
+
import { Pool } from "pg";
|
|
16
|
+
import { IsolationLevel } from "../adapter.js";
|
|
17
|
+
export function pgAdapter(config) {
|
|
18
|
+
let pool;
|
|
19
|
+
let listenClient;
|
|
20
|
+
const listenSlots = new Map();
|
|
21
|
+
let disconnected = false;
|
|
22
|
+
function getPool() {
|
|
23
|
+
if (!pool) {
|
|
24
|
+
pool = new Pool({ connectionString: config.connectionString, ...config.poolConfig });
|
|
25
|
+
pool.on("error", () => {
|
|
26
|
+
// Connection-level errors on idle clients are non-fatal for the
|
|
27
|
+
// adapter; pg removes the bad client from the pool automatically.
|
|
28
|
+
// Surfacing them would force every consumer to attach a handler.
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return pool;
|
|
32
|
+
}
|
|
33
|
+
async function ensureListenClient() {
|
|
34
|
+
if (listenClient)
|
|
35
|
+
return listenClient;
|
|
36
|
+
listenClient = await getPool().connect();
|
|
37
|
+
listenClient.on("notification", (msg) => {
|
|
38
|
+
const slots = listenSlots.get(msg.channel);
|
|
39
|
+
if (!slots)
|
|
40
|
+
return;
|
|
41
|
+
for (const slot of slots)
|
|
42
|
+
slot.callback(msg.payload);
|
|
43
|
+
});
|
|
44
|
+
return listenClient;
|
|
45
|
+
}
|
|
46
|
+
const adapter = {
|
|
47
|
+
async connect() {
|
|
48
|
+
// Force pool creation + a no-op query so connect() actually verifies
|
|
49
|
+
// reachability. Without this, a bad connection string would only
|
|
50
|
+
// surface on the first real query.
|
|
51
|
+
const result = await getPool().query("SELECT 1 AS ok");
|
|
52
|
+
if (result.rows[0]?.ok !== 1) {
|
|
53
|
+
throw new Error("pgAdapter.connect(): unexpected health-check response");
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async disconnect() {
|
|
57
|
+
if (disconnected)
|
|
58
|
+
return;
|
|
59
|
+
disconnected = true;
|
|
60
|
+
if (listenClient) {
|
|
61
|
+
try {
|
|
62
|
+
listenClient.release();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore — client may already be gone */
|
|
66
|
+
}
|
|
67
|
+
listenClient = undefined;
|
|
68
|
+
}
|
|
69
|
+
if (pool) {
|
|
70
|
+
await pool.end();
|
|
71
|
+
pool = undefined;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async query(sql, params) {
|
|
75
|
+
const result = await getPool().query(sql, params);
|
|
76
|
+
return result.rows;
|
|
77
|
+
},
|
|
78
|
+
async queryOne(sql, params) {
|
|
79
|
+
const result = await getPool().query(sql, params);
|
|
80
|
+
if (result.rows.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
if (result.rows.length > 1) {
|
|
83
|
+
throw new Error(`pgAdapter.queryOne: more than one row returned (got ${result.rows.length}). ` +
|
|
84
|
+
`Use query() for multi-row results.`);
|
|
85
|
+
}
|
|
86
|
+
return result.rows[0] ?? null;
|
|
87
|
+
},
|
|
88
|
+
async transaction(isolationLevel, fn) {
|
|
89
|
+
const client = await getPool().connect();
|
|
90
|
+
try {
|
|
91
|
+
await client.query(`BEGIN ISOLATION LEVEL ${isolationLevel}`);
|
|
92
|
+
const tx = {
|
|
93
|
+
async query(sql, params) {
|
|
94
|
+
const result = await client.query(sql, params);
|
|
95
|
+
return result.rows;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = await fn(tx);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
// ROLLBACK best-effort; preserve the ORIGINAL error.
|
|
104
|
+
try {
|
|
105
|
+
await client.query("ROLLBACK");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* ignore rollback failure — original error matters more */
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
await client.query("COMMIT");
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
client.release();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async listen(channel, onNotification) {
|
|
120
|
+
const client = await ensureListenClient();
|
|
121
|
+
const slot = { channel, callback: onNotification };
|
|
122
|
+
let slots = listenSlots.get(channel);
|
|
123
|
+
if (!slots) {
|
|
124
|
+
slots = new Set();
|
|
125
|
+
listenSlots.set(channel, slots);
|
|
126
|
+
// Channel identifiers cannot be parameterised in pg's LISTEN — use
|
|
127
|
+
// a safelist: alphanumerics + underscore only. Anything else is a
|
|
128
|
+
// SQL-injection risk by definition.
|
|
129
|
+
if (!/^[A-Za-z0-9_]+$/.test(channel)) {
|
|
130
|
+
throw new Error(`pgAdapter.listen: channel name must match /^[A-Za-z0-9_]+$/, got: ${channel}`);
|
|
131
|
+
}
|
|
132
|
+
await client.query(`LISTEN ${channel}`);
|
|
133
|
+
}
|
|
134
|
+
slots.add(slot);
|
|
135
|
+
return {
|
|
136
|
+
async unlisten() {
|
|
137
|
+
const cur = listenSlots.get(channel);
|
|
138
|
+
if (!cur)
|
|
139
|
+
return;
|
|
140
|
+
cur.delete(slot);
|
|
141
|
+
if (cur.size === 0) {
|
|
142
|
+
listenSlots.delete(channel);
|
|
143
|
+
try {
|
|
144
|
+
await client.query(`UNLISTEN ${channel}`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* connection may already be gone */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return adapter;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=pg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg.js","sourceRoot":"","sources":["../../src/adapters/pg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postgres.js (porsager) reference adapter for @kronos-ts/postgres.
|
|
3
|
+
*
|
|
4
|
+
* Import via the sub-path:
|
|
5
|
+
* import { postgresAdapter } from "@kronos-ts/postgres/adapters/postgres"
|
|
6
|
+
*
|
|
7
|
+
* Notable porsager/postgres quirks handled here:
|
|
8
|
+
* - Default behaviour transforms column names (snake_case → camelCase).
|
|
9
|
+
* We DISABLE this because the SQL we author (and the SP signatures
|
|
10
|
+
* produced by schema.ts) use snake_case column names — letting the
|
|
11
|
+
* transform fire would produce row objects with `sequencePosition`
|
|
12
|
+
* instead of `sequence_position`, breaking the engine code.
|
|
13
|
+
* - Transactions: sql.begin returns the callback's value when it resolves
|
|
14
|
+
* and ROLLBACKs on rejection. We use the `BEGIN ISOLATION LEVEL ${lvl}`
|
|
15
|
+
* option since sql.begin accepts isolation as part of the BEGIN clause.
|
|
16
|
+
* - LISTEN: sql.listen(channel, cb) returns a Promise<{ unlisten() }>.
|
|
17
|
+
* The shape already matches our ListenSubscription contract.
|
|
18
|
+
*/
|
|
19
|
+
import postgresClient from "postgres";
|
|
20
|
+
import type { PostgresAdapter } from "../adapter.js";
|
|
21
|
+
export interface PostgresAdapterConfig {
|
|
22
|
+
readonly connectionString: string;
|
|
23
|
+
/** Additional postgres.js options. `transform.column.from` is forced off regardless. */
|
|
24
|
+
readonly clientOptions?: Parameters<typeof postgresClient>[1];
|
|
25
|
+
}
|
|
26
|
+
export declare function postgresAdapter(config: PostgresAdapterConfig): PostgresAdapter;
|
|
27
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,cAAc,MAAM,UAAU,CAAA;AAErC,OAAO,KAAK,EACV,eAAe,EAIhB,MAAM,eAAe,CAAA;AAGtB,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,wFAAwF;IACxF,QAAQ,CAAC,aAAa,CAAC,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;CAC9D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAkG9E"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postgres.js (porsager) reference adapter for @kronos-ts/postgres.
|
|
3
|
+
*
|
|
4
|
+
* Import via the sub-path:
|
|
5
|
+
* import { postgresAdapter } from "@kronos-ts/postgres/adapters/postgres"
|
|
6
|
+
*
|
|
7
|
+
* Notable porsager/postgres quirks handled here:
|
|
8
|
+
* - Default behaviour transforms column names (snake_case → camelCase).
|
|
9
|
+
* We DISABLE this because the SQL we author (and the SP signatures
|
|
10
|
+
* produced by schema.ts) use snake_case column names — letting the
|
|
11
|
+
* transform fire would produce row objects with `sequencePosition`
|
|
12
|
+
* instead of `sequence_position`, breaking the engine code.
|
|
13
|
+
* - Transactions: sql.begin returns the callback's value when it resolves
|
|
14
|
+
* and ROLLBACKs on rejection. We use the `BEGIN ISOLATION LEVEL ${lvl}`
|
|
15
|
+
* option since sql.begin accepts isolation as part of the BEGIN clause.
|
|
16
|
+
* - LISTEN: sql.listen(channel, cb) returns a Promise<{ unlisten() }>.
|
|
17
|
+
* The shape already matches our ListenSubscription contract.
|
|
18
|
+
*/
|
|
19
|
+
import postgresClient from "postgres";
|
|
20
|
+
import { IsolationLevel } from "../adapter.js";
|
|
21
|
+
export function postgresAdapter(config) {
|
|
22
|
+
let sql;
|
|
23
|
+
let disconnected = false;
|
|
24
|
+
function getSql() {
|
|
25
|
+
if (!sql) {
|
|
26
|
+
sql = postgresClient(config.connectionString, {
|
|
27
|
+
...(config.clientOptions ?? {}),
|
|
28
|
+
// Hard-override the column transform so our snake_case SQL works
|
|
29
|
+
// verbatim. Users who pass a transform in clientOptions are told
|
|
30
|
+
// (via JSDoc) that the column-from transform is ignored.
|
|
31
|
+
transform: {
|
|
32
|
+
...(config.clientOptions?.transform ?? {}),
|
|
33
|
+
column: { from: undefined, to: undefined },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return sql;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
async connect() {
|
|
41
|
+
const c = getSql();
|
|
42
|
+
const rows = await c.unsafe("SELECT 1 AS ok");
|
|
43
|
+
if (rows[0]?.ok !== 1) {
|
|
44
|
+
throw new Error("postgresAdapter.connect: unexpected health-check response");
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async disconnect() {
|
|
48
|
+
if (disconnected)
|
|
49
|
+
return;
|
|
50
|
+
disconnected = true;
|
|
51
|
+
if (sql) {
|
|
52
|
+
await sql.end({ timeout: 5 });
|
|
53
|
+
sql = undefined;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async query(text, params) {
|
|
57
|
+
const c = getSql();
|
|
58
|
+
const rows = (await c.unsafe(text, (params ?? [])));
|
|
59
|
+
return rows;
|
|
60
|
+
},
|
|
61
|
+
async queryOne(text, params) {
|
|
62
|
+
const rows = await this.query(text, params);
|
|
63
|
+
if (rows.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
if (rows.length > 1) {
|
|
66
|
+
throw new Error(`postgresAdapter.queryOne: more than one row returned (got ${rows.length}). Use query() for multi-row results.`);
|
|
67
|
+
}
|
|
68
|
+
return rows[0] ?? null;
|
|
69
|
+
},
|
|
70
|
+
async transaction(isolationLevel, fn) {
|
|
71
|
+
const c = getSql();
|
|
72
|
+
// sql.begin's first argument is the BEGIN options; we pass the
|
|
73
|
+
// isolation level. The callback receives a scoped `sql` that
|
|
74
|
+
// pins to the underlying connection for the duration.
|
|
75
|
+
return (await c.begin(`ISOLATION LEVEL ${isolationLevel}`, async (txSql) => {
|
|
76
|
+
const tx = {
|
|
77
|
+
async query(text, params) {
|
|
78
|
+
const rows = (await txSql.unsafe(text, (params ?? [])));
|
|
79
|
+
return rows;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return fn(tx);
|
|
83
|
+
}));
|
|
84
|
+
},
|
|
85
|
+
async listen(channel, onNotification) {
|
|
86
|
+
if (!/^[A-Za-z0-9_]+$/.test(channel)) {
|
|
87
|
+
throw new Error(`postgresAdapter.listen: channel name must match /^[A-Za-z0-9_]+$/, got: ${channel}`);
|
|
88
|
+
}
|
|
89
|
+
const c = getSql();
|
|
90
|
+
const sub = await c.listen(channel, (payload) => onNotification(payload || undefined));
|
|
91
|
+
return {
|
|
92
|
+
async unlisten() {
|
|
93
|
+
await sub.unlisten();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory-lock taxonomy for the DCB conflict check (D-12.09/10/11).
|
|
3
|
+
*
|
|
4
|
+
* Three keyspaces, S/X-asymmetric:
|
|
5
|
+
*
|
|
6
|
+
* Leaf K(T, t) writer = X-lock, reader = S-lock
|
|
7
|
+
* Type-intent K(T, ε) writer = S-lock, reader = X-lock
|
|
8
|
+
* Global-intent K(ε, ε) writer = S-lock, reader = X-lock
|
|
9
|
+
*
|
|
10
|
+
* The asymmetry lets writers on DISJOINT (T,t) tuples run in parallel
|
|
11
|
+
* (their leaf X-locks don't conflict, and they only take S on the intent
|
|
12
|
+
* keys — multiple S-holders coexist). A Query.all() reader (rare) takes
|
|
13
|
+
* X on the intent keys, briefly blocking ALL writers — but that's the
|
|
14
|
+
* point: Query.all() needs to see a consistent snapshot.
|
|
15
|
+
*
|
|
16
|
+
* Locks are pg_advisory_xact_lock variants: held until tx commit/rollback,
|
|
17
|
+
* automatically released. NEVER use session-scoped locks here — PgBouncer
|
|
18
|
+
* in transaction-pooling mode would leak them.
|
|
19
|
+
*
|
|
20
|
+
* Reimplemented from principles per D-12.11. The kraken-tech version is
|
|
21
|
+
* a reference for the taxonomy and FNV-1a hash; the SQL and serialisation
|
|
22
|
+
* format below are original.
|
|
23
|
+
*/
|
|
24
|
+
import type { PostgresAdapterTransaction } from "./adapter.js";
|
|
25
|
+
/**
|
|
26
|
+
* FNV-1a 64-bit hash. Returns a BIGINT in the signed 64-bit range so it can
|
|
27
|
+
* be passed directly to `pg_advisory_xact_lock(BIGINT)`. Values with the
|
|
28
|
+
* high bit set are returned as negative numbers (two's complement).
|
|
29
|
+
*
|
|
30
|
+
* Per FNV-1a: h = offset_basis; for each byte b: h = (h XOR b) * prime mod 2^64.
|
|
31
|
+
* UTF-8 byte iteration via TextEncoder for predictable cross-runtime behaviour.
|
|
32
|
+
*/
|
|
33
|
+
export declare function hashLockKey(input: string): bigint;
|
|
34
|
+
export type LockKeyspace = "leaf" | "type-intent" | "global-intent";
|
|
35
|
+
export declare function leafKey(type: string, tag: string): bigint;
|
|
36
|
+
export declare function typeIntentKey(type: string): bigint;
|
|
37
|
+
export declare function globalIntentKey(): bigint;
|
|
38
|
+
export interface LockTarget {
|
|
39
|
+
readonly type: string;
|
|
40
|
+
readonly tag: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Writer pattern: X-lock on each unique leaf, S-lock on each unique
|
|
44
|
+
* type-intent + the global-intent. Writers on disjoint leaves run in
|
|
45
|
+
* parallel; writers sharing a leaf serialise.
|
|
46
|
+
*
|
|
47
|
+
* Locks are issued in a stable order (sorted by key value) to avoid
|
|
48
|
+
* deadlocks between concurrent writers that share multiple leaves.
|
|
49
|
+
*/
|
|
50
|
+
export declare function acquireWriteLocks(tx: PostgresAdapterTransaction, targets: ReadonlyArray<LockTarget>): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Reader pattern (Query.all): S on leaves, X on type-intent + global-intent.
|
|
53
|
+
* Inverse of writer; briefly excludes all writers while held.
|
|
54
|
+
*/
|
|
55
|
+
export declare function acquireReadLocks(tx: PostgresAdapterTransaction, targets: ReadonlyArray<LockTarget>): Promise<void>;
|
|
56
|
+
//# sourceMappingURL=advisory-locks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisory-locks.d.ts","sourceRoot":"","sources":["../src/advisory-locks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAa9D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASjD;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,eAAe,CAAA;AAEnE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,0BAA0B,EAC9B,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,0BAA0B,EAC9B,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAkBf"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory-lock taxonomy for the DCB conflict check (D-12.09/10/11).
|
|
3
|
+
*
|
|
4
|
+
* Three keyspaces, S/X-asymmetric:
|
|
5
|
+
*
|
|
6
|
+
* Leaf K(T, t) writer = X-lock, reader = S-lock
|
|
7
|
+
* Type-intent K(T, ε) writer = S-lock, reader = X-lock
|
|
8
|
+
* Global-intent K(ε, ε) writer = S-lock, reader = X-lock
|
|
9
|
+
*
|
|
10
|
+
* The asymmetry lets writers on DISJOINT (T,t) tuples run in parallel
|
|
11
|
+
* (their leaf X-locks don't conflict, and they only take S on the intent
|
|
12
|
+
* keys — multiple S-holders coexist). A Query.all() reader (rare) takes
|
|
13
|
+
* X on the intent keys, briefly blocking ALL writers — but that's the
|
|
14
|
+
* point: Query.all() needs to see a consistent snapshot.
|
|
15
|
+
*
|
|
16
|
+
* Locks are pg_advisory_xact_lock variants: held until tx commit/rollback,
|
|
17
|
+
* automatically released. NEVER use session-scoped locks here — PgBouncer
|
|
18
|
+
* in transaction-pooling mode would leak them.
|
|
19
|
+
*
|
|
20
|
+
* Reimplemented from principles per D-12.11. The kraken-tech version is
|
|
21
|
+
* a reference for the taxonomy and FNV-1a hash; the SQL and serialisation
|
|
22
|
+
* format below are original.
|
|
23
|
+
*/
|
|
24
|
+
const UNIT_SEPARATOR = ""; // ASCII Unit Separator (U+001F) — prevents tuple-collision hashing
|
|
25
|
+
const KEYSPACE_LEAF = "L";
|
|
26
|
+
const KEYSPACE_TYPE_INTENT = "T";
|
|
27
|
+
const KEYSPACE_GLOBAL_INTENT = "G";
|
|
28
|
+
// FNV-1a 64-bit constants
|
|
29
|
+
const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
|
|
30
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
31
|
+
const MASK_64 = (1n << 64n) - 1n;
|
|
32
|
+
const SIGN_THRESHOLD = 1n << 63n;
|
|
33
|
+
/**
|
|
34
|
+
* FNV-1a 64-bit hash. Returns a BIGINT in the signed 64-bit range so it can
|
|
35
|
+
* be passed directly to `pg_advisory_xact_lock(BIGINT)`. Values with the
|
|
36
|
+
* high bit set are returned as negative numbers (two's complement).
|
|
37
|
+
*
|
|
38
|
+
* Per FNV-1a: h = offset_basis; for each byte b: h = (h XOR b) * prime mod 2^64.
|
|
39
|
+
* UTF-8 byte iteration via TextEncoder for predictable cross-runtime behaviour.
|
|
40
|
+
*/
|
|
41
|
+
export function hashLockKey(input) {
|
|
42
|
+
const bytes = new TextEncoder().encode(input);
|
|
43
|
+
let h = FNV_OFFSET_BASIS;
|
|
44
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
45
|
+
h = (h ^ BigInt(bytes[i])) & MASK_64;
|
|
46
|
+
h = (h * FNV_PRIME) & MASK_64;
|
|
47
|
+
}
|
|
48
|
+
// Reinterpret as signed 64-bit
|
|
49
|
+
return h >= SIGN_THRESHOLD ? h - (1n << 64n) : h;
|
|
50
|
+
}
|
|
51
|
+
export function leafKey(type, tag) {
|
|
52
|
+
return hashLockKey(`${KEYSPACE_LEAF}${UNIT_SEPARATOR}${type}${UNIT_SEPARATOR}${tag}`);
|
|
53
|
+
}
|
|
54
|
+
export function typeIntentKey(type) {
|
|
55
|
+
return hashLockKey(`${KEYSPACE_TYPE_INTENT}${UNIT_SEPARATOR}${type}`);
|
|
56
|
+
}
|
|
57
|
+
export function globalIntentKey() {
|
|
58
|
+
return hashLockKey(KEYSPACE_GLOBAL_INTENT);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Writer pattern: X-lock on each unique leaf, S-lock on each unique
|
|
62
|
+
* type-intent + the global-intent. Writers on disjoint leaves run in
|
|
63
|
+
* parallel; writers sharing a leaf serialise.
|
|
64
|
+
*
|
|
65
|
+
* Locks are issued in a stable order (sorted by key value) to avoid
|
|
66
|
+
* deadlocks between concurrent writers that share multiple leaves.
|
|
67
|
+
*/
|
|
68
|
+
export async function acquireWriteLocks(tx, targets) {
|
|
69
|
+
if (targets.length === 0) {
|
|
70
|
+
// Even with no leaf targets, acquire the global-intent S-lock so that
|
|
71
|
+
// a Query.all() X on the global-intent can block us if needed.
|
|
72
|
+
await tx.query(`SELECT pg_advisory_xact_lock_shared($1)`, [globalIntentKey()]);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const leafKeys = uniqueSorted(targets.map((t) => leafKey(t.type, t.tag)));
|
|
76
|
+
const typeIntentKeys = uniqueSorted([...new Set(targets.map((t) => t.type))].map(typeIntentKey));
|
|
77
|
+
// X on leaves (sorted for deadlock-free acquisition order)
|
|
78
|
+
for (const k of leafKeys) {
|
|
79
|
+
await tx.query(`SELECT pg_advisory_xact_lock($1)`, [k]);
|
|
80
|
+
}
|
|
81
|
+
// S on type-intent
|
|
82
|
+
for (const k of typeIntentKeys) {
|
|
83
|
+
await tx.query(`SELECT pg_advisory_xact_lock_shared($1)`, [k]);
|
|
84
|
+
}
|
|
85
|
+
// S on global-intent
|
|
86
|
+
await tx.query(`SELECT pg_advisory_xact_lock_shared($1)`, [globalIntentKey()]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reader pattern (Query.all): S on leaves, X on type-intent + global-intent.
|
|
90
|
+
* Inverse of writer; briefly excludes all writers while held.
|
|
91
|
+
*/
|
|
92
|
+
export async function acquireReadLocks(tx, targets) {
|
|
93
|
+
if (targets.length === 0) {
|
|
94
|
+
// A truly empty Query.all() still needs the global-intent X to see a
|
|
95
|
+
// consistent snapshot across all types.
|
|
96
|
+
await tx.query(`SELECT pg_advisory_xact_lock($1)`, [globalIntentKey()]);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const leafKeys = uniqueSorted(targets.map((t) => leafKey(t.type, t.tag)));
|
|
100
|
+
const typeIntentKeys = uniqueSorted([...new Set(targets.map((t) => t.type))].map(typeIntentKey));
|
|
101
|
+
for (const k of leafKeys) {
|
|
102
|
+
await tx.query(`SELECT pg_advisory_xact_lock_shared($1)`, [k]);
|
|
103
|
+
}
|
|
104
|
+
for (const k of typeIntentKeys) {
|
|
105
|
+
await tx.query(`SELECT pg_advisory_xact_lock($1)`, [k]);
|
|
106
|
+
}
|
|
107
|
+
await tx.query(`SELECT pg_advisory_xact_lock($1)`, [globalIntentKey()]);
|
|
108
|
+
}
|
|
109
|
+
function uniqueSorted(xs) {
|
|
110
|
+
return [...new Set(xs)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=advisory-locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisory-locks.js","sourceRoot":"","sources":["../src/advisory-locks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventCriteria → SQL WHERE clause builder.
|
|
3
|
+
*
|
|
4
|
+
* Maps the discriminated-union criteria from @kronos-ts/messaging into a
|
|
5
|
+
* parameterised WHERE fragment + parameter array. The caller (Plan 04
|
|
6
|
+
* source() + the append SP body) splices this into a larger query.
|
|
7
|
+
*
|
|
8
|
+
* Tag semantics: `@>` (contains-all). NEVER `&&` (overlap). The reference
|
|
9
|
+
* is packages/eventsourcing/src/in-memory-event-store.ts:matchesTags —
|
|
10
|
+
* `criteria.tags.every(requiredTag => event.tags.some(...))` is exactly
|
|
11
|
+
* the meaning of `tags @> $required`.
|
|
12
|
+
*
|
|
13
|
+
* Tag encoding: each `{key, value}` is serialised as `${key}${value}`.
|
|
14
|
+
* Stored events use the same encoding (Plan 04 Task 3 — appendEvents flattens
|
|
15
|
+
* tags via the same scheme) so `@>` works literally against text[].
|
|
16
|
+
*/
|
|
17
|
+
import type { EventCriteria } from "@kronos-ts/messaging";
|
|
18
|
+
export interface CriteriaSQL {
|
|
19
|
+
/** SQL WHERE fragment (no leading "WHERE"). Always truthy — empty
|
|
20
|
+
* criteria collapse to `"true"`. */
|
|
21
|
+
readonly where: string;
|
|
22
|
+
/** Parameters in $-positional order. */
|
|
23
|
+
readonly params: ReadonlyArray<unknown>;
|
|
24
|
+
/** Next available $N index for chaining into a larger query. */
|
|
25
|
+
readonly nextParamIndex: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function encodeTag(key: string, value: string): string;
|
|
28
|
+
export declare function buildCriteriaWhere(criteria: EventCriteria, startIndex: number): CriteriaSQL;
|
|
29
|
+
//# sourceMappingURL=criteria-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-sql.d.ts","sourceRoot":"","sources":["../src/criteria-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,MAAM,WAAW,WAAW;IAC1B;yCACqC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IACvC,gEAAgE;IAChE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAID,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,MAAM,GACjB,WAAW,CAkDb"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventCriteria → SQL WHERE clause builder.
|
|
3
|
+
*
|
|
4
|
+
* Maps the discriminated-union criteria from @kronos-ts/messaging into a
|
|
5
|
+
* parameterised WHERE fragment + parameter array. The caller (Plan 04
|
|
6
|
+
* source() + the append SP body) splices this into a larger query.
|
|
7
|
+
*
|
|
8
|
+
* Tag semantics: `@>` (contains-all). NEVER `&&` (overlap). The reference
|
|
9
|
+
* is packages/eventsourcing/src/in-memory-event-store.ts:matchesTags —
|
|
10
|
+
* `criteria.tags.every(requiredTag => event.tags.some(...))` is exactly
|
|
11
|
+
* the meaning of `tags @> $required`.
|
|
12
|
+
*
|
|
13
|
+
* Tag encoding: each `{key, value}` is serialised as `${key}${value}`.
|
|
14
|
+
* Stored events use the same encoding (Plan 04 Task 3 — appendEvents flattens
|
|
15
|
+
* tags via the same scheme) so `@>` works literally against text[].
|
|
16
|
+
*/
|
|
17
|
+
const TAG_DELIMITER = ""; // ASCII Unit Separator (U+001F) — prevents key/value boundary collisions in encoded tag strings
|
|
18
|
+
export function encodeTag(key, value) {
|
|
19
|
+
return `${key}${TAG_DELIMITER}${value}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildCriteriaWhere(criteria, startIndex) {
|
|
22
|
+
switch (criteria.kind) {
|
|
23
|
+
case "any-tag":
|
|
24
|
+
return {
|
|
25
|
+
where: "cardinality(tags) > 0",
|
|
26
|
+
params: [],
|
|
27
|
+
nextParamIndex: startIndex,
|
|
28
|
+
};
|
|
29
|
+
case "tags": {
|
|
30
|
+
if (criteria.tags.length === 0) {
|
|
31
|
+
// Empty contains-all matches everything (the in-memory store's
|
|
32
|
+
// `tags.every(...)` over an empty array is vacuously true).
|
|
33
|
+
return { where: "true", params: [], nextParamIndex: startIndex };
|
|
34
|
+
}
|
|
35
|
+
const encoded = criteria.tags.map((t) => encodeTag(t.key, t.value));
|
|
36
|
+
return {
|
|
37
|
+
where: `tags @> $${startIndex}::text[]`,
|
|
38
|
+
params: [encoded],
|
|
39
|
+
nextParamIndex: startIndex + 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
case "type-restricted": {
|
|
43
|
+
const inner = buildCriteriaWhere(criteria.inner, startIndex);
|
|
44
|
+
const typeParam = inner.nextParamIndex;
|
|
45
|
+
return {
|
|
46
|
+
where: `(${inner.where}) AND type = ANY($${typeParam}::text[])`,
|
|
47
|
+
params: [...inner.params, criteria.types],
|
|
48
|
+
nextParamIndex: typeParam + 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case "either": {
|
|
52
|
+
const parts = [];
|
|
53
|
+
const params = [];
|
|
54
|
+
let next = startIndex;
|
|
55
|
+
for (const sub of criteria.criteria) {
|
|
56
|
+
const built = buildCriteriaWhere(sub, next);
|
|
57
|
+
parts.push(`(${built.where})`);
|
|
58
|
+
params.push(...built.params);
|
|
59
|
+
next = built.nextParamIndex;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
where: parts.join(" OR "),
|
|
63
|
+
params,
|
|
64
|
+
nextParamIndex: next,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=criteria-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-sql.js","sourceRoot":"","sources":["../src/criteria-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAcH,MAAM,aAAa,GAAG,GAAG,CAAA,CAAC,gGAAgG;AAE1H,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,KAAa;IAClD,OAAO,GAAG,GAAG,GAAG,aAAa,GAAG,KAAK,EAAE,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,QAAuB,EACvB,UAAkB;IAElB,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,SAAS;YACZ,OAAO;gBACL,KAAK,EAAE,uBAAuB;gBAC9B,MAAM,EAAE,EAAE;gBACV,cAAc,EAAE,UAAU;aAC3B,CAAA;QAEH,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC/B,+DAA+D;gBAC/D,4DAA4D;gBAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;YAClE,CAAC;YACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;YACnE,OAAO;gBACL,KAAK,EAAE,YAAY,UAAU,UAAU;gBACvC,MAAM,EAAE,CAAC,OAAO,CAAC;gBACjB,cAAc,EAAE,UAAU,GAAG,CAAC;aAC/B,CAAA;QACH,CAAC;QAED,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,MAAM,KAAK,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;YAC5D,MAAM,SAAS,GAAG,KAAK,CAAC,cAAc,CAAA;YACtC,OAAO;gBACL,KAAK,EAAE,IAAI,KAAK,CAAC,KAAK,qBAAqB,SAAS,WAAW;gBAC/D,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC;gBACzC,cAAc,EAAE,SAAS,GAAG,CAAC;aAC9B,CAAA;QACH,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,KAAK,GAAa,EAAE,CAAA;YAC1B,MAAM,MAAM,GAAc,EAAE,CAAA;YAC5B,IAAI,IAAI,GAAG,UAAU,CAAA;YACrB,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACpC,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;gBAC3C,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,CAAA;gBAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;gBAC5B,IAAI,GAAG,KAAK,CAAC,cAAc,CAAA;YAC7B,CAAC;YACD,OAAO;gBACL,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;gBACzB,MAAM;gBACN,cAAc,EAAE,IAAI;aACrB,CAAA;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLSTATE used by the schema-bootstrap stored procedure when a DCB
|
|
3
|
+
* append condition is violated. Per D-12.12: dedicated SQLSTATE via
|
|
4
|
+
* `RAISE ... USING ERRCODE`, never error-text parsing.
|
|
5
|
+
*
|
|
6
|
+
* `KR001` lives in the Postgres user-defined SQLSTATE range (KX–ZZ).
|
|
7
|
+
* It is intentionally distinct from:
|
|
8
|
+
* - `P0001` — generic RAISE (would over-match any unhandled plpgsql exception)
|
|
9
|
+
* - `23505` — unique_violation (used by primary key conflicts)
|
|
10
|
+
* Adapter layers translate `err.code === KR001` into AppendConditionError.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KRONOS_DCB_VIOLATION_SQLSTATE = "KR001";
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when an append condition is violated — optimistic concurrency
|
|
15
|
+
* failure. Structurally mirrors `@kronos-ts/eventsourcing`'s
|
|
16
|
+
* AppendConditionError so callers that catch either get equivalent
|
|
17
|
+
* behaviour, but we ship our own class so that the SQLSTATE-catch
|
|
18
|
+
* boundary lives inside this package's import graph.
|
|
19
|
+
*/
|
|
20
|
+
export declare class AppendConditionError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
static fromConflictCount(count: number, afterPosition: bigint): AppendConditionError;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adapter-agnostic check: pg and postgres.js both surface SQLSTATE on
|
|
26
|
+
* thrown errors as `.code` (string). Bun.sql follows the same convention.
|
|
27
|
+
* This helper keeps the SQLSTATE constant centralised.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isDcbViolation(err: unknown): boolean;
|
|
30
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6BAA6B,UAAU,CAAA;AAEpD;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;IAK3B,MAAM,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,oBAAoB;CAMrF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAOpD"}
|