@relaybus/postgres 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -1,17 +1,31 @@
1
- import { P as PostgresExecutor, a as PostgresParameter, b as PostgresRow } from './executor-CSM7ZOhz.cjs';
2
- import { PoolClient, Pool, PoolConfig } from 'pg';
1
+ import { P as PostgresExecutor, a as PostgresParameter, b as PostgresQueryOptions, c as PostgresRow } from './executor-VZGmsMX4.cjs';
3
2
  import { RelayBusLogger, EventStore, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics } from '@relaybus/core';
3
+ import { PoolClient, Pool, PoolConfig } from 'pg';
4
4
 
5
+ interface PgExecutorOptions {
6
+ /** Receives the warning sent when prepared statements are switched off. Default `console`. */
7
+ readonly logger?: RelayBusLogger;
8
+ }
5
9
  /**
6
10
  * Executor for a `pg` {@link Pool}. A transaction is a dedicated pool client
7
11
  * between `BEGIN` and `COMMIT`; it is handed to callers as the transaction
8
12
  * handle, so they can run their own queries on it.
13
+ *
14
+ * A statement sent with `prepare: true` is a named prepared statement: each
15
+ * connection parses and plans its text once and from then on only binds the
16
+ * parameters. Its name is derived from the text, so stores with different
17
+ * tables never share one. When the server reports that a prepared statement
18
+ * does not exist on the connection, or already does (a pooler such as
19
+ * PgBouncer before 1.21 in transaction mode hands every transaction another
20
+ * server session), the executor stops preparing for good, logs a warning, and
21
+ * runs the statement again unprepared unless it belonged to a transaction,
22
+ * which has failed by then and is the caller's to retry.
9
23
  */
10
24
  declare class PgExecutor implements PostgresExecutor<PoolClient> {
11
25
  #private;
12
- constructor(pool: Pool);
26
+ constructor(pool: Pool, options?: PgExecutorOptions);
13
27
  transaction<T>(work: (transaction: PoolClient) => Promise<T>): Promise<T>;
14
- query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient): Promise<readonly PostgresRow[]>;
28
+ query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
15
29
  }
16
30
 
17
31
  /** Where relaybus keeps its tables. */
@@ -24,6 +38,14 @@ interface PostgresSchemaOptions {
24
38
  * Lower-case letters, digits and underscores. Default `'relaybus_'`.
25
39
  */
26
40
  readonly tablePrefix?: string;
41
+ /**
42
+ * Keep an append-only record of completed deliveries in
43
+ * `<prefix>completed_deliveries`, so `listDeliveries({ status: 'completed' })`,
44
+ * `statistics().completed` and `retention.completed` keep working. Default
45
+ * `false`: a completed delivery is deleted, which avoids the dead tuples and
46
+ * index churn of updating it. Use the same value in every process.
47
+ */
48
+ readonly completionHistory?: boolean;
27
49
  }
28
50
  /** Validated, quoted names used to build SQL. */
29
51
  interface PostgresNames {
@@ -32,18 +54,32 @@ interface PostgresNames {
32
54
  readonly events: string;
33
55
  readonly deliveries: string;
34
56
  readonly keySequences: string;
57
+ /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
58
+ readonly lanes: string;
59
+ /** Unquoted, unqualified name of {@link lanes}, for catalog lookups. */
60
+ readonly lanesTableName: string;
61
+ /** One row per event that had a delivery complete: when the last one did. Retention of completed work is measured from it. */
62
+ readonly eventCompletions: string;
63
+ /** Completion history, present only with `completionHistory`. */
64
+ readonly completions: string;
65
+ readonly completionHistory: boolean;
66
+ /**
67
+ * Channel of the notifications that wake dispatchers when deliveries were published:
68
+ * `<schema>.<prefix>deliveries`, or a hash of it when that is longer than a channel name may be.
69
+ */
70
+ readonly notificationChannel: string;
35
71
  /** Index names, quoted but not schema-qualified (an index lives in its table's schema). */
36
72
  readonly indexes: {
37
73
  readonly deliveryEventHandler: string;
38
- readonly deliveryDue: string;
39
- readonly deliveryDueUnkeyed: string;
40
- readonly deliveryHandlerCreated: string;
74
+ readonly deliveryLaneDue: string;
75
+ readonly deliveryKeyPending: string;
41
76
  readonly eventCreated: string;
42
- readonly deliveryKeyUnfinished: string;
43
- readonly deliveryKeyProcessing: string;
44
- readonly deliveryStatusCreated: string;
45
77
  readonly deliveryCompleted: string;
46
78
  readonly deliveryDead: string;
79
+ readonly completionCompleted: string;
80
+ readonly eventCompletionsCompleted: string;
81
+ /** Indexes of the 0.1.0 schema that the current schema drops. */
82
+ readonly obsolete: readonly string[];
47
83
  };
48
84
  }
49
85
  /**
@@ -55,15 +91,23 @@ interface PostgresNames {
55
91
  declare function resolvePostgresNames(options?: PostgresSchemaOptions): PostgresNames;
56
92
 
57
93
  /**
58
- * Statements that create the schema, tables and indexes, in dependency order.
59
- * Every statement is idempotent (`IF NOT EXISTS`). Run them in one transaction
60
- * and serialize concurrent runs (see {@link PostgresEventStore.setup}), or
61
- * paste {@link createPostgresSchemaSql} into your own migration tool.
94
+ * Statements that create the schema, tables and indexes, in dependency order,
95
+ * and upgrade a 0.1.0 schema in place. Every statement is idempotent
96
+ * (`IF NOT EXISTS`, or guarded by a check for the 0.1.0 columns). Run them in
97
+ * one transaction and serialize concurrent runs (see
98
+ * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
99
+ * into your own migration tool.
62
100
  *
63
101
  * `events.id` and `deliveries.id` are `text` so that any caller-supplied id
64
102
  * (UUID, ULID, deterministic business id) is accepted. `deliveries.id` uses the
65
103
  * `C` collation so that ordering by id matches a byte-wise comparison on every
66
104
  * database, whatever its locale.
105
+ *
106
+ * A delivery row is written once (insert) and removed once (delete) on the
107
+ * normal path. The lease lives in the lane row a delivery belongs to, so
108
+ * claiming, renewing and completing never update an indexed column of the
109
+ * delivery. The lane table is tiny, has no index besides its primary key and
110
+ * leaves free space in its pages, so its updates are heap-only (HOT).
67
111
  */
68
112
  declare function createPostgresSchemaStatements(options?: PostgresSchemaOptions): string[];
69
113
  /**
@@ -87,7 +131,29 @@ interface PostgresEventStoreSettings extends PostgresSchemaOptions {
87
131
  * it, otherwise `pg` raises an unhandled `error` event and the process exits.
88
132
  */
89
133
  readonly logger?: RelayBusLogger;
134
+ /**
135
+ * Send the statements the store repeats (publish, claim, complete, renew) as named prepared
136
+ * statements, so each connection parses and plans their text once instead of on every call. Default
137
+ * `true`. Set `false` behind a pooler in transaction mode that does not keep prepared statements per
138
+ * client (PgBouncer before 1.21, or 1.21+ with `max_prepared_statements = 0`). Left on there, the
139
+ * first statement that lands on another server session fails once, then the store stops preparing
140
+ * and logs a warning. Only executors that support it prepare (`PgExecutor`, the default); the
141
+ * other adapters send every statement unprepared.
142
+ */
143
+ readonly preparedStatements?: boolean;
144
+ /**
145
+ * How dispatchers in other processes learn about new deliveries.
146
+ * - `'notify'` (default): a publishing transaction sends `NOTIFY` (at most one per 10 ms per store,
147
+ * the rest merged into one trailing notification after the commit) and each dispatcher listens
148
+ * with `LISTEN` on a connection of its own, so a delivery is picked up within milliseconds.
149
+ * - `'poll'`: no `NOTIFY`, no listening connection. Other processes find new deliveries when they
150
+ * poll (`dispatcher.pollInterval`, default 1 s); a dispatcher in the publishing process is still
151
+ * woken at once. Use it behind a pooler in transaction mode, where `LISTEN` does not work.
152
+ */
153
+ readonly wakeUp?: PostgresWakeUp;
90
154
  }
155
+ /** See {@link PostgresEventStoreSettings.wakeUp}. */
156
+ type PostgresWakeUp = 'notify' | 'poll';
91
157
  /** Use an existing `pg` pool. The store never closes it. */
92
158
  interface PostgresPoolOptions extends PostgresEventStoreSettings {
93
159
  readonly pool: Pool;
@@ -126,13 +192,26 @@ type PostgresEventStoreOptions<TTransaction = PoolClient> = PostgresPoolOptions
126
192
  * Publishing locks that row and holds the lock until commit, so two
127
193
  * transactions publishing the same key are serialized and sequence order
128
194
  * equals commit order. Claiming then hands out only the lowest unfinished
129
- * sequence of a (handler, key).
130
- * - **Safe parallel claiming.** One `FOR UPDATE SKIP LOCKED` statement leases
131
- * deliveries; any number of processes can claim at once.
195
+ * sequence of a (handler, key), and all deliveries of a key live in one lane,
196
+ * so one worker at a time sees them.
197
+ * - **Lane leases.** Workers lease lanes (256 hash buckets of the ordering key
198
+ * or event id), not single deliveries. A claim writes one lane row for a
199
+ * whole batch and never writes a delivery row; a completion deletes the
200
+ * delivery rows. Delivery rows are therefore inserted once and deleted once,
201
+ * which leaves no dead index entries from updates on the normal path.
132
202
  * - **Fenced state changes.** Complete, reschedule, bury and extend only apply
133
- * to the lease token that claimed the delivery.
203
+ * with the epoch of the lane lease that claimed the delivery. A lane taken
204
+ * over by another worker gets a new epoch, which fences everything the
205
+ * previous holder still runs. Completion takes a key-share lock on the lane
206
+ * row, so it cannot interleave with a takeover.
207
+ * - **Attempts survive crashes.** A claim records which deliveries it put in
208
+ * flight in the lane row. If the holder dies, the next holder charges one
209
+ * attempt to each of them, so a poison message still reaches the retry limit.
134
210
  * - **App clock.** Every timestamp comes from the `now` the caller passes in,
135
211
  * never from the database clock.
212
+ *
213
+ * Completed deliveries are deleted. Pass `completionHistory: true` to keep an
214
+ * append-only record for `listDeliveries({ status: 'completed' })`.
136
215
  */
137
216
  declare class PostgresEventStore<TTransaction = PoolClient> implements EventStore<TTransaction> {
138
217
  #private;
@@ -141,36 +220,58 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
141
220
  * Creates the schema, tables and indexes if they do not exist. Safe to call
142
221
  * from many processes at once: a transaction-scoped advisory lock serializes
143
222
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
223
+ * When the lane table already carries the marker of the current statements
224
+ * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
225
+ * that start next to running dispatchers cannot deadlock with their claims.
144
226
  * Skip it (`setupStore: false`) when a migration tool applies
145
227
  * `createPostgresSchemaSql()` instead.
146
228
  */
147
229
  setup(): Promise<void>;
148
230
  transaction<T>(work: (transaction: TTransaction) => Promise<T>): Promise<T>;
149
231
  /**
150
- * Writes the entries in `transaction`. Keyed events reserve their sequence
151
- * numbers first (locking the key rows in sorted order, so publishers cannot
152
- * deadlock inside one call), then events, then deliveries, each in a single
153
- * multi-row statement ordered by primary key. Two transactions that append
154
- * keys in opposite order across several calls can still deadlock (40P01);
155
- * the database rolls one back and the caller retries.
232
+ * Writes the entries in `transaction`, normally with one statement
233
+ * ({@link PostgresStatements.insertFreshEvents}): keyed events reserve their sequence numbers (locking
234
+ * the key rows in sorted order, so publishers cannot deadlock inside one call), then the new events and
235
+ * their deliveries are inserted, ordered by primary key, and one notification wakes the dispatchers
236
+ * when a delivery is due now. Events that already existed get their missing handlers' deliveries in
237
+ * two more statements. Two transactions that append keys in opposite order across several calls can
238
+ * still deadlock (40P01); the database rolls one back and the caller retries.
156
239
  * A duplicate event id still consumes a sequence number; gaps are harmless
157
240
  * because only the order of sequences matters.
158
241
  */
159
242
  append(transaction: TTransaction, entries: readonly AppendEntry[], now: Date): Promise<AppendResult>;
160
243
  /**
161
- * Leases up to `limit` due deliveries with `FOR UPDATE SKIP LOCKED`. The fast
162
- * statement examines the first due deliveries; only when they are mostly
163
- * blocked followers of busy keys does a second statement look at the heads of
164
- * all keys. See {@link PostgresStatements.claim} for why that keeps a
165
- * (handler, key) strictly one-at-a-time and in order.
244
+ * Claims up to `limit` due deliveries, normally with one statement
245
+ * ({@link PostgresStatements.claimLanes}): from the lanes this owner already holds, then from free lanes
246
+ * it takes in the same statement, recording the batch in the lane rows. A claim that would have to take
247
+ * over a lane whose previous holder still runs deliveries under an expired lease continues in a
248
+ * transaction that charges their lost attempts first, taking free lanes a few at a time (4, 8, 16, ...);
249
+ * so does a claim whose statement kept seeing a lane it holds change under it, and one that came up
250
+ * short with more free lanes left than it looked at. The step that fails after
251
+ * the first one has recorded deliveries returns those instead of failing: a recorded delivery nobody
252
+ * received would be held for a whole lease. See {@link PostgresStatements.claim} for the rules a delivery
253
+ * must satisfy.
166
254
  */
167
255
  claim(request: ClaimRequest): Promise<ClaimedDelivery[]>;
168
- extendLease(lease: LeaseReference, leaseExpiresAt: Date, now: Date): Promise<boolean>;
256
+ extendLease(lease: LeaseReference, leaseExpiresAt: Date): Promise<boolean>;
169
257
  complete(lease: LeaseReference, now: Date, transaction?: TTransaction): Promise<boolean>;
258
+ completeMany(leases: readonly LeaseReference[], now: Date): Promise<boolean[]>;
170
259
  reschedule(lease: LeaseReference, availableAt: Date, error: ErrorInfo, now: Date): Promise<boolean>;
171
260
  bury(lease: LeaseReference, error: ErrorInfo, now: Date): Promise<boolean>;
172
261
  /** Returns a leased delivery to pending, due now, without counting the attempt. */
173
262
  release(lease: LeaseReference, now: Date): Promise<boolean>;
263
+ /**
264
+ * Calls `listener` soon after a transaction that published deliveries due at once commits, in any
265
+ * process: `append` sends one `pg_notify` per call (merged per transaction, none for delayed
266
+ * deliveries), and this listens with `LISTEN` on a connection of its own, opened with the settings of
267
+ * the store's pool (one connection more than the pool's `max`). A lost connection is replaced after a
268
+ * second. Resolves with `undefined` for a store built on an `executor`, which has no connection of its
269
+ * own to listen on, and with `wakeUp: 'poll'`: polling then finds new deliveries (and the bus still
270
+ * wakes its own dispatcher).
271
+ */
272
+ watchNewDeliveries(listener: () => void): Promise<(() => Promise<void>) | undefined>;
273
+ /** Frees the lanes `leaseOwner` holds that run no delivery, so other workers need not wait for the lease to expire. */
274
+ relinquish(leaseOwner: string): Promise<void>;
174
275
  listDeliveries(query: DeliveryQuery): Promise<DeliveryRecord[]>;
175
276
  requeue(deliveryIds: readonly string[], now: Date): Promise<number>;
176
277
  /**
@@ -188,4 +289,4 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
188
289
  close(): Promise<void>;
189
290
  }
190
291
 
191
- export { PgExecutor, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresRow, type PostgresSchemaOptions, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
292
+ export { PgExecutor, type PgExecutorOptions, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresQueryOptions, PostgresRow, type PostgresSchemaOptions, type PostgresWakeUp, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,31 @@
1
- import { P as PostgresExecutor, a as PostgresParameter, b as PostgresRow } from './executor-CSM7ZOhz.js';
2
- import { PoolClient, Pool, PoolConfig } from 'pg';
1
+ import { P as PostgresExecutor, a as PostgresParameter, b as PostgresQueryOptions, c as PostgresRow } from './executor-VZGmsMX4.js';
3
2
  import { RelayBusLogger, EventStore, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics } from '@relaybus/core';
3
+ import { PoolClient, Pool, PoolConfig } from 'pg';
4
4
 
5
+ interface PgExecutorOptions {
6
+ /** Receives the warning sent when prepared statements are switched off. Default `console`. */
7
+ readonly logger?: RelayBusLogger;
8
+ }
5
9
  /**
6
10
  * Executor for a `pg` {@link Pool}. A transaction is a dedicated pool client
7
11
  * between `BEGIN` and `COMMIT`; it is handed to callers as the transaction
8
12
  * handle, so they can run their own queries on it.
13
+ *
14
+ * A statement sent with `prepare: true` is a named prepared statement: each
15
+ * connection parses and plans its text once and from then on only binds the
16
+ * parameters. Its name is derived from the text, so stores with different
17
+ * tables never share one. When the server reports that a prepared statement
18
+ * does not exist on the connection, or already does (a pooler such as
19
+ * PgBouncer before 1.21 in transaction mode hands every transaction another
20
+ * server session), the executor stops preparing for good, logs a warning, and
21
+ * runs the statement again unprepared unless it belonged to a transaction,
22
+ * which has failed by then and is the caller's to retry.
9
23
  */
10
24
  declare class PgExecutor implements PostgresExecutor<PoolClient> {
11
25
  #private;
12
- constructor(pool: Pool);
26
+ constructor(pool: Pool, options?: PgExecutorOptions);
13
27
  transaction<T>(work: (transaction: PoolClient) => Promise<T>): Promise<T>;
14
- query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient): Promise<readonly PostgresRow[]>;
28
+ query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
15
29
  }
16
30
 
17
31
  /** Where relaybus keeps its tables. */
@@ -24,6 +38,14 @@ interface PostgresSchemaOptions {
24
38
  * Lower-case letters, digits and underscores. Default `'relaybus_'`.
25
39
  */
26
40
  readonly tablePrefix?: string;
41
+ /**
42
+ * Keep an append-only record of completed deliveries in
43
+ * `<prefix>completed_deliveries`, so `listDeliveries({ status: 'completed' })`,
44
+ * `statistics().completed` and `retention.completed` keep working. Default
45
+ * `false`: a completed delivery is deleted, which avoids the dead tuples and
46
+ * index churn of updating it. Use the same value in every process.
47
+ */
48
+ readonly completionHistory?: boolean;
27
49
  }
28
50
  /** Validated, quoted names used to build SQL. */
29
51
  interface PostgresNames {
@@ -32,18 +54,32 @@ interface PostgresNames {
32
54
  readonly events: string;
33
55
  readonly deliveries: string;
34
56
  readonly keySequences: string;
57
+ /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
58
+ readonly lanes: string;
59
+ /** Unquoted, unqualified name of {@link lanes}, for catalog lookups. */
60
+ readonly lanesTableName: string;
61
+ /** One row per event that had a delivery complete: when the last one did. Retention of completed work is measured from it. */
62
+ readonly eventCompletions: string;
63
+ /** Completion history, present only with `completionHistory`. */
64
+ readonly completions: string;
65
+ readonly completionHistory: boolean;
66
+ /**
67
+ * Channel of the notifications that wake dispatchers when deliveries were published:
68
+ * `<schema>.<prefix>deliveries`, or a hash of it when that is longer than a channel name may be.
69
+ */
70
+ readonly notificationChannel: string;
35
71
  /** Index names, quoted but not schema-qualified (an index lives in its table's schema). */
36
72
  readonly indexes: {
37
73
  readonly deliveryEventHandler: string;
38
- readonly deliveryDue: string;
39
- readonly deliveryDueUnkeyed: string;
40
- readonly deliveryHandlerCreated: string;
74
+ readonly deliveryLaneDue: string;
75
+ readonly deliveryKeyPending: string;
41
76
  readonly eventCreated: string;
42
- readonly deliveryKeyUnfinished: string;
43
- readonly deliveryKeyProcessing: string;
44
- readonly deliveryStatusCreated: string;
45
77
  readonly deliveryCompleted: string;
46
78
  readonly deliveryDead: string;
79
+ readonly completionCompleted: string;
80
+ readonly eventCompletionsCompleted: string;
81
+ /** Indexes of the 0.1.0 schema that the current schema drops. */
82
+ readonly obsolete: readonly string[];
47
83
  };
48
84
  }
49
85
  /**
@@ -55,15 +91,23 @@ interface PostgresNames {
55
91
  declare function resolvePostgresNames(options?: PostgresSchemaOptions): PostgresNames;
56
92
 
57
93
  /**
58
- * Statements that create the schema, tables and indexes, in dependency order.
59
- * Every statement is idempotent (`IF NOT EXISTS`). Run them in one transaction
60
- * and serialize concurrent runs (see {@link PostgresEventStore.setup}), or
61
- * paste {@link createPostgresSchemaSql} into your own migration tool.
94
+ * Statements that create the schema, tables and indexes, in dependency order,
95
+ * and upgrade a 0.1.0 schema in place. Every statement is idempotent
96
+ * (`IF NOT EXISTS`, or guarded by a check for the 0.1.0 columns). Run them in
97
+ * one transaction and serialize concurrent runs (see
98
+ * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
99
+ * into your own migration tool.
62
100
  *
63
101
  * `events.id` and `deliveries.id` are `text` so that any caller-supplied id
64
102
  * (UUID, ULID, deterministic business id) is accepted. `deliveries.id` uses the
65
103
  * `C` collation so that ordering by id matches a byte-wise comparison on every
66
104
  * database, whatever its locale.
105
+ *
106
+ * A delivery row is written once (insert) and removed once (delete) on the
107
+ * normal path. The lease lives in the lane row a delivery belongs to, so
108
+ * claiming, renewing and completing never update an indexed column of the
109
+ * delivery. The lane table is tiny, has no index besides its primary key and
110
+ * leaves free space in its pages, so its updates are heap-only (HOT).
67
111
  */
68
112
  declare function createPostgresSchemaStatements(options?: PostgresSchemaOptions): string[];
69
113
  /**
@@ -87,7 +131,29 @@ interface PostgresEventStoreSettings extends PostgresSchemaOptions {
87
131
  * it, otherwise `pg` raises an unhandled `error` event and the process exits.
88
132
  */
89
133
  readonly logger?: RelayBusLogger;
134
+ /**
135
+ * Send the statements the store repeats (publish, claim, complete, renew) as named prepared
136
+ * statements, so each connection parses and plans their text once instead of on every call. Default
137
+ * `true`. Set `false` behind a pooler in transaction mode that does not keep prepared statements per
138
+ * client (PgBouncer before 1.21, or 1.21+ with `max_prepared_statements = 0`). Left on there, the
139
+ * first statement that lands on another server session fails once, then the store stops preparing
140
+ * and logs a warning. Only executors that support it prepare (`PgExecutor`, the default); the
141
+ * other adapters send every statement unprepared.
142
+ */
143
+ readonly preparedStatements?: boolean;
144
+ /**
145
+ * How dispatchers in other processes learn about new deliveries.
146
+ * - `'notify'` (default): a publishing transaction sends `NOTIFY` (at most one per 10 ms per store,
147
+ * the rest merged into one trailing notification after the commit) and each dispatcher listens
148
+ * with `LISTEN` on a connection of its own, so a delivery is picked up within milliseconds.
149
+ * - `'poll'`: no `NOTIFY`, no listening connection. Other processes find new deliveries when they
150
+ * poll (`dispatcher.pollInterval`, default 1 s); a dispatcher in the publishing process is still
151
+ * woken at once. Use it behind a pooler in transaction mode, where `LISTEN` does not work.
152
+ */
153
+ readonly wakeUp?: PostgresWakeUp;
90
154
  }
155
+ /** See {@link PostgresEventStoreSettings.wakeUp}. */
156
+ type PostgresWakeUp = 'notify' | 'poll';
91
157
  /** Use an existing `pg` pool. The store never closes it. */
92
158
  interface PostgresPoolOptions extends PostgresEventStoreSettings {
93
159
  readonly pool: Pool;
@@ -126,13 +192,26 @@ type PostgresEventStoreOptions<TTransaction = PoolClient> = PostgresPoolOptions
126
192
  * Publishing locks that row and holds the lock until commit, so two
127
193
  * transactions publishing the same key are serialized and sequence order
128
194
  * equals commit order. Claiming then hands out only the lowest unfinished
129
- * sequence of a (handler, key).
130
- * - **Safe parallel claiming.** One `FOR UPDATE SKIP LOCKED` statement leases
131
- * deliveries; any number of processes can claim at once.
195
+ * sequence of a (handler, key), and all deliveries of a key live in one lane,
196
+ * so one worker at a time sees them.
197
+ * - **Lane leases.** Workers lease lanes (256 hash buckets of the ordering key
198
+ * or event id), not single deliveries. A claim writes one lane row for a
199
+ * whole batch and never writes a delivery row; a completion deletes the
200
+ * delivery rows. Delivery rows are therefore inserted once and deleted once,
201
+ * which leaves no dead index entries from updates on the normal path.
132
202
  * - **Fenced state changes.** Complete, reschedule, bury and extend only apply
133
- * to the lease token that claimed the delivery.
203
+ * with the epoch of the lane lease that claimed the delivery. A lane taken
204
+ * over by another worker gets a new epoch, which fences everything the
205
+ * previous holder still runs. Completion takes a key-share lock on the lane
206
+ * row, so it cannot interleave with a takeover.
207
+ * - **Attempts survive crashes.** A claim records which deliveries it put in
208
+ * flight in the lane row. If the holder dies, the next holder charges one
209
+ * attempt to each of them, so a poison message still reaches the retry limit.
134
210
  * - **App clock.** Every timestamp comes from the `now` the caller passes in,
135
211
  * never from the database clock.
212
+ *
213
+ * Completed deliveries are deleted. Pass `completionHistory: true` to keep an
214
+ * append-only record for `listDeliveries({ status: 'completed' })`.
136
215
  */
137
216
  declare class PostgresEventStore<TTransaction = PoolClient> implements EventStore<TTransaction> {
138
217
  #private;
@@ -141,36 +220,58 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
141
220
  * Creates the schema, tables and indexes if they do not exist. Safe to call
142
221
  * from many processes at once: a transaction-scoped advisory lock serializes
143
222
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
223
+ * When the lane table already carries the marker of the current statements
224
+ * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
225
+ * that start next to running dispatchers cannot deadlock with their claims.
144
226
  * Skip it (`setupStore: false`) when a migration tool applies
145
227
  * `createPostgresSchemaSql()` instead.
146
228
  */
147
229
  setup(): Promise<void>;
148
230
  transaction<T>(work: (transaction: TTransaction) => Promise<T>): Promise<T>;
149
231
  /**
150
- * Writes the entries in `transaction`. Keyed events reserve their sequence
151
- * numbers first (locking the key rows in sorted order, so publishers cannot
152
- * deadlock inside one call), then events, then deliveries, each in a single
153
- * multi-row statement ordered by primary key. Two transactions that append
154
- * keys in opposite order across several calls can still deadlock (40P01);
155
- * the database rolls one back and the caller retries.
232
+ * Writes the entries in `transaction`, normally with one statement
233
+ * ({@link PostgresStatements.insertFreshEvents}): keyed events reserve their sequence numbers (locking
234
+ * the key rows in sorted order, so publishers cannot deadlock inside one call), then the new events and
235
+ * their deliveries are inserted, ordered by primary key, and one notification wakes the dispatchers
236
+ * when a delivery is due now. Events that already existed get their missing handlers' deliveries in
237
+ * two more statements. Two transactions that append keys in opposite order across several calls can
238
+ * still deadlock (40P01); the database rolls one back and the caller retries.
156
239
  * A duplicate event id still consumes a sequence number; gaps are harmless
157
240
  * because only the order of sequences matters.
158
241
  */
159
242
  append(transaction: TTransaction, entries: readonly AppendEntry[], now: Date): Promise<AppendResult>;
160
243
  /**
161
- * Leases up to `limit` due deliveries with `FOR UPDATE SKIP LOCKED`. The fast
162
- * statement examines the first due deliveries; only when they are mostly
163
- * blocked followers of busy keys does a second statement look at the heads of
164
- * all keys. See {@link PostgresStatements.claim} for why that keeps a
165
- * (handler, key) strictly one-at-a-time and in order.
244
+ * Claims up to `limit` due deliveries, normally with one statement
245
+ * ({@link PostgresStatements.claimLanes}): from the lanes this owner already holds, then from free lanes
246
+ * it takes in the same statement, recording the batch in the lane rows. A claim that would have to take
247
+ * over a lane whose previous holder still runs deliveries under an expired lease continues in a
248
+ * transaction that charges their lost attempts first, taking free lanes a few at a time (4, 8, 16, ...);
249
+ * so does a claim whose statement kept seeing a lane it holds change under it, and one that came up
250
+ * short with more free lanes left than it looked at. The step that fails after
251
+ * the first one has recorded deliveries returns those instead of failing: a recorded delivery nobody
252
+ * received would be held for a whole lease. See {@link PostgresStatements.claim} for the rules a delivery
253
+ * must satisfy.
166
254
  */
167
255
  claim(request: ClaimRequest): Promise<ClaimedDelivery[]>;
168
- extendLease(lease: LeaseReference, leaseExpiresAt: Date, now: Date): Promise<boolean>;
256
+ extendLease(lease: LeaseReference, leaseExpiresAt: Date): Promise<boolean>;
169
257
  complete(lease: LeaseReference, now: Date, transaction?: TTransaction): Promise<boolean>;
258
+ completeMany(leases: readonly LeaseReference[], now: Date): Promise<boolean[]>;
170
259
  reschedule(lease: LeaseReference, availableAt: Date, error: ErrorInfo, now: Date): Promise<boolean>;
171
260
  bury(lease: LeaseReference, error: ErrorInfo, now: Date): Promise<boolean>;
172
261
  /** Returns a leased delivery to pending, due now, without counting the attempt. */
173
262
  release(lease: LeaseReference, now: Date): Promise<boolean>;
263
+ /**
264
+ * Calls `listener` soon after a transaction that published deliveries due at once commits, in any
265
+ * process: `append` sends one `pg_notify` per call (merged per transaction, none for delayed
266
+ * deliveries), and this listens with `LISTEN` on a connection of its own, opened with the settings of
267
+ * the store's pool (one connection more than the pool's `max`). A lost connection is replaced after a
268
+ * second. Resolves with `undefined` for a store built on an `executor`, which has no connection of its
269
+ * own to listen on, and with `wakeUp: 'poll'`: polling then finds new deliveries (and the bus still
270
+ * wakes its own dispatcher).
271
+ */
272
+ watchNewDeliveries(listener: () => void): Promise<(() => Promise<void>) | undefined>;
273
+ /** Frees the lanes `leaseOwner` holds that run no delivery, so other workers need not wait for the lease to expire. */
274
+ relinquish(leaseOwner: string): Promise<void>;
174
275
  listDeliveries(query: DeliveryQuery): Promise<DeliveryRecord[]>;
175
276
  requeue(deliveryIds: readonly string[], now: Date): Promise<number>;
176
277
  /**
@@ -188,4 +289,4 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
188
289
  close(): Promise<void>;
189
290
  }
190
291
 
191
- export { PgExecutor, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresRow, type PostgresSchemaOptions, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
292
+ export { PgExecutor, type PgExecutorOptions, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresQueryOptions, PostgresRow, type PostgresSchemaOptions, type PostgresWakeUp, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };