@cratis/chronicle 6.33.0 → 6.35.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.
Files changed (243) hide show
  1. package/ChronicleClient.ts +43 -35
  2. package/ChronicleOptions.ts +27 -1
  3. package/EventStore.ts +22 -12
  4. package/Metrics.ts +70 -96
  5. package/Tracing.ts +4 -2
  6. package/WellKnownTelemetryNames.ts +67 -0
  7. package/artifacts/validateArtifactSchemas.spec.ts +1 -1
  8. package/artifacts/withActivatedArtifact.ts +7 -5
  9. package/connection/ChronicleConnection.ts +18 -6
  10. package/connection/CompatibilityPreflight.ts +2 -1
  11. package/connection/KernelKeepAlive.ts +13 -7
  12. package/connection/TokenProvider.ts +3 -1
  13. package/connection/TraceContextMiddleware.ts +30 -0
  14. package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
  15. package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
  16. package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
  17. package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
  18. package/correlation/CorrelationIdManager.ts +10 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +45 -35
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +8 -0
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +19 -1
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +3 -1
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +22 -12
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/Metrics.d.ts +23 -64
  31. package/dist/Metrics.d.ts.map +1 -1
  32. package/dist/Metrics.js +62 -92
  33. package/dist/Metrics.js.map +1 -1
  34. package/dist/Tracing.d.ts +1 -1
  35. package/dist/Tracing.d.ts.map +1 -1
  36. package/dist/Tracing.js +4 -2
  37. package/dist/Tracing.js.map +1 -1
  38. package/dist/WellKnownTelemetryNames.d.ts +65 -0
  39. package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
  40. package/dist/WellKnownTelemetryNames.js +67 -0
  41. package/dist/WellKnownTelemetryNames.js.map +1 -0
  42. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  43. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  44. package/dist/artifacts/withActivatedArtifact.js +5 -4
  45. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  46. package/dist/connection/ChronicleConnection.d.ts +5 -0
  47. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  48. package/dist/connection/ChronicleConnection.js +13 -6
  49. package/dist/connection/ChronicleConnection.js.map +1 -1
  50. package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
  51. package/dist/connection/CompatibilityPreflight.js +2 -1
  52. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  53. package/dist/connection/KernelKeepAlive.d.ts +3 -1
  54. package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
  55. package/dist/connection/KernelKeepAlive.js +10 -7
  56. package/dist/connection/KernelKeepAlive.js.map +1 -1
  57. package/dist/connection/TokenProvider.d.ts.map +1 -1
  58. package/dist/connection/TokenProvider.js +3 -1
  59. package/dist/connection/TokenProvider.js.map +1 -1
  60. package/dist/connection/TraceContextMiddleware.d.ts +4 -0
  61. package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
  62. package/dist/connection/TraceContextMiddleware.js +30 -0
  63. package/dist/connection/TraceContextMiddleware.js.map +1 -0
  64. package/dist/correlation/CorrelationIdManager.d.ts +4 -0
  65. package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
  66. package/dist/correlation/CorrelationIdManager.js +8 -0
  67. package/dist/correlation/CorrelationIdManager.js.map +1 -1
  68. package/dist/eventSequences/EventLog.d.ts +2 -1
  69. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  70. package/dist/eventSequences/EventLog.js +2 -2
  71. package/dist/eventSequences/EventLog.js.map +1 -1
  72. package/dist/eventSequences/EventSequence.d.ts +4 -1
  73. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  74. package/dist/eventSequences/EventSequence.js +63 -80
  75. package/dist/eventSequences/EventSequence.js.map +1 -1
  76. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
  77. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
  78. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
  79. package/dist/events/handles.d.ts +12 -0
  80. package/dist/events/handles.d.ts.map +1 -0
  81. package/dist/events/handles.js +33 -0
  82. package/dist/events/handles.js.map +1 -0
  83. package/dist/events/index.d.ts +1 -0
  84. package/dist/events/index.d.ts.map +1 -1
  85. package/dist/events/index.js +1 -0
  86. package/dist/events/index.js.map +1 -1
  87. package/dist/index.d.ts +7 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +4 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/logging/ChronicleLogEntry.d.ts +11 -0
  92. package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
  93. package/dist/logging/ChronicleLogEntry.js +4 -0
  94. package/dist/logging/ChronicleLogEntry.js.map +1 -0
  95. package/dist/logging/ChronicleLogLevel.d.ts +9 -0
  96. package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
  97. package/dist/logging/ChronicleLogLevel.js +12 -0
  98. package/dist/logging/ChronicleLogLevel.js.map +1 -0
  99. package/dist/logging/DiagChronicleLogger.d.ts +7 -0
  100. package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
  101. package/dist/logging/DiagChronicleLogger.js +10 -0
  102. package/dist/logging/DiagChronicleLogger.js.map +1 -0
  103. package/dist/logging/IChronicleLogger.d.ts +7 -0
  104. package/dist/logging/IChronicleLogger.d.ts.map +1 -0
  105. package/dist/logging/IChronicleLogger.js +4 -0
  106. package/dist/logging/IChronicleLogger.js.map +1 -0
  107. package/dist/logging/createLogger.d.ts +10 -0
  108. package/dist/logging/createLogger.d.ts.map +1 -0
  109. package/dist/logging/createLogger.js +55 -0
  110. package/dist/logging/createLogger.js.map +1 -0
  111. package/dist/observation/EventHandler.d.ts +7 -0
  112. package/dist/observation/EventHandler.d.ts.map +1 -0
  113. package/dist/observation/EventHandler.js +4 -0
  114. package/dist/observation/EventHandler.js.map +1 -0
  115. package/dist/observation/createHandlerInstanceValidator.d.ts +4 -0
  116. package/dist/observation/createHandlerInstanceValidator.d.ts.map +1 -0
  117. package/dist/observation/createHandlerInstanceValidator.js +40 -0
  118. package/dist/observation/createHandlerInstanceValidator.js.map +1 -0
  119. package/dist/observation/getEventHandlers.d.ts +5 -0
  120. package/dist/observation/getEventHandlers.d.ts.map +1 -0
  121. package/dist/observation/getEventHandlers.js +40 -0
  122. package/dist/observation/getEventHandlers.js.map +1 -0
  123. package/dist/observation/getInstanceMethods.d.ts +4 -0
  124. package/dist/observation/getInstanceMethods.d.ts.map +1 -0
  125. package/dist/observation/getInstanceMethods.js +29 -0
  126. package/dist/observation/getInstanceMethods.js.map +1 -0
  127. package/dist/projections/Projections.d.ts.map +1 -1
  128. package/dist/projections/Projections.js +4 -3
  129. package/dist/projections/Projections.js.map +1 -1
  130. package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
  131. package/dist/reactors/ReactorDispatcher.js +27 -32
  132. package/dist/reactors/ReactorDispatcher.js.map +1 -1
  133. package/dist/reactors/Reactors.d.ts +1 -0
  134. package/dist/reactors/Reactors.d.ts.map +1 -1
  135. package/dist/reactors/Reactors.js +49 -34
  136. package/dist/reactors/Reactors.js.map +1 -1
  137. package/dist/reactors/index.d.ts +1 -0
  138. package/dist/reactors/index.d.ts.map +1 -1
  139. package/dist/reactors/index.js +1 -0
  140. package/dist/reactors/index.js.map +1 -1
  141. package/dist/reducers/ReducerEventDispatcher.d.ts +1 -0
  142. package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -1
  143. package/dist/reducers/ReducerEventDispatcher.js +5 -17
  144. package/dist/reducers/ReducerEventDispatcher.js.map +1 -1
  145. package/dist/reducers/ReducerEventHandler.d.ts +3 -6
  146. package/dist/reducers/ReducerEventHandler.d.ts.map +1 -1
  147. package/dist/reducers/Reducers.d.ts +1 -0
  148. package/dist/reducers/Reducers.d.ts.map +1 -1
  149. package/dist/reducers/Reducers.js +47 -32
  150. package/dist/reducers/Reducers.js.map +1 -1
  151. package/dist/reducers/index.d.ts +1 -0
  152. package/dist/reducers/index.d.ts.map +1 -1
  153. package/dist/reducers/index.js +1 -0
  154. package/dist/reducers/index.js.map +1 -1
  155. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  156. package/dist/seeding/EventSeeding.js +4 -3
  157. package/dist/seeding/EventSeeding.js.map +1 -1
  158. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
  159. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
  160. package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
  161. package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
  162. package/dist/telemetry/observeOperation.d.ts +6 -0
  163. package/dist/telemetry/observeOperation.d.ts.map +1 -0
  164. package/dist/telemetry/observeOperation.js +23 -0
  165. package/dist/telemetry/observeOperation.js.map +1 -0
  166. package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
  167. package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
  168. package/dist/telemetry/runInBackgroundContext.js +9 -0
  169. package/dist/telemetry/runInBackgroundContext.js.map +1 -0
  170. package/dist/telemetry/spanAttributes.d.ts +14 -0
  171. package/dist/telemetry/spanAttributes.d.ts.map +1 -0
  172. package/dist/telemetry/spanAttributes.js +47 -0
  173. package/dist/telemetry/spanAttributes.js.map +1 -0
  174. package/dist/testing/ReactorScenario.d.ts +1 -0
  175. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  176. package/dist/testing/ReactorScenario.js +9 -1
  177. package/dist/testing/ReactorScenario.js.map +1 -1
  178. package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
  179. package/dist/testing/ReducerReadModelProcessor.js +1 -0
  180. package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
  181. package/dist/tsconfig.tsbuildinfo +1 -1
  182. package/eventSequences/EventLog.ts +4 -2
  183. package/eventSequences/EventSequence.ts +64 -81
  184. package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
  185. package/events/for_handles/when_decorating_a_method.legacy.spec.ts +91 -0
  186. package/events/for_handles/when_decorating_a_method.standard.spec.ts +102 -0
  187. package/events/handles.ts +37 -0
  188. package/events/index.ts +1 -0
  189. package/for_ChronicleClient/when_registering_artifacts/with_invalid_handler_mappings.spec.ts +142 -0
  190. package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
  191. package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
  192. package/index.ts +7 -1
  193. package/logging/ChronicleLogEntry.ts +14 -0
  194. package/logging/ChronicleLogLevel.ts +11 -0
  195. package/logging/DiagChronicleLogger.ts +13 -0
  196. package/logging/IChronicleLogger.ts +10 -0
  197. package/logging/createLogger.ts +50 -0
  198. package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
  199. package/observation/EventHandler.ts +9 -0
  200. package/observation/createHandlerInstanceValidator.ts +42 -0
  201. package/observation/for_getEventHandlers/when_discovering_handlers.spec.ts +137 -0
  202. package/observation/for_getEventHandlers/when_reobserving_explicit_handlers.spec.ts +73 -0
  203. package/observation/getEventHandlers.ts +43 -0
  204. package/observation/getInstanceMethods.ts +29 -0
  205. package/package.json +8 -2
  206. package/projections/Projections.ts +4 -3
  207. package/reactors/ReactorDispatcher.ts +26 -30
  208. package/reactors/Reactors.ts +55 -37
  209. package/reactors/decorators.standard.spec.ts +8 -0
  210. package/reactors/for_ReactorDispatcher/when_selecting_explicit_handlers.spec.ts +64 -0
  211. package/reactors/for_Reactors/when_activating_a_field_override.spec.ts +62 -0
  212. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
  213. package/reactors/for_Reactors/when_delivering_to_instance_handlers.spec.ts +108 -0
  214. package/reactors/for_Reactors/when_observing_explicit_handlers.legacy.spec.ts +77 -0
  215. package/reactors/for_Reactors/when_registering_explicit_handlers.spec.ts +53 -0
  216. package/reactors/index.ts +1 -0
  217. package/reducers/ReducerEventDispatcher.ts +5 -16
  218. package/reducers/ReducerEventHandler.ts +4 -6
  219. package/reducers/Reducers.ts +50 -35
  220. package/reducers/for_ReducerEventDispatcher/when_invoking_an_explicit_handler.spec.ts +51 -0
  221. package/reducers/for_ReducerEventDispatcher/when_invoking_instance_handlers.spec.ts +69 -0
  222. package/reducers/for_Reducers/when_activating_a_field_override.spec.ts +63 -0
  223. package/reducers/for_Reducers/when_observing_explicit_handlers.legacy.spec.ts +73 -0
  224. package/reducers/for_Reducers/when_registering_explicit_handlers.spec.ts +57 -0
  225. package/reducers/index.ts +1 -0
  226. package/seeding/EventSeeding.ts +6 -4
  227. package/telemetry/ChronicleTelemetryOptions.ts +8 -0
  228. package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
  229. package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
  230. package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
  231. package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
  232. package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
  233. package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
  234. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
  235. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
  236. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
  237. package/telemetry/observeOperation.ts +25 -0
  238. package/telemetry/runInBackgroundContext.ts +10 -0
  239. package/telemetry/spanAttributes.ts +49 -0
  240. package/testing/ReactorScenario.ts +9 -1
  241. package/testing/ReducerReadModelProcessor.ts +1 -0
  242. package/testing/for_ReactorScenario/when_delivering_to_an_explicit_handler.spec.ts +100 -0
  243. package/testing/for_ReducerReadModelProcessor/when_an_instance_field_hides_a_handler.spec.ts +29 -0
@@ -0,0 +1,53 @@
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 type { IClientArtifactsProvider } from '../../artifacts/index.js';
6
+ import type { ChronicleConnection } from '../../connection/index.js';
7
+ import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
8
+ import type { IEventLog } from '../../eventSequences/IEventLog.js';
9
+ import { eventType } from '../../events/eventTypeDecorator.js';
10
+ import { handles, reactor, Reactors } from '../index.js';
11
+
12
+ chai.should();
13
+
14
+ @eventType('registration-handled-event')
15
+ class AuthorRegistered {}
16
+ @eventType('unselected-handled-event')
17
+ class Unregistered {}
18
+
19
+ @reactor('valid-handlers')
20
+ class Valid { @handles(AuthorRegistered) notify() {} }
21
+ @reactor('missing-handler-event')
22
+ class Missing { @handles(Unregistered) notify() {} }
23
+ @reactor('duplicate-handlers')
24
+ class Duplicate {
25
+ @handles(AuthorRegistered) notify() {}
26
+ authorRegistered() {}
27
+ }
28
+
29
+ for (const [type, message] of [
30
+ [Missing, /notify.*Missing.*Unregistered.*no registered event type/],
31
+ [Duplicate, /multiple handlers.*registration-handled-event.*notify.*authorRegistered/]
32
+ ] as const) {
33
+ describe(`when registering a reactor with invalid explicit handlers on ${type.name}`, () => {
34
+ const observe = vi.fn();
35
+ let error: unknown;
36
+ beforeEach(async () => {
37
+ observe.mockReset();
38
+ error = undefined;
39
+ const artifacts = { reactors: [Valid, type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider;
40
+ const connection = { reactors: { observe } } as unknown as ChronicleConnection;
41
+ const reactors = new Reactors(artifacts, connection, 'store', 'tenant', new ConnectionLifecycle(), {} as IEventLog);
42
+ try { await reactors.register(); } catch (caught) { error = caught; }
43
+ finally { reactors.dispose(); }
44
+ });
45
+ it('should reject registration with an actionable error', () => {
46
+ (error instanceof Error).should.be.true;
47
+ (error as Error).message.should.match(message);
48
+ });
49
+ it('should not start any observation before validating all reactors', () => {
50
+ observe.mock.calls.should.have.lengthOf(0);
51
+ });
52
+ });
53
+ }
package/reactors/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  export { ReactorId } from './ReactorId.js';
5
5
  export { reactor, getReactorMetadata, isReactor } from './reactor.js';
6
+ export { handles } from '../events/handles.js';
6
7
  export { onceOnly } from './onceOnly.js';
7
8
  export { replay } from './replay.js';
8
9
  export type { ReactorMetadata } from './reactor.js';
@@ -2,31 +2,20 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import type { Constructor } from '@cratis/fundamentals';
5
- import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
5
+ import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
6
+ import { getEventHandlers } from '../observation/getEventHandlers.js';
6
7
  import type { EventContext } from '../events/EventContext.js';
7
8
  import type { ReducerEventHandler } from './ReducerEventHandler.js';
8
9
 
9
10
  /** Selects and invokes the same reducer handlers for kernel deliveries and in-process scenarios. */
10
11
  export class ReducerEventDispatcher {
11
12
  readonly handlers: ReducerEventHandler[];
13
+ readonly validateInstance: (instance: object) => void;
12
14
 
13
15
  /** Discovers event methods from the artifact event types. */
14
16
  constructor(reducerType: Constructor, eventTypes: readonly Constructor[]) {
15
- const proto = reducerType.prototype as Record<string, unknown>;
16
- this.handlers = [];
17
- for (const eventTypeClass of eventTypes) {
18
- const metadata = getEventTypeMetadata(eventTypeClass);
19
- if (!metadata) continue;
20
- const className = (eventTypeClass as Function).name;
21
- const methodName = className.charAt(0).toLowerCase() + className.slice(1);
22
- if (typeof proto[methodName] === 'function') {
23
- this.handlers.push({
24
- id: metadata.eventType.id.value,
25
- generation: metadata.eventType.generation.value,
26
- methodName
27
- });
28
- }
29
- }
17
+ this.handlers = getEventHandlers(reducerType, eventTypes);
18
+ this.validateInstance = createHandlerInstanceValidator(reducerType, this.handlers.map(handler => handler.methodName));
30
19
  }
31
20
 
32
21
  /** Resolves a registered event type, or returns undefined for an unsubscribed event. */
@@ -1,9 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- /** Event type and camel-case method selected for a reducer observation. */
5
- export interface ReducerEventHandler {
6
- readonly id: string;
7
- readonly generation: number;
8
- readonly methodName: string;
9
- }
4
+ import type { EventHandler } from '../observation/EventHandler.js';
5
+
6
+ /** Event type and method selected for a reducer observation. */
7
+ export type ReducerEventHandler = EventHandler;
@@ -2,7 +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 'reflect-metadata';
5
- import { diag } from '@opentelemetry/api';
5
+ import { createLogger } from '../logging/createLogger.js';
6
+ import { runInBackgroundContext } from '../telemetry/runInBackgroundContext.js';
6
7
  import { Constructor } from '@cratis/fundamentals';
7
8
  import { ObservationState, ReadModelObserverType, ReducerMessage, ReplayState } from '@cratis/chronicle.contracts';
8
9
  import { IClientArtifactsProvider } from '../artifacts/index.js';
@@ -102,8 +103,9 @@ export class Reducers implements IReducers {
102
103
  /** How long to wait before re-establishing an observation whose stream ended. */
103
104
  private static readonly _reobserveDelayMs = 2000;
104
105
 
105
- private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/reducers' });
106
+ private readonly _logger: ReturnType<typeof createLogger>;
106
107
  private readonly _lifecycle: ConnectionLifecycle;
108
+ private readonly _unsubscribeDisconnected: () => void;
107
109
  private readonly _reducers = new Map<string, Constructor>();
108
110
  private readonly _queues = new Map<string, AsyncQueue<ReducerMessage>>();
109
111
  private readonly _observations = new Map<string, AbortController>();
@@ -133,8 +135,9 @@ export class Reducers implements IReducers {
133
135
  private readonly _artifactActivator?: ClientArtifactsActivator,
134
136
  private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
135
137
  ) {
138
+ this._logger = createLogger('@cratis/chronicle/reducers', _connection.logger);
136
139
  this._lifecycle = lifecycle;
137
- lifecycle.onDisconnected(async () => {
140
+ this._unsubscribeDisconnected = lifecycle.onDisconnected(async () => {
138
141
  this._logger.info('Disconnected — stopping all reducer observations');
139
142
  this._registered = false;
140
143
  this.disconnectAll();
@@ -143,6 +146,7 @@ export class Reducers implements IReducers {
143
146
 
144
147
  /** Stops observations permanently when the owning client is disposed. */
145
148
  dispose(): void {
149
+ this._unsubscribeDisconnected();
146
150
  this._disposed = true;
147
151
  this._registered = false;
148
152
  this.disconnectAll();
@@ -170,13 +174,16 @@ export class Reducers implements IReducers {
170
174
  await this.discover();
171
175
  }
172
176
 
177
+ const registrations = [...this._reducers].map(([id, type]) => ({
178
+ id, type, dispatcher: new ReducerEventDispatcher(type, this._clientArtifacts.eventTypes)
179
+ }));
173
180
  assertUniqueReadModelIds(this._clientArtifacts.readModels);
174
181
  await this.registerReadModels();
175
182
 
176
183
  if (this._disposed) return;
177
184
  this._logger.info('Registering reducers', { count: this._reducers.size });
178
- for (const [id, reducerType] of this._reducers) {
179
- this.startObservation(id, reducerType);
185
+ for (const { id, type, dispatcher } of registrations) {
186
+ this.startObservation(id, type, dispatcher);
180
187
  }
181
188
 
182
189
  this._registered = true;
@@ -234,22 +241,24 @@ export class Reducers implements IReducers {
234
241
  return JSON.stringify(minimalSchema);
235
242
  }
236
243
 
237
- private startObservation(id: string, reducerType: Constructor): void {
238
- if (this._disposed) return;
239
- const metadata = getReducerMetadata(reducerType)!;
240
- const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
241
- const dispatcher = new ReducerEventDispatcher(reducerType, this._clientArtifacts.eventTypes);
242
- const readModelName = this.getReducerReadModelIdentifier(reducerType);
243
-
244
- this._logger.info('Starting reducer observation', {
245
- reducerId: id,
246
- eventSequenceId,
247
- readModel: readModelName,
248
- handlerCount: dispatcher.handlers.length,
249
- handlers: dispatcher.handlers.map(e => e.methodName)
250
- });
244
+ private startObservation(id: string, reducerType: Constructor,
245
+ dispatcher: ReducerEventDispatcher): void {
246
+ runInBackgroundContext(() => {
247
+ if (this._disposed) return;
248
+ const metadata = getReducerMetadata(reducerType)!;
249
+ const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
250
+ const readModelName = this.getReducerReadModelIdentifier(reducerType);
251
251
 
252
- void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
252
+ this._logger.info('Starting reducer observation', {
253
+ reducerId: id,
254
+ eventSequenceId,
255
+ readModel: readModelName,
256
+ handlerCount: dispatcher.handlers.length,
257
+ handlers: dispatcher.handlers.map(e => e.methodName)
258
+ });
259
+
260
+ void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
261
+ });
253
262
  }
254
263
 
255
264
  private async runObservation(
@@ -262,10 +271,10 @@ export class Reducers implements IReducers {
262
271
  try {
263
272
  await this.observeReducer(id, reducerType, eventSequenceId, readModelName, dispatcher);
264
273
  } catch (error) {
265
- this._logger.error('Reducer observation loop exited with error', { reducerId: id, error: String(error) });
274
+ this._logger.error('Reducer observation loop exited with error', { reducerId: id, error });
266
275
  }
267
276
 
268
- this.scheduleReobserve(id, reducerType);
277
+ this.scheduleReobserve(id, reducerType, dispatcher);
269
278
  }
270
279
 
271
280
  /**
@@ -278,21 +287,21 @@ export class Reducers implements IReducers {
278
287
  * retried, and the delay keeps a stream that keeps ending from becoming a hot
279
288
  * loop.
280
289
  */
281
- private scheduleReobserve(id: string, reducerType: Constructor): void {
290
+ private scheduleReobserve(id: string, reducerType: Constructor, dispatcher: ReducerEventDispatcher): void {
282
291
  // A disconnect clears the registration; the reconnect re-registers every
283
292
  // reducer from scratch, so retrying here as well would double up.
284
293
  if (!this._registered || this._disposed) {
285
294
  return;
286
295
  }
287
296
 
288
- const handle = setTimeout(() => {
297
+ const handle = runInBackgroundContext(() => setTimeout(() => {
289
298
  if (!this._registered || this._disposed) {
290
299
  return;
291
300
  }
292
301
 
293
302
  this._logger.info('Re-establishing reducer observation', { reducerId: id });
294
- this.startObservation(id, reducerType);
295
- }, Reducers._reobserveDelayMs);
303
+ this.startObservation(id, reducerType, dispatcher);
304
+ }, Reducers._reobserveDelayMs));
296
305
 
297
306
  handle.unref?.();
298
307
  }
@@ -349,6 +358,7 @@ export class Reducers implements IReducers {
349
358
 
350
359
  try {
351
360
  const reducerInstance = this._artifactActivator ? undefined : new (reducerType as new () => Record<string, Function>)();
361
+ if (reducerInstance) dispatcher.validateInstance(reducerInstance);
352
362
 
353
363
  for await (const operation of this._connection.reducers.observe(queue, { signal: controller.signal })) {
354
364
  let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
@@ -359,7 +369,6 @@ export class Reducers implements IReducers {
359
369
 
360
370
  this._logger.debug('Received reduce operation', {
361
371
  reducerId: id,
362
- partition: operation.Partition,
363
372
  count: operation.Events.length,
364
373
  hasInitialState: operation.InitialState !== '',
365
374
  replayState: operation.ReplayState
@@ -373,14 +382,17 @@ export class Reducers implements IReducers {
373
382
  readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
374
383
  signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
375
384
  replayState: operation.ReplayState
376
- }, this._artifactActivator, artifact => runActivated(artifact, () =>
377
- notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
378
- { delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }));
385
+ }, this._artifactActivator, artifact => {
386
+ dispatcher.validateInstance(artifact.instance);
387
+ return runActivated(artifact, () =>
388
+ notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
389
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState });
390
+ }, this._connection.logger);
379
391
  } else if (reducerInstance) {
380
392
  await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
381
393
  }
382
394
  } catch (err) {
383
- this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: String(err) });
395
+ this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: err });
384
396
  if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
385
397
  exceptionMessages.push(String(err.processingError));
386
398
  }
@@ -424,7 +436,7 @@ export class Reducers implements IReducers {
424
436
  { delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
425
437
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
426
438
  } catch (err) {
427
- this._logger.error('Error handling event in reducer', { reducerId: id, error: String(err) });
439
+ this._logger.error('Error handling event in reducer', { reducerId: id, error: err });
428
440
  exceptionMessages.push(String(err));
429
441
  exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
430
442
  state = ObservationState.Failed;
@@ -445,9 +457,12 @@ export class Reducers implements IReducers {
445
457
  readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
446
458
  signal: controller.signal, delivery: ArtifactDelivery.Events,
447
459
  eventContext: toClientEventContext(firstInvocableEvent.Context!)
448
- }, this._artifactActivator, processEvents);
460
+ }, this._artifactActivator, artifact => {
461
+ dispatcher.validateInstance(artifact.instance);
462
+ return processEvents(artifact);
463
+ }, this._connection.logger);
449
464
  } catch (err) {
450
- this._logger.error('Error activating reducer', { reducerId: id, error: String(err) });
465
+ this._logger.error('Error activating reducer', { reducerId: id, error: err });
451
466
  exceptionMessages.push(String(err));
452
467
  const completionStack = err instanceof Error ? (err.stack ?? '') : '';
453
468
  exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
@@ -483,7 +498,7 @@ export class Reducers implements IReducers {
483
498
  if (!this._queues.has(id)) {
484
499
  this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
485
500
  } else {
486
- this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: String(err) });
501
+ this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
487
502
  }
488
503
  } finally {
489
504
  // Only retire our own queue: a reconnect can already have replaced it,
@@ -0,0 +1,51 @@
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 { EventType } from '../../events/EventType.js';
7
+ import type { EventContext } from '../../events/EventContext.js';
8
+ import { handles } from '../index.js';
9
+ import { ReducerEventDispatcher } from '../ReducerEventDispatcher.js';
10
+
11
+ chai.should();
12
+
13
+ @eventType('explicit-reducer-delivery')
14
+ class AuthorRegistered { constructor(readonly name: string) {} }
15
+
16
+ class AuthorReducer {
17
+ @handles(AuthorRegistered)
18
+ update(event: AuthorRegistered, state: { names: string[] } | undefined, context: EventContext) {
19
+ return { names: [...(state?.names ?? []), event.name], id: context.eventSourceId };
20
+ }
21
+ }
22
+
23
+ class FieldReducer extends AuthorReducer {
24
+ override update = () => ({ names: [], id: 'hidden' });
25
+ }
26
+
27
+ describe('when an instance field hides an inherited reducer handler', () => {
28
+ it('should reject the field against the preflight method', () => {
29
+ const dispatcher = new ReducerEventDispatcher(FieldReducer, [AuthorRegistered]);
30
+ (() => dispatcher.validateInstance(new FieldReducer())).should.throw(
31
+ "Override 'update' on 'FieldReducer' hides @handles(AuthorRegistered) declared on 'AuthorReducer'; use a method with @handles instead of an instance field.");
32
+ });
33
+ });
34
+
35
+ describe('when invoking an explicit reducer handler', () => {
36
+ let result: unknown;
37
+ beforeEach(async () => {
38
+ const dispatcher = new ReducerEventDispatcher(AuthorReducer, [AuthorRegistered]);
39
+ const instance = new AuthorReducer() as unknown as Record<string, Function>;
40
+ const context: EventContext = {
41
+ sequenceNumber: 1n, eventSourceId: 'author', eventType: EventType.parse('explicit-reducer-delivery'),
42
+ occurred: new Date(), correlationId: '', causation: [], tags: []
43
+ };
44
+ const handler = dispatcher.handlerFor('explicit-reducer-delivery')!;
45
+ const initial = await dispatcher.invoke(instance, handler, new AuthorRegistered('Ada'), undefined, context);
46
+ result = await dispatcher.invoke(instance, handler, new AuthorRegistered('Grace'), initial, context);
47
+ });
48
+ it('should pass the event state and context to the named method', () => {
49
+ (result as object).should.deep.equal({ names: ['Ada', 'Grace'], id: 'author' });
50
+ });
51
+ });
@@ -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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import { eventType } from '../../events/eventTypeDecorator.js';
6
+ import { EventType } from '../../events/EventType.js';
7
+ import type { EventContext } from '../../events/EventContext.js';
8
+ import { handles } from '../index.js';
9
+ import { ReducerEventDispatcher } from '../ReducerEventDispatcher.js';
10
+
11
+ chai.should();
12
+
13
+ @eventType('instance-reducer-event')
14
+ class AuthorRegistered { constructor(readonly name: string) {} }
15
+
16
+ class BoundConventional {
17
+ readonly prefix = 'conventional';
18
+ constructor() { this.authorRegistered = this.authorRegistered.bind(this); }
19
+ authorRegistered(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
20
+ return [...(state ?? []), `${this.prefix}:${event.name}:${context.eventSourceId}`];
21
+ }
22
+ }
23
+
24
+ class BoundExplicit {
25
+ readonly prefix = 'explicit';
26
+ constructor() { this.update = this.update.bind(this); }
27
+ @handles(AuthorRegistered)
28
+ update(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
29
+ return [...(state ?? []), `${this.prefix}:${event.name}:${context.eventSourceId}`];
30
+ }
31
+ }
32
+
33
+ class Conventional {
34
+ authorRegistered(event: AuthorRegistered, state: string[] | undefined, context: EventContext) {
35
+ return [...(state ?? []), `base:${event.name}:${context.eventSourceId}`];
36
+ }
37
+ }
38
+
39
+ class FieldConventional extends Conventional {
40
+ override authorRegistered = (event: AuthorRegistered, state: string[] | undefined, context: EventContext) =>
41
+ [...(state ?? []), `field:${event.name}:${context.eventSourceId}`];
42
+ }
43
+
44
+ for (const { type, prefix } of [
45
+ { type: BoundConventional, prefix: 'conventional' },
46
+ { type: BoundExplicit, prefix: 'explicit' },
47
+ { type: FieldConventional, prefix: 'field' }
48
+ ]) {
49
+ describe(`when invoking the ${prefix} reducer instance handler`, () => {
50
+ let result: unknown;
51
+ beforeEach(async () => {
52
+ const dispatcher = new ReducerEventDispatcher(type, [AuthorRegistered]);
53
+ const instance = new type();
54
+ dispatcher.validateInstance(instance);
55
+ const context: EventContext = {
56
+ sequenceNumber: 1n, eventSourceId: 'author', eventType: EventType.parse('instance-reducer-event'),
57
+ occurred: new Date(), correlationId: '', causation: [], tags: []
58
+ };
59
+ const handler = dispatcher.handlerFor('instance-reducer-event')!;
60
+ const initial = await dispatcher.invoke(instance as unknown as Record<string, Function>, handler,
61
+ new AuthorRegistered('Ada'), undefined, context);
62
+ result = await dispatcher.invoke(instance as unknown as Record<string, Function>, handler,
63
+ new AuthorRegistered('Grace'), initial, context);
64
+ });
65
+ it('should invoke the instance handler with the event state and context', () => {
66
+ (result as string[]).should.deep.equal([`${prefix}:Ada:author`, `${prefix}:Grace:author`]);
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,63 @@
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 { EventContext, EventType, ObservationState, ReplayState, type ReducerMessage } from '@cratis/chronicle.contracts';
5
+ import { beforeEach, chai, describe, it } 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 { IEventStore } from '../../IEventStore.js';
10
+ import { eventType } from '../../events/eventTypeDecorator.js';
11
+ import { handles, reducer, Reducers } from '../index.js';
12
+
13
+ chai.should();
14
+
15
+ @eventType('field-reducer-event')
16
+ class AuthorRegistered {}
17
+ const calls: string[] = [];
18
+ class Base {
19
+ @handles(AuthorRegistered) update() { calls.push('base'); return {}; }
20
+ beginReplay() { calls.push('replay'); }
21
+ endReplay() {}
22
+ }
23
+ @reducer('field-reducer')
24
+ class Derived extends Base {
25
+ override update = () => { calls.push('field'); return {}; };
26
+ }
27
+
28
+ for (const replayState of [ReplayState.REPLAY_STATE_None, ReplayState.BeginReplay]) {
29
+ describe(`when activating a reducer with a field override for replay state ${replayState}`, () => {
30
+ let result: ReducerMessage;
31
+ let disposed: boolean;
32
+ beforeEach(async () => {
33
+ calls.length = 0;
34
+ disposed = false;
35
+ const lifecycle = new ConnectionLifecycle();
36
+ let finish!: () => void;
37
+ const finished = new Promise<void>(resolve => { finish = resolve; });
38
+ async function* stream(queue: AsyncIterable<ReducerMessage>) {
39
+ const messages = queue[Symbol.asyncIterator]();
40
+ await messages.next();
41
+ yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
42
+ EventType: EventType.fromPartial({ Id: 'field-reducer-event', Generation: 1 }), SequenceNumber: 1n
43
+ }) }], Partition: 'author', ReplayState: replayState, InitialState: '' };
44
+ result = (await messages.next()).value!;
45
+ await lifecycle.disconnected(error => { throw error; });
46
+ finish();
47
+ }
48
+ const reducers = new Reducers({ reducers: [Derived], eventTypes: [AuthorRegistered], readModels: [] } as unknown as IClientArtifactsProvider,
49
+ { reducers: { observe: stream }, readModels: { registerMany: async () => ({}) } } as unknown as ChronicleConnection,
50
+ 'store', 'tenant', lifecycle, 'sink', { readModels: {} } as IEventStore,
51
+ type => ({ instance: new type(), dispose: () => { disposed = true; } }));
52
+ try { await reducers.register(); await finished; } finally { reducers.dispose(); }
53
+ });
54
+ it('should fail delivery without invoking the field or replay notification', () => {
55
+ calls.should.have.lengthOf(0);
56
+ result.Content!.Value1!.State.should.equal(ObservationState.Failed);
57
+ result.Content!.Value1!.ReadModelState.should.equal('');
58
+ result.Content!.Value1!.ExceptionMessages.join(' ').should.contain(
59
+ "Override 'update' on 'Derived' hides @handles(AuthorRegistered) declared on 'Base'; use a method with @handles instead of an instance field.");
60
+ });
61
+ it('should release the rejected activation lease', () => { disposed.should.be.true; });
62
+ });
63
+ }
@@ -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 { EventContext, EventType, ObservationState, type ReducerMessage } from '@cratis/chronicle.contracts';
5
+ import { beforeEach, chai, describe, it } 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 { eventType } from '../../events/eventTypeDecorator.js';
10
+ import type { EventContext as ClientEventContext } from '../../events/EventContext.js';
11
+ import { handles, reducer, Reducers } from '../index.js';
12
+
13
+ chai.should();
14
+
15
+ @eventType('legacy-reduced-handler')
16
+ class AuthorRegistered { constructor(readonly name = '') {} }
17
+
18
+ @reducer('legacy-reduction')
19
+ class AuthorReducer {
20
+ @handles(AuthorRegistered)
21
+ update(event: AuthorRegistered, state: { names: string[] } | undefined, context: ClientEventContext) {
22
+ return { names: [...(state?.names ?? []), event.name], id: context.eventSourceId };
23
+ }
24
+ }
25
+
26
+ describe('when observing explicit legacy reducer handlers', () => {
27
+ let registration: ReducerMessage;
28
+ let result: ReducerMessage;
29
+ beforeEach(async () => {
30
+ const lifecycle = new ConnectionLifecycle();
31
+ let finish!: () => void;
32
+ const finished = new Promise<void>(resolve => { finish = resolve; });
33
+ async function* stream(queue: AsyncIterable<ReducerMessage>) {
34
+ const messages = queue[Symbol.asyncIterator]();
35
+ registration = (await messages.next()).value!;
36
+ yield {
37
+ Events: ['Ada', 'Grace'].map((name, index) => ({
38
+ Context: EventContext.fromPartial({
39
+ EventType: EventType.fromPartial({ Id: 'legacy-reduced-handler', Generation: 1 }),
40
+ EventSourceId: 'author', SequenceNumber: BigInt(index + 1)
41
+ }),
42
+ Content: JSON.stringify({ name })
43
+ })),
44
+ Partition: 'author', ReplayState: 0, InitialState: ''
45
+ };
46
+ result = (await messages.next()).value!;
47
+ await lifecycle.disconnected(error => { throw error; });
48
+ finish();
49
+ }
50
+ const connection = {
51
+ reducers: { observe: stream }, readModels: { registerMany: async () => ({}) }
52
+ } as unknown as ChronicleConnection;
53
+ const artifacts = {
54
+ reducers: [AuthorReducer], eventTypes: [AuthorRegistered], readModels: []
55
+ } as unknown as IClientArtifactsProvider;
56
+ const reducers = new Reducers(artifacts, connection, 'store', 'tenant', lifecycle, 'sink');
57
+ try {
58
+ await reducers.register();
59
+ await finished;
60
+ } finally { reducers.dispose(); }
61
+ });
62
+ it('should subscribe to the explicitly named event type', () => {
63
+ registration.Content!.Value0!.Reducer!.EventTypes.map(entry => entry.EventType!.Id)
64
+ .should.deep.equal(['legacy-reduced-handler']);
65
+ });
66
+ it('should pass the event state and context through the named method', () => {
67
+ JSON.parse(result.Content!.Value1!.ReadModelState).should.deep.equal({ names: ['Ada', 'Grace'], id: 'author' });
68
+ });
69
+ it('should acknowledge both deliveries successfully', () => {
70
+ result.Content!.Value1!.State.should.equal(ObservationState.Success);
71
+ result.Content!.Value1!.LastSuccessfulObservation.should.equal(2n);
72
+ });
73
+ });
@@ -0,0 +1,57 @@
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 type { IClientArtifactsProvider } from '../../artifacts/index.js';
6
+ import type { ChronicleConnection } from '../../connection/index.js';
7
+ import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
8
+ import { eventType } from '../../events/eventTypeDecorator.js';
9
+ import { handles, reducer, Reducers } from '../index.js';
10
+
11
+ chai.should();
12
+
13
+ @eventType('reducer-handled-event')
14
+ class AuthorRegistered {}
15
+ @eventType('unselected-reducer-event')
16
+ class Unregistered {}
17
+
18
+ @reducer('valid-handlers')
19
+ class Valid { @handles(AuthorRegistered) update() {} }
20
+ @reducer('missing-handler-event')
21
+ class Missing { @handles(Unregistered) update() {} }
22
+ @reducer('duplicate-handlers')
23
+ class Duplicate {
24
+ @handles(AuthorRegistered) update() {}
25
+ authorRegistered() {}
26
+ }
27
+
28
+ for (const [type, message] of [
29
+ [Missing, /update.*Missing.*Unregistered.*no registered event type/],
30
+ [Duplicate, /multiple handlers.*reducer-handled-event.*update.*authorRegistered/]
31
+ ] as const) {
32
+ describe(`when registering a reducer with invalid explicit handlers on ${type.name}`, () => {
33
+ const observe = vi.fn();
34
+ const registerMany = vi.fn();
35
+ let error: unknown;
36
+ beforeEach(async () => {
37
+ observe.mockReset();
38
+ registerMany.mockReset();
39
+ error = undefined;
40
+ const artifacts = { reducers: [Valid, type], eventTypes: [AuthorRegistered], readModels: [] } as unknown as IClientArtifactsProvider;
41
+ const connection = { reducers: { observe }, readModels: { registerMany } } as unknown as ChronicleConnection;
42
+ const reducers = new Reducers(artifacts, connection, 'store', 'tenant', new ConnectionLifecycle(), 'sink');
43
+ try { await reducers.register(); } catch (caught) { error = caught; }
44
+ finally { reducers.dispose(); }
45
+ });
46
+ it('should reject registration with an actionable error', () => {
47
+ (error instanceof Error).should.be.true;
48
+ (error as Error).message.should.match(message);
49
+ });
50
+ it('should not register read models before validating all handlers', () => {
51
+ registerMany.mock.calls.should.have.lengthOf(0);
52
+ });
53
+ it('should not start an observation before validating all reducers', () => {
54
+ observe.mock.calls.should.have.lengthOf(0);
55
+ });
56
+ });
57
+ }
package/reducers/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ export { handles } from '../events/handles.js';
4
5
  export { ReducerId } from './ReducerId.js';
5
6
  export { reducer, getReducerMetadata, isReducer } from './reducer.js';
6
7
  export type { ReducerMetadata } from './reducer.js';