@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,16 +11,20 @@
|
|
|
11
11
|
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
12
|
* the License.
|
|
13
13
|
*/
|
|
14
|
-
import { clone,
|
|
14
|
+
import { clone, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { AgentHistoryEntrySchema, } from "@spine-event-engine/proto/agent";
|
|
16
|
+
import { AgentHistoryKeys, AgentHistoryRecords, AgentModelPreferences, } from "@spine-event-engine/storage/provider";
|
|
17
|
+
import { AnyPacker } from "@spine-event-engine/core";
|
|
15
18
|
import {} from "@bufbuild/protobuf/wkt";
|
|
16
19
|
import { EventSchema } from "@spine-event-engine/proto";
|
|
17
20
|
import { EntityRecordSchema, } from "@spine-event-engine/proto/generated/spine/server/entity/entity_pb.js";
|
|
18
|
-
import { eventStoreRecordSpec } from "@spine-event-engine/storage/provider";
|
|
19
|
-
import { disabledEventHistoryPort, disabledStateHistoryPort, eventHistorySpec, stateHistorySpec, } from "@spine-event-engine/storage/provider";
|
|
21
|
+
import { eventStoreRecordSpec, EventStoreGroups } from "@spine-event-engine/storage/provider";
|
|
22
|
+
import { disabledEventHistoryPort, disabledStateHistoryPort, eventHistorySpec, stateHistorySpec, EntityCommitSnapshots, } from "@spine-event-engine/storage/provider";
|
|
20
23
|
import { RecordSpec } from "@spine-event-engine/storage";
|
|
21
24
|
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
22
|
-
import { Datastore } from "@google-cloud/datastore";
|
|
23
25
|
import { DatastoreRecordStorage } from "./record-storage.js";
|
|
26
|
+
import { AgentHistoryHash } from "./agent-history.js";
|
|
27
|
+
import { NativeTransactions } from "./transaction-rows.js";
|
|
24
28
|
/**
|
|
25
29
|
* Groups Entity current and optional history record families.
|
|
26
30
|
*
|
|
@@ -70,8 +74,19 @@ export class DatastoreEntityStorage {
|
|
|
70
74
|
close() {
|
|
71
75
|
if (this.#open) {
|
|
72
76
|
this.#open = false;
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
const errors = [];
|
|
78
|
+
for (const record of this.#records) {
|
|
79
|
+
try {
|
|
80
|
+
record.close();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
errors.push(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (errors.length === 1)
|
|
87
|
+
throw errors[0];
|
|
88
|
+
if (errors.length)
|
|
89
|
+
throw new AggregateError(errors, "Datastore Entity records close failed.");
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
/**
|
|
@@ -84,183 +99,574 @@ export class DatastoreEntityStorage {
|
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
/**
|
|
87
|
-
*
|
|
102
|
+
* Writes one Datastore Entity mutation in independent record order.
|
|
88
103
|
*/
|
|
89
104
|
export class DatastoreEntityCommitStorage {
|
|
90
105
|
input;
|
|
91
106
|
openRecords;
|
|
107
|
+
onClose;
|
|
92
108
|
#open = true;
|
|
109
|
+
#current;
|
|
110
|
+
/**
|
|
111
|
+
* Retained state-history records.
|
|
112
|
+
*/
|
|
113
|
+
#states;
|
|
114
|
+
#diagnostics;
|
|
115
|
+
#events;
|
|
116
|
+
#history;
|
|
117
|
+
#preferences;
|
|
93
118
|
/**
|
|
94
|
-
*
|
|
119
|
+
* Synchronous idle-close result, including any cleanup failure.
|
|
120
|
+
*/
|
|
121
|
+
#idleOutcome;
|
|
122
|
+
#active = new Set();
|
|
123
|
+
#closing;
|
|
124
|
+
/**
|
|
125
|
+
* Creates commit storage for one Entity persistence contract.
|
|
95
126
|
*
|
|
96
|
-
* @param input
|
|
97
|
-
* @param openRecords
|
|
127
|
+
* @param input Defines the Entity source and tenant boundary.
|
|
128
|
+
* @param openRecords Opens each generated record family.
|
|
129
|
+
* @param onClose Releases factory tracking after terminal cleanup.
|
|
98
130
|
*/
|
|
99
|
-
constructor(input, openRecords) {
|
|
131
|
+
constructor(input, openRecords, onClose = () => undefined) {
|
|
100
132
|
this.input = input;
|
|
101
133
|
this.openRecords = openRecords;
|
|
134
|
+
this.onClose = onClose;
|
|
102
135
|
}
|
|
103
136
|
/**
|
|
104
|
-
*
|
|
137
|
+
* Writes histories and delivery Events before the current Entity record.
|
|
138
|
+
*
|
|
139
|
+
* A failed save may leave a completed prefix. One transient retry replays
|
|
140
|
+
* captured records with the same versions and Event IDs.
|
|
105
141
|
*
|
|
106
142
|
* @typeParam I Entity identifier type.
|
|
107
143
|
* @typeParam S Entity state message type.
|
|
108
|
-
* @param input
|
|
109
|
-
* @returns Completion after
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
this
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
* @param input Supplies the prepared Entity mutation.
|
|
145
|
+
* @returns Completion after every requested record write.
|
|
146
|
+
*/
|
|
147
|
+
commit(input) {
|
|
148
|
+
if (!this.#open)
|
|
149
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
150
|
+
return this.track(() => {
|
|
151
|
+
this.validate(input);
|
|
152
|
+
const prepared = EntityCommitSnapshots.capture(input);
|
|
153
|
+
return this.persist(prepared);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Registers a commit before invoking provider construction callbacks.
|
|
158
|
+
*
|
|
159
|
+
* @param action The admitted provider operation.
|
|
160
|
+
* @returns The admitted provider operation completion.
|
|
161
|
+
* @typeParam T Result type of the admitted provider operation.
|
|
162
|
+
*/
|
|
163
|
+
track(action) {
|
|
164
|
+
let complete;
|
|
165
|
+
let fail;
|
|
166
|
+
const work = new Promise((resolve, reject) => {
|
|
167
|
+
complete = resolve;
|
|
168
|
+
fail = reject;
|
|
169
|
+
});
|
|
170
|
+
this.#active.add(work);
|
|
171
|
+
void work.then(() => this.#active.delete(work), () => this.#active.delete(work));
|
|
172
|
+
try {
|
|
173
|
+
action().then(complete, fail);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
fail(error);
|
|
177
|
+
}
|
|
178
|
+
return work;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reads a detached current preference image from native storage.
|
|
182
|
+
* @param entityId Typed Agent identifier.
|
|
183
|
+
* @returns Complete selections or absent record.
|
|
184
|
+
*/
|
|
185
|
+
readAgentPreferences(entityId) {
|
|
186
|
+
if (!this.#open)
|
|
187
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
188
|
+
return this.track(async () => {
|
|
189
|
+
const key = AgentModelPreferences.key(this.input, entityId);
|
|
190
|
+
const rows = this.agentRows();
|
|
191
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(key))}`;
|
|
192
|
+
const found = await rows.preferences.read(slot);
|
|
193
|
+
if (found === undefined)
|
|
194
|
+
return undefined;
|
|
195
|
+
AgentModelPreferences.requireIdentity(found, key);
|
|
196
|
+
return AgentModelPreferences.copy(found.preferences);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Commits Agent state, final audit, and preferences in one native transaction.
|
|
201
|
+
* @param input Captured live Agent transition.
|
|
202
|
+
* @returns Completion after a conditional group commits.
|
|
203
|
+
* @typeParam I Typed Agent identifier.
|
|
204
|
+
* @typeParam S Generated Agent state.
|
|
205
|
+
*/
|
|
206
|
+
commitAgent(input) {
|
|
207
|
+
if (!this.#open)
|
|
208
|
+
return Promise.reject(new Error("Entity commit storage is closed."));
|
|
209
|
+
return this.track(async () => {
|
|
210
|
+
const id = this.input.id.clone(input.entityId);
|
|
211
|
+
const key = AgentModelPreferences.key(this.input, id);
|
|
212
|
+
const expected = input.expectedPreferences === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: AgentModelPreferences.copy(input.expectedPreferences);
|
|
215
|
+
const next = AgentModelPreferences.record(key, input.preferences);
|
|
216
|
+
const mutation = input.entityCommit && EntityCommitSnapshots.capture(input.entityCommit);
|
|
217
|
+
const history = input.historyEntries?.map((entry) => clone(AgentHistoryEntrySchema, entry)) ?? [];
|
|
218
|
+
this.validateAgent(id, input.expectedVersion, mutation);
|
|
219
|
+
const rows = this.agentRows(mutation);
|
|
220
|
+
await this.agentTransaction(rows, id, input.expectedVersion, expected, next, mutation, history);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Acquires every missing native family before publishing retained references.
|
|
225
|
+
* @param mutation Optional Entity mutation requiring history families.
|
|
226
|
+
* @returns Native handles for one Agent commit.
|
|
227
|
+
*/
|
|
228
|
+
agentRows(mutation) {
|
|
229
|
+
const acquired = [];
|
|
230
|
+
try {
|
|
231
|
+
const rows = this.acquireAgentRows(mutation, acquired);
|
|
232
|
+
this.retainAgentRows(rows);
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const failures = this.closeGroup(acquired);
|
|
237
|
+
if (failures.length)
|
|
238
|
+
throw new AggregateError([error, ...failures], "Datastore Agent family acquisition failed.");
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Acquires only missing native Agent families without publishing partial acquisition.
|
|
244
|
+
* @param mutation Optional Entity mutation.
|
|
245
|
+
* @param acquired Collects newly opened handles for rollback.
|
|
246
|
+
* @returns Complete native family group.
|
|
247
|
+
*/
|
|
248
|
+
acquireAgentRows(mutation, acquired) {
|
|
249
|
+
const current = this.native(this.#current ?? this.openAgentRecord(this.input.recordSpec, undefined, acquired));
|
|
250
|
+
const { states, diagnostics, events } = this.agentMutationRows(mutation, acquired);
|
|
251
|
+
const history = this.#history ??
|
|
252
|
+
this.openAgentRecord(AgentHistoryRecords.spec((value) => AgentHistoryHash.value(value)), AgentHistoryRecords.group, acquired);
|
|
253
|
+
const preferences = this.#preferences ??
|
|
254
|
+
this.openAgentRecord(AgentModelPreferences.spec((value) => AgentHistoryHash.value(value)), AgentModelPreferences.group, acquired);
|
|
255
|
+
return {
|
|
256
|
+
current,
|
|
257
|
+
states,
|
|
258
|
+
diagnostics,
|
|
259
|
+
events,
|
|
260
|
+
history,
|
|
261
|
+
preferences,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Acquires mutation families needed by the admitted Agent turn.
|
|
266
|
+
* @param mutation Optional changed Entity mutation.
|
|
267
|
+
* @param acquired Newly opened handles retained for rollback.
|
|
268
|
+
* @returns Optional mutation families.
|
|
269
|
+
*/
|
|
270
|
+
agentMutationRows(mutation, acquired) {
|
|
271
|
+
const stateLayout = stateHistorySpec(this.input.stateSchema);
|
|
272
|
+
const eventLayout = eventHistorySpec(this.input.stateSchema);
|
|
273
|
+
const states = mutation?.states?.length
|
|
274
|
+
? this.native(this.#states ?? this.openAgentRecord(stateLayout.spec, stateLayout.group, acquired))
|
|
275
|
+
: undefined;
|
|
276
|
+
const diagnostics = mutation?.diagnostics?.length
|
|
277
|
+
? this.native(this.#diagnostics ?? this.openAgentRecord(eventLayout.spec, eventLayout.group, acquired))
|
|
118
278
|
: undefined;
|
|
119
|
-
const
|
|
120
|
-
? this.
|
|
279
|
+
const events = mutation?.events?.length
|
|
280
|
+
? this.native(this.#events ??
|
|
281
|
+
this.openAgentRecord(eventStoreRecordSpec, EventStoreGroups.forContext(this.input.context.name), acquired))
|
|
121
282
|
: undefined;
|
|
122
|
-
|
|
283
|
+
return { states, diagnostics, events };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Acquires one native record family and records its rollback obligation.
|
|
287
|
+
* @param spec Declarative record layout.
|
|
288
|
+
* @param group Optional physical family.
|
|
289
|
+
* @param acquired Newly opened handles.
|
|
290
|
+
* @returns Native record family.
|
|
291
|
+
* @typeParam Id Typed record identifier.
|
|
292
|
+
* @typeParam R Generated record.
|
|
293
|
+
*/
|
|
294
|
+
openAgentRecord(spec, group, acquired) {
|
|
295
|
+
const port = this.openRecords(spec, group);
|
|
296
|
+
acquired.push(port);
|
|
297
|
+
return this.native(port);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Stores the complete acquired Agent group after every family succeeds.
|
|
301
|
+
* @param rows Complete native family group.
|
|
302
|
+
*/
|
|
303
|
+
retainAgentRows(rows) {
|
|
304
|
+
this.#current ??= rows.current;
|
|
305
|
+
this.#states ??= rows.states;
|
|
306
|
+
this.#diagnostics ??= rows.diagnostics;
|
|
307
|
+
this.#events ??= rows.events;
|
|
308
|
+
this.#history ??= rows.history;
|
|
309
|
+
this.#preferences ??= rows.preferences;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Checks the built-in native transaction capability.
|
|
313
|
+
* @param port Selected record family.
|
|
314
|
+
* @returns Native record family.
|
|
315
|
+
* @typeParam Id Typed record identifier.
|
|
316
|
+
* @typeParam R Generated record.
|
|
317
|
+
*/
|
|
318
|
+
native(port) {
|
|
319
|
+
if (!(port instanceof DatastoreRecordStorage))
|
|
320
|
+
throw new Error("Live Agent commit requires native Datastore transactions.");
|
|
321
|
+
return port;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Validates the Entity scope and exactly one next Version before I/O.
|
|
325
|
+
* @param id Captured Agent ID.
|
|
326
|
+
* @param version Initial Entity Version.
|
|
327
|
+
* @param mutation Optional prepared Entity mutation.
|
|
328
|
+
* @typeParam I Typed Entity identifier.
|
|
329
|
+
* @typeParam S Generated Entity state.
|
|
330
|
+
*/
|
|
331
|
+
validateAgent(id, version, mutation) {
|
|
332
|
+
if (!Number.isSafeInteger(version) || version < 0)
|
|
333
|
+
throw new TypeError("Agent commit requires a valid initial Entity Version.");
|
|
334
|
+
if (mutation === undefined)
|
|
335
|
+
return;
|
|
336
|
+
this.validate(mutation);
|
|
337
|
+
if (this.input.id.key(id) !== this.input.id.key(mutation.entityId) ||
|
|
338
|
+
mutation.next.version?.number !== version + 1)
|
|
339
|
+
throw new Error("Agent commit requires the same Entity ID and one new Version.");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Executes a captured Agent transition in one native transaction.
|
|
343
|
+
* @param rows Retained native families.
|
|
344
|
+
* @param id Captured Agent ID.
|
|
345
|
+
* @param version Initial Entity version.
|
|
346
|
+
* @param expected Initial preference image.
|
|
347
|
+
* @param next Complete replacement preferences.
|
|
348
|
+
* @param mutation Optional Entity mutation.
|
|
349
|
+
* @param history Final audit entries.
|
|
350
|
+
* @typeParam I Typed Entity identifier.
|
|
351
|
+
* @typeParam S Generated Entity state.
|
|
352
|
+
* @returns Completion after the native transaction settles.
|
|
353
|
+
*/
|
|
354
|
+
async agentTransaction(rows, id, version, expected, next, mutation, history) {
|
|
355
|
+
const tx = rows.preferences.transaction();
|
|
123
356
|
try {
|
|
124
|
-
await
|
|
357
|
+
await tx.run();
|
|
358
|
+
const staged = [];
|
|
359
|
+
await this.agentLocked(tx, staged, rows, id, version, expected, next, mutation, history);
|
|
360
|
+
NativeTransactions.validateRows(staged);
|
|
361
|
+
for (const row of staged)
|
|
362
|
+
tx.save(row);
|
|
363
|
+
await tx.commit();
|
|
125
364
|
}
|
|
126
|
-
|
|
127
|
-
|
|
365
|
+
catch (error) {
|
|
366
|
+
await tx.rollback().catch(() => undefined);
|
|
367
|
+
throw error;
|
|
128
368
|
}
|
|
129
369
|
}
|
|
130
370
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* @
|
|
134
|
-
* @
|
|
135
|
-
* @param
|
|
136
|
-
* @param
|
|
137
|
-
* @param
|
|
138
|
-
* @param
|
|
139
|
-
* @param
|
|
140
|
-
* @
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
371
|
+
* Checks both current images and stages the complete final group.
|
|
372
|
+
* @param tx Native transaction.
|
|
373
|
+
* @param staged Rows saved only after all checks pass.
|
|
374
|
+
* @param rows Retained native families.
|
|
375
|
+
* @param id Captured Agent identifier.
|
|
376
|
+
* @param version Initial Entity Version.
|
|
377
|
+
* @param expected Initial preference image.
|
|
378
|
+
* @param next Replacement preference record.
|
|
379
|
+
* @param mutation Optional Entity mutation.
|
|
380
|
+
* @param history Final audit entries.
|
|
381
|
+
* @typeParam I Typed Entity identifier.
|
|
382
|
+
* @typeParam S Generated Entity state.
|
|
383
|
+
* @returns Completion after all conditional rows are staged.
|
|
384
|
+
*/
|
|
385
|
+
async agentLocked(tx, staged, rows, id, version, expected, next, mutation, history) {
|
|
386
|
+
if (next.key === undefined)
|
|
387
|
+
throw new Error("Agent preference identity is missing.");
|
|
388
|
+
const slot = `v1:${AgentHistoryHash.value(AgentModelPreferences.text(next.key))}`;
|
|
389
|
+
const prior = await this.agentRead(tx, rows.preferences, slot);
|
|
390
|
+
if (prior !== undefined)
|
|
391
|
+
AgentModelPreferences.requireIdentity(prior, next.key);
|
|
392
|
+
if (!AgentModelPreferences.same(prior?.preferences, expected))
|
|
393
|
+
throw new Error("Agent model preferences changed during the live turn.");
|
|
394
|
+
const current = await this.agentRead(tx, rows.current, id);
|
|
395
|
+
if ((current?.version?.number ?? 0) !== version)
|
|
396
|
+
throw new Error("Agent initial Entity Version is no longer current.");
|
|
397
|
+
for (const record of mutation?.states ?? [])
|
|
398
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.states), record);
|
|
399
|
+
for (const event of mutation?.diagnostics ?? [])
|
|
400
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.diagnostics), event);
|
|
401
|
+
for (const event of mutation?.events ?? [])
|
|
402
|
+
await this.agentImmutable(tx, staged, this.requiredNative(rows.events), event);
|
|
403
|
+
if (mutation !== undefined)
|
|
404
|
+
staged.push(rows.current.transactionEntity(mutation.next));
|
|
405
|
+
await this.appendAgentAudit(tx, staged, rows.history, id, history);
|
|
406
|
+
staged.push(rows.preferences.transactionEntity(next));
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Records final Agent audit rows in the admitted native transaction.
|
|
410
|
+
* @param history Final Agent audit entries.
|
|
411
|
+
* @param id Captured typed Agent identifier.
|
|
412
|
+
* @param staged Rows staged for atomic commit.
|
|
413
|
+
* @param storage Retained provider record family.
|
|
414
|
+
* @param tx Active native transaction.
|
|
415
|
+
* @returns Completion after final audit rows are staged.
|
|
416
|
+
*/
|
|
417
|
+
async appendAgentAudit(tx, staged, storage, id, history) {
|
|
418
|
+
for (const entry of history) {
|
|
419
|
+
if (AgentHistoryKeys.indexValue(AgentHistoryKeys.fromEntry(entry)).length > 1500)
|
|
420
|
+
throw new RangeError("Agent history order exceeds Datastore indexed-value capacity.");
|
|
421
|
+
const record = AgentHistoryRecords.record(this.input.stateSchema.typeName, this.input.id.key(id), entry);
|
|
422
|
+
await this.agentImmutable(tx, staged, storage, record);
|
|
157
423
|
}
|
|
158
|
-
throw new Error("Datastore transaction did not complete.");
|
|
159
424
|
}
|
|
160
425
|
/**
|
|
161
|
-
*
|
|
426
|
+
* Reads a native row within the Agent transaction.
|
|
427
|
+
* @param tx Active native transaction.
|
|
428
|
+
* @param storage Retained record family.
|
|
429
|
+
* @param id Typed record identifier.
|
|
430
|
+
* @returns Decoded row or absence.
|
|
431
|
+
* @typeParam Id Typed record identifier.
|
|
432
|
+
* @typeParam R Generated record.
|
|
433
|
+
*/
|
|
434
|
+
async agentRead(tx, storage, id) {
|
|
435
|
+
const raw = NativeTransactions.first((await tx.get(storage.transactionKey(id), { wrapNumbers: true })));
|
|
436
|
+
return raw === undefined ? undefined : storage.decodeTransactionEntity(raw);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Checks one immutable final record before staging it.
|
|
440
|
+
* @param tx Active native transaction.
|
|
441
|
+
* @param staged Collected native rows.
|
|
442
|
+
* @param storage Retained record family.
|
|
443
|
+
* @param record Proposed immutable record.
|
|
444
|
+
* @typeParam Id Typed record identifier.
|
|
445
|
+
* @typeParam R Generated record.
|
|
446
|
+
* @returns Completion after the immutable row is checked and staged.
|
|
447
|
+
*/
|
|
448
|
+
async agentImmutable(tx, staged, storage, record) {
|
|
449
|
+
const raw = NativeTransactions.first((await tx.get(storage.transactionEntity(record).key, { wrapNumbers: true })));
|
|
450
|
+
if (raw === undefined)
|
|
451
|
+
staged.push(storage.transactionEntity(record));
|
|
452
|
+
else if (!storage.matchesTransactionEntity(raw, record))
|
|
453
|
+
throw new Error("Immutable Agent history record has divergent content.");
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Checks an enabled native history family.
|
|
457
|
+
* @param storage Optional native family.
|
|
458
|
+
* @returns Enabled native record family.
|
|
459
|
+
* @typeParam Id Typed record ID.
|
|
460
|
+
* @typeParam R Generated record.
|
|
461
|
+
*/
|
|
462
|
+
requiredNative(storage) {
|
|
463
|
+
if (storage === undefined)
|
|
464
|
+
throw new Error("Agent Entity history storage is disabled.");
|
|
465
|
+
return storage;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Persists an admitted captured mutation and one safe retry.
|
|
162
469
|
*
|
|
163
|
-
* @
|
|
164
|
-
* @
|
|
165
|
-
* @
|
|
166
|
-
* @
|
|
167
|
-
* @param diagnostics Provides diagnostic history when enabled.
|
|
168
|
-
* @param events Provides delivery Event storage.
|
|
169
|
-
* @returns Deduplicated rows for the transaction.
|
|
470
|
+
* @param prepared The immutable prepared mutation.
|
|
471
|
+
* @returns Completion after the captured mutation is persisted.
|
|
472
|
+
* @typeParam I Entity identifier or record type.
|
|
473
|
+
* @typeParam S Generated Entity state message type.
|
|
170
474
|
*/
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
475
|
+
async persist(prepared) {
|
|
476
|
+
const [current, states, diagnostics, events] = this.openCommit(prepared);
|
|
477
|
+
try {
|
|
478
|
+
await this.apply(prepared, current, states, diagnostics, events);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
if (!this.retryable(error))
|
|
482
|
+
throw error;
|
|
483
|
+
await this.apply(prepared, current, states, diagnostics, events);
|
|
484
|
+
}
|
|
178
485
|
}
|
|
179
486
|
/**
|
|
180
|
-
*
|
|
487
|
+
* Acquires only missing requested families and rolls back a failed group.
|
|
181
488
|
*
|
|
182
|
-
* @
|
|
183
|
-
* @
|
|
184
|
-
* @
|
|
185
|
-
* @
|
|
489
|
+
* @param input The captured Entity persistence input.
|
|
490
|
+
* @returns The retained records required by this commit.
|
|
491
|
+
* @typeParam I Entity identifier or record type.
|
|
492
|
+
* @typeParam S Generated Entity state message type.
|
|
186
493
|
*/
|
|
187
|
-
|
|
188
|
-
const
|
|
494
|
+
openCommit(input) {
|
|
495
|
+
const acquired = [];
|
|
189
496
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
497
|
+
const current = this.#current ?? this.openRecords(this.input.recordSpec);
|
|
498
|
+
if (this.#current === undefined)
|
|
499
|
+
acquired.push(current);
|
|
500
|
+
const stateLayout = stateHistorySpec(this.input.stateSchema);
|
|
501
|
+
const diagnosticLayout = eventHistorySpec(this.input.stateSchema);
|
|
502
|
+
const states = input.states?.length
|
|
503
|
+
? (this.#states ?? this.openRecords(stateLayout.spec, stateLayout.group))
|
|
504
|
+
: undefined;
|
|
505
|
+
if (states !== undefined && this.#states === undefined)
|
|
506
|
+
acquired.push(states);
|
|
507
|
+
const diagnostics = input.diagnostics?.length
|
|
508
|
+
? (this.#diagnostics ?? this.openRecords(diagnosticLayout.spec, diagnosticLayout.group))
|
|
509
|
+
: undefined;
|
|
510
|
+
if (diagnostics !== undefined && this.#diagnostics === undefined)
|
|
511
|
+
acquired.push(diagnostics);
|
|
512
|
+
const events = input.events?.length
|
|
513
|
+
? (this.#events ??
|
|
514
|
+
this.openRecords(eventStoreRecordSpec, EventStoreGroups.forContext(this.input.context.name)))
|
|
515
|
+
: undefined;
|
|
516
|
+
if (events !== undefined && this.#events === undefined)
|
|
517
|
+
acquired.push(events);
|
|
518
|
+
this.#current = current;
|
|
519
|
+
this.#states ??= states;
|
|
520
|
+
this.#diagnostics ??= diagnostics;
|
|
521
|
+
this.#events ??= events;
|
|
522
|
+
return [current, states, diagnostics, events];
|
|
195
523
|
}
|
|
196
524
|
catch (error) {
|
|
197
|
-
|
|
525
|
+
const failures = this.closeGroup(acquired);
|
|
526
|
+
if (failures.length)
|
|
527
|
+
throw new AggregateError([error, ...failures], "Datastore family acquisition failed.");
|
|
198
528
|
throw error;
|
|
199
529
|
}
|
|
200
530
|
}
|
|
201
531
|
/**
|
|
202
|
-
*
|
|
532
|
+
* Writes independent record families in Entity commit order.
|
|
203
533
|
*
|
|
204
|
-
* @
|
|
205
|
-
* @
|
|
206
|
-
* @
|
|
534
|
+
* @typeParam I Entity identifier type.
|
|
535
|
+
* @typeParam S Entity state message type.
|
|
536
|
+
* @param input Supplies immutable prepared records.
|
|
537
|
+
* @param current Stores current Entity state.
|
|
538
|
+
* @param states Stores state history when requested.
|
|
539
|
+
* @param diagnostics Stores diagnostic Events when requested.
|
|
540
|
+
* @param events Stores delivery Events when requested.
|
|
541
|
+
* @returns Completion after every requested record write.
|
|
207
542
|
*/
|
|
208
|
-
async
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
543
|
+
async apply(input, current, states, diagnostics, events) {
|
|
544
|
+
if (states !== undefined)
|
|
545
|
+
for (const record of input.states ?? [])
|
|
546
|
+
await immutable(states, record);
|
|
547
|
+
if (diagnostics !== undefined)
|
|
548
|
+
for (const event of input.diagnostics ?? [])
|
|
549
|
+
await immutable(diagnostics, event);
|
|
550
|
+
if (events !== undefined)
|
|
551
|
+
for (const event of input.events ?? [])
|
|
552
|
+
await immutable(events, event);
|
|
553
|
+
const previous = await current.read(input.entityId);
|
|
554
|
+
if (previous === undefined ||
|
|
555
|
+
!same(toBinary(EntityRecordSchema, previous), toBinary(EntityRecordSchema, input.next)))
|
|
556
|
+
await current.write(input.next);
|
|
217
557
|
}
|
|
218
558
|
/**
|
|
219
|
-
*
|
|
559
|
+
* Determines whether a Datastore abort occurred through provider error causes.
|
|
220
560
|
*
|
|
221
|
-
* @param
|
|
222
|
-
* @
|
|
561
|
+
* @param error Supplies the operation failure.
|
|
562
|
+
* @returns Whether one captured-input replay is safe.
|
|
223
563
|
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
564
|
+
retryable(error) {
|
|
565
|
+
let current = error;
|
|
566
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
567
|
+
if (typeof current !== "object" || current === null)
|
|
568
|
+
return false;
|
|
569
|
+
const failure = current;
|
|
570
|
+
if (failure.code === 10)
|
|
571
|
+
return true;
|
|
572
|
+
current = failure.cause;
|
|
229
573
|
}
|
|
574
|
+
return false;
|
|
230
575
|
}
|
|
231
576
|
/**
|
|
232
|
-
*
|
|
577
|
+
* Closes this commit storage to further commits.
|
|
233
578
|
*
|
|
234
|
-
* @
|
|
235
|
-
* @param prepared Supplies current and immutable rows.
|
|
236
|
-
* @param values Supplies existing data for affected keys.
|
|
579
|
+
* @returns Completion after admitted work and record cleanup settle.
|
|
237
580
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
581
|
+
close() {
|
|
582
|
+
this.#open = false;
|
|
583
|
+
if (this.#idleOutcome !== undefined)
|
|
584
|
+
return (this.#closing ??=
|
|
585
|
+
this.#idleOutcome.error === undefined
|
|
586
|
+
? Promise.resolve()
|
|
587
|
+
: Promise.reject(this.#idleOutcome.error));
|
|
588
|
+
return (this.#closing ??= this.drain());
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Closes only when no commit remains admitted.
|
|
592
|
+
*/
|
|
593
|
+
closeIdle() {
|
|
594
|
+
this.assertIdle();
|
|
595
|
+
if (this.#idleOutcome !== undefined) {
|
|
596
|
+
if (this.#idleOutcome.error !== undefined)
|
|
597
|
+
throw this.#idleOutcome.error;
|
|
598
|
+
return;
|
|
243
599
|
}
|
|
600
|
+
this.#open = false;
|
|
601
|
+
const failure = this.closeRetained();
|
|
602
|
+
this.#idleOutcome = failure === undefined ? {} : { error: failure };
|
|
603
|
+
this.onClose();
|
|
604
|
+
if (failure !== undefined)
|
|
605
|
+
throw failure;
|
|
244
606
|
}
|
|
245
607
|
/**
|
|
246
|
-
*
|
|
608
|
+
* Rejects synchronous disposal while asynchronous work remains admitted.
|
|
609
|
+
*/
|
|
610
|
+
assertIdle() {
|
|
611
|
+
if (this.#active.size || (this.#closing !== undefined && this.#idleOutcome === undefined))
|
|
612
|
+
throw new Error("Entity commit cannot close idle while work is active.");
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Waits for every admitted commit before completing close.
|
|
247
616
|
*
|
|
248
|
-
* @
|
|
249
|
-
* @param states Supplies state history when enabled.
|
|
250
|
-
* @param diagnostics Supplies diagnostic history when enabled.
|
|
251
|
-
* @param events Supplies delivery Event storage.
|
|
617
|
+
* @returns Completion after admitted work and cleanup settle.
|
|
252
618
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
619
|
+
async drain() {
|
|
620
|
+
await Promise.allSettled([...this.#active]);
|
|
621
|
+
const failure = this.closeRetained();
|
|
622
|
+
this.onClose();
|
|
623
|
+
if (failure !== undefined)
|
|
624
|
+
throw failure;
|
|
258
625
|
}
|
|
259
626
|
/**
|
|
260
|
-
* Closes
|
|
627
|
+
* Closes every retained record once after admitted work settles.
|
|
628
|
+
*
|
|
629
|
+
* @returns A cleanup failure, if any.
|
|
261
630
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
631
|
+
closeRetained() {
|
|
632
|
+
const ports = [
|
|
633
|
+
this.#events,
|
|
634
|
+
this.#diagnostics,
|
|
635
|
+
this.#states,
|
|
636
|
+
this.#history,
|
|
637
|
+
this.#preferences,
|
|
638
|
+
this.#current,
|
|
639
|
+
].filter((port) => port !== undefined);
|
|
640
|
+
this.#events = undefined;
|
|
641
|
+
this.#diagnostics = undefined;
|
|
642
|
+
this.#states = undefined;
|
|
643
|
+
this.#history = undefined;
|
|
644
|
+
this.#preferences = undefined;
|
|
645
|
+
this.#current = undefined;
|
|
646
|
+
const failures = this.closeGroup(ports);
|
|
647
|
+
if (failures.length === 1)
|
|
648
|
+
return failures[0];
|
|
649
|
+
return failures.length
|
|
650
|
+
? new AggregateError(failures, "Datastore commit close failed.")
|
|
651
|
+
: undefined;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Closes every newly acquired handle and collects cleanup failures.
|
|
655
|
+
*
|
|
656
|
+
* @param ports The acquired record handles.
|
|
657
|
+
* @returns Cleanup failures from acquired handles.
|
|
658
|
+
*/
|
|
659
|
+
closeGroup(ports) {
|
|
660
|
+
const failures = [];
|
|
661
|
+
for (const port of ports) {
|
|
662
|
+
try {
|
|
663
|
+
port.close();
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
failures.push(error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return failures;
|
|
264
670
|
}
|
|
265
671
|
/**
|
|
266
672
|
* Validates the Entity, tenant, history, and Event boundaries of a commit.
|
|
@@ -270,15 +676,16 @@ export class DatastoreEntityCommitStorage {
|
|
|
270
676
|
* @param input Supplies the proposed commit.
|
|
271
677
|
*/
|
|
272
678
|
validate(input) {
|
|
273
|
-
if (!this.#open)
|
|
274
|
-
throw new Error("Entity commit storage is closed.");
|
|
275
679
|
if (input.entity.sourceType.typeName !== this.input.sourceType.typeName ||
|
|
276
|
-
|
|
680
|
+
input.entity.stateSchema.typeName !== this.input.stateSchema.typeName ||
|
|
681
|
+
TenantBoundary.of(input.context).key !== TenantBoundary.of(this.input.context).key ||
|
|
682
|
+
TenantBoundary.of(input.entity.context).key !== TenantBoundary.of(this.input.context).key)
|
|
277
683
|
throw new Error("Entity commit handle cannot commit another Entity source or tenant.");
|
|
278
684
|
validateCommitEntityId(input);
|
|
279
|
-
if ((input.states?.length ?? 0) > 0 && !input.entity.stateHistory)
|
|
685
|
+
if ((input.states?.length ?? 0) > 0 && (!this.input.stateHistory || !input.entity.stateHistory))
|
|
280
686
|
throw new Error("Entity commit cannot append state history when it is disabled.");
|
|
281
|
-
if ((input.diagnostics?.length ?? 0) > 0 &&
|
|
687
|
+
if ((input.diagnostics?.length ?? 0) > 0 &&
|
|
688
|
+
(!this.input.eventHistory || !input.entity.eventHistory))
|
|
282
689
|
throw new Error("Entity commit cannot append event history when it is disabled.");
|
|
283
690
|
validateEvents(input.events ?? []);
|
|
284
691
|
}
|
|
@@ -404,7 +811,7 @@ class StateHistory {
|
|
|
404
811
|
const found = page.entries[0]?.record;
|
|
405
812
|
return found?.state === undefined
|
|
406
813
|
? undefined
|
|
407
|
-
:
|
|
814
|
+
: AnyPacker.unpack(found.state, this.input.stateSchema);
|
|
408
815
|
}
|
|
409
816
|
/**
|
|
410
817
|
* Stores no more than the newest state-history records for an Entity.
|
|
@@ -416,6 +823,8 @@ class StateHistory {
|
|
|
416
823
|
async trim(id, keep) {
|
|
417
824
|
this.requireOpen();
|
|
418
825
|
requireKeep(keep);
|
|
826
|
+
let retained = 0;
|
|
827
|
+
let cursor;
|
|
419
828
|
for (;;) {
|
|
420
829
|
const page = await this.provider().queryProviderPage({
|
|
421
830
|
filters: [entityFilter(this.input, id)],
|
|
@@ -424,11 +833,15 @@ class StateHistory {
|
|
|
424
833
|
{ property: "created", direction: "desc" },
|
|
425
834
|
],
|
|
426
835
|
limit: 128,
|
|
836
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
427
837
|
});
|
|
428
|
-
const
|
|
838
|
+
const retainHere = Math.min(page.entries.length, Math.max(0, keep - retained));
|
|
839
|
+
retained += retainHere;
|
|
840
|
+
const removable = page.entries.slice(retainHere);
|
|
429
841
|
await this.provider().deleteProviderEntries(removable);
|
|
430
842
|
if (!page.hasMore)
|
|
431
843
|
break;
|
|
844
|
+
cursor = page.cursor;
|
|
432
845
|
}
|
|
433
846
|
}
|
|
434
847
|
/**
|
|
@@ -617,58 +1030,17 @@ class EventHistory {
|
|
|
617
1030
|
*/
|
|
618
1031
|
async function immutable(records, record) {
|
|
619
1032
|
const id = records.recordSpec.idValueIn(record);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Prepares Datastore rows for one record family.
|
|
630
|
-
*
|
|
631
|
-
* @typeParam I Record identifier type.
|
|
632
|
-
* @typeParam R Stored Protobuf message type.
|
|
633
|
-
* @param storage Provides the record-family handle when enabled.
|
|
634
|
-
* @param records Supplies records to prepare.
|
|
635
|
-
* @param immutable Marks whether existing rows must remain unchanged.
|
|
636
|
-
* @returns The prepared rows for the transaction.
|
|
637
|
-
*/
|
|
638
|
-
function preparedRows(storage, records, immutable) {
|
|
639
|
-
if (storage === undefined) {
|
|
640
|
-
if (records.length > 0)
|
|
641
|
-
throw new Error("Entity commit history is disabled.");
|
|
642
|
-
return [];
|
|
643
|
-
}
|
|
644
|
-
return records.map((record) => ({
|
|
645
|
-
storage,
|
|
646
|
-
record,
|
|
647
|
-
immutable,
|
|
648
|
-
entity: storage.transactionEntity(record),
|
|
649
|
-
}));
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Removes duplicate immutable rows and rejects conflicting content.
|
|
653
|
-
*
|
|
654
|
-
* @typeParam R Prepared row type.
|
|
655
|
-
* @param rows Supplies prepared current and history rows.
|
|
656
|
-
* @returns Rows with duplicate immutable keys removed.
|
|
657
|
-
*/
|
|
658
|
-
function coalesceImmutableRows(rows) {
|
|
659
|
-
const seen = new Map();
|
|
660
|
-
return rows.filter((row) => {
|
|
661
|
-
if (!row.immutable)
|
|
662
|
-
return true;
|
|
663
|
-
const previous = seen.get(keyId(row.entity.key));
|
|
664
|
-
if (previous === undefined) {
|
|
665
|
-
seen.set(keyId(row.entity.key), row);
|
|
666
|
-
return true;
|
|
667
|
-
}
|
|
668
|
-
if (!sameData(previous.entity.data, row.entity.data))
|
|
1033
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1034
|
+
if (await records.compareAndSet(id, undefined, record))
|
|
1035
|
+
return;
|
|
1036
|
+
const existing = await records.read(id);
|
|
1037
|
+
if (existing === undefined)
|
|
1038
|
+
continue;
|
|
1039
|
+
if (!same(toBinary(records.recordSpec.recordType, existing), toBinary(records.recordSpec.recordType, record)))
|
|
669
1040
|
throw new Error("Immutable history record has divergent content.");
|
|
670
|
-
return
|
|
671
|
-
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
throw new Error("Immutable history record was not retained.");
|
|
672
1044
|
}
|
|
673
1045
|
function same(left, right) {
|
|
674
1046
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
@@ -699,61 +1071,6 @@ function numberFilter(property, operator, value) {
|
|
|
699
1071
|
function timestampFilter(property, operator, value) {
|
|
700
1072
|
return { property, operator, value };
|
|
701
1073
|
}
|
|
702
|
-
function first(value) {
|
|
703
|
-
return Array.isArray(value) ? value[0] : undefined;
|
|
704
|
-
}
|
|
705
|
-
function keyId(key) {
|
|
706
|
-
return JSON.stringify(key);
|
|
707
|
-
}
|
|
708
|
-
function uniqueKeys(keys) {
|
|
709
|
-
return [...new Map(keys.map((key) => [keyId(key), key])).values()];
|
|
710
|
-
}
|
|
711
|
-
function sameData(left, right) {
|
|
712
|
-
if (left === undefined || right === undefined)
|
|
713
|
-
return left === right;
|
|
714
|
-
const bytes = left.bytes;
|
|
715
|
-
return (bytes instanceof Uint8Array && right.bytes instanceof Uint8Array && same(bytes, right.bytes));
|
|
716
|
-
}
|
|
717
|
-
function isAborted(error) {
|
|
718
|
-
return typeof error === "object" && error !== null && Reflect.get(error, "code") === 10;
|
|
719
|
-
}
|
|
720
|
-
function entityTransactionError(error) {
|
|
721
|
-
if (error instanceof Error && error.message.startsWith("Immutable history record"))
|
|
722
|
-
return error;
|
|
723
|
-
return new Error("Datastore Entity transaction failed.");
|
|
724
|
-
}
|
|
725
|
-
function abortBackoff(attempt) {
|
|
726
|
-
const delayMs = 20 * (attempt + 1) + Math.floor(Math.random() * 40);
|
|
727
|
-
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Rolls back a failed Datastore transaction when possible.
|
|
731
|
-
*
|
|
732
|
-
* @param transaction Supplies the transaction to roll back.
|
|
733
|
-
* @returns Completion after rollback finishes or fails.
|
|
734
|
-
*/
|
|
735
|
-
async function rollback(transaction) {
|
|
736
|
-
try {
|
|
737
|
-
await transaction.rollback();
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
/* original provider error remains authoritative */
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
function validateCommitSize(rows) {
|
|
744
|
-
if (rows.length > 500)
|
|
745
|
-
throw new Error("Entity commit exceeds the 500-mutation limit.");
|
|
746
|
-
if (uniqueKeys(rows.map((row) => row.key)).length > 25)
|
|
747
|
-
throw new Error("Entity commit exceeds the 25 entity-group limit.");
|
|
748
|
-
const bytes = rows.reduce((total, row) => total +
|
|
749
|
-
Buffer.byteLength(JSON.stringify(row.data, (_key, value) => value instanceof Uint8Array
|
|
750
|
-
? Buffer.from(value).toString("base64")
|
|
751
|
-
: typeof value === "bigint"
|
|
752
|
-
? value.toString()
|
|
753
|
-
: value), "utf8"), 0);
|
|
754
|
-
if (bytes > 9 * 1024 * 1024)
|
|
755
|
-
throw new Error("Entity commit exceeds the transaction payload limit.");
|
|
756
|
-
}
|
|
757
1074
|
function validateEvents(events) {
|
|
758
1075
|
const ids = events.map((event) => event.id?.value);
|
|
759
1076
|
if (ids.some((id) => id === undefined || id.trim().length === 0) ||
|