@relaybus/postgres 0.4.0 → 0.5.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 +28 -15
- package/dist/chunk-5WWPAN5Q.js +26 -0
- package/dist/chunk-5WWPAN5Q.js.map +1 -0
- package/dist/chunk-FY65AUUF.js +40 -0
- package/dist/chunk-FY65AUUF.js.map +1 -0
- package/dist/chunk-UDBN4RRV.js +35 -0
- package/dist/chunk-UDBN4RRV.js.map +1 -0
- package/dist/drizzle.cjs +24 -0
- package/dist/drizzle.cjs.map +1 -1
- package/dist/drizzle.d.cts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.js +2 -1
- package/dist/drizzle.js.map +1 -1
- package/dist/{executor-CfJgktC6.d.cts → executor-Bt90w33Q.d.cts} +24 -1
- package/dist/{executor-CfJgktC6.d.ts → executor-Bt90w33Q.d.ts} +24 -1
- package/dist/index.cjs +226 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -7
- package/dist/index.d.ts +25 -7
- package/dist/index.js +194 -79
- package/dist/index.js.map +1 -1
- package/dist/knex.cjs +24 -0
- package/dist/knex.cjs.map +1 -1
- package/dist/knex.d.cts +1 -0
- package/dist/knex.d.ts +1 -0
- package/dist/knex.js +2 -1
- package/dist/knex.js.map +1 -1
- package/dist/kysely.cjs +24 -0
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +1 -0
- package/dist/kysely.d.ts +1 -0
- package/dist/kysely.js +2 -1
- package/dist/kysely.js.map +1 -1
- package/dist/prisma.cjs +24 -0
- package/dist/prisma.cjs.map +1 -1
- package/dist/prisma.d.cts +2 -1
- package/dist/prisma.d.ts +2 -1
- package/dist/prisma.js +2 -1
- package/dist/prisma.js.map +1 -1
- package/dist/typeorm.cjs +46 -0
- package/dist/typeorm.cjs.map +1 -1
- package/dist/typeorm.d.cts +1 -0
- package/dist/typeorm.d.ts +1 -0
- package/dist/typeorm.js +11 -1
- package/dist/typeorm.js.map +1 -1
- package/package.json +6 -5
- package/dist/chunk-NNZJR5AB.js +0 -12
- package/dist/chunk-NNZJR5AB.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PostgresExecutor, a as
|
|
2
|
-
import { RelayBusLogger, EventStore, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
|
|
1
|
+
import { P as PostgresExecutor, a as PostgresIsolatedTransactionOptions, b as PostgresParameter, c as PostgresQueryOptions, d as PostgresRow } from './executor-Bt90w33Q.cjs';
|
|
2
|
+
import { RelayBusLogger, EventStore, TransactionIsolationLevel, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
|
|
3
3
|
import { PoolClient, Pool, PoolConfig } from 'pg';
|
|
4
4
|
|
|
5
5
|
interface PgExecutorOptions {
|
|
@@ -43,6 +43,11 @@ declare class PgExecutor implements PostgresExecutor<PoolClient> {
|
|
|
43
43
|
* connection that already keeps this budget only gets `BEGIN`.
|
|
44
44
|
*/
|
|
45
45
|
transactionWithBudget<T>(work: (transaction: PoolClient) => Promise<T>, timeoutMs: number): Promise<T>;
|
|
46
|
+
/**
|
|
47
|
+
* Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
|
|
48
|
+
* with the `BEGIN` of the same simple query, before the budget's first statement.
|
|
49
|
+
*/
|
|
50
|
+
transactionWithIsolation<T>(work: (transaction: PoolClient) => Promise<T>, { isolationLevel, timeoutMs }: PostgresIsolatedTransactionOptions): Promise<T>;
|
|
46
51
|
query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -70,6 +75,8 @@ interface PostgresNames {
|
|
|
70
75
|
readonly schema: string;
|
|
71
76
|
readonly quotedSchema: string;
|
|
72
77
|
readonly events: string;
|
|
78
|
+
/** Unquoted, unqualified name of {@link events}, for catalog lookups. */
|
|
79
|
+
readonly eventsTableName: string;
|
|
73
80
|
readonly deliveries: string;
|
|
74
81
|
readonly keySequences: string;
|
|
75
82
|
/** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
|
|
@@ -110,8 +117,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
|
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
119
|
* 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
|
|
120
|
+
* and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
|
|
121
|
+
* Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
|
|
115
122
|
* one transaction and serialize concurrent runs (see
|
|
116
123
|
* {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
|
|
117
124
|
* into your own migration tool.
|
|
@@ -250,13 +257,21 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
250
257
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
251
258
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
252
259
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
253
|
-
* When the lane table already carries the marker
|
|
254
|
-
*
|
|
260
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
261
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
255
262
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
256
263
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
257
264
|
* `createPostgresSchemaSql()` instead.
|
|
258
265
|
*/
|
|
259
266
|
setup(): Promise<void>;
|
|
267
|
+
/** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
|
|
268
|
+
readonly isolationLevels: readonly TransactionIsolationLevel[];
|
|
269
|
+
/**
|
|
270
|
+
* True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
|
|
271
|
+
* Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
|
|
272
|
+
* test with {@link PostgresExecutor.isSerializationFailure}.
|
|
273
|
+
*/
|
|
274
|
+
isSerializationFailure(error: unknown): boolean;
|
|
260
275
|
/**
|
|
261
276
|
* Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
|
|
262
277
|
* the server holds the transaction to the budget: `statement_timeout`, and for executors with
|
|
@@ -264,6 +279,9 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
264
279
|
* `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
|
|
265
280
|
* (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
|
|
266
281
|
* created they are kept per connection and sent only when the connection does not have them yet.
|
|
282
|
+
* With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
|
|
283
|
+
* `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
|
|
284
|
+
* ISOLATION LEVEL ...` before the budget statement.
|
|
267
285
|
*/
|
|
268
286
|
transaction<T>(work: (transaction: TTransaction) => Promise<T>, options?: EventStoreTransactionOptions): Promise<T>;
|
|
269
287
|
/**
|
|
@@ -341,5 +359,5 @@ declare class ReconcileFailedError extends RelayBusError {
|
|
|
341
359
|
readonly code: string;
|
|
342
360
|
}
|
|
343
361
|
|
|
344
|
-
export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
|
|
362
|
+
export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresIsolatedTransactionOptions, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
|
|
345
363
|
export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PostgresExecutor, a as
|
|
2
|
-
import { RelayBusLogger, EventStore, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
|
|
1
|
+
import { P as PostgresExecutor, a as PostgresIsolatedTransactionOptions, b as PostgresParameter, c as PostgresQueryOptions, d as PostgresRow } from './executor-Bt90w33Q.js';
|
|
2
|
+
import { RelayBusLogger, EventStore, TransactionIsolationLevel, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
|
|
3
3
|
import { PoolClient, Pool, PoolConfig } from 'pg';
|
|
4
4
|
|
|
5
5
|
interface PgExecutorOptions {
|
|
@@ -43,6 +43,11 @@ declare class PgExecutor implements PostgresExecutor<PoolClient> {
|
|
|
43
43
|
* connection that already keeps this budget only gets `BEGIN`.
|
|
44
44
|
*/
|
|
45
45
|
transactionWithBudget<T>(work: (transaction: PoolClient) => Promise<T>, timeoutMs: number): Promise<T>;
|
|
46
|
+
/**
|
|
47
|
+
* Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
|
|
48
|
+
* with the `BEGIN` of the same simple query, before the budget's first statement.
|
|
49
|
+
*/
|
|
50
|
+
transactionWithIsolation<T>(work: (transaction: PoolClient) => Promise<T>, { isolationLevel, timeoutMs }: PostgresIsolatedTransactionOptions): Promise<T>;
|
|
46
51
|
query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -70,6 +75,8 @@ interface PostgresNames {
|
|
|
70
75
|
readonly schema: string;
|
|
71
76
|
readonly quotedSchema: string;
|
|
72
77
|
readonly events: string;
|
|
78
|
+
/** Unquoted, unqualified name of {@link events}, for catalog lookups. */
|
|
79
|
+
readonly eventsTableName: string;
|
|
73
80
|
readonly deliveries: string;
|
|
74
81
|
readonly keySequences: string;
|
|
75
82
|
/** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
|
|
@@ -110,8 +117,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
|
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
119
|
* 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
|
|
120
|
+
* and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
|
|
121
|
+
* Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
|
|
115
122
|
* one transaction and serialize concurrent runs (see
|
|
116
123
|
* {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
|
|
117
124
|
* into your own migration tool.
|
|
@@ -250,13 +257,21 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
250
257
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
251
258
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
252
259
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
253
|
-
* When the lane table already carries the marker
|
|
254
|
-
*
|
|
260
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
261
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
255
262
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
256
263
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
257
264
|
* `createPostgresSchemaSql()` instead.
|
|
258
265
|
*/
|
|
259
266
|
setup(): Promise<void>;
|
|
267
|
+
/** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
|
|
268
|
+
readonly isolationLevels: readonly TransactionIsolationLevel[];
|
|
269
|
+
/**
|
|
270
|
+
* True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
|
|
271
|
+
* Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
|
|
272
|
+
* test with {@link PostgresExecutor.isSerializationFailure}.
|
|
273
|
+
*/
|
|
274
|
+
isSerializationFailure(error: unknown): boolean;
|
|
260
275
|
/**
|
|
261
276
|
* Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
|
|
262
277
|
* the server holds the transaction to the budget: `statement_timeout`, and for executors with
|
|
@@ -264,6 +279,9 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
|
|
|
264
279
|
* `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
|
|
265
280
|
* (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
|
|
266
281
|
* created they are kept per connection and sent only when the connection does not have them yet.
|
|
282
|
+
* With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
|
|
283
|
+
* `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
|
|
284
|
+
* ISOLATION LEVEL ...` before the budget statement.
|
|
267
285
|
*/
|
|
268
286
|
transaction<T>(work: (transaction: TTransaction) => Promise<T>, options?: EventStoreTransactionOptions): Promise<T>;
|
|
269
287
|
/**
|
|
@@ -341,5 +359,5 @@ declare class ReconcileFailedError extends RelayBusError {
|
|
|
341
359
|
readonly code: string;
|
|
342
360
|
}
|
|
343
361
|
|
|
344
|
-
export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
|
|
362
|
+
export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresIsolatedTransactionOptions, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
|
|
345
363
|
export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,10 @@
|
|
|
1
|
+
import { isolationKeywords, inIsolatedTransaction } from './chunk-FY65AUUF.js';
|
|
2
|
+
import { SESSION_DEFAULTS_SQL, budgetValue, inBudgetedTransaction, budgetSettingsSql } from './chunk-UDBN4RRV.js';
|
|
1
3
|
import { createHash } from 'crypto';
|
|
2
|
-
import { ConfigurationError, RelayBusError, systemClock } from '@relaybus/core';
|
|
4
|
+
import { ConfigurationError, RelayBusError, TRANSACTION_ISOLATION_LEVELS, systemClock } from '@relaybus/core';
|
|
3
5
|
import { LaneLeaseTokenFormat, HandedOutLedger, PositiveInteger, schedule, OwnerSerializedClaims, LeaseSettlement, CompositeKey, DeliveryLeaseIdentity } from '@relaybus/core/internal';
|
|
4
6
|
import { Client, Pool } from 'pg';
|
|
5
7
|
|
|
6
|
-
// src/pg-executor.ts
|
|
7
|
-
|
|
8
|
-
// src/transaction-timeouts.ts
|
|
9
|
-
function budgetSettingsSql({ value, sessionTimeouts, scope }) {
|
|
10
|
-
const local = scope === "transaction";
|
|
11
|
-
const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;
|
|
12
|
-
if (!sessionTimeouts) {
|
|
13
|
-
return statementTimeout;
|
|
14
|
-
}
|
|
15
|
-
return `${statementTimeout}, set_config('idle_in_transaction_session_timeout', ${value}, ${local}), CASE WHEN current_setting('server_version_num')::int >= 170000 THEN set_config('transaction_timeout', ${value}, ${local}) END`;
|
|
16
|
-
}
|
|
17
|
-
var SESSION_DEFAULTS_SQL = "SELECT set_config(name, reset_val, false) FROM pg_settings WHERE name IN ('statement_timeout', 'idle_in_transaction_session_timeout', 'transaction_timeout')";
|
|
18
|
-
function budgetValue(timeoutMs) {
|
|
19
|
-
return `${Math.ceil(timeoutMs)}ms`;
|
|
20
|
-
}
|
|
21
|
-
function inBudgetedTransaction(state, {
|
|
22
|
-
work,
|
|
23
|
-
timeoutMs
|
|
24
|
-
}) {
|
|
25
|
-
const { executor } = state;
|
|
26
|
-
if (executor.transactionWithBudget !== void 0) {
|
|
27
|
-
return executor.transactionWithBudget(work, timeoutMs);
|
|
28
|
-
}
|
|
29
|
-
const sql = budgetSettingsSql({
|
|
30
|
-
value: "$1",
|
|
31
|
-
sessionTimeouts: executor.sessionTimeouts === true,
|
|
32
|
-
scope: "transaction"
|
|
33
|
-
});
|
|
34
|
-
return executor.transaction(async (transaction) => {
|
|
35
|
-
await state.query({ sql, parameters: [budgetValue(timeoutMs)], transaction });
|
|
36
|
-
return work(transaction);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// src/pg-executor.ts
|
|
41
8
|
var FOREIGN_SESSION_ERRORS = /* @__PURE__ */ new Set(["26000", "42P05"]);
|
|
42
9
|
var STATEMENT_NAME_DIGITS = 24;
|
|
43
10
|
var PgExecutor = class {
|
|
@@ -67,14 +34,28 @@ var PgExecutor = class {
|
|
|
67
34
|
* connection that already keeps this budget only gets `BEGIN`.
|
|
68
35
|
*/
|
|
69
36
|
transactionWithBudget(work, timeoutMs) {
|
|
37
|
+
return this.#transaction(work, this.#budgetStart(timeoutMs, "BEGIN"));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
|
|
41
|
+
* with the `BEGIN` of the same simple query, before the budget's first statement.
|
|
42
|
+
*/
|
|
43
|
+
transactionWithIsolation(work, { isolationLevel, timeoutMs }) {
|
|
44
|
+
return this.#transaction(
|
|
45
|
+
work,
|
|
46
|
+
this.#budgetStart(timeoutMs, `BEGIN ISOLATION LEVEL ${isolationKeywords(isolationLevel)}`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
/** How a transaction starts on a connection when it is held to `timeoutMs`; `begin` is the `BEGIN` statement. */
|
|
50
|
+
#budgetStart(timeoutMs, begin) {
|
|
70
51
|
const value = budgetValue(timeoutMs);
|
|
71
52
|
const settings = (scope) => budgetSettingsSql({ value: `'${value}'`, sessionTimeouts: true, scope });
|
|
72
|
-
return
|
|
53
|
+
return (client) => {
|
|
73
54
|
if (!this.#keepsSessionBudget()) {
|
|
74
|
-
return { begin:
|
|
55
|
+
return { begin: `${begin}; ${settings("transaction")}` };
|
|
75
56
|
}
|
|
76
|
-
return this.#sessionValues.get(client) === value ? { begin
|
|
77
|
-
}
|
|
57
|
+
return this.#sessionValues.get(client) === value ? { begin } : { begin: `${begin}; ${settings("session")}`, sessionValue: value };
|
|
58
|
+
};
|
|
78
59
|
}
|
|
79
60
|
#recordSession(client, sessionValue) {
|
|
80
61
|
if (sessionValue === null) {
|
|
@@ -228,6 +209,7 @@ function resolvePostgresNames(options = {}) {
|
|
|
228
209
|
schema,
|
|
229
210
|
quotedSchema,
|
|
230
211
|
events: table("events"),
|
|
212
|
+
eventsTableName: name("events"),
|
|
231
213
|
deliveries: table("deliveries"),
|
|
232
214
|
keySequences: table("key_sequences"),
|
|
233
215
|
lanes: table("lanes"),
|
|
@@ -254,8 +236,17 @@ function laneExpression(keyExpression, eventIdExpression) {
|
|
|
254
236
|
|
|
255
237
|
// src/schema.ts
|
|
256
238
|
var SCHEMA_MARKER_DIGITS = 32;
|
|
239
|
+
var PAYLOAD_COLUMNS = ` payload json NOT NULL,
|
|
240
|
+
headers json NOT NULL DEFAULT '{}'::json,`;
|
|
241
|
+
var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
|
|
242
|
+
headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
|
|
257
243
|
function createPostgresSchemaStatements(options = {}) {
|
|
258
244
|
const names = resolvePostgresNames(options);
|
|
245
|
+
const statements = schemaBodyStatements(names);
|
|
246
|
+
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
|
|
247
|
+
return statements;
|
|
248
|
+
}
|
|
249
|
+
function schemaBodyStatements(names) {
|
|
259
250
|
const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
|
|
260
251
|
if (names.completionHistory) {
|
|
261
252
|
statements.push(...completionHistoryStatements(names));
|
|
@@ -263,7 +254,6 @@ function createPostgresSchemaStatements(options = {}) {
|
|
|
263
254
|
if (names.schema !== "public") {
|
|
264
255
|
statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
|
|
265
256
|
}
|
|
266
|
-
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
|
|
267
257
|
return statements;
|
|
268
258
|
}
|
|
269
259
|
function eventAndDeliveryStatements(names) {
|
|
@@ -277,14 +267,14 @@ function eventAndKeySequenceStatements(names) {
|
|
|
277
267
|
source text NOT NULL,
|
|
278
268
|
key text,
|
|
279
269
|
sequence bigint,
|
|
280
|
-
|
|
281
|
-
headers jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
270
|
+
${PAYLOAD_COLUMNS}
|
|
282
271
|
correlation_id text,
|
|
283
272
|
causation_id text,
|
|
284
273
|
occurred_at timestamptz NOT NULL,
|
|
285
274
|
created_at timestamptz NOT NULL,
|
|
286
275
|
handlers text[] NOT NULL DEFAULT '{}'
|
|
287
276
|
)`,
|
|
277
|
+
upgradePayloadToJson(names),
|
|
288
278
|
`CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
|
|
289
279
|
`CREATE TABLE IF NOT EXISTS ${names.keySequences} (
|
|
290
280
|
key text PRIMARY KEY,
|
|
@@ -380,6 +370,33 @@ BEGIN
|
|
|
380
370
|
END
|
|
381
371
|
$relaybus_upgrade$`;
|
|
382
372
|
}
|
|
373
|
+
function upgradePayloadToJson(names) {
|
|
374
|
+
return `DO $relaybus_payload_json$
|
|
375
|
+
BEGIN
|
|
376
|
+
IF EXISTS (
|
|
377
|
+
SELECT 1 FROM pg_attribute
|
|
378
|
+
WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
|
|
379
|
+
AND atttypid = 'jsonb'::regtype AND NOT attisdropped
|
|
380
|
+
) THEN
|
|
381
|
+
ALTER TABLE ${names.events}
|
|
382
|
+
ALTER COLUMN headers DROP DEFAULT,
|
|
383
|
+
ALTER COLUMN payload TYPE json USING payload::json,
|
|
384
|
+
ALTER COLUMN headers TYPE json USING headers::json,
|
|
385
|
+
ALTER COLUMN headers SET DEFAULT '{}'::json;
|
|
386
|
+
END IF;
|
|
387
|
+
END
|
|
388
|
+
$relaybus_payload_json$`;
|
|
389
|
+
}
|
|
390
|
+
function version040SchemaMarker(options = {}) {
|
|
391
|
+
const names = resolvePostgresNames(options);
|
|
392
|
+
return version040Marker(names, schemaBodyStatements(names));
|
|
393
|
+
}
|
|
394
|
+
function version040Marker(names, statements) {
|
|
395
|
+
const conversion = upgradePayloadToJson(names);
|
|
396
|
+
return postgresSchemaMarker(
|
|
397
|
+
statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
|
|
398
|
+
);
|
|
399
|
+
}
|
|
383
400
|
function createPostgresSchemaSql(options = {}) {
|
|
384
401
|
return `${createPostgresSchemaStatements(options).join(";\n\n")};
|
|
385
402
|
`;
|
|
@@ -500,10 +517,38 @@ function withRootPool(executor, root) {
|
|
|
500
517
|
...executor.transactionWithBudget === void 0 ? {} : {
|
|
501
518
|
transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
|
|
502
519
|
},
|
|
520
|
+
...executor.transactionWithIsolation === void 0 ? {} : {
|
|
521
|
+
transactionWithIsolation: (work, options) => executor.transactionWithIsolation(work, options)
|
|
522
|
+
},
|
|
523
|
+
...executor.isSerializationFailure === void 0 ? {} : { isSerializationFailure: (error) => executor.isSerializationFailure(error) },
|
|
503
524
|
// eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
|
|
504
525
|
query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
|
|
505
526
|
};
|
|
506
527
|
}
|
|
528
|
+
|
|
529
|
+
// src/serialization-failure.ts
|
|
530
|
+
var SERIALIZATION_FAILURE = "40001";
|
|
531
|
+
var PRISMA_WRITE_CONFLICT = "P2034";
|
|
532
|
+
var CONFLICT_CODES = /* @__PURE__ */ new Set([
|
|
533
|
+
SERIALIZATION_FAILURE,
|
|
534
|
+
PRISMA_WRITE_CONFLICT
|
|
535
|
+
]);
|
|
536
|
+
var MAXIMUM_DEPTH = 6;
|
|
537
|
+
var WRAPPED_ERROR_KEYS = ["cause", "driverError", "originalError"];
|
|
538
|
+
function isPostgresSerializationFailure(error, depth = 0) {
|
|
539
|
+
if (typeof error !== "object" || error === null || depth > MAXIMUM_DEPTH) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const shape = error;
|
|
543
|
+
if (CONFLICT_CODES.has(shape.code) || shape.originalCode === SERIALIZATION_FAILURE) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
const meta = shape.meta;
|
|
547
|
+
if (isPostgresSerializationFailure(meta, depth + 1) || isPostgresSerializationFailure(meta?.driverAdapterError, depth + 1)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return WRAPPED_ERROR_KEYS.some((key) => isPostgresSerializationFailure(shape[key], depth + 1));
|
|
551
|
+
}
|
|
507
552
|
function text(row, column) {
|
|
508
553
|
return row[column];
|
|
509
554
|
}
|
|
@@ -1444,6 +1489,67 @@ var PostgresMaintenance = class {
|
|
|
1444
1489
|
});
|
|
1445
1490
|
}
|
|
1446
1491
|
};
|
|
1492
|
+
|
|
1493
|
+
// src/postgres-setup.ts
|
|
1494
|
+
var PostgresSetup = class {
|
|
1495
|
+
#state;
|
|
1496
|
+
/** The conversion of `jsonb` payload columns. */
|
|
1497
|
+
#payloadUpgrade;
|
|
1498
|
+
constructor(state, options) {
|
|
1499
|
+
this.#state = state;
|
|
1500
|
+
this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
|
|
1501
|
+
}
|
|
1502
|
+
/** Implements {@link PostgresEventStore.setup}. */
|
|
1503
|
+
async run() {
|
|
1504
|
+
if (isCurrent(await this.#schemaState())) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
await this.#underSetupLock(async (transaction) => {
|
|
1508
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1509
|
+
});
|
|
1510
|
+
await this.#underSetupLock(async (transaction, schema) => {
|
|
1511
|
+
if (schema.marked) {
|
|
1512
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
for (const statement of this.#state.schemaStatements) {
|
|
1516
|
+
await this.#run(statement, transaction);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Runs `work` in a transaction under the setup lock, unless the schema became current. The
|
|
1522
|
+
* payload conversion goes first in a transaction of its own, so it holds no other table lock
|
|
1523
|
+
* while it waits for the event table (a completion locks deliveries before the event rows).
|
|
1524
|
+
*/
|
|
1525
|
+
async #underSetupLock(work) {
|
|
1526
|
+
await this.#state.executor.transaction(async (transaction) => {
|
|
1527
|
+
await this.#state.executor.query(
|
|
1528
|
+
this.#state.statements.setup.setupLock,
|
|
1529
|
+
[this.#state.setupLockKey],
|
|
1530
|
+
transaction
|
|
1531
|
+
);
|
|
1532
|
+
const schema = await this.#schemaState(transaction);
|
|
1533
|
+
if (!isCurrent(schema)) {
|
|
1534
|
+
await work(transaction, schema);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
async #run(statement, transaction) {
|
|
1539
|
+
await this.#state.executor.query(statement, [], transaction);
|
|
1540
|
+
}
|
|
1541
|
+
async #schemaState(transaction) {
|
|
1542
|
+
const rows = await this.#state.executor.query(
|
|
1543
|
+
this.#state.statements.setup.schemaCurrent,
|
|
1544
|
+
[this.#state.schemaMarker],
|
|
1545
|
+
transaction
|
|
1546
|
+
);
|
|
1547
|
+
return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
function isCurrent(schema) {
|
|
1551
|
+
return schema.marked && schema.payloadJson;
|
|
1552
|
+
}
|
|
1447
1553
|
var WAKE_UPS = ["notify", "poll"];
|
|
1448
1554
|
var DEFAULT_PURGE_BATCH_SIZE = 1e3;
|
|
1449
1555
|
function resolveStoreSettings(options) {
|
|
@@ -1518,8 +1624,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1518
1624
|
reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
|
|
1519
1625
|
+ row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
|
|
1520
1626
|
END AS sequence
|
|
1521
|
-
FROM
|
|
1522
|
-
ordinal int, id text, type text, source text, key text, payload
|
|
1627
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1628
|
+
ordinal int, id text, type text, source text, key text, payload json, headers json,
|
|
1523
1629
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1524
1630
|
LEFT JOIN reserved ON reserved.key = incoming.key
|
|
1525
1631
|
)`;
|
|
@@ -1530,7 +1636,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1530
1636
|
INSERT INTO ${this.#names.events}
|
|
1531
1637
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1532
1638
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1533
|
-
COALESCE(incoming.payload, 'null'::
|
|
1639
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1534
1640
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1535
1641
|
COALESCE(incoming.handlers, '{}')
|
|
1536
1642
|
FROM incoming
|
|
@@ -1583,11 +1689,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1583
1689
|
return `INSERT INTO ${this.#names.events}
|
|
1584
1690
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1585
1691
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1586
|
-
COALESCE(incoming.payload, 'null'::
|
|
1692
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1587
1693
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1588
1694
|
COALESCE(incoming.handlers, '{}')
|
|
1589
|
-
FROM
|
|
1590
|
-
id text, type text, source text, key text, sequence bigint, payload
|
|
1695
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1696
|
+
id text, type text, source text, key text, sequence bigint, payload json, headers json,
|
|
1591
1697
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1592
1698
|
ORDER BY incoming.id
|
|
1593
1699
|
ON CONFLICT (id) DO NOTHING
|
|
@@ -2837,8 +2943,10 @@ var SetupStatements = class {
|
|
|
2837
2943
|
return `relaybus:${this.#names.schema}`;
|
|
2838
2944
|
}
|
|
2839
2945
|
/**
|
|
2840
|
-
*
|
|
2841
|
-
*
|
|
2946
|
+
* The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
|
|
2947
|
+
* the lane table carries `marker` as its comment (the schema statements that produce it already
|
|
2948
|
+
* ran), and `payload_json`, whether the payload and headers columns of the event table are both
|
|
2949
|
+
* `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
|
|
2842
2950
|
*/
|
|
2843
2951
|
get schemaCurrent() {
|
|
2844
2952
|
return `SELECT EXISTS (
|
|
@@ -2847,7 +2955,14 @@ var SetupStatements = class {
|
|
|
2847
2955
|
JOIN pg_catalog.pg_description AS d
|
|
2848
2956
|
ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
|
|
2849
2957
|
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
|
|
2850
|
-
) AS
|
|
2958
|
+
) AS marked, (
|
|
2959
|
+
SELECT count(*) FROM pg_catalog.pg_attribute AS a
|
|
2960
|
+
JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
|
|
2961
|
+
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
|
|
2962
|
+
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
|
|
2963
|
+
AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
|
|
2964
|
+
AND NOT a.attisdropped
|
|
2965
|
+
) = 2 AS payload_json`;
|
|
2851
2966
|
}
|
|
2852
2967
|
/**
|
|
2853
2968
|
* Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
|
|
@@ -2885,6 +3000,7 @@ var PostgresEventStore = class {
|
|
|
2885
3000
|
#claims;
|
|
2886
3001
|
#completion;
|
|
2887
3002
|
#maintenance;
|
|
3003
|
+
#setup;
|
|
2888
3004
|
#owned = new OwnedConnections();
|
|
2889
3005
|
constructor(options) {
|
|
2890
3006
|
const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
|
|
@@ -2898,7 +3014,7 @@ var PostgresEventStore = class {
|
|
|
2898
3014
|
statements,
|
|
2899
3015
|
schemaStatements,
|
|
2900
3016
|
setupLockKey: statements.setup.setupLockKey,
|
|
2901
|
-
schemaMarker:
|
|
3017
|
+
schemaMarker: version040SchemaMarker(options),
|
|
2902
3018
|
purgeBatchSize,
|
|
2903
3019
|
completionHistory: names.completionHistory,
|
|
2904
3020
|
logger,
|
|
@@ -2922,42 +3038,30 @@ var PostgresEventStore = class {
|
|
|
2922
3038
|
this.#claims = new PostgresClaims(state);
|
|
2923
3039
|
this.#completion = new PostgresCompletion(state);
|
|
2924
3040
|
this.#maintenance = new PostgresMaintenance(state);
|
|
3041
|
+
this.#setup = new PostgresSetup(state, options);
|
|
2925
3042
|
}
|
|
2926
3043
|
/**
|
|
2927
3044
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
2928
3045
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
2929
3046
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
2930
|
-
* When the lane table already carries the marker
|
|
2931
|
-
*
|
|
3047
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
3048
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
2932
3049
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
2933
3050
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
2934
3051
|
* `createPostgresSchemaSql()` instead.
|
|
2935
3052
|
*/
|
|
2936
3053
|
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
|
-
});
|
|
3054
|
+
await this.#setup.run();
|
|
2953
3055
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3056
|
+
/** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
|
|
3057
|
+
isolationLevels = TRANSACTION_ISOLATION_LEVELS;
|
|
3058
|
+
/**
|
|
3059
|
+
* True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
|
|
3060
|
+
* Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
|
|
3061
|
+
* test with {@link PostgresExecutor.isSerializationFailure}.
|
|
3062
|
+
*/
|
|
3063
|
+
isSerializationFailure(error) {
|
|
3064
|
+
return isPostgresSerializationFailure(error) || this.#state.executor.isSerializationFailure?.(error) === true;
|
|
2961
3065
|
}
|
|
2962
3066
|
/**
|
|
2963
3067
|
* Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
|
|
@@ -2966,6 +3070,9 @@ var PostgresEventStore = class {
|
|
|
2966
3070
|
* `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
|
|
2967
3071
|
* (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
|
|
2968
3072
|
* created they are kept per connection and sent only when the connection does not have them yet.
|
|
3073
|
+
* With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
|
|
3074
|
+
* `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
|
|
3075
|
+
* ISOLATION LEVEL ...` before the budget statement.
|
|
2969
3076
|
*/
|
|
2970
3077
|
async transaction(work, options) {
|
|
2971
3078
|
const own = { wake: false };
|
|
@@ -2977,12 +3084,20 @@ var PostgresEventStore = class {
|
|
|
2977
3084
|
this.#state.ownTransactions.delete(transaction);
|
|
2978
3085
|
}
|
|
2979
3086
|
};
|
|
2980
|
-
const result = await
|
|
3087
|
+
const result = await this.#open(tracked, options);
|
|
2981
3088
|
if (own.wake) {
|
|
2982
3089
|
this.#appender.wakeAfterCommit();
|
|
2983
3090
|
}
|
|
2984
3091
|
return result;
|
|
2985
3092
|
}
|
|
3093
|
+
/** Opens the transaction `work` runs in: plain, held to the budget, or at an isolation level too. */
|
|
3094
|
+
#open(work, options) {
|
|
3095
|
+
if (options === void 0) {
|
|
3096
|
+
return this.#state.executor.transaction(work);
|
|
3097
|
+
}
|
|
3098
|
+
const { timeoutMs, isolationLevel } = options;
|
|
3099
|
+
return isolationLevel === void 0 ? inBudgetedTransaction(this.#state, { work, timeoutMs }) : inIsolatedTransaction(this.#state, { work, timeoutMs, isolationLevel });
|
|
3100
|
+
}
|
|
2986
3101
|
/**
|
|
2987
3102
|
* Writes the entries in `transaction`, normally with one statement
|
|
2988
3103
|
* (`AppendStatements.insertFreshEvents`): keyed events reserve their sequence numbers (locking
|