@cratis/chronicle 6.49.0 → 6.50.0

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 (126) hide show
  1. package/dist/eventSequences/AppendOptions.d.ts +10 -0
  2. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  3. package/dist/eventSequences/EventForEventSourceId.d.ts +6 -0
  4. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  5. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  6. package/dist/eventSequences/EventSequence.js +28 -8
  7. package/dist/eventSequences/EventSequence.js.map +1 -1
  8. package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -5
  9. package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
  10. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
  11. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
  12. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
  13. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
  14. package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -3
  15. package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
  16. package/dist/eventSequences/TransactionalEventSequence.js +7 -4
  17. package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
  18. package/dist/eventSequences/createAppendNotification.d.ts +2 -1
  19. package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
  20. package/dist/eventSequences/createAppendNotification.js +3 -2
  21. package/dist/eventSequences/createAppendNotification.js.map +1 -1
  22. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
  23. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
  24. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
  25. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
  26. package/dist/eventSequences/index.d.ts +1 -0
  27. package/dist/eventSequences/index.d.ts.map +1 -1
  28. package/dist/eventSequences/index.js +1 -0
  29. package/dist/eventSequences/index.js.map +1 -1
  30. package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
  31. package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
  32. package/dist/eventSequences/prepareBatchAppend.js +4 -1
  33. package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
  34. package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
  35. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
  36. package/dist/eventSequences/prepareSingleAppend.js +3 -1
  37. package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
  38. package/dist/events/EventContext.d.ts +7 -0
  39. package/dist/events/EventContext.d.ts.map +1 -1
  40. package/dist/events/InvalidNamedTag.d.ts +8 -0
  41. package/dist/events/InvalidNamedTag.d.ts.map +1 -0
  42. package/dist/events/InvalidNamedTag.js +13 -0
  43. package/dist/events/InvalidNamedTag.js.map +1 -0
  44. package/dist/events/NamedTag.d.ts +25 -0
  45. package/dist/events/NamedTag.d.ts.map +1 -0
  46. package/dist/events/NamedTag.js +39 -0
  47. package/dist/events/NamedTag.js.map +1 -0
  48. package/dist/events/index.d.ts +3 -0
  49. package/dist/events/index.d.ts.map +1 -1
  50. package/dist/events/index.js +3 -0
  51. package/dist/events/index.js.map +1 -1
  52. package/dist/events/mergeNamedTags.d.ts +13 -0
  53. package/dist/events/mergeNamedTags.d.ts.map +1 -0
  54. package/dist/events/mergeNamedTags.js +39 -0
  55. package/dist/events/mergeNamedTags.js.map +1 -0
  56. package/dist/events/toClientEventContext.d.ts.map +1 -1
  57. package/dist/events/toClientEventContext.js +3 -1
  58. package/dist/events/toClientEventContext.js.map +1 -1
  59. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
  60. package/dist/projections/eventContextPropertyExpression.js +2 -1
  61. package/dist/projections/eventContextPropertyExpression.js.map +1 -1
  62. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  63. package/dist/testing/InProcessEventSequence.js +17 -7
  64. package/dist/testing/InProcessEventSequence.js.map +1 -1
  65. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  66. package/dist/testing/ReactorScenario.js +6 -0
  67. package/dist/testing/ReactorScenario.js.map +1 -1
  68. package/dist/testing/singleAppendRoute.d.ts +5 -0
  69. package/dist/testing/singleAppendRoute.d.ts.map +1 -1
  70. package/dist/testing/singleAppendRoute.js +12 -1
  71. package/dist/testing/singleAppendRoute.js.map +1 -1
  72. package/dist/transactions/IUnitOfWork.d.ts +4 -3
  73. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  74. package/dist/transactions/TransactionalAppendOptions.d.ts +14 -0
  75. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
  76. package/dist/transactions/TransactionalAppendOptions.js +4 -0
  77. package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
  78. package/dist/transactions/UnitOfWork.d.ts +2 -2
  79. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  80. package/dist/transactions/UnitOfWork.js +9 -3
  81. package/dist/transactions/UnitOfWork.js.map +1 -1
  82. package/dist/transactions/index.d.ts +1 -0
  83. package/dist/transactions/index.d.ts.map +1 -1
  84. package/dist/tsconfig.tsbuildinfo +1 -1
  85. package/eventSequences/AppendOptions.ts +11 -0
  86. package/eventSequences/EventForEventSourceId.ts +7 -0
  87. package/eventSequences/EventSequence.ts +30 -8
  88. package/eventSequences/ITransactionalEventSequence.ts +5 -5
  89. package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
  90. package/eventSequences/TransactionalEventSequence.ts +8 -5
  91. package/eventSequences/createAppendNotification.ts +4 -2
  92. package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
  93. package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
  94. package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
  95. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
  96. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
  97. package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
  98. package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
  99. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
  100. package/eventSequences/index.ts +1 -0
  101. package/eventSequences/prepareBatchAppend.ts +4 -1
  102. package/eventSequences/prepareSingleAppend.ts +3 -1
  103. package/events/EventContext.ts +8 -0
  104. package/events/InvalidNamedTag.ts +13 -0
  105. package/events/NamedTag.ts +44 -0
  106. package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
  107. package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
  108. package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
  109. package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
  110. package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
  111. package/events/index.ts +3 -0
  112. package/events/mergeNamedTags.ts +37 -0
  113. package/events/toClientEventContext.ts +3 -1
  114. package/package.json +5 -5
  115. package/projections/eventContextPropertyExpression.ts +3 -2
  116. package/testing/InProcessEventSequence.ts +17 -7
  117. package/testing/ReactorScenario.ts +6 -0
  118. package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
  119. package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
  120. package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
  121. package/testing/singleAppendRoute.ts +13 -1
  122. package/transactions/IUnitOfWork.ts +4 -3
  123. package/transactions/TransactionalAppendOptions.ts +17 -0
  124. package/transactions/UnitOfWork.ts +13 -4
  125. package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
  126. package/transactions/index.ts +1 -0
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { Constructor } from '@cratis/fundamentals';
5
5
  import type { Tag } from '../events/Tag.js';
6
+ import type { NamedTag } from '../events/NamedTag.js';
6
7
 
7
8
  /**
8
9
  * Represents an event paired with the event source identifier it belongs to.
@@ -36,6 +37,12 @@ export interface EventForEventSourceId {
36
37
  */
37
38
  readonly tags?: ReadonlyArray<string | Tag>;
38
39
 
40
+ /**
41
+ * Optional structured named tags for this event. They are merged, in order, with the shared
42
+ * `AppendOptions.namedTags`; the first occurrence of each exact name and value pair is kept.
43
+ */
44
+ readonly namedTags?: ReadonlyArray<NamedTag>;
45
+
39
46
  /**
40
47
  * Optional registered event source definition (class decorated with `@eventSource`, or its name) for this event.
41
48
  * Overrides `AppendOptions.eventSource`, so one batch can mix several event sources.
@@ -14,6 +14,7 @@ import { prepareBatchAppend } from './prepareBatchAppend.js';
14
14
  import { createAppendNotification, mapAppendNotificationCausation } from './createAppendNotification.js';
15
15
  import { getEventTypeFor } from '../events/eventTypeDecorator.js';
16
16
  import type { AppendedEvent } from '../events/AppendedEvent.js';
17
+ import type { NamedTag } from '../events/NamedTag.js';
17
18
  import { toClientEventContext } from '../events/toClientEventContext.js';
18
19
  import { DecoratorType } from '../types/DecoratorType.js';
19
20
  import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
@@ -50,6 +51,7 @@ import type { ConcurrencyScope } from './ConcurrencyScope.js';
50
51
  import type { IEventSources } from '../eventSources/IEventSources.js';
51
52
  import { deriveConcurrencyScope, planDerivedGuards, resolveBatchRouting, resolveSingleRouting } from './resolveEventSourceRouting.js';
52
53
  import { IUnitOfWorkManager } from '../transactions/IUnitOfWorkManager.js';
54
+ import { ensureNamedTagBatchIsSupported } from './ensureNamedTagBatchIsSupported.js';
53
55
 
54
56
  /**
55
57
  * Implements {@link IEventSequence} by communicating with the Chronicle Kernel
@@ -75,7 +77,7 @@ export class EventSequence implements IEventSequence {
75
77
  /** @inheritdoc */
76
78
  async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
77
79
  const routing = resolveSingleRouting(this._eventSources, options);
78
- const { eventType, correlationId, content, tags, causationChain, identity, subject } = prepareSingleAppend(event, options);
80
+ const { eventType, correlationId, content, tags, namedTags, causationChain, identity, subject } = prepareSingleAppend(event, options);
79
81
 
80
82
  const sequenceMetricAttributes = {
81
83
  [names.attributes.eventStore]: this._eventStoreName,
@@ -95,7 +97,7 @@ export class EventSequence implements IEventSequence {
95
97
  const concurrencyScope = options?.concurrencyScope ?? (routing
96
98
  ? await deriveConcurrencyScope(routing, eventSourceId, options?.streamId, this.tailReader)
97
99
  : undefined);
98
- const response = await this._connection.eventSequences.append({
100
+ const request = {
99
101
  EventStore: this._eventStoreName,
100
102
  Namespace: this._namespace,
101
103
  EventSequenceId: this.id.value,
@@ -121,7 +123,11 @@ export class EventSequence implements IEventSequence {
121
123
  Tags: tags,
122
124
  Occurred: options?.occurred === undefined ? undefined : { Value: options.occurred.toISOString() },
123
125
  Subject: subject ?? eventSourceId
124
- });
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) });
125
131
 
126
132
  const appendResponse = ensureCommandResponse('append event', response);
127
133
  const durationInSeconds = (performance.now() - startTime) / 1000;
@@ -145,7 +151,7 @@ export class EventSequence implements IEventSequence {
145
151
 
146
152
  if (this.appendOperations.hasSubscribers) {
147
153
  this.appendOperations.publish([createAppendNotification(eventSourceId, event, result,
148
- correlationId.toString(), mapAppendNotificationCausation(causationChain), tags)]);
154
+ correlationId.toString(), mapAppendNotificationCausation(causationChain), tags, undefined, namedTags)]);
149
155
  }
150
156
 
151
157
  return result;
@@ -167,11 +173,12 @@ export class EventSequence implements IEventSequence {
167
173
  eventsOrOptions?: object[] | AppendOptions,
168
174
  options?: AppendOptions
169
175
  ): Promise<AppendResult[]> {
170
- const { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend } =
176
+ const { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend, namedTags } =
171
177
  prepareBatchAppend(eventSourceIdOrEvents, eventsOrOptions, options);
172
178
 
173
179
  const wireEvents: Array<(typeof eventsToAppend)[number] & { EventSource?: string }> = eventsToAppend;
174
180
  const routings = eventsForEventSourceIds.map(event => resolveBatchRouting(this._eventSources, event, appendOptions));
181
+ ensureNamedTagBatchIsSupported(namedTags.some(tagsForEvent => tagsForEvent.length > 0), routings.some(routing => routing !== undefined));
175
182
  const explicitIds = new Set([...concurrencyScopes].filter(([, scope]) => scope !== undefined).map(([id]) => id));
176
183
  const derivedGuards = planDerivedGuards(routings, eventsForEventSourceIds, appendOptions?.streamId, explicitIds);
177
184
  const distinctEventSourceIds = [...new Set(eventsForEventSourceIds.map(_ => _.eventSourceId))];
@@ -202,7 +209,7 @@ export class EventSequence implements IEventSequence {
202
209
  for (const [id, { routing, streamId }] of derivedGuards) {
203
210
  concurrencyScopes.set(id, await deriveConcurrencyScope(routing, id, streamId, this.tailReader));
204
211
  }
205
- const response = await this._connection.eventSequences.appendManyForEventSources({
212
+ const request = {
206
213
  EventStore: this._eventStoreName,
207
214
  Namespace: this._namespace,
208
215
  EventSequenceId: this.id.value,
@@ -218,7 +225,13 @@ export class EventSequence implements IEventSequence {
218
225
  EventSourceId: eventSourceId,
219
226
  Scope: this.toContractConcurrencyScope(scope)
220
227
  }))
221
- });
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
+ });
222
235
 
223
236
  const appendManyResponse = ensureCommandResponse('append many events', response);
224
237
  const durationInSeconds = (performance.now() - startTime) / 1000;
@@ -266,7 +279,7 @@ export class EventSequence implements IEventSequence {
266
279
  this.appendOperations.publish(result.map((appendResult: AppendResult, index: number) => {
267
280
  const { eventSourceId, event } = eventsForEventSourceIds[index];
268
281
  return createAppendNotification(eventSourceId, event, appendResult, correlationId.toString(),
269
- causationEntries, eventsToAppend[index].Tags, occurredAt);
282
+ causationEntries, eventsToAppend[index].Tags, occurredAt, namedTags[index]);
270
283
  }));
271
284
  }
272
285
 
@@ -644,6 +657,15 @@ export class EventSequence implements IEventSequence {
644
657
  }
645
658
  }
646
659
 
660
+ /**
661
+ * Converts structured named tags into the sequence contract shape.
662
+ * @param namedTags - The validated named tags.
663
+ * @returns The wire named tags.
664
+ */
665
+ function toContractsNamedTags(namedTags: readonly NamedTag[]): { Name: string; Value: string }[] {
666
+ return namedTags.map(tag => ({ Name: tag.name, Value: tag.value }));
667
+ }
668
+
647
669
  /**
648
670
  * Converts an {@link Identity} into the CausedBy shape used by Chronicle contracts.
649
671
  * @param identity - The identity to convert.
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { TransactionalEventRouting } from '../transactions/TransactionalEventRouting.js';
4
+ import type { TransactionalAppendOptions } from '../transactions/TransactionalAppendOptions.js';
5
5
  import { IUnitOfWork } from '../transactions/IUnitOfWork.js';
6
6
 
7
7
  /**
@@ -15,15 +15,15 @@ export interface ITransactionalEventSequence {
15
15
  * Adds a single event to the current unit of work.
16
16
  * @param eventSourceId - The identifier of the event source.
17
17
  * @param event - The event to append.
18
- * @param routing - Optional registered event source / stream routing.
18
+ * @param options - Optional registered event source / stream routing and named tags.
19
19
  */
20
- append(eventSourceId: string, event: object, routing?: TransactionalEventRouting): Promise<void>;
20
+ append(eventSourceId: string, event: object, options?: TransactionalAppendOptions): Promise<void>;
21
21
 
22
22
  /**
23
23
  * Adds multiple events to the current unit of work.
24
24
  * @param eventSourceId - The identifier of the event source.
25
25
  * @param events - The events to append.
26
- * @param routing - Optional registered event source / stream routing applied to every event.
26
+ * @param options - Optional registered event source / stream routing and named tags applied to every event.
27
27
  */
28
- appendMany(eventSourceId: string, events: object[], routing?: TransactionalEventRouting): Promise<void>;
28
+ appendMany(eventSourceId: string, events: object[], options?: TransactionalAppendOptions): Promise<void>;
29
29
  }
@@ -0,0 +1,12 @@
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
+ /** Named-tag batches cannot preserve registered event source routing on the current kernel. */
5
+ export class NamedTagsWithRegisteredEventSourceNotSupported extends Error {
6
+ constructor() {
7
+ super('Batches and unit-of-work commits cannot combine non-empty named tags with registered event sources: ' +
8
+ 'the kernel named-tag batch operation drops registered event source routing. ' +
9
+ 'Use single append calls instead. See https://github.com/Cratis/Chronicle/issues/4603.');
10
+ this.name = 'NamedTagsWithRegisteredEventSourceNotSupported';
11
+ }
12
+ }
@@ -1,7 +1,8 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { TransactionalEventRouting } from '../transactions/TransactionalEventRouting.js';
4
+ import type { TransactionalAppendOptions } from '../transactions/TransactionalAppendOptions.js';
5
+ import { mergeNamedTags } from '../events/mergeNamedTags.js';
5
6
  import { IUnitOfWork } from '../transactions/IUnitOfWork.js';
6
7
  import { IUnitOfWorkManager } from '../transactions/IUnitOfWorkManager.js';
7
8
  import { IEventSequence } from './IEventSequence.js';
@@ -22,14 +23,16 @@ export class TransactionalEventSequence implements ITransactionalEventSequence {
22
23
  }
23
24
 
24
25
  /** @inheritdoc */
25
- async append(eventSourceId: string, event: object, routing?: TransactionalEventRouting): Promise<void> {
26
- this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, routing);
26
+ async append(eventSourceId: string, event: object, options?: TransactionalAppendOptions): Promise<void> {
27
+ this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, options);
27
28
  }
28
29
 
29
30
  /** @inheritdoc */
30
- async appendMany(eventSourceId: string, events: object[], routing?: TransactionalEventRouting): Promise<void> {
31
+ async appendMany(eventSourceId: string, events: object[], options?: TransactionalAppendOptions): Promise<void> {
32
+ // Validate once up front so an invalid tag adds none of the events.
33
+ const shared = options?.namedTags === undefined ? options : { ...options, namedTags: mergeNamedTags(options.namedTags) };
31
34
  for (const event of events) {
32
- this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, routing);
35
+ this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, shared);
33
36
  }
34
37
  }
35
38
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { getEventTypeFor } from '../events/eventTypeDecorator.js';
5
5
  import { Tag } from '../events/Tag.js';
6
+ import type { NamedTag } from '../events/NamedTag.js';
6
7
  import type { CausationEntry } from '../events/CausationEntry.js';
7
8
  import type { AppendResult } from './AppendResult.js';
8
9
  import type { AppendedEventWithResult } from './AppendedEventWithResult.js';
@@ -17,7 +18,7 @@ export function mapAppendNotificationCausation(chain: readonly Causation[]): Cau
17
18
  /** Maps a successful append into the client-side appendOperations notification shape. */
18
19
  export function createAppendNotification(
19
20
  eventSourceId: string, event: object, result: AppendResult, correlationId: string,
20
- causation: CausationEntry[], tags: readonly string[], occurredAt = new Date()
21
+ causation: CausationEntry[], tags: readonly string[], occurredAt = new Date(), namedTags: readonly NamedTag[] = []
21
22
  ): AppendedEventWithResult {
22
23
  const eventType = getEventTypeFor(event.constructor);
23
24
  return {
@@ -25,7 +26,8 @@ export function createAppendNotification(
25
26
  context: {
26
27
  sequenceNumber: result.sequenceNumber.value, eventSourceId, eventType, occurred: occurredAt, correlationId,
27
28
  causation,
28
- tags: tags.map(value => new Tag(value))
29
+ tags: tags.map(value => new Tag(value)),
30
+ namedTags: [...namedTags]
29
31
  },
30
32
  eventType,
31
33
  content: event as Record<string, unknown>
@@ -0,0 +1,11 @@
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 { NamedTagsWithRegisteredEventSourceNotSupported } from './NamedTagsWithRegisteredEventSourceNotSupported.js';
5
+
6
+ /** Rejects the combination before any RPC, even when tags and registered routing belong to different entries. */
7
+ export function ensureNamedTagBatchIsSupported(hasNamedTags: boolean, hasRegisteredEventSource: boolean): void {
8
+ if (hasNamedTags && hasRegisteredEventSource) {
9
+ throw new NamedTagsWithRegisteredEventSourceNotSupported();
10
+ }
11
+ }
@@ -0,0 +1,107 @@
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 { randomUUID } from 'node:crypto';
5
+ import { afterAll, beforeAll, chai, describe, it } from 'vitest';
6
+ import { field } from '@cratis/fundamentals';
7
+ import { ObserverRunningState } from '@cratis/chronicle.contracts';
8
+ import { ChronicleClient, ChronicleOptions, NamedTag, eventType, reactor, type AppendedEvent, type EventContext,
9
+ type IClientArtifactsProvider, type IEventStore } from '../../../index.js';
10
+ import type { ChronicleConnection } from '../../../connection/index.js';
11
+
12
+ chai.should();
13
+ function should(value: unknown): ReturnType<typeof chai.expect> {
14
+ return (value as { should: ReturnType<typeof chai.expect> }).should;
15
+ }
16
+ const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
17
+
18
+ @eventType()
19
+ class NamedTagsItemImported {
20
+ @field(String) sku = '';
21
+ }
22
+
23
+ const delivered = new Map<string, EventContext>();
24
+ @reactor()
25
+ class NamedTagsImportTracker {
26
+ namedTagsItemImported(event: NamedTagsItemImported, context: EventContext) {
27
+ delivered.set(event.sku, context);
28
+ }
29
+ }
30
+
31
+ const artifacts: IClientArtifactsProvider = {
32
+ eventTypes: [NamedTagsItemImported], reactors: [NamedTagsImportTracker], reducers: [], readModels: [], seeders: [],
33
+ constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
34
+ };
35
+
36
+ async function eventually(accept: () => boolean | Promise<boolean>, timeoutMs = 30_000): Promise<void> {
37
+ const deadline = Date.now() + timeoutMs;
38
+ while (!await accept()) {
39
+ if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
40
+ await new Promise(resolve => setTimeout(resolve, 250));
41
+ }
42
+ }
43
+
44
+ const pairs = (context: { namedTags?: ReadonlyArray<NamedTag> }) => (context.namedTags ?? []).map(tag => `${tag.name}=${tag.value}`);
45
+ const item = (sku: string) => Object.assign(new NamedTagsItemImported(), { sku });
46
+
47
+ describe.skipIf(!connectionString && !process.env.CI)('when appending named tags against a kernel', () => {
48
+ const storeName = `NamedTags${randomUUID().replaceAll('-', '').slice(0, 12)}`;
49
+ const source = randomUUID();
50
+ let client: ChronicleClient;
51
+ let store: IEventStore;
52
+ let stored: Map<string, AppendedEvent>;
53
+
54
+ beforeAll(async () => {
55
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
56
+ discoveryPatterns: [], clientArtifactsProvider: artifacts
57
+ }));
58
+ store = await client.getEventStore(storeName);
59
+ const connection = (store as unknown as { _connection: ChronicleConnection })._connection;
60
+ await eventually(async () => {
61
+ const observer = await connection.observers.getObserverInformation({ EventStore: storeName, Namespace: 'Default',
62
+ ObserverId: NamedTagsImportTracker.name, EventSequenceId: 'event-log' });
63
+ return observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active;
64
+ });
65
+
66
+ const single = await store.eventLog.append(source, item('single'), {
67
+ tags: ['import'],
68
+ namedTags: [new NamedTag('import-batch', 'b-1'), new NamedTag('import-batch', 'B-1'), new NamedTag('empty', ''),
69
+ new NamedTag('import-batch', 'b-1')]
70
+ });
71
+ should(single.isSuccess).be.true;
72
+ const batch = await store.eventLog.appendMany([
73
+ { eventSourceId: source, event: item('debit'), namedTags: [new NamedTag('side', 'debit')] },
74
+ { eventSourceId: source, event: item('credit'), namedTags: [new NamedTag('side', 'credit'), new NamedTag('transfer', 't-1')] }
75
+ ], { namedTags: [new NamedTag('transfer', 't-1')] });
76
+ should(batch.every(result => result.isSuccess)).be.true;
77
+ const untagged = await store.eventLog.append(source, item('untagged'));
78
+ should(untagged.isSuccess).be.true;
79
+
80
+ const unitOfWork = store.unitOfWorkManager.begin();
81
+ await store.eventLog.transactional.append(source, item('transactional'), { namedTags: [new NamedTag('unit', 'u-1')] });
82
+ await unitOfWork.commit();
83
+ should(unitOfWork.isSuccess).be.true;
84
+
85
+ const events = await store.eventLog.getForEventSourceIdAndEventTypes(source, [NamedTagsItemImported]);
86
+ stored = new Map(events.map(event => [(event.content as { sku: string }).sku, event]));
87
+ await eventually(() => delivered.has('transactional'));
88
+ });
89
+ afterAll(() => client?.dispose());
90
+
91
+ it('should round-trip distinct single-append named tags in order through a read', () =>
92
+ should(pairs(stored.get('single')!.context)).deep.equal(['import-batch=b-1', 'import-batch=B-1', 'empty=']));
93
+ it('should keep plain tags beside named tags', () =>
94
+ should(stored.get('single')!.context.tags.map(tag => tag.value)).deep.equal(['import']));
95
+ it('should store each batch event with its own tags followed by the shared tags', () => {
96
+ should(pairs(stored.get('debit')!.context)).deep.equal(['side=debit', 'transfer=t-1']);
97
+ should(pairs(stored.get('credit')!.context)).deep.equal(['side=credit', 'transfer=t-1']);
98
+ });
99
+ it('should store an untagged event with no named tags', () => should(pairs(stored.get('untagged')!.context)).deep.equal([]));
100
+ it('should store named tags added through a unit of work', () =>
101
+ should(pairs(stored.get('transactional')!.context)).deep.equal(['unit=u-1']));
102
+ it('should deliver the named tags to reactors in the event context', () => {
103
+ should(pairs(delivered.get('single')!)).deep.equal(['import-batch=b-1', 'import-batch=B-1', 'empty=']);
104
+ should(pairs(delivered.get('credit')!)).deep.equal(['side=credit', 'transfer=t-1']);
105
+ should(pairs(delivered.get('untagged')!)).deep.equal([]);
106
+ });
107
+ });
@@ -0,0 +1,38 @@
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 { vi } from 'vitest';
5
+ import { EventSequence, EventSequenceId } from '../../index.js';
6
+ import { eventType } from '../../../events/index.js';
7
+ import type { ChronicleConnection } from '../../../connection/index.js';
8
+ import type { IUnitOfWorkManager } from '../../../transactions/index.js';
9
+ import { ConcurrencyDimensions, type IEventSources } from '../../../eventSources/index.js';
10
+
11
+ export class Account {}
12
+
13
+ /** A registered source, optionally requiring a derived tail read. */
14
+ export function registered_sources(concurrency = ConcurrencyDimensions.none): IEventSources {
15
+ return {
16
+ getFor: () => ({ type: Account, name: 'Account', description: '', concurrency, streams: [] })
17
+ } as unknown as IEventSources;
18
+ }
19
+
20
+ export class NamedTagRecorded { constructor(readonly value = 'payload') {} }
21
+ eventType('NamedTagRecorded')(NamedTagRecorded);
22
+
23
+ /** An event sequence whose four append calls are recorded. */
24
+ export function a_sequence(eventSources?: IEventSources) {
25
+ const single = { Response: { SequenceNumber: 0n, ConstraintViolations: [], Errors: [] } };
26
+ const batch = async (request: { Events: unknown[] }) =>
27
+ ({ Response: { SequenceNumbers: request.Events.map((_, index) => BigInt(index)), ConstraintViolations: [], Errors: [] } });
28
+ const append = vi.fn().mockResolvedValue(single);
29
+ const appendWithNamedTags = vi.fn().mockResolvedValue(single);
30
+ const appendManyForEventSources = vi.fn().mockImplementation(batch);
31
+ const appendManyForEventSourcesWithNamedTags = vi.fn().mockImplementation(batch);
32
+ const tailSequenceNumber = vi.fn().mockResolvedValue({ Data: { SequenceNumber: 0n } });
33
+ const connection = { eventSequences: { append, appendWithNamedTags, appendManyForEventSources, appendManyForEventSourcesWithNamedTags, tailSequenceNumber } } as unknown as ChronicleConnection;
34
+ return {
35
+ sequence: new EventSequence(EventSequenceId.eventLog, 'store', 'tenant', connection, {} as IUnitOfWorkManager, undefined, undefined, eventSources),
36
+ append, appendWithNamedTags, appendManyForEventSources, appendManyForEventSourcesWithNamedTags, tailSequenceNumber
37
+ };
38
+ }
@@ -0,0 +1,49 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import type { AppendManyForEventSourcesWithNamedTagsRequest } from '@cratis/chronicle.contracts';
6
+ import { NamedTag } from '../../../events/index.js';
7
+ import { a_sequence, NamedTagRecorded } from './given.fixture.js';
8
+
9
+ chai.should();
10
+
11
+ describe('when appending a batch with per-event and shared named tags', () => {
12
+ let calls: ReturnType<typeof a_sequence>;
13
+ let request: AppendManyForEventSourcesWithNamedTagsRequest;
14
+ beforeEach(async () => {
15
+ calls = a_sequence();
16
+ await calls.sequence.appendMany([
17
+ { eventSourceId: 'from', event: new NamedTagRecorded(), namedTags: [new NamedTag('side', 'debit'), new NamedTag('transfer', 't-1')] },
18
+ { eventSourceId: 'to', event: new NamedTagRecorded(), namedTags: [new NamedTag('side', 'credit')] },
19
+ { eventSourceId: 'audit', event: new NamedTagRecorded() }
20
+ ], { namedTags: [new NamedTag('transfer', 't-1')] });
21
+ request = calls.appendManyForEventSourcesWithNamedTags.mock.calls[0][0];
22
+ });
23
+ it('should use the dedicated named-tag batch call', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(0));
24
+ it('should send each event its own tags before the shared tags without duplicates', () =>
25
+ request.Events.map(event => event.NamedTags.map(tag => `${tag.Name}=${tag.Value}`)).should.deep.equal([
26
+ ['side=debit', 'transfer=t-1'], ['side=credit', 'transfer=t-1'], ['transfer=t-1']
27
+ ]));
28
+ });
29
+
30
+ describe('when appending many events for one source with shared named tags', () => {
31
+ let request: AppendManyForEventSourcesWithNamedTagsRequest;
32
+ beforeEach(async () => {
33
+ const calls = a_sequence();
34
+ await calls.sequence.appendMany('source', [new NamedTagRecorded(), new NamedTagRecorded()], { namedTags: [new NamedTag('import', 'i-1')] });
35
+ request = calls.appendManyForEventSourcesWithNamedTags.mock.calls[0][0];
36
+ });
37
+ it('should give every event the shared named tags', () =>
38
+ request.Events.map(event => event.NamedTags).should.deep.equal([[{ Name: 'import', Value: 'i-1' }], [{ Name: 'import', Value: 'i-1' }]]));
39
+ });
40
+
41
+ describe('when appending a batch without named tags', () => {
42
+ let calls: ReturnType<typeof a_sequence>;
43
+ beforeEach(async () => {
44
+ calls = a_sequence();
45
+ await calls.sequence.appendMany([{ eventSourceId: 'one', event: new NamedTagRecorded(), namedTags: [] }]);
46
+ });
47
+ it('should keep using the plain batch call', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(1));
48
+ it('should not use the named-tag batch call', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
49
+ });
@@ -0,0 +1,47 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import { AppendWithNamedTagsRequest } from '@cratis/chronicle.contracts';
6
+ import { NamedTag } from '../../../events/index.js';
7
+ import type { AppendedEventWithResult } from '../../AppendedEventWithResult.js';
8
+ import { a_sequence, NamedTagRecorded } from './given.fixture.js';
9
+
10
+ chai.should();
11
+
12
+ describe('when appending a single event with named tags', () => {
13
+ let calls: ReturnType<typeof a_sequence>;
14
+ let request: AppendWithNamedTagsRequest;
15
+ let notification: AppendedEventWithResult;
16
+ beforeEach(async () => {
17
+ calls = a_sequence();
18
+ const iterator = calls.sequence.appendOperations[Symbol.asyncIterator]();
19
+ const next = iterator.next();
20
+ await calls.sequence.append('source', new NamedTagRecorded(), {
21
+ tags: ['checkout'],
22
+ namedTags: [new NamedTag('session', 's-1'), new NamedTag('session', 's-1'), new NamedTag('channel', 'web')]
23
+ });
24
+ notification = (await next).value[0];
25
+ await iterator.return?.();
26
+ request = calls.appendWithNamedTags.mock.calls[0][0];
27
+ });
28
+ it('should use the dedicated named-tag call instead of the plain append', () => calls.append.mock.calls.should.have.lengthOf(0));
29
+ it('should send the distinct named tags in order', () =>
30
+ request.NamedTags.should.deep.equal([{ Name: 'session', Value: 's-1' }, { Name: 'channel', Value: 'web' }]));
31
+ it('should keep the plain tags beside the named tags', () => request.Tags.should.deep.equal(['checkout']));
32
+ it('should encode through the published contract', () =>
33
+ AppendWithNamedTagsRequest.decode(AppendWithNamedTagsRequest.encode(AppendWithNamedTagsRequest.fromPartial(request)).finish())
34
+ .NamedTags.should.have.lengthOf(2));
35
+ it('should carry the named tags on the append notification context', () =>
36
+ notification.event.context.namedTags!.map(tag => tag.name).should.deep.equal(['session', 'channel']));
37
+ });
38
+
39
+ describe('when appending a single event with an empty named tag list', () => {
40
+ let calls: ReturnType<typeof a_sequence>;
41
+ beforeEach(async () => {
42
+ calls = a_sequence();
43
+ await calls.sequence.append('source', new NamedTagRecorded(), { namedTags: [] });
44
+ });
45
+ it('should use the plain append call', () => calls.append.mock.calls.should.have.lengthOf(1));
46
+ it('should not use the named-tag call', () => calls.appendWithNamedTags.mock.calls.should.have.lengthOf(0));
47
+ });
@@ -0,0 +1,36 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import { InvalidNamedTag, type NamedTag } from '../../../events/index.js';
6
+ import { a_sequence, NamedTagRecorded } from './given.fixture.js';
7
+
8
+ chai.should();
9
+ const blank = { name: '', value: 'x' } as NamedTag;
10
+
11
+ describe('when appending a single event with an invalid named tag', () => {
12
+ let calls: ReturnType<typeof a_sequence>;
13
+ let error: unknown;
14
+ beforeEach(async () => {
15
+ calls = a_sequence();
16
+ error = await calls.sequence.append('source', new NamedTagRecorded(), { namedTags: [blank] }).catch(caught => caught);
17
+ });
18
+ it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
19
+ it('should not call the kernel', () =>
20
+ (calls.append.mock.calls.length + calls.appendWithNamedTags.mock.calls.length).should.equal(0));
21
+ });
22
+
23
+ describe('when appending a batch where one entry has an invalid named tag', () => {
24
+ let calls: ReturnType<typeof a_sequence>;
25
+ let error: unknown;
26
+ beforeEach(async () => {
27
+ calls = a_sequence();
28
+ error = await calls.sequence.appendMany([
29
+ { eventSourceId: 'one', event: new NamedTagRecorded() },
30
+ { eventSourceId: 'two', event: new NamedTagRecorded(), namedTags: [null as unknown as NamedTag] }
31
+ ]).catch(caught => caught);
32
+ });
33
+ it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
34
+ it('should not append any of the events', () =>
35
+ (calls.appendManyForEventSources.mock.calls.length + calls.appendManyForEventSourcesWithNamedTags.mock.calls.length).should.equal(0));
36
+ });
@@ -0,0 +1,74 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import { ConcurrencyDimensions } from '../../../eventSources/index.js';
6
+ import { NamedTag } from '../../../events/index.js';
7
+ import { NamedTagsWithRegisteredEventSourceNotSupported, type AppendOptions, type EventForEventSourceId } from '../../index.js';
8
+ import { Account, a_sequence, NamedTagRecorded, registered_sources } from './given.fixture.js';
9
+
10
+ chai.should();
11
+ const namedTags = [new NamedTag('batch', 'b-1')];
12
+ const entry = (options: Partial<EventForEventSourceId> = {}): EventForEventSourceId =>
13
+ ({ eventSourceId: 'source', event: new NamedTagRecorded(), ...options });
14
+
15
+ const batches: Array<{ description: string; events: EventForEventSourceId[]; options?: AppendOptions }> = [
16
+ { description: 'tags and a registered source on the same entry', events: [entry({ eventSource: Account, namedTags })] },
17
+ { description: 'a tagged entry before an untagged registered entry', events: [entry({ namedTags }), entry({ eventSource: 'Account' })] },
18
+ { description: 'an untagged registered entry before a tagged entry', events: [entry({ eventSource: 'Account' }), entry({ namedTags })] },
19
+ { description: 'shared tags and a registered entry', events: [entry({ eventSource: 'Account' })], options: { namedTags } },
20
+ { description: 'per-entry tags and a shared registered source', events: [entry({ namedTags })], options: { eventSource: Account } }
21
+ ];
22
+
23
+ for (const batch of batches) {
24
+ describe(`when appending a batch with ${batch.description}`, () => {
25
+ let calls: ReturnType<typeof a_sequence>;
26
+ let error: unknown;
27
+ beforeEach(async () => {
28
+ calls = a_sequence(registered_sources(ConcurrencyDimensions.eventSourceId));
29
+ error = await calls.sequence.appendMany(batch.events, batch.options).catch(caught => caught);
30
+ });
31
+ it('should reject with the typed unsupported combination error', () =>
32
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
33
+ it('should explain the kernel limitation', () => (error as Error).message.should.contain('drops registered event source routing'));
34
+ it('should link to the kernel issue', () => (error as Error).message.should.contain('https://github.com/Cratis/Chronicle/issues/4603'));
35
+ it('should not send a plain batch', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(0));
36
+ it('should not send a named-tag batch', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
37
+ it('should not read a tail to derive a guard', () => calls.tailSequenceNumber.mock.calls.should.have.lengthOf(0));
38
+ it('should not fall back to single appends', () =>
39
+ [...calls.append.mock.calls, ...calls.appendWithNamedTags.mock.calls].should.have.lengthOf(0));
40
+ });
41
+ }
42
+
43
+ describe('when appending a same-source batch with shared named tags and registered routing', () => {
44
+ let error: unknown;
45
+ beforeEach(async () => {
46
+ const calls = a_sequence(registered_sources());
47
+ error = await calls.sequence.appendMany('source', [new NamedTagRecorded()], { eventSource: Account, namedTags }).catch(caught => caught);
48
+ });
49
+ it('should reject with the typed unsupported combination error', () =>
50
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
51
+ });
52
+
53
+ describe('when appending a registered-source batch with empty named tags', () => {
54
+ let calls: ReturnType<typeof a_sequence>;
55
+ beforeEach(async () => {
56
+ calls = a_sequence(registered_sources());
57
+ await calls.sequence.appendMany([entry({ eventSource: Account, namedTags: [] })], { namedTags: [] });
58
+ });
59
+ it('should retain registered routing on the plain batch call', () =>
60
+ calls.appendManyForEventSources.mock.calls[0][0].Events[0].EventSource.should.equal('Account'));
61
+ it('should not send a named-tag batch', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
62
+ });
63
+
64
+ describe('when appending a single event with named tags and a registered source', () => {
65
+ let calls: ReturnType<typeof a_sequence>;
66
+ beforeEach(async () => {
67
+ calls = a_sequence(registered_sources());
68
+ await calls.sequence.append('source', new NamedTagRecorded(), { eventSource: Account, namedTags });
69
+ });
70
+ it('should retain registered routing on the dedicated single append call', () =>
71
+ calls.appendWithNamedTags.mock.calls[0][0].EventSource.should.equal('Account'));
72
+ it('should retain the named tags on the single append', () =>
73
+ calls.appendWithNamedTags.mock.calls[0][0].NamedTags.should.deep.equal([{ Name: 'batch', Value: 'b-1' }]));
74
+ });