@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,86 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
6
|
+
import { ReadModelWatcher } from '../ReadModelWatcher.js';
|
|
7
|
+
import { a_kernel_stream } from './given/a_kernel_stream.js';
|
|
8
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when a read model stream cannot recover', () => {
|
|
13
|
+
it.each([3, 7, 16])('should terminate on non-transport status %s', async code => {
|
|
14
|
+
const context = new a_watched_read_model();
|
|
15
|
+
await context.connect();
|
|
16
|
+
const watcher = context.readModels.createWatcher(context.model, { resume: true });
|
|
17
|
+
const failure = { code };
|
|
18
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
19
|
+
const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
20
|
+
try {
|
|
21
|
+
context.streams[0].fail(failure);
|
|
22
|
+
(await readiness).should.equal(failure);
|
|
23
|
+
(await next).should.equal(failure);
|
|
24
|
+
await context.disconnect();
|
|
25
|
+
await context.connect();
|
|
26
|
+
context.streams.should.have.lengthOf(1);
|
|
27
|
+
} finally { watcher.dispose(); }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should not retry a transport-coded compliance or conversion failure', async () => {
|
|
31
|
+
const lifecycle = new ConnectionLifecycle();
|
|
32
|
+
await lifecycle.connected(error => { throw error; });
|
|
33
|
+
const stream = new a_kernel_stream();
|
|
34
|
+
const failure = { code: 14 };
|
|
35
|
+
let opened = 0;
|
|
36
|
+
const watcher = new ReadModelWatcher(() => { opened++; return stream; }, async () => { throw failure; }, new AbortController().signal, lifecycle, { resume: true });
|
|
37
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
38
|
+
const next = watcher.next().catch(error => error);
|
|
39
|
+
try {
|
|
40
|
+
stream.send({ ReadModel: '{}' });
|
|
41
|
+
(await readiness).should.equal(failure);
|
|
42
|
+
(await next).should.equal(failure);
|
|
43
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
44
|
+
await lifecycle.connected(error => { throw error; });
|
|
45
|
+
opened.should.equal(1);
|
|
46
|
+
} finally { watcher.dispose(); }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each([false, true])('should retain completed standalone changes until consumed or disposed (disposed: %s)', async disposed => {
|
|
50
|
+
const stream = new a_kernel_stream();
|
|
51
|
+
const watcher = new ReadModelWatcher(() => stream, async change => ({
|
|
52
|
+
namespace: change.Namespace, key: change.ModelKey, readModel: {}, removed: false
|
|
53
|
+
}), new AbortController().signal);
|
|
54
|
+
try {
|
|
55
|
+
stream.send({ Subscribed: true });
|
|
56
|
+
stream.send({ ModelKey: 'first' });
|
|
57
|
+
stream.send({ ModelKey: 'second' });
|
|
58
|
+
stream.end();
|
|
59
|
+
await watcher.subscribed;
|
|
60
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
61
|
+
if (disposed) watcher.dispose();
|
|
62
|
+
else {
|
|
63
|
+
(await watcher.next()).value.key.should.equal('first');
|
|
64
|
+
(await watcher.next()).value.key.should.equal('second');
|
|
65
|
+
}
|
|
66
|
+
(await watcher.next()).done!.should.be.true;
|
|
67
|
+
} finally { watcher.dispose(); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([false, true])('should end a standalone stream cleanly (acknowledged: %s)', async acknowledged => {
|
|
71
|
+
const stream = new a_kernel_stream();
|
|
72
|
+
const watcher = new ReadModelWatcher(() => stream, async () => { throw new Error('Unexpected data'); }, new AbortController().signal);
|
|
73
|
+
try {
|
|
74
|
+
if (acknowledged) {
|
|
75
|
+
stream.send({ Subscribed: true });
|
|
76
|
+
await watcher.subscribed;
|
|
77
|
+
}
|
|
78
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
79
|
+
const next = watcher.next();
|
|
80
|
+
stream.end();
|
|
81
|
+
(await next).done!.should.be.true;
|
|
82
|
+
if (!acknowledged) (await readiness).message.should.contain('before subscription acknowledgment');
|
|
83
|
+
else await readiness;
|
|
84
|
+
} finally { watcher.dispose(); }
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when applying read model watch backpressure', () => {
|
|
10
|
+
let context: a_watched_read_model;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
context = new a_watched_read_model();
|
|
13
|
+
await context.connect();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => context.readModels.dispose());
|
|
16
|
+
|
|
17
|
+
it('should pause a created watcher for a slow consumer and deliver more than the buffer limit without failure', async () => {
|
|
18
|
+
const iterator = context.readModels.createWatcher(context.model)[Symbol.asyncIterator]();
|
|
19
|
+
const stream = context.streams[0];
|
|
20
|
+
stream.send({ Subscribed: true });
|
|
21
|
+
for (let index = 0; index < 2050; index++) stream.send({ ModelKey: `${index}`, ReadModel: '{}' });
|
|
22
|
+
await context.flush();
|
|
23
|
+
stream.pulls.should.equal(1025); // One acknowledgment and 1,024 buffered changes.
|
|
24
|
+
stream.signal.aborted.should.be.false;
|
|
25
|
+
(await iterator.next()).value.key.should.equal('0');
|
|
26
|
+
await context.flush();
|
|
27
|
+
stream.pulls.should.equal(1026);
|
|
28
|
+
for (let index = 1; index < 2050; index++) {
|
|
29
|
+
(await iterator.next()).value.key.should.equal(`${index}`);
|
|
30
|
+
}
|
|
31
|
+
stream.end();
|
|
32
|
+
(await iterator.next()).done!.should.be.true;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should reach acknowledgment with a full pre-acknowledgment buffer then pause until consumption', async () => {
|
|
36
|
+
const watcher = context.readModels.createWatcher(context.model);
|
|
37
|
+
const stream = context.streams[0];
|
|
38
|
+
for (let index = 0; index < 1024; index++) stream.send({ ModelKey: `${index}`, ReadModel: '{}' });
|
|
39
|
+
stream.send({ Subscribed: true });
|
|
40
|
+
stream.send({ ModelKey: 'after', ReadModel: '{}' });
|
|
41
|
+
await watcher.subscribed;
|
|
42
|
+
await context.flush();
|
|
43
|
+
stream.pulls.should.equal(1025);
|
|
44
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
45
|
+
for (let index = 0; index < 1024; index++) (await iterator.next()).value.key.should.equal(`${index}`);
|
|
46
|
+
(await iterator.next()).value.key.should.equal('after');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should cancel a paused read on disposal', async () => {
|
|
50
|
+
const watcher = context.readModels.createWatcher(context.model);
|
|
51
|
+
const stream = context.streams[0];
|
|
52
|
+
stream.send({ Subscribed: true });
|
|
53
|
+
for (let index = 0; index < 1024; index++) stream.send({ ReadModel: '{}' });
|
|
54
|
+
await context.flush();
|
|
55
|
+
watcher.dispose();
|
|
56
|
+
await context.flush();
|
|
57
|
+
stream.closed.should.be.true;
|
|
58
|
+
stream.signal.aborted.should.be.true;
|
|
59
|
+
stream.pulls.should.equal(1025);
|
|
60
|
+
(await watcher[Symbol.asyncIterator]().next()).done!.should.be.true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should read eagerly again until each resumed stream acknowledges even with a full buffer', async () => {
|
|
64
|
+
const watcher = context.readModels.createWatcher(context.model, { resume: true });
|
|
65
|
+
context.streams[0].send({ Subscribed: true });
|
|
66
|
+
for (let index = 0; index < 1024; index++) context.streams[0].send({ ModelKey: `${index}`, ReadModel: '{}' });
|
|
67
|
+
await context.flush();
|
|
68
|
+
await context.disconnect();
|
|
69
|
+
await context.connect();
|
|
70
|
+
context.streams[1].send({ Subscribed: true });
|
|
71
|
+
context.streams[1].send({ ModelKey: 'resumed', ReadModel: '{}' });
|
|
72
|
+
await watcher.subscribed;
|
|
73
|
+
await context.flush();
|
|
74
|
+
context.streams[0].closed.should.be.true;
|
|
75
|
+
context.streams[1].pulls.should.equal(1);
|
|
76
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
77
|
+
for (let index = 0; index < 1024; index++) (await iterator.next()).value.key.should.equal(`${index}`);
|
|
78
|
+
(await iterator.next()).value.key.should.equal('resumed');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each(['transport', 'stream', 'connection'] as const)('when a read model watcher has buffered changes before a terminal %s failure', kind => {
|
|
10
|
+
let context: a_watched_read_model;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
context = new a_watched_read_model();
|
|
13
|
+
await context.connect();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => context.readModels.dispose());
|
|
16
|
+
|
|
17
|
+
it.each([false, true])('should drain received changes before rejecting with the original error (acknowledged: %s)', async acknowledged => {
|
|
18
|
+
const watcher = context.readModels.createWatcher(context.model, { resume: kind !== 'transport' });
|
|
19
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
20
|
+
const stream = context.streams[0];
|
|
21
|
+
if (acknowledged) {
|
|
22
|
+
stream.send({ Subscribed: true });
|
|
23
|
+
await readiness;
|
|
24
|
+
}
|
|
25
|
+
stream.send({ ModelKey: 'first', ReadModel: '{"id":"first"}' });
|
|
26
|
+
stream.send({ ModelKey: 'second', ReadModel: '{"id":"second"}' });
|
|
27
|
+
await context.flush();
|
|
28
|
+
const failure = Object.assign(new Error('terminal failure'), { code: kind === 'transport' ? 14 : 7 });
|
|
29
|
+
if (kind === 'connection') await context.lifecycle.failed(failure, error => { throw error; });
|
|
30
|
+
else stream.fail(failure);
|
|
31
|
+
await context.flush();
|
|
32
|
+
if (!acknowledged) (await readiness).should.equal(failure);
|
|
33
|
+
stream.signal.aborted.should.be.true;
|
|
34
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
35
|
+
(await iterator.next()).value.should.contain({ key: 'first' });
|
|
36
|
+
(await iterator.next()).value.should.contain({ key: 'second' });
|
|
37
|
+
(await iterator.next().catch(error => error)).should.equal(failure);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each(['watch', 'createWatcher'] as const)('when %s has not opted into resumption', method => {
|
|
10
|
+
let context: a_watched_read_model;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
vi.useFakeTimers();
|
|
13
|
+
context = new a_watched_read_model();
|
|
14
|
+
await context.connect();
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
context.readModels.dispose();
|
|
18
|
+
vi.useRealTimers();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([1, 4, 13, 14])('should reject iteration with the original transport error for status %s', async code => {
|
|
22
|
+
const iterator = context.readModels[method](context.model)[Symbol.asyncIterator]();
|
|
23
|
+
const next = iterator.next().catch(error => error);
|
|
24
|
+
context.streams[0].send({ Subscribed: true });
|
|
25
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
26
|
+
const failure = Object.assign(new Error('transport failed'), { code });
|
|
27
|
+
// A keep-alive disconnect arriving first must not replace the watch stream error.
|
|
28
|
+
await context.disconnect();
|
|
29
|
+
context.streams[0].closed.should.be.false;
|
|
30
|
+
context.streams[0].fail(failure);
|
|
31
|
+
(await next).should.equal(failure);
|
|
32
|
+
await context.connect();
|
|
33
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
34
|
+
context.streams.should.have.lengthOf(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should complete iteration without restarting after stream completion', async () => {
|
|
38
|
+
const iterator = context.readModels[method](context.model)[Symbol.asyncIterator]();
|
|
39
|
+
const next = iterator.next();
|
|
40
|
+
context.streams[0].send({ Subscribed: true });
|
|
41
|
+
context.streams[0].send({ ModelKey: 'received', ReadModel: '{}' });
|
|
42
|
+
context.streams[0].end();
|
|
43
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
44
|
+
(await next).value.key.should.equal('received');
|
|
45
|
+
(await iterator.next()).done!.should.be.true;
|
|
46
|
+
context.streams.should.have.lengthOf(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should stop opting into resumption after the last callback is removed', async () => {
|
|
50
|
+
const watcher = context.readModels.createWatcher(context.model);
|
|
51
|
+
const unsubscribe = watcher.onResubscribed(() => {});
|
|
52
|
+
context.streams[0].send({ Subscribed: true });
|
|
53
|
+
await watcher.subscribed;
|
|
54
|
+
unsubscribe();
|
|
55
|
+
const failure = { code: 14 };
|
|
56
|
+
const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
57
|
+
context.streams[0].fail(failure);
|
|
58
|
+
(await next).should.equal(failure);
|
|
59
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
60
|
+
context.streams.should.have.lengthOf(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
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 { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import type { IReadModelWatcher } from '../IReadModelWatcher.js';
|
|
6
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when notifying read model resubscription', () => {
|
|
11
|
+
let context: a_watched_read_model;
|
|
12
|
+
let watcher: IReadModelWatcher<{ id: string }>;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
context = new a_watched_read_model();
|
|
15
|
+
await context.connect();
|
|
16
|
+
watcher = context.readModels.createWatcher(context.model);
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => context.readModels.dispose());
|
|
19
|
+
|
|
20
|
+
it('should notify once per acknowledged resubscription and ignore duplicate markers', async () => {
|
|
21
|
+
const callback = vi.fn();
|
|
22
|
+
watcher.onResubscribed(callback);
|
|
23
|
+
context.streams[0].send({ Subscribed: true });
|
|
24
|
+
await watcher.subscribed;
|
|
25
|
+
for (let generation = 1; generation <= 2; generation++) {
|
|
26
|
+
await context.disconnect();
|
|
27
|
+
await context.connect();
|
|
28
|
+
callback.mock.calls.should.have.lengthOf(generation - 1);
|
|
29
|
+
context.streams[generation].send({ Subscribed: true });
|
|
30
|
+
context.streams[generation].send({ Subscribed: true });
|
|
31
|
+
await context.flush();
|
|
32
|
+
callback.mock.calls.should.have.lengthOf(generation);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should not notify for the first acknowledgment after an unacknowledged disconnect', async () => {
|
|
37
|
+
const callback = vi.fn();
|
|
38
|
+
watcher.onResubscribed(callback);
|
|
39
|
+
await context.disconnect();
|
|
40
|
+
await context.connect();
|
|
41
|
+
context.streams[1].send({ Subscribed: true });
|
|
42
|
+
await watcher.subscribed;
|
|
43
|
+
await context.flush();
|
|
44
|
+
callback.mock.calls.should.have.lengthOf(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should unregister callbacks idempotently without removing other callbacks', async () => {
|
|
48
|
+
const removed = vi.fn();
|
|
49
|
+
const remaining = vi.fn();
|
|
50
|
+
const unsubscribe = watcher.onResubscribed(removed);
|
|
51
|
+
watcher.onResubscribed(remaining);
|
|
52
|
+
context.streams[0].send({ Subscribed: true });
|
|
53
|
+
await watcher.subscribed;
|
|
54
|
+
unsubscribe();
|
|
55
|
+
unsubscribe();
|
|
56
|
+
await context.disconnect();
|
|
57
|
+
await context.connect();
|
|
58
|
+
context.streams[1].send({ Subscribed: true });
|
|
59
|
+
await context.flush();
|
|
60
|
+
removed.mock.calls.should.have.lengthOf(0);
|
|
61
|
+
remaining.mock.calls.should.have.lengthOf(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should await refresh callbacks in registration order before delivering resumed changes', async () => {
|
|
65
|
+
let finishRefresh!: () => void;
|
|
66
|
+
const refresh = new Promise<void>(resolve => { finishRefresh = resolve; });
|
|
67
|
+
const calls: string[] = [];
|
|
68
|
+
watcher.onResubscribed(async () => {
|
|
69
|
+
calls.push('refresh');
|
|
70
|
+
await refresh;
|
|
71
|
+
});
|
|
72
|
+
watcher.onResubscribed(() => { calls.push('refreshed'); });
|
|
73
|
+
context.streams[0].send({ Subscribed: true });
|
|
74
|
+
await watcher.subscribed;
|
|
75
|
+
const next = watcher[Symbol.asyncIterator]().next().then(change => {
|
|
76
|
+
calls.push('change');
|
|
77
|
+
return change;
|
|
78
|
+
});
|
|
79
|
+
await context.disconnect();
|
|
80
|
+
await context.connect();
|
|
81
|
+
context.streams[1].send({ Subscribed: true });
|
|
82
|
+
context.streams[1].send({ ModelKey: 'new', ReadModel: '{}' });
|
|
83
|
+
await context.flush();
|
|
84
|
+
calls.should.deep.equal(['refresh']);
|
|
85
|
+
finishRefresh();
|
|
86
|
+
(await next).value.key.should.equal('new');
|
|
87
|
+
calls.should.deep.equal(['refresh', 'refreshed', 'change']);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it.each([false, true])('should fail iteration on a callback failure even with a transport code (async: %s)', async asynchronous => {
|
|
91
|
+
const failure = { code: 14 };
|
|
92
|
+
watcher.onResubscribed(() => {
|
|
93
|
+
if (asynchronous) return Promise.reject(failure);
|
|
94
|
+
throw failure;
|
|
95
|
+
});
|
|
96
|
+
context.streams[0].send({ Subscribed: true });
|
|
97
|
+
await watcher.subscribed;
|
|
98
|
+
const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
99
|
+
await context.disconnect();
|
|
100
|
+
await context.connect();
|
|
101
|
+
context.streams[1].send({ Subscribed: true });
|
|
102
|
+
(await next).should.equal(failure);
|
|
103
|
+
context.streams[1].signal.aborted.should.be.true;
|
|
104
|
+
await context.disconnect();
|
|
105
|
+
await context.connect();
|
|
106
|
+
context.streams.should.have.lengthOf(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should stop notifying callbacks when disposed during a refresh', async () => {
|
|
110
|
+
const remaining = vi.fn();
|
|
111
|
+
watcher.onResubscribed(() => watcher.dispose());
|
|
112
|
+
watcher.onResubscribed(remaining);
|
|
113
|
+
context.streams[0].send({ Subscribed: true });
|
|
114
|
+
await watcher.subscribed;
|
|
115
|
+
const next = watcher[Symbol.asyncIterator]().next();
|
|
116
|
+
await context.disconnect();
|
|
117
|
+
await context.connect();
|
|
118
|
+
context.streams[1].send({ Subscribed: true });
|
|
119
|
+
(await next).done!.should.be.true;
|
|
120
|
+
await context.flush();
|
|
121
|
+
remaining.mock.calls.should.have.lengthOf(0);
|
|
122
|
+
const unsubscribe = watcher.onResubscribed(remaining);
|
|
123
|
+
unsubscribe();
|
|
124
|
+
await context.disconnect();
|
|
125
|
+
await context.connect();
|
|
126
|
+
context.streams.should.have.lengthOf(2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when opting into watch overflow failure', () => {
|
|
10
|
+
let context: a_watched_read_model;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
context = new a_watched_read_model();
|
|
13
|
+
await context.connect();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => context.readModels.dispose());
|
|
16
|
+
|
|
17
|
+
it.each([false, true])('should fail instead of dropping changes above the explicit limit (acknowledged: %s)', async acknowledged => {
|
|
18
|
+
const watcher = context.readModels.createWatcher(context.model, { maxBuffered: 2, resume: true });
|
|
19
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
20
|
+
if (acknowledged) {
|
|
21
|
+
context.streams[0].send({ Subscribed: true });
|
|
22
|
+
await readiness;
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < 3; index++) context.streams[0].send({ ReadModel: '{}' });
|
|
25
|
+
await context.flush();
|
|
26
|
+
const failure = await watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
27
|
+
failure.message.should.contain('buffer exceeded 2 changes');
|
|
28
|
+
if (!acknowledged) (await readiness).should.equal(failure);
|
|
29
|
+
context.streams[0].signal.aborted.should.be.true;
|
|
30
|
+
await context.disconnect();
|
|
31
|
+
await context.connect();
|
|
32
|
+
context.streams.should.have.lengthOf(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([0, -1, 1.5, Infinity, NaN, Number.MAX_SAFE_INTEGER + 1])('should reject an invalid limit %s before opening a stream', maxBuffered => {
|
|
36
|
+
(() => context.readModels.createWatcher(context.model, { maxBuffered })).should.throw(RangeError);
|
|
37
|
+
context.streams.should.have.lengthOf(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { Guid } from '@cratis/fundamentals';
|
|
5
|
+
import { ReadModelChangeType as ContractChangeType } from '@cratis/chronicle.contracts';
|
|
6
|
+
import { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
7
|
+
import { toContractsGuid } from '../../connection/Guid.js';
|
|
8
|
+
import type { IReadModelWatcher } from '../IReadModelWatcher.js';
|
|
9
|
+
import { ReadModelChangeType } from '../ReadModelChangeType.js';
|
|
10
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
|
|
14
|
+
describe('when receiving read model changes', () => {
|
|
15
|
+
let context: a_watched_read_model;
|
|
16
|
+
let watcher: IReadModelWatcher<{ id: string }>;
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
context = new a_watched_read_model();
|
|
19
|
+
await context.connect();
|
|
20
|
+
watcher = context.readModels.createWatcher(context.model);
|
|
21
|
+
context.streams[0].send({ Subscribed: true });
|
|
22
|
+
await watcher.subscribed;
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => watcher.dispose());
|
|
25
|
+
|
|
26
|
+
for (const [contractKind, kind] of [
|
|
27
|
+
[ContractChangeType.Added, ReadModelChangeType.Added],
|
|
28
|
+
[ContractChangeType.Modified, ReadModelChangeType.Modified],
|
|
29
|
+
[ContractChangeType.Removed, ReadModelChangeType.Removed],
|
|
30
|
+
[ContractChangeType.UNRECOGNIZED, ReadModelChangeType.Modified]
|
|
31
|
+
]) {
|
|
32
|
+
it(`should preserve ${ContractChangeType[contractKind]} changes and their triggering context`, async () => {
|
|
33
|
+
const removed = contractKind === ContractChangeType.Removed;
|
|
34
|
+
const occurred = '2026-06-07T08:09:10.123Z';
|
|
35
|
+
const correlationId = '12345678-1234-5678-9abc-123456789abc';
|
|
36
|
+
context.streams[0].send({ Namespace: 'tenant', ModelKey: 'one', ReadModel: removed ? '' : '{"id":"one"}',
|
|
37
|
+
Removed: removed, ChangeType: contractKind, EventSequenceNumber: 9007199254740997n,
|
|
38
|
+
CorrelationId: toContractsGuid(Guid.as(correlationId)), Occurred: { Value: occurred } });
|
|
39
|
+
const change = (await watcher[Symbol.asyncIterator]().next()).value;
|
|
40
|
+
change.namespace.should.equal('tenant');
|
|
41
|
+
change.key.should.equal('one');
|
|
42
|
+
change.removed.should.equal(removed);
|
|
43
|
+
change.readModel.should.be.instanceOf(context.model);
|
|
44
|
+
change.changeType.should.equal(kind);
|
|
45
|
+
change.changeContext.should.deep.equal({ eventStore: 'store', namespace: 'tenant',
|
|
46
|
+
sequenceNumber: 9007199254740997n, correlationId, occurred: new Date(occurred) });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it('should leave absent timestamp and correlation metadata absent', async () => {
|
|
51
|
+
context.streams[0].send({ ReadModel: '{}' });
|
|
52
|
+
const change = (await watcher[Symbol.asyncIterator]().next()).value;
|
|
53
|
+
(change.changeContext.occurred === undefined).should.be.true;
|
|
54
|
+
(change.changeContext.correlationId === undefined).should.be.true;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it.each(['', 'not a timestamp'])('should leave an empty or invalid timestamp (%s) absent', async value => {
|
|
58
|
+
context.streams[0].send({ ReadModel: '{}', Occurred: { Value: value } });
|
|
59
|
+
const change = (await watcher[Symbol.asyncIterator]().next()).value;
|
|
60
|
+
(change.changeContext.occurred === undefined).should.be.true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should surface a deserialization failure and stop the stream', async () => {
|
|
64
|
+
const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
65
|
+
context.streams[0].send({ ReadModel: 'not json' });
|
|
66
|
+
(await next).should.be.instanceOf(SyntaxError);
|
|
67
|
+
context.streams[0].signal.aborted.should.be.true;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import type { IReadModelWatcher } from '../IReadModelWatcher.js';
|
|
6
|
+
import { a_watched_read_model } from './given/a_watched_read_model.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when reconnecting a read model watcher', () => {
|
|
11
|
+
let context: a_watched_read_model;
|
|
12
|
+
let watcher: IReadModelWatcher<{ id: string }>;
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
context = new a_watched_read_model();
|
|
15
|
+
await context.connect();
|
|
16
|
+
watcher = context.readModels.createWatcher(context.model, { resume: true });
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => context.readModels.dispose());
|
|
19
|
+
|
|
20
|
+
it('should replace resolved readiness at disconnect and continue the same iterator after a fresh acknowledgment', async () => {
|
|
21
|
+
context.streams[0].send({ Subscribed: true });
|
|
22
|
+
const firstReadiness = watcher.subscribed;
|
|
23
|
+
await firstReadiness;
|
|
24
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
25
|
+
const next = iterator.next();
|
|
26
|
+
await context.disconnect();
|
|
27
|
+
context.streams[0].signal.aborted.should.be.true;
|
|
28
|
+
watcher.subscribed.should.not.equal(firstReadiness);
|
|
29
|
+
let ready = false;
|
|
30
|
+
void watcher.subscribed.then(() => { ready = true; });
|
|
31
|
+
await context.connect();
|
|
32
|
+
await context.flush();
|
|
33
|
+
ready.should.be.false;
|
|
34
|
+
context.streams[1].send({ Subscribed: true });
|
|
35
|
+
await watcher.subscribed;
|
|
36
|
+
ready.should.be.true;
|
|
37
|
+
context.streams[1].send({ ModelKey: 'new', ReadModel: '{"id":"new"}' });
|
|
38
|
+
(await next).value.key.should.equal('new');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should preserve still pending readiness across a disconnect before acknowledgment', async () => {
|
|
42
|
+
const pending = watcher.subscribed;
|
|
43
|
+
await context.disconnect();
|
|
44
|
+
watcher.subscribed.should.equal(pending);
|
|
45
|
+
await context.connect();
|
|
46
|
+
context.streams[1].send({ Subscribed: true });
|
|
47
|
+
await pending;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should deliver buffered changes from the previous connection before new changes', async () => {
|
|
51
|
+
context.streams[0].send({ Subscribed: true });
|
|
52
|
+
context.streams[0].send({ ModelKey: 'old', ReadModel: '{"id":"old"}' });
|
|
53
|
+
await watcher.subscribed;
|
|
54
|
+
await context.flush();
|
|
55
|
+
await context.disconnect();
|
|
56
|
+
await context.connect();
|
|
57
|
+
context.streams[1].send({ Subscribed: true });
|
|
58
|
+
context.streams[1].send({ ModelKey: 'new', ReadModel: '{"id":"new"}' });
|
|
59
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
60
|
+
(await iterator.next()).value.key.should.equal('old');
|
|
61
|
+
(await iterator.next()).value.key.should.equal('new');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should acknowledge a new subscription before consuming its buffered changes', async () => {
|
|
65
|
+
context.streams[0].send({ Subscribed: true });
|
|
66
|
+
await watcher.subscribed;
|
|
67
|
+
const iterator = watcher[Symbol.asyncIterator]();
|
|
68
|
+
await context.disconnect();
|
|
69
|
+
const readiness = watcher.subscribed;
|
|
70
|
+
await context.connect();
|
|
71
|
+
context.streams[1].send({ ModelKey: 'new', ReadModel: '{"id":"new"}' });
|
|
72
|
+
context.streams[1].send({ Subscribed: true });
|
|
73
|
+
await readiness;
|
|
74
|
+
(await iterator.next()).value.key.should.equal('new');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should not create duplicate streams for repeated connected notifications', async () => {
|
|
78
|
+
await context.connect();
|
|
79
|
+
context.streams.should.have.lengthOf(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should not restart a disposed watcher', async () => {
|
|
83
|
+
watcher.dispose();
|
|
84
|
+
await context.disconnect();
|
|
85
|
+
await context.connect();
|
|
86
|
+
context.streams.should.have.lengthOf(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should defer opening a watcher created while disconnected', async () => {
|
|
90
|
+
watcher.dispose();
|
|
91
|
+
await context.disconnect();
|
|
92
|
+
watcher = context.readModels.createWatcher(context.model);
|
|
93
|
+
context.streams.should.have.lengthOf(1);
|
|
94
|
+
await context.connect();
|
|
95
|
+
context.streams.should.have.lengthOf(2);
|
|
96
|
+
context.streams[1].send({ Subscribed: true });
|
|
97
|
+
await watcher.subscribed;
|
|
98
|
+
});
|
|
99
|
+
});
|