@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,62 +11,16 @@
|
|
|
11
11
|
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
12
|
* the License.
|
|
13
13
|
*/
|
|
14
|
-
import { toBinary } from "@bufbuild/protobuf";
|
|
14
|
+
import { clone, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { AgentHistoryEntrySchema, } from "@spine-event-engine/proto/agent";
|
|
15
16
|
import { EntityRecordSchema, } from "@spine-event-engine/proto/generated/spine/server/entity/entity_pb.js";
|
|
16
17
|
import {} from "@spine-event-engine/storage";
|
|
17
|
-
import { TenantBoundary, eventHistorySpec, eventStoreRecordSpec, stateHistorySpec, } from "@spine-event-engine/storage/provider";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { TenantBoundary, AgentHistoryRecords, AgentModelPreferences, EntityCommitSnapshots, eventHistorySpec, eventStoreRecordSpec, EventStoreGroups, stateHistorySpec, } from "@spine-event-engine/storage/provider";
|
|
19
|
+
import { AgentHistoryHash } from "./agent-history.js";
|
|
20
|
+
import { PostgresStorageErrors, PostgresStorageOperationError } from "./errors.js";
|
|
21
|
+
import { PostgresRecordStorage } from "./record-storage.js";
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
class PostgresEntityCommitCoordinator {
|
|
24
|
-
lifecycle;
|
|
25
|
-
/**
|
|
26
|
-
* Creates a coordinator for one tenant-bound record lifecycle.
|
|
27
|
-
*
|
|
28
|
-
* @param lifecycle Acquires transaction clients for the selected database.
|
|
29
|
-
*/
|
|
30
|
-
constructor(lifecycle) {
|
|
31
|
-
this.lifecycle = lifecycle;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Commits one complete Entity change with one serialization retry.
|
|
35
|
-
*
|
|
36
|
-
* @typeParam T Result returned by the transaction work.
|
|
37
|
-
* @param work Applies the Entity commit on an acquired client.
|
|
38
|
-
* @returns The committed work result.
|
|
39
|
-
*/
|
|
40
|
-
async commit(work) {
|
|
41
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
42
|
-
const client = await this.lifecycle.acquire();
|
|
43
|
-
let discard;
|
|
44
|
-
try {
|
|
45
|
-
await client.query("BEGIN");
|
|
46
|
-
const result = await work(client);
|
|
47
|
-
await client.query("COMMIT");
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
try {
|
|
52
|
-
await client.query("ROLLBACK");
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
discard = PostgresRollbackErrors.discard(error);
|
|
56
|
-
}
|
|
57
|
-
if (attempt === 0 && PostgresTransactionErrors.retryable(error))
|
|
58
|
-
continue;
|
|
59
|
-
throw PostgresCommitErrors.operation(error);
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
client.release(discard);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
throw new Error("Unreachable PostgreSQL Entity commit retry.");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Opens and closes the exact record families participating in one commit.
|
|
23
|
+
* Stores current and optional record families across commits on one handle.
|
|
70
24
|
*
|
|
71
25
|
* @typeParam I Entity identifier type.
|
|
72
26
|
* @typeParam S Entity state message type.
|
|
@@ -76,26 +30,81 @@ class PostgresCommitRecords {
|
|
|
76
30
|
states;
|
|
77
31
|
diagnostics;
|
|
78
32
|
events;
|
|
33
|
+
history;
|
|
34
|
+
preferences;
|
|
79
35
|
#handles;
|
|
80
36
|
/**
|
|
81
|
-
*
|
|
37
|
+
* Whether this record family is closed.
|
|
38
|
+
*/
|
|
39
|
+
#closed = false;
|
|
40
|
+
/**
|
|
41
|
+
* Acquires the current record family for one Entity scope.
|
|
82
42
|
*
|
|
83
|
-
* @param input Describes the Entity commit and enabled histories.
|
|
84
43
|
* @param open Opens one record family.
|
|
44
|
+
* @param entity The Entity persistence contract.
|
|
45
|
+
*/
|
|
46
|
+
constructor(entity, open) {
|
|
47
|
+
this.current = open(entity.recordSpec);
|
|
48
|
+
this.#handles = [this.current];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Acquires only missing requested families, rolling back a failed new group.
|
|
52
|
+
*
|
|
53
|
+
* @param input The captured Entity persistence input.
|
|
54
|
+
* @param open Opens a generated record family.
|
|
55
|
+
*/
|
|
56
|
+
ensure(input, open) {
|
|
57
|
+
const acquired = [];
|
|
58
|
+
try {
|
|
59
|
+
const states = input.states?.length
|
|
60
|
+
? (this.states ?? this.openState(input, open, acquired))
|
|
61
|
+
: undefined;
|
|
62
|
+
const diagnostics = input.diagnostics?.length
|
|
63
|
+
? (this.diagnostics ?? this.openDiagnostic(input, open, acquired))
|
|
64
|
+
: undefined;
|
|
65
|
+
const events = input.events?.length
|
|
66
|
+
? (this.events ?? this.openEvents(input, open, acquired))
|
|
67
|
+
: undefined;
|
|
68
|
+
this.states = states ?? this.states;
|
|
69
|
+
this.diagnostics = diagnostics ?? this.diagnostics;
|
|
70
|
+
this.events = events ?? this.events;
|
|
71
|
+
this.#handles.push(...acquired);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const failures = this.closeGroup(acquired);
|
|
75
|
+
if (failures.length)
|
|
76
|
+
throw new AggregateError([error, ...failures], "PostgreSQL family acquisition failed.");
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stores the two Agent record families, rolling back only failed acquisition.
|
|
82
|
+
* @param open Opens a private record family.
|
|
85
83
|
*/
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
ensureAgent(open) {
|
|
85
|
+
const acquired = [];
|
|
86
|
+
try {
|
|
87
|
+
const history = this.history ??
|
|
88
|
+
open(AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), AgentHistoryRecords.group);
|
|
89
|
+
if (this.history === undefined)
|
|
90
|
+
acquired.push(history);
|
|
91
|
+
const preferences = this.preferences ??
|
|
92
|
+
open(AgentModelPreferences.spec((value) => AgentHistoryHash.value(value)), AgentModelPreferences.group);
|
|
93
|
+
if (this.preferences === undefined)
|
|
94
|
+
acquired.push(preferences);
|
|
95
|
+
this.history = history;
|
|
96
|
+
this.preferences = preferences;
|
|
97
|
+
this.#handles.push(...acquired);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const failures = this.closeGroup(acquired);
|
|
101
|
+
if (failures.length)
|
|
102
|
+
throw new AggregateError([error, ...failures], "PostgreSQL Agent family acquisition failed.");
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
96
105
|
}
|
|
97
106
|
/**
|
|
98
|
-
* Prepares every record family before
|
|
107
|
+
* Prepares every record family before ordered writes begin.
|
|
99
108
|
*
|
|
100
109
|
* @returns Completion after all required tables are ready.
|
|
101
110
|
*/
|
|
@@ -104,51 +113,99 @@ class PostgresCommitRecords {
|
|
|
104
113
|
await handle.prepare();
|
|
105
114
|
}
|
|
106
115
|
/**
|
|
107
|
-
*
|
|
116
|
+
* Writes the fenced Entity mutation through the transaction's client.
|
|
117
|
+
*
|
|
118
|
+
* @param client Existing conditional-completion transaction client.
|
|
119
|
+
* @param snapshot Captured immutable Entity mutation.
|
|
120
|
+
* @param expectedVersion Version checked under the current-record lock.
|
|
121
|
+
* @returns Completion after current and history records are written.
|
|
122
|
+
*/
|
|
123
|
+
async applyConditional(client, snapshot, expectedVersion) {
|
|
124
|
+
const current = await this.current
|
|
125
|
+
.historyExecutor()
|
|
126
|
+
.read(client, snapshot.entityId, "for-update");
|
|
127
|
+
if ((current?.version?.number ?? 0) !== expectedVersion)
|
|
128
|
+
throw new Error("Agent execution initial Entity Version is no longer current.");
|
|
129
|
+
for (const state of snapshot.states ?? [])
|
|
130
|
+
await this.states?.historyExecutor().appendImmutable(client, state);
|
|
131
|
+
for (const diagnostic of snapshot.diagnostics ?? [])
|
|
132
|
+
await this.diagnostics?.historyExecutor().appendImmutable(client, diagnostic);
|
|
133
|
+
for (const event of snapshot.events ?? [])
|
|
134
|
+
await this.events?.historyExecutor().appendImmutable(client, event);
|
|
135
|
+
if (!PostgresCommitValues.same(current, snapshot.next))
|
|
136
|
+
await this.current.historyExecutor().write(client, snapshot.next);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Closes every retained record-family handle.
|
|
108
140
|
*/
|
|
109
141
|
close() {
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
if (this.#closed)
|
|
143
|
+
return;
|
|
144
|
+
this.#closed = true;
|
|
145
|
+
const failures = this.closeGroup(this.#handles);
|
|
146
|
+
if (failures.length === 1)
|
|
147
|
+
throw failures[0];
|
|
148
|
+
if (failures.length)
|
|
149
|
+
throw new AggregateError(failures, "PostgreSQL commit records close failed.");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Closes every newly acquired handle and collects cleanup failures.
|
|
153
|
+
*
|
|
154
|
+
* @param handles The acquired record handles.
|
|
155
|
+
* @returns Cleanup failures from acquired handles.
|
|
156
|
+
*/
|
|
157
|
+
closeGroup(handles) {
|
|
158
|
+
const failures = [];
|
|
159
|
+
for (const handle of handles) {
|
|
160
|
+
try {
|
|
161
|
+
handle.close();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
failures.push(error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return failures;
|
|
112
168
|
}
|
|
113
169
|
/**
|
|
114
|
-
*
|
|
170
|
+
* Acquires the Entity state-history record family.
|
|
115
171
|
*
|
|
116
172
|
* @param input Describes the Entity state schema.
|
|
117
173
|
* @param open Opens one record family.
|
|
118
174
|
* @param handles Collects handles that must close after the commit.
|
|
119
|
-
* @returns The state-history
|
|
175
|
+
* @returns The state-history record storage.
|
|
120
176
|
*/
|
|
121
177
|
openState(input, open, handles) {
|
|
122
178
|
const layout = stateHistorySpec(input.entity.stateSchema);
|
|
123
179
|
const handle = open(layout.spec, layout.group);
|
|
124
180
|
handles.push(handle);
|
|
125
|
-
return handle
|
|
181
|
+
return handle;
|
|
126
182
|
}
|
|
127
183
|
/**
|
|
128
|
-
*
|
|
184
|
+
* Acquires the Entity diagnostic-event history record family.
|
|
129
185
|
*
|
|
130
186
|
* @param input Describes the Entity state schema.
|
|
131
187
|
* @param open Opens one record family.
|
|
132
188
|
* @param handles Collects handles that must close after the commit.
|
|
133
|
-
* @returns The diagnostic-history
|
|
189
|
+
* @returns The diagnostic-history record storage.
|
|
134
190
|
*/
|
|
135
191
|
openDiagnostic(input, open, handles) {
|
|
136
192
|
const layout = eventHistorySpec(input.entity.stateSchema);
|
|
137
193
|
const handle = open(layout.spec, layout.group);
|
|
138
194
|
handles.push(handle);
|
|
139
|
-
return handle
|
|
195
|
+
return handle;
|
|
140
196
|
}
|
|
141
197
|
/**
|
|
142
|
-
*
|
|
198
|
+
* Acquires the shared event-store record family.
|
|
143
199
|
*
|
|
200
|
+
* @param input Supplies the context and Event family.
|
|
144
201
|
* @param open Opens one record family.
|
|
145
202
|
* @param handles Collects handles that must close after the commit.
|
|
146
|
-
* @returns The event-store
|
|
203
|
+
* @returns The event-store record storage.
|
|
147
204
|
*/
|
|
148
|
-
openEvents(open, handles) {
|
|
149
|
-
const handle = open(eventStoreRecordSpec);
|
|
205
|
+
openEvents(input, open, handles) {
|
|
206
|
+
const handle = open(eventStoreRecordSpec, EventStoreGroups.forContext(input.context.name));
|
|
150
207
|
handles.push(handle);
|
|
151
|
-
return handle
|
|
208
|
+
return handle;
|
|
152
209
|
}
|
|
153
210
|
}
|
|
154
211
|
/**
|
|
@@ -162,20 +219,24 @@ export class PostgresEntityCommitStorage {
|
|
|
162
219
|
open;
|
|
163
220
|
onClose;
|
|
164
221
|
#open = true;
|
|
165
|
-
#
|
|
222
|
+
#records;
|
|
223
|
+
/**
|
|
224
|
+
* Synchronous idle-close result, including any cleanup failure.
|
|
225
|
+
*/
|
|
226
|
+
#idleOutcome;
|
|
227
|
+
#active = new Set();
|
|
228
|
+
#closing;
|
|
166
229
|
/**
|
|
167
230
|
* Creates a PostgreSQL Entity commit handle for one Entity storage boundary.
|
|
168
231
|
*
|
|
169
232
|
* @param entity Defines the captured Entity source, state, and tenant scope.
|
|
170
|
-
* @param open Opens
|
|
171
|
-
* @param lifecycle Acquires transaction clients for the captured database.
|
|
233
|
+
* @param open Opens current and optional history record-family handles.
|
|
172
234
|
* @param onClose Removes this handle from the factory's live-handle set.
|
|
173
235
|
*/
|
|
174
|
-
constructor(entity, open,
|
|
236
|
+
constructor(entity, open, onClose) {
|
|
175
237
|
this.entity = entity;
|
|
176
238
|
this.open = open;
|
|
177
239
|
this.onClose = onClose;
|
|
178
|
-
this.#coordinator = new PostgresEntityCommitCoordinator(lifecycle);
|
|
179
240
|
}
|
|
180
241
|
/**
|
|
181
242
|
* Commits one current Entity record and its optional immutable records.
|
|
@@ -183,132 +244,337 @@ export class PostgresEntityCommitStorage {
|
|
|
183
244
|
* @typeParam Id Entity identifier type supplied by the commit.
|
|
184
245
|
* @typeParam State Entity state message type supplied by the commit.
|
|
185
246
|
* @param input Defines the next current record plus histories.
|
|
186
|
-
* @returns Completion after
|
|
247
|
+
* @returns Completion after every requested record write.
|
|
248
|
+
*/
|
|
249
|
+
commit(input) {
|
|
250
|
+
if (!this.#open)
|
|
251
|
+
return Promise.reject(new PostgresStorageOperationError("Entity commit storage is closed."));
|
|
252
|
+
return this.track(() => {
|
|
253
|
+
this.validate(input);
|
|
254
|
+
const prepared = EntityCommitSnapshots.capture(input);
|
|
255
|
+
return this.persist(prepared);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Registers a commit before invoking provider construction callbacks.
|
|
260
|
+
*
|
|
261
|
+
* @param action The admitted provider operation.
|
|
262
|
+
* @returns The admitted provider operation completion.
|
|
263
|
+
* @typeParam T Result type of the admitted provider operation.
|
|
187
264
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
265
|
+
track(action) {
|
|
266
|
+
let complete;
|
|
267
|
+
let fail;
|
|
268
|
+
const work = new Promise((resolve, reject) => {
|
|
269
|
+
complete = resolve;
|
|
270
|
+
fail = reject;
|
|
271
|
+
});
|
|
272
|
+
this.#active.add(work);
|
|
273
|
+
void work.then(() => this.#active.delete(work), () => this.#active.delete(work));
|
|
191
274
|
try {
|
|
192
|
-
|
|
193
|
-
await this.#coordinator.commit((client) => this.apply(client, input, records));
|
|
275
|
+
action().then(complete, fail);
|
|
194
276
|
}
|
|
195
|
-
|
|
196
|
-
|
|
277
|
+
catch (error) {
|
|
278
|
+
fail(error);
|
|
197
279
|
}
|
|
280
|
+
return work;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Reads one detached preference image through the retained private family.
|
|
284
|
+
* @param entityId Typed Agent identifier.
|
|
285
|
+
* @returns Current complete image or absence.
|
|
286
|
+
*/
|
|
287
|
+
readAgentPreferences(entityId) {
|
|
288
|
+
if (!this.#open)
|
|
289
|
+
return Promise.reject(new PostgresStorageOperationError("Entity commit storage is closed."));
|
|
290
|
+
return this.track(async () => {
|
|
291
|
+
const key = AgentModelPreferences.key(this.entity, entityId);
|
|
292
|
+
const records = this.agentRecords();
|
|
293
|
+
await records.prepare();
|
|
294
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(key))}`;
|
|
295
|
+
const storage = records.preferences;
|
|
296
|
+
if (storage === undefined)
|
|
297
|
+
throw new Error("Agent preference storage is unavailable.");
|
|
298
|
+
const found = await storage.read(slot);
|
|
299
|
+
if (found === undefined)
|
|
300
|
+
return undefined;
|
|
301
|
+
AgentModelPreferences.requireIdentity(found, key);
|
|
302
|
+
return AgentModelPreferences.copy(found.preferences);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Commits state, final audit, and complete preferences in one native transaction.
|
|
307
|
+
* @param input Captured Agent transition and initial images.
|
|
308
|
+
* @returns Completion after the conditional group commits.
|
|
309
|
+
* @typeParam Id Typed Agent identifier.
|
|
310
|
+
* @typeParam State Generated Agent state.
|
|
311
|
+
*/
|
|
312
|
+
commitAgent(input) {
|
|
313
|
+
if (!this.#open)
|
|
314
|
+
return Promise.reject(new PostgresStorageOperationError("Entity commit storage is closed."));
|
|
315
|
+
return this.track(async () => {
|
|
316
|
+
const id = this.entity.id.clone(input.entityId);
|
|
317
|
+
const key = AgentModelPreferences.key(this.entity, id);
|
|
318
|
+
const expected = input.expectedPreferences === undefined
|
|
319
|
+
? undefined
|
|
320
|
+
: AgentModelPreferences.copy(input.expectedPreferences);
|
|
321
|
+
const next = AgentModelPreferences.record(key, input.preferences);
|
|
322
|
+
const mutation = input.entityCommit && EntityCommitSnapshots.capture(input.entityCommit);
|
|
323
|
+
const history = input.historyEntries?.map((entry) => clone(AgentHistoryEntrySchema, entry)) ?? [];
|
|
324
|
+
this.validateAgent(id, input.expectedVersion, mutation);
|
|
325
|
+
const records = this.agentRecords();
|
|
326
|
+
if (mutation !== undefined)
|
|
327
|
+
records.ensure(mutation, this.open);
|
|
328
|
+
await records.prepare();
|
|
329
|
+
await this.commitAgentPrepared(records, id, input.expectedVersion, expected, next, mutation, history);
|
|
330
|
+
});
|
|
198
331
|
}
|
|
199
332
|
/**
|
|
200
|
-
*
|
|
333
|
+
* Acquires the retained current, Agent audit, and preference families.
|
|
201
334
|
*
|
|
202
|
-
* @
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
*/
|
|
207
|
-
async prepareConditional(input) {
|
|
208
|
-
this.validate(input);
|
|
209
|
-
const records = new PostgresCommitRecords(input, this.open);
|
|
335
|
+
* @returns The retained family group.
|
|
336
|
+
*/
|
|
337
|
+
agentRecords() {
|
|
338
|
+
const records = this.#records ?? new PostgresCommitRecords(this.entity, this.open);
|
|
210
339
|
try {
|
|
211
|
-
|
|
340
|
+
records.ensureAgent(this.open);
|
|
341
|
+
this.#records ??= records;
|
|
342
|
+
return records;
|
|
212
343
|
}
|
|
213
344
|
catch (error) {
|
|
214
|
-
records
|
|
345
|
+
if (this.#records === undefined) {
|
|
346
|
+
try {
|
|
347
|
+
records.close();
|
|
348
|
+
}
|
|
349
|
+
catch (cleanup) {
|
|
350
|
+
throw new AggregateError([error, cleanup], "PostgreSQL Agent family cleanup failed.");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
215
353
|
throw error;
|
|
216
354
|
}
|
|
217
|
-
return {
|
|
218
|
-
apply: (client, expectedVersion) => this.apply(client, input, records, expectedVersion),
|
|
219
|
-
close: () => {
|
|
220
|
-
records.close();
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
355
|
}
|
|
224
356
|
/**
|
|
225
|
-
*
|
|
357
|
+
* Checks the captured Entity scope and next version before native writes.
|
|
358
|
+
* @param id Captured Agent identifier.
|
|
359
|
+
* @param version Initial Entity version.
|
|
360
|
+
* @param mutation Optional prepared Entity mutation.
|
|
361
|
+
* @typeParam Id Typed Agent identifier.
|
|
362
|
+
* @typeParam State Generated Agent state.
|
|
226
363
|
*/
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
364
|
+
validateAgent(id, version, mutation) {
|
|
365
|
+
if (!Number.isSafeInteger(version) || version < 0)
|
|
366
|
+
throw new TypeError("Agent commit requires a valid initial Entity Version.");
|
|
367
|
+
if (mutation === undefined)
|
|
368
|
+
return;
|
|
369
|
+
this.validate(mutation);
|
|
370
|
+
if (this.entity.id.key(id) !== this.entity.id.key(mutation.entityId) ||
|
|
371
|
+
mutation.next.version?.number !== version + 1)
|
|
372
|
+
throw new Error("Agent commit requires the same Entity ID and one new Version.");
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Applies one transaction lock even when preference and current rows are absent.
|
|
376
|
+
* @param records Retained families.
|
|
377
|
+
* @param id Captured Agent ID.
|
|
378
|
+
* @param version Initial Entity version.
|
|
379
|
+
* @param expected Initial preference image.
|
|
380
|
+
* @param next Complete replacement record.
|
|
381
|
+
* @param mutation Optional Entity mutation.
|
|
382
|
+
* @param history Final audit entries.
|
|
383
|
+
* @typeParam Id Typed Agent identifier.
|
|
384
|
+
* @typeParam State Generated Agent state.
|
|
385
|
+
* @returns Completion after the conditional Agent transaction commits.
|
|
386
|
+
*/
|
|
387
|
+
async commitAgentPrepared(records, id, version, expected, next, mutation, history) {
|
|
388
|
+
const preferenceStorage = records.preferences;
|
|
389
|
+
const historyStorage = records.history;
|
|
390
|
+
if (preferenceStorage === undefined || historyStorage === undefined || next.key === undefined)
|
|
391
|
+
throw new Error("Agent commit storage is incomplete.");
|
|
392
|
+
const preferences = preferenceStorage.historyExecutor();
|
|
393
|
+
const key = next.key;
|
|
394
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(key))}`;
|
|
395
|
+
await preferences.transaction(async (client) => {
|
|
396
|
+
await client.query("SELECT pg_advisory_xact_lock($1)", [
|
|
397
|
+
preferences.lock("agent-preferences", slot),
|
|
398
|
+
]);
|
|
399
|
+
const prior = await preferences.read(client, slot, "for-update");
|
|
400
|
+
if (prior !== undefined)
|
|
401
|
+
AgentModelPreferences.requireIdentity(prior, key);
|
|
402
|
+
if (!AgentModelPreferences.same(prior?.preferences, expected))
|
|
403
|
+
throw new Error("Agent model preferences changed during the live turn.");
|
|
404
|
+
if (mutation === undefined) {
|
|
405
|
+
const current = await records.current.historyExecutor().read(client, id, "for-update");
|
|
406
|
+
if ((current?.version?.number ?? 0) !== version)
|
|
407
|
+
throw new Error("Agent initial Entity Version is no longer current.");
|
|
408
|
+
}
|
|
409
|
+
else
|
|
410
|
+
await records.applyConditional(client, mutation, version);
|
|
411
|
+
await this.appendAgentHistory(client, historyStorage, id, history);
|
|
412
|
+
await preferences.write(client, next);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Adds final Agent audit through the admitted transaction client.
|
|
417
|
+
* @param client Active native transaction.
|
|
418
|
+
* @param storage Retained Agent history family.
|
|
419
|
+
* @param id Captured Agent identifier.
|
|
420
|
+
* @param history Final audit entries.
|
|
421
|
+
* @returns Completion after final audit rows are appended.
|
|
422
|
+
*/
|
|
423
|
+
async appendAgentHistory(client, storage, id, history) {
|
|
424
|
+
const executor = storage.historyExecutor();
|
|
425
|
+
for (const entry of history)
|
|
426
|
+
await executor.appendImmutable(client, AgentHistoryRecords.record(this.entity.stateSchema.typeName, this.entity.id.key(id), entry));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Persists an admitted captured mutation, including a safe retry.
|
|
430
|
+
*
|
|
431
|
+
* @param prepared The immutable prepared mutation.
|
|
432
|
+
* @returns Completion after the captured mutation is persisted.
|
|
433
|
+
* @typeParam Id Entity identifier or record type.
|
|
434
|
+
* @typeParam State Generated Entity state message type.
|
|
435
|
+
*/
|
|
436
|
+
async persist(prepared) {
|
|
437
|
+
let records = this.#records;
|
|
438
|
+
let fresh = false;
|
|
439
|
+
try {
|
|
440
|
+
if (records === undefined) {
|
|
441
|
+
records = new PostgresCommitRecords(this.entity, this.open);
|
|
442
|
+
fresh = true;
|
|
443
|
+
}
|
|
444
|
+
records.ensure(prepared, this.open);
|
|
445
|
+
this.#records ??= records;
|
|
446
|
+
await records.prepare();
|
|
447
|
+
try {
|
|
448
|
+
await this.apply(prepared, records);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (!this.retryable(error))
|
|
452
|
+
throw error;
|
|
453
|
+
await this.apply(prepared, records);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (fresh && this.#records === undefined) {
|
|
458
|
+
try {
|
|
459
|
+
records?.close();
|
|
460
|
+
}
|
|
461
|
+
catch (cleanup) {
|
|
462
|
+
throw new AggregateError([error, cleanup], "PostgreSQL commit acquisition failed.");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw PostgresStorageErrors.operation(error);
|
|
231
466
|
}
|
|
232
467
|
}
|
|
233
468
|
/**
|
|
234
|
-
*
|
|
469
|
+
* Determines whether a driver serialization failure occurred through provider error causes.
|
|
235
470
|
*
|
|
236
|
-
* @
|
|
237
|
-
* @
|
|
238
|
-
* @param client PostgreSQL transaction client.
|
|
239
|
-
* @param input Defines the Entity records to commit.
|
|
240
|
-
* @param records Provides the prepared record families.
|
|
241
|
-
* @returns Completion after records are committed.
|
|
242
|
-
* @param expectedVersion Expected current Entity Version.
|
|
471
|
+
* @param error Supplies the operation failure.
|
|
472
|
+
* @returns Whether one captured-input retry is safe.
|
|
243
473
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
474
|
+
retryable(error) {
|
|
475
|
+
let current = error;
|
|
476
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
477
|
+
if (typeof current !== "object" || current === null)
|
|
478
|
+
return false;
|
|
479
|
+
const failure = current;
|
|
480
|
+
if (failure.code === "40P01" || failure.code === "40001")
|
|
481
|
+
return true;
|
|
482
|
+
current = failure.cause;
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
253
485
|
}
|
|
254
486
|
/**
|
|
255
|
-
*
|
|
487
|
+
* Closes this handle to new commits while allowing started work to settle.
|
|
256
488
|
*
|
|
257
|
-
* @
|
|
258
|
-
* @typeParam State Entity state message type supplied by the commit.
|
|
259
|
-
* @param client PostgreSQL transaction client.
|
|
260
|
-
* @param input Defines the Entity records to commit.
|
|
261
|
-
* @param records Provides the prepared record families.
|
|
262
|
-
* @returns Completion after all transaction locks are acquired.
|
|
489
|
+
* @returns Completion after admitted work and retained records close.
|
|
263
490
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
491
|
+
close() {
|
|
492
|
+
this.#open = false;
|
|
493
|
+
if (this.#idleOutcome !== undefined)
|
|
494
|
+
return (this.#closing ??=
|
|
495
|
+
this.#idleOutcome.error === undefined
|
|
496
|
+
? Promise.resolve()
|
|
497
|
+
: Promise.reject(this.#idleOutcome.error));
|
|
498
|
+
return (this.#closing ??= this.drain());
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Closes only when no commit remains admitted.
|
|
502
|
+
*/
|
|
503
|
+
closeIdle() {
|
|
504
|
+
if (this.#active.size)
|
|
505
|
+
throw new Error("Entity commit cannot close idle while work is active.");
|
|
506
|
+
if (this.#idleOutcome !== undefined) {
|
|
507
|
+
if (this.#idleOutcome.error !== undefined)
|
|
508
|
+
throw this.#idleOutcome.error;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (this.#closing !== undefined)
|
|
512
|
+
throw new Error("Entity commit close is in progress.");
|
|
513
|
+
this.#open = false;
|
|
514
|
+
const errors = [];
|
|
515
|
+
try {
|
|
516
|
+
this.#records?.close();
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
errors.push(error);
|
|
520
|
+
}
|
|
521
|
+
this.#records = undefined;
|
|
522
|
+
try {
|
|
523
|
+
this.onClose();
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
errors.push(error);
|
|
527
|
+
}
|
|
528
|
+
const failure = errors.length > 1 ? new AggregateError(errors) : errors[0];
|
|
529
|
+
this.#idleOutcome = failure === undefined ? {} : { error: failure };
|
|
530
|
+
if (failure !== undefined)
|
|
531
|
+
throw failure;
|
|
276
532
|
}
|
|
277
533
|
/**
|
|
278
|
-
*
|
|
534
|
+
* Waits for admitted commits before releasing factory tracking.
|
|
279
535
|
*
|
|
280
|
-
* @
|
|
281
|
-
* @typeParam State Entity state message type supplied by the commit.
|
|
282
|
-
* @param client PostgreSQL transaction client.
|
|
283
|
-
* @param input Defines the immutable records to verify.
|
|
284
|
-
* @param records Provides the prepared record families.
|
|
285
|
-
* @returns Completion after all immutable checks pass.
|
|
536
|
+
* @returns Completion after admitted work and cleanup settle.
|
|
286
537
|
*/
|
|
287
|
-
async
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
538
|
+
async drain() {
|
|
539
|
+
await Promise.allSettled([...this.#active]);
|
|
540
|
+
const errors = [];
|
|
541
|
+
try {
|
|
542
|
+
this.#records?.close();
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
errors.push(error);
|
|
546
|
+
}
|
|
547
|
+
this.#records = undefined;
|
|
548
|
+
try {
|
|
549
|
+
this.onClose();
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
errors.push(error);
|
|
553
|
+
}
|
|
554
|
+
if (errors.length === 1)
|
|
555
|
+
throw errors[0];
|
|
556
|
+
if (errors.length)
|
|
557
|
+
throw new AggregateError(errors, "PostgreSQL commit close failed.");
|
|
294
558
|
}
|
|
295
559
|
/**
|
|
296
|
-
*
|
|
560
|
+
* Applies one validated Entity commit as ordered independent writes.
|
|
297
561
|
*
|
|
298
562
|
* @typeParam Id Entity identifier type supplied by the commit.
|
|
299
563
|
* @typeParam State Entity state message type supplied by the commit.
|
|
300
|
-
* @param
|
|
301
|
-
* @param input Defines the immutable records to append.
|
|
564
|
+
* @param input Defines the Entity records to commit.
|
|
302
565
|
* @param records Provides the prepared record families.
|
|
303
|
-
* @returns Completion after
|
|
566
|
+
* @returns Completion after records are committed.
|
|
304
567
|
*/
|
|
305
|
-
async
|
|
568
|
+
async apply(input, records) {
|
|
306
569
|
for (const record of input.states ?? [])
|
|
307
|
-
await records.states?.
|
|
570
|
+
await records.states?.writeImmutable(record);
|
|
308
571
|
for (const record of input.diagnostics ?? [])
|
|
309
|
-
await records.diagnostics?.
|
|
572
|
+
await records.diagnostics?.writeImmutable(record);
|
|
310
573
|
for (const record of input.events ?? [])
|
|
311
|
-
await records.events?.
|
|
574
|
+
await records.events?.writeImmutable(record);
|
|
575
|
+
const current = await records.current.read(input.entityId);
|
|
576
|
+
if (!PostgresCommitValues.same(current, input.next))
|
|
577
|
+
await records.current.write(input.next);
|
|
312
578
|
}
|
|
313
579
|
/**
|
|
314
580
|
* Validates the commit against the Entity boundary captured by this handle.
|
|
@@ -318,8 +584,6 @@ export class PostgresEntityCommitStorage {
|
|
|
318
584
|
* @param input Defines the Entity records to validate.
|
|
319
585
|
*/
|
|
320
586
|
validate(input) {
|
|
321
|
-
if (!this.#open)
|
|
322
|
-
throw new PostgresStorageOperationError("Entity commit storage is closed.");
|
|
323
587
|
if (input.entity.sourceType.typeName !== this.entity.sourceType.typeName)
|
|
324
588
|
throw new PostgresStorageOperationError("Entity commit source type is incompatible.");
|
|
325
589
|
if (input.entity.stateSchema.typeName !== this.entity.stateSchema.typeName)
|
|
@@ -391,17 +655,4 @@ const PostgresCommitValues = Object.freeze({
|
|
|
391
655
|
return TenantBoundary.of(left).key === TenantBoundary.of(right).key;
|
|
392
656
|
},
|
|
393
657
|
});
|
|
394
|
-
const PostgresCommitErrors = Object.freeze({
|
|
395
|
-
/**
|
|
396
|
-
* Converts an internal commit failure to the stable provider error.
|
|
397
|
-
*
|
|
398
|
-
* @param error Failure raised during the Entity commit.
|
|
399
|
-
* @returns Stable public commit error.
|
|
400
|
-
*/
|
|
401
|
-
operation(error) {
|
|
402
|
-
return error instanceof PostgresStorageOperationError
|
|
403
|
-
? error
|
|
404
|
-
: new PostgresStorageOperationError("PostgreSQL Entity commit failed.");
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
658
|
//# sourceMappingURL=entity-commit.js.map
|