@cratis/chronicle 6.49.0 → 6.51.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 (223) 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/constraints/ConstraintBuilder.d.ts +7 -0
  49. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  50. package/dist/events/constraints/ConstraintBuilder.js +13 -0
  51. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  52. package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
  53. package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
  54. package/dist/events/constraints/ConstraintEventSequences.js +39 -0
  55. package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
  56. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  57. package/dist/events/constraints/Constraints.js +24 -3
  58. package/dist/events/constraints/Constraints.js.map +1 -1
  59. package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
  60. package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
  61. package/dist/events/constraints/UniqueOptions.d.ts +15 -0
  62. package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
  63. package/dist/events/constraints/UniqueOptions.js +4 -0
  64. package/dist/events/constraints/UniqueOptions.js.map +1 -0
  65. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
  66. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
  67. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
  68. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
  69. package/dist/events/constraints/index.d.ts +2 -0
  70. package/dist/events/constraints/index.d.ts.map +1 -1
  71. package/dist/events/constraints/index.js.map +1 -1
  72. package/dist/events/constraints/unique.d.ts +10 -0
  73. package/dist/events/constraints/unique.d.ts.map +1 -1
  74. package/dist/events/constraints/unique.js +11 -10
  75. package/dist/events/constraints/unique.js.map +1 -1
  76. package/dist/events/index.d.ts +3 -0
  77. package/dist/events/index.d.ts.map +1 -1
  78. package/dist/events/index.js +3 -0
  79. package/dist/events/index.js.map +1 -1
  80. package/dist/events/mergeNamedTags.d.ts +13 -0
  81. package/dist/events/mergeNamedTags.d.ts.map +1 -0
  82. package/dist/events/mergeNamedTags.js +39 -0
  83. package/dist/events/mergeNamedTags.js.map +1 -0
  84. package/dist/events/toClientEventContext.d.ts.map +1 -1
  85. package/dist/events/toClientEventContext.js +3 -1
  86. package/dist/events/toClientEventContext.js.map +1 -1
  87. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
  88. package/dist/projections/eventContextPropertyExpression.js +2 -1
  89. package/dist/projections/eventContextPropertyExpression.js.map +1 -1
  90. package/dist/testing/EventScenario.d.ts.map +1 -1
  91. package/dist/testing/EventScenario.js +8 -3
  92. package/dist/testing/EventScenario.js.map +1 -1
  93. package/dist/testing/InProcessConstraints.d.ts +4 -1
  94. package/dist/testing/InProcessConstraints.d.ts.map +1 -1
  95. package/dist/testing/InProcessConstraints.js +5 -1
  96. package/dist/testing/InProcessConstraints.js.map +1 -1
  97. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  98. package/dist/testing/InProcessEventSequence.js +19 -8
  99. package/dist/testing/InProcessEventSequence.js.map +1 -1
  100. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  101. package/dist/testing/ReactorScenario.js +6 -0
  102. package/dist/testing/ReactorScenario.js.map +1 -1
  103. package/dist/testing/singleAppendRoute.d.ts +5 -0
  104. package/dist/testing/singleAppendRoute.d.ts.map +1 -1
  105. package/dist/testing/singleAppendRoute.js +12 -1
  106. package/dist/testing/singleAppendRoute.js.map +1 -1
  107. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
  108. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
  109. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
  110. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
  111. package/dist/transactions/IUnitOfWork.d.ts +5 -3
  112. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  113. package/dist/transactions/TransactionalAppendOptions.d.ts +35 -0
  114. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
  115. package/dist/transactions/TransactionalAppendOptions.js +4 -0
  116. package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
  117. package/dist/transactions/UnitOfWork.d.ts +3 -2
  118. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  119. package/dist/transactions/UnitOfWork.js +32 -11
  120. package/dist/transactions/UnitOfWork.js.map +1 -1
  121. package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
  122. package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
  123. package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
  124. package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
  125. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
  126. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
  127. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
  128. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
  129. package/dist/transactions/index.d.ts +2 -0
  130. package/dist/transactions/index.d.ts.map +1 -1
  131. package/dist/transactions/index.js +1 -0
  132. package/dist/transactions/index.js.map +1 -1
  133. package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
  134. package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
  135. package/dist/transactions/toEventForEventSourceId.js +29 -0
  136. package/dist/transactions/toEventForEventSourceId.js.map +1 -0
  137. package/dist/tsconfig.tsbuildinfo +1 -1
  138. package/eventSequences/AppendOptions.ts +11 -0
  139. package/eventSequences/EventForEventSourceId.ts +7 -0
  140. package/eventSequences/EventSequence.ts +30 -8
  141. package/eventSequences/ITransactionalEventSequence.ts +5 -5
  142. package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
  143. package/eventSequences/TransactionalEventSequence.ts +8 -5
  144. package/eventSequences/createAppendNotification.ts +4 -2
  145. package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
  146. package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
  147. package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
  148. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
  149. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
  150. package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
  151. package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
  152. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
  153. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
  154. package/eventSequences/index.ts +1 -0
  155. package/eventSequences/prepareBatchAppend.ts +4 -1
  156. package/eventSequences/prepareSingleAppend.ts +3 -1
  157. package/events/EventContext.ts +8 -0
  158. package/events/InvalidNamedTag.ts +13 -0
  159. package/events/NamedTag.ts +44 -0
  160. package/events/constraints/ConstraintBuilder.ts +16 -0
  161. package/events/constraints/ConstraintEventSequences.ts +41 -0
  162. package/events/constraints/Constraints.ts +22 -3
  163. package/events/constraints/IConstraintBuilder.ts +20 -0
  164. package/events/constraints/UniqueOptions.ts +20 -0
  165. package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
  166. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
  167. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
  168. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
  169. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
  170. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
  171. package/events/constraints/index.ts +2 -0
  172. package/events/constraints/unique.ts +22 -2
  173. package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
  174. package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
  175. package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
  176. package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
  177. package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
  178. package/events/index.ts +3 -0
  179. package/events/mergeNamedTags.ts +37 -0
  180. package/events/toClientEventContext.ts +3 -1
  181. package/package.json +6 -6
  182. package/projections/eventContextPropertyExpression.ts +3 -2
  183. package/testing/EventScenario.fixtures.spec.ts +1 -1
  184. package/testing/EventScenario.ts +10 -4
  185. package/testing/InProcessConstraints.ts +5 -1
  186. package/testing/InProcessEventSequence.ts +19 -8
  187. package/testing/ReactorScenario.ts +6 -0
  188. package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
  189. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
  190. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
  191. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
  192. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
  193. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
  194. package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
  195. package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
  196. package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
  197. package/testing/singleAppendRoute.ts +13 -1
  198. package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
  199. package/transactions/IUnitOfWork.ts +5 -3
  200. package/transactions/TransactionalAppendOptions.ts +45 -0
  201. package/transactions/UnitOfWork.ts +37 -12
  202. package/transactions/concurrencyScopesAreEqual.ts +23 -0
  203. package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
  204. package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
  205. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
  206. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
  207. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
  208. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
  209. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
  210. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
  211. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
  212. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
  213. package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
  214. package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
  215. package/transactions/index.ts +2 -0
  216. package/transactions/toEventForEventSourceId.ts +32 -0
  217. package/vitest.integration.config.ts +2 -0
  218. package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
  219. package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
  220. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
  221. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
  222. package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
  223. package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
@@ -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 append metadata (routing, subject, occurrence time, tags, named tags, concurrency scope) kept with the event until commit.
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 append metadata (routing, subject, occurrence time, tags, named tags, concurrency scope) 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
+ });
@@ -0,0 +1,41 @@
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, vi } from 'vitest';
5
+ import { InvalidNamedTag, NamedTag } from '../../events/index.js';
6
+ import type { IUnitOfWorkManager } from '../../transactions/index.js';
7
+ import { EventSequenceId } from '../EventSequenceId.js';
8
+ import type { IEventSequence } from '../IEventSequence.js';
9
+ import { TransactionalEventSequence } from '../TransactionalEventSequence.js';
10
+
11
+ chai.should();
12
+
13
+ function a_transactional_sequence() {
14
+ const addEvent = vi.fn();
15
+ const manager = { current: { addEvent } } as unknown as IUnitOfWorkManager;
16
+ return { sequence: new TransactionalEventSequence({ id: EventSequenceId.eventLog } as IEventSequence, manager), addEvent };
17
+ }
18
+
19
+ describe('when appending many events transactionally with shared named tags', () => {
20
+ let addEvent: ReturnType<typeof vi.fn>;
21
+ beforeEach(async () => {
22
+ const given = a_transactional_sequence();
23
+ addEvent = given.addEvent;
24
+ await given.sequence.appendMany('source', [{ one: 1 }, { two: 2 }], { eventSource: 'Account', namedTags: [new NamedTag('import', 'i-1')] });
25
+ });
26
+ it('should add every event with the shared named tags and routing', () =>
27
+ addEvent.mock.calls.map(call => [call[3].eventSource, call[3].namedTags.map((tag: NamedTag) => tag.value)])
28
+ .should.deep.equal([['Account', ['i-1']], ['Account', ['i-1']]]));
29
+ });
30
+
31
+ describe('when appending many events transactionally with an invalid named tag', () => {
32
+ let addEvent: ReturnType<typeof vi.fn>;
33
+ let error: unknown;
34
+ beforeEach(async () => {
35
+ const given = a_transactional_sequence();
36
+ addEvent = given.addEvent;
37
+ error = await given.sequence.appendMany('source', [{ one: 1 }], { namedTags: [{ name: '', value: '' } as NamedTag] }).catch(caught => caught);
38
+ });
39
+ it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
40
+ it('should not add any event', () => addEvent.mock.calls.should.have.lengthOf(0));
41
+ });
@@ -0,0 +1,29 @@
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, vi, type Mock } from 'vitest';
5
+ import { EventSequenceId } from '../EventSequenceId.js';
6
+ import type { IEventSequence } from '../IEventSequence.js';
7
+ import type { IUnitOfWorkManager } from '../../transactions/IUnitOfWorkManager.js';
8
+ import type { TransactionalAppendOptions } from '../../transactions/TransactionalAppendOptions.js';
9
+ import { TransactionalEventSequence } from '../TransactionalEventSequence.js';
10
+
11
+ chai.should();
12
+
13
+ describe('when appending many with options to a transactional event sequence', () => {
14
+ const outbox = new EventSequenceId('outbox');
15
+ const options: TransactionalAppendOptions = { subject: 'subject-1', occurred: new Date(0), tags: ['t'], concurrencyScope: { sequenceNumber: 2n } };
16
+ let addEvent: Mock;
17
+
18
+ beforeEach(async () => {
19
+ addEvent = vi.fn();
20
+ const manager = { current: { addEvent } } as unknown as IUnitOfWorkManager;
21
+ const sequence = new TransactionalEventSequence({ id: outbox } as unknown as IEventSequence, manager);
22
+ await sequence.appendMany('a-1', [{ order: 0 }, { order: 1 }], options);
23
+ });
24
+
25
+ it('should enroll every event with the options in order', () => addEvent.mock.calls.should.deep.equal([
26
+ [outbox, 'a-1', { order: 0 }, options],
27
+ [outbox, 'a-1', { order: 1 }, options]
28
+ ]));
29
+ });
@@ -10,6 +10,7 @@ export type { AppendResult } from './AppendResult.js';
10
10
  export type { AppendedEventWithResult } from './AppendedEventWithResult.js';
11
11
  export type { AppendOptions } from './AppendOptions.js';
12
12
  export { CompleteStreamError } from './CompleteStreamError.js';
13
+ export { NamedTagsWithRegisteredEventSourceNotSupported } from './NamedTagsWithRegisteredEventSourceNotSupported.js';
13
14
  export type { CompleteStreamResult } from './CompleteStreamResult.js';
14
15
  export type { ConcurrencyScope } from './ConcurrencyScope.js';
15
16
  export type { EventForEventSourceId } from './EventForEventSourceId.js';
@@ -7,6 +7,7 @@ import { correlationIdManager } from '../correlation/index.js';
7
7
  import { getEventTypeFor } from '../events/eventTypeDecorator.js';
8
8
  import { getTagsFor } from '../events/tagDecorator.js';
9
9
  import { mergeTags } from '../events/mergeTags.js';
10
+ import { mergeNamedTags } from '../events/mergeNamedTags.js';
10
11
  import { identityProvider } from '../identity/index.js';
11
12
  import { resolveEventSubject } from '../compliance/resolveEventSubject.js';
12
13
  import type { AppendOptions } from './AppendOptions.js';
@@ -45,6 +46,8 @@ export function prepareBatchAppend(
45
46
  for (const { eventSourceId } of eventsForEventSourceIds) {
46
47
  if (!concurrencyScopes.has(eventSourceId)) concurrencyScopes.set(eventSourceId, appendOptions?.concurrencyScope);
47
48
  }
49
+ // Validate every event's named tags before anything is sent; the order of entries is preserved.
50
+ const namedTags = eventsForEventSourceIds.map(({ namedTags: instanceNamedTags }) => mergeNamedTags(instanceNamedTags, appendOptions?.namedTags));
48
51
  const eventsToAppend = eventsForEventSourceIds.map(({ eventSourceId, event, eventStreamType, eventStreamId, eventSourceType, subject, occurred, tags: instanceTags }) => {
49
52
  const eventType = getEventTypeFor(event.constructor as Function);
50
53
  const tags = mergeTags(getTagsFor(event.constructor as Function), instanceTags, appendOptions?.tags);
@@ -60,5 +63,5 @@ export function prepareBatchAppend(
60
63
  Subject: subject ?? appendOptions?.subject ?? resolveEventSubject(event) ?? eventSourceId
61
64
  };
62
65
  });
63
- return { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend };
66
+ return { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend, namedTags };
64
67
  }
@@ -5,6 +5,7 @@ import { Guid, JsonSerializer } from '@cratis/fundamentals';
5
5
  import { getEventTypeFor } from '../events/eventTypeDecorator.js';
6
6
  import { getTagsFor } from '../events/tagDecorator.js';
7
7
  import { mergeTags } from '../events/mergeTags.js';
8
+ import { mergeNamedTags } from '../events/mergeNamedTags.js';
8
9
  import { identityProvider } from '../identity/index.js';
9
10
  import { causationManager, CausationType } from '../auditing/index.js';
10
11
  import { correlationIdManager } from '../correlation/index.js';
@@ -20,8 +21,9 @@ export function prepareSingleAppend(event: object, options?: AppendOptions) {
20
21
  const subject = options?.subject ?? resolveEventSubject(event);
21
22
  const content = JsonSerializer.serialize(event);
22
23
  const tags = mergeTags(getTagsFor(event.constructor as Function), options?.tags);
24
+ const namedTags = mergeNamedTags(options?.namedTags);
23
25
  const causationChain = causationManager.run(CausationType.appendEvent, { eventType: eventType.id.value },
24
26
  () => causationManager.getCurrentChain());
25
27
  const identity = identityProvider.getCurrent();
26
- return { eventType, correlationId, content, tags, causationChain, identity, subject };
28
+ return { eventType, correlationId, content, tags, namedTags, causationChain, identity, subject };
27
29
  }
@@ -4,6 +4,7 @@
4
4
  import { EventType } from './EventType.js';
5
5
  import { CausationEntry } from './CausationEntry.js';
6
6
  import { Tag } from './Tag.js';
7
+ import type { NamedTag } from './NamedTag.js';
7
8
  import type { Identity } from '../identity/Identity.js';
8
9
 
9
10
  /**
@@ -63,4 +64,11 @@ export interface EventContext {
63
64
 
64
65
  /** The tags the event carries. */
65
66
  readonly tags: ReadonlyArray<Tag>;
67
+
68
+ /**
69
+ * The structured named tags the event carries, in the order the kernel stored them.
70
+ * Always present on events read from or delivered by the kernel (empty when the event has none);
71
+ * optional so that contexts constructed by hand remain valid.
72
+ */
73
+ readonly namedTags?: ReadonlyArray<NamedTag>;
66
74
  }
@@ -0,0 +1,13 @@
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
+ /**
5
+ * The error that is thrown when a named tag has a blank name or a non-string value, or a named tag
6
+ * collection contains an entry that is not a named tag. Mirrors the .NET client's `InvalidNamedTag`.
7
+ */
8
+ export class InvalidNamedTag extends Error {
9
+ constructor() {
10
+ super('Named tags require a nonblank name and a non-null value; named tag collections cannot contain null elements.');
11
+ this.name = 'InvalidNamedTag';
12
+ }
13
+ }