@cratis/chronicle 6.48.0 → 6.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EventStore.ts +8 -2
- package/IEventStore.ts +8 -0
- package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
- package/artifacts/IClientArtifactsProvider.ts +3 -0
- package/connection/ChronicleConnection.ts +6 -0
- package/connection/ChronicleServices.ts +2 -0
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +7 -2
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +7 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
- package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +1 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +5 -1
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +2 -1
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/eventSequences/AppendOptions.d.ts +22 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +17 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +4 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +59 -13
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -2
- package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
- package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -2
- package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.js +7 -4
- package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +2 -1
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
- package/dist/eventSequences/createAppendNotification.js +3 -2
- package/dist/eventSequences/createAppendNotification.js.map +1 -1
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
- package/dist/eventSequences/index.d.ts +1 -0
- package/dist/eventSequences/index.d.ts.map +1 -1
- package/dist/eventSequences/index.js +1 -0
- package/dist/eventSequences/index.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +4 -1
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.js +3 -1
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
- package/dist/eventSequences/resolveEventSourceRouting.d.ts +39 -0
- package/dist/eventSequences/resolveEventSourceRouting.d.ts.map +1 -0
- package/dist/eventSequences/resolveEventSourceRouting.js +92 -0
- package/dist/eventSequences/resolveEventSourceRouting.js.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts +19 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.js +19 -0
- package/dist/eventSources/ConcurrencyDimensions.js.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts +6 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.js +12 -0
- package/dist/eventSources/DuplicateEventSourceName.js.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts +7 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.js +14 -0
- package/dist/eventSources/DuplicateEventStreamName.js.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts +9 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js +18 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js.map +1 -0
- package/dist/eventSources/EventSourceDefinition.d.ts +20 -0
- package/dist/eventSources/EventSourceDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventSourceDefinition.js +4 -0
- package/dist/eventSources/EventSourceDefinition.js.map +1 -0
- package/dist/eventSources/EventSources.d.ts +31 -0
- package/dist/eventSources/EventSources.d.ts.map +1 -0
- package/dist/eventSources/EventSources.js +90 -0
- package/dist/eventSources/EventSources.js.map +1 -0
- package/dist/eventSources/EventStreamDefinition.d.ts +13 -0
- package/dist/eventSources/EventStreamDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDefinition.js +4 -0
- package/dist/eventSources/EventStreamDefinition.js.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts +7 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js +14 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts +6 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js +12 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js.map +1 -0
- package/dist/eventSources/IEventSources.d.ts +27 -0
- package/dist/eventSources/IEventSources.d.ts.map +1 -0
- package/dist/eventSources/IEventSources.js +4 -0
- package/dist/eventSources/IEventSources.js.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts +37 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.js +60 -0
- package/dist/eventSources/ResolvedEventRouting.js.map +1 -0
- package/dist/eventSources/UnknownEventSource.d.ts +6 -0
- package/dist/eventSources/UnknownEventSource.d.ts.map +1 -0
- package/dist/eventSources/UnknownEventSource.js +12 -0
- package/dist/eventSources/UnknownEventSource.js.map +1 -0
- package/dist/eventSources/eventSource.d.ts +60 -0
- package/dist/eventSources/eventSource.d.ts.map +1 -0
- package/dist/eventSources/eventSource.js +72 -0
- package/dist/eventSources/eventSource.js.map +1 -0
- package/dist/eventSources/index.d.ts +17 -0
- package/dist/eventSources/index.d.ts.map +1 -0
- package/dist/eventSources/index.js +13 -0
- package/dist/eventSources/index.js.map +1 -0
- package/dist/events/EventContext.d.ts +12 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/InvalidNamedTag.d.ts +8 -0
- package/dist/events/InvalidNamedTag.d.ts.map +1 -0
- package/dist/events/InvalidNamedTag.js +13 -0
- package/dist/events/InvalidNamedTag.js.map +1 -0
- package/dist/events/NamedTag.d.ts +25 -0
- package/dist/events/NamedTag.d.ts.map +1 -0
- package/dist/events/NamedTag.js +39 -0
- package/dist/events/NamedTag.js.map +1 -0
- package/dist/events/WireEventContext.d.ts +2 -0
- package/dist/events/WireEventContext.d.ts.map +1 -1
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeNamedTags.d.ts +13 -0
- package/dist/events/mergeNamedTags.d.ts.map +1 -0
- package/dist/events/mergeNamedTags.js +39 -0
- package/dist/events/mergeNamedTags.js.map +1 -0
- package/dist/events/toClientEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.js +4 -1
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +2 -0
- package/dist/projections/eventContextPropertyExpression.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +17 -7
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +6 -0
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js +2 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +5 -0
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +12 -1
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/transactions/IUnitOfWork.d.ts +4 -1
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalAppendOptions.d.ts +14 -0
- package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
- package/dist/transactions/TransactionalAppendOptions.js +4 -0
- package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
- package/dist/transactions/TransactionalEventRouting.d.ts +12 -0
- package/dist/transactions/TransactionalEventRouting.d.ts.map +1 -0
- package/dist/transactions/TransactionalEventRouting.js +4 -0
- package/dist/transactions/TransactionalEventRouting.js.map +1 -0
- package/dist/transactions/UnitOfWork.d.ts +2 -1
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +17 -2
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/index.d.ts +2 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/DecoratorType.d.ts +3 -1
- package/dist/types/DecoratorType.d.ts.map +1 -1
- package/dist/types/DecoratorType.js +2 -0
- package/dist/types/DecoratorType.js.map +1 -1
- package/eventSequences/AppendOptions.ts +25 -0
- package/eventSequences/EventForEventSourceId.ts +20 -0
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +62 -13
- package/eventSequences/ITransactionalEventSequence.ts +5 -2
- package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
- package/eventSequences/TransactionalEventSequence.ts +8 -4
- package/eventSequences/createAppendNotification.ts +4 -2
- package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
- package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
- package/eventSequences/index.ts +1 -0
- package/eventSequences/prepareBatchAppend.ts +4 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/eventSequences/resolveEventSourceRouting.ts +125 -0
- package/eventSources/ConcurrencyDimensions.ts +26 -0
- package/eventSources/DuplicateEventSourceName.ts +10 -0
- package/eventSources/DuplicateEventStreamName.ts +10 -0
- package/eventSources/EventRoutingContradictsEventSource.ts +10 -0
- package/eventSources/EventSourceDefinition.ts +27 -0
- package/eventSources/EventSources.ts +93 -0
- package/eventSources/EventStreamDefinition.ts +18 -0
- package/eventSources/EventStreamDoesNotBelongToEventSource.ts +10 -0
- package/eventSources/EventStreamRequiresEventSource.ts +10 -0
- package/eventSources/IEventSources.ts +33 -0
- package/eventSources/ResolvedEventRouting.ts +78 -0
- package/eventSources/UnknownEventSource.ts +10 -0
- package/eventSources/eventSource.ts +116 -0
- package/eventSources/for_EventSources/against_a_kernel/when_appending_through_registered_sources.integration.spec.ts +106 -0
- package/eventSources/for_EventSources/when_appending_through_a_definition.spec.ts +190 -0
- package/eventSources/for_EventSources/when_batch_requires_differing_guards.spec.ts +139 -0
- package/eventSources/for_EventSources/when_discovering_and_registering.spec.ts +94 -0
- package/eventSources/index.ts +19 -0
- package/events/EventContext.ts +14 -0
- package/events/InvalidNamedTag.ts +13 -0
- package/events/NamedTag.ts +44 -0
- package/events/WireEventContext.ts +2 -0
- package/events/for_EventContext/with_event_source.spec.ts +22 -0
- package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
- package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
- package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
- package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
- package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
- package/events/index.ts +3 -0
- package/events/mergeNamedTags.ts +37 -0
- package/events/toClientEventContext.ts +4 -1
- package/for_EventStore/when_implementing_a_legacy_event_store.spec.ts +19 -0
- package/index.ts +3 -0
- package/package.json +6 -6
- package/projections/eventContextPropertyExpression.ts +3 -1
- package/testing/EventScenario.event-sources.spec.ts +34 -0
- package/testing/InProcessEventSequence.ts +17 -7
- package/testing/ReactorScenario.ts +6 -0
- package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
- package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
- package/testing/projections/ProjectionReadModelProcessor.ts +2 -1
- package/testing/singleAppendRoute.ts +13 -1
- package/transactions/IUnitOfWork.ts +4 -1
- package/transactions/TransactionalAppendOptions.ts +17 -0
- package/transactions/TransactionalEventRouting.ts +16 -0
- package/transactions/UnitOfWork.ts +22 -2
- package/transactions/for_UnitOfWork/when_committing_events_with_event_source_routing.spec.ts +41 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
- package/transactions/index.ts +2 -0
- package/types/DecoratorType.ts +4 -1
|
@@ -0,0 +1,78 @@
|
|
|
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 { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import type { ConcurrencyDimensionFlags } from './ConcurrencyDimensions.js';
|
|
6
|
+
import type { EventSourceDefinition } from './EventSourceDefinition.js';
|
|
7
|
+
import type { EventStreamDefinition } from './EventStreamDefinition.js';
|
|
8
|
+
import type { IEventSources } from './IEventSources.js';
|
|
9
|
+
import { EventRoutingContradictsEventSource } from './EventRoutingContradictsEventSource.js';
|
|
10
|
+
import { EventStreamDoesNotBelongToEventSource } from './EventStreamDoesNotBelongToEventSource.js';
|
|
11
|
+
import { EventStreamRequiresEventSource } from './EventStreamRequiresEventSource.js';
|
|
12
|
+
import { UnknownEventSource } from './UnknownEventSource.js';
|
|
13
|
+
|
|
14
|
+
/** The explicit routing a caller supplied next to a definition. */
|
|
15
|
+
export interface ExplicitRouting {
|
|
16
|
+
/** Explicit event source type, if any. */
|
|
17
|
+
sourceType?: string;
|
|
18
|
+
|
|
19
|
+
/** Explicit event stream type, if any. */
|
|
20
|
+
streamType?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The routing of an append resolved through an event source definition. */
|
|
24
|
+
export class ResolvedEventRouting {
|
|
25
|
+
private constructor(readonly definition: EventSourceDefinition, readonly stream?: EventStreamDefinition) {}
|
|
26
|
+
|
|
27
|
+
/** The registered event source name written on the event. */
|
|
28
|
+
get eventSource(): string { return this.definition.name; }
|
|
29
|
+
|
|
30
|
+
/** The event source type written on the event. */
|
|
31
|
+
get sourceType(): string { return this.definition.name; }
|
|
32
|
+
|
|
33
|
+
/** The event stream type to write, or undefined to let the Kernel choose. */
|
|
34
|
+
get streamType(): string | undefined { return this.stream?.name; }
|
|
35
|
+
|
|
36
|
+
/** The concurrency dimensions that apply: the stream's when it declares any, otherwise the source's. */
|
|
37
|
+
get dimensions(): ConcurrencyDimensionFlags {
|
|
38
|
+
return this.stream && this.stream.concurrency !== 0 ? this.stream.concurrency : this.definition.concurrency;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the routing of an append.
|
|
43
|
+
* @param eventSources - The event sources; undefined when the store has none.
|
|
44
|
+
* @param eventSource - The definition class or name; undefined for a legacy append.
|
|
45
|
+
* @param stream - The declared stream name, if any.
|
|
46
|
+
* @param explicit - Explicit routing that must not contradict the definition.
|
|
47
|
+
* @returns The routing, or undefined when no definition is involved.
|
|
48
|
+
* @throws UnknownEventSource, EventStreamDoesNotBelongToEventSource, EventRoutingContradictsEventSource, EventStreamRequiresEventSource.
|
|
49
|
+
*/
|
|
50
|
+
static resolve(
|
|
51
|
+
eventSources: IEventSources | undefined,
|
|
52
|
+
eventSource: Constructor | string | undefined,
|
|
53
|
+
stream: string | undefined,
|
|
54
|
+
explicit: ExplicitRouting = {}
|
|
55
|
+
): ResolvedEventRouting | undefined {
|
|
56
|
+
if (eventSource === undefined) {
|
|
57
|
+
if (stream !== undefined) throw new EventStreamRequiresEventSource(stream);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!eventSources) throw new UnknownEventSource(typeof eventSource === 'string' ? eventSource : eventSource.name);
|
|
61
|
+
const definition = eventSources.getFor(eventSource);
|
|
62
|
+
|
|
63
|
+
const isSet = (value: string | undefined, unspecified: string[]) => value !== undefined && value !== '' && !unspecified.includes(value);
|
|
64
|
+
if (isSet(explicit.sourceType, ['Default', 'Unspecified']) && explicit.sourceType !== definition.name) {
|
|
65
|
+
throw new EventRoutingContradictsEventSource(definition.name, 'event source type', definition.name, explicit.sourceType!);
|
|
66
|
+
}
|
|
67
|
+
const explicitStream = isSet(explicit.streamType, ['All']) ? explicit.streamType : undefined;
|
|
68
|
+
if (stream !== undefined && explicitStream !== undefined && stream !== explicitStream) {
|
|
69
|
+
throw new EventRoutingContradictsEventSource(definition.name, 'event stream type', stream, explicitStream);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const name = stream ?? explicitStream;
|
|
73
|
+
if (name === undefined) return new ResolvedEventRouting(definition);
|
|
74
|
+
const found = definition.streams.find(candidate => candidate.name === name);
|
|
75
|
+
if (!found) throw new EventStreamDoesNotBelongToEventSource(definition.name, name);
|
|
76
|
+
return new ResolvedEventRouting(definition, found);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
/** Thrown when an append names an event source that is not a discovered definition. */
|
|
5
|
+
export class UnknownEventSource extends Error {
|
|
6
|
+
constructor(readonly eventSource: string) {
|
|
7
|
+
super(`The event source '${eventSource}' is not a discovered event source definition.`);
|
|
8
|
+
this.name = 'UnknownEventSource';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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 type { Constructor } from '@cratis/fundamentals';
|
|
6
|
+
import { DecoratorType, TypeDiscoverer } from '../types/index.js';
|
|
7
|
+
import type { ChronicleClassDecorator } from '../types/standardDecoratorMetadata.js';
|
|
8
|
+
import type { ConcurrencyDimensionFlags } from './ConcurrencyDimensions.js';
|
|
9
|
+
|
|
10
|
+
const EVENT_SOURCE_METADATA_KEY = 'chronicle:eventSource';
|
|
11
|
+
const EVENT_STREAMS_METADATA_KEY = 'chronicle:eventSource:streams';
|
|
12
|
+
let registrationCounter = 0;
|
|
13
|
+
|
|
14
|
+
/** Optional settings for {@link eventSource}. */
|
|
15
|
+
export interface EventSourceOptions {
|
|
16
|
+
/** The stable name. Defaults to the class name without a trailing `EventSource`. */
|
|
17
|
+
name?: string;
|
|
18
|
+
|
|
19
|
+
/** The description of the event source. */
|
|
20
|
+
description?: string;
|
|
21
|
+
|
|
22
|
+
/** The default dimensions that take part in concurrency checks; combine {@link ConcurrencyDimensions} values with `|`. */
|
|
23
|
+
concurrency?: ConcurrencyDimensionFlags;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Optional settings for {@link eventStream}. */
|
|
27
|
+
export interface EventStreamOptions {
|
|
28
|
+
/** The description of the stream. */
|
|
29
|
+
description?: string;
|
|
30
|
+
|
|
31
|
+
/** The dimensions that take part in concurrency checks for the stream. Falls back to the event source when omitted or none. */
|
|
32
|
+
concurrency?: ConcurrencyDimensionFlags;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Metadata recorded by {@link eventSource}. */
|
|
36
|
+
export interface EventSourceMetadata {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly description: string;
|
|
39
|
+
readonly concurrency: ConcurrencyDimensionFlags;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Metadata recorded by {@link eventStream}. */
|
|
43
|
+
export interface EventStreamMetadata {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
readonly concurrency: ConcurrencyDimensionFlags;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Declares a class as an event source definition, discovered and registered with the Kernel at startup.
|
|
51
|
+
* The definition is not tied to event types; an append routes through it with `AppendOptions.eventSource`.
|
|
52
|
+
* Supports legacy and standard class decorators.
|
|
53
|
+
* @param options - Optional name (defaults to the class name without a trailing `EventSource`), description and concurrency.
|
|
54
|
+
* @returns A class decorator.
|
|
55
|
+
*/
|
|
56
|
+
export function eventSource(options?: EventSourceOptions): ChronicleClassDecorator {
|
|
57
|
+
return ((target: Function, context?: ClassDecoratorContext) => {
|
|
58
|
+
let constructor = target;
|
|
59
|
+
const metadata: EventSourceMetadata = {
|
|
60
|
+
name: options?.name ?? defaultNameFor(constructor),
|
|
61
|
+
description: options?.description ?? '',
|
|
62
|
+
concurrency: options?.concurrency ?? 0
|
|
63
|
+
};
|
|
64
|
+
Reflect.defineMetadata(EVENT_SOURCE_METADATA_KEY, metadata, target);
|
|
65
|
+
TypeDiscoverer.default.register(DecoratorType.EventSource, constructor as Constructor, `${metadata.name}#${registrationCounter++}`);
|
|
66
|
+
context?.addInitializer(function () {
|
|
67
|
+
if (this !== constructor) {
|
|
68
|
+
constructor = this as Function;
|
|
69
|
+
Reflect.defineMetadata(EVENT_SOURCE_METADATA_KEY, metadata, constructor);
|
|
70
|
+
Reflect.defineMetadata(EVENT_STREAMS_METADATA_KEY, Reflect.getOwnMetadata(EVENT_STREAMS_METADATA_KEY, target), constructor);
|
|
71
|
+
TypeDiscoverer.default.register(DecoratorType.EventSource, constructor as Constructor, `${metadata.name}#${registrationCounter++}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}) as ChronicleClassDecorator;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Declares a stream of an event source. Repeat the decorator for several streams.
|
|
79
|
+
* Only takes effect on a class that is also decorated with {@link eventSource}.
|
|
80
|
+
* @param name - The stable name of the stream, which is the event stream type of appended events.
|
|
81
|
+
* @param options - Optional description and concurrency dimensions.
|
|
82
|
+
* @returns A class decorator.
|
|
83
|
+
*/
|
|
84
|
+
export function eventStream(name: string, options?: EventStreamOptions): ChronicleClassDecorator {
|
|
85
|
+
return ((target: Function) => {
|
|
86
|
+
const existing = (Reflect.getOwnMetadata(EVENT_STREAMS_METADATA_KEY, target) as EventStreamMetadata[] | undefined) ?? [];
|
|
87
|
+
// Decorators evaluate bottom-up; unshift keeps declaration order.
|
|
88
|
+
Reflect.defineMetadata(EVENT_STREAMS_METADATA_KEY, [
|
|
89
|
+
{ name, description: options?.description ?? '', concurrency: options?.concurrency ?? 0 },
|
|
90
|
+
...existing
|
|
91
|
+
], target);
|
|
92
|
+
}) as ChronicleClassDecorator;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gets the event source metadata declared on a class.
|
|
97
|
+
* @param target - The class constructor.
|
|
98
|
+
* @returns The metadata, or undefined when the class is not an event source.
|
|
99
|
+
*/
|
|
100
|
+
export function getEventSourceMetadata(target: Function): EventSourceMetadata | undefined {
|
|
101
|
+
return Reflect.getOwnMetadata(EVENT_SOURCE_METADATA_KEY, target) as EventSourceMetadata | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the streams declared on a class, in declaration order (duplicates are preserved so they can be rejected).
|
|
106
|
+
* @param target - The class constructor.
|
|
107
|
+
* @returns The declared streams.
|
|
108
|
+
*/
|
|
109
|
+
export function getEventStreamsFor(target: Function): ReadonlyArray<EventStreamMetadata> {
|
|
110
|
+
return (Reflect.getOwnMetadata(EVENT_STREAMS_METADATA_KEY, target) as EventStreamMetadata[] | undefined) ?? [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function defaultNameFor(type: Function): string {
|
|
114
|
+
const suffix = 'EventSource';
|
|
115
|
+
return type.name.length > suffix.length && type.name.endsWith(suffix) ? type.name.slice(0, -suffix.length) : type.name;
|
|
116
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ChronicleClient, ChronicleOptions, EventSequenceNumber, ConcurrencyDimensions, eventSource, eventStream, eventType,
|
|
8
|
+
type IClientArtifactsProvider, type IEventStore } from '../../../index.js';
|
|
9
|
+
|
|
10
|
+
const should = chai.should();
|
|
11
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
12
|
+
|
|
13
|
+
@eventType()
|
|
14
|
+
class FundsDeposited { @field(Number) amount = 0; }
|
|
15
|
+
|
|
16
|
+
@eventSource({ name: 'Account', description: 'A bank account', concurrency: ConcurrencyDimensions.eventSourceId })
|
|
17
|
+
@eventStream('Transactions', { description: 'Money movements' })
|
|
18
|
+
class AccountEventSource {}
|
|
19
|
+
|
|
20
|
+
@eventSource({ name: 'Customer' })
|
|
21
|
+
class CustomerEventSource {}
|
|
22
|
+
|
|
23
|
+
@eventSource({ name: 'Ledger' })
|
|
24
|
+
@eventStream('Monthly', { concurrency: ConcurrencyDimensions.eventSourceId | ConcurrencyDimensions.eventStreamId })
|
|
25
|
+
class LedgerEventSource {}
|
|
26
|
+
|
|
27
|
+
const artifacts: IClientArtifactsProvider = {
|
|
28
|
+
eventTypes: [FundsDeposited], eventSources: [AccountEventSource, CustomerEventSource, LedgerEventSource], readModels: [], reactors: [], reducers: [],
|
|
29
|
+
seeders: [], constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sequenceStart = EventSequenceNumber.first;
|
|
33
|
+
const deposit = (amount: number) => Object.assign(new FundsDeposited(), { amount });
|
|
34
|
+
|
|
35
|
+
describe.skipIf(!connectionString && !process.env.CI)('when appending through registered event sources against a kernel', () => {
|
|
36
|
+
const storeName = `Sources${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
37
|
+
let client: ChronicleClient;
|
|
38
|
+
let store: IEventStore;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, { discoveryPatterns: [], clientArtifactsProvider: artifacts }));
|
|
42
|
+
store = await client.getEventStore(storeName);
|
|
43
|
+
});
|
|
44
|
+
afterAll(() => client?.dispose());
|
|
45
|
+
|
|
46
|
+
// The sequence-number read does not narrow by route, so it also returns events routed through a definition.
|
|
47
|
+
const eventsFor = (id: string) => store.eventLog.getFromSequenceNumber(sequenceStart, id, [FundsDeposited]);
|
|
48
|
+
|
|
49
|
+
it('should register the definitions with the kernel', async () => {
|
|
50
|
+
const connection = (store as unknown as { _connection: { eventSources: { allEventSources(request: unknown): Promise<{ Data: Array<{ Name: string; Streams: Array<{ Name: string }> }> }> } } })._connection;
|
|
51
|
+
const response = await connection.eventSources.allEventSources({ EventStore: storeName });
|
|
52
|
+
const account = response.Data.find(_ => _.Name === 'Account');
|
|
53
|
+
should.exist(account);
|
|
54
|
+
account!.Streams.map(_ => _.Name).should.deep.equal(['Transactions']);
|
|
55
|
+
response.Data.some(_ => _.Name === 'Customer').should.equal(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should round-trip the event source and stream on appended events', async () => {
|
|
59
|
+
const id = randomUUID();
|
|
60
|
+
const result = await store.eventLog.append(id, deposit(10), { eventSource: AccountEventSource, eventStream: 'Transactions', streamId: 'tx-1' });
|
|
61
|
+
result.isSuccess.should.equal(true);
|
|
62
|
+
const [event] = await eventsFor(id);
|
|
63
|
+
event.context.eventSource!.should.equal('Account');
|
|
64
|
+
event.context.eventSourceType!.should.equal('Account');
|
|
65
|
+
event.context.eventStreamType!.should.equal('Transactions');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should leave the source unset for legacy appends', async () => {
|
|
69
|
+
const id = randomUUID();
|
|
70
|
+
await store.eventLog.append(id, deposit(1));
|
|
71
|
+
const [event] = await eventsFor(id);
|
|
72
|
+
should.equal(event.context.eventSource, undefined);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject a stale explicit scope on append-many and keep the batch atomic', async () => {
|
|
76
|
+
const id = randomUUID();
|
|
77
|
+
await store.eventLog.appendMany(id, [deposit(1), deposit(2)], { eventSource: 'Account' });
|
|
78
|
+
const stale = { sequenceNumber: 0n, eventSourceId: true };
|
|
79
|
+
const result = await store.eventLog.appendMany(id, [deposit(3)], { eventSource: 'Account', concurrencyScope: stale }).catch(error => error);
|
|
80
|
+
const events = await eventsFor(id);
|
|
81
|
+
events.should.have.length(2);
|
|
82
|
+
(result instanceof Error || result.some((appended: { isSuccess: boolean }) => !appended.isSuccess)).should.equal(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should route a mixed-source batch per event', async () => {
|
|
86
|
+
const account = randomUUID(); const customer = randomUUID();
|
|
87
|
+
await store.eventLog.appendMany([
|
|
88
|
+
{ eventSourceId: account, event: deposit(1) },
|
|
89
|
+
{ eventSourceId: customer, event: deposit(2), eventSource: CustomerEventSource }
|
|
90
|
+
], { eventSource: AccountEventSource });
|
|
91
|
+
const [accountEvent] = await eventsFor(account);
|
|
92
|
+
const [customerEvent] = await eventsFor(customer);
|
|
93
|
+
accountEvent.context.eventSource!.should.equal('Account');
|
|
94
|
+
customerEvent.context.eventSource!.should.equal('Customer');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should guard a shared event source id once and reject differing guards without writing', async () => {
|
|
98
|
+
const id = randomUUID();
|
|
99
|
+
const ledger = (streamId: string) => ({ eventSourceId: id, event: deposit(1), eventSource: LedgerEventSource, eventStream: 'Monthly', eventStreamId: streamId });
|
|
100
|
+
const shared = await store.eventLog.appendMany([ledger('2026-01'), ledger('2026-01')]);
|
|
101
|
+
shared.every(_ => _.isSuccess).should.equal(true);
|
|
102
|
+
const error = await store.eventLog.appendMany([ledger('2026-01'), ledger('2026-02')]).catch(e => e as Error);
|
|
103
|
+
(error instanceof Error).should.equal(true);
|
|
104
|
+
(await eventsFor(id)).should.have.length(2);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
6
|
+
import { EventSequence, EventSequenceId } from '../../eventSequences/index.js';
|
|
7
|
+
import { eventType } from '../../events/index.js';
|
|
8
|
+
import type { IUnitOfWorkManager } from '../../transactions/index.js';
|
|
9
|
+
import {
|
|
10
|
+
ConcurrencyDimensions, eventSource, eventStream, EventSources,
|
|
11
|
+
EventRoutingContradictsEventSource, EventStreamDoesNotBelongToEventSource, EventStreamRequiresEventSource, UnknownEventSource
|
|
12
|
+
} from '../index.js';
|
|
13
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
14
|
+
|
|
15
|
+
const should = chai.should();
|
|
16
|
+
class Deposited { constructor(readonly amount = 1) {} }
|
|
17
|
+
eventType('DepositedToAccount')(Deposited);
|
|
18
|
+
|
|
19
|
+
@eventSource({ concurrency: ConcurrencyDimensions.eventSourceId })
|
|
20
|
+
@eventStream('Transactions', { concurrency: ConcurrencyDimensions.eventSourceId | ConcurrencyDimensions.eventStreamType })
|
|
21
|
+
class AccountEventSource {}
|
|
22
|
+
|
|
23
|
+
@eventSource({ name: 'Customer' })
|
|
24
|
+
@eventStream('Profile')
|
|
25
|
+
class CustomerEventSource {}
|
|
26
|
+
|
|
27
|
+
@eventSource({ name: 'Plain' })
|
|
28
|
+
class PlainEventSource {}
|
|
29
|
+
|
|
30
|
+
async function create(tail = 7n) {
|
|
31
|
+
const append = vi.fn().mockResolvedValue({ Response: { SequenceNumber: 0n } });
|
|
32
|
+
const appendManyForEventSources = vi.fn().mockResolvedValue({ Response: { SequenceNumbers: [0n, 1n] } });
|
|
33
|
+
const tailSequenceNumber = vi.fn().mockResolvedValue({ Data: { SequenceNumber: tail } });
|
|
34
|
+
const connection = { eventSequences: { append, appendManyForEventSources, tailSequenceNumber } } as unknown as ChronicleConnection;
|
|
35
|
+
const sources = new EventSources('store', connection, { eventSources: [AccountEventSource, CustomerEventSource, PlainEventSource] } as unknown as IClientArtifactsProvider);
|
|
36
|
+
await sources.discover();
|
|
37
|
+
return { sequence: new EventSequence(EventSequenceId.eventLog, 'store', 'ns', connection, {} as IUnitOfWorkManager, undefined, undefined, sources), append, appendManyForEventSources, tailSequenceNumber };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('when appending through a definition and stream', () => {
|
|
41
|
+
let request: any; let tailRequest: any;
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
const c = await create();
|
|
44
|
+
await c.sequence.append('acc-1', new Deposited(), { eventSource: AccountEventSource, eventStream: 'Transactions', streamId: 'tx-1' });
|
|
45
|
+
request = c.append.mock.calls[0][0];
|
|
46
|
+
tailRequest = c.tailSequenceNumber.mock.calls[0][0];
|
|
47
|
+
});
|
|
48
|
+
it('should set the wire event source key and routing', () => {
|
|
49
|
+
request.EventSource.should.equal('Account');
|
|
50
|
+
request.EventSourceType.should.equal('Account');
|
|
51
|
+
request.EventStreamType.should.equal('Transactions');
|
|
52
|
+
request.EventStreamId.should.equal('tx-1');
|
|
53
|
+
});
|
|
54
|
+
it('should derive the scope from the stream dimensions', () => {
|
|
55
|
+
request.ConcurrencyScope.SequenceNumber.should.equal(7n);
|
|
56
|
+
request.ConcurrencyScope.EventSourceId.should.equal(true);
|
|
57
|
+
request.ConcurrencyScope.EventStreamType.should.equal('Transactions');
|
|
58
|
+
request.ConcurrencyScope.EventSourceType.should.equal('');
|
|
59
|
+
});
|
|
60
|
+
it('should read the tail with exactly the scoped dimensions', () => {
|
|
61
|
+
tailRequest.EventSourceId.should.equal('acc-1');
|
|
62
|
+
tailRequest.EventStreamType.should.equal('Transactions');
|
|
63
|
+
tailRequest.EventSourceType.should.equal('');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('when appending through a definition by name without a stream', () => {
|
|
68
|
+
it('should use the source dimensions and leave the stream to the kernel', async () => {
|
|
69
|
+
const c = await create();
|
|
70
|
+
await c.sequence.append('acc-1', new Deposited(), { eventSource: 'Account' });
|
|
71
|
+
const request = c.append.mock.calls[0][0];
|
|
72
|
+
request.EventSource.should.equal('Account');
|
|
73
|
+
should.equal(request.EventStreamType, undefined);
|
|
74
|
+
request.ConcurrencyScope.EventSourceId.should.equal(true);
|
|
75
|
+
request.ConcurrencyScope.EventStreamType.should.equal('');
|
|
76
|
+
});
|
|
77
|
+
it('should expect no matching event when the scope is empty', async () => {
|
|
78
|
+
const c = await create(18446744073709551615n);
|
|
79
|
+
await c.sequence.append('acc-1', new Deposited(), { eventSource: 'Account' });
|
|
80
|
+
c.append.mock.calls[0][0].ConcurrencyScope.ExpectsNoMatchingEvent.should.equal(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('when an explicit concurrency scope is given', () => {
|
|
85
|
+
it('should win over the definition and not read the tail', async () => {
|
|
86
|
+
const c = await create();
|
|
87
|
+
await c.sequence.append('acc-1', new Deposited(), { eventSource: 'Account', concurrencyScope: { sequenceNumber: 3n } });
|
|
88
|
+
c.append.mock.calls[0][0].ConcurrencyScope.SequenceNumber.should.equal(3n);
|
|
89
|
+
c.tailSequenceNumber.mock.calls.length.should.equal(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('when the definition declares no concurrency', () => {
|
|
94
|
+
it('should not add a scope', async () => {
|
|
95
|
+
const c = await create();
|
|
96
|
+
await c.sequence.append('c-1', new Deposited(), { eventSource: 'Plain' });
|
|
97
|
+
const request = c.append.mock.calls[0][0];
|
|
98
|
+
request.EventSource.should.equal('Plain');
|
|
99
|
+
request.ConcurrencyScope.SequenceNumber.should.equal(18446744073709551615n);
|
|
100
|
+
c.tailSequenceNumber.mock.calls.length.should.equal(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('when appending without a definition', () => {
|
|
105
|
+
it('should keep legacy behavior and send no event source key', async () => {
|
|
106
|
+
const c = await create();
|
|
107
|
+
await c.sequence.append('acc-1', new Deposited(), { sourceType: 'Legacy' });
|
|
108
|
+
const request = c.append.mock.calls[0][0];
|
|
109
|
+
should.equal(request.EventSource, undefined);
|
|
110
|
+
request.EventSourceType.should.equal('Legacy');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('when routing is invalid', () => {
|
|
115
|
+
it('should reject an unknown source', async () => {
|
|
116
|
+
const c = await create();
|
|
117
|
+
let error: unknown; await c.sequence.append('a', new Deposited(), { eventSource: 'Ghost' }).catch(e => error = e);
|
|
118
|
+
(error as Error).should.be.instanceOf(UnknownEventSource);
|
|
119
|
+
c.append.mock.calls.length.should.equal(0);
|
|
120
|
+
});
|
|
121
|
+
it('should reject an undeclared stream', async () => {
|
|
122
|
+
const c = await create();
|
|
123
|
+
let error: unknown; await c.sequence.append('a', new Deposited(), { eventSource: 'Account', eventStream: 'Profile' }).catch(e => error = e);
|
|
124
|
+
(error as Error).should.be.instanceOf(EventStreamDoesNotBelongToEventSource);
|
|
125
|
+
});
|
|
126
|
+
it('should reject a stream without a source', async () => {
|
|
127
|
+
const c = await create();
|
|
128
|
+
let error: unknown; await c.sequence.append('a', new Deposited(), { eventStream: 'Transactions' }).catch(e => error = e);
|
|
129
|
+
(error as Error).should.be.instanceOf(EventStreamRequiresEventSource);
|
|
130
|
+
});
|
|
131
|
+
it('should reject a contradicting explicit source type or stream type', async () => {
|
|
132
|
+
const c = await create();
|
|
133
|
+
const errors: unknown[] = [];
|
|
134
|
+
await c.sequence.append('a', new Deposited(), { eventSource: 'Account', sourceType: 'Other' }).catch(e => errors.push(e));
|
|
135
|
+
await c.sequence.append('a', new Deposited(), { eventSource: 'Account', eventStream: 'Transactions', streamType: 'Other' }).catch(e => errors.push(e));
|
|
136
|
+
errors.every(e => e instanceof EventRoutingContradictsEventSource).should.equal(true);
|
|
137
|
+
errors.should.have.length(2);
|
|
138
|
+
});
|
|
139
|
+
it('should accept explicit values that agree', async () => {
|
|
140
|
+
const c = await create();
|
|
141
|
+
await c.sequence.append('a', new Deposited(), { eventSource: 'Account', eventStream: 'Transactions', sourceType: 'Account', streamType: 'Transactions' });
|
|
142
|
+
c.append.mock.calls.length.should.equal(1);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('when appending many through a definition', () => {
|
|
147
|
+
it('should route every event and derive one scope per event source id', async () => {
|
|
148
|
+
const c = await create();
|
|
149
|
+
await c.sequence.appendMany('acc-1', [new Deposited(), new Deposited()], { eventSource: 'Account', eventStream: 'Transactions' });
|
|
150
|
+
const request = c.appendManyForEventSources.mock.calls[0][0];
|
|
151
|
+
request.Events.map((e: any) => e.EventSource).should.deep.equal(['Account', 'Account']);
|
|
152
|
+
request.Events.map((e: any) => e.EventStreamType).should.deep.equal(['Transactions', 'Transactions']);
|
|
153
|
+
request.ConcurrencyScopes.should.have.length(1);
|
|
154
|
+
request.ConcurrencyScopes[0].EventSourceId.should.equal('acc-1');
|
|
155
|
+
request.ConcurrencyScopes[0].Scope.SequenceNumber.should.equal(7n);
|
|
156
|
+
});
|
|
157
|
+
it('should let per-event routing override shared options in a mixed batch without inheriting the stream', async () => {
|
|
158
|
+
const c = await create();
|
|
159
|
+
await c.sequence.appendMany([
|
|
160
|
+
{ eventSourceId: 'acc-1', event: new Deposited() },
|
|
161
|
+
{ eventSourceId: 'cust-1', event: new Deposited(), eventSource: CustomerEventSource },
|
|
162
|
+
{ eventSourceId: 'cust-2', event: new Deposited(), eventSource: 'Customer', eventStream: 'Profile' },
|
|
163
|
+
{ eventSourceId: 'old-1', event: new Deposited(), eventSource: 'Plain' }
|
|
164
|
+
], { eventSource: 'Account', eventStream: 'Transactions' });
|
|
165
|
+
const events = c.appendManyForEventSources.mock.calls[0][0].Events;
|
|
166
|
+
events.map((e: any) => e.EventSource).should.deep.equal(['Account', 'Customer', 'Customer', 'Plain']);
|
|
167
|
+
events.map((e: any) => e.EventStreamType).should.deep.equal(['Transactions', undefined, 'Profile', undefined]);
|
|
168
|
+
events.map((e: any) => e.EventSourceType).should.deep.equal(['Account', 'Customer', 'Customer', 'Plain']);
|
|
169
|
+
});
|
|
170
|
+
it('should let an explicit per-id scope win and a shared scope win over derivation', async () => {
|
|
171
|
+
const c = await create();
|
|
172
|
+
await c.sequence.appendMany([
|
|
173
|
+
{ eventSourceId: 'acc-1', event: new Deposited() },
|
|
174
|
+
{ eventSourceId: 'acc-2', event: new Deposited() }
|
|
175
|
+
], { eventSource: 'Account', concurrencyScopes: { 'acc-1': { sequenceNumber: 1n } } });
|
|
176
|
+
const scopes = c.appendManyForEventSources.mock.calls[0][0].ConcurrencyScopes;
|
|
177
|
+
scopes.find((s: any) => s.EventSourceId === 'acc-1').Scope.SequenceNumber.should.equal(1n);
|
|
178
|
+
scopes.find((s: any) => s.EventSourceId === 'acc-2').Scope.SequenceNumber.should.equal(7n);
|
|
179
|
+
});
|
|
180
|
+
it('should send nothing when any event routing is invalid (atomic)', async () => {
|
|
181
|
+
const c = await create();
|
|
182
|
+
let error: unknown;
|
|
183
|
+
await c.sequence.appendMany([
|
|
184
|
+
{ eventSourceId: 'a', event: new Deposited() },
|
|
185
|
+
{ eventSourceId: 'b', event: new Deposited(), eventSource: 'Ghost' }
|
|
186
|
+
], { eventSource: 'Account' }).catch(e => error = e);
|
|
187
|
+
(error as Error).should.be.instanceOf(UnknownEventSource);
|
|
188
|
+
c.appendManyForEventSources.mock.calls.length.should.equal(0);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
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 { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
6
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
7
|
+
import { EventSequence, EventSequenceId } from '../../eventSequences/index.js';
|
|
8
|
+
import { eventType } from '../../events/index.js';
|
|
9
|
+
import { UnitOfWork } from '../../transactions/UnitOfWork.js';
|
|
10
|
+
import type { IUnitOfWorkManager } from '../../transactions/index.js';
|
|
11
|
+
import { ConcurrencyDimensions, eventSource, eventStream, EventSources } from '../index.js';
|
|
12
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
13
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
14
|
+
|
|
15
|
+
chai.should();
|
|
16
|
+
class Booked { constructor(readonly amount = 1) {} }
|
|
17
|
+
eventType('BookedToLedger')(Booked);
|
|
18
|
+
|
|
19
|
+
const sourceAndStreamId = ConcurrencyDimensions.eventSourceId | ConcurrencyDimensions.eventStreamType | ConcurrencyDimensions.eventStreamId;
|
|
20
|
+
|
|
21
|
+
@eventSource({ name: 'Ledger' })
|
|
22
|
+
@eventStream('Monthly', { concurrency: sourceAndStreamId })
|
|
23
|
+
class LedgerEventSource {}
|
|
24
|
+
|
|
25
|
+
@eventSource({ name: 'Archive', concurrency: ConcurrencyDimensions.eventSourceId })
|
|
26
|
+
class ArchiveEventSource {}
|
|
27
|
+
|
|
28
|
+
@eventSource({ name: 'Mirror' })
|
|
29
|
+
@eventStream('Monthly', { concurrency: sourceAndStreamId })
|
|
30
|
+
class MirrorEventSource {}
|
|
31
|
+
|
|
32
|
+
@eventSource({ name: 'Open' })
|
|
33
|
+
class OpenEventSource {}
|
|
34
|
+
|
|
35
|
+
@eventSource({ name: 'Routed', concurrency: ConcurrencyDimensions.eventSourceId })
|
|
36
|
+
class RoutedEventSource {}
|
|
37
|
+
|
|
38
|
+
async function create() {
|
|
39
|
+
const appendManyForEventSources = vi.fn().mockResolvedValue({ Response: { SequenceNumbers: [0n, 1n] } });
|
|
40
|
+
const tailSequenceNumber = vi.fn().mockResolvedValue({ Data: { SequenceNumber: 5n } });
|
|
41
|
+
const connection = { eventSequences: { appendManyForEventSources, tailSequenceNumber } } as unknown as ChronicleConnection;
|
|
42
|
+
const sources = new EventSources('store', connection, {
|
|
43
|
+
eventSources: [LedgerEventSource, ArchiveEventSource, MirrorEventSource, OpenEventSource, RoutedEventSource]
|
|
44
|
+
} as unknown as IClientArtifactsProvider);
|
|
45
|
+
await sources.discover();
|
|
46
|
+
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'ns', connection, {} as IUnitOfWorkManager, undefined, undefined, sources);
|
|
47
|
+
return { sequence, appendManyForEventSources, tailSequenceNumber, sources };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ledger = (streamId: string, eventSourceId = 'a-1') =>
|
|
51
|
+
({ eventSourceId, event: new Booked(), eventSource: LedgerEventSource, eventStream: 'Monthly', eventStreamId: streamId });
|
|
52
|
+
|
|
53
|
+
describe('when a batch needs differing stream id guards for one event source id', () => {
|
|
54
|
+
it('should throw before any wire write', async () => {
|
|
55
|
+
const c = await create();
|
|
56
|
+
let error: Error | undefined;
|
|
57
|
+
try { await c.sequence.appendMany([ledger('2026-01'), ledger('2026-02')]); } catch (e) { error = e as Error; }
|
|
58
|
+
(error?.message ?? '').should.contain("'a-1'").and.contain('separate batches');
|
|
59
|
+
c.appendManyForEventSources.mock.calls.length.should.equal(0);
|
|
60
|
+
c.tailSequenceNumber.mock.calls.length.should.equal(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('when a batch has the same stream id guard repeated', () => {
|
|
65
|
+
it('should send one shared scope for the id', async () => {
|
|
66
|
+
const c = await create();
|
|
67
|
+
await c.sequence.appendMany([ledger('2026-01'), ledger('2026-01')]);
|
|
68
|
+
const scopes = c.appendManyForEventSources.mock.calls[0][0].ConcurrencyScopes;
|
|
69
|
+
scopes.length.should.equal(1);
|
|
70
|
+
scopes[0].Scope.EventStreamId.should.equal('2026-01');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('when a batch needs differing guards from different definitions for one id', () => {
|
|
75
|
+
it('should throw before any wire write', async () => {
|
|
76
|
+
const c = await create();
|
|
77
|
+
let error: Error | undefined;
|
|
78
|
+
try {
|
|
79
|
+
await c.sequence.appendMany([ledger('2026-01'), { eventSourceId: 'a-1', event: new Booked(), eventSource: ArchiveEventSource }]);
|
|
80
|
+
} catch (e) { error = e as Error; }
|
|
81
|
+
(error?.message ?? '').should.contain('differing concurrency guards');
|
|
82
|
+
c.appendManyForEventSources.mock.calls.length.should.equal(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('when equivalent guards differ only in values the dimensions do not select', () => {
|
|
87
|
+
it('should share one conservative scope', async () => {
|
|
88
|
+
const c = await create();
|
|
89
|
+
await c.sequence.appendMany([
|
|
90
|
+
{ eventSourceId: 'a-1', event: new Booked(), eventSource: ArchiveEventSource, eventStreamId: 'x' },
|
|
91
|
+
{ eventSourceId: 'a-1', event: new Booked(), eventSource: ArchiveEventSource, eventStreamId: 'y' }
|
|
92
|
+
]);
|
|
93
|
+
const scopes = c.appendManyForEventSources.mock.calls[0][0].ConcurrencyScopes;
|
|
94
|
+
scopes.length.should.equal(1);
|
|
95
|
+
scopes[0].Scope.EventSourceId.should.equal(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('when an unguarded definition event precedes a guarded one for the same id', () => {
|
|
100
|
+
it('should still guard the id', async () => {
|
|
101
|
+
const c = await create();
|
|
102
|
+
await c.sequence.appendMany([
|
|
103
|
+
{ eventSourceId: 'a-1', event: new Booked(), eventSource: OpenEventSource },
|
|
104
|
+
{ eventSourceId: 'a-1', event: new Booked(), eventSource: ArchiveEventSource }
|
|
105
|
+
]);
|
|
106
|
+
const scopes = c.appendManyForEventSources.mock.calls[0][0].ConcurrencyScopes;
|
|
107
|
+
scopes[0].Scope.EventSourceId.should.equal(true);
|
|
108
|
+
scopes[0].Scope.SequenceNumber.should.equal(5n);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('when an explicit broader scope exists for the id', () => {
|
|
113
|
+
it('should send it and not require agreement between the derived guards', async () => {
|
|
114
|
+
const c = await create();
|
|
115
|
+
await c.sequence.appendMany([ledger('2026-01'), ledger('2026-02')],
|
|
116
|
+
{ concurrencyScopes: { 'a-1': { sequenceNumber: 3n, eventSourceId: true } } });
|
|
117
|
+
const scopes = c.appendManyForEventSources.mock.calls[0][0].ConcurrencyScopes;
|
|
118
|
+
scopes.length.should.equal(1);
|
|
119
|
+
scopes[0].Scope.SequenceNumber.should.equal(3n);
|
|
120
|
+
c.tailSequenceNumber.mock.calls.length.should.equal(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('when a unit of work holds a conflicting group after a valid group', () => {
|
|
125
|
+
it('should reject on commit before any group is written', async () => {
|
|
126
|
+
const c = await create();
|
|
127
|
+
const appendMany = vi.fn().mockResolvedValue([]);
|
|
128
|
+
const store = { eventSources: c.sources, getEventSequence: () => ({ appendMany }) } as unknown as IEventStore;
|
|
129
|
+
const unitOfWork = new UnitOfWork(Guid.create(), () => {}, store);
|
|
130
|
+
unitOfWork.addEvent(EventSequenceId.eventLog, 'ok-1', new Booked(), { eventSource: ArchiveEventSource });
|
|
131
|
+
const other = new EventSequenceId('other');
|
|
132
|
+
unitOfWork.addEvent(other, 'a-1', new Booked(), { eventSource: LedgerEventSource, eventStream: 'Monthly' });
|
|
133
|
+
unitOfWork.addEvent(other, 'a-1', new Booked(), { eventSource: ArchiveEventSource });
|
|
134
|
+
let error: Error | undefined;
|
|
135
|
+
try { await unitOfWork.commit(); } catch (e) { error = e as Error; }
|
|
136
|
+
(error?.message ?? '').should.contain('differing concurrency guards');
|
|
137
|
+
appendMany.mock.calls.length.should.equal(0);
|
|
138
|
+
});
|
|
139
|
+
});
|