@cratis/chronicle 3.0.0 → 3.1.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 +17 -1
- package/IEventStore.ts +16 -0
- package/auditing/CausationType.ts +10 -0
- package/compliance/ComplianceContracts.ts +84 -0
- package/compliance/IPIIManager.ts +17 -0
- package/compliance/PIIManager.ts +31 -0
- package/compliance/index.ts +2 -0
- package/connection/ChronicleConnection.ts +7 -1
- package/connection/ChronicleServices.ts +2 -0
- package/connection/Guid.ts +18 -0
- package/dist/EventStore.d.ts +8 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +13 -1
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +12 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/auditing/CausationType.d.ts +8 -0
- package/dist/auditing/CausationType.d.ts.map +1 -1
- package/dist/auditing/CausationType.js +8 -0
- package/dist/auditing/CausationType.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +40 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +71 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +15 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -0
- package/dist/compliance/IPIIManager.js +4 -0
- package/dist/compliance/IPIIManager.js.map +1 -0
- package/dist/compliance/PIIManager.d.ts +20 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -0
- package/dist/compliance/PIIManager.js +30 -0
- package/dist/compliance/PIIManager.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +1 -0
- package/dist/compliance/index.js.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 +6 -1
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +2 -0
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/connection/Guid.d.ts +5 -0
- package/dist/connection/Guid.d.ts.map +1 -1
- package/dist/connection/Guid.js +15 -0
- package/dist/connection/Guid.js.map +1 -1
- package/dist/eventSequences/AppendOperationsBroadcaster.d.ts +16 -0
- package/dist/eventSequences/AppendOperationsBroadcaster.d.ts.map +1 -0
- package/dist/eventSequences/AppendOperationsBroadcaster.js +77 -0
- package/dist/eventSequences/AppendOperationsBroadcaster.js.map +1 -0
- package/dist/eventSequences/AppendOptions.d.ts +7 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/AppendResult.d.ts +14 -0
- package/dist/eventSequences/AppendResult.d.ts.map +1 -1
- package/dist/eventSequences/AppendedEventWithResult.d.ts +13 -0
- package/dist/eventSequences/AppendedEventWithResult.d.ts.map +1 -0
- package/dist/eventSequences/AppendedEventWithResult.js +4 -0
- package/dist/eventSequences/AppendedEventWithResult.js.map +1 -0
- package/dist/eventSequences/CompleteStreamError.d.ts +17 -0
- package/dist/eventSequences/CompleteStreamError.d.ts.map +1 -0
- package/dist/eventSequences/CompleteStreamError.js +20 -0
- package/dist/eventSequences/CompleteStreamError.js.map +1 -0
- package/dist/eventSequences/CompleteStreamResult.d.ts +14 -0
- package/dist/eventSequences/CompleteStreamResult.d.ts.map +1 -0
- package/dist/eventSequences/CompleteStreamResult.js +4 -0
- package/dist/eventSequences/CompleteStreamResult.js.map +1 -0
- package/dist/eventSequences/ConcurrencyViolation.d.ts +13 -0
- package/dist/eventSequences/ConcurrencyViolation.d.ts.map +1 -0
- package/dist/eventSequences/ConcurrencyViolation.js +4 -0
- package/dist/eventSequences/ConcurrencyViolation.js.map +1 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts +8 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +35 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +350 -19
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/IEventSequence.d.ts +85 -1
- package/dist/eventSequences/IEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/WaitForCompletionResult.d.ts +11 -0
- package/dist/eventSequences/WaitForCompletionResult.d.ts.map +1 -0
- package/dist/eventSequences/WaitForCompletionResult.js +4 -0
- package/dist/eventSequences/WaitForCompletionResult.js.map +1 -0
- package/dist/eventSequences/index.d.ts +5 -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/eventStoreSubscriptions/EventStoreSubscriptions.d.ts +3 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +9 -0
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.d.ts +6 -0
- package/dist/eventStoreSubscriptions/IEventStoreSubscriptions.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/contracts.d.ts +18 -0
- package/dist/eventStoreSubscriptions/contracts.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/contracts.js +106 -0
- package/dist/eventStoreSubscriptions/contracts.js.map +1 -1
- package/dist/events/constraints/Constraints.js +1 -1
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/externalServices/ExternalServiceBuilder.d.ts +42 -0
- package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -0
- package/dist/externalServices/ExternalServiceBuilder.js +110 -0
- package/dist/externalServices/ExternalServiceBuilder.js.map +1 -0
- package/dist/externalServices/ExternalServices.d.ts +19 -0
- package/dist/externalServices/ExternalServices.d.ts.map +1 -0
- package/dist/externalServices/ExternalServices.js +30 -0
- package/dist/externalServices/ExternalServices.js.map +1 -0
- package/dist/externalServices/ExternalServicesContracts.d.ts +137 -0
- package/dist/externalServices/ExternalServicesContracts.d.ts.map +1 -0
- package/dist/externalServices/ExternalServicesContracts.js +654 -0
- package/dist/externalServices/ExternalServicesContracts.js.map +1 -0
- package/dist/externalServices/IExternalServiceBuilder.d.ts +67 -0
- package/dist/externalServices/IExternalServiceBuilder.d.ts.map +1 -0
- package/dist/externalServices/IExternalServiceBuilder.js +4 -0
- package/dist/externalServices/IExternalServiceBuilder.js.map +1 -0
- package/dist/externalServices/IExternalServices.d.ts +13 -0
- package/dist/externalServices/IExternalServices.d.ts.map +1 -0
- package/dist/externalServices/IExternalServices.js +4 -0
- package/dist/externalServices/IExternalServices.js.map +1 -0
- package/dist/externalServices/index.d.ts +5 -0
- package/dist/externalServices/index.d.ts.map +1 -0
- package/dist/externalServices/index.js +5 -0
- package/dist/externalServices/index.js.map +1 -0
- package/dist/identities/IIdentityManager.d.ts +17 -0
- package/dist/identities/IIdentityManager.d.ts.map +1 -0
- package/dist/identities/IIdentityManager.js +4 -0
- package/dist/identities/IIdentityManager.js.map +1 -0
- package/dist/identities/IdentityManager.d.ts +20 -0
- package/dist/identities/IdentityManager.d.ts.map +1 -0
- package/dist/identities/IdentityManager.js +31 -0
- package/dist/identities/IdentityManager.js.map +1 -0
- package/dist/identities/index.d.ts +3 -0
- package/dist/identities/index.d.ts.map +1 -0
- package/dist/identities/index.js +4 -0
- package/dist/identities/index.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/observation/FailedPartition.d.ts +15 -0
- package/dist/observation/FailedPartition.d.ts.map +1 -0
- package/dist/observation/FailedPartition.js +4 -0
- package/dist/observation/FailedPartition.js.map +1 -0
- package/dist/observation/FailedPartitionAttempt.d.ts +15 -0
- package/dist/observation/FailedPartitionAttempt.d.ts.map +1 -0
- package/dist/observation/FailedPartitionAttempt.js +4 -0
- package/dist/observation/FailedPartitionAttempt.js.map +1 -0
- package/dist/observation/FailedPartitions.d.ts +24 -0
- package/dist/observation/FailedPartitions.d.ts.map +1 -0
- package/dist/observation/FailedPartitions.js +39 -0
- package/dist/observation/FailedPartitions.js.map +1 -0
- package/dist/observation/ICanBeNotifiedWhenPartitionReplayed.d.ts +19 -0
- package/dist/observation/ICanBeNotifiedWhenPartitionReplayed.d.ts.map +1 -0
- package/dist/observation/ICanBeNotifiedWhenPartitionReplayed.js +4 -0
- package/dist/observation/ICanBeNotifiedWhenPartitionReplayed.js.map +1 -0
- package/dist/observation/ICanBeNotifiedWhenReplay.d.ts +17 -0
- package/dist/observation/ICanBeNotifiedWhenReplay.d.ts.map +1 -0
- package/dist/observation/ICanBeNotifiedWhenReplay.js +4 -0
- package/dist/observation/ICanBeNotifiedWhenReplay.js.map +1 -0
- package/dist/observation/IFailedPartitions.d.ts +18 -0
- package/dist/observation/IFailedPartitions.d.ts.map +1 -0
- package/dist/observation/IFailedPartitions.js +4 -0
- package/dist/observation/IFailedPartitions.js.map +1 -0
- package/dist/observation/index.d.ts +6 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/notifyReplayLifecycle.d.ts +13 -0
- package/dist/observation/notifyReplayLifecycle.d.ts.map +1 -0
- package/dist/observation/notifyReplayLifecycle.js +54 -0
- package/dist/observation/notifyReplayLifecycle.js.map +1 -0
- package/dist/observation/toClientFailedPartition.d.ts +9 -0
- package/dist/observation/toClientFailedPartition.d.ts.map +1 -0
- package/dist/observation/toClientFailedPartition.js +23 -0
- package/dist/observation/toClientFailedPartition.js.map +1 -0
- package/dist/projections/Projections.d.ts +0 -4
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +25 -103
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/declarative/AddBuilder.d.ts +17 -0
- package/dist/projections/declarative/AddBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/AddBuilder.js +28 -0
- package/dist/projections/declarative/AddBuilder.js.map +1 -0
- package/dist/projections/declarative/AddChildBuilder.d.ts +56 -0
- package/dist/projections/declarative/AddChildBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/AddChildBuilder.js +72 -0
- package/dist/projections/declarative/AddChildBuilder.js.map +1 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts +28 -0
- package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/ChildrenBuilder.js +72 -0
- package/dist/projections/declarative/ChildrenBuilder.js.map +1 -0
- package/dist/projections/declarative/CompositeKeyBuilder.d.ts +16 -0
- package/dist/projections/declarative/CompositeKeyBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/CompositeKeyBuilder.js +29 -0
- package/dist/projections/declarative/CompositeKeyBuilder.js.map +1 -0
- package/dist/projections/declarative/FromBuilder.d.ts +8 -6
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +44 -14
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/ICompositeKeyBuilder.d.ts +5 -0
- package/dist/projections/declarative/ICompositeKeyBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts +7 -5
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +50 -16
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/NestedBuilder.d.ts +24 -0
- package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/NestedBuilder.js +60 -0
- package/dist/projections/declarative/NestedBuilder.js.map +1 -0
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts +127 -0
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -0
- package/dist/projections/declarative/ProjectionBuilderCore.js +180 -0
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -32
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +24 -118
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/SubtractBuilder.d.ts +17 -0
- package/dist/projections/declarative/SubtractBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/SubtractBuilder.js +28 -0
- package/dist/projections/declarative/SubtractBuilder.js.map +1 -0
- package/dist/projections/declarative/index.d.ts +9 -0
- package/dist/projections/declarative/index.d.ts.map +1 -1
- package/dist/projections/declarative/index.js +8 -0
- package/dist/projections/declarative/index.js.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +90 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +290 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -0
- package/dist/reactors/ReactorSideEffects.d.ts +29 -0
- package/dist/reactors/ReactorSideEffects.d.ts.map +1 -0
- package/dist/reactors/ReactorSideEffects.js +70 -0
- package/dist/reactors/ReactorSideEffects.js.map +1 -0
- package/dist/reactors/Reactors.d.ts +4 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +23 -4
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +15 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/reducer.d.ts +9 -1
- package/dist/reducers/reducer.d.ts.map +1 -1
- package/dist/reducers/reducer.js +4 -2
- package/dist/reducers/reducer.js.map +1 -1
- package/dist/transactions/IUnitOfWork.d.ts +18 -0
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.d.ts +9 -0
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +14 -0
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOperationsBroadcaster.ts +83 -0
- package/eventSequences/AppendOptions.ts +8 -0
- package/eventSequences/AppendResult.ts +16 -0
- package/eventSequences/AppendedEventWithResult.ts +17 -0
- package/eventSequences/CompleteStreamError.ts +20 -0
- package/eventSequences/CompleteStreamResult.ts +13 -0
- package/eventSequences/ConcurrencyViolation.ts +18 -0
- package/eventSequences/EventForEventSourceId.ts +12 -0
- package/eventSequences/EventSequence.spec.ts +588 -0
- package/eventSequences/EventSequence.ts +406 -20
- package/eventSequences/IEventSequence.ts +109 -1
- package/eventSequences/WaitForCompletionResult.ts +15 -0
- package/eventSequences/index.ts +5 -0
- package/eventStoreSubscriptions/EventStoreSubscriptions.spec.ts +52 -0
- package/eventStoreSubscriptions/EventStoreSubscriptions.ts +19 -1
- package/eventStoreSubscriptions/IEventStoreSubscriptions.ts +7 -0
- package/eventStoreSubscriptions/contracts.ts +126 -0
- package/events/constraints/Constraints.ts +1 -1
- package/externalServices/ExternalServiceBuilder.ts +128 -0
- package/externalServices/ExternalServices.ts +34 -0
- package/externalServices/ExternalServicesContracts.ts +709 -0
- package/externalServices/IExternalServiceBuilder.ts +76 -0
- package/externalServices/IExternalServices.ts +16 -0
- package/externalServices/index.ts +7 -0
- package/identities/IIdentityManager.ts +19 -0
- package/identities/IdentityManager.ts +32 -0
- package/identities/index.ts +5 -0
- package/index.ts +6 -0
- package/observation/FailedPartition.ts +21 -0
- package/observation/FailedPartitionAttempt.ts +21 -0
- package/observation/FailedPartitions.spec.ts +75 -0
- package/observation/FailedPartitions.ts +44 -0
- package/observation/ICanBeNotifiedWhenPartitionReplayed.ts +22 -0
- package/observation/ICanBeNotifiedWhenReplay.ts +20 -0
- package/observation/IFailedPartitions.ts +22 -0
- package/observation/index.ts +6 -0
- package/observation/notifyReplayLifecycle.spec.ts +98 -0
- package/observation/notifyReplayLifecycle.ts +61 -0
- package/observation/toClientFailedPartition.ts +26 -0
- package/package.json +12 -2
- package/projections/Projections.childrenAndNested.spec.ts +188 -0
- package/projections/Projections.spec.ts +131 -0
- package/projections/Projections.ts +37 -119
- package/projections/declarative/AddBuilder.ts +28 -0
- package/projections/declarative/AddChildBuilder.ts +97 -0
- package/projections/declarative/ChildrenBuilder.spec.ts +157 -0
- package/projections/declarative/ChildrenBuilder.ts +90 -0
- package/projections/declarative/CompositeKeyBuilder.spec.ts +117 -0
- package/projections/declarative/CompositeKeyBuilder.ts +38 -0
- package/projections/declarative/FromBuilder.ts +60 -15
- package/projections/declarative/ICompositeKeyBuilder.ts +6 -0
- package/projections/declarative/JoinBuilder.ts +66 -17
- package/projections/declarative/NestedBuilder.ts +76 -0
- package/projections/declarative/ProjectionBuilderCore.ts +257 -0
- package/projections/declarative/ProjectionBuilderFor.spec.ts +115 -0
- package/projections/declarative/ProjectionBuilderFor.ts +29 -168
- package/projections/declarative/SubtractBuilder.ts +28 -0
- package/projections/declarative/index.ts +9 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +354 -0
- package/reactors/ReactorSideEffects.spec.ts +182 -0
- package/reactors/ReactorSideEffects.ts +97 -0
- package/reactors/Reactors.ts +29 -4
- package/reducers/Reducers.spec.ts +87 -0
- package/reducers/Reducers.ts +15 -3
- package/reducers/reducer.spec.ts +40 -0
- package/reducers/reducer.ts +11 -2
- package/transactions/IUnitOfWork.ts +21 -0
- package/transactions/UnitOfWork.spec.ts +82 -0
- package/transactions/UnitOfWork.ts +20 -0
|
@@ -0,0 +1,182 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { IEventLog } from '../eventSequences/IEventLog';
|
|
6
|
+
import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber';
|
|
7
|
+
import { eventType } from '../events/eventTypeDecorator';
|
|
8
|
+
import { appendReactorSideEffects } from './ReactorSideEffects';
|
|
9
|
+
|
|
10
|
+
class SomethingHappened {
|
|
11
|
+
constructor(readonly value: string = '') {}
|
|
12
|
+
}
|
|
13
|
+
eventType('c3f6a2f0-6f2f-4a3c-9d3f-6f2f4a3c9d3h')(SomethingHappened);
|
|
14
|
+
|
|
15
|
+
class SomethingElseHappened {
|
|
16
|
+
constructor(readonly value: string = '') {}
|
|
17
|
+
}
|
|
18
|
+
eventType('d3f6a2f0-6f2f-4a3c-9d3f-6f2f4a3c9d3i')(SomethingElseHappened);
|
|
19
|
+
|
|
20
|
+
function createEventLog(appendManyResult?: unknown) {
|
|
21
|
+
const appendMany = vi.fn().mockResolvedValue(appendManyResult ?? [
|
|
22
|
+
{ sequenceNumber: new EventSequenceNumber(1n), constraintViolations: [], errors: [], isSuccess: true }
|
|
23
|
+
]);
|
|
24
|
+
const eventLog = { appendMany } as unknown as IEventLog;
|
|
25
|
+
return { eventLog, appendMany };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const successResultFor = (count: number) =>
|
|
29
|
+
Array.from({ length: count }, (_, index) => ({
|
|
30
|
+
sequenceNumber: new EventSequenceNumber(BigInt(index + 1)),
|
|
31
|
+
constraintViolations: [],
|
|
32
|
+
errors: [],
|
|
33
|
+
isSuccess: true
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe('appendReactorSideEffects', () => {
|
|
37
|
+
describe('when the handler returns undefined', () => {
|
|
38
|
+
const { eventLog, appendMany } = createEventLog();
|
|
39
|
+
|
|
40
|
+
it('should not append anything', async () => {
|
|
41
|
+
const result = await appendReactorSideEffects(eventLog, undefined, 'triggering-source', 'Default', 'triggering-source');
|
|
42
|
+
|
|
43
|
+
expect(appendMany).not.toHaveBeenCalled();
|
|
44
|
+
expect(result.isSuccess).toBe(true);
|
|
45
|
+
expect(result.errors).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('when the handler returns a single bare event', () => {
|
|
50
|
+
const { eventLog, appendMany } = createEventLog(successResultFor(1));
|
|
51
|
+
|
|
52
|
+
it('should append it targeting the triggering event\'s event source, stream, and subject', async () => {
|
|
53
|
+
const result = await appendReactorSideEffects(
|
|
54
|
+
eventLog,
|
|
55
|
+
new SomethingHappened('a'),
|
|
56
|
+
'triggering-source',
|
|
57
|
+
'custom-stream-type',
|
|
58
|
+
'custom-stream-id');
|
|
59
|
+
|
|
60
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
61
|
+
const [events] = appendMany.mock.calls[0];
|
|
62
|
+
expect(events).toEqual([{
|
|
63
|
+
eventSourceId: 'triggering-source',
|
|
64
|
+
event: expect.any(SomethingHappened),
|
|
65
|
+
eventStreamType: 'custom-stream-type',
|
|
66
|
+
eventStreamId: 'custom-stream-id',
|
|
67
|
+
subject: 'triggering-source'
|
|
68
|
+
}]);
|
|
69
|
+
expect(result.isSuccess).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('when the handler returns an array of bare events', () => {
|
|
74
|
+
const { eventLog, appendMany } = createEventLog(successResultFor(2));
|
|
75
|
+
|
|
76
|
+
it('should append all of them in a single atomic AppendMany call', async () => {
|
|
77
|
+
await appendReactorSideEffects(
|
|
78
|
+
eventLog,
|
|
79
|
+
[new SomethingHappened('a'), new SomethingElseHappened('b')],
|
|
80
|
+
'triggering-source',
|
|
81
|
+
'Default',
|
|
82
|
+
'triggering-source');
|
|
83
|
+
|
|
84
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
85
|
+
const [events] = appendMany.mock.calls[0];
|
|
86
|
+
expect(events).toHaveLength(2);
|
|
87
|
+
expect(events[0].event).toBeInstanceOf(SomethingHappened);
|
|
88
|
+
expect(events[1].event).toBeInstanceOf(SomethingElseHappened);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('when the handler returns a single EventForEventSourceId', () => {
|
|
93
|
+
const { eventLog, appendMany } = createEventLog(successResultFor(1));
|
|
94
|
+
|
|
95
|
+
it('should append it using its own target rather than the triggering event\'s', async () => {
|
|
96
|
+
const wrapper = {
|
|
97
|
+
eventSourceId: 'other-source',
|
|
98
|
+
event: new SomethingHappened('a'),
|
|
99
|
+
eventStreamType: 'other-stream-type',
|
|
100
|
+
eventStreamId: 'other-stream-id',
|
|
101
|
+
subject: 'other-subject'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await appendReactorSideEffects(eventLog, wrapper, 'triggering-source', 'Default', 'triggering-source');
|
|
105
|
+
|
|
106
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
107
|
+
const [events] = appendMany.mock.calls[0];
|
|
108
|
+
expect(events).toEqual([wrapper]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('when the handler returns an array of EventForEventSourceId', () => {
|
|
113
|
+
const { eventLog, appendMany } = createEventLog(successResultFor(2));
|
|
114
|
+
|
|
115
|
+
it('should append all of them, each keeping its own target, in one atomic call', async () => {
|
|
116
|
+
const wrappers = [
|
|
117
|
+
{ eventSourceId: 'source-1', event: new SomethingHappened('a') },
|
|
118
|
+
{ eventSourceId: 'source-2', event: new SomethingElseHappened('b') }
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
await appendReactorSideEffects(eventLog, wrappers, 'triggering-source', 'Default', 'triggering-source');
|
|
122
|
+
|
|
123
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
124
|
+
const [events] = appendMany.mock.calls[0];
|
|
125
|
+
expect(events).toEqual(wrappers);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('when the handler returns a mix of bare events and EventForEventSourceId', () => {
|
|
130
|
+
const { eventLog, appendMany } = createEventLog(successResultFor(2));
|
|
131
|
+
|
|
132
|
+
it('should append the bare event using the triggering context and the wrapper using its own, in one atomic call', async () => {
|
|
133
|
+
const wrapper = { eventSourceId: 'other-source', event: new SomethingElseHappened('b') };
|
|
134
|
+
const bareEvent = new SomethingHappened('a');
|
|
135
|
+
|
|
136
|
+
await appendReactorSideEffects(eventLog, [bareEvent, wrapper], 'triggering-source', 'Default', 'triggering-source');
|
|
137
|
+
|
|
138
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
139
|
+
const [events] = appendMany.mock.calls[0];
|
|
140
|
+
expect(events).toEqual([
|
|
141
|
+
{
|
|
142
|
+
eventSourceId: 'triggering-source',
|
|
143
|
+
event: bareEvent,
|
|
144
|
+
eventStreamType: 'Default',
|
|
145
|
+
eventStreamId: 'triggering-source',
|
|
146
|
+
subject: 'triggering-source'
|
|
147
|
+
},
|
|
148
|
+
wrapper
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('when a side-effect event fails to append', () => {
|
|
154
|
+
const { eventLog, appendMany } = createEventLog([
|
|
155
|
+
{
|
|
156
|
+
sequenceNumber: EventSequenceNumber.unset,
|
|
157
|
+
constraintViolations: [{ constraintId: 'unique', message: 'Value must be unique', details: {} }],
|
|
158
|
+
errors: [],
|
|
159
|
+
isSuccess: false
|
|
160
|
+
}
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
it('should report failure with the constraint violation message', async () => {
|
|
164
|
+
const result = await appendReactorSideEffects(eventLog, new SomethingHappened('a'), 'triggering-source', 'Default', 'triggering-source');
|
|
165
|
+
|
|
166
|
+
expect(appendMany).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(result.isSuccess).toBe(false);
|
|
168
|
+
expect(result.errors).toEqual(['Value must be unique']);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('when the handler returns a plain object that is not a known event type or EventForEventSourceId', () => {
|
|
173
|
+
const { eventLog, appendMany } = createEventLog();
|
|
174
|
+
|
|
175
|
+
it('should ignore it and not append anything', async () => {
|
|
176
|
+
const result = await appendReactorSideEffects(eventLog, { some: 'value' }, 'triggering-source', 'Default', 'triggering-source');
|
|
177
|
+
|
|
178
|
+
expect(appendMany).not.toHaveBeenCalled();
|
|
179
|
+
expect(result.isSuccess).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { hasEventType } from '../events/eventTypeDecorator';
|
|
5
|
+
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId';
|
|
6
|
+
import type { IEventLog } from '../eventSequences/IEventLog';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents the outcome of appending a reactor handler's returned side-effect events.
|
|
10
|
+
*/
|
|
11
|
+
export interface ReactorSideEffectResult {
|
|
12
|
+
/** Whether every side-effect event appended successfully. */
|
|
13
|
+
readonly isSuccess: boolean;
|
|
14
|
+
|
|
15
|
+
/** Constraint violation and error messages, populated when {@link isSuccess} is false. */
|
|
16
|
+
readonly errors: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const noSideEffects: ReactorSideEffectResult = { isSuccess: true, errors: [] };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Determines whether a value is shaped like an {@link EventForEventSourceId} — a plain object
|
|
23
|
+
* carrying its own `eventSourceId` and `event` — rather than an event object returned directly.
|
|
24
|
+
*/
|
|
25
|
+
function isEventForEventSourceId(value: unknown): value is EventForEventSourceId {
|
|
26
|
+
if (typeof value !== 'object' || value === null) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const candidate = value as Record<string, unknown>;
|
|
31
|
+
return typeof candidate.eventSourceId === 'string' &&
|
|
32
|
+
typeof candidate.event === 'object' && candidate.event !== null &&
|
|
33
|
+
hasEventType((candidate.event as object).constructor as Function);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Appends the events a `@reactor`-decorated class's handler method returned as side effects, in
|
|
38
|
+
* a single atomic `AppendMany` call — never one append per item, which would neither be atomic
|
|
39
|
+
* nor efficient.
|
|
40
|
+
* @param eventLog - The {@link IEventLog} to append the side effects to.
|
|
41
|
+
* @param handlerResult - Whatever the reactor handler method returned: `undefined`/`void`, a
|
|
42
|
+
* single event object, an array of events, a single {@link EventForEventSourceId}, an array of
|
|
43
|
+
* them, or a mix of bare events and {@link EventForEventSourceId} entries.
|
|
44
|
+
* @param triggeringEventSourceId - The event source id of the event that triggered the reactor,
|
|
45
|
+
* used as the target for bare event returns.
|
|
46
|
+
* @param triggeringEventStreamType - The event stream type of the triggering event, used for bare
|
|
47
|
+
* event returns.
|
|
48
|
+
* @param triggeringEventStreamId - The event stream identifier of the triggering event, used for
|
|
49
|
+
* bare event returns.
|
|
50
|
+
* @returns The outcome of the append. Each {@link EventForEventSourceId} entry keeps its own
|
|
51
|
+
* target (event source id, stream type/id, subject); bare events use the triggering event's.
|
|
52
|
+
*/
|
|
53
|
+
export async function appendReactorSideEffects(
|
|
54
|
+
eventLog: IEventLog,
|
|
55
|
+
handlerResult: unknown,
|
|
56
|
+
triggeringEventSourceId: string,
|
|
57
|
+
triggeringEventStreamType: string,
|
|
58
|
+
triggeringEventStreamId: string
|
|
59
|
+
): Promise<ReactorSideEffectResult> {
|
|
60
|
+
if (handlerResult === undefined || handlerResult === null) {
|
|
61
|
+
return noSideEffects;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const items = Array.isArray(handlerResult) ? handlerResult : [handlerResult];
|
|
65
|
+
const events: EventForEventSourceId[] = [];
|
|
66
|
+
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (isEventForEventSourceId(item)) {
|
|
69
|
+
events.push(item);
|
|
70
|
+
} else if (typeof item === 'object' && item !== null && hasEventType(item.constructor as Function)) {
|
|
71
|
+
events.push({
|
|
72
|
+
eventSourceId: triggeringEventSourceId,
|
|
73
|
+
event: item,
|
|
74
|
+
eventStreamType: triggeringEventStreamType,
|
|
75
|
+
eventStreamId: triggeringEventStreamId,
|
|
76
|
+
subject: triggeringEventSourceId
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Anything else — undefined array entries, plain objects that are neither a registered
|
|
80
|
+
// event type nor EventForEventSourceId-shaped — is not a recognized side-effect and is
|
|
81
|
+
// silently ignored, the same as a handler that returns Task/void.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (events.length === 0) {
|
|
85
|
+
return noSideEffects;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const results = await eventLog.appendMany(events);
|
|
89
|
+
const errors = results
|
|
90
|
+
.filter(result => !result.isSuccess)
|
|
91
|
+
.flatMap(result => [
|
|
92
|
+
...result.constraintViolations.map(violation => violation.message),
|
|
93
|
+
...result.errors.map(error => error.message)
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
return { isSuccess: errors.length === 0, errors };
|
|
97
|
+
}
|
package/reactors/Reactors.ts
CHANGED
|
@@ -13,7 +13,10 @@ import { EventContext } from '../events/EventContext';
|
|
|
13
13
|
import { EventTypeId } from '../events/EventTypeId';
|
|
14
14
|
import { EventTypeGeneration } from '../events/EventTypeGeneration';
|
|
15
15
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
16
|
+
import type { IEventLog } from '../eventSequences/IEventLog';
|
|
17
|
+
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
|
|
16
18
|
import { IReactors } from './IReactors';
|
|
19
|
+
import { appendReactorSideEffects } from './ReactorSideEffects';
|
|
17
20
|
import { getReactorMetadata } from './reactor';
|
|
18
21
|
|
|
19
22
|
/** Expression used to partition reactor observations by event source ID. */
|
|
@@ -107,13 +110,15 @@ export class Reactors implements IReactors {
|
|
|
107
110
|
* @param _eventStoreName - The name of the event store.
|
|
108
111
|
* @param _namespace - The namespace within the event store.
|
|
109
112
|
* @param lifecycle - The connection lifecycle used to react to disconnect events.
|
|
113
|
+
* @param _eventLog - The event log used to append side-effect events a reactor handler returns.
|
|
110
114
|
*/
|
|
111
115
|
constructor(
|
|
112
116
|
private readonly _clientArtifacts: IClientArtifactsProvider,
|
|
113
117
|
private readonly _connection: ChronicleConnection,
|
|
114
118
|
private readonly _eventStoreName: string,
|
|
115
119
|
private readonly _namespace: string,
|
|
116
|
-
lifecycle: ConnectionLifecycle
|
|
120
|
+
lifecycle: ConnectionLifecycle,
|
|
121
|
+
private readonly _eventLog: IEventLog
|
|
117
122
|
) {
|
|
118
123
|
this._lifecycle = lifecycle;
|
|
119
124
|
lifecycle.onDisconnected(async () => {
|
|
@@ -258,10 +263,20 @@ export class Reactors implements IReactors {
|
|
|
258
263
|
this._logger.debug('Received events to observe', {
|
|
259
264
|
reactorId: id,
|
|
260
265
|
partition: eventsToObserve.Partition,
|
|
261
|
-
count: eventsToObserve.Events.length
|
|
266
|
+
count: eventsToObserve.Events.length,
|
|
267
|
+
replayState: eventsToObserve.ReplayState
|
|
262
268
|
});
|
|
263
269
|
|
|
264
|
-
|
|
270
|
+
try {
|
|
271
|
+
await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
|
|
274
|
+
exceptionMessages.push(String(err));
|
|
275
|
+
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
276
|
+
state = ObservationState.Failed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const event of state === ObservationState.Failed ? [] : eventsToObserve.Events) {
|
|
265
280
|
try {
|
|
266
281
|
const eventTypeId = event.Context?.EventType?.Id;
|
|
267
282
|
if (!eventTypeId) {
|
|
@@ -298,7 +313,17 @@ export class Reactors implements IReactors {
|
|
|
298
313
|
eventTypeId
|
|
299
314
|
});
|
|
300
315
|
|
|
301
|
-
await reactorInstance[entry.methodName](content, context);
|
|
316
|
+
const handlerResult = await reactorInstance[entry.methodName](content, context);
|
|
317
|
+
const sideEffectResult = await appendReactorSideEffects(
|
|
318
|
+
this._eventLog,
|
|
319
|
+
handlerResult,
|
|
320
|
+
event.Context!.EventSourceId,
|
|
321
|
+
event.Context!.EventStreamType,
|
|
322
|
+
event.Context!.EventStreamId);
|
|
323
|
+
if (!sideEffectResult.isSuccess) {
|
|
324
|
+
throw new Error(`Reactor side effect failed to append: ${sideEffectResult.errors.join('; ')}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
302
327
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
303
328
|
} catch (err) {
|
|
304
329
|
this._logger.error('Error handling event in reactor', { reactorId: id, error: String(err) });
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../artifacts';
|
|
7
|
+
import type { ChronicleConnection } from '../connection';
|
|
8
|
+
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle';
|
|
9
|
+
import { reducer } from './reducer';
|
|
10
|
+
import { Reducers } from './Reducers';
|
|
11
|
+
|
|
12
|
+
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A minimal client artifacts provider exposing only the reducer types under test —
|
|
16
|
+
* Reducers.ts only reads .reducers and .eventTypes off this during discovery.
|
|
17
|
+
*/
|
|
18
|
+
function createArtifacts(reducers: Constructor[]): IClientArtifactsProvider {
|
|
19
|
+
return {
|
|
20
|
+
eventTypes: [],
|
|
21
|
+
readModels: [],
|
|
22
|
+
reactors: [],
|
|
23
|
+
reducers,
|
|
24
|
+
seeders: [],
|
|
25
|
+
constraints: [],
|
|
26
|
+
projections: [],
|
|
27
|
+
webhooks: [],
|
|
28
|
+
eventTypeMigrations: []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Captures the first message a reducer observation pushes onto its queue (the registration
|
|
34
|
+
* message), by having the mocked `observe` call itself consume the queue's async iterator.
|
|
35
|
+
*/
|
|
36
|
+
function createConnection() {
|
|
37
|
+
const registrationMessages: unknown[] = [];
|
|
38
|
+
const observe = vi.fn().mockImplementation(async function* (queue: AsyncIterable<unknown>) {
|
|
39
|
+
for await (const message of queue) {
|
|
40
|
+
registrationMessages.push(message);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
45
|
+
const connection = {
|
|
46
|
+
reducers: { observe },
|
|
47
|
+
readModels: { registerMany }
|
|
48
|
+
} as unknown as ChronicleConnection;
|
|
49
|
+
|
|
50
|
+
return { connection, registrationMessages };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('Reducers', () => {
|
|
54
|
+
describe('when registering an active reducer (the default)', () => {
|
|
55
|
+
class SomeReducer {}
|
|
56
|
+
reducer('some-active-reducer')(SomeReducer);
|
|
57
|
+
|
|
58
|
+
it('should send a registration message with IsActive true', async () => {
|
|
59
|
+
const { connection, registrationMessages } = createConnection();
|
|
60
|
+
const reducers = new Reducers(createArtifacts([SomeReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink');
|
|
61
|
+
|
|
62
|
+
await reducers.register();
|
|
63
|
+
await flush();
|
|
64
|
+
|
|
65
|
+
expect(registrationMessages).toHaveLength(1);
|
|
66
|
+
const message = registrationMessages[0] as { Content: { Value0: { Reducer: { IsActive: boolean } } } };
|
|
67
|
+
expect(message.Content.Value0.Reducer.IsActive).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('when registering a passive (isActive: false) reducer', () => {
|
|
72
|
+
class SomePassiveReducer {}
|
|
73
|
+
reducer('some-passive-reducer', undefined, undefined, false)(SomePassiveReducer);
|
|
74
|
+
|
|
75
|
+
it('should send a registration message with IsActive false', async () => {
|
|
76
|
+
const { connection, registrationMessages } = createConnection();
|
|
77
|
+
const reducers = new Reducers(createArtifacts([SomePassiveReducer]), connection, 'my-event-store', 'my-namespace', new ConnectionLifecycle(), 'default-sink');
|
|
78
|
+
|
|
79
|
+
await reducers.register();
|
|
80
|
+
await flush();
|
|
81
|
+
|
|
82
|
+
expect(registrationMessages).toHaveLength(1);
|
|
83
|
+
const message = registrationMessages[0] as { Content: { Value0: { Reducer: { IsActive: boolean } } } };
|
|
84
|
+
expect(message.Content.Value0.Reducer.IsActive).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
package/reducers/Reducers.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { toContractsGuid } from '../connection/Guid';
|
|
|
11
11
|
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle';
|
|
12
12
|
import { getEventTypeMetadata } from '../events/eventTypeDecorator';
|
|
13
13
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
14
|
+
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
|
|
14
15
|
import { IReducers } from './IReducers';
|
|
15
16
|
import { getReducerMetadata } from './reducer';
|
|
16
17
|
import { getReadModelMetadata } from '../readModels';
|
|
@@ -292,6 +293,7 @@ export class Reducers implements IReducers {
|
|
|
292
293
|
): Promise<void> {
|
|
293
294
|
const queue = new AsyncQueue<ReducerMessage>();
|
|
294
295
|
this._queues.set(id, queue);
|
|
296
|
+
const isActive = getReducerMetadata(reducerType)?.isActive ?? true;
|
|
295
297
|
|
|
296
298
|
queue.send({
|
|
297
299
|
Content: {
|
|
@@ -307,7 +309,7 @@ export class Reducers implements IReducers {
|
|
|
307
309
|
Key: EVENT_SOURCE_ID_KEY
|
|
308
310
|
})),
|
|
309
311
|
ReadModel: readModelName,
|
|
310
|
-
IsActive:
|
|
312
|
+
IsActive: isActive,
|
|
311
313
|
Tags: [],
|
|
312
314
|
Filters: {
|
|
313
315
|
FilterTags: [],
|
|
@@ -334,14 +336,24 @@ export class Reducers implements IReducers {
|
|
|
334
336
|
reducerId: id,
|
|
335
337
|
partition: operation.Partition,
|
|
336
338
|
count: operation.Events.length,
|
|
337
|
-
hasInitialState: operation.InitialState !== ''
|
|
339
|
+
hasInitialState: operation.InitialState !== '',
|
|
340
|
+
replayState: operation.ReplayState
|
|
338
341
|
});
|
|
339
342
|
|
|
343
|
+
try {
|
|
344
|
+
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: String(err) });
|
|
347
|
+
exceptionMessages.push(String(err));
|
|
348
|
+
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
349
|
+
state = ObservationState.Failed;
|
|
350
|
+
}
|
|
351
|
+
|
|
340
352
|
let currentState: unknown = operation.InitialState
|
|
341
353
|
? JSON.parse(operation.InitialState) as unknown
|
|
342
354
|
: undefined;
|
|
343
355
|
|
|
344
|
-
for (const event of operation.Events) {
|
|
356
|
+
for (const event of state === ObservationState.Failed ? [] : operation.Events) {
|
|
345
357
|
try {
|
|
346
358
|
const eventTypeId = event.Context?.EventType?.Id;
|
|
347
359
|
if (!eventTypeId) {
|
|
@@ -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 { describe, expect, it } from 'vitest';
|
|
5
|
+
import { getReducerMetadata, reducer } from './reducer';
|
|
6
|
+
|
|
7
|
+
describe('reducer', () => {
|
|
8
|
+
describe('when decorating a class without specifying isActive', () => {
|
|
9
|
+
class SomeReducer {}
|
|
10
|
+
reducer('some-reducer')(SomeReducer);
|
|
11
|
+
|
|
12
|
+
it('should default isActive to true', () => {
|
|
13
|
+
const metadata = getReducerMetadata(SomeReducer);
|
|
14
|
+
|
|
15
|
+
expect(metadata?.isActive).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('when decorating a class with isActive explicitly true', () => {
|
|
20
|
+
class SomeActiveReducer {}
|
|
21
|
+
reducer('some-active-reducer', undefined, undefined, true)(SomeActiveReducer);
|
|
22
|
+
|
|
23
|
+
it('should set isActive to true', () => {
|
|
24
|
+
const metadata = getReducerMetadata(SomeActiveReducer);
|
|
25
|
+
|
|
26
|
+
expect(metadata?.isActive).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('when decorating a class with isActive false', () => {
|
|
31
|
+
class SomePassiveReducer {}
|
|
32
|
+
reducer('some-passive-reducer', undefined, undefined, false)(SomePassiveReducer);
|
|
33
|
+
|
|
34
|
+
it('should set isActive to false', () => {
|
|
35
|
+
const metadata = getReducerMetadata(SomePassiveReducer);
|
|
36
|
+
|
|
37
|
+
expect(metadata?.isActive).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
package/reducers/reducer.ts
CHANGED
|
@@ -21,6 +21,13 @@ export interface ReducerMetadata {
|
|
|
21
21
|
|
|
22
22
|
/** The optional read model type produced by the reducer. */
|
|
23
23
|
readonly readModel: Constructor | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the reducer is active. An inactive (passive) reducer is registered with the
|
|
27
|
+
* Chronicle Kernel but does not automatically observe events — it can still be invoked
|
|
28
|
+
* on demand. Defaults to true.
|
|
29
|
+
*/
|
|
30
|
+
readonly isActive: boolean;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -34,6 +41,8 @@ export interface ReducerMetadata {
|
|
|
34
41
|
* @param id - The unique identifier for the reducer. Defaults to the class name if omitted.
|
|
35
42
|
* @param eventSequenceId - Optional explicit event sequence identifier.
|
|
36
43
|
* @param readModel - Optional read model type produced by the reducer.
|
|
44
|
+
* @param isActive - Whether the reducer is active (automatically observes events). Defaults to true.
|
|
45
|
+
* A passive reducer is registered but must be invoked on demand rather than observing automatically.
|
|
37
46
|
* @returns A class decorator.
|
|
38
47
|
*
|
|
39
48
|
* @example
|
|
@@ -46,11 +55,11 @@ export interface ReducerMetadata {
|
|
|
46
55
|
* }
|
|
47
56
|
* ```
|
|
48
57
|
*/
|
|
49
|
-
export function reducer(id: string = '', eventSequenceId?: string, readModel?: Constructor): ClassDecorator {
|
|
58
|
+
export function reducer(id: string = '', eventSequenceId?: string, readModel?: Constructor, isActive: boolean = true): ClassDecorator {
|
|
50
59
|
return (target: object) => {
|
|
51
60
|
const constructor = target as Function;
|
|
52
61
|
const reducerId = new ReducerId(id || constructor.name);
|
|
53
|
-
const metadata: ReducerMetadata = { id: reducerId, eventSequenceId, readModel };
|
|
62
|
+
const metadata: ReducerMetadata = { id: reducerId, eventSequenceId, readModel, isActive };
|
|
54
63
|
Reflect.defineMetadata(REDUCER_METADATA_KEY, metadata, target);
|
|
55
64
|
TypeDiscoverer.default.register(
|
|
56
65
|
DecoratorType.Reducer,
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { AppendError } from '../eventSequences/AppendError';
|
|
6
|
+
import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation';
|
|
7
|
+
import { ConstraintViolation } from '../eventSequences/ConstraintViolation';
|
|
5
8
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
6
9
|
import { AppendResult } from '../eventSequences/AppendResult';
|
|
7
10
|
|
|
@@ -38,6 +41,24 @@ export interface IUnitOfWork {
|
|
|
38
41
|
*/
|
|
39
42
|
getAppendResults(): ReadonlyArray<AppendResult>;
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Gets any constraint violations that occurred across the append results from the latest commit.
|
|
46
|
+
* @returns A thin filter over {@link getAppendResults}.
|
|
47
|
+
*/
|
|
48
|
+
getConstraintViolations(): ReadonlyArray<ConstraintViolation>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gets any concurrency violations that occurred across the append results from the latest commit.
|
|
52
|
+
* @returns A thin filter over {@link getAppendResults}.
|
|
53
|
+
*/
|
|
54
|
+
getConcurrencyViolations(): ReadonlyArray<ConcurrencyViolation>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets any errors that occurred while attempting to commit.
|
|
58
|
+
* @returns A thin filter over {@link getAppendResults}.
|
|
59
|
+
*/
|
|
60
|
+
getAppendErrors(): ReadonlyArray<AppendError>;
|
|
61
|
+
|
|
41
62
|
/**
|
|
42
63
|
* Commits all buffered events.
|
|
43
64
|
*/
|