@relaybus/postgres 0.3.0 → 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/dist/index.cjs CHANGED
@@ -189,6 +189,26 @@ var DEFAULT_SCHEMA = "public";
189
189
  var MAXIMUM_CHANNEL_LENGTH = 63;
190
190
  var DEFAULT_TABLE_PREFIX = "relaybus_";
191
191
  var TABLE_PREFIX_PATTERN = /^[a-z0-9_]*$/;
192
+ function indexNames(index) {
193
+ return {
194
+ deliveryEventHandler: index("deliveries_event_handler_unique"),
195
+ deliveryLaneDue: index("deliveries_lane_due_idx"),
196
+ deliveryKeyPending: index("deliveries_key_pending_idx"),
197
+ eventCreated: index("events_created_idx"),
198
+ deliveryCompleted: index("deliveries_completed_idx"),
199
+ deliveryDead: index("deliveries_dead_idx"),
200
+ completionCompleted: index("completed_deliveries_completed_idx"),
201
+ eventCompletionsCompleted: index("event_completions_completed_idx"),
202
+ obsolete: [
203
+ index("deliveries_due_idx"),
204
+ index("deliveries_due_unkeyed_idx"),
205
+ index("deliveries_handler_created_idx"),
206
+ index("deliveries_key_unfinished_idx"),
207
+ index("deliveries_key_processing_idx"),
208
+ index("deliveries_status_created_idx")
209
+ ]
210
+ };
211
+ }
192
212
  function resolvePostgresNames(options = {}) {
193
213
  const schema = options.schema ?? DEFAULT_SCHEMA;
194
214
  const tablePrefix = options.tablePrefix ?? DEFAULT_TABLE_PREFIX;
@@ -218,24 +238,7 @@ function resolvePostgresNames(options = {}) {
218
238
  completions: table("completed_deliveries"),
219
239
  completionHistory: options.completionHistory ?? false,
220
240
  notificationChannel: channelName(`${schema}.${tablePrefix}deliveries`),
221
- indexes: {
222
- deliveryEventHandler: index("deliveries_event_handler_unique"),
223
- deliveryLaneDue: index("deliveries_lane_due_idx"),
224
- deliveryKeyPending: index("deliveries_key_pending_idx"),
225
- eventCreated: index("events_created_idx"),
226
- deliveryCompleted: index("deliveries_completed_idx"),
227
- deliveryDead: index("deliveries_dead_idx"),
228
- completionCompleted: index("completed_deliveries_completed_idx"),
229
- eventCompletionsCompleted: index("event_completions_completed_idx"),
230
- obsolete: [
231
- index("deliveries_due_idx"),
232
- index("deliveries_due_unkeyed_idx"),
233
- index("deliveries_handler_created_idx"),
234
- index("deliveries_key_unfinished_idx"),
235
- index("deliveries_key_processing_idx"),
236
- index("deliveries_status_created_idx")
237
- ]
238
- }
241
+ indexes: indexNames(index)
239
242
  };
240
243
  }
241
244
  var CHANNEL_HASH_DIGITS = 32;
@@ -266,6 +269,9 @@ function createPostgresSchemaStatements(options = {}) {
266
269
  return statements;
267
270
  }
268
271
  function eventAndDeliveryStatements(names) {
272
+ return [...eventAndKeySequenceStatements(names), ...deliveryStatements(names)];
273
+ }
274
+ function eventAndKeySequenceStatements(names) {
269
275
  return [
270
276
  `CREATE TABLE IF NOT EXISTS ${names.events} (
271
277
  id text PRIMARY KEY,
@@ -285,7 +291,11 @@ function eventAndDeliveryStatements(names) {
285
291
  `CREATE TABLE IF NOT EXISTS ${names.keySequences} (
286
292
  key text PRIMARY KEY,
287
293
  sequence bigint NOT NULL
288
- )`,
294
+ )`
295
+ ];
296
+ }
297
+ function deliveryStatements(names) {
298
+ return [
289
299
  `CREATE TABLE IF NOT EXISTS ${names.deliveries} (
290
300
  id text COLLATE "C" PRIMARY KEY,
291
301
  event_id text NOT NULL REFERENCES ${names.events} (id) ON DELETE CASCADE,
@@ -377,6 +387,16 @@ function createPostgresSchemaSql(options = {}) {
377
387
  `;
378
388
  }
379
389
  var RECONNECT_DELAY_MS = 1e3;
390
+ function endOnce(connection, ended) {
391
+ let ending = Promise.resolve();
392
+ return () => {
393
+ if (!ended.value) {
394
+ ended.value = true;
395
+ ending = connection.end().catch(() => void 0);
396
+ }
397
+ return ending;
398
+ };
399
+ }
380
400
  var NotificationListener = class {
381
401
  #connect;
382
402
  #channel;
@@ -395,15 +415,8 @@ var NotificationListener = class {
395
415
  /** Connects and listens. Rejects when that fails; a connection lost later is replaced. */
396
416
  async start() {
397
417
  const connection = await this.#connect();
398
- let ended = false;
399
- let ending = Promise.resolve();
400
- const end = () => {
401
- if (!ended) {
402
- ended = true;
403
- ending = connection.end().catch(() => void 0);
404
- }
405
- return ending;
406
- };
418
+ const ended = { value: false };
419
+ const end = endOnce(connection, ended);
407
420
  if (this.#closed) {
408
421
  await end();
409
422
  return;
@@ -411,7 +424,7 @@ var NotificationListener = class {
411
424
  this.#drop = end;
412
425
  let listening = false;
413
426
  const onLost = (error) => {
414
- if (!listening || ended) {
427
+ if (!listening || ended.value) {
415
428
  return;
416
429
  }
417
430
  void end();
@@ -427,14 +440,8 @@ var NotificationListener = class {
427
440
  try {
428
441
  await connection.query(`LISTEN "${this.#channel}"`);
429
442
  } catch (error) {
430
- if (this.#drop === end) {
431
- this.#drop = void 0;
432
- }
433
- await end();
434
- if (this.#closed) {
435
- return;
436
- }
437
- throw error;
443
+ await this.#abandonFailedListen(end, error);
444
+ return;
438
445
  }
439
446
  if (this.#closed) {
440
447
  await end();
@@ -442,6 +449,17 @@ var NotificationListener = class {
442
449
  }
443
450
  listening = true;
444
451
  }
452
+ /** Forgets and ends a connection whose LISTEN failed; rethrows the failure unless closing caused it. */
453
+ async #abandonFailedListen(end, error) {
454
+ if (this.#drop === end) {
455
+ this.#drop = void 0;
456
+ }
457
+ await end();
458
+ if (this.#closed) {
459
+ return;
460
+ }
461
+ throw error;
462
+ }
445
463
  #scheduleReconnect() {
446
464
  this.#reconnectTimer = internal.schedule(core.systemClock, {
447
465
  run: () => {
@@ -476,7 +494,18 @@ var NotificationListener = class {
476
494
  }
477
495
  };
478
496
 
479
- // src/row-mapping.ts
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
+ }
480
509
  function text(row, column) {
481
510
  return row[column];
482
511
  }
@@ -503,16 +532,16 @@ function mapEventEnvelope(row) {
503
532
  occurredAt: new Date(text(row, "event_occurred_at"))
504
533
  };
505
534
  }
535
+ var LEASE_TOKENS = new internal.LaneLeaseTokenFormat({
536
+ laneCount: LANE_COUNT,
537
+ epochDigits: 18,
538
+ claimsDigits: 9
539
+ });
506
540
  function formatLeaseToken(lane, { epoch, claims }) {
507
- return `${lane}:${epoch}:${claims}`;
541
+ return LEASE_TOKENS.format(lane, { epoch, claims });
508
542
  }
509
- var LEASE_TOKEN_PATTERN = /^(\d{1,3}):(\d{1,18}):(\d{1,9})$/;
510
543
  function parseLeaseToken(token) {
511
- const match = LEASE_TOKEN_PATTERN.exec(token);
512
- if (match === null || Number(match[1]) >= LANE_COUNT) {
513
- return void 0;
514
- }
515
- return { lane: Number(match[1]), epoch: match[2], claims: Number(match[3]) };
544
+ return LEASE_TOKENS.parse(token);
516
545
  }
517
546
  function mapClaimedDelivery(row, leaseExpiresAt) {
518
547
  return {
@@ -564,22 +593,11 @@ var PostgresAppend = class {
564
593
  const createdAt = now.toISOString();
565
594
  const wakeAt = this.#wakeAt(plan.deliveries, { now, transaction });
566
595
  const handlersByEvent = handlersByEventOf(plan.deliveries);
567
- const keyCounts = keyCountsOf(plan.events);
568
- const fresh = await this.#state.query({
569
- sql: this.#state.statements.append.insertFreshEvents,
570
- parameters: [
571
- JSON.stringify(
572
- plan.events.map((event, ordinal) => ({
573
- ordinal,
574
- ...toEventRow(event, { sequence: null, handlers: handlersByEvent.get(event.id) ?? [] })
575
- }))
576
- ),
577
- createdAt,
578
- JSON.stringify(plan.deliveries),
579
- JSON.stringify([...keyCounts].map(([key, count]) => ({ key, count }))),
580
- wakeAt
581
- ],
582
- transaction
596
+ const fresh = await this.#insertFreshEvents(transaction, {
597
+ plan,
598
+ handlersByEvent,
599
+ createdAt,
600
+ wakeAt
583
601
  });
584
602
  const insertedEventIds = new Set(
585
603
  fresh.filter((row) => row["inserted"] === true).map((row) => row["id"])
@@ -601,6 +619,40 @@ var PostgresAppend = class {
601
619
  }
602
620
  return { ...splitOutcomes(plan, insertedEventIds), appendedDeliveryCount };
603
621
  }
622
+ /** Runs the publishing statement that inserts the events not stored yet, with their deliveries. */
623
+ async #insertFreshEvents(transaction, {
624
+ plan,
625
+ handlersByEvent,
626
+ createdAt,
627
+ wakeAt
628
+ }) {
629
+ const keyCounts = keyCountsOf(plan.events);
630
+ return this.#state.query({
631
+ sql: this.#state.statements.append.insertFreshEvents,
632
+ parameters: [
633
+ JSON.stringify(
634
+ plan.events.map((event, ordinal) => ({
635
+ ordinal,
636
+ ...toEventRow(event, { sequence: null, handlers: handlersByEvent.get(event.id) ?? [] })
637
+ }))
638
+ ),
639
+ createdAt,
640
+ JSON.stringify(plan.deliveries),
641
+ JSON.stringify([...keyCounts].map(([key, count]) => ({ key, count }))),
642
+ wakeAt
643
+ ],
644
+ transaction
645
+ });
646
+ }
647
+ /** Locks the given events and returns the ids of those that still exist. */
648
+ async #lockExistingEvents(transaction, existing) {
649
+ const locked = await this.#state.query({
650
+ sql: this.#state.statements.append.lockExistingEvents,
651
+ parameters: [JSON.stringify(existing.map((row) => row.id).sort())],
652
+ transaction
653
+ });
654
+ return new Set(locked.map((row) => row["id"]));
655
+ }
604
656
  /**
605
657
  * The time the publishing statements notify the dispatchers for (deliveries due by then wake them),
606
658
  * or `null` for no notification. An append in a transaction of the caller's always notifies, at its
@@ -668,13 +720,7 @@ var PostgresAppend = class {
668
720
  })
669
721
  );
670
722
  while (existing.length > 0) {
671
- const locked = new Set(
672
- (await this.#state.query({
673
- sql: this.#state.statements.append.lockExistingEvents,
674
- parameters: [JSON.stringify(existing.map((row) => row.id).sort())],
675
- transaction
676
- })).map((row) => row["id"])
677
- );
723
+ const locked = await this.#lockExistingEvents(transaction, existing);
678
724
  const gone = existing.filter((row) => !locked.has(row.id));
679
725
  if (gone.length > 0) {
680
726
  const inserted2 = await this.#state.query({
@@ -711,7 +757,7 @@ function planAppend(entries) {
711
757
  events.push(event);
712
758
  }
713
759
  for (const delivery of newDeliveries) {
714
- const identity = `${event.id}\0${delivery.handler}`;
760
+ const identity = internal.CompositeKey.of(event.id, delivery.handler);
715
761
  if (!seenDeliveries.has(identity)) {
716
762
  seenDeliveries.add(identity);
717
763
  deliveries.push({
@@ -776,49 +822,46 @@ var ReconcileFailedError = class extends core.RelayBusError {
776
822
  name = "ReconcileFailedError";
777
823
  code = "RELAYBUS_POSTGRES_RECONCILE_FAILED";
778
824
  };
779
-
780
- // src/postgres-store-state.ts
781
- function handedOutKey(deliveryId, leaseToken) {
782
- return `${deliveryId}\0${leaseToken}`;
825
+ function leftWorkBehind(row) {
826
+ return row["takeover_waiting"] === true || row["unexamined_free_lanes"] === true || row["stale"] === true;
827
+ }
828
+ function keepKeyHeads(picked, limit) {
829
+ const seenKeys = /* @__PURE__ */ new Set();
830
+ const kept = [...picked].sort((left, right) => compareSequence(left, right)).filter((row) => {
831
+ if (row["delivery_key"] === null) {
832
+ return true;
833
+ }
834
+ const group = internal.CompositeKey.of(row["handler"], row["delivery_key"]);
835
+ return !seenKeys.has(group) && Boolean(seenKeys.add(group));
836
+ });
837
+ kept.sort(compareDue);
838
+ return kept.slice(0, limit);
839
+ }
840
+ function compareSequence(left, right) {
841
+ const first = sequenceOf(left);
842
+ const second = sequenceOf(right);
843
+ return Number(first > second) - Number(first < second);
844
+ }
845
+ function sequenceOf(row) {
846
+ return BigInt(row["sequence"] ?? 0);
847
+ }
848
+ function compareDue(left, right) {
849
+ return compareText(left["due_at"], right["due_at"]) || compareText(left["delivery_id"], right["delivery_id"]);
850
+ }
851
+ function compareText(left, right) {
852
+ return Number(left > right) - Number(left < right);
783
853
  }
784
854
 
785
855
  // src/postgres-claim.ts
786
856
  var INITIAL_LANES_PER_ROUND = 4;
787
857
  var MAXIMUM_STALE_CLAIM_ATTEMPTS = 3;
788
- var PostgresClaims = class {
858
+ var PostgresClaims = class extends internal.OwnerSerializedClaims {
789
859
  #state;
790
860
  constructor(state) {
861
+ super();
791
862
  this.#state = state;
792
863
  }
793
- /** Implements {@link PostgresEventStore.claim}. */
794
- claim(request) {
795
- if (request.handlers.length === 0 || !(request.limit > 0)) {
796
- return Promise.resolve([]);
797
- }
798
- return this.#serialized(request.leaseOwner, () => this.#claim(request));
799
- }
800
- /**
801
- * Runs `work` after the earlier claims of the same owner on this store settled. The
802
- * bookkeeping of what was handed out and the reconciliation of a failed claim need
803
- * one claim at a time per owner (the dispatcher claims one batch at a time anyway;
804
- * `runOnce` next to a running dispatcher is what this guards against).
805
- */
806
- #serialized(owner, work) {
807
- const previous = this.#state.ownerQueues.get(owner) ?? Promise.resolve();
808
- const run = previous.then(work, work);
809
- const tail = run.then(
810
- () => void 0,
811
- () => void 0
812
- );
813
- this.#state.ownerQueues.set(owner, tail);
814
- void tail.then(() => {
815
- if (this.#state.ownerQueues.get(owner) === tail) {
816
- this.#state.ownerQueues.delete(owner);
817
- }
818
- });
819
- return run;
820
- }
821
- async #claim(request) {
864
+ async claimInTurn(request) {
822
865
  await this.#assertReconciled(request.leaseOwner);
823
866
  const heldFirst = this.#state.drainingOwners.has(request.leaseOwner);
824
867
  let rows = await this.#claimFirst(request, heldFirst);
@@ -990,7 +1033,7 @@ var PostgresClaims = class {
990
1033
  #handOut(rows, leaseOwner) {
991
1034
  for (const row of rows) {
992
1035
  const claim = mapClaimedDelivery(row, /* @__PURE__ */ new Date(0));
993
- this.#state.handedOut.set(handedOutKey(claim.deliveryId, claim.leaseToken), {
1036
+ this.#state.handedOut.register(claim, {
994
1037
  owner: leaseOwner,
995
1038
  deliveryId: claim.deliveryId,
996
1039
  ...parseLeaseToken(claim.leaseToken)
@@ -1011,11 +1054,9 @@ var PostgresClaims = class {
1011
1054
  ...JSON.parse(row["running_ids"]),
1012
1055
  ...claimed.map((picked) => picked["delivery_id"])
1013
1056
  ]);
1014
- for (const [key, entry] of this.#state.handedOut) {
1015
- if (entry.owner === leaseOwner && heldLanes.has(entry.lane) && !running.has(entry.deliveryId)) {
1016
- this.#state.handedOut.delete(key);
1017
- }
1018
- }
1057
+ this.#state.handedOut.forgetWhere(
1058
+ (entry) => entry.owner === leaseOwner && heldLanes.has(entry.lane) && !running.has(entry.deliveryId)
1059
+ );
1019
1060
  }
1020
1061
  /**
1021
1062
  * After a claim failed, possibly after the database recorded its batch: gives back the recorded
@@ -1025,7 +1066,7 @@ var PostgresClaims = class {
1025
1066
  */
1026
1067
  async #reconcile(leaseOwner) {
1027
1068
  const generation = this.#generation(leaseOwner, true);
1028
- const handedOut = [...this.#state.handedOut.values()].filter((entry) => entry.owner === leaseOwner).map(({ deliveryId, lane, epoch, claims }) => ({ id: deliveryId, lane, epoch, claims }));
1069
+ const handedOut = this.#state.handedOut.entriesOf(leaseOwner).map(({ deliveryId, lane, epoch, claims }) => ({ id: deliveryId, lane, epoch, claims }));
1029
1070
  try {
1030
1071
  await this.#state.query({
1031
1072
  sql: this.#state.statements.claim.reconcileClaims,
@@ -1062,16 +1103,7 @@ var PostgresClaims = class {
1062
1103
  (row) => row["delivery_id"] !== null && !pickedIds.has(row["delivery_id"])
1063
1104
  )
1064
1105
  );
1065
- const seenKeys = /* @__PURE__ */ new Set();
1066
- const kept = [...picked].sort((left, right) => compareSequence(left, right)).filter((row) => {
1067
- if (row["delivery_key"] === null) {
1068
- return true;
1069
- }
1070
- const group = `${row["handler"]}\0${row["delivery_key"]}`;
1071
- return !seenKeys.has(group) && Boolean(seenKeys.add(group));
1072
- });
1073
- kept.sort(compareDue);
1074
- return kept.slice(0, limit);
1106
+ return keepKeyHeads(picked, limit);
1075
1107
  }
1076
1108
  return picked;
1077
1109
  }
@@ -1103,43 +1135,12 @@ var PostgresClaims = class {
1103
1135
  }
1104
1136
  };
1105
1137
  var GENERATION_STEP = 1n;
1106
- function leftWorkBehind(row) {
1107
- return row["takeover_waiting"] === true || row["unexamined_free_lanes"] === true || row["stale"] === true;
1108
- }
1109
- function compareSequence(left, right) {
1110
- const first = sequenceOf(left);
1111
- const second = sequenceOf(right);
1112
- return Number(first > second) - Number(first < second);
1113
- }
1114
- function sequenceOf(row) {
1115
- return BigInt(row["sequence"] ?? 0);
1116
- }
1117
- function compareDue(left, right) {
1118
- return compareText(left["due_at"], right["due_at"]) || compareText(left["delivery_id"], right["delivery_id"]);
1119
- }
1120
- function compareText(left, right) {
1121
- return Number(left > right) - Number(left < right);
1122
- }
1123
-
1124
- // src/postgres-completion.ts
1125
- var PostgresCompletion = class {
1138
+ var PostgresCompletion = class extends internal.LeaseSettlement {
1126
1139
  #state;
1127
1140
  constructor(state) {
1141
+ super();
1128
1142
  this.#state = state;
1129
1143
  }
1130
- /** Implements {@link PostgresEventStore.extendLease}. */
1131
- extendLease(lease, leaseExpiresAt) {
1132
- return this.#whenIssued(
1133
- lease,
1134
- (held) => this.#changed(this.#state.statements.completion.extendLease, [
1135
- lease.deliveryId,
1136
- held.lane,
1137
- held.epoch,
1138
- held.claims,
1139
- leaseExpiresAt.toISOString()
1140
- ])
1141
- );
1142
- }
1143
1144
  /** Implements {@link PostgresEventStore.complete}. */
1144
1145
  async complete(lease, { now, transaction }) {
1145
1146
  return (await this.#completeLeases([lease], { now, transaction }))[0];
@@ -1148,76 +1149,44 @@ var PostgresCompletion = class {
1148
1149
  completeMany(leases, now) {
1149
1150
  return this.#completeLeases(leases, { now, transaction: void 0 });
1150
1151
  }
1151
- /** Implements {@link PostgresEventStore.reschedule}. */
1152
- reschedule(lease, {
1153
- availableAt,
1154
- error,
1155
- now
1156
- }) {
1157
- return this.#settled(
1158
- lease,
1159
- this.#whenIssued(
1160
- lease,
1161
- (held) => this.#changed(this.#state.statements.completion.reschedule, [
1162
- lease.deliveryId,
1163
- held.lane,
1164
- held.epoch,
1165
- held.claims,
1166
- availableAt.toISOString(),
1167
- JSON.stringify(error),
1168
- now.toISOString()
1169
- ])
1170
- )
1171
- );
1152
+ /** A lease this store issued; any other token cannot be held. */
1153
+ findHeld(lease) {
1154
+ return parseLeaseToken(lease.leaseToken);
1172
1155
  }
1173
- /** Implements {@link PostgresEventStore.bury}. */
1174
- bury(lease, { error, now }) {
1175
- return this.#settled(
1176
- lease,
1177
- this.#whenIssued(
1178
- lease,
1179
- (held) => this.#changed(this.#state.statements.completion.bury, [
1180
- lease.deliveryId,
1181
- held.lane,
1182
- held.epoch,
1183
- held.claims,
1184
- JSON.stringify(error),
1185
- now.toISOString()
1186
- ])
1187
- )
1188
- );
1156
+ writeExtension(held, { lease, leaseExpiresAt }) {
1157
+ return this.#changed(this.#state.statements.completion.extendLease, [
1158
+ ...fenceOf(lease, held),
1159
+ leaseExpiresAt.toISOString()
1160
+ ]);
1189
1161
  }
1190
- /** Implements {@link PostgresEventStore.release}. */
1191
- release(lease, now) {
1192
- return this.#settled(
1193
- lease,
1194
- this.#whenIssued(
1195
- lease,
1196
- (held) => this.#changed(this.#state.statements.completion.release, [
1197
- lease.deliveryId,
1198
- held.lane,
1199
- held.epoch,
1200
- held.claims,
1201
- now.toISOString()
1202
- ])
1203
- )
1204
- );
1162
+ writeChange(held, { lease, change }) {
1163
+ const { completion } = this.#state.statements;
1164
+ const fence = fenceOf(lease, held);
1165
+ if (change.kind === "reschedule") {
1166
+ return this.#changed(completion.reschedule, [
1167
+ ...fence,
1168
+ change.availableAt.toISOString(),
1169
+ JSON.stringify(change.error),
1170
+ change.now.toISOString()
1171
+ ]);
1172
+ }
1173
+ if (change.kind === "bury") {
1174
+ return this.#changed(completion.bury, [
1175
+ ...fence,
1176
+ JSON.stringify(change.error),
1177
+ change.now.toISOString()
1178
+ ]);
1179
+ }
1180
+ return this.#changed(completion.release, [...fence, change.now.toISOString()]);
1205
1181
  }
1206
1182
  /** Forgets a handed-out lease once the store answered about it (any answer: the claim is over or fenced out). */
1207
- async #settled(lease, work) {
1208
- const result = await work;
1209
- this.#state.handedOut.delete(handedOutKey(lease.deliveryId, lease.leaseToken));
1210
- return result;
1183
+ forgetHandedOut(lease) {
1184
+ this.#state.handedOut.forget(lease);
1211
1185
  }
1212
1186
  async #changed(sql, parameters) {
1213
1187
  const rows = await this.#state.query({ sql, parameters });
1214
1188
  return readCount(rows[0], "count") > 0;
1215
1189
  }
1216
- /** Runs `operation` for a lease this store issued; any other token cannot be held. */
1217
- async #whenIssued(lease, operation) {
1218
- const held = parseLeaseToken(lease.leaseToken);
1219
- return held !== void 0 && await operation(held);
1220
- }
1221
1190
  /**
1222
1191
  * Deletes the delivery of every lease that is still the current claim of its
1223
1192
  * delivery. Returns one flag per lease, in order. Results are matched to leases
@@ -1225,16 +1194,7 @@ var PostgresCompletion = class {
1225
1194
  * their own answers; a lease listed twice completes once, at its first position.
1226
1195
  */
1227
1196
  async #completeLeases(leases, { now, transaction }) {
1228
- const issued = [];
1229
- const seen = /* @__PURE__ */ new Set();
1230
- leases.forEach((lease, ord) => {
1231
- const held = parseLeaseToken(lease.leaseToken);
1232
- const identity = `${lease.deliveryId}\0${lease.leaseToken}`;
1233
- if (held !== void 0 && !seen.has(identity)) {
1234
- seen.add(identity);
1235
- issued.push({ ord, id: lease.deliveryId, ...held });
1236
- }
1237
- });
1197
+ const issued = issuedLeases(leases);
1238
1198
  const completed = /* @__PURE__ */ new Set();
1239
1199
  if (issued.length > 0) {
1240
1200
  const rows = await this.#state.query({
@@ -1248,12 +1208,28 @@ var PostgresCompletion = class {
1248
1208
  }
1249
1209
  if (transaction === void 0) {
1250
1210
  for (const lease of leases) {
1251
- this.#state.handedOut.delete(handedOutKey(lease.deliveryId, lease.leaseToken));
1211
+ this.#state.handedOut.forget(lease);
1252
1212
  }
1253
1213
  }
1254
1214
  return leases.map((_, ord) => completed.has(ord));
1255
1215
  }
1256
1216
  };
1217
+ function fenceOf(lease, held) {
1218
+ return [lease.deliveryId, held.lane, held.epoch, held.claims];
1219
+ }
1220
+ function issuedLeases(leases) {
1221
+ const issued = [];
1222
+ const seen = /* @__PURE__ */ new Set();
1223
+ leases.forEach((lease, ord) => {
1224
+ const held = parseLeaseToken(lease.leaseToken);
1225
+ const identity = internal.DeliveryLeaseIdentity.of(lease.deliveryId, lease.leaseToken);
1226
+ if (held !== void 0 && !seen.has(identity)) {
1227
+ seen.add(identity);
1228
+ issued.push({ ord, id: lease.deliveryId, ...held });
1229
+ }
1230
+ });
1231
+ return issued;
1232
+ }
1257
1233
  var OwnedConnections = class {
1258
1234
  #clients = /* @__PURE__ */ new Set();
1259
1235
  /**
@@ -1352,6 +1328,24 @@ var PostgresMaintenance = class {
1352
1328
  /** Implements {@link PostgresEventStore.purge}. */
1353
1329
  async purge(request) {
1354
1330
  const total = { deletedDeliveries: 0, deletedEvents: 0 };
1331
+ const batches = this.#purgeBatchPlan(request);
1332
+ for (const [sql, cutoff, deleteOrphans] of batches) {
1333
+ if (cutoff === void 0) {
1334
+ continue;
1335
+ }
1336
+ let deletedInBatch = this.#state.purgeBatchSize;
1337
+ while (deletedInBatch === this.#state.purgeBatchSize) {
1338
+ const batch = await this.#purgeBatch(sql, { cutoff, deleteOrphans });
1339
+ deletedInBatch = batch.deletedDeliveries;
1340
+ total.deletedDeliveries += batch.deletedDeliveries;
1341
+ total.deletedEvents += batch.deletedEvents;
1342
+ }
1343
+ }
1344
+ total.deletedEvents += await this.#purgeAllOrphanEvents(request);
1345
+ return total;
1346
+ }
1347
+ /** The purge statements to run in order: SQL, cutoff (undefined skips it) and the orphan-event delete. */
1348
+ #purgeBatchPlan(request) {
1355
1349
  const batches = [
1356
1350
  [
1357
1351
  this.#state.statements.purge.purgeCompletedBatch(),
@@ -1371,21 +1365,14 @@ var PostgresMaintenance = class {
1371
1365
  this.#state.statements.purge.deleteOrphanEventsWithoutCompletion
1372
1366
  ]);
1373
1367
  }
1374
- for (const [sql, cutoff, deleteOrphans] of batches) {
1375
- if (cutoff === void 0) {
1376
- continue;
1377
- }
1378
- let deletedInBatch = this.#state.purgeBatchSize;
1379
- while (deletedInBatch === this.#state.purgeBatchSize) {
1380
- const batch = await this.#purgeBatch(sql, { cutoff, deleteOrphans });
1381
- deletedInBatch = batch.deletedDeliveries;
1382
- total.deletedDeliveries += batch.deletedDeliveries;
1383
- total.deletedEvents += batch.deletedEvents;
1384
- }
1385
- }
1368
+ return batches;
1369
+ }
1370
+ /** Removes the events left without deliveries, in two passes; returns how many events were deleted. */
1371
+ async #purgeAllOrphanEvents(request) {
1372
+ let deletedEvents = 0;
1386
1373
  const orphanCutoff = [request.completedBefore, request.deadBefore].filter((cutoff) => cutoff !== void 0).sort((first, second) => first.getTime() - second.getTime())[0];
1387
1374
  if (orphanCutoff !== void 0) {
1388
- total.deletedEvents += await this.#purgeOrphanEvents(
1375
+ deletedEvents += await this.#purgeOrphanEvents(
1389
1376
  this.#state.statements.purge.lockOrphanEventsBatch,
1390
1377
  {
1391
1378
  deleteStatement: this.#state.statements.purge.deleteOrphanEventsWithoutCompletion,
@@ -1395,7 +1382,7 @@ var PostgresMaintenance = class {
1395
1382
  );
1396
1383
  }
1397
1384
  if (request.completedBefore !== void 0) {
1398
- total.deletedEvents += await this.#purgeOrphanEvents(
1385
+ deletedEvents += await this.#purgeOrphanEvents(
1399
1386
  this.#state.statements.purge.lockCompletedOrphanEventsBatch,
1400
1387
  {
1401
1388
  deleteStatement: this.#state.statements.purge.deleteExpiredCompletedOrphanEvents,
@@ -1404,7 +1391,7 @@ var PostgresMaintenance = class {
1404
1391
  }
1405
1392
  );
1406
1393
  }
1407
- return total;
1394
+ return deletedEvents;
1408
1395
  }
1409
1396
  async #purgeOrphanEvents(lockStatement, {
1410
1397
  deleteStatement,
@@ -1459,6 +1446,37 @@ var PostgresMaintenance = class {
1459
1446
  });
1460
1447
  }
1461
1448
  };
1449
+ var WAKE_UPS = ["notify", "poll"];
1450
+ var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1451
+ function resolveStoreSettings(options) {
1452
+ const sources = [options.executor ?? options.pool, options.connectionString].filter(
1453
+ (source) => source !== void 0
1454
+ );
1455
+ if (sources.length !== 1) {
1456
+ throw new core.ConfigurationError(
1457
+ "PostgresEventStore needs exactly one of pool, connectionString or executor"
1458
+ );
1459
+ }
1460
+ const purgeBatchSize = internal.PositiveInteger.of(
1461
+ options.purgeBatchSize ?? DEFAULT_PURGE_BATCH_SIZE,
1462
+ "purgeBatchSize"
1463
+ );
1464
+ const wakeUp = options.wakeUp ?? "notify";
1465
+ if (!WAKE_UPS.includes(wakeUp)) {
1466
+ throw new core.ConfigurationError(`wakeUp must be 'notify' or 'poll', received ${String(wakeUp)}`);
1467
+ }
1468
+ const preparedStatements = options.preparedStatements ?? true;
1469
+ if (typeof preparedStatements !== "boolean") {
1470
+ throw new core.ConfigurationError(
1471
+ `preparedStatements must be a boolean, received ${String(preparedStatements)}`
1472
+ );
1473
+ }
1474
+ return {
1475
+ purgeBatchSize,
1476
+ wakeUp,
1477
+ queryOptions: preparedStatements ? { prepare: true } : void 0
1478
+ };
1479
+ }
1462
1480
 
1463
1481
  // src/statements-append.ts
1464
1482
  var AppendStatements = class {
@@ -1476,15 +1494,27 @@ var AppendStatements = class {
1476
1494
  * inserted. Deliveries of events that already existed are left to {@link insertDeliveries}.
1477
1495
  */
1478
1496
  get insertFreshEvents() {
1479
- const events = this.#names.events;
1480
- return `WITH reserved AS (
1497
+ return `WITH ${this.#reservedSequences()}, ${this.#incomingEvents()}, ${this.#insertedEvents()}, ${this.#insertedDeliveries()}, ${this.#wakeUp("inserted_deliveries", "$5")}
1498
+ SELECT incoming.id, incoming.sequence::text AS sequence,
1499
+ incoming.id IN (SELECT inserted_events.id FROM inserted_events) AS inserted,
1500
+ (SELECT count(*) FROM inserted_deliveries)::int AS deliveries
1501
+ FROM incoming
1502
+ WHERE (SELECT count(*) FROM woken) >= 0`;
1503
+ }
1504
+ /** The CTE `reserved` of {@link insertFreshEvents}: the sequences the keys of `$4` reserve. */
1505
+ #reservedSequences() {
1506
+ return `reserved AS (
1481
1507
  INSERT INTO ${this.#names.keySequences} AS reserved (key, sequence)
1482
1508
  SELECT requested.key, requested.count
1483
1509
  FROM jsonb_to_recordset($4::jsonb) AS requested (key text, count bigint)
1484
1510
  ORDER BY requested.key
1485
1511
  ON CONFLICT (key) DO UPDATE SET sequence = reserved.sequence + EXCLUDED.sequence
1486
1512
  RETURNING reserved.key AS key, reserved.sequence AS sequence
1487
- ), incoming AS (
1513
+ )`;
1514
+ }
1515
+ /** The CTE `incoming` of {@link insertFreshEvents}: the events of `$1` with their key sequences. */
1516
+ #incomingEvents() {
1517
+ return `incoming AS (
1488
1518
  SELECT incoming.*,
1489
1519
  CASE WHEN incoming.key IS NULL THEN NULL ELSE
1490
1520
  reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
@@ -1494,8 +1524,12 @@ var AppendStatements = class {
1494
1524
  ordinal int, id text, type text, source text, key text, payload jsonb, headers jsonb,
1495
1525
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1496
1526
  LEFT JOIN reserved ON reserved.key = incoming.key
1497
- ), inserted_events AS (
1498
- INSERT INTO ${events}
1527
+ )`;
1528
+ }
1529
+ /** The CTE `inserted_events` of {@link insertFreshEvents}: the events of `$1` that do not exist yet. */
1530
+ #insertedEvents() {
1531
+ return `inserted_events AS (
1532
+ INSERT INTO ${this.#names.events}
1499
1533
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1500
1534
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1501
1535
  COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
@@ -1505,7 +1539,11 @@ var AppendStatements = class {
1505
1539
  ORDER BY incoming.id
1506
1540
  ON CONFLICT (id) DO NOTHING
1507
1541
  RETURNING id, key, sequence
1508
- ), inserted_deliveries AS (
1542
+ )`;
1543
+ }
1544
+ /** The CTE `inserted_deliveries` of {@link insertFreshEvents}: the deliveries of `$3` of the inserted events. */
1545
+ #insertedDeliveries() {
1546
+ return `inserted_deliveries AS (
1509
1547
  INSERT INTO ${this.#names.deliveries}
1510
1548
  (id, event_id, handler, lane, status, attempts, available_at, key, sequence, created_at, updated_at)
1511
1549
  SELECT delivery.id, delivery.event_id, delivery.handler, ${laneExpression("event.key", "event.id")}, 'pending', 0,
@@ -1515,16 +1553,15 @@ var AppendStatements = class {
1515
1553
  ORDER BY delivery.event_id, delivery.handler
1516
1554
  ON CONFLICT (event_id, handler) DO NOTHING
1517
1555
  RETURNING available_at
1518
- ), ${this.#wakeUp("inserted_deliveries", "$5")}
1519
- SELECT incoming.id, incoming.sequence::text AS sequence,
1520
- incoming.id IN (SELECT inserted_events.id FROM inserted_events) AS inserted,
1521
- (SELECT count(*) FROM inserted_deliveries)::int AS deliveries
1522
- FROM incoming
1523
- WHERE (SELECT count(*) FROM woken) >= 0`;
1556
+ )`;
1524
1557
  }
1525
- /** 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
+ */
1526
1563
  get notify() {
1527
- return `SELECT pg_notify('${this.#names.notificationChannel}', '')`;
1564
+ return `SELECT true AS notified FROM pg_notify('${this.#names.notificationChannel}', '')`;
1528
1565
  }
1529
1566
  /**
1530
1567
  * A CTE `woken` that sends one notification on {@link PostgresNames.notificationChannel} when a row of
@@ -2047,11 +2084,146 @@ SELECT count(*)::int AS count FROM changed`;
2047
2084
  }
2048
2085
  };
2049
2086
 
2050
- // src/statements-lanes.ts
2051
- var FREE_LANE_SURPLUS = 16;
2087
+ // src/statements-candidates.ts
2052
2088
  function notRunning(running, alias) {
2053
2089
  return running === null ? "true" : `NOT EXISTS (SELECT 1 FROM ${running} AS running WHERE running.handler = ${alias}.handler AND running.key = ${alias}.key)`;
2054
2090
  }
2091
+ var CandidateStatements = class {
2092
+ #names;
2093
+ constructor(names) {
2094
+ this.#names = names;
2095
+ }
2096
+ /**
2097
+ * CTEs that pick up to `limit` claimable deliveries from the lanes of the CTE `scope` (columns `lane`,
2098
+ * `epoch`, `in_flight`), named after `prefix`; the result is `<prefix>_candidates (id, lane, available_at)`.
2099
+ * The rules are those of `ClaimStatements.claim`. The fast window reads the first {@link LANE_SCAN_WINDOW} due
2100
+ * deliveries of each lane (at most {@link CLAIM_SCAN_WINDOW} together); of a key, only the lowest sequence
2101
+ * in the window can be its head, so only that one is checked for an earlier pending delivery. Only when
2102
+ * that yields fewer than `limit` are the lanes whose window filled up looked at again with a loose index
2103
+ * scan over the heads of all their keys. `running` names the CTE of the keyed deliveries running in the
2104
+ * lanes, or is null when nothing runs there.
2105
+ */
2106
+ candidates(prefix, {
2107
+ scope,
2108
+ running,
2109
+ limit
2110
+ }) {
2111
+ const spec = { prefix, scope, running, limit };
2112
+ return this.#fastCandidates(spec) + this.#slowCandidates(spec);
2113
+ }
2114
+ /** The fast window of each lane and the claimable deliveries in it (`<prefix>_window`, `<prefix>_fast`). */
2115
+ #fastCandidates({ prefix, scope, running, limit }) {
2116
+ const deliveries = this.#names.deliveries;
2117
+ return `${prefix}_window AS MATERIALIZED (
2118
+ SELECT recent.id, recent.lane, recent.available_at, recent.handler, recent.key, recent.sequence
2119
+ FROM ${scope}
2120
+ CROSS JOIN LATERAL (
2121
+ SELECT delivery.id, delivery.lane, delivery.available_at, delivery.handler, delivery.key, delivery.sequence
2122
+ FROM ${deliveries} AS delivery
2123
+ WHERE delivery.lane = ${scope}.lane AND delivery.status = 'pending'
2124
+ AND delivery.available_at <= $1::timestamptz AND ${HANDLER_PREDICATE}
2125
+ AND delivery.id <> ALL (${scope}.in_flight)
2126
+ ORDER BY delivery.available_at, delivery.id
2127
+ LIMIT ${LANE_SCAN_WINDOW}
2128
+ ) AS recent
2129
+ ORDER BY recent.available_at, recent.id
2130
+ LIMIT ${CLAIM_SCAN_WINDOW}
2131
+ ), ${prefix}_fast AS MATERIALIZED (
2132
+ SELECT picked.id, picked.lane, picked.available_at FROM (
2133
+ SELECT unkeyed.id, unkeyed.lane, unkeyed.available_at FROM ${prefix}_window AS unkeyed WHERE unkeyed.key IS NULL
2134
+ UNION ALL
2135
+ SELECT head.id, head.lane, head.available_at
2136
+ FROM (
2137
+ SELECT DISTINCT ON (keyed.handler, keyed.key) keyed.id, keyed.lane, keyed.available_at, keyed.handler, keyed.key, keyed.sequence
2138
+ FROM ${prefix}_window AS keyed
2139
+ WHERE keyed.key IS NOT NULL
2140
+ ORDER BY keyed.handler, keyed.key, keyed.sequence
2141
+ ) AS head
2142
+ WHERE NOT EXISTS (
2143
+ SELECT 1 FROM ${deliveries} AS earlier
2144
+ WHERE earlier.lane = head.lane AND earlier.handler = head.handler AND earlier.key = head.key
2145
+ AND earlier.status = 'pending' AND earlier.key IS NOT NULL AND earlier.sequence < head.sequence
2146
+ ) AND ${notRunning(running, "head")}
2147
+ ) AS picked
2148
+ ORDER BY picked.available_at, picked.id
2149
+ LIMIT ${limit}
2150
+ `;
2151
+ }
2152
+ /**
2153
+ * For lanes whose window filled up while the fast pick came up short: the heads of all their keys and
2154
+ * their unkeyed deliveries, then the union with the fast pick (`<prefix>_candidates`).
2155
+ */
2156
+ #slowCandidates(spec) {
2157
+ return this.#saturatedLanes(spec) + this.#keyHeads(spec) + this.#keyedHeads(spec) + this.#unkeyedRecent(spec) + this.#unionCandidates(spec);
2158
+ }
2159
+ /** `<prefix>_saturated`: the lanes whose window filled up while the fast pick came up short. */
2160
+ #saturatedLanes({ prefix, scope, limit }) {
2161
+ return `), ${prefix}_saturated AS MATERIALIZED (
2162
+ SELECT ${scope}.lane, ${scope}.in_flight FROM ${scope}
2163
+ WHERE (SELECT count(*) FROM ${prefix}_fast) < ${limit}
2164
+ AND ((SELECT count(*) FROM ${prefix}_window) >= ${CLAIM_SCAN_WINDOW}
2165
+ OR ${scope}.lane IN (
2166
+ SELECT full_lane.lane FROM ${prefix}_window AS full_lane
2167
+ GROUP BY full_lane.lane HAVING count(*) >= ${LANE_SCAN_WINDOW}
2168
+ ))
2169
+ `;
2170
+ }
2171
+ /** `<prefix>_heads`: the head of every key of the saturated lanes. */
2172
+ #keyHeads({ prefix }) {
2173
+ return `), ${prefix}_heads AS MATERIALIZED (
2174
+ SELECT head.id, head.lane, head.available_at, head.handler, head.key
2175
+ FROM ${prefix}_saturated AS scope
2176
+ CROSS JOIN LATERAL (${keyHeads(this.#names, "scope")}) AS head
2177
+ `;
2178
+ }
2179
+ /** `<prefix>_keyed`: the due heads that are not in flight and whose key runs nowhere. */
2180
+ #keyedHeads({ prefix, running, limit }) {
2181
+ return `), ${prefix}_keyed AS MATERIALIZED (
2182
+ SELECT heads.id, heads.lane, heads.available_at
2183
+ FROM ${prefix}_heads AS heads
2184
+ JOIN ${prefix}_saturated AS scope ON scope.lane = heads.lane
2185
+ WHERE heads.available_at <= $1::timestamptz AND heads.id <> ALL (scope.in_flight) AND ${notRunning(running, "heads")}
2186
+ ORDER BY heads.available_at, heads.id
2187
+ LIMIT ${limit}
2188
+ `;
2189
+ }
2190
+ /** `<prefix>_unkeyed`: the due unkeyed deliveries of the saturated lanes that are not in flight. */
2191
+ #unkeyedRecent({ prefix, limit }) {
2192
+ const deliveries = this.#names.deliveries;
2193
+ return `), ${prefix}_unkeyed AS MATERIALIZED (
2194
+ SELECT recent.id, recent.lane, recent.available_at
2195
+ FROM ${prefix}_saturated AS scope
2196
+ CROSS JOIN LATERAL (
2197
+ SELECT delivery.id, delivery.lane, delivery.available_at
2198
+ FROM ${deliveries} AS delivery
2199
+ WHERE delivery.lane = scope.lane AND delivery.key IS NULL AND delivery.status = 'pending'
2200
+ AND delivery.available_at <= $1::timestamptz AND ${HANDLER_PREDICATE}
2201
+ AND delivery.id <> ALL (scope.in_flight)
2202
+ ORDER BY delivery.available_at, delivery.id
2203
+ LIMIT ${limit}
2204
+ ) AS recent
2205
+ ORDER BY recent.available_at, recent.id
2206
+ LIMIT ${limit}
2207
+ `;
2208
+ }
2209
+ /** `<prefix>_candidates`: the union of the fast pick and the slow picks, oldest first, at most `limit`. */
2210
+ #unionCandidates({ prefix, limit }) {
2211
+ return `), ${prefix}_candidates AS MATERIALIZED (
2212
+ SELECT chosen.id, chosen.lane, chosen.available_at FROM (
2213
+ SELECT fast.id, fast.lane, fast.available_at FROM ${prefix}_fast AS fast
2214
+ UNION
2215
+ SELECT keyed.id, keyed.lane, keyed.available_at FROM ${prefix}_keyed AS keyed
2216
+ UNION
2217
+ SELECT unkeyed.id, unkeyed.lane, unkeyed.available_at FROM ${prefix}_unkeyed AS unkeyed
2218
+ ) AS chosen
2219
+ ORDER BY chosen.available_at, chosen.id
2220
+ LIMIT ${limit}
2221
+ )`;
2222
+ }
2223
+ };
2224
+
2225
+ // src/statements-lanes.ts
2226
+ var FREE_LANE_SURPLUS = 16;
2055
2227
  var freeLane = (alias) => `(${alias}.lease_expires_at IS NULL OR ${alias}.lease_expires_at <= $1::timestamptz
2056
2228
  OR (${alias}.lease_owner = $4::text AND ${alias}.claim_floor < $6::bigint))`;
2057
2229
  var NO_FREE_LANES = `), free_lanes AS (
@@ -2063,8 +2235,10 @@ var NO_FREE_LANES = `), free_lanes AS (
2063
2235
  )`;
2064
2236
  var LaneStatements = class {
2065
2237
  #names;
2238
+ #candidateStatements;
2066
2239
  constructor(names) {
2067
2240
  this.#names = names;
2241
+ this.#candidateStatements = new CandidateStatements(names);
2068
2242
  }
2069
2243
  /**
2070
2244
  * A claim in one statement, the only one in the common case. Parameters:
@@ -2139,7 +2313,7 @@ var LaneStatements = class {
2139
2313
  CROSS JOIN LATERAL unnest(held_scope.in_flight) AS flying (id)
2140
2314
  JOIN ${deliveries} AS delivery ON delivery.id = flying.id
2141
2315
  WHERE delivery.status = 'pending' AND delivery.key IS NOT NULL
2142
- ), ${this.#candidates("held", { scope: "held_scope", running: "held_running", limit: "$3::int" })}, wanted AS MATERIALIZED (
2316
+ ), ${this.#candidateStatements.candidates("held", { scope: "held_scope", running: "held_running", limit: "$3::int" })}, wanted AS MATERIALIZED (
2143
2317
  SELECT CASE WHEN (SELECT value FROM stale) THEN 0
2144
2318
  ELSE $3::int - (SELECT count(*) FROM held_candidates)::int END AS count
2145
2319
  `;
@@ -2228,7 +2402,7 @@ var LaneStatements = class {
2228
2402
  WHERE locked.epoch = seen.epoch AND locked.in_flight = seen.in_flight
2229
2403
  AND locked.lease_owner IS NOT DISTINCT FROM seen.lease_owner
2230
2404
  AND locked.lease_expires_at IS NOT DISTINCT FROM seen.lease_expires_at
2231
- ), ${this.#candidates("free", { scope: "free_scope", running: null, limit: "(SELECT count FROM wanted)" })}`;
2405
+ ), ${this.#candidateStatements.candidates("free", { scope: "free_scope", running: null, limit: "(SELECT count FROM wanted)" })}`;
2232
2406
  }
2233
2407
  /** The picked deliveries of held and taken lanes with their events, and the picks per lane. */
2234
2408
  #claimPicks() {
@@ -2317,113 +2491,6 @@ SELECT (SELECT value FROM stale) AS stale,
2317
2491
  FROM (SELECT 1) AS anchor
2318
2492
  LEFT JOIN picked_rows AS picked ON true
2319
2493
  ORDER BY picked.due_at, picked.delivery_id`;
2320
- }
2321
- /**
2322
- * CTEs that pick up to `limit` claimable deliveries from the lanes of the CTE `scope` (columns `lane`,
2323
- * `epoch`, `in_flight`), named after `prefix`; the result is `<prefix>_candidates (id, lane, available_at)`.
2324
- * The rules are those of `ClaimStatements.claim`. The fast window reads the first {@link LANE_SCAN_WINDOW} due
2325
- * deliveries of each lane (at most {@link CLAIM_SCAN_WINDOW} together); of a key, only the lowest sequence
2326
- * in the window can be its head, so only that one is checked for an earlier pending delivery. Only when
2327
- * that yields fewer than `limit` are the lanes whose window filled up looked at again with a loose index
2328
- * scan over the heads of all their keys. `running` names the CTE of the keyed deliveries running in the
2329
- * lanes, or is null when nothing runs there.
2330
- */
2331
- #candidates(prefix, {
2332
- scope,
2333
- running,
2334
- limit
2335
- }) {
2336
- const spec = { prefix, scope, running, limit };
2337
- return this.#fastCandidates(spec) + this.#slowCandidates(spec);
2338
- }
2339
- /** The fast window of each lane and the claimable deliveries in it (`<prefix>_window`, `<prefix>_fast`). */
2340
- #fastCandidates({ prefix, scope, running, limit }) {
2341
- const deliveries = this.#names.deliveries;
2342
- return `${prefix}_window AS MATERIALIZED (
2343
- SELECT recent.id, recent.lane, recent.available_at, recent.handler, recent.key, recent.sequence
2344
- FROM ${scope}
2345
- CROSS JOIN LATERAL (
2346
- SELECT delivery.id, delivery.lane, delivery.available_at, delivery.handler, delivery.key, delivery.sequence
2347
- FROM ${deliveries} AS delivery
2348
- WHERE delivery.lane = ${scope}.lane AND delivery.status = 'pending'
2349
- AND delivery.available_at <= $1::timestamptz AND ${HANDLER_PREDICATE}
2350
- AND delivery.id <> ALL (${scope}.in_flight)
2351
- ORDER BY delivery.available_at, delivery.id
2352
- LIMIT ${LANE_SCAN_WINDOW}
2353
- ) AS recent
2354
- ORDER BY recent.available_at, recent.id
2355
- LIMIT ${CLAIM_SCAN_WINDOW}
2356
- ), ${prefix}_fast AS MATERIALIZED (
2357
- SELECT picked.id, picked.lane, picked.available_at FROM (
2358
- SELECT unkeyed.id, unkeyed.lane, unkeyed.available_at FROM ${prefix}_window AS unkeyed WHERE unkeyed.key IS NULL
2359
- UNION ALL
2360
- SELECT head.id, head.lane, head.available_at
2361
- FROM (
2362
- SELECT DISTINCT ON (keyed.handler, keyed.key) keyed.id, keyed.lane, keyed.available_at, keyed.handler, keyed.key, keyed.sequence
2363
- FROM ${prefix}_window AS keyed
2364
- WHERE keyed.key IS NOT NULL
2365
- ORDER BY keyed.handler, keyed.key, keyed.sequence
2366
- ) AS head
2367
- WHERE NOT EXISTS (
2368
- SELECT 1 FROM ${deliveries} AS earlier
2369
- WHERE earlier.lane = head.lane AND earlier.handler = head.handler AND earlier.key = head.key
2370
- AND earlier.status = 'pending' AND earlier.key IS NOT NULL AND earlier.sequence < head.sequence
2371
- ) AND ${notRunning(running, "head")}
2372
- ) AS picked
2373
- ORDER BY picked.available_at, picked.id
2374
- LIMIT ${limit}
2375
- `;
2376
- }
2377
- /**
2378
- * For lanes whose window filled up while the fast pick came up short: the heads of all their keys and
2379
- * their unkeyed deliveries, then the union with the fast pick (`<prefix>_candidates`).
2380
- */
2381
- #slowCandidates({ prefix, scope, running, limit }) {
2382
- const deliveries = this.#names.deliveries;
2383
- return `), ${prefix}_saturated AS MATERIALIZED (
2384
- SELECT ${scope}.lane, ${scope}.in_flight FROM ${scope}
2385
- WHERE (SELECT count(*) FROM ${prefix}_fast) < ${limit}
2386
- AND ((SELECT count(*) FROM ${prefix}_window) >= ${CLAIM_SCAN_WINDOW}
2387
- OR ${scope}.lane IN (
2388
- SELECT full_lane.lane FROM ${prefix}_window AS full_lane
2389
- GROUP BY full_lane.lane HAVING count(*) >= ${LANE_SCAN_WINDOW}
2390
- ))
2391
- ), ${prefix}_heads AS MATERIALIZED (
2392
- SELECT head.id, head.lane, head.available_at, head.handler, head.key
2393
- FROM ${prefix}_saturated AS scope
2394
- CROSS JOIN LATERAL (${keyHeads(this.#names, "scope")}) AS head
2395
- ), ${prefix}_keyed AS MATERIALIZED (
2396
- SELECT heads.id, heads.lane, heads.available_at
2397
- FROM ${prefix}_heads AS heads
2398
- JOIN ${prefix}_saturated AS scope ON scope.lane = heads.lane
2399
- WHERE heads.available_at <= $1::timestamptz AND heads.id <> ALL (scope.in_flight) AND ${notRunning(running, "heads")}
2400
- ORDER BY heads.available_at, heads.id
2401
- LIMIT ${limit}
2402
- ), ${prefix}_unkeyed AS MATERIALIZED (
2403
- SELECT recent.id, recent.lane, recent.available_at
2404
- FROM ${prefix}_saturated AS scope
2405
- CROSS JOIN LATERAL (
2406
- SELECT delivery.id, delivery.lane, delivery.available_at
2407
- FROM ${deliveries} AS delivery
2408
- WHERE delivery.lane = scope.lane AND delivery.key IS NULL AND delivery.status = 'pending'
2409
- AND delivery.available_at <= $1::timestamptz AND ${HANDLER_PREDICATE}
2410
- AND delivery.id <> ALL (scope.in_flight)
2411
- ORDER BY delivery.available_at, delivery.id
2412
- LIMIT ${limit}
2413
- ) AS recent
2414
- ORDER BY recent.available_at, recent.id
2415
- LIMIT ${limit}
2416
- ), ${prefix}_candidates AS MATERIALIZED (
2417
- SELECT chosen.id, chosen.lane, chosen.available_at FROM (
2418
- SELECT fast.id, fast.lane, fast.available_at FROM ${prefix}_fast AS fast
2419
- UNION
2420
- SELECT keyed.id, keyed.lane, keyed.available_at FROM ${prefix}_keyed AS keyed
2421
- UNION
2422
- SELECT unkeyed.id, unkeyed.lane, unkeyed.available_at FROM ${prefix}_unkeyed AS unkeyed
2423
- ) AS chosen
2424
- ORDER BY chosen.available_at, chosen.id
2425
- LIMIT ${limit}
2426
- )`;
2427
2494
  }
2428
2495
  /**
2429
2496
  * Takes up to `$3` free lanes (no lease, or an expired one) that have a due
@@ -2784,8 +2851,12 @@ var SetupStatements = class {
2784
2851
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2785
2852
  ) AS current`;
2786
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
+ */
2787
2858
  get setupLock() {
2788
- 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))";
2789
2860
  }
2790
2861
  };
2791
2862
 
@@ -2810,8 +2881,6 @@ var PostgresStatements = class {
2810
2881
  };
2811
2882
 
2812
2883
  // src/postgres-event-store.ts
2813
- var WAKE_UPS = ["notify", "poll"];
2814
- var DEFAULT_PURGE_BATCH_SIZE = 1e3;
2815
2884
  var PostgresEventStore = class {
2816
2885
  #state;
2817
2886
  #appender;
@@ -2838,10 +2907,9 @@ var PostgresEventStore = class {
2838
2907
  queryOptions,
2839
2908
  notifies: wakeUp === "notify",
2840
2909
  query: ({ sql, parameters, transaction }) => executor.query(sql, parameters, transaction, queryOptions),
2841
- handedOut: /* @__PURE__ */ new Map(),
2910
+ handedOut: new internal.HandedOutLedger(),
2842
2911
  unreconciled: /* @__PURE__ */ new Set(),
2843
2912
  generations: /* @__PURE__ */ new Map(),
2844
- ownerQueues: /* @__PURE__ */ new Map(),
2845
2913
  drainingOwners: /* @__PURE__ */ new Set(),
2846
2914
  ownedPool,
2847
2915
  pool,
@@ -2948,7 +3016,7 @@ var PostgresEventStore = class {
2948
3016
  return this.#claims.claim(request);
2949
3017
  }
2950
3018
  extendLease(lease, leaseExpiresAt) {
2951
- return this.#completion.extendLease(lease, leaseExpiresAt);
3019
+ return Promise.resolve(this.#completion.extendLease({ lease, leaseExpiresAt }));
2952
3020
  }
2953
3021
  // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
2954
3022
  complete(lease, now, transaction) {
@@ -2959,15 +3027,24 @@ var PostgresEventStore = class {
2959
3027
  }
2960
3028
  // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
2961
3029
  reschedule(lease, availableAt, error, now) {
2962
- return this.#completion.reschedule(lease, { availableAt, error, now });
3030
+ return Promise.resolve(
3031
+ this.#completion.changeClaim({
3032
+ lease,
3033
+ change: { kind: "reschedule", availableAt, error, now }
3034
+ })
3035
+ );
2963
3036
  }
2964
3037
  // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
2965
3038
  bury(lease, error, now) {
2966
- return this.#completion.bury(lease, { error, now });
3039
+ return Promise.resolve(
3040
+ this.#completion.changeClaim({ lease, change: { kind: "bury", error, now } })
3041
+ );
2967
3042
  }
2968
3043
  /** Returns a leased delivery to pending, due now, without counting the attempt. */
2969
3044
  release(lease, now) {
2970
- return this.#completion.release(lease, now);
3045
+ return Promise.resolve(
3046
+ this.#completion.changeClaim({ lease, change: { kind: "release", now } })
3047
+ );
2971
3048
  }
2972
3049
  /**
2973
3050
  * Calls `listener` soon after a transaction that published deliveries due at once commits, in any
@@ -3046,38 +3123,14 @@ var PostgresEventStore = class {
3046
3123
  }
3047
3124
  }
3048
3125
  };
3049
- function resolveStoreSettings(options) {
3050
- const sources = [options.pool, options.connectionString, options.executor].filter(
3051
- (source) => source !== void 0
3052
- );
3053
- if (sources.length !== 1) {
3054
- throw new core.ConfigurationError(
3055
- "PostgresEventStore needs exactly one of pool, connectionString or executor"
3056
- );
3057
- }
3058
- const purgeBatchSize = internal.assertPositiveInteger(
3059
- options.purgeBatchSize ?? DEFAULT_PURGE_BATCH_SIZE,
3060
- "purgeBatchSize"
3061
- );
3062
- const wakeUp = options.wakeUp ?? "notify";
3063
- if (!WAKE_UPS.includes(wakeUp)) {
3064
- throw new core.ConfigurationError(`wakeUp must be 'notify' or 'poll', received ${String(wakeUp)}`);
3065
- }
3066
- const preparedStatements = options.preparedStatements ?? true;
3067
- if (typeof preparedStatements !== "boolean") {
3068
- throw new core.ConfigurationError(
3069
- `preparedStatements must be a boolean, received ${String(preparedStatements)}`
3070
- );
3071
- }
3072
- return {
3073
- purgeBatchSize,
3074
- wakeUp,
3075
- queryOptions: preparedStatements ? { prepare: true } : void 0
3076
- };
3077
- }
3078
3126
  function connect(options, { logger, owned }) {
3079
3127
  if (options.executor !== void 0) {
3080
- 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
+ };
3081
3134
  }
3082
3135
  const pool = options.pool ?? new pg.Pool({
3083
3136
  ...options.poolConfig,