@spine-event-engine/storage-datastore 2.0.0-snapshot.24 → 2.0.0-snapshot.26
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 +22 -21
- package/REFERENCE.md +55 -50
- package/dist/datastore/agent-history.d.ts +36 -6
- package/dist/datastore/agent-history.d.ts.map +1 -1
- package/dist/datastore/agent-history.js +92 -6
- package/dist/datastore/agent-history.js.map +1 -1
- package/dist/datastore/column-mapping.d.ts.map +1 -1
- package/dist/datastore/column-mapping.js +6 -3
- package/dist/datastore/column-mapping.js.map +1 -1
- package/dist/datastore/entity-history.d.ts +197 -3
- package/dist/datastore/entity-history.d.ts.map +1 -1
- package/dist/datastore/entity-history.js +514 -33
- package/dist/datastore/entity-history.js.map +1 -1
- package/dist/datastore/id-column.d.ts +7 -0
- package/dist/datastore/id-column.d.ts.map +1 -1
- package/dist/datastore/id-column.js +27 -3
- package/dist/datastore/id-column.js.map +1 -1
- package/dist/datastore/record-kind.d.ts +8 -0
- package/dist/datastore/record-kind.d.ts.map +1 -0
- package/dist/datastore/record-kind.js +26 -0
- package/dist/datastore/record-kind.js.map +1 -0
- package/dist/datastore/record-storage.d.ts +11 -10
- package/dist/datastore/record-storage.d.ts.map +1 -1
- package/dist/datastore/record-storage.js +28 -28
- package/dist/datastore/record-storage.js.map +1 -1
- package/dist/datastore/storage-factory.d.ts +246 -40
- package/dist/datastore/storage-factory.d.ts.map +1 -1
- package/dist/datastore/storage-factory.js +672 -110
- package/dist/datastore/storage-factory.js.map +1 -1
- package/dist/datastore/tenant-catalog.d.ts +12 -25
- package/dist/datastore/tenant-catalog.d.ts.map +1 -1
- package/dist/datastore/tenant-catalog.js +43 -121
- package/dist/datastore/tenant-catalog.js.map +1 -1
- package/dist/datastore/transaction-rows.d.ts +30 -0
- package/dist/datastore/transaction-rows.d.ts.map +1 -0
- package/dist/datastore/transaction-rows.js +43 -0
- package/dist/datastore/transaction-rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.yaml +0 -9
- package/package.json +5 -6
- package/dist/datastore/agent-execution.d.ts +0 -337
- package/dist/datastore/agent-execution.d.ts.map +0 -1
- package/dist/datastore/agent-execution.js +0 -789
- package/dist/datastore/agent-execution.js.map +0 -1
|
@@ -11,15 +11,20 @@
|
|
|
11
11
|
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
12
|
* the License.
|
|
13
13
|
*/
|
|
14
|
-
import { clone,
|
|
14
|
+
import { clone, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { AgentHistoryEntrySchema, } from "@spine-event-engine/proto/agent";
|
|
16
|
+
import { AgentHistoryKeys, AgentHistoryRecords, AgentModelPreferences, } from "@spine-event-engine/storage/provider";
|
|
17
|
+
import { AnyPacker } from "@spine-event-engine/core";
|
|
15
18
|
import {} from "@bufbuild/protobuf/wkt";
|
|
16
19
|
import { EventSchema } from "@spine-event-engine/proto";
|
|
17
20
|
import { EntityRecordSchema, } from "@spine-event-engine/proto/generated/spine/server/entity/entity_pb.js";
|
|
18
|
-
import { eventStoreRecordSpec } from "@spine-event-engine/storage/provider";
|
|
21
|
+
import { eventStoreRecordSpec, EventStoreGroups } from "@spine-event-engine/storage/provider";
|
|
19
22
|
import { disabledEventHistoryPort, disabledStateHistoryPort, eventHistorySpec, stateHistorySpec, EntityCommitSnapshots, } from "@spine-event-engine/storage/provider";
|
|
20
23
|
import { RecordSpec } from "@spine-event-engine/storage";
|
|
21
24
|
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
22
25
|
import { DatastoreRecordStorage } from "./record-storage.js";
|
|
26
|
+
import { AgentHistoryHash } from "./agent-history.js";
|
|
27
|
+
import { NativeTransactions } from "./transaction-rows.js";
|
|
23
28
|
/**
|
|
24
29
|
* Groups Entity current and optional history record families.
|
|
25
30
|
*
|
|
@@ -69,8 +74,19 @@ export class DatastoreEntityStorage {
|
|
|
69
74
|
close() {
|
|
70
75
|
if (this.#open) {
|
|
71
76
|
this.#open = false;
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const errors = [];
|
|
78
|
+
for (const record of this.#records) {
|
|
79
|
+
try {
|
|
80
|
+
record.close();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
errors.push(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (errors.length === 1)
|
|
87
|
+
throw errors[0];
|
|
88
|
+
if (errors.length)
|
|
89
|
+
throw new AggregateError(errors, "Datastore Entity records close failed.");
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
/**
|
|
@@ -88,16 +104,34 @@ export class DatastoreEntityStorage {
|
|
|
88
104
|
export class DatastoreEntityCommitStorage {
|
|
89
105
|
input;
|
|
90
106
|
openRecords;
|
|
107
|
+
onClose;
|
|
91
108
|
#open = true;
|
|
109
|
+
#current;
|
|
110
|
+
/**
|
|
111
|
+
* Retained state-history records.
|
|
112
|
+
*/
|
|
113
|
+
#states;
|
|
114
|
+
#diagnostics;
|
|
115
|
+
#events;
|
|
116
|
+
#history;
|
|
117
|
+
#preferences;
|
|
118
|
+
/**
|
|
119
|
+
* Synchronous idle-close result, including any cleanup failure.
|
|
120
|
+
*/
|
|
121
|
+
#idleOutcome;
|
|
122
|
+
#active = new Set();
|
|
123
|
+
#closing;
|
|
92
124
|
/**
|
|
93
125
|
* Creates commit storage for one Entity persistence contract.
|
|
94
126
|
*
|
|
95
127
|
* @param input Defines the Entity source and tenant boundary.
|
|
96
128
|
* @param openRecords Opens each generated record family.
|
|
129
|
+
* @param onClose Releases factory tracking after terminal cleanup.
|
|
97
130
|
*/
|
|
98
|
-
constructor(input, openRecords) {
|
|
131
|
+
constructor(input, openRecords, onClose = () => undefined) {
|
|
99
132
|
this.input = input;
|
|
100
133
|
this.openRecords = openRecords;
|
|
134
|
+
this.onClose = onClose;
|
|
101
135
|
}
|
|
102
136
|
/**
|
|
103
137
|
* Writes histories and delivery Events before the current Entity record.
|
|
@@ -110,34 +144,388 @@ export class DatastoreEntityCommitStorage {
|
|
|
110
144
|
* @param input Supplies the prepared Entity mutation.
|
|
111
145
|
* @returns Completion after every requested record write.
|
|
112
146
|
*/
|
|
113
|
-
|
|
114
|
-
this
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
commit(input) {
|
|
148
|
+
if (!this.#open)
|
|
149
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
150
|
+
return this.track(() => {
|
|
151
|
+
this.validate(input);
|
|
152
|
+
const prepared = EntityCommitSnapshots.capture(input);
|
|
153
|
+
return this.persist(prepared);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Registers a commit before invoking provider construction callbacks.
|
|
158
|
+
*
|
|
159
|
+
* @param action The admitted provider operation.
|
|
160
|
+
* @returns The admitted provider operation completion.
|
|
161
|
+
* @typeParam T Result type of the admitted provider operation.
|
|
162
|
+
*/
|
|
163
|
+
track(action) {
|
|
164
|
+
let complete;
|
|
165
|
+
let fail;
|
|
166
|
+
const work = new Promise((resolve, reject) => {
|
|
167
|
+
complete = resolve;
|
|
168
|
+
fail = reject;
|
|
169
|
+
});
|
|
170
|
+
this.#active.add(work);
|
|
171
|
+
void work.then(() => this.#active.delete(work), () => this.#active.delete(work));
|
|
172
|
+
try {
|
|
173
|
+
action().then(complete, fail);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
fail(error);
|
|
177
|
+
}
|
|
178
|
+
return work;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reads a detached current preference image from native storage.
|
|
182
|
+
* @param entityId Typed Agent identifier.
|
|
183
|
+
* @returns Complete selections or absent record.
|
|
184
|
+
*/
|
|
185
|
+
readAgentPreferences(entityId) {
|
|
186
|
+
if (!this.#open)
|
|
187
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
188
|
+
return this.track(async () => {
|
|
189
|
+
const key = AgentModelPreferences.key(this.input, entityId);
|
|
190
|
+
const rows = this.agentRows();
|
|
191
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(key))}`;
|
|
192
|
+
const found = await rows.preferences.read(slot);
|
|
193
|
+
if (found === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
AgentModelPreferences.requireIdentity(found, key);
|
|
196
|
+
return AgentModelPreferences.copy(found.preferences);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Commits Agent state, final audit, and preferences in one native transaction.
|
|
201
|
+
* @param input Captured live Agent transition.
|
|
202
|
+
* @returns Completion after a conditional group commits.
|
|
203
|
+
* @typeParam I Typed Agent identifier.
|
|
204
|
+
* @typeParam S Generated Agent state.
|
|
205
|
+
*/
|
|
206
|
+
commitAgent(input) {
|
|
207
|
+
if (!this.#open)
|
|
208
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
209
|
+
return this.track(async () => {
|
|
210
|
+
const id = this.input.id.clone(input.entityId);
|
|
211
|
+
const key = AgentModelPreferences.key(this.input, id);
|
|
212
|
+
const expected = input.expectedPreferences === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: AgentModelPreferences.copy(input.expectedPreferences);
|
|
215
|
+
const next = AgentModelPreferences.record(key, input.preferences);
|
|
216
|
+
const mutation = input.entityCommit && EntityCommitSnapshots.capture(input.entityCommit);
|
|
217
|
+
const history = input.historyEntries?.map((entry) => clone(AgentHistoryEntrySchema, entry)) ?? [];
|
|
218
|
+
this.validateAgent(id, input.expectedVersion, mutation);
|
|
219
|
+
const rows = this.agentRows(mutation);
|
|
220
|
+
await this.agentTransaction(rows, id, input.expectedVersion, expected, next, mutation, history);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Acquires every missing native family before publishing retained references.
|
|
225
|
+
* @param mutation Optional Entity mutation requiring history families.
|
|
226
|
+
* @returns Native handles for one Agent commit.
|
|
227
|
+
*/
|
|
228
|
+
agentRows(mutation) {
|
|
229
|
+
const acquired = [];
|
|
230
|
+
try {
|
|
231
|
+
const rows = this.acquireAgentRows(mutation, acquired);
|
|
232
|
+
this.retainAgentRows(rows);
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const failures = this.closeGroup(acquired);
|
|
237
|
+
if (failures.length)
|
|
238
|
+
throw new AggregateError([error, ...failures], "Datastore Agent family acquisition failed.");
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Acquires only missing native Agent families without publishing partial acquisition.
|
|
244
|
+
* @param mutation Optional Entity mutation.
|
|
245
|
+
* @param acquired Collects newly opened handles for rollback.
|
|
246
|
+
* @returns Complete native family group.
|
|
247
|
+
*/
|
|
248
|
+
acquireAgentRows(mutation, acquired) {
|
|
249
|
+
const current = this.native(this.#current ?? this.openAgentRecord(this.input.recordSpec, undefined, acquired));
|
|
250
|
+
const { states, diagnostics, events } = this.agentMutationRows(mutation, acquired);
|
|
251
|
+
const history = this.#history ??
|
|
252
|
+
this.openAgentRecord(AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), AgentHistoryRecords.group, acquired);
|
|
253
|
+
const preferences = this.#preferences ??
|
|
254
|
+
this.openAgentRecord(AgentModelPreferences.spec((value) => AgentHistoryHash.value(value)), AgentModelPreferences.group, acquired);
|
|
255
|
+
return {
|
|
256
|
+
current,
|
|
257
|
+
states,
|
|
258
|
+
diagnostics,
|
|
259
|
+
events,
|
|
260
|
+
history,
|
|
261
|
+
preferences,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Acquires mutation families needed by the admitted Agent turn.
|
|
266
|
+
* @param mutation Optional changed Entity mutation.
|
|
267
|
+
* @param acquired Newly opened handles retained for rollback.
|
|
268
|
+
* @returns Optional mutation families.
|
|
269
|
+
*/
|
|
270
|
+
agentMutationRows(mutation, acquired) {
|
|
271
|
+
const stateLayout = stateHistorySpec(this.input.stateSchema);
|
|
272
|
+
const eventLayout = eventHistorySpec(this.input.stateSchema);
|
|
273
|
+
const states = mutation?.states?.length
|
|
274
|
+
? this.native(this.#states ?? this.openAgentRecord(stateLayout.spec, stateLayout.group, acquired))
|
|
275
|
+
: undefined;
|
|
276
|
+
const diagnostics = mutation?.diagnostics?.length
|
|
277
|
+
? this.native(this.#diagnostics ?? this.openAgentRecord(eventLayout.spec, eventLayout.group, acquired))
|
|
121
278
|
: undefined;
|
|
122
|
-
const
|
|
123
|
-
? this.
|
|
279
|
+
const events = mutation?.events?.length
|
|
280
|
+
? this.native(this.#events ??
|
|
281
|
+
this.openAgentRecord(eventStoreRecordSpec, EventStoreGroups.forContext(this.input.context.name), acquired))
|
|
124
282
|
: undefined;
|
|
125
|
-
|
|
283
|
+
return { states, diagnostics, events };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Acquires one native record family and records its rollback obligation.
|
|
287
|
+
* @param spec Declarative record layout.
|
|
288
|
+
* @param group Optional physical family.
|
|
289
|
+
* @param acquired Newly opened handles.
|
|
290
|
+
* @returns Native record family.
|
|
291
|
+
* @typeParam Id Typed record identifier.
|
|
292
|
+
* @typeParam R Generated record.
|
|
293
|
+
*/
|
|
294
|
+
openAgentRecord(spec, group, acquired) {
|
|
295
|
+
const port = this.openRecords(spec, group);
|
|
296
|
+
acquired.push(port);
|
|
297
|
+
return this.native(port);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Stores the complete acquired Agent group after every family succeeds.
|
|
301
|
+
* @param rows Complete native family group.
|
|
302
|
+
*/
|
|
303
|
+
retainAgentRows(rows) {
|
|
304
|
+
this.#current ??= rows.current;
|
|
305
|
+
this.#states ??= rows.states;
|
|
306
|
+
this.#diagnostics ??= rows.diagnostics;
|
|
307
|
+
this.#events ??= rows.events;
|
|
308
|
+
this.#history ??= rows.history;
|
|
309
|
+
this.#preferences ??= rows.preferences;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Checks the built-in native transaction capability.
|
|
313
|
+
* @param port Selected record family.
|
|
314
|
+
* @returns Native record family.
|
|
315
|
+
* @typeParam Id Typed record identifier.
|
|
316
|
+
* @typeParam R Generated record.
|
|
317
|
+
*/
|
|
318
|
+
native(port) {
|
|
319
|
+
if (!(port instanceof DatastoreRecordStorage))
|
|
320
|
+
throw new Error("Live Agent commit requires native Datastore transactions.");
|
|
321
|
+
return port;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Validates the Entity scope and exactly one next Version before I/O.
|
|
325
|
+
* @param id Captured Agent ID.
|
|
326
|
+
* @param version Initial Entity Version.
|
|
327
|
+
* @param mutation Optional prepared Entity mutation.
|
|
328
|
+
* @typeParam I Typed Entity identifier.
|
|
329
|
+
* @typeParam S Generated Entity state.
|
|
330
|
+
*/
|
|
331
|
+
validateAgent(id, version, mutation) {
|
|
332
|
+
if (!Number.isSafeInteger(version) || version < 0)
|
|
333
|
+
throw new TypeError("Agent commit requires a valid initial Entity Version.");
|
|
334
|
+
if (mutation === undefined)
|
|
335
|
+
return;
|
|
336
|
+
this.validate(mutation);
|
|
337
|
+
if (this.input.id.key(id) !== this.input.id.key(mutation.entityId) ||
|
|
338
|
+
mutation.next.version?.number !== version + 1)
|
|
339
|
+
throw new Error("Agent commit requires the same Entity ID and one new Version.");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Executes a captured Agent transition in one native transaction.
|
|
343
|
+
* @param rows Retained native families.
|
|
344
|
+
* @param id Captured Agent ID.
|
|
345
|
+
* @param version Initial Entity version.
|
|
346
|
+
* @param expected Initial preference image.
|
|
347
|
+
* @param next Complete replacement preferences.
|
|
348
|
+
* @param mutation Optional Entity mutation.
|
|
349
|
+
* @param history Final audit entries.
|
|
350
|
+
* @typeParam I Typed Entity identifier.
|
|
351
|
+
* @typeParam S Generated Entity state.
|
|
352
|
+
* @returns Completion after the native transaction settles.
|
|
353
|
+
*/
|
|
354
|
+
async agentTransaction(rows, id, version, expected, next, mutation, history) {
|
|
355
|
+
const tx = rows.preferences.transaction();
|
|
126
356
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
357
|
+
await tx.run();
|
|
358
|
+
const staged = [];
|
|
359
|
+
await this.agentLocked(tx, staged, rows, id, version, expected, next, mutation, history);
|
|
360
|
+
NativeTransactions.validateRows(staged);
|
|
361
|
+
for (const row of staged)
|
|
362
|
+
tx.save(row);
|
|
363
|
+
await tx.commit();
|
|
135
364
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
365
|
+
catch (error) {
|
|
366
|
+
await tx.rollback().catch(() => undefined);
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Checks both current images and stages the complete final group.
|
|
372
|
+
* @param tx Native transaction.
|
|
373
|
+
* @param staged Rows saved only after all checks pass.
|
|
374
|
+
* @param rows Retained native families.
|
|
375
|
+
* @param id Captured Agent identifier.
|
|
376
|
+
* @param version Initial Entity Version.
|
|
377
|
+
* @param expected Initial preference image.
|
|
378
|
+
* @param next Replacement preference record.
|
|
379
|
+
* @param mutation Optional Entity mutation.
|
|
380
|
+
* @param history Final audit entries.
|
|
381
|
+
* @typeParam I Typed Entity identifier.
|
|
382
|
+
* @typeParam S Generated Entity state.
|
|
383
|
+
* @returns Completion after all conditional rows are staged.
|
|
384
|
+
*/
|
|
385
|
+
async agentLocked(tx, staged, rows, id, version, expected, next, mutation, history) {
|
|
386
|
+
if (next.key === undefined)
|
|
387
|
+
throw new Error("Agent preference identity is missing.");
|
|
388
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(next.key))}`;
|
|
389
|
+
const prior = await this.agentRead(tx, rows.preferences, slot);
|
|
390
|
+
if (prior !== undefined)
|
|
391
|
+
AgentModelPreferences.requireIdentity(prior, next.key);
|
|
392
|
+
if (!AgentModelPreferences.same(prior?.preferences, expected))
|
|
393
|
+
throw new Error("Agent model preferences changed during the live turn.");
|
|
394
|
+
const current = await this.agentRead(tx, rows.current, id);
|
|
395
|
+
if ((current?.version?.number ?? 0) !== version)
|
|
396
|
+
throw new Error("Agent initial Entity Version is no longer current.");
|
|
397
|
+
for (const record of mutation?.states ?? [])
|
|
398
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.states), record);
|
|
399
|
+
for (const event of mutation?.diagnostics ?? [])
|
|
400
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.diagnostics), event);
|
|
401
|
+
for (const event of mutation?.events ?? [])
|
|
402
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.events), event);
|
|
403
|
+
if (mutation !== undefined)
|
|
404
|
+
staged.push(rows.current.transactionEntity(mutation.next));
|
|
405
|
+
await this.appendAgentAudit(tx, staged, rows.history, id, history);
|
|
406
|
+
staged.push(rows.preferences.transactionEntity(next));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Records final Agent audit rows in the admitted native transaction.
|
|
410
|
+
* @param history Final Agent audit entries.
|
|
411
|
+
* @param id Captured typed Agent identifier.
|
|
412
|
+
* @param staged Rows staged for atomic commit.
|
|
413
|
+
* @param storage Retained provider record family.
|
|
414
|
+
* @param tx Active native transaction.
|
|
415
|
+
* @returns Completion after final audit rows are staged.
|
|
416
|
+
*/
|
|
417
|
+
async appendAgentAudit(tx, staged, storage, id, history) {
|
|
418
|
+
for (const entry of history) {
|
|
419
|
+
if (AgentHistoryKeys.indexValue(AgentHistoryKeys.fromEntry(entry)).length > 1500)
|
|
420
|
+
throw new RangeError("Agent history order exceeds Datastore indexed-value capacity.");
|
|
421
|
+
const record = AgentHistoryRecords.record(this.input.stateSchema.typeName, this.input.id.key(id), entry);
|
|
422
|
+
await this.agentImmutable(tx, staged, storage, record);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Reads a native row within the Agent transaction.
|
|
427
|
+
* @param tx Active native transaction.
|
|
428
|
+
* @param storage Retained record family.
|
|
429
|
+
* @param id Typed record identifier.
|
|
430
|
+
* @returns Decoded row or absence.
|
|
431
|
+
* @typeParam Id Typed record identifier.
|
|
432
|
+
* @typeParam R Generated record.
|
|
433
|
+
*/
|
|
434
|
+
async agentRead(tx, storage, id) {
|
|
435
|
+
const raw = NativeTransactions.first((await tx.get(storage.transactionKey(id), { wrapNumbers: true })));
|
|
436
|
+
return raw === undefined ? undefined : storage.decodeTransactionEntity(raw);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Checks one immutable final record before staging it.
|
|
440
|
+
* @param tx Active native transaction.
|
|
441
|
+
* @param staged Collected native rows.
|
|
442
|
+
* @param storage Retained record family.
|
|
443
|
+
* @param record Proposed immutable record.
|
|
444
|
+
* @typeParam Id Typed record identifier.
|
|
445
|
+
* @typeParam R Generated record.
|
|
446
|
+
* @returns Completion after the immutable row is checked and staged.
|
|
447
|
+
*/
|
|
448
|
+
async agentImmutable(tx, staged, storage, record) {
|
|
449
|
+
const raw = NativeTransactions.first((await tx.get(storage.transactionEntity(record).key, { wrapNumbers: true })));
|
|
450
|
+
if (raw === undefined)
|
|
451
|
+
staged.push(storage.transactionEntity(record));
|
|
452
|
+
else if (!storage.matchesTransactionEntity(raw, record))
|
|
453
|
+
throw new Error("Immutable Agent history record has divergent content.");
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Checks an enabled native history family.
|
|
457
|
+
* @param storage Optional native family.
|
|
458
|
+
* @returns Enabled native record family.
|
|
459
|
+
* @typeParam Id Typed record ID.
|
|
460
|
+
* @typeParam R Generated record.
|
|
461
|
+
*/
|
|
462
|
+
requiredNative(storage) {
|
|
463
|
+
if (storage === undefined)
|
|
464
|
+
throw new Error("Agent Entity history storage is disabled.");
|
|
465
|
+
return storage;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Persists an admitted captured mutation and one safe retry.
|
|
469
|
+
*
|
|
470
|
+
* @param prepared The immutable prepared mutation.
|
|
471
|
+
* @returns Completion after the captured mutation is persisted.
|
|
472
|
+
* @typeParam I Entity identifier or record type.
|
|
473
|
+
* @typeParam S Generated Entity state message type.
|
|
474
|
+
*/
|
|
475
|
+
async persist(prepared) {
|
|
476
|
+
const [current, states, diagnostics, events] = this.openCommit(prepared);
|
|
477
|
+
try {
|
|
478
|
+
await this.apply(prepared, current, states, diagnostics, events);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
if (!this.retryable(error))
|
|
482
|
+
throw error;
|
|
483
|
+
await this.apply(prepared, current, states, diagnostics, events);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Acquires only missing requested families and rolls back a failed group.
|
|
488
|
+
*
|
|
489
|
+
* @param input The captured Entity persistence input.
|
|
490
|
+
* @returns The retained records required by this commit.
|
|
491
|
+
* @typeParam I Entity identifier or record type.
|
|
492
|
+
* @typeParam S Generated Entity state message type.
|
|
493
|
+
*/
|
|
494
|
+
openCommit(input) {
|
|
495
|
+
const acquired = [];
|
|
496
|
+
try {
|
|
497
|
+
const current = this.#current ?? this.openRecords(this.input.recordSpec);
|
|
498
|
+
if (this.#current === undefined)
|
|
499
|
+
acquired.push(current);
|
|
500
|
+
const stateLayout = stateHistorySpec(this.input.stateSchema);
|
|
501
|
+
const diagnosticLayout = eventHistorySpec(this.input.stateSchema);
|
|
502
|
+
const states = input.states?.length
|
|
503
|
+
? (this.#states ?? this.openRecords(stateLayout.spec, stateLayout.group))
|
|
504
|
+
: undefined;
|
|
505
|
+
if (states !== undefined && this.#states === undefined)
|
|
506
|
+
acquired.push(states);
|
|
507
|
+
const diagnostics = input.diagnostics?.length
|
|
508
|
+
? (this.#diagnostics ?? this.openRecords(diagnosticLayout.spec, diagnosticLayout.group))
|
|
509
|
+
: undefined;
|
|
510
|
+
if (diagnostics !== undefined && this.#diagnostics === undefined)
|
|
511
|
+
acquired.push(diagnostics);
|
|
512
|
+
const events = input.events?.length
|
|
513
|
+
? (this.#events ??
|
|
514
|
+
this.openRecords(eventStoreRecordSpec, EventStoreGroups.forContext(this.input.context.name)))
|
|
515
|
+
: undefined;
|
|
516
|
+
if (events !== undefined && this.#events === undefined)
|
|
517
|
+
acquired.push(events);
|
|
518
|
+
this.#current = current;
|
|
519
|
+
this.#states ??= states;
|
|
520
|
+
this.#diagnostics ??= diagnostics;
|
|
521
|
+
this.#events ??= events;
|
|
522
|
+
return [current, states, diagnostics, events];
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
const failures = this.closeGroup(acquired);
|
|
526
|
+
if (failures.length)
|
|
527
|
+
throw new AggregateError([error, ...failures], "Datastore family acquisition failed.");
|
|
528
|
+
throw error;
|
|
141
529
|
}
|
|
142
530
|
}
|
|
143
531
|
/**
|
|
@@ -187,9 +575,98 @@ export class DatastoreEntityCommitStorage {
|
|
|
187
575
|
}
|
|
188
576
|
/**
|
|
189
577
|
* Closes this commit storage to further commits.
|
|
578
|
+
*
|
|
579
|
+
* @returns Completion after admitted work and record cleanup settle.
|
|
190
580
|
*/
|
|
191
581
|
close() {
|
|
192
582
|
this.#open = false;
|
|
583
|
+
if (this.#idleOutcome !== undefined)
|
|
584
|
+
return (this.#closing ??=
|
|
585
|
+
this.#idleOutcome.error === undefined
|
|
586
|
+
? Promise.resolve()
|
|
587
|
+
: Promise.reject(this.#idleOutcome.error));
|
|
588
|
+
return (this.#closing ??= this.drain());
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Closes only when no commit remains admitted.
|
|
592
|
+
*/
|
|
593
|
+
closeIdle() {
|
|
594
|
+
this.assertIdle();
|
|
595
|
+
if (this.#idleOutcome !== undefined) {
|
|
596
|
+
if (this.#idleOutcome.error !== undefined)
|
|
597
|
+
throw this.#idleOutcome.error;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
this.#open = false;
|
|
601
|
+
const failure = this.closeRetained();
|
|
602
|
+
this.#idleOutcome = failure === undefined ? {} : { error: failure };
|
|
603
|
+
this.onClose();
|
|
604
|
+
if (failure !== undefined)
|
|
605
|
+
throw failure;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Rejects synchronous disposal while asynchronous work remains admitted.
|
|
609
|
+
*/
|
|
610
|
+
assertIdle() {
|
|
611
|
+
if (this.#active.size || (this.#closing !== undefined && this.#idleOutcome === undefined))
|
|
612
|
+
throw new Error("Entity commit cannot close idle while work is active.");
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Waits for every admitted commit before completing close.
|
|
616
|
+
*
|
|
617
|
+
* @returns Completion after admitted work and cleanup settle.
|
|
618
|
+
*/
|
|
619
|
+
async drain() {
|
|
620
|
+
await Promise.allSettled([...this.#active]);
|
|
621
|
+
const failure = this.closeRetained();
|
|
622
|
+
this.onClose();
|
|
623
|
+
if (failure !== undefined)
|
|
624
|
+
throw failure;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Closes every retained record once after admitted work settles.
|
|
628
|
+
*
|
|
629
|
+
* @returns A cleanup failure, if any.
|
|
630
|
+
*/
|
|
631
|
+
closeRetained() {
|
|
632
|
+
const ports = [
|
|
633
|
+
this.#events,
|
|
634
|
+
this.#diagnostics,
|
|
635
|
+
this.#states,
|
|
636
|
+
this.#history,
|
|
637
|
+
this.#preferences,
|
|
638
|
+
this.#current,
|
|
639
|
+
].filter((port) => port !== undefined);
|
|
640
|
+
this.#events = undefined;
|
|
641
|
+
this.#diagnostics = undefined;
|
|
642
|
+
this.#states = undefined;
|
|
643
|
+
this.#history = undefined;
|
|
644
|
+
this.#preferences = undefined;
|
|
645
|
+
this.#current = undefined;
|
|
646
|
+
const failures = this.closeGroup(ports);
|
|
647
|
+
if (failures.length === 1)
|
|
648
|
+
return failures[0];
|
|
649
|
+
return failures.length
|
|
650
|
+
? new AggregateError(failures, "Datastore commit close failed.")
|
|
651
|
+
: undefined;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Closes every newly acquired handle and collects cleanup failures.
|
|
655
|
+
*
|
|
656
|
+
* @param ports The acquired record handles.
|
|
657
|
+
* @returns Cleanup failures from acquired handles.
|
|
658
|
+
*/
|
|
659
|
+
closeGroup(ports) {
|
|
660
|
+
const failures = [];
|
|
661
|
+
for (const port of ports) {
|
|
662
|
+
try {
|
|
663
|
+
port.close();
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
failures.push(error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return failures;
|
|
193
670
|
}
|
|
194
671
|
/**
|
|
195
672
|
* Validates the Entity, tenant, history, and Event boundaries of a commit.
|
|
@@ -199,8 +676,6 @@ export class DatastoreEntityCommitStorage {
|
|
|
199
676
|
* @param input Supplies the proposed commit.
|
|
200
677
|
*/
|
|
201
678
|
validate(input) {
|
|
202
|
-
if (!this.#open)
|
|
203
|
-
throw new Error("Entity commit storage is closed.");
|
|
204
679
|
if (input.entity.sourceType.typeName !== this.input.sourceType.typeName ||
|
|
205
680
|
input.entity.stateSchema.typeName !== this.input.stateSchema.typeName ||
|
|
206
681
|
TenantBoundary.of(input.context).key !== TenantBoundary.of(this.input.context).key ||
|
|
@@ -336,7 +811,7 @@ class StateHistory {
|
|
|
336
811
|
const found = page.entries[0]?.record;
|
|
337
812
|
return found?.state === undefined
|
|
338
813
|
? undefined
|
|
339
|
-
:
|
|
814
|
+
: AnyPacker.unpack(found.state, this.input.stateSchema);
|
|
340
815
|
}
|
|
341
816
|
/**
|
|
342
817
|
* Stores no more than the newest state-history records for an Entity.
|
|
@@ -348,6 +823,8 @@ class StateHistory {
|
|
|
348
823
|
async trim(id, keep) {
|
|
349
824
|
this.requireOpen();
|
|
350
825
|
requireKeep(keep);
|
|
826
|
+
let retained = 0;
|
|
827
|
+
let cursor;
|
|
351
828
|
for (;;) {
|
|
352
829
|
const page = await this.provider().queryProviderPage({
|
|
353
830
|
filters: [entityFilter(this.input, id)],
|
|
@@ -356,11 +833,15 @@ class StateHistory {
|
|
|
356
833
|
{ property: "created", direction: "desc" },
|
|
357
834
|
],
|
|
358
835
|
limit: 128,
|
|
836
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
359
837
|
});
|
|
360
|
-
const
|
|
838
|
+
const retainHere = Math.min(page.entries.length, Math.max(0, keep - retained));
|
|
839
|
+
retained += retainHere;
|
|
840
|
+
const removable = page.entries.slice(retainHere);
|
|
361
841
|
await this.provider().deleteProviderEntries(removable);
|
|
362
842
|
if (!page.hasMore)
|
|
363
843
|
break;
|
|
844
|
+
cursor = page.cursor;
|
|
364
845
|
}
|
|
365
846
|
}
|
|
366
847
|
/**
|