@cratis/chronicle 6.9.1 → 6.11.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 (288) hide show
  1. package/ChronicleClient.ts +52 -13
  2. package/ChronicleOptions.spec.ts +54 -0
  3. package/ChronicleOptions.ts +20 -5
  4. package/EventStore.ts +7 -1
  5. package/README.md +2 -3
  6. package/artifacts/DefaultClientArtifactsProvider.ts +4 -1
  7. package/connection/ChronicleConnection.auth.spec.ts +270 -0
  8. package/connection/ChronicleConnection.ts +90 -31
  9. package/connection/ChronicleServices.ts +1 -1
  10. package/connection/CompatibilityPreflight.ts +1 -1
  11. package/connection/RejectedChronicleCredentials.ts +10 -0
  12. package/connection/TokenProvider.spec.ts +5 -6
  13. package/connection/TokenProvider.ts +26 -17
  14. package/connection/clientVersion.spec.ts +27 -0
  15. package/connection/clientVersion.ts +9 -0
  16. package/connection/describeRequestError.spec.ts +22 -0
  17. package/connection/fetchOAuthAccessToken.ts +33 -2
  18. package/dist/ChronicleClient.d.ts +3 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +55 -13
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +5 -1
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +20 -5
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +2 -0
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +6 -1
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  31. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -1
  32. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  33. package/dist/connection/ChronicleConnection.d.ts +3 -3
  34. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  35. package/dist/connection/ChronicleConnection.js +98 -26
  36. package/dist/connection/ChronicleConnection.js.map +1 -1
  37. package/dist/connection/ChronicleServices.d.ts +1 -2
  38. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  39. package/dist/connection/CompatibilityPreflight.js +1 -1
  40. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  41. package/dist/connection/RejectedChronicleCredentials.d.ts +5 -0
  42. package/dist/connection/RejectedChronicleCredentials.d.ts.map +1 -0
  43. package/dist/connection/RejectedChronicleCredentials.js +10 -0
  44. package/dist/connection/RejectedChronicleCredentials.js.map +1 -0
  45. package/dist/connection/TokenProvider.d.ts +8 -6
  46. package/dist/connection/TokenProvider.d.ts.map +1 -1
  47. package/dist/connection/TokenProvider.js +25 -11
  48. package/dist/connection/TokenProvider.js.map +1 -1
  49. package/dist/connection/clientVersion.d.ts +3 -0
  50. package/dist/connection/clientVersion.d.ts.map +1 -0
  51. package/dist/connection/clientVersion.js +7 -0
  52. package/dist/connection/clientVersion.js.map +1 -0
  53. package/dist/connection/fetchOAuthAccessToken.d.ts +12 -0
  54. package/dist/connection/fetchOAuthAccessToken.d.ts.map +1 -1
  55. package/dist/connection/fetchOAuthAccessToken.js +37 -2
  56. package/dist/connection/fetchOAuthAccessToken.js.map +1 -1
  57. package/dist/eventSequences/AppendResult.d.ts +7 -0
  58. package/dist/eventSequences/AppendResult.d.ts.map +1 -1
  59. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  60. package/dist/eventSequences/EventSequence.js +26 -17
  61. package/dist/eventSequences/EventSequence.js.map +1 -1
  62. package/dist/eventSequences/WaitForCompletionOptions.d.ts +8 -0
  63. package/dist/eventSequences/WaitForCompletionOptions.d.ts.map +1 -0
  64. package/dist/eventSequences/WaitForCompletionOptions.js +4 -0
  65. package/dist/eventSequences/WaitForCompletionOptions.js.map +1 -0
  66. package/dist/eventSequences/index.d.ts +1 -0
  67. package/dist/eventSequences/index.d.ts.map +1 -1
  68. package/dist/eventSequences/index.js.map +1 -1
  69. package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
  70. package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
  71. package/dist/externalServices/ExternalServiceBuilder.js +1 -1
  72. package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
  73. package/dist/externalServices/ExternalServices.d.ts.map +1 -1
  74. package/dist/externalServices/ExternalServices.js +3 -1
  75. package/dist/externalServices/ExternalServices.js.map +1 -1
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/projections/Projections.d.ts.map +1 -1
  81. package/dist/projections/Projections.js +9 -7
  82. package/dist/projections/Projections.js.map +1 -1
  83. package/dist/projections/constantValueExpression.d.ts +3 -0
  84. package/dist/projections/constantValueExpression.d.ts.map +1 -0
  85. package/dist/projections/constantValueExpression.js +26 -0
  86. package/dist/projections/constantValueExpression.js.map +1 -0
  87. package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
  88. package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
  89. package/dist/projections/declarative/ChildrenBuilder.js +5 -0
  90. package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
  91. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  92. package/dist/projections/declarative/FromBuilder.js +3 -2
  93. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  94. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  95. package/dist/projections/declarative/JoinBuilder.js +2 -1
  96. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  97. package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
  98. package/dist/projections/declarative/NestedBuilder.js +3 -1
  99. package/dist/projections/declarative/NestedBuilder.js.map +1 -1
  100. package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
  101. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  102. package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
  103. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  104. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  105. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  106. package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
  107. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  108. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  109. package/dist/projections/declarative/SetBuilder.js +2 -1
  110. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  111. package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
  112. package/dist/projections/modelBound/addFrom.js +3 -2
  113. package/dist/projections/modelBound/addFrom.js.map +1 -1
  114. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
  115. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  116. package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
  117. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  118. package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
  119. package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
  120. package/dist/projections/modelBound/childrenFrom.js +8 -4
  121. package/dist/projections/modelBound/childrenFrom.js.map +1 -1
  122. package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
  123. package/dist/projections/modelBound/clearWith.js +3 -2
  124. package/dist/projections/modelBound/clearWith.js.map +1 -1
  125. package/dist/projections/modelBound/count.d.ts.map +1 -1
  126. package/dist/projections/modelBound/count.js +3 -2
  127. package/dist/projections/modelBound/count.js.map +1 -1
  128. package/dist/projections/modelBound/decrement.d.ts.map +1 -1
  129. package/dist/projections/modelBound/decrement.js +3 -2
  130. package/dist/projections/modelBound/decrement.js.map +1 -1
  131. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  132. package/dist/projections/modelBound/fromAll.js +3 -2
  133. package/dist/projections/modelBound/fromAll.js.map +1 -1
  134. package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
  135. package/dist/projections/modelBound/fromEvery.js +3 -2
  136. package/dist/projections/modelBound/fromEvery.js.map +1 -1
  137. package/dist/projections/modelBound/increment.d.ts.map +1 -1
  138. package/dist/projections/modelBound/increment.js +3 -2
  139. package/dist/projections/modelBound/increment.js.map +1 -1
  140. package/dist/projections/modelBound/join.d.ts.map +1 -1
  141. package/dist/projections/modelBound/join.js +3 -2
  142. package/dist/projections/modelBound/join.js.map +1 -1
  143. package/dist/projections/modelBound/nested.d.ts.map +1 -1
  144. package/dist/projections/modelBound/nested.js +3 -2
  145. package/dist/projections/modelBound/nested.js.map +1 -1
  146. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
  147. package/dist/projections/modelBound/noAutoMap.js +3 -2
  148. package/dist/projections/modelBound/noAutoMap.js.map +1 -1
  149. package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
  150. package/dist/projections/modelBound/removedWith.js +3 -2
  151. package/dist/projections/modelBound/removedWith.js.map +1 -1
  152. package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
  153. package/dist/projections/modelBound/removedWithJoin.js +3 -2
  154. package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
  155. package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
  156. package/dist/projections/modelBound/setFrom.js +3 -2
  157. package/dist/projections/modelBound/setFrom.js.map +1 -1
  158. package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
  159. package/dist/projections/modelBound/setFromContext.js +3 -2
  160. package/dist/projections/modelBound/setFromContext.js.map +1 -1
  161. package/dist/projections/modelBound/setValue.d.ts.map +1 -1
  162. package/dist/projections/modelBound/setValue.js +3 -2
  163. package/dist/projections/modelBound/setValue.js.map +1 -1
  164. package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
  165. package/dist/projections/modelBound/subtractFrom.js +3 -2
  166. package/dist/projections/modelBound/subtractFrom.js.map +1 -1
  167. package/dist/projections/notSetPropertyPath.d.ts +3 -0
  168. package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
  169. package/dist/projections/notSetPropertyPath.js +5 -0
  170. package/dist/projections/notSetPropertyPath.js.map +1 -0
  171. package/dist/reactors/Reactors.d.ts +4 -0
  172. package/dist/reactors/Reactors.d.ts.map +1 -1
  173. package/dist/reactors/Reactors.js +24 -6
  174. package/dist/reactors/Reactors.js.map +1 -1
  175. package/dist/reactors/reactor.d.ts +2 -2
  176. package/dist/reactors/reactor.js +2 -2
  177. package/dist/readModels/IReadModels.d.ts +3 -2
  178. package/dist/readModels/IReadModels.d.ts.map +1 -1
  179. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  180. package/dist/readModels/MaterializedReadModels.js +2 -4
  181. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  182. package/dist/readModels/ReadModels.d.ts +3 -5
  183. package/dist/readModels/ReadModels.d.ts.map +1 -1
  184. package/dist/readModels/ReadModels.js +17 -14
  185. package/dist/readModels/ReadModels.js.map +1 -1
  186. package/dist/readModels/deserializeReadModel.d.ts +5 -0
  187. package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
  188. package/dist/readModels/deserializeReadModel.js +84 -0
  189. package/dist/readModels/deserializeReadModel.js.map +1 -0
  190. package/dist/readModels/rootReadModelTypes.d.ts +5 -0
  191. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
  192. package/dist/readModels/rootReadModelTypes.js +69 -0
  193. package/dist/readModels/rootReadModelTypes.js.map +1 -0
  194. package/dist/reducers/Reducers.d.ts +4 -0
  195. package/dist/reducers/Reducers.d.ts.map +1 -1
  196. package/dist/reducers/Reducers.js +24 -6
  197. package/dist/reducers/Reducers.js.map +1 -1
  198. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  199. package/dist/schemas/JsonSchemaGenerator.js +4 -4
  200. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  201. package/dist/tsconfig.tsbuildinfo +1 -1
  202. package/dist/types/TypeDiscoverer.d.ts +8 -1
  203. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  204. package/dist/types/TypeDiscoverer.js +25 -5
  205. package/dist/types/TypeDiscoverer.js.map +1 -1
  206. package/dist/types/TypeIntrospector.d.ts.map +1 -1
  207. package/dist/types/TypeIntrospector.js +3 -2
  208. package/dist/types/TypeIntrospector.js.map +1 -1
  209. package/dist/types/conceptValueType.d.ts +4 -0
  210. package/dist/types/conceptValueType.d.ts.map +1 -0
  211. package/dist/types/conceptValueType.js +10 -0
  212. package/dist/types/conceptValueType.js.map +1 -0
  213. package/dist/types/modelBoundProperty.d.ts +6 -0
  214. package/dist/types/modelBoundProperty.d.ts.map +1 -0
  215. package/dist/types/modelBoundProperty.js +53 -0
  216. package/dist/types/modelBoundProperty.js.map +1 -0
  217. package/eventSequences/AppendResult.ts +8 -0
  218. package/eventSequences/EventSequence.spec.ts +76 -1
  219. package/eventSequences/EventSequence.ts +28 -20
  220. package/eventSequences/WaitForCompletionOptions.ts +11 -0
  221. package/eventSequences/index.ts +1 -0
  222. package/externalServices/ExternalServiceBuilder.ts +4 -4
  223. package/externalServices/ExternalServices.spec.ts +87 -0
  224. package/externalServices/ExternalServices.ts +3 -1
  225. package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
  226. package/index.ts +1 -0
  227. package/observation/observationDisposal.spec.ts +74 -0
  228. package/package.json +2 -1
  229. package/projections/Projections.childrenAndNested.spec.ts +2 -0
  230. package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
  231. package/projections/Projections.ts +9 -7
  232. package/projections/constantValueExpression.spec.ts +31 -0
  233. package/projections/constantValueExpression.ts +28 -0
  234. package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
  235. package/projections/declarative/ChildrenBuilder.ts +6 -0
  236. package/projections/declarative/FromBuilder.ts +3 -2
  237. package/projections/declarative/JoinBuilder.ts +2 -1
  238. package/projections/declarative/NestedBuilder.ts +3 -1
  239. package/projections/declarative/ProjectionBuilderCore.ts +20 -2
  240. package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
  241. package/projections/declarative/ProjectionBuilderFor.ts +7 -0
  242. package/projections/declarative/SetBuilder.ts +2 -1
  243. package/projections/modelBound/addFrom.ts +3 -2
  244. package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
  245. package/projections/modelBound/childrenFrom.ts +11 -5
  246. package/projections/modelBound/clearWith.ts +3 -2
  247. package/projections/modelBound/count.ts +3 -2
  248. package/projections/modelBound/decorators.standard.spec.ts +45 -0
  249. package/projections/modelBound/decrement.ts +3 -2
  250. package/projections/modelBound/fromAll.ts +3 -2
  251. package/projections/modelBound/fromEvery.ts +3 -2
  252. package/projections/modelBound/increment.ts +3 -2
  253. package/projections/modelBound/join.ts +3 -2
  254. package/projections/modelBound/nested.ts +3 -2
  255. package/projections/modelBound/noAutoMap.ts +3 -2
  256. package/projections/modelBound/removedWith.ts +3 -2
  257. package/projections/modelBound/removedWithJoin.ts +3 -2
  258. package/projections/modelBound/setFrom.ts +3 -2
  259. package/projections/modelBound/setFromContext.ts +3 -2
  260. package/projections/modelBound/setValue.ts +3 -2
  261. package/projections/modelBound/subtractFrom.ts +3 -2
  262. package/projections/notSetPropertyPath.ts +5 -0
  263. package/reactors/Reactors.ts +22 -7
  264. package/reactors/reactor.ts +2 -2
  265. package/readModels/IReadModels.ts +3 -2
  266. package/readModels/MaterializedReadModels.spec.ts +22 -0
  267. package/readModels/MaterializedReadModels.ts +2 -4
  268. package/readModels/ReadModels.spec.ts +71 -6
  269. package/readModels/ReadModels.ts +18 -15
  270. package/readModels/conceptValueType.spec.ts +20 -0
  271. package/readModels/deserializeReadModel.spec.ts +84 -0
  272. package/readModels/deserializeReadModel.ts +82 -0
  273. package/readModels/discovery.spec.ts +27 -0
  274. package/readModels/rootReadModelTypes.spec.ts +115 -0
  275. package/readModels/rootReadModelTypes.ts +65 -0
  276. package/reducers/Reducers.ts +22 -7
  277. package/schemas/JsonSchemaGenerator.ts +4 -4
  278. package/schemas/legacyDesignMetadata.spec.ts +24 -2
  279. package/types/TypeDiscoverer.spec.ts +57 -0
  280. package/types/TypeDiscoverer.ts +27 -6
  281. package/types/TypeIntrospector.ts +3 -2
  282. package/types/conceptValueType.ts +11 -0
  283. package/types/modelBoundProperty.ts +53 -0
  284. package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
  285. package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
  286. package/dist/externalServices/ExternalServicesContracts.js +0 -654
  287. package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
  288. package/externalServices/ExternalServicesContracts.ts +0 -709
@@ -558,6 +558,81 @@ describe('EventSequence', () => {
558
558
  const options = waitForCompletion.mock.calls[0][1];
559
559
  expect(options.signal).toBeInstanceOf(AbortSignal);
560
560
  });
561
+
562
+ it('should clear the timeout after completion', async () => {
563
+ vi.useFakeTimers();
564
+ try {
565
+ const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
566
+ await appendResult.waitForCompletion({ timeoutMs: 1000 });
567
+ expect(vi.getTimerCount()).toBe(0);
568
+ } finally {
569
+ vi.useRealTimers();
570
+ }
571
+ });
572
+ });
573
+
574
+ describe('when a caller cancels a completion wait', () => {
575
+ it('should reject a pre-aborted signal without starting the RPC or a timer', async () => {
576
+ const { eventSequence, waitForCompletion } = createEventSequence();
577
+ const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
578
+ const controller = new AbortController();
579
+ const reason = new Error('already canceled');
580
+ controller.abort(reason);
581
+ vi.useFakeTimers();
582
+ try {
583
+ await expect(appendResult.waitForCompletion({ signal: controller.signal })).rejects.toBe(reason);
584
+ expect(waitForCompletion).not.toHaveBeenCalled();
585
+ expect(vi.getTimerCount()).toBe(0);
586
+ } finally {
587
+ vi.useRealTimers();
588
+ }
589
+ });
590
+
591
+ it('should abort the underlying RPC and reject with the caller reason', async () => {
592
+ const waitForCompletion = vi.fn().mockImplementation((_request, { signal }: { signal: AbortSignal }) =>
593
+ new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(signal.reason), { once: true })));
594
+ const { eventSequence } = createEventSequence({}, { waitForCompletion });
595
+ const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
596
+ const controller = new AbortController();
597
+ const reason = new Error('caller canceled');
598
+ const pending = appendResult.waitForCompletion({ timeoutMs: 1000, signal: controller.signal });
599
+ const signal = waitForCompletion.mock.calls[0][1].signal as AbortSignal;
600
+
601
+ controller.abort(reason);
602
+
603
+ await expect(pending).rejects.toBe(reason);
604
+ expect(signal.aborted).toBe(true);
605
+ expect(signal.reason).toBe(reason);
606
+ });
607
+
608
+ it('should clear the timeout when the caller aborts', async () => {
609
+ const waitForCompletion = vi.fn().mockImplementation((_request, { signal }: { signal: AbortSignal }) =>
610
+ new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(signal.reason), { once: true })));
611
+ const { eventSequence } = createEventSequence({}, { waitForCompletion });
612
+ const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
613
+ const controller = new AbortController();
614
+ vi.useFakeTimers();
615
+ try {
616
+ const pending = appendResult.waitForCompletion({ timeoutMs: 1000, signal: controller.signal });
617
+ expect(vi.getTimerCount()).toBe(1);
618
+ const reason = new Error('caller canceled');
619
+ controller.abort(reason);
620
+ await expect(pending).rejects.toBe(reason);
621
+ expect(vi.getTimerCount()).toBe(0);
622
+ } finally {
623
+ vi.useRealTimers();
624
+ }
625
+ });
626
+
627
+ it('should cancel the underlying RPC when the options timeout expires', async () => {
628
+ const waitForCompletion = vi.fn().mockImplementation((_request, { signal }: { signal: AbortSignal }) =>
629
+ new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(signal.reason), { once: true })));
630
+ const { eventSequence } = createEventSequence({}, { waitForCompletion });
631
+ const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
632
+
633
+ await expect(appendResult.waitForCompletion({ timeoutMs: 5 })).rejects.toMatchObject({ name: 'TimeoutError' });
634
+ expect(waitForCompletion.mock.calls[0][1].signal.aborted).toBe(true);
635
+ });
561
636
  });
562
637
 
563
638
  describe('when waiting for completion and observers report a failed partition', () => {
@@ -577,7 +652,7 @@ describe('EventSequence', () => {
577
652
 
578
653
  it('should report failure with the failed partitions', async () => {
579
654
  const appendResult = await eventSequence.append('some-event-source', new SomethingHappened('a'));
580
- const result = await appendResult.waitForCompletion();
655
+ const result = await appendResult.waitForCompletion({ timeoutMs: 1234 });
581
656
 
582
657
  expect(result.isSuccess).toBe(false);
583
658
  expect(result.failedPartitions).toHaveLength(1);
@@ -29,6 +29,7 @@ import { EventSequenceId } from './EventSequenceId.js';
29
29
  import { EventSequenceNumber } from './EventSequenceNumber.js';
30
30
  import { TransactionalEventSequence } from './TransactionalEventSequence.js';
31
31
  import { WaitForCompletionResult } from './WaitForCompletionResult.js';
32
+ import type { WaitForCompletionOptions } from './WaitForCompletionOptions.js';
32
33
 
33
34
  /** Default timeout for {@link AppendResult.waitForCompletion}, matching the C# client's default. */
34
35
  const DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS = 5000;
@@ -725,7 +726,7 @@ export class EventSequence implements IEventSequence {
725
726
  concurrencyViolation: mappedConcurrencyViolation,
726
727
  errors: mappedErrors,
727
728
  isSuccess,
728
- waitForCompletion: (timeoutMs?: number) => this.waitForObserverCompletion(eventSequenceNumber, isSuccess, timeoutMs)
729
+ waitForCompletion: (options?: number | WaitForCompletionOptions) => this.waitForObserverCompletion(eventSequenceNumber, isSuccess, options)
729
730
  };
730
731
  }
731
732
 
@@ -736,25 +737,38 @@ export class EventSequence implements IEventSequence {
736
737
  private async waitForObserverCompletion(
737
738
  tailSequenceNumber: EventSequenceNumber,
738
739
  appendWasSuccessful: boolean,
739
- timeoutMs = DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS
740
+ options: number | WaitForCompletionOptions = DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS
740
741
  ): Promise<WaitForCompletionResult> {
741
742
  if (!appendWasSuccessful) {
742
743
  return { isSuccess: true, failedPartitions: [] };
743
744
  }
744
745
 
745
- const response = await this._connection.observers.waitForCompletion(
746
- {
747
- EventStore: this._eventStoreName,
748
- Namespace: this._namespace,
749
- EventSequenceId: this.id.value,
750
- TailEventSequenceNumber: tailSequenceNumber.value
751
- },
752
- { signal: AbortSignal.timeout(timeoutMs) });
746
+ const timeoutMs = typeof options === 'number' ? options : options.timeoutMs ?? DEFAULT_WAIT_FOR_COMPLETION_TIMEOUT_MS;
747
+ const callerSignal = typeof options === 'number' ? undefined : options.signal;
748
+ if (callerSignal?.aborted) throw callerSignal.reason;
749
+
750
+ const controller = new AbortController();
751
+ const forwardAbort = () => controller.abort(callerSignal?.reason);
752
+ callerSignal?.addEventListener('abort', forwardAbort, { once: true });
753
+ const timer = setTimeout(() => controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError')), timeoutMs);
754
+ try {
755
+ const response = await this._connection.observers.waitForCompletion(
756
+ {
757
+ EventStore: this._eventStoreName,
758
+ Namespace: this._namespace,
759
+ EventSequenceId: this.id.value,
760
+ TailEventSequenceNumber: tailSequenceNumber.value
761
+ },
762
+ { signal: controller.signal });
753
763
 
754
- return {
755
- isSuccess: response.IsSuccess,
756
- failedPartitions: (response.FailedPartitions ?? []).map(failedPartition => toClientFailedPartition(failedPartition))
757
- };
764
+ return {
765
+ isSuccess: response.IsSuccess,
766
+ failedPartitions: (response.FailedPartitions ?? []).map(failedPartition => toClientFailedPartition(failedPartition))
767
+ };
768
+ } finally {
769
+ clearTimeout(timer);
770
+ callerSignal?.removeEventListener('abort', forwardAbort);
771
+ }
758
772
  }
759
773
 
760
774
  private toContractConcurrencyScope(scope?: ConcurrencyScope) {
@@ -776,12 +790,6 @@ export class EventSequence implements IEventSequence {
776
790
  }
777
791
  }
778
792
 
779
- /**
780
- * Converts a RFC 4122 Guid string into the protobuf Guid shape used by Chronicle contracts.
781
- * @param guid - The Guid to convert.
782
- * @returns The converted protobuf Guid with fixed64-safe hi/lo values.
783
- */
784
-
785
793
  /**
786
794
  * Converts an {@link Identity} into the CausedBy shape used by Chronicle contracts.
787
795
  * @param identity - The identity to convert.
@@ -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
+ /** Options for waiting until observers complete an append. */
5
+ export interface WaitForCompletionOptions {
6
+ /** Timeout in milliseconds. Defaults to 5000 (5 seconds). */
7
+ readonly timeoutMs?: number;
8
+
9
+ /** Cancels the underlying wait RPC when aborted. */
10
+ readonly signal?: AbortSignal;
11
+ }
@@ -14,6 +14,7 @@ export type { CompleteStreamResult } from './CompleteStreamResult.js';
14
14
  export type { ConcurrencyScope } from './ConcurrencyScope.js';
15
15
  export type { EventForEventSourceId } from './EventForEventSourceId.js';
16
16
  export type { WaitForCompletionResult } from './WaitForCompletionResult.js';
17
+ export type { WaitForCompletionOptions } from './WaitForCompletionOptions.js';
17
18
  export type { IEventSequence } from './IEventSequence.js';
18
19
  export type { IEventLog } from './IEventLog.js';
19
20
  export type { ITransactionalEventSequence } from './ITransactionalEventSequence.js';
@@ -2,11 +2,11 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import {
5
- ExternalServiceDefinition,
6
- ExternalServiceEndpoint,
5
+ type ExternalServiceDefinition,
6
+ type ExternalServiceEndpoint,
7
7
  ExternalServiceEndpointType,
8
- OneOfBasicAuthorizationBearerTokenAuthorizationOAuthAuthorization
9
- } from './ExternalServicesContracts.js';
8
+ type OneOfBasicAuthorizationBearerTokenAuthorizationOAuthAuthorization
9
+ } from '@cratis/chronicle.contracts';
10
10
  import { IExternalServiceBuilder } from './IExternalServiceBuilder.js';
11
11
 
12
12
  /**
@@ -0,0 +1,87 @@
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 { AddExternalServicesRequest, ExternalServiceEndpointType } from '@cratis/chronicle.contracts';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import type { ChronicleConnection } from '../connection/index.js';
7
+ import { ChronicleCallFailed } from '../connection/callResults.js';
8
+ import type { IExternalServiceBuilder } from './IExternalServiceBuilder.js';
9
+ import { ExternalServices } from './ExternalServices.js';
10
+
11
+ function createServices(result = { IsAuthorized: true, ValidationResults: [], ExceptionMessages: [], AuthorizationFailureReason: '' }) {
12
+ const addExternalServices = vi.fn().mockResolvedValue(result);
13
+ const connection = { externalServices: { addExternalServices } } as unknown as ChronicleConnection;
14
+ return { services: new ExternalServices('my-event-store', connection), addExternalServices };
15
+ }
16
+
17
+ async function requestFor(configure: (builder: IExternalServiceBuilder) => void): Promise<AddExternalServicesRequest> {
18
+ const { services, addExternalServices } = createServices();
19
+ await services.register('Customers', configure);
20
+ expect(addExternalServices).toHaveBeenCalledTimes(1);
21
+ // Round-trip through the generated codec to assert the actual wire request, not just the in-memory object.
22
+ const request = AddExternalServicesRequest.fromPartial(addExternalServices.mock.calls[0][0]);
23
+ return AddExternalServicesRequest.decode(AddExternalServicesRequest.encode(request).finish());
24
+ }
25
+
26
+ describe('ExternalServices registration', () => {
27
+ it('sends an HTTP bearer endpoint with headers', async () => {
28
+ expect(await requestFor(builder => builder.http('https://example.com')
29
+ .withBearerToken('secret').withHeader('X-Tenant', 'acme').withHeader('X-Trace', 'enabled'))).toEqual({
30
+ EventStore: 'my-event-store',
31
+ ExternalServices: [{
32
+ Id: 'Customers', Name: 'Customers', Endpoint: {
33
+ Type: ExternalServiceEndpointType.Http,
34
+ Http: {
35
+ Url: 'https://example.com', Headers: { 'X-Tenant': 'acme', 'X-Trace': 'enabled' },
36
+ Authorization: { Value0: undefined, Value1: { Token: 'secret' }, Value2: undefined }
37
+ },
38
+ Database: undefined
39
+ }
40
+ }]
41
+ });
42
+ });
43
+
44
+ it('sends basic authorization', async () => {
45
+ const request = await requestFor(builder => builder.http('https://example.com').withBasicAuth('user', 'password'));
46
+ expect(request.ExternalServices[0].Endpoint?.Http?.Authorization).toEqual({
47
+ Value0: { Username: 'user', Password: 'password' }, Value1: undefined, Value2: undefined
48
+ });
49
+ expect(request.ExternalServices[0].Endpoint?.Http?.Headers).toEqual({});
50
+ });
51
+
52
+ it('sends OAuth authorization', async () => {
53
+ const request = await requestFor(builder => builder.http('https://example.com').withOAuth('https://auth', 'client', 'secret'));
54
+ expect(request.ExternalServices[0].Endpoint?.Http?.Authorization).toEqual({
55
+ Value0: undefined, Value1: undefined,
56
+ Value2: { Authority: 'https://auth', ClientId: 'client', ClientSecret: 'secret' }
57
+ });
58
+ });
59
+
60
+ it.each([
61
+ ['msSql', ExternalServiceEndpointType.MsSql, 1433],
62
+ ['postgreSql', ExternalServiceEndpointType.PostgreSql, 5432]
63
+ ] as const)('sends %s with a port and options', async (method, type, port) => {
64
+ const request = await requestFor(builder => builder[method]('db.example.com', 'customers', 'user', 'password', port)
65
+ .withOption('sslmode', 'require'));
66
+ expect(request.ExternalServices[0]).toEqual({
67
+ Id: 'Customers', Name: 'Customers', Endpoint: {
68
+ Type: type, Http: undefined,
69
+ Database: {
70
+ Host: 'db.example.com', Port: port, Database: 'customers',
71
+ Username: 'user', Password: 'password', Options: { sslmode: 'require' }
72
+ }
73
+ }
74
+ });
75
+ });
76
+
77
+ it('rejects a failed command result', async () => {
78
+ const { services } = createServices({
79
+ IsAuthorized: true, ValidationResults: [{ Message: 'invalid endpoint', Members: ['Endpoint'] }],
80
+ ExceptionMessages: [], AuthorizationFailureReason: ''
81
+ });
82
+ await expect(services.register('Customers', builder => builder.http('https://example.com')))
83
+ .rejects.toThrow(ChronicleCallFailed);
84
+ await expect(services.register('Customers', builder => builder.http('https://example.com')))
85
+ .rejects.toThrow(/register external service.*invalid endpoint/);
86
+ });
87
+ });
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { ChronicleConnection } from '../connection/index.js';
5
+ import { ensureCommandSuccess } from '../connection/callResults.js';
5
6
  import { ExternalServiceBuilder } from './ExternalServiceBuilder.js';
6
7
  import { IExternalServiceBuilder } from './IExternalServiceBuilder.js';
7
8
  import { IExternalServices } from './IExternalServices.js';
@@ -26,9 +27,10 @@ export class ExternalServices implements IExternalServices {
26
27
  configure(builder);
27
28
  const definition = builder.build(name, name);
28
29
 
29
- await this._connection.externalServices.add({
30
+ const result = await this._connection.externalServices.addExternalServices({
30
31
  EventStore: this._eventStore,
31
32
  ExternalServices: [definition]
32
33
  });
34
+ ensureCommandSuccess('register external service', result);
33
35
  }
34
36
  }
@@ -0,0 +1,67 @@
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, describe, expect, it, vi } from 'vitest';
5
+ import { ChronicleClient } from '../ChronicleClient.js';
6
+ import { ChronicleOptions } from '../ChronicleOptions.js';
7
+ import { RejectedChronicleCredentials } from '../connection/RejectedChronicleCredentials.js';
8
+
9
+ const transport = vi.hoisted(() => ({ connect: vi.fn(), resetChannel: vi.fn(), disconnect: vi.fn(), getVersionInfo: vi.fn(), allEventStores: vi.fn() }));
10
+ vi.mock('../connection/ChronicleConnection.js', () => ({
11
+ ChronicleConnection: class {
12
+ connect = transport.connect;
13
+ resetChannel = transport.resetChannel;
14
+ disconnect = transport.disconnect;
15
+ server = { getVersionInfo: transport.getVersionInfo };
16
+ eventStores = { allEventStores: transport.allEventStores };
17
+ }
18
+ }));
19
+ vi.mock('../connection/KernelKeepAlive.js', () => ({ KernelKeepAlive: class { async start() {} } }));
20
+
21
+ let client: ChronicleClient;
22
+ beforeEach(() => {
23
+ vi.useFakeTimers();
24
+ vi.clearAllMocks();
25
+ transport.connect.mockResolvedValue(undefined);
26
+ transport.resetChannel.mockResolvedValue(undefined);
27
+ transport.getVersionInfo.mockResolvedValue({});
28
+ transport.allEventStores.mockResolvedValue({ IsAuthorized: true, Data: [] });
29
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [] }));
30
+ });
31
+ afterEach(() => { client.dispose(); vi.useRealTimers(); });
32
+
33
+ describe('client authentication recovery', () => {
34
+ it('connects when an anonymous kernel accepts calls despite an unavailable token endpoint', async () => {
35
+ await expect(client.getEventStores()).resolves.toEqual([]);
36
+ expect(transport.connect).toHaveBeenCalledTimes(1);
37
+ });
38
+
39
+ it('preserves a typed terminal error when an established call enters withReconnect', async () => {
40
+ await client.getEventStores();
41
+ transport.allEventStores.mockRejectedValue(new RejectedChronicleCredentials('rejected'));
42
+ await expect(client.getEventStores()).rejects.toBeInstanceOf(RejectedChronicleCredentials);
43
+ await expect(client.getEventStores()).rejects.toThrow('rejected');
44
+ expect(transport.connect).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it('makes a terminal error on the retried call permanent', async () => {
48
+ await client.getEventStores();
49
+ const rejection = new RejectedChronicleCredentials('rejected after reconnect');
50
+ transport.allEventStores.mockRejectedValueOnce(Object.assign(new Error('unavailable'), { code: 14 }))
51
+ .mockRejectedValueOnce(rejection);
52
+ await expect(client.getEventStores()).rejects.toBe(rejection);
53
+ const calls = transport.allEventStores.mock.calls.length;
54
+ await expect(client.getEventStores()).rejects.toBe(rejection);
55
+ expect(transport.allEventStores).toHaveBeenCalledTimes(calls);
56
+ });
57
+
58
+ it('continues retrying a network outage until disposed', async () => {
59
+ transport.connect.mockRejectedValue(new Error('UNAVAILABLE: connection refused'));
60
+ const pending = client.getEventStores().then(() => undefined, error => error as Error);
61
+ await vi.advanceTimersByTimeAsync(4500);
62
+ expect(transport.connect.mock.calls.length).toBeGreaterThan(1);
63
+ client.dispose();
64
+ await vi.runAllTimersAsync();
65
+ expect((await pending).message).toMatch(/disposed/);
66
+ });
67
+ });
package/index.ts CHANGED
@@ -27,6 +27,7 @@ import * as confidentiality from './confidentiality/index.js';
27
27
 
28
28
  export { ChronicleClient } from './ChronicleClient.js';
29
29
  export { IncompatibleChronicleServer } from './connection/IncompatibleChronicleServer.js';
30
+ export { RejectedChronicleCredentials } from './connection/RejectedChronicleCredentials.js';
30
31
  export type { IChronicleClient } from './IChronicleClient.js';
31
32
  export { ChronicleOptions } from './ChronicleOptions.js';
32
33
  export { EventStore } from './EventStore.js';
@@ -0,0 +1,74 @@
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 { describe, expect, it, vi } from 'vitest';
5
+ import type { Constructor } from '@cratis/fundamentals';
6
+ import type { IClientArtifactsProvider } from '../artifacts/index.js';
7
+ import { ChronicleClient } from '../ChronicleClient.js';
8
+ import { ChronicleOptions } from '../ChronicleOptions.js';
9
+ import type { ChronicleConnection } from '../connection/index.js';
10
+ import { ConnectionLifecycle } from '../connection/ConnectionLifecycle.js';
11
+ import type { EventStore } from '../EventStore.js';
12
+ import type { IEventLog } from '../eventSequences/IEventLog.js';
13
+ import { Reactors } from '../reactors/Reactors.js';
14
+ import { reactor } from '../reactors/reactor.js';
15
+ import { Reducers } from '../reducers/Reducers.js';
16
+ import { reducer } from '../reducers/reducer.js';
17
+
18
+ class ExampleReactor {}
19
+ reactor('disposal-reactor')(ExampleReactor);
20
+ class ExampleReducer {}
21
+ reducer('disposal-reducer')(ExampleReducer);
22
+
23
+ const artifacts = {
24
+ reactors: [ExampleReactor as Constructor], reducers: [ExampleReducer as Constructor],
25
+ eventTypes: [], readModels: []
26
+ } as unknown as IClientArtifactsProvider;
27
+
28
+ function stream() {
29
+ const signals: AbortSignal[] = [];
30
+ const observe = vi.fn((_queue: AsyncIterable<unknown>, options: { signal: AbortSignal }) => {
31
+ signals.push(options.signal);
32
+ return {
33
+ async *[Symbol.asyncIterator]() {
34
+ await new Promise<void>(resolve => options.signal.addEventListener('abort', () => resolve(), { once: true }));
35
+ }
36
+ };
37
+ });
38
+ return { observe, signals };
39
+ }
40
+
41
+ describe('observation disposal', () => {
42
+ it('aborts reactor and reducer streams even without a connected lifecycle and prevents re-observation', async () => {
43
+ const reactorStream = stream();
44
+ const reducerStream = stream();
45
+ const connection = {
46
+ reactors: { observe: reactorStream.observe }, reducers: { observe: reducerStream.observe },
47
+ readModels: { registerMany: vi.fn().mockResolvedValue({}) }
48
+ } as unknown as ChronicleConnection;
49
+ const lifecycle = new ConnectionLifecycle();
50
+ const reactors = new Reactors(artifacts, connection, 'store', 'Default', lifecycle, {} as IEventLog);
51
+ const reducers = new Reducers(artifacts, connection, 'store', 'Default', lifecycle, 'sink');
52
+ await reactors.register();
53
+ await reducers.register();
54
+ expect(reactorStream.observe).toHaveBeenCalledOnce();
55
+ expect(reducerStream.observe).toHaveBeenCalledOnce();
56
+
57
+ reactors.dispose();
58
+ reducers.dispose();
59
+ expect(reactorStream.signals[0].aborted).toBe(true);
60
+ expect(reducerStream.signals[0].aborted).toBe(true);
61
+ await reactors.register();
62
+ await reducers.register();
63
+ expect(reactorStream.observe).toHaveBeenCalledOnce();
64
+ expect(reducerStream.observe).toHaveBeenCalledOnce();
65
+ });
66
+
67
+ it('disposes cached store observations even when the lifecycle is not connected', () => {
68
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [] }));
69
+ const disposeObservations = vi.fn();
70
+ (client as unknown as { _stores: Map<string, EventStore> })._stores.set('store/Default', { disposeObservations } as unknown as EventStore);
71
+ client.dispose();
72
+ expect(disposeObservations).toHaveBeenCalledOnce();
73
+ });
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.9.1",
3
+ "version": "6.11.0",
4
4
  "description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -170,6 +170,7 @@
170
170
  "@cratis/chronicle.contracts": "19.4.0",
171
171
  "@grpc/grpc-js": "^1.14.4",
172
172
  "@opentelemetry/api": "^1.9.1",
173
+ "glob": "^13.0.6",
173
174
  "nice-grpc": "^2.1.17",
174
175
  "nice-grpc-common": "^2.0.4",
175
176
  "reflect-metadata": "^0.2.2",
@@ -167,6 +167,7 @@ describe('Projections with childrenFrom, nested and clearWith', () => {
167
167
  const definition = registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition;
168
168
  const summary = definition.Nested.summary;
169
169
 
170
+ expect(summary.IdentifiedBy).toBe('*NotSet*');
170
171
  const updateEntry = summary.From.find(candidate => candidate.Key.Id === 'SummaryUpdated')!;
171
172
  expect(updateEntry.Value.Properties.total).toBe('total');
172
173
  expect(summary.RemovedWith.some(candidate => candidate.Key.Id === 'SummaryCleared')).toBe(true);
@@ -179,6 +180,7 @@ describe('Projections with childrenFrom, nested and clearWith', () => {
179
180
  const definition = registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition;
180
181
  const note = definition.Nested.note;
181
182
 
183
+ expect(note.IdentifiedBy).toBe('*NotSet*');
182
184
  const updateEntry = note.From.find(candidate => candidate.Key.Id === 'NoteAdded')!;
183
185
  expect(updateEntry.Value.Properties.text).toBe('text');
184
186
  expect(note.RemovedWith.some(candidate => candidate.Key.Id === 'NoteCleared')).toBe(true);
@@ -12,7 +12,9 @@ import { eventLog, eventSequence } from './modelBound/eventSequence.js';
12
12
  import { fromAll } from './modelBound/fromAll.js';
13
13
  import { fromEvent } from './modelBound/fromEvent.js';
14
14
  import { noAutoMap } from './modelBound/noAutoMap.js';
15
+ import { join } from './modelBound/join.js';
15
16
  import { setFrom } from './modelBound/setFrom.js';
17
+ import { setValue } from './modelBound/setValue.js';
16
18
  import { Projections } from './Projections.js';
17
19
 
18
20
  // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
@@ -112,6 +114,28 @@ setFrom(MbProjectNoted, 'note')(MbProjectNotes.prototype, 'note');
112
114
  clearWith(MbProjectNoteCleared)(MbProjectNotes.prototype, 'note');
113
115
  fromEvent(MbProjectNoted)(MbProjectNotes);
114
116
 
117
+ class MbConstants {
118
+ active!: boolean;
119
+ status!: string;
120
+ total!: number;
121
+ started!: Date;
122
+ cleared!: string | null;
123
+ wrapped!: string;
124
+ }
125
+ setValue(MbProjectNoted, true)(MbConstants.prototype, 'active');
126
+ setValue(MbProjectNoted, 'on-loan')(MbConstants.prototype, 'status');
127
+ setValue(MbProjectNoted, 2.5)(MbConstants.prototype, 'total');
128
+ setValue(MbProjectNoted, new Date('2025-01-02T03:04:05.006Z'))(MbConstants.prototype, 'started');
129
+ setValue(MbProjectNoted, null)(MbConstants.prototype, 'cleared');
130
+ setValue(MbProjectNoted, { value: 'wrapped' })(MbConstants.prototype, 'wrapped');
131
+ fromEvent(MbProjectNoted, { constantKey: 'singleton' })(MbConstants);
132
+
133
+ class MbJoinedNote {
134
+ id!: string;
135
+ note!: string;
136
+ }
137
+ join(MbProjectNoted)(MbJoinedNote.prototype, 'note');
138
+
115
139
  interface BuiltFromEntry {
116
140
  Key: { Id: string };
117
141
  Value: { Properties: Record<string, string>; Key: string; ParentKey: string };
@@ -121,6 +145,7 @@ interface BuiltDefinition {
121
145
  EventSequenceId: string;
122
146
  AutoMap: AutoMap;
123
147
  NoAutoMapProperties: string[];
148
+ Join: Array<{ Value: { On: string } }>;
124
149
  All: { Properties: Record<string, string> };
125
150
  From: BuiltFromEntry[];
126
151
  }
@@ -208,6 +233,28 @@ describe('Projections model-bound completeness', () => {
208
233
  });
209
234
  });
210
235
 
236
+ describe('when a property is set to a constant', () => {
237
+ it('should send value and null expressions for model-bound mappings', async () => {
238
+ const definition = await registerAndGetDefinition(MbConstants);
239
+ expect(findFromEntry(definition, 'MbProjectNoted').Value.Properties).toMatchObject({
240
+ active: '$value(true)',
241
+ status: '$value(on-loan)',
242
+ total: '$value(2.5)',
243
+ started: '$value(2025-01-02T03:04:05.006Z)',
244
+ cleared: '$null',
245
+ wrapped: '$value(wrapped)'
246
+ });
247
+ expect(findFromEntry(definition, 'MbProjectNoted').Value.Key).toBe('$value(singleton)');
248
+ });
249
+ });
250
+
251
+ describe('when a model-bound join has no explicit on property', () => {
252
+ it('should use the mapped property as the join path', async () => {
253
+ const definition = await registerAndGetDefinition(MbJoinedNote);
254
+ expect(definition.Join[0].Value.On).toBe('note');
255
+ });
256
+ });
257
+
211
258
  describe('when a root scalar property is cleared with an event', () => {
212
259
  it('should add a $null mapping for that property on the clearing event', async () => {
213
260
  const definition = await registerAndGetDefinition(MbProjectNotes);