@cratis/chronicle 6.51.0 → 6.52.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 +127 -99
- package/ChronicleOptions.ts +23 -0
- package/dist/ChronicleClient.d.ts +7 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +115 -95
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +14 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +16 -0
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +12 -0
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +202 -175
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/PreparedBatchAppend.d.ts +4 -0
- package/dist/eventSequences/PreparedBatchAppend.d.ts.map +1 -0
- package/dist/eventSequences/PreparedBatchAppend.js +4 -0
- package/dist/eventSequences/PreparedBatchAppend.js.map +1 -0
- package/dist/eventSequences/PreparedSingleAppend.d.ts +4 -0
- package/dist/eventSequences/PreparedSingleAppend.d.ts.map +1 -0
- package/dist/eventSequences/PreparedSingleAppend.js +4 -0
- package/dist/eventSequences/PreparedSingleAppend.js.map +1 -0
- package/dist/eventSequences/WireBatchEvent.d.ts +6 -0
- package/dist/eventSequences/WireBatchEvent.d.ts.map +1 -0
- package/dist/eventSequences/WireBatchEvent.js +4 -0
- package/dist/eventSequences/WireBatchEvent.js.map +1 -0
- package/dist/eventSequences/toContractsCausation.d.ts +16 -0
- package/dist/eventSequences/toContractsCausation.d.ts.map +1 -0
- package/dist/eventSequences/toContractsCausation.js +15 -0
- package/dist/eventSequences/toContractsCausation.js.map +1 -0
- package/dist/observation/FastForwardOutcome.d.ts +14 -0
- package/dist/observation/FastForwardOutcome.d.ts.map +1 -0
- package/dist/observation/FastForwardOutcome.js +17 -0
- package/dist/observation/FastForwardOutcome.js.map +1 -0
- package/dist/observation/FastForwardResult.d.ts +14 -0
- package/dist/observation/FastForwardResult.d.ts.map +1 -0
- package/dist/observation/FastForwardResult.js +4 -0
- package/dist/observation/FastForwardResult.js.map +1 -0
- package/dist/observation/IObservers.d.ts +9 -0
- package/dist/observation/IObservers.d.ts.map +1 -1
- package/dist/observation/Observers.d.ts +3 -0
- package/dist/observation/Observers.d.ts.map +1 -1
- package/dist/observation/Observers.js +11 -0
- package/dist/observation/Observers.js.map +1 -1
- package/dist/observation/index.d.ts +3 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +2 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toFastForwardResult.d.ts +16 -0
- package/dist/observation/toFastForwardResult.d.ts.map +1 -0
- package/dist/observation/toFastForwardResult.js +37 -0
- package/dist/observation/toFastForwardResult.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +3 -1
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reducers/ReducerObservation.d.ts +18 -0
- package/dist/reducers/ReducerObservation.d.ts.map +1 -0
- package/dist/reducers/ReducerObservation.js +4 -0
- package/dist/reducers/ReducerObservation.js.map +1 -0
- package/dist/reducers/ReducerObservationSession.d.ts +11 -0
- package/dist/reducers/ReducerObservationSession.d.ts.map +1 -0
- package/dist/reducers/ReducerObservationSession.js +4 -0
- package/dist/reducers/ReducerObservationSession.js.map +1 -0
- package/dist/reducers/Reducers.d.ts +9 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +150 -154
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/ReductionOutcome.d.ts +25 -0
- package/dist/reducers/ReductionOutcome.d.ts.map +1 -0
- package/dist/reducers/ReductionOutcome.js +66 -0
- package/dist/reducers/ReductionOutcome.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webhooks/WebhookDefinitionBuilder.d.ts.map +1 -1
- package/dist/webhooks/WebhookDefinitionBuilder.js +3 -1
- package/dist/webhooks/WebhookDefinitionBuilder.js.map +1 -1
- package/dist/webhooks/Webhooks.d.ts.map +1 -1
- package/dist/webhooks/Webhooks.js +3 -1
- package/dist/webhooks/Webhooks.js.map +1 -1
- package/eventSequences/EventSequence.ts +265 -186
- package/eventSequences/PreparedBatchAppend.ts +7 -0
- package/eventSequences/PreparedSingleAppend.ts +7 -0
- package/eventSequences/WireBatchEvent.ts +7 -0
- package/eventSequences/toContractsCausation.ts +17 -0
- package/for_ChronicleClient/messageSizes.spec.ts +37 -0
- package/for_ChronicleOptions/when_configuring_message_sizes/with_message_sizes.spec.ts +29 -0
- package/observation/FastForwardOutcome.ts +19 -0
- package/observation/FastForwardResult.ts +19 -0
- package/observation/IObservers.ts +10 -0
- package/observation/Observers.spec.ts +41 -4
- package/observation/Observers.ts +14 -0
- package/observation/index.ts +3 -0
- package/observation/toFastForwardResult.ts +40 -0
- package/package.json +2 -2
- package/reactors/Reactors.ts +3 -1
- package/reducers/ReducerObservation.ts +25 -0
- package/reducers/ReducerObservationSession.ts +15 -0
- package/reducers/Reducers.ts +186 -171
- package/reducers/ReductionOutcome.ts +75 -0
- package/webhooks/WebhookDefinitionBuilder.ts +3 -1
- package/webhooks/Webhooks.ts +3 -1
package/reducers/Reducers.ts
CHANGED
|
@@ -5,7 +5,7 @@ import 'reflect-metadata';
|
|
|
5
5
|
import { createLogger } from '../logging/createLogger.js';
|
|
6
6
|
import { runInBackgroundContext } from '../telemetry/runInBackgroundContext.js';
|
|
7
7
|
import { Constructor } from '@cratis/fundamentals';
|
|
8
|
-
import {
|
|
8
|
+
import { AppendedEvent, EventGenerationDelivery, ReadModelObserverType, ReduceOperationMessage, ReducerMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
9
9
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
10
10
|
import { ChronicleConnection } from '../connection/index.js';
|
|
11
11
|
import { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
|
|
@@ -19,6 +19,9 @@ import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle.js';
|
|
|
19
19
|
import { IReducers } from './IReducers.js';
|
|
20
20
|
import { getReducerMetadata } from './reducer.js';
|
|
21
21
|
import { ReducerEventDispatcher } from './ReducerEventDispatcher.js';
|
|
22
|
+
import { ReducerObservation } from './ReducerObservation.js';
|
|
23
|
+
import { ReducerObservationSession } from './ReducerObservationSession.js';
|
|
24
|
+
import { ReductionOutcome } from './ReductionOutcome.js';
|
|
22
25
|
import { getReadModelMetadata } from '../readModels/index.js';
|
|
23
26
|
import { getReadModelId } from '../readModels/readModel.js';
|
|
24
27
|
import { buildReadModelDefinition } from '../readModels/buildReadModelDefinition.js';
|
|
@@ -31,14 +34,10 @@ import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
|
|
|
31
34
|
import { ArtifactKind } from '../artifacts/ArtifactKind.js';
|
|
32
35
|
import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
|
|
33
36
|
import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
|
|
34
|
-
import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
|
|
35
37
|
|
|
36
38
|
/** Expression used to partition reducer observations by event source ID. */
|
|
37
39
|
const EVENT_SOURCE_ID_KEY = '$eventSourceId';
|
|
38
40
|
|
|
39
|
-
/** Sentinel sequence number sent back when no event was successfully processed. */
|
|
40
|
-
const SEQUENCE_NUMBER_UNAVAILABLE = 4294967295n;
|
|
41
|
-
|
|
42
41
|
/**
|
|
43
42
|
* A push-based async queue that implements {@link AsyncIterable} for use with nice-grpc
|
|
44
43
|
* bidirectional streaming. Values pushed via {@link send} are yielded in order to
|
|
@@ -259,24 +258,18 @@ export class Reducers implements IReducers {
|
|
|
259
258
|
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
260
259
|
});
|
|
261
260
|
|
|
262
|
-
void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
261
|
+
void this.runObservation({ id, reducerType, eventSequenceId, readModelName, dispatcher });
|
|
263
262
|
});
|
|
264
263
|
}
|
|
265
264
|
|
|
266
|
-
private async runObservation(
|
|
267
|
-
id: string,
|
|
268
|
-
reducerType: Constructor,
|
|
269
|
-
eventSequenceId: string,
|
|
270
|
-
readModelName: string,
|
|
271
|
-
dispatcher: ReducerEventDispatcher
|
|
272
|
-
): Promise<void> {
|
|
265
|
+
private async runObservation(observation: ReducerObservation): Promise<void> {
|
|
273
266
|
try {
|
|
274
|
-
await this.observeReducer(
|
|
267
|
+
await this.observeReducer(observation);
|
|
275
268
|
} catch (error) {
|
|
276
|
-
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error });
|
|
269
|
+
this._logger.error('Reducer observation loop exited with error', { reducerId: observation.id, error });
|
|
277
270
|
}
|
|
278
271
|
|
|
279
|
-
this.scheduleReobserve(id, reducerType, dispatcher);
|
|
272
|
+
this.scheduleReobserve(observation.id, observation.reducerType, observation.dispatcher);
|
|
280
273
|
}
|
|
281
274
|
|
|
282
275
|
/**
|
|
@@ -317,20 +310,51 @@ export class Reducers implements IReducers {
|
|
|
317
310
|
return (reducerType as Function).name;
|
|
318
311
|
}
|
|
319
312
|
|
|
320
|
-
private async observeReducer(
|
|
321
|
-
id
|
|
322
|
-
reducerType: Constructor,
|
|
323
|
-
eventSequenceId: string,
|
|
324
|
-
readModelName: string,
|
|
325
|
-
dispatcher: ReducerEventDispatcher
|
|
326
|
-
): Promise<void> {
|
|
313
|
+
private async observeReducer(observation: ReducerObservation): Promise<void> {
|
|
314
|
+
const { id, reducerType } = observation;
|
|
327
315
|
const queue = new AsyncQueue<ReducerMessage>();
|
|
328
316
|
const controller = new AbortController();
|
|
329
317
|
this._queues.set(id, queue);
|
|
330
318
|
this._observations.set(id, controller);
|
|
331
|
-
const isActive = getReducerMetadata(reducerType)?.isActive ?? true;
|
|
332
319
|
|
|
333
|
-
queue.send(
|
|
320
|
+
queue.send(this.createRegistrationMessage(observation));
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const reducerInstance = this._artifactActivator ? undefined : new (reducerType as new () => Record<string, Function>)();
|
|
324
|
+
if (reducerInstance) observation.dispatcher.validateInstance(reducerInstance);
|
|
325
|
+
const session: ReducerObservationSession = { ...observation, signal: controller.signal, reducerInstance };
|
|
326
|
+
|
|
327
|
+
for await (const operation of this._connection.reducers.observe(queue, { signal: controller.signal })) {
|
|
328
|
+
// Delivery steps are awaited here, not inside a wrapper async function, so that
|
|
329
|
+
// async-context updates made by handlers reach this loop as they did originally.
|
|
330
|
+
const outcome = this.beginReduction(session, operation);
|
|
331
|
+
await this.notifyReplayTransition(session, operation, outcome);
|
|
332
|
+
outcome.currentState = operation.InitialState
|
|
333
|
+
? JSON.parse(operation.InitialState) as unknown
|
|
334
|
+
: undefined;
|
|
335
|
+
await this.reduceEvents(session, operation, outcome);
|
|
336
|
+
queue.send(outcome.toMessage(operation.Partition));
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (!this._queues.has(id)) {
|
|
340
|
+
this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
|
|
341
|
+
} else {
|
|
342
|
+
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
// Only retire our own queue: a reconnect can already have replaced it,
|
|
346
|
+
// and deleting the new one would leave that observation untracked and
|
|
347
|
+
// leak a duplicate stream on every reconnect.
|
|
348
|
+
if (this._queues.get(id) === queue) {
|
|
349
|
+
this._queues.delete(id);
|
|
350
|
+
this._observations.delete(id);
|
|
351
|
+
}
|
|
352
|
+
queue.complete();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private createRegistrationMessage({ id, reducerType, eventSequenceId, readModelName, dispatcher }: ReducerObservation): ReducerMessage {
|
|
357
|
+
return {
|
|
334
358
|
Content: {
|
|
335
359
|
Value0: {
|
|
336
360
|
ConnectionId: this._lifecycle.connectionId,
|
|
@@ -345,7 +369,8 @@ export class Reducers implements IReducers {
|
|
|
345
369
|
})),
|
|
346
370
|
ReadModel: readModelName,
|
|
347
371
|
Hash: '',
|
|
348
|
-
IsActive: isActive,
|
|
372
|
+
IsActive: getReducerMetadata(reducerType)?.isActive ?? true,
|
|
373
|
+
GenerationDelivery: EventGenerationDelivery.Compatibility,
|
|
349
374
|
Tags: getTagsFor(reducerType).map(t => t.value),
|
|
350
375
|
Filters: {
|
|
351
376
|
FilterTags: getFilterTagsFor(reducerType).map(t => t.value),
|
|
@@ -356,162 +381,152 @@ export class Reducers implements IReducers {
|
|
|
356
381
|
},
|
|
357
382
|
Value1: undefined
|
|
358
383
|
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private beginReduction(session: ReducerObservationSession, operation: ReduceOperationMessage): ReductionOutcome {
|
|
388
|
+
this._logger.debug('Received reduce operation', {
|
|
389
|
+
reducerId: session.id,
|
|
390
|
+
count: operation.Events.length,
|
|
391
|
+
hasInitialState: operation.InitialState !== '',
|
|
392
|
+
replayState: operation.ReplayState
|
|
359
393
|
});
|
|
360
394
|
|
|
395
|
+
return new ReductionOutcome();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private async notifyReplayTransition(
|
|
399
|
+
session: ReducerObservationSession,
|
|
400
|
+
operation: ReduceOperationMessage,
|
|
401
|
+
outcome: ReductionOutcome
|
|
402
|
+
): Promise<void> {
|
|
361
403
|
try {
|
|
362
|
-
|
|
363
|
-
|
|
404
|
+
if (this._artifactActivator && operation.ReplayState !== ReplayState.REPLAY_STATE_None) {
|
|
405
|
+
await this.notifyActivatedReplayTransition(session, operation, this._artifactActivator);
|
|
406
|
+
} else if (session.reducerInstance) {
|
|
407
|
+
await notifyReplayLifecycle(session.reducerInstance, operation.ReplayState, operation.Partition);
|
|
408
|
+
}
|
|
409
|
+
} catch (err) {
|
|
410
|
+
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: session.id, error: err });
|
|
411
|
+
outcome.recordReplayNotificationFailure(err);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
364
414
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
415
|
+
private async notifyActivatedReplayTransition(
|
|
416
|
+
{ id, reducerType, eventSequenceId, dispatcher, signal }: ReducerObservationSession,
|
|
417
|
+
operation: ReduceOperationMessage,
|
|
418
|
+
activator: ClientArtifactsActivator
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const eventStore = this.requireEventStore();
|
|
421
|
+
await withActivatedArtifact(reducerType, {
|
|
422
|
+
kind: ArtifactKind.Reducer, artifactId: id, eventStore,
|
|
423
|
+
readModels: eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
424
|
+
signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
425
|
+
replayState: operation.ReplayState
|
|
426
|
+
}, activator, artifact => {
|
|
427
|
+
dispatcher.validateInstance(artifact.instance);
|
|
428
|
+
return runActivated(artifact, () =>
|
|
429
|
+
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
|
|
430
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState });
|
|
431
|
+
}, this._connection.logger);
|
|
432
|
+
}
|
|
378
433
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
kind: ArtifactKind.Reducer, artifactId: id, eventStore: this._eventStore,
|
|
384
|
-
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
385
|
-
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
386
|
-
replayState: operation.ReplayState
|
|
387
|
-
}, this._artifactActivator, artifact => {
|
|
388
|
-
dispatcher.validateInstance(artifact.instance);
|
|
389
|
-
return runActivated(artifact, () =>
|
|
390
|
-
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
|
|
391
|
-
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState });
|
|
392
|
-
}, this._connection.logger);
|
|
393
|
-
} else if (reducerInstance) {
|
|
394
|
-
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
395
|
-
}
|
|
396
|
-
} catch (err) {
|
|
397
|
-
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: err });
|
|
398
|
-
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
399
|
-
exceptionMessages.push(String(err.processingError));
|
|
400
|
-
}
|
|
401
|
-
exceptionMessages.push(String(err));
|
|
402
|
-
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
403
|
-
state = ObservationState.Failed;
|
|
404
|
-
}
|
|
434
|
+
private requireEventStore(): IEventStore {
|
|
435
|
+
if (!this._eventStore) throw new Error('Reducer activation requires the owning event store.');
|
|
436
|
+
return this._eventStore;
|
|
437
|
+
}
|
|
405
438
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
439
|
+
private async reduceEvents(
|
|
440
|
+
session: ReducerObservationSession,
|
|
441
|
+
operation: ReduceOperationMessage,
|
|
442
|
+
outcome: ReductionOutcome
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const { id, reducerType, eventSequenceId, dispatcher, signal, reducerInstance } = session;
|
|
445
|
+
const firstInvocableEvent = this._artifactActivator && !outcome.failed
|
|
446
|
+
? operation.Events.find(event =>
|
|
447
|
+
!!event.Context?.EventType?.Id && !!dispatcher.handlerFor(event.Context.EventType.Id))
|
|
448
|
+
: undefined;
|
|
449
|
+
if (!firstInvocableEvent || !this._artifactActivator) {
|
|
450
|
+
await this.processEvents(session, operation, outcome, { instance: reducerInstance ?? {} });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
409
453
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
427
|
-
const context = toClientEventContext(event.Context!);
|
|
428
|
-
|
|
429
|
-
this._logger.info('Invoking reducer handler', {
|
|
430
|
-
reducerId: id,
|
|
431
|
-
method: entry.methodName,
|
|
432
|
-
sequenceNumber: event.Context!.SequenceNumber.toString(),
|
|
433
|
-
eventTypeId,
|
|
434
|
-
hasState: currentState !== undefined
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
currentState = await runActivated(artifact, () => dispatcher.invoke(artifact.instance, entry, content, currentState, context),
|
|
438
|
-
{ delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
|
|
439
|
-
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
440
|
-
} catch (err) {
|
|
441
|
-
this._logger.error('Error handling event in reducer', { reducerId: id, error: err });
|
|
442
|
-
exceptionMessages.push(String(err));
|
|
443
|
-
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
444
|
-
state = ObservationState.Failed;
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
};
|
|
454
|
+
try {
|
|
455
|
+
const eventStore = this.requireEventStore();
|
|
456
|
+
await withActivatedArtifact(reducerType as new () => Record<string, Function>, {
|
|
457
|
+
kind: ArtifactKind.Reducer, artifactId: id, eventStore,
|
|
458
|
+
readModels: eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
459
|
+
signal, delivery: ArtifactDelivery.Events,
|
|
460
|
+
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
461
|
+
}, this._artifactActivator, artifact => {
|
|
462
|
+
dispatcher.validateInstance(artifact.instance);
|
|
463
|
+
return this.processEvents(session, operation, outcome, artifact);
|
|
464
|
+
}, this._connection.logger);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
this._logger.error('Error activating reducer', { reducerId: id, error: err });
|
|
467
|
+
outcome.recordActivationFailure(err);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
449
470
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
exceptionMessages.push(String(err));
|
|
469
|
-
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
470
|
-
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
471
|
-
? `${exceptionStackTrace}\n${completionStack}` : completionStack;
|
|
472
|
-
state = ObservationState.Failed;
|
|
473
|
-
if (err instanceof ArtifactCompletionFailed) {
|
|
474
|
-
lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
} else {
|
|
478
|
-
await processEvents({ instance: reducerInstance ?? {} });
|
|
479
|
-
}
|
|
471
|
+
private async processEvents(
|
|
472
|
+
session: ReducerObservationSession,
|
|
473
|
+
operation: ReduceOperationMessage,
|
|
474
|
+
outcome: ReductionOutcome,
|
|
475
|
+
artifact: ActivatedArtifact<Record<string, Function>>
|
|
476
|
+
): Promise<void> {
|
|
477
|
+
for (const event of outcome.failed ? [] : operation.Events) {
|
|
478
|
+
try {
|
|
479
|
+
// Skipped events stay synchronous; only an actual handler invocation is awaited.
|
|
480
|
+
const pending = this.processEvent(session, event, outcome, artifact);
|
|
481
|
+
if (pending) await pending;
|
|
482
|
+
} catch (err) {
|
|
483
|
+
this._logger.error('Error handling event in reducer', { reducerId: session.id, error: err });
|
|
484
|
+
outcome.recordFailure(err);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
480
489
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
490
|
+
private processEvent(
|
|
491
|
+
{ id, dispatcher }: ReducerObservationSession,
|
|
492
|
+
event: AppendedEvent,
|
|
493
|
+
outcome: ReductionOutcome,
|
|
494
|
+
artifact: ActivatedArtifact<Record<string, Function>>
|
|
495
|
+
): Promise<void> | undefined {
|
|
496
|
+
const eventTypeId = event.Context?.EventType?.Id;
|
|
497
|
+
if (!eventTypeId) {
|
|
498
|
+
this._logger.warn('Event missing event type context', { reducerId: id });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
484
501
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
State: state,
|
|
491
|
-
LastSuccessfulObservation: lastSuccessfullyObservedEvent,
|
|
492
|
-
ExceptionMessages: exceptionMessages,
|
|
493
|
-
ExceptionStackTrace: exceptionStackTrace,
|
|
494
|
-
ReadModelState: readModelState ?? ''
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
} catch (err) {
|
|
500
|
-
if (!this._queues.has(id)) {
|
|
501
|
-
this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
|
|
502
|
-
} else {
|
|
503
|
-
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
|
|
504
|
-
}
|
|
505
|
-
} finally {
|
|
506
|
-
// Only retire our own queue: a reconnect can already have replaced it,
|
|
507
|
-
// and deleting the new one would leave that observation untracked and
|
|
508
|
-
// leak a duplicate stream on every reconnect.
|
|
509
|
-
if (this._queues.get(id) === queue) {
|
|
510
|
-
this._queues.delete(id);
|
|
511
|
-
this._observations.delete(id);
|
|
512
|
-
}
|
|
513
|
-
queue.complete();
|
|
502
|
+
const entry = dispatcher.handlerFor(eventTypeId);
|
|
503
|
+
if (!entry) {
|
|
504
|
+
this._logger.debug('No handler registered for event type — skipping', { reducerId: id, eventTypeId });
|
|
505
|
+
outcome.lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
506
|
+
return;
|
|
514
507
|
}
|
|
508
|
+
|
|
509
|
+
const content = JSON.parse(event.Content) as Record<string, unknown>;
|
|
510
|
+
const context = toClientEventContext(event.Context!);
|
|
511
|
+
|
|
512
|
+
this._logger.info('Invoking reducer handler', {
|
|
513
|
+
reducerId: id,
|
|
514
|
+
method: entry.methodName,
|
|
515
|
+
sequenceNumber: event.Context!.SequenceNumber.toString(),
|
|
516
|
+
eventTypeId,
|
|
517
|
+
hasState: outcome.currentState !== undefined
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return this.invokeHandler(
|
|
521
|
+
() => runActivated(artifact, () => dispatcher.invoke(artifact.instance, entry, content, outcome.currentState, context),
|
|
522
|
+
{ delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName }),
|
|
523
|
+
outcome,
|
|
524
|
+
event.Context!.SequenceNumber);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private async invokeHandler(invoke: () => Promise<unknown>, outcome: ReductionOutcome, sequenceNumber: bigint): Promise<void> {
|
|
528
|
+
outcome.currentState = await invoke();
|
|
529
|
+
outcome.lastSuccessfullyObservedEvent = sequenceNumber;
|
|
515
530
|
}
|
|
516
531
|
|
|
517
532
|
private disconnectAll(): void {
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ObservationState, ReducerMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
|
|
6
|
+
|
|
7
|
+
/** Sentinel sequence number sent back when no event was successfully processed. */
|
|
8
|
+
export const SEQUENCE_NUMBER_UNAVAILABLE = 4294967295n;
|
|
9
|
+
|
|
10
|
+
const stackOf = (error: unknown): string => error instanceof Error ? (error.stack ?? '') : '';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Accumulates the result of reducing one reduce operation: the state, the failure details and the
|
|
14
|
+
* last event that was successfully reduced.
|
|
15
|
+
*/
|
|
16
|
+
export class ReductionOutcome {
|
|
17
|
+
state = ObservationState.Success;
|
|
18
|
+
lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
19
|
+
readonly exceptionMessages: string[] = [];
|
|
20
|
+
exceptionStackTrace = '';
|
|
21
|
+
currentState: unknown;
|
|
22
|
+
|
|
23
|
+
/** Gets whether the reduction has failed. */
|
|
24
|
+
get failed(): boolean {
|
|
25
|
+
return this.state === ObservationState.Failed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Records a failure of the reduction. */
|
|
29
|
+
recordFailure(error: unknown): void {
|
|
30
|
+
this.exceptionMessages.push(String(error));
|
|
31
|
+
this.exceptionStackTrace = stackOf(error);
|
|
32
|
+
this.state = ObservationState.Failed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Records a failure to notify of a replay lifecycle transition. */
|
|
36
|
+
recordReplayNotificationFailure(error: unknown): void {
|
|
37
|
+
if (error instanceof ArtifactCompletionFailed && error.processingError !== undefined) {
|
|
38
|
+
this.exceptionMessages.push(String(error.processingError));
|
|
39
|
+
}
|
|
40
|
+
this.recordFailure(error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Records a failure to activate the reducer or to complete its activation. */
|
|
44
|
+
recordActivationFailure(error: unknown): void {
|
|
45
|
+
this.exceptionMessages.push(String(error));
|
|
46
|
+
const completionStack = stackOf(error);
|
|
47
|
+
this.exceptionStackTrace = error instanceof ArtifactCompletionFailed && this.exceptionStackTrace
|
|
48
|
+
? `${this.exceptionStackTrace}\n${completionStack}` : completionStack;
|
|
49
|
+
this.state = ObservationState.Failed;
|
|
50
|
+
if (error instanceof ArtifactCompletionFailed) {
|
|
51
|
+
this.lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Creates the result message reported back to the kernel. */
|
|
56
|
+
toMessage(partition: string): ReducerMessage {
|
|
57
|
+
const readModelState = this.state === ObservationState.Success && this.currentState !== undefined
|
|
58
|
+
? JSON.stringify(this.currentState)
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
Content: {
|
|
63
|
+
Value0: undefined,
|
|
64
|
+
Value1: {
|
|
65
|
+
Partition: partition,
|
|
66
|
+
State: this.state,
|
|
67
|
+
LastSuccessfulObservation: this.lastSuccessfullyObservedEvent,
|
|
68
|
+
ExceptionMessages: this.exceptionMessages,
|
|
69
|
+
ExceptionStackTrace: this.exceptionStackTrace,
|
|
70
|
+
ReadModelState: readModelState ?? ''
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -138,7 +138,9 @@ export class WebhookDefinitionBuilder implements IWebhookDefinitionBuilder {
|
|
|
138
138
|
Headers: Object.fromEntries(this._headers)
|
|
139
139
|
},
|
|
140
140
|
IsReplayable: this._isReplayable,
|
|
141
|
-
IsActive: this._isActive
|
|
141
|
+
IsActive: this._isActive,
|
|
142
|
+
IsNotReplayable: !this._isReplayable,
|
|
143
|
+
IsInactive: !this._isActive
|
|
142
144
|
};
|
|
143
145
|
}
|
|
144
146
|
}
|
package/webhooks/Webhooks.ts
CHANGED
|
@@ -102,7 +102,9 @@ export class Webhooks implements IWebhooks {
|
|
|
102
102
|
Headers: webhook.Headers
|
|
103
103
|
},
|
|
104
104
|
IsReplayable: webhook.IsReplayable,
|
|
105
|
-
IsActive: webhook.IsActive
|
|
105
|
+
IsActive: webhook.IsActive,
|
|
106
|
+
IsNotReplayable: !webhook.IsReplayable,
|
|
107
|
+
IsInactive: !webhook.IsActive
|
|
106
108
|
}));
|
|
107
109
|
}
|
|
108
110
|
|