@relaybus/postgres 0.3.1 → 0.4.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 CHANGED
@@ -102,15 +102,29 @@ 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 is not integration-tested yet.** The adapter is typed structurally (no Prisma dependency) and unit-tested
106
- against a fake client only. Prisma interactive transactions default to a 5 s timeout, so pass
107
- `createPrismaExecutor(prisma, { timeout, maxWait })` if handlers run longer.
105
+ **Prisma** is integration-tested with a generated Prisma Client 7.10 (and 6.19 in CI) on the `@prisma/adapter-pg`
106
+ driver adapter: the full event store contract, publishing in `prisma.$transaction`, rollback and concurrent
107
+ transactions. The adapter is typed structurally, so the package has no Prisma dependency. Prisma interactive
108
+ transactions default to a 5 s timeout, so pass `createPrismaExecutor(prisma, { timeout, maxWait })` if handlers run
109
+ longer.
110
+
111
+ **Give an ORM store the `pg` pool too.** ORMs send raw statements unprepared and offer no connection to `LISTEN` on.
112
+ Pass the `pg` pool your ORM's driver uses as `pool` next to `executor`: the statements the store sends outside your
113
+ transactions (the dispatcher's claims, completions and renewals) then run on it as prepared statements, and the
114
+ store listens for wake-ups, so an event published in your ORM transaction is dispatched right after that
115
+ transaction commits. Your transactions still go through the ORM, and the store never closes the pool.
116
+
117
+ ```ts
118
+ const pool = new Pool({ connectionString });
119
+ const prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
120
+ const store = new PostgresEventStore({ executor: createPrismaExecutor(prisma), pool });
121
+ ```
108
122
 
109
123
  ## Options
110
124
 
111
125
  | Option | Default | Meaning |
112
126
  | ---------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------ |
113
- | `pool` / `connectionString` / `executor` | required, exactly one | How to reach the database. |
127
+ | `pool` / `connectionString` / `executor` | required, exactly one | How to reach the database. With `executor`, `pool` may be given too (see above). |
114
128
  | `poolConfig` | none | Extra `pg` pool settings when `connectionString` is used. |
115
129
  | `schema` | `'public'` | Schema of the tables. Created by `setup()` when it is not `public`. |
116
130
  | `tablePrefix` | `'relaybus_'` | Tables are `<prefix>events`, `<prefix>deliveries`, `<prefix>key_sequences`, `<prefix>lanes`. |
@@ -274,11 +288,16 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
274
288
  that channel (`<schema>.<prefix>deliveries`, hashed when longer than 63 characters) on one connection of its own,
275
289
  opened with the pool's settings outside the pool, so an event published by any process runs within milliseconds
276
290
  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. A store built on an `executor` cannot listen and relies on
278
- polling (and on the wake-up of its own process after a publish). `wakeUp: 'poll'` sends and listens for nothing.
291
+ net, so a missed notification only costs latency. The notification of a publish in your own transaction is sent
292
+ by PostgreSQL at its commit, however late that is, so the handler starts within milliseconds of the commit
293
+ (`wake-after-commit.integration.test.ts`). A store built on an `executor` listens only when it is given `pool`
294
+ as well; without it, it relies on polling and on the wake-ups of its own process after a publish (see
295
+ `bus.wake()`). `wakeUp: 'poll'` sends and listens for nothing.
279
296
  - **Prepared statements.** With `preparedStatements: true` (the default) the publish, claim, completion and renewal
280
297
  statements are named prepared statements: each connection parses and plans them once. Only `PgExecutor` (the
281
- default `pool` / `connectionString` path) prepares; the library adapters send every statement unprepared. See
298
+ default `pool` / `connectionString` path, and the `pool` given next to an `executor`) prepares; the library
299
+ adapters send the statements of your transactions unprepared (a dispatch batch on an ORM store parses 2 statements
300
+ without `pool`, 0 with it: `perf-design-orm-executors.integration.test.ts`). See
282
301
  `docs/performance/publish-path.md` for what that saves.
283
302
  - **Pooling.** Claim, complete, extend and purge need a free connection besides your transactions; size the pool for
284
303
  `dispatcher.concurrency` plus publishers; the listening connection comes on top of the pool's `max`. Listening and
@@ -310,12 +329,12 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
310
329
  ## Testing
311
330
 
312
331
  Integration tests use Testcontainers against `postgres:17-alpine` and `postgres:13-alpine` (override with
313
- `RELAYBUS_POSTGRES_IMAGES`). Every adapter except Prisma runs the full `@relaybus/testing` event store contract plus
314
- transaction tests against the real database.
332
+ `RELAYBUS_POSTGRES_IMAGES`). Every adapter (Kysely, Knex, TypeORM, Drizzle, Prisma with and without `pool`) runs the
333
+ full `@relaybus/testing` event store contract plus transaction tests against the real database.
315
334
 
316
335
  ## When to use @relaybus/postgres, and when not to
317
336
 
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), and the Prisma adapter is typed and unit-tested but not yet integration-tested.
337
+ 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
338
 
320
339
  Documentation: <https://berkcansavur.github.io/relaybus/packages/postgres>
321
340
 
@@ -1,5 +1,5 @@
1
1
  import { SQL } from 'drizzle-orm';
2
- import { P as PostgresExecutor } from './executor-CfJgktC6.cjs';
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 { type DrizzleLike, type DrizzleQueryable, createDrizzleExecutor };
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 { P as PostgresExecutor } from './executor-CfJgktC6.js';
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 { type DrizzleLike, type DrizzleQueryable, createDrizzleExecutor };
20
+ export { createDrizzleExecutor };
21
+ export type { DrizzleLike, DrizzleQueryable };
package/dist/index.cjs CHANGED
@@ -493,6 +493,19 @@ var NotificationListener = class {
493
493
  await drop?.();
494
494
  }
495
495
  };
496
+
497
+ // src/root-pool-executor.ts
498
+ function withRootPool(executor, root) {
499
+ return {
500
+ ...executor.sessionTimeouts === void 0 ? {} : { sessionTimeouts: executor.sessionTimeouts },
501
+ transaction: (work) => executor.transaction(work),
502
+ ...executor.transactionWithBudget === void 0 ? {} : {
503
+ transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
504
+ },
505
+ // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
506
+ query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
507
+ };
508
+ }
496
509
  function text(row, column) {
497
510
  return row[column];
498
511
  }
@@ -1436,7 +1449,7 @@ var PostgresMaintenance = class {
1436
1449
  var WAKE_UPS = ["notify", "poll"];
1437
1450
  var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1438
1451
  function resolveStoreSettings(options) {
1439
- const sources = [options.pool, options.connectionString, options.executor].filter(
1452
+ const sources = [options.executor ?? options.pool, options.connectionString].filter(
1440
1453
  (source) => source !== void 0
1441
1454
  );
1442
1455
  if (sources.length !== 1) {
@@ -1542,9 +1555,13 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1542
1555
  RETURNING available_at
1543
1556
  )`;
1544
1557
  }
1545
- /** One wake-up notification on {@link PostgresNames.notificationChannel}, outside any publishing transaction. */
1558
+ /**
1559
+ * One wake-up notification on {@link PostgresNames.notificationChannel}, outside any publishing
1560
+ * transaction. Returns a boolean column rather than the `void` of `pg_notify`, which some libraries
1561
+ * cannot read (Prisma's raw queries reject a `void` column).
1562
+ */
1546
1563
  get notify() {
1547
- return `SELECT pg_notify('${this.#names.notificationChannel}', '')`;
1564
+ return `SELECT true AS notified FROM pg_notify('${this.#names.notificationChannel}', '')`;
1548
1565
  }
1549
1566
  /**
1550
1567
  * A CTE `woken` that sends one notification on {@link PostgresNames.notificationChannel} when a row of
@@ -2834,8 +2851,12 @@ var SetupStatements = class {
2834
2851
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2835
2852
  ) AS current`;
2836
2853
  }
2854
+ /**
2855
+ * Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
2856
+ * some libraries cannot read (Prisma's raw queries reject a `void` column).
2857
+ */
2837
2858
  get setupLock() {
2838
- return "SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))";
2859
+ return "SELECT true AS locked FROM pg_advisory_xact_lock(hashtextextended($1::text, 0))";
2839
2860
  }
2840
2861
  };
2841
2862
 
@@ -3104,7 +3125,12 @@ var PostgresEventStore = class {
3104
3125
  };
3105
3126
  function connect(options, { logger, owned }) {
3106
3127
  if (options.executor !== void 0) {
3107
- return { executor: options.executor, pool: void 0, ownedPool: void 0 };
3128
+ const { executor: executor2, pool: pool2 } = options;
3129
+ return {
3130
+ executor: pool2 === void 0 ? executor2 : withRootPool(executor2, new PgExecutor(pool2, { logger })),
3131
+ pool: pool2,
3132
+ ownedPool: void 0
3133
+ };
3108
3134
  }
3109
3135
  const pool = options.pool ?? new pg.Pool({
3110
3136
  ...options.poolConfig,