@relaybus/postgres 0.3.1 → 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 +46 -24
- package/dist/drizzle.d.cts +3 -2
- package/dist/drizzle.d.ts +3 -2
- package/dist/index.cjs +155 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +155 -44
- package/dist/index.js.map +1 -1
- package/dist/knex.d.cts +3 -2
- package/dist/knex.d.ts +3 -2
- package/dist/kysely.d.cts +3 -2
- package/dist/kysely.d.ts +3 -2
- package/dist/prisma.cjs.map +1 -1
- package/dist/prisma.d.cts +4 -4
- package/dist/prisma.d.ts +4 -4
- package/dist/prisma.js.map +1 -1
- package/dist/typeorm.d.cts +3 -2
- package/dist/typeorm.d.ts +3 -2
- package/package.json +20 -12
package/README.md
CHANGED
|
@@ -102,15 +102,30 @@ Where the budget lives depends on who owns the pool:
|
|
|
102
102
|
- **A pool or executor you pass in**: the budget is transaction-scoped (`SET LOCAL` semantics), set with `BEGIN` by
|
|
103
103
|
`PgExecutor` and by its own statement for the other adapters. Session settings of your pool are never changed.
|
|
104
104
|
|
|
105
|
-
**Prisma
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
**Prisma** 5, 6 and 7 are supported (optional peer `@prisma/client` `^5.0.0 || ^6.0.0 || ^7.0.0`). It is
|
|
106
|
+
integration-tested with a generated Prisma Client 7.10 (and 6.19 and 5.22 in CI) on the `@prisma/adapter-pg`
|
|
107
|
+
driver adapter: the full event store contract, publishing in `prisma.$transaction`, rollback and concurrent
|
|
108
|
+
transactions. The adapter is typed structurally, so the package has no Prisma dependency. Prisma interactive
|
|
109
|
+
transactions default to a 5 s timeout, so pass `createPrismaExecutor(prisma, { timeout, maxWait })` if handlers run
|
|
110
|
+
longer.
|
|
111
|
+
|
|
112
|
+
**Give an ORM store the `pg` pool too.** ORMs send raw statements unprepared and offer no connection to `LISTEN` on.
|
|
113
|
+
Pass the `pg` pool your ORM's driver uses as `pool` next to `executor`: the statements the store sends outside your
|
|
114
|
+
transactions (the dispatcher's claims, completions and renewals) then run on it as prepared statements, and the
|
|
115
|
+
store listens for wake-ups, so an event published in your ORM transaction is dispatched right after that
|
|
116
|
+
transaction commits. Your transactions still go through the ORM, and the store never closes the pool.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const pool = new Pool({ connectionString });
|
|
120
|
+
const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
|
|
121
|
+
const store = new PostgresEventStore({ executor: createPrismaExecutor(prisma), pool });
|
|
122
|
+
```
|
|
108
123
|
|
|
109
124
|
## Options
|
|
110
125
|
|
|
111
126
|
| Option | Default | Meaning |
|
|
112
127
|
| ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
113
|
-
| `pool` / `connectionString` / `executor` | required, exactly one | How to reach the database.
|
|
128
|
+
| `pool` / `connectionString` / `executor` | required, exactly one | How to reach the database. With `executor`, `pool` may be given too (see above). |
|
|
114
129
|
| `poolConfig` | none | Extra `pg` pool settings when `connectionString` is used. |
|
|
115
130
|
| `schema` | `'public'` | Schema of the tables. Created by `setup()` when it is not `public`. |
|
|
116
131
|
| `tablePrefix` | `'relaybus_'` | Tables are `<prefix>events`, `<prefix>deliveries`, `<prefix>key_sequences`, `<prefix>lanes`. |
|
|
@@ -141,14 +156,14 @@ import { createPostgresSchemaSql, createPostgresSchemaStatements } from '@relayb
|
|
|
141
156
|
const migration = createPostgresSchemaSql({ schema: 'app', tablePrefix: 'bus_' });
|
|
142
157
|
```
|
|
143
158
|
|
|
144
|
-
| Table | Purpose
|
|
145
|
-
| ---------------------- |
|
|
146
|
-
| `events` | One row per event: `id` (**text**, so any caller id works), `type`, `source`, `key`, `sequence`, `payload
|
|
147
|
-
| `deliveries` | One row per (event, handler) that is not finished: `lane`, `status` (`pending` or `dead`), `attempts`, `available_at`, `last_error jsonb`, key and sequence copied from the event, timestamps. `unique (event_id, handler)`, foreign key to events with `ON DELETE CASCADE`.
|
|
148
|
-
| `lanes` | 256 rows, one per lane: `epoch` (the fencing token), `lease_owner`, `lease_expires_at` and `in_flight` (ids of the deliveries the owner claimed). `fillfactor = 50` and no secondary index, so its updates are heap-only (HOT).
|
|
149
|
-
| `event_completions` | One row per event that had a delivery complete, with the time of its last completion. Retention of completed work (`retention.completed`) is measured from it, however old the event is; events that never had a delivery complete are measured from their creation.
|
|
150
|
-
| `key_sequences` | One counter row per ordering key.
|
|
151
|
-
| `completed_deliveries` | Only with `completionHistory`: one insert-only row per completed delivery.
|
|
159
|
+
| Table | Purpose |
|
|
160
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `events` | One row per event: `id` (**text**, so any caller id works), `type`, `source`, `key`, `sequence`, `payload json`, `headers json`, correlation and causation ids, `occurred_at`, `created_at`, and `handlers`, the handlers that ever got a delivery (it outlives the deliveries). |
|
|
162
|
+
| `deliveries` | One row per (event, handler) that is not finished: `lane`, `status` (`pending` or `dead`), `attempts`, `available_at`, `last_error jsonb`, key and sequence copied from the event, timestamps. `unique (event_id, handler)`, foreign key to events with `ON DELETE CASCADE`. |
|
|
163
|
+
| `lanes` | 256 rows, one per lane: `epoch` (the fencing token), `lease_owner`, `lease_expires_at` and `in_flight` (ids of the deliveries the owner claimed). `fillfactor = 50` and no secondary index, so its updates are heap-only (HOT). |
|
|
164
|
+
| `event_completions` | One row per event that had a delivery complete, with the time of its last completion. Retention of completed work (`retention.completed`) is measured from it, however old the event is; events that never had a delivery complete are measured from their creation. |
|
|
165
|
+
| `key_sequences` | One counter row per ordering key. |
|
|
166
|
+
| `completed_deliveries` | Only with `completionHistory`: one insert-only row per completed delivery. |
|
|
152
167
|
|
|
153
168
|
A delivery row is inserted once and deleted once when it completes. Claiming, renewing a lease and completing never
|
|
154
169
|
update it: they update the lane row (claim, renewal) or delete the delivery (completion), so the indexed columns of a
|
|
@@ -243,10 +258,12 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
|
|
|
243
258
|
- **Key counters.** `key_sequences` keeps one row per key ever published and rows are never deleted; the table grows
|
|
244
259
|
with the number of distinct keys (a bigint counter each). Pick keys with bounded cardinality, or prune counters of keys
|
|
245
260
|
you know are retired once nothing for them is pending.
|
|
246
|
-
- **Stored JSON.** Payloads and headers are stored as `
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
- **Stored JSON.** Payloads and headers are stored as `json`, the text the producer serialized: an event read back
|
|
262
|
+
serializes byte for byte as it was published, key order included. (Before 0.4.1 they were `jsonb`, which re-orders
|
|
263
|
+
object keys; `setup()` converts the columns, and rows stored before the upgrade keep the order `jsonb` gave them. See
|
|
264
|
+
[Upgrading](https://berkcansavur.github.io/relaybus/upgrading).) A string containing `\u0000` (NUL) or a lone UTF-16
|
|
265
|
+
surrogate is rejected by PostgreSQL and fails the publish with a database error; strip or encode such text before
|
|
266
|
+
publishing.
|
|
250
267
|
- **Sequence gaps.** Republishing an existing event id consumes a sequence number. Gaps are normal; only order matters.
|
|
251
268
|
- **Vacuum.** Completing a delivery deletes its row, which leaves a dead tuple until autovacuum runs. The tables are
|
|
252
269
|
created with `autovacuum_vacuum_scale_factor = 0.02`; for sustained throughput above a few thousand deliveries per
|
|
@@ -274,11 +291,16 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
|
|
|
274
291
|
that channel (`<schema>.<prefix>deliveries`, hashed when longer than 63 characters) on one connection of its own,
|
|
275
292
|
opened with the pool's settings outside the pool, so an event published by any process runs within milliseconds
|
|
276
293
|
instead of at the next poll. A lost listening connection is replaced after a second; polling remains the safety
|
|
277
|
-
net, so a missed notification only costs latency.
|
|
278
|
-
|
|
294
|
+
net, so a missed notification only costs latency. The notification of a publish in your own transaction is sent
|
|
295
|
+
by PostgreSQL at its commit, however late that is, so the handler starts within milliseconds of the commit
|
|
296
|
+
(`wake-after-commit.integration.test.ts`). A store built on an `executor` listens only when it is given `pool`
|
|
297
|
+
as well; without it, it relies on polling and on the wake-ups of its own process after a publish (see
|
|
298
|
+
`bus.wake()`). `wakeUp: 'poll'` sends and listens for nothing.
|
|
279
299
|
- **Prepared statements.** With `preparedStatements: true` (the default) the publish, claim, completion and renewal
|
|
280
300
|
statements are named prepared statements: each connection parses and plans them once. Only `PgExecutor` (the
|
|
281
|
-
default `pool` / `connectionString` path
|
|
301
|
+
default `pool` / `connectionString` path, and the `pool` given next to an `executor`) prepares; the library
|
|
302
|
+
adapters send the statements of your transactions unprepared (a dispatch batch on an ORM store parses 2 statements
|
|
303
|
+
without `pool`, 0 with it: `perf-design-orm-executors.integration.test.ts`). See
|
|
282
304
|
`docs/performance/publish-path.md` for what that saves.
|
|
283
305
|
- **Pooling.** Claim, complete, extend and purge need a free connection besides your transactions; size the pool for
|
|
284
306
|
`dispatcher.concurrency` plus publishers; the listening connection comes on top of the pool's `max`. Listening and
|
|
@@ -296,8 +318,8 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
|
|
|
296
318
|
`createPostgresSchemaSql()`.
|
|
297
319
|
- `permission denied for schema`: the role needs `CREATE` on the schema for `setup()`; with `setupStore: false` it needs
|
|
298
320
|
only DML rights.
|
|
299
|
-
- `invalid input syntax for type json` /
|
|
300
|
-
|
|
321
|
+
- `invalid input syntax for type json` / `unsupported Unicode escape sequence`: the payload holds a NUL character or a
|
|
322
|
+
lone UTF-16 surrogate, which PostgreSQL rejects.
|
|
301
323
|
- Foreign key error on append: the only known cause is republishing an event id in the same instant its last delivery
|
|
302
324
|
is purged. The publishing transaction rolls back and can simply be retried.
|
|
303
325
|
- A key looks stuck: a pending or processing head blocks it. Inspect `listDeliveries({ status: 'processing' })` and
|
|
@@ -310,12 +332,12 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
|
|
|
310
332
|
## Testing
|
|
311
333
|
|
|
312
334
|
Integration tests use Testcontainers against `postgres:17-alpine` and `postgres:13-alpine` (override with
|
|
313
|
-
`RELAYBUS_POSTGRES_IMAGES`). Every adapter
|
|
314
|
-
transaction tests against the real database.
|
|
335
|
+
`RELAYBUS_POSTGRES_IMAGES`). Every adapter (Kysely, Knex, TypeORM, Drizzle, Prisma with and without `pool`) runs the
|
|
336
|
+
full `@relaybus/testing` event store contract plus transaction tests against the real database.
|
|
315
337
|
|
|
316
338
|
## When to use @relaybus/postgres, and when not to
|
|
317
339
|
|
|
318
|
-
Use it when your data lives in PostgreSQL 13 or newer and events must commit or roll back with it. It is not for MySQL or MongoDB (see `@relaybus/mongodb` for MongoDB)
|
|
340
|
+
Use it when your data lives in PostgreSQL 13 or newer and events must commit or roll back with it. It is not for MySQL or MongoDB (see `@relaybus/mongodb` for MongoDB).
|
|
319
341
|
|
|
320
342
|
Documentation: <https://berkcansavur.github.io/relaybus/packages/postgres>
|
|
321
343
|
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQL } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
2
|
+
import { PostgresExecutor } from './index.cjs';
|
|
3
3
|
|
|
4
4
|
/** What a Drizzle database and a Drizzle transaction have in common. */
|
|
5
5
|
interface DrizzleQueryable {
|
|
@@ -17,4 +17,5 @@ interface DrizzleLike extends DrizzleQueryable {
|
|
|
17
17
|
*/
|
|
18
18
|
declare function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable>;
|
|
19
19
|
|
|
20
|
-
export {
|
|
20
|
+
export { createDrizzleExecutor };
|
|
21
|
+
export type { DrizzleLike, DrizzleQueryable };
|
package/dist/drizzle.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQL } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
2
|
+
import { PostgresExecutor } from './index.js';
|
|
3
3
|
|
|
4
4
|
/** What a Drizzle database and a Drizzle transaction have in common. */
|
|
5
5
|
interface DrizzleQueryable {
|
|
@@ -17,4 +17,5 @@ interface DrizzleLike extends DrizzleQueryable {
|
|
|
17
17
|
*/
|
|
18
18
|
declare function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable>;
|
|
19
19
|
|
|
20
|
-
export {
|
|
20
|
+
export { createDrizzleExecutor };
|
|
21
|
+
export type { DrizzleLike, DrizzleQueryable };
|
package/dist/index.cjs
CHANGED
|
@@ -230,6 +230,7 @@ function resolvePostgresNames(options = {}) {
|
|
|
230
230
|
schema,
|
|
231
231
|
quotedSchema,
|
|
232
232
|
events: table("events"),
|
|
233
|
+
eventsTableName: name("events"),
|
|
233
234
|
deliveries: table("deliveries"),
|
|
234
235
|
keySequences: table("key_sequences"),
|
|
235
236
|
lanes: table("lanes"),
|
|
@@ -256,8 +257,17 @@ function laneExpression(keyExpression, eventIdExpression) {
|
|
|
256
257
|
|
|
257
258
|
// src/schema.ts
|
|
258
259
|
var SCHEMA_MARKER_DIGITS = 32;
|
|
260
|
+
var PAYLOAD_COLUMNS = ` payload json NOT NULL,
|
|
261
|
+
headers json NOT NULL DEFAULT '{}'::json,`;
|
|
262
|
+
var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
|
|
263
|
+
headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
|
|
259
264
|
function createPostgresSchemaStatements(options = {}) {
|
|
260
265
|
const names = resolvePostgresNames(options);
|
|
266
|
+
const statements = schemaBodyStatements(names);
|
|
267
|
+
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
|
|
268
|
+
return statements;
|
|
269
|
+
}
|
|
270
|
+
function schemaBodyStatements(names) {
|
|
261
271
|
const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
|
|
262
272
|
if (names.completionHistory) {
|
|
263
273
|
statements.push(...completionHistoryStatements(names));
|
|
@@ -265,7 +275,6 @@ function createPostgresSchemaStatements(options = {}) {
|
|
|
265
275
|
if (names.schema !== "public") {
|
|
266
276
|
statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
|
|
267
277
|
}
|
|
268
|
-
statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
|
|
269
278
|
return statements;
|
|
270
279
|
}
|
|
271
280
|
function eventAndDeliveryStatements(names) {
|
|
@@ -279,14 +288,14 @@ function eventAndKeySequenceStatements(names) {
|
|
|
279
288
|
source text NOT NULL,
|
|
280
289
|
key text,
|
|
281
290
|
sequence bigint,
|
|
282
|
-
|
|
283
|
-
headers jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
291
|
+
${PAYLOAD_COLUMNS}
|
|
284
292
|
correlation_id text,
|
|
285
293
|
causation_id text,
|
|
286
294
|
occurred_at timestamptz NOT NULL,
|
|
287
295
|
created_at timestamptz NOT NULL,
|
|
288
296
|
handlers text[] NOT NULL DEFAULT '{}'
|
|
289
297
|
)`,
|
|
298
|
+
upgradePayloadToJson(names),
|
|
290
299
|
`CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
|
|
291
300
|
`CREATE TABLE IF NOT EXISTS ${names.keySequences} (
|
|
292
301
|
key text PRIMARY KEY,
|
|
@@ -382,6 +391,33 @@ BEGIN
|
|
|
382
391
|
END
|
|
383
392
|
$relaybus_upgrade$`;
|
|
384
393
|
}
|
|
394
|
+
function upgradePayloadToJson(names) {
|
|
395
|
+
return `DO $relaybus_payload_json$
|
|
396
|
+
BEGIN
|
|
397
|
+
IF EXISTS (
|
|
398
|
+
SELECT 1 FROM pg_attribute
|
|
399
|
+
WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
|
|
400
|
+
AND atttypid = 'jsonb'::regtype AND NOT attisdropped
|
|
401
|
+
) THEN
|
|
402
|
+
ALTER TABLE ${names.events}
|
|
403
|
+
ALTER COLUMN headers DROP DEFAULT,
|
|
404
|
+
ALTER COLUMN payload TYPE json USING payload::json,
|
|
405
|
+
ALTER COLUMN headers TYPE json USING headers::json,
|
|
406
|
+
ALTER COLUMN headers SET DEFAULT '{}'::json;
|
|
407
|
+
END IF;
|
|
408
|
+
END
|
|
409
|
+
$relaybus_payload_json$`;
|
|
410
|
+
}
|
|
411
|
+
function version040SchemaMarker(options = {}) {
|
|
412
|
+
const names = resolvePostgresNames(options);
|
|
413
|
+
return version040Marker(names, schemaBodyStatements(names));
|
|
414
|
+
}
|
|
415
|
+
function version040Marker(names, statements) {
|
|
416
|
+
const conversion = upgradePayloadToJson(names);
|
|
417
|
+
return postgresSchemaMarker(
|
|
418
|
+
statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
|
|
419
|
+
);
|
|
420
|
+
}
|
|
385
421
|
function createPostgresSchemaSql(options = {}) {
|
|
386
422
|
return `${createPostgresSchemaStatements(options).join(";\n\n")};
|
|
387
423
|
`;
|
|
@@ -493,6 +529,19 @@ var NotificationListener = class {
|
|
|
493
529
|
await drop?.();
|
|
494
530
|
}
|
|
495
531
|
};
|
|
532
|
+
|
|
533
|
+
// src/root-pool-executor.ts
|
|
534
|
+
function withRootPool(executor, root) {
|
|
535
|
+
return {
|
|
536
|
+
...executor.sessionTimeouts === void 0 ? {} : { sessionTimeouts: executor.sessionTimeouts },
|
|
537
|
+
transaction: (work) => executor.transaction(work),
|
|
538
|
+
...executor.transactionWithBudget === void 0 ? {} : {
|
|
539
|
+
transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
|
|
540
|
+
},
|
|
541
|
+
// eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
|
|
542
|
+
query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
496
545
|
function text(row, column) {
|
|
497
546
|
return row[column];
|
|
498
547
|
}
|
|
@@ -1433,10 +1482,71 @@ var PostgresMaintenance = class {
|
|
|
1433
1482
|
});
|
|
1434
1483
|
}
|
|
1435
1484
|
};
|
|
1485
|
+
|
|
1486
|
+
// src/postgres-setup.ts
|
|
1487
|
+
var PostgresSetup = class {
|
|
1488
|
+
#state;
|
|
1489
|
+
/** The conversion of `jsonb` payload columns. */
|
|
1490
|
+
#payloadUpgrade;
|
|
1491
|
+
constructor(state, options) {
|
|
1492
|
+
this.#state = state;
|
|
1493
|
+
this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
|
|
1494
|
+
}
|
|
1495
|
+
/** Implements {@link PostgresEventStore.setup}. */
|
|
1496
|
+
async run() {
|
|
1497
|
+
if (isCurrent(await this.#schemaState())) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
await this.#underSetupLock(async (transaction) => {
|
|
1501
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1502
|
+
});
|
|
1503
|
+
await this.#underSetupLock(async (transaction, schema) => {
|
|
1504
|
+
if (schema.marked) {
|
|
1505
|
+
await this.#run(this.#payloadUpgrade, transaction);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
for (const statement of this.#state.schemaStatements) {
|
|
1509
|
+
await this.#run(statement, transaction);
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Runs `work` in a transaction under the setup lock, unless the schema became current. The
|
|
1515
|
+
* payload conversion goes first in a transaction of its own, so it holds no other table lock
|
|
1516
|
+
* while it waits for the event table (a completion locks deliveries before the event rows).
|
|
1517
|
+
*/
|
|
1518
|
+
async #underSetupLock(work) {
|
|
1519
|
+
await this.#state.executor.transaction(async (transaction) => {
|
|
1520
|
+
await this.#state.executor.query(
|
|
1521
|
+
this.#state.statements.setup.setupLock,
|
|
1522
|
+
[this.#state.setupLockKey],
|
|
1523
|
+
transaction
|
|
1524
|
+
);
|
|
1525
|
+
const schema = await this.#schemaState(transaction);
|
|
1526
|
+
if (!isCurrent(schema)) {
|
|
1527
|
+
await work(transaction, schema);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
async #run(statement, transaction) {
|
|
1532
|
+
await this.#state.executor.query(statement, [], transaction);
|
|
1533
|
+
}
|
|
1534
|
+
async #schemaState(transaction) {
|
|
1535
|
+
const rows = await this.#state.executor.query(
|
|
1536
|
+
this.#state.statements.setup.schemaCurrent,
|
|
1537
|
+
[this.#state.schemaMarker],
|
|
1538
|
+
transaction
|
|
1539
|
+
);
|
|
1540
|
+
return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
function isCurrent(schema) {
|
|
1544
|
+
return schema.marked && schema.payloadJson;
|
|
1545
|
+
}
|
|
1436
1546
|
var WAKE_UPS = ["notify", "poll"];
|
|
1437
1547
|
var DEFAULT_PURGE_BATCH_SIZE = 1e3;
|
|
1438
1548
|
function resolveStoreSettings(options) {
|
|
1439
|
-
const sources = [options.
|
|
1549
|
+
const sources = [options.executor ?? options.pool, options.connectionString].filter(
|
|
1440
1550
|
(source) => source !== void 0
|
|
1441
1551
|
);
|
|
1442
1552
|
if (sources.length !== 1) {
|
|
@@ -1507,8 +1617,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1507
1617
|
reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
|
|
1508
1618
|
+ row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
|
|
1509
1619
|
END AS sequence
|
|
1510
|
-
FROM
|
|
1511
|
-
ordinal int, id text, type text, source text, key text, payload
|
|
1620
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1621
|
+
ordinal int, id text, type text, source text, key text, payload json, headers json,
|
|
1512
1622
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1513
1623
|
LEFT JOIN reserved ON reserved.key = incoming.key
|
|
1514
1624
|
)`;
|
|
@@ -1519,7 +1629,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1519
1629
|
INSERT INTO ${this.#names.events}
|
|
1520
1630
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1521
1631
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1522
|
-
COALESCE(incoming.payload, 'null'::
|
|
1632
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1523
1633
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1524
1634
|
COALESCE(incoming.handlers, '{}')
|
|
1525
1635
|
FROM incoming
|
|
@@ -1542,9 +1652,13 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1542
1652
|
RETURNING available_at
|
|
1543
1653
|
)`;
|
|
1544
1654
|
}
|
|
1545
|
-
/**
|
|
1655
|
+
/**
|
|
1656
|
+
* One wake-up notification on {@link PostgresNames.notificationChannel}, outside any publishing
|
|
1657
|
+
* transaction. Returns a boolean column rather than the `void` of `pg_notify`, which some libraries
|
|
1658
|
+
* cannot read (Prisma's raw queries reject a `void` column).
|
|
1659
|
+
*/
|
|
1546
1660
|
get notify() {
|
|
1547
|
-
return `SELECT pg_notify('${this.#names.notificationChannel}', '')`;
|
|
1661
|
+
return `SELECT true AS notified FROM pg_notify('${this.#names.notificationChannel}', '')`;
|
|
1548
1662
|
}
|
|
1549
1663
|
/**
|
|
1550
1664
|
* A CTE `woken` that sends one notification on {@link PostgresNames.notificationChannel} when a row of
|
|
@@ -1568,11 +1682,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
|
|
|
1568
1682
|
return `INSERT INTO ${this.#names.events}
|
|
1569
1683
|
(id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
|
|
1570
1684
|
SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
|
|
1571
|
-
COALESCE(incoming.payload, 'null'::
|
|
1685
|
+
COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
|
|
1572
1686
|
incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
|
|
1573
1687
|
COALESCE(incoming.handlers, '{}')
|
|
1574
|
-
FROM
|
|
1575
|
-
id text, type text, source text, key text, sequence bigint, payload
|
|
1688
|
+
FROM json_to_recordset($1::json) AS incoming (
|
|
1689
|
+
id text, type text, source text, key text, sequence bigint, payload json, headers json,
|
|
1576
1690
|
correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
|
|
1577
1691
|
ORDER BY incoming.id
|
|
1578
1692
|
ON CONFLICT (id) DO NOTHING
|
|
@@ -2822,8 +2936,10 @@ var SetupStatements = class {
|
|
|
2822
2936
|
return `relaybus:${this.#names.schema}`;
|
|
2823
2937
|
}
|
|
2824
2938
|
/**
|
|
2825
|
-
*
|
|
2826
|
-
*
|
|
2939
|
+
* The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
|
|
2940
|
+
* the lane table carries `marker` as its comment (the schema statements that produce it already
|
|
2941
|
+
* ran), and `payload_json`, whether the payload and headers columns of the event table are both
|
|
2942
|
+
* `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
|
|
2827
2943
|
*/
|
|
2828
2944
|
get schemaCurrent() {
|
|
2829
2945
|
return `SELECT EXISTS (
|
|
@@ -2832,10 +2948,21 @@ var SetupStatements = class {
|
|
|
2832
2948
|
JOIN pg_catalog.pg_description AS d
|
|
2833
2949
|
ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
|
|
2834
2950
|
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
|
|
2835
|
-
) AS
|
|
2951
|
+
) AS marked, (
|
|
2952
|
+
SELECT count(*) FROM pg_catalog.pg_attribute AS a
|
|
2953
|
+
JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
|
|
2954
|
+
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
|
|
2955
|
+
WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
|
|
2956
|
+
AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
|
|
2957
|
+
AND NOT a.attisdropped
|
|
2958
|
+
) = 2 AS payload_json`;
|
|
2836
2959
|
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
|
|
2962
|
+
* some libraries cannot read (Prisma's raw queries reject a `void` column).
|
|
2963
|
+
*/
|
|
2837
2964
|
get setupLock() {
|
|
2838
|
-
return "SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))";
|
|
2965
|
+
return "SELECT true AS locked FROM pg_advisory_xact_lock(hashtextextended($1::text, 0))";
|
|
2839
2966
|
}
|
|
2840
2967
|
};
|
|
2841
2968
|
|
|
@@ -2866,6 +2993,7 @@ var PostgresEventStore = class {
|
|
|
2866
2993
|
#claims;
|
|
2867
2994
|
#completion;
|
|
2868
2995
|
#maintenance;
|
|
2996
|
+
#setup;
|
|
2869
2997
|
#owned = new OwnedConnections();
|
|
2870
2998
|
constructor(options) {
|
|
2871
2999
|
const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
|
|
@@ -2879,7 +3007,7 @@ var PostgresEventStore = class {
|
|
|
2879
3007
|
statements,
|
|
2880
3008
|
schemaStatements,
|
|
2881
3009
|
setupLockKey: statements.setup.setupLockKey,
|
|
2882
|
-
schemaMarker:
|
|
3010
|
+
schemaMarker: version040SchemaMarker(options),
|
|
2883
3011
|
purgeBatchSize,
|
|
2884
3012
|
completionHistory: names.completionHistory,
|
|
2885
3013
|
logger,
|
|
@@ -2903,42 +3031,20 @@ var PostgresEventStore = class {
|
|
|
2903
3031
|
this.#claims = new PostgresClaims(state);
|
|
2904
3032
|
this.#completion = new PostgresCompletion(state);
|
|
2905
3033
|
this.#maintenance = new PostgresMaintenance(state);
|
|
3034
|
+
this.#setup = new PostgresSetup(state, options);
|
|
2906
3035
|
}
|
|
2907
3036
|
/**
|
|
2908
3037
|
* Creates the schema, tables and indexes if they do not exist. Safe to call
|
|
2909
3038
|
* from many processes at once: a transaction-scoped advisory lock serializes
|
|
2910
3039
|
* the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
|
|
2911
|
-
* When the lane table already carries the marker
|
|
2912
|
-
*
|
|
3040
|
+
* When the lane table already carries the schema marker ({@link version040SchemaMarker})
|
|
3041
|
+
* and the payload columns are `json`, nothing runs and no table is locked, so instances
|
|
2913
3042
|
* that start next to running dispatchers cannot deadlock with their claims.
|
|
2914
3043
|
* Skip it (`setupStore: false`) when a migration tool applies
|
|
2915
3044
|
* `createPostgresSchemaSql()` instead.
|
|
2916
3045
|
*/
|
|
2917
3046
|
async setup() {
|
|
2918
|
-
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
await this.#state.executor.transaction(async (transaction) => {
|
|
2922
|
-
await this.#state.executor.query(
|
|
2923
|
-
this.#state.statements.setup.setupLock,
|
|
2924
|
-
[this.#state.setupLockKey],
|
|
2925
|
-
transaction
|
|
2926
|
-
);
|
|
2927
|
-
if (await this.#schemaIsCurrent(transaction)) {
|
|
2928
|
-
return;
|
|
2929
|
-
}
|
|
2930
|
-
for (const statement of this.#state.schemaStatements) {
|
|
2931
|
-
await this.#state.executor.query(statement, [], transaction);
|
|
2932
|
-
}
|
|
2933
|
-
});
|
|
2934
|
-
}
|
|
2935
|
-
async #schemaIsCurrent(transaction) {
|
|
2936
|
-
const rows = await this.#state.executor.query(
|
|
2937
|
-
this.#state.statements.setup.schemaCurrent,
|
|
2938
|
-
[this.#state.schemaMarker],
|
|
2939
|
-
transaction
|
|
2940
|
-
);
|
|
2941
|
-
return rows[0]["current"] === true;
|
|
3047
|
+
await this.#setup.run();
|
|
2942
3048
|
}
|
|
2943
3049
|
/**
|
|
2944
3050
|
* Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
|
|
@@ -3104,7 +3210,12 @@ var PostgresEventStore = class {
|
|
|
3104
3210
|
};
|
|
3105
3211
|
function connect(options, { logger, owned }) {
|
|
3106
3212
|
if (options.executor !== void 0) {
|
|
3107
|
-
|
|
3213
|
+
const { executor: executor2, pool: pool2 } = options;
|
|
3214
|
+
return {
|
|
3215
|
+
executor: pool2 === void 0 ? executor2 : withRootPool(executor2, new PgExecutor(pool2, { logger })),
|
|
3216
|
+
pool: pool2,
|
|
3217
|
+
ownedPool: void 0
|
|
3218
|
+
};
|
|
3108
3219
|
}
|
|
3109
3220
|
const pool = options.pool ?? new pg.Pool({
|
|
3110
3221
|
...options.poolConfig,
|