@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.
- package/ChronicleClient.ts +127 -99
- package/ChronicleOptions.ts +23 -0
- package/dist/ChronicleClient.d.ts +7 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +115 -95
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +14 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +16 -0
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +12 -0
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +202 -175
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +2 -2
- package/dist/eventSequences/PreparedBatchAppend.d.ts +4 -0
- package/dist/eventSequences/PreparedBatchAppend.d.ts.map +1 -0
- package/dist/eventSequences/PreparedBatchAppend.js +4 -0
- package/dist/eventSequences/PreparedBatchAppend.js.map +1 -0
- package/dist/eventSequences/PreparedSingleAppend.d.ts +4 -0
- package/dist/eventSequences/PreparedSingleAppend.d.ts.map +1 -0
- package/dist/eventSequences/PreparedSingleAppend.js +4 -0
- package/dist/eventSequences/PreparedSingleAppend.js.map +1 -0
- package/dist/eventSequences/WireBatchEvent.d.ts +6 -0
- package/dist/eventSequences/WireBatchEvent.d.ts.map +1 -0
- package/dist/eventSequences/WireBatchEvent.js +4 -0
- package/dist/eventSequences/WireBatchEvent.js.map +1 -0
- package/dist/eventSequences/toContractsCausation.d.ts +16 -0
- package/dist/eventSequences/toContractsCausation.d.ts.map +1 -0
- package/dist/eventSequences/toContractsCausation.js +15 -0
- package/dist/eventSequences/toContractsCausation.js.map +1 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.js +13 -0
- package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
- package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
- package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
- package/dist/events/constraints/ConstraintEventSequences.js +39 -0
- package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +24 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
- package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueOptions.d.ts +15 -0
- package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
- package/dist/events/constraints/UniqueOptions.js +4 -0
- package/dist/events/constraints/UniqueOptions.js.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
- package/dist/events/constraints/index.d.ts +2 -0
- package/dist/events/constraints/index.d.ts.map +1 -1
- package/dist/events/constraints/index.js.map +1 -1
- package/dist/events/constraints/unique.d.ts +10 -0
- package/dist/events/constraints/unique.d.ts.map +1 -1
- package/dist/events/constraints/unique.js +11 -10
- package/dist/events/constraints/unique.js.map +1 -1
- package/dist/reducers/ReducerObservation.d.ts +18 -0
- package/dist/reducers/ReducerObservation.d.ts.map +1 -0
- package/dist/reducers/ReducerObservation.js +4 -0
- package/dist/reducers/ReducerObservation.js.map +1 -0
- package/dist/reducers/ReducerObservationSession.d.ts +11 -0
- package/dist/reducers/ReducerObservationSession.d.ts.map +1 -0
- package/dist/reducers/ReducerObservationSession.js +4 -0
- package/dist/reducers/ReducerObservationSession.js.map +1 -0
- package/dist/reducers/Reducers.d.ts +9 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +149 -154
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/ReductionOutcome.d.ts +25 -0
- package/dist/reducers/ReductionOutcome.d.ts.map +1 -0
- package/dist/reducers/ReductionOutcome.js +66 -0
- package/dist/reducers/ReductionOutcome.js.map +1 -0
- package/dist/testing/EventScenario.d.ts.map +1 -1
- package/dist/testing/EventScenario.js +8 -3
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/InProcessConstraints.d.ts +4 -1
- package/dist/testing/InProcessConstraints.d.ts.map +1 -1
- package/dist/testing/InProcessConstraints.js +5 -1
- package/dist/testing/InProcessConstraints.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +2 -1
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
- package/dist/transactions/IUnitOfWork.d.ts +2 -1
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalAppendOptions.d.ts +23 -2
- package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.d.ts +1 -0
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +28 -13
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
- package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
- package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
- package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
- package/dist/transactions/index.d.ts +1 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +1 -0
- package/dist/transactions/index.js.map +1 -1
- package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
- package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
- package/dist/transactions/toEventForEventSourceId.js +29 -0
- package/dist/transactions/toEventForEventSourceId.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventSequence.ts +265 -186
- package/eventSequences/ITransactionalEventSequence.ts +2 -2
- package/eventSequences/PreparedBatchAppend.ts +7 -0
- package/eventSequences/PreparedSingleAppend.ts +7 -0
- package/eventSequences/WireBatchEvent.ts +7 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
- package/eventSequences/toContractsCausation.ts +17 -0
- package/events/constraints/ConstraintBuilder.ts +16 -0
- package/events/constraints/ConstraintEventSequences.ts +41 -0
- package/events/constraints/Constraints.ts +22 -3
- package/events/constraints/IConstraintBuilder.ts +20 -0
- package/events/constraints/UniqueOptions.ts +20 -0
- package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
- package/events/constraints/index.ts +2 -0
- package/events/constraints/unique.ts +22 -2
- package/for_ChronicleClient/messageSizes.spec.ts +37 -0
- package/for_ChronicleOptions/when_configuring_message_sizes/with_message_sizes.spec.ts +29 -0
- package/package.json +2 -2
- package/reducers/ReducerObservation.ts +25 -0
- package/reducers/ReducerObservationSession.ts +15 -0
- package/reducers/Reducers.ts +185 -171
- package/reducers/ReductionOutcome.ts +75 -0
- package/testing/EventScenario.fixtures.spec.ts +1 -1
- package/testing/EventScenario.ts +10 -4
- package/testing/InProcessConstraints.ts +5 -1
- package/testing/InProcessEventSequence.ts +2 -1
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
- package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
- package/transactions/IUnitOfWork.ts +2 -1
- package/transactions/TransactionalAppendOptions.ts +30 -2
- package/transactions/UnitOfWork.ts +29 -13
- package/transactions/concurrencyScopesAreEqual.ts +23 -0
- package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
- package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
- package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
- package/transactions/index.ts +1 -0
- package/transactions/toEventForEventSourceId.ts +32 -0
- package/vitest.integration.config.ts +2 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
- 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: [] };
|
package/testing/EventScenario.ts
CHANGED
|
@@ -85,9 +85,15 @@ export class EventScenario {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 && (
|
|
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' &&
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
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
|
-
{
|
|
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
|
+
}
|