@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.
Files changed (47) hide show
  1. package/README.md +22 -21
  2. package/REFERENCE.md +55 -50
  3. package/dist/datastore/agent-history.d.ts +36 -6
  4. package/dist/datastore/agent-history.d.ts.map +1 -1
  5. package/dist/datastore/agent-history.js +92 -6
  6. package/dist/datastore/agent-history.js.map +1 -1
  7. package/dist/datastore/column-mapping.d.ts.map +1 -1
  8. package/dist/datastore/column-mapping.js +6 -3
  9. package/dist/datastore/column-mapping.js.map +1 -1
  10. package/dist/datastore/entity-history.d.ts +197 -3
  11. package/dist/datastore/entity-history.d.ts.map +1 -1
  12. package/dist/datastore/entity-history.js +514 -33
  13. package/dist/datastore/entity-history.js.map +1 -1
  14. package/dist/datastore/id-column.d.ts +7 -0
  15. package/dist/datastore/id-column.d.ts.map +1 -1
  16. package/dist/datastore/id-column.js +27 -3
  17. package/dist/datastore/id-column.js.map +1 -1
  18. package/dist/datastore/record-kind.d.ts +8 -0
  19. package/dist/datastore/record-kind.d.ts.map +1 -0
  20. package/dist/datastore/record-kind.js +26 -0
  21. package/dist/datastore/record-kind.js.map +1 -0
  22. package/dist/datastore/record-storage.d.ts +11 -10
  23. package/dist/datastore/record-storage.d.ts.map +1 -1
  24. package/dist/datastore/record-storage.js +28 -28
  25. package/dist/datastore/record-storage.js.map +1 -1
  26. package/dist/datastore/storage-factory.d.ts +246 -40
  27. package/dist/datastore/storage-factory.d.ts.map +1 -1
  28. package/dist/datastore/storage-factory.js +672 -110
  29. package/dist/datastore/storage-factory.js.map +1 -1
  30. package/dist/datastore/tenant-catalog.d.ts +12 -25
  31. package/dist/datastore/tenant-catalog.d.ts.map +1 -1
  32. package/dist/datastore/tenant-catalog.js +43 -121
  33. package/dist/datastore/tenant-catalog.js.map +1 -1
  34. package/dist/datastore/transaction-rows.d.ts +30 -0
  35. package/dist/datastore/transaction-rows.d.ts.map +1 -0
  36. package/dist/datastore/transaction-rows.js +43 -0
  37. package/dist/datastore/transaction-rows.js.map +1 -0
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/index.yaml +0 -9
  43. package/package.json +5 -6
  44. package/dist/datastore/agent-execution.d.ts +0 -337
  45. package/dist/datastore/agent-execution.d.ts.map +0 -1
  46. package/dist/datastore/agent-execution.js +0 -789
  47. package/dist/datastore/agent-execution.js.map +0 -1
@@ -11,19 +11,18 @@
11
11
  * or implied. See the License for the specific language governing permissions and limitations under
12
12
  * the License.
13
13
  */
14
- import { Datastore } from "@google-cloud/datastore";
15
- import { StringifierRegistry } from "@spine-event-engine/core";
14
+ import { and, Datastore, PropertyFilter } from "@google-cloud/datastore";
15
+ import { defaultStringifierRegistry, StringifierRegistry } from "@spine-event-engine/core";
16
16
  import { RecordStorage, StorageFactory, } from "@spine-event-engine/storage";
17
- import {} from "@spine-event-engine/storage/provider";
18
- import { AgentExecutionRecords, AgentHistoryRecords, eventHistorySpec, stateHistorySpec, } from "@spine-event-engine/storage/provider";
19
- import { eventStoreRecordSpec } from "@spine-event-engine/storage/provider";
17
+ import { StoragePreparations, TenantBoundary, } from "@spine-event-engine/storage/provider";
18
+ import { AgentHistoryRecords, AgentModelPreferences, eventStoreRecordSpec, EventStoreGroups, eventHistorySpec, stateHistorySpec, } from "@spine-event-engine/storage/provider";
20
19
  import { EntityCommitStorageFactories } from "@spine-event-engine/storage/provider";
21
20
  import { AgentHistoryStorageFactories } from "@spine-event-engine/storage/provider";
22
- import { AgentExecutionStorageFactories } from "@spine-event-engine/storage/provider";
23
21
  import { DatastoreEntityCommitStorage, DatastoreEntityStorage, } from "./entity-history.js";
24
22
  import { DatastoreRecordStorage } from "./record-storage.js";
23
+ import { RecordKinds } from "./record-kind.js";
24
+ import { DatastoreIdColumn } from "./id-column.js";
25
25
  import { DatastoreAgentHistory, AgentHistoryHash } from "./agent-history.js";
26
- import { DatastoreAgentExecution } from "./agent-execution.js";
27
26
  import { DefaultNamespaceConverter, NamespaceAssignments, } from "./namespace.js";
28
27
  import { DatastoreTenantCatalog } from "./tenant-catalog.js";
29
28
  const maxClientSideScan = 1_000;
@@ -31,6 +30,12 @@ const maxClientSideScan = 1_000;
31
30
  * A Google Cloud Datastore-backed implementation of the Spine TS storage port.
32
31
  */
33
32
  export class DatastoreStorageFactory extends StorageFactory {
33
+ #commits = new Set();
34
+ #bundles = new Set();
35
+ /**
36
+ * Repeatable asynchronous close completion.
37
+ */
38
+ #closing;
34
39
  /**
35
40
  * Caller-supplied Datastore client.
36
41
  */
@@ -39,6 +44,10 @@ export class DatastoreStorageFactory extends StorageFactory {
39
44
  * Custom record creators indexed by source and record type.
40
45
  */
41
46
  #recordCreators;
47
+ /**
48
+ * Declarative custom record preparation callbacks by family.
49
+ */
50
+ #recordPreparers;
42
51
  /**
43
52
  * Physical layouts indexed by source and record type.
44
53
  */
@@ -47,6 +56,23 @@ export class DatastoreStorageFactory extends StorageFactory {
47
56
  * Custom Entity creators indexed by state type.
48
57
  */
49
58
  #entityCreators;
59
+ /**
60
+ * Declarative custom coherent Entity preparation callbacks by source type.
61
+ */
62
+ #entityPreparers;
63
+ /**
64
+ * Native physical definitions and successful or pending metadata checks.
65
+ */
66
+ #prepared = new Map();
67
+ #namespaces = new Map();
68
+ /**
69
+ * Complete admitted preparation and connection-check operations.
70
+ */
71
+ #work = new Set();
72
+ /**
73
+ * Remembered shared metadata check outcome while successful or in progress.
74
+ */
75
+ #connection;
50
76
  /**
51
77
  * Reversible tenant namespace assignment.
52
78
  */
@@ -55,6 +81,7 @@ export class DatastoreStorageFactory extends StorageFactory {
55
81
  * Message-valued ID and column conversion registry.
56
82
  */
57
83
  #stringifiers;
84
+ #eventStoreKinds;
58
85
  /**
59
86
  * Native tenant namespace catalog.
60
87
  */
@@ -65,130 +92,392 @@ export class DatastoreStorageFactory extends StorageFactory {
65
92
  * @returns A new factory builder.
66
93
  */
67
94
  static newBuilder() {
68
- return new Builder((client, recordCreators, layouts, entityCreators, namespaceConverter, stringifiers) => new DatastoreStorageFactory(client, recordCreators, layouts, entityCreators, namespaceConverter, stringifiers));
95
+ return new Builder((client, recordCreators, recordPreparers, layouts, entityCreators, entityPreparers, namespaceConverter, stringifiers, eventStoreKinds) => new DatastoreStorageFactory(client, recordCreators, recordPreparers, layouts, entityCreators, entityPreparers, namespaceConverter, stringifiers, eventStoreKinds));
69
96
  }
70
97
  /**
71
98
  * Creates a factory from resolved dependencies.
72
99
  *
73
100
  * @param client The caller-owned Datastore client.
74
101
  * @param recordCreators Custom record-storage providers by record family.
102
+ * @param recordPreparers Declarative preparation callbacks by record family.
75
103
  * @param layouts Physical record layouts by record family.
76
104
  * @param entityCreators Custom Entity persistence providers by Entity type.
105
+ * @param entityPreparers Declarative preparation callbacks by Entity type.
77
106
  * @param namespaceConverter Converts complete tenants to native namespaces.
78
107
  * @param stringifiers Converts message-valued IDs and columns.
108
+ * @param eventStoreKinds Explicit native kinds by Event Store identity.
79
109
  * @internal
80
110
  */
81
- constructor(client, recordCreators, layouts, entityCreators, namespaceConverter, stringifiers) {
111
+ constructor(client, recordCreators, recordPreparers, layouts, entityCreators, entityPreparers, namespaceConverter, stringifiers, eventStoreKinds) {
82
112
  super();
83
113
  this.#client = client;
84
114
  this.#recordCreators = recordCreators;
115
+ this.#recordPreparers = recordPreparers;
85
116
  this.#layouts = layouts;
86
117
  this.#entityCreators = entityCreators;
118
+ this.#entityPreparers = entityPreparers;
87
119
  this.#namespaceConverter = new NamespaceAssignments(namespaceConverter);
88
- this.#stringifiers = new StringifierRegistry(stringifiers);
120
+ const shared = stringifiers === defaultStringifierRegistry;
121
+ this.#stringifiers = shared ? stringifiers : new StringifierRegistry(stringifiers);
122
+ this.#eventStoreKinds = eventStoreKinds;
89
123
  this.#catalog = new DatastoreTenantCatalog(client, this.#namespaceConverter);
124
+ StoragePreparations.register(this, {
125
+ prepareRecord: (context, spec, group, mode) => this.prepareRecord(context, spec, group, mode),
126
+ prepareEntity: (input, mode) => this.prepareEntity(input, mode),
127
+ prepareAgentHistory: (input) => this.prepareAgentHistory(input),
128
+ prepareAgentCommit: (input, mode) => this.prepareAgentCommit(input, mode),
129
+ });
90
130
  EntityCommitStorageFactories.register(this, {
91
- createEntityCommitStorage: (input) => this.createEntityStorage(input).commits,
131
+ createEntityCommitStorage: (input) => this.createEntityCommitStorage(input),
92
132
  });
93
133
  this.registerAgentHistory();
94
- this.registerAgentExecution();
95
134
  }
96
135
  /**
97
- * Registers the required provider-only Agent history capability.
136
+ * Checks the shared namespace metadata with a bounded read-only query.
137
+ * @returns Completion after the metadata read; failures can be retried.
98
138
  */
99
- registerAgentHistory() {
100
- AgentHistoryStorageFactories.register(this, {
101
- createAgentHistoryStorage: (input) => {
102
- if (!this.isOpen())
103
- throw new Error("StorageFactory is closed.");
104
- return new DatastoreAgentHistory(input, new DatastoreRecordStorage(input.context, AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), this.#client, maxClientSideScan, AgentHistoryRecords.group, "spine_agent_history", this.#namespaceConverter, this.#stringifiers));
105
- },
139
+ checkConnection() {
140
+ if (!this.isOpen())
141
+ return Promise.reject(new Error("StorageFactory is closed."));
142
+ if (this.#connection !== undefined)
143
+ return this.#connection;
144
+ const pending = this.admit(() => this.probe(this.#client.namespace ?? ""));
145
+ this.#connection = pending;
146
+ void pending.catch(() => {
147
+ if (this.#connection === pending)
148
+ this.#connection = undefined;
106
149
  });
150
+ return pending;
107
151
  }
108
152
  /**
109
- * Registers the native transactional Agent execution capability.
153
+ * Performs one exact-key metadata read for a native namespace.
154
+ * @param namespace Native namespace to inspect.
155
+ * @returns Completion after the metadata query.
110
156
  */
111
- registerAgentExecution() {
112
- AgentExecutionStorageFactories.register(this, {
113
- createAgentExecutionStorage: (input) => this.createAgentExecution(input),
114
- });
157
+ async probe(namespace) {
158
+ const key = this.#client.key({ namespace: "", path: ["__namespace__", namespace || 1] });
159
+ const query = this.#client
160
+ .createQuery("", "__namespace__")
161
+ .select("__key__")
162
+ .filter(and([new PropertyFilter("__key__", ">=", key), new PropertyFilter("__key__", "<=", key)]));
163
+ await this.#client.runQuery(query);
115
164
  }
116
165
  /**
117
- * Opens one Agent execution handle after factory lifecycle validation.
118
- *
119
- * @param input Requested fenced execution change.
120
- * @returns Tenant-scoped Agent execution storage handle.
121
- * @typeParam I Typed entity identifier.
122
- * @typeParam S Generated Entity state.
166
+ * Admits asynchronous physical preparation before invoking callbacks.
167
+ * @param operation Work to perform within the factory lifetime.
168
+ * @returns Completion or rejection from the admitted work.
123
169
  */
124
- createAgentExecution(input) {
170
+ admit(operation) {
125
171
  if (!this.isOpen())
126
- throw new Error("StorageFactory is closed.");
127
- return new DatastoreAgentExecution(input, this.executionRows(input));
172
+ return Promise.reject(new Error("StorageFactory is closed."));
173
+ const pending = Promise.resolve().then(operation);
174
+ this.#work.add(pending);
175
+ void pending.finally(() => this.#work.delete(pending)).catch(() => undefined);
176
+ return pending;
128
177
  }
129
178
  /**
130
- * Opens the fixed execution kinds and existing Entity record families.
131
- *
132
- * @param input Requested fenced execution change.
133
- * @returns Tenant-scoped native record handles.
134
- * @typeParam I Typed entity identifier.
135
- * @typeParam S Generated Entity state.
179
+ * Tracks a history batch before synchronously capturing caller-supplied values.
180
+ * @param operation Complete immutable audit group.
181
+ * @returns The admitted group outcome.
136
182
  */
137
- executionRows(input) {
138
- const context = input.entity.context;
139
- const digest = (value) => AgentHistoryHash.value(value);
140
- return {
141
- invocation: this.executionRecord(context, AgentExecutionRecords.invocationSpec(digest), AgentExecutionRecords.invocationGroup, "spine_agent_execution"),
142
- head: this.executionRecord(context, AgentExecutionRecords.headSpec(digest), AgentExecutionRecords.headGroup, "spine_agent_execution_head"),
143
- history: this.executionRecord(context, AgentHistoryRecords.spec(digest), AgentHistoryRecords.group, "spine_agent_history"),
144
- current: this.executionRecord(context, input.entity.recordSpec),
145
- ...this.optionalExecutionRows(input),
146
- events: this.executionRecord(context, eventStoreRecordSpec),
147
- };
183
+ admitHistory = (operation) => {
184
+ if (!this.isOpen())
185
+ return Promise.reject(new Error("StorageFactory is closed."));
186
+ const { promise, resolve, reject } = Promise.withResolvers();
187
+ this.#work.add(promise);
188
+ void promise.finally(() => this.#work.delete(promise)).catch(() => undefined);
189
+ try {
190
+ void operation().then(() => {
191
+ resolve(undefined);
192
+ }, reject);
193
+ }
194
+ catch (error) {
195
+ reject(error);
196
+ }
197
+ return promise;
198
+ };
199
+ /**
200
+ * Prepares a declared record family without opening a storage handle.
201
+ * @typeParam I Record identifier type.
202
+ * @typeParam R Stored record type.
203
+ * @param context Captured tenant context.
204
+ * @param spec The physical record declaration.
205
+ * @param group Optional history family.
206
+ * @param mode Creation or read-only checking.
207
+ * @returns Completion after native or custom preparation.
208
+ */
209
+ prepareRecord(context, spec, group, mode) {
210
+ return this.admit(() => this.prepareRecordAdmitted(context, spec, group, mode, true));
148
211
  }
149
212
  /**
150
- * Opens optional existing Entity state and diagnostic record families.
151
- *
152
- * @param input Typed Agent repository and history configuration.
153
- * @returns Optional native state and diagnostic handles.
213
+ * Prepares a family within an existing factory admission.
214
+ * @typeParam I Record identifier type.
215
+ * @typeParam R Stored record type.
216
+ * @param context Captured tenant context.
217
+ * @param spec The physical record declaration.
218
+ * @param group Optional history family.
219
+ * @param mode Creation or read-only checking.
220
+ * @param allowCustom Whether ordinary custom creators are selected.
221
+ * @returns Completion after compatible preparation.
222
+ */
223
+ prepareRecordAdmitted(context, spec, group, mode, allowCustom) {
224
+ const resolved = this.resolve(spec, group, allowCustom);
225
+ if (resolved.creator !== undefined) {
226
+ const prepare = resolved.prepare;
227
+ if (prepare === undefined)
228
+ return Promise.reject(new Error("Custom Datastore record preparation is required."));
229
+ return prepare(context, spec, this.#client, mode);
230
+ }
231
+ return this.prepareNative(context, spec, group, resolved.layout?.kind);
232
+ }
233
+ /**
234
+ * Prepares current Entity and enabled history declarations without storage handles.
235
+ * @typeParam I Entity identifier type.
236
+ * @typeParam S Entity state type.
237
+ * @param input The declarative Entity family.
238
+ * @param mode Creation or read-only checking.
239
+ * @returns Completion after all requested families are checked.
240
+ */
241
+ prepareEntity(input, mode) {
242
+ return this.admit(async () => {
243
+ const custom = this.#entityCreators.get(input.sourceType.typeName);
244
+ if (custom !== undefined) {
245
+ const prepare = this.#entityPreparers.get(input.sourceType.typeName);
246
+ if (prepare === undefined)
247
+ throw new Error("Custom Datastore Entity preparation is required.");
248
+ await prepare(input, this.#client, mode);
249
+ return;
250
+ }
251
+ if (this.columnSignature(input.columns) !== this.columnSignature(input.recordSpec.columns))
252
+ throw new Error("Datastore Entity query columns differ from current records.");
253
+ await this.prepareRecordAdmitted(input.context, input.recordSpec, undefined, mode, false);
254
+ if (input.stateHistory) {
255
+ const history = stateHistorySpec(input.stateSchema);
256
+ await this.prepareRecordAdmitted(input.context, history.spec, history.group, mode, false);
257
+ }
258
+ if (input.eventHistory) {
259
+ const history = eventHistorySpec(input.stateSchema);
260
+ await this.prepareRecordAdmitted(input.context, history.spec, history.group, mode, false);
261
+ }
262
+ await this.prepareRecordAdmitted(input.context, eventStoreRecordSpec, EventStoreGroups.forContext(input.context.name), mode, false);
263
+ });
264
+ }
265
+ /**
266
+ * Prepares the surviving Agent audit family.
267
+ * @typeParam I Agent identifier type.
268
+ * @param input Agent state and complete tenant context.
269
+ * @returns Completion after audit record compatibility is checked.
270
+ */
271
+ prepareAgentHistory(input) {
272
+ return this.admit(() => {
273
+ TenantBoundary.of(input.context);
274
+ if (!input.stateType.trim())
275
+ throw new TypeError("Agent state type is required.");
276
+ const spec = AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value));
277
+ return this.prepareNative(input.context, spec, AgentHistoryRecords.group, "spine_agent_history");
278
+ });
279
+ }
280
+ /**
281
+ * Creates native live Agent current, audit, and preference requirements.
282
+ * @param input Declarative Agent Entity layout.
283
+ * @param mode Creation or read-only metadata check.
284
+ * @returns Completion after native definitions are compatible.
154
285
  * @typeParam I Typed Agent identifier.
155
286
  * @typeParam S Generated Agent state.
156
287
  */
157
- optionalExecutionRows(input) {
158
- const context = input.entity.context;
159
- const state = input.entity.stateHistory
160
- ? stateHistorySpec(input.entity.stateSchema)
161
- : undefined;
162
- const diagnostic = input.entity.eventHistory
163
- ? eventHistorySpec(input.entity.stateSchema)
164
- : undefined;
165
- return {
166
- ...(state === undefined
167
- ? {}
168
- : {
169
- states: this.executionRecord(context, state.spec, state.group),
170
- }),
171
- ...(diagnostic === undefined
172
- ? {}
173
- : {
174
- diagnostics: this.executionRecord(context, diagnostic.spec, diagnostic.group),
175
- }),
288
+ prepareAgentCommit(input, mode) {
289
+ return this.admit(async () => {
290
+ if (this.#entityCreators.has(input.sourceType.typeName))
291
+ throw new Error("Custom Datastore Entity storage does not support native Agent transactions.");
292
+ if (this.columnSignature(input.columns) !== this.columnSignature(input.recordSpec.columns))
293
+ throw new Error("Datastore Entity query columns differ from current records.");
294
+ await this.prepareRecordAdmitted(input.context, input.recordSpec, undefined, mode, false);
295
+ if (input.stateHistory) {
296
+ const layout = stateHistorySpec(input.stateSchema);
297
+ await this.prepareRecordAdmitted(input.context, layout.spec, layout.group, mode, false);
298
+ }
299
+ if (input.eventHistory) {
300
+ const layout = eventHistorySpec(input.stateSchema);
301
+ await this.prepareRecordAdmitted(input.context, layout.spec, layout.group, mode, false);
302
+ }
303
+ await this.prepareRecordAdmitted(input.context, eventStoreRecordSpec, EventStoreGroups.forContext(input.context.name), mode, false);
304
+ await this.prepareAgentMetadata(input.context);
305
+ });
306
+ }
307
+ /**
308
+ * Checks the two native Agent audit families after current Entity declarations.
309
+ * @param context Complete tenant context.
310
+ * @returns Completion after both families are accessible.
311
+ */
312
+ async prepareAgentMetadata(context) {
313
+ await this.prepareNative(context, AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), AgentHistoryRecords.group, "spine_agent_history");
314
+ await this.prepareNative(context, AgentModelPreferences.spec((value) => AgentHistoryHash.value(value)), AgentModelPreferences.group, "spine_agent_model_preferences");
315
+ }
316
+ /**
317
+ * Checks one native physical definition and its namespace read access.
318
+ * @typeParam I Record identifier type.
319
+ * @typeParam R Stored record type.
320
+ * @param context Complete tenant context.
321
+ * @param spec The declared record family.
322
+ * @param group Optional physical group.
323
+ * @param explicitKind Configured or fixed native kind.
324
+ * @returns Completion after a compatible namespace read.
325
+ */
326
+ prepareNative(context, spec, group, explicitKind) {
327
+ const { namespace, kind, signature } = this.nativeDefinition(context, spec, group, explicitKind);
328
+ const family = JSON.stringify([namespace, kind]);
329
+ const prior = this.#prepared.get(family);
330
+ if (prior !== undefined && prior.signature !== signature)
331
+ throw new Error(`Datastore kind "${kind}" has an incompatible record definition.`);
332
+ if (prior?.ready)
333
+ return Promise.resolve();
334
+ if (prior?.pending !== undefined)
335
+ return prior.pending;
336
+ const entry = prior ?? {
337
+ signature,
338
+ ready: false,
339
+ declared: false,
340
+ pending: undefined,
176
341
  };
342
+ if (prior === undefined)
343
+ this.#prepared.set(family, entry);
344
+ return this.probePrepared(namespace, family, entry);
345
+ }
346
+ /**
347
+ * Checks one retained definition and records only successful readiness.
348
+ * @param namespace Native namespace to inspect.
349
+ * @param family Canonical physical family key.
350
+ * @param entry Existing compatible physical declaration.
351
+ * @returns Completion after the read-only metadata check.
352
+ */
353
+ probePrepared(namespace, family, entry) {
354
+ const pending = this.probe(namespace).then(() => {
355
+ entry.ready = true;
356
+ entry.pending = undefined;
357
+ }, (error) => {
358
+ entry.pending = undefined;
359
+ if (!entry.declared && this.#prepared.get(family) === entry)
360
+ this.#prepared.delete(family);
361
+ throw error;
362
+ });
363
+ entry.pending = pending;
364
+ return pending;
365
+ }
366
+ /**
367
+ * Records a successful native handle's pure layout for later compatibility checks.
368
+ * @typeParam I Record identifier type.
369
+ * @typeParam R Stored record type.
370
+ * @param context Complete tenant context.
371
+ * @param spec The declared record family.
372
+ * @param group Optional physical group.
373
+ * @param explicitKind Configured or fixed native kind.
374
+ * @param mapping Actual native mapping from an opened handle.
375
+ */
376
+ declareNative(context, spec, group, explicitKind, mapping) {
377
+ const { namespace, kind, signature } = this.nativeDefinition(context, spec, group, explicitKind, mapping);
378
+ const family = JSON.stringify([namespace, kind]);
379
+ const prior = this.#prepared.get(family);
380
+ if (prior !== undefined && prior.signature !== signature)
381
+ throw new Error(`Datastore kind "${kind}" has an incompatible record definition.`);
382
+ if (prior === undefined)
383
+ this.#prepared.set(family, { signature, ready: false, declared: true, pending: undefined });
384
+ else
385
+ prior.declared = true;
177
386
  }
178
387
  /**
179
- * Resolves one native kind through the existing record layout.
388
+ * Finds the pure native namespace, kind, and record layout declaration.
389
+ * @typeParam I Record identifier type.
390
+ * @typeParam R Stored record type.
391
+ * @param context Complete tenant context.
392
+ * @param spec The declared record family.
393
+ * @param group Optional physical group.
394
+ * @param explicitKind Configured or fixed native kind.
395
+ * @param mapping Actual native mapping from an opened handle.
396
+ * @returns Canonical physical identity and signature.
397
+ */
398
+ nativeDefinition(context, spec, group, explicitKind, mapping) {
399
+ const namespace = mapping === undefined ? this.nativeNamespace(context) : (mapping.namespace ?? "");
400
+ const kind = mapping?.kind ?? this.selectedKind(spec, group, explicitKind);
401
+ if (!kind.trim() || Buffer.byteLength(kind, "utf8") > 1_500)
402
+ throw new Error("Datastore record kind must be non-blank and at most 1,500 bytes.");
403
+ new DatastoreIdColumn(spec.idType, this.#stringifiers);
404
+ if (typeof spec.idType !== "string")
405
+ this.#stringifiers.forMessage(spec.idType);
406
+ for (const column of spec.columns)
407
+ if (column.type.kind === "message")
408
+ this.#stringifiers.forMessage(column.type.message);
409
+ const signature = JSON.stringify([
410
+ spec.sourceType.typeName,
411
+ group === undefined ? null : (EventStoreGroups.identity(group) ?? group.name),
412
+ spec.recordType.typeName,
413
+ typeof spec.idType === "string" ? spec.idType : spec.idType.typeName,
414
+ this.columnSignature(spec.columns),
415
+ ]);
416
+ return { namespace, kind, signature };
417
+ }
418
+ /**
419
+ * Returns a configured or default native record kind.
180
420
  *
181
- * @param context Captured event storage context.
182
- * @param fixedKind Fixed native Datastore kind.
183
- * @param group Fixed storage group.
184
- * @param spec Typed record layout.
185
- * @returns Typed native record handle.
186
- * @typeParam I Typed entity identifier.
187
- * @typeParam R Typed persisted record.
421
+ * @typeParam I Record identifier type.
422
+ * @typeParam R Stored record message type.
423
+ * @param spec Declared record family.
424
+ * @param group Optional physical group.
425
+ * @param explicitKind Kind from a record layout.
426
+ * @returns The selected native kind.
427
+ */
428
+ selectedKind(spec, group, explicitKind) {
429
+ const identity = group === undefined ? undefined : EventStoreGroups.identity(group);
430
+ return ((identity === undefined ? undefined : this.#eventStoreKinds.get(identity)) ??
431
+ explicitKind ??
432
+ RecordKinds.of(spec.sourceType.typeName, spec.recordType.typeName, group));
433
+ }
434
+ /**
435
+ * Converts one complete tenant boundary to its native namespace.
436
+ * @param context Complete tenant context.
437
+ * @returns Its native Datastore namespace.
438
+ */
439
+ nativeNamespace(context) {
440
+ const boundary = TenantBoundary.of(context);
441
+ if (boundary.single)
442
+ return this.#client.namespace ?? "";
443
+ const cached = this.#namespaces.get(boundary.key);
444
+ if (cached !== undefined)
445
+ return cached;
446
+ const namespace = this.#namespaceConverter.toNamespace(boundary.tenantId);
447
+ this.#namespaces.set(boundary.key, namespace);
448
+ return namespace;
449
+ }
450
+ /**
451
+ * Captures physical column declarations without evaluating record callbacks.
452
+ * @typeParam R Stored record type.
453
+ * @param columns Declared query columns.
454
+ * @returns Stable physical column signature.
188
455
  */
189
- executionRecord(context, spec, group, fixedKind) {
190
- const kind = fixedKind ?? this.resolve(spec, group, false).layout?.kind;
191
- return new DatastoreRecordStorage(context, spec, this.#client, maxClientSideScan, group, kind, this.#namespaceConverter, this.#stringifiers);
456
+ columnSignature(columns) {
457
+ return JSON.stringify(columns
458
+ .map(({ name, type }) => [
459
+ name,
460
+ type.kind,
461
+ type.kind === "scalar"
462
+ ? `${String(type.scalar)}:${String(type.longAsString)}`
463
+ : type.kind === "enum"
464
+ ? type.enum.typeName
465
+ : type.message.typeName,
466
+ ])
467
+ .sort(([first], [second]) => String(first).localeCompare(String(second))));
468
+ }
469
+ /**
470
+ * Registers the required provider-only Agent history capability.
471
+ */
472
+ registerAgentHistory() {
473
+ AgentHistoryStorageFactories.register(this, {
474
+ createAgentHistoryStorage: (input) => {
475
+ if (!this.isOpen())
476
+ throw new Error("StorageFactory is closed.");
477
+ const spec = AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value));
478
+ return new DatastoreAgentHistory(input, this.openNative(input.context, spec, AgentHistoryRecords.group, "spine_agent_history"), this.admitHistory);
479
+ },
480
+ });
192
481
  }
193
482
  /**
194
483
  * Creates the selected Entity provider bundle.
@@ -203,11 +492,104 @@ export class DatastoreStorageFactory extends StorageFactory {
203
492
  throw new Error("StorageFactory is closed.");
204
493
  const custom = this.#entityCreators.get(input.sourceType.typeName);
205
494
  if (custom !== undefined)
206
- return custom(input, this.#client);
495
+ return this.retainBundle(custom(input, this.#client));
207
496
  const openRecords = (recordSpec, group) => this.createEntityRecordStorage(input.context, recordSpec, group);
208
497
  const storage = new DatastoreEntityStorage(input, openRecords);
209
498
  const commits = new DatastoreEntityCommitStorage(input, openRecords);
210
- return new DefaultEntityHandle(storage, commits);
499
+ return this.retainBundle(new DefaultEntityHandle(storage, commits));
500
+ }
501
+ /**
502
+ * Registers a coherent Entity bundle for factory shutdown.
503
+ *
504
+ * @param bundle The coherent Entity storage bundle.
505
+ * @returns The tracked Entity storage bundle.
506
+ * @typeParam I Entity identifier or record type.
507
+ * @typeParam S Generated Entity state message type.
508
+ */
509
+ retainBundle(bundle) {
510
+ const retained = {
511
+ current: bundle.current,
512
+ states: bundle.states,
513
+ events: bundle.events,
514
+ commits: bundle.commits,
515
+ isOpen: () => bundle.isOpen(),
516
+ ...this.retainCleanup(bundle, () => this.#bundles.delete(retained)),
517
+ };
518
+ this.#bundles.add(retained);
519
+ return retained;
520
+ }
521
+ /**
522
+ * Acquires direct built-in commit access without an unused Entity read bundle.
523
+ *
524
+ * @param input The captured Entity persistence input.
525
+ * @returns The provider commit handle.
526
+ * @typeParam I Entity identifier or record type.
527
+ * @typeParam S Generated Entity state message type.
528
+ */
529
+ createEntityCommitStorage(input) {
530
+ if (!this.isOpen())
531
+ throw new Error("StorageFactory is closed.");
532
+ const custom = this.#entityCreators.get(input.sourceType.typeName);
533
+ if (custom !== undefined)
534
+ return this.createCustomCommitStorage(input, custom);
535
+ const openRecords = (recordSpec, group) => this.createEntityRecordStorage(input.context, recordSpec, group);
536
+ const commits = new DatastoreEntityCommitStorage(input, openRecords, () => this.#commits.delete(commits));
537
+ this.#commits.add(commits);
538
+ return commits;
539
+ }
540
+ /**
541
+ * Wraps a custom bundle while retaining its complete cleanup obligation.
542
+ * @param custom Registered custom Entity storage creator.
543
+ * @param input Captured Entity storage layout.
544
+ * @typeParam I Typed Entity identifier.
545
+ * @typeParam S Generated Entity state.
546
+ * @returns The adapted custom commit handle.
547
+ */
548
+ createCustomCommitStorage(input, custom) {
549
+ const bundle = custom(input, this.#client);
550
+ const unsupported = () => Promise.reject(new Error("Custom Datastore Entity storage does not support native Agent transactions."));
551
+ const adapter = {
552
+ commit: (work) => bundle.commits.commit(work),
553
+ readAgentPreferences: unsupported,
554
+ commitAgent: unsupported,
555
+ ...this.retainCleanup(bundle, () => this.#commits.delete(adapter)),
556
+ };
557
+ this.#commits.add(adapter);
558
+ return adapter;
559
+ }
560
+ /**
561
+ * Creates repeatable cleanup methods that retain a bundle until closure settles.
562
+ *
563
+ * @param bundle The complete provider bundle.
564
+ * @param onRemove Removes the settled factory root.
565
+ * @returns Repeatable close methods.
566
+ */
567
+ retainCleanup(bundle, onRemove) {
568
+ let closing;
569
+ return {
570
+ close: () => {
571
+ if (closing !== undefined)
572
+ return closing;
573
+ try {
574
+ closing = bundle.close();
575
+ }
576
+ catch (error) {
577
+ closing = Promise.reject(error instanceof Error
578
+ ? error
579
+ : new Error("Datastore cleanup failed.", { cause: error }));
580
+ }
581
+ return (closing = closing.then(() => {
582
+ onRemove();
583
+ }, (error) => {
584
+ onRemove();
585
+ throw error;
586
+ }));
587
+ },
588
+ closeIdle: () => {
589
+ bundle.closeIdle();
590
+ onRemove();
591
+ },
592
+ };
211
593
  }
212
594
  /**
213
595
  * Creates provider-specific Datastore record storage.
@@ -224,10 +606,10 @@ export class DatastoreStorageFactory extends StorageFactory {
224
606
  const creator = resolved.creator;
225
607
  if (creator !== undefined)
226
608
  return creator(context, recordSpec, this.#client, maxClientSideScan);
227
- return new DatastoreRecordStorage(context, recordSpec, this.#client, maxClientSideScan, group, resolved.layout?.kind, this.#namespaceConverter, this.#stringifiers);
609
+ return this.openNative(context, recordSpec, group, resolved.layout?.kind);
228
610
  }
229
611
  /**
230
- * Opens the built-in record provider for generated Entity families.
612
+ * Acquires the built-in record provider for generated Entity families.
231
613
  *
232
614
  * @typeParam I Record identifier type.
233
615
  * @typeParam R Stored record message type.
@@ -237,11 +619,35 @@ export class DatastoreStorageFactory extends StorageFactory {
237
619
  * @returns A built-in Datastore record handle.
238
620
  */
239
621
  createEntityRecordStorage(context, recordSpec, group) {
622
+ if (group?.name === AgentHistoryRecords.group.name)
623
+ return this.openNative(context, recordSpec, group, "spine_agent_history");
624
+ if (group?.name === AgentModelPreferences.group.name)
625
+ return this.openNative(context, recordSpec, group, "spine_agent_model_preferences");
240
626
  const resolved = this.resolve(recordSpec, group, false);
241
- return new DatastoreRecordStorage(context, recordSpec, this.#client, maxClientSideScan, group, resolved.layout?.kind, this.#namespaceConverter, this.#stringifiers);
627
+ return this.openNative(context, recordSpec, group, resolved.layout?.kind);
628
+ }
629
+ /**
630
+ * Acquires one native record handle and retains its pure physical definition.
631
+ * @typeParam I Record identifier type.
632
+ * @typeParam R Stored record type.
633
+ * @param context Complete tenant context.
634
+ * @param spec The declared record family.
635
+ * @param group Optional physical group.
636
+ * @param kind Configured or fixed native kind.
637
+ * @returns The native record handle.
638
+ */
639
+ openNative(context, spec, group, kind) {
640
+ const selectedKind = this.selectedKind(spec, group, kind);
641
+ const mapping = this.nativeDefinition(context, spec, group, selectedKind);
642
+ this.declareNative(context, spec, group, selectedKind, {
643
+ namespace: mapping.namespace,
644
+ kind: mapping.kind,
645
+ });
646
+ const storage = new DatastoreRecordStorage(context, spec, this.#client, maxClientSideScan, group, selectedKind, this.#namespaceConverter, this.#stringifiers, mapping.namespace);
647
+ return storage;
242
648
  }
243
649
  /**
244
- * Resolves the registered creator and physical layout for a record family.
650
+ * Finds the registered creator and physical layout for a record family.
245
651
  *
246
652
  * @typeParam I Record identifier type.
247
653
  * @typeParam R Stored record message type.
@@ -255,10 +661,14 @@ export class DatastoreStorageFactory extends StorageFactory {
255
661
  const record = recordSpec.recordType.typeName;
256
662
  const exact = key(group?.name ?? source, record);
257
663
  const fallback = key(group === undefined ? source : record, group === undefined ? source : record);
664
+ const selected = includeCreator
665
+ ? this.#recordCreators.has(exact)
666
+ ? exact
667
+ : fallback
668
+ : undefined;
258
669
  return {
259
- creator: includeCreator
260
- ? (this.#recordCreators.get(exact) ?? this.#recordCreators.get(fallback))
261
- : undefined,
670
+ creator: selected === undefined ? undefined : this.#recordCreators.get(selected),
671
+ prepare: selected === undefined ? undefined : this.#recordPreparers.get(selected),
262
672
  layout: this.#layouts.get(exact) ?? this.#layouts.get(fallback),
263
673
  };
264
674
  }
@@ -272,16 +682,54 @@ export class DatastoreStorageFactory extends StorageFactory {
272
682
  }
273
683
  /**
274
684
  * Closes the factory and its native tenant catalog.
685
+ *
686
+ * @returns Completion after retained resources are closed.
275
687
  */
276
688
  close() {
277
- super.close();
278
- void this.#catalog.close();
689
+ this.fenceClose();
690
+ if (this.#closing !== undefined)
691
+ return this.#closing;
692
+ const closing = Promise.withResolvers();
693
+ this.#closing = closing.promise;
694
+ this.drain().then(() => {
695
+ closing.resolve(undefined);
696
+ }, closing.reject);
697
+ return closing.promise;
698
+ }
699
+ /**
700
+ * Waits for admitted commits before closing remaining factory resources.
701
+ *
702
+ * @returns Completion after admitted work and cleanup settle.
703
+ */
704
+ async drain() {
705
+ const errors = [];
706
+ await Promise.allSettled([...this.#work]);
707
+ const roots = [...this.#commits, ...this.#bundles];
708
+ const pending = roots.map((handle) => {
709
+ try {
710
+ return handle.close();
711
+ }
712
+ catch (error) {
713
+ return Promise.reject(error instanceof Error ? error : new Error("Datastore cleanup failed.", { cause: error }));
714
+ }
715
+ });
716
+ for (const result of await Promise.allSettled(pending))
717
+ if (result.status === "rejected")
718
+ errors.push(result.reason);
719
+ try {
720
+ await this.#catalog.close();
721
+ }
722
+ catch (error) {
723
+ errors.push(error);
724
+ }
725
+ if (errors.length === 1)
726
+ throw errors[0];
727
+ if (errors.length)
728
+ throw new AggregateError(errors, "Datastore factory close failed.");
279
729
  }
280
730
  }
281
731
  /**
282
-
283
732
  * Configures a Datastore storage factory before construction.
284
-
285
733
  */
286
734
  class Builder {
287
735
  create;
@@ -293,6 +741,10 @@ class Builder {
293
741
  * Custom record creators indexed by family.
294
742
  */
295
743
  #recordCreators = new Map();
744
+ /**
745
+ * Optional preparation callbacks paired with custom record creators.
746
+ */
747
+ #recordPreparers = new Map();
296
748
  /**
297
749
  * Physical kind layouts indexed by family.
298
750
  */
@@ -301,6 +753,10 @@ class Builder {
301
753
  * Custom Entity creators indexed by state type.
302
754
  */
303
755
  #entityCreators = new Map();
756
+ /**
757
+ * Optional preparation callbacks paired with custom Entity creators.
758
+ */
759
+ #entityPreparers = new Map();
304
760
  /**
305
761
  * Converter for multitenant Datastore namespaces.
306
762
  */
@@ -308,7 +764,8 @@ class Builder {
308
764
  /**
309
765
  * Converter for message-valued IDs and columns.
310
766
  */
311
- #stringifiers = new StringifierRegistry();
767
+ #stringifiers = defaultStringifierRegistry;
768
+ #eventStoreKinds = new Map();
312
769
  /**
313
770
  * Creates a builder with the selected factory constructor.
314
771
  *
@@ -344,7 +801,25 @@ class Builder {
344
801
  * @returns This builder.
345
802
  */
346
803
  setStringifierRegistry(registry) {
347
- this.#stringifiers = new StringifierRegistry(registry);
804
+ this.#stringifiers =
805
+ registry === defaultStringifierRegistry ? registry : new StringifierRegistry(registry);
806
+ return this;
807
+ }
808
+ /**
809
+ * Sets the kind for one exact domain or system Event Store.
810
+ *
811
+ * @param boundedContextName Logical bounded-context name.
812
+ * @param kind Physical Datastore kind.
813
+ * @param role Domain or system Event Store role.
814
+ * @returns This builder.
815
+ */
816
+ setEventStoreKind(boundedContextName, kind, role = "domain") {
817
+ checkedLayout({ kind });
818
+ const group = EventStoreGroups.forContext(boundedContextName, role);
819
+ const identity = EventStoreGroups.identity(group);
820
+ if (identity === undefined)
821
+ throw new Error("Event Store identity is missing.");
822
+ this.#eventStoreKinds.set(identity, kind);
348
823
  return this;
349
824
  }
350
825
  /**
@@ -368,13 +843,21 @@ class Builder {
368
843
  * @param first Record type, or source type when three arguments are supplied.
369
844
  * @param second Creator, or record type when three arguments are supplied.
370
845
  * @param third Creator for a source-specific registration.
846
+ * @param fourth Declarative preparation for a source-specific registration.
371
847
  * @returns This builder.
372
848
  */
373
- useRecordStorage(first, second, third) {
374
- const [source, record, creator] = third === undefined
375
- ? [undefined, first, second]
376
- : [first, second, third];
377
- this.#recordCreators.set(key(source?.typeName ?? record.typeName, record.typeName), creator);
849
+ useRecordStorage(first, second, third, fourth) {
850
+ const sourceSpecific = typeof second !== "function";
851
+ const source = sourceSpecific ? first : undefined;
852
+ const record = sourceSpecific ? second : first;
853
+ const creator = sourceSpecific ? third : second;
854
+ const prepare = sourceSpecific ? fourth : third;
855
+ const family = key(source?.typeName ?? record.typeName, record.typeName);
856
+ this.#recordCreators.set(family, creator);
857
+ if (prepare === undefined)
858
+ this.#recordPreparers.delete(family);
859
+ else
860
+ this.#recordPreparers.set(family, prepare);
378
861
  return this;
379
862
  }
380
863
  /**
@@ -383,10 +866,15 @@ class Builder {
383
866
  * @typeParam S Entity state message type.
384
867
  * @param sourceType Selects the Entity state type.
385
868
  * @param creator Builds its storage handle.
869
+ * @param prepare Optional declarative preparation for the coherent provider.
386
870
  * @returns This builder.
387
871
  */
388
- useEntityStorage(sourceType, creator) {
872
+ useEntityStorage(sourceType, creator, prepare) {
389
873
  this.#entityCreators.set(sourceType.typeName, creator);
874
+ if (prepare === undefined)
875
+ this.#entityPreparers.delete(sourceType.typeName);
876
+ else
877
+ this.#entityPreparers.set(sourceType.typeName, prepare);
390
878
  return this;
391
879
  }
392
880
  /**
@@ -398,7 +886,7 @@ class Builder {
398
886
  if (this.#client === undefined)
399
887
  throw new Error("DatastoreStorageFactory builder requires a client.");
400
888
  rejectKindCollisions(this.#layouts);
401
- return this.create(this.#client, new Map(this.#recordCreators), new Map(this.#layouts), new Map(this.#entityCreators), this.#namespaceConverter, this.#stringifiers);
889
+ return this.create(this.#client, new Map(this.#recordCreators), new Map(this.#recordPreparers), new Map(this.#layouts), new Map(this.#entityCreators), new Map(this.#entityPreparers), this.#namespaceConverter, this.#stringifiers, new Map(this.#eventStoreKinds));
402
890
  }
403
891
  }
404
892
  /**
@@ -408,6 +896,9 @@ class Builder {
408
896
  * @typeParam S Entity state message type.
409
897
  */
410
898
  class DefaultEntityHandle {
899
+ #open = true;
900
+ #idleOutcome;
901
+ #closing;
411
902
  /**
412
903
  * Current Entity record access.
413
904
  */
@@ -447,14 +938,85 @@ class DefaultEntityHandle {
447
938
  * @returns Whether operations remain available.
448
939
  */
449
940
  isOpen() {
450
- return this.#storage.isOpen();
941
+ return this.#open && this.#storage.isOpen();
451
942
  }
452
943
  /**
453
944
  * Closes generated records and commit access.
945
+ *
946
+ * @returns Completion after retained resources are closed.
454
947
  */
455
948
  close() {
456
- this.#storage.close();
457
- this.commits.close();
949
+ if (this.#closing !== undefined)
950
+ return this.#closing;
951
+ this.#open = false;
952
+ if (this.#idleOutcome !== undefined) {
953
+ const failure = this.#idleOutcome.error;
954
+ return (this.#closing = failure === undefined ? Promise.resolve() : Promise.reject(failure));
955
+ }
956
+ const completion = this.commits.close();
957
+ return (this.#closing = this.drain(completion));
958
+ }
959
+ /**
960
+ * Closes read records after an admitted commit settles.
961
+ *
962
+ * @param completion The admitted commit completion.
963
+ * @returns Completion after admitted work and cleanup settle.
964
+ */
965
+ async drain(completion) {
966
+ const errors = [];
967
+ try {
968
+ await completion;
969
+ }
970
+ catch (error) {
971
+ errors.push(error);
972
+ }
973
+ try {
974
+ this.#storage.close();
975
+ }
976
+ catch (error) {
977
+ errors.push(error);
978
+ }
979
+ if (errors.length === 1)
980
+ throw errors[0];
981
+ if (errors.length)
982
+ throw new AggregateError(errors, "Datastore Entity close failed.");
983
+ }
984
+ /**
985
+ * Closes a bundle synchronously when no commit or close remains in progress.
986
+ */
987
+ closeIdle() {
988
+ if (this.#idleOutcome !== undefined) {
989
+ if (this.#idleOutcome.error !== undefined)
990
+ throw this.#idleOutcome.error;
991
+ return;
992
+ }
993
+ if (this.#closing !== undefined)
994
+ throw new Error("Entity bundle close is in progress.");
995
+ this.commits.assertIdle();
996
+ this.#open = false;
997
+ const errors = [];
998
+ try {
999
+ this.commits.closeIdle();
1000
+ }
1001
+ catch (error) {
1002
+ errors.push(error);
1003
+ }
1004
+ try {
1005
+ this.#storage.close();
1006
+ }
1007
+ catch (error) {
1008
+ errors.push(error);
1009
+ }
1010
+ const failure = errors.length > 1
1011
+ ? new AggregateError(errors, "Datastore bundle idle close failed.")
1012
+ : errors[0] === undefined
1013
+ ? undefined
1014
+ : errors[0] instanceof Error
1015
+ ? errors[0]
1016
+ : new Error("Datastore cleanup failed.", { cause: errors[0] });
1017
+ this.#idleOutcome = failure === undefined ? {} : { error: failure };
1018
+ if (failure !== undefined)
1019
+ throw failure;
458
1020
  }
459
1021
  }
460
1022
  function key(source, record) {