@cratis/chronicle 6.47.0 → 6.49.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 +3 -0
- package/EventStore.ts +10 -3
- package/IEventStore.ts +8 -0
- package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
- package/artifacts/IClientArtifactsProvider.ts +3 -0
- package/connection/ChronicleConnection.ts +6 -0
- package/connection/ChronicleServices.ts +2 -0
- package/connection/ConnectionLifecycle.ts +22 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +3 -0
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +9 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +7 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
- package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +1 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +5 -1
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +2 -1
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/connection/ConnectionLifecycle.d.ts +8 -0
- package/dist/connection/ConnectionLifecycle.d.ts.map +1 -1
- package/dist/connection/ConnectionLifecycle.js +21 -0
- package/dist/connection/ConnectionLifecycle.js.map +1 -1
- package/dist/eventSequences/AppendOptions.d.ts +12 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +11 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +4 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +32 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -2
- package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -2
- package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.js +4 -4
- package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
- package/dist/eventSequences/resolveEventSourceRouting.d.ts +39 -0
- package/dist/eventSequences/resolveEventSourceRouting.d.ts.map +1 -0
- package/dist/eventSequences/resolveEventSourceRouting.js +92 -0
- package/dist/eventSequences/resolveEventSourceRouting.js.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts +19 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.js +19 -0
- package/dist/eventSources/ConcurrencyDimensions.js.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts +6 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.js +12 -0
- package/dist/eventSources/DuplicateEventSourceName.js.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts +7 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.js +14 -0
- package/dist/eventSources/DuplicateEventStreamName.js.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts +9 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js +18 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js.map +1 -0
- package/dist/eventSources/EventSourceDefinition.d.ts +20 -0
- package/dist/eventSources/EventSourceDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventSourceDefinition.js +4 -0
- package/dist/eventSources/EventSourceDefinition.js.map +1 -0
- package/dist/eventSources/EventSources.d.ts +31 -0
- package/dist/eventSources/EventSources.d.ts.map +1 -0
- package/dist/eventSources/EventSources.js +90 -0
- package/dist/eventSources/EventSources.js.map +1 -0
- package/dist/eventSources/EventStreamDefinition.d.ts +13 -0
- package/dist/eventSources/EventStreamDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDefinition.js +4 -0
- package/dist/eventSources/EventStreamDefinition.js.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts +7 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js +14 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts +6 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js +12 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js.map +1 -0
- package/dist/eventSources/IEventSources.d.ts +27 -0
- package/dist/eventSources/IEventSources.d.ts.map +1 -0
- package/dist/eventSources/IEventSources.js +4 -0
- package/dist/eventSources/IEventSources.js.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts +37 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.js +60 -0
- package/dist/eventSources/ResolvedEventRouting.js.map +1 -0
- package/dist/eventSources/UnknownEventSource.d.ts +6 -0
- package/dist/eventSources/UnknownEventSource.d.ts.map +1 -0
- package/dist/eventSources/UnknownEventSource.js +12 -0
- package/dist/eventSources/UnknownEventSource.js.map +1 -0
- package/dist/eventSources/eventSource.d.ts +60 -0
- package/dist/eventSources/eventSource.d.ts.map +1 -0
- package/dist/eventSources/eventSource.js +72 -0
- package/dist/eventSources/eventSource.js.map +1 -0
- package/dist/eventSources/index.d.ts +17 -0
- package/dist/eventSources/index.d.ts.map +1 -0
- package/dist/eventSources/index.js +13 -0
- package/dist/eventSources/index.js.map +1 -0
- package/dist/events/EventContext.d.ts +5 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/WireEventContext.d.ts +2 -0
- package/dist/events/WireEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.js +1 -0
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +1 -0
- package/dist/projections/eventContextPropertyExpression.js.map +1 -1
- package/dist/readModels/IReadModelWatcher.d.ts +30 -0
- package/dist/readModels/IReadModelWatcher.d.ts.map +1 -0
- package/dist/readModels/IReadModelWatcher.js +4 -0
- package/dist/readModels/IReadModelWatcher.js.map +1 -0
- package/dist/readModels/IReadModels.d.ts +16 -2
- package/dist/readModels/IReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModelChangeContext.d.ts +9 -0
- package/dist/readModels/ReadModelChangeContext.d.ts.map +1 -0
- package/dist/readModels/ReadModelChangeContext.js +4 -0
- package/dist/readModels/ReadModelChangeContext.js.map +1 -0
- package/dist/readModels/ReadModelChangeType.d.ts +7 -0
- package/dist/readModels/ReadModelChangeType.d.ts.map +1 -0
- package/dist/readModels/ReadModelChangeType.js +10 -0
- package/dist/readModels/ReadModelChangeType.js.map +1 -0
- package/dist/readModels/ReadModelChangeset.d.ts +6 -0
- package/dist/readModels/ReadModelChangeset.d.ts.map +1 -1
- package/dist/readModels/ReadModelWatchOptions.d.ts +19 -0
- package/dist/readModels/ReadModelWatchOptions.d.ts.map +1 -0
- package/dist/readModels/ReadModelWatchOptions.js +4 -0
- package/dist/readModels/ReadModelWatchOptions.js.map +1 -0
- package/dist/readModels/ReadModelWatcher.d.ts +52 -0
- package/dist/readModels/ReadModelWatcher.d.ts.map +1 -0
- package/dist/readModels/ReadModelWatcher.js +276 -0
- package/dist/readModels/ReadModelWatcher.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts +11 -1
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +47 -11
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.d.ts +15 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.d.ts.map +1 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.js +45 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.js.map +1 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.d.ts +21 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.d.ts.map +1 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.js +35 -0
- package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.js.map +1 -0
- package/dist/readModels/index.d.ts +4 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +2 -0
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js +2 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
- package/dist/transactions/IUnitOfWork.d.ts +3 -1
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalEventRouting.d.ts +12 -0
- package/dist/transactions/TransactionalEventRouting.d.ts.map +1 -0
- package/dist/transactions/TransactionalEventRouting.js +4 -0
- package/dist/transactions/TransactionalEventRouting.js.map +1 -0
- package/dist/transactions/UnitOfWork.d.ts +2 -1
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +11 -2
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/index.d.ts +1 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/DecoratorType.d.ts +3 -1
- package/dist/types/DecoratorType.d.ts.map +1 -1
- package/dist/types/DecoratorType.js +2 -0
- package/dist/types/DecoratorType.js.map +1 -1
- package/eventSequences/AppendOptions.ts +14 -0
- package/eventSequences/EventForEventSourceId.ts +13 -0
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +33 -6
- package/eventSequences/ITransactionalEventSequence.ts +5 -2
- package/eventSequences/TransactionalEventSequence.ts +5 -4
- package/eventSequences/resolveEventSourceRouting.ts +125 -0
- package/eventSources/ConcurrencyDimensions.ts +26 -0
- package/eventSources/DuplicateEventSourceName.ts +10 -0
- package/eventSources/DuplicateEventStreamName.ts +10 -0
- package/eventSources/EventRoutingContradictsEventSource.ts +10 -0
- package/eventSources/EventSourceDefinition.ts +27 -0
- package/eventSources/EventSources.ts +93 -0
- package/eventSources/EventStreamDefinition.ts +18 -0
- package/eventSources/EventStreamDoesNotBelongToEventSource.ts +10 -0
- package/eventSources/EventStreamRequiresEventSource.ts +10 -0
- package/eventSources/IEventSources.ts +33 -0
- package/eventSources/ResolvedEventRouting.ts +78 -0
- package/eventSources/UnknownEventSource.ts +10 -0
- package/eventSources/eventSource.ts +116 -0
- package/eventSources/for_EventSources/against_a_kernel/when_appending_through_registered_sources.integration.spec.ts +106 -0
- package/eventSources/for_EventSources/when_appending_through_a_definition.spec.ts +190 -0
- package/eventSources/for_EventSources/when_batch_requires_differing_guards.spec.ts +139 -0
- package/eventSources/for_EventSources/when_discovering_and_registering.spec.ts +94 -0
- package/eventSources/index.ts +19 -0
- package/events/EventContext.ts +6 -0
- package/events/WireEventContext.ts +2 -0
- package/events/for_EventContext/with_event_source.spec.ts +22 -0
- package/events/toClientEventContext.ts +1 -0
- package/for_ChronicleClient/when_reconnecting/with_waiting_read_model_watchers.spec.ts +117 -0
- package/for_EventStore/when_implementing_a_legacy_event_store.spec.ts +19 -0
- package/index.ts +3 -0
- package/jobs/for_Jobs/against_a_kernel/when_managing_a_replay_job.integration.spec.ts +24 -4
- package/package.json +3 -3
- package/projections/eventContextPropertyExpression.ts +2 -1
- package/reactors/for_Reactors/against_a_kernel/when_replaying_reactors_with_once_only_handlers.integration.spec.ts +27 -8
- package/readModels/IReadModelWatcher.ts +35 -0
- package/readModels/IReadModels.ts +17 -2
- package/readModels/ReadModelChangeContext.ts +11 -0
- package/readModels/ReadModelChangeType.ts +9 -0
- package/readModels/ReadModelChangeset.ts +9 -0
- package/readModels/ReadModelWatchOptions.ts +23 -0
- package/readModels/ReadModelWatcher.ts +271 -0
- package/readModels/ReadModels.ts +60 -11
- package/readModels/for_ReadModelWatcher/against_a_kernel/when_watching_changes.integration.spec.ts +104 -0
- package/readModels/for_ReadModelWatcher/given/a_kernel_stream.ts +50 -0
- package/readModels/for_ReadModelWatcher/given/a_watched_read_model.ts +37 -0
- package/readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts +29 -0
- package/readModels/for_ReadModelWatcher/when_a_stream_cannot_recover.spec.ts +86 -0
- package/readModels/for_ReadModelWatcher/when_applying_backpressure.spec.ts +80 -0
- package/readModels/for_ReadModelWatcher/when_failing_with_buffered_changes.spec.ts +39 -0
- package/readModels/for_ReadModelWatcher/when_not_opting_into_resume.spec.ts +62 -0
- package/readModels/for_ReadModelWatcher/when_notifying_resubscription.spec.ts +128 -0
- package/readModels/for_ReadModelWatcher/when_opting_into_overflow.spec.ts +39 -0
- package/readModels/for_ReadModelWatcher/when_receiving_changes.spec.ts +69 -0
- package/readModels/for_ReadModelWatcher/when_reconnecting.spec.ts +99 -0
- package/readModels/for_ReadModelWatcher/when_reconnecting_during_conversion.spec.ts +84 -0
- package/readModels/for_ReadModelWatcher/when_recovering_a_stream.spec.ts +106 -0
- package/readModels/for_ReadModelWatcher/when_stopping.spec.ts +85 -0
- package/readModels/for_ReadModelWatcher/when_subscribing.spec.ts +62 -0
- package/readModels/for_ReadModels/when_watching.spec.ts +85 -0
- package/readModels/index.ts +4 -0
- package/testing/EventScenario.event-sources.spec.ts +34 -0
- package/testing/ReactorScenario.spec.ts +18 -1
- package/testing/ReactorScenario.ts +1 -0
- package/testing/projections/ProjectionReadModelProcessor.ts +2 -1
- package/transactions/IUnitOfWork.ts +3 -1
- package/transactions/TransactionalEventRouting.ts +16 -0
- package/transactions/UnitOfWork.ts +13 -2
- package/transactions/for_UnitOfWork/when_committing_events_with_event_source_routing.spec.ts +41 -0
- package/transactions/index.ts +1 -0
- package/types/DecoratorType.ts +4 -1
|
@@ -0,0 +1,271 @@
|
|
|
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 type { ReadModelChangeset as ContractChangeset } from '@cratis/chronicle.contracts';
|
|
5
|
+
import type { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
|
|
6
|
+
import type { IReadModelWatcher } from './IReadModelWatcher.js';
|
|
7
|
+
import type { ReadModelChangeset } from './ReadModelChangeset.js';
|
|
8
|
+
import type { ReadModelWatchOptions } from './ReadModelWatchOptions.js';
|
|
9
|
+
|
|
10
|
+
/** @internal Owns one cancellable stream per connection generation. */
|
|
11
|
+
export class ReadModelWatcher<TReadModel> implements IReadModelWatcher<TReadModel>, AsyncIterableIterator<ReadModelChangeset<TReadModel>> {
|
|
12
|
+
private static readonly _restartDelayMs = 1000;
|
|
13
|
+
private readonly _bufferLimit: number;
|
|
14
|
+
private readonly _failOnOverflow: boolean;
|
|
15
|
+
private readonly _resume: boolean;
|
|
16
|
+
private _resumeReading?: () => void;
|
|
17
|
+
|
|
18
|
+
private _subscribed!: Promise<void>;
|
|
19
|
+
private _resolveSubscribed!: () => void;
|
|
20
|
+
private _rejectSubscribed!: (error: unknown) => void;
|
|
21
|
+
private _acknowledged = false;
|
|
22
|
+
private _hasSubscribed = false;
|
|
23
|
+
private _delivery = Promise.resolve();
|
|
24
|
+
private readonly _onResubscribed = new Set<() => void | Promise<void>>();
|
|
25
|
+
private _controller?: AbortController;
|
|
26
|
+
private _stopped = false;
|
|
27
|
+
private _failed = false;
|
|
28
|
+
private _failure?: unknown;
|
|
29
|
+
private readonly _buffered: ReadModelChangeset<TReadModel>[] = [];
|
|
30
|
+
private _restartTimer?: ReturnType<typeof setTimeout>;
|
|
31
|
+
private readonly _waiting: {
|
|
32
|
+
resolve: (result: IteratorResult<ReadModelChangeset<TReadModel>>) => void;
|
|
33
|
+
reject: (error: unknown) => void;
|
|
34
|
+
}[] = [];
|
|
35
|
+
private readonly _unsubscribe: (() => void)[] = [];
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly _open: (signal: AbortSignal) => AsyncIterable<ContractChangeset>,
|
|
39
|
+
private readonly _convert: (change: ContractChangeset) => Promise<ReadModelChangeset<TReadModel>>,
|
|
40
|
+
signal: AbortSignal,
|
|
41
|
+
private readonly _lifecycle?: ConnectionLifecycle,
|
|
42
|
+
options: ReadModelWatchOptions = {}
|
|
43
|
+
) {
|
|
44
|
+
if (options.maxBuffered !== undefined && (!Number.isSafeInteger(options.maxBuffered) || options.maxBuffered < 1)) {
|
|
45
|
+
throw new RangeError('maxBuffered must be a positive safe integer.');
|
|
46
|
+
}
|
|
47
|
+
this._bufferLimit = options.maxBuffered ?? 1024;
|
|
48
|
+
this._failOnOverflow = options.maxBuffered !== undefined;
|
|
49
|
+
this._resume = options.resume === true;
|
|
50
|
+
this.resetReadiness();
|
|
51
|
+
const abort = () => this.finish(signal.reason, false);
|
|
52
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
53
|
+
this._unsubscribe.push(() => signal.removeEventListener('abort', abort));
|
|
54
|
+
if (_lifecycle) {
|
|
55
|
+
// Without opt-in, let the watch stream propagate its original transport error.
|
|
56
|
+
this._unsubscribe.push(_lifecycle.onDisconnected(async () => {
|
|
57
|
+
if (this.shouldResume()) this.disconnect();
|
|
58
|
+
}));
|
|
59
|
+
this._unsubscribe.push(_lifecycle.onConnected(async () => this.start()));
|
|
60
|
+
this._unsubscribe.push(_lifecycle.onFailed(async error => this.finish(error, true, true)));
|
|
61
|
+
}
|
|
62
|
+
if (signal.aborted) {
|
|
63
|
+
abort();
|
|
64
|
+
} else if (_lifecycle?.failure) {
|
|
65
|
+
this.finish(_lifecycle.failure, true, true);
|
|
66
|
+
} else if (!_lifecycle || _lifecycle.isConnected) {
|
|
67
|
+
this.start();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get subscribed(): Promise<void> {
|
|
72
|
+
return this._subscribed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onResubscribed(callback: () => void | Promise<void>): () => void {
|
|
76
|
+
if (this._stopped) return () => {};
|
|
77
|
+
this._onResubscribed.add(callback);
|
|
78
|
+
return () => { this._onResubscribed.delete(callback); };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<ReadModelChangeset<TReadModel>> {
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
next(): Promise<IteratorResult<ReadModelChangeset<TReadModel>>> {
|
|
86
|
+
const value = this._buffered.shift();
|
|
87
|
+
if (value) {
|
|
88
|
+
this.resumeReading();
|
|
89
|
+
return Promise.resolve({ done: false, value });
|
|
90
|
+
}
|
|
91
|
+
if (this._failed) return Promise.reject(this._failure);
|
|
92
|
+
if (this._stopped) return Promise.resolve({ done: true, value: undefined });
|
|
93
|
+
return new Promise((resolve, reject) => this._waiting.push({ resolve, reject }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async return(): Promise<IteratorResult<ReadModelChangeset<TReadModel>>> {
|
|
97
|
+
this.dispose();
|
|
98
|
+
return { done: true, value: undefined };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dispose(): void {
|
|
102
|
+
this._buffered.length = 0;
|
|
103
|
+
this.finish(new DOMException('Read model watcher disposed.', 'AbortError'), false);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private resetReadiness(): void {
|
|
107
|
+
this._acknowledged = false;
|
|
108
|
+
this._subscribed = new Promise<void>((resolve, reject) => {
|
|
109
|
+
this._resolveSubscribed = resolve;
|
|
110
|
+
this._rejectSubscribed = reject;
|
|
111
|
+
});
|
|
112
|
+
// Legacy consumers only iterate. They still receive stream failures from next();
|
|
113
|
+
// not observing the optional readiness promise must not cause an unhandled rejection.
|
|
114
|
+
this._subscribed.catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private start(): void {
|
|
118
|
+
if (this._stopped || this._controller) return;
|
|
119
|
+
this.clearRestartTimer();
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
this._controller = controller;
|
|
122
|
+
void this.read(controller);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private disconnect(): void {
|
|
126
|
+
if (this._stopped) return;
|
|
127
|
+
this.clearRestartTimer();
|
|
128
|
+
this.cancelStream();
|
|
129
|
+
// Received changes are not replayed by the kernel. Keep them across reconnects.
|
|
130
|
+
if (this._acknowledged) this.resetReadiness();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private shouldResume(): boolean {
|
|
134
|
+
return this._resume || this._onResubscribed.size > 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private restart(): void {
|
|
138
|
+
this.disconnect();
|
|
139
|
+
if (this._stopped || (this._lifecycle && !this._lifecycle.isConnected)) return;
|
|
140
|
+
// The watch and keep-alive share a transport, but can fail in either order.
|
|
141
|
+
// Also recover a watch-only failure if no disconnected notification follows.
|
|
142
|
+
this._restartTimer = setTimeout(() => {
|
|
143
|
+
this._restartTimer = undefined;
|
|
144
|
+
if (!this._lifecycle || this._lifecycle.isConnected) this.start();
|
|
145
|
+
}, ReadModelWatcher._restartDelayMs);
|
|
146
|
+
this._restartTimer.unref?.();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private clearRestartTimer(): void {
|
|
150
|
+
clearTimeout(this._restartTimer);
|
|
151
|
+
this._restartTimer = undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private cancelStream(): void {
|
|
155
|
+
const controller = this._controller;
|
|
156
|
+
this._controller = undefined;
|
|
157
|
+
controller?.abort();
|
|
158
|
+
this.resumeReading();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private resumeReading(): void {
|
|
162
|
+
this._resumeReading?.();
|
|
163
|
+
this._resumeReading = undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private waitForCapacity(controller: AbortController): Promise<void> | undefined {
|
|
167
|
+
if (this._controller === controller && this._acknowledged && !this._failOnOverflow &&
|
|
168
|
+
this._buffered.length >= this._bufferLimit) {
|
|
169
|
+
return new Promise<void>(resolve => { this._resumeReading = resolve; })
|
|
170
|
+
.then(() => this.waitForCapacity(controller));
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async read(controller: AbortController): Promise<void> {
|
|
176
|
+
try {
|
|
177
|
+
for await (const change of this._open(controller.signal)) {
|
|
178
|
+
if (this._controller !== controller) return;
|
|
179
|
+
if (change.Subscribed) {
|
|
180
|
+
if (this._acknowledged) continue;
|
|
181
|
+
this._acknowledged = true;
|
|
182
|
+
this._resolveSubscribed();
|
|
183
|
+
const resubscribed = this._hasSubscribed;
|
|
184
|
+
this._hasSubscribed = true;
|
|
185
|
+
if (resubscribed) {
|
|
186
|
+
const callbacks = [...this._onResubscribed];
|
|
187
|
+
this._delivery = this._delivery.then(() => this.notifyResubscribed(callbacks));
|
|
188
|
+
await this._delivery;
|
|
189
|
+
}
|
|
190
|
+
const capacity = this.waitForCapacity(controller);
|
|
191
|
+
if (capacity) await capacity;
|
|
192
|
+
if (this._controller !== controller) return;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Finish converting changes already received, even if their stream disconnects.
|
|
196
|
+
// A new stream queues behind them so conversion cannot reorder delivery.
|
|
197
|
+
this._delivery = this._delivery.then(() => this.deliver(change));
|
|
198
|
+
await this._delivery;
|
|
199
|
+
const capacity = this.waitForCapacity(controller);
|
|
200
|
+
if (capacity) await capacity;
|
|
201
|
+
if (this._controller !== controller) return;
|
|
202
|
+
}
|
|
203
|
+
if (this._controller === controller) {
|
|
204
|
+
if (this.shouldResume()) this.restart();
|
|
205
|
+
else this.finish(new Error('Read model watch ended before subscription acknowledgment.'), false, true);
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (this._controller !== controller) return;
|
|
209
|
+
const code = (error as { code?: number })?.code;
|
|
210
|
+
if (this.shouldResume() && code !== undefined && [1, 4, 13, 14].includes(code)) this.restart();
|
|
211
|
+
// Let the consumer drain received changes before observing the terminal stream error.
|
|
212
|
+
else this.finish(error, true, true);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async deliver(change: ContractChangeset): Promise<void> {
|
|
217
|
+
if (this._stopped) return;
|
|
218
|
+
let converted: ReadModelChangeset<TReadModel>;
|
|
219
|
+
try {
|
|
220
|
+
converted = await this._convert(change);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Conversion includes compliance RPCs: even transport-coded errors here
|
|
223
|
+
// are terminal, not permission to skip an unreleased change.
|
|
224
|
+
this.finish(error, true);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (this._stopped) return;
|
|
228
|
+
const waiting = this._waiting.shift();
|
|
229
|
+
if (waiting) {
|
|
230
|
+
waiting.resolve({ done: false, value: converted });
|
|
231
|
+
} else {
|
|
232
|
+
// Before Subscribed we must keep reading to reach the readiness marker.
|
|
233
|
+
// Afterward the default pauses reads; only an explicit limit opts into failure.
|
|
234
|
+
if (this._buffered.length >= this._bufferLimit) {
|
|
235
|
+
this.finish(new Error(`Read model watcher buffer exceeded ${this._bufferLimit} changes. Consume changes faster or refresh the read model and create a new watcher.`), true);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this._buffered.push(converted);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async notifyResubscribed(callbacks: (() => void | Promise<void>)[]): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
for (const callback of callbacks) {
|
|
245
|
+
if (this._stopped) return;
|
|
246
|
+
if (this._onResubscribed.has(callback)) await callback();
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// A failed refresh is not a recovered watch, even for transport-coded errors.
|
|
250
|
+
this.finish(error, true);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private finish(reason: unknown, failed: boolean, preserveBuffered = false): void {
|
|
255
|
+
if (this._stopped) return;
|
|
256
|
+
this._stopped = true;
|
|
257
|
+
this._failed = failed;
|
|
258
|
+
this._failure = reason;
|
|
259
|
+
this._rejectSubscribed(reason);
|
|
260
|
+
this.clearRestartTimer();
|
|
261
|
+
this.cancelStream();
|
|
262
|
+
if (!preserveBuffered) this._buffered.length = 0;
|
|
263
|
+
for (const unsubscribe of this._unsubscribe) unsubscribe();
|
|
264
|
+
this._unsubscribe.length = 0;
|
|
265
|
+
this._onResubscribed.clear();
|
|
266
|
+
for (const waiting of this._waiting.splice(0)) {
|
|
267
|
+
if (failed) waiting.reject(reason);
|
|
268
|
+
else waiting.resolve({ done: true, value: undefined });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
package/readModels/ReadModels.ts
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
AppendedEvent,
|
|
6
|
+
ReadModelChangeset as ContractReadModelChangeset,
|
|
6
7
|
ReadModelObserverType
|
|
7
8
|
} from '@cratis/chronicle.contracts';
|
|
8
9
|
import {
|
|
9
|
-
ReadModelObserverType as ContractReadModelObserverType
|
|
10
|
+
ReadModelObserverType as ContractReadModelObserverType,
|
|
11
|
+
ReadModelChangeType as ContractReadModelChangeType
|
|
10
12
|
} from '@cratis/chronicle.contracts';
|
|
11
13
|
import type { Constructor } from '@cratis/fundamentals';
|
|
12
14
|
import { JsonSerializer } from '@cratis/fundamentals';
|
|
13
15
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
14
16
|
import { ChronicleConnection } from '../connection/index.js';
|
|
17
|
+
import type { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
|
|
18
|
+
import { fromContractsGuid } from '../connection/Guid.js';
|
|
15
19
|
import { ensureQuerySuccess } from '../connection/callResults.js';
|
|
16
20
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
17
21
|
import { getProjectionMetadata } from '../projections/declarative/projection.js';
|
|
@@ -31,7 +35,11 @@ import { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
|
|
|
31
35
|
import { deserializeReadModel } from './deserializeReadModel.js';
|
|
32
36
|
import type { ReadModelNamingPolicy } from './ReadModelNamingPolicy.js';
|
|
33
37
|
import type { IReadModels } from './IReadModels.js';
|
|
38
|
+
import type { IReadModelWatcher } from './IReadModelWatcher.js';
|
|
39
|
+
import { ReadModelWatcher } from './ReadModelWatcher.js';
|
|
34
40
|
import type { ReadModelChangeset } from './ReadModelChangeset.js';
|
|
41
|
+
import type { ReadModelWatchOptions } from './ReadModelWatchOptions.js';
|
|
42
|
+
import { ReadModelChangeType } from './ReadModelChangeType.js';
|
|
35
43
|
import type { ReadModelSnapshot } from './ReadModelSnapshot.js';
|
|
36
44
|
|
|
37
45
|
const unlimitedEventCount = BigInt('18446744073709551615');
|
|
@@ -52,6 +60,7 @@ interface ResolvedReadModel {
|
|
|
52
60
|
*/
|
|
53
61
|
export class ReadModels implements IReadModels {
|
|
54
62
|
readonly materialized: IMaterializedReadModels;
|
|
63
|
+
private readonly _watchCancellation = new AbortController();
|
|
55
64
|
|
|
56
65
|
constructor(
|
|
57
66
|
private readonly _eventStore: string,
|
|
@@ -60,7 +69,8 @@ export class ReadModels implements IReadModels {
|
|
|
60
69
|
private readonly _clientArtifacts: IClientArtifactsProvider,
|
|
61
70
|
private readonly _defaultSinkTypeId: string,
|
|
62
71
|
private readonly _isModelBoundProjectionRegistered?: (readModelType: Constructor) => boolean,
|
|
63
|
-
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
|
|
72
|
+
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy,
|
|
73
|
+
private readonly _lifecycle?: ConnectionLifecycle
|
|
64
74
|
) {
|
|
65
75
|
this.materialized = new MaterializedReadModels(_eventStore, _namespace, _connection);
|
|
66
76
|
}
|
|
@@ -185,18 +195,35 @@ export class ReadModels implements IReadModels {
|
|
|
185
195
|
continue;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
|
|
189
|
-
const requiresRelease = !changeset.Removed && readModel.observerType === ContractReadModelObserverType.Reducer &&
|
|
190
|
-
this.schemaHasComplianceMetadata(readModel.schema);
|
|
191
|
-
yield {
|
|
192
|
-
namespace: changeset.Namespace,
|
|
193
|
-
key: changeset.ModelKey,
|
|
194
|
-
readModel: requiresRelease ? await this.release(readModelType, instance) : instance,
|
|
195
|
-
removed: changeset.Removed
|
|
196
|
-
};
|
|
198
|
+
yield await this.toChangeset(readModelType, readModel, changeset);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
/** @inheritdoc */
|
|
203
|
+
createWatcher<TReadModel>(readModelType: Constructor<TReadModel>, options?: ReadModelWatchOptions): IReadModelWatcher<TReadModel> {
|
|
204
|
+
const readModel = this.resolveReadModel(readModelType);
|
|
205
|
+
const signal = AbortSignal.any(options?.signal
|
|
206
|
+
? [options.signal, this._watchCancellation.signal]
|
|
207
|
+
: [this._watchCancellation.signal]);
|
|
208
|
+
return new ReadModelWatcher(
|
|
209
|
+
streamSignal => this._connection.readModels.watch({
|
|
210
|
+
EventStore: this._eventStore,
|
|
211
|
+
Namespace: this._namespace,
|
|
212
|
+
ReadModelIdentifier: readModel.identifier,
|
|
213
|
+
EventSequenceId: readModel.eventSequenceId
|
|
214
|
+
}, { signal: streamSignal }),
|
|
215
|
+
changeset => this.toChangeset(readModelType, readModel, changeset),
|
|
216
|
+
signal,
|
|
217
|
+
this._lifecycle,
|
|
218
|
+
options
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Stops all watchers created through createWatcher for this event store. */
|
|
223
|
+
dispose(): void {
|
|
224
|
+
this._watchCancellation.abort();
|
|
225
|
+
}
|
|
226
|
+
|
|
200
227
|
/** @inheritdoc */
|
|
201
228
|
async dehydrateSession<TReadModel>(sessionId: string, readModelType: Constructor<TReadModel>, key: string): Promise<void> {
|
|
202
229
|
const readModel = this.resolveReadModel(readModelType);
|
|
@@ -355,6 +382,28 @@ export class ReadModels implements IReadModels {
|
|
|
355
382
|
return typeof value === 'string' && value.length > 0;
|
|
356
383
|
}
|
|
357
384
|
|
|
385
|
+
private async toChangeset<TReadModel>(readModelType: Constructor<TReadModel>, readModel: ResolvedReadModel, changeset: ContractReadModelChangeset): Promise<ReadModelChangeset<TReadModel>> {
|
|
386
|
+
const instance = this.deserializeReadModel(readModelType, changeset.ReadModel);
|
|
387
|
+
const occurred = changeset.Occurred?.Value ? new Date(changeset.Occurred.Value) : undefined;
|
|
388
|
+
const requiresRelease = !changeset.Removed && readModel.observerType === ContractReadModelObserverType.Reducer &&
|
|
389
|
+
this.schemaHasComplianceMetadata(readModel.schema);
|
|
390
|
+
return {
|
|
391
|
+
namespace: changeset.Namespace,
|
|
392
|
+
key: changeset.ModelKey,
|
|
393
|
+
readModel: requiresRelease ? await this.release(readModelType, instance) : instance,
|
|
394
|
+
removed: changeset.Removed,
|
|
395
|
+
changeType: changeset.ChangeType === ContractReadModelChangeType.Added ? ReadModelChangeType.Added :
|
|
396
|
+
changeset.ChangeType === ContractReadModelChangeType.Removed ? ReadModelChangeType.Removed : ReadModelChangeType.Modified,
|
|
397
|
+
changeContext: {
|
|
398
|
+
eventStore: this._eventStore,
|
|
399
|
+
namespace: changeset.Namespace,
|
|
400
|
+
sequenceNumber: changeset.EventSequenceNumber,
|
|
401
|
+
correlationId: changeset.CorrelationId ? fromContractsGuid(changeset.CorrelationId).toString() : undefined,
|
|
402
|
+
occurred: occurred && !Number.isNaN(occurred.getTime()) ? occurred : undefined
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
358
407
|
private async releaseSnapshotInstances<TReadModel>(readModelType: Constructor<TReadModel>, snapshots: ReadModelSnapshot<TReadModel>[]): Promise<ReadModelSnapshot<TReadModel>[]> {
|
|
359
408
|
return Promise.all(
|
|
360
409
|
snapshots.map(async snapshot => ({
|
package/readModels/for_ReadModelWatcher/against_a_kernel/when_watching_changes.integration.spec.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
7
|
+
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
8
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
9
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
10
|
+
import { eventType } from '../../../events/index.js';
|
|
11
|
+
import { fromEvent, removedWith } from '../../../projections/index.js';
|
|
12
|
+
import type { IReadModelWatcher } from '../../IReadModelWatcher.js';
|
|
13
|
+
import type { ReadModelChangeset } from '../../ReadModelChangeset.js';
|
|
14
|
+
import { ReadModelChangeType } from '../../ReadModelChangeType.js';
|
|
15
|
+
|
|
16
|
+
chai.should();
|
|
17
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
18
|
+
|
|
19
|
+
@eventType()
|
|
20
|
+
class NameChanged {
|
|
21
|
+
@field(String) name = '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@eventType()
|
|
25
|
+
class EntryRemoved {
|
|
26
|
+
@field(String) reason = 'removed';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@fromEvent(NameChanged)
|
|
30
|
+
@removedWith(EntryRemoved)
|
|
31
|
+
class WatchedEntry {
|
|
32
|
+
@field(String) name = '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const artifacts: IClientArtifactsProvider = {
|
|
36
|
+
eventTypes: [NameChanged, EntryRemoved], readModels: [WatchedEntry],
|
|
37
|
+
projections: [], reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [],
|
|
38
|
+
eventTypeMigrations: [], globalForHandlers: []
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe.skipIf(!connectionString && !process.env.CI)('when awaiting readiness before appending against a kernel', () => {
|
|
42
|
+
const storeName = `Watcher${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
43
|
+
const source = randomUUID();
|
|
44
|
+
const correlationIds = [randomUUID(), randomUUID(), randomUUID()];
|
|
45
|
+
const occurred = new Date('2026-01-02T03:04:05.123Z');
|
|
46
|
+
const changes: ReadModelChangeset<WatchedEntry>[] = [];
|
|
47
|
+
const sequenceNumbers: bigint[] = [];
|
|
48
|
+
let client: ChronicleClient;
|
|
49
|
+
let watcher: IReadModelWatcher<WatchedEntry>;
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
53
|
+
discoveryPatterns: [], clientArtifactsProvider: artifacts
|
|
54
|
+
}));
|
|
55
|
+
const store = await client.getEventStore(storeName);
|
|
56
|
+
watcher = store.readModels.createWatcher(WatchedEntry, { signal: AbortSignal.timeout(60_000) });
|
|
57
|
+
// No iteration, arbitrary sleep, or append before the server's readiness barrier.
|
|
58
|
+
await watcher.subscribed;
|
|
59
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
60
|
+
const events = [Object.assign(new NameChanged(), { name: 'first' }),
|
|
61
|
+
Object.assign(new NameChanged(), { name: 'second' }), new EntryRemoved()];
|
|
62
|
+
for (const [index, event] of events.entries()) {
|
|
63
|
+
const result = await store.eventLog.append(source, event, { correlationId: correlationIds[index], occurred });
|
|
64
|
+
result.isSuccess.should.be.true;
|
|
65
|
+
sequenceNumbers.push(result.sequenceNumber.value);
|
|
66
|
+
const change = await iterator.next();
|
|
67
|
+
if (change.done) throw new Error('Watcher ended before the appended change arrived.');
|
|
68
|
+
changes.push(change.value);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
afterAll(() => {
|
|
72
|
+
watcher?.dispose();
|
|
73
|
+
client?.dispose();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should observe creation, modification, and removal in order', () => {
|
|
77
|
+
changes.map(change => change.changeType).should.deep.equal([
|
|
78
|
+
ReadModelChangeType.Added, ReadModelChangeType.Modified, ReadModelChangeType.Removed
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
it('should preserve the existing model, key, namespace and removal fields', () => {
|
|
82
|
+
changes[0].readModel.should.be.instanceOf(WatchedEntry);
|
|
83
|
+
changes[0].readModel.name.should.equal('first');
|
|
84
|
+
changes[1].readModel.name.should.equal('second');
|
|
85
|
+
changes.map(change => change.key).should.deep.equal([source, source, source]);
|
|
86
|
+
changes.map(change => change.namespace).should.deep.equal(['Default', 'Default', 'Default']);
|
|
87
|
+
changes.map(change => change.removed).should.deep.equal([false, false, true]);
|
|
88
|
+
});
|
|
89
|
+
it('should preserve each triggering sequence number without numeric conversion', () => {
|
|
90
|
+
changes.map(change => change.changeContext!.sequenceNumber).should.deep.equal(sequenceNumbers);
|
|
91
|
+
});
|
|
92
|
+
it('should preserve each triggering correlation identifier', () => {
|
|
93
|
+
changes.map(change => change.changeContext!.correlationId).should.deep.equal(correlationIds);
|
|
94
|
+
});
|
|
95
|
+
it('should preserve each triggering timestamp', () => {
|
|
96
|
+
changes.map(change => change.changeContext!.occurred!.toISOString()).should.deep.equal([
|
|
97
|
+
occurred.toISOString(), occurred.toISOString(), occurred.toISOString()
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
it('should include the store and namespace in the triggering context', () => {
|
|
101
|
+
changes.every(change => change.changeContext!.eventStore === storeName &&
|
|
102
|
+
change.changeContext!.namespace === 'Default').should.be.true;
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { ReadModelChangeset } from '@cratis/chronicle.contracts';
|
|
5
|
+
|
|
6
|
+
export class a_kernel_stream implements AsyncIterableIterator<ReadModelChangeset> {
|
|
7
|
+
private readonly _messages: IteratorResult<ReadModelChangeset>[] = [];
|
|
8
|
+
private _waiting?: {
|
|
9
|
+
resolve: (result: IteratorResult<ReadModelChangeset>) => void;
|
|
10
|
+
reject: (error: unknown) => void;
|
|
11
|
+
};
|
|
12
|
+
signal!: AbortSignal;
|
|
13
|
+
closed = false;
|
|
14
|
+
pulls = 0;
|
|
15
|
+
|
|
16
|
+
[Symbol.asyncIterator]() { return this; }
|
|
17
|
+
|
|
18
|
+
next(): Promise<IteratorResult<ReadModelChangeset>> {
|
|
19
|
+
this.pulls++;
|
|
20
|
+
const message = this._messages.shift();
|
|
21
|
+
if (message) return Promise.resolve(message);
|
|
22
|
+
if (this.closed) return Promise.resolve({ done: true, value: undefined });
|
|
23
|
+
return new Promise((resolve, reject) => { this._waiting = { resolve, reject }; });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async return(): Promise<IteratorResult<ReadModelChangeset>> {
|
|
27
|
+
this.end();
|
|
28
|
+
return { done: true, value: undefined };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
send(change: Partial<ReadModelChangeset>): void {
|
|
32
|
+
const result = { done: false as const, value: ReadModelChangeset.fromPartial(change) };
|
|
33
|
+
if (this._waiting) {
|
|
34
|
+
this._waiting.resolve(result);
|
|
35
|
+
this._waiting = undefined;
|
|
36
|
+
} else this._messages.push(result);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
end(): void {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
this._waiting?.resolve({ done: true, value: undefined });
|
|
42
|
+
this._waiting = undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fail(error: unknown): void {
|
|
46
|
+
if (!this._waiting) throw new Error('The stream must be reading before failing it.');
|
|
47
|
+
this._waiting.reject(error);
|
|
48
|
+
this._waiting = undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { vi } from 'vitest';
|
|
5
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
6
|
+
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
8
|
+
import { fromEvent } from '../../../projections/index.js';
|
|
9
|
+
import { ReadModels } from '../../ReadModels.js';
|
|
10
|
+
import { a_kernel_stream } from './a_kernel_stream.js';
|
|
11
|
+
|
|
12
|
+
export class a_watched_read_model {
|
|
13
|
+
readonly model = class Model { id = ''; };
|
|
14
|
+
readonly lifecycle = new ConnectionLifecycle();
|
|
15
|
+
readonly streams: a_kernel_stream[] = [];
|
|
16
|
+
readonly watch = vi.fn((_request: unknown, options?: { signal: AbortSignal }) => {
|
|
17
|
+
const stream = new a_kernel_stream();
|
|
18
|
+
if (options) {
|
|
19
|
+
stream.signal = options.signal;
|
|
20
|
+
options.signal.addEventListener('abort', () => stream.end(), { once: true });
|
|
21
|
+
}
|
|
22
|
+
this.streams.push(stream);
|
|
23
|
+
return stream;
|
|
24
|
+
});
|
|
25
|
+
readonly readModels: ReadModels;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
fromEvent(class Changed {})(this.model);
|
|
29
|
+
const artifacts = { readModels: [this.model], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
|
|
30
|
+
const connection = { readModels: { watch: this.watch } } as unknown as ChronicleConnection;
|
|
31
|
+
this.readModels = new ReadModels('store', 'tenant', connection, artifacts, 'sink', undefined, undefined, this.lifecycle);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async connect() { await this.lifecycle.connected(error => { throw error; }); }
|
|
35
|
+
async disconnect() { await this.lifecycle.disconnected(error => { throw error; }); }
|
|
36
|
+
async flush() { await new Promise<void>(resolve => setImmediate(resolve)); }
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { chai, describe, expectTypeOf, it } from 'vitest';
|
|
6
|
+
import type { IReadModels } from '../IReadModels.js';
|
|
7
|
+
import type { ReadModels } from '../ReadModels.js';
|
|
8
|
+
import type { ReadModelChangeset } from '../ReadModelChangeset.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when implementing the original watch contract', () => {
|
|
13
|
+
it('should keep exactly the signature exported on main', () => {
|
|
14
|
+
expectTypeOf<IReadModels['watch']>().toEqualTypeOf<
|
|
15
|
+
<TReadModel>(readModelType: Constructor<TReadModel>) => AsyncIterable<ReadModelChangeset<TReadModel>>
|
|
16
|
+
>();
|
|
17
|
+
expectTypeOf<ReadModels['watch']>().toEqualTypeOf<IReadModels['watch']>();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should accept an existing async generator implementation without watcher members', async () => {
|
|
21
|
+
const mock: Pick<IReadModels, 'watch'> = {
|
|
22
|
+
async *watch<TReadModel>(readModelType: Constructor<TReadModel>) {
|
|
23
|
+
yield { namespace: 'tenant', key: 'one', readModel: new readModelType(), removed: false };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const iterator = mock.watch(class Model {})[Symbol.asyncIterator]();
|
|
27
|
+
(await iterator.next()).value.key.should.equal('one');
|
|
28
|
+
});
|
|
29
|
+
});
|