@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
@@ -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, ReactorMessage, ReplayState } from '@cratis/chronicle.contracts';
8
9
  import { IClientArtifactsProvider } from '../artifacts/index.js';
@@ -13,6 +14,7 @@ import { getTagsFor } from '../events/tagDecorator.js';
13
14
  import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
14
15
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
15
16
  import type { IEventLog } from '../eventSequences/IEventLog.js';
17
+ import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
16
18
  import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle.js';
17
19
  import { IReactors } from './IReactors.js';
18
20
  import { dispatchReactorSideEffects } from './ReactorSideEffects.js';
@@ -102,8 +104,9 @@ export class Reactors implements IReactors {
102
104
  /** How long to wait before re-establishing an observation whose stream ended. */
103
105
  private static readonly _reobserveDelayMs = 2000;
104
106
 
105
- private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/reactors' });
107
+ private readonly _logger: ReturnType<typeof createLogger>;
106
108
  private readonly _lifecycle: ConnectionLifecycle;
109
+ private readonly _unsubscribeDisconnected: () => void;
107
110
  private readonly _reactors = new Map<string, Constructor>();
108
111
  private readonly _queues = new Map<string, AsyncQueue<ReactorMessage>>();
109
112
  private readonly _observations = new Map<string, AbortController>();
@@ -130,8 +133,9 @@ export class Reactors implements IReactors {
130
133
  private readonly _eventStore?: IEventStore,
131
134
  private readonly _artifactActivator?: ClientArtifactsActivator
132
135
  ) {
136
+ this._logger = createLogger('@cratis/chronicle/reactors', _connection.logger);
133
137
  this._lifecycle = lifecycle;
134
- lifecycle.onDisconnected(async () => {
138
+ this._unsubscribeDisconnected = lifecycle.onDisconnected(async () => {
135
139
  this._logger.info('Disconnected — stopping all reactor observations');
136
140
  this._registered = false;
137
141
  this.disconnectAll();
@@ -140,6 +144,7 @@ export class Reactors implements IReactors {
140
144
 
141
145
  /** Stops observations permanently when the owning client is disposed. */
142
146
  dispose(): void {
147
+ this._unsubscribeDisconnected();
143
148
  this._disposed = true;
144
149
  this._registered = false;
145
150
  this.disconnectAll();
@@ -168,43 +173,50 @@ export class Reactors implements IReactors {
168
173
  }
169
174
 
170
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))) }));
171
180
  this._logger.info('Registering reactors', { count: this._reactors.size });
172
- for (const [id, reactorType] of this._reactors) {
173
- this.startObservation(id, reactorType);
181
+ for (const { id, type, eventTypes, validateInstance } of registrations) {
182
+ this.startObservation(id, type, eventTypes, validateInstance);
174
183
  }
175
184
 
176
185
  this._registered = true;
177
186
  }
178
187
 
179
- private startObservation(id: string, reactorType: Constructor): void {
180
- if (this._disposed) return;
181
- const metadata = getReactorMetadata(reactorType)!;
182
- const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
183
- const eventTypes = getReactorEventTypes(reactorType, this._clientArtifacts.eventTypes);
184
-
185
- this._logger.info('Starting reactor observation', {
186
- reactorId: id,
187
- eventSequenceId,
188
- handlerCount: eventTypes.length,
189
- handlers: eventTypes.map(e => e.methodName)
188
+ private startObservation(id: string, reactorType: Constructor,
189
+ eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
190
+ runInBackgroundContext(() => {
191
+ if (this._disposed) return;
192
+ const metadata = getReactorMetadata(reactorType)!;
193
+ const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
194
+
195
+ this._logger.info('Starting reactor observation', {
196
+ reactorId: id,
197
+ eventSequenceId,
198
+ handlerCount: eventTypes.length,
199
+ handlers: eventTypes.map(e => e.methodName)
200
+ });
201
+
202
+ void this.runObservation(id, reactorType, eventSequenceId, eventTypes, validateInstance);
190
203
  });
191
-
192
- void this.runObservation(id, reactorType, eventSequenceId, eventTypes);
193
204
  }
194
205
 
195
206
  private async runObservation(
196
207
  id: string,
197
208
  reactorType: Constructor,
198
209
  eventSequenceId: string,
199
- eventTypes: ReactorEventTypeEntry[]
210
+ eventTypes: ReactorEventTypeEntry[],
211
+ validateInstance: (instance: object) => void
200
212
  ): Promise<void> {
201
213
  try {
202
- await this.observeReactor(id, reactorType, eventSequenceId, eventTypes);
214
+ await this.observeReactor(id, reactorType, eventSequenceId, eventTypes, validateInstance);
203
215
  } catch (error) {
204
- this._logger.error('Reactor observation loop exited with error', { reactorId: id, error: String(error) });
216
+ this._logger.error('Reactor observation loop exited with error', { reactorId: id, error });
205
217
  }
206
218
 
207
- this.scheduleReobserve(id, reactorType);
219
+ this.scheduleReobserve(id, reactorType, eventTypes, validateInstance);
208
220
  }
209
221
 
210
222
  /**
@@ -216,21 +228,21 @@ export class Reactors implements IReactors {
216
228
  * the whole client reconnects. Both endings are therefore retried, and the delay
217
229
  * keeps a stream that keeps ending from becoming a hot loop.
218
230
  */
219
- private scheduleReobserve(id: string, reactorType: Constructor): void {
231
+ private scheduleReobserve(id: string, reactorType: Constructor, eventTypes: ReactorEventTypeEntry[], validateInstance: (instance: object) => void): void {
220
232
  // A disconnect clears the registration; the reconnect re-registers every
221
233
  // reactor from scratch, so retrying here as well would double up.
222
234
  if (!this._registered || this._disposed) {
223
235
  return;
224
236
  }
225
237
 
226
- const handle = setTimeout(() => {
238
+ const handle = runInBackgroundContext(() => setTimeout(() => {
227
239
  if (!this._registered || this._disposed) {
228
240
  return;
229
241
  }
230
242
 
231
243
  this._logger.info('Re-establishing reactor observation', { reactorId: id });
232
- this.startObservation(id, reactorType);
233
- }, Reactors._reobserveDelayMs);
244
+ this.startObservation(id, reactorType, eventTypes, validateInstance);
245
+ }, Reactors._reobserveDelayMs));
234
246
 
235
247
  handle.unref?.();
236
248
  }
@@ -239,7 +251,8 @@ export class Reactors implements IReactors {
239
251
  id: string,
240
252
  reactorType: Constructor,
241
253
  eventSequenceId: string,
242
- eventTypes: ReactorEventTypeEntry[]
254
+ eventTypes: ReactorEventTypeEntry[],
255
+ validateInstance: (instance: object) => void
243
256
  ): Promise<void> {
244
257
  const queue = new AsyncQueue<ReactorMessage>();
245
258
  const controller = new AbortController();
@@ -274,6 +287,7 @@ export class Reactors implements IReactors {
274
287
 
275
288
  try {
276
289
  const reactorInstance = this._artifactActivator ? undefined : new (reactorType as new () => Record<string, Function>)();
290
+ if (reactorInstance) validateInstance(reactorInstance);
277
291
  const services: ReactorServices | undefined = this._eventStore && {
278
292
  eventStore: this._eventStore, readModels: this._eventStore.readModels, signal: controller.signal
279
293
  };
@@ -286,7 +300,6 @@ export class Reactors implements IReactors {
286
300
 
287
301
  this._logger.debug('Received events to observe', {
288
302
  reactorId: id,
289
- partition: eventsToObserve.Partition,
290
303
  count: eventsToObserve.Events.length,
291
304
  replayState: eventsToObserve.ReplayState
292
305
  });
@@ -299,14 +312,17 @@ export class Reactors implements IReactors {
299
312
  readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
300
313
  signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
301
314
  replayState: eventsToObserve.ReplayState
302
- }, this._artifactActivator, artifact => runActivated(artifact, () =>
303
- notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
304
- { delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState }));
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);
305
321
  } else if (reactorInstance) {
306
322
  await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
307
323
  }
308
324
  } catch (err) {
309
- this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
325
+ this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: err });
310
326
  if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
311
327
  exceptionMessages.push(String(err.processingError));
312
328
  }
@@ -348,7 +364,6 @@ export class Reactors implements IReactors {
348
364
  }
349
365
 
350
366
  const content = JSON.parse(event.Content) as Record<string, unknown>;
351
- this._logger.debug('Event content', { reactorId: id, eventTypeId, contentKeys: Object.keys(content), rawContent: event.Content.substring(0, 200) });
352
367
  const context = toClientEventContext(event.Context!);
353
368
 
354
369
  this._logger.info('Invoking reactor handler', {
@@ -364,7 +379,7 @@ export class Reactors implements IReactors {
364
379
 
365
380
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
366
381
  } catch (err) {
367
- this._logger.error('Error handling event in reactor', { reactorId: id, error: String(err) });
382
+ this._logger.error('Error handling event in reactor', { reactorId: id, error: err });
368
383
  exceptionMessages.push(String(err));
369
384
  exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
370
385
  state = ObservationState.Failed;
@@ -386,9 +401,12 @@ export class Reactors implements IReactors {
386
401
  readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
387
402
  signal: controller.signal, delivery: ArtifactDelivery.Events,
388
403
  eventContext: toClientEventContext(firstInvocableEvent.Context!)
389
- }, this._artifactActivator, processEvents);
404
+ }, this._artifactActivator, artifact => {
405
+ validateInstance(artifact.instance);
406
+ return processEvents(artifact);
407
+ }, this._connection.logger);
390
408
  } catch (err) {
391
- this._logger.error('Error activating reactor', { reactorId: id, error: String(err) });
409
+ this._logger.error('Error activating reactor', { reactorId: id, error: err });
392
410
  exceptionMessages.push(String(err));
393
411
  const completionStack = err instanceof Error ? (err.stack ?? '') : '';
394
412
  exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
@@ -419,7 +437,7 @@ export class Reactors implements IReactors {
419
437
  if (!this._queues.has(id)) {
420
438
  this._logger.debug('Reactor observation stream closed cleanly', { reactorId: id });
421
439
  } else {
422
- this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error: String(err) });
440
+ this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error: err });
423
441
  }
424
442
  } finally {
425
443
  // Only retire our own queue: a reconnect can already have replaced it,
@@ -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);
@@ -0,0 +1,64 @@
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 { EventObservationState } from '@cratis/chronicle.contracts';
5
+ import { beforeEach, chai, describe, it } from 'vitest';
6
+ import { eventType } from '../../events/eventTypeDecorator.js';
7
+ import { handles, onceOnly, replay } from '../index.js';
8
+ import { getReactorEventTypes, selectReactorHandler } from '../ReactorDispatcher.js';
9
+
10
+ chai.should();
11
+
12
+ @eventType('selected-handler')
13
+ class AuthorRegistered {}
14
+
15
+ function selected(type: new () => object, state: EventObservationState) {
16
+ return selectReactorHandler(getReactorEventTypes(type, [AuthorRegistered]), type, undefined, 'selected-handler', state)!;
17
+ }
18
+
19
+ describe('when selecting an explicit handler with a replay alternative', () => {
20
+ class Observer {
21
+ @handles(AuthorRegistered)
22
+ @onceOnly()
23
+ notify() {}
24
+
25
+ @replay(AuthorRegistered)
26
+ rebuild() {}
27
+ }
28
+ it('should select the explicit method for live events', () => {
29
+ selected(Observer, EventObservationState.Initial).methodName!.should.equal('notify');
30
+ });
31
+ it('should select the replay alternative even when the live method is onceOnly', () => {
32
+ selected(Observer, EventObservationState.Replay).should.deep.equal({ methodName: 'rebuild', isReplay: true, skipReplay: false });
33
+ });
34
+ });
35
+
36
+ describe('when selecting an explicit handler without a replay alternative', () => {
37
+ class Observer { @handles(AuthorRegistered) notify() {} }
38
+ let handler: ReturnType<typeof selected>;
39
+ beforeEach(() => { handler = selected(Observer, EventObservationState.Replay); });
40
+ it('should use the explicit method during replay too', () => {
41
+ handler.should.deep.equal({ methodName: 'notify', isReplay: true, skipReplay: false });
42
+ });
43
+ });
44
+
45
+ describe('when an explicit handler is onceOnly', () => {
46
+ class Observer { @onceOnly() @handles(AuthorRegistered) notify() {} }
47
+ it('should not skip live delivery', () => {
48
+ selected(Observer, EventObservationState.Initial).skipReplay.should.be.false;
49
+ });
50
+ it('should skip replay delivery', () => {
51
+ selected(Observer, EventObservationState.Replay).skipReplay.should.be.true;
52
+ });
53
+ });
54
+
55
+ describe('when a method declares both live and replay roles', () => {
56
+ class Observer { @handles(AuthorRegistered) @replay(AuthorRegistered) notify() {} }
57
+ it('should reject the ambiguous declaration during discovery', () => {
58
+ (() => getReactorEventTypes(Observer, [AuthorRegistered])).should.throw(/notify.*Observer.*cannot combine @handles and @replay/);
59
+ });
60
+ it('should reject replay above handles too', () => {
61
+ class Reversed { @replay(AuthorRegistered) @handles(AuthorRegistered) notify() {} }
62
+ (() => getReactorEventTypes(Reversed, [AuthorRegistered])).should.throw(/notify.*Reversed.*cannot combine @handles and @replay/);
63
+ });
64
+ });
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { EventContext, EventType, ObservationState, ReplayState, type ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
10
+ import type { IEventStore } from '../../IEventStore.js';
11
+ import { eventType } from '../../events/eventTypeDecorator.js';
12
+ import { handles, reactor, Reactors } from '../index.js';
13
+
14
+ chai.should();
15
+
16
+ @eventType('field-reactor-event')
17
+ class AuthorRegistered {}
18
+ const calls: string[] = [];
19
+ class Base {
20
+ @handles(AuthorRegistered) notify() { calls.push('base'); }
21
+ beginReplay() { calls.push('replay'); }
22
+ endReplay() {}
23
+ }
24
+ @reactor('field-reactor')
25
+ class Derived extends Base {
26
+ override notify = () => { calls.push('field'); };
27
+ }
28
+
29
+ for (const replayState of [ReplayState.REPLAY_STATE_None, ReplayState.BeginReplay]) {
30
+ describe(`when activating a reactor with a field override for replay state ${replayState}`, () => {
31
+ let result: ReactorMessage;
32
+ let disposed: boolean;
33
+ beforeEach(async () => {
34
+ calls.length = 0;
35
+ disposed = false;
36
+ const lifecycle = new ConnectionLifecycle();
37
+ let finish!: () => void;
38
+ const finished = new Promise<void>(resolve => { finish = resolve; });
39
+ async function* stream(queue: AsyncIterable<ReactorMessage>) {
40
+ const messages = queue[Symbol.asyncIterator]();
41
+ await messages.next();
42
+ yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
43
+ EventType: EventType.fromPartial({ Id: 'field-reactor-event', Generation: 1 }), SequenceNumber: 1n
44
+ }) }], Partition: 'author', ReplayState: replayState, InitialState: '' };
45
+ result = (await messages.next()).value!;
46
+ await lifecycle.disconnected(error => { throw error; });
47
+ finish();
48
+ }
49
+ const reactors = new Reactors({ reactors: [Derived], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider,
50
+ { reactors: { observe: stream } } as unknown as ChronicleConnection, 'store', 'tenant', lifecycle, {} as IEventLog,
51
+ undefined, { readModels: {} } as IEventStore, type => ({ instance: new type(), dispose: () => { disposed = true; } }));
52
+ try { await reactors.register(); await finished; } finally { reactors.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!.ExceptionMessages.join(' ').should.contain(
58
+ "Override 'notify' on 'Derived' hides @handles(AuthorRegistered) declared on 'Base'; use a method with @handles instead of an instance field.");
59
+ });
60
+ it('should release the rejected activation lease', () => { disposed.should.be.true; });
61
+ });
62
+ }
@@ -4,7 +4,8 @@
4
4
  import { EventContext as WireContext, EventObservationState, EventType, ObservationState, type ReactorMessage } from '@cratis/chronicle.contracts';
5
5
  import { ReplayState } from '../../../index.js';
6
6
  import { chai, describe, it, vi } from 'vitest';
7
- import { diag, DiagLogLevel, type DiagLogger } from '@opentelemetry/api';
7
+ import type { IChronicleLogger } from '../../../logging/IChronicleLogger.js';
8
+ import type { ChronicleLogEntry } from '../../../logging/ChronicleLogEntry.js';
8
9
  import type { ChronicleConnection } from '../../../connection/index.js';
9
10
  import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
10
11
  import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
@@ -98,7 +99,7 @@ function delivery(events: ReturnType<typeof event>[], replayState = ReplayState.
98
99
  }
99
100
 
100
101
  async function observe(type: Function, batches: ReturnType<typeof delivery>[], namespace = 'tenant-a',
101
- activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void) {
102
+ activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void, logger?: IChronicleLogger) {
102
103
  const acknowledgements: NonNullable<ReactorMessage['Content']>['Value1'][] = [];
103
104
  const append = vi.fn().mockResolvedValue({ isSuccess: appendResult });
104
105
  const appendMany = vi.fn().mockImplementation(async () => {
@@ -122,7 +123,7 @@ async function observe(type: Function, batches: ReturnType<typeof delivery>[], n
122
123
  await lifecycle.disconnected(error => { throw error; });
123
124
  finished();
124
125
  }
125
- const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
126
+ const connection = { logger, reactors: { observe: stream } } as unknown as ChronicleConnection;
126
127
  const artifacts = { reactors: [type], eventTypes: [ActivationEvent] } as unknown as IClientArtifactsProvider;
127
128
  await new Reactors(artifacts, connection, 'store', namespace, lifecycle, eventLog, undefined, store, activator).register();
128
129
  await complete;
@@ -462,16 +463,15 @@ describe('when delivering reactor batches', () => {
462
463
  });
463
464
 
464
465
  it('should log disposal failure without changing successful acknowledgement', async () => {
465
- const errors: string[] = [];
466
- diag.setLogger({ error: (...values: unknown[]) => { errors.push(values.map(String).join(' ')); } } as DiagLogger, DiagLogLevel.ERROR);
467
- try {
468
- const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: async () => { throw new Error('cleanup'); } });
469
- const result = await observe(ActivationReactor, [delivery([event('live', 1n)])], 'tenant-a', activator);
470
- result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
471
- result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
472
- errors.some(value => value.includes('Error disposing activated artifact')).should.be.true;
473
- } finally {
474
- diag.disable();
475
- }
466
+ const entries: ChronicleLogEntry[] = [];
467
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: async () => { throw new Error('private cleanup details'); } });
468
+ const result = await observe(ActivationReactor, [delivery([event('private payload', 1n)])], 'tenant-a', activator,
469
+ true, undefined, { log: entry => entries.push(entry) });
470
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
471
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
472
+ entries.filter(entry => entry.attributes['exception.type'] === 'Error').should.have.lengthOf(1);
473
+ JSON.stringify(entries).should.not.contain('private cleanup details');
474
+ JSON.stringify(entries).should.not.contain('private payload');
475
+ JSON.stringify(entries).should.not.contain('book-1');
476
476
  });
477
477
  });
@@ -0,0 +1,108 @@
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, EventObservationState, EventType, ObservationState, ReplayState, type ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
10
+ import type { IEventStore } from '../../IEventStore.js';
11
+ import { eventType } from '../../events/eventTypeDecorator.js';
12
+ import { handles, reactor, Reactors, replay } from '../index.js';
13
+
14
+ chai.should();
15
+
16
+ @eventType('instance-reactor-event')
17
+ class AuthorRegistered {}
18
+ const calls: string[] = [];
19
+
20
+ @reactor('bound-conventional-reactor')
21
+ class BoundConventional {
22
+ readonly marker = 'bound conventional';
23
+ constructor() { this.authorRegistered = this.authorRegistered.bind(this); }
24
+ authorRegistered() { calls.push(this.marker); }
25
+ }
26
+
27
+ @reactor('bound-explicit-reactor')
28
+ class BoundExplicit {
29
+ readonly marker = 'bound explicit';
30
+ constructor() { this.notify = this.notify.bind(this); }
31
+ @handles(AuthorRegistered)
32
+ notify() { calls.push(this.marker); }
33
+ }
34
+
35
+ class BoundExplicitBase {
36
+ readonly marker = 'twice bound explicit';
37
+ constructor() { this.notify = this.notify.bind(this); }
38
+ @handles(AuthorRegistered)
39
+ notify() { calls.push(this.marker); }
40
+ }
41
+
42
+ @reactor('twice-bound-explicit-reactor')
43
+ class TwiceBoundExplicit extends BoundExplicitBase {
44
+ constructor() { super(); this.notify = this.notify.bind(this); }
45
+ }
46
+
47
+ class Conventional {
48
+ authorRegistered() { calls.push('base'); }
49
+ }
50
+
51
+ @reactor('field-conventional-reactor')
52
+ class FieldConventional extends Conventional {
53
+ override authorRegistered = () => { calls.push('field conventional'); };
54
+ }
55
+
56
+ @reactor('bound-replay-reactor')
57
+ class BoundReplay {
58
+ readonly marker = 'bound replay';
59
+ constructor() { this.restore = this.restore.bind(this); }
60
+ @replay(AuthorRegistered)
61
+ restore() { calls.push(this.marker); }
62
+ }
63
+
64
+ class Replay {
65
+ @replay(AuthorRegistered)
66
+ restore() { calls.push('base replay'); }
67
+ }
68
+
69
+ @reactor('field-replay-reactor')
70
+ class FieldReplay extends Replay {
71
+ override restore = () => { calls.push('field replay'); };
72
+ }
73
+
74
+ for (const { type, expected, observationState } of [
75
+ { type: BoundConventional, expected: 'bound conventional', observationState: EventObservationState.Initial },
76
+ { type: BoundExplicit, expected: 'bound explicit', observationState: EventObservationState.Initial },
77
+ { type: TwiceBoundExplicit, expected: 'twice bound explicit', observationState: EventObservationState.Initial },
78
+ { type: FieldConventional, expected: 'field conventional', observationState: EventObservationState.Initial },
79
+ { type: BoundReplay, expected: 'bound replay', observationState: EventObservationState.Replay },
80
+ { type: FieldReplay, expected: 'field replay', observationState: EventObservationState.Replay }
81
+ ]) {
82
+ describe(`when delivering to the ${expected} reactor handler without an activator`, () => {
83
+ let result: ReactorMessage;
84
+ beforeEach(async () => {
85
+ calls.length = 0;
86
+ const lifecycle = new ConnectionLifecycle();
87
+ let finish!: () => void;
88
+ const finished = new Promise<void>(resolve => { finish = resolve; });
89
+ async function* stream(queue: AsyncIterable<ReactorMessage>) {
90
+ const messages = queue[Symbol.asyncIterator]();
91
+ await messages.next();
92
+ yield { Events: [{ Content: '{}', Context: EventContext.fromPartial({
93
+ EventType: EventType.fromPartial({ Id: 'instance-reactor-event', Generation: 1 }), SequenceNumber: 1n,
94
+ ObservationState: observationState
95
+ }) }], Partition: 'author', ReplayState: ReplayState.REPLAY_STATE_None, InitialState: '' };
96
+ result = (await messages.next()).value!;
97
+ await lifecycle.disconnected(error => { throw error; });
98
+ finish();
99
+ }
100
+ const reactors = new Reactors({ reactors: [type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider,
101
+ { reactors: { observe: stream } } as unknown as ChronicleConnection, 'store', 'tenant', lifecycle, {} as IEventLog,
102
+ undefined, { readModels: {} } as IEventStore);
103
+ try { await reactors.register(); await finished; } finally { reactors.dispose(); }
104
+ });
105
+ it('should invoke the instance handler', () => { calls.should.deep.equal([expected]); });
106
+ it('should complete delivery', () => { result.Content!.Value1!.State.should.equal(ObservationState.Success); });
107
+ });
108
+ }
@@ -0,0 +1,77 @@
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, EventObservationState, EventType, type ReactorMessage } 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 { IEventLog } from '../../eventSequences/IEventLog.js';
10
+ import { eventType } from '../../events/eventTypeDecorator.js';
11
+ import { handles, onceOnly, reactor, Reactors, replay } from '../index.js';
12
+
13
+ chai.should();
14
+
15
+ @eventType('legacy-delivered-handler')
16
+ class AuthorRegistered { constructor(readonly name = '') {} }
17
+
18
+ const calls: string[] = [];
19
+ @reactor('legacy-delivery')
20
+ class AuthorReactor {
21
+ @handles(AuthorRegistered)
22
+ @onceOnly()
23
+ notify(event: AuthorRegistered) { calls.push(`live:${event.name}`); }
24
+ }
25
+ @reactor('legacy-delivery-with-replay')
26
+ class ReplayAuthorReactor extends AuthorReactor {
27
+ @replay(AuthorRegistered)
28
+ rebuild(event: AuthorRegistered) { calls.push(`replay:${event.name}`); }
29
+ }
30
+
31
+ for (const [type, expected] of [
32
+ [AuthorReactor, ['live:Ada']],
33
+ [ReplayAuthorReactor, ['live:Ada', 'replay:Ada']]
34
+ ] as const) {
35
+ describe(`when observing live and replay events with explicit legacy handlers on ${type.name}`, () => {
36
+ let registration: ReactorMessage;
37
+ let result: ReactorMessage;
38
+ beforeEach(async () => {
39
+ calls.length = 0;
40
+ const lifecycle = new ConnectionLifecycle();
41
+ let finish!: () => void;
42
+ const finished = new Promise<void>(resolve => { finish = resolve; });
43
+ async function* stream(queue: AsyncIterable<ReactorMessage>) {
44
+ const messages = queue[Symbol.asyncIterator]();
45
+ registration = (await messages.next()).value!;
46
+ yield {
47
+ Events: [EventObservationState.Initial, EventObservationState.Replay].map((state, index) => ({
48
+ Context: EventContext.fromPartial({
49
+ EventType: EventType.fromPartial({ Id: 'legacy-delivered-handler', Generation: 1 }),
50
+ SequenceNumber: BigInt(index + 1), ObservationState: state
51
+ }),
52
+ Content: JSON.stringify({ name: 'Ada' })
53
+ })),
54
+ Partition: 'author', ReplayState: 0, InitialState: ''
55
+ };
56
+ result = (await messages.next()).value!;
57
+ await lifecycle.disconnected(error => { throw error; });
58
+ finish();
59
+ }
60
+ const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
61
+ const artifacts = { reactors: [type], eventTypes: [AuthorRegistered] } as unknown as IClientArtifactsProvider;
62
+ const reactors = new Reactors(artifacts, connection, 'store', 'tenant', lifecycle, {} as IEventLog);
63
+ try {
64
+ await reactors.register();
65
+ await finished;
66
+ } finally { reactors.dispose(); }
67
+ });
68
+ it('should subscribe to the explicitly named event type', () => {
69
+ registration.Content!.Value0!.Reactor!.EventTypes.map(entry => entry.EventType!.Id)
70
+ .should.deep.equal(['legacy-delivered-handler']);
71
+ });
72
+ it('should invoke only the appropriate handlers', () => { calls.should.deep.equal(expected); });
73
+ it('should acknowledge both deliveries', () => {
74
+ result.Content!.Value1!.LastSuccessfulObservation.should.equal(2n);
75
+ });
76
+ });
77
+ }