@cratis/chronicle 6.16.0 → 6.17.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 (163) hide show
  1. package/artifacts/ActivatedArtifact.ts +5 -1
  2. package/artifacts/ArtifactCompletionFailed.ts +20 -0
  3. package/artifacts/ArtifactInvocationContext.ts +21 -0
  4. package/artifacts/for_withActivatedArtifact/when_completing/after_processing_failure.spec.ts +34 -0
  5. package/artifacts/index.ts +2 -0
  6. package/artifacts/withActivatedArtifact.ts +20 -3
  7. package/dist/artifacts/ActivatedArtifact.d.ts +4 -1
  8. package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -1
  9. package/dist/artifacts/ArtifactCompletionFailed.d.ts +9 -0
  10. package/dist/artifacts/ArtifactCompletionFailed.d.ts.map +1 -0
  11. package/dist/artifacts/ArtifactCompletionFailed.js +19 -0
  12. package/dist/artifacts/ArtifactCompletionFailed.js.map +1 -0
  13. package/dist/artifacts/ArtifactInvocationContext.d.ts +18 -0
  14. package/dist/artifacts/ArtifactInvocationContext.d.ts.map +1 -0
  15. package/dist/artifacts/ArtifactInvocationContext.js +4 -0
  16. package/dist/artifacts/ArtifactInvocationContext.js.map +1 -0
  17. package/dist/artifacts/index.d.ts +2 -0
  18. package/dist/artifacts/index.d.ts.map +1 -1
  19. package/dist/artifacts/index.js +1 -0
  20. package/dist/artifacts/index.js.map +1 -1
  21. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  22. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  23. package/dist/artifacts/withActivatedArtifact.js +22 -3
  24. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  25. package/dist/projections/CompiledProjectionDefinitions.d.ts +6 -0
  26. package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -1
  27. package/dist/projections/InvalidEventContextPropertyError.d.ts +10 -0
  28. package/dist/projections/InvalidEventContextPropertyError.d.ts.map +1 -0
  29. package/dist/projections/InvalidEventContextPropertyError.js +16 -0
  30. package/dist/projections/InvalidEventContextPropertyError.js.map +1 -0
  31. package/dist/projections/ProjectionCapabilityProvenance.d.ts +8 -0
  32. package/dist/projections/ProjectionCapabilityProvenance.d.ts.map +1 -0
  33. package/dist/projections/ProjectionCapabilityProvenance.js +4 -0
  34. package/dist/projections/ProjectionCapabilityProvenance.js.map +1 -0
  35. package/dist/projections/ProjectionDefinitionCompiler.d.ts +2 -0
  36. package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
  37. package/dist/projections/ProjectionDefinitionCompiler.js +148 -7
  38. package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
  39. package/dist/projections/ProjectionEventSchema.d.ts +10 -0
  40. package/dist/projections/ProjectionEventSchema.d.ts.map +1 -0
  41. package/dist/projections/ProjectionEventSchema.js +4 -0
  42. package/dist/projections/ProjectionEventSchema.js.map +1 -0
  43. package/dist/projections/VariantReclassifier.d.ts +2 -0
  44. package/dist/projections/VariantReclassifier.d.ts.map +1 -1
  45. package/dist/projections/VariantReclassifier.js.map +1 -1
  46. package/dist/projections/captureProjectionProvenance.d.ts +4 -0
  47. package/dist/projections/captureProjectionProvenance.d.ts.map +1 -0
  48. package/dist/projections/captureProjectionProvenance.js +56 -0
  49. package/dist/projections/captureProjectionProvenance.js.map +1 -0
  50. package/dist/projections/declarative/AllSetBuilder.d.ts.map +1 -1
  51. package/dist/projections/declarative/AllSetBuilder.js +2 -1
  52. package/dist/projections/declarative/AllSetBuilder.js.map +1 -1
  53. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  54. package/dist/projections/declarative/FromBuilder.js +12 -2
  55. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  56. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  57. package/dist/projections/declarative/JoinBuilder.js +2 -1
  58. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  59. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  60. package/dist/projections/declarative/ProjectionBuilderCore.js +8 -0
  61. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  62. package/dist/projections/declarative/RemovedWithBuilder.d.ts.map +1 -1
  63. package/dist/projections/declarative/RemovedWithBuilder.js +2 -1
  64. package/dist/projections/declarative/RemovedWithBuilder.js.map +1 -1
  65. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  66. package/dist/projections/declarative/SetBuilder.js +2 -1
  67. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  68. package/dist/projections/declarative/projectionBuilderProvenance.d.ts +16 -0
  69. package/dist/projections/declarative/projectionBuilderProvenance.d.ts.map +1 -0
  70. package/dist/projections/declarative/projectionBuilderProvenance.js +28 -0
  71. package/dist/projections/declarative/projectionBuilderProvenance.js.map +1 -0
  72. package/dist/projections/eventContextPropertyExpression.d.ts +3 -0
  73. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -0
  74. package/dist/projections/eventContextPropertyExpression.js +69 -0
  75. package/dist/projections/eventContextPropertyExpression.js.map +1 -0
  76. package/dist/projections/eventContractPath.d.ts +4 -0
  77. package/dist/projections/eventContractPath.d.ts.map +1 -0
  78. package/dist/projections/eventContractPath.js +7 -0
  79. package/dist/projections/eventContractPath.js.map +1 -0
  80. package/dist/projections/index.d.ts +1 -0
  81. package/dist/projections/index.d.ts.map +1 -1
  82. package/dist/projections/index.js +1 -0
  83. package/dist/projections/index.js.map +1 -1
  84. package/dist/projections/invalidEventContextPropertyInProjection.d.ts +4 -0
  85. package/dist/projections/invalidEventContextPropertyInProjection.d.ts.map +1 -0
  86. package/dist/projections/invalidEventContextPropertyInProjection.js +7 -0
  87. package/dist/projections/invalidEventContextPropertyInProjection.js.map +1 -0
  88. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  89. package/dist/projections/modelBound/childrenAndNestedBuilder.js +2 -1
  90. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  91. package/dist/reactors/Reactors.d.ts.map +1 -1
  92. package/dist/reactors/Reactors.js +12 -3
  93. package/dist/reactors/Reactors.js.map +1 -1
  94. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -1
  95. package/dist/readModels/rootReadModelTypes.js +12 -1
  96. package/dist/readModels/rootReadModelTypes.js.map +1 -1
  97. package/dist/reducers/Reducers.d.ts.map +1 -1
  98. package/dist/reducers/Reducers.js +12 -3
  99. package/dist/reducers/Reducers.js.map +1 -1
  100. package/dist/testing/projections/ProjectionCapabilities.d.ts +17 -0
  101. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -0
  102. package/dist/testing/projections/ProjectionCapabilities.js +233 -0
  103. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -0
  104. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts +11 -0
  105. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts.map +1 -0
  106. package/dist/testing/projections/UnsupportedProjectionOperation.js +16 -0
  107. package/dist/testing/projections/UnsupportedProjectionOperation.js.map +1 -0
  108. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +7 -0
  109. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -0
  110. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +16 -0
  111. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -0
  112. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts +10 -0
  113. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts.map +1 -0
  114. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js +20 -0
  115. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js.map +1 -0
  116. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts +3 -0
  117. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts.map +1 -0
  118. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js +7 -0
  119. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js.map +1 -0
  120. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts +12 -0
  121. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts.map +1 -0
  122. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js +34 -0
  123. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js.map +1 -0
  124. package/dist/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +1 -1
  126. package/projections/CompiledProjectionDefinitions.ts +6 -0
  127. package/projections/InvalidEventContextPropertyError.ts +14 -0
  128. package/projections/ProjectionCapabilityProvenance.ts +10 -0
  129. package/projections/ProjectionDefinitionCompiler.ts +146 -7
  130. package/projections/ProjectionEventSchema.ts +13 -0
  131. package/projections/VariantReclassifier.ts +2 -0
  132. package/projections/captureProjectionProvenance.ts +50 -0
  133. package/projections/declarative/AllSetBuilder.ts +2 -1
  134. package/projections/declarative/CompositeKeyBuilder.spec.ts +1 -1
  135. package/projections/declarative/FromBuilder.ts +12 -2
  136. package/projections/declarative/JoinBuilder.ts +2 -1
  137. package/projections/declarative/ProjectionBuilderCore.ts +7 -0
  138. package/projections/declarative/ProjectionBuilderFor.spec.ts +4 -4
  139. package/projections/declarative/RemovedWithBuilder.ts +2 -1
  140. package/projections/declarative/SetBuilder.ts +2 -1
  141. package/projections/declarative/projectionBuilderProvenance.ts +40 -0
  142. package/projections/eventContextExpressions.spec.ts +273 -0
  143. package/projections/eventContextPropertyExpression.ts +72 -0
  144. package/projections/eventContractPath.ts +9 -0
  145. package/projections/index.ts +1 -0
  146. package/projections/invalidEventContextPropertyInProjection.ts +13 -0
  147. package/projections/modelBound/childrenAndNestedBuilder.ts +2 -1
  148. package/reactors/Reactors.ts +13 -3
  149. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +93 -2
  150. package/readModels/rootReadModelTypes.ts +11 -1
  151. package/reducers/Reducers.ts +14 -3
  152. package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +67 -1
  153. package/testing/projections/ProjectionCapabilities.ts +213 -0
  154. package/testing/projections/UnsupportedProjectionOperation.ts +16 -0
  155. package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +12 -0
  156. package/testing/projections/for_ProjectionCapabilities/given/Model.ts +13 -0
  157. package/testing/projections/for_ProjectionCapabilities/given/Removed.ts +7 -0
  158. package/testing/projections/for_ProjectionCapabilities/given/compile.ts +39 -0
  159. package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts +68 -0
  160. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +230 -0
  161. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +73 -0
  162. package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +118 -0
  163. package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +103 -0
@@ -0,0 +1,273 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { Constructor, field } from '@cratis/fundamentals';
6
+ import { chai, describe, expect, it } from 'vitest';
7
+ import type { IClientArtifactsProvider } from '../artifacts/index.js';
8
+ import type { ChronicleConnection } from '../connection/index.js';
9
+ import { InvalidEventContextPropertyError } from '../index.js';
10
+ import { eventType } from '../events/eventTypeDecorator.js';
11
+ import { ProjectionBuilderFor } from './declarative/ProjectionBuilderFor.js';
12
+ import { projection } from './declarative/projection.js';
13
+ import { childrenFrom } from './modelBound/childrenFrom.js';
14
+ import { fromEvent } from './modelBound/fromEvent.js';
15
+ import { fromEvery } from './modelBound/fromEvery.js';
16
+ import { nested } from './modelBound/nested.js';
17
+ import { setFromContext } from './modelBound/setFromContext.js';
18
+ import { ProjectionDefinitionCompiler } from './ProjectionDefinitionCompiler.js';
19
+ import { Projections } from './Projections.js';
20
+
21
+ class Recorded { reference!: string; }
22
+ eventType()(Recorded);
23
+ class Removed {}
24
+ eventType()(Removed);
25
+ class Child { happened!: Date; }
26
+ setFromContext(Recorded, 'occurred')(Child.prototype, 'happened');
27
+ class Model { occurred!: Date; happened!: Date; viaAccessor!: Date; children!: Child[]; detail!: Child; }
28
+ setFromContext(Recorded)(Model.prototype, 'occurred');
29
+ setFromContext(Recorded, 'occurred')(Model.prototype, 'happened');
30
+ setFromContext(Recorded, context => context.occurred)(Model.prototype, 'viaAccessor');
31
+ childrenFrom(Recorded, Child)(Model.prototype, 'children');
32
+ field(Child)(Model.prototype, 'detail');
33
+ nested(Model.prototype, 'detail');
34
+ fromEvery(undefined, 'sequenceNumber')(Model.prototype, 'happened');
35
+ fromEvent(Recorded)(Model);
36
+
37
+ interface FromRecord { Value: { Properties: Record<string, string>; Key: string; ParentKey: string } }
38
+ interface JoinRecord { Value: { Properties: Record<string, string>; Key: string } }
39
+ interface Definition {
40
+ From: FromRecord[];
41
+ Join: JoinRecord[];
42
+ All: { Properties: Record<string, string> };
43
+ RemovedWith: Array<{ Value: { Key: string } }>;
44
+ Children: Record<string, { From: FromRecord[] }>;
45
+ Nested: Record<string, { From: FromRecord[] }>;
46
+ }
47
+
48
+ function declarative(configure: (builder: ProjectionBuilderFor<Model>) => void): Definition {
49
+ const builder = new ProjectionBuilderFor<Model>();
50
+ configure(builder);
51
+ return builder.build('context-spec', 'Model') as unknown as Definition;
52
+ }
53
+
54
+ function modelBound(readModel: Constructor = Model): Definition {
55
+ const artifacts: IClientArtifactsProvider = {
56
+ projections: [], readModels: [readModel], globalForHandlers: [], eventTypes: [Recorded],
57
+ reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
58
+ };
59
+ return new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([], [readModel]).definitions[0] as unknown as Definition;
60
+ }
61
+
62
+ function compileDeclarative(configure: (builder: ProjectionBuilderFor<Model>) => void): void {
63
+ class InvalidDeclarativeProjection {
64
+ define(builder: ProjectionBuilderFor<Model>): void { configure(builder); }
65
+ }
66
+ projection('invalid-context', Model)(InvalidDeclarativeProjection);
67
+ const artifacts: IClientArtifactsProvider = {
68
+ projections: [InvalidDeclarativeProjection], readModels: [Model], globalForHandlers: [], eventTypes: [Recorded],
69
+ reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
70
+ };
71
+ new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([InvalidDeclarativeProjection], []);
72
+ }
73
+
74
+ function register(projections: Constructor[], readModels: Constructor[]): Promise<void> {
75
+ const artifacts: IClientArtifactsProvider = {
76
+ projections, readModels, globalForHandlers: [], eventTypes: [Recorded],
77
+ reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
78
+ };
79
+ return new Projections('test-store', 'test-namespace', {} as ChronicleConnection, artifacts, 'test-sink').register();
80
+ }
81
+
82
+ chai.should();
83
+
84
+ describe('event context expressions', () => {
85
+ it('should map a model-bound implicit context property to the kernel expression', () => {
86
+ modelBound().From[0].Value.Properties.occurred.should.equal('$eventContext(Occurred)');
87
+ });
88
+
89
+ it('should map a model-bound explicit context property to the kernel expression', () => {
90
+ modelBound().From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
91
+ });
92
+
93
+ it('should map a model-bound context accessor to the kernel expression', () => {
94
+ modelBound().From[0].Value.Properties.viaAccessor.should.equal('$eventContext(Occurred)');
95
+ });
96
+
97
+ it('should map a child model-bound context property to the kernel expression', () => {
98
+ modelBound().Children.children.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
99
+ });
100
+
101
+ it('should map a nested model-bound context property to the kernel expression', () => {
102
+ modelBound().Nested.detail.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
103
+ });
104
+
105
+ it('should map a model-bound fromEvery context property to the kernel expression', () => {
106
+ modelBound().All.Properties.happened.should.equal('$eventContext(SequenceNumber)');
107
+ });
108
+
109
+ it('should set an event context property on a declarative from mapping', () => {
110
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred')))
111
+ .From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
112
+ });
113
+
114
+ it('should set an event context property on a declarative join mapping', () => {
115
+ declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).set(model => model.happened).toEventContextProperty('occurred')))
116
+ .Join[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
117
+ });
118
+
119
+ it('should set an event context property on a declarative fromEvery mapping', () => {
120
+ declarative(builder => builder.fromEvery(all => all.set(model => model.happened).toEventContextProperty('sequenceNumber')))
121
+ .All.Properties.happened.should.equal('$eventContext(SequenceNumber)');
122
+ });
123
+
124
+ it('should preserve PascalCase and capitalize every nested context segment', () => {
125
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('CausedBy.subject')))
126
+ .From[0].Value.Properties.happened.should.equal('$eventContext(CausedBy.Subject)');
127
+ });
128
+
129
+ it('should allow a kernel-derived function on a known context property', () => {
130
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred.week()')))
131
+ .From[0].Value.Properties.happened.should.equal('$eventContext(Occurred.Week())');
132
+ });
133
+
134
+ it('should normalize a mixed-case kernel-derived function', () => {
135
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred.WEEK()')))
136
+ .From[0].Value.Properties.happened.should.equal('$eventContext(Occurred.Week())');
137
+ });
138
+
139
+ it('should reject the week function on a non-date property, in a nonterminal position, or without parentheses', () => {
140
+ for (const path of ['eventSourceId.week()', 'occurred.week.day', 'occurred.week', 'causedBy.subject.week()']) {
141
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
142
+ all.set(model => model.happened).toEventContextProperty(path))))
143
+ .toThrow(`Invalid event context property '${path}'`);
144
+ }
145
+ });
146
+
147
+ it('should reject a derived function the kernel does not recognize', () => {
148
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
149
+ all.set(model => model.happened).toEventContextProperty('occurred.ISOWeek()'))))
150
+ .toThrow(/Invalid event context property 'occurred.ISOWeek\(\)'.*'InvalidDeclarativeProjection'/);
151
+ });
152
+
153
+ it('should map the on-behalf-of identity as a whole', () => {
154
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('causedBy.onBehalfOf')))
155
+ .From[0].Value.Properties.happened.should.equal('$eventContext(CausedBy.OnBehalfOf)');
156
+ });
157
+
158
+ it('should reject unknown identity members and deeper onBehalfOf chains', () => {
159
+ for (const path of ['causedBy.unknown', 'causedBy.onBehalfOf.unknown', 'causedBy.subject.name',
160
+ 'causedBy.onBehalfOf.userName', 'causedBy.onBehalfOf.onBehalfOf.userName', 'causedBy.onBehalfOf.onBehalfOf']) {
161
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
162
+ all.set(model => model.happened).toEventContextProperty(path))))
163
+ .toThrow(`Invalid event context property '${path}'`);
164
+ }
165
+ });
166
+
167
+ it('should map the whole causation collection', () => {
168
+ declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('causation')))
169
+ .From[0].Value.Properties.happened.should.equal('$eventContext(Causation)');
170
+ });
171
+
172
+ it('should reject paths into the causation and tags collections', () => {
173
+ for (const path of ['causation.type', 'causation.unknown', 'causation.properties.unknown', 'tags.name']) {
174
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
175
+ all.set(model => model.happened).toEventContextProperty(path))))
176
+ .toThrow(`Invalid event context property '${path}'`);
177
+ }
178
+ });
179
+
180
+ it('should use a context property as a from key', () => {
181
+ declarative(builder => builder.from(Recorded, from => from.usingKeyFromContext('eventSourceId')))
182
+ .From[0].Value.Key.should.equal('$eventContext(EventSourceId)');
183
+ });
184
+
185
+ it('should use a context property as a from parent key', () => {
186
+ declarative(builder => builder.from(Recorded, from => from.usingParentKeyFromContext('eventSourceId')))
187
+ .From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
188
+ });
189
+
190
+ it('should use a context property as a join key', () => {
191
+ declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).usingKeyFromContext('eventSourceId')))
192
+ .Join[0].Value.Key.should.equal('$eventContext(EventSourceId)');
193
+ });
194
+
195
+ it('should leave a join parent key unset because JoinDefinition has no parent key', () => {
196
+ declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).usingParentKeyFromContext('eventSourceId')))
197
+ .Join[0].Value.Key.should.equal('$eventSourceId');
198
+ });
199
+
200
+ it('should use a context property as a removal key', () => {
201
+ declarative(builder => builder.removedWith(Removed, removed => removed.usingKeyFromContext('eventSourceId')))
202
+ .RemovedWith[0].Value.Key.should.equal('$eventContext(EventSourceId)');
203
+ });
204
+
205
+ it('should name the model-bound projection and read model identifier on registration failure', async () => {
206
+ class InvalidModel { static readonly readModelId = 'invalid-model-id'; happened!: Date; }
207
+ setFromContext(Recorded, 'invalidProperty')(InvalidModel.prototype, 'happened');
208
+ fromEvent(Recorded)(InvalidModel);
209
+ const attempt = register([], [InvalidModel]);
210
+ await expect(attempt).rejects.toThrow("Invalid event context property 'invalidProperty' in projection 'InvalidModel' (read model 'invalid-model-id').");
211
+ await expect(attempt).rejects.toMatchObject({ cause: expect.any(InvalidEventContextPropertyError) });
212
+ });
213
+
214
+ it('should reject an unknown nested identity member in a model-bound projection', () => {
215
+ class InvalidIdentityModel { happened!: string; }
216
+ setFromContext(Recorded, 'causedBy.missing')(InvalidIdentityModel.prototype, 'happened');
217
+ fromEvent(Recorded)(InvalidIdentityModel);
218
+ expect(() => modelBound(InvalidIdentityModel))
219
+ .toThrow(/Invalid event context property 'causedBy.missing'.*'InvalidIdentityModel'/);
220
+ });
221
+
222
+ it('should reject an invalid implicit model-bound context property at registration', () => {
223
+ class ImplicitInvalidModel { unknown!: string; }
224
+ setFromContext(Recorded)(ImplicitInvalidModel.prototype, 'unknown');
225
+ fromEvent(Recorded)(ImplicitInvalidModel);
226
+ expect(() => modelBound(ImplicitInvalidModel)).toThrow(/Invalid event context property 'unknown'.*'ImplicitInvalidModel'/);
227
+ });
228
+
229
+ it('should name the declarative projection and read model identifier on registration failure', async () => {
230
+ class InvalidDeclarativeModel { static readonly readModelId = 'declarative-model-id'; happened!: Date; }
231
+ class InvalidDeclarativeProjection {
232
+ define(builder: ProjectionBuilderFor<InvalidDeclarativeModel>): void {
233
+ builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('unknown'));
234
+ }
235
+ }
236
+ projection('invalid-context', InvalidDeclarativeModel)(InvalidDeclarativeProjection);
237
+ const attempt = register([InvalidDeclarativeProjection], [InvalidDeclarativeModel]);
238
+ await expect(attempt).rejects.toThrow("Invalid event context property 'unknown' in projection 'InvalidDeclarativeProjection' (read model 'declarative-model-id').");
239
+ await expect(attempt).rejects.toMatchObject({ cause: expect.any(InvalidEventContextPropertyError) });
240
+ });
241
+
242
+ it('should reject an invalid declarative all-set context property at registration', () => {
243
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
244
+ all.set(model => model.happened).toEventContextProperty('unknown'))))
245
+ .toThrow(/Invalid event context property 'unknown'.*'InvalidDeclarativeProjection'.*'Model'/);
246
+ });
247
+
248
+ it('should reject invalid context paths that the kernel cannot parse', () => {
249
+ expect(() => compileDeclarative(builder => builder.fromEvery(all =>
250
+ all.set(model => model.happened).toEventContextProperty('causedBy.subject[0]'))))
251
+ .toThrow(/Invalid event context property 'causedBy.subject\[0\]'.*'InvalidDeclarativeProjection'/);
252
+ });
253
+
254
+ it('should reject invalid context properties in key and parent-key APIs at registration', () => {
255
+ const mappings = [
256
+ (builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingKeyFromContext('missingKey')),
257
+ (builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingParentKeyFromContext('missingParent')),
258
+ (builder: ProjectionBuilderFor<Model>) => builder.join(Recorded, join => join.on(model => model.occurred).usingKeyFromContext('missingJoinKey')),
259
+ (builder: ProjectionBuilderFor<Model>) => builder.removedWith(Removed, removed => removed.usingKeyFromContext('missingRemovalKey'))
260
+ ];
261
+ for (const configure of mappings) {
262
+ expect(() => compileDeclarative(configure)).toThrow(/Invalid event context property 'missing.*' in projection 'InvalidDeclarativeProjection'/);
263
+ }
264
+ });
265
+
266
+ it('should use a context property on a declarative child and child parent key', () => {
267
+ const child = declarative(builder => builder.children<Child>(model => model.children, children => children
268
+ .from(Recorded, from => from.usingParentKeyFromContext('eventSourceId')
269
+ .set(model => model.happened).toEventContextProperty('occurred'))));
270
+ child.Children.children.From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
271
+ child.Children.children.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
272
+ });
273
+ });
@@ -0,0 +1,72 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { EventContext } from '../events/EventContext.js';
5
+ import { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
6
+
7
+ // EventContext is an interface (erased at runtime). Keep this exhaustive against its keys so
8
+ // additions/removals to the SDK type cannot silently leave registration validation out of date.
9
+ const contextProperties = {
10
+ sequenceNumber: true,
11
+ eventSourceId: true,
12
+ eventStore: true,
13
+ namespace: true,
14
+ eventSourceType: true,
15
+ eventStreamType: true,
16
+ eventStreamId: true,
17
+ subject: true,
18
+ hash: true,
19
+ causedBy: true,
20
+ observationState: true,
21
+ eventType: true,
22
+ occurred: true,
23
+ correlationId: true,
24
+ causation: true,
25
+ tags: true
26
+ } satisfies Record<keyof EventContext, true>;
27
+
28
+ // Mirror the kernel's DerivedPropertyFunctions registry: Week() applies to dates only.
29
+ const derivedFunctions: Record<string, string> = { week: 'Week' };
30
+ const dateProperties = new Set(['occurred']);
31
+ const identityProperties = new Set(['subject', 'name', 'userName']);
32
+
33
+ function lower(segment: string): string {
34
+ return segment.charAt(0).toLowerCase() + segment.slice(1);
35
+ }
36
+
37
+ // Accept only paths the kernel can resolve: a root property or an identity member of causedBy.
38
+ // causedBy.onBehalfOf is allowed only as a whole value: traversing it when it is missing makes the
39
+ // kernel construct identities recursively. Nothing inside the causation or tags collections, which a
40
+ // dotted path cannot traverse.
41
+ function validNestedPath(root: string, segments: string[]): boolean {
42
+ if (segments.length === 0) return true;
43
+ if (root !== 'causedBy') return false;
44
+ const members = segments.map(lower);
45
+ return members.length === 1 && (identityProperties.has(members[0]) || members[0] === 'onBehalfOf');
46
+ }
47
+
48
+ /** Build the expression read by Chronicle's event-context resolver, using CLR property casing. */
49
+ export function eventContextPropertyExpression(propertyPath: string): string {
50
+ // The kernel accepts dotted CLR paths (and terminal derived-property functions) using
51
+ // [A-Za-z.()]. Reject unsupported characters and unknown root properties before registration.
52
+ const segments = propertyPath.split('.');
53
+ const contextProperty = segments[0].charAt(0).toLowerCase() + segments[0].slice(1);
54
+ if (!/^[A-Za-z]+(?:\.[A-Za-z]+)*(?:\(\))?$/.test(propertyPath) ||
55
+ !Object.hasOwn(contextProperties, contextProperty)) {
56
+ throw new InvalidEventContextPropertyError(propertyPath);
57
+ }
58
+ const lastIndex = segments.length - 1;
59
+ const isFunction = segments[lastIndex].endsWith('()');
60
+ if (isFunction) {
61
+ const functionName = segments[lastIndex].slice(0, -2).toLowerCase();
62
+ if (!Object.hasOwn(derivedFunctions, functionName) || lastIndex !== 1 || !dateProperties.has(contextProperty)) {
63
+ throw new InvalidEventContextPropertyError(propertyPath);
64
+ }
65
+ segments[lastIndex] = derivedFunctions[functionName];
66
+ } else if (segments.some(segment => Object.hasOwn(derivedFunctions, segment.toLowerCase())) ||
67
+ !validNestedPath(contextProperty, segments.slice(1))) {
68
+ throw new InvalidEventContextPropertyError(propertyPath);
69
+ }
70
+ const clrPath = segments.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join('.');
71
+ return `$eventContext(${clrPath}${propertyPath.endsWith('()') ? '()' : ''})`;
72
+ }
@@ -0,0 +1,9 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { ContractEventType } from './declarative/ProjectionBuilderCore.js';
5
+
6
+ /** Stable, generation-aware path shared by provenance and capability diagnostics. */
7
+ export function eventContractPath(section: string, eventType: ContractEventType): string {
8
+ return `${section}[${eventType.Id}:${eventType.Generation}]`;
9
+ }
@@ -7,6 +7,7 @@ export { Projections } from './Projections.js';
7
7
  export type { ProjectionState } from './ProjectionState.js';
8
8
  export type { ProjectionQueryResult } from './ProjectionQueryResult.js';
9
9
  export { UnableToQueryProjection } from './UnableToQueryProjection.js';
10
+ export { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
10
11
  export { VariantMustDeclareEntersOnEvent } from './VariantMustDeclareEntersOnEvent.js';
11
12
  export { GlobalHandlerPropertyNotOnVariant } from './GlobalHandlerPropertyNotOnVariant.js';
12
13
  export * from './declarative/index.js';
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
5
+
6
+ /** Add projection and read-model context to a rejected event-context path. */
7
+ export function invalidEventContextPropertyInProjection(
8
+ error: InvalidEventContextPropertyError,
9
+ projectionName: string,
10
+ readModelId: string
11
+ ): Error {
12
+ return new Error(`Invalid event context property '${error.propertyPath}' in projection '${projectionName}' (read model '${readModelId}').`, { cause: error });
13
+ }
@@ -6,6 +6,7 @@ import { AutoMap } from '@cratis/chronicle.contracts';
6
6
  import { Constructor, Fields } from '@cratis/fundamentals';
7
7
  import { TypeIntrospector } from '../../types/index.js';
8
8
  import { constantValueExpression } from '../constantValueExpression.js';
9
+ import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
9
10
  import { notSetPropertyPath } from '../notSetPropertyPath.js';
10
11
  import { getEventTypeFor } from '../../events/eventTypeDecorator.js';
11
12
  import { getAddFromMetadata } from './addFrom.js';
@@ -109,7 +110,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
109
110
 
110
111
  for (const mapping of getSetFromContextMetadata(prototype, property)) {
111
112
  const entry = ensureFromEntry(fromByEventType, mapping.eventType);
112
- entry.Value.Properties[property] = mapping.contextPropertyName ?? property;
113
+ entry.Value.Properties[property] = eventContextPropertyExpression(mapping.contextPropertyName ?? property);
113
114
  }
114
115
 
115
116
  for (const mapping of getSetValueMetadata(prototype, property)) {
@@ -28,6 +28,7 @@ import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
28
28
  import { ArtifactKind } from '../artifacts/ArtifactKind.js';
29
29
  import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
30
30
  import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
31
+ import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
31
32
 
32
33
  /** Expression used to partition reactor observations by event source ID. */
33
34
  const EVENT_SOURCE_ID_KEY = '$eventSourceId';
@@ -306,12 +307,16 @@ export class Reactors implements IReactors {
306
307
  signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
307
308
  replayState: eventsToObserve.ReplayState
308
309
  }, this._artifactActivator, artifact => runActivated(artifact, () =>
309
- notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition)));
310
+ notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
311
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState }));
310
312
  } else if (reactorInstance) {
311
313
  await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
312
314
  }
313
315
  } catch (err) {
314
316
  this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
317
+ if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
318
+ exceptionMessages.push(String(err.processingError));
319
+ }
315
320
  exceptionMessages.push(String(err));
316
321
  exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
317
322
  state = ObservationState.Failed;
@@ -368,7 +373,7 @@ export class Reactors implements IReactors {
368
373
  const handlerResult = await artifact.instance[methodName](content, context, services);
369
374
  await dispatchReactorSideEffects(this._eventLog, handlerResult, context, reactorType as Function,
370
375
  this._eventStoreName, this._namespace, this._resultHandler);
371
- });
376
+ }, { delivery: ArtifactDelivery.Events, eventContext: context, methodName });
372
377
 
373
378
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
374
379
  } catch (err) {
@@ -398,8 +403,13 @@ export class Reactors implements IReactors {
398
403
  } catch (err) {
399
404
  this._logger.error('Error activating reactor', { reactorId: id, error: String(err) });
400
405
  exceptionMessages.push(String(err));
401
- exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
406
+ const completionStack = err instanceof Error ? (err.stack ?? '') : '';
407
+ exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
408
+ ? `${exceptionStackTrace}\n${completionStack}` : completionStack;
402
409
  state = ObservationState.Failed;
410
+ if (err instanceof ArtifactCompletionFailed) {
411
+ lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
412
+ }
403
413
  }
404
414
  } else {
405
415
  await processEvents({ instance: reactorInstance ?? {} });
@@ -11,6 +11,7 @@ import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifac
11
11
  import { ArtifactDelivery } from '../../../artifacts/ArtifactDelivery.js';
12
12
  import { ArtifactKind } from '../../../artifacts/ArtifactKind.js';
13
13
  import type { ArtifactActivationContext } from '../../../artifacts/ArtifactActivationContext.js';
14
+ import type { ArtifactInvocationContext } from '../../../artifacts/ArtifactInvocationContext.js';
14
15
  import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
15
16
  import type { IEventStore } from '../../../IEventStore.js';
16
17
  import type { IEventLog } from '../../../eventSequences/IEventLog.js';
@@ -159,10 +160,11 @@ describe('when delivering reactor batches', () => {
159
160
  observed.length = 0;
160
161
  serviceReferences.length = 0;
161
162
  const contexts: ArtifactActivationContext[] = [];
163
+ const invocations: ArtifactInvocationContext[] = [];
162
164
  const boundaries: string[] = [];
163
165
  const activator: ClientArtifactsActivator = (type, context) => {
164
166
  contexts.push(context);
165
- return { instance: new type(), run: async callback => { boundaries.push('enter');
167
+ return { instance: new type(), run: async (callback, invocation) => { invocations.push(invocation!); boundaries.push('enter');
166
168
  try { return await callback(); } finally { boundaries.push('exit'); } },
167
169
  dispose: () => { boundaries.push('dispose'); } };
168
170
  };
@@ -177,6 +179,11 @@ describe('when delivering reactor batches', () => {
177
179
  contexts[0].readModels.should.equal(result.store.readModels);
178
180
  if (contexts[0].delivery === ArtifactDelivery.Events) contexts[0].eventContext.sequenceNumber.should.equal(1n);
179
181
  boundaries.should.deep.equal(['enter', 'exit', 'enter', 'exit', 'dispose', 'enter', 'exit', 'dispose']);
182
+ invocations.map(invocation => invocation.delivery).should.deep.equal([ArtifactDelivery.Events, ArtifactDelivery.Events, ArtifactDelivery.Events]);
183
+ invocations.map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.eventContext.sequenceNumber : 0n)
184
+ .should.deep.equal([1n, 2n, 3n]);
185
+ invocations.map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.methodName : '')
186
+ .should.deep.equal(['activationEvent', 'activationEvent', 'activationEvent']);
180
187
  observed.should.deep.equal(['one:book-1', 'two:book-1', 'three:book-1']);
181
188
  serviceReferences.every(services => services.eventStore === result.store && services.readModels === result.store.readModels).should.be.true;
182
189
  });
@@ -241,9 +248,16 @@ describe('when delivering reactor batches', () => {
241
248
  it('should notify separately before processing events in the same message', async () => {
242
249
  observed.length = 0;
243
250
  const contexts: ArtifactActivationContext[] = [];
244
- const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type() }; };
251
+ const invocations: ArtifactInvocationContext[] = [];
252
+ const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type(),
253
+ run: async (callback, invocation) => { invocations.push(invocation!); return callback(); } }; };
245
254
  const result = await observe(ActivationReactor, [delivery([event('live', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
246
255
  observed.should.deep.equal(['notification', 'live:book-1']);
256
+ invocations.should.deep.equal([
257
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: ReplayState.BeginReplay },
258
+ { delivery: ArtifactDelivery.Events, eventContext: contexts[1].delivery === ArtifactDelivery.Events ? contexts[1].eventContext : undefined,
259
+ methodName: 'activationEvent' }
260
+ ]);
247
261
  contexts.map(context => context.delivery).should.deep.equal([ArtifactDelivery.ReplayNotification, ArtifactDelivery.Events]);
248
262
  result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
249
263
  });
@@ -370,6 +384,83 @@ describe('when delivering reactor batches', () => {
370
384
  reactors.dispose();
371
385
  });
372
386
 
387
+ it('should fail a replay notification when completion fails and suppress event handling', async () => {
388
+ observed.length = 0;
389
+ const steps: string[] = [];
390
+ const activator: ClientArtifactsActivator = (type, context) => ({ instance: new type(),
391
+ complete: () => { steps.push('complete'); if (context.delivery === ArtifactDelivery.ReplayNotification) throw new Error('replay cleanup'); },
392
+ dispose: () => { steps.push('dispose'); } });
393
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
394
+ observed.should.deep.equal(['notification']);
395
+ steps.should.deep.equal(['complete', 'dispose']);
396
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
397
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: replay cleanup']);
398
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
399
+ });
400
+
401
+ it('should preserve both replay notification and completion errors', async () => {
402
+ const steps: string[] = [];
403
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
404
+ complete: () => { steps.push('complete'); throw new Error('replay cleanup'); },
405
+ dispose: () => { steps.push('dispose'); } });
406
+ const result = await observe(FailingNotificationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
407
+ steps.should.deep.equal(['complete', 'dispose']);
408
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([
409
+ 'Error: notification failed', 'ArtifactCompletionFailed: Artifact completion failed: Error: replay cleanup'
410
+ ]);
411
+ });
412
+
413
+ it('should complete each successful lease before acknowledgement and then dispose it', async () => {
414
+ const steps: string[] = [];
415
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
416
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
417
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator);
418
+ steps.should.deep.equal(['complete', 'dispose']);
419
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
420
+ });
421
+
422
+ it('should complete after processing failure before acknowledgement and preserve the processing error', async () => {
423
+ const steps: string[] = [];
424
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
425
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
426
+ const result = await observe(AppendReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, false);
427
+ steps.should.deep.equal(['complete', 'dispose']);
428
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: append rejected']);
429
+ });
430
+
431
+ it('should fail completion distinctly without advancing a successful checkpoint and still dispose', async () => {
432
+ const steps: string[] = [];
433
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
434
+ complete: async () => { steps.push('complete'); throw new Error('scope cleanup'); },
435
+ dispose: () => { steps.push('dispose'); } });
436
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator);
437
+ steps.should.deep.equal(['complete', 'dispose']);
438
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
439
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
440
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup']);
441
+ });
442
+
443
+ it('should report both processing and completion errors without acknowledging earlier successful events', async () => {
444
+ const steps: string[] = [];
445
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
446
+ complete: () => { steps.push('complete'); throw new Error('scope cleanup'); },
447
+ dispose: () => { steps.push('dispose'); } });
448
+ const result = await observe(AppendReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator, false);
449
+ steps.should.deep.equal(['complete', 'dispose']);
450
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([
451
+ 'Error: append rejected', 'ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup'
452
+ ]);
453
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
454
+ });
455
+
456
+ it('should leave a processing failure unchanged when legacy disposal also fails', async () => {
457
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
458
+ dispose: () => { throw new Error('legacy cleanup'); } });
459
+ const result = await observe(AppendReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, false);
460
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
461
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: append rejected']);
462
+ });
463
+
373
464
  it('should log disposal failure without changing successful acknowledgement', async () => {
374
465
  const errors: string[] = [];
375
466
  diag.setLogger({ error: (...values: unknown[]) => { errors.push(values.map(String).join(' ')); } } as DiagLogger, DiagLogLevel.ERROR);
@@ -4,6 +4,9 @@
4
4
  import { type Constructor } from '@cratis/fundamentals';
5
5
  import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
6
6
  import { getProjectionMetadata } from '../projections/declarative/projection.js';
7
+ import { InvalidEventContextPropertyError } from '../projections/InvalidEventContextPropertyError.js';
8
+ import { invalidEventContextPropertyInProjection } from '../projections/invalidEventContextPropertyInProjection.js';
9
+ import { getReadModelId } from './readModel.js';
7
10
  import { ProjectionBuilderFor } from '../projections/declarative/ProjectionBuilderFor.js';
8
11
  import type { ChildrenDefinitionLike } from '../projections/declarative/ProjectionBuilderCore.js';
9
12
  import type { IProjectionFor } from '../projections/declarative/IProjectionFor.js';
@@ -53,7 +56,14 @@ function inspectReadModelTypes(artifacts: IClientArtifactsProvider): { models: C
53
56
  if (!model) continue;
54
57
  inspect(model);
55
58
  const builder = new ProjectionBuilderFor<unknown>();
56
- (new type() as IProjectionFor<unknown>).define(builder);
59
+ try {
60
+ (new type() as IProjectionFor<unknown>).define(builder);
61
+ } catch (error) {
62
+ if (error instanceof InvalidEventContextPropertyError) {
63
+ throw invalidEventContextPropertyInProjection(error, type.name, getReadModelId(model));
64
+ }
65
+ throw error;
66
+ }
57
67
  inspectDeclarative(model, builder.getSubobjectDefinitions());
58
68
  }
59
69
  for (const type of artifacts.reducers) {