@relaybus/postgres 0.4.0 → 0.4.1
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 +18 -15
- package/dist/index.cjs +124 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +124 -39
- package/dist/index.js.map +1 -1
- package/dist/prisma.cjs.map +1 -1
- package/dist/prisma.d.cts +1 -1
- package/dist/prisma.d.ts +1 -1
- package/dist/prisma.js.map +1 -1
- package/package.json +6 -5
package/dist/index.d.cts
CHANGED
|
@@ -70,6 +70,8 @@ interface PostgresNames {
|
|
|
70
70
|
readonly schema: string;
|
|
71
71
|
readonly quotedSchema: string;
|
|
72
72
|
readonly events: string;
|
|
73
|
+
/** Unquoted, unqualified name of {@link events}, for catalog lookups. */
|
|
74
|
+
readonly eventsTableName: string;
|
|
73
75
|
readonly deliveries: string;
|
|
74
76
|
readonly keySequences: string;
|
|
75
77
|
/** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
|
|
@@ -110,8 +112,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
|
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
114
|
* Statements that create the schema, tables and indexes, in dependency order,
|
|
113
|
-
* and upgrade a 0.1.0 schema
|
|
114
|
-
* (`IF NOT EXISTS`, or guarded by a check for the
|
|
115
|
+
* and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
|
|
116
|
+
* Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
|
|
115
117
|
* one transaction and serialize concurrent runs (see
|
|
116
118
|
* {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
|
|
117
119
|
* into your own migration tool.
|
|
@@ -250,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
250
252
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
251
253
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
252
254
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
253
|
-
* When the lane table already carries the marker
|
|
254
|
-
*
|
|
255
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
256
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
255
257
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
256
258
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
257
259
|
* `createPostgresSchemaSql()` instead.
|
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ interface PostgresNames {
|
|
|
70
70
|
readonly schema: string;
|
|
71
71
|
readonly quotedSchema: string;
|
|
72
72
|
readonly events: string;
|
|
73
|
+
/** Unquoted, unqualified name of {@link events}, for catalog lookups. */
|
|
74
|
+
readonly eventsTableName: string;
|
|
73
75
|
readonly deliveries: string;
|
|
74
76
|
readonly keySequences: string;
|
|
75
77
|
/** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
|
|
@@ -110,8 +112,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
|
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
114
|
* Statements that create the schema, tables and indexes, in dependency order,
|
|
113
|
-
* and upgrade a 0.1.0 schema
|
|
114
|
-
* (`IF NOT EXISTS`, or guarded by a check for the
|
|
115
|
+
* and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
|
|
116
|
+
* Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
|
|
115
117
|
* one transaction and serialize concurrent runs (see
|
|
116
118
|
* {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
|
|
117
119
|
* into your own migration tool.
|
|
@@ -250,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
250
252
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
251
253
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
252
254
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
253
|
-
* When the lane table already carries the marker
|
|
254
|
-
*
|
|
255
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
256
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
255
257
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
256
258
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
257
259
|
* `createPostgresSchemaSql()` instead.
|
package/dist/index.js
CHANGED
|
@@ -228,6 +228,7 @@ function resolvePostgresNames(options = {}) {
|
|
|
228
228
|
schema,
|
|
229
229
|
quotedSchema,
|
|
230
230
|
events: table("events"),
|
|
231
|
+
eventsTableName: name("events"),
|
|
231
232
|
deliveries: table("deliveries"),
|
|
232
233
|
keySequences: table("key_sequences"),
|
|
233
234
|
lanes: table("lanes"),
|
|
@@ -254,8 +255,17 @@ function laneExpression(keyExpression, eventIdExpression) {
|
|
|
254
255
|
|
|
255
256
|
// src/schema.ts
|
|
256
257
|
var SCHEMA_MARKER_DIGITS = 32;
|
|
258
|
+
var PAYLOAD_COLUMNS = ` payload json NOT NULL,
|
|
259
|
+
headers json NOT NULL DEFAULT '{}'::json,`;
|
|
260
|
+
var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
|
|
261
|
+
headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
|
|
257
262
|
function createPostgresSchemaStatements(options = {}) {
|
|
258
263
|
const names = resolvePostgresNames(options);
|
|
264
|
+
const statements = schemaBodyStatements(names);
|
|
265
|
+
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
|
|
266
|
+
return statements;
|
|
267
|
+
}
|
|
268
|
+
function schemaBodyStatements(names) {
|
|
259
269
|
const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
|
|
260
270
|
if (names.completionHistory) {
|
|
261
271
|
statements.push(...completionHistoryStatements(names));
|
|
@@ -263,7 +273,6 @@ function createPostgresSchemaStatements(options = {}) {
|
|
|
263
273
|
if (names.schema !== "public") {
|
|
264
274
|
statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
|
|
265
275
|
}
|
|
266
|
-
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
|
|
267
276
|
return statements;
|
|
268
277
|
}
|
|
269
278
|
function eventAndDeliveryStatements(names) {
|
|
@@ -277,14 +286,14 @@ function eventAndKeySequenceStatements(names) {
|
|
|
277
286
|
source text NOT NULL,
|
|
278
287
|
key text,
|
|
279
288
|
sequence bigint,
|
|
280
|
-
|
|
281
|
-
headers jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
289
|
+
${PAYLOAD_COLUMNS}
|
|
282
290
|
correlation_id text,
|
|
283
291
|
causation_id text,
|
|
284
292
|
occurred_at timestamptz NOT NULL,
|
|
285
293
|
created_at timestamptz NOT NULL,
|
|
286
294
|
handlers text[] NOT NULL DEFAULT '{}'
|
|
287
295
|
)`,
|
|
296
|
+
upgradePayloadToJson(names),
|
|
288
297
|
`CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
|
|
289
298
|
`CREATE TABLE IF NOT EXISTS ${names.keySequences} (
|
|
290
299
|
key text PRIMARY KEY,
|
|
@@ -380,6 +389,33 @@ BEGIN
|
|
|
380
389
|
END
|
|
381
390
|
$relaybus_upgrade$`;
|
|
382
391
|
}
|
|
392
|
+
function upgradePayloadToJson(names) {
|
|
393
|
+
return `DO $relaybus_payload_json$
|
|
394
|
+
BEGIN
|
|
395
|
+
IF EXISTS (
|
|
396
|
+
SELECT 1 FROM pg_attribute
|
|
397
|
+
WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
|
|
398
|
+
AND atttypid = 'jsonb'::regtype AND NOT attisdropped
|
|
399
|
+
) THEN
|
|
400
|
+
ALTER TABLE ${names.events}
|
|
401
|
+
ALTER COLUMN headers DROP DEFAULT,
|
|
402
|
+
ALTER COLUMN payload TYPE json USING payload::json,
|
|
403
|
+
ALTER COLUMN headers TYPE json USING headers::json,
|
|
404
|
+
ALTER COLUMN headers SET DEFAULT '{}'::json;
|
|
405
|
+
END IF;
|
|
406
|
+
END
|
|
407
|
+
$relaybus_payload_json$`;
|
|
408
|
+
}
|
|
409
|
+
function version040SchemaMarker(options = {}) {
|
|
410
|
+
const names = resolvePostgresNames(options);
|
|
411
|
+
return version040Marker(names, schemaBodyStatements(names));
|
|
412
|
+
}
|
|
413
|
+
function version040Marker(names, statements) {
|
|
414
|
+
const conversion = upgradePayloadToJson(names);
|
|
415
|
+
return postgresSchemaMarker(
|
|
416
|
+
statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
|
|
417
|
+
);
|
|
418
|
+
}
|
|
383
419
|
function createPostgresSchemaSql(options = {}) {
|
|
384
420
|
return `${createPostgresSchemaStatements(options).join(";\n\n")};
|
|
385
421
|
`;
|
|
@@ -1444,6 +1480,67 @@ var PostgresMaintenance = class {
|
|
|
1444
1480
|
});
|
|
1445
1481
|
}
|
|
1446
1482
|
};
|
|
1483
|
+
|
|
1484
|
+
// src/postgres-setup.ts
|
|
1485
|
+
var PostgresSetup = class {
|
|
1486
|
+
#state;
|
|
1487
|
+
/** The conversion of `jsonb` payload columns. */
|
|
1488
|
+
#payloadUpgrade;
|
|
1489
|
+
constructor(state, options) {
|
|
1490
|
+
this.#state = state;
|
|
1491
|
+
this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
|
|
1492
|
+
}
|
|
1493
|
+
/** Implements {@link PostgresEventStore.setup}. */
|
|
1494
|
+
async run() {
|
|
1495
|
+
if (isCurrent(await this.#schemaState())) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
await this.#underSetupLock(async (transaction) => {
|
|
1499
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1500
|
+
});
|
|
1501
|
+
await this.#underSetupLock(async (transaction, schema) => {
|
|
1502
|
+
if (schema.marked) {
|
|
1503
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
for (const statement of this.#state.schemaStatements) {
|
|
1507
|
+
await this.#run(statement, transaction);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Runs `work` in a transaction under the setup lock, unless the schema became current. The
|
|
1513
|
+
* payload conversion goes first in a transaction of its own, so it holds no other table lock
|
|
1514
|
+
* while it waits for the event table (a completion locks deliveries before the event rows).
|
|
1515
|
+
*/
|
|
1516
|
+
async #underSetupLock(work) {
|
|
1517
|
+
await this.#state.executor.transaction(async (transaction) => {
|
|
1518
|
+
await this.#state.executor.query(
|
|
1519
|
+
this.#state.statements.setup.setupLock,
|
|
1520
|
+
[this.#state.setupLockKey],
|
|
1521
|
+
transaction
|
|
1522
|
+
);
|
|
1523
|
+
const schema = await this.#schemaState(transaction);
|
|
1524
|
+
if (!isCurrent(schema)) {
|
|
1525
|
+
await work(transaction, schema);
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
async #run(statement, transaction) {
|
|
1530
|
+
await this.#state.executor.query(statement, [], transaction);
|
|
1531
|
+
}
|
|
1532
|
+
async #schemaState(transaction) {
|
|
1533
|
+
const rows = await this.#state.executor.query(
|
|
1534
|
+
this.#state.statements.setup.schemaCurrent,
|
|
1535
|
+
[this.#state.schemaMarker],
|
|
1536
|
+
transaction
|
|
1537
|
+
);
|
|
1538
|
+
return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
function isCurrent(schema) {
|
|
1542
|
+
return schema.marked && schema.payloadJson;
|
|
1543
|
+
}
|
|
1447
1544
|
var WAKE_UPS = ["notify", "poll"];
|
|
1448
1545
|
var DEFAULT_PURGE_BATCH_SIZE = 1e3;
|
|
1449
1546
|
function resolveStoreSettings(options) {
|
|
@@ -1518,8 +1615,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1518
1615
|
reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
|
|
1519
1616
|
+ row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
|
|
1520
1617
|
END AS sequence
|
|
1521
|
-
FROM
|
|
1522
|
-
ordinal int, id text, type text, source text, key text, payload
|
|
1618
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1619
|
+
ordinal int, id text, type text, source text, key text, payload json, headers json,
|
|
1523
1620
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1524
1621
|
LEFT JOIN reserved ON reserved.key = incoming.key
|
|
1525
1622
|
)`;
|
|
@@ -1530,7 +1627,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1530
1627
|
INSERT INTO ${this.#names.events}
|
|
1531
1628
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1532
1629
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1533
|
-
COALESCE(incoming.payload, 'null'::
|
|
1630
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1534
1631
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1535
1632
|
COALESCE(incoming.handlers, '{}')
|
|
1536
1633
|
FROM incoming
|
|
@@ -1583,11 +1680,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1583
1680
|
return `INSERT INTO ${this.#names.events}
|
|
1584
1681
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1585
1682
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1586
|
-
COALESCE(incoming.payload, 'null'::
|
|
1683
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1587
1684
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1588
1685
|
COALESCE(incoming.handlers, '{}')
|
|
1589
|
-
FROM
|
|
1590
|
-
id text, type text, source text, key text, sequence bigint, payload
|
|
1686
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1687
|
+
id text, type text, source text, key text, sequence bigint, payload json, headers json,
|
|
1591
1688
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1592
1689
|
ORDER BY incoming.id
|
|
1593
1690
|
ON CONFLICT (id) DO NOTHING
|
|
@@ -2837,8 +2934,10 @@ var SetupStatements = class {
|
|
|
2837
2934
|
return `relaybus:${this.#names.schema}`;
|
|
2838
2935
|
}
|
|
2839
2936
|
/**
|
|
2840
|
-
*
|
|
2841
|
-
*
|
|
2937
|
+
* The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
|
|
2938
|
+
* the lane table carries `marker` as its comment (the schema statements that produce it already
|
|
2939
|
+
* ran), and `payload_json`, whether the payload and headers columns of the event table are both
|
|
2940
|
+
* `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
|
|
2842
2941
|
*/
|
|
2843
2942
|
get schemaCurrent() {
|
|
2844
2943
|
return `SELECT EXISTS (
|
|
@@ -2847,7 +2946,14 @@ var SetupStatements = class {
|
|
|
2847
2946
|
JOIN pg_catalog.pg_description AS d
|
|
2848
2947
|
ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
|
|
2849
2948
|
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
|
|
2850
|
-
) AS
|
|
2949
|
+
) AS marked, (
|
|
2950
|
+
SELECT count(*) FROM pg_catalog.pg_attribute AS a
|
|
2951
|
+
JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
|
|
2952
|
+
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
|
|
2953
|
+
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
|
|
2954
|
+
AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
|
|
2955
|
+
AND NOT a.attisdropped
|
|
2956
|
+
) = 2 AS payload_json`;
|
|
2851
2957
|
}
|
|
2852
2958
|
/**
|
|
2853
2959
|
* Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
|
|
@@ -2885,6 +2991,7 @@ var PostgresEventStore = class {
|
|
|
2885
2991
|
#claims;
|
|
2886
2992
|
#completion;
|
|
2887
2993
|
#maintenance;
|
|
2994
|
+
#setup;
|
|
2888
2995
|
#owned = new OwnedConnections();
|
|
2889
2996
|
constructor(options) {
|
|
2890
2997
|
const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
|
|
@@ -2898,7 +3005,7 @@ var PostgresEventStore = class {
|
|
|
2898
3005
|
statements,
|
|
2899
3006
|
schemaStatements,
|
|
2900
3007
|
setupLockKey: statements.setup.setupLockKey,
|
|
2901
|
-
schemaMarker:
|
|
3008
|
+
schemaMarker: version040SchemaMarker(options),
|
|
2902
3009
|
purgeBatchSize,
|
|
2903
3010
|
completionHistory: names.completionHistory,
|
|
2904
3011
|
logger,
|
|
@@ -2922,42 +3029,20 @@ var PostgresEventStore = class {
|
|
|
2922
3029
|
this.#claims = new PostgresClaims(state);
|
|
2923
3030
|
this.#completion = new PostgresCompletion(state);
|
|
2924
3031
|
this.#maintenance = new PostgresMaintenance(state);
|
|
3032
|
+
this.#setup = new PostgresSetup(state, options);
|
|
2925
3033
|
}
|
|
2926
3034
|
/**
|
|
2927
3035
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
2928
3036
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
2929
3037
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
2930
|
-
* When the lane table already carries the marker
|
|
2931
|
-
*
|
|
3038
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
3039
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
2932
3040
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
2933
3041
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
2934
3042
|
* `createPostgresSchemaSql()` instead.
|
|
2935
3043
|
*/
|
|
2936
3044
|
async setup() {
|
|
2937
|
-
|
|
2938
|
-
return;
|
|
2939
|
-
}
|
|
2940
|
-
await this.#state.executor.transaction(async (transaction) => {
|
|
2941
|
-
await this.#state.executor.query(
|
|
2942
|
-
this.#state.statements.setup.setupLock,
|
|
2943
|
-
[this.#state.setupLockKey],
|
|
2944
|
-
transaction
|
|
2945
|
-
);
|
|
2946
|
-
if (await this.#schemaIsCurrent(transaction)) {
|
|
2947
|
-
return;
|
|
2948
|
-
}
|
|
2949
|
-
for (const statement of this.#state.schemaStatements) {
|
|
2950
|
-
await this.#state.executor.query(statement, [], transaction);
|
|
2951
|
-
}
|
|
2952
|
-
});
|
|
2953
|
-
}
|
|
2954
|
-
async #schemaIsCurrent(transaction) {
|
|
2955
|
-
const rows = await this.#state.executor.query(
|
|
2956
|
-
this.#state.statements.setup.schemaCurrent,
|
|
2957
|
-
[this.#state.schemaMarker],
|
|
2958
|
-
transaction
|
|
2959
|
-
);
|
|
2960
|
-
return rows[0]["current"] === true;
|
|
3045
|
+
await this.#setup.run();
|
|
2961
3046
|
}
|
|
2962
3047
|
/**
|
|
2963
3048
|
* Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
|