@cratis/chronicle 6.15.0 → 6.16.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 +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/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/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/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/reducers/Reducers.ts +75 -31
- package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +177 -0
package/reducers/Reducers.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 { ObservationState, ReadModelObserverType, ReducerMessage } from '@cratis/chronicle.contracts';
|
|
7
|
+
import { ObservationState, ReadModelObserverType, ReducerMessage, 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,12 @@ import { getReadModelId } from '../readModels/readModel.js';
|
|
|
21
21
|
import { buildReadModelDefinition } from '../readModels/buildReadModelDefinition.js';
|
|
22
22
|
import { assertUniqueReadModelIds } from '../readModels/assertUniqueReadModelIds.js';
|
|
23
23
|
import { JsonSchemaGenerator } from '../schemas/index.js';
|
|
24
|
+
import type { IEventStore } from '../IEventStore.js';
|
|
25
|
+
import type { ClientArtifactsActivator } from '../artifacts/ClientArtifactsActivator.js';
|
|
26
|
+
import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
|
|
27
|
+
import { ArtifactKind } from '../artifacts/ArtifactKind.js';
|
|
28
|
+
import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
|
|
29
|
+
import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
|
|
24
30
|
|
|
25
31
|
/** Expression used to partition reducer observations by event source ID. */
|
|
26
32
|
const EVENT_SOURCE_ID_KEY = '$eventSourceId';
|
|
@@ -117,7 +123,9 @@ export class Reducers implements IReducers {
|
|
|
117
123
|
private readonly _eventStoreName: string,
|
|
118
124
|
private readonly _namespace: string,
|
|
119
125
|
lifecycle: ConnectionLifecycle,
|
|
120
|
-
private readonly _defaultSinkTypeId: string
|
|
126
|
+
private readonly _defaultSinkTypeId: string,
|
|
127
|
+
private readonly _eventStore?: IEventStore,
|
|
128
|
+
private readonly _artifactActivator?: ClientArtifactsActivator
|
|
121
129
|
) {
|
|
122
130
|
this._lifecycle = lifecycle;
|
|
123
131
|
lifecycle.onDisconnected(async () => {
|
|
@@ -333,7 +341,7 @@ export class Reducers implements IReducers {
|
|
|
333
341
|
});
|
|
334
342
|
|
|
335
343
|
try {
|
|
336
|
-
const reducerInstance = new (reducerType as new () => Record<string, Function>)();
|
|
344
|
+
const reducerInstance = this._artifactActivator ? undefined : new (reducerType as new () => Record<string, Function>)();
|
|
337
345
|
|
|
338
346
|
for await (const operation of this._connection.reducers.observe(queue, { signal: controller.signal })) {
|
|
339
347
|
let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
@@ -351,7 +359,18 @@ export class Reducers implements IReducers {
|
|
|
351
359
|
});
|
|
352
360
|
|
|
353
361
|
try {
|
|
354
|
-
|
|
362
|
+
if (this._artifactActivator && operation.ReplayState !== ReplayState.REPLAY_STATE_None) {
|
|
363
|
+
if (!this._eventStore) throw new Error('Reducer activation requires the owning event store.');
|
|
364
|
+
await withActivatedArtifact(reducerType, {
|
|
365
|
+
kind: ArtifactKind.Reducer, artifactId: id, eventStore: this._eventStore,
|
|
366
|
+
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
367
|
+
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
368
|
+
replayState: operation.ReplayState
|
|
369
|
+
}, this._artifactActivator, artifact => runActivated(artifact, () =>
|
|
370
|
+
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition)));
|
|
371
|
+
} else if (reducerInstance) {
|
|
372
|
+
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
373
|
+
}
|
|
355
374
|
} catch (err) {
|
|
356
375
|
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: String(err) });
|
|
357
376
|
exceptionMessages.push(String(err));
|
|
@@ -363,41 +382,66 @@ export class Reducers implements IReducers {
|
|
|
363
382
|
? JSON.parse(operation.InitialState) as unknown
|
|
364
383
|
: undefined;
|
|
365
384
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
385
|
+
const processEvents = async (artifact: ActivatedArtifact<Record<string, Function>>) => {
|
|
386
|
+
for (const event of state === ObservationState.Failed ? [] : operation.Events) {
|
|
387
|
+
try {
|
|
388
|
+
const eventTypeId = event.Context?.EventType?.Id;
|
|
389
|
+
if (!eventTypeId) {
|
|
390
|
+
this._logger.warn('Event missing event type context', { reducerId: id });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const entry = dispatcher.handlerFor(eventTypeId);
|
|
395
|
+
if (!entry) {
|
|
396
|
+
this._logger.debug('No handler registered for event type — skipping', { reducerId: id, eventTypeId });
|
|
397
|
+
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
402
|
+
const context = toClientEventContext(event.Context!);
|
|
403
|
+
|
|
404
|
+
this._logger.info('Invoking reducer handler', {
|
|
405
|
+
reducerId: id,
|
|
406
|
+
method: entry.methodName,
|
|
407
|
+
sequenceNumber: event.Context!.SequenceNumber.toString(),
|
|
408
|
+
eventTypeId,
|
|
409
|
+
hasState: currentState !== undefined
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
currentState = await runActivated(artifact, () => dispatcher.invoke(artifact.instance, entry, content, currentState, context));
|
|
377
413
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
378
|
-
|
|
414
|
+
} catch (err) {
|
|
415
|
+
this._logger.error('Error handling event in reducer', { reducerId: id, error: String(err) });
|
|
416
|
+
exceptionMessages.push(String(err));
|
|
417
|
+
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
418
|
+
state = ObservationState.Failed;
|
|
419
|
+
break;
|
|
379
420
|
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
380
423
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
424
|
+
const firstInvocableEvent = this._artifactActivator && state === ObservationState.Success
|
|
425
|
+
? operation.Events.find(event =>
|
|
426
|
+
!!event.Context?.EventType?.Id && !!dispatcher.handlerFor(event.Context.EventType.Id))
|
|
427
|
+
: undefined;
|
|
428
|
+
if (firstInvocableEvent && this._artifactActivator) {
|
|
429
|
+
try {
|
|
430
|
+
if (!this._eventStore) throw new Error('Reducer activation requires the owning event store.');
|
|
431
|
+
await withActivatedArtifact(reducerType as new () => Record<string, Function>, {
|
|
432
|
+
kind: ArtifactKind.Reducer, artifactId: id, eventStore: this._eventStore,
|
|
433
|
+
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
434
|
+
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
435
|
+
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
436
|
+
}, this._artifactActivator, processEvents);
|
|
394
437
|
} catch (err) {
|
|
395
|
-
this._logger.error('Error
|
|
438
|
+
this._logger.error('Error activating reducer', { reducerId: id, error: String(err) });
|
|
396
439
|
exceptionMessages.push(String(err));
|
|
397
440
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
398
441
|
state = ObservationState.Failed;
|
|
399
|
-
break;
|
|
400
442
|
}
|
|
443
|
+
} else {
|
|
444
|
+
await processEvents({ instance: reducerInstance ?? {} });
|
|
401
445
|
}
|
|
402
446
|
|
|
403
447
|
if (state === ObservationState.Success && currentState !== undefined) {
|
|
@@ -0,0 +1,177 @@
|
|
|
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, ReplayState, ObservationState, type ReducerMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
6
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
7
|
+
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
8
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
9
|
+
import { ArtifactDelivery } from '../../../artifacts/ArtifactDelivery.js';
|
|
10
|
+
import { ArtifactKind } from '../../../artifacts/ArtifactKind.js';
|
|
11
|
+
import type { ArtifactActivationContext } from '../../../artifacts/ArtifactActivationContext.js';
|
|
12
|
+
import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
|
|
13
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
14
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
15
|
+
import { reducer } from '../../reducer.js';
|
|
16
|
+
import { Reducers } from '../../Reducers.js';
|
|
17
|
+
|
|
18
|
+
chai.should();
|
|
19
|
+
|
|
20
|
+
@eventType('reduction-activation-event')
|
|
21
|
+
class ReductionEvent { constructor(readonly amount: number = 0) {} }
|
|
22
|
+
class ReductionState { count = 0; }
|
|
23
|
+
|
|
24
|
+
const instances: object[] = [];
|
|
25
|
+
const notifications: string[] = [];
|
|
26
|
+
|
|
27
|
+
@reducer('reduction-activation', undefined, ReductionState)
|
|
28
|
+
class CountingReducer {
|
|
29
|
+
constructor() { instances.push(this); }
|
|
30
|
+
beginReplay() { notifications.push('begin'); }
|
|
31
|
+
endReplay() {}
|
|
32
|
+
reductionEvent(event: ReductionEvent, state: ReductionState | undefined) {
|
|
33
|
+
return { count: (state?.count ?? 0) + event.amount };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@reducer('reduction-notification-failure', undefined, ReductionState)
|
|
38
|
+
class FailingNotificationReducer {
|
|
39
|
+
beginReplay() { throw new Error('notification failed'); }
|
|
40
|
+
endReplay() {}
|
|
41
|
+
reductionEvent() { notifications.push('unexpected event'); return { count: 42 }; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@reducer('reduction-failure', undefined, ReductionState)
|
|
45
|
+
class FailingReducer {
|
|
46
|
+
reductionEvent() { throw new Error('reduction failed'); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function event(amount: number, sequence: bigint) {
|
|
50
|
+
return { Content: JSON.stringify({ amount }), Context: EventContext.fromPartial({
|
|
51
|
+
EventType: EventType.fromPartial({ Id: 'reduction-activation-event', Generation: 1 }),
|
|
52
|
+
EventSourceId: 'book-1', SequenceNumber: sequence
|
|
53
|
+
}) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function batch(events: ReturnType<typeof event>[], replayState = ReplayState.REPLAY_STATE_None) {
|
|
57
|
+
return { Events: events, ReplayState: replayState, Partition: 'book-1', InitialState: '' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function observe(type: Function, batches: ReturnType<typeof batch>[], activator?: ClientArtifactsActivator) {
|
|
61
|
+
const acknowledgements: NonNullable<ReducerMessage['Content']>['Value1'][] = [];
|
|
62
|
+
const store = { namespace: { value: 'tenant-a' }, readModels: {} } as IEventStore;
|
|
63
|
+
const lifecycle = new ConnectionLifecycle();
|
|
64
|
+
let finished!: () => void;
|
|
65
|
+
const complete = new Promise<void>(resolve => { finished = resolve; });
|
|
66
|
+
async function* stream(queue: AsyncIterable<ReducerMessage>) {
|
|
67
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
68
|
+
await messages.next();
|
|
69
|
+
for (const delivery of batches) {
|
|
70
|
+
yield delivery;
|
|
71
|
+
acknowledgements.push((await messages.next()).value!.Content!.Value1);
|
|
72
|
+
}
|
|
73
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
74
|
+
finished();
|
|
75
|
+
}
|
|
76
|
+
const connection = { reducers: { observe: stream }, readModels: { registerMany: async () => ({}) } } as unknown as ChronicleConnection;
|
|
77
|
+
const artifacts = { reducers: [type], eventTypes: [ReductionEvent], readModels: [] } as unknown as IClientArtifactsProvider;
|
|
78
|
+
await new Reducers(artifacts, connection, 'store', 'tenant-a', lifecycle, 'sink', store, activator).register();
|
|
79
|
+
await complete;
|
|
80
|
+
return { acknowledgements, store };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('when delivering reducer batches', () => {
|
|
84
|
+
it('should reuse the default instance across deliveries and preserve reduction state', async () => {
|
|
85
|
+
instances.length = 0;
|
|
86
|
+
const result = await observe(CountingReducer, [batch([event(1, 1n), event(2, 2n)]), batch([event(3, 3n)])]);
|
|
87
|
+
instances.length.should.equal(1);
|
|
88
|
+
result.acknowledgements.map(value => JSON.parse(value!.ReadModelState)).should.deep.equal([{ count: 3 }, { count: 3 }]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should activate once per batch and separately for replay notifications', async () => {
|
|
92
|
+
instances.length = 0;
|
|
93
|
+
notifications.length = 0;
|
|
94
|
+
const contexts: ArtifactActivationContext[] = [];
|
|
95
|
+
const steps: string[] = [];
|
|
96
|
+
const activator: ClientArtifactsActivator = (type, context) => {
|
|
97
|
+
contexts.push(context);
|
|
98
|
+
return { instance: new type(), run: async callback => { steps.push('enter');
|
|
99
|
+
try { return await callback(); } finally { steps.push('exit'); } },
|
|
100
|
+
dispose: () => { steps.push('dispose'); } };
|
|
101
|
+
};
|
|
102
|
+
const result = await observe(CountingReducer, [batch([event(1, 1n), event(2, 2n)], ReplayState.BeginReplay), batch([event(3, 3n)])], activator);
|
|
103
|
+
instances.length.should.equal(3);
|
|
104
|
+
contexts.map(context => context.delivery).should.deep.equal([ArtifactDelivery.ReplayNotification, ArtifactDelivery.Events, ArtifactDelivery.Events]);
|
|
105
|
+
contexts[0].kind.should.equal(ArtifactKind.Reducer);
|
|
106
|
+
contexts[1].eventStore.should.equal(result.store);
|
|
107
|
+
contexts[1].readModels.should.equal(result.store.readModels);
|
|
108
|
+
if (contexts[1].delivery === ArtifactDelivery.Events) contexts[1].eventContext.sequenceNumber.should.equal(1n);
|
|
109
|
+
notifications.should.deep.equal(['begin']);
|
|
110
|
+
steps.should.deep.equal(['enter', 'exit', 'dispose', 'enter', 'exit', 'enter', 'exit', 'dispose', 'enter', 'exit', 'dispose']);
|
|
111
|
+
result.acknowledgements[0]?.ReadModelState.should.equal('{"count":3}');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should notify before events on the default path', async () => {
|
|
115
|
+
notifications.length = 0;
|
|
116
|
+
const result = await observe(CountingReducer, [batch([event(1, 1n)], ReplayState.BeginReplay)]);
|
|
117
|
+
notifications.should.deep.equal(['begin']);
|
|
118
|
+
result.acknowledgements[0]?.ReadModelState.should.equal('{"count":1}');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should fail notification without running events in either mode', async () => {
|
|
122
|
+
for (const activator of [undefined, (type => ({ instance: new type() })) as ClientArtifactsActivator]) {
|
|
123
|
+
notifications.length = 0;
|
|
124
|
+
const result = await observe(FailingNotificationReducer, [batch([event(1, 1n)], ReplayState.BeginReplay)], activator);
|
|
125
|
+
notifications.should.deep.equal([]);
|
|
126
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
127
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: notification failed']);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should fail event delivery on synchronous and asynchronous activation errors without disposing a lease', async () => {
|
|
132
|
+
for (const mode of ['sync', 'async'] as const) {
|
|
133
|
+
const dispose = vi.fn();
|
|
134
|
+
const message = `${mode} reducer activation failed`;
|
|
135
|
+
const activator: ClientArtifactsActivator = type => {
|
|
136
|
+
const lease = { instance: new type(), dispose };
|
|
137
|
+
if (mode === 'sync') throw new Error(message);
|
|
138
|
+
return Promise.reject(new Error(message)).then(() => lease);
|
|
139
|
+
};
|
|
140
|
+
const result = await observe(CountingReducer, [batch([event(1, 1n)])], activator);
|
|
141
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
142
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([`Error: ${message}`]);
|
|
143
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
|
|
144
|
+
result.acknowledgements[0]?.ReadModelState.should.equal('');
|
|
145
|
+
dispose.mock.calls.length.should.equal(0);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should fail replay notification on synchronous and asynchronous activation errors without disposing a lease', async () => {
|
|
150
|
+
for (const mode of ['sync', 'async'] as const) {
|
|
151
|
+
notifications.length = 0;
|
|
152
|
+
const dispose = vi.fn();
|
|
153
|
+
const message = `${mode} reducer replay activation failed`;
|
|
154
|
+
const activator: ClientArtifactsActivator = type => {
|
|
155
|
+
const lease = { instance: new type(), dispose };
|
|
156
|
+
if (mode === 'sync') throw new Error(message);
|
|
157
|
+
return Promise.reject(new Error(message)).then(() => lease);
|
|
158
|
+
};
|
|
159
|
+
const result = await observe(CountingReducer, [batch([event(1, 1n)], ReplayState.BeginReplay)], activator);
|
|
160
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
161
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([`Error: ${message}`]);
|
|
162
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
|
|
163
|
+
result.acknowledgements[0]?.ReadModelState.should.equal('');
|
|
164
|
+
notifications.should.deep.equal([]);
|
|
165
|
+
dispose.mock.calls.length.should.equal(0);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should dispose after handler failure without acknowledging the failed event', async () => {
|
|
170
|
+
let disposed = 0;
|
|
171
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: () => { disposed++; } });
|
|
172
|
+
const result = await observe(FailingReducer, [batch([event(1, 1n)])], activator);
|
|
173
|
+
disposed.should.equal(1);
|
|
174
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
|
|
175
|
+
result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: reduction failed']);
|
|
176
|
+
});
|
|
177
|
+
});
|