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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +25 -10
  2. package/REFERENCE.md +66 -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 +206 -55
  11. package/dist/datastore/entity-history.d.ts.map +1 -1
  12. package/dist/datastore/entity-history.js +556 -239
  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 +31 -32
  25. package/dist/datastore/record-storage.js.map +1 -1
  26. package/dist/datastore/storage-factory.d.ts +293 -89
  27. package/dist/datastore/storage-factory.d.ts.map +1 -1
  28. package/dist/datastore/storage-factory.js +734 -222
  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 -5
  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
  48. package/dist/datastore/delivery-cleanup.d.ts +0 -31
  49. package/dist/datastore/delivery-cleanup.d.ts.map +0 -1
  50. package/dist/datastore/delivery-cleanup.js +0 -111
  51. package/dist/datastore/delivery-cleanup.js.map +0 -1
@@ -11,21 +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
- import { DeliveryCleanupStorageFactories } from "@spine-event-engine/storage/provider";
22
20
  import { AgentHistoryStorageFactories } from "@spine-event-engine/storage/provider";
23
- import { AgentExecutionStorageFactories } from "@spine-event-engine/storage/provider";
24
21
  import { DatastoreEntityCommitStorage, DatastoreEntityStorage, } from "./entity-history.js";
25
22
  import { DatastoreRecordStorage } from "./record-storage.js";
23
+ import { RecordKinds } from "./record-kind.js";
24
+ import { DatastoreIdColumn } from "./id-column.js";
26
25
  import { DatastoreAgentHistory, AgentHistoryHash } from "./agent-history.js";
27
- import { DatastoreAgentExecution } from "./agent-execution.js";
28
- import { DatastoreDeliveryCleanupStorage } from "./delivery-cleanup.js";
29
26
  import { DefaultNamespaceConverter, NamespaceAssignments, } from "./namespace.js";
30
27
  import { DatastoreTenantCatalog } from "./tenant-catalog.js";
31
28
  const maxClientSideScan = 1_000;
@@ -33,44 +30,60 @@ const maxClientSideScan = 1_000;
33
30
  * A Google Cloud Datastore-backed implementation of the Spine TS storage port.
34
31
  */
35
32
  export class DatastoreStorageFactory extends StorageFactory {
33
+ #commits = new Set();
34
+ #bundles = new Set();
35
+ /**
36
+ * Repeatable asynchronous close completion.
37
+ */
38
+ #closing;
36
39
  /**
37
40
  * Caller-supplied Datastore client.
38
41
  */
39
42
  #client;
40
43
  /**
41
-
42
- * Custom record providers by family.
43
-
44
+ * Custom record creators indexed by source and record type.
44
45
  */
45
46
  #recordCreators;
46
47
  /**
47
-
48
- * Physical kinds by record family.
49
-
48
+ * Declarative custom record preparation callbacks by family.
49
+ */
50
+ #recordPreparers;
51
+ /**
52
+ * Physical layouts indexed by source and record type.
50
53
  */
51
54
  #layouts;
52
55
  /**
53
-
54
- * Custom Entity providers by state type.
55
-
56
+ * Custom Entity creators indexed by state type.
56
57
  */
57
58
  #entityCreators;
58
59
  /**
59
-
60
- * Complete tenant-to-namespace conversion.
61
-
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;
76
+ /**
77
+ * Reversible tenant namespace assignment.
62
78
  */
63
79
  #namespaceConverter;
64
80
  /**
65
-
66
- * Schema-bound message stringifiers.
67
-
81
+ * Message-valued ID and column conversion registry.
68
82
  */
69
83
  #stringifiers;
84
+ #eventStoreKinds;
70
85
  /**
71
-
72
- * Native tenant catalog for the selected client.
73
-
86
+ * Native tenant namespace catalog.
74
87
  */
75
88
  #catalog;
76
89
  /**
@@ -79,216 +92,583 @@ export class DatastoreStorageFactory extends StorageFactory {
79
92
  * @returns A new factory builder.
80
93
  */
81
94
  static newBuilder() {
82
- 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));
83
96
  }
84
97
  /**
85
98
  * Creates a factory from resolved dependencies.
86
99
  *
87
100
  * @param client The caller-owned Datastore client.
88
101
  * @param recordCreators Custom record-storage providers by record family.
102
+ * @param recordPreparers Declarative preparation callbacks by record family.
89
103
  * @param layouts Physical record layouts by record family.
90
104
  * @param entityCreators Custom Entity persistence providers by Entity type.
105
+ * @param entityPreparers Declarative preparation callbacks by Entity type.
91
106
  * @param namespaceConverter Converts complete tenants to native namespaces.
92
107
  * @param stringifiers Converts message-valued IDs and columns.
108
+ * @param eventStoreKinds Explicit native kinds by Event Store identity.
93
109
  * @internal
94
110
  */
95
- constructor(client, recordCreators, layouts, entityCreators, namespaceConverter, stringifiers) {
111
+ constructor(client, recordCreators, recordPreparers, layouts, entityCreators, entityPreparers, namespaceConverter, stringifiers, eventStoreKinds) {
96
112
  super();
97
113
  this.#client = client;
98
114
  this.#recordCreators = recordCreators;
115
+ this.#recordPreparers = recordPreparers;
99
116
  this.#layouts = layouts;
100
117
  this.#entityCreators = entityCreators;
118
+ this.#entityPreparers = entityPreparers;
101
119
  this.#namespaceConverter = new NamespaceAssignments(namespaceConverter);
102
- this.#stringifiers = new StringifierRegistry(stringifiers);
120
+ const shared = stringifiers === defaultStringifierRegistry;
121
+ this.#stringifiers = shared ? stringifiers : new StringifierRegistry(stringifiers);
122
+ this.#eventStoreKinds = eventStoreKinds;
103
123
  this.#catalog = new DatastoreTenantCatalog(client, this.#namespaceConverter);
104
- EntityCommitStorageFactories.register(this, {
105
- createEntityCommitStorage: (input) => this.createEntityStorage(input).commits,
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),
106
129
  });
107
- DeliveryCleanupStorageFactories.register(this, {
108
- createDeliveryCleanupStorage: () => new DatastoreDeliveryCleanupStorage((context, spec) => this.cleanupStorage(context, spec)),
130
+ EntityCommitStorageFactories.register(this, {
131
+ createEntityCommitStorage: (input) => this.createEntityCommitStorage(input),
109
132
  });
110
133
  this.registerAgentHistory();
111
- this.registerAgentExecution();
112
134
  }
113
135
  /**
114
- * 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.
115
138
  */
116
- registerAgentHistory() {
117
- AgentHistoryStorageFactories.register(this, {
118
- createAgentHistoryStorage: (input) => {
119
- if (!this.isOpen())
120
- throw new Error("StorageFactory is closed.");
121
- 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));
122
- },
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;
123
149
  });
150
+ return pending;
124
151
  }
125
152
  /**
126
- * 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.
127
156
  */
128
- registerAgentExecution() {
129
- AgentExecutionStorageFactories.register(this, {
130
- createAgentExecutionStorage: (input) => this.createAgentExecution(input),
131
- });
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);
132
164
  }
133
165
  /**
134
- * Opens one Agent execution handle after factory lifecycle validation.
135
- *
136
- * @param input Requested fenced execution change.
137
- * @returns Tenant-scoped Agent execution storage handle.
138
- * @typeParam I Typed entity identifier.
139
- * @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.
140
169
  */
141
- createAgentExecution(input) {
170
+ admit(operation) {
142
171
  if (!this.isOpen())
143
- throw new Error("StorageFactory is closed.");
144
- 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;
145
177
  }
146
178
  /**
147
- * Opens the fixed execution kinds and existing Entity record families.
148
- *
149
- * @param input Requested fenced execution change.
150
- * @returns Tenant-scoped native record handles.
151
- * @typeParam I Typed entity identifier.
152
- * @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.
153
182
  */
154
- executionRows(input) {
155
- const context = input.entity.context;
156
- const digest = (value) => AgentHistoryHash.value(value);
157
- return {
158
- invocation: this.executionRecord(context, AgentExecutionRecords.invocationSpec(digest), AgentExecutionRecords.invocationGroup, "spine_agent_execution"),
159
- head: this.executionRecord(context, AgentExecutionRecords.headSpec(digest), AgentExecutionRecords.headGroup, "spine_agent_execution_head"),
160
- history: this.executionRecord(context, AgentHistoryRecords.spec(digest), AgentHistoryRecords.group, "spine_agent_history"),
161
- current: this.executionRecord(context, input.entity.recordSpec),
162
- ...this.optionalExecutionRows(input),
163
- events: this.executionRecord(context, eventStoreRecordSpec),
164
- };
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));
165
211
  }
166
212
  /**
167
- * Opens optional existing Entity state and diagnostic record families.
168
- *
169
- * @param input Typed Agent repository and history configuration.
170
- * @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.
171
285
  * @typeParam I Typed Agent identifier.
172
286
  * @typeParam S Generated Agent state.
173
287
  */
174
- optionalExecutionRows(input) {
175
- const context = input.entity.context;
176
- const state = input.entity.stateHistory
177
- ? stateHistorySpec(input.entity.stateSchema)
178
- : undefined;
179
- const diagnostic = input.entity.eventHistory
180
- ? eventHistorySpec(input.entity.stateSchema)
181
- : undefined;
182
- return {
183
- ...(state === undefined
184
- ? {}
185
- : {
186
- states: this.executionRecord(context, state.spec, state.group),
187
- }),
188
- ...(diagnostic === undefined
189
- ? {}
190
- : {
191
- diagnostics: this.executionRecord(context, diagnostic.spec, diagnostic.group),
192
- }),
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,
193
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;
194
386
  }
195
387
  /**
196
- * 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.
197
420
  *
198
- * @param context Captured event storage context.
199
- * @param fixedKind Fixed native Datastore kind.
200
- * @param group Fixed storage group.
201
- * @param spec Typed record layout.
202
- * @returns Typed native record handle.
203
- * @typeParam I Typed entity identifier.
204
- * @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.
205
438
  */
206
- executionRecord(context, spec, group, fixedKind) {
207
- const kind = fixedKind ?? this.resolve(spec, group, false).layout?.kind;
208
- return new DatastoreRecordStorage(context, spec, this.#client, maxClientSideScan, group, kind, this.#namespaceConverter, this.#stringifiers);
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.
455
+ */
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
+ });
209
481
  }
210
482
  /**
211
483
  * Creates the selected Entity provider bundle.
212
484
  *
485
+ * @typeParam I Entity identifier type.
486
+ * @typeParam S Entity state message type.
213
487
  * @param input The Entity persistence contract.
214
488
  * @returns The coherent Entity storage handle.
215
- * @typeParam I Typed Entity identifier.
216
- * @typeParam S Entity state message.
217
489
  */
218
490
  createEntityStorage(input) {
219
491
  if (!this.isOpen())
220
492
  throw new Error("StorageFactory is closed.");
221
493
  const custom = this.#entityCreators.get(input.sourceType.typeName);
222
494
  if (custom !== undefined)
223
- return custom(input, this.#client);
495
+ return this.retainBundle(custom(input, this.#client));
224
496
  const openRecords = (recordSpec, group) => this.createEntityRecordStorage(input.context, recordSpec, group);
225
497
  const storage = new DatastoreEntityStorage(input, openRecords);
226
498
  const commits = new DatastoreEntityCommitStorage(input, openRecords);
227
- 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
+ };
228
593
  }
229
594
  /**
230
595
  * Creates provider-specific Datastore record storage.
231
596
  *
597
+ * @typeParam I Record identifier type.
598
+ * @typeParam R Stored record message type.
232
599
  * @param context The storage context.
233
600
  * @param recordSpec The record specification.
234
601
  * @param group Separates records that share one source type.
235
602
  * @returns The created Datastore record storage.
236
- * @typeParam I Typed record identifier.
237
- * @typeParam R Stored record message.
238
603
  */
239
604
  onCreateRecordStorage(context, recordSpec, group) {
240
605
  const resolved = this.resolve(recordSpec, group, true);
241
606
  const creator = resolved.creator;
242
607
  if (creator !== undefined)
243
608
  return creator(context, recordSpec, this.#client, maxClientSideScan);
244
- 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);
245
610
  }
246
611
  /**
247
- * Creates a canonical Entity record handle without a custom provider callback.
612
+ * Acquires the built-in record provider for generated Entity families.
248
613
  *
249
- * @param context Complete storage and tenant scope.
250
- * @param recordSpec Generated record layout.
251
- * @param group Optional record family group.
252
- * @returns Native record handle.
253
- * @typeParam I Typed record identifier.
254
- * @typeParam R Stored record message.
614
+ * @typeParam I Record identifier type.
615
+ * @typeParam R Stored record message type.
616
+ * @param context Identifies the storage tenant and context.
617
+ * @param recordSpec Defines the generated record family.
618
+ * @param group Selects an optional grouped history family.
619
+ * @returns A built-in Datastore record handle.
255
620
  */
256
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");
257
626
  const resolved = this.resolve(recordSpec, group, false);
258
- 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);
259
628
  }
260
629
  /**
261
- * Creates a canonical delivery-cleanup record handle.
262
- *
263
- * @param context Complete storage and tenant scope.
264
- * @param recordSpec Generated record layout.
265
- * @returns Native record handle.
266
- * @typeParam I Typed record identifier.
267
- * @typeParam R Stored record message.
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.
268
638
  */
269
- cleanupStorage(context, recordSpec) {
270
- const resolved = this.resolve(recordSpec, undefined, false);
271
- return new DatastoreRecordStorage(context, recordSpec, this.#client, maxClientSideScan, undefined, resolved.layout?.kind, this.#namespaceConverter, this.#stringifiers);
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;
272
648
  }
273
649
  /**
274
- * Resolves a declared record family's native kind and optional callback.
650
+ * Finds the registered creator and physical layout for a record family.
275
651
  *
276
- * @param recordSpec Generated record layout.
277
- * @param group Optional record family group.
278
- * @param includeCreator Whether custom callbacks are permitted.
279
- * @returns Matching custom provider and physical layout, when configured.
280
- * @typeParam I Typed record identifier.
281
- * @typeParam R Stored record message.
652
+ * @typeParam I Record identifier type.
653
+ * @typeParam R Stored record message type.
654
+ * @param recordSpec Defines the requested family.
655
+ * @param group Selects an optional grouped family.
656
+ * @param includeCreator Allows a custom record creator for ordinary records.
657
+ * @returns Matching custom creator and physical layout, when registered.
282
658
  */
283
659
  resolve(recordSpec, group, includeCreator) {
284
660
  const source = recordSpec.sourceType.typeName;
285
661
  const record = recordSpec.recordType.typeName;
286
662
  const exact = key(group?.name ?? source, record);
287
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;
288
669
  return {
289
- creator: includeCreator
290
- ? (this.#recordCreators.get(exact) ?? this.#recordCreators.get(fallback))
291
- : undefined,
670
+ creator: selected === undefined ? undefined : this.#recordCreators.get(selected),
671
+ prepare: selected === undefined ? undefined : this.#recordPreparers.get(selected),
292
672
  layout: this.#layouts.get(exact) ?? this.#layouts.get(fallback),
293
673
  };
294
674
  }
@@ -302,57 +682,94 @@ export class DatastoreStorageFactory extends StorageFactory {
302
682
  }
303
683
  /**
304
684
  * Closes the factory and its native tenant catalog.
685
+ *
686
+ * @returns Completion after retained resources are closed.
305
687
  */
306
688
  close() {
307
- super.close();
308
- 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.");
309
729
  }
310
730
  }
311
731
  /**
312
-
313
- * Builds a Datastore factory from caller-supplied client and layout options.
314
-
732
+ * Configures a Datastore storage factory before construction.
315
733
  */
316
734
  class Builder {
317
735
  create;
318
736
  /**
319
- * Caller-supplied Datastore client.
737
+ * Caller-supplied client required by build.
320
738
  */
321
739
  #client;
322
740
  /**
323
-
324
- * Custom providers by record-family identity.
325
-
741
+ * Custom record creators indexed by family.
326
742
  */
327
743
  #recordCreators = new Map();
328
744
  /**
329
-
330
- * Physical kinds by record-family identity.
331
-
745
+ * Optional preparation callbacks paired with custom record creators.
746
+ */
747
+ #recordPreparers = new Map();
748
+ /**
749
+ * Physical kind layouts indexed by family.
332
750
  */
333
751
  #layouts = new Map();
334
752
  /**
335
-
336
- * Custom Entity providers by generated state type.
337
-
753
+ * Custom Entity creators indexed by state type.
338
754
  */
339
755
  #entityCreators = new Map();
340
756
  /**
341
-
342
- * Complete tenant-to-namespace converter.
343
-
757
+ * Optional preparation callbacks paired with custom Entity creators.
758
+ */
759
+ #entityPreparers = new Map();
760
+ /**
761
+ * Converter for multitenant Datastore namespaces.
344
762
  */
345
763
  #namespaceConverter = new DefaultNamespaceConverter();
346
764
  /**
347
-
348
- * Schema-bound message stringifiers.
349
-
765
+ * Converter for message-valued IDs and columns.
350
766
  */
351
- #stringifiers = new StringifierRegistry();
767
+ #stringifiers = defaultStringifierRegistry;
768
+ #eventStoreKinds = new Map();
352
769
  /**
353
- * Binds the resolved configuration to a factory constructor.
770
+ * Creates a builder with the selected factory constructor.
354
771
  *
355
- * @param create Creates the configured Datastore factory.
772
+ * @param create Constructs the configured factory.
356
773
  */
357
774
  constructor(create) {
358
775
  this.create = create;
@@ -360,7 +777,7 @@ class Builder {
360
777
  /**
361
778
  * Sets the caller-supplied Datastore client.
362
779
  *
363
- * @param client Datastore client.
780
+ * @param client Connects to the chosen Datastore project.
364
781
  * @returns This builder.
365
782
  */
366
783
  setClient(client) {
@@ -368,9 +785,9 @@ class Builder {
368
785
  return this;
369
786
  }
370
787
  /**
371
- * Sets reversible complete tenant namespace conversion.
788
+ * Sets reversible tenant namespace conversion.
372
789
  *
373
- * @param converter Tenant namespace converter.
790
+ * @param converter Converts complete tenant identities.
374
791
  * @returns This builder.
375
792
  */
376
793
  setNamespaceConverter(converter) {
@@ -378,23 +795,41 @@ class Builder {
378
795
  return this;
379
796
  }
380
797
  /**
381
- * Sets schema-bound message stringifiers for IDs and columns.
798
+ * Sets message-valued ID and column converters.
382
799
  *
383
- * @param registry Custom message stringifiers.
800
+ * @param registry Supplies schema-bound message stringifiers.
384
801
  * @returns This builder.
385
802
  */
386
803
  setStringifierRegistry(registry) {
387
- 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);
388
823
  return this;
389
824
  }
390
825
  /**
391
- * Maps a record family to one native Datastore kind.
826
+ * Registers a physical kind for one source and record family.
392
827
  *
393
- * @param first Source or record message type.
394
- * @param second Record message type or kind layout.
395
- * @param third Kind layout for a source-record pair.
828
+ * @typeParam R Stored record message type.
829
+ * @param first Record type, or source type when three arguments are supplied.
830
+ * @param second Layout, or record type when three arguments are supplied.
831
+ * @param third Layout for a source-specific registration.
396
832
  * @returns This builder.
397
- * @typeParam R Stored record message.
398
833
  */
399
834
  organizeRecords(first, second, third) {
400
835
  const [source, record, layout] = third === undefined ? [undefined, first, second] : [first, second, third];
@@ -402,85 +837,93 @@ class Builder {
402
837
  return this;
403
838
  }
404
839
  /**
405
- * Registers one custom record provider for a record family.
840
+ * Registers a custom record provider for one family.
406
841
  *
407
- * @param first Source or record message type.
408
- * @param second Record message type or provider callback.
409
- * @param third Provider callback for a source-record pair.
842
+ * @typeParam R Stored record message type.
843
+ * @param first Record type, or source type when three arguments are supplied.
844
+ * @param second Creator, or record type when three arguments are supplied.
845
+ * @param third Creator for a source-specific registration.
846
+ * @param fourth Declarative preparation for a source-specific registration.
410
847
  * @returns This builder.
411
- * @typeParam R Stored record message.
412
848
  */
413
- useRecordStorage(first, second, third) {
414
- const [source, record, creator] = third === undefined
415
- ? [undefined, first, second]
416
- : [first, second, third];
417
- 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);
418
861
  return this;
419
862
  }
420
863
  /**
421
- * Registers one custom Entity provider for a generated state type.
864
+ * Registers a custom persistence provider for an Entity state type.
422
865
  *
423
- * @param sourceType Generated Entity state type.
424
- * @param creator Custom Entity provider callback.
866
+ * @typeParam S Entity state message type.
867
+ * @param sourceType Selects the Entity state type.
868
+ * @param creator Builds its storage handle.
869
+ * @param prepare Optional declarative preparation for the coherent provider.
425
870
  * @returns This builder.
426
- * @typeParam S Entity state message.
427
871
  */
428
- useEntityStorage(sourceType, creator) {
872
+ useEntityStorage(sourceType, creator, prepare) {
429
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);
430
878
  return this;
431
879
  }
432
880
  /**
433
- * Builds the configured Datastore factory.
881
+ * Builds a factory after validating its client and kind registrations.
434
882
  *
435
- * @returns Factory bound to the supplied client and layout configuration.
883
+ * @returns The configured Datastore storage factory.
436
884
  */
437
885
  build() {
438
886
  if (this.#client === undefined)
439
887
  throw new Error("DatastoreStorageFactory builder requires a client.");
440
888
  rejectKindCollisions(this.#layouts);
441
- 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));
442
890
  }
443
891
  }
444
892
  /**
445
- * Groups current Entity records, histories, and commit access in one handle.
893
+ * Combines generated Entity records and commit access for one source.
446
894
  *
447
- * @typeParam I Typed Entity identifier.
448
- * @typeParam S Entity state message.
895
+ * @typeParam I Entity identifier type.
896
+ * @typeParam S Entity state message type.
449
897
  */
450
898
  class DefaultEntityHandle {
899
+ #open = true;
900
+ #idleOutcome;
901
+ #closing;
451
902
  /**
452
903
  * Current Entity record access.
453
904
  */
454
905
  current;
455
906
  /**
456
-
457
- * Retained Entity state history.
458
-
907
+ * Retained state history access.
459
908
  */
460
909
  states;
461
910
  /**
462
-
463
- * Retained diagnostic Event history.
464
-
911
+ * Retained diagnostic history access.
465
912
  */
466
913
  events;
467
914
  /**
468
-
469
- * Atomic Entity commit access.
470
-
915
+ * Ordered Entity commit access.
471
916
  */
472
917
  commits;
473
918
  /**
474
-
475
- * Backing Entity storage handle.
476
-
919
+ * Shared record-family handles.
477
920
  */
478
921
  #storage;
479
922
  /**
480
- * Binds Entity storage and commit access to one closeable handle.
923
+ * Connects the generated storage and commit handle.
481
924
  *
482
- * @param storage Current record and history access.
483
- * @param commits Atomic Entity commit access.
925
+ * @param storage Opens current and retained Entity records.
926
+ * @param commits Writes one prepared Entity mutation.
484
927
  */
485
928
  constructor(storage, commits) {
486
929
  this.#storage = storage;
@@ -490,21 +933,90 @@ class DefaultEntityHandle {
490
933
  this.commits = commits;
491
934
  }
492
935
  /**
493
- * Checks whether the backing Entity storage remains open.
936
+ * Returns whether the generated storage remains open.
494
937
  *
495
- * @returns Whether operations are accepted.
938
+ * @returns Whether operations remain available.
496
939
  */
497
940
  isOpen() {
498
- return this.#storage.isOpen();
941
+ return this.#open && this.#storage.isOpen();
499
942
  }
500
943
  /**
501
-
502
- * Closes all Entity storage and commit handles.
503
-
944
+ * Closes generated records and commit access.
945
+ *
946
+ * @returns Completion after retained resources are closed.
504
947
  */
505
948
  close() {
506
- this.#storage.close();
507
- 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;
508
1020
  }
509
1021
  }
510
1022
  function key(source, record) {