@cratis/chronicle 6.47.0 → 6.49.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 (260) hide show
  1. package/ChronicleClient.ts +3 -0
  2. package/EventStore.ts +10 -3
  3. package/IEventStore.ts +8 -0
  4. package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
  5. package/artifacts/IClientArtifactsProvider.ts +3 -0
  6. package/connection/ChronicleConnection.ts +6 -0
  7. package/connection/ChronicleServices.ts +2 -0
  8. package/connection/ConnectionLifecycle.ts +22 -0
  9. package/dist/ChronicleClient.d.ts.map +1 -1
  10. package/dist/ChronicleClient.js +3 -0
  11. package/dist/ChronicleClient.js.map +1 -1
  12. package/dist/EventStore.d.ts +2 -0
  13. package/dist/EventStore.d.ts.map +1 -1
  14. package/dist/EventStore.js +9 -3
  15. package/dist/EventStore.js.map +1 -1
  16. package/dist/IEventStore.d.ts +7 -0
  17. package/dist/IEventStore.d.ts.map +1 -1
  18. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
  19. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  20. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
  21. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  22. package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
  23. package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
  24. package/dist/connection/ChronicleConnection.d.ts +1 -0
  25. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  26. package/dist/connection/ChronicleConnection.js +5 -1
  27. package/dist/connection/ChronicleConnection.js.map +1 -1
  28. package/dist/connection/ChronicleServices.d.ts +2 -1
  29. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  30. package/dist/connection/ConnectionLifecycle.d.ts +8 -0
  31. package/dist/connection/ConnectionLifecycle.d.ts.map +1 -1
  32. package/dist/connection/ConnectionLifecycle.js +21 -0
  33. package/dist/connection/ConnectionLifecycle.js.map +1 -1
  34. package/dist/eventSequences/AppendOptions.d.ts +12 -0
  35. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  36. package/dist/eventSequences/EventForEventSourceId.d.ts +11 -0
  37. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  38. package/dist/eventSequences/EventLog.d.ts +2 -1
  39. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  40. package/dist/eventSequences/EventLog.js +2 -2
  41. package/dist/eventSequences/EventLog.js.map +1 -1
  42. package/dist/eventSequences/EventSequence.d.ts +4 -1
  43. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  44. package/dist/eventSequences/EventSequence.js +32 -6
  45. package/dist/eventSequences/EventSequence.js.map +1 -1
  46. package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -2
  47. package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
  48. package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -2
  49. package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
  50. package/dist/eventSequences/TransactionalEventSequence.js +4 -4
  51. package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
  52. package/dist/eventSequences/resolveEventSourceRouting.d.ts +39 -0
  53. package/dist/eventSequences/resolveEventSourceRouting.d.ts.map +1 -0
  54. package/dist/eventSequences/resolveEventSourceRouting.js +92 -0
  55. package/dist/eventSequences/resolveEventSourceRouting.js.map +1 -0
  56. package/dist/eventSources/ConcurrencyDimensions.d.ts +19 -0
  57. package/dist/eventSources/ConcurrencyDimensions.d.ts.map +1 -0
  58. package/dist/eventSources/ConcurrencyDimensions.js +19 -0
  59. package/dist/eventSources/ConcurrencyDimensions.js.map +1 -0
  60. package/dist/eventSources/DuplicateEventSourceName.d.ts +6 -0
  61. package/dist/eventSources/DuplicateEventSourceName.d.ts.map +1 -0
  62. package/dist/eventSources/DuplicateEventSourceName.js +12 -0
  63. package/dist/eventSources/DuplicateEventSourceName.js.map +1 -0
  64. package/dist/eventSources/DuplicateEventStreamName.d.ts +7 -0
  65. package/dist/eventSources/DuplicateEventStreamName.d.ts.map +1 -0
  66. package/dist/eventSources/DuplicateEventStreamName.js +14 -0
  67. package/dist/eventSources/DuplicateEventStreamName.js.map +1 -0
  68. package/dist/eventSources/EventRoutingContradictsEventSource.d.ts +9 -0
  69. package/dist/eventSources/EventRoutingContradictsEventSource.d.ts.map +1 -0
  70. package/dist/eventSources/EventRoutingContradictsEventSource.js +18 -0
  71. package/dist/eventSources/EventRoutingContradictsEventSource.js.map +1 -0
  72. package/dist/eventSources/EventSourceDefinition.d.ts +20 -0
  73. package/dist/eventSources/EventSourceDefinition.d.ts.map +1 -0
  74. package/dist/eventSources/EventSourceDefinition.js +4 -0
  75. package/dist/eventSources/EventSourceDefinition.js.map +1 -0
  76. package/dist/eventSources/EventSources.d.ts +31 -0
  77. package/dist/eventSources/EventSources.d.ts.map +1 -0
  78. package/dist/eventSources/EventSources.js +90 -0
  79. package/dist/eventSources/EventSources.js.map +1 -0
  80. package/dist/eventSources/EventStreamDefinition.d.ts +13 -0
  81. package/dist/eventSources/EventStreamDefinition.d.ts.map +1 -0
  82. package/dist/eventSources/EventStreamDefinition.js +4 -0
  83. package/dist/eventSources/EventStreamDefinition.js.map +1 -0
  84. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts +7 -0
  85. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts.map +1 -0
  86. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js +14 -0
  87. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js.map +1 -0
  88. package/dist/eventSources/EventStreamRequiresEventSource.d.ts +6 -0
  89. package/dist/eventSources/EventStreamRequiresEventSource.d.ts.map +1 -0
  90. package/dist/eventSources/EventStreamRequiresEventSource.js +12 -0
  91. package/dist/eventSources/EventStreamRequiresEventSource.js.map +1 -0
  92. package/dist/eventSources/IEventSources.d.ts +27 -0
  93. package/dist/eventSources/IEventSources.d.ts.map +1 -0
  94. package/dist/eventSources/IEventSources.js +4 -0
  95. package/dist/eventSources/IEventSources.js.map +1 -0
  96. package/dist/eventSources/ResolvedEventRouting.d.ts +37 -0
  97. package/dist/eventSources/ResolvedEventRouting.d.ts.map +1 -0
  98. package/dist/eventSources/ResolvedEventRouting.js +60 -0
  99. package/dist/eventSources/ResolvedEventRouting.js.map +1 -0
  100. package/dist/eventSources/UnknownEventSource.d.ts +6 -0
  101. package/dist/eventSources/UnknownEventSource.d.ts.map +1 -0
  102. package/dist/eventSources/UnknownEventSource.js +12 -0
  103. package/dist/eventSources/UnknownEventSource.js.map +1 -0
  104. package/dist/eventSources/eventSource.d.ts +60 -0
  105. package/dist/eventSources/eventSource.d.ts.map +1 -0
  106. package/dist/eventSources/eventSource.js +72 -0
  107. package/dist/eventSources/eventSource.js.map +1 -0
  108. package/dist/eventSources/index.d.ts +17 -0
  109. package/dist/eventSources/index.d.ts.map +1 -0
  110. package/dist/eventSources/index.js +13 -0
  111. package/dist/eventSources/index.js.map +1 -0
  112. package/dist/events/EventContext.d.ts +5 -0
  113. package/dist/events/EventContext.d.ts.map +1 -1
  114. package/dist/events/WireEventContext.d.ts +2 -0
  115. package/dist/events/WireEventContext.d.ts.map +1 -1
  116. package/dist/events/toClientEventContext.d.ts.map +1 -1
  117. package/dist/events/toClientEventContext.js +1 -0
  118. package/dist/events/toClientEventContext.js.map +1 -1
  119. package/dist/index.d.ts +3 -1
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +3 -1
  122. package/dist/index.js.map +1 -1
  123. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
  124. package/dist/projections/eventContextPropertyExpression.js +1 -0
  125. package/dist/projections/eventContextPropertyExpression.js.map +1 -1
  126. package/dist/readModels/IReadModelWatcher.d.ts +30 -0
  127. package/dist/readModels/IReadModelWatcher.d.ts.map +1 -0
  128. package/dist/readModels/IReadModelWatcher.js +4 -0
  129. package/dist/readModels/IReadModelWatcher.js.map +1 -0
  130. package/dist/readModels/IReadModels.d.ts +16 -2
  131. package/dist/readModels/IReadModels.d.ts.map +1 -1
  132. package/dist/readModels/ReadModelChangeContext.d.ts +9 -0
  133. package/dist/readModels/ReadModelChangeContext.d.ts.map +1 -0
  134. package/dist/readModels/ReadModelChangeContext.js +4 -0
  135. package/dist/readModels/ReadModelChangeContext.js.map +1 -0
  136. package/dist/readModels/ReadModelChangeType.d.ts +7 -0
  137. package/dist/readModels/ReadModelChangeType.d.ts.map +1 -0
  138. package/dist/readModels/ReadModelChangeType.js +10 -0
  139. package/dist/readModels/ReadModelChangeType.js.map +1 -0
  140. package/dist/readModels/ReadModelChangeset.d.ts +6 -0
  141. package/dist/readModels/ReadModelChangeset.d.ts.map +1 -1
  142. package/dist/readModels/ReadModelWatchOptions.d.ts +19 -0
  143. package/dist/readModels/ReadModelWatchOptions.d.ts.map +1 -0
  144. package/dist/readModels/ReadModelWatchOptions.js +4 -0
  145. package/dist/readModels/ReadModelWatchOptions.js.map +1 -0
  146. package/dist/readModels/ReadModelWatcher.d.ts +52 -0
  147. package/dist/readModels/ReadModelWatcher.d.ts.map +1 -0
  148. package/dist/readModels/ReadModelWatcher.js +276 -0
  149. package/dist/readModels/ReadModelWatcher.js.map +1 -0
  150. package/dist/readModels/ReadModels.d.ts +11 -1
  151. package/dist/readModels/ReadModels.d.ts.map +1 -1
  152. package/dist/readModels/ReadModels.js +47 -11
  153. package/dist/readModels/ReadModels.js.map +1 -1
  154. package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.d.ts +15 -0
  155. package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.d.ts.map +1 -0
  156. package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.js +45 -0
  157. package/dist/readModels/for_ReadModelWatcher/given/a_kernel_stream.js.map +1 -0
  158. package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.d.ts +21 -0
  159. package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.d.ts.map +1 -0
  160. package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.js +35 -0
  161. package/dist/readModels/for_ReadModelWatcher/given/a_watched_read_model.js.map +1 -0
  162. package/dist/readModels/index.d.ts +4 -0
  163. package/dist/readModels/index.d.ts.map +1 -1
  164. package/dist/readModels/index.js +1 -0
  165. package/dist/readModels/index.js.map +1 -1
  166. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  167. package/dist/testing/ReactorScenario.js +2 -0
  168. package/dist/testing/ReactorScenario.js.map +1 -1
  169. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
  170. package/dist/testing/projections/ProjectionReadModelProcessor.js +2 -1
  171. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
  172. package/dist/transactions/IUnitOfWork.d.ts +3 -1
  173. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  174. package/dist/transactions/TransactionalEventRouting.d.ts +12 -0
  175. package/dist/transactions/TransactionalEventRouting.d.ts.map +1 -0
  176. package/dist/transactions/TransactionalEventRouting.js +4 -0
  177. package/dist/transactions/TransactionalEventRouting.js.map +1 -0
  178. package/dist/transactions/UnitOfWork.d.ts +2 -1
  179. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  180. package/dist/transactions/UnitOfWork.js +11 -2
  181. package/dist/transactions/UnitOfWork.js.map +1 -1
  182. package/dist/transactions/index.d.ts +1 -0
  183. package/dist/transactions/index.d.ts.map +1 -1
  184. package/dist/tsconfig.tsbuildinfo +1 -1
  185. package/dist/types/DecoratorType.d.ts +3 -1
  186. package/dist/types/DecoratorType.d.ts.map +1 -1
  187. package/dist/types/DecoratorType.js +2 -0
  188. package/dist/types/DecoratorType.js.map +1 -1
  189. package/eventSequences/AppendOptions.ts +14 -0
  190. package/eventSequences/EventForEventSourceId.ts +13 -0
  191. package/eventSequences/EventLog.ts +4 -2
  192. package/eventSequences/EventSequence.ts +33 -6
  193. package/eventSequences/ITransactionalEventSequence.ts +5 -2
  194. package/eventSequences/TransactionalEventSequence.ts +5 -4
  195. package/eventSequences/resolveEventSourceRouting.ts +125 -0
  196. package/eventSources/ConcurrencyDimensions.ts +26 -0
  197. package/eventSources/DuplicateEventSourceName.ts +10 -0
  198. package/eventSources/DuplicateEventStreamName.ts +10 -0
  199. package/eventSources/EventRoutingContradictsEventSource.ts +10 -0
  200. package/eventSources/EventSourceDefinition.ts +27 -0
  201. package/eventSources/EventSources.ts +93 -0
  202. package/eventSources/EventStreamDefinition.ts +18 -0
  203. package/eventSources/EventStreamDoesNotBelongToEventSource.ts +10 -0
  204. package/eventSources/EventStreamRequiresEventSource.ts +10 -0
  205. package/eventSources/IEventSources.ts +33 -0
  206. package/eventSources/ResolvedEventRouting.ts +78 -0
  207. package/eventSources/UnknownEventSource.ts +10 -0
  208. package/eventSources/eventSource.ts +116 -0
  209. package/eventSources/for_EventSources/against_a_kernel/when_appending_through_registered_sources.integration.spec.ts +106 -0
  210. package/eventSources/for_EventSources/when_appending_through_a_definition.spec.ts +190 -0
  211. package/eventSources/for_EventSources/when_batch_requires_differing_guards.spec.ts +139 -0
  212. package/eventSources/for_EventSources/when_discovering_and_registering.spec.ts +94 -0
  213. package/eventSources/index.ts +19 -0
  214. package/events/EventContext.ts +6 -0
  215. package/events/WireEventContext.ts +2 -0
  216. package/events/for_EventContext/with_event_source.spec.ts +22 -0
  217. package/events/toClientEventContext.ts +1 -0
  218. package/for_ChronicleClient/when_reconnecting/with_waiting_read_model_watchers.spec.ts +117 -0
  219. package/for_EventStore/when_implementing_a_legacy_event_store.spec.ts +19 -0
  220. package/index.ts +3 -0
  221. package/jobs/for_Jobs/against_a_kernel/when_managing_a_replay_job.integration.spec.ts +24 -4
  222. package/package.json +3 -3
  223. package/projections/eventContextPropertyExpression.ts +2 -1
  224. package/reactors/for_Reactors/against_a_kernel/when_replaying_reactors_with_once_only_handlers.integration.spec.ts +27 -8
  225. package/readModels/IReadModelWatcher.ts +35 -0
  226. package/readModels/IReadModels.ts +17 -2
  227. package/readModels/ReadModelChangeContext.ts +11 -0
  228. package/readModels/ReadModelChangeType.ts +9 -0
  229. package/readModels/ReadModelChangeset.ts +9 -0
  230. package/readModels/ReadModelWatchOptions.ts +23 -0
  231. package/readModels/ReadModelWatcher.ts +271 -0
  232. package/readModels/ReadModels.ts +60 -11
  233. package/readModels/for_ReadModelWatcher/against_a_kernel/when_watching_changes.integration.spec.ts +104 -0
  234. package/readModels/for_ReadModelWatcher/given/a_kernel_stream.ts +50 -0
  235. package/readModels/for_ReadModelWatcher/given/a_watched_read_model.ts +37 -0
  236. package/readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts +29 -0
  237. package/readModels/for_ReadModelWatcher/when_a_stream_cannot_recover.spec.ts +86 -0
  238. package/readModels/for_ReadModelWatcher/when_applying_backpressure.spec.ts +80 -0
  239. package/readModels/for_ReadModelWatcher/when_failing_with_buffered_changes.spec.ts +39 -0
  240. package/readModels/for_ReadModelWatcher/when_not_opting_into_resume.spec.ts +62 -0
  241. package/readModels/for_ReadModelWatcher/when_notifying_resubscription.spec.ts +128 -0
  242. package/readModels/for_ReadModelWatcher/when_opting_into_overflow.spec.ts +39 -0
  243. package/readModels/for_ReadModelWatcher/when_receiving_changes.spec.ts +69 -0
  244. package/readModels/for_ReadModelWatcher/when_reconnecting.spec.ts +99 -0
  245. package/readModels/for_ReadModelWatcher/when_reconnecting_during_conversion.spec.ts +84 -0
  246. package/readModels/for_ReadModelWatcher/when_recovering_a_stream.spec.ts +106 -0
  247. package/readModels/for_ReadModelWatcher/when_stopping.spec.ts +85 -0
  248. package/readModels/for_ReadModelWatcher/when_subscribing.spec.ts +62 -0
  249. package/readModels/for_ReadModels/when_watching.spec.ts +85 -0
  250. package/readModels/index.ts +4 -0
  251. package/testing/EventScenario.event-sources.spec.ts +34 -0
  252. package/testing/ReactorScenario.spec.ts +18 -1
  253. package/testing/ReactorScenario.ts +1 -0
  254. package/testing/projections/ProjectionReadModelProcessor.ts +2 -1
  255. package/transactions/IUnitOfWork.ts +3 -1
  256. package/transactions/TransactionalEventRouting.ts +16 -0
  257. package/transactions/UnitOfWork.ts +13 -2
  258. package/transactions/for_UnitOfWork/when_committing_events_with_event_source_routing.spec.ts +41 -0
  259. package/transactions/index.ts +1 -0
  260. package/types/DecoratorType.ts +4 -1
@@ -0,0 +1,84 @@
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 { ReadModelWatcher } from '../ReadModelWatcher.js';
6
+ import { a_watched_read_model } from './given/a_watched_read_model.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when reconnecting during read model conversion', () => {
11
+ let context: a_watched_read_model;
12
+ let watcher: ReadModelWatcher<object>;
13
+ let finishConversion: () => void;
14
+ let failConversion: (error: unknown) => void;
15
+ const converted: string[] = [];
16
+ beforeEach(async () => {
17
+ converted.length = 0;
18
+ context = new a_watched_read_model();
19
+ await context.connect();
20
+ const conversion = new Promise<void>((resolve, reject) => {
21
+ finishConversion = resolve;
22
+ failConversion = reject;
23
+ });
24
+ watcher = new ReadModelWatcher(signal => context.watch(undefined, { signal }), async change => {
25
+ converted.push(change.ModelKey);
26
+ if (change.ModelKey === 'received') await conversion;
27
+ return { namespace: change.Namespace, key: change.ModelKey, readModel: {}, removed: false };
28
+ }, new AbortController().signal, context.lifecycle, { resume: true });
29
+ context.streams[0].send({ Subscribed: true });
30
+ await watcher.subscribed;
31
+ context.streams[0].send({ ModelKey: 'received' });
32
+ await context.flush();
33
+ await context.disconnect();
34
+ await context.connect();
35
+ context.streams[1].send({ Subscribed: true });
36
+ context.streams[1].send({ ModelKey: 'resumed' });
37
+ await watcher.subscribed;
38
+ });
39
+ afterEach(() => {
40
+ finishConversion();
41
+ watcher.dispose();
42
+ context.readModels.dispose();
43
+ });
44
+
45
+ it('should finish received conversion and preserve order across connections', async () => {
46
+ await context.flush();
47
+ converted.should.deep.equal(['received']);
48
+ finishConversion();
49
+ (await watcher.next()).value.key.should.equal('received');
50
+ (await watcher.next()).value.key.should.equal('resumed');
51
+ converted.should.deep.equal(['received', 'resumed']);
52
+ });
53
+
54
+ it('should not replay an acknowledgment to a callback registered while earlier conversion finishes', async () => {
55
+ let notifications = 0;
56
+ watcher.onResubscribed(() => { notifications++; });
57
+ finishConversion();
58
+ await context.flush();
59
+ notifications.should.equal(0);
60
+ await context.disconnect();
61
+ await context.connect();
62
+ context.streams[2].send({ Subscribed: true });
63
+ await context.flush();
64
+ notifications.should.equal(1);
65
+ });
66
+
67
+ it('should still fail on a received change that cannot be released after reconnect', async () => {
68
+ const failure = { code: 14 };
69
+ const next = watcher.next().catch(error => error);
70
+ failConversion(failure);
71
+ (await next).should.equal(failure);
72
+ context.streams[1].signal.aborted.should.be.true;
73
+ converted.should.deep.equal(['received']);
74
+ });
75
+
76
+ it('should not deliver an in-flight conversion after disposal', async () => {
77
+ const next = watcher.next();
78
+ watcher.dispose();
79
+ finishConversion();
80
+ (await next).done!.should.be.true;
81
+ await context.flush();
82
+ (await watcher.next()).done!.should.be.true;
83
+ });
84
+ });
@@ -0,0 +1,106 @@
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 type { IReadModelWatcher } from '../IReadModelWatcher.js';
6
+ import { a_watched_read_model } from './given/a_watched_read_model.js';
7
+
8
+ chai.should();
9
+
10
+ const recoveries = [1, 4, 13, 14, undefined].flatMap(code => ['option', 'callback'].map(optIn => ({ code, optIn })));
11
+
12
+ describe.each(recoveries)('when resumption uses $optIn after stream status $code', ({ code, optIn }) => {
13
+ let context: a_watched_read_model;
14
+ let watcher: IReadModelWatcher<{ id: string }>;
15
+ const endStream = () => {
16
+ if (code === undefined) context.streams[0].end();
17
+ else context.streams[0].fail({ code });
18
+ };
19
+ beforeEach(async () => {
20
+ vi.useFakeTimers();
21
+ context = new a_watched_read_model();
22
+ await context.connect();
23
+ watcher = context.readModels.createWatcher(context.model, { resume: optIn === 'option' });
24
+ if (optIn === 'callback') watcher.onResubscribed(() => {});
25
+ });
26
+ afterEach(() => {
27
+ context.readModels.dispose();
28
+ vi.useRealTimers();
29
+ });
30
+
31
+ it('should renew readiness and deliver the next connection change to the same pending iterator', async () => {
32
+ context.streams[0].send({ Subscribed: true });
33
+ const acknowledged = watcher.subscribed;
34
+ await acknowledged;
35
+ const iterator = watcher[Symbol.asyncIterator]();
36
+ const next = iterator.next();
37
+ endStream();
38
+ await vi.advanceTimersByTimeAsync(0);
39
+ watcher.subscribed.should.not.equal(acknowledged);
40
+ const readiness = watcher.subscribed;
41
+ await context.disconnect();
42
+ watcher.subscribed.should.equal(readiness);
43
+ await vi.advanceTimersByTimeAsync(1000);
44
+ context.streams.should.have.lengthOf(1);
45
+ await context.connect();
46
+ context.streams[1].send({ Subscribed: true });
47
+ await readiness;
48
+ context.streams[1].send({ ModelKey: 'next', ReadModel: '{}' });
49
+ (await next).value.key.should.equal('next');
50
+ await vi.advanceTimersByTimeAsync(1000);
51
+ context.streams.should.have.lengthOf(2);
52
+ });
53
+
54
+ it('should preserve pending readiness and retry if the lifecycle never disconnects', async () => {
55
+ const readiness = watcher.subscribed;
56
+ endStream();
57
+ await vi.advanceTimersByTimeAsync(0);
58
+ watcher.subscribed.should.equal(readiness);
59
+ context.streams.should.have.lengthOf(1);
60
+ await vi.advanceTimersByTimeAsync(1000);
61
+ context.streams.should.have.lengthOf(2);
62
+ context.streams[1].send({ Subscribed: true });
63
+ await readiness;
64
+ });
65
+
66
+ it('should notify resubscription only after recovery acknowledgment and continue iteration', async () => {
67
+ const resubscribed = vi.fn();
68
+ watcher.onResubscribed(resubscribed);
69
+ context.streams[0].send({ Subscribed: true });
70
+ await watcher.subscribed;
71
+ resubscribed.mock.calls.should.have.lengthOf(0);
72
+ const next = watcher[Symbol.asyncIterator]().next();
73
+ endStream();
74
+ await vi.advanceTimersByTimeAsync(1000);
75
+ resubscribed.mock.calls.should.have.lengthOf(0);
76
+ context.streams[1].send({ Subscribed: true });
77
+ context.streams[1].send({ ModelKey: 'resumed', ReadModel: '{}' });
78
+ (await next).value.key.should.equal('resumed');
79
+ resubscribed.mock.calls.should.have.lengthOf(1);
80
+ });
81
+
82
+ it('should keep received buffered changes after a transport failure or completion', async () => {
83
+ context.streams[0].send({ Subscribed: true });
84
+ await watcher.subscribed;
85
+ context.streams[0].send({ ModelKey: 'received', ReadModel: '{}' });
86
+ await vi.advanceTimersByTimeAsync(0);
87
+ endStream();
88
+ await vi.advanceTimersByTimeAsync(1000);
89
+ context.streams[1].send({ Subscribed: true });
90
+ context.streams[1].send({ ModelKey: 'resumed', ReadModel: '{}' });
91
+ await watcher.subscribed;
92
+ const iterator = watcher[Symbol.asyncIterator]();
93
+ (await iterator.next()).value.key.should.equal('received');
94
+ (await iterator.next()).value.key.should.equal('resumed');
95
+ });
96
+
97
+ it('should cancel a scheduled restart when disposed', async () => {
98
+ const readiness = watcher.subscribed.catch(error => error);
99
+ endStream();
100
+ await vi.advanceTimersByTimeAsync(0);
101
+ watcher.dispose();
102
+ await vi.advanceTimersByTimeAsync(1000);
103
+ (await readiness).name.should.equal('AbortError');
104
+ context.streams.should.have.lengthOf(1);
105
+ });
106
+ });
@@ -0,0 +1,85 @@
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 type { IReadModelWatcher } from '../IReadModelWatcher.js';
6
+ import { a_watched_read_model } from './given/a_watched_read_model.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when stopping a read model watcher', () => {
11
+ let context: a_watched_read_model;
12
+ let watcher: IReadModelWatcher<{ id: string }>;
13
+ let cancellation: AbortController;
14
+ beforeEach(async () => {
15
+ context = new a_watched_read_model();
16
+ await context.connect();
17
+ cancellation = new AbortController();
18
+ watcher = context.readModels.createWatcher(context.model, { signal: cancellation.signal });
19
+ });
20
+ afterEach(() => context.readModels.dispose());
21
+
22
+ it('should cancel the transport and settle pending iteration and readiness on disposal', async () => {
23
+ const readiness = watcher.subscribed.catch(error => error);
24
+ const next = watcher[Symbol.asyncIterator]().next();
25
+ watcher.dispose();
26
+ watcher.dispose();
27
+ context.streams[0].signal.aborted.should.be.true;
28
+ (await readiness).name.should.equal('AbortError');
29
+ (await next).done!.should.be.true;
30
+ });
31
+
32
+ it('should complete iteration when the caller aborts', async () => {
33
+ const readiness = watcher.subscribed.catch(error => error);
34
+ const next = watcher[Symbol.asyncIterator]().next();
35
+ const reason = new Error('caller cancelled');
36
+ cancellation.abort(reason);
37
+ (await readiness).should.equal(reason);
38
+ (await next).done!.should.be.true;
39
+ context.streams[0].signal.aborted.should.be.true;
40
+ });
41
+
42
+ it('should not open a stream with an already aborted signal', async () => {
43
+ watcher.dispose();
44
+ cancellation.abort();
45
+ watcher = context.readModels.createWatcher(context.model, { signal: cancellation.signal });
46
+ const failure = await watcher.subscribed.catch(error => error);
47
+ failure.name.should.equal('AbortError');
48
+ context.streams.should.have.lengthOf(1);
49
+ });
50
+
51
+ it('should cancel on an early for await break', async () => {
52
+ context.streams[0].send({ Subscribed: true });
53
+ context.streams[0].send({ ReadModel: '{"id":"one"}' });
54
+ for await (const change of watcher) {
55
+ change.readModel.id.should.equal('one');
56
+ break;
57
+ }
58
+ context.streams[0].signal.aborted.should.be.true;
59
+ });
60
+
61
+ it('should cancel watchers when their owner is disposed', async () => {
62
+ const readiness = watcher.subscribed.catch(error => error);
63
+ context.readModels.dispose();
64
+ (await readiness).name.should.equal('AbortError');
65
+ context.streams[0].signal.aborted.should.be.true;
66
+ });
67
+
68
+ it('should surface a stream failure to readiness and iteration', async () => {
69
+ const failure = new Error('transport failed');
70
+ const readiness = watcher.subscribed.catch(error => error);
71
+ const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
72
+ context.streams[0].fail(failure);
73
+ (await readiness).should.equal(failure);
74
+ (await next).should.equal(failure);
75
+ });
76
+
77
+ it('should surface a later stream failure without changing an already resolved promise', async () => {
78
+ context.streams[0].send({ Subscribed: true });
79
+ await watcher.subscribed;
80
+ const next = watcher[Symbol.asyncIterator]().next().catch(error => error);
81
+ context.streams[0].fail(new Error('later failure'));
82
+ (await next).message.should.equal('later failure');
83
+ await watcher.subscribed;
84
+ });
85
+ });
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterEach, beforeEach, chai, describe, it } from 'vitest';
5
+ import type { IReadModelWatcher } from '../IReadModelWatcher.js';
6
+ import { a_watched_read_model } from './given/a_watched_read_model.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when subscribing to read model changes', () => {
11
+ let context: a_watched_read_model;
12
+ let watcher: IReadModelWatcher<{ id: string }>;
13
+ beforeEach(async () => {
14
+ context = new a_watched_read_model();
15
+ await context.connect();
16
+ watcher = context.readModels.createWatcher(context.model);
17
+ });
18
+ afterEach(() => watcher.dispose());
19
+
20
+ it('should start the stream without iteration and await the actual acknowledgment', async () => {
21
+ let ready = false;
22
+ void watcher.subscribed.then(() => { ready = true; });
23
+ await context.flush();
24
+ ready.should.be.false;
25
+ context.watch.mock.calls[0][0].should.deep.equal({
26
+ EventStore: 'store', Namespace: 'tenant', ReadModelIdentifier: 'Model', EventSequenceId: 'event-log'
27
+ });
28
+ context.streams[0].send({ Subscribed: true });
29
+ await watcher.subscribed;
30
+ ready.should.be.true;
31
+ });
32
+
33
+ it('should not yield subscription markers or lose a change before iteration begins', async () => {
34
+ context.streams[0].send({ Subscribed: true });
35
+ context.streams[0].send({ Namespace: 'tenant', ModelKey: 'one', ReadModel: '{"id":"one"}' });
36
+ await watcher.subscribed;
37
+ await context.flush();
38
+ const result = await watcher[Symbol.asyncIterator]().next();
39
+ result.done!.should.be.false;
40
+ result.value.key.should.equal('one');
41
+ });
42
+
43
+ it('should acknowledge without iteration even when changes arrive before the marker', async () => {
44
+ context.streams[0].send({ ModelKey: 'one', ReadModel: '{"id":"one"}' });
45
+ context.streams[0].send({ ModelKey: 'two', ReadModel: '{"id":"two"}' });
46
+ context.streams[0].send({ Subscribed: true });
47
+ await watcher.subscribed;
48
+ const iterator = watcher[Symbol.asyncIterator]();
49
+ (await iterator.next()).value.key.should.equal('one');
50
+ (await iterator.next()).value.key.should.equal('two');
51
+ });
52
+
53
+ it('should fail explicitly on pre-acknowledgment overflow instead of hanging or silently dropping changes', async () => {
54
+ const failure = watcher.subscribed.catch(error => error);
55
+ for (let index = 0; index <= 1024; index++) context.streams[0].send({ ReadModel: '{}' });
56
+ context.streams[0].send({ Subscribed: true });
57
+ (await failure).message.should.contain('buffer exceeded 1024 changes');
58
+ const iterationFailure = await watcher[Symbol.asyncIterator]().next().catch(error => error);
59
+ iterationFailure.should.equal(await failure);
60
+ context.streams[0].signal.aborted.should.be.true;
61
+ });
62
+ });
@@ -0,0 +1,85 @@
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 { ReadModelChangeType as ContractChangeType } from '@cratis/chronicle.contracts';
5
+ import { Guid } from '@cratis/fundamentals';
6
+ import { afterEach, beforeEach, chai, describe, it } from 'vitest';
7
+ import { toContractsGuid } from '../../connection/Guid.js';
8
+ import { ReadModelChangeType } from '../ReadModelChangeType.js';
9
+ import { a_watched_read_model } from '../for_ReadModelWatcher/given/a_watched_read_model.js';
10
+
11
+ chai.should();
12
+
13
+ describe('when watching with the original pull-based API', () => {
14
+ let context: a_watched_read_model;
15
+ beforeEach(async () => {
16
+ context = new a_watched_read_model();
17
+ await context.connect();
18
+ });
19
+ afterEach(() => context.readModels.dispose());
20
+
21
+ it('should not open or read the stream until the first next call', async () => {
22
+ const changes = context.readModels.watch(context.model);
23
+ await context.flush();
24
+ context.watch.mock.calls.should.have.lengthOf(0);
25
+ const iterator = changes[Symbol.asyncIterator]();
26
+ await context.flush();
27
+ context.watch.mock.calls.should.have.lengthOf(0);
28
+ const next = iterator.next();
29
+ context.watch.mock.calls.should.have.lengthOf(1);
30
+ context.streams[0].pulls.should.equal(1);
31
+ context.streams[0].send({ ModelKey: 'first', ReadModel: '{}' });
32
+ (await next).value.key.should.equal('first');
33
+ await iterator.return!();
34
+ context.streams[0].closed.should.be.true;
35
+ });
36
+
37
+ it('should defer model resolution failures until iteration starts', async () => {
38
+ const changes = context.readModels.watch(class Unknown {});
39
+ context.watch.mock.calls.should.have.lengthOf(0);
40
+ const failure = await changes[Symbol.asyncIterator]().next().catch(error => error);
41
+ failure.message.should.contain("Unknown read model 'Unknown'");
42
+ });
43
+
44
+ it.each([0, 2050, undefined])('should pull more than the watcher buffer limit without read-ahead or overflow (acknowledgment after %s changes)', async acknowledgeAfter => {
45
+ const iterator = context.readModels.watch(context.model)[Symbol.asyncIterator]();
46
+ const first = iterator.next();
47
+ const stream = context.streams[0];
48
+ if (acknowledgeAfter === 0) stream.send({ Subscribed: true });
49
+ for (let index = 0; index < 2050; index++) {
50
+ stream.send({ ModelKey: `${index}`, ReadModel: '{}' });
51
+ }
52
+ if (acknowledgeAfter === 2050) stream.send({ Subscribed: true });
53
+ stream.end();
54
+ try {
55
+ for (let index = 0; index < 2050; index++) {
56
+ const result = await (index === 0 ? first : iterator.next());
57
+ result.done!.should.be.false;
58
+ result.value.key.should.equal(`${index}`);
59
+ if (index === 0 || index === 1024 || index === 2049) {
60
+ // Simulate a slow consumer: no next() means no further transport pulls.
61
+ await context.flush();
62
+ stream.pulls.should.equal(index + 1 + (acknowledgeAfter === 0 ? 1 : 0));
63
+ }
64
+ }
65
+ (await iterator.next()).done!.should.be.true;
66
+ } finally {
67
+ await iterator.return!();
68
+ }
69
+ });
70
+
71
+ it('should retain optional change kind and context on pull-based changes', async () => {
72
+ const iterator = context.readModels.watch(context.model)[Symbol.asyncIterator]();
73
+ const next = iterator.next();
74
+ const correlationId = '12345678-1234-5678-9abc-123456789abc';
75
+ const occurred = '2026-06-07T08:09:10.123Z';
76
+ context.streams[0].send({ Namespace: 'tenant', ModelKey: 'one', ReadModel: '{"id":"one"}',
77
+ ChangeType: ContractChangeType.Added, EventSequenceNumber: 9007199254740997n,
78
+ CorrelationId: toContractsGuid(Guid.as(correlationId)), Occurred: { Value: occurred } });
79
+ const change = (await next).value;
80
+ change.changeType.should.equal(ReadModelChangeType.Added);
81
+ change.changeContext.should.deep.equal({ eventStore: 'store', namespace: 'tenant',
82
+ sequenceNumber: 9007199254740997n, correlationId, occurred: new Date(occurred) });
83
+ await iterator.return!();
84
+ });
85
+ });
@@ -9,6 +9,10 @@ export { ReadModels } from './ReadModels.js';
9
9
  export { MaterializedReadModels } from './MaterializedReadModels.js';
10
10
  export { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
11
11
  export type { ReadModelChangeset } from './ReadModelChangeset.js';
12
+ export type { IReadModelWatcher } from './IReadModelWatcher.js';
13
+ export type { ReadModelWatchOptions } from './ReadModelWatchOptions.js';
14
+ export type { ReadModelChangeContext } from './ReadModelChangeContext.js';
15
+ export { ReadModelChangeType } from './ReadModelChangeType.js';
12
16
  export type { ReadModelSnapshot } from './ReadModelSnapshot.js';
13
17
  export type { ReadModelNamingPolicy } from './ReadModelNamingPolicy.js';
14
18
  export { readModel, getReadModelMetadata, isReadModel } from './readModel.js';
@@ -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 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { field } from '@cratis/fundamentals';
7
+ import { eventType } from '../events/eventTypeDecorator.js';
8
+ import { EventScenario, UnsupportedEventSequenceOperation } from './index.js';
9
+
10
+ chai.should();
11
+ class ScenarioSourceEvent { @field(String) name: string; constructor(name: string) { this.name = name; } }
12
+ eventType('ScenarioSourceEvent')(ScenarioSourceEvent);
13
+
14
+ const rejects = async (action: () => Promise<unknown>) => {
15
+ let caught: unknown;
16
+ try { await action(); } catch (error) { caught = error; }
17
+ (caught instanceof UnsupportedEventSequenceOperation).should.be.true;
18
+ };
19
+ const create = () => new EventScenario({ artifacts: { eventTypes: [ScenarioSourceEvent] } });
20
+
21
+ describe('when appending with registered event sources in an in-process scenario', () => {
22
+ it('should reject the eventSource option on append instead of dropping it', async () => {
23
+ await rejects(() => create().append('A', new ScenarioSourceEvent('x'), { eventSource: 'Account' }));
24
+ });
25
+ it('should reject the eventSource option on appendMany instead of dropping it', async () => {
26
+ await rejects(() => create().appendMany([{ eventSourceId: 'A', event: new ScenarioSourceEvent('x') }], { eventSource: 'Account' }));
27
+ });
28
+ it('should reject a per-event eventSource on appendMany instead of dropping it', async () => {
29
+ await rejects(() => create().appendMany([{ eventSourceId: 'A', event: new ScenarioSourceEvent('x'), eventSource: 'Account' }]));
30
+ });
31
+ it('should reject a per-event eventStream on appendMany instead of dropping it', async () => {
32
+ await rejects(() => create().appendMany([{ eventSourceId: 'A', event: new ScenarioSourceEvent('x'), eventStream: 'Transactions' }]));
33
+ });
34
+ });
@@ -175,18 +175,35 @@ describe('ReactorScenario live delivery', () => {
175
175
  try {
176
176
  for await (const _change of services.readModels.watch(Registered)) { /* Unsupported. */ }
177
177
  } catch (error) { errors.push(error); }
178
+ try {
179
+ await services.readModels.createWatcher(Registered).subscribed;
180
+ } catch (error) { errors.push(error); }
178
181
  }
179
182
  }
180
183
  const scenario = new ReactorScenario(ServiceRejection, options);
181
184
  let failure: unknown;
182
185
  await scenario.when.forEventSource('A').events(new Registered('first')).catch(error => { failure = error; });
183
- errors.length.should.equal(10);
186
+ errors.length.should.equal(11);
184
187
  errors.every(error => error instanceof UnsupportedReactorOperation && String(error).includes('Use a kernel-backed test.')).should.be.true;
185
188
  // Swallowed rejections still fail the delivery with the first unsupported operation.
186
189
  (failure === errors[0]).should.be.true;
187
190
  scenario.results[0].completed.should.be.false;
188
191
  });
189
192
 
193
+ it('should complete delivery if a reactor creates a watch without iterating it', async () => {
194
+ @reactor('uniterated-watch-reactor')
195
+ class UniteratedWatch {
196
+ registered(_event: Registered, _context: EventContext, services: ReactorServices) {
197
+ services.readModels.watch(Registered);
198
+ return new Skipped('produced');
199
+ }
200
+ }
201
+ const scenario = new ReactorScenario(UniteratedWatch, options);
202
+ await scenario.when.forEventSource('A').events(new Registered('first'));
203
+ scenario.results[0].completed.should.be.true;
204
+ scenario.shouldHaveProduced(Skipped, event => event.label === 'produced');
205
+ });
206
+
190
207
  it('rejects behavior pattern queries through the default event store', async () => {
191
208
  let error: unknown;
192
209
  @reactor('pattern-rejection-reactor')
@@ -105,6 +105,7 @@ export class ReactorScenario {
105
105
  'getSnapshotsById', 'dehydrateSession', 'release', 'releaseMany']);
106
106
  const readModels = new Proxy({}, { get: (_, key) => {
107
107
  if (key === 'watch') return async function* () { unsupported('readModels.watch'); };
108
+ if (key === 'createWatcher') return () => unsupported('readModels.createWatcher');
108
109
  if (key === 'materialized') return unsupported('readModels.materialized');
109
110
  if (typeof key !== 'string' || !readModelMethods.has(key)) return undefined;
110
111
  return async () => unsupported(`readModels.${key}`);
@@ -138,7 +138,8 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
138
138
  if (!state) {
139
139
  // SetInitialState: an event only a child subscribes to creates an uninitialized parent.
140
140
  state = from ? this.initialState(key, event) : this.identityState(key, event, false);
141
- } else if (state.__initialized === false) {
141
+ } else if (state.__initialized === false && from) {
142
+ // Chronicle 19.30.0 no longer initializes a parent on events only a child subscribes to.
142
143
  for (const [name, value] of Object.entries(this.initialValues())) if (!(name in state)) state[name] = value;
143
144
  state.__initialized = true;
144
145
  }
@@ -6,6 +6,7 @@ import { AppendError } from '../eventSequences/AppendError.js';
6
6
  import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation.js';
7
7
  import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
8
8
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
9
+ import { TransactionalEventRouting } from './TransactionalEventRouting.js';
9
10
  import { AppendResult } from '../eventSequences/AppendResult.js';
10
11
 
11
12
  /**
@@ -26,8 +27,9 @@ export interface IUnitOfWork {
26
27
  * @param eventSequenceId - The identifier of the event sequence to append to.
27
28
  * @param eventSourceId - The event source identifier.
28
29
  * @param event - The event payload.
30
+ * @param routing - Optional registered event source / stream routing for this event.
29
31
  */
30
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object): void;
32
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, routing?: TransactionalEventRouting): void;
31
33
 
32
34
  /**
33
35
  * Gets all events currently buffered in this unit of work.
@@ -0,0 +1,16 @@
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
+
6
+ /**
7
+ * Optional registered event source routing for an event added to a unit of work.
8
+ * Carried through to the append on commit, where it is validated like any other append.
9
+ */
10
+ export interface TransactionalEventRouting {
11
+ /** Registered event source definition (class decorated with `@eventSource`, or its name). */
12
+ readonly eventSource?: Constructor | string;
13
+
14
+ /** Stream name declared by {@link eventSource}. */
15
+ readonly eventStream?: string;
16
+ }
@@ -8,7 +8,9 @@ import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation.js'
8
8
  import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
9
9
  import { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
10
10
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
11
+ import { planDerivedGuards, resolveBatchRouting } from '../eventSequences/resolveEventSourceRouting.js';
11
12
  import { IEventStore } from '../IEventStore.js';
13
+ import { TransactionalEventRouting } from './TransactionalEventRouting.js';
12
14
  import { IUnitOfWork } from './IUnitOfWork.js';
13
15
 
14
16
  interface EventForEventSourceIdWithSequenceNumber {
@@ -46,14 +48,16 @@ export class UnitOfWork implements IUnitOfWork {
46
48
  }
47
49
 
48
50
  /** @inheritdoc */
49
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object): void {
51
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, routing?: TransactionalEventRouting): void {
50
52
  this.throwIfCompleted();
51
53
  this._events.push({
52
54
  sequenceNumber: this._events.length,
53
55
  eventSequenceId,
54
56
  eventForEventSourceId: {
55
57
  eventSourceId,
56
- event
58
+ event,
59
+ ...(routing?.eventSource !== undefined ? { eventSource: routing.eventSource } : {}),
60
+ ...(routing?.eventStream !== undefined ? { eventStream: routing.eventStream } : {})
57
61
  }
58
62
  });
59
63
  }
@@ -100,6 +104,13 @@ export class UnitOfWork implements IUnitOfWork {
100
104
  eventsByEventSequence.set(key, events);
101
105
  }
102
106
 
107
+ // Resolve routing and required guards for every group first: a rejection must not follow a written group.
108
+ const eventSources = this._eventStore.eventSources;
109
+ for (const eventsForSequence of eventSources ? eventsByEventSequence.values() : []) {
110
+ const events = eventsForSequence.map(_ => _.eventForEventSourceId);
111
+ planDerivedGuards(events.map(_ => resolveBatchRouting(eventSources, _)), events, undefined);
112
+ }
113
+
103
114
  for (const [eventSequenceId, eventsForSequence] of eventsByEventSequence) {
104
115
  const sequence = this._eventStore.getEventSequence(new EventSequenceId(eventSequenceId));
105
116
  const appendResults = await sequence.appendMany(
@@ -0,0 +1,41 @@
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 { Guid } from '@cratis/fundamentals';
5
+ import { beforeEach, chai, describe, it, vi } from 'vitest';
6
+ import { EventSequenceId } from '../../eventSequences/index.js';
7
+ import type { IEventStore } from '../../IEventStore.js';
8
+ import { UnitOfWork } from '../UnitOfWork.js';
9
+
10
+ const should = chai.should();
11
+
12
+ describe('when committing events with event source routing in a unit of work', () => {
13
+ let batch: any[];
14
+ beforeEach(async () => {
15
+ const appendMany = vi.fn().mockResolvedValue([{ isSuccess: true, constraintViolations: [], errors: [] }, { isSuccess: true, constraintViolations: [], errors: [] }]);
16
+ const store = { getEventSequence: () => ({ appendMany }) } as unknown as IEventStore;
17
+ const unitOfWork = new UnitOfWork(Guid.create(), () => {}, store);
18
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { first: 1 }, { eventSource: 'Account', eventStream: 'Transactions' });
19
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'c-1', { second: 2 }, { eventSource: 'Customer' });
20
+ await unitOfWork.commit();
21
+ batch = appendMany.mock.calls[0][0];
22
+ });
23
+ it('should keep the source and stream of the first event', () => {
24
+ batch[0].eventSource.should.equal('Account');
25
+ batch[0].eventStream.should.equal('Transactions');
26
+ });
27
+ it('should keep a different source on the second event without a stream', () => {
28
+ batch[1].eventSource.should.equal('Customer');
29
+ should.not.exist(batch[1].eventStream);
30
+ });
31
+ });
32
+
33
+ describe('when adding an event to a unit of work without routing', () => {
34
+ it('should not add routing keys to the buffered event', async () => {
35
+ const appendMany = vi.fn().mockResolvedValue([{ isSuccess: true, constraintViolations: [], errors: [] }]);
36
+ const unitOfWork = new UnitOfWork(Guid.create(), () => {}, { getEventSequence: () => ({ appendMany }) } as unknown as IEventStore);
37
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', {});
38
+ await unitOfWork.commit();
39
+ Object.keys(appendMany.mock.calls[0][0][0]).sort().should.deep.equal(['event', 'eventSourceId']);
40
+ });
41
+ });
@@ -6,3 +6,4 @@ export type { IUnitOfWorkManager } from './IUnitOfWorkManager.js';
6
6
  export { NoUnitOfWorkHasBeenStarted } from './NoUnitOfWorkHasBeenStarted.js';
7
7
  export { UnitOfWork } from './UnitOfWork.js';
8
8
  export { UnitOfWorkManager } from './UnitOfWorkManager.js';
9
+ export type { TransactionalEventRouting } from './TransactionalEventRouting.js';