@cratis/chronicle 6.33.0 → 6.35.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/ChronicleClient.ts +43 -35
- package/ChronicleOptions.ts +27 -1
- package/EventStore.ts +22 -12
- package/Metrics.ts +70 -96
- package/Tracing.ts +4 -2
- package/WellKnownTelemetryNames.ts +67 -0
- package/artifacts/validateArtifactSchemas.spec.ts +1 -1
- package/artifacts/withActivatedArtifact.ts +7 -5
- package/connection/ChronicleConnection.ts +18 -6
- package/connection/CompatibilityPreflight.ts +2 -1
- package/connection/KernelKeepAlive.ts +13 -7
- package/connection/TokenProvider.ts +3 -1
- package/connection/TraceContextMiddleware.ts +30 -0
- package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
- package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
- package/correlation/CorrelationIdManager.ts +10 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +45 -35
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +8 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +19 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +3 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +22 -12
- package/dist/EventStore.js.map +1 -1
- package/dist/Metrics.d.ts +23 -64
- package/dist/Metrics.d.ts.map +1 -1
- package/dist/Metrics.js +62 -92
- package/dist/Metrics.js.map +1 -1
- package/dist/Tracing.d.ts +1 -1
- package/dist/Tracing.d.ts.map +1 -1
- package/dist/Tracing.js +4 -2
- package/dist/Tracing.js.map +1 -1
- package/dist/WellKnownTelemetryNames.d.ts +65 -0
- package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
- package/dist/WellKnownTelemetryNames.js +67 -0
- package/dist/WellKnownTelemetryNames.js.map +1 -0
- package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/withActivatedArtifact.js +5 -4
- package/dist/artifacts/withActivatedArtifact.js.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +5 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +13 -6
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
- package/dist/connection/CompatibilityPreflight.js +2 -1
- package/dist/connection/CompatibilityPreflight.js.map +1 -1
- package/dist/connection/KernelKeepAlive.d.ts +3 -1
- package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
- package/dist/connection/KernelKeepAlive.js +10 -7
- package/dist/connection/KernelKeepAlive.js.map +1 -1
- package/dist/connection/TokenProvider.d.ts.map +1 -1
- package/dist/connection/TokenProvider.js +3 -1
- package/dist/connection/TokenProvider.js.map +1 -1
- package/dist/connection/TraceContextMiddleware.d.ts +4 -0
- package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
- package/dist/connection/TraceContextMiddleware.js +30 -0
- package/dist/connection/TraceContextMiddleware.js.map +1 -0
- package/dist/correlation/CorrelationIdManager.d.ts +4 -0
- package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
- package/dist/correlation/CorrelationIdManager.js +8 -0
- package/dist/correlation/CorrelationIdManager.js.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 +63 -80
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
- package/dist/events/handles.d.ts +12 -0
- package/dist/events/handles.d.ts.map +1 -0
- package/dist/events/handles.js +33 -0
- package/dist/events/handles.js.map +1 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ChronicleLogEntry.d.ts +11 -0
- package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
- package/dist/logging/ChronicleLogEntry.js +4 -0
- package/dist/logging/ChronicleLogEntry.js.map +1 -0
- package/dist/logging/ChronicleLogLevel.d.ts +9 -0
- package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
- package/dist/logging/ChronicleLogLevel.js +12 -0
- package/dist/logging/ChronicleLogLevel.js.map +1 -0
- package/dist/logging/DiagChronicleLogger.d.ts +7 -0
- package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
- package/dist/logging/DiagChronicleLogger.js +10 -0
- package/dist/logging/DiagChronicleLogger.js.map +1 -0
- package/dist/logging/IChronicleLogger.d.ts +7 -0
- package/dist/logging/IChronicleLogger.d.ts.map +1 -0
- package/dist/logging/IChronicleLogger.js +4 -0
- package/dist/logging/IChronicleLogger.js.map +1 -0
- package/dist/logging/createLogger.d.ts +10 -0
- package/dist/logging/createLogger.d.ts.map +1 -0
- package/dist/logging/createLogger.js +55 -0
- package/dist/logging/createLogger.js.map +1 -0
- package/dist/observation/EventHandler.d.ts +7 -0
- package/dist/observation/EventHandler.d.ts.map +1 -0
- package/dist/observation/EventHandler.js +4 -0
- package/dist/observation/EventHandler.js.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts +4 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.js +40 -0
- package/dist/observation/createHandlerInstanceValidator.js.map +1 -0
- package/dist/observation/getEventHandlers.d.ts +5 -0
- package/dist/observation/getEventHandlers.d.ts.map +1 -0
- package/dist/observation/getEventHandlers.js +40 -0
- package/dist/observation/getEventHandlers.js.map +1 -0
- package/dist/observation/getInstanceMethods.d.ts +4 -0
- package/dist/observation/getInstanceMethods.d.ts.map +1 -0
- package/dist/observation/getInstanceMethods.js +29 -0
- package/dist/observation/getInstanceMethods.js.map +1 -0
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +4 -3
- package/dist/projections/Projections.js.map +1 -1
- package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
- package/dist/reactors/ReactorDispatcher.js +27 -32
- package/dist/reactors/ReactorDispatcher.js.map +1 -1
- package/dist/reactors/Reactors.d.ts +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +49 -34
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/index.d.ts +1 -0
- package/dist/reactors/index.d.ts.map +1 -1
- package/dist/reactors/index.js +1 -0
- package/dist/reactors/index.js.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.d.ts +1 -0
- package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.js +5 -17
- package/dist/reducers/ReducerEventDispatcher.js.map +1 -1
- package/dist/reducers/ReducerEventHandler.d.ts +3 -6
- package/dist/reducers/ReducerEventHandler.d.ts.map +1 -1
- package/dist/reducers/Reducers.d.ts +1 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +47 -32
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/index.d.ts +1 -0
- package/dist/reducers/index.d.ts.map +1 -1
- package/dist/reducers/index.js +1 -0
- package/dist/reducers/index.js.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +4 -3
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
- package/dist/telemetry/observeOperation.d.ts +6 -0
- package/dist/telemetry/observeOperation.d.ts.map +1 -0
- package/dist/telemetry/observeOperation.js +23 -0
- package/dist/telemetry/observeOperation.js.map +1 -0
- package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
- package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
- package/dist/telemetry/runInBackgroundContext.js +9 -0
- package/dist/telemetry/runInBackgroundContext.js.map +1 -0
- package/dist/telemetry/spanAttributes.d.ts +14 -0
- package/dist/telemetry/spanAttributes.d.ts.map +1 -0
- package/dist/telemetry/spanAttributes.js +47 -0
- package/dist/telemetry/spanAttributes.js.map +1 -0
- package/dist/testing/ReactorScenario.d.ts +1 -0
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +9 -1
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.js +1 -0
- package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +64 -81
- package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
- package/events/for_handles/when_decorating_a_method.legacy.spec.ts +91 -0
- package/events/for_handles/when_decorating_a_method.standard.spec.ts +102 -0
- package/events/handles.ts +37 -0
- package/events/index.ts +1 -0
- package/for_ChronicleClient/when_registering_artifacts/with_invalid_handler_mappings.spec.ts +142 -0
- package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
- package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
- package/index.ts +7 -1
- package/logging/ChronicleLogEntry.ts +14 -0
- package/logging/ChronicleLogLevel.ts +11 -0
- package/logging/DiagChronicleLogger.ts +13 -0
- package/logging/IChronicleLogger.ts +10 -0
- package/logging/createLogger.ts +50 -0
- package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
- package/observation/EventHandler.ts +9 -0
- package/observation/createHandlerInstanceValidator.ts +42 -0
- package/observation/for_getEventHandlers/when_discovering_handlers.spec.ts +137 -0
- package/observation/for_getEventHandlers/when_reobserving_explicit_handlers.spec.ts +73 -0
- package/observation/getEventHandlers.ts +43 -0
- package/observation/getInstanceMethods.ts +29 -0
- package/package.json +8 -2
- package/projections/Projections.ts +4 -3
- package/reactors/ReactorDispatcher.ts +26 -30
- package/reactors/Reactors.ts +55 -37
- package/reactors/decorators.standard.spec.ts +8 -0
- package/reactors/for_ReactorDispatcher/when_selecting_explicit_handlers.spec.ts +64 -0
- package/reactors/for_Reactors/when_activating_a_field_override.spec.ts +62 -0
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
- package/reactors/for_Reactors/when_delivering_to_instance_handlers.spec.ts +108 -0
- package/reactors/for_Reactors/when_observing_explicit_handlers.legacy.spec.ts +77 -0
- package/reactors/for_Reactors/when_registering_explicit_handlers.spec.ts +53 -0
- package/reactors/index.ts +1 -0
- package/reducers/ReducerEventDispatcher.ts +5 -16
- package/reducers/ReducerEventHandler.ts +4 -6
- package/reducers/Reducers.ts +50 -35
- package/reducers/for_ReducerEventDispatcher/when_invoking_an_explicit_handler.spec.ts +51 -0
- package/reducers/for_ReducerEventDispatcher/when_invoking_instance_handlers.spec.ts +69 -0
- package/reducers/for_Reducers/when_activating_a_field_override.spec.ts +63 -0
- package/reducers/for_Reducers/when_observing_explicit_handlers.legacy.spec.ts +73 -0
- package/reducers/for_Reducers/when_registering_explicit_handlers.spec.ts +57 -0
- package/reducers/index.ts +1 -0
- package/seeding/EventSeeding.ts +6 -4
- package/telemetry/ChronicleTelemetryOptions.ts +8 -0
- package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
- package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
- package/telemetry/observeOperation.ts +25 -0
- package/telemetry/runInBackgroundContext.ts +10 -0
- package/telemetry/spanAttributes.ts +49 -0
- package/testing/ReactorScenario.ts +9 -1
- package/testing/ReducerReadModelProcessor.ts +1 -0
- package/testing/for_ReactorScenario/when_delivering_to_an_explicit_handler.spec.ts +100 -0
- package/testing/for_ReducerReadModelProcessor/when_an_instance_field_hides_a_handler.spec.ts +29 -0
|
@@ -0,0 +1,53 @@
|
|
|
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 { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
6
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
7
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
8
|
+
import type { IEventLog } from '../../eventSequences/IEventLog.js';
|
|
9
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
10
|
+
import { handles, reactor, Reactors } from '../index.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
|
|
14
|
+
@eventType('registration-handled-event')
|
|
15
|
+
class AuthorRegistered {}
|
|
16
|
+
@eventType('unselected-handled-event')
|
|
17
|
+
class Unregistered {}
|
|
18
|
+
|
|
19
|
+
@reactor('valid-handlers')
|
|
20
|
+
class Valid { @handles(AuthorRegistered) notify() {} }
|
|
21
|
+
@reactor('missing-handler-event')
|
|
22
|
+
class Missing { @handles(Unregistered) notify() {} }
|
|
23
|
+
@reactor('duplicate-handlers')
|
|
24
|
+
class Duplicate {
|
|
25
|
+
@handles(AuthorRegistered) notify() {}
|
|
26
|
+
authorRegistered() {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [type, message] of [
|
|
30
|
+
[Missing, /notify.*Missing.*Unregistered.*no registered event type/],
|
|
31
|
+
[Duplicate, /multiple handlers.*registration-handled-event.*notify.*authorRegistered/]
|
|
32
|
+
] as const) {
|
|
33
|
+
describe(`when registering a reactor with invalid explicit handlers on ${type.name}`, () => {
|
|
34
|
+
const observe = vi.fn();
|
|
35
|
+
let error: unknown;
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
observe.mockReset();
|
|
38
|
+
error = undefined;
|
|
39
|
+
const artifacts = { reactors: [Valid, type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider;
|
|
40
|
+
const connection = { reactors: { observe } } as unknown as ChronicleConnection;
|
|
41
|
+
const reactors = new Reactors(artifacts, connection, 'store', 'tenant', new ConnectionLifecycle(), {} as IEventLog);
|
|
42
|
+
try { await reactors.register(); } catch (caught) { error = caught; }
|
|
43
|
+
finally { reactors.dispose(); }
|
|
44
|
+
});
|
|
45
|
+
it('should reject registration with an actionable error', () => {
|
|
46
|
+
(error instanceof Error).should.be.true;
|
|
47
|
+
(error as Error).message.should.match(message);
|
|
48
|
+
});
|
|
49
|
+
it('should not start any observation before validating all reactors', () => {
|
|
50
|
+
observe.mock.calls.should.have.lengthOf(0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
package/reactors/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
export { ReactorId } from './ReactorId.js';
|
|
5
5
|
export { reactor, getReactorMetadata, isReactor } from './reactor.js';
|
|
6
|
+
export { handles } from '../events/handles.js';
|
|
6
7
|
export { onceOnly } from './onceOnly.js';
|
|
7
8
|
export { replay } from './replay.js';
|
|
8
9
|
export type { ReactorMetadata } from './reactor.js';
|
|
@@ -2,31 +2,20 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
-
import {
|
|
5
|
+
import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
|
|
6
|
+
import { getEventHandlers } from '../observation/getEventHandlers.js';
|
|
6
7
|
import type { EventContext } from '../events/EventContext.js';
|
|
7
8
|
import type { ReducerEventHandler } from './ReducerEventHandler.js';
|
|
8
9
|
|
|
9
10
|
/** Selects and invokes the same reducer handlers for kernel deliveries and in-process scenarios. */
|
|
10
11
|
export class ReducerEventDispatcher {
|
|
11
12
|
readonly handlers: ReducerEventHandler[];
|
|
13
|
+
readonly validateInstance: (instance: object) => void;
|
|
12
14
|
|
|
13
15
|
/** Discovers event methods from the artifact event types. */
|
|
14
16
|
constructor(reducerType: Constructor, eventTypes: readonly Constructor[]) {
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
for (const eventTypeClass of eventTypes) {
|
|
18
|
-
const metadata = getEventTypeMetadata(eventTypeClass);
|
|
19
|
-
if (!metadata) continue;
|
|
20
|
-
const className = (eventTypeClass as Function).name;
|
|
21
|
-
const methodName = className.charAt(0).toLowerCase() + className.slice(1);
|
|
22
|
-
if (typeof proto[methodName] === 'function') {
|
|
23
|
-
this.handlers.push({
|
|
24
|
-
id: metadata.eventType.id.value,
|
|
25
|
-
generation: metadata.eventType.generation.value,
|
|
26
|
-
methodName
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
17
|
+
this.handlers = getEventHandlers(reducerType, eventTypes);
|
|
18
|
+
this.validateInstance = createHandlerInstanceValidator(reducerType, this.handlers.map(handler => handler.methodName));
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/** Resolves a registered event type, or returns undefined for an unsubscribed event. */
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly methodName: string;
|
|
9
|
-
}
|
|
4
|
+
import type { EventHandler } from '../observation/EventHandler.js';
|
|
5
|
+
|
|
6
|
+
/** Event type and method selected for a reducer observation. */
|
|
7
|
+
export type ReducerEventHandler = EventHandler;
|
package/reducers/Reducers.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import 'reflect-metadata';
|
|
5
|
-
import {
|
|
5
|
+
import { createLogger } from '../logging/createLogger.js';
|
|
6
|
+
import { runInBackgroundContext } from '../telemetry/runInBackgroundContext.js';
|
|
6
7
|
import { Constructor } from '@cratis/fundamentals';
|
|
7
8
|
import { ObservationState, ReadModelObserverType, ReducerMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
8
9
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
@@ -102,8 +103,9 @@ export class Reducers implements IReducers {
|
|
|
102
103
|
/** How long to wait before re-establishing an observation whose stream ended. */
|
|
103
104
|
private static readonly _reobserveDelayMs = 2000;
|
|
104
105
|
|
|
105
|
-
private readonly _logger
|
|
106
|
+
private readonly _logger: ReturnType<typeof createLogger>;
|
|
106
107
|
private readonly _lifecycle: ConnectionLifecycle;
|
|
108
|
+
private readonly _unsubscribeDisconnected: () => void;
|
|
107
109
|
private readonly _reducers = new Map<string, Constructor>();
|
|
108
110
|
private readonly _queues = new Map<string, AsyncQueue<ReducerMessage>>();
|
|
109
111
|
private readonly _observations = new Map<string, AbortController>();
|
|
@@ -133,8 +135,9 @@ export class Reducers implements IReducers {
|
|
|
133
135
|
private readonly _artifactActivator?: ClientArtifactsActivator,
|
|
134
136
|
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
|
|
135
137
|
) {
|
|
138
|
+
this._logger = createLogger('@cratis/chronicle/reducers', _connection.logger);
|
|
136
139
|
this._lifecycle = lifecycle;
|
|
137
|
-
lifecycle.onDisconnected(async () => {
|
|
140
|
+
this._unsubscribeDisconnected = lifecycle.onDisconnected(async () => {
|
|
138
141
|
this._logger.info('Disconnected — stopping all reducer observations');
|
|
139
142
|
this._registered = false;
|
|
140
143
|
this.disconnectAll();
|
|
@@ -143,6 +146,7 @@ export class Reducers implements IReducers {
|
|
|
143
146
|
|
|
144
147
|
/** Stops observations permanently when the owning client is disposed. */
|
|
145
148
|
dispose(): void {
|
|
149
|
+
this._unsubscribeDisconnected();
|
|
146
150
|
this._disposed = true;
|
|
147
151
|
this._registered = false;
|
|
148
152
|
this.disconnectAll();
|
|
@@ -170,13 +174,16 @@ export class Reducers implements IReducers {
|
|
|
170
174
|
await this.discover();
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
const registrations = [...this._reducers].map(([id, type]) => ({
|
|
178
|
+
id, type, dispatcher: new ReducerEventDispatcher(type, this._clientArtifacts.eventTypes)
|
|
179
|
+
}));
|
|
173
180
|
assertUniqueReadModelIds(this._clientArtifacts.readModels);
|
|
174
181
|
await this.registerReadModels();
|
|
175
182
|
|
|
176
183
|
if (this._disposed) return;
|
|
177
184
|
this._logger.info('Registering reducers', { count: this._reducers.size });
|
|
178
|
-
for (const
|
|
179
|
-
this.startObservation(id,
|
|
185
|
+
for (const { id, type, dispatcher } of registrations) {
|
|
186
|
+
this.startObservation(id, type, dispatcher);
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
this._registered = true;
|
|
@@ -234,22 +241,24 @@ export class Reducers implements IReducers {
|
|
|
234
241
|
return JSON.stringify(minimalSchema);
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
private startObservation(id: string, reducerType: Constructor
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this._logger.info('Starting reducer observation', {
|
|
245
|
-
reducerId: id,
|
|
246
|
-
eventSequenceId,
|
|
247
|
-
readModel: readModelName,
|
|
248
|
-
handlerCount: dispatcher.handlers.length,
|
|
249
|
-
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
250
|
-
});
|
|
244
|
+
private startObservation(id: string, reducerType: Constructor,
|
|
245
|
+
dispatcher: ReducerEventDispatcher): void {
|
|
246
|
+
runInBackgroundContext(() => {
|
|
247
|
+
if (this._disposed) return;
|
|
248
|
+
const metadata = getReducerMetadata(reducerType)!;
|
|
249
|
+
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
250
|
+
const readModelName = this.getReducerReadModelIdentifier(reducerType);
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
this._logger.info('Starting reducer observation', {
|
|
253
|
+
reducerId: id,
|
|
254
|
+
eventSequenceId,
|
|
255
|
+
readModel: readModelName,
|
|
256
|
+
handlerCount: dispatcher.handlers.length,
|
|
257
|
+
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
261
|
+
});
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
private async runObservation(
|
|
@@ -262,10 +271,10 @@ export class Reducers implements IReducers {
|
|
|
262
271
|
try {
|
|
263
272
|
await this.observeReducer(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
264
273
|
} catch (error) {
|
|
265
|
-
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error
|
|
274
|
+
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error });
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
this.scheduleReobserve(id, reducerType);
|
|
277
|
+
this.scheduleReobserve(id, reducerType, dispatcher);
|
|
269
278
|
}
|
|
270
279
|
|
|
271
280
|
/**
|
|
@@ -278,21 +287,21 @@ export class Reducers implements IReducers {
|
|
|
278
287
|
* retried, and the delay keeps a stream that keeps ending from becoming a hot
|
|
279
288
|
* loop.
|
|
280
289
|
*/
|
|
281
|
-
private scheduleReobserve(id: string, reducerType: Constructor): void {
|
|
290
|
+
private scheduleReobserve(id: string, reducerType: Constructor, dispatcher: ReducerEventDispatcher): void {
|
|
282
291
|
// A disconnect clears the registration; the reconnect re-registers every
|
|
283
292
|
// reducer from scratch, so retrying here as well would double up.
|
|
284
293
|
if (!this._registered || this._disposed) {
|
|
285
294
|
return;
|
|
286
295
|
}
|
|
287
296
|
|
|
288
|
-
const handle = setTimeout(() => {
|
|
297
|
+
const handle = runInBackgroundContext(() => setTimeout(() => {
|
|
289
298
|
if (!this._registered || this._disposed) {
|
|
290
299
|
return;
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
this._logger.info('Re-establishing reducer observation', { reducerId: id });
|
|
294
|
-
this.startObservation(id, reducerType);
|
|
295
|
-
}, Reducers._reobserveDelayMs);
|
|
303
|
+
this.startObservation(id, reducerType, dispatcher);
|
|
304
|
+
}, Reducers._reobserveDelayMs));
|
|
296
305
|
|
|
297
306
|
handle.unref?.();
|
|
298
307
|
}
|
|
@@ -349,6 +358,7 @@ export class Reducers implements IReducers {
|
|
|
349
358
|
|
|
350
359
|
try {
|
|
351
360
|
const reducerInstance = this._artifactActivator ? undefined : new (reducerType as new () => Record<string, Function>)();
|
|
361
|
+
if (reducerInstance) dispatcher.validateInstance(reducerInstance);
|
|
352
362
|
|
|
353
363
|
for await (const operation of this._connection.reducers.observe(queue, { signal: controller.signal })) {
|
|
354
364
|
let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
@@ -359,7 +369,6 @@ export class Reducers implements IReducers {
|
|
|
359
369
|
|
|
360
370
|
this._logger.debug('Received reduce operation', {
|
|
361
371
|
reducerId: id,
|
|
362
|
-
partition: operation.Partition,
|
|
363
372
|
count: operation.Events.length,
|
|
364
373
|
hasInitialState: operation.InitialState !== '',
|
|
365
374
|
replayState: operation.ReplayState
|
|
@@ -373,14 +382,17 @@ export class Reducers implements IReducers {
|
|
|
373
382
|
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
374
383
|
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
375
384
|
replayState: operation.ReplayState
|
|
376
|
-
}, this._artifactActivator, artifact =>
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
}, this._artifactActivator, artifact => {
|
|
386
|
+
dispatcher.validateInstance(artifact.instance);
|
|
387
|
+
return runActivated(artifact, () =>
|
|
388
|
+
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
|
|
389
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState });
|
|
390
|
+
}, this._connection.logger);
|
|
379
391
|
} else if (reducerInstance) {
|
|
380
392
|
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
381
393
|
}
|
|
382
394
|
} catch (err) {
|
|
383
|
-
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error:
|
|
395
|
+
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: err });
|
|
384
396
|
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
385
397
|
exceptionMessages.push(String(err.processingError));
|
|
386
398
|
}
|
|
@@ -424,7 +436,7 @@ export class Reducers implements IReducers {
|
|
|
424
436
|
{ delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
|
|
425
437
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
426
438
|
} catch (err) {
|
|
427
|
-
this._logger.error('Error handling event in reducer', { reducerId: id, error:
|
|
439
|
+
this._logger.error('Error handling event in reducer', { reducerId: id, error: err });
|
|
428
440
|
exceptionMessages.push(String(err));
|
|
429
441
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
430
442
|
state = ObservationState.Failed;
|
|
@@ -445,9 +457,12 @@ export class Reducers implements IReducers {
|
|
|
445
457
|
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
446
458
|
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
447
459
|
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
448
|
-
}, this._artifactActivator,
|
|
460
|
+
}, this._artifactActivator, artifact => {
|
|
461
|
+
dispatcher.validateInstance(artifact.instance);
|
|
462
|
+
return processEvents(artifact);
|
|
463
|
+
}, this._connection.logger);
|
|
449
464
|
} catch (err) {
|
|
450
|
-
this._logger.error('Error activating reducer', { reducerId: id, error:
|
|
465
|
+
this._logger.error('Error activating reducer', { reducerId: id, error: err });
|
|
451
466
|
exceptionMessages.push(String(err));
|
|
452
467
|
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
453
468
|
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
@@ -483,7 +498,7 @@ export class Reducers implements IReducers {
|
|
|
483
498
|
if (!this._queues.has(id)) {
|
|
484
499
|
this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
|
|
485
500
|
} else {
|
|
486
|
-
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error:
|
|
501
|
+
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
|
|
487
502
|
}
|
|
488
503
|
} finally {
|
|
489
504
|
// Only retire our own queue: a reconnect can already have replaced it,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
6
|
+
import { EventType } from '../../events/EventType.js';
|
|
7
|
+
import type { EventContext } from '../../events/EventContext.js';
|
|
8
|
+
import { handles } from '../index.js';
|
|
9
|
+
import { ReducerEventDispatcher } from '../ReducerEventDispatcher.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
@eventType('explicit-reducer-delivery')
|
|
14
|
+
class AuthorRegistered { constructor(readonly name: string) {} }
|
|
15
|
+
|
|
16
|
+
class AuthorReducer {
|
|
17
|
+
@handles(AuthorRegistered)
|
|
18
|
+
update(event: AuthorRegistered, state: { names: string[] } | undefined, context: EventContext) {
|
|
19
|
+
return { names: [...(state?.names ?? []), event.name], id: context.eventSourceId };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class FieldReducer extends AuthorReducer {
|
|
24
|
+
override update = () => ({ names: [], id: 'hidden' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('when an instance field hides an inherited reducer handler', () => {
|
|
28
|
+
it('should reject the field against the preflight method', () => {
|
|
29
|
+
const dispatcher = new ReducerEventDispatcher(FieldReducer, [AuthorRegistered]);
|
|
30
|
+
(() => dispatcher.validateInstance(new FieldReducer())).should.throw(
|
|
31
|
+
"Override 'update' on 'FieldReducer' hides @handles(AuthorRegistered) declared on 'AuthorReducer'; use a method with @handles instead of an instance field.");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('when invoking an explicit reducer handler', () => {
|
|
36
|
+
let result: unknown;
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
const dispatcher = new ReducerEventDispatcher(AuthorReducer, [AuthorRegistered]);
|
|
39
|
+
const instance = new AuthorReducer() as unknown as Record<string, Function>;
|
|
40
|
+
const context: EventContext = {
|
|
41
|
+
sequenceNumber: 1n, eventSourceId: 'author', eventType: EventType.parse('explicit-reducer-delivery'),
|
|
42
|
+
occurred: new Date(), correlationId: '', causation: [], tags: []
|
|
43
|
+
};
|
|
44
|
+
const handler = dispatcher.handlerFor('explicit-reducer-delivery')!;
|
|
45
|
+
const initial = await dispatcher.invoke(instance, handler, new AuthorRegistered('Ada'), undefined, context);
|
|
46
|
+
result = await dispatcher.invoke(instance, handler, new AuthorRegistered('Grace'), initial, context);
|
|
47
|
+
});
|
|
48
|
+
it('should pass the event state and context to the named method', () => {
|
|
49
|
+
(result as object).should.deep.equal({ names: ['Ada', 'Grace'], id: 'author' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
6
|
+
import { EventType } from '../../events/EventType.js';
|
|
7
|
+
import type { EventContext } from '../../events/EventContext.js';
|
|
8
|
+
import { handles } from '../index.js';
|
|
9
|
+
import { ReducerEventDispatcher } from '../ReducerEventDispatcher.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
@eventType('instance-reducer-event')
|
|
14
|
+
class AuthorRegistered { constructor(readonly name: string) {} }
|
|
15
|
+
|
|
16
|
+
class BoundConventional {
|
|
17
|
+
readonly prefix = 'conventional';
|
|
18
|
+
constructor() { this.authorRegistered = this.authorRegistered.bind(this); }
|
|
19
|
+
authorRegistered(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
|
|
20
|
+
return [...(state ?? []), `${this.prefix}:${event.name}:${context.eventSourceId}`];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class BoundExplicit {
|
|
25
|
+
readonly prefix = 'explicit';
|
|
26
|
+
constructor() { this.update = this.update.bind(this); }
|
|
27
|
+
@handles(AuthorRegistered)
|
|
28
|
+
update(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
|
|
29
|
+
return [...(state ?? []), `${this.prefix}:${event.name}:${context.eventSourceId}`];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class Conventional {
|
|
34
|
+
authorRegistered(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
|
|
35
|
+
return [...(state ?? []), `base:${event.name}:${context.eventSourceId}`];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class FieldConventional extends Conventional {
|
|
40
|
+
override authorRegistered = (event: AuthorRegistered, state: string[] | undefined, context: EventContext) =>
|
|
41
|
+
[...(state ?? []), `field:${event.name}:${context.eventSourceId}`];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const { type, prefix } of [
|
|
45
|
+
{ type: BoundConventional, prefix: 'conventional' },
|
|
46
|
+
{ type: BoundExplicit, prefix: 'explicit' },
|
|
47
|
+
{ type: FieldConventional, prefix: 'field' }
|
|
48
|
+
]) {
|
|
49
|
+
describe(`when invoking the ${prefix} reducer instance handler`, () => {
|
|
50
|
+
let result: unknown;
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
const dispatcher = new ReducerEventDispatcher(type, [AuthorRegistered]);
|
|
53
|
+
const instance = new type();
|
|
54
|
+
dispatcher.validateInstance(instance);
|
|
55
|
+
const context: EventContext = {
|
|
56
|
+
sequenceNumber: 1n, eventSourceId: 'author', eventType: EventType.parse('instance-reducer-event'),
|
|
57
|
+
occurred: new Date(), correlationId: '', causation: [], tags: []
|
|
58
|
+
};
|
|
59
|
+
const handler = dispatcher.handlerFor('instance-reducer-event')!;
|
|
60
|
+
const initial = await dispatcher.invoke(instance as unknown as Record<string, Function>, handler,
|
|
61
|
+
new AuthorRegistered('Ada'), undefined, context);
|
|
62
|
+
result = await dispatcher.invoke(instance as unknown as Record<string, Function>, handler,
|
|
63
|
+
new AuthorRegistered('Grace'), initial, context);
|
|
64
|
+
});
|
|
65
|
+
it('should invoke the instance handler with the event state and context', () => {
|
|
66
|
+
(result as string[]).should.deep.equal([`${prefix}:Ada:author`, `${prefix}:Grace:author`]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { EventContext, EventType, ObservationState, ReplayState, type ReducerMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
8
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
9
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
10
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
11
|
+
import { handles, reducer, Reducers } from '../index.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
|
|
15
|
+
@eventType('field-reducer-event')
|
|
16
|
+
class AuthorRegistered {}
|
|
17
|
+
const calls: string[] = [];
|
|
18
|
+
class Base {
|
|
19
|
+
@handles(AuthorRegistered) update() { calls.push('base'); return {}; }
|
|
20
|
+
beginReplay() { calls.push('replay'); }
|
|
21
|
+
endReplay() {}
|
|
22
|
+
}
|
|
23
|
+
@reducer('field-reducer')
|
|
24
|
+
class Derived extends Base {
|
|
25
|
+
override update = () => { calls.push('field'); return {}; };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const replayState of [ReplayState.REPLAY_STATE_None, ReplayState.BeginReplay]) {
|
|
29
|
+
describe(`when activating a reducer with a field override for replay state ${replayState}`, () => {
|
|
30
|
+
let result: ReducerMessage;
|
|
31
|
+
let disposed: boolean;
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
calls.length = 0;
|
|
34
|
+
disposed = false;
|
|
35
|
+
const lifecycle = new ConnectionLifecycle();
|
|
36
|
+
let finish!: () => void;
|
|
37
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
38
|
+
async function* stream(queue: AsyncIterable<ReducerMessage>) {
|
|
39
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
40
|
+
await messages.next();
|
|
41
|
+
yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
|
|
42
|
+
EventType: EventType.fromPartial({ Id: 'field-reducer-event', Generation: 1 }), SequenceNumber: 1n
|
|
43
|
+
}) }], Partition: 'author', ReplayState: replayState, InitialState: '' };
|
|
44
|
+
result = (await messages.next()).value!;
|
|
45
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
46
|
+
finish();
|
|
47
|
+
}
|
|
48
|
+
const reducers = new Reducers({ reducers: [Derived], eventTypes: [AuthorRegistered], readModels: [] } as unknown as IClientArtifactsProvider,
|
|
49
|
+
{ reducers: { observe: stream }, readModels: { registerMany: async () => ({}) } } as unknown as ChronicleConnection,
|
|
50
|
+
'store', 'tenant', lifecycle, 'sink', { readModels: {} } as IEventStore,
|
|
51
|
+
type => ({ instance: new type(), dispose: () => { disposed = true; } }));
|
|
52
|
+
try { await reducers.register(); await finished; } finally { reducers.dispose(); }
|
|
53
|
+
});
|
|
54
|
+
it('should fail delivery without invoking the field or replay notification', () => {
|
|
55
|
+
calls.should.have.lengthOf(0);
|
|
56
|
+
result.Content!.Value1!.State.should.equal(ObservationState.Failed);
|
|
57
|
+
result.Content!.Value1!.ReadModelState.should.equal('');
|
|
58
|
+
result.Content!.Value1!.ExceptionMessages.join(' ').should.contain(
|
|
59
|
+
"Override 'update' on 'Derived' hides @handles(AuthorRegistered) declared on 'Base'; use a method with @handles instead of an instance field.");
|
|
60
|
+
});
|
|
61
|
+
it('should release the rejected activation lease', () => { disposed.should.be.true; });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 { EventContext, EventType, ObservationState, type ReducerMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
8
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
9
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
10
|
+
import type { EventContext as ClientEventContext } from '../../events/EventContext.js';
|
|
11
|
+
import { handles, reducer, Reducers } from '../index.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
|
|
15
|
+
@eventType('legacy-reduced-handler')
|
|
16
|
+
class AuthorRegistered { constructor(readonly name = '') {} }
|
|
17
|
+
|
|
18
|
+
@reducer('legacy-reduction')
|
|
19
|
+
class AuthorReducer {
|
|
20
|
+
@handles(AuthorRegistered)
|
|
21
|
+
update(event: AuthorRegistered, state: { names: string[] } | undefined, context: ClientEventContext) {
|
|
22
|
+
return { names: [...(state?.names ?? []), event.name], id: context.eventSourceId };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('when observing explicit legacy reducer handlers', () => {
|
|
27
|
+
let registration: ReducerMessage;
|
|
28
|
+
let result: ReducerMessage;
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
const lifecycle = new ConnectionLifecycle();
|
|
31
|
+
let finish!: () => void;
|
|
32
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
33
|
+
async function* stream(queue: AsyncIterable<ReducerMessage>) {
|
|
34
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
35
|
+
registration = (await messages.next()).value!;
|
|
36
|
+
yield {
|
|
37
|
+
Events: ['Ada', 'Grace'].map((name, index) => ({
|
|
38
|
+
Context: EventContext.fromPartial({
|
|
39
|
+
EventType: EventType.fromPartial({ Id: 'legacy-reduced-handler', Generation: 1 }),
|
|
40
|
+
EventSourceId: 'author', SequenceNumber: BigInt(index + 1)
|
|
41
|
+
}),
|
|
42
|
+
Content: JSON.stringify({ name })
|
|
43
|
+
})),
|
|
44
|
+
Partition: 'author', ReplayState: 0, InitialState: ''
|
|
45
|
+
};
|
|
46
|
+
result = (await messages.next()).value!;
|
|
47
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
48
|
+
finish();
|
|
49
|
+
}
|
|
50
|
+
const connection = {
|
|
51
|
+
reducers: { observe: stream }, readModels: { registerMany: async () => ({}) }
|
|
52
|
+
} as unknown as ChronicleConnection;
|
|
53
|
+
const artifacts = {
|
|
54
|
+
reducers: [AuthorReducer], eventTypes: [AuthorRegistered], readModels: []
|
|
55
|
+
} as unknown as IClientArtifactsProvider;
|
|
56
|
+
const reducers = new Reducers(artifacts, connection, 'store', 'tenant', lifecycle, 'sink');
|
|
57
|
+
try {
|
|
58
|
+
await reducers.register();
|
|
59
|
+
await finished;
|
|
60
|
+
} finally { reducers.dispose(); }
|
|
61
|
+
});
|
|
62
|
+
it('should subscribe to the explicitly named event type', () => {
|
|
63
|
+
registration.Content!.Value0!.Reducer!.EventTypes.map(entry => entry.EventType!.Id)
|
|
64
|
+
.should.deep.equal(['legacy-reduced-handler']);
|
|
65
|
+
});
|
|
66
|
+
it('should pass the event state and context through the named method', () => {
|
|
67
|
+
JSON.parse(result.Content!.Value1!.ReadModelState).should.deep.equal({ names: ['Ada', 'Grace'], id: 'author' });
|
|
68
|
+
});
|
|
69
|
+
it('should acknowledge both deliveries successfully', () => {
|
|
70
|
+
result.Content!.Value1!.State.should.equal(ObservationState.Success);
|
|
71
|
+
result.Content!.Value1!.LastSuccessfulObservation.should.equal(2n);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
6
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
7
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
8
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
9
|
+
import { handles, reducer, Reducers } from '../index.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
@eventType('reducer-handled-event')
|
|
14
|
+
class AuthorRegistered {}
|
|
15
|
+
@eventType('unselected-reducer-event')
|
|
16
|
+
class Unregistered {}
|
|
17
|
+
|
|
18
|
+
@reducer('valid-handlers')
|
|
19
|
+
class Valid { @handles(AuthorRegistered) update() {} }
|
|
20
|
+
@reducer('missing-handler-event')
|
|
21
|
+
class Missing { @handles(Unregistered) update() {} }
|
|
22
|
+
@reducer('duplicate-handlers')
|
|
23
|
+
class Duplicate {
|
|
24
|
+
@handles(AuthorRegistered) update() {}
|
|
25
|
+
authorRegistered() {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [type, message] of [
|
|
29
|
+
[Missing, /update.*Missing.*Unregistered.*no registered event type/],
|
|
30
|
+
[Duplicate, /multiple handlers.*reducer-handled-event.*update.*authorRegistered/]
|
|
31
|
+
] as const) {
|
|
32
|
+
describe(`when registering a reducer with invalid explicit handlers on ${type.name}`, () => {
|
|
33
|
+
const observe = vi.fn();
|
|
34
|
+
const registerMany = vi.fn();
|
|
35
|
+
let error: unknown;
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
observe.mockReset();
|
|
38
|
+
registerMany.mockReset();
|
|
39
|
+
error = undefined;
|
|
40
|
+
const artifacts = { reducers: [Valid, type], eventTypes: [AuthorRegistered], readModels: [] } as unknown as IClientArtifactsProvider;
|
|
41
|
+
const connection = { reducers: { observe }, readModels: { registerMany } } as unknown as ChronicleConnection;
|
|
42
|
+
const reducers = new Reducers(artifacts, connection, 'store', 'tenant', new ConnectionLifecycle(), 'sink');
|
|
43
|
+
try { await reducers.register(); } catch (caught) { error = caught; }
|
|
44
|
+
finally { reducers.dispose(); }
|
|
45
|
+
});
|
|
46
|
+
it('should reject registration with an actionable error', () => {
|
|
47
|
+
(error instanceof Error).should.be.true;
|
|
48
|
+
(error as Error).message.should.match(message);
|
|
49
|
+
});
|
|
50
|
+
it('should not register read models before validating all handlers', () => {
|
|
51
|
+
registerMany.mock.calls.should.have.lengthOf(0);
|
|
52
|
+
});
|
|
53
|
+
it('should not start an observation before validating all reducers', () => {
|
|
54
|
+
observe.mock.calls.should.have.lengthOf(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
package/reducers/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
export { handles } from '../events/handles.js';
|
|
4
5
|
export { ReducerId } from './ReducerId.js';
|
|
5
6
|
export { reducer, getReducerMetadata, isReducer } from './reducer.js';
|
|
6
7
|
export type { ReducerMetadata } from './reducer.js';
|