@cratis/chronicle 6.46.0 → 6.48.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 +2 -1
- 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.map +1 -1
- package/dist/EventStore.js +2 -1
- package/dist/EventStore.js.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/events/eventSourceTypeDecorator.d.ts +18 -0
- package/dist/events/eventSourceTypeDecorator.d.ts.map +1 -0
- package/dist/events/eventSourceTypeDecorator.js +25 -0
- package/dist/events/eventSourceTypeDecorator.js.map +1 -0
- package/dist/events/eventStreamTypeDecorator.d.ts +20 -0
- package/dist/events/eventStreamTypeDecorator.d.ts.map +1 -0
- package/dist/events/eventStreamTypeDecorator.js +29 -0
- package/dist/events/eventStreamTypeDecorator.js.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +2 -0
- package/dist/events/index.js.map +1 -1
- package/dist/reactors/ReactorSideEffects.d.ts +6 -3
- package/dist/reactors/ReactorSideEffects.d.ts.map +1 -1
- package/dist/reactors/ReactorSideEffects.js +12 -6
- package/dist/reactors/ReactorSideEffects.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +4 -2
- package/dist/reactors/Reactors.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/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +4 -2
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +11 -1
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.js +13 -0
- package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
- package/dist/testing/UnsupportedReducerOperation.d.ts +7 -0
- package/dist/testing/UnsupportedReducerOperation.d.ts.map +1 -0
- package/dist/testing/UnsupportedReducerOperation.js +14 -0
- package/dist/testing/UnsupportedReducerOperation.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/events/eventSourceTypeDecorator.ts +28 -0
- package/events/eventStreamTypeDecorator.ts +33 -0
- package/events/for_observerFilters/when_registering/filter_registration_behaviors.fixture.ts +55 -0
- package/events/for_observerFilters/when_registering/with_legacy_decorators.legacy.spec.ts +30 -0
- package/events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts +30 -0
- package/events/index.ts +2 -0
- package/for_ChronicleClient/when_reconnecting/with_waiting_read_model_watchers.spec.ts +117 -0
- package/jobs/for_Jobs/against_a_kernel/when_managing_a_replay_job.integration.spec.ts +24 -4
- package/package.json +2 -2
- package/reactors/ReactorSideEffects.ts +16 -6
- package/reactors/Reactors.ts +4 -2
- package/reactors/for_ReactorSideEffects/when_dispatching/with_reactor_source_and_stream_types.spec.ts +75 -0
- package/reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts +147 -0
- 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/reducers/Reducers.ts +4 -2
- package/testing/ReactorScenario.spec.ts +18 -1
- package/testing/ReactorScenario.ts +11 -1
- package/testing/ReducerReadModelProcessor.ts +13 -0
- package/testing/UnsupportedReducerOperation.ts +10 -0
- package/testing/for_ReactorScenario/when_recording_side_effects/with_declared_stream_type.spec.ts +50 -0
- package/testing/for_observerScenarios/when_selecting_filters/with_reducer_tag_filters.spec.ts +50 -0
- package/testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts +48 -0
- package/testing/index.ts +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
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 { eventSourceType, eventStreamType, filterEventsByTag, tag } from '../../../index.js';
|
|
5
|
+
import { filterRegistrationBehaviors } from './filter_registration_behaviors.fixture.js';
|
|
6
|
+
|
|
7
|
+
class Unfiltered {}
|
|
8
|
+
@eventSourceType('Customers')
|
|
9
|
+
class SourceFiltered {}
|
|
10
|
+
@eventStreamType('Orders')
|
|
11
|
+
class StreamFiltered {}
|
|
12
|
+
@eventSourceType('Customers')
|
|
13
|
+
@eventStreamType('Orders')
|
|
14
|
+
@filterEventsByTag('vip')
|
|
15
|
+
@filterEventsByTag('priority')
|
|
16
|
+
@tag('Analytics')
|
|
17
|
+
class Combined {}
|
|
18
|
+
class Inherited extends Combined {}
|
|
19
|
+
@eventSourceType('')
|
|
20
|
+
@eventStreamType('All')
|
|
21
|
+
class Reset extends Combined {}
|
|
22
|
+
|
|
23
|
+
filterRegistrationBehaviors([
|
|
24
|
+
{ type: Unfiltered, source: '', stream: 'All', tags: [] },
|
|
25
|
+
{ type: SourceFiltered, source: 'Customers', stream: 'All', tags: [] },
|
|
26
|
+
{ type: StreamFiltered, source: '', stream: 'Orders', tags: [] },
|
|
27
|
+
{ type: Combined, source: 'Customers', stream: 'Orders', tags: ['priority', 'vip'] },
|
|
28
|
+
{ type: Inherited, source: 'Customers', stream: 'Orders', tags: ['priority', 'vip'] },
|
|
29
|
+
{ type: Reset, source: '', stream: 'All', tags: ['priority', 'vip'] }
|
|
30
|
+
]);
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { eventSourceType, eventStreamType, filterEventsByTag, tag } from '../../../index.js';
|
|
5
|
+
import { filterRegistrationBehaviors } from './filter_registration_behaviors.fixture.js';
|
|
6
|
+
|
|
7
|
+
class Unfiltered {}
|
|
8
|
+
@eventSourceType('Customers')
|
|
9
|
+
class SourceFiltered {}
|
|
10
|
+
@eventStreamType('Orders')
|
|
11
|
+
class StreamFiltered {}
|
|
12
|
+
@eventSourceType('Customers')
|
|
13
|
+
@eventStreamType('Orders')
|
|
14
|
+
@filterEventsByTag('vip')
|
|
15
|
+
@filterEventsByTag('priority')
|
|
16
|
+
@tag('Analytics')
|
|
17
|
+
class Combined {}
|
|
18
|
+
class Inherited extends Combined {}
|
|
19
|
+
@eventSourceType('')
|
|
20
|
+
@eventStreamType('All')
|
|
21
|
+
class Reset extends Combined {}
|
|
22
|
+
|
|
23
|
+
filterRegistrationBehaviors([
|
|
24
|
+
{ type: Unfiltered, source: '', stream: 'All', tags: [] },
|
|
25
|
+
{ type: SourceFiltered, source: 'Customers', stream: 'All', tags: [] },
|
|
26
|
+
{ type: StreamFiltered, source: '', stream: 'Orders', tags: [] },
|
|
27
|
+
{ type: Combined, source: 'Customers', stream: 'Orders', tags: ['priority', 'vip'] },
|
|
28
|
+
{ type: Inherited, source: 'Customers', stream: 'Orders', tags: ['priority', 'vip'] },
|
|
29
|
+
{ type: Reset, source: '', stream: 'All', tags: ['priority', 'vip'] }
|
|
30
|
+
]);
|
package/events/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { isRegisteredEvent } from './isRegisteredEvent.js';
|
|
|
18
18
|
export { Tag } from './Tag.js';
|
|
19
19
|
export { tag, tags, getTagsFor } from './tagDecorator.js';
|
|
20
20
|
export { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator.js';
|
|
21
|
+
export { eventSourceType, getEventSourceTypeFor } from './eventSourceTypeDecorator.js';
|
|
22
|
+
export { eventStreamType, getEventStreamTypeFor } from './eventStreamTypeDecorator.js';
|
|
21
23
|
export { mergeTags } from './mergeTags.js';
|
|
22
24
|
export * from './constraints/index.js';
|
|
23
25
|
export * from './migrations/index.js';
|
|
@@ -0,0 +1,117 @@
|
|
|
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 { ChronicleClient } from '../../ChronicleClient.js';
|
|
6
|
+
import { ChronicleOptions } from '../../ChronicleOptions.js';
|
|
7
|
+
import { EventStore } from '../../EventStore.js';
|
|
8
|
+
import type { IEventStore } from '../../IEventStore.js';
|
|
9
|
+
import { IncompatibleChronicleServer } from '../../connection/IncompatibleChronicleServer.js';
|
|
10
|
+
import { RejectedChronicleCredentials } from '../../connection/RejectedChronicleCredentials.js';
|
|
11
|
+
import { projection } from '../../projections/declarative/projection.js';
|
|
12
|
+
import type { IReadModelWatcher } from '../../readModels/IReadModelWatcher.js';
|
|
13
|
+
import { a_kernel_stream } from '../../readModels/for_ReadModelWatcher/given/a_kernel_stream.js';
|
|
14
|
+
|
|
15
|
+
chai.should();
|
|
16
|
+
|
|
17
|
+
const transport = vi.hoisted(() => ({
|
|
18
|
+
resetChannel: vi.fn(), connect: vi.fn(), disconnect: vi.fn(), getVersionInfo: vi.fn(),
|
|
19
|
+
ensureEventStore: vi.fn(), watch: vi.fn(),
|
|
20
|
+
lost: undefined as ((reason: string, error: unknown) => void) | undefined
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('../../connection/ChronicleConnection', () => ({
|
|
23
|
+
ChronicleConnection: class {
|
|
24
|
+
resetChannel = transport.resetChannel;
|
|
25
|
+
connect = transport.connect;
|
|
26
|
+
disconnect = transport.disconnect;
|
|
27
|
+
server = { getVersionInfo: transport.getVersionInfo };
|
|
28
|
+
eventStores = { ensureEventStore: transport.ensureEventStore };
|
|
29
|
+
readModels = { watch: transport.watch };
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('../../connection/KernelKeepAlive', () => ({
|
|
33
|
+
KernelKeepAlive: class {
|
|
34
|
+
constructor(_connections: unknown, onLost: (reason: string, error: unknown) => void) { transport.lost = onLost; }
|
|
35
|
+
async start() {}
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
class Model { id = ''; }
|
|
40
|
+
class ModelProjection { define() {} }
|
|
41
|
+
projection('model', Model)(ModelProjection);
|
|
42
|
+
|
|
43
|
+
describe('when reconnecting with waiting read model watchers', () => {
|
|
44
|
+
let client: ChronicleClient;
|
|
45
|
+
let store: IEventStore;
|
|
46
|
+
let watcher: IReadModelWatcher<Model>;
|
|
47
|
+
let stream: a_kernel_stream;
|
|
48
|
+
let rejectReconnect: (error: Error) => void;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
vi.useFakeTimers();
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
// Registration is outside this connection-lifetime specification; retain the real store and watchers.
|
|
54
|
+
vi.spyOn(EventStore.prototype, 'registerArtifacts').mockResolvedValue(undefined);
|
|
55
|
+
transport.resetChannel.mockResolvedValue(undefined);
|
|
56
|
+
transport.connect.mockResolvedValue(undefined);
|
|
57
|
+
transport.getVersionInfo.mockResolvedValue({});
|
|
58
|
+
transport.ensureEventStore.mockResolvedValue({ IsSuccess: true, IsAuthorized: true });
|
|
59
|
+
stream = new a_kernel_stream();
|
|
60
|
+
transport.watch.mockImplementation((_request, options: { signal: AbortSignal }) => {
|
|
61
|
+
stream.signal = options.signal;
|
|
62
|
+
options.signal.addEventListener('abort', () => stream.end(), { once: true });
|
|
63
|
+
return stream;
|
|
64
|
+
});
|
|
65
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', {
|
|
66
|
+
discoveryPatterns: [],
|
|
67
|
+
clientArtifactsProvider: {
|
|
68
|
+
readModels: [Model], projections: [ModelProjection], eventTypes: [], reactors: [], reducers: [],
|
|
69
|
+
seeders: [], constraints: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
store = await client.getEventStore('store');
|
|
73
|
+
watcher = store.readModels.createWatcher(Model, { resume: true });
|
|
74
|
+
stream.send({ Subscribed: true });
|
|
75
|
+
await watcher.subscribed;
|
|
76
|
+
transport.connect.mockImplementation(() => new Promise<void>((_resolve, reject) => { rejectReconnect = reject; }));
|
|
77
|
+
transport.lost!('keep-alive-ended', { code: 14 });
|
|
78
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
79
|
+
stream.signal.aborted.should.be.true;
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
client.dispose();
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it.each([
|
|
88
|
+
['incompatible server', IncompatibleChronicleServer],
|
|
89
|
+
['rejected credentials', RejectedChronicleCredentials]
|
|
90
|
+
] as const)('should reject readiness and iteration on terminal %s', async (_reason, Failure) => {
|
|
91
|
+
const failure = new Failure('Replacement connection rejected');
|
|
92
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
93
|
+
const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
|
|
94
|
+
rejectReconnect(failure);
|
|
95
|
+
(await readiness).should.equal(failure);
|
|
96
|
+
(await next).should.equal(failure);
|
|
97
|
+
await vi.advanceTimersByTimeAsync(15000);
|
|
98
|
+
transport.watch.mock.calls.should.have.lengthOf(1);
|
|
99
|
+
transport.connect.mock.calls.should.have.lengthOf(2);
|
|
100
|
+
// A cached store cannot create a watcher that waits forever after the terminal verdict.
|
|
101
|
+
const later = store.readModels.createWatcher(Model);
|
|
102
|
+
(await later.subscribed.catch(error => error)).should.equal(failure);
|
|
103
|
+
(await later[Symbol.asyncIterator]().next().catch(error => error)).should.equal(failure);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should reject pending readiness but complete iteration cleanly if disposed during recovery', async () => {
|
|
107
|
+
const readiness = watcher.subscribed.catch(error => error);
|
|
108
|
+
const next = watcher[Symbol.asyncIterator]().next();
|
|
109
|
+
client.dispose();
|
|
110
|
+
(await readiness).name.should.equal('AbortError');
|
|
111
|
+
(await next).done!.should.be.true;
|
|
112
|
+
rejectReconnect(new IncompatibleChronicleServer('Late reconnect failure'));
|
|
113
|
+
await vi.advanceTimersByTimeAsync(15000);
|
|
114
|
+
(await watcher[Symbol.asyncIterator]().next()).done!.should.be.true;
|
|
115
|
+
transport.watch.mock.calls.should.have.lengthOf(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
-
import { JobStatus, JobStepStatus, type JobStepSummaryResponse, type JobSummaryResponse } from '@cratis/chronicle.contracts';
|
|
6
|
+
import { JobStatus, JobStepStatus, ObserverRunningState, type JobStepSummaryResponse, type JobSummaryResponse } from '@cratis/chronicle.contracts';
|
|
7
7
|
import { field, Guid } from '@cratis/fundamentals';
|
|
8
8
|
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
9
9
|
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
10
10
|
import { fromContractsGuid } from '../../../connection/Guid.js';
|
|
11
11
|
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
12
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
12
13
|
import { eventType } from '../../../events/index.js';
|
|
13
14
|
import type { IEventStore } from '../../../IEventStore.js';
|
|
14
15
|
import { fromEvent } from '../../../projections/index.js';
|
|
@@ -55,8 +56,10 @@ async function eventually<T>(read: () => Promise<T>, accept: (value: T) => boole
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
describe.skipIf(!connectionString && !process.env.CI)('when managing a replay job against a kernel', () => {
|
|
59
|
+
const storeName = `Jobs${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
58
60
|
let client: ChronicleClient;
|
|
59
61
|
let store: IEventStore;
|
|
62
|
+
let connection: ChronicleConnection;
|
|
60
63
|
let jobId: JobId;
|
|
61
64
|
let listed: JobSummaryResponse[];
|
|
62
65
|
let stopped: JobSummaryResponse | undefined;
|
|
@@ -65,19 +68,36 @@ describe.skipIf(!connectionString && !process.env.CI)('when managing a replay jo
|
|
|
65
68
|
let stopUnknownJob: unknown;
|
|
66
69
|
let deleteUnknownJob: unknown;
|
|
67
70
|
|
|
71
|
+
const waitForActiveObserver = (lastHandledSequenceNumber?: bigint) => eventually(
|
|
72
|
+
() => connection.observers.getObserverInformation({
|
|
73
|
+
EventStore: storeName,
|
|
74
|
+
Namespace: 'Default',
|
|
75
|
+
ObserverId: 'Item',
|
|
76
|
+
EventSequenceId: 'event-log'
|
|
77
|
+
}),
|
|
78
|
+
observer => observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active &&
|
|
79
|
+
(lastHandledSequenceNumber === undefined || observer.LastHandledEventSequenceNumber === lastHandledSequenceNumber),
|
|
80
|
+
15_000);
|
|
81
|
+
|
|
82
|
+
// Readiness (30s) and the existing job-state waits (90s) leave 30s for RPC overhead.
|
|
68
83
|
beforeAll(async () => {
|
|
69
84
|
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
70
85
|
discoveryPatterns: [],
|
|
71
86
|
clientArtifactsProvider: artifacts
|
|
72
87
|
}));
|
|
73
|
-
store = await client.getEventStore(
|
|
88
|
+
store = await client.getEventStore(storeName);
|
|
89
|
+
connection = (store as unknown as { _connection: ChronicleConnection })._connection;
|
|
74
90
|
|
|
91
|
+
// Append only after initial catch-up completes, so it cannot race the explicit replay.
|
|
92
|
+
await waitForActiveObserver();
|
|
75
93
|
// Enough events that the replay is still running when it is stopped; a stopped job stays until deleted,
|
|
76
94
|
// where a completed one is removed and could not be listed reliably.
|
|
77
|
-
await store.eventLog.appendMany(Array.from({ length: 500 }, (_, index) => ({
|
|
95
|
+
const appended = await store.eventLog.appendMany(Array.from({ length: 500 }, (_, index) => ({
|
|
78
96
|
eventSourceId: randomUUID(),
|
|
79
97
|
event: Object.assign(new ItemAdded(), { name: `Item ${index}` })
|
|
80
98
|
})));
|
|
99
|
+
// ReplayForModel uses Observer.Replay too; wait for acknowledged progress before requesting it.
|
|
100
|
+
await waitForActiveObserver(appended.at(-1)!.sequenceNumber.value);
|
|
81
101
|
|
|
82
102
|
jobId = await store.projections.replayForModel(Item);
|
|
83
103
|
|
|
@@ -98,7 +118,7 @@ describe.skipIf(!connectionString && !process.env.CI)('when managing a replay jo
|
|
|
98
118
|
|
|
99
119
|
stopUnknownJob = await store.jobs.stop(randomUUID()).catch((error: unknown) => error);
|
|
100
120
|
deleteUnknownJob = await store.jobs.delete(randomUUID()).catch((error: unknown) => error);
|
|
101
|
-
});
|
|
121
|
+
}, 150_000);
|
|
102
122
|
|
|
103
123
|
afterAll(() => client?.dispose());
|
|
104
124
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.48.0",
|
|
4
4
|
"description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
|
|
5
5
|
"author": "Cratis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"clean": "rm -rf dist",
|
|
174
174
|
"test": "vitest run",
|
|
175
175
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
176
|
-
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts",
|
|
176
|
+
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts",
|
|
177
177
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
178
178
|
},
|
|
179
179
|
"dependencies": {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { hasEventType } from '../events/eventTypeDecorator.js';
|
|
5
|
+
import { getEventSourceTypeFor } from '../events/eventSourceTypeDecorator.js';
|
|
6
|
+
import { getDeclaredEventStreamTypeFor } from '../events/eventStreamTypeDecorator.js';
|
|
5
7
|
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
6
8
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
7
9
|
import type { EventContext } from '../events/EventContext.js';
|
|
@@ -25,7 +27,8 @@ export async function dispatchReactorSideEffects(eventLog: IEventLog, result: un
|
|
|
25
27
|
reactorType: Function, eventStore: string, namespace: string, handler?: ReactorResultHandler): Promise<void> {
|
|
26
28
|
if (await handler?.(result, context, reactorType, eventStore, namespace)) return;
|
|
27
29
|
const appended = await appendReactorSideEffects(eventLog, result, context.eventSourceId,
|
|
28
|
-
context.eventStreamType ?? 'Default', context.eventStreamId ?? context.eventSourceId
|
|
30
|
+
context.eventStreamType ?? 'Default', context.eventStreamId ?? context.eventSourceId,
|
|
31
|
+
getEventSourceTypeFor(reactorType), getDeclaredEventStreamTypeFor(reactorType));
|
|
29
32
|
if (!appended.isSuccess) throw new Error(`Reactor side effect failed to append: ${appended.errors.join('; ')}`);
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -58,22 +61,27 @@ function isEventForEventSourceId(value: unknown): value is EventForEventSourceId
|
|
|
58
61
|
* event returns.
|
|
59
62
|
* @param triggeringEventStreamId - The event stream identifier of the triggering event, used for
|
|
60
63
|
* bare event returns.
|
|
64
|
+
* @param reactorEventSourceType - Non-empty reactor source type metadata for bare event returns.
|
|
65
|
+
* @param reactorEventStreamType - Non-empty declared reactor stream type metadata for bare event returns,
|
|
66
|
+
* overriding the triggering stream type. Omitted metadata preserves the triggering stream type.
|
|
61
67
|
* @returns The outcome of the append. Each {@link EventForEventSourceId} entry keeps its own
|
|
62
|
-
* target
|
|
68
|
+
* target metadata; bare events use the triggering target with the reactor's declared types.
|
|
63
69
|
*/
|
|
64
70
|
export async function appendReactorSideEffects(
|
|
65
71
|
eventLog: IEventLog,
|
|
66
72
|
handlerResult: unknown,
|
|
67
73
|
triggeringEventSourceId: string,
|
|
68
74
|
triggeringEventStreamType: string,
|
|
69
|
-
triggeringEventStreamId: string
|
|
75
|
+
triggeringEventStreamId: string,
|
|
76
|
+
reactorEventSourceType?: string,
|
|
77
|
+
reactorEventStreamType?: string
|
|
70
78
|
): Promise<ReactorSideEffectResult> {
|
|
71
79
|
if (handlerResult === undefined || handlerResult === null) {
|
|
72
80
|
return noSideEffects;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
const events = normalizeReactorSideEffects(handlerResult, triggeringEventSourceId,
|
|
76
|
-
triggeringEventStreamType, triggeringEventStreamId);
|
|
84
|
+
triggeringEventStreamType, triggeringEventStreamId, reactorEventSourceType, reactorEventStreamType);
|
|
77
85
|
if (events.length === 0) {
|
|
78
86
|
return noSideEffects;
|
|
79
87
|
}
|
|
@@ -91,7 +99,8 @@ export async function appendReactorSideEffects(
|
|
|
91
99
|
|
|
92
100
|
/** Production return-shape normalization; unknown values are ignored by production dispatch. */
|
|
93
101
|
export function normalizeReactorSideEffects(handlerResult: unknown, triggeringEventSourceId: string,
|
|
94
|
-
triggeringEventStreamType: string, triggeringEventStreamId: string
|
|
102
|
+
triggeringEventStreamType: string, triggeringEventStreamId: string,
|
|
103
|
+
reactorEventSourceType?: string, reactorEventStreamType?: string): EventForEventSourceId[] {
|
|
95
104
|
const items = Array.isArray(handlerResult) ? handlerResult : [handlerResult];
|
|
96
105
|
const events: EventForEventSourceId[] = [];
|
|
97
106
|
|
|
@@ -102,7 +111,8 @@ export function normalizeReactorSideEffects(handlerResult: unknown, triggeringEv
|
|
|
102
111
|
events.push({
|
|
103
112
|
eventSourceId: triggeringEventSourceId,
|
|
104
113
|
event: item,
|
|
105
|
-
eventStreamType: triggeringEventStreamType,
|
|
114
|
+
eventStreamType: reactorEventStreamType || triggeringEventStreamType,
|
|
115
|
+
...(reactorEventSourceType ? { eventSourceType: reactorEventSourceType } : {}),
|
|
106
116
|
eventStreamId: triggeringEventStreamId,
|
|
107
117
|
subject: triggeringEventSourceId
|
|
108
118
|
});
|
package/reactors/Reactors.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
|
|
|
12
12
|
import { toClientEventContext } from '../events/toClientEventContext.js';
|
|
13
13
|
import { getTagsFor } from '../events/tagDecorator.js';
|
|
14
14
|
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
|
|
15
|
+
import { getEventSourceTypeFor } from '../events/eventSourceTypeDecorator.js';
|
|
16
|
+
import { getEventStreamTypeFor } from '../events/eventStreamTypeDecorator.js';
|
|
15
17
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
16
18
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
17
19
|
import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
|
|
@@ -277,8 +279,8 @@ export class Reactors implements IReactors {
|
|
|
277
279
|
Tags: getTagsFor(reactorType).map(t => t.value),
|
|
278
280
|
Filters: {
|
|
279
281
|
FilterTags: getFilterTagsFor(reactorType).map(t => t.value),
|
|
280
|
-
EventSourceType:
|
|
281
|
-
EventStreamType:
|
|
282
|
+
EventSourceType: getEventSourceTypeFor(reactorType),
|
|
283
|
+
EventStreamType: getEventStreamTypeFor(reactorType)
|
|
282
284
|
}
|
|
283
285
|
}
|
|
284
286
|
},
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import { eventSourceType } from '../../../events/eventSourceTypeDecorator.js';
|
|
6
|
+
import { eventStreamType } from '../../../events/eventStreamTypeDecorator.js';
|
|
7
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
8
|
+
import type { EventContext } from '../../../events/EventContext.js';
|
|
9
|
+
import type { EventForEventSourceId } from '../../../eventSequences/EventForEventSourceId.js';
|
|
10
|
+
import type { IEventLog } from '../../../eventSequences/IEventLog.js';
|
|
11
|
+
import { dispatchReactorSideEffects } from '../../ReactorSideEffects.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
@eventType('side-effect-with-reactor-types')
|
|
15
|
+
class Outbound {}
|
|
16
|
+
@eventSourceType('my-source')
|
|
17
|
+
class SourceReactor {}
|
|
18
|
+
@eventStreamType('my-stream')
|
|
19
|
+
class StreamReactor extends SourceReactor {}
|
|
20
|
+
class InheritedReactor extends StreamReactor {}
|
|
21
|
+
@eventSourceType('')
|
|
22
|
+
@eventStreamType('')
|
|
23
|
+
class EmptyReactor extends StreamReactor {}
|
|
24
|
+
@eventStreamType('All')
|
|
25
|
+
class AllStreamsReactor {}
|
|
26
|
+
const context = { eventSourceId: 'trigger', eventStreamType: 'trigger-stream', eventStreamId: 'trigger-stream-id' } as EventContext;
|
|
27
|
+
|
|
28
|
+
for (const [reactorType, sourceType, streamType] of [
|
|
29
|
+
[SourceReactor, 'my-source', 'trigger-stream'],
|
|
30
|
+
[StreamReactor, 'my-source', 'my-stream'],
|
|
31
|
+
[InheritedReactor, 'my-source', 'my-stream'],
|
|
32
|
+
[EmptyReactor, undefined, 'trigger-stream'],
|
|
33
|
+
[AllStreamsReactor, undefined, 'All']
|
|
34
|
+
] as const) {
|
|
35
|
+
describe(`when dispatching a bare returned event from ${reactorType.name}`, () => {
|
|
36
|
+
let appended: EventForEventSourceId[];
|
|
37
|
+
const outbound = new Outbound();
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
const eventLog = { appendMany: vi.fn(async (events: EventForEventSourceId[]) => {
|
|
40
|
+
appended = events;
|
|
41
|
+
return [{ isSuccess: true, errors: [], constraintViolations: [] }];
|
|
42
|
+
}) } as unknown as IEventLog;
|
|
43
|
+
await dispatchReactorSideEffects(eventLog, outbound, context, reactorType, 'store', 'namespace');
|
|
44
|
+
});
|
|
45
|
+
it('should append the returned event with the reactor types and triggering target', () => {
|
|
46
|
+
appended.should.deep.equal([{ eventSourceId: 'trigger', event: outbound, eventStreamType: streamType,
|
|
47
|
+
...(sourceType ? { eventSourceType: sourceType } : {}), eventStreamId: 'trigger-stream-id', subject: 'trigger' }]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('when dispatching mixed bare and explicitly targeted returned events', () => {
|
|
53
|
+
let appended: EventForEventSourceId[];
|
|
54
|
+
const explicit = { eventSourceId: 'other', event: new Outbound(), eventSourceType: 'explicit-source',
|
|
55
|
+
eventStreamType: 'explicit-stream', eventStreamId: 'other-stream', subject: 'other-subject' };
|
|
56
|
+
const omitted = { eventSourceId: 'omitted', event: new Outbound() };
|
|
57
|
+
const empty = { eventSourceId: 'empty', event: new Outbound(), eventSourceType: '', eventStreamType: '' };
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
const eventLog = { appendMany: vi.fn(async (events: EventForEventSourceId[]) => {
|
|
60
|
+
appended = events;
|
|
61
|
+
return [{ isSuccess: true, errors: [], constraintViolations: [] }];
|
|
62
|
+
}) } as unknown as IEventLog;
|
|
63
|
+
await dispatchReactorSideEffects(eventLog, [new Outbound(), explicit, omitted, empty], context, StreamReactor, 'store', 'namespace');
|
|
64
|
+
});
|
|
65
|
+
it('should apply reactor metadata to only the bare event', () => {
|
|
66
|
+
appended[0].eventSourceType!.should.equal('my-source');
|
|
67
|
+
appended[0].eventStreamType!.should.equal('my-stream');
|
|
68
|
+
});
|
|
69
|
+
it('should preserve explicit targets including omitted and empty type values', () => {
|
|
70
|
+
appended.slice(1).should.deep.equal([explicit, omitted, empty]);
|
|
71
|
+
appended[1].should.equal(explicit);
|
|
72
|
+
appended[2].should.equal(omitted);
|
|
73
|
+
appended[3].should.equal(empty);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ObserverRunningState } from '@cratis/chronicle.contracts';
|
|
8
|
+
import { ChronicleClient, ChronicleOptions, eventType, eventSourceType, eventStreamType, filterEventsByTag,
|
|
9
|
+
reactor, reducer, tag, type IEventStore, type IClientArtifactsProvider } from '../../../index.js';
|
|
10
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
function should(value: unknown): ReturnType<typeof chai.expect> {
|
|
14
|
+
return (value as { should: ReturnType<typeof chai.expect> }).should;
|
|
15
|
+
}
|
|
16
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
17
|
+
|
|
18
|
+
@eventType()
|
|
19
|
+
class FilteredOrderPlaced {
|
|
20
|
+
@field(String) name = '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const reactorCalls = new Map<string, string[]>();
|
|
24
|
+
const reducerCalls = new Map<string, string[]>();
|
|
25
|
+
class Totals { count = 0; lastName = ''; }
|
|
26
|
+
class SourceTotals extends Totals {}
|
|
27
|
+
class StreamTotals extends Totals {}
|
|
28
|
+
class CombinedTotals extends Totals {}
|
|
29
|
+
|
|
30
|
+
@reactor()
|
|
31
|
+
class AllOrders {
|
|
32
|
+
filteredOrderPlaced(event: FilteredOrderPlaced) {
|
|
33
|
+
const name = this.constructor.name;
|
|
34
|
+
reactorCalls.set(name, [...reactorCalls.get(name) ?? [], event.name]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
@reactor()
|
|
38
|
+
@eventSourceType('Customers')
|
|
39
|
+
class CustomerOrders extends AllOrders {}
|
|
40
|
+
@reactor()
|
|
41
|
+
@eventStreamType('Orders')
|
|
42
|
+
class OrdersStream extends AllOrders {}
|
|
43
|
+
@reactor()
|
|
44
|
+
@eventSourceType('Customers')
|
|
45
|
+
@eventStreamType('Orders')
|
|
46
|
+
@filterEventsByTag('vip')
|
|
47
|
+
@filterEventsByTag('priority')
|
|
48
|
+
@tag('Analytics')
|
|
49
|
+
class PriorityCustomerOrders extends AllOrders {}
|
|
50
|
+
|
|
51
|
+
@reducer('AllOrderTotals', undefined, Totals)
|
|
52
|
+
class AllOrderTotals {
|
|
53
|
+
filteredOrderPlaced(event: FilteredOrderPlaced, current: Totals | undefined): Totals {
|
|
54
|
+
const name = this.constructor.name;
|
|
55
|
+
reducerCalls.set(name, [...reducerCalls.get(name) ?? [], event.name]);
|
|
56
|
+
return { count: (current?.count ?? 0) + 1, lastName: event.name };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
@reducer('CustomerOrderTotals', undefined, SourceTotals)
|
|
60
|
+
@eventSourceType('Customers')
|
|
61
|
+
class CustomerOrderTotals extends AllOrderTotals {}
|
|
62
|
+
@reducer('OrdersStreamTotals', undefined, StreamTotals)
|
|
63
|
+
@eventStreamType('Orders')
|
|
64
|
+
class OrdersStreamTotals extends AllOrderTotals {}
|
|
65
|
+
@reducer('PriorityCustomerOrderTotals', undefined, CombinedTotals)
|
|
66
|
+
@eventSourceType('Customers')
|
|
67
|
+
@eventStreamType('Orders')
|
|
68
|
+
@filterEventsByTag('vip')
|
|
69
|
+
@filterEventsByTag('priority')
|
|
70
|
+
@tag('Analytics')
|
|
71
|
+
class PriorityCustomerOrderTotals extends AllOrderTotals {}
|
|
72
|
+
|
|
73
|
+
const artifacts: IClientArtifactsProvider = {
|
|
74
|
+
eventTypes: [FilteredOrderPlaced], reactors: [AllOrders, CustomerOrders, OrdersStream, PriorityCustomerOrders],
|
|
75
|
+
reducers: [AllOrderTotals, CustomerOrderTotals, OrdersStreamTotals, PriorityCustomerOrderTotals],
|
|
76
|
+
readModels: [Totals, SourceTotals, StreamTotals, CombinedTotals], seeders: [], constraints: [], projections: [],
|
|
77
|
+
webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
async function eventually(accept: () => boolean | Promise<boolean>, timeoutMs = 30_000): Promise<void> {
|
|
81
|
+
const deadline = Date.now() + timeoutMs;
|
|
82
|
+
while (!await accept()) {
|
|
83
|
+
if (Date.now() > deadline) throw new Error('Timed out waiting for filtered observers');
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A final matching event is an ordered barrier in every observer's shared source partition.
|
|
89
|
+
// The unrestricted observers also prove all excluded input was actually committed and delivered.
|
|
90
|
+
const expected = [
|
|
91
|
+
['wrong-source', 'wrong-stream', 'wrong-tag', 'no-tag', 'priority', 'barrier'],
|
|
92
|
+
['wrong-stream', 'wrong-tag', 'no-tag', 'priority', 'barrier'],
|
|
93
|
+
['wrong-source', 'wrong-tag', 'no-tag', 'priority', 'barrier'],
|
|
94
|
+
['priority', 'barrier']
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
describe.skipIf(!connectionString && !process.env.CI)('when filtering reactor and reducer delivery against a kernel', () => {
|
|
98
|
+
const storeName = `Filters${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
99
|
+
const sourceId = randomUUID();
|
|
100
|
+
let client: ChronicleClient;
|
|
101
|
+
let store: IEventStore;
|
|
102
|
+
|
|
103
|
+
beforeAll(async () => {
|
|
104
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
105
|
+
discoveryPatterns: [], clientArtifactsProvider: artifacts
|
|
106
|
+
}));
|
|
107
|
+
store = await client.getEventStore(storeName);
|
|
108
|
+
const connection = (store as unknown as { _connection: ChronicleConnection })._connection;
|
|
109
|
+
await eventually(async () => {
|
|
110
|
+
const observers = await Promise.all([...artifacts.reactors, ...artifacts.reducers].map(type =>
|
|
111
|
+
connection.observers.getObserverInformation({ EventStore: storeName, Namespace: 'Default',
|
|
112
|
+
ObserverId: type.name, EventSequenceId: 'event-log' })));
|
|
113
|
+
return observers.every(observer => observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active);
|
|
114
|
+
});
|
|
115
|
+
const input = [
|
|
116
|
+
{ name: 'wrong-source', source: 'Other', stream: 'Orders', tags: ['vip'] },
|
|
117
|
+
{ name: 'wrong-stream', source: 'Customers', stream: 'Other', tags: ['vip'] },
|
|
118
|
+
{ name: 'wrong-tag', source: 'Customers', stream: 'Orders', tags: ['regular'] },
|
|
119
|
+
{ name: 'no-tag', source: 'Customers', stream: 'Orders', tags: [] },
|
|
120
|
+
{ name: 'priority', source: 'Customers', stream: 'Orders', tags: ['priority'] },
|
|
121
|
+
{ name: 'barrier', source: 'Customers', stream: 'Orders', tags: ['vip'] }
|
|
122
|
+
];
|
|
123
|
+
const results = await store.eventLog.appendMany(input.map(item => ({ eventSourceId: sourceId,
|
|
124
|
+
event: Object.assign(new FilteredOrderPlaced(), { name: item.name }), eventSourceType: item.source,
|
|
125
|
+
eventStreamType: item.stream, eventStreamId: 'Default', tags: item.tags })));
|
|
126
|
+
should(results).have.lengthOf(input.length);
|
|
127
|
+
should(results.every(result => result.isSuccess)).be.true;
|
|
128
|
+
await eventually(() => [...artifacts.reactors].every(type => reactorCalls.get(type.name)?.at(-1) === 'barrier') &&
|
|
129
|
+
[...artifacts.reducers].every(type => reducerCalls.get(type.name)?.at(-1) === 'barrier'));
|
|
130
|
+
await eventually(async () => {
|
|
131
|
+
const states = await Promise.all(artifacts.readModels.map(type => store.readModels.findInstanceById(type, sourceId)));
|
|
132
|
+
return states.every(state => (state as Totals | null)?.lastName === 'barrier');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
afterAll(() => client?.dispose());
|
|
136
|
+
|
|
137
|
+
for (let index = 0; index < expected.length; index++) {
|
|
138
|
+
it(`should deliver only the selected events to reactor ${artifacts.reactors[index].name}`, () => {
|
|
139
|
+
should(reactorCalls.get(artifacts.reactors[index].name)).deep.equal(expected[index]);
|
|
140
|
+
});
|
|
141
|
+
it(`should deliver only the selected events to reducer ${artifacts.reducers[index].name}`, async () => {
|
|
142
|
+
should(reducerCalls.get(artifacts.reducers[index].name)).deep.equal(expected[index]);
|
|
143
|
+
const state = await store.readModels.getInstanceById(artifacts.readModels[index], sourceId) as Totals;
|
|
144
|
+
should(state.count).equal(expected[index].length);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|