@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.
- package/README.md +25 -10
- package/REFERENCE.md +66 -50
- package/dist/datastore/agent-history.d.ts +36 -6
- package/dist/datastore/agent-history.d.ts.map +1 -1
- package/dist/datastore/agent-history.js +92 -6
- package/dist/datastore/agent-history.js.map +1 -1
- package/dist/datastore/column-mapping.d.ts.map +1 -1
- package/dist/datastore/column-mapping.js +6 -3
- package/dist/datastore/column-mapping.js.map +1 -1
- package/dist/datastore/entity-history.d.ts +206 -55
- package/dist/datastore/entity-history.d.ts.map +1 -1
- package/dist/datastore/entity-history.js +556 -239
- package/dist/datastore/entity-history.js.map +1 -1
- package/dist/datastore/id-column.d.ts +7 -0
- package/dist/datastore/id-column.d.ts.map +1 -1
- package/dist/datastore/id-column.js +27 -3
- package/dist/datastore/id-column.js.map +1 -1
- package/dist/datastore/record-kind.d.ts +8 -0
- package/dist/datastore/record-kind.d.ts.map +1 -0
- package/dist/datastore/record-kind.js +26 -0
- package/dist/datastore/record-kind.js.map +1 -0
- package/dist/datastore/record-storage.d.ts +11 -10
- package/dist/datastore/record-storage.d.ts.map +1 -1
- package/dist/datastore/record-storage.js +31 -32
- package/dist/datastore/record-storage.js.map +1 -1
- package/dist/datastore/storage-factory.d.ts +293 -89
- package/dist/datastore/storage-factory.d.ts.map +1 -1
- package/dist/datastore/storage-factory.js +734 -222
- package/dist/datastore/storage-factory.js.map +1 -1
- package/dist/datastore/tenant-catalog.d.ts +12 -25
- package/dist/datastore/tenant-catalog.d.ts.map +1 -1
- package/dist/datastore/tenant-catalog.js +43 -121
- package/dist/datastore/tenant-catalog.js.map +1 -1
- package/dist/datastore/transaction-rows.d.ts +30 -0
- package/dist/datastore/transaction-rows.d.ts.map +1 -0
- package/dist/datastore/transaction-rows.js +43 -0
- package/dist/datastore/transaction-rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.yaml +0 -9
- package/package.json +5 -5
- package/dist/datastore/agent-execution.d.ts +0 -337
- package/dist/datastore/agent-execution.d.ts.map +0 -1
- package/dist/datastore/agent-execution.js +0 -789
- package/dist/datastore/agent-execution.js.map +0 -1
- package/dist/datastore/delivery-cleanup.d.ts +0 -31
- package/dist/datastore/delivery-cleanup.d.ts.map +0 -1
- package/dist/datastore/delivery-cleanup.js +0 -111
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
130
|
+
EntityCommitStorageFactories.register(this, {
|
|
131
|
+
createEntityCommitStorage: (input) => this.createEntityCommitStorage(input),
|
|
109
132
|
});
|
|
110
133
|
this.registerAgentHistory();
|
|
111
|
-
this.registerAgentExecution();
|
|
112
134
|
}
|
|
113
135
|
/**
|
|
114
|
-
*
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
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
|
-
|
|
170
|
+
admit(operation) {
|
|
142
171
|
if (!this.isOpen())
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* @
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* @
|
|
170
|
-
* @
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
199
|
-
* @
|
|
200
|
-
* @param
|
|
201
|
-
* @param
|
|
202
|
-
* @
|
|
203
|
-
* @
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
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
|
|
609
|
+
return this.openNative(context, recordSpec, group, resolved.layout?.kind);
|
|
245
610
|
}
|
|
246
611
|
/**
|
|
247
|
-
*
|
|
612
|
+
* Acquires the built-in record provider for generated Entity families.
|
|
248
613
|
*
|
|
249
|
-
* @
|
|
250
|
-
* @
|
|
251
|
-
* @param
|
|
252
|
-
* @
|
|
253
|
-
* @
|
|
254
|
-
* @
|
|
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
|
|
627
|
+
return this.openNative(context, recordSpec, group, resolved.layout?.kind);
|
|
259
628
|
}
|
|
260
629
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* @
|
|
264
|
-
* @param
|
|
265
|
-
* @
|
|
266
|
-
* @
|
|
267
|
-
* @
|
|
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
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
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
|
-
*
|
|
650
|
+
* Finds the registered creator and physical layout for a record family.
|
|
275
651
|
*
|
|
276
|
-
* @
|
|
277
|
-
* @
|
|
278
|
-
* @param
|
|
279
|
-
* @
|
|
280
|
-
* @
|
|
281
|
-
* @
|
|
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:
|
|
290
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
767
|
+
#stringifiers = defaultStringifierRegistry;
|
|
768
|
+
#eventStoreKinds = new Map();
|
|
352
769
|
/**
|
|
353
|
-
*
|
|
770
|
+
* Creates a builder with the selected factory constructor.
|
|
354
771
|
*
|
|
355
|
-
* @param create
|
|
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
|
|
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
|
|
788
|
+
* Sets reversible tenant namespace conversion.
|
|
372
789
|
*
|
|
373
|
-
* @param 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
|
|
798
|
+
* Sets message-valued ID and column converters.
|
|
382
799
|
*
|
|
383
|
-
* @param registry
|
|
800
|
+
* @param registry Supplies schema-bound message stringifiers.
|
|
384
801
|
* @returns This builder.
|
|
385
802
|
*/
|
|
386
803
|
setStringifierRegistry(registry) {
|
|
387
|
-
this.#stringifiers =
|
|
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
|
-
*
|
|
826
|
+
* Registers a physical kind for one source and record family.
|
|
392
827
|
*
|
|
393
|
-
* @
|
|
394
|
-
* @param
|
|
395
|
-
* @param
|
|
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
|
|
840
|
+
* Registers a custom record provider for one family.
|
|
406
841
|
*
|
|
407
|
-
* @
|
|
408
|
-
* @param
|
|
409
|
-
* @param
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
864
|
+
* Registers a custom persistence provider for an Entity state type.
|
|
422
865
|
*
|
|
423
|
-
* @
|
|
424
|
-
* @param
|
|
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
|
|
881
|
+
* Builds a factory after validating its client and kind registrations.
|
|
434
882
|
*
|
|
435
|
-
* @returns
|
|
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
|
-
*
|
|
893
|
+
* Combines generated Entity records and commit access for one source.
|
|
446
894
|
*
|
|
447
|
-
* @typeParam I
|
|
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
|
-
*
|
|
923
|
+
* Connects the generated storage and commit handle.
|
|
481
924
|
*
|
|
482
|
-
* @param storage
|
|
483
|
-
* @param commits
|
|
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
|
-
*
|
|
936
|
+
* Returns whether the generated storage remains open.
|
|
494
937
|
*
|
|
495
|
-
* @returns Whether operations
|
|
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
|
-
*
|
|
503
|
-
|
|
944
|
+
* Closes generated records and commit access.
|
|
945
|
+
*
|
|
946
|
+
* @returns Completion after retained resources are closed.
|
|
504
947
|
*/
|
|
505
948
|
close() {
|
|
506
|
-
this.#
|
|
507
|
-
|
|
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) {
|