@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.
- package/README.md +34 -16
- package/REFERENCE.md +66 -26
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/postgres/agent-history.d.ts +60 -18
- package/dist/postgres/agent-history.d.ts.map +1 -1
- package/dist/postgres/agent-history.js +114 -37
- package/dist/postgres/agent-history.js.map +1 -1
- package/dist/postgres/column-mapping.d.ts.map +1 -1
- package/dist/postgres/column-mapping.js +6 -3
- package/dist/postgres/column-mapping.js.map +1 -1
- package/dist/postgres/entity-commit.d.ts +90 -56
- package/dist/postgres/entity-commit.d.ts.map +1 -1
- package/dist/postgres/entity-commit.js +439 -188
- package/dist/postgres/entity-commit.js.map +1 -1
- package/dist/postgres/entity-history.d.ts +4 -2
- package/dist/postgres/entity-history.d.ts.map +1 -1
- package/dist/postgres/entity-history.js +163 -262
- package/dist/postgres/entity-history.js.map +1 -1
- package/dist/postgres/errors.d.ts +1 -1
- package/dist/postgres/errors.d.ts.map +1 -1
- package/dist/postgres/errors.js +5 -3
- package/dist/postgres/errors.js.map +1 -1
- package/dist/postgres/id-column.d.ts.map +1 -1
- package/dist/postgres/id-column.js +6 -3
- package/dist/postgres/id-column.js.map +1 -1
- package/dist/postgres/record-storage.d.ts +228 -117
- package/dist/postgres/record-storage.d.ts.map +1 -1
- package/dist/postgres/record-storage.js +350 -204
- package/dist/postgres/record-storage.js.map +1 -1
- package/dist/postgres/storage-factory.d.ts +267 -30
- package/dist/postgres/storage-factory.d.ts.map +1 -1
- package/dist/postgres/storage-factory.js +786 -145
- package/dist/postgres/storage-factory.js.map +1 -1
- package/dist/postgres/table-initializer.d.ts +7 -4
- package/dist/postgres/table-initializer.d.ts.map +1 -1
- package/dist/postgres/table-initializer.js +21 -15
- package/dist/postgres/table-initializer.js.map +1 -1
- package/dist/postgres/table-resolver.d.ts +15 -1
- package/dist/postgres/table-resolver.d.ts.map +1 -1
- package/dist/postgres/table-resolver.js +36 -7
- package/dist/postgres/table-resolver.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/dist/postgres/agent-execution.d.ts +0 -284
- package/dist/postgres/agent-execution.d.ts.map +0 -1
- package/dist/postgres/agent-execution.js +0 -648
- package/dist/postgres/agent-execution.js.map +0 -1
- package/dist/postgres/delivery-cleanup.d.ts +0 -76
- package/dist/postgres/delivery-cleanup.d.ts.map +0 -1
- package/dist/postgres/delivery-cleanup.js +0 -224
- 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 {
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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.
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
.
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
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
|
|
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
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|