@cratis/chronicle 6.33.0 → 6.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/ChronicleClient.ts +43 -35
  2. package/ChronicleOptions.ts +27 -1
  3. package/EventStore.ts +22 -12
  4. package/Metrics.ts +70 -96
  5. package/Tracing.ts +4 -2
  6. package/WellKnownTelemetryNames.ts +67 -0
  7. package/artifacts/validateArtifactSchemas.spec.ts +1 -1
  8. package/artifacts/withActivatedArtifact.ts +7 -5
  9. package/connection/ChronicleConnection.ts +18 -6
  10. package/connection/CompatibilityPreflight.ts +2 -1
  11. package/connection/KernelKeepAlive.ts +13 -7
  12. package/connection/TokenProvider.ts +3 -1
  13. package/connection/TraceContextMiddleware.ts +30 -0
  14. package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
  15. package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
  16. package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
  17. package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
  18. package/correlation/CorrelationIdManager.ts +10 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +45 -35
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +8 -0
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +19 -1
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +3 -1
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +22 -12
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/Metrics.d.ts +23 -64
  31. package/dist/Metrics.d.ts.map +1 -1
  32. package/dist/Metrics.js +62 -92
  33. package/dist/Metrics.js.map +1 -1
  34. package/dist/Tracing.d.ts +1 -1
  35. package/dist/Tracing.d.ts.map +1 -1
  36. package/dist/Tracing.js +4 -2
  37. package/dist/Tracing.js.map +1 -1
  38. package/dist/WellKnownTelemetryNames.d.ts +65 -0
  39. package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
  40. package/dist/WellKnownTelemetryNames.js +67 -0
  41. package/dist/WellKnownTelemetryNames.js.map +1 -0
  42. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  43. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  44. package/dist/artifacts/withActivatedArtifact.js +5 -4
  45. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  46. package/dist/connection/ChronicleConnection.d.ts +5 -0
  47. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  48. package/dist/connection/ChronicleConnection.js +13 -6
  49. package/dist/connection/ChronicleConnection.js.map +1 -1
  50. package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
  51. package/dist/connection/CompatibilityPreflight.js +2 -1
  52. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  53. package/dist/connection/KernelKeepAlive.d.ts +3 -1
  54. package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
  55. package/dist/connection/KernelKeepAlive.js +10 -7
  56. package/dist/connection/KernelKeepAlive.js.map +1 -1
  57. package/dist/connection/TokenProvider.d.ts.map +1 -1
  58. package/dist/connection/TokenProvider.js +3 -1
  59. package/dist/connection/TokenProvider.js.map +1 -1
  60. package/dist/connection/TraceContextMiddleware.d.ts +4 -0
  61. package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
  62. package/dist/connection/TraceContextMiddleware.js +30 -0
  63. package/dist/connection/TraceContextMiddleware.js.map +1 -0
  64. package/dist/correlation/CorrelationIdManager.d.ts +4 -0
  65. package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
  66. package/dist/correlation/CorrelationIdManager.js +8 -0
  67. package/dist/correlation/CorrelationIdManager.js.map +1 -1
  68. package/dist/eventSequences/EventLog.d.ts +2 -1
  69. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  70. package/dist/eventSequences/EventLog.js +2 -2
  71. package/dist/eventSequences/EventLog.js.map +1 -1
  72. package/dist/eventSequences/EventSequence.d.ts +4 -1
  73. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  74. package/dist/eventSequences/EventSequence.js +63 -80
  75. package/dist/eventSequences/EventSequence.js.map +1 -1
  76. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
  77. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
  78. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
  79. package/dist/events/handles.d.ts +12 -0
  80. package/dist/events/handles.d.ts.map +1 -0
  81. package/dist/events/handles.js +33 -0
  82. package/dist/events/handles.js.map +1 -0
  83. package/dist/events/index.d.ts +1 -0
  84. package/dist/events/index.d.ts.map +1 -1
  85. package/dist/events/index.js +1 -0
  86. package/dist/events/index.js.map +1 -1
  87. package/dist/index.d.ts +7 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +4 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/logging/ChronicleLogEntry.d.ts +11 -0
  92. package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
  93. package/dist/logging/ChronicleLogEntry.js +4 -0
  94. package/dist/logging/ChronicleLogEntry.js.map +1 -0
  95. package/dist/logging/ChronicleLogLevel.d.ts +9 -0
  96. package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
  97. package/dist/logging/ChronicleLogLevel.js +12 -0
  98. package/dist/logging/ChronicleLogLevel.js.map +1 -0
  99. package/dist/logging/DiagChronicleLogger.d.ts +7 -0
  100. package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
  101. package/dist/logging/DiagChronicleLogger.js +10 -0
  102. package/dist/logging/DiagChronicleLogger.js.map +1 -0
  103. package/dist/logging/IChronicleLogger.d.ts +7 -0
  104. package/dist/logging/IChronicleLogger.d.ts.map +1 -0
  105. package/dist/logging/IChronicleLogger.js +4 -0
  106. package/dist/logging/IChronicleLogger.js.map +1 -0
  107. package/dist/logging/createLogger.d.ts +10 -0
  108. package/dist/logging/createLogger.d.ts.map +1 -0
  109. package/dist/logging/createLogger.js +55 -0
  110. package/dist/logging/createLogger.js.map +1 -0
  111. package/dist/observation/EventHandler.d.ts +7 -0
  112. package/dist/observation/EventHandler.d.ts.map +1 -0
  113. package/dist/observation/EventHandler.js +4 -0
  114. package/dist/observation/EventHandler.js.map +1 -0
  115. package/dist/observation/createHandlerInstanceValidator.d.ts +4 -0
  116. package/dist/observation/createHandlerInstanceValidator.d.ts.map +1 -0
  117. package/dist/observation/createHandlerInstanceValidator.js +40 -0
  118. package/dist/observation/createHandlerInstanceValidator.js.map +1 -0
  119. package/dist/observation/getEventHandlers.d.ts +5 -0
  120. package/dist/observation/getEventHandlers.d.ts.map +1 -0
  121. package/dist/observation/getEventHandlers.js +40 -0
  122. package/dist/observation/getEventHandlers.js.map +1 -0
  123. package/dist/observation/getInstanceMethods.d.ts +4 -0
  124. package/dist/observation/getInstanceMethods.d.ts.map +1 -0
  125. package/dist/observation/getInstanceMethods.js +29 -0
  126. package/dist/observation/getInstanceMethods.js.map +1 -0
  127. package/dist/projections/Projections.d.ts.map +1 -1
  128. package/dist/projections/Projections.js +4 -3
  129. package/dist/projections/Projections.js.map +1 -1
  130. package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
  131. package/dist/reactors/ReactorDispatcher.js +27 -32
  132. package/dist/reactors/ReactorDispatcher.js.map +1 -1
  133. package/dist/reactors/Reactors.d.ts +1 -0
  134. package/dist/reactors/Reactors.d.ts.map +1 -1
  135. package/dist/reactors/Reactors.js +49 -34
  136. package/dist/reactors/Reactors.js.map +1 -1
  137. package/dist/reactors/index.d.ts +1 -0
  138. package/dist/reactors/index.d.ts.map +1 -1
  139. package/dist/reactors/index.js +1 -0
  140. package/dist/reactors/index.js.map +1 -1
  141. package/dist/reducers/ReducerEventDispatcher.d.ts +1 -0
  142. package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -1
  143. package/dist/reducers/ReducerEventDispatcher.js +5 -17
  144. package/dist/reducers/ReducerEventDispatcher.js.map +1 -1
  145. package/dist/reducers/ReducerEventHandler.d.ts +3 -6
  146. package/dist/reducers/ReducerEventHandler.d.ts.map +1 -1
  147. package/dist/reducers/Reducers.d.ts +1 -0
  148. package/dist/reducers/Reducers.d.ts.map +1 -1
  149. package/dist/reducers/Reducers.js +47 -32
  150. package/dist/reducers/Reducers.js.map +1 -1
  151. package/dist/reducers/index.d.ts +1 -0
  152. package/dist/reducers/index.d.ts.map +1 -1
  153. package/dist/reducers/index.js +1 -0
  154. package/dist/reducers/index.js.map +1 -1
  155. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  156. package/dist/seeding/EventSeeding.js +4 -3
  157. package/dist/seeding/EventSeeding.js.map +1 -1
  158. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
  159. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
  160. package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
  161. package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
  162. package/dist/telemetry/observeOperation.d.ts +6 -0
  163. package/dist/telemetry/observeOperation.d.ts.map +1 -0
  164. package/dist/telemetry/observeOperation.js +23 -0
  165. package/dist/telemetry/observeOperation.js.map +1 -0
  166. package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
  167. package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
  168. package/dist/telemetry/runInBackgroundContext.js +9 -0
  169. package/dist/telemetry/runInBackgroundContext.js.map +1 -0
  170. package/dist/telemetry/spanAttributes.d.ts +14 -0
  171. package/dist/telemetry/spanAttributes.d.ts.map +1 -0
  172. package/dist/telemetry/spanAttributes.js +47 -0
  173. package/dist/telemetry/spanAttributes.js.map +1 -0
  174. package/dist/testing/ReactorScenario.d.ts +1 -0
  175. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  176. package/dist/testing/ReactorScenario.js +9 -1
  177. package/dist/testing/ReactorScenario.js.map +1 -1
  178. package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
  179. package/dist/testing/ReducerReadModelProcessor.js +1 -0
  180. package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
  181. package/dist/tsconfig.tsbuildinfo +1 -1
  182. package/eventSequences/EventLog.ts +4 -2
  183. package/eventSequences/EventSequence.ts +64 -81
  184. package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
  185. package/events/for_handles/when_decorating_a_method.legacy.spec.ts +91 -0
  186. package/events/for_handles/when_decorating_a_method.standard.spec.ts +102 -0
  187. package/events/handles.ts +37 -0
  188. package/events/index.ts +1 -0
  189. package/for_ChronicleClient/when_registering_artifacts/with_invalid_handler_mappings.spec.ts +142 -0
  190. package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
  191. package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
  192. package/index.ts +7 -1
  193. package/logging/ChronicleLogEntry.ts +14 -0
  194. package/logging/ChronicleLogLevel.ts +11 -0
  195. package/logging/DiagChronicleLogger.ts +13 -0
  196. package/logging/IChronicleLogger.ts +10 -0
  197. package/logging/createLogger.ts +50 -0
  198. package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
  199. package/observation/EventHandler.ts +9 -0
  200. package/observation/createHandlerInstanceValidator.ts +42 -0
  201. package/observation/for_getEventHandlers/when_discovering_handlers.spec.ts +137 -0
  202. package/observation/for_getEventHandlers/when_reobserving_explicit_handlers.spec.ts +73 -0
  203. package/observation/getEventHandlers.ts +43 -0
  204. package/observation/getInstanceMethods.ts +29 -0
  205. package/package.json +8 -2
  206. package/projections/Projections.ts +4 -3
  207. package/reactors/ReactorDispatcher.ts +26 -30
  208. package/reactors/Reactors.ts +55 -37
  209. package/reactors/decorators.standard.spec.ts +8 -0
  210. package/reactors/for_ReactorDispatcher/when_selecting_explicit_handlers.spec.ts +64 -0
  211. package/reactors/for_Reactors/when_activating_a_field_override.spec.ts +62 -0
  212. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
  213. package/reactors/for_Reactors/when_delivering_to_instance_handlers.spec.ts +108 -0
  214. package/reactors/for_Reactors/when_observing_explicit_handlers.legacy.spec.ts +77 -0
  215. package/reactors/for_Reactors/when_registering_explicit_handlers.spec.ts +53 -0
  216. package/reactors/index.ts +1 -0
  217. package/reducers/ReducerEventDispatcher.ts +5 -16
  218. package/reducers/ReducerEventHandler.ts +4 -6
  219. package/reducers/Reducers.ts +50 -35
  220. package/reducers/for_ReducerEventDispatcher/when_invoking_an_explicit_handler.spec.ts +51 -0
  221. package/reducers/for_ReducerEventDispatcher/when_invoking_instance_handlers.spec.ts +69 -0
  222. package/reducers/for_Reducers/when_activating_a_field_override.spec.ts +63 -0
  223. package/reducers/for_Reducers/when_observing_explicit_handlers.legacy.spec.ts +73 -0
  224. package/reducers/for_Reducers/when_registering_explicit_handlers.spec.ts +57 -0
  225. package/reducers/index.ts +1 -0
  226. package/seeding/EventSeeding.ts +6 -4
  227. package/telemetry/ChronicleTelemetryOptions.ts +8 -0
  228. package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
  229. package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
  230. package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
  231. package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
  232. package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
  233. package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
  234. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
  235. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
  236. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
  237. package/telemetry/observeOperation.ts +25 -0
  238. package/telemetry/runInBackgroundContext.ts +10 -0
  239. package/telemetry/spanAttributes.ts +49 -0
  240. package/testing/ReactorScenario.ts +9 -1
  241. package/testing/ReducerReadModelProcessor.ts +1 -0
  242. package/testing/for_ReactorScenario/when_delivering_to_an_explicit_handler.spec.ts +100 -0
  243. package/testing/for_ReducerReadModelProcessor/when_an_instance_field_hides_a_handler.spec.ts +29 -0
@@ -0,0 +1,142 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
5
+ import { ChronicleClient } from '../../ChronicleClient.js';
6
+ import { ConnectionLifecycle } from '../../connection/ConnectionLifecycle.js';
7
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
8
+ import type { IClientArtifactsProvider } from '../../artifacts/IClientArtifactsProvider.js';
9
+ import { eventType } from '../../events/eventTypeDecorator.js';
10
+ import { handles } from '../../events/handles.js';
11
+ import { reactor, replay } from '../../reactors/index.js';
12
+ import { reducer } from '../../reducers/index.js';
13
+
14
+ chai.should();
15
+
16
+ const transport = vi.hoisted(() => ({
17
+ ensureEventStore: vi.fn(),
18
+ register: vi.fn(),
19
+ observe: vi.fn()
20
+ }));
21
+
22
+ vi.mock('../../connection/ChronicleConnection', () => ({
23
+ ChronicleConnection: class {
24
+ async resetChannel() {}
25
+ async connect() {}
26
+ disconnect() {}
27
+ server = { getVersionInfo: async () => ({}) };
28
+ eventStores = { ensureEventStore: transport.ensureEventStore };
29
+ eventTypes = new Proxy({}, { get: () => transport.register });
30
+ constraints = new Proxy({}, { get: () => transport.register });
31
+ projections = new Proxy({}, { get: () => transport.register });
32
+ readModels = new Proxy({}, { get: () => transport.register });
33
+ webhooks = new Proxy({}, { get: () => transport.register });
34
+ eventSeeding = new Proxy({}, { get: () => transport.register });
35
+ reactors = { observe: transport.observe };
36
+ reducers = { observe: transport.observe };
37
+ }
38
+ }));
39
+ vi.mock('../../connection/KernelKeepAlive', () => ({
40
+ KernelKeepAlive: class { async start() {} }
41
+ }));
42
+
43
+ @eventType('preflight-author')
44
+ class AuthorRegistered {}
45
+ @eventType('preflight-missing')
46
+ class Unregistered {}
47
+
48
+ @reactor('valid-reactor')
49
+ class ValidReactor { @handles(AuthorRegistered) notify() {} }
50
+ @reducer('valid-reducer')
51
+ class ValidReducer { @handles(AuthorRegistered) update() {} }
52
+
53
+ @reactor('missing-event')
54
+ class MissingReactor { @handles(Unregistered) notify() {} }
55
+ @reactor('duplicate-event')
56
+ class DuplicateReactor {
57
+ @handles(AuthorRegistered) notify() {}
58
+ authorRegistered() {}
59
+ }
60
+ @reactor('ambiguous-replay')
61
+ class AmbiguousReactor { @replay(AuthorRegistered) @handles(AuthorRegistered) notify() {} }
62
+
63
+ @reducer('missing-event')
64
+ class MissingReducer { @handles(Unregistered) update() {} }
65
+ @reducer('duplicate-event')
66
+ class DuplicateReducer {
67
+ @handles(AuthorRegistered) update() {}
68
+ authorRegistered() {}
69
+ }
70
+
71
+ for (const [kind, type, message] of [
72
+ ['reactor', MissingReactor, /notify.*MissingReactor.*Unregistered.*no registered event type/],
73
+ ['reactor', DuplicateReactor, /multiple handlers.*preflight-author.*notify.*authorRegistered/],
74
+ ['reactor', AmbiguousReactor, /notify.*AmbiguousReactor.*cannot combine @handles and @replay/],
75
+ ['reducer', MissingReducer, /update.*MissingReducer.*Unregistered.*no registered event type/],
76
+ ['reducer', DuplicateReducer, /multiple handlers.*preflight-author.*update.*authorRegistered/]
77
+ ] as const) {
78
+ describe(`when getting an event store with invalid mappings on ${type.name}`, () => {
79
+ let client: ChronicleClient;
80
+ let artifacts: IClientArtifactsProvider;
81
+ let result: PromiseSettledResult<unknown>;
82
+ let disconnectedSubscriptions: Set<unknown>;
83
+ let clientSubscriptionCount: number;
84
+ beforeEach(async () => {
85
+ vi.clearAllMocks();
86
+ disconnectedSubscriptions = new Set();
87
+ const onDisconnected = ConnectionLifecycle.prototype.onDisconnected;
88
+ vi.spyOn(ConnectionLifecycle.prototype, 'onDisconnected').mockImplementation(function (this: ConnectionLifecycle, handler) {
89
+ disconnectedSubscriptions.add(handler);
90
+ const unsubscribe = onDisconnected.call(this, handler);
91
+ return () => { unsubscribe(); disconnectedSubscriptions.delete(handler); };
92
+ });
93
+ transport.ensureEventStore.mockResolvedValue({ IsAuthorized: true });
94
+ transport.register.mockResolvedValue({});
95
+ transport.observe.mockImplementation(async function* () {
96
+ yield { Events: [], Partition: '', ReplayState: 0, InitialState: '' };
97
+ });
98
+ artifacts = {
99
+ reactors: kind === 'reactor' ? [ValidReactor, type] : [ValidReactor],
100
+ reducers: kind === 'reducer' ? [ValidReducer, type] : [ValidReducer],
101
+ eventTypes: [AuthorRegistered], readModels: [], projections: [], constraints: [],
102
+ webhooks: [], seeders: [], eventTypeMigrations: [], globalForHandlers: []
103
+ };
104
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', {
105
+ discoveryPatterns: [], clientArtifactsProvider: artifacts
106
+ }));
107
+ clientSubscriptionCount = disconnectedSubscriptions.size;
108
+ [result] = await Promise.allSettled([client.getEventStore('store')]);
109
+ });
110
+ afterEach(() => { client.dispose(); vi.restoreAllMocks(); });
111
+
112
+ it('should reject getEventStore with the handler validation error', () => {
113
+ result.status.should.equal('rejected');
114
+ if (result.status === 'rejected') (result.reason as Error).message.should.match(message);
115
+ });
116
+ it('should leave no discarded-store lifecycle subscriptions after either failed retrieval', async () => {
117
+ disconnectedSubscriptions.size.should.equal(clientSubscriptionCount);
118
+ await Promise.allSettled([client.getEventStore('store')]);
119
+ disconnectedSubscriptions.size.should.equal(clientSubscriptionCount);
120
+ });
121
+ it('should not register any artifacts or begin observations', () => {
122
+ transport.register.mock.calls.should.have.lengthOf(0);
123
+ transport.observe.mock.calls.should.have.lengthOf(0);
124
+ });
125
+ it('should reject a second retrieval instead of returning the failed store', async () => {
126
+ const [retry] = await Promise.allSettled([client.getEventStore('store')]);
127
+ retry.status.should.equal('rejected');
128
+ if (retry.status === 'rejected') (retry.reason as Error).message.should.match(message);
129
+ transport.register.mock.calls.should.have.lengthOf(0);
130
+ transport.observe.mock.calls.should.have.lengthOf(0);
131
+ });
132
+ it('should register a fresh store after the invalid mapping is removed and cache only that store', async () => {
133
+ artifacts.reactors.splice(1);
134
+ artifacts.reducers.splice(1);
135
+ const store = await client.getEventStore('store');
136
+ transport.register.mock.calls.length.should.be.greaterThan(0);
137
+ transport.observe.mock.calls.should.have.lengthOf(2);
138
+ (await client.getEventStore('store')).should.equal(store);
139
+ transport.ensureEventStore.mock.calls.should.have.lengthOf(2);
140
+ });
141
+ });
142
+ }
@@ -0,0 +1,150 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterEach, beforeEach, chai, describe, it } from 'vitest';
5
+ import { context, propagation, trace } from '@opentelemetry/api';
6
+ import { createServer, ServerError, Status, type ServiceImplementation } from 'nice-grpc';
7
+ import { Metadata } from 'nice-grpc-common';
8
+ import { ConnectionServiceDefinition, EventStoresDefinition, ServerDefinition, ReactorsDefinition, ReducersDefinition, ReadModelsDefinition } from '@cratis/chronicle.contracts';
9
+ import { ChronicleClient } from '../../ChronicleClient.js';
10
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
11
+ import type { IClientArtifactsProvider } from '../../artifacts/IClientArtifactsProvider.js';
12
+ import { CorrelationId, correlationIdManager } from '../../correlation/index.js';
13
+ import type { ChronicleLogEntry } from '../../logging/ChronicleLogEntry.js';
14
+ import { reactor } from '../../reactors/reactor.js';
15
+ import { reducer } from '../../reducers/reducer.js';
16
+ import { telemetrySession } from '../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
17
+
18
+ const should = chai.should();
19
+ const telemetry = telemetrySession();
20
+ class BackgroundReactor {}
21
+ reactor('background-reactor')(BackgroundReactor);
22
+ class BackgroundReducer {}
23
+ reducer('background-reducer')(BackgroundReducer);
24
+ const artifacts: IClientArtifactsProvider = {
25
+ eventTypes: [], readModels: [], reactors: [BackgroundReactor], reducers: [BackgroundReducer],
26
+ seeders: [], constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
27
+ };
28
+ let server: ReturnType<typeof createServer>;
29
+ let client: ChronicleClient;
30
+ let received: { method: string; metadata: Metadata }[];
31
+ let logs: ChronicleLogEntry[];
32
+ let hostTraceId: string;
33
+ const correlation = new CorrelationId('a4450131-7cd4-40ba-9438-69c680efc1f3');
34
+
35
+ function deferred() {
36
+ let resolve!: () => void;
37
+ const promise = new Promise<void>(complete => { resolve = complete; });
38
+ return { promise, resolve };
39
+ }
40
+
41
+ beforeEach(async () => {
42
+ received = [];
43
+ logs = [];
44
+ const reobserved = deferred();
45
+ const reconnected = deferred();
46
+ const endKeepAlive = deferred();
47
+ const counts = new Map<string, number>();
48
+ server = createServer().use(async function* (call, callContext) {
49
+ const method = call.method.path;
50
+ received.push({ method, metadata: Metadata(callContext.metadata) });
51
+ const count = (counts.get(method) ?? 0) + 1;
52
+ counts.set(method, count);
53
+ const observations = [...counts].filter(([path]) => path.endsWith('/Observe'));
54
+ if (observations.length === 2 && observations.every(([, total]) => total >= 2)) reobserved.resolve();
55
+ if (observations.length === 2 && observations.every(([, total]) => total >= 3)) reconnected.resolve();
56
+ return yield* call.next(call.request, callContext);
57
+ });
58
+ const unused = async () => { throw new Error('Unexpected RPC'); };
59
+ const success = async () => ({ IsAuthorized: true, ValidationResults: [], ExceptionMessages: [] });
60
+ let connections = 0;
61
+ server.add(ConnectionServiceDefinition, {
62
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unused])),
63
+ checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' }),
64
+ connectionKeepAlive: async () => ({}),
65
+ connect: async function* (request, callContext) {
66
+ const first = ++connections === 1;
67
+ yield { ConnectionId: request.ConnectionId };
68
+ if (first) await endKeepAlive.promise;
69
+ else await new Promise<void>(resolve => callContext.signal.addEventListener('abort', () => resolve(), { once: true }));
70
+ }
71
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
72
+ server.add(ServerDefinition, {
73
+ ...Object.fromEntries(Object.keys(ServerDefinition.methods).map(name => [name, unused])),
74
+ getVersionInfo: async () => ({})
75
+ } as ServiceImplementation<typeof ServerDefinition>);
76
+ server.add(EventStoresDefinition, {
77
+ allEventStores: unused, ensureEventStore: success, observeEventStores: unused
78
+ } as ServiceImplementation<typeof EventStoresDefinition>);
79
+ server.add(ReadModelsDefinition, {
80
+ ...Object.fromEntries(Object.keys(ReadModelsDefinition.methods).map(name => [name, unused])),
81
+ registerMany: success
82
+ } as ServiceImplementation<typeof ReadModelsDefinition>);
83
+ server.add(ReactorsDefinition, {
84
+ ...Object.fromEntries(Object.keys(ReactorsDefinition.methods).map(name => [name, unused])),
85
+ observe: async function* (requests) {
86
+ for await (const _request of requests) {
87
+ yield { Events: [], Partition: '', ReplayState: 0 };
88
+ throw new ServerError(Status.UNAVAILABLE, 'End stream to exercise re-observation');
89
+ }
90
+ }
91
+ } as ServiceImplementation<typeof ReactorsDefinition>);
92
+ server.add(ReducersDefinition, {
93
+ ...Object.fromEntries(Object.keys(ReducersDefinition.methods).map(name => [name, unused])),
94
+ observe: async function* (requests) {
95
+ for await (const _request of requests) {
96
+ yield { Events: [], Partition: '', ReplayState: 0 };
97
+ throw new ServerError(Status.UNAVAILABLE, 'End stream to exercise re-observation');
98
+ }
99
+ }
100
+ } as ServiceImplementation<typeof ReducersDefinition>);
101
+ const port = await server.listen('127.0.0.1:0');
102
+ const span = telemetry.traces.getTracer('host').startSpan('host');
103
+ hostTraceId = span.spanContext().traceId;
104
+ const active = propagation.setBaggage(trace.setSpan(context.active(), span), propagation.createBaggage({
105
+ 'cratis.correlation_id': { value: correlation.toString() }, email: { value: 'private@example.com' }
106
+ }));
107
+ try {
108
+ await context.with(active, () => correlationIdManager.run(correlation, async () => {
109
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://127.0.0.1:${port}?disableTls=true&apiKey=test`, {
110
+ discoveryPatterns: [], clientArtifactsProvider: artifacts, logger: { log: entry => logs.push(entry) }
111
+ }));
112
+ await client.getEventStore('store');
113
+ context.active().should.equal(active);
114
+ correlationIdManager.scoped!.should.equal(correlation);
115
+ }));
116
+ await reobserved.promise;
117
+ endKeepAlive.resolve();
118
+ await reconnected.promise;
119
+ } finally { span.end(); endKeepAlive.resolve(); }
120
+ });
121
+
122
+ afterEach(async () => {
123
+ client?.dispose();
124
+ await server?.shutdown();
125
+ });
126
+
127
+ describe('when a correlated getEventStore starts background streams and their recovery loops', () => {
128
+ it('should not propagate the caller trace or baggage on observations or keep-alive RPCs', () => {
129
+ const background = received.filter(({ method }) => /\/(Observe|Connect|ConnectionKeepAlive)$/.test(method));
130
+ background.length.should.be.greaterThanOrEqual(10);
131
+ for (const { metadata } of background) {
132
+ should.equal(metadata.get('traceparent'), undefined);
133
+ should.equal(metadata.get('baggage'), undefined);
134
+ }
135
+ });
136
+ it('should retain the caller context on the foreground operation', () => {
137
+ const foreground = received.find(({ method }) => method.endsWith('/EnsureEventStore'))!;
138
+ foreground.metadata.get('traceparent')!.should.contain(hostTraceId);
139
+ should.equal(foreground.metadata.get('baggage'), `cratis.correlation_id=${correlation}`);
140
+ });
141
+ it('should not label background-loop diagnostics with the initiating business correlation or trace', () => {
142
+ const background = logs.filter(entry => /observation|keep-alive|Reconnect/.test(entry.message));
143
+ background.length.should.be.greaterThan(0);
144
+ for (const entry of background) {
145
+ entry.attributes.should.not.have.property('cratis.correlation_id');
146
+ entry.attributes.should.not.have.property('trace_id');
147
+ entry.attributes.should.not.have.property('span_id');
148
+ }
149
+ });
150
+ });
@@ -0,0 +1,44 @@
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 { runInNewContext } from 'node:vm';
6
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
7
+ import type { ChronicleTelemetryOptions } from '../../telemetry/ChronicleTelemetryOptions.js';
8
+
9
+ chai.should();
10
+
11
+ for (const policy of [
12
+ { mode: 'unknown' }, {}, null,
13
+ { mode: 'hmac' }, { mode: 'hmac', key: '' }, { mode: 'hmac', key: 'secret' },
14
+ { mode: 'hmac', key: [] }, { mode: 'hmac', key: new Uint8Array() },
15
+ { mode: 'hmac', key: Buffer.alloc(0) }, { mode: 'hmac', key: new Uint16Array([1]) },
16
+ { mode: 'hmac', key: new DataView(new ArrayBuffer(1)) },
17
+ { mode: 'hmac', key: runInNewContext('new Uint8Array()') },
18
+ { mode: 'hmac', key: runInNewContext('new Uint16Array([1])') }
19
+ ]) {
20
+ describe(`when configuring event source privacy with ${JSON.stringify(policy)}`, () => {
21
+ it('should reject invalid telemetry configuration before a client can make business calls', () => {
22
+ const telemetry = { eventSourceId: policy } as unknown as ChronicleTelemetryOptions;
23
+ (() => ChronicleOptions.development({ telemetry })).should.throw(TypeError);
24
+ (() => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry })).should.throw(TypeError);
25
+ });
26
+ });
27
+ }
28
+
29
+ describe('when configuring an HMAC key created in another realm', () => {
30
+ it('should accept a non-empty byte array without relying on its realm constructor', () => {
31
+ const key = runInNewContext('new Uint8Array([1, 2, 3])') as Uint8Array;
32
+ (key instanceof Uint8Array).should.be.false;
33
+ (() => ChronicleOptions.development({ telemetry: { eventSourceId: { mode: 'hmac', key } } })).should.not.throw();
34
+ (() => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry: { eventSourceId: { mode: 'hmac', key } } })).should.not.throw();
35
+ });
36
+ });
37
+
38
+ for (const eventSourceId of [undefined, { mode: 'raw' }, { mode: 'hmac', key: new Uint8Array([1]) }, { mode: 'hmac', key: Buffer.from('key') }] as const) {
39
+ describe('when configuring a supported event source privacy policy', () => {
40
+ it('should accept omitted identifiers, raw identifiers and non-empty byte keys', () => {
41
+ (() => ChronicleOptions.development({ telemetry: { eventSourceId } })).should.not.throw();
42
+ });
43
+ });
44
+ }
package/index.ts CHANGED
@@ -37,7 +37,13 @@ export { EventStoreNamespaceName } from './EventStoreNamespaceName.js';
37
37
  export { Guid } from '@cratis/fundamentals';
38
38
  export { ReplayState } from '@cratis/chronicle.contracts';
39
39
  export { ChronicleInstrumentationName, ChronicleTracer } from './Tracing.js';
40
- export { ChronicleMeter, ChronicleMeterName, ChronicleMetrics } from './Metrics.js';
40
+ export { ChronicleMeter, ChronicleMeterName, ChronicleMetrics, ChronicleConventionMetrics } from './Metrics.js';
41
+ export { WellKnownTelemetryNames } from './WellKnownTelemetryNames.js';
42
+ export type { ChronicleTelemetryOptions } from './telemetry/ChronicleTelemetryOptions.js';
43
+ export type { IChronicleLogger } from './logging/IChronicleLogger.js';
44
+ export type { ChronicleLogEntry } from './logging/ChronicleLogEntry.js';
45
+ export { ChronicleLogLevel } from './logging/ChronicleLogLevel.js';
46
+ export { DiagChronicleLogger } from './logging/DiagChronicleLogger.js';
41
47
 
42
48
  export * from './events/index.js';
43
49
  export * from './eventSequences/index.js';
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { Attributes } from '@opentelemetry/api';
5
+ import type { ChronicleLogLevel } from './ChronicleLogLevel.js';
6
+
7
+ /** Structured diagnostic without event payloads, error messages or error stacks. */
8
+ export interface ChronicleLogEntry {
9
+ readonly category: string;
10
+ readonly level: ChronicleLogLevel;
11
+ readonly message: string;
12
+ /** Safe diagnostic fields, including correlation and valid trace/span identifiers when available. */
13
+ readonly attributes: Readonly<Attributes>;
14
+ }
@@ -0,0 +1,11 @@
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
+ /** Severity of a Chronicle diagnostic. */
5
+ export enum ChronicleLogLevel {
6
+ Verbose = 'verbose',
7
+ Debug = 'debug',
8
+ Info = 'info',
9
+ Warn = 'warn',
10
+ Error = 'error'
11
+ }
@@ -0,0 +1,13 @@
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 { diag } from '@opentelemetry/api';
5
+ import type { IChronicleLogger } from './IChronicleLogger.js';
6
+ import type { ChronicleLogEntry } from './ChronicleLogEntry.js';
7
+
8
+ /** Compatibility adapter used when no application logger is supplied during the minor-release overlap. */
9
+ export class DiagChronicleLogger implements IChronicleLogger {
10
+ log(entry: ChronicleLogEntry): void {
11
+ diag.createComponentLogger({ namespace: entry.category })[entry.level](entry.message, entry.attributes);
12
+ }
13
+ }
@@ -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 type { ChronicleLogEntry } from './ChronicleLogEntry.js';
5
+
6
+ /** Application-owned diagnostic sink, configured separately for each Chronicle client. */
7
+ export interface IChronicleLogger {
8
+ /** Receives one sanitized diagnostic. Implementations should not throw. */
9
+ log(entry: ChronicleLogEntry): void;
10
+ }
@@ -0,0 +1,50 @@
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, trace, isSpanContextValid, type Attributes } from '@opentelemetry/api';
5
+ import { ClientError, ServerError } from 'nice-grpc-common';
6
+ import { correlationIdManager } from '../correlation/index.js';
7
+ import { WellKnownTelemetryNames } from '../WellKnownTelemetryNames.js';
8
+ import { exceptionType } from '../telemetry/spanAttributes.js';
9
+ import { ChronicleLogLevel } from './ChronicleLogLevel.js';
10
+ import { DiagChronicleLogger } from './DiagChronicleLogger.js';
11
+ import type { IChronicleLogger } from './IChronicleLogger.js';
12
+
13
+ /** Binds a category to a client-owned sink and adds safe correlation fields. */
14
+ export function createLogger(category: string, sink: IChronicleLogger = new DiagChronicleLogger()) {
15
+ const log = (level: ChronicleLogLevel) => (message: string, fields: Record<string, unknown> = {}) => {
16
+ const attributes: Attributes = {};
17
+ for (const [key, value] of Object.entries(fields)) {
18
+ if (key === 'error') {
19
+ attributes['error.type'] = exceptionType(value);
20
+ attributes['exception.type'] = exceptionType(value);
21
+ if (value instanceof ClientError || value instanceof ServerError) attributes['rpc.grpc.status_code'] = value.code;
22
+ } else if (key === 'sequenceNumber') {
23
+ const number = Number(value);
24
+ if (Number.isSafeInteger(number)) attributes[WellKnownTelemetryNames.attributes.sequenceNumber] = number;
25
+ } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
26
+ attributes[key] = value;
27
+ } else if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
28
+ attributes[key] = value;
29
+ }
30
+ }
31
+ const active = context.active();
32
+ const correlationKey = WellKnownTelemetryNames.attributes.correlationId;
33
+ const correlation = correlationIdManager.scoped?.toString() ?? propagation.getBaggage(active)?.getEntry(correlationKey)?.value;
34
+ if (correlation) attributes[correlationKey] = correlation;
35
+ const spanContext = trace.getSpanContext(active);
36
+ if (spanContext && isSpanContextValid(spanContext)) {
37
+ attributes.trace_id = spanContext.traceId;
38
+ attributes.span_id = spanContext.spanId;
39
+ }
40
+ try {
41
+ sink.log({ category, level, message, attributes });
42
+ } catch {
43
+ // Diagnostics are not the operation's outcome. A broken sink must not change RPCs or acknowledgements.
44
+ }
45
+ };
46
+ return {
47
+ verbose: log(ChronicleLogLevel.Verbose), debug: log(ChronicleLogLevel.Debug),
48
+ info: log(ChronicleLogLevel.Info), warn: log(ChronicleLogLevel.Warn), error: log(ChronicleLogLevel.Error)
49
+ };
50
+ }
@@ -0,0 +1,96 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { beforeEach, chai, describe, it, vi } from 'vitest';
5
+ import { diag, DiagLogLevel } from '@opentelemetry/api';
6
+ import { ClientError, ServerError, Status } from 'nice-grpc-common';
7
+ import { ChronicleClient } from '../../../ChronicleClient.js';
8
+ import { ChronicleOptions } from '../../../ChronicleOptions.js';
9
+ import { createLogger } from '../../createLogger.js';
10
+ import type { ChronicleLogEntry } from '../../ChronicleLogEntry.js';
11
+ import { telemetrySession } from '../../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
12
+ import { CorrelationId, correlationIdManager } from '../../../correlation/index.js';
13
+
14
+ vi.mock('../../../connection/ChronicleConnection.js', () => ({ ChronicleConnection: class { disconnect() {} } }));
15
+ chai.should();
16
+ const telemetry = telemetrySession();
17
+
18
+ describe('when two clients have different diagnostic sinks', () => {
19
+ const first: ChronicleLogEntry[] = [];
20
+ const second: ChronicleLogEntry[] = [];
21
+ beforeEach(() => {
22
+ first.length = 0;
23
+ second.length = 0;
24
+ const one = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [], logger: { log: entry => first.push(entry) } }));
25
+ const two = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [], logger: { log: entry => second.push(entry) } }));
26
+ one.dispose();
27
+ two.dispose();
28
+ });
29
+ it('should route each diagnostic only to its owning sink', () => {
30
+ first.should.have.lengthOf(2);
31
+ second.should.have.lengthOf(2);
32
+ (first[0] === second[0]).should.be.false;
33
+ });
34
+ });
35
+
36
+ describe('when logging in a correlated trace', () => {
37
+ let entry: ChronicleLogEntry;
38
+ beforeEach(async () => {
39
+ const logger = createLogger('test', { log: value => { entry = value; } });
40
+ await correlationIdManager.run(new CorrelationId('business-id'), () => telemetry.traces.getTracer('host').startActiveSpan('operation', async span => {
41
+ try { logger.error('Operation failed', { error: new TypeError('secret'), sequenceNumber: 9007199254740992n }); }
42
+ finally { span.end(); }
43
+ }));
44
+ });
45
+ it('should export error types with business and trace identifiers but no exception details', () => {
46
+ entry.attributes.should.include({ 'error.type': 'TypeError', 'exception.type': 'TypeError', 'cratis.correlation_id': 'business-id' });
47
+ entry.attributes.should.have.property('trace_id');
48
+ entry.attributes.should.have.property('span_id');
49
+ entry.attributes.should.not.have.property('cratis.event_sequence.number');
50
+ JSON.stringify(entry).should.not.contain('secret');
51
+ });
52
+ });
53
+
54
+ for (const error of [new ClientError('/test', Status.UNAVAILABLE, 'secret'), new ServerError(Status.UNAVAILABLE, 'secret')]) {
55
+ describe('when logging a gRPC failure', () => {
56
+ let entry: ChronicleLogEntry;
57
+ beforeEach(() => {
58
+ createLogger('test', { log: value => { entry = value; } }).error('Operation failed', { error });
59
+ });
60
+ it('should retain the numeric status without exporting sensitive exception details', () => {
61
+ entry.attributes.should.include({ 'rpc.grpc.status_code': Status.UNAVAILABLE });
62
+ JSON.stringify(entry).should.not.contain('secret');
63
+ });
64
+ });
65
+ }
66
+
67
+ for (const error of [new DOMException('secret', 'TimeoutError'), new DOMException('secret', 'AbortError'), { code: Status.UNAVAILABLE, message: 'secret' }]) {
68
+ describe('when logging a non-gRPC error with a numeric code', () => {
69
+ let entry: ChronicleLogEntry;
70
+ beforeEach(() => {
71
+ createLogger('test', { log: value => { entry = value; } }).error('Operation failed', { error });
72
+ });
73
+ it('should not label its code as a gRPC status', () => {
74
+ entry.attributes.should.not.have.property('rpc.grpc.status_code');
75
+ JSON.stringify(entry).should.not.contain('secret');
76
+ });
77
+ });
78
+ }
79
+
80
+ describe('when no logger is configured during the compatibility period', () => {
81
+ it('should still deliver sanitized diagnostics to diag', () => {
82
+ const error = vi.fn();
83
+ diag.setLogger({ error, warn: vi.fn(), info: vi.fn(), debug: vi.fn(), verbose: vi.fn() }, DiagLogLevel.ERROR);
84
+ try {
85
+ createLogger('test').error('Operation failed', { error: new Error('secret') });
86
+ error.mock.calls.should.have.lengthOf(1);
87
+ JSON.stringify(error.mock.calls).should.not.contain('secret');
88
+ } finally { diag.disable(); }
89
+ });
90
+ });
91
+
92
+ describe('when a diagnostic sink throws', () => {
93
+ it('should leave the operation unaffected', () => {
94
+ (() => createLogger('test', { log: () => { throw new Error('sink failed'); } }).info('Diagnostic')).should.not.throw();
95
+ });
96
+ });
@@ -0,0 +1,9 @@
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
+ /** A registered event type and its selected instance method. */
5
+ export interface EventHandler {
6
+ readonly id: string;
7
+ readonly generation: number;
8
+ readonly methodName: string;
9
+ }
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { Constructor } from '@cratis/fundamentals';
5
+ import { getHandledEventType } from '../events/handles.js';
6
+ import { getInstanceMethods } from './getInstanceMethods.js';
7
+
8
+ /** Captures @handles prototype methods during preflight and rejects instance properties hiding them, except bound methods. */
9
+ export function createHandlerInstanceValidator(type: Constructor, names: Iterable<string>, methods = getInstanceMethods(type)):
10
+ (instance: object) => void {
11
+ const handlers = [...new Set(names)].filter(name => getHandledEventType(methods.get(name)!) !== undefined).map(name => {
12
+ const method = methods.get(name)!;
13
+ let declaring = type.prototype;
14
+ while (declaring && Object.getOwnPropertyDescriptor(declaring, name)?.value !== method) {
15
+ declaring = Object.getPrototypeOf(declaring);
16
+ }
17
+ const eventType = getHandledEventType(method)!;
18
+ const declaration = `@handles(${(eventType as Function).name})`;
19
+ return { name, method, declaration, declaringType: declaring?.constructor.name ?? type.name };
20
+ });
21
+ return instance => {
22
+ for (const { name, method, declaration, declaringType } of handlers) {
23
+ let current: object | null = instance;
24
+ while (current && !Object.hasOwn(current, name)) current = Object.getPrototypeOf(current) as object | null;
25
+ const value: unknown = current ? Object.getOwnPropertyDescriptor(current, name)?.value : undefined;
26
+ if (current === instance && typeof value === 'function' && isBoundCopyOf(value, method)) continue;
27
+ if (current === instance || !current || value !== method) {
28
+ throw new Error(`Override '${name}' on '${type.name}' hides ${declaration} declared on '${declaringType}'; use a method with @handles instead of an instance field.`);
29
+ }
30
+ }
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Checks whether a function is a (possibly repeatedly) Function.prototype.bind copy of a method.
36
+ * @param value - The function found on the instance.
37
+ * @param method - The declared method.
38
+ * @returns True when the function name is one or more `bound ` prefixes followed by the method name.
39
+ */
40
+ function isBoundCopyOf(value: Function, method: Function): boolean {
41
+ return value.name.startsWith('bound ') && value.name.replace(/^(bound )+/, '') === method.name;
42
+ }