@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.
Files changed (121) hide show
  1. package/README.md +12 -3
  2. package/REFERENCE.md +90 -63
  3. package/dist/entity/agent-history-record-spec.d.ts +8 -0
  4. package/dist/entity/agent-history-record-spec.d.ts.map +1 -1
  5. package/dist/entity/agent-history-record-spec.js +12 -1
  6. package/dist/entity/agent-history-record-spec.js.map +1 -1
  7. package/dist/entity/agent-history.d.ts +25 -0
  8. package/dist/entity/agent-history.d.ts.map +1 -1
  9. package/dist/entity/agent-history.js.map +1 -1
  10. package/dist/entity/history-conformance.d.ts +38 -8
  11. package/dist/entity/history-conformance.d.ts.map +1 -1
  12. package/dist/entity/history-conformance.js +103 -58
  13. package/dist/entity/history-conformance.js.map +1 -1
  14. package/dist/event/event-store.d.ts +80 -74
  15. package/dist/event/event-store.d.ts.map +1 -1
  16. package/dist/event/event-store.js +264 -264
  17. package/dist/event/event-store.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/internal/agent-model-preferences.d.ts +76 -0
  23. package/dist/internal/agent-model-preferences.d.ts.map +1 -0
  24. package/dist/internal/agent-model-preferences.js +133 -0
  25. package/dist/internal/agent-model-preferences.js.map +1 -0
  26. package/dist/internal/entity-commit.d.ts +92 -17
  27. package/dist/internal/entity-commit.d.ts.map +1 -1
  28. package/dist/internal/entity-commit.js +45 -2
  29. package/dist/internal/entity-commit.js.map +1 -1
  30. package/dist/internal/entity-history.d.ts +1 -7
  31. package/dist/internal/entity-history.d.ts.map +1 -1
  32. package/dist/internal/entity-history.js +0 -5
  33. package/dist/internal/entity-history.js.map +1 -1
  34. package/dist/internal/event-store.d.ts +1 -1
  35. package/dist/internal/event-store.d.ts.map +1 -1
  36. package/dist/internal/event-store.js +1 -1
  37. package/dist/internal/event-store.js.map +1 -1
  38. package/dist/internal/storage-preparation.d.ts +88 -0
  39. package/dist/internal/storage-preparation.d.ts.map +1 -0
  40. package/dist/internal/storage-preparation.js +41 -0
  41. package/dist/internal/storage-preparation.js.map +1 -0
  42. package/dist/internal/tenancy.d.ts +7 -0
  43. package/dist/internal/tenancy.d.ts.map +1 -1
  44. package/dist/memory/in-memory-agent-history.d.ts.map +1 -1
  45. package/dist/memory/in-memory-agent-history.js +22 -7
  46. package/dist/memory/in-memory-agent-history.js.map +1 -1
  47. package/dist/memory/in-memory-entity-commit.d.ts +237 -42
  48. package/dist/memory/in-memory-entity-commit.d.ts.map +1 -1
  49. package/dist/memory/in-memory-entity-commit.js +529 -325
  50. package/dist/memory/in-memory-entity-commit.js.map +1 -1
  51. package/dist/memory/in-memory-entity-history.d.ts.map +1 -1
  52. package/dist/memory/in-memory-entity-history.js +4 -3
  53. package/dist/memory/in-memory-entity-history.js.map +1 -1
  54. package/dist/memory/in-memory-storage-backend.d.ts +33 -1
  55. package/dist/memory/in-memory-storage-backend.d.ts.map +1 -1
  56. package/dist/memory/in-memory-storage-backend.js +118 -1
  57. package/dist/memory/in-memory-storage-backend.js.map +1 -1
  58. package/dist/memory/in-memory-storage-factory.d.ts +72 -13
  59. package/dist/memory/in-memory-storage-factory.d.ts.map +1 -1
  60. package/dist/memory/in-memory-storage-factory.js +199 -30
  61. package/dist/memory/in-memory-storage-factory.js.map +1 -1
  62. package/dist/memory/tenant-records.d.ts +1 -1
  63. package/dist/memory/tenant-records.d.ts.map +1 -1
  64. package/dist/memory/tenant-records.js +3 -2
  65. package/dist/memory/tenant-records.js.map +1 -1
  66. package/dist/provider.d.ts +2 -1
  67. package/dist/provider.d.ts.map +1 -1
  68. package/dist/provider.js +2 -1
  69. package/dist/provider.js.map +1 -1
  70. package/dist/record/record-query.d.ts +10 -2
  71. package/dist/record/record-query.d.ts.map +1 -1
  72. package/dist/record/record-query.js +9 -0
  73. package/dist/record/record-query.js.map +1 -1
  74. package/dist/storage/storage-factory.d.ts +27 -4
  75. package/dist/storage/storage-factory.d.ts.map +1 -1
  76. package/dist/storage/storage-factory.js +30 -1
  77. package/dist/storage/storage-factory.js.map +1 -1
  78. package/dist/storage/storage.d.ts +2 -1
  79. package/dist/storage/storage.d.ts.map +1 -1
  80. package/dist/tsconfig.tsbuildinfo +1 -1
  81. package/package.json +3 -3
  82. package/dist/entity/agent-execution-record-spec.d.ts +0 -87
  83. package/dist/entity/agent-execution-record-spec.d.ts.map +0 -1
  84. package/dist/entity/agent-execution-record-spec.js +0 -230
  85. package/dist/entity/agent-execution-record-spec.js.map +0 -1
  86. package/dist/entity/agent-execution-sizes.d.ts +0 -30
  87. package/dist/entity/agent-execution-sizes.d.ts.map +0 -1
  88. package/dist/entity/agent-execution-sizes.js +0 -51
  89. package/dist/entity/agent-execution-sizes.js.map +0 -1
  90. package/dist/entity/agent-execution-transitions.d.ts +0 -71
  91. package/dist/entity/agent-execution-transitions.d.ts.map +0 -1
  92. package/dist/entity/agent-execution-transitions.js +0 -182
  93. package/dist/entity/agent-execution-transitions.js.map +0 -1
  94. package/dist/entity/agent-execution-values.d.ts +0 -142
  95. package/dist/entity/agent-execution-values.d.ts.map +0 -1
  96. package/dist/entity/agent-execution-values.js +0 -250
  97. package/dist/entity/agent-execution-values.js.map +0 -1
  98. package/dist/entity/agent-execution.d.ts +0 -234
  99. package/dist/entity/agent-execution.d.ts.map +0 -1
  100. package/dist/entity/agent-execution.js +0 -15
  101. package/dist/entity/agent-execution.js.map +0 -1
  102. package/dist/internal/agent-execution.d.ts +0 -51
  103. package/dist/internal/agent-execution.d.ts.map +0 -1
  104. package/dist/internal/agent-execution.js +0 -53
  105. package/dist/internal/agent-execution.js.map +0 -1
  106. package/dist/internal/delivery-cleanup.d.ts +0 -99
  107. package/dist/internal/delivery-cleanup.d.ts.map +0 -1
  108. package/dist/internal/delivery-cleanup.js +0 -38
  109. package/dist/internal/delivery-cleanup.js.map +0 -1
  110. package/dist/memory/agent-execution-head-index.d.ts +0 -52
  111. package/dist/memory/agent-execution-head-index.d.ts.map +0 -1
  112. package/dist/memory/agent-execution-head-index.js +0 -91
  113. package/dist/memory/agent-execution-head-index.js.map +0 -1
  114. package/dist/memory/in-memory-agent-execution.d.ts +0 -21
  115. package/dist/memory/in-memory-agent-execution.d.ts.map +0 -1
  116. package/dist/memory/in-memory-agent-execution.js +0 -587
  117. package/dist/memory/in-memory-agent-execution.js.map +0 -1
  118. package/dist/memory/memory-delivery-cleanup.d.ts +0 -32
  119. package/dist/memory/memory-delivery-cleanup.d.ts.map +0 -1
  120. package/dist/memory/memory-delivery-cleanup.js +0 -61
  121. 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, ScalarType, toBinary } from "@bufbuild/protobuf";
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 savedEventStores = new WeakMap();
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 events before queued work, rejects missing, blank, and
26
- * duplicate IDs in one batch, and rejects IDs already stored for the same
27
- * captured storage context.
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
- savedEventStores.set(this, { context: this.#context, factory });
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
- * Closes this Event Store. Operation-selected storage handles close after use.
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
- * Validates that one generated Spine event can be appended without storing it.
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
- * Accepts one event, runs caller acceptance, and appends using one captured
71
- * storage context.
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 generated Spine event, rejecting missing, blank, or duplicate IDs.
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.appendUnique([record], EventContexts.snapshotForEvent(this.#context, record));
225
+ await this.appendImmutable([record], EventContexts.snapshotForEvent(this.#context, record));
94
226
  }
95
227
  /**
96
- * Writes generated Spine events in order, rejecting missing, blank, or duplicate IDs.
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.appendUnique(records, EventContexts.batch(this.#context, records));
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
- const storage = this.#factory.createRecordStorage(EventContexts.snapshot(this.#context), eventStoreRecordSpec);
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
- * Persists one batch only when every original Event ID is available.
252
+ * Writes strictly new Events after caller acceptance.
150
253
  *
151
- * @param records Original Event envelopes.
152
- * @param context Captured tenant and Bounded Context.
153
- * @returns Completion after all unique Events are persisted.
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 eventStoreAccess.withLock(this.#factory, context, async () => {
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
- * Deletes the IDs inserted by a failed legacy batch operation.
265
+ * Writes captured immutable Events, accepting identical stored retries.
171
266
  *
172
- * @param ids IDs inserted by the current batch.
173
- * @param context Captured tenant and Bounded Context.
174
- * @returns Completion after those IDs are removed.
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 deleteIds(ids, context) {
271
+ async appendImmutable(records, context) {
177
272
  this.requireOpen();
178
- await eventStoreAccess.withLock(this.#factory, context, async () => {
179
- const storage = this.#factory.createRecordStorage(context, eventStoreRecordSpec);
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 that no ID in a batch is already stored for this Bounded Context and tenant.
277
+ * Checks strict admission without writing an Event.
192
278
  *
193
- * @param ids Original Event IDs.
194
- * @param context Captured tenant and Bounded Context.
195
- * @returns Completion after duplicate validation.
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 eventStoreAccess.withLock(this.#factory, context, async () => {
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
- * Rejects calls after this Event Store closes.
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
- * @param factory Storage provider used for the Event family.
224
- * @param context Captured tenant and Bounded Context.
225
- * @param work Operation run under the Bounded Context and tenant lock.
226
- * @returns Result returned by the operation.
227
- * @typeParam T Operation result.
228
- */
229
- async withLock(factory, context, work) {
230
- const queues = this.queueMap(factory);
231
- const key = EventContexts.key(context);
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 next;
301
+ return await work;
238
302
  }
239
303
  finally {
240
- if (queues.get(key) === stored) {
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
- return queues;
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
- * Runs work under the same factory/context lock used by direct Event Store appends.
308
+ * Returns retained record storage for the captured tenant boundary.
268
309
  *
269
- * @param factory The factory that owns the Event Store records.
270
- * @param context The Event Store context to serialize.
271
- * @param work The operation to run while holding the lock.
272
- * @returns The operation result.
273
- * @typeParam T Result returned by the transaction callback.
274
- */
275
- withLock(factory, context, work) {
276
- return EventStoreLocks.withLock(factory, context, work);
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
- * Reuses one EventStore's captured tenant and Bounded Context for saved-output retry.
324
+ * Rejects operations after this Event Store closes.
280
325
  */
281
- async appendOrVerifyOriginal(store, event) {
282
- if (!store.isOpen())
326
+ requireOpen() {
327
+ if (!this.#open)
283
328
  throw new Error("EventStore is closed.");
284
- const binding = savedEventStores.get(store);
285
- if (binding === undefined)
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
- * Keeps saved-output insert and complete-envelope comparison together.
332
+ * Gives server assembly access to synchronous idle cleanup through the provider entrypoint.
333
+ * @internal
304
334
  */
305
- const SavedEvents = {
335
+ export const eventStoreAccess = {
306
336
  /**
307
- * Applies atomic insert and compares every original Event byte after a collision.
337
+ * Closes an idle Event Store during synchronous assembly rollback.
308
338
  *
309
- * @param event Original event envelope.
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
- async appendOrVerify(storage, id, event) {
315
- if (!storage.atomicCompareAndSet)
316
- throw new Error("Saved Agent Event requires atomic record compare-and-set.");
317
- try {
318
- if (await storage.compareAndSet(id, undefined, event))
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 event to have a non-blank ID.
354
+ * Validates that an Event has a non-blank ID.
351
355
  *
352
- * @param event Original event envelope.
353
- * @returns Present value or an error when missing.
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 ids Original event identities.
367
- * @param storage Native record storage for this row.
368
- * @returns Completion after duplicate-ID validation.
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 event ID and rejects the whole batch on collision.
380
+ * Writes each strictly new Event, retaining a completed prefix on failure.
377
381
  *
378
- * @param records Records participating in this operation.
379
- * @param storage Native record storage for this row.
380
- * @returns Completion after unique Event IDs are persisted.
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 inserted = [];
387
- try {
388
- for (const record of records) {
389
- const id = EventIds.require(record);
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
- catch (error) {
397
- const failures = [];
398
- for (const { id, record } of inserted.reverse()) {
399
- try {
400
- if (!(await storage.compareAndSet(id, record, undefined))) {
401
- failures.push(new Error("EventStore append rollback lost its stored event."));
402
- }
403
- }
404
- catch (rollbackError) {
405
- failures.push(rollbackError);
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 Original event identities.
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 and their lock keys.
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 Captured event storage context.
437
- * @returns Base event storage context.
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
- * Captures the selected tenant for an Event Store read.
460
+ * Returns a complete storage context for a read.
452
461
  *
453
- * @param context Event Store storage context and selected tenant.
454
- * @returns Immutable storage context for the read.
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 Captured event storage context.
473
- * @param event Original event envelope.
474
- * @returns Captured storage context for this Event and tenant.
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
- * Checks every Event in one batch uses the same captured tenant.
498
+ * Returns one tenant boundary for every Event in a batch.
490
499
  *
491
- * @param context Event Store storage context before envelope validation.
492
- * @param events Original Event envelopes in the batch.
493
- * @returns Immutable storage context shared by every Event in the batch.
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 Original event envelope.
511
- * @returns Tenant encoded in the original Event when present.
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 Tenant identifier captured for this operation.
527
- * @returns Tenant identity when configured.
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.