@cratis/chronicle 6.51.0 → 6.51.1

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 (59) hide show
  1. package/ChronicleClient.ts +127 -99
  2. package/ChronicleOptions.ts +23 -0
  3. package/dist/ChronicleClient.d.ts +7 -0
  4. package/dist/ChronicleClient.d.ts.map +1 -1
  5. package/dist/ChronicleClient.js +115 -95
  6. package/dist/ChronicleClient.js.map +1 -1
  7. package/dist/ChronicleOptions.d.ts +14 -0
  8. package/dist/ChronicleOptions.d.ts.map +1 -1
  9. package/dist/ChronicleOptions.js +16 -0
  10. package/dist/ChronicleOptions.js.map +1 -1
  11. package/dist/eventSequences/EventSequence.d.ts +12 -0
  12. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  13. package/dist/eventSequences/EventSequence.js +202 -175
  14. package/dist/eventSequences/EventSequence.js.map +1 -1
  15. package/dist/eventSequences/PreparedBatchAppend.d.ts +4 -0
  16. package/dist/eventSequences/PreparedBatchAppend.d.ts.map +1 -0
  17. package/dist/eventSequences/PreparedBatchAppend.js +4 -0
  18. package/dist/eventSequences/PreparedBatchAppend.js.map +1 -0
  19. package/dist/eventSequences/PreparedSingleAppend.d.ts +4 -0
  20. package/dist/eventSequences/PreparedSingleAppend.d.ts.map +1 -0
  21. package/dist/eventSequences/PreparedSingleAppend.js +4 -0
  22. package/dist/eventSequences/PreparedSingleAppend.js.map +1 -0
  23. package/dist/eventSequences/WireBatchEvent.d.ts +6 -0
  24. package/dist/eventSequences/WireBatchEvent.d.ts.map +1 -0
  25. package/dist/eventSequences/WireBatchEvent.js +4 -0
  26. package/dist/eventSequences/WireBatchEvent.js.map +1 -0
  27. package/dist/eventSequences/toContractsCausation.d.ts +16 -0
  28. package/dist/eventSequences/toContractsCausation.d.ts.map +1 -0
  29. package/dist/eventSequences/toContractsCausation.js +15 -0
  30. package/dist/eventSequences/toContractsCausation.js.map +1 -0
  31. package/dist/reducers/ReducerObservation.d.ts +18 -0
  32. package/dist/reducers/ReducerObservation.d.ts.map +1 -0
  33. package/dist/reducers/ReducerObservation.js +4 -0
  34. package/dist/reducers/ReducerObservation.js.map +1 -0
  35. package/dist/reducers/ReducerObservationSession.d.ts +11 -0
  36. package/dist/reducers/ReducerObservationSession.d.ts.map +1 -0
  37. package/dist/reducers/ReducerObservationSession.js +4 -0
  38. package/dist/reducers/ReducerObservationSession.js.map +1 -0
  39. package/dist/reducers/Reducers.d.ts +9 -0
  40. package/dist/reducers/Reducers.d.ts.map +1 -1
  41. package/dist/reducers/Reducers.js +149 -154
  42. package/dist/reducers/Reducers.js.map +1 -1
  43. package/dist/reducers/ReductionOutcome.d.ts +25 -0
  44. package/dist/reducers/ReductionOutcome.d.ts.map +1 -0
  45. package/dist/reducers/ReductionOutcome.js +66 -0
  46. package/dist/reducers/ReductionOutcome.js.map +1 -0
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/eventSequences/EventSequence.ts +265 -186
  49. package/eventSequences/PreparedBatchAppend.ts +7 -0
  50. package/eventSequences/PreparedSingleAppend.ts +7 -0
  51. package/eventSequences/WireBatchEvent.ts +7 -0
  52. package/eventSequences/toContractsCausation.ts +17 -0
  53. package/for_ChronicleClient/messageSizes.spec.ts +37 -0
  54. package/for_ChronicleOptions/when_configuring_message_sizes/with_message_sizes.spec.ts +29 -0
  55. package/package.json +1 -1
  56. package/reducers/ReducerObservation.ts +25 -0
  57. package/reducers/ReducerObservationSession.ts +15 -0
  58. package/reducers/Reducers.ts +185 -171
  59. package/reducers/ReductionOutcome.ts +75 -0
@@ -11,6 +11,10 @@ import type {
11
11
  import { Constructor } from '@cratis/fundamentals';
12
12
  import { prepareSingleAppend } from './prepareSingleAppend.js';
13
13
  import { prepareBatchAppend } from './prepareBatchAppend.js';
14
+ import type { PreparedBatchAppend } from './PreparedBatchAppend.js';
15
+ import type { PreparedSingleAppend } from './PreparedSingleAppend.js';
16
+ import type { WireBatchEvent } from './WireBatchEvent.js';
17
+ import { toContractsCausation } from './toContractsCausation.js';
14
18
  import { createAppendNotification, mapAppendNotificationCausation } from './createAppendNotification.js';
15
19
  import { getEventTypeFor } from '../events/eventTypeDecorator.js';
16
20
  import type { AppendedEvent } from '../events/AppendedEvent.js';
@@ -46,7 +50,7 @@ import { ChronicleConventionMetrics } from '../Metrics.js';
46
50
  import { identityProvider, Identity } from '../identity/index.js';
47
51
  import { causationManager, CausationType } from '../auditing/index.js';
48
52
  import { toContractsGuid } from '../connection/Guid.js';
49
- import { ensureCommandResponse, ensureCommandSuccess, ensureQuerySuccess } from '../connection/callResults.js';
53
+ import { ensureCommandResponse, ensureCommandSuccess, ensureQuerySuccess, type CallResultLike } from '../connection/callResults.js';
50
54
  import type { ConcurrencyScope } from './ConcurrencyScope.js';
51
55
  import type { IEventSources } from '../eventSources/IEventSources.js';
52
56
  import { deriveConcurrencyScope, planDerivedGuards, resolveBatchRouting, resolveSingleRouting } from './resolveEventSourceRouting.js';
@@ -77,13 +81,10 @@ export class EventSequence implements IEventSequence {
77
81
  /** @inheritdoc */
78
82
  async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
79
83
  const routing = resolveSingleRouting(this._eventSources, options);
80
- const { eventType, correlationId, content, tags, namedTags, causationChain, identity, subject } = prepareSingleAppend(event, options);
84
+ const prepared = prepareSingleAppend(event, options);
85
+ const { eventType, correlationId, tags, namedTags, causationChain } = prepared;
81
86
 
82
- const sequenceMetricAttributes = {
83
- [names.attributes.eventStore]: this._eventStoreName,
84
- [names.attributes.namespace]: this._namespace,
85
- [names.attributes.eventSequenceId]: this.id.value
86
- };
87
+ const sequenceMetricAttributes = this.sequenceMetricAttributes;
87
88
  const metricAttributes = { ...sequenceMetricAttributes, [names.attributes.eventTypeId]: eventType.id.value };
88
89
 
89
90
  return observeOperation('append', async span => {
@@ -94,40 +95,7 @@ export class EventSequence implements IEventSequence {
94
95
  if (options?.sourceType) span.setAttribute(names.attributes.eventSourceType, options.sourceType);
95
96
  const startTime = performance.now();
96
97
  try {
97
- const concurrencyScope = options?.concurrencyScope ?? (routing
98
- ? await deriveConcurrencyScope(routing, eventSourceId, options?.streamId, this.tailReader)
99
- : undefined);
100
- const request = {
101
- EventStore: this._eventStoreName,
102
- Namespace: this._namespace,
103
- EventSequenceId: this.id.value,
104
- CorrelationId: toContractsGuid(correlationId),
105
- EventSourceType: routing?.sourceType ?? options?.sourceType,
106
- EventSourceId: eventSourceId,
107
- EventStreamType: routing?.streamType ?? options?.streamType,
108
- EventStreamId: options?.streamId,
109
- EventType: {
110
- Id: eventType.id.value,
111
- Generation: eventType.generation.value,
112
- Tombstone: eventType.tombstone
113
- },
114
- Content: content,
115
- Causation: causationChain.map(c => ({
116
- Occurred: { Value: c.occurred.toISOString() },
117
- Type: c.type.name,
118
- Properties: { ...c.properties }
119
- })),
120
- CausedBy: toContractsCausedBy(identity),
121
- ConcurrencyScope: this.toContractConcurrencyScope(concurrencyScope),
122
- EventSource: routing?.eventSource,
123
- Tags: tags,
124
- Occurred: options?.occurred === undefined ? undefined : { Value: options.occurred.toISOString() },
125
- Subject: subject ?? eventSourceId
126
- };
127
- // Named tags travel on the dedicated call so a kernel without support rejects them instead of dropping them.
128
- const response = namedTags.length === 0
129
- ? await this._connection.eventSequences.append(request)
130
- : await this._connection.eventSequences.appendWithNamedTags({ ...request, NamedTags: toContractsNamedTags(namedTags) });
98
+ const response = await this.sendAppend(eventSourceId, prepared, routing, options);
131
99
 
132
100
  const appendResponse = ensureCommandResponse('append event', response);
133
101
  const durationInSeconds = (performance.now() - startTime) / 1000;
@@ -140,14 +108,7 @@ export class EventSequence implements IEventSequence {
140
108
  setSequenceNumber(span, result.sequenceNumber.value);
141
109
  span.setStatus({ code: SpanStatusCode.OK });
142
110
 
143
- ChronicleConventionMetrics.eventsAppended.add(1, metricAttributes);
144
- ChronicleConventionMetrics.appendDuration.record(durationInSeconds, metricAttributes);
145
- if (result.constraintViolations.length > 0) {
146
- ChronicleConventionMetrics.constraintViolations.add(result.constraintViolations.length, sequenceMetricAttributes);
147
- }
148
- if (result.errors.length > 0) {
149
- ChronicleConventionMetrics.appendErrors.add(result.errors.length, sequenceMetricAttributes);
150
- }
111
+ this.recordAppendMetrics(result, durationInSeconds, metricAttributes, sequenceMetricAttributes);
151
112
 
152
113
  if (this.appendOperations.hasSubscribers) {
153
114
  this.appendOperations.publish([createAppendNotification(eventSourceId, event, result,
@@ -165,6 +126,83 @@ export class EventSequence implements IEventSequence {
165
126
  }, correlationId);
166
127
  }
167
128
 
129
+ private get sequenceMetricAttributes(): Record<string, string> {
130
+ return {
131
+ [names.attributes.eventStore]: this._eventStoreName,
132
+ [names.attributes.namespace]: this._namespace,
133
+ [names.attributes.eventSequenceId]: this.id.value
134
+ };
135
+ }
136
+
137
+ private async sendAppend(
138
+ eventSourceId: string,
139
+ prepared: PreparedSingleAppend,
140
+ routing: ReturnType<typeof resolveSingleRouting>,
141
+ options?: AppendOptions
142
+ ) {
143
+ const concurrencyScope = options?.concurrencyScope ?? (routing
144
+ ? await deriveConcurrencyScope(routing, eventSourceId, options?.streamId, this.tailReader)
145
+ : undefined);
146
+ const request = this.createAppendRequest(eventSourceId, prepared, concurrencyScope, this.toAppendRouteFields(routing, options));
147
+ // Named tags travel on the dedicated call so a kernel without support rejects them instead of dropping them.
148
+ return prepared.namedTags.length === 0
149
+ ? await this._connection.eventSequences.append(request)
150
+ : await this._connection.eventSequences.appendWithNamedTags({ ...request, NamedTags: toContractsNamedTags(prepared.namedTags) });
151
+ }
152
+
153
+ private toAppendRouteFields(routing: ReturnType<typeof resolveSingleRouting>, options?: AppendOptions) {
154
+ return {
155
+ EventSourceType: routing?.sourceType ?? options?.sourceType,
156
+ EventStreamType: routing?.streamType ?? options?.streamType,
157
+ EventStreamId: options?.streamId,
158
+ EventSource: routing?.eventSource,
159
+ Occurred: toContractsOccurred(options?.occurred)
160
+ };
161
+ }
162
+
163
+ private createAppendRequest(
164
+ eventSourceId: string,
165
+ { eventType, correlationId, content, tags, causationChain, identity, subject }: PreparedSingleAppend,
166
+ concurrencyScope: ConcurrencyScope | undefined,
167
+ routeFields: ReturnType<EventSequence['toAppendRouteFields']>
168
+ ) {
169
+ return {
170
+ EventStore: this._eventStoreName,
171
+ Namespace: this._namespace,
172
+ EventSequenceId: this.id.value,
173
+ CorrelationId: toContractsGuid(correlationId),
174
+ EventSourceId: eventSourceId,
175
+ EventType: {
176
+ Id: eventType.id.value,
177
+ Generation: eventType.generation.value,
178
+ Tombstone: eventType.tombstone
179
+ },
180
+ Content: content,
181
+ Causation: toContractsCausation(causationChain),
182
+ CausedBy: toContractsCausedBy(identity),
183
+ ConcurrencyScope: this.toContractConcurrencyScope(concurrencyScope),
184
+ Tags: tags,
185
+ Subject: subject ?? eventSourceId,
186
+ ...routeFields
187
+ };
188
+ }
189
+
190
+ private recordAppendMetrics(
191
+ result: AppendResult,
192
+ durationInSeconds: number,
193
+ metricAttributes: Record<string, string>,
194
+ sequenceMetricAttributes: Record<string, string>
195
+ ): void {
196
+ ChronicleConventionMetrics.eventsAppended.add(1, metricAttributes);
197
+ ChronicleConventionMetrics.appendDuration.record(durationInSeconds, metricAttributes);
198
+ if (result.constraintViolations.length > 0) {
199
+ ChronicleConventionMetrics.constraintViolations.add(result.constraintViolations.length, sequenceMetricAttributes);
200
+ }
201
+ if (result.errors.length > 0) {
202
+ ChronicleConventionMetrics.appendErrors.add(result.errors.length, sequenceMetricAttributes);
203
+ }
204
+ }
205
+
168
206
  /** @inheritdoc */
169
207
  async appendMany(eventSourceId: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
170
208
  async appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
@@ -173,21 +211,17 @@ export class EventSequence implements IEventSequence {
173
211
  eventsOrOptions?: object[] | AppendOptions,
174
212
  options?: AppendOptions
175
213
  ): Promise<AppendResult[]> {
176
- const { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend, namedTags } =
177
- prepareBatchAppend(eventSourceIdOrEvents, eventsOrOptions, options);
214
+ const prepared = prepareBatchAppend(eventSourceIdOrEvents, eventsOrOptions, options);
215
+ const { eventsForEventSourceIds, appendOptions, correlationId, concurrencyScopes, eventsToAppend, namedTags } = prepared;
178
216
 
179
- const wireEvents: Array<(typeof eventsToAppend)[number] & { EventSource?: string }> = eventsToAppend;
217
+ const wireEvents: WireBatchEvent[] = eventsToAppend;
180
218
  const routings = eventsForEventSourceIds.map(event => resolveBatchRouting(this._eventSources, event, appendOptions));
181
219
  ensureNamedTagBatchIsSupported(namedTags.some(tagsForEvent => tagsForEvent.length > 0), routings.some(routing => routing !== undefined));
182
220
  const explicitIds = new Set([...concurrencyScopes].filter(([, scope]) => scope !== undefined).map(([id]) => id));
183
221
  const derivedGuards = planDerivedGuards(routings, eventsForEventSourceIds, appendOptions?.streamId, explicitIds);
184
222
  const distinctEventSourceIds = [...new Set(eventsForEventSourceIds.map(_ => _.eventSourceId))];
185
223
 
186
- const batchMetricAttributes = {
187
- [names.attributes.eventStore]: this._eventStoreName,
188
- [names.attributes.namespace]: this._namespace,
189
- [names.attributes.eventSequenceId]: this.id.value
190
- };
224
+ const batchMetricAttributes = this.sequenceMetricAttributes;
191
225
 
192
226
  return observeOperation('appendMany', async span => {
193
227
  this.setSequenceAttributes(span);
@@ -197,91 +231,19 @@ export class EventSequence implements IEventSequence {
197
231
  setTelemetryAttribute(span, 'eventCount', eventsForEventSourceIds.length);
198
232
  const startTime = performance.now();
199
233
  try {
200
- routings.forEach((routing, index) => {
201
- if (!routing) return;
202
- wireEvents[index] = {
203
- ...wireEvents[index],
204
- EventSourceType: routing.sourceType,
205
- EventStreamType: routing.streamType ?? wireEvents[index].EventStreamType,
206
- EventSource: routing.eventSource
207
- };
208
- });
234
+ this.applyBatchRouting(wireEvents, routings);
209
235
  for (const [id, { routing, streamId }] of derivedGuards) {
210
236
  concurrencyScopes.set(id, await deriveConcurrencyScope(routing, id, streamId, this.tailReader));
211
237
  }
212
- const request = {
213
- EventStore: this._eventStoreName,
214
- Namespace: this._namespace,
215
- EventSequenceId: this.id.value,
216
- CorrelationId: toContractsGuid(correlationId),
217
- Events: wireEvents,
218
- Causation: batchCausationChain.map(c => ({
219
- Occurred: { Value: c.occurred.toISOString() },
220
- Type: c.type.name,
221
- Properties: { ...c.properties }
222
- })),
223
- CausedBy: toContractsCausedBy(identity),
224
- ConcurrencyScopes: [...concurrencyScopes].map(([eventSourceId, scope]) => ({
225
- EventSourceId: eventSourceId,
226
- Scope: this.toContractConcurrencyScope(scope)
227
- }))
228
- };
229
- const response = namedTags.every(tagsForEvent => tagsForEvent.length === 0)
230
- ? await this._connection.eventSequences.appendManyForEventSources(request)
231
- : await this._connection.eventSequences.appendManyForEventSourcesWithNamedTags({
232
- ...request,
233
- Events: wireEvents.map((wireEvent, index) => ({ ...wireEvent, NamedTags: toContractsNamedTags(namedTags[index]) }))
234
- });
238
+ const response = await this.sendAppendMany(prepared, wireEvents);
235
239
 
236
240
  const appendManyResponse = ensureCommandResponse('append many events', response);
237
241
  const durationInSeconds = (performance.now() - startTime) / 1000;
238
- // Mirrors the C# client: every per-event AppendResult in a batch carries all
239
- // constraint violations and the first concurrency violation of the whole batch —
240
- // the wire response doesn't correlate either back to a specific event index.
241
- const firstConcurrencyViolation: ContractsAppendManyResponse['ConcurrencyViolations'][number] | undefined =
242
- (appendManyResponse.ConcurrencyViolations ?? [])[0];
243
- const sequenceNumbers: ContractsAppendManyResponse['SequenceNumbers'] = appendManyResponse.SequenceNumbers ?? [];
244
- const constraintViolations: ContractsAppendManyResponse['ConstraintViolations'] = appendManyResponse.ConstraintViolations ?? [];
245
- const errors: ContractsAppendManyResponse['Errors'] = appendManyResponse.Errors ?? [];
246
- const batchWasRejected = sequenceNumbers.length === 0 &&
247
- (constraintViolations.length > 0 || errors.length > 0 || firstConcurrencyViolation !== undefined);
248
- if (sequenceNumbers.length === 0 && eventsForEventSourceIds.length > 0 && !batchWasRejected) {
249
- throw new Error('Append many events returned no sequence numbers or rejection details.');
250
- }
251
- const result = batchWasRejected
252
- ? eventsForEventSourceIds.map(() => this.mapAppendResponse(0n, constraintViolations, errors, firstConcurrencyViolation))
253
- : sequenceNumbers.map((sequenceNumber, index) =>
254
- this.mapAppendResponse(
255
- sequenceNumber,
256
- constraintViolations,
257
- errors.filter((_, errorIndex) => errorIndex === index),
258
- firstConcurrencyViolation
259
- )
260
- );
242
+ const result = this.toAppendManyResults(appendManyResponse, eventsForEventSourceIds.length);
261
243
  span.setStatus({ code: SpanStatusCode.OK });
262
244
 
263
- ChronicleConventionMetrics.batchAppendsPerformed.add(1, batchMetricAttributes);
264
- ChronicleConventionMetrics.eventsAppended.add(eventsForEventSourceIds.length, batchMetricAttributes);
265
- ChronicleConventionMetrics.appendManyDuration.record(durationInSeconds, batchMetricAttributes);
266
-
267
- const totalViolations = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.constraintViolations.length, 0);
268
- if (totalViolations > 0) {
269
- ChronicleConventionMetrics.constraintViolations.add(totalViolations, batchMetricAttributes);
270
- }
271
- const totalErrors = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.errors.length, 0);
272
- if (totalErrors > 0) {
273
- ChronicleConventionMetrics.appendErrors.add(totalErrors, batchMetricAttributes);
274
- }
275
-
276
- if (this.appendOperations.hasSubscribers && result.length > 0) {
277
- const occurredAt = new Date();
278
- const causationEntries = mapAppendNotificationCausation(batchCausationChain);
279
- this.appendOperations.publish(result.map((appendResult: AppendResult, index: number) => {
280
- const { eventSourceId, event } = eventsForEventSourceIds[index];
281
- return createAppendNotification(eventSourceId, event, appendResult, correlationId.toString(),
282
- causationEntries, eventsToAppend[index].Tags, occurredAt, namedTags[index]);
283
- }));
284
- }
245
+ this.recordAppendManyMetrics(result, eventsForEventSourceIds.length, durationInSeconds, batchMetricAttributes);
246
+ this.publishAppendMany(prepared, result);
285
247
 
286
248
  return result;
287
249
  } catch (error) {
@@ -294,6 +256,106 @@ export class EventSequence implements IEventSequence {
294
256
  }, correlationId);
295
257
  }
296
258
 
259
+ private applyBatchRouting(wireEvents: WireBatchEvent[], routings: ReturnType<typeof resolveBatchRouting>[]): void {
260
+ routings.forEach((routing, index) => {
261
+ if (!routing) return;
262
+ wireEvents[index] = {
263
+ ...wireEvents[index],
264
+ EventSourceType: routing.sourceType,
265
+ EventStreamType: routing.streamType ?? wireEvents[index].EventStreamType,
266
+ EventSource: routing.eventSource
267
+ };
268
+ });
269
+ }
270
+
271
+ private async sendAppendMany(
272
+ { correlationId, batchCausationChain, identity, concurrencyScopes, namedTags }: PreparedBatchAppend,
273
+ wireEvents: WireBatchEvent[]
274
+ ) {
275
+ const request = {
276
+ EventStore: this._eventStoreName,
277
+ Namespace: this._namespace,
278
+ EventSequenceId: this.id.value,
279
+ CorrelationId: toContractsGuid(correlationId),
280
+ Events: wireEvents,
281
+ Causation: toContractsCausation(batchCausationChain),
282
+ CausedBy: toContractsCausedBy(identity),
283
+ ConcurrencyScopes: [...concurrencyScopes].map(([eventSourceId, scope]) => ({
284
+ EventSourceId: eventSourceId,
285
+ Scope: this.toContractConcurrencyScope(scope)
286
+ }))
287
+ };
288
+ return namedTags.every(tagsForEvent => tagsForEvent.length === 0)
289
+ ? await this._connection.eventSequences.appendManyForEventSources(request)
290
+ : await this._connection.eventSequences.appendManyForEventSourcesWithNamedTags({
291
+ ...request,
292
+ Events: wireEvents.map((wireEvent, index) => ({ ...wireEvent, NamedTags: toContractsNamedTags(namedTags[index]) }))
293
+ });
294
+ }
295
+
296
+ private toAppendManyResults(appendManyResponse: ContractsAppendManyResponse, eventCount: number): AppendResult[] {
297
+ // Mirrors the C# client: every per-event AppendResult in a batch carries all
298
+ // constraint violations and the first concurrency violation of the whole batch —
299
+ // the wire response doesn't correlate either back to a specific event index.
300
+ const firstConcurrencyViolation: ContractsAppendManyResponse['ConcurrencyViolations'][number] | undefined =
301
+ (appendManyResponse.ConcurrencyViolations ?? [])[0];
302
+ const sequenceNumbers: ContractsAppendManyResponse['SequenceNumbers'] = appendManyResponse.SequenceNumbers ?? [];
303
+ const constraintViolations: ContractsAppendManyResponse['ConstraintViolations'] = appendManyResponse.ConstraintViolations ?? [];
304
+ const errors: ContractsAppendManyResponse['Errors'] = appendManyResponse.Errors ?? [];
305
+ if (sequenceNumbers.length === 0) {
306
+ const hasRejectionDetails = constraintViolations.length > 0 || errors.length > 0 || firstConcurrencyViolation !== undefined;
307
+ if (hasRejectionDetails) {
308
+ return Array.from({ length: eventCount }, () => this.mapAppendResponse(0n, constraintViolations, errors, firstConcurrencyViolation));
309
+ }
310
+ if (eventCount > 0) {
311
+ throw new Error('Append many events returned no sequence numbers or rejection details.');
312
+ }
313
+ }
314
+ return sequenceNumbers.map((sequenceNumber, index) =>
315
+ this.mapAppendResponse(
316
+ sequenceNumber,
317
+ constraintViolations,
318
+ errors.filter((_, errorIndex) => errorIndex === index),
319
+ firstConcurrencyViolation
320
+ )
321
+ );
322
+ }
323
+
324
+ private recordAppendManyMetrics(
325
+ result: AppendResult[],
326
+ eventCount: number,
327
+ durationInSeconds: number,
328
+ batchMetricAttributes: Record<string, string>
329
+ ): void {
330
+ ChronicleConventionMetrics.batchAppendsPerformed.add(1, batchMetricAttributes);
331
+ ChronicleConventionMetrics.eventsAppended.add(eventCount, batchMetricAttributes);
332
+ ChronicleConventionMetrics.appendManyDuration.record(durationInSeconds, batchMetricAttributes);
333
+
334
+ const totalViolations = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.constraintViolations.length, 0);
335
+ if (totalViolations > 0) {
336
+ ChronicleConventionMetrics.constraintViolations.add(totalViolations, batchMetricAttributes);
337
+ }
338
+ const totalErrors = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.errors.length, 0);
339
+ if (totalErrors > 0) {
340
+ ChronicleConventionMetrics.appendErrors.add(totalErrors, batchMetricAttributes);
341
+ }
342
+ }
343
+
344
+ private publishAppendMany(
345
+ { eventsForEventSourceIds, correlationId, batchCausationChain, eventsToAppend, namedTags }: PreparedBatchAppend,
346
+ result: AppendResult[]
347
+ ): void {
348
+ if (!this.appendOperations.hasSubscribers || result.length === 0) return;
349
+
350
+ const occurredAt = new Date();
351
+ const causationEntries = mapAppendNotificationCausation(batchCausationChain);
352
+ this.appendOperations.publish(result.map((appendResult: AppendResult, index: number) => {
353
+ const { eventSourceId, event } = eventsForEventSourceIds[index];
354
+ return createAppendNotification(eventSourceId, event, appendResult, correlationId.toString(),
355
+ causationEntries, eventsToAppend[index].Tags, occurredAt, namedTags[index]);
356
+ }));
357
+ }
358
+
297
359
  /** @inheritdoc */
298
360
  async getNextSequenceNumber(): Promise<EventSequenceNumber> {
299
361
  const tail = await this.getTailSequenceNumber();
@@ -447,61 +509,55 @@ export class EventSequence implements IEventSequence {
447
509
  /** @inheritdoc */
448
510
  async redact(sequenceNumber: EventSequenceNumber, reason: string): Promise<void> {
449
511
  causationManager.add(CausationType.redact, { sequenceNumber: sequenceNumber.value.toString() });
450
- const causationChain = causationManager.getCurrentChain();
451
- const identity = identityProvider.getCurrent();
452
512
 
453
- return observeOperation('redact', async span => {
454
- this.setSequenceAttributes(span);
455
- setSequenceNumber(span, sequenceNumber.value);
456
- try {
457
- ensureCommandSuccess('redact event', await this._connection.eventSequences.redact({
458
- EventStore: this._eventStoreName,
459
- Namespace: this._namespace,
460
- EventSequenceId: this.id.value,
461
- SequenceNumber: sequenceNumber.value,
462
- Reason: reason,
463
- Causation: causationChain.map(c => ({
464
- Occurred: { Value: c.occurred.toISOString() },
465
- Type: c.type.name,
466
- Properties: { ...c.properties }
467
- })),
468
- CausedBy: toContractsCausedBy(identity)
469
- }));
470
- span.setStatus({ code: SpanStatusCode.OK });
471
- } catch (error) {
472
- recordSafeException(span, error);
473
- throw error;
474
- } finally {
475
- span.end();
476
- }
477
- });
513
+ return this.sendRedaction('redact', 'redact event',
514
+ span => setSequenceNumber(span, sequenceNumber.value),
515
+ causedBy => this._connection.eventSequences.redact({
516
+ EventStore: this._eventStoreName,
517
+ Namespace: this._namespace,
518
+ EventSequenceId: this.id.value,
519
+ SequenceNumber: sequenceNumber.value,
520
+ Reason: reason,
521
+ ...causedBy
522
+ }));
478
523
  }
479
524
 
480
525
  /** @inheritdoc */
481
526
  async redactForEventSource(eventSourceId: string, reason: string, eventTypes?: Constructor[]): Promise<void> {
482
527
  causationManager.add(CausationType.redactForEventSource, { eventSourceId });
528
+ const wireEventTypeIds = (eventTypes ?? []).map(constructor => getEventTypeFor(constructor as unknown as Function).id.value);
529
+
530
+ return this.sendRedaction('redactForEventSource', 'redact event source',
531
+ span => setEventSourceId(span, eventSourceId, this._telemetry),
532
+ causedBy => this._connection.eventSequences.redactForEventSource({
533
+ EventStore: this._eventStoreName,
534
+ Namespace: this._namespace,
535
+ EventSequenceId: this.id.value,
536
+ EventSourceId: eventSourceId,
537
+ Reason: reason,
538
+ EventTypes: wireEventTypeIds,
539
+ ...causedBy
540
+ }));
541
+ }
542
+
543
+ private async sendRedaction(
544
+ operationName: 'redact' | 'redactForEventSource',
545
+ description: string,
546
+ describeSpan: (span: Span) => void,
547
+ send: (causedBy: { Causation: ReturnType<typeof toContractsCausation>; CausedBy: object }) => Promise<CallResultLike>
548
+ ): Promise<void> {
483
549
  const causationChain = causationManager.getCurrentChain();
484
550
  const identity = identityProvider.getCurrent();
485
- const wireEventTypeIds = (eventTypes ?? []).map(constructor => getEventTypeFor(constructor as unknown as Function).id.value);
486
551
 
487
- return observeOperation('redactForEventSource', async span => {
552
+ return observeOperation(operationName, async span => {
488
553
  this.setSequenceAttributes(span);
489
- setEventSourceId(span, eventSourceId, this._telemetry);
554
+ describeSpan(span);
490
555
  try {
491
- ensureCommandSuccess('redact event source', await this._connection.eventSequences.redactForEventSource({
492
- EventStore: this._eventStoreName,
493
- Namespace: this._namespace,
494
- EventSequenceId: this.id.value,
495
- EventSourceId: eventSourceId,
496
- Reason: reason,
497
- EventTypes: wireEventTypeIds,
498
- Causation: causationChain.map(c => ({
499
- Occurred: { Value: c.occurred.toISOString() },
500
- Type: c.type.name,
501
- Properties: { ...c.properties }
502
- })),
556
+ const causedBy = {
557
+ Causation: toContractsCausation(causationChain),
503
558
  CausedBy: toContractsCausedBy(identity)
504
- }));
559
+ };
560
+ ensureCommandSuccess(description, await send(causedBy));
505
561
  span.setStatus({ code: SpanStatusCode.OK });
506
562
  } catch (error) {
507
563
  recordSafeException(span, error);
@@ -607,8 +663,7 @@ export class EventSequence implements IEventSequence {
607
663
  return { isSuccess: true, failedPartitions: [] };
608
664
  }
609
665
 
610
- const timeoutMs = typeof options === 'number' ? options : options.timeoutMs ?? DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS;
611
- const callerSignal = typeof options === 'number' ? undefined : options.signal;
666
+ const { timeoutMs, callerSignal } = this.resolveWaitOptions(options);
612
667
  if (callerSignal?.aborted) throw callerSignal.reason;
613
668
 
614
669
  const controller = new AbortController();
@@ -635,28 +690,52 @@ export class EventSequence implements IEventSequence {
635
690
  }
636
691
  }
637
692
 
693
+ private resolveWaitOptions(options: number | WaitForCompletionOptions): { timeoutMs: number; callerSignal: AbortSignal | undefined } {
694
+ if (typeof options === 'number') return { timeoutMs: options, callerSignal: undefined };
695
+ return { timeoutMs: options.timeoutMs ?? DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS, callerSignal: options.signal };
696
+ }
697
+
638
698
  private readonly tailReader = (source?: string, sourceType?: string, streamType?: string, streamId?: string) =>
639
699
  this.getTailSequenceNumber(source, sourceType, streamType, streamId);
640
700
 
641
701
  private toContractConcurrencyScope(scope?: ConcurrencyScope) {
702
+ const expectsNoMatchingEvent = scope?.sequenceNumber === EventSequenceNumber.beforeFirst.value;
642
703
  return {
643
- SequenceNumber: scope?.sequenceNumber === EventSequenceNumber.beforeFirst.value
704
+ SequenceNumber: expectsNoMatchingEvent
644
705
  ? EventSequenceNumber.unset.value
645
706
  : scope?.sequenceNumber ?? EventSequenceNumber.unset.value,
646
- ExpectsNoMatchingEvent: scope?.sequenceNumber === EventSequenceNumber.beforeFirst.value,
707
+ ExpectsNoMatchingEvent: expectsNoMatchingEvent,
647
708
  EventSourceId: scope?.eventSourceId ?? false,
648
709
  EventStreamType: scope?.eventStreamType ?? '',
649
710
  EventStreamId: scope?.eventStreamId ?? '',
650
711
  EventSourceType: scope?.eventSourceType ?? '',
651
- EventTypes: (scope?.eventTypes ?? []).map(eventType => ({
652
- Id: eventType.id.value,
653
- Generation: eventType.generation.value,
654
- Tombstone: eventType.tombstone
655
- }))
712
+ EventTypes: toContractsEventTypes(scope)
656
713
  };
657
714
  }
658
715
  }
659
716
 
717
+ /**
718
+ * Converts an optional occurrence time into the contracts timestamp shape.
719
+ * @param occurred - The occurrence time, if any.
720
+ * @returns The contracts timestamp, or undefined when no time was given.
721
+ */
722
+ function toContractsOccurred(occurred: Date | undefined): { Value: string } | undefined {
723
+ return occurred === undefined ? undefined : { Value: occurred.toISOString() };
724
+ }
725
+
726
+ /**
727
+ * Converts the event types of a concurrency scope into the contracts shape.
728
+ * @param scope - The concurrency scope, if any.
729
+ * @returns The contracts event types.
730
+ */
731
+ function toContractsEventTypes(scope: ConcurrencyScope | undefined) {
732
+ return (scope?.eventTypes ?? []).map(eventType => ({
733
+ Id: eventType.id.value,
734
+ Generation: eventType.generation.value,
735
+ Tombstone: eventType.tombstone
736
+ }));
737
+ }
738
+
660
739
  /**
661
740
  * Converts structured named tags into the sequence contract shape.
662
741
  * @param namedTags - The validated named tags.
@@ -0,0 +1,7 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { prepareBatchAppend } from './prepareBatchAppend.js';
5
+
6
+ /** The serialized form of a batch append, ready to be sent to the kernel. */
7
+ export type PreparedBatchAppend = ReturnType<typeof prepareBatchAppend>;
@@ -0,0 +1,7 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { prepareSingleAppend } from './prepareSingleAppend.js';
5
+
6
+ /** The serialized form of a single append, ready to be sent to the kernel. */
7
+ export type PreparedSingleAppend = ReturnType<typeof prepareSingleAppend>;
@@ -0,0 +1,7 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { PreparedBatchAppend } from './PreparedBatchAppend.js';
5
+
6
+ /** A wire event of a batch append, optionally carrying the definition event source it is routed to. */
7
+ export type WireBatchEvent = PreparedBatchAppend['eventsToAppend'][number] & { EventSource?: string };
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { causationManager } from '../auditing/index.js';
5
+
6
+ /**
7
+ * Converts a causation chain into the Causation shape used by Chronicle contracts.
8
+ * @param causationChain - The causation chain to convert.
9
+ * @returns The contracts causation entries.
10
+ */
11
+ export function toContractsCausation(causationChain: ReturnType<typeof causationManager.getCurrentChain>) {
12
+ return causationChain.map(c => ({
13
+ Occurred: { Value: c.occurred.toISOString() },
14
+ Type: c.type.name,
15
+ Properties: { ...c.properties }
16
+ }));
17
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { ChronicleClient } from '../ChronicleClient.js';
6
+ import { ChronicleOptions } from '../ChronicleOptions.js';
7
+
8
+ const connections = vi.hoisted(() => [] as Record<string, unknown>[]);
9
+ vi.mock('../connection/ChronicleConnection.js', () => ({
10
+ ChronicleConnection: class {
11
+ constructor(options: Record<string, unknown>) { connections.push(options); }
12
+ disconnect() {}
13
+ }
14
+ }));
15
+ vi.mock('../connection/KernelKeepAlive.js', () => ({ KernelKeepAlive: class { async start() {} } }));
16
+
17
+ describe('when creating a client', () => {
18
+ it('should give the connection the default message sizes, so a read model past 4 MB is not refused', () => {
19
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [] }));
20
+ client.dispose();
21
+ expect(connections.at(-1)?.maxReceiveMessageSize).toBe(100 * 1024 * 1024);
22
+ expect(connections.at(-1)?.maxSendMessageSize).toBe(100 * 1024 * 1024);
23
+ });
24
+
25
+ it('should give the connection the configured message sizes', () => {
26
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [], maxReceiveMessageSize: 1, maxSendMessageSize: 2 }));
27
+ client.dispose();
28
+ expect(connections.at(-1)?.maxReceiveMessageSize).toBe(1);
29
+ expect(connections.at(-1)?.maxSendMessageSize).toBe(2);
30
+ });
31
+
32
+ it('should leave the gRPC default in place when asked for null', () => {
33
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [], maxReceiveMessageSize: null }));
34
+ client.dispose();
35
+ expect(connections.at(-1)?.maxReceiveMessageSize).toBeUndefined();
36
+ });
37
+ });