@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
@@ -0,0 +1,40 @@
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 { chai, describe, it, type Assertion } from 'vitest';
5
+ import type { Constructor } from '@cratis/fundamentals';
6
+ import { EventScenario } from '../../index.js';
7
+
8
+ chai.should();
9
+ function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
10
+
11
+ /**
12
+ * Proves a numeric unique constraint applicable to the event log is not rejected as non-isolated because another
13
+ * constraint is scoped only to a different event sequence.
14
+ * @param claimed - Event type constructor with a numeric key.
15
+ * @param other - Event type constructor used by the outbox-only constraint.
16
+ * @param constraints - The numeric event log constraint and the outbox-only constraint.
17
+ */
18
+ export function otherSequenceExtendedKeyBehaviors(
19
+ claimed: new () => object,
20
+ other: new () => object,
21
+ constraints: Constructor[]
22
+ ): void {
23
+ describe('with a numeric key and a constraint scoped to another event sequence', () => {
24
+ const create = () => new EventScenario({ artifacts: { eventTypes: [claimed, other], constraints } });
25
+
26
+ it('should construct the scenario', () => {
27
+ should(create).not.throw();
28
+ });
29
+
30
+ it('should accept the first event', async () => {
31
+ should((await create().append('A', new claimed())).isSuccess).equal(true);
32
+ });
33
+
34
+ it('should enforce the numeric constraint on the event log', async () => {
35
+ const scenario = create();
36
+ await scenario.append('A', new claimed());
37
+ should((await scenario.append('B', new claimed())).isSuccess).equal(false);
38
+ });
39
+ });
40
+ }
@@ -0,0 +1,55 @@
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 'reflect-metadata';
5
+ import { field, type Constructor } from '@cratis/fundamentals';
6
+ import { describe } from 'vitest';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { constraint } from '../../../events/constraints/constraint.js';
9
+ import { removeConstraint } from '../../../events/constraints/removeConstraint.js';
10
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
11
+ import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
12
+ import { eventSequenceScenarioBehaviors } from './event_sequence_scenario_behaviors.fixture.js';
13
+
14
+ @eventType('OracleDomainText')
15
+ class Text { @field(String) key: string; constructor(key: string) { this.key = key; } }
16
+ @eventType('OracleDomainRemoved')
17
+ class Removed { @field(String) label: string; constructor(label: string) { this.label = label; } }
18
+ @eventType('OracleCycleFirst')
19
+ class First { @field(String) label: string; constructor(label: string) { this.label = label; } }
20
+ @eventType('OracleCycleSibling')
21
+ class Sibling { @field(String) label: string; constructor(label: string) { this.label = label; } }
22
+ @eventType('OracleCycleRemoved')
23
+ @removeConstraint('OracleSequencedCycle')
24
+ class CycleRemoved { @field(String) label: string; constructor(label: string) { this.label = label; } }
25
+ @eventType('OracleCycleExpired')
26
+ @removeConstraint('OracleSequencedCycle')
27
+ class CycleExpired { @field(String) label: string; constructor(label: string) { this.label = label; } }
28
+
29
+ const outbox = new EventSequenceId('outbox');
30
+
31
+ function constraints(caseName: string): Constructor[] {
32
+ function select(builder: IConstraintBuilder): IConstraintBuilder {
33
+ if (caseName.endsWith('-event-log-and-outbox')) return builder.forEventSequences('outbox', EventSequenceId.eventLog);
34
+ return caseName.endsWith('-event-log') ? builder.forEventLog() : builder.forEventSequences(outbox);
35
+ }
36
+ @constraint('OracleSequencedKey')
37
+ class Key {
38
+ define(builder: IConstraintBuilder) {
39
+ select(builder).unique(key => key.on(Text, event => event.key).removedWith(Removed).withMessage('Taken: {PropertyValue}'));
40
+ }
41
+ }
42
+ @constraint('OracleSequencedFirst')
43
+ class FirstCycle { define(builder: IConstraintBuilder) { select(builder).uniqueFor(First, 'Cycle occupied', 'OracleSequencedCycle'); } }
44
+ @constraint('OracleSequencedSibling')
45
+ class SiblingCycle { define(builder: IConstraintBuilder) { select(builder).uniqueFor(Sibling, 'Cycle occupied', 'OracleSequencedCycle'); } }
46
+ @constraint('OracleSequencedOnceConstraint')
47
+ class Once { define(builder: IConstraintBuilder) { select(builder).uniqueFor(First, 'Already recorded', 'OracleSequencedOnce'); } }
48
+ const kind = caseName.split('-')[0];
49
+ return kind === 'property' ? [Key] : kind === 'once' ? [Once] : [FirstCycle, SiblingCycle];
50
+ }
51
+
52
+ describe('when appending event sequence constraints with legacy decorators', () => {
53
+ eventSequenceScenarioBehaviors({ string: Text, removed: Removed, first: First, sibling: Sibling,
54
+ cycleRemoved: CycleRemoved, cycleExpired: CycleExpired }, constraints);
55
+ });
@@ -0,0 +1,54 @@
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 { field, type Constructor } from '@cratis/fundamentals';
5
+ import { describe } from 'vitest';
6
+ import { eventType } from '../../../events/eventTypeDecorator.js';
7
+ import { constraint } from '../../../events/constraints/constraint.js';
8
+ import { removeConstraint } from '../../../events/constraints/removeConstraint.js';
9
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
10
+ import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
11
+ import { eventSequenceScenarioBehaviors } from './event_sequence_scenario_behaviors.fixture.js';
12
+
13
+ @eventType('OracleDomainText')
14
+ class Text { @field(String) key: string; constructor(key: string) { this.key = key; } }
15
+ @eventType('OracleDomainRemoved')
16
+ class Removed { @field(String) label: string; constructor(label: string) { this.label = label; } }
17
+ @eventType('OracleCycleFirst')
18
+ class First { @field(String) label: string; constructor(label: string) { this.label = label; } }
19
+ @eventType('OracleCycleSibling')
20
+ class Sibling { @field(String) label: string; constructor(label: string) { this.label = label; } }
21
+ @eventType('OracleCycleRemoved')
22
+ @removeConstraint('OracleSequencedCycle')
23
+ class CycleRemoved { @field(String) label: string; constructor(label: string) { this.label = label; } }
24
+ @eventType('OracleCycleExpired')
25
+ @removeConstraint('OracleSequencedCycle')
26
+ class CycleExpired { @field(String) label: string; constructor(label: string) { this.label = label; } }
27
+
28
+ const outbox = new EventSequenceId('outbox');
29
+
30
+ function constraints(caseName: string): Constructor[] {
31
+ function select(builder: IConstraintBuilder): IConstraintBuilder {
32
+ if (caseName.endsWith('-event-log-and-outbox')) return builder.forEventSequences('outbox', EventSequenceId.eventLog);
33
+ return caseName.endsWith('-event-log') ? builder.forEventLog() : builder.forEventSequences(outbox);
34
+ }
35
+ @constraint('OracleSequencedKey')
36
+ class Key {
37
+ define(builder: IConstraintBuilder) {
38
+ select(builder).unique(key => key.on(Text, event => event.key).removedWith(Removed).withMessage('Taken: {PropertyValue}'));
39
+ }
40
+ }
41
+ @constraint('OracleSequencedFirst')
42
+ class FirstCycle { define(builder: IConstraintBuilder) { select(builder).uniqueFor(First, 'Cycle occupied', 'OracleSequencedCycle'); } }
43
+ @constraint('OracleSequencedSibling')
44
+ class SiblingCycle { define(builder: IConstraintBuilder) { select(builder).uniqueFor(Sibling, 'Cycle occupied', 'OracleSequencedCycle'); } }
45
+ @constraint('OracleSequencedOnceConstraint')
46
+ class Once { define(builder: IConstraintBuilder) { select(builder).uniqueFor(First, 'Already recorded', 'OracleSequencedOnce'); } }
47
+ const kind = caseName.split('-')[0];
48
+ return kind === 'property' ? [Key] : kind === 'once' ? [Once] : [FirstCycle, SiblingCycle];
49
+ }
50
+
51
+ describe('when appending event sequence constraints with standard decorators', () => {
52
+ eventSequenceScenarioBehaviors({ string: Text, removed: Removed, first: First, sibling: Sibling,
53
+ cycleRemoved: CycleRemoved, cycleExpired: CycleExpired }, constraints);
54
+ });
@@ -0,0 +1,48 @@
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 { NamedTag } from '../../../events/NamedTag.js';
6
+ import { EventScenario, UnsupportedEventSequenceOperation } from '../../index.js';
7
+ import { artifacts, Claim, createEvents, rejection } from '../given/routed_scenario.fixture.js';
8
+
9
+ chai.should();
10
+ const namedTags = [new NamedTag('batch', 'b-1')];
11
+
12
+ describe('when appending a single event with named tags in an event scenario', () => {
13
+ let scenario: EventScenario;
14
+ let error: UnsupportedEventSequenceOperation;
15
+ beforeEach(async () => {
16
+ scenario = createEvents();
17
+ error = await rejection(() => scenario.append('source', new Claim('one'), { namedTags }));
18
+ });
19
+ it('should reject the named tags explicitly', () => error.message.should.contain('append.namedTags'));
20
+ it('should not append the event', () => scenario.appendedEvents.should.have.lengthOf(0));
21
+ });
22
+
23
+ describe('when appending a batch with shared named tags in an event scenario', () => {
24
+ let error: UnsupportedEventSequenceOperation;
25
+ beforeEach(async () => {
26
+ error = await rejection(() => createEvents().appendMany('source', [new Claim('one')], { namedTags }));
27
+ });
28
+ it('should reject the named tags explicitly', () => error.message.should.contain('appendMany.namedTags'));
29
+ });
30
+
31
+ describe('when appending a batch entry with named tags in an event scenario', () => {
32
+ let error: UnsupportedEventSequenceOperation;
33
+ beforeEach(async () => {
34
+ error = await rejection(() => createEvents().appendMany([{ eventSourceId: 'source', event: new Claim('one'), namedTags }]));
35
+ });
36
+ it('should reject the named tags explicitly', () => error.message.should.contain('appendMany.namedTags'));
37
+ });
38
+
39
+ describe('when appending with an empty named tag list in an event scenario', () => {
40
+ let scenario: EventScenario;
41
+ beforeEach(async () => {
42
+ scenario = new EventScenario({ artifacts });
43
+ await scenario.append('source', new Claim('one'), { namedTags: [] });
44
+ });
45
+ it('should append the event', () => scenario.appendedEvents.should.have.lengthOf(1));
46
+ it('should expose an empty named tag list on the stored context like a kernel delivery', () =>
47
+ scenario.appendedEvents[0].context.namedTags!.should.deep.equal([]));
48
+ });
@@ -0,0 +1,119 @@
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 { field } from '@cratis/fundamentals';
5
+ import { beforeEach, chai, describe, it } from 'vitest';
6
+ import { eventType } from '../../../events/eventTypeDecorator.js';
7
+ import { InvalidNamedTag } from '../../../events/InvalidNamedTag.js';
8
+ import { NamedTag } from '../../../events/NamedTag.js';
9
+ import { NamedTagsWithRegisteredEventSourceNotSupported } from '../../../eventSequences/index.js';
10
+ import type { EventContext } from '../../../events/EventContext.js';
11
+ import { reactor } from '../../../reactors/reactor.js';
12
+ import { ReactorScenario } from '../../ReactorScenario.js';
13
+
14
+ chai.should();
15
+ @eventType('scenario-named-tag-input')
16
+ class NamedTagInput { @field(String) name = 'input'; }
17
+ @eventType('scenario-named-tag-output')
18
+ class NamedTagOutput { @field(String) name = 'output'; }
19
+
20
+ const contexts: EventContext[] = [];
21
+ @reactor('scenario-named-tag-reactor')
22
+ class NamedTagReactor {
23
+ namedTagInput(_: NamedTagInput, context: EventContext) {
24
+ contexts.push(context);
25
+ return { eventSourceId: 'other', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', context.eventSourceId)] };
26
+ }
27
+ }
28
+ @reactor('scenario-invalid-named-tag-reactor')
29
+ class InvalidNamedTagReactor {
30
+ namedTagInput() {
31
+ return { eventSourceId: 'other', event: new NamedTagOutput(), namedTags: [{ name: ' ', value: 'x' }] };
32
+ }
33
+ }
34
+ @reactor('scenario-named-tags-registered-source-same-entry')
35
+ class NamedTagRegisteredSourceReactor {
36
+ namedTagInput() {
37
+ return { eventSourceId: 'other', event: new NamedTagOutput(), eventSource: 'Account', namedTags: [new NamedTag('origin', 'trigger')] };
38
+ }
39
+ }
40
+ @reactor('scenario-named-tags-registered-source-cross-entry')
41
+ class NamedTagCrossEntryReactor {
42
+ namedTagInput() {
43
+ return [
44
+ { eventSourceId: 'tagged', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', 'trigger')] },
45
+ { eventSourceId: 'registered', event: new NamedTagOutput(), eventSource: 'Account' }
46
+ ];
47
+ }
48
+ }
49
+ @reactor('scenario-named-tags-registered-source-cross-entry-reversed')
50
+ class NamedTagCrossEntryReversedReactor {
51
+ namedTagInput() {
52
+ return [
53
+ { eventSourceId: 'registered', event: new NamedTagOutput(), eventSource: 'Account' },
54
+ { eventSourceId: 'tagged', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', 'trigger')] }
55
+ ];
56
+ }
57
+ }
58
+ @reactor('scenario-empty-named-tags-registered-source')
59
+ class EmptyNamedTagsRegisteredSourceReactor {
60
+ namedTagInput() {
61
+ return { eventSourceId: 'other', event: new NamedTagOutput(), eventSource: 'Account', namedTags: [] };
62
+ }
63
+ }
64
+ const artifacts = { eventTypes: [NamedTagInput, NamedTagOutput] };
65
+
66
+ describe('when recording a side effect with named tags', () => {
67
+ let scenario: ReactorScenario;
68
+ beforeEach(async () => {
69
+ contexts.length = 0;
70
+ scenario = new ReactorScenario(NamedTagReactor, { artifacts, constraints: 'disabled' });
71
+ await scenario.when.forEventSource('trigger').events(new NamedTagInput());
72
+ });
73
+ it('should record the named tags on the side-effect target', () =>
74
+ scenario.sideEffects[0].target.namedTags!.map(tag => `${tag.name}=${tag.value}`).should.deep.equal(['origin=trigger']));
75
+ it('should deliver an empty named tag list like a kernel delivery of an untagged event', () =>
76
+ contexts[0].namedTags!.should.deep.equal([]));
77
+ });
78
+
79
+ describe('when recording a side effect with an invalid named tag', () => {
80
+ let error: unknown;
81
+ beforeEach(async () => {
82
+ const scenario = new ReactorScenario(InvalidNamedTagReactor, { artifacts, constraints: 'disabled' });
83
+ error = await scenario.when.forEventSource('trigger').events(new NamedTagInput()).catch(caught => caught);
84
+ });
85
+ it('should fail the delivery with an invalid named tag error, as production append does', () =>
86
+ (error as Error).should.be.instanceOf(InvalidNamedTag));
87
+ });
88
+
89
+ for (const [description, reactorType] of [
90
+ ['the same entry', NamedTagRegisteredSourceReactor],
91
+ ['a tagged entry before a registered entry', NamedTagCrossEntryReactor],
92
+ ['a registered entry before a tagged entry', NamedTagCrossEntryReversedReactor]
93
+ ] as const) {
94
+ describe(`when recording named tags and a registered source in ${description}`, () => {
95
+ let scenario: ReactorScenario;
96
+ let error: unknown;
97
+ beforeEach(async () => {
98
+ scenario = new ReactorScenario(reactorType, { artifacts, constraints: 'disabled' });
99
+ error = await scenario.when.forEventSource('trigger').events(new NamedTagInput()).catch(caught => caught);
100
+ });
101
+ it('should reject with the production unsupported combination error', () =>
102
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
103
+ it('should record an unsuccessful delivery', () => scenario.results[0].completed.should.be.false);
104
+ it('should retain the rejection on the delivery result', () => scenario.results[0].error!.should.equal(error));
105
+ it('should not record any returned side effect', () => scenario.sideEffects.should.have.lengthOf(0));
106
+ it('should not report any produced event', () => scenario.produced.should.have.lengthOf(0));
107
+ });
108
+ }
109
+
110
+ describe('when recording a registered source with empty named tags', () => {
111
+ let scenario: ReactorScenario;
112
+ beforeEach(async () => {
113
+ scenario = new ReactorScenario(EmptyNamedTagsRegisteredSourceReactor, { artifacts, constraints: 'disabled' });
114
+ await scenario.when.forEventSource('trigger').events(new NamedTagInput());
115
+ });
116
+ it('should complete the delivery', () => scenario.results[0].completed.should.be.true);
117
+ it('should retain the registered source on the recorded effect', () => scenario.sideEffects[0].target.eventSource!.should.equal('Account'));
118
+ it('should retain the empty named tag array', () => scenario.sideEffects[0].target.namedTags!.should.deep.equal([]));
119
+ });
@@ -0,0 +1,35 @@
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 { NamedTag } from '../../../events/index.js';
6
+ import { reducer } from '../../../reducers/reducer.js';
7
+ import { ReadModelScenario, UnsupportedEventSequenceOperation } from '../../index.js';
8
+ import { Claim, rejection } from '../../for_EventScenario/given/routed_scenario.fixture.js';
9
+
10
+ chai.should();
11
+ class Count { claimed = 0; }
12
+ class Counter { claim(_event: Claim, current: Count | undefined): Count { return { claimed: (current?.claimed ?? 0) + 1 }; } }
13
+ reducer('NamedTagSetupCounter', undefined, Count)(Counter);
14
+ const create = () => new ReadModelScenario(Count, { eventTypes: [Claim], reducers: [Counter], projections: [] });
15
+
16
+ describe('when read-model setup receives non-empty named tags', () => {
17
+ let scenario: ReadModelScenario<Count>;
18
+ let error: Error;
19
+ beforeEach(async () => {
20
+ scenario = create();
21
+ error = await rejection(() => scenario.given.forEventSource('A', { namedTags: [new NamedTag('batch', 'b-1')] }).events(new Claim('Rejected')));
22
+ });
23
+ it('should reject with the typed unsupported operation error', () => error.should.be.instanceOf(UnsupportedEventSequenceOperation));
24
+ it('should identify the append named tags operation', () => error.message.should.contain('UnsupportedEventSequenceOperation: append.namedTags (Claim)'));
25
+ it('should not collect the rejected event', async () => ((await scenario.instance) === null).should.be.true);
26
+ });
27
+
28
+ describe('when read-model setup receives an empty named tag array', () => {
29
+ let scenario: ReadModelScenario<Count>;
30
+ beforeEach(() => {
31
+ scenario = create();
32
+ scenario.given.forEventSource('A', { namedTags: [] }).events(new Claim('Accepted'));
33
+ });
34
+ it('should accept and fold the event', async () => (await scenario.instance)!.claimed.should.equal(1));
35
+ });
@@ -4,6 +4,14 @@
4
4
  import type { AppendOptions } from '../eventSequences/AppendOptions.js';
5
5
  import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
6
6
 
7
+ /**
8
+ * Whether a named-tag option asks for anything. An explicitly empty array carries no tags, exactly like an
9
+ * omitted option; anything else is reported, including malformed values the production client would reject.
10
+ */
11
+ export function hasNamedTags(namedTags: unknown): boolean {
12
+ return namedTags !== undefined && !(Array.isArray(namedTags) && namedTags.length === 0);
13
+ }
14
+
7
15
  /** The fixture-proven single-append route boundary, shared with read-model setup. */
8
16
  export function singleAppendRoute(options: AppendOptions | undefined, artifact: string,
9
17
  unsupported = (operation: string, artifact: string, reason: string) => new UnsupportedEventSequenceOperation(operation, artifact, reason),
@@ -12,8 +20,12 @@ export function singleAppendRoute(options: AppendOptions | undefined, artifact:
12
20
  if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
13
21
  throw unsupported('append.options', artifact, 'Append options must be an object.');
14
22
  }
23
+ if (options && hasNamedTags(options.namedTags)) {
24
+ throw unsupported('append.namedTags', artifact, 'Named tags are not fixture-backed.');
25
+ }
15
26
  const allowed = ['sourceType', 'streamType', 'streamId', ...(allowSubject ? ['subject'] : [])];
16
- if (options && (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
27
+ // An explicitly empty namedTags array is accepted (checked above) but not advertised as a supported option.
28
+ if (options && (Reflect.ownKeys(options).some(key => key !== 'namedTags' && !allowed.includes(String(key))) ||
17
29
  options.correlationId !== undefined || (!allowSubject && options.subject !== undefined) || options.occurred !== undefined ||
18
30
  options.eventSourceId !== undefined || options.concurrencyScope !== undefined || options.tags !== undefined ||
19
31
  options.concurrencyScopes !== undefined)) {
@@ -0,0 +1,19 @@
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
+ * Error thrown when a unit of work is given a concurrency scope for an event source identifier that already has a different
6
+ * scope in the same event sequence. Chronicle validates one scope per event source identifier in an append, so the unit of work
7
+ * cannot commit both.
8
+ */
9
+ export class ConflictingConcurrencyScopesInUnitOfWork extends Error {
10
+ /**
11
+ * Initializes a new instance of the {@link ConflictingConcurrencyScopesInUnitOfWork} class.
12
+ * @param eventSequenceId - The identifier of the event sequence the events are added to.
13
+ */
14
+ constructor(readonly eventSequenceId: string) {
15
+ super(
16
+ `An event source in event sequence '${eventSequenceId}' already has a different concurrency scope in this unit of work. ` +
17
+ 'Use the same scope for every event of the event source, or commit the events in separate units of work.');
18
+ }
19
+ }
@@ -6,7 +6,7 @@ import { AppendError } from '../eventSequences/AppendError.js';
6
6
  import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation.js';
7
7
  import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
8
8
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
9
- import { TransactionalEventRouting } from './TransactionalEventRouting.js';
9
+ import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
10
10
  import { AppendResult } from '../eventSequences/AppendResult.js';
11
11
 
12
12
  /**
@@ -27,9 +27,11 @@ export interface IUnitOfWork {
27
27
  * @param eventSequenceId - The identifier of the event sequence to append to.
28
28
  * @param eventSourceId - The event source identifier.
29
29
  * @param event - The event payload.
30
- * @param routing - Optional registered event source / stream routing for this event.
30
+ * @param options - Optional append metadata for this event: registered event source / stream routing, stream and source
31
+ * types, stream identifier, subject, occurrence time, tags, named tags and concurrency scope. Kept with the event until commit.
32
+ * @throws {@link InvalidNamedTag} when a named tag is invalid; the event is not added.
31
33
  */
32
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, routing?: TransactionalEventRouting): void;
34
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void;
33
35
 
34
36
  /**
35
37
  * Gets all events currently buffered in this unit of work.
@@ -0,0 +1,45 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { NamedTag } from '../events/NamedTag.js';
5
+ import type { Tag } from '../events/Tag.js';
6
+ import type { ConcurrencyScope } from '../eventSequences/ConcurrencyScope.js';
7
+ import type { TransactionalEventRouting } from './TransactionalEventRouting.js';
8
+
9
+ /**
10
+ * Optional append metadata for an event added to a unit of work.
11
+ * Every value is kept with the event and carried to the append when the unit of work commits,
12
+ * where it is validated like any other append.
13
+ */
14
+ export interface TransactionalAppendOptions extends TransactionalEventRouting {
15
+ /** Optional stream type. When omitted or empty, the kernel selects the route. */
16
+ readonly eventStreamType?: string;
17
+
18
+ /** Optional stream identifier. When omitted or empty, the kernel selects the route. */
19
+ readonly eventStreamId?: string;
20
+
21
+ /** Optional source type. When omitted or empty, the kernel selects the route. */
22
+ readonly eventSourceType?: string;
23
+
24
+ /** Optional occurrence time. When omitted, the kernel supplies the timestamp. */
25
+ readonly occurred?: Date;
26
+
27
+ /** Optional compliance subject. Overrides the event's @subject() value; otherwise defaults to the event source identifier. */
28
+ readonly subject?: string;
29
+
30
+ /** Optional tags, combined with any static tags declared on the event type via `@tag()`/`@tags()`. */
31
+ readonly tags?: ReadonlyArray<string | Tag>;
32
+
33
+ /**
34
+ * Optional concurrency scope for the event source identifier, validated when the unit of work commits.
35
+ * Chronicle accepts one scope per event source identifier in an append, so every event a unit of work holds
36
+ * for the same event source identifier in the same event sequence must use an equal scope or none.
37
+ */
38
+ readonly concurrencyScope?: ConcurrencyScope;
39
+
40
+ /**
41
+ * Structured named tags for the event. Validated when the event is added, so an invalid tag fails
42
+ * before anything is committed; the first occurrence of each exact name and value pair is kept.
43
+ */
44
+ readonly namedTags?: ReadonlyArray<NamedTag>;
45
+ }
@@ -9,8 +9,13 @@ import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
9
9
  import { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
10
10
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
11
11
  import { planDerivedGuards, resolveBatchRouting } from '../eventSequences/resolveEventSourceRouting.js';
12
+ import { ensureNamedTagBatchIsSupported } from '../eventSequences/ensureNamedTagBatchIsSupported.js';
12
13
  import { IEventStore } from '../IEventStore.js';
13
- import { TransactionalEventRouting } from './TransactionalEventRouting.js';
14
+ import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
15
+ import type { ConcurrencyScope } from '../eventSequences/ConcurrencyScope.js';
16
+ import { ConflictingConcurrencyScopesInUnitOfWork } from './ConflictingConcurrencyScopesInUnitOfWork.js';
17
+ import { concurrencyScopesAreEqual } from './concurrencyScopesAreEqual.js';
18
+ import { toEventForEventSourceId } from './toEventForEventSourceId.js';
14
19
  import { IUnitOfWork } from './IUnitOfWork.js';
15
20
 
16
21
  interface EventForEventSourceIdWithSequenceNumber {
@@ -24,6 +29,7 @@ interface EventForEventSourceIdWithSequenceNumber {
24
29
  */
25
30
  export class UnitOfWork implements IUnitOfWork {
26
31
  private _events: EventForEventSourceIdWithSequenceNumber[] = [];
32
+ private _concurrencyScopes = new Map<string, Map<string, ConcurrencyScope>>();
27
33
  private _appendResults: AppendResult[] = [];
28
34
  private _isCommitted = false;
29
35
  private _isRolledBack = false;
@@ -48,17 +54,23 @@ export class UnitOfWork implements IUnitOfWork {
48
54
  }
49
55
 
50
56
  /** @inheritdoc */
51
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, routing?: TransactionalEventRouting): void {
57
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void {
52
58
  this.throwIfCompleted();
59
+ const eventForEventSourceId = toEventForEventSourceId(eventSourceId, event, options);
60
+ const scope = options?.concurrencyScope;
61
+ const scopesForSequence = this._concurrencyScopes.get(eventSequenceId.value) ?? new Map<string, ConcurrencyScope>();
62
+ if (scope !== undefined) {
63
+ const enrolled = scopesForSequence.get(eventSourceId);
64
+ if (enrolled !== undefined && !concurrencyScopesAreEqual(enrolled, scope)) {
65
+ throw new ConflictingConcurrencyScopesInUnitOfWork(eventSequenceId.value);
66
+ }
67
+ scopesForSequence.set(eventSourceId, { ...scope, ...(scope.eventTypes ? { eventTypes: [...scope.eventTypes] } : {}) });
68
+ this._concurrencyScopes.set(eventSequenceId.value, scopesForSequence);
69
+ }
53
70
  this._events.push({
54
71
  sequenceNumber: this._events.length,
55
72
  eventSequenceId,
56
- eventForEventSourceId: {
57
- eventSourceId,
58
- event,
59
- ...(routing?.eventSource !== undefined ? { eventSource: routing.eventSource } : {}),
60
- ...(routing?.eventStream !== undefined ? { eventStream: routing.eventStream } : {})
61
- }
73
+ eventForEventSourceId
62
74
  });
63
75
  }
64
76
 
@@ -94,6 +106,11 @@ export class UnitOfWork implements IUnitOfWork {
94
106
  this.throwIfCompleted();
95
107
 
96
108
  if (this._events.length > 0) {
109
+ // Validate the entire commit before writing any sequence, including combinations in different groups.
110
+ ensureNamedTagBatchIsSupported(
111
+ this._events.some(entry => (entry.eventForEventSourceId.namedTags?.length ?? 0) > 0),
112
+ this._events.some(entry => entry.eventForEventSourceId.eventSource !== undefined)
113
+ );
97
114
  const resultsBySequenceNumber = new Map<number, AppendResult>();
98
115
  const eventsByEventSequence = new Map<string, EventForEventSourceIdWithSequenceNumber[]>();
99
116
 
@@ -106,16 +123,23 @@ export class UnitOfWork implements IUnitOfWork {
106
123
 
107
124
  // Resolve routing and required guards for every group first: a rejection must not follow a written group.
108
125
  const eventSources = this._eventStore.eventSources;
109
- for (const eventsForSequence of eventSources ? eventsByEventSequence.values() : []) {
110
- const events = eventsForSequence.map(_ => _.eventForEventSourceId);
111
- planDerivedGuards(events.map(_ => resolveBatchRouting(eventSources, _)), events, undefined);
126
+ if (eventSources) {
127
+ for (const [eventSequenceId, eventsForSequence] of eventsByEventSequence) {
128
+ const events = eventsForSequence.map(_ => _.eventForEventSourceId);
129
+ const explicitIds = new Set(this._concurrencyScopes.get(eventSequenceId)?.keys() ?? []);
130
+ planDerivedGuards(events.map(_ => resolveBatchRouting(eventSources, _)), events, undefined, explicitIds);
131
+ }
112
132
  }
113
133
 
114
134
  for (const [eventSequenceId, eventsForSequence] of eventsByEventSequence) {
115
135
  const sequence = this._eventStore.getEventSequence(new EventSequenceId(eventSequenceId));
136
+ const scopes = this._concurrencyScopes.get(eventSequenceId);
116
137
  const appendResults = await sequence.appendMany(
117
138
  eventsForSequence.map(_ => _.eventForEventSourceId),
118
- { correlationId: this.correlationId }
139
+ {
140
+ correlationId: this.correlationId,
141
+ ...(scopes && scopes.size > 0 ? { concurrencyScopes: Object.fromEntries(scopes) } : {})
142
+ }
119
143
  );
120
144
 
121
145
  appendResults.forEach((appendResult, index) => {
@@ -139,6 +163,7 @@ export class UnitOfWork implements IUnitOfWork {
139
163
  this.throwIfCompleted();
140
164
  this._isRolledBack = true;
141
165
  this._events = [];
166
+ this._concurrencyScopes = new Map();
142
167
  this._appendResults = [];
143
168
  this._onCompleted(this);
144
169
  }
@@ -0,0 +1,23 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { ConcurrencyScope } from '../eventSequences/ConcurrencyScope.js';
5
+
6
+ /**
7
+ * Compares two concurrency scopes by value.
8
+ * @param first - The first scope.
9
+ * @param second - The second scope.
10
+ * @returns True when both scopes validate the same expectation.
11
+ */
12
+ export function concurrencyScopesAreEqual(first: ConcurrencyScope, second: ConcurrencyScope): boolean {
13
+ // Event type filters are sets of id + generation: omitted equals empty, duplicates and order do not matter.
14
+ const eventTypeKeys = (scope: ConcurrencyScope) =>
15
+ [...new Set((scope.eventTypes ?? []).map(_ => `${_.id.value}+${_.generation.value}`))].sort().join(',');
16
+
17
+ return first.sequenceNumber === second.sequenceNumber
18
+ && (first.eventSourceId ?? false) === (second.eventSourceId ?? false)
19
+ && first.eventStreamType === second.eventStreamType
20
+ && first.eventStreamId === second.eventStreamId
21
+ && first.eventSourceType === second.eventSourceType
22
+ && eventTypeKeys(first) === eventTypeKeys(second);
23
+ }