@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
package/reactors/Reactors.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, ReactorMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
8
9
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
@@ -13,6 +14,7 @@ import { getTagsFor } from '../events/tagDecorator.js';
|
|
|
13
14
|
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
|
|
14
15
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
15
16
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
17
|
+
import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
|
|
16
18
|
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle.js';
|
|
17
19
|
import { IReactors } from './IReactors.js';
|
|
18
20
|
import { dispatchReactorSideEffects } from './ReactorSideEffects.js';
|
|
@@ -102,8 +104,9 @@ export class Reactors implements IReactors {
|
|
|
102
104
|
/** How long to wait before re-establishing an observation whose stream ended. */
|
|
103
105
|
private static readonly _reobserveDelayMs = 2000;
|
|
104
106
|
|
|
105
|
-
private readonly _logger
|
|
107
|
+
private readonly _logger: ReturnType<typeof createLogger>;
|
|
106
108
|
private readonly _lifecycle: ConnectionLifecycle;
|
|
109
|
+
private readonly _unsubscribeDisconnected: () => void;
|
|
107
110
|
private readonly _reactors = new Map<string, Constructor>();
|
|
108
111
|
private readonly _queues = new Map<string, AsyncQueue<ReactorMessage>>();
|
|
109
112
|
private readonly _observations = new Map<string, AbortController>();
|
|
@@ -130,8 +133,9 @@ export class Reactors implements IReactors {
|
|
|
130
133
|
private readonly _eventStore?: IEventStore,
|
|
131
134
|
private readonly _artifactActivator?: ClientArtifactsActivator
|
|
132
135
|
) {
|
|
136
|
+
this._logger = createLogger('@cratis/chronicle/reactors', _connection.logger);
|
|
133
137
|
this._lifecycle = lifecycle;
|
|
134
|
-
lifecycle.onDisconnected(async () => {
|
|
138
|
+
this._unsubscribeDisconnected = lifecycle.onDisconnected(async () => {
|
|
135
139
|
this._logger.info('Disconnected — stopping all reactor observations');
|
|
136
140
|
this._registered = false;
|
|
137
141
|
this.disconnectAll();
|
|
@@ -140,6 +144,7 @@ export class Reactors implements IReactors {
|
|
|
140
144
|
|
|
141
145
|
/** Stops observations permanently when the owning client is disposed. */
|
|
142
146
|
dispose(): void {
|
|
147
|
+
this._unsubscribeDisconnected();
|
|
143
148
|
this._disposed = true;
|
|
144
149
|
this._registered = false;
|
|
145
150
|
this.disconnectAll();
|
|
@@ -168,43 +173,50 @@ export class Reactors implements IReactors {
|
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
if (this._disposed) return;
|
|
176
|
+
const registrations = [...this._reactors].map(([id, type]) => ({
|
|
177
|
+
id, type, eventTypes: getReactorEventTypes(type, this._clientArtifacts.eventTypes)
|
|
178
|
+
})).map(registration => ({ ...registration, validateInstance: createHandlerInstanceValidator(registration.type,
|
|
179
|
+
registration.eventTypes.flatMap(entry => [entry.methodName, entry.replayMethodName].filter((name): name is string => !!name))) }));
|
|
171
180
|
this._logger.info('Registering reactors', { count: this._reactors.size });
|
|
172
|
-
for (const
|
|
173
|
-
this.startObservation(id,
|
|
181
|
+
for (const { id, type, eventTypes, validateInstance } of registrations) {
|
|
182
|
+
this.startObservation(id, type, eventTypes, validateInstance);
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
this._registered = true;
|
|
177
186
|
}
|
|
178
187
|
|
|
179
|
-
private startObservation(id: string, reactorType: Constructor
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
private startObservation(id: string, reactorType: Constructor,
|
|
189
|
+
eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
|
|
190
|
+
runInBackgroundContext(() => {
|
|
191
|
+
if (this._disposed) return;
|
|
192
|
+
const metadata = getReactorMetadata(reactorType)!;
|
|
193
|
+
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
194
|
+
|
|
195
|
+
this._logger.info('Starting reactor observation', {
|
|
196
|
+
reactorId: id,
|
|
197
|
+
eventSequenceId,
|
|
198
|
+
handlerCount: eventTypes.length,
|
|
199
|
+
handlers: eventTypes.map(e => e.methodName)
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
void this.runObservation(id, reactorType, eventSequenceId, eventTypes, validateInstance);
|
|
190
203
|
});
|
|
191
|
-
|
|
192
|
-
void this.runObservation(id, reactorType, eventSequenceId, eventTypes);
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
private async runObservation(
|
|
196
207
|
id: string,
|
|
197
208
|
reactorType: Constructor,
|
|
198
209
|
eventSequenceId: string,
|
|
199
|
-
eventTypes: ReactorEventTypeEntry[]
|
|
210
|
+
eventTypes: ReactorEventTypeEntry[],
|
|
211
|
+
validateInstance: (instance: object) => void
|
|
200
212
|
): Promise<void> {
|
|
201
213
|
try {
|
|
202
|
-
await this.observeReactor(id, reactorType, eventSequenceId, eventTypes);
|
|
214
|
+
await this.observeReactor(id, reactorType, eventSequenceId, eventTypes, validateInstance);
|
|
203
215
|
} catch (error) {
|
|
204
|
-
this._logger.error('Reactor observation loop exited with error', { reactorId: id, error
|
|
216
|
+
this._logger.error('Reactor observation loop exited with error', { reactorId: id, error });
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
this.scheduleReobserve(id, reactorType);
|
|
219
|
+
this.scheduleReobserve(id, reactorType, eventTypes, validateInstance);
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
/**
|
|
@@ -216,21 +228,21 @@ export class Reactors implements IReactors {
|
|
|
216
228
|
* the whole client reconnects. Both endings are therefore retried, and the delay
|
|
217
229
|
* keeps a stream that keeps ending from becoming a hot loop.
|
|
218
230
|
*/
|
|
219
|
-
private scheduleReobserve(id: string, reactorType: Constructor): void {
|
|
231
|
+
private scheduleReobserve(id: string, reactorType: Constructor, eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
|
|
220
232
|
// A disconnect clears the registration; the reconnect re-registers every
|
|
221
233
|
// reactor from scratch, so retrying here as well would double up.
|
|
222
234
|
if (!this._registered || this._disposed) {
|
|
223
235
|
return;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
|
-
const handle = setTimeout(() => {
|
|
238
|
+
const handle = runInBackgroundContext(() => setTimeout(() => {
|
|
227
239
|
if (!this._registered || this._disposed) {
|
|
228
240
|
return;
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
this._logger.info('Re-establishing reactor observation', { reactorId: id });
|
|
232
|
-
this.startObservation(id, reactorType);
|
|
233
|
-
}, Reactors._reobserveDelayMs);
|
|
244
|
+
this.startObservation(id, reactorType, eventTypes, validateInstance);
|
|
245
|
+
}, Reactors._reobserveDelayMs));
|
|
234
246
|
|
|
235
247
|
handle.unref?.();
|
|
236
248
|
}
|
|
@@ -239,7 +251,8 @@ export class Reactors implements IReactors {
|
|
|
239
251
|
id: string,
|
|
240
252
|
reactorType: Constructor,
|
|
241
253
|
eventSequenceId: string,
|
|
242
|
-
eventTypes: ReactorEventTypeEntry[]
|
|
254
|
+
eventTypes: ReactorEventTypeEntry[],
|
|
255
|
+
validateInstance: (instance: object) => void
|
|
243
256
|
): Promise<void> {
|
|
244
257
|
const queue = new AsyncQueue<ReactorMessage>();
|
|
245
258
|
const controller = new AbortController();
|
|
@@ -274,6 +287,7 @@ export class Reactors implements IReactors {
|
|
|
274
287
|
|
|
275
288
|
try {
|
|
276
289
|
const reactorInstance = this._artifactActivator ? undefined : new (reactorType as new () => Record<string, Function>)();
|
|
290
|
+
if (reactorInstance) validateInstance(reactorInstance);
|
|
277
291
|
const services: ReactorServices | undefined = this._eventStore && {
|
|
278
292
|
eventStore: this._eventStore, readModels: this._eventStore.readModels, signal: controller.signal
|
|
279
293
|
};
|
|
@@ -286,7 +300,6 @@ export class Reactors implements IReactors {
|
|
|
286
300
|
|
|
287
301
|
this._logger.debug('Received events to observe', {
|
|
288
302
|
reactorId: id,
|
|
289
|
-
partition: eventsToObserve.Partition,
|
|
290
303
|
count: eventsToObserve.Events.length,
|
|
291
304
|
replayState: eventsToObserve.ReplayState
|
|
292
305
|
});
|
|
@@ -299,14 +312,17 @@ export class Reactors implements IReactors {
|
|
|
299
312
|
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
300
313
|
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
301
314
|
replayState: eventsToObserve.ReplayState
|
|
302
|
-
}, this._artifactActivator, artifact =>
|
|
303
|
-
|
|
304
|
-
|
|
315
|
+
}, this._artifactActivator, artifact => {
|
|
316
|
+
validateInstance(artifact.instance);
|
|
317
|
+
return runActivated(artifact, () =>
|
|
318
|
+
notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
|
|
319
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState });
|
|
320
|
+
}, this._connection.logger);
|
|
305
321
|
} else if (reactorInstance) {
|
|
306
322
|
await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
|
|
307
323
|
}
|
|
308
324
|
} catch (err) {
|
|
309
|
-
this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error:
|
|
325
|
+
this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: err });
|
|
310
326
|
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
311
327
|
exceptionMessages.push(String(err.processingError));
|
|
312
328
|
}
|
|
@@ -348,7 +364,6 @@ export class Reactors implements IReactors {
|
|
|
348
364
|
}
|
|
349
365
|
|
|
350
366
|
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
351
|
-
this._logger.debug('Event content', { reactorId: id, eventTypeId, contentKeys: Object.keys(content), rawContent: event.Content.substring(0, 200) });
|
|
352
367
|
const context = toClientEventContext(event.Context!);
|
|
353
368
|
|
|
354
369
|
this._logger.info('Invoking reactor handler', {
|
|
@@ -364,7 +379,7 @@ export class Reactors implements IReactors {
|
|
|
364
379
|
|
|
365
380
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
366
381
|
} catch (err) {
|
|
367
|
-
this._logger.error('Error handling event in reactor', { reactorId: id, error:
|
|
382
|
+
this._logger.error('Error handling event in reactor', { reactorId: id, error: err });
|
|
368
383
|
exceptionMessages.push(String(err));
|
|
369
384
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
370
385
|
state = ObservationState.Failed;
|
|
@@ -386,9 +401,12 @@ export class Reactors implements IReactors {
|
|
|
386
401
|
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
387
402
|
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
388
403
|
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
389
|
-
}, this._artifactActivator,
|
|
404
|
+
}, this._artifactActivator, artifact => {
|
|
405
|
+
validateInstance(artifact.instance);
|
|
406
|
+
return processEvents(artifact);
|
|
407
|
+
}, this._connection.logger);
|
|
390
408
|
} catch (err) {
|
|
391
|
-
this._logger.error('Error activating reactor', { reactorId: id, error:
|
|
409
|
+
this._logger.error('Error activating reactor', { reactorId: id, error: err });
|
|
392
410
|
exceptionMessages.push(String(err));
|
|
393
411
|
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
394
412
|
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
@@ -419,7 +437,7 @@ export class Reactors implements IReactors {
|
|
|
419
437
|
if (!this._queues.has(id)) {
|
|
420
438
|
this._logger.debug('Reactor observation stream closed cleanly', { reactorId: id });
|
|
421
439
|
} else {
|
|
422
|
-
this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error:
|
|
440
|
+
this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error: err });
|
|
423
441
|
}
|
|
424
442
|
} finally {
|
|
425
443
|
// Only retire our own queue: a reconnect can already have replaced it,
|
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
5
|
import { onceOnly, isOnceOnly } from './onceOnly.js';
|
|
6
6
|
import { replay, getReplayEventType } from './replay.js';
|
|
7
|
+
import { handles } from '../index.js';
|
|
8
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
7
9
|
|
|
8
10
|
class EventForReplay {}
|
|
9
11
|
|
|
10
12
|
@onceOnly()
|
|
11
13
|
class StandardReactor {
|
|
14
|
+
@handles(EventForReplay)
|
|
15
|
+
@onceOnly()
|
|
16
|
+
notify() {}
|
|
17
|
+
|
|
12
18
|
@onceOnly()
|
|
13
19
|
eventForReplay() {}
|
|
14
20
|
|
|
@@ -31,6 +37,8 @@ describe('standard reactor policy decorators', () => {
|
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
it('marks methods without modifying the class or other methods', () => {
|
|
40
|
+
expect(getHandledEventType(StandardReactor.prototype.notify)).toBe(EventForReplay);
|
|
41
|
+
expect(isOnceOnly(StandardReactor.prototype.notify)).toBe(true);
|
|
34
42
|
expect(isOnceOnly(StandardReactor.prototype.eventForReplay)).toBe(true);
|
|
35
43
|
expect(isOnceOnly(StandardReactor.prototype.replayEventForReplay)).toBe(false);
|
|
36
44
|
expect(getReplayEventType(StandardReactor.prototype.replayEventForReplay)).toBe(true);
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { EventObservationState } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
7
|
+
import { handles, onceOnly, replay } from '../index.js';
|
|
8
|
+
import { getReactorEventTypes, selectReactorHandler } from '../ReactorDispatcher.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
@eventType('selected-handler')
|
|
13
|
+
class AuthorRegistered {}
|
|
14
|
+
|
|
15
|
+
function selected(type: new () => object, state: EventObservationState) {
|
|
16
|
+
return selectReactorHandler(getReactorEventTypes(type, [AuthorRegistered]), type, undefined, 'selected-handler', state)!;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('when selecting an explicit handler with a replay alternative', () => {
|
|
20
|
+
class Observer {
|
|
21
|
+
@handles(AuthorRegistered)
|
|
22
|
+
@onceOnly()
|
|
23
|
+
notify() {}
|
|
24
|
+
|
|
25
|
+
@replay(AuthorRegistered)
|
|
26
|
+
rebuild() {}
|
|
27
|
+
}
|
|
28
|
+
it('should select the explicit method for live events', () => {
|
|
29
|
+
selected(Observer, EventObservationState.Initial).methodName!.should.equal('notify');
|
|
30
|
+
});
|
|
31
|
+
it('should select the replay alternative even when the live method is onceOnly', () => {
|
|
32
|
+
selected(Observer, EventObservationState.Replay).should.deep.equal({ methodName: 'rebuild', isReplay: true, skipReplay: false });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('when selecting an explicit handler without a replay alternative', () => {
|
|
37
|
+
class Observer { @handles(AuthorRegistered) notify() {} }
|
|
38
|
+
let handler: ReturnType<typeof selected>;
|
|
39
|
+
beforeEach(() => { handler = selected(Observer, EventObservationState.Replay); });
|
|
40
|
+
it('should use the explicit method during replay too', () => {
|
|
41
|
+
handler.should.deep.equal({ methodName: 'notify', isReplay: true, skipReplay: false });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('when an explicit handler is onceOnly', () => {
|
|
46
|
+
class Observer { @onceOnly() @handles(AuthorRegistered) notify() {} }
|
|
47
|
+
it('should not skip live delivery', () => {
|
|
48
|
+
selected(Observer, EventObservationState.Initial).skipReplay.should.be.false;
|
|
49
|
+
});
|
|
50
|
+
it('should skip replay delivery', () => {
|
|
51
|
+
selected(Observer, EventObservationState.Replay).skipReplay.should.be.true;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('when a method declares both live and replay roles', () => {
|
|
56
|
+
class Observer { @handles(AuthorRegistered) @replay(AuthorRegistered) notify() {} }
|
|
57
|
+
it('should reject the ambiguous declaration during discovery', () => {
|
|
58
|
+
(() => getReactorEventTypes(Observer, [AuthorRegistered])).should.throw(/notify.*Observer.*cannot combine @handles and @replay/);
|
|
59
|
+
});
|
|
60
|
+
it('should reject replay above handles too', () => {
|
|
61
|
+
class Reversed { @replay(AuthorRegistered) @handles(AuthorRegistered) notify() {} }
|
|
62
|
+
(() => getReactorEventTypes(Reversed, [AuthorRegistered])).should.throw(/notify.*Reversed.*cannot combine @handles and @replay/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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 ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
|
|
10
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
11
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
12
|
+
import { handles, reactor, Reactors } from '../index.js';
|
|
13
|
+
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
@eventType('field-reactor-event')
|
|
17
|
+
class AuthorRegistered {}
|
|
18
|
+
const calls: string[] = [];
|
|
19
|
+
class Base {
|
|
20
|
+
@handles(AuthorRegistered) notify() { calls.push('base'); }
|
|
21
|
+
beginReplay() { calls.push('replay'); }
|
|
22
|
+
endReplay() {}
|
|
23
|
+
}
|
|
24
|
+
@reactor('field-reactor')
|
|
25
|
+
class Derived extends Base {
|
|
26
|
+
override notify = () => { calls.push('field'); };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const replayState of [ReplayState.REPLAY_STATE_None, ReplayState.BeginReplay]) {
|
|
30
|
+
describe(`when activating a reactor with a field override for replay state ${replayState}`, () => {
|
|
31
|
+
let result: ReactorMessage;
|
|
32
|
+
let disposed: boolean;
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
calls.length = 0;
|
|
35
|
+
disposed = false;
|
|
36
|
+
const lifecycle = new ConnectionLifecycle();
|
|
37
|
+
let finish!: () => void;
|
|
38
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
39
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
40
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
41
|
+
await messages.next();
|
|
42
|
+
yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
|
|
43
|
+
EventType: EventType.fromPartial({ Id: 'field-reactor-event', Generation: 1 }), SequenceNumber: 1n
|
|
44
|
+
}) }], Partition: 'author', ReplayState: replayState, InitialState: '' };
|
|
45
|
+
result = (await messages.next()).value!;
|
|
46
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
47
|
+
finish();
|
|
48
|
+
}
|
|
49
|
+
const reactors = new Reactors({ reactors: [Derived], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider,
|
|
50
|
+
{ reactors: { observe: stream } } as unknown as ChronicleConnection, 'store', 'tenant', lifecycle, {} as IEventLog,
|
|
51
|
+
undefined, { readModels: {} } as IEventStore, type => ({ instance: new type(), dispose: () => { disposed = true; } }));
|
|
52
|
+
try { await reactors.register(); await finished; } finally { reactors.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!.ExceptionMessages.join(' ').should.contain(
|
|
58
|
+
"Override 'notify' on 'Derived' hides @handles(AuthorRegistered) declared on 'Base'; use a method with @handles instead of an instance field.");
|
|
59
|
+
});
|
|
60
|
+
it('should release the rejected activation lease', () => { disposed.should.be.true; });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { EventContext as WireContext, EventObservationState, EventType, ObservationState, type ReactorMessage } from '@cratis/chronicle.contracts';
|
|
5
5
|
import { ReplayState } from '../../../index.js';
|
|
6
6
|
import { chai, describe, it, vi } from 'vitest';
|
|
7
|
-
import {
|
|
7
|
+
import type { IChronicleLogger } from '../../../logging/IChronicleLogger.js';
|
|
8
|
+
import type { ChronicleLogEntry } from '../../../logging/ChronicleLogEntry.js';
|
|
8
9
|
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
9
10
|
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
10
11
|
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
@@ -98,7 +99,7 @@ function delivery(events: ReturnType<typeof event>[], replayState = ReplayState.
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async function observe(type: Function, batches: ReturnType<typeof delivery>[], namespace = 'tenant-a',
|
|
101
|
-
activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void) {
|
|
102
|
+
activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void, logger?: IChronicleLogger) {
|
|
102
103
|
const acknowledgements: NonNullable<ReactorMessage['Content']>['Value1'][] = [];
|
|
103
104
|
const append = vi.fn().mockResolvedValue({ isSuccess: appendResult });
|
|
104
105
|
const appendMany = vi.fn().mockImplementation(async () => {
|
|
@@ -122,7 +123,7 @@ async function observe(type: Function, batches: ReturnType<typeof delivery>[], n
|
|
|
122
123
|
await lifecycle.disconnected(error => { throw error; });
|
|
123
124
|
finished();
|
|
124
125
|
}
|
|
125
|
-
const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
126
|
+
const connection = { logger, reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
126
127
|
const artifacts = { reactors: [type], eventTypes: [ActivationEvent] } as unknown as IClientArtifactsProvider;
|
|
127
128
|
await new Reactors(artifacts, connection, 'store', namespace, lifecycle, eventLog, undefined, store, activator).register();
|
|
128
129
|
await complete;
|
|
@@ -462,16 +463,15 @@ describe('when delivering reactor batches', () => {
|
|
|
462
463
|
});
|
|
463
464
|
|
|
464
465
|
it('should log disposal failure without changing successful acknowledgement', async () => {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
466
|
+
const entries: ChronicleLogEntry[] = [];
|
|
467
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: async () => { throw new Error('private cleanup details'); } });
|
|
468
|
+
const result = await observe(ActivationReactor, [delivery([event('private payload', 1n)])], 'tenant-a', activator,
|
|
469
|
+
true, undefined, { log: entry => entries.push(entry) });
|
|
470
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
471
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
|
|
472
|
+
entries.filter(entry => entry.attributes['exception.type'] === 'Error').should.have.lengthOf(1);
|
|
473
|
+
JSON.stringify(entries).should.not.contain('private cleanup details');
|
|
474
|
+
JSON.stringify(entries).should.not.contain('private payload');
|
|
475
|
+
JSON.stringify(entries).should.not.contain('book-1');
|
|
476
476
|
});
|
|
477
477
|
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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, EventObservationState, EventType, ObservationState, ReplayState, type ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
|
|
10
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
11
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
12
|
+
import { handles, reactor, Reactors, replay } from '../index.js';
|
|
13
|
+
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
@eventType('instance-reactor-event')
|
|
17
|
+
class AuthorRegistered {}
|
|
18
|
+
const calls: string[] = [];
|
|
19
|
+
|
|
20
|
+
@reactor('bound-conventional-reactor')
|
|
21
|
+
class BoundConventional {
|
|
22
|
+
readonly marker = 'bound conventional';
|
|
23
|
+
constructor() { this.authorRegistered = this.authorRegistered.bind(this); }
|
|
24
|
+
authorRegistered() { calls.push(this.marker); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@reactor('bound-explicit-reactor')
|
|
28
|
+
class BoundExplicit {
|
|
29
|
+
readonly marker = 'bound explicit';
|
|
30
|
+
constructor() { this.notify = this.notify.bind(this); }
|
|
31
|
+
@handles(AuthorRegistered)
|
|
32
|
+
notify() { calls.push(this.marker); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class BoundExplicitBase {
|
|
36
|
+
readonly marker = 'twice bound explicit';
|
|
37
|
+
constructor() { this.notify = this.notify.bind(this); }
|
|
38
|
+
@handles(AuthorRegistered)
|
|
39
|
+
notify() { calls.push(this.marker); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@reactor('twice-bound-explicit-reactor')
|
|
43
|
+
class TwiceBoundExplicit extends BoundExplicitBase {
|
|
44
|
+
constructor() { super(); this.notify = this.notify.bind(this); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Conventional {
|
|
48
|
+
authorRegistered() { calls.push('base'); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@reactor('field-conventional-reactor')
|
|
52
|
+
class FieldConventional extends Conventional {
|
|
53
|
+
override authorRegistered = () => { calls.push('field conventional'); };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@reactor('bound-replay-reactor')
|
|
57
|
+
class BoundReplay {
|
|
58
|
+
readonly marker = 'bound replay';
|
|
59
|
+
constructor() { this.restore = this.restore.bind(this); }
|
|
60
|
+
@replay(AuthorRegistered)
|
|
61
|
+
restore() { calls.push(this.marker); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Replay {
|
|
65
|
+
@replay(AuthorRegistered)
|
|
66
|
+
restore() { calls.push('base replay'); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@reactor('field-replay-reactor')
|
|
70
|
+
class FieldReplay extends Replay {
|
|
71
|
+
override restore = () => { calls.push('field replay'); };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const { type, expected, observationState } of [
|
|
75
|
+
{ type: BoundConventional, expected: 'bound conventional', observationState: EventObservationState.Initial },
|
|
76
|
+
{ type: BoundExplicit, expected: 'bound explicit', observationState: EventObservationState.Initial },
|
|
77
|
+
{ type: TwiceBoundExplicit, expected: 'twice bound explicit', observationState: EventObservationState.Initial },
|
|
78
|
+
{ type: FieldConventional, expected: 'field conventional', observationState: EventObservationState.Initial },
|
|
79
|
+
{ type: BoundReplay, expected: 'bound replay', observationState: EventObservationState.Replay },
|
|
80
|
+
{ type: FieldReplay, expected: 'field replay', observationState: EventObservationState.Replay }
|
|
81
|
+
]) {
|
|
82
|
+
describe(`when delivering to the ${expected} reactor handler without an activator`, () => {
|
|
83
|
+
let result: ReactorMessage;
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
calls.length = 0;
|
|
86
|
+
const lifecycle = new ConnectionLifecycle();
|
|
87
|
+
let finish!: () => void;
|
|
88
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
89
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
90
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
91
|
+
await messages.next();
|
|
92
|
+
yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
|
|
93
|
+
EventType: EventType.fromPartial({ Id: 'instance-reactor-event', Generation: 1 }), SequenceNumber: 1n,
|
|
94
|
+
ObservationState: observationState
|
|
95
|
+
}) }], Partition: 'author', ReplayState: ReplayState.REPLAY_STATE_None, InitialState: '' };
|
|
96
|
+
result = (await messages.next()).value!;
|
|
97
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
98
|
+
finish();
|
|
99
|
+
}
|
|
100
|
+
const reactors = new Reactors({ reactors: [type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider,
|
|
101
|
+
{ reactors: { observe: stream } } as unknown as ChronicleConnection, 'store', 'tenant', lifecycle, {} as IEventLog,
|
|
102
|
+
undefined, { readModels: {} } as IEventStore);
|
|
103
|
+
try { await reactors.register(); await finished; } finally { reactors.dispose(); }
|
|
104
|
+
});
|
|
105
|
+
it('should invoke the instance handler', () => { calls.should.deep.equal([expected]); });
|
|
106
|
+
it('should complete delivery', () => { result.Content!.Value1!.State.should.equal(ObservationState.Success); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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, EventObservationState, EventType, type ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
|
|
10
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
11
|
+
import { handles, onceOnly, reactor, Reactors, replay } from '../index.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
|
|
15
|
+
@eventType('legacy-delivered-handler')
|
|
16
|
+
class AuthorRegistered { constructor(readonly name = '') {} }
|
|
17
|
+
|
|
18
|
+
const calls: string[] = [];
|
|
19
|
+
@reactor('legacy-delivery')
|
|
20
|
+
class AuthorReactor {
|
|
21
|
+
@handles(AuthorRegistered)
|
|
22
|
+
@onceOnly()
|
|
23
|
+
notify(event: AuthorRegistered) { calls.push(`live:${event.name}`); }
|
|
24
|
+
}
|
|
25
|
+
@reactor('legacy-delivery-with-replay')
|
|
26
|
+
class ReplayAuthorReactor extends AuthorReactor {
|
|
27
|
+
@replay(AuthorRegistered)
|
|
28
|
+
rebuild(event: AuthorRegistered) { calls.push(`replay:${event.name}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const [type, expected] of [
|
|
32
|
+
[AuthorReactor, ['live:Ada']],
|
|
33
|
+
[ReplayAuthorReactor, ['live:Ada', 'replay:Ada']]
|
|
34
|
+
] as const) {
|
|
35
|
+
describe(`when observing live and replay events with explicit legacy handlers on ${type.name}`, () => {
|
|
36
|
+
let registration: ReactorMessage;
|
|
37
|
+
let result: ReactorMessage;
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
calls.length = 0;
|
|
40
|
+
const lifecycle = new ConnectionLifecycle();
|
|
41
|
+
let finish!: () => void;
|
|
42
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
43
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
44
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
45
|
+
registration = (await messages.next()).value!;
|
|
46
|
+
yield {
|
|
47
|
+
Events: [EventObservationState.Initial, EventObservationState.Replay].map((state, index) => ({
|
|
48
|
+
Context: EventContext.fromPartial({
|
|
49
|
+
EventType: EventType.fromPartial({ Id: 'legacy-delivered-handler', Generation: 1 }),
|
|
50
|
+
SequenceNumber: BigInt(index + 1), ObservationState: state
|
|
51
|
+
}),
|
|
52
|
+
Content: JSON.stringify({ name: 'Ada' })
|
|
53
|
+
})),
|
|
54
|
+
Partition: 'author', ReplayState: 0, InitialState: ''
|
|
55
|
+
};
|
|
56
|
+
result = (await messages.next()).value!;
|
|
57
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
58
|
+
finish();
|
|
59
|
+
}
|
|
60
|
+
const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
61
|
+
const artifacts = { reactors: [type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider;
|
|
62
|
+
const reactors = new Reactors(artifacts, connection, 'store', 'tenant', lifecycle, {} as IEventLog);
|
|
63
|
+
try {
|
|
64
|
+
await reactors.register();
|
|
65
|
+
await finished;
|
|
66
|
+
} finally { reactors.dispose(); }
|
|
67
|
+
});
|
|
68
|
+
it('should subscribe to the explicitly named event type', () => {
|
|
69
|
+
registration.Content!.Value0!.Reactor!.EventTypes.map(entry => entry.EventType!.Id)
|
|
70
|
+
.should.deep.equal(['legacy-delivered-handler']);
|
|
71
|
+
});
|
|
72
|
+
it('should invoke only the appropriate handlers', () => { calls.should.deep.equal(expected); });
|
|
73
|
+
it('should acknowledge both deliveries', () => {
|
|
74
|
+
result.Content!.Value1!.LastSuccessfulObservation.should.equal(2n);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|