@cratis/chronicle 6.9.0 → 6.10.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 (268) 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 +219 -0
  8. package/connection/ChronicleConnection.ts +70 -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 +74 -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/externalServices/ExternalServiceBuilder.d.ts +1 -1
  58. package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
  59. package/dist/externalServices/ExternalServiceBuilder.js +1 -1
  60. package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
  61. package/dist/externalServices/ExternalServices.d.ts.map +1 -1
  62. package/dist/externalServices/ExternalServices.js +3 -1
  63. package/dist/externalServices/ExternalServices.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/projections/Projections.d.ts.map +1 -1
  69. package/dist/projections/Projections.js +9 -7
  70. package/dist/projections/Projections.js.map +1 -1
  71. package/dist/projections/constantValueExpression.d.ts +3 -0
  72. package/dist/projections/constantValueExpression.d.ts.map +1 -0
  73. package/dist/projections/constantValueExpression.js +26 -0
  74. package/dist/projections/constantValueExpression.js.map +1 -0
  75. package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
  76. package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
  77. package/dist/projections/declarative/ChildrenBuilder.js +5 -0
  78. package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
  79. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  80. package/dist/projections/declarative/FromBuilder.js +3 -2
  81. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  82. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  83. package/dist/projections/declarative/JoinBuilder.js +2 -1
  84. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  85. package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
  86. package/dist/projections/declarative/NestedBuilder.js +3 -1
  87. package/dist/projections/declarative/NestedBuilder.js.map +1 -1
  88. package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
  89. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  90. package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
  91. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  92. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  93. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  94. package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
  95. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  96. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  97. package/dist/projections/declarative/SetBuilder.js +2 -1
  98. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  99. package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
  100. package/dist/projections/modelBound/addFrom.js +3 -2
  101. package/dist/projections/modelBound/addFrom.js.map +1 -1
  102. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
  103. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  104. package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
  105. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  106. package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
  107. package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
  108. package/dist/projections/modelBound/childrenFrom.js +8 -4
  109. package/dist/projections/modelBound/childrenFrom.js.map +1 -1
  110. package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
  111. package/dist/projections/modelBound/clearWith.js +3 -2
  112. package/dist/projections/modelBound/clearWith.js.map +1 -1
  113. package/dist/projections/modelBound/count.d.ts.map +1 -1
  114. package/dist/projections/modelBound/count.js +3 -2
  115. package/dist/projections/modelBound/count.js.map +1 -1
  116. package/dist/projections/modelBound/decrement.d.ts.map +1 -1
  117. package/dist/projections/modelBound/decrement.js +3 -2
  118. package/dist/projections/modelBound/decrement.js.map +1 -1
  119. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  120. package/dist/projections/modelBound/fromAll.js +3 -2
  121. package/dist/projections/modelBound/fromAll.js.map +1 -1
  122. package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
  123. package/dist/projections/modelBound/fromEvery.js +3 -2
  124. package/dist/projections/modelBound/fromEvery.js.map +1 -1
  125. package/dist/projections/modelBound/increment.d.ts.map +1 -1
  126. package/dist/projections/modelBound/increment.js +3 -2
  127. package/dist/projections/modelBound/increment.js.map +1 -1
  128. package/dist/projections/modelBound/join.d.ts.map +1 -1
  129. package/dist/projections/modelBound/join.js +3 -2
  130. package/dist/projections/modelBound/join.js.map +1 -1
  131. package/dist/projections/modelBound/nested.d.ts.map +1 -1
  132. package/dist/projections/modelBound/nested.js +3 -2
  133. package/dist/projections/modelBound/nested.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
  135. package/dist/projections/modelBound/noAutoMap.js +3 -2
  136. package/dist/projections/modelBound/noAutoMap.js.map +1 -1
  137. package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
  138. package/dist/projections/modelBound/removedWith.js +3 -2
  139. package/dist/projections/modelBound/removedWith.js.map +1 -1
  140. package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
  141. package/dist/projections/modelBound/removedWithJoin.js +3 -2
  142. package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
  143. package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
  144. package/dist/projections/modelBound/setFrom.js +3 -2
  145. package/dist/projections/modelBound/setFrom.js.map +1 -1
  146. package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
  147. package/dist/projections/modelBound/setFromContext.js +3 -2
  148. package/dist/projections/modelBound/setFromContext.js.map +1 -1
  149. package/dist/projections/modelBound/setValue.d.ts.map +1 -1
  150. package/dist/projections/modelBound/setValue.js +3 -2
  151. package/dist/projections/modelBound/setValue.js.map +1 -1
  152. package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
  153. package/dist/projections/modelBound/subtractFrom.js +3 -2
  154. package/dist/projections/modelBound/subtractFrom.js.map +1 -1
  155. package/dist/projections/notSetPropertyPath.d.ts +3 -0
  156. package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
  157. package/dist/projections/notSetPropertyPath.js +5 -0
  158. package/dist/projections/notSetPropertyPath.js.map +1 -0
  159. package/dist/reactors/Reactors.d.ts +4 -0
  160. package/dist/reactors/Reactors.d.ts.map +1 -1
  161. package/dist/reactors/Reactors.js +24 -6
  162. package/dist/reactors/Reactors.js.map +1 -1
  163. package/dist/reactors/reactor.d.ts +2 -2
  164. package/dist/reactors/reactor.js +2 -2
  165. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  166. package/dist/readModels/MaterializedReadModels.js +2 -4
  167. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  168. package/dist/readModels/ReadModels.d.ts +2 -1
  169. package/dist/readModels/ReadModels.d.ts.map +1 -1
  170. package/dist/readModels/ReadModels.js +19 -10
  171. package/dist/readModels/ReadModels.js.map +1 -1
  172. package/dist/readModels/deserializeReadModel.d.ts +5 -0
  173. package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
  174. package/dist/readModels/deserializeReadModel.js +84 -0
  175. package/dist/readModels/deserializeReadModel.js.map +1 -0
  176. package/dist/readModels/rootReadModelTypes.d.ts +5 -0
  177. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
  178. package/dist/readModels/rootReadModelTypes.js +69 -0
  179. package/dist/readModels/rootReadModelTypes.js.map +1 -0
  180. package/dist/reducers/Reducers.d.ts +4 -0
  181. package/dist/reducers/Reducers.d.ts.map +1 -1
  182. package/dist/reducers/Reducers.js +24 -6
  183. package/dist/reducers/Reducers.js.map +1 -1
  184. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  185. package/dist/schemas/JsonSchemaGenerator.js +4 -4
  186. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  187. package/dist/tsconfig.tsbuildinfo +1 -1
  188. package/dist/types/TypeDiscoverer.d.ts +8 -1
  189. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  190. package/dist/types/TypeDiscoverer.js +25 -5
  191. package/dist/types/TypeDiscoverer.js.map +1 -1
  192. package/dist/types/TypeIntrospector.d.ts.map +1 -1
  193. package/dist/types/TypeIntrospector.js +3 -2
  194. package/dist/types/TypeIntrospector.js.map +1 -1
  195. package/dist/types/conceptValueType.d.ts +4 -0
  196. package/dist/types/conceptValueType.d.ts.map +1 -0
  197. package/dist/types/conceptValueType.js +10 -0
  198. package/dist/types/conceptValueType.js.map +1 -0
  199. package/dist/types/modelBoundProperty.d.ts +6 -0
  200. package/dist/types/modelBoundProperty.d.ts.map +1 -0
  201. package/dist/types/modelBoundProperty.js +53 -0
  202. package/dist/types/modelBoundProperty.js.map +1 -0
  203. package/externalServices/ExternalServiceBuilder.ts +4 -4
  204. package/externalServices/ExternalServices.spec.ts +87 -0
  205. package/externalServices/ExternalServices.ts +3 -1
  206. package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
  207. package/index.ts +1 -0
  208. package/observation/observationDisposal.spec.ts +74 -0
  209. package/package.json +2 -1
  210. package/projections/Projections.childrenAndNested.spec.ts +2 -0
  211. package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
  212. package/projections/Projections.ts +9 -7
  213. package/projections/constantValueExpression.spec.ts +31 -0
  214. package/projections/constantValueExpression.ts +28 -0
  215. package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
  216. package/projections/declarative/ChildrenBuilder.ts +6 -0
  217. package/projections/declarative/FromBuilder.ts +3 -2
  218. package/projections/declarative/JoinBuilder.ts +2 -1
  219. package/projections/declarative/NestedBuilder.ts +3 -1
  220. package/projections/declarative/ProjectionBuilderCore.ts +20 -2
  221. package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
  222. package/projections/declarative/ProjectionBuilderFor.ts +7 -0
  223. package/projections/declarative/SetBuilder.ts +2 -1
  224. package/projections/modelBound/addFrom.ts +3 -2
  225. package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
  226. package/projections/modelBound/childrenFrom.ts +11 -5
  227. package/projections/modelBound/clearWith.ts +3 -2
  228. package/projections/modelBound/count.ts +3 -2
  229. package/projections/modelBound/decorators.standard.spec.ts +45 -0
  230. package/projections/modelBound/decrement.ts +3 -2
  231. package/projections/modelBound/fromAll.ts +3 -2
  232. package/projections/modelBound/fromEvery.ts +3 -2
  233. package/projections/modelBound/increment.ts +3 -2
  234. package/projections/modelBound/join.ts +3 -2
  235. package/projections/modelBound/nested.ts +3 -2
  236. package/projections/modelBound/noAutoMap.ts +3 -2
  237. package/projections/modelBound/removedWith.ts +3 -2
  238. package/projections/modelBound/removedWithJoin.ts +3 -2
  239. package/projections/modelBound/setFrom.ts +3 -2
  240. package/projections/modelBound/setFromContext.ts +3 -2
  241. package/projections/modelBound/setValue.ts +3 -2
  242. package/projections/modelBound/subtractFrom.ts +3 -2
  243. package/projections/notSetPropertyPath.ts +5 -0
  244. package/reactors/Reactors.ts +22 -7
  245. package/reactors/reactor.ts +2 -2
  246. package/readModels/MaterializedReadModels.spec.ts +22 -0
  247. package/readModels/MaterializedReadModels.ts +2 -4
  248. package/readModels/ReadModels.spec.ts +109 -1
  249. package/readModels/ReadModels.ts +21 -11
  250. package/readModels/conceptValueType.spec.ts +20 -0
  251. package/readModels/deserializeReadModel.spec.ts +84 -0
  252. package/readModels/deserializeReadModel.ts +82 -0
  253. package/readModels/discovery.spec.ts +27 -0
  254. package/readModels/rootReadModelTypes.spec.ts +115 -0
  255. package/readModels/rootReadModelTypes.ts +65 -0
  256. package/reducers/Reducers.ts +22 -7
  257. package/schemas/JsonSchemaGenerator.ts +4 -4
  258. package/schemas/legacyDesignMetadata.spec.ts +24 -2
  259. package/types/TypeDiscoverer.spec.ts +57 -0
  260. package/types/TypeDiscoverer.ts +27 -6
  261. package/types/TypeIntrospector.ts +3 -2
  262. package/types/conceptValueType.ts +11 -0
  263. package/types/modelBoundProperty.ts +53 -0
  264. package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
  265. package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
  266. package/dist/externalServices/ExternalServicesContracts.js +0 -654
  267. package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
  268. package/externalServices/ExternalServicesContracts.ts +0 -709
@@ -20,6 +20,12 @@ import { getSetFromMetadata, setFrom } from './setFrom.js';
20
20
  import { getSetValueMetadata, setValue } from './setValue.js';
21
21
  import { getSubtractFromMetadata, subtractFrom } from './subtractFrom.js';
22
22
  import { getTrackedJsonSchemaProperties, jsonSchemaProperty } from '../../schemas/jsonSchemaProperty.js';
23
+ import { DecoratorType } from '../../types/DecoratorType.js';
24
+ import { TypeDiscoverer } from '../../types/TypeDiscoverer.js';
25
+ import { DefaultClientArtifactsProvider } from '../../artifacts/DefaultClientArtifactsProvider.js';
26
+ import { ReadModels } from '../../readModels/ReadModels.js';
27
+ import type { ChronicleConnection } from '../../connection/ChronicleConnection.js';
28
+ import { vi } from 'vitest';
23
29
 
24
30
  class Changed {}
25
31
 
@@ -45,7 +51,46 @@ class Mappings {
45
51
  @jsonSchemaProperty() tracked!: string;
46
52
  }
47
53
 
54
+ class StandardMappedOnly {
55
+ @setFrom(Changed) value = '';
56
+ }
57
+
58
+ class UnconstructedMappedOnly {
59
+ @setFrom(Changed) value = '';
60
+ }
61
+
48
62
  describe('standard model-bound decorators', () => {
63
+ it('collects a standard-mapped class for lazy root discovery when constructed without file discovery', () => {
64
+ new StandardMappedOnly();
65
+ new StandardMappedOnly();
66
+ expect(TypeDiscoverer.default.getTypeByDecoratorTypeAndName(DecoratorType.ReadModel, 'StandardMappedOnly'))
67
+ .toBeUndefined();
68
+ expect(DefaultClientArtifactsProvider.default.readModels.filter(type => type === StandardMappedOnly)).toHaveLength(1);
69
+ });
70
+
71
+ it('registers the declaring class once even when subclasses and many instances are constructed', () => {
72
+ class Base {
73
+ @setFrom(Changed) value = '';
74
+ }
75
+ class Derived extends Base {}
76
+ const track = vi.spyOn(TypeDiscoverer.default, 'trackModelBoundProperty');
77
+ try {
78
+ for (let index = 0; index < 50; index++) new Base();
79
+ new Derived();
80
+ expect(track.mock.calls.filter(([type]) => type === Base)).toHaveLength(1);
81
+ expect(track.mock.calls.some(([type]) => type === Derived)).toBe(false);
82
+ } finally {
83
+ track.mockRestore();
84
+ }
85
+ });
86
+
87
+ it('should explain that an unconstructed property-only model was never registered', async () => {
88
+ const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '{"value":"stored"}' });
89
+ const connection = { readModels: { getInstanceByKey } } as unknown as ChronicleConnection;
90
+ const readModels = new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink');
91
+ await expect(readModels.findInstanceById(UnconstructedMappedOnly, 'id')).rejects.toThrow(/add a class-level @fromEvent/);
92
+ expect(getInstanceByKey).not.toHaveBeenCalled();
93
+ });
49
94
  it('stores class and property annotations without constructing an instance', () => {
50
95
  const target = Mappings.prototype;
51
96
  expect(isNoAutoMap(Mappings)).toBe(true);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the decrement property decorator. */
9
10
  export interface DecrementMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:decrement';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function decrement(eventType: Function, constantKey?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: DecrementMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the fromAll property decorator. */
9
10
  export interface FromAllMetadata {
@@ -25,7 +26,7 @@ const METADATA_KEY = 'chronicle:projection:fromAll';
25
26
  * @returns A property decorator.
26
27
  */
27
28
  export function fromAll(property?: string, contextProperty?: string): ChroniclePropertyDecorator {
28
- return decorateProperty((target: object, propertyKey: string | symbol) => {
29
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
29
30
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propertyKey.toString());
30
31
  const metadata: FromAllMetadata = { property, contextProperty };
31
32
  Reflect.defineMetadata(METADATA_KEY, metadata, target, propertyKey.toString());
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the fromEvery property decorator. */
9
10
  export interface FromEveryMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:fromEvery';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function fromEvery(property?: string, contextProperty?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propertyKey.toString());
27
28
  const metadata: FromEveryMetadata = { property, contextProperty };
28
29
  Reflect.defineMetadata(METADATA_KEY, metadata, target, propertyKey.toString());
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the increment property decorator. */
9
10
  export interface IncrementMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:increment';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function increment(eventType: Function, constantKey?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: IncrementMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the join property decorator. */
9
10
  export interface JoinMetadata {
@@ -25,7 +26,7 @@ const METADATA_KEY = 'chronicle:projection:join';
25
26
  * @returns A property decorator.
26
27
  */
27
28
  export function join(eventType: Function, on?: string, eventPropertyName?: string): ChroniclePropertyDecorator {
28
- return decorateProperty((target: object, propertyKey: string | symbol) => {
29
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
29
30
  const key = propertyKey.toString();
30
31
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
31
32
  const existing: JoinMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { decorateProperty, hasPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { hasPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
8
 
8
9
  const METADATA_KEY = 'chronicle:projection:nested';
9
10
 
@@ -13,7 +14,7 @@ const METADATA_KEY = 'chronicle:projection:nested';
13
14
  * @param target - The class prototype.
14
15
  * @param propertyKey - The property name.
15
16
  */
16
- const decorateNested = decorateProperty((target: object, propertyKey: string | symbol): void => {
17
+ const decorateNested = decorateModelBoundProperty((target: object, propertyKey: string | symbol): void => {
17
18
  const key = propertyKey.toString();
18
19
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
19
20
  Reflect.defineMetadata(METADATA_KEY, true, target, key);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { decorateClassOrProperty, hasPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { hasPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
+ import { decorateModelBoundClassOrProperty } from '../../types/modelBoundProperty.js';
7
8
 
8
9
  const CLASS_METADATA_KEY = 'chronicle:projection:noAutoMap:class';
9
10
  const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property';
@@ -18,7 +19,7 @@ const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property';
18
19
  * @param target - The class constructor, or the class prototype when used on a property.
19
20
  * @param propertyKey - The property name, when used as a property decorator.
20
21
  */
21
- const decorateNoAutoMap = decorateClassOrProperty((target: object, propertyKey?: string | symbol): void => {
22
+ const decorateNoAutoMap = decorateModelBoundClassOrProperty((target: object, propertyKey?: string | symbol): void => {
22
23
  if (propertyKey !== undefined) {
23
24
  const key = propertyKey.toString();
24
25
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChronicleClassOrPropertyDecorator, decorateClassOrProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundClassOrProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChronicleClassOrPropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the removedWith class or property decorator. */
9
10
  export interface RemovedWithMetadata {
@@ -28,7 +29,7 @@ const PROPERTY_METADATA_KEY = 'chronicle:projection:removedWith:property';
28
29
  * @returns A class and property decorator.
29
30
  */
30
31
  export function removedWith(eventType: Function, key?: string, parentKey?: string): ChronicleClassOrPropertyDecorator {
31
- return decorateClassOrProperty((target: object, propertyKey?: string | symbol) => {
32
+ return decorateModelBoundClassOrProperty((target: object, propertyKey?: string | symbol) => {
32
33
  if (propertyKey !== undefined) {
33
34
  const propKey = propertyKey.toString();
34
35
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propKey);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChronicleClassOrPropertyDecorator, decorateClassOrProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundClassOrProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChronicleClassOrPropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the removedWithJoin class or property decorator. */
9
10
  export interface RemovedWithJoinMetadata {
@@ -23,7 +24,7 @@ const PROPERTY_METADATA_KEY = 'chronicle:projection:removedWithJoin:property';
23
24
  * @returns A class and property decorator.
24
25
  */
25
26
  export function removedWithJoin(eventType: Function, key?: string): ChronicleClassOrPropertyDecorator {
26
- return decorateClassOrProperty((target: object, propertyKey?: string | symbol) => {
27
+ return decorateModelBoundClassOrProperty((target: object, propertyKey?: string | symbol) => {
27
28
  if (propertyKey !== undefined) {
28
29
  const propKey = propertyKey.toString();
29
30
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propKey);
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the setFrom property decorator. */
9
10
  export interface SetFromMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:setFrom';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function setFrom(eventType: Function, eventPropertyName?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: SetFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -5,7 +5,8 @@ import 'reflect-metadata';
5
5
  import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
6
6
  import type { EventContext } from '../../events/index.js';
7
7
  import { TypeIntrospector } from '../../types/index.js';
8
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
8
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
9
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
9
10
 
10
11
  /** Metadata stored by the setFromContext property decorator. */
11
12
  export interface SetFromContextMetadata {
@@ -29,7 +30,7 @@ export function setFromContext(
29
30
  eventType: Function,
30
31
  contextPropertyOrAccessor?: string | PropertyAccessor<EventContext>
31
32
  ): ChroniclePropertyDecorator {
32
- return decorateProperty((target: object, propertyKey: string | symbol) => {
33
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
33
34
  const key = propertyKey.toString();
34
35
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
35
36
  const existing: SetFromContextMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the setValue property decorator. */
9
10
  export interface SetValueMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:setValue';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function setValue(eventType: Function, value: unknown): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: SetValueMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -3,7 +3,8 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { TypeIntrospector } from '../../types/index.js';
6
- import { ChroniclePropertyDecorator, decorateProperty, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
6
+ import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
7
+ import { ChroniclePropertyDecorator, getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
7
8
 
8
9
  /** Metadata stored by the subtractFrom property decorator. */
9
10
  export interface SubtractFromMetadata {
@@ -22,7 +23,7 @@ const METADATA_KEY = 'chronicle:projection:subtractFrom';
22
23
  * @returns A property decorator.
23
24
  */
24
25
  export function subtractFrom(eventType: Function, eventPropertyName?: string): ChroniclePropertyDecorator {
25
- return decorateProperty((target: object, propertyKey: string | symbol) => {
26
+ return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
26
27
  const key = propertyKey.toString();
27
28
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
28
29
  const existing: SubtractFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
@@ -0,0 +1,5 @@
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
+ /** The wire representation of PropertyPath.NotSet, used for nested projections. */
5
+ export const notSetPropertyPath = '*NotSet*';
@@ -105,7 +105,9 @@ export class Reactors implements IReactors {
105
105
  private readonly _lifecycle: ConnectionLifecycle;
106
106
  private readonly _reactors = new Map<string, Constructor>();
107
107
  private readonly _queues = new Map<string, AsyncQueue<ReactorMessage>>();
108
+ private readonly _observations = new Map<string, AbortController>();
108
109
  private _registered = false;
110
+ private _disposed = false;
109
111
 
110
112
  /**
111
113
  * Creates a new {@link Reactors} instance.
@@ -133,6 +135,13 @@ export class Reactors implements IReactors {
133
135
  });
134
136
  }
135
137
 
138
+ /** Stops observations permanently when the owning client is disposed. */
139
+ dispose(): void {
140
+ this._disposed = true;
141
+ this._registered = false;
142
+ this.disconnectAll();
143
+ }
144
+
136
145
  /** @inheritdoc */
137
146
  async discover(): Promise<void> {
138
147
  this._reactors.clear();
@@ -147,7 +156,7 @@ export class Reactors implements IReactors {
147
156
 
148
157
  /** @inheritdoc */
149
158
  async register(): Promise<void> {
150
- if (this._registered) {
159
+ if (this._registered || this._disposed) {
151
160
  return;
152
161
  }
153
162
 
@@ -155,6 +164,7 @@ export class Reactors implements IReactors {
155
164
  await this.discover();
156
165
  }
157
166
 
167
+ if (this._disposed) return;
158
168
  this._logger.info('Registering reactors', { count: this._reactors.size });
159
169
  for (const [id, reactorType] of this._reactors) {
160
170
  this.startObservation(id, reactorType);
@@ -164,6 +174,7 @@ export class Reactors implements IReactors {
164
174
  }
165
175
 
166
176
  private startObservation(id: string, reactorType: Constructor): void {
177
+ if (this._disposed) return;
167
178
  const metadata = getReactorMetadata(reactorType)!;
168
179
  const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
169
180
  const eventTypes = this.getEventTypesFor(reactorType);
@@ -205,12 +216,12 @@ export class Reactors implements IReactors {
205
216
  private scheduleReobserve(id: string, reactorType: Constructor): void {
206
217
  // A disconnect clears the registration; the reconnect re-registers every
207
218
  // reactor from scratch, so retrying here as well would double up.
208
- if (!this._registered) {
219
+ if (!this._registered || this._disposed) {
209
220
  return;
210
221
  }
211
222
 
212
223
  const handle = setTimeout(() => {
213
- if (!this._registered) {
224
+ if (!this._registered || this._disposed) {
214
225
  return;
215
226
  }
216
227
 
@@ -228,7 +239,9 @@ export class Reactors implements IReactors {
228
239
  eventTypes: EventTypeEntry[]
229
240
  ): Promise<void> {
230
241
  const queue = new AsyncQueue<ReactorMessage>();
242
+ const controller = new AbortController();
231
243
  this._queues.set(id, queue);
244
+ this._observations.set(id, controller);
232
245
 
233
246
  queue.send({
234
247
  Content: {
@@ -259,7 +272,7 @@ export class Reactors implements IReactors {
259
272
  try {
260
273
  const reactorInstance = new (reactorType as new () => Record<string, Function>)();
261
274
 
262
- for await (const eventsToObserve of this._connection.reactors.observe(queue)) {
275
+ for await (const eventsToObserve of this._connection.reactors.observe(queue, { signal: controller.signal })) {
263
276
  let lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
264
277
  let state = ObservationState.Success;
265
278
  const exceptionMessages: string[] = [];
@@ -358,7 +371,9 @@ export class Reactors implements IReactors {
358
371
  // leak a duplicate stream on every reconnect.
359
372
  if (this._queues.get(id) === queue) {
360
373
  this._queues.delete(id);
374
+ this._observations.delete(id);
361
375
  }
376
+ queue.complete();
362
377
  }
363
378
  }
364
379
 
@@ -422,9 +437,9 @@ export class Reactors implements IReactors {
422
437
  }
423
438
 
424
439
  private disconnectAll(): void {
425
- for (const queue of this._queues.values()) {
426
- queue.complete();
427
- }
440
+ for (const controller of this._observations.values()) controller.abort();
441
+ for (const queue of this._queues.values()) queue.complete();
442
+ this._observations.clear();
428
443
  this._queues.clear();
429
444
  }
430
445
  }
@@ -25,8 +25,8 @@ export interface ReactorMetadata {
25
25
  * identifier. This is the TypeScript equivalent of the C# `[Reactor]` attribute.
26
26
  *
27
27
  * Reactors observe events from an event sequence and produce side effects.
28
- * Method dispatch is by convention: the type of the first parameter of each public method
29
- * determines which events it handles.
28
+ * Method dispatch is by convention: a handler's name is the event class name with
29
+ * its first letter lowercased (for example, `projectRegistered` handles `ProjectRegistered`).
30
30
  *
31
31
  * @param id - The unique identifier for the reactor. Defaults to the class name if omitted.
32
32
  * @param eventSequenceId - Optional explicit event sequence identifier.
@@ -25,6 +25,28 @@ function createMaterializedReadModels(instancesJson: string[], releaseResponse:
25
25
  }
26
26
 
27
27
  describe('MaterializedReadModels', () => {
28
+ describe('when materialized models have undecorated members', () => {
29
+ class Model { name = ''; count = 0; occurred = new Date(0); }
30
+ const json = JSON.stringify({ name: 'stored', count: 3, occurred: '2024-02-01T00:00:00.000Z' });
31
+
32
+ it('should restore members in paged reads', async () => {
33
+ const { readModels } = createMaterializedReadModels([json]);
34
+ const [instance] = await readModels.getInstances(Model);
35
+ expect(instance).toMatchObject({ name: 'stored', count: 3 });
36
+ expect(instance.occurred).toEqual(new Date('2024-02-01T00:00:00.000Z'));
37
+ });
38
+
39
+ it('should restore members in observed pages', async () => {
40
+ const connection = { materializedReadModels: { observeInstances: async function* () {
41
+ yield { Instances: [json] };
42
+ } } } as unknown as ChronicleConnection;
43
+ const readModels = new MaterializedReadModels('store', 'tenant', connection);
44
+ const response = await readModels.observeInstances(Model)[Symbol.asyncIterator]().next();
45
+ expect(response.value?.[0].name).toBe('stored');
46
+ expect(response.value?.[0].occurred).toBeInstanceOf(Date);
47
+ });
48
+ });
49
+
28
50
  describe('when a read model has a property decorated with @subject()', () => {
29
51
  class Employee {
30
52
  id = '';
@@ -7,6 +7,7 @@ import { ChronicleConnection } from '../connection/index.js';
7
7
  import { JsonSchemaGenerator } from '../schemas/index.js';
8
8
  import { getReadModelMetadata } from './readModel.js';
9
9
  import { ReadModelSubjectResolver } from './ReadModelSubjectResolver.js';
10
+ import { deserializeReadModel } from './deserializeReadModel.js';
10
11
  import type { IMaterializedReadModels } from './IMaterializedReadModels.js';
11
12
 
12
13
  const defaultTake = 50;
@@ -82,10 +83,7 @@ export class MaterializedReadModels implements IMaterializedReadModels {
82
83
  }
83
84
 
84
85
  private deserialize<TReadModel>(readModelType: Constructor<TReadModel>, json: string): TReadModel {
85
- if (!json) {
86
- return Object.create(readModelType.prototype) as TReadModel;
87
- }
88
- return JsonSerializer.deserialize(readModelType as Constructor<object>, json) as TReadModel;
86
+ return deserializeReadModel(readModelType, json);
89
87
  }
90
88
 
91
89
  private async releaseInstances<TReadModel>(readModelType: Constructor<TReadModel>, instances: TReadModel[]): Promise<TReadModel[]> {
@@ -2,7 +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 'reflect-metadata';
5
- import { field, type Constructor } from '@cratis/fundamentals';
5
+ import { ConceptAs, field, Guid, type Constructor } from '@cratis/fundamentals';
6
6
  import { describe, expect, it, vi } from 'vitest';
7
7
  import type { IClientArtifactsProvider } from '../artifacts/index.js';
8
8
  import type { ChronicleConnection } from '../connection/index.js';
@@ -10,6 +10,7 @@ import { subject } from '../compliance/subject.js';
10
10
  import { pii } from '../compliance/pii.js';
11
11
  import { reducer } from '../reducers/reducer.js';
12
12
  import { fromEvent } from '../projections/modelBound/fromEvent.js';
13
+ import { setFrom } from '../projections/modelBound/setFrom.js';
13
14
  import { ReadModels } from './ReadModels.js';
14
15
 
15
16
  // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
@@ -91,6 +92,62 @@ describe('ReadModels', () => {
91
92
  });
92
93
  });
93
94
 
95
+ describe('when reading a model with undecorated and model-bound members', () => {
96
+ class Code extends ConceptAs<string> {}
97
+ class MappedModel {
98
+ id = '';
99
+ name = '';
100
+ count = 0;
101
+ active = false;
102
+ occurred = new Date(0);
103
+ mapped = '';
104
+ code = new Code('initial');
105
+ guid = Guid.empty;
106
+ typed = Guid.empty;
107
+ }
108
+ field(Guid)(MappedModel.prototype, 'typed');
109
+ setFrom(SomeEvent, 'value')(MappedModel.prototype, 'mapped');
110
+ fromEvent(SomeEvent)(MappedModel);
111
+ const json = JSON.stringify({ id: 'a', name: 'stored', count: 42, active: true,
112
+ occurred: '2024-01-02T00:00:00.000Z', mapped: 'from event', code: 'stored-code',
113
+ guid: 'e951d9a0-720f-4e37-835f-701707f7b678',
114
+ typed: 'e951d9a0-720f-4e37-835f-701707f7b678', __internal: 'ignored' });
115
+
116
+ it('should restore all declared members for single and multiple reads', async () => {
117
+ const { readModels, getInstanceByKey } = createReadModels(MappedModel);
118
+ getInstanceByKey.mockResolvedValue({ ReadModel: json });
119
+ const getAllInstances = vi.fn().mockResolvedValue({ Instances: [json] });
120
+ const connection = { readModels: { getAllInstances } } as unknown as ChronicleConnection;
121
+ const provider = { readModels: [MappedModel], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
122
+ const all = await new ReadModels('store', 'tenant', connection, provider, 'sink').getInstances(MappedModel);
123
+ for (const instance of [await readModels.findInstanceById(MappedModel, 'a'),
124
+ await readModels.getInstanceById(MappedModel, 'a'), ...all]) {
125
+ expect(instance).toMatchObject({ id: 'a', name: 'stored', count: 42, active: true, mapped: 'from event' });
126
+ expect(instance?.occurred).toEqual(new Date('2024-01-02T00:00:00.000Z'));
127
+ expect(instance?.code).toBeInstanceOf(Code);
128
+ expect(instance?.code.value).toBe('stored-code');
129
+ expect(instance?.guid).toBeInstanceOf(Guid);
130
+ expect(instance?.typed).toBeInstanceOf(Guid);
131
+ expect(Object.hasOwn(instance as object, '__internal')).toBe(false);
132
+ }
133
+ });
134
+
135
+ it('should restore snapshot and watched instances', async () => {
136
+ const connection = { readModelExplorer: { allSnapshotsForReadModel: vi.fn().mockResolvedValue({ Data: [{ Instance: json }] }) },
137
+ readModels: { watch: async function* () {
138
+ yield { Namespace: 'tenant', ModelKey: 'a', ReadModel: json, Removed: false };
139
+ } } } as unknown as ChronicleConnection;
140
+ const provider = { readModels: [MappedModel], projections: [], reducers: [] } as unknown as IClientArtifactsProvider;
141
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
142
+ const snapshots = await readModels.getSnapshotsById(MappedModel, 'a');
143
+ const watched = await readModels.watch(MappedModel)[Symbol.asyncIterator]().next();
144
+ expect(snapshots[0].readModel.mapped).toBe('from event');
145
+ expect(snapshots[0].readModel.occurred).toBeInstanceOf(Date);
146
+ expect(watched.value?.readModel.name).toBe('stored');
147
+ expect(watched.value?.readModel.occurred).toBeInstanceOf(Date);
148
+ });
149
+ });
150
+
94
151
  describe('when reading a compliance-bearing reducer', () => {
95
152
  class PrivateModel { id = ''; ssn = ''; }
96
153
  field(String)(PrivateModel.prototype, 'id');
@@ -115,6 +172,34 @@ describe('ReadModels', () => {
115
172
  });
116
173
  });
117
174
 
175
+ describe('when watching a projection', () => {
176
+ class ProjectedModel { id = ''; }
177
+ field(String)(ProjectedModel.prototype, 'id');
178
+ fromEvent(SomeEvent)(ProjectedModel);
179
+
180
+ it('should skip the subscription marker and yield changes and removals', async () => {
181
+ const connection = {
182
+ readModels: { watch: async function* () {
183
+ yield { Subscribed: true, Namespace: '', ModelKey: '', ReadModel: '', Removed: false };
184
+ yield { Subscribed: false, Namespace: 'tenant', ModelKey: 'a', ReadModel: '{"id":"a"}', Removed: false };
185
+ yield { Subscribed: false, Namespace: 'tenant', ModelKey: '', ReadModel: '{"id":"empty-key"}', Removed: false };
186
+ yield { Subscribed: false, Namespace: 'tenant', ModelKey: 'a', ReadModel: '', Removed: true };
187
+ } }
188
+ } as unknown as ChronicleConnection;
189
+ const provider = { reducers: [], projections: [], readModels: [ProjectedModel] } as unknown as IClientArtifactsProvider;
190
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
191
+ const changesets = [];
192
+ for await (const changeset of readModels.watch(ProjectedModel)) {
193
+ changesets.push(changeset);
194
+ }
195
+
196
+ expect(changesets).toHaveLength(3);
197
+ expect(changesets[0]).toMatchObject({ namespace: 'tenant', key: 'a', removed: false, readModel: { id: 'a' } });
198
+ expect(changesets[1]).toMatchObject({ namespace: 'tenant', key: '', removed: false, readModel: { id: 'empty-key' } });
199
+ expect(changesets[2]).toMatchObject({ namespace: 'tenant', key: 'a', removed: true });
200
+ });
201
+ });
202
+
118
203
  describe('when watching a compliance-bearing reducer', () => {
119
204
  class PrivateModel { id = ''; ssn = ''; }
120
205
  field(String)(PrivateModel.prototype, 'id');
@@ -123,6 +208,29 @@ describe('ReadModels', () => {
123
208
  class PrivateReducer {}
124
209
  reducer('PrivateReducer', undefined, PrivateModel)(PrivateReducer);
125
210
 
211
+ it('should skip the subscription marker while releasing changes and retaining removals', async () => {
212
+ const release = vi.fn().mockResolvedValue({ HasError: false, Payload: '{"id":"a","ssn":"cleartext"}' });
213
+ const connection = {
214
+ readModels: { watch: async function* () {
215
+ yield { Subscribed: true, Namespace: '', ModelKey: '', ReadModel: '', Removed: false };
216
+ yield { Subscribed: false, Namespace: 'tenant', ModelKey: 'a', ReadModel: '{"id":"a","ssn":"ciphertext"}', Removed: false };
217
+ yield { Subscribed: false, Namespace: 'tenant', ModelKey: 'a', ReadModel: '', Removed: true };
218
+ } },
219
+ compliance: { release }
220
+ } as unknown as ChronicleConnection;
221
+ const provider = { reducers: [PrivateReducer], projections: [], readModels: [] } as unknown as IClientArtifactsProvider;
222
+ const readModels = new ReadModels('store', 'tenant', connection, provider, 'sink');
223
+ const changesets = [];
224
+ for await (const changeset of readModels.watch(PrivateModel)) {
225
+ changesets.push(changeset);
226
+ }
227
+
228
+ expect(changesets).toHaveLength(2);
229
+ expect(changesets[0]).toMatchObject({ namespace: 'tenant', key: 'a', removed: false, readModel: { id: 'a', ssn: 'cleartext' } });
230
+ expect(changesets[1]).toMatchObject({ namespace: 'tenant', key: 'a', removed: true });
231
+ expect(release).toHaveBeenCalledTimes(1);
232
+ });
233
+
126
234
  it('should reject rather than expose an unreleased changeset', async () => {
127
235
  const release = vi.fn().mockResolvedValue({ HasError: true, Error: 'denied' });
128
236
  const connection = {