@cratis/chronicle 6.50.0 → 6.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/ChronicleClient.ts +127 -99
  2. package/ChronicleOptions.ts +23 -0
  3. package/dist/ChronicleClient.d.ts +7 -0
  4. package/dist/ChronicleClient.d.ts.map +1 -1
  5. package/dist/ChronicleClient.js +115 -95
  6. package/dist/ChronicleClient.js.map +1 -1
  7. package/dist/ChronicleOptions.d.ts +14 -0
  8. package/dist/ChronicleOptions.d.ts.map +1 -1
  9. package/dist/ChronicleOptions.js +16 -0
  10. package/dist/ChronicleOptions.js.map +1 -1
  11. package/dist/eventSequences/EventSequence.d.ts +12 -0
  12. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  13. package/dist/eventSequences/EventSequence.js +202 -175
  14. package/dist/eventSequences/EventSequence.js.map +1 -1
  15. package/dist/eventSequences/ITransactionalEventSequence.d.ts +2 -2
  16. package/dist/eventSequences/PreparedBatchAppend.d.ts +4 -0
  17. package/dist/eventSequences/PreparedBatchAppend.d.ts.map +1 -0
  18. package/dist/eventSequences/PreparedBatchAppend.js +4 -0
  19. package/dist/eventSequences/PreparedBatchAppend.js.map +1 -0
  20. package/dist/eventSequences/PreparedSingleAppend.d.ts +4 -0
  21. package/dist/eventSequences/PreparedSingleAppend.d.ts.map +1 -0
  22. package/dist/eventSequences/PreparedSingleAppend.js +4 -0
  23. package/dist/eventSequences/PreparedSingleAppend.js.map +1 -0
  24. package/dist/eventSequences/WireBatchEvent.d.ts +6 -0
  25. package/dist/eventSequences/WireBatchEvent.d.ts.map +1 -0
  26. package/dist/eventSequences/WireBatchEvent.js +4 -0
  27. package/dist/eventSequences/WireBatchEvent.js.map +1 -0
  28. package/dist/eventSequences/toContractsCausation.d.ts +16 -0
  29. package/dist/eventSequences/toContractsCausation.d.ts.map +1 -0
  30. package/dist/eventSequences/toContractsCausation.js +15 -0
  31. package/dist/eventSequences/toContractsCausation.js.map +1 -0
  32. package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
  33. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  34. package/dist/events/constraints/ConstraintBuilder.js +13 -0
  35. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  36. package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
  37. package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
  38. package/dist/events/constraints/ConstraintEventSequences.js +39 -0
  39. package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
  40. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  41. package/dist/events/constraints/Constraints.js +24 -3
  42. package/dist/events/constraints/Constraints.js.map +1 -1
  43. package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
  44. package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
  45. package/dist/events/constraints/UniqueOptions.d.ts +15 -0
  46. package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
  47. package/dist/events/constraints/UniqueOptions.js +4 -0
  48. package/dist/events/constraints/UniqueOptions.js.map +1 -0
  49. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
  50. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
  51. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
  52. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
  53. package/dist/events/constraints/index.d.ts +2 -0
  54. package/dist/events/constraints/index.d.ts.map +1 -1
  55. package/dist/events/constraints/index.js.map +1 -1
  56. package/dist/events/constraints/unique.d.ts +10 -0
  57. package/dist/events/constraints/unique.d.ts.map +1 -1
  58. package/dist/events/constraints/unique.js +11 -10
  59. package/dist/events/constraints/unique.js.map +1 -1
  60. package/dist/reducers/ReducerObservation.d.ts +18 -0
  61. package/dist/reducers/ReducerObservation.d.ts.map +1 -0
  62. package/dist/reducers/ReducerObservation.js +4 -0
  63. package/dist/reducers/ReducerObservation.js.map +1 -0
  64. package/dist/reducers/ReducerObservationSession.d.ts +11 -0
  65. package/dist/reducers/ReducerObservationSession.d.ts.map +1 -0
  66. package/dist/reducers/ReducerObservationSession.js +4 -0
  67. package/dist/reducers/ReducerObservationSession.js.map +1 -0
  68. package/dist/reducers/Reducers.d.ts +9 -0
  69. package/dist/reducers/Reducers.d.ts.map +1 -1
  70. package/dist/reducers/Reducers.js +149 -154
  71. package/dist/reducers/Reducers.js.map +1 -1
  72. package/dist/reducers/ReductionOutcome.d.ts +25 -0
  73. package/dist/reducers/ReductionOutcome.d.ts.map +1 -0
  74. package/dist/reducers/ReductionOutcome.js +66 -0
  75. package/dist/reducers/ReductionOutcome.js.map +1 -0
  76. package/dist/testing/EventScenario.d.ts.map +1 -1
  77. package/dist/testing/EventScenario.js +8 -3
  78. package/dist/testing/EventScenario.js.map +1 -1
  79. package/dist/testing/InProcessConstraints.d.ts +4 -1
  80. package/dist/testing/InProcessConstraints.d.ts.map +1 -1
  81. package/dist/testing/InProcessConstraints.js +5 -1
  82. package/dist/testing/InProcessConstraints.js.map +1 -1
  83. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  84. package/dist/testing/InProcessEventSequence.js +2 -1
  85. package/dist/testing/InProcessEventSequence.js.map +1 -1
  86. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
  87. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
  88. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
  89. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
  90. package/dist/transactions/IUnitOfWork.d.ts +2 -1
  91. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  92. package/dist/transactions/TransactionalAppendOptions.d.ts +23 -2
  93. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -1
  94. package/dist/transactions/UnitOfWork.d.ts +1 -0
  95. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  96. package/dist/transactions/UnitOfWork.js +28 -13
  97. package/dist/transactions/UnitOfWork.js.map +1 -1
  98. package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
  99. package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
  100. package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
  101. package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
  102. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
  103. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
  104. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
  105. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
  106. package/dist/transactions/index.d.ts +1 -0
  107. package/dist/transactions/index.d.ts.map +1 -1
  108. package/dist/transactions/index.js +1 -0
  109. package/dist/transactions/index.js.map +1 -1
  110. package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
  111. package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
  112. package/dist/transactions/toEventForEventSourceId.js +29 -0
  113. package/dist/transactions/toEventForEventSourceId.js.map +1 -0
  114. package/dist/tsconfig.tsbuildinfo +1 -1
  115. package/eventSequences/EventSequence.ts +265 -186
  116. package/eventSequences/ITransactionalEventSequence.ts +2 -2
  117. package/eventSequences/PreparedBatchAppend.ts +7 -0
  118. package/eventSequences/PreparedSingleAppend.ts +7 -0
  119. package/eventSequences/WireBatchEvent.ts +7 -0
  120. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
  121. package/eventSequences/toContractsCausation.ts +17 -0
  122. package/events/constraints/ConstraintBuilder.ts +16 -0
  123. package/events/constraints/ConstraintEventSequences.ts +41 -0
  124. package/events/constraints/Constraints.ts +22 -3
  125. package/events/constraints/IConstraintBuilder.ts +20 -0
  126. package/events/constraints/UniqueOptions.ts +20 -0
  127. package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
  128. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
  129. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
  130. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
  131. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
  132. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
  133. package/events/constraints/index.ts +2 -0
  134. package/events/constraints/unique.ts +22 -2
  135. package/for_ChronicleClient/messageSizes.spec.ts +37 -0
  136. package/for_ChronicleOptions/when_configuring_message_sizes/with_message_sizes.spec.ts +29 -0
  137. package/package.json +2 -2
  138. package/reducers/ReducerObservation.ts +25 -0
  139. package/reducers/ReducerObservationSession.ts +15 -0
  140. package/reducers/Reducers.ts +185 -171
  141. package/reducers/ReductionOutcome.ts +75 -0
  142. package/testing/EventScenario.fixtures.spec.ts +1 -1
  143. package/testing/EventScenario.ts +10 -4
  144. package/testing/InProcessConstraints.ts +5 -1
  145. package/testing/InProcessEventSequence.ts +2 -1
  146. package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
  147. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
  148. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
  149. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
  150. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
  151. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
  152. package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
  153. package/transactions/IUnitOfWork.ts +2 -1
  154. package/transactions/TransactionalAppendOptions.ts +30 -2
  155. package/transactions/UnitOfWork.ts +29 -13
  156. package/transactions/concurrencyScopesAreEqual.ts +23 -0
  157. package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
  158. package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
  159. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
  160. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
  161. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
  162. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
  163. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
  164. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
  165. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
  166. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
  167. package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
  168. package/transactions/index.ts +1 -0
  169. package/transactions/toEventForEventSourceId.ts +32 -0
  170. package/vitest.integration.config.ts +2 -0
  171. package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
  172. package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
  173. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
  174. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
  175. package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
  176. package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
@@ -59,7 +59,7 @@ const fixtures = readdirSync(directory).filter(name => name.endsWith('.json') &&
59
59
  !['batches.json', 'batch-omitted-routes.json', 'batch-rollback.json', 'builders.json', 'constraints.json', 'constraints-isolation.json', 'constraints-key-domain.json',
60
60
  'constraints-property-lifecycle.json', 'constraints-property-covered-removal.json', 'constraints-event-type-cycles.json',
61
61
  'constraints-event-type-siblings.json', 'constraints-composite.json',
62
- 'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json'].includes(name)).map(name => ({
62
+ 'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json', 'constraints-event-sequences.json'].includes(name)).map(name => ({
63
63
  name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture
64
64
  }));
65
65
  const artifacts = { eventTypes: [OracleEventRecorded, AlternateRecorded, BoundaryRecorded], constraints: [] };
@@ -85,9 +85,15 @@ export class EventScenario {
85
85
  }
86
86
  }
87
87
  }
88
- constraints = new InProcessConstraints(definitions, new Map(eventTypes.map(type =>
89
- [getEventTypeFor(type).id.value, getEventTypeMetadata(type)!.schema])));
90
- for (const [name, capture] of definitions) {
88
+ // The scenario appends to the event log only. A definition scoped to other event sequences is neither
89
+ // validated nor indexed there by the kernel (constraints-event-sequences.json), so it takes no part.
90
+ const eventLog = EventSequenceId.eventLog.value;
91
+ const applicable = new Map([...definitions].filter(([, capture]) =>
92
+ !capture.eventSequences?.length || capture.eventSequences.includes(eventLog)));
93
+ const inapplicable = new Set([...definitions.keys()].filter(name => !applicable.has(name)));
94
+ constraints = new InProcessConstraints(applicable, new Map(eventTypes.map(type =>
95
+ [getEventTypeFor(type).id.value, getEventTypeMetadata(type)!.schema])), inapplicable);
96
+ for (const [name, capture] of applicable) {
91
97
  const ids = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
92
98
  capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType?.eventTypeId];
93
99
  const removedWith = [...(capture.uniqueConstraint?.removedWithEventTypeIds ?? []),
@@ -121,7 +127,7 @@ export class EventScenario {
121
127
  `Unique keys with schema ${schema?.type ?? 'missing'}/${schema?.format ?? 'unformatted'} are not fixture-backed; date, object and other numeric formats require a kernel-backed test.`);
122
128
  }
123
129
  const extendedKey = schemas.some(schema => schema?.type === 'number' || schema?.format === 'guid');
124
- if (extendedKey && (definitions.size !== 1 || capture.uniqueConstraint!.eventDefinitions.length !== 1 ||
130
+ if (extendedKey && (applicable.size !== 1 || capture.uniqueConstraint!.eventDefinitions.length !== 1 ||
125
131
  removedWith.some(id => id !== undefined))) {
126
132
  throw new UnsupportedEventSequenceOperation('artifacts.constraints', name,
127
133
  'Guid and numeric keys require one isolated definition covering one event type without removers; shared definitions and removal combinations are not fixture-backed.');
@@ -69,7 +69,8 @@ export class InProcessConstraints {
69
69
  private readonly _propertyRemovalTypes = new Set<string>();
70
70
 
71
71
  constructor(private readonly _definitions: ReadonlyMap<string, ConstraintCapture>,
72
- private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map()) {
72
+ private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map(),
73
+ private readonly _inapplicable: ReadonlySet<string> = new Set()) {
73
74
  const coveredTypes = new Map<string, string>();
74
75
  const removalOwners = new Map<string, string>();
75
76
  for (const [name, capture] of _definitions) {
@@ -161,6 +162,9 @@ export class InProcessConstraints {
161
162
 
162
163
  hasRemovalType(eventTypeId: string): boolean { return this._removalTypes.has(eventTypeId); }
163
164
 
165
+ /** Whether a named definition is scoped to event sequences other than the scenario's, so it takes no part. */
166
+ isInapplicable(name: string): boolean { return this._inapplicable.has(name); }
167
+
164
168
  isConstrainedProperty(eventTypeId: string, property: string): boolean {
165
169
  return this._constrainedProperties.get(eventTypeId)?.has(property) ?? false;
166
170
  }
@@ -110,7 +110,8 @@ export class InProcessEventSequence implements IEventSequence {
110
110
  if (eventType.generation.value !== 1 || eventType.tombstone) {
111
111
  throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
112
112
  }
113
- if (options.constraints !== 'disabled' && getRemovedConstraintNames(type).length &&
113
+ if (options.constraints !== 'disabled' &&
114
+ getRemovedConstraintNames(type).some(name => !this._constraints?.isInapplicable(name)) &&
114
115
  !this._constraints?.hasRemovalType(eventType.id.value)) {
115
116
  throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint removal is not fixture-backed.');
116
117
  }
@@ -0,0 +1,101 @@
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 { readFileSync } from 'node:fs';
5
+ import { chai, describe, it, vi, type Assertion } from 'vitest';
6
+ import type { Constructor } from '@cratis/fundamentals';
7
+ import { getEventTypeMetadata } from '../../../events/eventTypeDecorator.js';
8
+ import { compileConstraints } from '../../../events/constraints/Constraints.js';
9
+ import { EventScenario } from '../../index.js';
10
+ import { InProcessConstraints } from '../../InProcessConstraints.js';
11
+
12
+ chai.should();
13
+ function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
14
+
15
+ type Entry = { source: string; type: string; value: string };
16
+ type Definition = { kind: string; name: string; events?: Array<{ type: string; properties: string[] }>; eventTypes?: string[];
17
+ ignoreCasing?: boolean; removedWithEventTypeIds: string[]; message: string; eventSequences: string[] };
18
+ type History = { sequence: string; source: string; sourceType?: string; streamType?: string; streamId?: string;
19
+ type: string; content: Record<string, unknown>; hash?: string };
20
+ type Outcome = { success: boolean; sequences: string[]; errors: string[];
21
+ wireViolations: Array<{ EventTypeId: string; SequenceNumber: string; ConstraintType: number; ConstraintName: string; Message: string; Details: Record<string, string> }>;
22
+ violations: Array<{ id: string; message: string; details: Record<string, string> }>; history: History[]; next: string };
23
+ type SequenceCase = { name: string; kind: string; eventSchemas: Record<string, { eventTypeId: string; properties: Record<string, string> }>;
24
+ constraintDefinitions: Definition[]; isolatedConstraintOperations: Array<{ mode: string; events: Entry[] }> };
25
+ type Fixture = { eventSequenceCases: SequenceCase[]; expected: { cases: Array<{ name: string; result: { outcomes: Outcome[] } }> } };
26
+
27
+ /**
28
+ * Replays constraints-event-sequences.json: a definition applying to the event log behaves as unscoped, and one
29
+ * applying only to other event sequences is neither validated nor indexed on the scenario's event log.
30
+ * @param types - Event constructors by fixture alias.
31
+ * @param constraints - Selects the constraint classes for a case name.
32
+ */
33
+ export function eventSequenceScenarioBehaviors(
34
+ types: Record<string, new (value: string) => object>,
35
+ constraints: (caseName: string) => Constructor[]
36
+ ): void {
37
+ const fixture = JSON.parse(readFileSync(new URL('../../fixtures/constraints-event-sequences.json', import.meta.url), 'utf8')) as Fixture;
38
+ const historyOf = (scenario: EventScenario): History[] => scenario.appendedEvents.map(event => ({
39
+ sequence: event.context.sequenceNumber.toString(), source: event.context.eventSourceId,
40
+ sourceType: event.context.eventSourceType, streamType: event.context.eventStreamType, streamId: event.context.eventStreamId,
41
+ type: event.eventType.id.value, content: event.content, hash: event.context.hash
42
+ }));
43
+ const aliases = Object.fromEntries(Object.entries(types).map(([alias, type]) => [getEventTypeMetadata(type)!.eventType.id.value, alias]));
44
+
45
+ it('should retain every event sequence case', () => {
46
+ should(fixture.eventSequenceCases.map(test => test.name)).deep.equal([
47
+ 'property-event-log', 'property-event-log-and-outbox', 'property-outbox',
48
+ 'cycle-event-log', 'cycle-outbox', 'once-event-log', 'once-outbox'
49
+ ]);
50
+ should(fixture.expected.cases.map(test => test.name)).deep.equal(fixture.eventSequenceCases.map(test => test.name));
51
+ });
52
+
53
+ for (const [caseIndex, test] of fixture.eventSequenceCases.entries()) {
54
+ describe(`with ${test.name}`, () => {
55
+ it('should match the installed definitions and kernel violations, history and next sequence', async () => {
56
+ const selected = Object.keys(test.eventSchemas).map(alias => types[alias]);
57
+ const selectedConstraints = constraints(test.name);
58
+ const compiled = compileConstraints({ eventTypes: selected, constraints: selectedConstraints });
59
+ should([...compiled].map(([name, capture]) => ({
60
+ kind: capture.uniqueConstraint ? 'uniqueProperty' : 'uniqueEventType', name,
61
+ ...(capture.uniqueConstraint ? {
62
+ events: capture.uniqueConstraint.eventDefinitions.map(entry => ({ type: aliases[entry.eventTypeId], properties: entry.properties })),
63
+ ignoreCasing: capture.uniqueConstraint.ignoreCasing
64
+ } : { eventTypes: (capture.uniqueEventType!.eventTypeIds ?? [capture.uniqueEventType!.eventTypeId]).map(id => aliases[id]) }),
65
+ removedWithEventTypeIds: (capture.uniqueConstraint?.removedWithEventTypeIds ?? capture.uniqueEventType?.removedWithEventTypeIds ?? []).map(id => aliases[id]),
66
+ message: capture.uniqueConstraint?.message ?? capture.uniqueEventType?.message ?? '',
67
+ eventSequences: capture.eventSequences ?? []
68
+ }))).deep.equal(test.constraintDefinitions);
69
+ const scenario = new EventScenario({ artifacts: { eventTypes: selected, constraints: selectedConstraints } });
70
+ const validate = vi.spyOn(InProcessConstraints.prototype, 'validate');
71
+ const expected = fixture.expected.cases[caseIndex].result.outcomes;
72
+ should(expected.length).equal(test.isolatedConstraintOperations.length);
73
+ try {
74
+ for (const [index, operation] of test.isolatedConstraintOperations.entries()) {
75
+ const before = historyOf(scenario);
76
+ const entries = operation.events.map(entry => ({ eventSourceId: entry.source, event: new types[entry.type](entry.value) }));
77
+ const results = operation.mode === 'single'
78
+ ? [await scenario.append(entries[0].eventSourceId, entries[0].event)]
79
+ : await scenario.appendMany(entries);
80
+ const outcome = expected[index];
81
+ const raw = validate.mock.results.at(-1)!.value as ReturnType<InProcessConstraints['validate']>;
82
+ should(raw.map(({ SequenceNumber, ...violation }) => ({ ...violation, SequenceNumber: SequenceNumber.toString() })))
83
+ .deep.equal(outcome.wireViolations);
84
+ for (const [position, result] of results.entries()) {
85
+ should(result.isSuccess).equal(outcome.success);
86
+ should(result.sequenceNumber.value.toString()).equal(outcome.success ? outcome.sequences[position] : '0');
87
+ should(result.constraintViolations.map(violation => ({ id: violation.constraintId, message: violation.message, details: violation.details })))
88
+ .deep.equal(outcome.violations);
89
+ should(result.errors.map(error => error.message)).deep.equal(outcome.errors);
90
+ }
91
+ if (!outcome.success) should(historyOf(scenario)).deep.equal(before);
92
+ should(historyOf(scenario)).deep.equal(outcome.history);
93
+ should((await scenario.eventLog.getNextSequenceNumber()).value.toString()).equal(outcome.next);
94
+ }
95
+ } finally {
96
+ validate.mockRestore();
97
+ }
98
+ });
99
+ });
100
+ }
101
+ }
@@ -0,0 +1,28 @@
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 } 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 type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
10
+ import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
11
+
12
+ @eventType('OracleOtherSequenceNumber')
13
+ class NumberClaimed { @field(Number) key = 1; }
14
+ @eventType('OracleOtherSequenceOutbox')
15
+ class OutboxClaimed { @field(String) label = 'x'; }
16
+
17
+ @constraint('OracleOtherSequenceNumberKey')
18
+ class NumberKey {
19
+ define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
20
+ }
21
+ @constraint('OracleOtherSequenceOutboxKey')
22
+ class OutboxKey {
23
+ define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
24
+ }
25
+
26
+ describe('when appending extended keys beside other event sequence constraints with decorators', () => {
27
+ otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
28
+ });
@@ -0,0 +1,27 @@
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 { describe } from 'vitest';
6
+ import { eventType } from '../../../events/eventTypeDecorator.js';
7
+ import { constraint } from '../../../events/constraints/constraint.js';
8
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
9
+ import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
10
+
11
+ @eventType('OracleOtherSequenceNumber')
12
+ class NumberClaimed { @field(Number) key = 1; }
13
+ @eventType('OracleOtherSequenceOutbox')
14
+ class OutboxClaimed { @field(String) label = 'x'; }
15
+
16
+ @constraint('OracleOtherSequenceNumberKey')
17
+ class NumberKey {
18
+ define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
19
+ }
20
+ @constraint('OracleOtherSequenceOutboxKey')
21
+ class OutboxKey {
22
+ define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
23
+ }
24
+
25
+ describe('when appending extended keys beside other event sequence constraints with decorators', () => {
26
+ otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
27
+ });
@@ -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,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
+ }
@@ -27,7 +27,8 @@ 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 options - Optional registered event source / stream routing and named tags 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.
31
32
  * @throws {@link InvalidNamedTag} when a named tag is invalid; the event is not added.
32
33
  */
33
34
  addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void;
@@ -2,13 +2,41 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import type { NamedTag } from '../events/NamedTag.js';
5
+ import type { Tag } from '../events/Tag.js';
6
+ import type { ConcurrencyScope } from '../eventSequences/ConcurrencyScope.js';
5
7
  import type { TransactionalEventRouting } from './TransactionalEventRouting.js';
6
8
 
7
9
  /**
8
- * Optional metadata for an event added to a unit of work: registered event source routing and
9
- * structured named tags. Carried through to the append on commit.
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.
10
13
  */
11
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
+
12
40
  /**
13
41
  * Structured named tags for the event. Validated when the event is added, so an invalid tag fails
14
42
  * before anything is committed; the first occurrence of each exact name and value pair is kept.
@@ -12,7 +12,10 @@ import { planDerivedGuards, resolveBatchRouting } from '../eventSequences/resolv
12
12
  import { ensureNamedTagBatchIsSupported } from '../eventSequences/ensureNamedTagBatchIsSupported.js';
13
13
  import { IEventStore } from '../IEventStore.js';
14
14
  import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
15
- import { mergeNamedTags } from '../events/mergeNamedTags.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';
16
19
  import { IUnitOfWork } from './IUnitOfWork.js';
17
20
 
18
21
  interface EventForEventSourceIdWithSequenceNumber {
@@ -26,6 +29,7 @@ interface EventForEventSourceIdWithSequenceNumber {
26
29
  */
27
30
  export class UnitOfWork implements IUnitOfWork {
28
31
  private _events: EventForEventSourceIdWithSequenceNumber[] = [];
32
+ private _concurrencyScopes = new Map<string, Map<string, ConcurrencyScope>>();
29
33
  private _appendResults: AppendResult[] = [];
30
34
  private _isCommitted = false;
31
35
  private _isRolledBack = false;
@@ -52,17 +56,21 @@ export class UnitOfWork implements IUnitOfWork {
52
56
  /** @inheritdoc */
53
57
  addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void {
54
58
  this.throwIfCompleted();
55
- const namedTags = mergeNamedTags(options?.namedTags);
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
+ }
56
70
  this._events.push({
57
71
  sequenceNumber: this._events.length,
58
72
  eventSequenceId,
59
- eventForEventSourceId: {
60
- eventSourceId,
61
- event,
62
- ...(options?.eventSource !== undefined ? { eventSource: options.eventSource } : {}),
63
- ...(options?.eventStream !== undefined ? { eventStream: options.eventStream } : {}),
64
- ...(namedTags.length > 0 ? { namedTags } : {})
65
- }
73
+ eventForEventSourceId
66
74
  });
67
75
  }
68
76
 
@@ -115,16 +123,23 @@ export class UnitOfWork implements IUnitOfWork {
115
123
 
116
124
  // Resolve routing and required guards for every group first: a rejection must not follow a written group.
117
125
  const eventSources = this._eventStore.eventSources;
118
- for (const eventsForSequence of eventSources ? eventsByEventSequence.values() : []) {
119
- const events = eventsForSequence.map(_ => _.eventForEventSourceId);
120
- 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
+ }
121
132
  }
122
133
 
123
134
  for (const [eventSequenceId, eventsForSequence] of eventsByEventSequence) {
124
135
  const sequence = this._eventStore.getEventSequence(new EventSequenceId(eventSequenceId));
136
+ const scopes = this._concurrencyScopes.get(eventSequenceId);
125
137
  const appendResults = await sequence.appendMany(
126
138
  eventsForSequence.map(_ => _.eventForEventSourceId),
127
- { correlationId: this.correlationId }
139
+ {
140
+ correlationId: this.correlationId,
141
+ ...(scopes && scopes.size > 0 ? { concurrencyScopes: Object.fromEntries(scopes) } : {})
142
+ }
128
143
  );
129
144
 
130
145
  appendResults.forEach((appendResult, index) => {
@@ -148,6 +163,7 @@ export class UnitOfWork implements IUnitOfWork {
148
163
  this.throwIfCompleted();
149
164
  this._isRolledBack = true;
150
165
  this._events = [];
166
+ this._concurrencyScopes = new Map();
151
167
  this._appendResults = [];
152
168
  this._onCompleted(this);
153
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
+ }