@spine-event-engine/storage 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 +12 -3
- package/REFERENCE.md +90 -63
- package/dist/entity/agent-history-record-spec.d.ts +8 -0
- package/dist/entity/agent-history-record-spec.d.ts.map +1 -1
- package/dist/entity/agent-history-record-spec.js +12 -1
- package/dist/entity/agent-history-record-spec.js.map +1 -1
- package/dist/entity/agent-history.d.ts +25 -0
- package/dist/entity/agent-history.d.ts.map +1 -1
- package/dist/entity/agent-history.js.map +1 -1
- package/dist/entity/history-conformance.d.ts +38 -8
- package/dist/entity/history-conformance.d.ts.map +1 -1
- package/dist/entity/history-conformance.js +103 -58
- package/dist/entity/history-conformance.js.map +1 -1
- package/dist/event/event-store.d.ts +80 -74
- package/dist/event/event-store.d.ts.map +1 -1
- package/dist/event/event-store.js +264 -264
- package/dist/event/event-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-model-preferences.d.ts +76 -0
- package/dist/internal/agent-model-preferences.d.ts.map +1 -0
- package/dist/internal/agent-model-preferences.js +133 -0
- package/dist/internal/agent-model-preferences.js.map +1 -0
- package/dist/internal/entity-commit.d.ts +92 -17
- package/dist/internal/entity-commit.d.ts.map +1 -1
- package/dist/internal/entity-commit.js +45 -2
- package/dist/internal/entity-commit.js.map +1 -1
- package/dist/internal/entity-history.d.ts +1 -7
- package/dist/internal/entity-history.d.ts.map +1 -1
- package/dist/internal/entity-history.js +0 -5
- package/dist/internal/entity-history.js.map +1 -1
- package/dist/internal/event-store.d.ts +1 -1
- package/dist/internal/event-store.d.ts.map +1 -1
- package/dist/internal/event-store.js +1 -1
- package/dist/internal/event-store.js.map +1 -1
- package/dist/internal/storage-preparation.d.ts +88 -0
- package/dist/internal/storage-preparation.d.ts.map +1 -0
- package/dist/internal/storage-preparation.js +41 -0
- package/dist/internal/storage-preparation.js.map +1 -0
- package/dist/internal/tenancy.d.ts +7 -0
- package/dist/internal/tenancy.d.ts.map +1 -1
- package/dist/memory/in-memory-agent-history.d.ts.map +1 -1
- package/dist/memory/in-memory-agent-history.js +22 -7
- package/dist/memory/in-memory-agent-history.js.map +1 -1
- package/dist/memory/in-memory-entity-commit.d.ts +237 -42
- package/dist/memory/in-memory-entity-commit.d.ts.map +1 -1
- package/dist/memory/in-memory-entity-commit.js +529 -325
- package/dist/memory/in-memory-entity-commit.js.map +1 -1
- package/dist/memory/in-memory-entity-history.d.ts.map +1 -1
- package/dist/memory/in-memory-entity-history.js +4 -3
- package/dist/memory/in-memory-entity-history.js.map +1 -1
- package/dist/memory/in-memory-storage-backend.d.ts +33 -1
- package/dist/memory/in-memory-storage-backend.d.ts.map +1 -1
- package/dist/memory/in-memory-storage-backend.js +118 -1
- package/dist/memory/in-memory-storage-backend.js.map +1 -1
- package/dist/memory/in-memory-storage-factory.d.ts +72 -13
- package/dist/memory/in-memory-storage-factory.d.ts.map +1 -1
- package/dist/memory/in-memory-storage-factory.js +199 -30
- package/dist/memory/in-memory-storage-factory.js.map +1 -1
- package/dist/memory/tenant-records.d.ts +1 -1
- package/dist/memory/tenant-records.d.ts.map +1 -1
- package/dist/memory/tenant-records.js +3 -2
- package/dist/memory/tenant-records.js.map +1 -1
- package/dist/provider.d.ts +2 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +2 -1
- package/dist/provider.js.map +1 -1
- package/dist/record/record-query.d.ts +10 -2
- package/dist/record/record-query.d.ts.map +1 -1
- package/dist/record/record-query.js +9 -0
- package/dist/record/record-query.js.map +1 -1
- package/dist/storage/storage-factory.d.ts +27 -4
- package/dist/storage/storage-factory.d.ts.map +1 -1
- package/dist/storage/storage-factory.js +30 -1
- package/dist/storage/storage-factory.js.map +1 -1
- package/dist/storage/storage.d.ts +2 -1
- package/dist/storage/storage.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/dist/entity/agent-execution-record-spec.d.ts +0 -87
- package/dist/entity/agent-execution-record-spec.d.ts.map +0 -1
- package/dist/entity/agent-execution-record-spec.js +0 -230
- package/dist/entity/agent-execution-record-spec.js.map +0 -1
- package/dist/entity/agent-execution-sizes.d.ts +0 -30
- package/dist/entity/agent-execution-sizes.d.ts.map +0 -1
- package/dist/entity/agent-execution-sizes.js +0 -51
- package/dist/entity/agent-execution-sizes.js.map +0 -1
- package/dist/entity/agent-execution-transitions.d.ts +0 -71
- package/dist/entity/agent-execution-transitions.d.ts.map +0 -1
- package/dist/entity/agent-execution-transitions.js +0 -182
- package/dist/entity/agent-execution-transitions.js.map +0 -1
- package/dist/entity/agent-execution-values.d.ts +0 -142
- package/dist/entity/agent-execution-values.d.ts.map +0 -1
- package/dist/entity/agent-execution-values.js +0 -250
- package/dist/entity/agent-execution-values.js.map +0 -1
- package/dist/entity/agent-execution.d.ts +0 -234
- package/dist/entity/agent-execution.d.ts.map +0 -1
- package/dist/entity/agent-execution.js +0 -15
- package/dist/entity/agent-execution.js.map +0 -1
- package/dist/internal/agent-execution.d.ts +0 -51
- package/dist/internal/agent-execution.d.ts.map +0 -1
- package/dist/internal/agent-execution.js +0 -53
- package/dist/internal/agent-execution.js.map +0 -1
- package/dist/internal/delivery-cleanup.d.ts +0 -99
- package/dist/internal/delivery-cleanup.d.ts.map +0 -1
- package/dist/internal/delivery-cleanup.js +0 -38
- package/dist/internal/delivery-cleanup.js.map +0 -1
- package/dist/memory/agent-execution-head-index.d.ts +0 -52
- package/dist/memory/agent-execution-head-index.d.ts.map +0 -1
- package/dist/memory/agent-execution-head-index.js +0 -91
- package/dist/memory/agent-execution-head-index.js.map +0 -1
- package/dist/memory/in-memory-agent-execution.d.ts +0 -21
- package/dist/memory/in-memory-agent-execution.d.ts.map +0 -1
- package/dist/memory/in-memory-agent-execution.js +0 -587
- package/dist/memory/in-memory-agent-execution.js.map +0 -1
- package/dist/memory/memory-delivery-cleanup.d.ts +0 -32
- package/dist/memory/memory-delivery-cleanup.d.ts.map +0 -1
- package/dist/memory/memory-delivery-cleanup.js +0 -61
- package/dist/memory/memory-delivery-cleanup.js.map +0 -1
|
@@ -11,35 +11,94 @@
|
|
|
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, create,
|
|
14
|
+
import { clone, create, equals, ScalarType } from "@bufbuild/protobuf";
|
|
15
15
|
import { TimestampSchema } from "@bufbuild/protobuf/wkt";
|
|
16
16
|
import { EventIdSchema, EventSchema, TenantIdSchema } from "@spine-event-engine/proto";
|
|
17
17
|
import { RecordColumn } from "../record/record-column.js";
|
|
18
18
|
import { ColumnTypes } from "../record/column-type.js";
|
|
19
19
|
import { RecordSpec } from "../record/record-spec.js";
|
|
20
|
+
import { StorageGroup } from "../record/storage-group.js";
|
|
20
21
|
import { TenantBoundary } from "../internal/tenancy.js";
|
|
21
|
-
const
|
|
22
|
+
const idleClosers = new WeakMap();
|
|
23
|
+
const eventStoreSelections = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Stable physical Event record family for one logical context and store role.
|
|
26
|
+
*/
|
|
27
|
+
export const EventStoreGroups = Object.freeze({
|
|
28
|
+
/**
|
|
29
|
+
* Selects the readable Bounded Context name and retains the Event Store role.
|
|
30
|
+
*
|
|
31
|
+
* @param contextName Framework-validated logical context name.
|
|
32
|
+
* @param role Domain or paired System Event store.
|
|
33
|
+
* @returns Stable physical Event family selector.
|
|
34
|
+
*/
|
|
35
|
+
forContext(contextName, role = "domain") {
|
|
36
|
+
const group = new StorageGroup(contextName);
|
|
37
|
+
eventStoreSelections.set(group, { name: contextName, role });
|
|
38
|
+
return group;
|
|
39
|
+
},
|
|
40
|
+
identity(group) {
|
|
41
|
+
const selection = eventStoreSelections.get(group);
|
|
42
|
+
return selection === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: JSON.stringify(["EventStore", selection.role, selection.name]);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
22
47
|
/**
|
|
23
48
|
* Framework event store backed by record storage.
|
|
24
49
|
*
|
|
25
|
-
* Snapshots input
|
|
26
|
-
*
|
|
27
|
-
*
|
|
50
|
+
* Snapshots input Events and rejects missing, blank, or repeated IDs within a
|
|
51
|
+
* batch. Immutable append accepts identical complete stored Events and leaves
|
|
52
|
+
* completed prefixes when a later insert fails. Admission methods reject every
|
|
53
|
+
* existing ID, including an identical Event.
|
|
28
54
|
*/
|
|
29
55
|
export class EventStore {
|
|
56
|
+
/**
|
|
57
|
+
* Selected Event Store context.
|
|
58
|
+
*/
|
|
30
59
|
#context;
|
|
60
|
+
/**
|
|
61
|
+
* Opens Event record-family handles.
|
|
62
|
+
*/
|
|
31
63
|
#factory;
|
|
64
|
+
/**
|
|
65
|
+
* Record family selected by the logical context and store role.
|
|
66
|
+
*/
|
|
67
|
+
#group;
|
|
68
|
+
/**
|
|
69
|
+
* Retains one record storage per complete tenant boundary.
|
|
70
|
+
*/
|
|
71
|
+
#storages = new Map();
|
|
72
|
+
/**
|
|
73
|
+
* Tracks admitted physical storage groups until their complete operation settles.
|
|
74
|
+
*/
|
|
75
|
+
#active = new Set();
|
|
76
|
+
/**
|
|
77
|
+
* Reuses one asynchronous close result across repeated calls.
|
|
78
|
+
*/
|
|
79
|
+
#closing;
|
|
80
|
+
/**
|
|
81
|
+
* Retains the terminal result of synchronous assembly cleanup.
|
|
82
|
+
*/
|
|
83
|
+
#idleOutcome;
|
|
84
|
+
/**
|
|
85
|
+
* Whether this Event Store accepts operations.
|
|
86
|
+
*/
|
|
32
87
|
#open = true;
|
|
33
88
|
/**
|
|
34
89
|
* Creates an event store for one storage context.
|
|
35
90
|
*
|
|
36
91
|
* @param context Specifies the storage context.
|
|
37
92
|
* @param factory Creates the backing record storage.
|
|
93
|
+
* @param group Selects a separate Event record family when supplied.
|
|
38
94
|
*/
|
|
39
|
-
constructor(context, factory) {
|
|
95
|
+
constructor(context, factory, group) {
|
|
40
96
|
this.#context = EventContexts.base(context);
|
|
41
97
|
this.#factory = factory;
|
|
42
|
-
|
|
98
|
+
this.#group = group ?? EventStoreGroups.forContext(this.#context.name);
|
|
99
|
+
idleClosers.set(this, () => {
|
|
100
|
+
this.#closeIdle();
|
|
101
|
+
});
|
|
43
102
|
}
|
|
44
103
|
/**
|
|
45
104
|
* Returns whether this Event Store still accepts operations.
|
|
@@ -50,13 +109,84 @@ export class EventStore {
|
|
|
50
109
|
return this.#open;
|
|
51
110
|
}
|
|
52
111
|
/**
|
|
53
|
-
*
|
|
112
|
+
* Rejects new work, drains admitted storage groups, and closes retained handles.
|
|
113
|
+
* Repeated calls return the same close result.
|
|
114
|
+
*
|
|
115
|
+
* @returns Completion after all admitted work and storage cleanup settle.
|
|
54
116
|
*/
|
|
55
117
|
close() {
|
|
118
|
+
if (this.#closing !== undefined)
|
|
119
|
+
return this.#closing;
|
|
120
|
+
if (this.#idleOutcome !== undefined) {
|
|
121
|
+
const error = this.#idleOutcome.error;
|
|
122
|
+
return error === undefined ? Promise.resolve() : Promise.reject(error);
|
|
123
|
+
}
|
|
56
124
|
this.#open = false;
|
|
125
|
+
this.#closing = this.drainAndClose();
|
|
126
|
+
return this.#closing;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Closes an idle store during synchronous assembly cleanup.
|
|
130
|
+
* Active storage work is a misuse and leaves all handles open.
|
|
131
|
+
*
|
|
132
|
+
*/
|
|
133
|
+
#closeIdle() {
|
|
134
|
+
if (this.#active.size > 0 || (this.#closing !== undefined && this.#idleOutcome === undefined))
|
|
135
|
+
throw new Error("EventStore closeIdle requires no active storage work.");
|
|
136
|
+
if (this.#idleOutcome !== undefined) {
|
|
137
|
+
if (this.#idleOutcome.error !== undefined)
|
|
138
|
+
throw this.#idleOutcome.error;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.#open = false;
|
|
142
|
+
try {
|
|
143
|
+
this.closeStorages();
|
|
144
|
+
this.#idleOutcome = {};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
this.#idleOutcome = { error: error };
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
57
150
|
}
|
|
58
151
|
/**
|
|
59
|
-
*
|
|
152
|
+
* Waits for admitted physical work and then closes every retained handle.
|
|
153
|
+
*
|
|
154
|
+
* @returns Completion after admitted work and cleanup settle.
|
|
155
|
+
*/
|
|
156
|
+
async drainAndClose() {
|
|
157
|
+
await Promise.allSettled([...this.#active]);
|
|
158
|
+
try {
|
|
159
|
+
this.closeStorages();
|
|
160
|
+
this.#idleOutcome = {};
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
this.#idleOutcome = { error: error };
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Closes every retained handle and reports all cleanup failures.
|
|
169
|
+
*/
|
|
170
|
+
closeStorages() {
|
|
171
|
+
const storages = [...this.#storages.values()];
|
|
172
|
+
this.#storages.clear();
|
|
173
|
+
const errors = [];
|
|
174
|
+
for (const storage of storages) {
|
|
175
|
+
try {
|
|
176
|
+
storage.close();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
errors.push(error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (errors.length === 1)
|
|
183
|
+
throw errors[0];
|
|
184
|
+
if (errors.length > 1)
|
|
185
|
+
throw new AggregateError(errors, "EventStore close failed.");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Validates strict new admission without storing an Event. An identical
|
|
189
|
+
* existing Event still rejects; this precheck does not reserve its ID.
|
|
60
190
|
*
|
|
61
191
|
* @param event Supplies the event to validate.
|
|
62
192
|
* @returns Completes when the event is accepted.
|
|
@@ -67,8 +197,9 @@ export class EventStore {
|
|
|
67
197
|
await this.checkUnique([EventIds.require(record)], context);
|
|
68
198
|
}
|
|
69
199
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
200
|
+
* Checks strict new admission, runs caller acceptance, then atomically inserts
|
|
201
|
+
* under one captured context. Competing callbacks may both run, but only one
|
|
202
|
+
* absent-slot insertion can succeed.
|
|
72
203
|
*
|
|
73
204
|
* @param event Supplies the event to accept.
|
|
74
205
|
* @param onAccepted Runs after uniqueness validation and before append.
|
|
@@ -83,52 +214,30 @@ export class EventStore {
|
|
|
83
214
|
return clone(EventSchema, record);
|
|
84
215
|
}
|
|
85
216
|
/**
|
|
86
|
-
* Writes one
|
|
217
|
+
* Writes one complete Event, accepting an identical immutable retry without
|
|
218
|
+
* overwriting it. A conflicting Event at the same ID rejects.
|
|
87
219
|
*
|
|
88
220
|
* @param event Supplies the event to append.
|
|
89
221
|
* @returns Completes when the event is appended.
|
|
90
222
|
*/
|
|
91
223
|
async append(event) {
|
|
92
224
|
const record = clone(EventSchema, event);
|
|
93
|
-
await this.
|
|
225
|
+
await this.appendImmutable([record], EventContexts.snapshotForEvent(this.#context, record));
|
|
94
226
|
}
|
|
95
227
|
/**
|
|
96
|
-
* Writes generated
|
|
228
|
+
* Writes generated Events in order, accepting identical immutable retries.
|
|
229
|
+
* A later failure retains the completed prefix for retry with the same IDs.
|
|
97
230
|
*
|
|
98
231
|
* @param events Supplies the events to append.
|
|
99
232
|
* @returns Completes when the events are appended.
|
|
100
233
|
*/
|
|
101
234
|
async appendAll(events) {
|
|
235
|
+
this.requireOpen();
|
|
102
236
|
const records = [...events].map((event) => clone(EventSchema, event));
|
|
103
237
|
if (records.length > 0) {
|
|
104
|
-
await this.
|
|
238
|
+
await this.appendImmutable(records, EventContexts.batch(this.#context, records));
|
|
105
239
|
}
|
|
106
240
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Writes generated Spine events and returns a one-shot rollback token.
|
|
109
|
-
*
|
|
110
|
-
* @param events Supplies the events to append.
|
|
111
|
-
* @returns Resolves to the rollback token for this append.
|
|
112
|
-
*/
|
|
113
|
-
async appendAllWithRollback(events) {
|
|
114
|
-
const records = [...events].map((event) => clone(EventSchema, event));
|
|
115
|
-
const ids = records.map((record) => EventIds.require(record));
|
|
116
|
-
const context = records.length === 0 ? undefined : EventContexts.batch(this.#context, records);
|
|
117
|
-
if (context !== undefined) {
|
|
118
|
-
await this.appendUnique(records, context);
|
|
119
|
-
}
|
|
120
|
-
let used = false;
|
|
121
|
-
return Object.freeze({
|
|
122
|
-
rollback: async () => {
|
|
123
|
-
if (used) {
|
|
124
|
-
throw new Error("Event rollback token has already been used.");
|
|
125
|
-
}
|
|
126
|
-
used = true;
|
|
127
|
-
if (context !== undefined)
|
|
128
|
-
await this.deleteIds(ids, context);
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
241
|
/**
|
|
133
242
|
* Reads persisted events through the underlying record-storage query seam.
|
|
134
243
|
*
|
|
@@ -137,208 +246,103 @@ export class EventStore {
|
|
|
137
246
|
*/
|
|
138
247
|
async read(query = {}) {
|
|
139
248
|
this.requireOpen();
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
return await storage.query(query);
|
|
143
|
-
}
|
|
144
|
-
finally {
|
|
145
|
-
storage.close();
|
|
146
|
-
}
|
|
249
|
+
return this.runStorage(EventContexts.snapshot(this.#context), (storage) => storage.query(query));
|
|
147
250
|
}
|
|
148
251
|
/**
|
|
149
|
-
*
|
|
252
|
+
* Writes strictly new Events after caller acceptance.
|
|
150
253
|
*
|
|
151
|
-
* @param records
|
|
152
|
-
* @param context
|
|
153
|
-
* @returns Completion after
|
|
254
|
+
* @param records Supplies validated Event snapshots.
|
|
255
|
+
* @param context Selects their storage boundary.
|
|
256
|
+
* @returns Completion after every strict insertion.
|
|
154
257
|
*/
|
|
155
258
|
async appendUnique(records, context) {
|
|
156
259
|
this.requireOpen();
|
|
157
260
|
const ids = records.map((record) => EventIds.require(record));
|
|
158
261
|
EventIds.rejectDuplicates(ids);
|
|
159
|
-
await
|
|
160
|
-
const storage = this.#factory.createRecordStorage(context, eventStoreRecordSpec);
|
|
161
|
-
try {
|
|
162
|
-
await EventIds.insertUnique(storage, records);
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
storage.close();
|
|
166
|
-
}
|
|
167
|
-
});
|
|
262
|
+
await this.runStorage(context, (storage) => EventIds.insertUnique(storage, records));
|
|
168
263
|
}
|
|
169
264
|
/**
|
|
170
|
-
*
|
|
265
|
+
* Writes captured immutable Events, accepting identical stored retries.
|
|
171
266
|
*
|
|
172
|
-
* @param
|
|
173
|
-
* @param context
|
|
174
|
-
* @returns Completion after
|
|
267
|
+
* @param records Supplies ordered Event snapshots.
|
|
268
|
+
* @param context Selects their single storage boundary.
|
|
269
|
+
* @returns Completion after every immutable Event is confirmed.
|
|
175
270
|
*/
|
|
176
|
-
async
|
|
271
|
+
async appendImmutable(records, context) {
|
|
177
272
|
this.requireOpen();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
for (const id of ids) {
|
|
182
|
-
await storage.delete(id);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
finally {
|
|
186
|
-
storage.close();
|
|
187
|
-
}
|
|
188
|
-
});
|
|
273
|
+
EventIds.rejectDuplicates(records.map((record) => EventIds.require(record)));
|
|
274
|
+
await this.runStorage(context, (storage) => EventIds.insertImmutable(storage, records));
|
|
189
275
|
}
|
|
190
276
|
/**
|
|
191
|
-
* Checks
|
|
277
|
+
* Checks strict admission without writing an Event.
|
|
192
278
|
*
|
|
193
|
-
* @param ids
|
|
194
|
-
* @param context
|
|
195
|
-
* @returns Completion
|
|
279
|
+
* @param ids Supplies IDs that must be absent.
|
|
280
|
+
* @param context Selects the storage boundary.
|
|
281
|
+
* @returns Completion when no ID is stored.
|
|
196
282
|
*/
|
|
197
283
|
async checkUnique(ids, context) {
|
|
198
284
|
this.requireOpen();
|
|
199
285
|
EventIds.rejectDuplicates(ids);
|
|
200
|
-
await
|
|
201
|
-
const storage = this.#factory.createRecordStorage(context, eventStoreRecordSpec);
|
|
202
|
-
try {
|
|
203
|
-
await EventIds.rejectStored(storage, ids);
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
storage.close();
|
|
207
|
-
}
|
|
208
|
-
});
|
|
286
|
+
await this.runStorage(context, (storage) => EventIds.rejectStored(storage, ids));
|
|
209
287
|
}
|
|
210
288
|
/**
|
|
211
|
-
*
|
|
212
|
-
*/
|
|
213
|
-
requireOpen() {
|
|
214
|
-
if (!this.#open)
|
|
215
|
-
throw new Error("EventStore is closed.");
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const EventStoreLocks = Object.freeze({
|
|
219
|
-
queues: new WeakMap(),
|
|
220
|
-
/**
|
|
221
|
-
* Serializes Event ID checks and insertion for one captured Bounded Context and tenant.
|
|
289
|
+
* Executes one complete physical storage operation through settlement.
|
|
222
290
|
*
|
|
223
|
-
* @
|
|
224
|
-
* @param context Captured tenant
|
|
225
|
-
* @param
|
|
226
|
-
* @returns
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const previous = queues.get(key) ?? Promise.resolve();
|
|
233
|
-
const next = previous.then(work, work);
|
|
234
|
-
const stored = next.then(() => undefined, () => undefined);
|
|
235
|
-
queues.set(key, stored);
|
|
291
|
+
* @typeParam T Operation result type.
|
|
292
|
+
* @param context Captured tenant scope.
|
|
293
|
+
* @param onStorage Work admitted against the retained handle.
|
|
294
|
+
* @returns The operation result or its original failure.
|
|
295
|
+
*/
|
|
296
|
+
async runStorage(context, onStorage) {
|
|
297
|
+
const storage = this.storageFor(context);
|
|
298
|
+
const work = Promise.resolve().then(() => onStorage(storage));
|
|
299
|
+
this.#active.add(work);
|
|
236
300
|
try {
|
|
237
|
-
return await
|
|
301
|
+
return await work;
|
|
238
302
|
}
|
|
239
303
|
finally {
|
|
240
|
-
|
|
241
|
-
queues.delete(key);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
/**
|
|
246
|
-
* Returns the serial queue map for one storage provider.
|
|
247
|
-
*
|
|
248
|
-
* @param factory Storage provider used for the Event family.
|
|
249
|
-
* @returns Bounded Context and tenant queues for this provider.
|
|
250
|
-
*/
|
|
251
|
-
queueMap(factory) {
|
|
252
|
-
let queues = this.queues.get(factory);
|
|
253
|
-
if (queues === undefined) {
|
|
254
|
-
queues = new Map();
|
|
255
|
-
this.queues.set(factory, queues);
|
|
304
|
+
this.#active.delete(work);
|
|
256
305
|
}
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
/**
|
|
261
|
-
* Exposes captured Event Store coordination to provider and Agent delivery paths.
|
|
262
|
-
* @internal
|
|
263
|
-
*/
|
|
264
|
-
export const eventStoreAccess = Object.freeze({
|
|
265
|
-
// prettier-ignore
|
|
306
|
+
}
|
|
266
307
|
/**
|
|
267
|
-
*
|
|
308
|
+
* Returns retained record storage for the captured tenant boundary.
|
|
268
309
|
*
|
|
269
|
-
* @param
|
|
270
|
-
* @
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
310
|
+
* @param context Complete tenant-scoped storage context.
|
|
311
|
+
* @returns The retained record storage.
|
|
312
|
+
*/
|
|
313
|
+
storageFor(context) {
|
|
314
|
+
this.requireOpen();
|
|
315
|
+
const key = TenantBoundary.of(context).key;
|
|
316
|
+
const existing = this.#storages.get(key);
|
|
317
|
+
if (existing !== undefined)
|
|
318
|
+
return existing;
|
|
319
|
+
const storage = this.#factory.createRecordStorage(context, eventStoreRecordSpec, this.#group);
|
|
320
|
+
this.#storages.set(key, storage);
|
|
321
|
+
return storage;
|
|
322
|
+
}
|
|
278
323
|
/**
|
|
279
|
-
*
|
|
324
|
+
* Rejects operations after this Event Store closes.
|
|
280
325
|
*/
|
|
281
|
-
|
|
282
|
-
if (!
|
|
326
|
+
requireOpen() {
|
|
327
|
+
if (!this.#open)
|
|
283
328
|
throw new Error("EventStore is closed.");
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
throw new Error("Saved Agent Event requires an EventStore instance.");
|
|
287
|
-
const record = clone(EventSchema, event);
|
|
288
|
-
const context = EventContexts.snapshotForEvent(binding.context, record);
|
|
289
|
-
const id = EventIds.require(record);
|
|
290
|
-
await this.withLock(binding.factory, context, async () => {
|
|
291
|
-
const storage = binding.factory.createRecordStorage(context, eventStoreRecordSpec);
|
|
292
|
-
try {
|
|
293
|
-
await SavedEvents.appendOrVerify(storage, id, record);
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
storage.close();
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
return record;
|
|
300
|
-
},
|
|
301
|
-
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
302
331
|
/**
|
|
303
|
-
*
|
|
332
|
+
* Gives server assembly access to synchronous idle cleanup through the provider entrypoint.
|
|
333
|
+
* @internal
|
|
304
334
|
*/
|
|
305
|
-
const
|
|
335
|
+
export const eventStoreAccess = {
|
|
306
336
|
/**
|
|
307
|
-
*
|
|
337
|
+
* Closes an idle Event Store during synchronous assembly rollback.
|
|
308
338
|
*
|
|
309
|
-
* @param
|
|
310
|
-
* @param id Complete persisted record identity.
|
|
311
|
-
* @param storage Native record storage for this row.
|
|
312
|
-
* @returns Completion after exact-envelope insertion or verification.
|
|
339
|
+
* @param store Framework Event Store to close.
|
|
313
340
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
const stored = await storage.read(id);
|
|
323
|
-
if (stored !== undefined && this.same(stored, event))
|
|
324
|
-
return;
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
const stored = await storage.read(id);
|
|
328
|
-
if (stored === undefined || !this.same(stored, event))
|
|
329
|
-
throw new Error("Saved Agent Event ID conflicts with another envelope.");
|
|
330
|
-
},
|
|
331
|
-
/**
|
|
332
|
-
* Compares complete Protobuf envelopes, including unknown fields.
|
|
333
|
-
*
|
|
334
|
-
* @param left First value in the comparison.
|
|
335
|
-
* @param right Second value in the comparison.
|
|
336
|
-
* @returns Whether the two original Event envelopes have identical bytes.
|
|
337
|
-
*/
|
|
338
|
-
same(left, right) {
|
|
339
|
-
const a = toBinary(EventSchema, left);
|
|
340
|
-
const b = toBinary(EventSchema, right);
|
|
341
|
-
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
341
|
+
closeIdle(store) {
|
|
342
|
+
const close = idleClosers.get(store);
|
|
343
|
+
if (close === undefined)
|
|
344
|
+
throw new TypeError("EventStore idle cleanup requires an EventStore.");
|
|
345
|
+
close();
|
|
342
346
|
},
|
|
343
347
|
};
|
|
344
348
|
/**
|
|
@@ -347,10 +351,10 @@ const SavedEvents = {
|
|
|
347
351
|
const EventIds = {
|
|
348
352
|
// prettier-ignore
|
|
349
353
|
/**
|
|
350
|
-
* Validates an
|
|
354
|
+
* Validates that an Event has a non-blank ID.
|
|
351
355
|
*
|
|
352
|
-
* @param event
|
|
353
|
-
* @returns
|
|
356
|
+
* @param event Supplies the Event envelope.
|
|
357
|
+
* @returns Its validated ID.
|
|
354
358
|
*/
|
|
355
359
|
require(event) {
|
|
356
360
|
if (event.id === undefined)
|
|
@@ -363,9 +367,9 @@ const EventIds = {
|
|
|
363
367
|
/**
|
|
364
368
|
* Rejects IDs that already exist in storage.
|
|
365
369
|
*
|
|
366
|
-
* @param
|
|
367
|
-
* @param
|
|
368
|
-
* @returns Completion
|
|
370
|
+
* @param storage Reads existing Events.
|
|
371
|
+
* @param ids Supplies IDs that must be absent.
|
|
372
|
+
* @returns Completion when no ID exists.
|
|
369
373
|
*/
|
|
370
374
|
async rejectStored(storage, ids) {
|
|
371
375
|
if ((await storage.index({ ids })).length > 0) {
|
|
@@ -373,48 +377,53 @@ const EventIds = {
|
|
|
373
377
|
}
|
|
374
378
|
},
|
|
375
379
|
/**
|
|
376
|
-
* Writes each
|
|
380
|
+
* Writes each strictly new Event, retaining a completed prefix on failure.
|
|
377
381
|
*
|
|
378
|
-
* @param
|
|
379
|
-
* @param
|
|
380
|
-
* @returns Completion after
|
|
382
|
+
* @param storage Supplies atomic record insertion.
|
|
383
|
+
* @param records Supplies accepted Event snapshots.
|
|
384
|
+
* @returns Completion after every new Event is stored.
|
|
381
385
|
*/
|
|
382
386
|
async insertUnique(storage, records) {
|
|
383
387
|
if (!storage.atomicCompareAndSet) {
|
|
384
388
|
throw new Error("EventStore requires atomic record compare-and-set.");
|
|
385
389
|
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!(await storage.compareAndSet(id, undefined, record))) {
|
|
391
|
-
throw new Error("EventStore requires unique event IDs.");
|
|
392
|
-
}
|
|
393
|
-
inserted.push({ id, record });
|
|
390
|
+
for (const record of records) {
|
|
391
|
+
const id = EventIds.require(record);
|
|
392
|
+
if (!(await storage.compareAndSet(id, undefined, record))) {
|
|
393
|
+
throw new Error("EventStore requires unique event IDs.");
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
},
|
|
397
|
+
/**
|
|
398
|
+
* Writes immutable Events or confirms complete equality at the same ID.
|
|
399
|
+
*
|
|
400
|
+
* @param storage Supplies atomic single-record insertion and reads.
|
|
401
|
+
* @param records Supplies ordered Event snapshots.
|
|
402
|
+
* @returns Completion after each Event is present unchanged.
|
|
403
|
+
*/
|
|
404
|
+
async insertImmutable(storage, records) {
|
|
405
|
+
if (!storage.atomicCompareAndSet)
|
|
406
|
+
throw new Error("EventStore requires atomic record compare-and-set.");
|
|
407
|
+
for (const record of records) {
|
|
408
|
+
const id = EventIds.require(record);
|
|
409
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
410
|
+
if (await storage.compareAndSet(id, undefined, record))
|
|
411
|
+
break;
|
|
412
|
+
const stored = await storage.read(id);
|
|
413
|
+
if (stored !== undefined) {
|
|
414
|
+
if (!equals(EventSchema, stored, record))
|
|
415
|
+
throw new Error("EventStore requires unique event IDs.");
|
|
416
|
+
break;
|
|
406
417
|
}
|
|
418
|
+
if (attempt === 1)
|
|
419
|
+
throw new Error("EventStore immutable insert could not confirm its ID.");
|
|
407
420
|
}
|
|
408
|
-
if (failures.length > 0) {
|
|
409
|
-
throw new AggregateError([error, ...failures], "EventStore append rollback failed.");
|
|
410
|
-
}
|
|
411
|
-
throw error;
|
|
412
421
|
}
|
|
413
422
|
},
|
|
414
423
|
/**
|
|
415
424
|
* Rejects repeated IDs within one append operation.
|
|
416
425
|
*
|
|
417
|
-
* @param ids
|
|
426
|
+
* @param ids Supplies the IDs in the operation.
|
|
418
427
|
*/
|
|
419
428
|
rejectDuplicates(ids) {
|
|
420
429
|
const seen = new Set();
|
|
@@ -426,15 +435,15 @@ const EventIds = {
|
|
|
426
435
|
},
|
|
427
436
|
};
|
|
428
437
|
/**
|
|
429
|
-
* Captures tenant-aware event-store contexts
|
|
438
|
+
* Captures tenant-aware event-store contexts.
|
|
430
439
|
*/
|
|
431
440
|
const EventContexts = {
|
|
432
441
|
// prettier-ignore
|
|
433
442
|
/**
|
|
434
443
|
* Captures one storage context.
|
|
435
444
|
*
|
|
436
|
-
* @param context
|
|
437
|
-
* @returns
|
|
445
|
+
* @param context Supplies context and optional default tenant.
|
|
446
|
+
* @returns An immutable context snapshot.
|
|
438
447
|
*/
|
|
439
448
|
base(context) {
|
|
440
449
|
return context.multitenant
|
|
@@ -448,10 +457,10 @@ const EventContexts = {
|
|
|
448
457
|
: Object.freeze({ name: context.name, multitenant: false });
|
|
449
458
|
},
|
|
450
459
|
/**
|
|
451
|
-
*
|
|
460
|
+
* Returns a complete storage context for a read.
|
|
452
461
|
*
|
|
453
|
-
* @param context
|
|
454
|
-
* @returns
|
|
462
|
+
* @param context Supplies an explicit default tenant when required.
|
|
463
|
+
* @returns A complete tenant-scoped storage context.
|
|
455
464
|
*/
|
|
456
465
|
snapshot(context) {
|
|
457
466
|
if (!context.multitenant)
|
|
@@ -469,9 +478,9 @@ const EventContexts = {
|
|
|
469
478
|
/**
|
|
470
479
|
* Captures one context using an event envelope tenant when present.
|
|
471
480
|
*
|
|
472
|
-
* @param context
|
|
473
|
-
* @param event
|
|
474
|
-
* @returns
|
|
481
|
+
* @param context Supplies the Event Store default boundary.
|
|
482
|
+
* @param event Supplies an optional envelope tenant.
|
|
483
|
+
* @returns The Event's complete storage context.
|
|
475
484
|
*/
|
|
476
485
|
snapshotForEvent(context, event) {
|
|
477
486
|
if (!context.multitenant)
|
|
@@ -486,11 +495,11 @@ const EventContexts = {
|
|
|
486
495
|
});
|
|
487
496
|
},
|
|
488
497
|
/**
|
|
489
|
-
*
|
|
498
|
+
* Returns one tenant boundary for every Event in a batch.
|
|
490
499
|
*
|
|
491
|
-
* @param context Event Store
|
|
492
|
-
* @param events
|
|
493
|
-
* @returns
|
|
500
|
+
* @param context Supplies the Event Store default boundary.
|
|
501
|
+
* @param events Supplies the complete Event batch.
|
|
502
|
+
* @returns Their shared storage context.
|
|
494
503
|
*/
|
|
495
504
|
batch(context, events) {
|
|
496
505
|
const first = events[0];
|
|
@@ -507,8 +516,8 @@ const EventContexts = {
|
|
|
507
516
|
/**
|
|
508
517
|
* Reads an explicit tenant from an event envelope.
|
|
509
518
|
*
|
|
510
|
-
* @param event
|
|
511
|
-
* @returns
|
|
519
|
+
* @param event Supplies the envelope origin.
|
|
520
|
+
* @returns Its tenant when present.
|
|
512
521
|
*/
|
|
513
522
|
readEventTenant(event) {
|
|
514
523
|
switch (event.context?.origin.case) {
|
|
@@ -523,21 +532,12 @@ const EventContexts = {
|
|
|
523
532
|
/**
|
|
524
533
|
* Converts a typed tenant ID to its storage-scope value.
|
|
525
534
|
*
|
|
526
|
-
* @param tenantId
|
|
527
|
-
* @returns
|
|
535
|
+
* @param tenantId Supplies the optional tenant ID.
|
|
536
|
+
* @returns An independent tenant ID when present.
|
|
528
537
|
*/
|
|
529
538
|
tenantValue(tenantId) {
|
|
530
539
|
return tenantId === undefined ? undefined : clone(TenantIdSchema, tenantId);
|
|
531
540
|
},
|
|
532
|
-
/**
|
|
533
|
-
* Creates a deterministic key for a context-scoped append lock.
|
|
534
|
-
*
|
|
535
|
-
* @param context Captured event storage context.
|
|
536
|
-
* @returns Complete encoded invocation identity.
|
|
537
|
-
*/
|
|
538
|
-
key(context) {
|
|
539
|
-
return TenantBoundary.of(context).key;
|
|
540
|
-
},
|
|
541
541
|
};
|
|
542
542
|
/**
|
|
543
543
|
* Provides the canonical event-store record layout to provider-only internals.
|