@cratis/chronicle 6.34.0 → 6.36.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 +8 -2
- package/EventStore.ts +8 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +10 -2
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +8 -0
- package/dist/EventStore.js.map +1 -1
- package/dist/events/handles.d.ts +12 -0
- package/dist/events/handles.d.ts.map +1 -0
- package/dist/events/handles.js +33 -0
- package/dist/events/handles.js.map +1 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/observation/EventHandler.d.ts +7 -0
- package/dist/observation/EventHandler.d.ts.map +1 -0
- package/dist/observation/EventHandler.js +4 -0
- package/dist/observation/EventHandler.js.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts +4 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.js +40 -0
- package/dist/observation/createHandlerInstanceValidator.js.map +1 -0
- package/dist/observation/getEventHandlers.d.ts +5 -0
- package/dist/observation/getEventHandlers.d.ts.map +1 -0
- package/dist/observation/getEventHandlers.js +40 -0
- package/dist/observation/getEventHandlers.js.map +1 -0
- package/dist/observation/getInstanceMethods.d.ts +4 -0
- package/dist/observation/getInstanceMethods.d.ts.map +1 -0
- package/dist/observation/getInstanceMethods.js +29 -0
- package/dist/observation/getInstanceMethods.js.map +1 -0
- package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
- package/dist/reactors/ReactorDispatcher.js +27 -32
- package/dist/reactors/ReactorDispatcher.js.map +1 -1
- package/dist/reactors/Reactors.d.ts +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +27 -14
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/index.d.ts +1 -0
- package/dist/reactors/index.d.ts.map +1 -1
- package/dist/reactors/index.js +1 -0
- package/dist/reactors/index.js.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.d.ts +1 -0
- package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.js +5 -17
- package/dist/reducers/ReducerEventDispatcher.js.map +1 -1
- package/dist/reducers/ReducerEventHandler.d.ts +3 -6
- package/dist/reducers/ReducerEventHandler.d.ts.map +1 -1
- package/dist/reducers/Reducers.d.ts +1 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +22 -10
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/index.d.ts +1 -0
- package/dist/reducers/index.d.ts.map +1 -1
- package/dist/reducers/index.js +1 -0
- package/dist/reducers/index.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts +1 -0
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +9 -1
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.js +1 -0
- package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
- package/dist/testing/projections/ProjectionArithmetic.d.ts +11 -0
- package/dist/testing/projections/ProjectionArithmetic.d.ts.map +1 -0
- package/dist/testing/projections/ProjectionArithmetic.js +70 -0
- package/dist/testing/projections/ProjectionArithmetic.js.map +1 -0
- package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionCapabilities.js +26 -10
- package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js +10 -3
- package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/events/for_handles/when_decorating_a_method.legacy.spec.ts +91 -0
- package/events/for_handles/when_decorating_a_method.standard.spec.ts +102 -0
- package/events/handles.ts +37 -0
- package/events/index.ts +1 -0
- package/for_ChronicleClient/when_registering_artifacts/with_invalid_handler_mappings.spec.ts +142 -0
- package/observation/EventHandler.ts +9 -0
- package/observation/createHandlerInstanceValidator.ts +42 -0
- package/observation/for_getEventHandlers/when_discovering_handlers.spec.ts +137 -0
- package/observation/for_getEventHandlers/when_reobserving_explicit_handlers.spec.ts +73 -0
- package/observation/getEventHandlers.ts +43 -0
- package/observation/getInstanceMethods.ts +29 -0
- package/package.json +2 -2
- package/reactors/ReactorDispatcher.ts +26 -30
- package/reactors/Reactors.ts +32 -16
- package/reactors/decorators.standard.spec.ts +8 -0
- package/reactors/for_ReactorDispatcher/when_selecting_explicit_handlers.spec.ts +64 -0
- package/reactors/for_Reactors/when_activating_a_field_override.spec.ts +62 -0
- package/reactors/for_Reactors/when_delivering_to_instance_handlers.spec.ts +108 -0
- package/reactors/for_Reactors/when_observing_explicit_handlers.legacy.spec.ts +77 -0
- package/reactors/for_Reactors/when_registering_explicit_handlers.spec.ts +53 -0
- package/reactors/index.ts +1 -0
- package/reducers/ReducerEventDispatcher.ts +5 -16
- package/reducers/ReducerEventHandler.ts +4 -6
- package/reducers/Reducers.ts +24 -12
- package/reducers/for_ReducerEventDispatcher/when_invoking_an_explicit_handler.spec.ts +51 -0
- package/reducers/for_ReducerEventDispatcher/when_invoking_instance_handlers.spec.ts +69 -0
- package/reducers/for_Reducers/when_activating_a_field_override.spec.ts +63 -0
- package/reducers/for_Reducers/when_observing_explicit_handlers.legacy.spec.ts +73 -0
- package/reducers/for_Reducers/when_registering_explicit_handlers.spec.ts +57 -0
- package/reducers/index.ts +1 -0
- package/testing/ReactorScenario.ts +9 -1
- package/testing/ReducerReadModelProcessor.ts +1 -0
- package/testing/for_ReactorScenario/when_delivering_to_an_explicit_handler.spec.ts +100 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_a_fluent_projection.spec.ts +85 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_legacy_decorators.legacy.spec.ts +91 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts +92 -0
- package/testing/for_ReducerReadModelProcessor/when_an_instance_field_hides_a_handler.spec.ts +29 -0
- package/testing/projections/ProjectionArithmetic.ts +71 -0
- package/testing/projections/ProjectionCapabilities.ts +27 -9
- package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +9 -6
- package/testing/projections/ProjectionReadModelProcessor.ts +9 -3
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +3 -3
- package/testing/projections/for_ProjectionCapabilities/when_validating_arithmetic.spec.ts +127 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +4 -5
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
6
|
+
import { getInstanceMethods } from './getInstanceMethods.js';
|
|
7
|
+
|
|
8
|
+
/** Captures @handles prototype methods during preflight and rejects instance properties hiding them, except bound methods. */
|
|
9
|
+
export function createHandlerInstanceValidator(type: Constructor, names: Iterable<string>, methods = getInstanceMethods(type)):
|
|
10
|
+
(instance: object) => void {
|
|
11
|
+
const handlers = [...new Set(names)].filter(name => getHandledEventType(methods.get(name)!) !== undefined).map(name => {
|
|
12
|
+
const method = methods.get(name)!;
|
|
13
|
+
let declaring = type.prototype;
|
|
14
|
+
while (declaring && Object.getOwnPropertyDescriptor(declaring, name)?.value !== method) {
|
|
15
|
+
declaring = Object.getPrototypeOf(declaring);
|
|
16
|
+
}
|
|
17
|
+
const eventType = getHandledEventType(method)!;
|
|
18
|
+
const declaration = `@handles(${(eventType as Function).name})`;
|
|
19
|
+
return { name, method, declaration, declaringType: declaring?.constructor.name ?? type.name };
|
|
20
|
+
});
|
|
21
|
+
return instance => {
|
|
22
|
+
for (const { name, method, declaration, declaringType } of handlers) {
|
|
23
|
+
let current: object | null = instance;
|
|
24
|
+
while (current && !Object.hasOwn(current, name)) current = Object.getPrototypeOf(current) as object | null;
|
|
25
|
+
const value: unknown = current ? Object.getOwnPropertyDescriptor(current, name)?.value : undefined;
|
|
26
|
+
if (current === instance && typeof value === 'function' && isBoundCopyOf(value, method)) continue;
|
|
27
|
+
if (current === instance || !current || value !== method) {
|
|
28
|
+
throw new Error(`Override '${name}' on '${type.name}' hides ${declaration} declared on '${declaringType}'; use a method with @handles instead of an instance field.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks whether a function is a (possibly repeatedly) Function.prototype.bind copy of a method.
|
|
36
|
+
* @param value - The function found on the instance.
|
|
37
|
+
* @param method - The declared method.
|
|
38
|
+
* @returns True when the function name is one or more `bound ` prefixes followed by the method name.
|
|
39
|
+
*/
|
|
40
|
+
function isBoundCopyOf(value: Function, method: Function): boolean {
|
|
41
|
+
return value.name.startsWith('bound ') && value.name.replace(/^(bound )+/, '') === method.name;
|
|
42
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
6
|
+
import { handles } from '../../events/handles.js';
|
|
7
|
+
import { getReactorEventTypes } from '../../reactors/ReactorDispatcher.js';
|
|
8
|
+
import { ReducerEventDispatcher } from '../../reducers/ReducerEventDispatcher.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
@eventType('explicit-author', 2)
|
|
13
|
+
class AuthorRegistered {}
|
|
14
|
+
@eventType('conventional-book')
|
|
15
|
+
class BookBorrowed {}
|
|
16
|
+
|
|
17
|
+
const registeredTypes = [AuthorRegistered, BookBorrowed];
|
|
18
|
+
const discoverers = [
|
|
19
|
+
{ name: 'reactor', discover: getReactorEventTypes },
|
|
20
|
+
{ name: 'reducer', discover: (type: new () => object, types: readonly (new () => object)[]) => new ReducerEventDispatcher(type, types).handlers }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const { name, discover } of discoverers) {
|
|
24
|
+
describe(`when discovering explicit and conventional ${name} handlers`, () => {
|
|
25
|
+
class Observer {
|
|
26
|
+
@handles(AuthorRegistered)
|
|
27
|
+
notify() {}
|
|
28
|
+
bookBorrowed() {}
|
|
29
|
+
helper() {}
|
|
30
|
+
get unrelated() { throw new Error('A getter must not run during discovery'); }
|
|
31
|
+
}
|
|
32
|
+
let entries: ReturnType<typeof getReactorEventTypes>;
|
|
33
|
+
beforeEach(() => { entries = discover(Observer, registeredTypes); });
|
|
34
|
+
it('should include both event types without treating helpers as handlers', () => {
|
|
35
|
+
entries.should.deep.equal([
|
|
36
|
+
{ id: 'explicit-author', generation: 2, methodName: 'notify' },
|
|
37
|
+
{ id: 'conventional-book', generation: 1, methodName: 'bookBorrowed' }
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe(`when an explicit ${name} method has another event's conventional name`, () => {
|
|
43
|
+
class Observer { @handles(AuthorRegistered) bookBorrowed() {} }
|
|
44
|
+
it('should use only the explicit event type', () => {
|
|
45
|
+
discover(Observer, registeredTypes).should.deep.equal([{ id: 'explicit-author', generation: 2, methodName: 'bookBorrowed' }]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe(`when an explicit ${name} method has its own conventional name`, () => {
|
|
50
|
+
class Observer { @handles(AuthorRegistered) authorRegistered() {} }
|
|
51
|
+
it('should register it only once', () => {
|
|
52
|
+
discover(Observer, registeredTypes).should.have.lengthOf(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe(`when inheriting an explicit ${name} handler`, () => {
|
|
57
|
+
class Base { @handles(AuthorRegistered) notify() {} }
|
|
58
|
+
class Inherited extends Base {}
|
|
59
|
+
class Overridden extends Base { notify() {} }
|
|
60
|
+
class Remapped extends Base { @handles(BookBorrowed) notify() {} }
|
|
61
|
+
it('should discover the inherited handler', () => {
|
|
62
|
+
discover(Inherited, registeredTypes)[0].methodName!.should.equal('notify');
|
|
63
|
+
});
|
|
64
|
+
it('should reject an override that hides the base declaration', () => {
|
|
65
|
+
(() => discover(Overridden, registeredTypes)).should.throw(
|
|
66
|
+
"Override 'notify' on 'Overridden' hides @handles(AuthorRegistered) declared on 'Base'; redecorate the override.");
|
|
67
|
+
});
|
|
68
|
+
it('should reject an inherited undecorated override across multiple levels', () => {
|
|
69
|
+
class Derived extends Overridden {}
|
|
70
|
+
(() => discover(Derived, registeredTypes)).should.throw(
|
|
71
|
+
"Override 'notify' on 'Overridden' hides @handles(AuthorRegistered) declared on 'Base'; redecorate the override.");
|
|
72
|
+
});
|
|
73
|
+
it('should allow an override explicitly keeping the same event type', () => {
|
|
74
|
+
class Reaffirmed extends Base { @handles(AuthorRegistered) notify() {} }
|
|
75
|
+
discover(Reaffirmed, registeredTypes).should.deep.equal([{ id: 'explicit-author', generation: 2, methodName: 'notify' }]);
|
|
76
|
+
});
|
|
77
|
+
it('should use the override event type instead of the base declaration', () => {
|
|
78
|
+
discover(Remapped, registeredTypes).should.deep.equal([{ id: 'conventional-book', generation: 1, methodName: 'notify' }]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe(`when explicit ${name} event classes share a name`, () => {
|
|
83
|
+
const OtherAuthorRegistered = class AuthorRegistered {};
|
|
84
|
+
eventType('another-author')(OtherAuthorRegistered);
|
|
85
|
+
class Observer {
|
|
86
|
+
@handles(AuthorRegistered) notify() {}
|
|
87
|
+
@handles(OtherAuthorRegistered) archive() {}
|
|
88
|
+
}
|
|
89
|
+
it('should resolve event constructors rather than class names', () => {
|
|
90
|
+
discover(Observer, [AuthorRegistered, OtherAuthorRegistered]).map(entry => entry.id)
|
|
91
|
+
.should.deep.equal(['explicit-author', 'another-author']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe(`when an explicit ${name} event type is not registered`, () => {
|
|
96
|
+
class Observer { @handles(AuthorRegistered) notify() {} }
|
|
97
|
+
it('should name the observer method and missing event constructor', () => {
|
|
98
|
+
(() => discover(Observer, [BookBorrowed])).should.throw(/notify.*Observer.*AuthorRegistered.*no registered event type/);
|
|
99
|
+
});
|
|
100
|
+
it('should not accept a different constructor with the same event id', () => {
|
|
101
|
+
@eventType('explicit-author', 2)
|
|
102
|
+
class OtherAuthor {}
|
|
103
|
+
(() => discover(Observer, [OtherAuthor])).should.throw(/notify.*no registered event type/);
|
|
104
|
+
});
|
|
105
|
+
it('should not accept a listed class without event metadata', () => {
|
|
106
|
+
class Unknown {}
|
|
107
|
+
class Invalid { @handles(Unknown) notify() {} }
|
|
108
|
+
(() => discover(Invalid, [Unknown])).should.throw(/notify.*Unknown.*no registered event type/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe(`when two ${name} methods handle one event type`, () => {
|
|
113
|
+
class Explicit {
|
|
114
|
+
@handles(AuthorRegistered) notify() {}
|
|
115
|
+
@handles(AuthorRegistered) archive() {}
|
|
116
|
+
}
|
|
117
|
+
class Mixed {
|
|
118
|
+
@handles(AuthorRegistered) notify() {}
|
|
119
|
+
authorRegistered() {}
|
|
120
|
+
}
|
|
121
|
+
class Base { @handles(AuthorRegistered) notify() {} }
|
|
122
|
+
class Inherited extends Base { @handles(AuthorRegistered) archive() {} }
|
|
123
|
+
class Conventional extends Base { authorRegistered() {} }
|
|
124
|
+
it('should reject two explicit handlers', () => {
|
|
125
|
+
(() => discover(Explicit, registeredTypes)).should.throw(/multiple handlers.*explicit-author.*notify.*archive/);
|
|
126
|
+
});
|
|
127
|
+
it('should reject an explicit handler and a conventional handler', () => {
|
|
128
|
+
(() => discover(Mixed, registeredTypes)).should.throw(/multiple handlers.*explicit-author.*notify.*authorRegistered/);
|
|
129
|
+
});
|
|
130
|
+
it('should include inherited methods in duplicate detection', () => {
|
|
131
|
+
(() => discover(Inherited, registeredTypes)).should.throw(/multiple handlers.*explicit-author.*archive.*notify/);
|
|
132
|
+
});
|
|
133
|
+
it('should reject a conventional handler duplicating an inherited explicit handler', () => {
|
|
134
|
+
(() => discover(Conventional, registeredTypes)).should.throw(/multiple handlers.*explicit-author.*notify.*authorRegistered/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { ReactorMessage, ReducerMessage } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
8
|
+
import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
|
|
9
|
+
import type { IEventLog } from '../../eventSequences/IEventLog.js';
|
|
10
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
11
|
+
import { handles } from '../../events/handles.js';
|
|
12
|
+
import { onceOnly, reactor, Reactors } from '../../reactors/index.js';
|
|
13
|
+
import { reducer, Reducers } from '../../reducers/index.js';
|
|
14
|
+
|
|
15
|
+
chai.should();
|
|
16
|
+
|
|
17
|
+
@eventType('reobserved-event')
|
|
18
|
+
class AuthorRegistered {}
|
|
19
|
+
|
|
20
|
+
@onceOnly()
|
|
21
|
+
@reactor('reobserved-reactor')
|
|
22
|
+
class AuthorReactor { @handles(AuthorRegistered) notify() {} }
|
|
23
|
+
|
|
24
|
+
@reducer('reobserved-reducer')
|
|
25
|
+
class AuthorReducer { @handles(AuthorRegistered) update() {} }
|
|
26
|
+
|
|
27
|
+
for (const kind of ['reactor', 'reducer'] as const) {
|
|
28
|
+
describe(`when reobserving an explicit ${kind} handler`, () => {
|
|
29
|
+
const registrations: (ReactorMessage | ReducerMessage)[] = [];
|
|
30
|
+
let observations: Reactors | Reducers;
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
registrations.length = 0;
|
|
34
|
+
const lifecycle = new ConnectionLifecycle();
|
|
35
|
+
async function* stream(queue: AsyncIterable<ReactorMessage | ReducerMessage>) {
|
|
36
|
+
registrations.push((await queue[Symbol.asyncIterator]().next()).value!);
|
|
37
|
+
yield { Events: [], Partition: 'author', ReplayState: 0, InitialState: '' };
|
|
38
|
+
}
|
|
39
|
+
const connection = {
|
|
40
|
+
reactors: { observe: stream }, reducers: { observe: stream },
|
|
41
|
+
readModels: { registerMany: async () => ({}) }
|
|
42
|
+
} as unknown as ChronicleConnection;
|
|
43
|
+
const artifacts = {
|
|
44
|
+
reactors: [AuthorReactor], reducers: [AuthorReducer], eventTypes: [AuthorRegistered], readModels: []
|
|
45
|
+
} as unknown as IClientArtifactsProvider;
|
|
46
|
+
observations = kind === 'reactor'
|
|
47
|
+
? new Reactors(artifacts, connection, 'store', 'tenant', lifecycle, {} as IEventLog)
|
|
48
|
+
: new Reducers(artifacts, connection, 'store', 'tenant', lifecycle, 'sink');
|
|
49
|
+
await observations.register();
|
|
50
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
51
|
+
// Changing the catalog must not invalidate an already validated observation on its timer callback.
|
|
52
|
+
artifacts.eventTypes.splice(0);
|
|
53
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
observations.dispose();
|
|
57
|
+
vi.clearAllTimers();
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
it('should reuse the registered handler mapping when the stream ends', () => {
|
|
61
|
+
registrations.should.have.lengthOf(2);
|
|
62
|
+
registrations[1].should.deep.equal(registrations[0]);
|
|
63
|
+
const registration = registrations[1].Content!.Value0!;
|
|
64
|
+
const observer = 'Reactor' in registration ? registration.Reactor : registration.Reducer;
|
|
65
|
+
observer!.EventTypes.map(entry => entry.EventType!.Id).should.deep.equal(['reobserved-event']);
|
|
66
|
+
});
|
|
67
|
+
if (kind === 'reactor') {
|
|
68
|
+
it('should register a class-level onceOnly reactor with an explicit handler as non-replayable', () => {
|
|
69
|
+
(registrations[0] as ReactorMessage).Content!.Value0!.Reactor!.IsReplayable.should.be.false;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
6
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
7
|
+
import type { EventHandler } from './EventHandler.js';
|
|
8
|
+
import { getInstanceMethods } from './getInstanceMethods.js';
|
|
9
|
+
|
|
10
|
+
/** Resolves explicit event constructors first, with camelCase discovery for undecorated methods. */
|
|
11
|
+
export function getEventHandlers(type: Constructor, registeredTypes: readonly Constructor[], methods = getInstanceMethods(type)): EventHandler[] {
|
|
12
|
+
const eventTypes = new Map(registeredTypes.flatMap(eventType => {
|
|
13
|
+
const metadata = getEventTypeMetadata(eventType);
|
|
14
|
+
return metadata ? [[eventType, metadata.eventType] as const] : [];
|
|
15
|
+
}));
|
|
16
|
+
const handlers = new Map<string, EventHandler>();
|
|
17
|
+
const addHandler = (eventType: Constructor, methodName: string) => {
|
|
18
|
+
const metadata = eventTypes.get(eventType)!;
|
|
19
|
+
const id = metadata.id.value;
|
|
20
|
+
const previous = handlers.get(id);
|
|
21
|
+
if (previous && previous.methodName !== methodName) {
|
|
22
|
+
throw new Error(`'${(type as Function).name}' has multiple handlers for event type '${id}': '${previous.methodName}' and '${methodName}'.`);
|
|
23
|
+
}
|
|
24
|
+
handlers.set(id, { id, generation: metadata.generation.value, methodName });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const [name, method] of methods) {
|
|
28
|
+
const eventType = getHandledEventType(method);
|
|
29
|
+
if (eventType === undefined) continue;
|
|
30
|
+
if (!eventTypes.has(eventType)) {
|
|
31
|
+
throw new Error(`Handler '${name}' on '${(type as Function).name}' names event type '${(eventType as Function).name}', which has no registered event type. Register that event constructor with the event store.`);
|
|
32
|
+
}
|
|
33
|
+
addHandler(eventType, name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const eventType of eventTypes.keys()) {
|
|
37
|
+
const className = (eventType as Function).name;
|
|
38
|
+
const name = className.charAt(0).toLowerCase() + className.slice(1);
|
|
39
|
+
const method = methods.get(name);
|
|
40
|
+
if (method && getHandledEventType(method) === undefined) addHandler(eventType, name);
|
|
41
|
+
}
|
|
42
|
+
return [...handlers.values()];
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
6
|
+
|
|
7
|
+
/** Finds prototype methods without invoking getters; explicit handlers require explicitly decorated overrides. */
|
|
8
|
+
export function getInstanceMethods(type: Constructor): Map<string, Function> {
|
|
9
|
+
const methods = new Map<string, Function>();
|
|
10
|
+
const declaringTypes = new Map<string, string>();
|
|
11
|
+
for (let current = type.prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)) {
|
|
12
|
+
for (const name of Object.getOwnPropertyNames(current)) {
|
|
13
|
+
if (name === 'constructor') continue;
|
|
14
|
+
const method = Object.getOwnPropertyDescriptor(current, name)?.value;
|
|
15
|
+
const declaringType = declaringTypes.get(name);
|
|
16
|
+
if (declaringType !== undefined) {
|
|
17
|
+
const eventType = typeof method === 'function' ? getHandledEventType(method) : undefined;
|
|
18
|
+
const override = methods.get(name);
|
|
19
|
+
if (eventType && (!override || getHandledEventType(override) === undefined)) {
|
|
20
|
+
throw new Error(`Override '${name}' on '${declaringType}' hides @handles(${(eventType as Function).name}) declared on '${current.constructor.name}'; redecorate the override.`);
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
declaringTypes.set(name, current.constructor.name);
|
|
25
|
+
if (typeof method === 'function') methods.set(name, method);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return methods;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.36.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",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"clean": "rm -rf dist",
|
|
169
169
|
"test": "vitest run",
|
|
170
170
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
171
|
-
"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/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",
|
|
171
|
+
"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/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",
|
|
172
172
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
173
173
|
},
|
|
174
174
|
"dependencies": {
|
|
@@ -8,6 +8,9 @@ import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
|
|
|
8
8
|
import { runActivated } from '../artifacts/withActivatedArtifact.js';
|
|
9
9
|
import type { EventContext } from '../events/EventContext.js';
|
|
10
10
|
import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
11
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
12
|
+
import { getEventHandlers } from '../observation/getEventHandlers.js';
|
|
13
|
+
import { getInstanceMethods } from '../observation/getInstanceMethods.js';
|
|
11
14
|
import { isOnceOnly } from './onceOnly.js';
|
|
12
15
|
import { getReplayEventType } from './replay.js';
|
|
13
16
|
import type { ReactorServices } from './ReactorServices.js';
|
|
@@ -21,8 +24,7 @@ export interface ReactorEventTypeEntry {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function getReactorEventTypes(reactorType: Constructor, registeredTypes: readonly Constructor[]): ReactorEventTypeEntry[] {
|
|
24
|
-
const
|
|
25
|
-
const entries: ReactorEventTypeEntry[] = [];
|
|
27
|
+
const methods = getInstanceMethods(reactorType);
|
|
26
28
|
const replayHandlers = new Map<string, string>();
|
|
27
29
|
const eventTypes = new Map<string, { eventTypeClass: Function; id: string; generation: number }>();
|
|
28
30
|
|
|
@@ -37,40 +39,34 @@ export function getReactorEventTypes(reactorType: Constructor, registeredTypes:
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!eventType) {
|
|
55
|
-
throw new Error(`Replay handler '${name}' on reactor '${(reactorType as Function).name}' has no registered event type.`);
|
|
56
|
-
}
|
|
57
|
-
if (replayHandlers.has(eventType.id)) {
|
|
58
|
-
throw new Error(`Reactor '${(reactorType as Function).name}' has multiple replay handlers for event type '${eventType.id}': '${replayHandlers.get(eventType.id)}' and '${name}'.`);
|
|
59
|
-
}
|
|
60
|
-
replayHandlers.set(eventType.id, name);
|
|
42
|
+
for (const [name, method] of methods) {
|
|
43
|
+
const replayEventType = getReplayEventType(method);
|
|
44
|
+
if (replayEventType === undefined) continue;
|
|
45
|
+
if (getHandledEventType(method) !== undefined) {
|
|
46
|
+
throw new Error(`Handler '${name}' on reactor '${(reactorType as Function).name}' cannot combine @handles and @replay. Use separate live and replay methods.`);
|
|
47
|
+
}
|
|
48
|
+
const eventType = replayEventType === true
|
|
49
|
+
? eventTypes.get(name.startsWith('replay') ? name.slice('replay'.length) : '')
|
|
50
|
+
: [...eventTypes.values()].find(candidate => candidate.eventTypeClass === replayEventType);
|
|
51
|
+
if (!eventType) {
|
|
52
|
+
throw new Error(`Replay handler '${name}' on reactor '${(reactorType as Function).name}' has no registered event type.`);
|
|
53
|
+
}
|
|
54
|
+
if (replayHandlers.has(eventType.id)) {
|
|
55
|
+
throw new Error(`Reactor '${(reactorType as Function).name}' has multiple replay handlers for event type '${eventType.id}': '${replayHandlers.get(eventType.id)}' and '${name}'.`);
|
|
61
56
|
}
|
|
57
|
+
replayHandlers.set(eventType.id, name);
|
|
58
|
+
methods.delete(name);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const liveMethodName = typeof liveMethod === 'function' && getReplayEventType(liveMethod) === undefined ? methodName : undefined;
|
|
61
|
+
const entries = new Map<string, ReactorEventTypeEntry>(getEventHandlers(reactorType, registeredTypes, methods)
|
|
62
|
+
.map(handler => [handler.id, handler]));
|
|
63
|
+
for (const eventType of eventTypes.values()) {
|
|
68
64
|
const replayMethodName = replayHandlers.get(eventType.id);
|
|
69
|
-
if (
|
|
70
|
-
entries.
|
|
65
|
+
if (replayMethodName) {
|
|
66
|
+
entries.set(eventType.id, { id: eventType.id, generation: eventType.generation, ...entries.get(eventType.id), replayMethodName });
|
|
71
67
|
}
|
|
72
68
|
}
|
|
73
|
-
return entries;
|
|
69
|
+
return [...entries.values()];
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
export function selectReactorHandler(entries: readonly ReactorEventTypeEntry[], reactorType: Constructor,
|
package/reactors/Reactors.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getTagsFor } from '../events/tagDecorator.js';
|
|
|
14
14
|
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
|
|
15
15
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
16
16
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
17
|
+
import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
|
|
17
18
|
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle.js';
|
|
18
19
|
import { IReactors } from './IReactors.js';
|
|
19
20
|
import { dispatchReactorSideEffects } from './ReactorSideEffects.js';
|
|
@@ -105,6 +106,7 @@ export class Reactors implements IReactors {
|
|
|
105
106
|
|
|
106
107
|
private readonly _logger: ReturnType<typeof createLogger>;
|
|
107
108
|
private readonly _lifecycle: ConnectionLifecycle;
|
|
109
|
+
private readonly _unsubscribeDisconnected: () => void;
|
|
108
110
|
private readonly _reactors = new Map<string, Constructor>();
|
|
109
111
|
private readonly _queues = new Map<string, AsyncQueue<ReactorMessage>>();
|
|
110
112
|
private readonly _observations = new Map<string, AbortController>();
|
|
@@ -133,7 +135,7 @@ export class Reactors implements IReactors {
|
|
|
133
135
|
) {
|
|
134
136
|
this._logger = createLogger('@cratis/chronicle/reactors', _connection.logger);
|
|
135
137
|
this._lifecycle = lifecycle;
|
|
136
|
-
lifecycle.onDisconnected(async () => {
|
|
138
|
+
this._unsubscribeDisconnected = lifecycle.onDisconnected(async () => {
|
|
137
139
|
this._logger.info('Disconnected — stopping all reactor observations');
|
|
138
140
|
this._registered = false;
|
|
139
141
|
this.disconnectAll();
|
|
@@ -142,6 +144,7 @@ export class Reactors implements IReactors {
|
|
|
142
144
|
|
|
143
145
|
/** Stops observations permanently when the owning client is disposed. */
|
|
144
146
|
dispose(): void {
|
|
147
|
+
this._unsubscribeDisconnected();
|
|
145
148
|
this._disposed = true;
|
|
146
149
|
this._registered = false;
|
|
147
150
|
this.disconnectAll();
|
|
@@ -170,20 +173,24 @@ export class Reactors implements IReactors {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
if (this._disposed) return;
|
|
176
|
+
const registrations = [...this._reactors].map(([id, type]) => ({
|
|
177
|
+
id, type, eventTypes: getReactorEventTypes(type, this._clientArtifacts.eventTypes)
|
|
178
|
+
})).map(registration => ({ ...registration, validateInstance: createHandlerInstanceValidator(registration.type,
|
|
179
|
+
registration.eventTypes.flatMap(entry => [entry.methodName, entry.replayMethodName].filter((name): name is string => !!name))) }));
|
|
173
180
|
this._logger.info('Registering reactors', { count: this._reactors.size });
|
|
174
|
-
for (const
|
|
175
|
-
this.startObservation(id,
|
|
181
|
+
for (const { id, type, eventTypes, validateInstance } of registrations) {
|
|
182
|
+
this.startObservation(id, type, eventTypes, validateInstance);
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
this._registered = true;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
private startObservation(id: string, reactorType: Constructor
|
|
188
|
+
private startObservation(id: string, reactorType: Constructor,
|
|
189
|
+
eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
|
|
182
190
|
runInBackgroundContext(() => {
|
|
183
191
|
if (this._disposed) return;
|
|
184
192
|
const metadata = getReactorMetadata(reactorType)!;
|
|
185
193
|
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
186
|
-
const eventTypes = getReactorEventTypes(reactorType, this._clientArtifacts.eventTypes);
|
|
187
194
|
|
|
188
195
|
this._logger.info('Starting reactor observation', {
|
|
189
196
|
reactorId: id,
|
|
@@ -192,7 +199,7 @@ export class Reactors implements IReactors {
|
|
|
192
199
|
handlers: eventTypes.map(e => e.methodName)
|
|
193
200
|
});
|
|
194
201
|
|
|
195
|
-
void this.runObservation(id, reactorType, eventSequenceId, eventTypes);
|
|
202
|
+
void this.runObservation(id, reactorType, eventSequenceId, eventTypes, validateInstance);
|
|
196
203
|
});
|
|
197
204
|
}
|
|
198
205
|
|
|
@@ -200,15 +207,16 @@ export class Reactors implements IReactors {
|
|
|
200
207
|
id: string,
|
|
201
208
|
reactorType: Constructor,
|
|
202
209
|
eventSequenceId: string,
|
|
203
|
-
eventTypes: ReactorEventTypeEntry[]
|
|
210
|
+
eventTypes: ReactorEventTypeEntry[],
|
|
211
|
+
validateInstance: (instance: object) => void
|
|
204
212
|
): Promise<void> {
|
|
205
213
|
try {
|
|
206
|
-
await this.observeReactor(id, reactorType, eventSequenceId, eventTypes);
|
|
214
|
+
await this.observeReactor(id, reactorType, eventSequenceId, eventTypes, validateInstance);
|
|
207
215
|
} catch (error) {
|
|
208
216
|
this._logger.error('Reactor observation loop exited with error', { reactorId: id, error });
|
|
209
217
|
}
|
|
210
218
|
|
|
211
|
-
this.scheduleReobserve(id, reactorType);
|
|
219
|
+
this.scheduleReobserve(id, reactorType, eventTypes, validateInstance);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
222
|
/**
|
|
@@ -220,7 +228,7 @@ export class Reactors implements IReactors {
|
|
|
220
228
|
* the whole client reconnects. Both endings are therefore retried, and the delay
|
|
221
229
|
* keeps a stream that keeps ending from becoming a hot loop.
|
|
222
230
|
*/
|
|
223
|
-
private scheduleReobserve(id: string, reactorType: Constructor): void {
|
|
231
|
+
private scheduleReobserve(id: string, reactorType: Constructor, eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
|
|
224
232
|
// A disconnect clears the registration; the reconnect re-registers every
|
|
225
233
|
// reactor from scratch, so retrying here as well would double up.
|
|
226
234
|
if (!this._registered || this._disposed) {
|
|
@@ -233,7 +241,7 @@ export class Reactors implements IReactors {
|
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
this._logger.info('Re-establishing reactor observation', { reactorId: id });
|
|
236
|
-
this.startObservation(id, reactorType);
|
|
244
|
+
this.startObservation(id, reactorType, eventTypes, validateInstance);
|
|
237
245
|
}, Reactors._reobserveDelayMs));
|
|
238
246
|
|
|
239
247
|
handle.unref?.();
|
|
@@ -243,7 +251,8 @@ export class Reactors implements IReactors {
|
|
|
243
251
|
id: string,
|
|
244
252
|
reactorType: Constructor,
|
|
245
253
|
eventSequenceId: string,
|
|
246
|
-
eventTypes: ReactorEventTypeEntry[]
|
|
254
|
+
eventTypes: ReactorEventTypeEntry[],
|
|
255
|
+
validateInstance: (instance: object) => void
|
|
247
256
|
): Promise<void> {
|
|
248
257
|
const queue = new AsyncQueue<ReactorMessage>();
|
|
249
258
|
const controller = new AbortController();
|
|
@@ -278,6 +287,7 @@ export class Reactors implements IReactors {
|
|
|
278
287
|
|
|
279
288
|
try {
|
|
280
289
|
const reactorInstance = this._artifactActivator ? undefined : new (reactorType as new () => Record<string, Function>)();
|
|
290
|
+
if (reactorInstance) validateInstance(reactorInstance);
|
|
281
291
|
const services: ReactorServices | undefined = this._eventStore && {
|
|
282
292
|
eventStore: this._eventStore, readModels: this._eventStore.readModels, signal: controller.signal
|
|
283
293
|
};
|
|
@@ -302,9 +312,12 @@ export class Reactors implements IReactors {
|
|
|
302
312
|
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
303
313
|
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
304
314
|
replayState: eventsToObserve.ReplayState
|
|
305
|
-
}, this._artifactActivator, artifact =>
|
|
306
|
-
|
|
307
|
-
|
|
315
|
+
}, this._artifactActivator, artifact => {
|
|
316
|
+
validateInstance(artifact.instance);
|
|
317
|
+
return runActivated(artifact, () =>
|
|
318
|
+
notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
|
|
319
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState });
|
|
320
|
+
}, this._connection.logger);
|
|
308
321
|
} else if (reactorInstance) {
|
|
309
322
|
await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
|
|
310
323
|
}
|
|
@@ -388,7 +401,10 @@ export class Reactors implements IReactors {
|
|
|
388
401
|
readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
|
|
389
402
|
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
390
403
|
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
391
|
-
}, this._artifactActivator,
|
|
404
|
+
}, this._artifactActivator, artifact => {
|
|
405
|
+
validateInstance(artifact.instance);
|
|
406
|
+
return processEvents(artifact);
|
|
407
|
+
}, this._connection.logger);
|
|
392
408
|
} catch (err) {
|
|
393
409
|
this._logger.error('Error activating reactor', { reactorId: id, error: err });
|
|
394
410
|
exceptionMessages.push(String(err));
|
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
5
|
import { onceOnly, isOnceOnly } from './onceOnly.js';
|
|
6
6
|
import { replay, getReplayEventType } from './replay.js';
|
|
7
|
+
import { handles } from '../index.js';
|
|
8
|
+
import { getHandledEventType } from '../events/handles.js';
|
|
7
9
|
|
|
8
10
|
class EventForReplay {}
|
|
9
11
|
|
|
10
12
|
@onceOnly()
|
|
11
13
|
class StandardReactor {
|
|
14
|
+
@handles(EventForReplay)
|
|
15
|
+
@onceOnly()
|
|
16
|
+
notify() {}
|
|
17
|
+
|
|
12
18
|
@onceOnly()
|
|
13
19
|
eventForReplay() {}
|
|
14
20
|
|
|
@@ -31,6 +37,8 @@ describe('standard reactor policy decorators', () => {
|
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
it('marks methods without modifying the class or other methods', () => {
|
|
40
|
+
expect(getHandledEventType(StandardReactor.prototype.notify)).toBe(EventForReplay);
|
|
41
|
+
expect(isOnceOnly(StandardReactor.prototype.notify)).toBe(true);
|
|
34
42
|
expect(isOnceOnly(StandardReactor.prototype.eventForReplay)).toBe(true);
|
|
35
43
|
expect(isOnceOnly(StandardReactor.prototype.replayEventForReplay)).toBe(false);
|
|
36
44
|
expect(getReplayEventType(StandardReactor.prototype.replayEventForReplay)).toBe(true);
|