@spine-event-engine/storage-postgres 2.0.0-snapshot.23 → 2.0.0-snapshot.25

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.
Files changed (53) hide show
  1. package/README.md +34 -16
  2. package/REFERENCE.md +66 -26
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/postgres/agent-history.d.ts +60 -18
  7. package/dist/postgres/agent-history.d.ts.map +1 -1
  8. package/dist/postgres/agent-history.js +114 -37
  9. package/dist/postgres/agent-history.js.map +1 -1
  10. package/dist/postgres/column-mapping.d.ts.map +1 -1
  11. package/dist/postgres/column-mapping.js +6 -3
  12. package/dist/postgres/column-mapping.js.map +1 -1
  13. package/dist/postgres/entity-commit.d.ts +90 -56
  14. package/dist/postgres/entity-commit.d.ts.map +1 -1
  15. package/dist/postgres/entity-commit.js +439 -188
  16. package/dist/postgres/entity-commit.js.map +1 -1
  17. package/dist/postgres/entity-history.d.ts +4 -2
  18. package/dist/postgres/entity-history.d.ts.map +1 -1
  19. package/dist/postgres/entity-history.js +163 -262
  20. package/dist/postgres/entity-history.js.map +1 -1
  21. package/dist/postgres/errors.d.ts +1 -1
  22. package/dist/postgres/errors.d.ts.map +1 -1
  23. package/dist/postgres/errors.js +5 -3
  24. package/dist/postgres/errors.js.map +1 -1
  25. package/dist/postgres/id-column.d.ts.map +1 -1
  26. package/dist/postgres/id-column.js +6 -3
  27. package/dist/postgres/id-column.js.map +1 -1
  28. package/dist/postgres/record-storage.d.ts +228 -117
  29. package/dist/postgres/record-storage.d.ts.map +1 -1
  30. package/dist/postgres/record-storage.js +350 -204
  31. package/dist/postgres/record-storage.js.map +1 -1
  32. package/dist/postgres/storage-factory.d.ts +267 -30
  33. package/dist/postgres/storage-factory.d.ts.map +1 -1
  34. package/dist/postgres/storage-factory.js +786 -145
  35. package/dist/postgres/storage-factory.js.map +1 -1
  36. package/dist/postgres/table-initializer.d.ts +7 -4
  37. package/dist/postgres/table-initializer.d.ts.map +1 -1
  38. package/dist/postgres/table-initializer.js +21 -15
  39. package/dist/postgres/table-initializer.js.map +1 -1
  40. package/dist/postgres/table-resolver.d.ts +15 -1
  41. package/dist/postgres/table-resolver.d.ts.map +1 -1
  42. package/dist/postgres/table-resolver.js +36 -7
  43. package/dist/postgres/table-resolver.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +5 -5
  46. package/dist/postgres/agent-execution.d.ts +0 -284
  47. package/dist/postgres/agent-execution.d.ts.map +0 -1
  48. package/dist/postgres/agent-execution.js +0 -648
  49. package/dist/postgres/agent-execution.js.map +0 -1
  50. package/dist/postgres/delivery-cleanup.d.ts +0 -76
  51. package/dist/postgres/delivery-cleanup.d.ts.map +0 -1
  52. package/dist/postgres/delivery-cleanup.js +0 -224
  53. package/dist/postgres/delivery-cleanup.js.map +0 -1
@@ -11,10 +11,11 @@
11
11
  * or implied. See the License for the specific language governing permissions and limitations under
12
12
  * the License.
13
13
  */
14
- import { fromBinary } from "@bufbuild/protobuf";
14
+ import {} from "@bufbuild/protobuf";
15
+ import { AnyPacker } from "@spine-event-engine/core";
15
16
  import { disabledEventHistoryPort, disabledStateHistoryPort, eventHistorySpec, stateHistorySpec, } from "@spine-event-engine/storage/provider";
16
- import { PostgresClientDisposal, PostgresStorageDataError, PostgresStorageErrors, PostgresStorageOperationError, } from "./errors.js";
17
- import { PostgresRecordStorage } from "./record-storage.js";
17
+ import { PostgresStorageDataError, PostgresStorageErrors } from "./errors.js";
18
+ import { PostgresRecordStorage, } from "./record-storage.js";
18
19
  /**
19
20
  * Provides PostgreSQL-backed current Entity storage and optional disabled history ports.
20
21
  *
@@ -24,6 +25,7 @@ import { PostgresRecordStorage } from "./record-storage.js";
24
25
  export class PostgresEntityStorage {
25
26
  records;
26
27
  onClose;
28
+ onAdmit;
27
29
  /**
28
30
  * Exposes current Entity records.
29
31
  */
@@ -44,21 +46,23 @@ export class PostgresEntityStorage {
44
46
  * @param records Stores current Entity record envelopes.
45
47
  * @param open Opens grouped PostgreSQL record families.
46
48
  * @param onClose Unregisters this Entity handle from its factory.
49
+ * @param onAdmit Admits complete public Entity operations before callbacks.
47
50
  */
48
- constructor(input, records, open, onClose) {
51
+ constructor(input, records, open, onClose, onAdmit) {
49
52
  this.records = records;
50
53
  this.onClose = onClose;
51
- this.current = new PostgresCurrentStorage(input, records);
54
+ this.onAdmit = onAdmit;
55
+ this.current = new PostgresCurrentStorage(input, records, onAdmit);
52
56
  const states = input.stateHistory ? stateHistorySpec(input.stateSchema) : undefined;
53
57
  this.states =
54
58
  states === undefined
55
59
  ? disabledStateHistoryPort()
56
- : new PostgresStates(input, open(states.spec, states.group));
60
+ : new PostgresStates(input, open(states.spec, states.group), onAdmit);
57
61
  const events = input.eventHistory ? eventHistorySpec(input.stateSchema) : undefined;
58
62
  this.events =
59
63
  events === undefined
60
64
  ? disabledEventHistoryPort()
61
- : new PostgresEvents(input, open(events.spec, events.group));
65
+ : new PostgresEvents(input, open(events.spec, events.group), onAdmit);
62
66
  }
63
67
  /**
64
68
  * Returns whether this Entity handle accepts operations.
@@ -90,6 +94,7 @@ export class PostgresEntityStorage {
90
94
  class PostgresStates {
91
95
  input;
92
96
  records;
97
+ onAdmit;
93
98
  #executor;
94
99
  #open = true;
95
100
  /**
@@ -97,10 +102,12 @@ class PostgresStates {
97
102
  *
98
103
  * @param input Supplies Entity identity and state-schema definitions.
99
104
  * @param records Stores immutable Entity state records.
105
+ * @param onAdmit Admits complete public history operations.
100
106
  */
101
- constructor(input, records) {
107
+ constructor(input, records, onAdmit) {
102
108
  this.input = input;
103
109
  this.records = records;
110
+ this.onAdmit = onAdmit;
104
111
  this.#executor = records.historyExecutor();
105
112
  }
106
113
  /**
@@ -110,16 +117,12 @@ class PostgresStates {
110
117
  * @returns Completion after PostgreSQL stores the record.
111
118
  */
112
119
  append(record) {
113
- if (!this.#open)
114
- return Promise.reject(new Error("Entity state history is closed."));
115
- return this.#executor
116
- .transaction(async (client) => {
117
- await client.query("SELECT pg_advisory_xact_lock_shared($1)", [this.familyKey()]);
118
- await client.query("SELECT pg_advisory_xact_lock($1)", [this.entityKey(record.entityId)]);
119
- await this.#executor.appendImmutable(client, record);
120
- })
121
- .catch((error) => {
122
- throw PostgresStorageErrors.operation(error);
120
+ return this.onAdmit(() => {
121
+ if (!this.#open)
122
+ return Promise.reject(new Error("Entity state history is closed."));
123
+ return this.records.writeImmutable(record).catch((error) => {
124
+ throw PostgresStorageErrors.operation(error);
125
+ });
123
126
  });
124
127
  }
125
128
  /**
@@ -130,12 +133,22 @@ class PostgresStates {
130
133
  * @param startingFromVersion Optional inclusive starting version.
131
134
  * @returns Entity state records in descending version order.
132
135
  */
133
- async backward(entityId, depth, startingFromVersion) {
136
+ backward(entityId, depth, startingFromVersion) {
137
+ return this.onAdmit(() => this.backwardAdmitted(entityId, depth, startingFromVersion));
138
+ }
139
+ /**
140
+ * Reads an admitted state-history page.
141
+ * @param entityId Entity whose history is read.
142
+ * @param depth Maximum number of states to return.
143
+ * @param startingFromVersion Optional inclusive version.
144
+ * @returns Entity state records in descending version order.
145
+ */
146
+ async backwardAdmitted(entityId, depth, startingFromVersion) {
134
147
  this.assertOpen();
135
148
  HistoryValues.assertDepth(depth);
136
149
  await this.#executor.prepare();
137
150
  return this.#executor
138
- .transaction((client) => this.#executor.query(client, this.backwardSql(startingFromVersion), this.backwardValues(entityId, depth, startingFromVersion)))
151
+ .using((client) => this.#executor.query(client, this.backwardSql(startingFromVersion), this.backwardValues(entityId, depth, startingFromVersion)))
139
152
  .catch((error) => {
140
153
  throw PostgresStorageErrors.operation(error);
141
154
  });
@@ -147,11 +160,20 @@ class PostgresStates {
147
160
  * @param time Inclusive state timestamp.
148
161
  * @returns Decoded state, or `undefined` when no state existed.
149
162
  */
150
- async stateAt(entityId, time) {
163
+ stateAt(entityId, time) {
164
+ return this.onAdmit(() => this.stateAtAdmitted(entityId, time));
165
+ }
166
+ /**
167
+ * Reads an admitted Entity state at a timestamp.
168
+ * @param entityId Entity whose state is read.
169
+ * @param time Inclusive state timestamp.
170
+ * @returns Decoded state, or absence.
171
+ */
172
+ async stateAtAdmitted(entityId, time) {
151
173
  this.assertOpen();
152
174
  await this.#executor.prepare();
153
175
  const records = await this.#executor
154
- .transaction((client) => this.#executor.query(client, this.stateAtSql(), [
176
+ .using((client) => this.#executor.query(client, this.stateAtSql(), [
155
177
  this.entityValue(entityId),
156
178
  HistoryValues.nanos(time),
157
179
  1,
@@ -163,7 +185,7 @@ class PostgresStates {
163
185
  if (record?.state === undefined)
164
186
  return undefined;
165
187
  try {
166
- return fromBinary(this.input.stateSchema, record.state.value);
188
+ return AnyPacker.unpack(record.state, this.input.stateSchema);
167
189
  }
168
190
  catch (error) {
169
191
  throw new PostgresStorageDataError("Stored PostgreSQL state data is invalid.", {
@@ -178,33 +200,25 @@ class PostgresStates {
178
200
  * @param keepMostRecent Number of newest states to retain.
179
201
  * @returns Completion after all older pages are deleted.
180
202
  */
181
- async trim(entityId, keepMostRecent) {
203
+ trim(entityId, keepMostRecent) {
204
+ return this.onAdmit(() => this.trimAdmitted(entityId, keepMostRecent));
205
+ }
206
+ /**
207
+ * Removes older admitted Entity state history.
208
+ * @param entityId Entity whose history is trimmed.
209
+ * @param keepMostRecent Number of newest states to retain.
210
+ * @returns Completion after all older pages are deleted.
211
+ */
212
+ async trimAdmitted(entityId, keepMostRecent) {
182
213
  this.assertOpen();
183
214
  HistoryValues.assertKeep(keepMostRecent);
184
215
  await this.#executor
185
216
  .using(async (client) => {
186
- const [family, entity] = this.trimLocks(entityId);
187
- const acquired = [];
188
- let operationFailure;
189
- try {
190
- await client.query("SELECT pg_advisory_lock_shared($1)", [family]);
191
- acquired.push([family, true]);
192
- await client.query("SELECT pg_advisory_lock($1)", [entity]);
193
- acquired.push([entity, false]);
194
- let cursor = await this.trimBoundary(client, entityId, keepMostRecent);
195
- let includeCursor = true;
196
- while (cursor !== undefined && this.#open) {
197
- cursor = await this.trimPage(client, entityId, cursor, includeCursor);
198
- includeCursor = false;
199
- // The next page starts only while this history remains open.
200
- }
201
- }
202
- catch (error) {
203
- operationFailure = error;
204
- throw error;
205
- }
206
- finally {
207
- await PostgresSessionLocks.cleanup(client, acquired.reverse(), operationFailure);
217
+ let cursor = await this.trimBoundary(client, entityId, keepMostRecent);
218
+ let includeCursor = true;
219
+ while (cursor !== undefined && this.#open) {
220
+ cursor = await this.trimPage(client, entityId, cursor, includeCursor);
221
+ includeCursor = false;
208
222
  }
209
223
  })
210
224
  .catch((error) => {
@@ -217,32 +231,27 @@ class PostgresStates {
217
231
  * @param olderThan Exclusive history cutoff.
218
232
  * @returns Completion after all matching pages are deleted.
219
233
  */
220
- async truncate(olderThan) {
234
+ truncate(olderThan) {
235
+ return this.onAdmit(() => this.truncateAdmitted(olderThan));
236
+ }
237
+ /**
238
+ * Deletes admitted Entity state history.
239
+ * @param olderThan Exclusive history cutoff.
240
+ * @returns Completion after all matching pages are deleted.
241
+ */
242
+ async truncateAdmitted(olderThan) {
221
243
  this.assertOpen();
222
- await this.#executor.using(async (client) => {
223
- const key = this.#executor.lock("history-family", this.#executor.table());
224
- await client.query("SELECT pg_advisory_lock($1)", [key]);
225
- let operationFailure;
226
- try {
227
- const cutoff = HistoryValues.nanos(olderThan);
228
- const highWater = await this.highWater(client, cutoff);
229
- if (highWater !== undefined)
230
- while (this.#open && (await this.truncatePage(client, cutoff, highWater))) {
231
- // The next page starts only while this history remains open.
232
- }
233
- }
234
- catch (error) {
235
- operationFailure = error;
236
- }
237
- const cleanup = await PostgresSessionLocks.unlock(client, key, false).catch((error) => error);
238
- if (cleanup instanceof Error)
239
- PostgresClientDisposal.mark(operationFailure ?? cleanup);
240
- if (operationFailure instanceof Error)
241
- throw operationFailure;
242
- if (operationFailure !== undefined)
243
- throw new PostgresStorageOperationError("PostgreSQL state history operation failed.");
244
- if (cleanup instanceof Error)
245
- throw cleanup;
244
+ await this.#executor
245
+ .using(async (client) => {
246
+ const cutoff = HistoryValues.nanos(olderThan);
247
+ const highWater = await this.highWater(client, cutoff);
248
+ if (highWater !== undefined)
249
+ while (this.#open && (await this.truncatePage(client, cutoff, highWater))) {
250
+ // Each completed page remains deleted if a later page fails.
251
+ }
252
+ })
253
+ .catch((error) => {
254
+ throw PostgresStorageErrors.operation(error);
246
255
  });
247
256
  }
248
257
  /**
@@ -302,49 +311,6 @@ class PostgresStates {
302
311
  entityValue(id) {
303
312
  return this.#executor.column("entity_id", this.input.id.pack(id));
304
313
  }
305
- /**
306
- * Calculates the lock key shared by this history family.
307
- *
308
- * @returns PostgreSQL advisory-lock key.
309
- */
310
- familyKey() {
311
- return this.#executor.lock("history-family", this.#executor.table());
312
- }
313
- /**
314
- * Calculates the family and Entity lock keys used while trimming.
315
- *
316
- * @param id Entity identifier.
317
- * @returns Family lock followed by Entity lock.
318
- */
319
- trimLocks(id) {
320
- return [
321
- this.familyKey(),
322
- this.#executor.lock("entity-mutation", this.input.sourceType.typeName, this.entityIdentity(id)),
323
- ];
324
- }
325
- /**
326
- * Calculates the mutation lock for a stored Entity record.
327
- *
328
- * @param entityId Packed Entity identifier from the record.
329
- * @returns PostgreSQL advisory-lock key.
330
- */
331
- entityKey(entityId) {
332
- if (entityId === undefined)
333
- throw new Error("State history requires EntityRecord.entityId.");
334
- const id = this.input.id.unpack(entityId);
335
- if (id === undefined)
336
- throw new Error("State history EntityRecord ID does not match storage.");
337
- return this.#executor.lock("entity-mutation", this.input.sourceType.typeName, this.entityIdentity(id));
338
- }
339
- /**
340
- * Returns the stable string identity used in Entity mutation locks.
341
- *
342
- * @param id Entity identifier.
343
- * @returns Stable Entity identity string.
344
- */
345
- entityIdentity(id) {
346
- return this.input.id.key(id);
347
- }
348
314
  /**
349
315
  * Rejects state-history work after this port has closed.
350
316
  */
@@ -355,58 +321,42 @@ class PostgresStates {
355
321
  /**
356
322
  * Deletes one bounded page of old states for an Entity.
357
323
  *
358
- * @param client PostgreSQL client holding the session locks.
324
+ * @param client Acquired PostgreSQL client.
359
325
  * @param id Entity identifier.
360
326
  * @param cursor Last retained or deleted history key.
361
327
  * @param includeCursor Whether the page includes the supplied cursor.
362
328
  * @returns The next page cursor, or `undefined` after the final page.
363
329
  */
364
330
  async trimPage(client, id, cursor, includeCursor) {
365
- await client.query("BEGIN");
366
- try {
367
- const rows = await client.query(this.trimSql(includeCursor), [
368
- this.entityValue(id),
369
- ...cursor,
370
- 128,
371
- ]);
372
- const keys = rows.rows.map(({ ID }) => ID);
373
- await this.deleteKeys(client, keys);
374
- await client.query("COMMIT");
375
- const last = rows.rows.at(-1);
376
- return keys.length === 128 && last !== undefined
377
- ? [last.version, last.created, last.ID]
378
- : undefined;
379
- }
380
- catch (error) {
381
- await client.query("ROLLBACK").catch(() => undefined);
382
- throw error;
383
- }
331
+ const rows = await client.query(this.trimSql(includeCursor), [
332
+ this.entityValue(id),
333
+ ...cursor,
334
+ 128,
335
+ ]);
336
+ const keys = rows.rows.map(({ ID }) => ID);
337
+ await this.deleteKeys(client, keys);
338
+ const last = rows.rows.at(-1);
339
+ return keys.length === 128 && last !== undefined
340
+ ? [last.version, last.created, last.ID]
341
+ : undefined;
384
342
  }
385
343
  /**
386
344
  * Deletes one bounded page below a timestamp and stable high-water key.
387
345
  *
388
- * @param client PostgreSQL client holding the family lock.
346
+ * @param client Acquired PostgreSQL client.
389
347
  * @param olderThan Exclusive timestamp cutoff in nanoseconds.
390
348
  * @param highWater Last key admitted when truncation began.
391
349
  * @returns Whether another full page may remain.
392
350
  */
393
351
  async truncatePage(client, olderThan, highWater) {
394
- await client.query("BEGIN");
395
- try {
396
- const keys = await this.keys(client, this.truncateSql(), [olderThan, ...highWater, 128]);
397
- await this.deleteKeys(client, keys);
398
- await client.query("COMMIT");
399
- return keys.length === 128;
400
- }
401
- catch (error) {
402
- await client.query("ROLLBACK").catch(() => undefined);
403
- throw error;
404
- }
352
+ const keys = await this.keys(client, this.truncateSql(), [olderThan, ...highWater, 128]);
353
+ await this.deleteKeys(client, keys);
354
+ return keys.length === 128;
405
355
  }
406
356
  /**
407
357
  * Reads record identifiers for one bounded deletion page.
408
358
  *
409
- * @param client PostgreSQL transaction client.
359
+ * @param client Acquired PostgreSQL client.
410
360
  * @param sql Parameterized key query.
411
361
  * @param values PostgreSQL bind values.
412
362
  * @returns Stored record identifiers.
@@ -417,7 +367,7 @@ class PostgresStates {
417
367
  /**
418
368
  * Deletes records identified by one bounded key page.
419
369
  *
420
- * @param client PostgreSQL transaction client.
370
+ * @param client Acquired PostgreSQL client.
421
371
  * @param keys Stored record identifiers.
422
372
  * @returns Completion after the deletion query.
423
373
  */
@@ -445,7 +395,7 @@ class PostgresStates {
445
395
  /**
446
396
  * Finds the first state-history key that trimming may delete.
447
397
  *
448
- * @param client PostgreSQL client holding the session locks.
398
+ * @param client Acquired PostgreSQL client.
449
399
  * @param id Entity identifier.
450
400
  * @param keep Number of newest states to retain.
451
401
  * @returns First deletable key, or `undefined` when nothing must be removed.
@@ -473,7 +423,7 @@ class PostgresStates {
473
423
  /**
474
424
  * Captures the last state key eligible when truncation begins.
475
425
  *
476
- * @param client PostgreSQL client holding the family lock.
426
+ * @param client Acquired PostgreSQL client.
477
427
  * @param olderThan Exclusive timestamp cutoff in nanoseconds.
478
428
  * @returns Stable high-water key, or `undefined` when nothing is eligible.
479
429
  */
@@ -495,6 +445,7 @@ class PostgresStates {
495
445
  class PostgresEvents {
496
446
  input;
497
447
  records;
448
+ onAdmit;
498
449
  #executor;
499
450
  #open = true;
500
451
  /**
@@ -502,10 +453,12 @@ class PostgresEvents {
502
453
  *
503
454
  * @param input Supplies Entity identity and state-schema definitions.
504
455
  * @param records Stores immutable Entity events.
456
+ * @param onAdmit Admits complete public event-history operations.
505
457
  */
506
- constructor(input, records) {
458
+ constructor(input, records, onAdmit) {
507
459
  this.input = input;
508
460
  this.records = records;
461
+ this.onAdmit = onAdmit;
509
462
  this.#executor = records.historyExecutor();
510
463
  }
511
464
  /**
@@ -515,15 +468,12 @@ class PostgresEvents {
515
468
  * @returns Completion after PostgreSQL stores the event.
516
469
  */
517
470
  append(record) {
518
- if (!this.#open)
519
- return Promise.reject(new Error("Entity event history is closed."));
520
- return this.#executor
521
- .transaction(async (client) => {
522
- await client.query("SELECT pg_advisory_xact_lock_shared($1)", [this.familyKey()]);
523
- await this.#executor.appendImmutable(client, record);
524
- })
525
- .catch((error) => {
526
- throw PostgresStorageErrors.operation(error);
471
+ return this.onAdmit(() => {
472
+ if (!this.#open)
473
+ return Promise.reject(new Error("Entity event history is closed."));
474
+ return this.records.writeImmutable(record).catch((error) => {
475
+ throw PostgresStorageErrors.operation(error);
476
+ });
527
477
  });
528
478
  }
529
479
  /**
@@ -534,12 +484,22 @@ class PostgresEvents {
534
484
  * @param version Optional inclusive starting version.
535
485
  * @returns Events in descending version order.
536
486
  */
537
- async backward(id, depth, version) {
487
+ backward(id, depth, version) {
488
+ return this.onAdmit(() => this.backwardAdmitted(id, depth, version));
489
+ }
490
+ /**
491
+ * Reads an admitted Entity event-history page.
492
+ * @param id Entity identifier.
493
+ * @param depth Maximum number of events to return.
494
+ * @param version Optional inclusive starting version.
495
+ * @returns Events in descending version order.
496
+ */
497
+ async backwardAdmitted(id, depth, version) {
538
498
  this.assertOpen();
539
499
  HistoryValues.assertDepth(depth);
540
500
  await this.#executor.prepare();
541
501
  return this.#executor
542
- .transaction((client) => this.#executor.query(client, this.backwardSql(version), this.backwardValues(id, depth, version)))
502
+ .using((client) => this.#executor.query(client, this.backwardSql(version), this.backwardValues(id, depth, version)))
543
503
  .catch((error) => {
544
504
  throw PostgresStorageErrors.operation(error);
545
505
  });
@@ -550,32 +510,27 @@ class PostgresEvents {
550
510
  * @param olderThan Exclusive history cutoff.
551
511
  * @returns Completion after all matching pages are deleted.
552
512
  */
553
- async truncate(olderThan) {
513
+ truncate(olderThan) {
514
+ return this.onAdmit(() => this.truncateAdmitted(olderThan));
515
+ }
516
+ /**
517
+ * Deletes admitted Entity event history.
518
+ * @param olderThan Exclusive history cutoff.
519
+ * @returns Completion after all matching pages are deleted.
520
+ */
521
+ async truncateAdmitted(olderThan) {
554
522
  this.assertOpen();
555
- await this.#executor.using(async (client) => {
556
- const key = this.familyKey();
557
- await client.query("SELECT pg_advisory_lock($1)", [key]);
558
- let operationFailure;
559
- try {
560
- const cutoff = HistoryValues.nanos(olderThan);
561
- const highWater = await this.highWater(client, cutoff);
562
- if (highWater !== undefined)
563
- while (this.#open && (await this.deletePage(client, cutoff, highWater))) {
564
- // The next page starts only while this history remains open.
565
- }
566
- }
567
- catch (error) {
568
- operationFailure = error;
569
- }
570
- const cleanup = await PostgresSessionLocks.unlock(client, key, false).catch((error) => error);
571
- if (cleanup instanceof Error)
572
- PostgresClientDisposal.mark(operationFailure ?? cleanup);
573
- if (operationFailure instanceof Error)
574
- throw operationFailure;
575
- if (operationFailure !== undefined)
576
- throw new PostgresStorageOperationError("PostgreSQL event history operation failed.");
577
- if (cleanup instanceof Error)
578
- throw cleanup;
523
+ await this.#executor
524
+ .using(async (client) => {
525
+ const cutoff = HistoryValues.nanos(olderThan);
526
+ const highWater = await this.highWater(client, cutoff);
527
+ if (highWater !== undefined)
528
+ while (this.#open && (await this.deletePage(client, cutoff, highWater))) {
529
+ // Each completed page remains deleted if a later page fails.
530
+ }
531
+ })
532
+ .catch((error) => {
533
+ throw PostgresStorageErrors.operation(error);
579
534
  });
580
535
  }
581
536
  /**
@@ -614,14 +569,6 @@ class PostgresEvents {
614
569
  const entity = this.#executor.column("entity_id", this.input.id.pack(id));
615
570
  return version === undefined ? [entity, depth] : [entity, version, depth];
616
571
  }
617
- /**
618
- * Calculates the lock key shared by this history family.
619
- *
620
- * @returns PostgreSQL advisory-lock key.
621
- */
622
- familyKey() {
623
- return this.#executor.lock("history-family", this.#executor.table());
624
- }
625
572
  /**
626
573
  * Rejects event-history work after this port has closed.
627
574
  */
@@ -632,7 +579,7 @@ class PostgresEvents {
632
579
  /**
633
580
  * Captures the last event key eligible when truncation begins.
634
581
  *
635
- * @param client PostgreSQL client holding the family lock.
582
+ * @param client Acquired PostgreSQL client.
636
583
  * @param olderThan Exclusive timestamp cutoff in nanoseconds.
637
584
  * @returns Stable high-water key, or `undefined` when nothing is eligible.
638
585
  */
@@ -644,28 +591,20 @@ class PostgresEvents {
644
591
  /**
645
592
  * Deletes one bounded page below a timestamp and stable high-water key.
646
593
  *
647
- * @param client PostgreSQL client holding the family lock.
594
+ * @param client Acquired PostgreSQL client.
648
595
  * @param olderThan Exclusive timestamp cutoff in nanoseconds.
649
596
  * @param highWater Last key admitted when truncation began.
650
597
  * @returns Whether another full page may remain.
651
598
  */
652
599
  async deletePage(client, olderThan, highWater) {
653
- await client.query("BEGIN");
654
- try {
655
- const keys = await this.keys(client, [olderThan, ...highWater, 128]);
656
- await this.deleteKeys(client, keys);
657
- await client.query("COMMIT");
658
- return keys.length === 128;
659
- }
660
- catch (error) {
661
- await client.query("ROLLBACK").catch(() => undefined);
662
- throw error;
663
- }
600
+ const keys = await this.keys(client, [olderThan, ...highWater, 128]);
601
+ await this.deleteKeys(client, keys);
602
+ return keys.length === 128;
664
603
  }
665
604
  /**
666
605
  * Reads event identifiers for one bounded deletion page.
667
606
  *
668
- * @param client PostgreSQL transaction client.
607
+ * @param client Acquired PostgreSQL client.
669
608
  * @param values PostgreSQL bind values.
670
609
  * @returns Stored event identifiers.
671
610
  */
@@ -677,7 +616,7 @@ class PostgresEvents {
677
616
  /**
678
617
  * Deletes events identified by one bounded key page.
679
618
  *
680
- * @param client PostgreSQL transaction client.
619
+ * @param client Acquired PostgreSQL client.
681
620
  * @param keys Stored event identifiers.
682
621
  * @returns Completion after the deletion query.
683
622
  */
@@ -742,49 +681,6 @@ const HistoryValues = Object.freeze({
742
681
  return value.seconds * 1000000000n + BigInt(value.nanos || 0);
743
682
  },
744
683
  });
745
- const PostgresSessionLocks = Object.freeze({
746
- /**
747
- * Clears all session locks and marks the client after a release failure.
748
- *
749
- * @param client PostgreSQL client holding the locks.
750
- * @param locks Lock keys paired with their shared/exclusive mode.
751
- * @param operationFailure Earlier operation failure, when present.
752
- * @returns Completion after all releases are attempted.
753
- */
754
- async cleanup(client, locks, operationFailure) {
755
- for (const [key, shared] of locks) {
756
- const cleanup = await this.unlock(client, key, shared).catch((error) => error);
757
- if (cleanup instanceof Error) {
758
- PostgresClientDisposal.mark(operationFailure ?? cleanup);
759
- if (operationFailure === undefined)
760
- throw cleanup;
761
- }
762
- }
763
- },
764
- /**
765
- * Clears one PostgreSQL advisory session lock.
766
- *
767
- * @param client PostgreSQL client holding the lock.
768
- * @param key Advisory-lock key.
769
- * @param shared Whether the held lock is shared.
770
- * @returns Completion after PostgreSQL confirms the release.
771
- */
772
- async unlock(client, key, shared) {
773
- try {
774
- const result = await client.query(shared ? "SELECT pg_advisory_unlock_shared($1)" : "SELECT pg_advisory_unlock($1)", [key]);
775
- const unlocked = shared
776
- ? result.rows[0]?.pg_advisory_unlock_shared
777
- : result.rows[0]?.pg_advisory_unlock;
778
- if (unlocked !== true)
779
- throw new PostgresStorageOperationError("PostgreSQL history cleanup failed.");
780
- }
781
- catch (error) {
782
- if (error instanceof PostgresStorageOperationError)
783
- throw error;
784
- throw new PostgresStorageOperationError("PostgreSQL history cleanup failed.");
785
- }
786
- },
787
- });
788
684
  /**
789
685
  * Adapts current Entity records to the provider Entity-storage contract.
790
686
  *
@@ -794,15 +690,18 @@ const PostgresSessionLocks = Object.freeze({
794
690
  class PostgresCurrentStorage {
795
691
  input;
796
692
  records;
693
+ onAdmit;
797
694
  /**
798
695
  * Creates current Entity storage around one record family.
799
696
  *
800
697
  * @param input Supplies materialized Entity columns.
801
698
  * @param records Stores current Entity records.
699
+ * @param onAdmit Admits complete public current-record operations.
802
700
  */
803
- constructor(input, records) {
701
+ constructor(input, records, onAdmit) {
804
702
  this.input = input;
805
703
  this.records = records;
704
+ this.onAdmit = onAdmit;
806
705
  }
807
706
  /**
808
707
  * Reads one current Entity record.
@@ -811,7 +710,7 @@ class PostgresCurrentStorage {
811
710
  * @returns The current record, or `undefined` when absent.
812
711
  */
813
712
  read(id) {
814
- return this.records.read(id);
713
+ return this.onAdmit(() => this.records.read(id));
815
714
  }
816
715
  /**
817
716
  * Writes one current Entity record.
@@ -820,7 +719,7 @@ class PostgresCurrentStorage {
820
719
  * @returns Completion after PostgreSQL stores the record.
821
720
  */
822
721
  write(record) {
823
- return this.records.write(record);
722
+ return this.onAdmit(() => this.records.write(record));
824
723
  }
825
724
  /**
826
725
  * Executes one normalized current-Entity query.
@@ -829,12 +728,14 @@ class PostgresCurrentStorage {
829
728
  * @returns Matching records with materialized column values.
830
729
  */
831
730
  async query(plan) {
832
- const entries = await this.records.queryPlanEntries(plan);
833
- return entries.map((entry) => ({
834
- id: entry.id,
835
- record: entry.record,
836
- columns: new Map(this.input.columns.map((column) => [column.name, column.valueIn(entry.record)])),
837
- }));
731
+ return this.onAdmit(async () => {
732
+ const entries = await this.records.queryPlanEntries(plan);
733
+ return entries.map((entry) => ({
734
+ id: entry.id,
735
+ record: entry.record,
736
+ columns: new Map(this.input.columns.map((column) => [column.name, column.valueIn(entry.record)])),
737
+ }));
738
+ });
838
739
  }
839
740
  }
840
741
  //# sourceMappingURL=entity-history.js.map