@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
@@ -1,7 +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
- import { diag } from '@opentelemetry/api';
4
+ import { createLogger } from '../logging/createLogger.js';
5
5
  import { IClientArtifactsProvider } from '../artifacts/index.js';
6
6
  import { ChronicleConnection } from '../connection/index.js';
7
7
  import { ensureCommandSuccess } from '../connection/callResults.js';
@@ -27,7 +27,7 @@ interface SeedingEntry {
27
27
  * with the Chronicle Kernel.
28
28
  */
29
29
  export class EventSeeding implements IEventSeeding {
30
- private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/EventSeeding' });
30
+ private readonly _logger: ReturnType<typeof createLogger>;
31
31
  private readonly _entries: SeedingEntry[] = [];
32
32
 
33
33
  /**
@@ -40,7 +40,9 @@ export class EventSeeding implements IEventSeeding {
40
40
  private readonly _eventStoreName: string,
41
41
  private readonly _connection: ChronicleConnection,
42
42
  private readonly _clientArtifacts: IClientArtifactsProvider
43
- ) {}
43
+ ) {
44
+ this._logger = createLogger('@cratis/chronicle/EventSeeding', _connection.logger);
45
+ }
44
46
 
45
47
  /** @inheritdoc */
46
48
  for<TEvent extends object>(eventSourceId: string, events: Iterable<TEvent>): IEventSeedingBuilder {
@@ -69,7 +71,7 @@ export class EventSeeding implements IEventSeeding {
69
71
  } catch (error) {
70
72
  this._logger.warn('Failed to activate or execute event seeder - skipping seeder', {
71
73
  seederType: seederType.name,
72
- error: String(error)
74
+ error
73
75
  });
74
76
  }
75
77
  }
@@ -0,0 +1,8 @@
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
+ /** Per-client privacy settings; instrumentation itself is always available. */
5
+ export interface ChronicleTelemetryOptions {
6
+ /** Absent means no event source identifiers. HMAC uses SHA-256 and a deployment-owned key. */
7
+ eventSourceId?: { mode: 'raw' } | { mode: 'hmac'; key: Uint8Array };
8
+ }
@@ -0,0 +1,36 @@
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 { beforeAll, beforeEach, afterAll } from 'vitest';
5
+ import { context, metrics, propagation, trace } from '@opentelemetry/api';
6
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
7
+ import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
8
+ import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
9
+ import { InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader, AggregationTemporality } from '@opentelemetry/sdk-metrics';
10
+
11
+ /** In-process providers only; no export outside the spec process. */
12
+ export function telemetrySession() {
13
+ const spans = new InMemorySpanExporter();
14
+ const traces = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(spans)] });
15
+ const metricExporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
16
+ const reader = new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: 60000 });
17
+ const meters = new MeterProvider({ readers: [reader] });
18
+ const contextManager = new AsyncLocalStorageContextManager();
19
+ beforeAll(() => {
20
+ context.setGlobalContextManager(contextManager.enable());
21
+ trace.setGlobalTracerProvider(traces);
22
+ metrics.setGlobalMeterProvider(meters);
23
+ propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
24
+ });
25
+ beforeEach(() => spans.reset());
26
+ afterAll(async () => {
27
+ await traces.shutdown();
28
+ await meters.shutdown();
29
+ context.disable();
30
+ trace.disable();
31
+ metrics.disable();
32
+ propagation.disable();
33
+ contextManager.disable();
34
+ });
35
+ return { spans, traces, reader };
36
+ }
@@ -0,0 +1,29 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { vi } from 'vitest';
5
+ import { EventSequence } from '../../../eventSequences/EventSequence.js';
6
+ import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
9
+ import type { IUnitOfWorkManager } from '../../../transactions/IUnitOfWorkManager.js';
10
+ import type { ChronicleTelemetryOptions } from '../../ChronicleTelemetryOptions.js';
11
+
12
+ export class Recorded { constructor(readonly value = 'sensitive payload') {} }
13
+ eventType('telemetry-recorded')(Recorded);
14
+
15
+ export function eventSequence(telemetry?: ChronicleTelemetryOptions) {
16
+ const services = {
17
+ append: vi.fn().mockResolvedValue({ Response: { SequenceNumber: 42n } }),
18
+ appendManyForEventSources: vi.fn().mockResolvedValue({ Response: { SequenceNumbers: [42n, 43n] } }),
19
+ tailSequenceNumber: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: { SequenceNumber: 42n } }),
20
+ hasEventsForEventSourceId: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: { HasEvents: true } }),
21
+ forEventSourceIdAndEventTypes: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: [] }),
22
+ fromSequenceNumber: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: [] }),
23
+ redact: vi.fn().mockResolvedValue({}),
24
+ redactForEventSource: vi.fn().mockResolvedValue({})
25
+ };
26
+ const connection = { eventSequences: services } as unknown as ChronicleConnection;
27
+ return { services, sequence: new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
28
+ {} as IUnitOfWorkManager, undefined, telemetry) };
29
+ }
@@ -0,0 +1,58 @@
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 { SpanKind, context, propagation } from '@opentelemetry/api';
6
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
7
+ import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
8
+ import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
9
+ import { clientVersion } from '../../../connection/clientVersion.js';
10
+ import { CorrelationId, correlationIdManager } from '../../../correlation/index.js';
11
+
12
+ const should = chai.should();
13
+ const telemetry = telemetrySession();
14
+ const correlation = '95bd1c83-de97-45f7-84a0-fb462af14d0c';
15
+
16
+ describe('when appending with a business correlation override', () => {
17
+ let span: ReadableSpan;
18
+ let injectedCorrelation: string | undefined;
19
+ beforeEach(async () => {
20
+ const { sequence, services } = eventSequence();
21
+ services.append.mockImplementation(async () => {
22
+ await Promise.resolve();
23
+ injectedCorrelation = propagation.getBaggage(context.active())?.getEntry('cratis.correlation_id')?.value;
24
+ return { Response: { SequenceNumber: 42n } };
25
+ });
26
+ await correlationIdManager.run(new CorrelationId('outer'), () => sequence.append('sensitive-id', new Recorded(), { correlationId: correlation }));
27
+ span = telemetry.spans.getFinishedSpans()[0];
28
+ });
29
+ it('should produce one client span with the unchanged operation name', () => {
30
+ telemetry.spans.getFinishedSpans().should.have.lengthOf(1);
31
+ span.kind.should.equal(SpanKind.CLIENT);
32
+ span.name.should.equal('chronicle.event_sequences.append');
33
+ });
34
+ it('should use the versioned shared instrumentation scope', () => {
35
+ span.instrumentationScope.name.should.equal('Cratis.Chronicle.Client');
36
+ should.equal(span.instrumentationScope.version, clientVersion);
37
+ });
38
+ it('should emit both registries on the same span', () => {
39
+ span.attributes.should.include({
40
+ 'cratis.event_store.name': 'store', 'chronicle.event_store': 'store',
41
+ 'cratis.event_store.namespace': 'namespace', 'chronicle.namespace': 'namespace',
42
+ 'cratis.event_sequence.id': 'event-log', 'chronicle.event_sequence_id': 'event-log',
43
+ 'cratis.event_type.id': 'telemetry-recorded', 'chronicle.event_type_id': 'telemetry-recorded',
44
+ 'cratis.event_type.generation': 1, 'chronicle.event_type_generation': 1,
45
+ 'cratis.event_sequence.number': 42, 'chronicle.sequence_number': '42'
46
+ });
47
+ });
48
+ it('should scope the resolved correlation consistently for propagation', () => {
49
+ should.equal(span.attributes['cratis.correlation_id'], correlation);
50
+ should.equal(injectedCorrelation, correlation);
51
+ should.equal(correlationIdManager.scoped, undefined);
52
+ });
53
+ it('should not record identifiers or payloads by default', () => {
54
+ JSON.stringify(span.attributes).should.not.contain('sensitive');
55
+ should.equal(span.attributes['cratis.event_source.id'], undefined);
56
+ should.equal(span.attributes['chronicle.event_source_id'], undefined);
57
+ });
58
+ });
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { chai, describe, it } from 'vitest';
5
+ import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
6
+
7
+ chai.should();
8
+ describe('when appending without an OpenTelemetry SDK', () => {
9
+ it('should preserve append behavior without telemetry setup', async () => {
10
+ const { sequence, services } = eventSequence();
11
+ const result = await sequence.append('source', new Recorded());
12
+ result.sequenceNumber.value.should.equal(42n);
13
+ services.append.mock.calls.should.have.lengthOf(1);
14
+ });
15
+ });
@@ -0,0 +1,15 @@
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 { readFileSync } from 'node:fs';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { WellKnownTelemetryNames } from '../../../WellKnownTelemetryNames.js';
7
+ import { telemetryReference } from '../../../scripts/telemetry-reference.mjs';
8
+
9
+ chai.should();
10
+ describe('when publishing the telemetry reference', () => {
11
+ it('should keep the documented registry identical to the public constants', () => {
12
+ const documentation = readFileSync(new URL('../../../../Documentation/observability.md', import.meta.url), 'utf8');
13
+ documentation.should.contain(telemetryReference(WellKnownTelemetryNames));
14
+ });
15
+ });
@@ -0,0 +1,34 @@
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 { SpanStatusCode } from '@opentelemetry/api';
6
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
7
+ import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
8
+ import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
9
+
10
+ chai.should();
11
+ const telemetry = telemetrySession();
12
+
13
+ describe('when an append fails with sensitive exception details', () => {
14
+ const failure = new TypeError('sensitive token in error message');
15
+ let span: ReadableSpan;
16
+ let thrown: unknown;
17
+ beforeEach(async () => {
18
+ const { sequence, services } = eventSequence();
19
+ services.append.mockRejectedValue(failure);
20
+ try { await sequence.append('private', new Recorded()); } catch (error) { thrown = error; }
21
+ span = telemetry.spans.getFinishedSpans()[0];
22
+ });
23
+ it('should rethrow the original failure', () => { (thrown === failure).should.be.true; });
24
+ it('should record only failure types and error status', () => {
25
+ span.status.should.deep.equal({ code: SpanStatusCode.ERROR });
26
+ span.attributes.should.include({ 'error.type': 'TypeError' });
27
+ span.events.should.have.lengthOf(1);
28
+ span.events[0].attributes!.should.deep.equal({ 'exception.type': 'TypeError' });
29
+ });
30
+ it('should never export the exception message or stack', () => {
31
+ JSON.stringify({ attributes: span.attributes, status: span.status, events: span.events }).should.not.contain('sensitive');
32
+ span.events[0].attributes!.should.not.have.property('exception.stacktrace');
33
+ });
34
+ });
@@ -0,0 +1,92 @@
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 { createHmac } from 'node:crypto';
5
+ import { beforeEach, chai, describe, it } from 'vitest';
6
+ import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
7
+ import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
8
+ import { EventSequenceNumber } from '../../../eventSequences/EventSequenceNumber.js';
9
+ import { ChronicleOptions } from '../../../ChronicleOptions.js';
10
+ import type { ChronicleTelemetryOptions } from '../../ChronicleTelemetryOptions.js';
11
+
12
+ const should = chai.should();
13
+ const telemetry = telemetrySession();
14
+ const source = 'sensitive-source';
15
+ const key = new Uint8Array([10, 20, 30, 40]);
16
+
17
+ for (const [label, policy, expected] of [
18
+ ['absent', undefined, undefined],
19
+ ['raw', { eventSourceId: { mode: 'raw' } }, source],
20
+ ['hmac', { eventSourceId: { mode: 'hmac', key } }, createHmac('sha256', key).update(source).digest('hex')]
21
+ ] as const) {
22
+ describe(`when recording event source identifiers with ${label} privacy`, () => {
23
+ beforeEach(async () => {
24
+ const { sequence, services } = eventSequence(policy as ChronicleTelemetryOptions | undefined);
25
+ await sequence.append(source, new Recorded());
26
+ await sequence.appendMany(source, [new Recorded(), new Recorded()]);
27
+ await sequence.appendMany([{ eventSourceId: source, event: new Recorded() }, { eventSourceId: source, event: new Recorded() }]);
28
+ await sequence.getTailSequenceNumber(source);
29
+ await sequence.hasEventsFor(source);
30
+ await sequence.getForEventSourceIdAndEventTypes(source, [Recorded]);
31
+ await sequence.getFromSequenceNumber(new EventSequenceNumber(1n), source);
32
+ await sequence.redactForEventSource(source, 'private reason');
33
+ services.append.mock.calls[0][0].EventSourceId.should.equal(source);
34
+ });
35
+ it('should apply the policy equally to both names on every operation', () => {
36
+ telemetry.spans.getFinishedSpans().should.have.lengthOf(8);
37
+ for (const span of telemetry.spans.getFinishedSpans()) {
38
+ should.equal(span.attributes['cratis.event_source.id'], expected);
39
+ should.equal(span.attributes['chronicle.event_source_id'], expected);
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ describe('when a validated telemetry policy is later mutated by an untyped caller', () => {
46
+ beforeEach(async () => {
47
+ const options = ChronicleOptions.development({ telemetry: { eventSourceId: { mode: 'hmac', key } } });
48
+ Object.assign(options.telemetry!.eventSourceId!, { key: undefined });
49
+ const { sequence, services } = eventSequence(options.telemetry);
50
+ await sequence.append(source, new Recorded());
51
+ await sequence.getTailSequenceNumber(source);
52
+ await sequence.redactForEventSource(source, 'reason');
53
+ services.append.mock.calls.should.have.lengthOf(1);
54
+ services.tailSequenceNumber.mock.calls.should.have.lengthOf(1);
55
+ services.redactForEventSource.mock.calls.should.have.lengthOf(1);
56
+ });
57
+ it('should omit the failed enrichment without failing business calls or leaving spans open', () => {
58
+ telemetry.spans.getFinishedSpans().should.have.lengthOf(3);
59
+ for (const span of telemetry.spans.getFinishedSpans()) {
60
+ span.attributes.should.not.have.property('cratis.event_source.id');
61
+ span.attributes.should.not.have.property('chronicle.event_source_id');
62
+ }
63
+ });
64
+ });
65
+
66
+ describe('when appending a batch with multiple event sources', () => {
67
+ beforeEach(async () => {
68
+ await eventSequence({ eventSourceId: { mode: 'raw' } }).sequence.appendMany([
69
+ { eventSourceId: 'first', event: new Recorded() }, { eventSourceId: 'second', event: new Recorded() }
70
+ ]);
71
+ });
72
+ it('should record the count without an identifier array', () => {
73
+ const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
74
+ attributes.should.include({ 'cratis.event.count': 2, 'chronicle.events_count': 2 });
75
+ should.equal(attributes['cratis.event_source.id'], undefined);
76
+ should.equal(attributes['chronicle.event_source_id'], undefined);
77
+ });
78
+ });
79
+
80
+ describe('when clients use different identifier policies concurrently', () => {
81
+ beforeEach(async () => {
82
+ await Promise.all([
83
+ eventSequence().sequence.append(source, new Recorded()),
84
+ eventSequence({ eventSourceId: { mode: 'raw' } }).sequence.append(source, new Recorded()),
85
+ eventSequence({ eventSourceId: { mode: 'hmac', key } }).sequence.append(source, new Recorded())
86
+ ]);
87
+ });
88
+ it('should keep each policy local to its client', () => {
89
+ const identifiers = telemetry.spans.getFinishedSpans().map(span => span.attributes['cratis.event_source.id']);
90
+ identifiers.should.have.members([undefined, source, createHmac('sha256', key).update(source).digest('hex')]);
91
+ });
92
+ });
@@ -0,0 +1,40 @@
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 { telemetrySession } from '../given/a_telemetry_session.fixture.js';
6
+ import { ChronicleMetrics } from '../../../Metrics.js';
7
+ import { clientVersion } from '../../../connection/clientVersion.js';
8
+ import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
9
+
10
+ chai.should();
11
+ const telemetry = telemetrySession();
12
+ describe('when recording metrics after installing the SDK', () => {
13
+ let scope: ScopeMetrics;
14
+ beforeEach(async () => {
15
+ // Metrics was imported before provider installation: recording must still reach the SDK.
16
+ const attributes = { 'chronicle.event_store': 'store', 'chronicle.namespace': 'namespace',
17
+ 'chronicle.event_sequence_id': 'event-log', 'chronicle.events_count': 237 };
18
+ ChronicleMetrics.batchAppendsPerformed.add(1, attributes);
19
+ ChronicleMetrics.appendManyDuration.record(250, attributes);
20
+ scope = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics[0];
21
+ });
22
+ it('should use the shared scope and installed version', () => {
23
+ scope.scope.should.include({ name: 'Cratis.Chronicle.Client', version: clientVersion });
24
+ });
25
+ it('should retain legacy milliseconds while emitting seconds', () => {
26
+ const legacy = scope.metrics.find(metric => metric.descriptor.name === 'chronicle.events.append_many_duration')!;
27
+ const shared = scope.metrics.find(metric => metric.descriptor.name === 'cratis.chronicle.event_sequence.append_many_duration')!;
28
+ legacy.descriptor.unit.should.equal('ms');
29
+ shared.descriptor.unit.should.equal('s');
30
+ legacy.dataPoints[0].value.should.include({ min: 250, max: 250 });
31
+ shared.dataPoints[0].value.should.include({ min: 0.25, max: 0.25 });
32
+ });
33
+ it('should exclude batch size from only the new metric dimensions', () => {
34
+ const legacy = scope.metrics.find(metric => metric.descriptor.name === 'chronicle.events.batch_appends')!;
35
+ const shared = scope.metrics.find(metric => metric.descriptor.name === 'cratis.chronicle.event_sequence.batch_appends')!;
36
+ legacy.dataPoints[0].attributes.should.have.property('chronicle.events_count', 237);
37
+ shared.dataPoints[0].attributes.should.deep.equal({ 'cratis.event_store.name': 'store',
38
+ 'cratis.event_store.namespace': 'namespace', 'cratis.event_sequence.id': 'event-log' });
39
+ });
40
+ });
@@ -0,0 +1,23 @@
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 { telemetrySession } from '../given/a_telemetry_session.fixture.js';
6
+ import { eventSequence } from '../given/an_event_sequence.fixture.js';
7
+
8
+ const should = chai.should();
9
+ const telemetry = telemetrySession();
10
+ for (const [number, expected] of [[42n, 42], [9007199254740991n, Number.MAX_SAFE_INTEGER], [9007199254740992n, undefined], [18446744073709551615n, undefined]] as const) {
11
+ describe(`when reading sequence number ${number}`, () => {
12
+ beforeEach(async () => {
13
+ const { sequence, services } = eventSequence();
14
+ services.tailSequenceNumber.mockResolvedValue({ IsAuthorized: true, Data: { SequenceNumber: number } });
15
+ await sequence.getTailSequenceNumber();
16
+ });
17
+ it('should keep the legacy string and emit only exactly representable shared integers', () => {
18
+ const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
19
+ should.equal(attributes['cratis.event_sequence.number'], expected);
20
+ should.equal(attributes['chronicle.sequence_number'], number.toString());
21
+ });
22
+ });
23
+ }
@@ -0,0 +1,25 @@
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 { context, propagation, SpanKind, type Span } from '@opentelemetry/api';
5
+ import { ChronicleTracer } from '../Tracing.js';
6
+ import { correlationIdManager } from '../correlation/index.js';
7
+ import { CorrelationId } from '../correlation/CorrelationId.js';
8
+ import { WellKnownTelemetryNames } from '../WellKnownTelemetryNames.js';
9
+
10
+ /** Opens one client span, scoping the resolved business correlation without generating a new one. */
11
+ export function observeOperation<T>(name: string, action: (span: Span) => Promise<T>, correlationId?: { toString(): string }): Promise<T> {
12
+ const resolved = correlationId ? new CorrelationId(correlationId.toString()) : correlationIdManager.scoped;
13
+ const run = () => {
14
+ const active = context.active();
15
+ const key = WellKnownTelemetryNames.attributes.correlationId;
16
+ const correlation = resolved?.toString() ?? propagation.getBaggage(active)?.getEntry(key)?.value;
17
+ const parent = correlation ? propagation.setBaggage(active,
18
+ (propagation.getBaggage(active) ?? propagation.createBaggage()).setEntry(key, { value: correlation })) : active;
19
+ return ChronicleTracer.startActiveSpan(name, {
20
+ kind: SpanKind.CLIENT,
21
+ attributes: correlation ? { [key]: correlation } : {}
22
+ }, parent, action);
23
+ };
24
+ return resolved ? correlationIdManager.run(resolved, run) : run();
25
+ }
@@ -0,0 +1,10 @@
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 { context, ROOT_CONTEXT } from '@opentelemetry/api';
5
+ import { correlationIdManager } from '../correlation/index.js';
6
+
7
+ /** Starts client-owned work without retaining the initiating request's trace or business correlation. */
8
+ export function runInBackgroundContext<T>(action: () => T): T {
9
+ return context.with(ROOT_CONTEXT, () => correlationIdManager.runWithout(action));
10
+ }
@@ -0,0 +1,49 @@
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 { createHmac } from 'node:crypto';
5
+ import { SpanStatusCode, type Span, type AttributeValue } from '@opentelemetry/api';
6
+ import { WellKnownTelemetryNames as names } from '../WellKnownTelemetryNames.js';
7
+ import type { ChronicleTelemetryOptions } from './ChronicleTelemetryOptions.js';
8
+
9
+ /** Records a shared attribute and its legacy alias on the same span. */
10
+ export function setTelemetryAttribute(span: Span, name: keyof typeof names.legacyAttributes, value: AttributeValue): void {
11
+ span.setAttribute(names.legacyAttributes[name], value);
12
+ if (name in names.attributes) {
13
+ span.setAttribute(names.attributes[name as keyof typeof names.attributes], value);
14
+ }
15
+ }
16
+
17
+ /** Preserves the legacy string; records the shared integer only when exactly representable. */
18
+ export function setSequenceNumber(span: Span, value: bigint): void {
19
+ span.setAttribute(names.legacyAttributes.sequenceNumber, value.toString());
20
+ const number = Number(value);
21
+ if (Number.isSafeInteger(number)) span.setAttribute(names.attributes.sequenceNumber, number);
22
+ }
23
+
24
+ /** Applies the same privacy policy to both event source identifier names. */
25
+ export function setEventSourceId(span: Span, value: string | undefined, options?: ChronicleTelemetryOptions): void {
26
+ try {
27
+ const policy = options?.eventSourceId;
28
+ if (value === undefined || !policy || !span.isRecording()) return;
29
+ if (policy.mode !== 'raw' && policy.mode !== 'hmac') return;
30
+ const recorded = policy.mode === 'raw' ? value : createHmac('sha256', policy.key).update(value).digest('hex');
31
+ setTelemetryAttribute(span, 'eventSourceId', recorded);
32
+ } catch {
33
+ // Options are validated at construction, but callers can mutate them later.
34
+ // Optional telemetry enrichment must never prevent an RPC or span completion.
35
+ }
36
+ }
37
+
38
+ /** Classifies failures without serializing arbitrary thrown values, messages or stacks. */
39
+ export function exceptionType(error: unknown): string {
40
+ return error instanceof Error ? error.name : typeof error;
41
+ }
42
+
43
+ /** Records failure type only; preserves the original exception for the caller. */
44
+ export function recordSafeException(span: Span, error: unknown): void {
45
+ const type = exceptionType(error);
46
+ span.setStatus({ code: SpanStatusCode.ERROR });
47
+ span.setAttribute(names.attributes.errorType, type);
48
+ span.addEvent('exception', { [names.attributes.exceptionType]: type });
49
+ }
@@ -21,6 +21,7 @@ import { EventStoreNamespaceName } from '../EventStoreNamespaceName.js';
21
21
  import type { IEventStore } from '../IEventStore.js';
22
22
  import type { IReadModels } from '../readModels/IReadModels.js';
23
23
  import { getReactorEventTypes, invokeReactorHandler, selectReactorHandler } from '../reactors/ReactorDispatcher.js';
24
+ import { createHandlerInstanceValidator } from '../observation/createHandlerInstanceValidator.js';
24
25
  import { getReactorMetadata } from '../reactors/reactor.js';
25
26
  import { normalizeReactorSideEffects } from '../reactors/ReactorSideEffects.js';
26
27
  import type { ReactorServices } from '../reactors/ReactorServices.js';
@@ -40,6 +41,7 @@ export class ReactorScenario {
40
41
  private readonly _entries: ReturnType<typeof getReactorEventTypes>;
41
42
  private readonly _eventTypes: readonly Constructor[];
42
43
  private readonly _instance?: Record<string, Function>;
44
+ private readonly _validateInstance: (instance: object) => void;
43
45
  private readonly _store: IEventStore;
44
46
  private readonly _controller = new AbortController();
45
47
  private readonly _effects: RecordedReactorSideEffect[] = [];
@@ -76,9 +78,12 @@ export class ReactorScenario {
76
78
  eventTypeMigrations: eventTypeMigrations === undefined ? undefined : [...eventTypeMigrations]
77
79
  } });
78
80
  this._entries = getReactorEventTypes(_reactor, this._eventTypes);
81
+ this._validateInstance = createHandlerInstanceValidator(_reactor,
82
+ this._entries.flatMap(entry => [entry.methodName, entry.replayMethodName].filter((name): name is string => !!name)));
79
83
  // Every unsupported event-sequence operation is latched at its source, whichever result or stream carries it.
80
84
  observeUnsupportedOperations(this._events.eventSequence, error => { this.latch(error); });
81
85
  this._instance = _options.artifactActivator ? undefined : new (_reactor as new () => Record<string, Function>)();
86
+ if (this._instance) this._validateInstance(this._instance);
82
87
  this._store = _options.servicesEventStore ?? this.scenarioStore();
83
88
  this.given = { forEventSource: id => ({ events: (...events) => this.deliver(id, events, true) }) };
84
89
  this.when = { forEventSource: id => ({ events: (...events) => this.deliver(id, events, false) }) };
@@ -246,7 +251,10 @@ export class ReactorScenario {
246
251
  artifactId: getReactorMetadata(this._reactor)!.id.value, eventStore: this._store,
247
252
  readModels: services.readModels, eventSequenceId: 'event-log', partition: sourceId,
248
253
  signal: services.signal, delivery: ArtifactDelivery.Events, eventContext: first.context },
249
- this._options.artifactActivator, process);
254
+ this._options.artifactActivator, artifact => {
255
+ this._validateInstance(artifact.instance);
256
+ return process(artifact);
257
+ });
250
258
  } else {
251
259
  await process({ instance: this._instance ?? {} });
252
260
  }
@@ -18,6 +18,7 @@ export class ReducerReadModelProcessor<TReadModel extends object> implements IRe
18
18
  async process(events: readonly ScenarioEvent[]): Promise<Map<string, ReadModelState<TReadModel>>> {
19
19
  const results = new Map<string, ReadModelState<TReadModel>>();
20
20
  const reducer = new (this._reducerType as new () => Record<string, Function>)();
21
+ this._dispatcher.validateInstance(reducer);
21
22
  for (const event of events) {
22
23
  const handler = this._dispatcher.handlerFor(event.context.eventType.id.value);
23
24
  if (!handler) continue;
@@ -0,0 +1,100 @@
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 { field } from '@cratis/fundamentals';
5
+ import { beforeEach, chai, describe, it } from 'vitest';
6
+ import { eventType, handles, onceOnly, reactor, type EventContext, type ReactorServices } from '../../index.js';
7
+ import { ReactorScenario } from '../index.js';
8
+
9
+ chai.should();
10
+
11
+ @eventType('scenario-explicit-author')
12
+ class AuthorRegistered {
13
+ @field(String) name: string;
14
+ constructor(name: string) { this.name = name; }
15
+ }
16
+ @eventType('scenario-explicit-notification')
17
+ class AuthorNotified {
18
+ @field(String) name: string;
19
+ constructor(name: string) { this.name = name; }
20
+ }
21
+
22
+ const calls: { name: string; source: string; method: string }[] = [];
23
+ @reactor('explicit-scenario-handler')
24
+ class AuthorReactor {
25
+ @onceOnly()
26
+ @handles(AuthorRegistered)
27
+ notify(event: AuthorRegistered, context: EventContext, services: ReactorServices) {
28
+ (services.readModels === services.eventStore.readModels).should.be.true;
29
+ calls.push({ name: event.name, source: context.eventSourceId, method: 'notify' });
30
+ return new AuthorNotified(event.name);
31
+ }
32
+ }
33
+
34
+ const options = { artifacts: { eventTypes: [AuthorRegistered, AuthorNotified] }, constraints: 'disabled' as const };
35
+
36
+ describe('when delivering a live event to an explicitly named onceOnly handler in ReactorScenario', () => {
37
+ let scenario: ReactorScenario;
38
+ beforeEach(async () => {
39
+ calls.length = 0;
40
+ scenario = new ReactorScenario(AuthorReactor, options);
41
+ await scenario.when.forEventSource('author-1').events(new AuthorRegistered('Ada'));
42
+ });
43
+ it('should invoke the method with the event and context', () => {
44
+ calls.should.deep.equal([{ name: 'Ada', source: 'author-1', method: 'notify' }]);
45
+ });
46
+ it('should process returned side effects', () => {
47
+ scenario.shouldHaveProduced(AuthorNotified, event => event.name === 'Ada');
48
+ });
49
+ it('should complete delivery', () => {
50
+ scenario.results[0].completed.should.be.true;
51
+ });
52
+ });
53
+
54
+ describe('when ReactorScenario has an unregistered explicit handler', () => {
55
+ it('should reject it before constructing the reactor', () => {
56
+ let constructed = false;
57
+ @reactor('invalid-explicit-scenario')
58
+ class Invalid {
59
+ constructor() { constructed = true; }
60
+ @handles(AuthorRegistered) notify() {}
61
+ }
62
+ (() => new ReactorScenario(Invalid, { ...options, artifacts: { eventTypes: [AuthorNotified] } }))
63
+ .should.throw(/notify.*AuthorRegistered.*no registered event type/);
64
+ constructed.should.be.false;
65
+ });
66
+ });
67
+
68
+ @reactor('field-shadowing-scenario')
69
+ class FieldReactor extends AuthorReactor {
70
+ override notify = () => new AuthorNotified('hidden');
71
+ }
72
+
73
+ describe('when an instance field hides an inherited ReactorScenario handler', () => {
74
+ it('should reject the field after constructing the reactor', () => {
75
+ (() => new ReactorScenario(FieldReactor, options)).should.throw(
76
+ "Override 'notify' on 'FieldReactor' hides @handles(AuthorRegistered) declared on 'AuthorReactor'; use a method with @handles instead of an instance field.");
77
+ });
78
+ it('should reject an activator-created field before delivery and dispose its lease', async () => {
79
+ let disposed = false;
80
+ const scenario = new ReactorScenario(FieldReactor, { ...options, artifactActivator: type => ({
81
+ instance: new type(), dispose: () => { disposed = true; }
82
+ }) });
83
+ const [result] = await Promise.allSettled([scenario.when.forEventSource('author').events(new AuthorRegistered('Ada'))]);
84
+ result.status.should.equal('rejected');
85
+ if (result.status === 'rejected') (result.reason as Error).message.should.match(/Override 'notify'.*use a method/);
86
+ scenario.produced.should.have.lengthOf(0);
87
+ disposed.should.be.true;
88
+ });
89
+ });
90
+
91
+ describe('when ReactorScenario has duplicate explicit and conventional handlers', () => {
92
+ @reactor('duplicate-explicit-scenario')
93
+ class Invalid {
94
+ @handles(AuthorRegistered) notify() {}
95
+ authorRegistered() {}
96
+ }
97
+ it('should reject the ambiguous event delivery', () => {
98
+ (() => new ReactorScenario(Invalid, options)).should.throw(/multiple handlers.*scenario-explicit-author/);
99
+ });
100
+ });