@cratis/chronicle 6.15.0 → 6.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChronicleClient.ts +1 -1
- package/ChronicleOptions.ts +9 -1
- package/EventStore.ts +5 -3
- package/artifacts/ActivatedArtifact.ts +12 -0
- package/artifacts/ArtifactActivationContext.ts +37 -0
- package/artifacts/ArtifactDelivery.ts +8 -0
- package/artifacts/ArtifactKind.ts +8 -0
- package/artifacts/ClientArtifactsActivator.ts +10 -0
- package/artifacts/for_ChronicleOptions/when_configuring/with_an_artifact_activator.spec.ts +21 -0
- package/artifacts/index.ts +6 -0
- package/artifacts/withActivatedArtifact.ts +32 -0
- package/dist/ChronicleClient.js +1 -1
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +4 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +5 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +2 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +3 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/artifacts/ActivatedArtifact.d.ts +10 -0
- package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -0
- package/dist/artifacts/ActivatedArtifact.js +4 -0
- package/dist/artifacts/ActivatedArtifact.js.map +1 -0
- package/dist/artifacts/ArtifactActivationContext.d.ts +34 -0
- package/dist/artifacts/ArtifactActivationContext.d.ts.map +1 -0
- package/dist/artifacts/ArtifactActivationContext.js +4 -0
- package/dist/artifacts/ArtifactActivationContext.js.map +1 -0
- package/dist/artifacts/ArtifactDelivery.d.ts +6 -0
- package/dist/artifacts/ArtifactDelivery.d.ts.map +1 -0
- package/dist/artifacts/ArtifactDelivery.js +9 -0
- package/dist/artifacts/ArtifactDelivery.js.map +1 -0
- package/dist/artifacts/ArtifactKind.d.ts +6 -0
- package/dist/artifacts/ArtifactKind.d.ts.map +1 -0
- package/dist/artifacts/ArtifactKind.js +9 -0
- package/dist/artifacts/ArtifactKind.js.map +1 -0
- package/dist/artifacts/ClientArtifactsActivator.d.ts +6 -0
- package/dist/artifacts/ClientArtifactsActivator.d.ts.map +1 -0
- package/dist/artifacts/ClientArtifactsActivator.js +4 -0
- package/dist/artifacts/ClientArtifactsActivator.js.map +1 -0
- package/dist/artifacts/index.d.ts +5 -0
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +2 -0
- package/dist/artifacts/index.js.map +1 -1
- package/dist/artifacts/withActivatedArtifact.d.ts +9 -0
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -0
- package/dist/artifacts/withActivatedArtifact.js +24 -0
- package/dist/artifacts/withActivatedArtifact.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/projections/InvalidEventContextPropertyError.d.ts +10 -0
- package/dist/projections/InvalidEventContextPropertyError.d.ts.map +1 -0
- package/dist/projections/InvalidEventContextPropertyError.js +16 -0
- package/dist/projections/InvalidEventContextPropertyError.js.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +18 -3
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/declarative/AllSetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/AllSetBuilder.js +2 -1
- package/dist/projections/declarative/AllSetBuilder.js.map +1 -1
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +3 -2
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +2 -1
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/RemovedWithBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/RemovedWithBuilder.js +2 -1
- package/dist/projections/declarative/RemovedWithBuilder.js.map +1 -1
- package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/SetBuilder.js +2 -1
- package/dist/projections/declarative/SetBuilder.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts +3 -0
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -0
- package/dist/projections/eventContextPropertyExpression.js +69 -0
- package/dist/projections/eventContextPropertyExpression.js.map +1 -0
- package/dist/projections/index.d.ts +1 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/invalidEventContextPropertyInProjection.d.ts +4 -0
- package/dist/projections/invalidEventContextPropertyInProjection.d.ts.map +1 -0
- package/dist/projections/invalidEventContextPropertyInProjection.js +7 -0
- package/dist/projections/invalidEventContextPropertyInProjection.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +2 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/reactors/ReactorServices.d.ts +12 -0
- package/dist/reactors/ReactorServices.d.ts.map +1 -0
- package/dist/reactors/ReactorServices.js +4 -0
- package/dist/reactors/ReactorServices.js.map +1 -0
- package/dist/reactors/Reactors.d.ts +5 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +99 -39
- 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/readModels/rootReadModelTypes.d.ts.map +1 -1
- package/dist/readModels/rootReadModelTypes.js +12 -1
- package/dist/readModels/rootReadModelTypes.js.map +1 -1
- package/dist/reducers/Reducers.d.ts +5 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +73 -28
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/projections/InvalidEventContextPropertyError.ts +14 -0
- package/projections/ProjectionDefinitionCompiler.ts +19 -3
- package/projections/declarative/AllSetBuilder.ts +2 -1
- package/projections/declarative/CompositeKeyBuilder.spec.ts +1 -1
- package/projections/declarative/FromBuilder.ts +3 -2
- package/projections/declarative/JoinBuilder.ts +2 -1
- package/projections/declarative/ProjectionBuilderFor.spec.ts +4 -4
- package/projections/declarative/RemovedWithBuilder.ts +2 -1
- package/projections/declarative/SetBuilder.ts +2 -1
- package/projections/eventContextExpressions.spec.ts +273 -0
- package/projections/eventContextPropertyExpression.ts +72 -0
- package/projections/index.ts +1 -0
- package/projections/invalidEventContextPropertyInProjection.ts +13 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +2 -1
- package/reactors/ReactorServices.ts +15 -0
- package/reactors/Reactors.ts +101 -42
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +386 -0
- package/reactors/index.ts +1 -0
- package/readModels/rootReadModelTypes.ts +11 -1
- package/reducers/Reducers.ts +75 -31
- package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +177 -0
package/reactors/Reactors.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import 'reflect-metadata';
|
|
5
5
|
import { diag } from '@opentelemetry/api';
|
|
6
6
|
import { Constructor } from '@cratis/fundamentals';
|
|
7
|
-
import { EventObservationState, ObservationState, ReactorMessage } from '@cratis/chronicle.contracts';
|
|
7
|
+
import { EventObservationState, ObservationState, ReactorMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
8
8
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
9
9
|
import { ChronicleConnection } from '../connection/index.js';
|
|
10
10
|
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
|
|
@@ -21,6 +21,13 @@ import { getReactorMetadata } from './reactor.js';
|
|
|
21
21
|
import { isOnceOnly } from './onceOnly.js';
|
|
22
22
|
import { getReplayEventType } from './replay.js';
|
|
23
23
|
import type { ReactorResultHandler } from './ReactorResultHandler.js';
|
|
24
|
+
import type { ReactorServices } from './ReactorServices.js';
|
|
25
|
+
import type { IEventStore } from '../IEventStore.js';
|
|
26
|
+
import type { ClientArtifactsActivator } from '../artifacts/ClientArtifactsActivator.js';
|
|
27
|
+
import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
|
|
28
|
+
import { ArtifactKind } from '../artifacts/ArtifactKind.js';
|
|
29
|
+
import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
|
|
30
|
+
import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
|
|
24
31
|
|
|
25
32
|
/** Expression used to partition reactor observations by event source ID. */
|
|
26
33
|
const EVENT_SOURCE_ID_KEY = '$eventSourceId';
|
|
@@ -125,7 +132,9 @@ export class Reactors implements IReactors {
|
|
|
125
132
|
private readonly _namespace: string,
|
|
126
133
|
lifecycle: ConnectionLifecycle,
|
|
127
134
|
private readonly _eventLog: IEventLog,
|
|
128
|
-
private readonly _resultHandler?: ReactorResultHandler
|
|
135
|
+
private readonly _resultHandler?: ReactorResultHandler,
|
|
136
|
+
private readonly _eventStore?: IEventStore,
|
|
137
|
+
private readonly _artifactActivator?: ClientArtifactsActivator
|
|
129
138
|
) {
|
|
130
139
|
this._lifecycle = lifecycle;
|
|
131
140
|
lifecycle.onDisconnected(async () => {
|
|
@@ -270,7 +279,10 @@ export class Reactors implements IReactors {
|
|
|
270
279
|
});
|
|
271
280
|
|
|
272
281
|
try {
|
|
273
|
-
const reactorInstance = new (reactorType as new () => Record<string, Function>)();
|
|
282
|
+
const reactorInstance = this._artifactActivator ? undefined : new (reactorType as new () => Record<string, Function>)();
|
|
283
|
+
const services: ReactorServices | undefined = this._eventStore && {
|
|
284
|
+
eventStore: this._eventStore, readModels: this._eventStore.readModels, signal: controller.signal
|
|
285
|
+
};
|
|
274
286
|
|
|
275
287
|
for await (const eventsToObserve of this._connection.reactors.observe(queue, { signal: controller.signal })) {
|
|
276
288
|
let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
@@ -286,7 +298,18 @@ export class Reactors implements IReactors {
|
|
|
286
298
|
});
|
|
287
299
|
|
|
288
300
|
try {
|
|
289
|
-
|
|
301
|
+
if (this._artifactActivator && eventsToObserve.ReplayState !== ReplayState.REPLAY_STATE_None) {
|
|
302
|
+
if (!services) throw new Error('Reactor activation requires the owning event store.');
|
|
303
|
+
await withActivatedArtifact(reactorType, {
|
|
304
|
+
kind: ArtifactKind.Reactor, artifactId: id, eventStore: services.eventStore,
|
|
305
|
+
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
306
|
+
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
307
|
+
replayState: eventsToObserve.ReplayState
|
|
308
|
+
}, this._artifactActivator, artifact => runActivated(artifact, () =>
|
|
309
|
+
notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition)));
|
|
310
|
+
} else if (reactorInstance) {
|
|
311
|
+
await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
|
|
312
|
+
}
|
|
290
313
|
} catch (err) {
|
|
291
314
|
this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
|
|
292
315
|
exceptionMessages.push(String(err));
|
|
@@ -294,56 +317,92 @@ export class Reactors implements IReactors {
|
|
|
294
317
|
state = ObservationState.Failed;
|
|
295
318
|
}
|
|
296
319
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
const selectHandler = (event: typeof eventsToObserve.Events[number]) => {
|
|
321
|
+
const entry = eventTypes.find(candidate => candidate.id === event.Context?.EventType?.Id);
|
|
322
|
+
if (!entry) return undefined;
|
|
323
|
+
const isReplay = (event.Context!.ObservationState & EventObservationState.Replay) !== 0;
|
|
324
|
+
const methodName = isReplay ? (entry.replayMethodName ?? entry.methodName) : entry.methodName;
|
|
325
|
+
const method = methodName ? (reactorInstance?.[methodName] ?? (reactorType.prototype as Record<string, Function>)[methodName]) : undefined;
|
|
326
|
+
return { methodName, isReplay, skipReplay: isReplay && method !== undefined && isOnceOnly(method) };
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const processEvents = async (artifact: ActivatedArtifact<Record<string, Function>>) => {
|
|
330
|
+
for (const event of state === ObservationState.Failed ? [] : eventsToObserve.Events) {
|
|
331
|
+
try {
|
|
332
|
+
const eventTypeId = event.Context?.EventType?.Id;
|
|
333
|
+
if (!eventTypeId) {
|
|
334
|
+
this._logger.warn('Event missing event type context', { reactorId: id });
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
304
337
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
const selection = selectHandler(event);
|
|
339
|
+
if (!selection) {
|
|
340
|
+
this._logger.debug('No reactor handler found', { reactorId: id, eventTypeId });
|
|
341
|
+
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
311
344
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
345
|
+
const { methodName, isReplay, skipReplay } = selection;
|
|
346
|
+
if (!methodName || skipReplay) {
|
|
347
|
+
if (skipReplay) {
|
|
348
|
+
this._logger.debug('Reactor handler skipped for replay', { reactorId: id, eventTypeId, method: methodName });
|
|
349
|
+
} else {
|
|
350
|
+
this._logger.debug('No reactor handler found', { reactorId: id, eventTypeId, isReplay });
|
|
351
|
+
}
|
|
352
|
+
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
353
|
+
continue;
|
|
319
354
|
}
|
|
320
|
-
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
355
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
356
|
+
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
357
|
+
this._logger.debug('Event content', { reactorId: id, eventTypeId, contentKeys: Object.keys(content), rawContent: event.Content.substring(0, 200) });
|
|
358
|
+
const context = toClientEventContext(event.Context!);
|
|
327
359
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
360
|
+
this._logger.info('Invoking reactor handler', {
|
|
361
|
+
reactorId: id,
|
|
362
|
+
method: methodName,
|
|
363
|
+
sequenceNumber: event.Context!.SequenceNumber.toString(),
|
|
364
|
+
eventTypeId
|
|
365
|
+
});
|
|
334
366
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
367
|
+
await runActivated(artifact, async () => {
|
|
368
|
+
const handlerResult = await artifact.instance[methodName](content, context, services);
|
|
369
|
+
await dispatchReactorSideEffects(this._eventLog, handlerResult, context, reactorType as Function,
|
|
370
|
+
this._eventStoreName, this._namespace, this._resultHandler);
|
|
371
|
+
});
|
|
338
372
|
|
|
339
|
-
|
|
373
|
+
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
this._logger.error('Error handling event in reactor', { reactorId: id, error: String(err) });
|
|
376
|
+
exceptionMessages.push(String(err));
|
|
377
|
+
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
378
|
+
state = ObservationState.Failed;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const firstInvocableEvent = this._artifactActivator && state === ObservationState.Success
|
|
385
|
+
? eventsToObserve.Events.find(event => {
|
|
386
|
+
const selection = selectHandler(event);
|
|
387
|
+
return !!selection?.methodName && !selection.skipReplay;
|
|
388
|
+
}) : undefined;
|
|
389
|
+
if (firstInvocableEvent && this._artifactActivator) {
|
|
390
|
+
try {
|
|
391
|
+
if (!services) throw new Error('Reactor activation requires the owning event store.');
|
|
392
|
+
await withActivatedArtifact(reactorType as new () => Record<string, Function>, {
|
|
393
|
+
kind: ArtifactKind.Reactor, artifactId: id, eventStore: services.eventStore,
|
|
394
|
+
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
395
|
+
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
396
|
+
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
397
|
+
}, this._artifactActivator, processEvents);
|
|
340
398
|
} catch (err) {
|
|
341
|
-
this._logger.error('Error
|
|
399
|
+
this._logger.error('Error activating reactor', { reactorId: id, error: String(err) });
|
|
342
400
|
exceptionMessages.push(String(err));
|
|
343
401
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
344
402
|
state = ObservationState.Failed;
|
|
345
|
-
break;
|
|
346
403
|
}
|
|
404
|
+
} else {
|
|
405
|
+
await processEvents({ instance: reactorInstance ?? {} });
|
|
347
406
|
}
|
|
348
407
|
|
|
349
408
|
queue.send({
|
|
@@ -0,0 +1,386 @@
|
|
|
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 as WireContext, EventObservationState, EventType, ObservationState, type ReactorMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { ReplayState } from '../../../index.js';
|
|
6
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
7
|
+
import { diag, DiagLogLevel, type DiagLogger } from '@opentelemetry/api';
|
|
8
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
9
|
+
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
10
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
11
|
+
import { ArtifactDelivery } from '../../../artifacts/ArtifactDelivery.js';
|
|
12
|
+
import { ArtifactKind } from '../../../artifacts/ArtifactKind.js';
|
|
13
|
+
import type { ArtifactActivationContext } from '../../../artifacts/ArtifactActivationContext.js';
|
|
14
|
+
import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
|
|
15
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
16
|
+
import type { IEventLog } from '../../../eventSequences/IEventLog.js';
|
|
17
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
18
|
+
import type { EventContext } from '../../../events/EventContext.js';
|
|
19
|
+
import type { ReactorServices } from '../../ReactorServices.js';
|
|
20
|
+
import { onceOnly } from '../../onceOnly.js';
|
|
21
|
+
import { reactor } from '../../reactor.js';
|
|
22
|
+
import { Reactors } from '../../Reactors.js';
|
|
23
|
+
|
|
24
|
+
chai.should();
|
|
25
|
+
|
|
26
|
+
@eventType('activation-event')
|
|
27
|
+
class ActivationEvent {
|
|
28
|
+
constructor(readonly value: string = '') {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const observed: string[] = [];
|
|
32
|
+
const instances: object[] = [];
|
|
33
|
+
const serviceReferences: ReactorServices[] = [];
|
|
34
|
+
|
|
35
|
+
@reactor('activation-reactor')
|
|
36
|
+
class ActivationReactor {
|
|
37
|
+
constructor() { instances.push(this); }
|
|
38
|
+
beginReplay() { observed.push('notification'); }
|
|
39
|
+
endReplay() {}
|
|
40
|
+
activationEvent(event: ActivationEvent, context: EventContext, services: ReactorServices) {
|
|
41
|
+
serviceReferences.push(services);
|
|
42
|
+
observed.push(`${event.value}:${context.eventSourceId}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@reactor('legacy-reactor')
|
|
47
|
+
class LegacyReactor {
|
|
48
|
+
constructor() { instances.push(this); }
|
|
49
|
+
activationEvent(event: ActivationEvent, context: EventContext) { observed.push(`${event.value}:${context.eventSourceId}`); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@reactor('dependency-reactor')
|
|
53
|
+
class DependencyReactor {
|
|
54
|
+
constructor(private readonly dependency: { record(value: string): void }) {}
|
|
55
|
+
activationEvent(event: ActivationEvent) { this.dependency.record(event.value); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@reactor('one-argument-reactor')
|
|
59
|
+
class OneArgumentReactor {
|
|
60
|
+
activationEvent(event: ActivationEvent) { observed.push(event.value); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@reactor('append-reactor')
|
|
64
|
+
class AppendReactor {
|
|
65
|
+
@onceOnly()
|
|
66
|
+
async activationEvent(event: ActivationEvent, context: EventContext, services: ReactorServices) {
|
|
67
|
+
serviceReferences.push(services);
|
|
68
|
+
const model = await services.readModels.findInstanceById(ActivationEvent, context.eventSourceId);
|
|
69
|
+
if (!model) throw new Error('model not ready');
|
|
70
|
+
const result = await services.eventStore.eventLog.append(context.eventSourceId, new ActivationEvent(model.value));
|
|
71
|
+
if (!result.isSuccess) throw new Error('append rejected');
|
|
72
|
+
observed.push(`${event.value}:${model.value}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@reactor('failing-notification-reactor')
|
|
77
|
+
class FailingNotificationReactor {
|
|
78
|
+
beginReplay() { throw new Error('notification failed'); }
|
|
79
|
+
endReplay() {}
|
|
80
|
+
activationEvent() { observed.push('unexpected event'); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@reactor('return-reactor')
|
|
84
|
+
class ReturnReactor {
|
|
85
|
+
activationEvent() { observed.push('handler'); return new ActivationEvent('effect'); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function event(value: string, sequence: bigint, observationState = EventObservationState.Initial) {
|
|
89
|
+
return { Content: JSON.stringify({ value }), Context: WireContext.fromPartial({
|
|
90
|
+
EventType: EventType.fromPartial({ Id: 'activation-event', Generation: 1 }),
|
|
91
|
+
EventSourceId: 'book-1', SequenceNumber: sequence, ObservationState: observationState
|
|
92
|
+
}) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function delivery(events: ReturnType<typeof event>[], replayState = ReplayState.REPLAY_STATE_None) {
|
|
96
|
+
return { Events: events, ReplayState: replayState, Partition: 'book-1', InitialState: '' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function observe(type: Function, batches: ReturnType<typeof delivery>[], namespace = 'tenant-a',
|
|
100
|
+
activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void) {
|
|
101
|
+
const acknowledgements: NonNullable<ReactorMessage['Content']>['Value1'][] = [];
|
|
102
|
+
const append = vi.fn().mockResolvedValue({ isSuccess: appendResult });
|
|
103
|
+
const appendMany = vi.fn().mockImplementation(async () => {
|
|
104
|
+
onAppendMany?.();
|
|
105
|
+
return [{ isSuccess: true, errors: [], constraintViolations: [] }];
|
|
106
|
+
});
|
|
107
|
+
const findInstanceById = vi.fn().mockResolvedValue(new ActivationEvent('current'));
|
|
108
|
+
const eventLog = { append, appendMany } as unknown as IEventLog;
|
|
109
|
+
const store = { name: { value: 'store' }, namespace: { value: namespace }, eventLog,
|
|
110
|
+
readModels: { findInstanceById } } as unknown as IEventStore;
|
|
111
|
+
const lifecycle = new ConnectionLifecycle();
|
|
112
|
+
let finished!: () => void;
|
|
113
|
+
const complete = new Promise<void>(resolve => { finished = resolve; });
|
|
114
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
115
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
116
|
+
await messages.next(); // registration
|
|
117
|
+
for (const batch of batches) {
|
|
118
|
+
yield batch;
|
|
119
|
+
acknowledgements.push((await messages.next()).value!.Content!.Value1);
|
|
120
|
+
}
|
|
121
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
122
|
+
finished();
|
|
123
|
+
}
|
|
124
|
+
const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
125
|
+
const artifacts = { reactors: [type], eventTypes: [ActivationEvent] } as unknown as IClientArtifactsProvider;
|
|
126
|
+
await new Reactors(artifacts, connection, 'store', namespace, lifecycle, eventLog, undefined, store, activator).register();
|
|
127
|
+
await complete;
|
|
128
|
+
return { acknowledgements, store, append, appendMany, findInstanceById };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('when delivering reactor batches', () => {
|
|
132
|
+
it('should reuse a default instance across deliveries and accept a two-argument handler', async () => {
|
|
133
|
+
instances.length = 0;
|
|
134
|
+
observed.length = 0;
|
|
135
|
+
await observe(LegacyReactor, [delivery([event('first', 1n)]), delivery([event('second', 2n)])]);
|
|
136
|
+
instances.length.should.equal(1);
|
|
137
|
+
observed.should.deep.equal(['first:book-1', 'second:book-1']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should accept a one-argument legacy handler', async () => {
|
|
141
|
+
observed.length = 0;
|
|
142
|
+
await observe(OneArgumentReactor, [delivery([event('first', 1n)])]);
|
|
143
|
+
observed.should.deep.equal(['first']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should await an asynchronous activator that resolves constructor dependencies', async () => {
|
|
147
|
+
observed.length = 0;
|
|
148
|
+
const activator: ClientArtifactsActivator = async type => {
|
|
149
|
+
await Promise.resolve();
|
|
150
|
+
return { instance: new type({ record: (value: string) => observed.push(value) }) };
|
|
151
|
+
};
|
|
152
|
+
const result = await observe(DependencyReactor, [delivery([event('injected', 1n)])], 'tenant-a', activator);
|
|
153
|
+
observed.should.deep.equal(['injected']);
|
|
154
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should activate once per batch, wrap each handler, and dispose each lease', async () => {
|
|
158
|
+
instances.length = 0;
|
|
159
|
+
observed.length = 0;
|
|
160
|
+
serviceReferences.length = 0;
|
|
161
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
162
|
+
const boundaries: string[] = [];
|
|
163
|
+
const activator: ClientArtifactsActivator = (type, context) => {
|
|
164
|
+
contexts.push(context);
|
|
165
|
+
return { instance: new type(), run: async callback => { boundaries.push('enter');
|
|
166
|
+
try { return await callback(); } finally { boundaries.push('exit'); } },
|
|
167
|
+
dispose: () => { boundaries.push('dispose'); } };
|
|
168
|
+
};
|
|
169
|
+
const result = await observe(ActivationReactor, [delivery([event('one', 1n), event('two', 2n)]), delivery([event('three', 3n)])], 'tenant-a', activator);
|
|
170
|
+
instances.length.should.equal(2);
|
|
171
|
+
contexts.map(context => context.delivery).should.deep.equal([ArtifactDelivery.Events, ArtifactDelivery.Events]);
|
|
172
|
+
contexts[0].kind.should.equal(ArtifactKind.Reactor);
|
|
173
|
+
contexts[0].artifactId.should.equal('activation-reactor');
|
|
174
|
+
contexts[0].eventSequenceId.should.equal('event-log');
|
|
175
|
+
contexts[0].partition.should.equal('book-1');
|
|
176
|
+
contexts[0].eventStore.should.equal(result.store);
|
|
177
|
+
contexts[0].readModels.should.equal(result.store.readModels);
|
|
178
|
+
if (contexts[0].delivery === ArtifactDelivery.Events) contexts[0].eventContext.sequenceNumber.should.equal(1n);
|
|
179
|
+
boundaries.should.deep.equal(['enter', 'exit', 'enter', 'exit', 'dispose', 'enter', 'exit', 'dispose']);
|
|
180
|
+
observed.should.deep.equal(['one:book-1', 'two:book-1', 'three:book-1']);
|
|
181
|
+
serviceReferences.every(services => services.eventStore === result.store && services.readModels === result.store.readModels).should.be.true;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should isolate concurrent activations and handler services by owning store and namespace', async () => {
|
|
185
|
+
observed.length = 0;
|
|
186
|
+
serviceReferences.length = 0;
|
|
187
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
188
|
+
let release!: () => void;
|
|
189
|
+
const bothActivated = new Promise<void>(resolve => { release = resolve; });
|
|
190
|
+
const activator: ClientArtifactsActivator = (type, context) => {
|
|
191
|
+
contexts.push(context);
|
|
192
|
+
if (contexts.length === 2) release();
|
|
193
|
+
return { instance: new type(), run: async callback => { await bothActivated; return callback(); } };
|
|
194
|
+
};
|
|
195
|
+
const [first, second] = await Promise.all([
|
|
196
|
+
observe(ActivationReactor, [delivery([event('first', 1n)])], 'tenant-a', activator),
|
|
197
|
+
observe(ActivationReactor, [delivery([event('second', 2n)])], 'tenant-b', activator)
|
|
198
|
+
]);
|
|
199
|
+
contexts.length.should.equal(2);
|
|
200
|
+
contexts.find(context => context.eventStore.namespace.value === 'tenant-a')!.eventStore.should.equal(first.store);
|
|
201
|
+
contexts.find(context => context.eventStore.namespace.value === 'tenant-b')!.eventStore.should.equal(second.store);
|
|
202
|
+
serviceReferences.length.should.equal(2);
|
|
203
|
+
serviceReferences.find(services => services.eventStore.namespace.value === 'tenant-a')!.eventStore.should.equal(first.store);
|
|
204
|
+
serviceReferences.find(services => services.eventStore.namespace.value === 'tenant-b')!.eventStore.should.equal(second.store);
|
|
205
|
+
serviceReferences[0].readModels.should.equal(serviceReferences[0].eventStore.readModels);
|
|
206
|
+
serviceReferences[1].readModels.should.equal(serviceReferences[1].eventStore.readModels);
|
|
207
|
+
observed.sort().should.deep.equal(['first:book-1', 'second:book-1']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should keep returned side-effect dispatch inside the same boundary', async () => {
|
|
211
|
+
const steps: string[] = [];
|
|
212
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(),
|
|
213
|
+
run: async callback => { steps.push('enter'); const value = await callback(); steps.push('exit'); return value; },
|
|
214
|
+
dispose: () => { steps.push('dispose'); } });
|
|
215
|
+
observed.length = 0;
|
|
216
|
+
const result = await observe(ReturnReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, true, () => steps.push('append'));
|
|
217
|
+
result.appendMany.mock.calls.length.should.equal(1);
|
|
218
|
+
observed.should.deep.equal(['handler']);
|
|
219
|
+
steps.should.deep.equal(['enter', 'append', 'exit', 'dispose']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should bind append and read-model services to each namespace, and fail on a rejected append', async () => {
|
|
223
|
+
observed.length = 0;
|
|
224
|
+
serviceReferences.length = 0;
|
|
225
|
+
const first = await observe(AppendReactor, [delivery([event('a', 1n)])], 'tenant-a');
|
|
226
|
+
const second = await observe(AppendReactor, [delivery([event('b', 1n)])], 'tenant-b', undefined, false);
|
|
227
|
+
first.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
228
|
+
second.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
229
|
+
second.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: append rejected']);
|
|
230
|
+
first.append.mock.calls.length.should.equal(1);
|
|
231
|
+
second.append.mock.calls.length.should.equal(1);
|
|
232
|
+
first.findInstanceById.mock.calls.length.should.equal(1);
|
|
233
|
+
second.findInstanceById.mock.calls.length.should.equal(1);
|
|
234
|
+
serviceReferences[0].eventStore.should.equal(first.store);
|
|
235
|
+
serviceReferences[1].eventStore.should.equal(second.store);
|
|
236
|
+
serviceReferences[0].readModels.should.equal(first.store.readModels);
|
|
237
|
+
serviceReferences[1].readModels.should.equal(second.store.readModels);
|
|
238
|
+
(serviceReferences[0].signal !== serviceReferences[1].signal).should.be.true;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should notify separately before processing events in the same message', async () => {
|
|
242
|
+
observed.length = 0;
|
|
243
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
244
|
+
const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type() }; };
|
|
245
|
+
const result = await observe(ActivationReactor, [delivery([event('live', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
|
|
246
|
+
observed.should.deep.equal(['notification', 'live:book-1']);
|
|
247
|
+
contexts.map(context => context.delivery).should.deep.equal([ArtifactDelivery.ReplayNotification, ArtifactDelivery.Events]);
|
|
248
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should notify and then process the same message without an activator', async () => {
|
|
252
|
+
observed.length = 0;
|
|
253
|
+
instances.length = 0;
|
|
254
|
+
const result = await observe(ActivationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)]);
|
|
255
|
+
observed.should.deep.equal(['notification', 'one:book-1']);
|
|
256
|
+
instances.length.should.equal(1);
|
|
257
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should fail the partition on notification failure and not process its events in either mode', async () => {
|
|
261
|
+
for (const activator of [undefined, (type => ({ instance: new type() })) as ClientArtifactsActivator]) {
|
|
262
|
+
observed.length = 0;
|
|
263
|
+
const result = await observe(FailingNotificationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
|
|
264
|
+
observed.should.deep.equal([]);
|
|
265
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
266
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: notification failed']);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should skip replay-only excluded handlers before activation and acknowledge the event', async () => {
|
|
271
|
+
let activations = 0;
|
|
272
|
+
const activator: ClientArtifactsActivator = type => { activations++; return { instance: new type() }; };
|
|
273
|
+
const result = await observe(AppendReactor, [delivery([event('one', 1n, EventObservationState.Replay)])], 'tenant-a', activator);
|
|
274
|
+
activations.should.equal(0);
|
|
275
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
276
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should use the first handled event context when earlier events are skipped', async () => {
|
|
280
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
281
|
+
const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type() }; };
|
|
282
|
+
const result = await observe(AppendReactor, [delivery([
|
|
283
|
+
event('replay', 1n, EventObservationState.Replay), event('live', 2n)
|
|
284
|
+
])], 'tenant-a', activator);
|
|
285
|
+
contexts.length.should.equal(1);
|
|
286
|
+
if (contexts[0].delivery === ArtifactDelivery.Events) contexts[0].eventContext.sequenceNumber.should.equal(2n);
|
|
287
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(2n);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should dispose after a failed handler execution without acknowledging the failed event', async () => {
|
|
291
|
+
let disposed = 0;
|
|
292
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: () => { disposed++; } });
|
|
293
|
+
const result = await observe(AppendReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, false);
|
|
294
|
+
disposed.should.equal(1);
|
|
295
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should fail event delivery on synchronous and asynchronous activation errors without disposing a lease', async () => {
|
|
299
|
+
for (const mode of ['sync', 'async'] as const) {
|
|
300
|
+
observed.length = 0;
|
|
301
|
+
const dispose = vi.fn();
|
|
302
|
+
const message = `${mode} reactor activation failed`;
|
|
303
|
+
const activator: ClientArtifactsActivator = type => {
|
|
304
|
+
const lease = { instance: new type(), dispose };
|
|
305
|
+
if (mode === 'sync') throw new Error(message);
|
|
306
|
+
return Promise.reject(new Error(message)).then(() => lease);
|
|
307
|
+
};
|
|
308
|
+
const result = await observe(ActivationReactor, [delivery([event('one', 1n)])], 'tenant-a', activator);
|
|
309
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
310
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([`Error: ${message}`]);
|
|
311
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
|
|
312
|
+
observed.should.deep.equal([]);
|
|
313
|
+
dispose.mock.calls.length.should.equal(0);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should fail replay notification on synchronous and asynchronous activation errors without disposing a lease', async () => {
|
|
318
|
+
for (const mode of ['sync', 'async'] as const) {
|
|
319
|
+
observed.length = 0;
|
|
320
|
+
const dispose = vi.fn();
|
|
321
|
+
const message = `${mode} reactor replay activation failed`;
|
|
322
|
+
const activator: ClientArtifactsActivator = type => {
|
|
323
|
+
const lease = { instance: new type(), dispose };
|
|
324
|
+
if (mode === 'sync') throw new Error(message);
|
|
325
|
+
return Promise.reject(new Error(message)).then(() => lease);
|
|
326
|
+
};
|
|
327
|
+
const result = await observe(ActivationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
|
|
328
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
329
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([`Error: ${message}`]);
|
|
330
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
|
|
331
|
+
observed.should.deep.equal([]);
|
|
332
|
+
dispose.mock.calls.length.should.equal(0);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should use a new activation signal after reconnect rather than retaining the prior generation', async () => {
|
|
337
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
338
|
+
const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type() }; };
|
|
339
|
+
const lifecycle = new ConnectionLifecycle();
|
|
340
|
+
const eventLog = { appendMany: async () => [] } as unknown as IEventLog;
|
|
341
|
+
const store = { eventLog, readModels: {} } as IEventStore;
|
|
342
|
+
let delivered = 0;
|
|
343
|
+
let firstDone!: () => void;
|
|
344
|
+
let secondDone!: () => void;
|
|
345
|
+
const first = new Promise<void>(resolve => { firstDone = resolve; });
|
|
346
|
+
const second = new Promise<void>(resolve => { secondDone = resolve; });
|
|
347
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
348
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
349
|
+
await messages.next();
|
|
350
|
+
const generation = ++delivered;
|
|
351
|
+
yield delivery([event('one', BigInt(generation))]);
|
|
352
|
+
await messages.next();
|
|
353
|
+
if (generation === 1) {
|
|
354
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
355
|
+
firstDone();
|
|
356
|
+
} else {
|
|
357
|
+
secondDone();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
361
|
+
const artifacts = { reactors: [ActivationReactor], eventTypes: [ActivationEvent] } as unknown as IClientArtifactsProvider;
|
|
362
|
+
const reactors = new Reactors(artifacts, connection, 'store', 'tenant-a', lifecycle, eventLog, undefined, store, activator);
|
|
363
|
+
await reactors.register();
|
|
364
|
+
await first;
|
|
365
|
+
contexts[0].signal.aborted.should.be.true;
|
|
366
|
+
await reactors.register();
|
|
367
|
+
await second;
|
|
368
|
+
contexts[1].signal.aborted.should.be.false;
|
|
369
|
+
(contexts[0].signal !== contexts[1].signal).should.be.true;
|
|
370
|
+
reactors.dispose();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should log disposal failure without changing successful acknowledgement', async () => {
|
|
374
|
+
const errors: string[] = [];
|
|
375
|
+
diag.setLogger({ error: (...values: unknown[]) => { errors.push(values.map(String).join(' ')); } } as DiagLogger, DiagLogLevel.ERROR);
|
|
376
|
+
try {
|
|
377
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: async () => { throw new Error('cleanup'); } });
|
|
378
|
+
const result = await observe(ActivationReactor, [delivery([event('live', 1n)])], 'tenant-a', activator);
|
|
379
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
380
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
|
|
381
|
+
errors.some(value => value.includes('Error disposing activated artifact')).should.be.true;
|
|
382
|
+
} finally {
|
|
383
|
+
diag.disable();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
package/reactors/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export type { ReactorMetadata } from './reactor.js';
|
|
|
9
9
|
export type { IReactors } from './IReactors.js';
|
|
10
10
|
export { Reactors } from './Reactors.js';
|
|
11
11
|
export type { ReactorResultHandler } from './ReactorResultHandler.js';
|
|
12
|
+
export type { ReactorServices } from './ReactorServices.js';
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { type Constructor } from '@cratis/fundamentals';
|
|
5
5
|
import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
6
6
|
import { getProjectionMetadata } from '../projections/declarative/projection.js';
|
|
7
|
+
import { InvalidEventContextPropertyError } from '../projections/InvalidEventContextPropertyError.js';
|
|
8
|
+
import { invalidEventContextPropertyInProjection } from '../projections/invalidEventContextPropertyInProjection.js';
|
|
9
|
+
import { getReadModelId } from './readModel.js';
|
|
7
10
|
import { ProjectionBuilderFor } from '../projections/declarative/ProjectionBuilderFor.js';
|
|
8
11
|
import type { ChildrenDefinitionLike } from '../projections/declarative/ProjectionBuilderCore.js';
|
|
9
12
|
import type { IProjectionFor } from '../projections/declarative/IProjectionFor.js';
|
|
@@ -53,7 +56,14 @@ function inspectReadModelTypes(artifacts: IClientArtifactsProvider): { models: C
|
|
|
53
56
|
if (!model) continue;
|
|
54
57
|
inspect(model);
|
|
55
58
|
const builder = new ProjectionBuilderFor<unknown>();
|
|
56
|
-
|
|
59
|
+
try {
|
|
60
|
+
(new type() as IProjectionFor<unknown>).define(builder);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof InvalidEventContextPropertyError) {
|
|
63
|
+
throw invalidEventContextPropertyInProjection(error, type.name, getReadModelId(model));
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
57
67
|
inspectDeclarative(model, builder.getSubobjectDefinitions());
|
|
58
68
|
}
|
|
59
69
|
for (const type of artifacts.reducers) {
|