@cratis/chronicle 6.20.0 → 6.22.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.
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +6 -100
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +15 -0
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -0
- package/dist/eventSequences/createAppendNotification.js +25 -0
- package/dist/eventSequences/createAppendNotification.js.map +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts +31 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -0
- package/dist/eventSequences/prepareBatchAppend.js +54 -0
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -0
- package/dist/reactors/ReactorDispatcher.d.ts +20 -0
- package/dist/reactors/ReactorDispatcher.d.ts.map +1 -0
- package/dist/reactors/ReactorDispatcher.js +76 -0
- package/dist/reactors/ReactorDispatcher.js.map +1 -0
- package/dist/reactors/ReactorSideEffects.d.ts +3 -0
- package/dist/reactors/ReactorSideEffects.d.ts.map +1 -1
- package/dist/reactors/ReactorSideEffects.js +16 -11
- package/dist/reactors/ReactorSideEffects.js.map +1 -1
- package/dist/reactors/Reactors.d.ts +0 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +5 -71
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/testing/EventScenario.d.ts +1 -1
- package/dist/testing/EventScenario.d.ts.map +1 -1
- package/dist/testing/EventScenario.js +7 -4
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/EventScenarioGiven.d.ts +1 -1
- package/dist/testing/EventScenarioGiven.d.ts.map +1 -1
- package/dist/testing/EventScenarioGiven.js +2 -5
- package/dist/testing/EventScenarioGiven.js.map +1 -1
- package/dist/testing/EventScenarioSeed.d.ts +2 -2
- package/dist/testing/EventScenarioSeed.d.ts.map +1 -1
- package/dist/testing/EventScenarioSeed.js +2 -2
- package/dist/testing/EventScenarioSeed.js.map +1 -1
- package/dist/testing/EventScenarioWhen.d.ts +1 -1
- package/dist/testing/EventScenarioWhen.d.ts.map +1 -1
- package/dist/testing/EventScenarioWhen.js +2 -3
- package/dist/testing/EventScenarioWhen.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts +26 -7
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +253 -43
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorDeliveryResult.d.ts +10 -0
- package/dist/testing/ReactorDeliveryResult.d.ts.map +1 -0
- package/dist/testing/ReactorDeliveryResult.js +4 -0
- package/dist/testing/ReactorDeliveryResult.js.map +1 -0
- package/dist/testing/ReactorScenario.d.ts +55 -0
- package/dist/testing/ReactorScenario.d.ts.map +1 -0
- package/dist/testing/ReactorScenario.js +275 -0
- package/dist/testing/ReactorScenario.js.map +1 -0
- package/dist/testing/ReactorScenarioOptions.d.ts +17 -0
- package/dist/testing/ReactorScenarioOptions.d.ts.map +1 -0
- package/dist/testing/ReactorScenarioOptions.js +4 -0
- package/dist/testing/ReactorScenarioOptions.js.map +1 -0
- package/dist/testing/RecordedReactorSideEffect.d.ts +13 -0
- package/dist/testing/RecordedReactorSideEffect.d.ts.map +1 -0
- package/dist/testing/RecordedReactorSideEffect.js +4 -0
- package/dist/testing/RecordedReactorSideEffect.js.map +1 -0
- package/dist/testing/UnsupportedReactorOperation.d.ts +7 -0
- package/dist/testing/UnsupportedReactorOperation.d.ts.map +1 -0
- package/dist/testing/UnsupportedReactorOperation.js +14 -0
- package/dist/testing/UnsupportedReactorOperation.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventSequence.ts +10 -107
- package/eventSequences/createAppendNotification.ts +35 -0
- package/eventSequences/prepareBatchAppend.ts +63 -0
- package/package.json +1 -1
- package/reactors/ReactorDispatcher.ts +95 -0
- package/reactors/ReactorSideEffects.ts +21 -13
- package/reactors/Reactors.ts +11 -83
- package/testing/EventScenario.batches.spec.ts +537 -0
- package/testing/EventScenario.fixtures.spec.ts +2 -13
- package/testing/EventScenario.rejections.spec.ts +5 -39
- package/testing/EventScenario.ts +7 -5
- package/testing/EventScenarioGiven.ts +2 -5
- package/testing/EventScenarioSeed.ts +4 -4
- package/testing/EventScenarioWhen.ts +2 -4
- package/testing/InProcessEventSequence.ts +239 -39
- package/testing/ReactorDeliveryResult.ts +13 -0
- package/testing/ReactorScenario.rejections.spec.ts +100 -0
- package/testing/ReactorScenario.spec.ts +611 -0
- package/testing/ReactorScenario.ts +279 -0
- package/testing/ReactorScenarioOptions.ts +20 -0
- package/testing/RecordedReactorSideEffect.ts +16 -0
- package/testing/UnsupportedReactorOperation.ts +10 -0
- package/testing/index.ts +5 -0
package/testing/EventScenario.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { InProcessEventSequence } from './InProcessEventSequence.js';
|
|
|
19
19
|
import { registerScenarioSeed } from './EventScenarioSeed.js';
|
|
20
20
|
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
21
21
|
|
|
22
|
-
/** A kernel-free, fixture-bounded
|
|
22
|
+
/** A kernel-free, fixture-bounded append scenario. No observer runs automatically. */
|
|
23
23
|
export class EventScenario {
|
|
24
24
|
readonly eventSequence: IEventSequence;
|
|
25
25
|
readonly given: EventScenarioGivenBuilder;
|
|
@@ -46,7 +46,7 @@ export class EventScenario {
|
|
|
46
46
|
}
|
|
47
47
|
const sequence = new InProcessEventSequence(options, eventTypes);
|
|
48
48
|
this.eventSequence = sequence;
|
|
49
|
-
registerScenarioSeed(this, (source,
|
|
49
|
+
registerScenarioSeed(this, (source, events) => sequence.seed(source, events));
|
|
50
50
|
this.given = new EventScenarioGivenBuilder(this);
|
|
51
51
|
this.when = new EventScenarioWhenBuilder(this);
|
|
52
52
|
}
|
|
@@ -63,9 +63,11 @@ export class EventScenario {
|
|
|
63
63
|
}
|
|
64
64
|
appendMany(source: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
65
65
|
appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
66
|
-
async appendMany(
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
async appendMany(sourceOrEvents: string | EventForEventSourceId[], eventsOrOptions?: object[] | AppendOptions, options?: AppendOptions): Promise<AppendResult[]> {
|
|
67
|
+
if (typeof sourceOrEvents === 'string') {
|
|
68
|
+
return this.eventSequence.appendMany(sourceOrEvents, eventsOrOptions as object[], options);
|
|
69
|
+
}
|
|
70
|
+
return this.eventSequence.appendMany(sourceOrEvents, eventsOrOptions as AppendOptions | undefined);
|
|
69
71
|
}
|
|
70
72
|
get results(): readonly AppendResult[] { return (this.eventSequence as InProcessEventSequence).results; }
|
|
71
73
|
get appendedEvents(): readonly AppendedEvent[] { return (this.eventSequence as InProcessEventSequence).appendedEvents; }
|
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import type { EventScenario } from './EventScenario.js';
|
|
5
5
|
import { seedScenario } from './EventScenarioSeed.js';
|
|
6
|
-
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
7
6
|
|
|
8
|
-
/** Seeds
|
|
7
|
+
/** Seeds events in order through individual single appends, as in the .NET given builder. */
|
|
9
8
|
export class EventScenarioGiven {
|
|
10
9
|
constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
|
|
11
10
|
async events(...events: object[]): Promise<void> {
|
|
12
|
-
|
|
13
|
-
'Zero or multiple setup events are not supported in this increment; call events once per event.');
|
|
14
|
-
await seedScenario(this._scenario, this._id, events[0]);
|
|
11
|
+
await seedScenario(this._scenario, this._id, events);
|
|
15
12
|
}
|
|
16
13
|
}
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import type { EventScenario } from './EventScenario.js';
|
|
5
5
|
|
|
6
|
-
const seeders = new WeakMap<EventScenario, (source: string,
|
|
6
|
+
const seeders = new WeakMap<EventScenario, (source: string, events: object[]) => Promise<void>>();
|
|
7
7
|
|
|
8
8
|
/** Internal builder wiring; not exported by the testing package. */
|
|
9
|
-
export function registerScenarioSeed(scenario: EventScenario, seed: (source: string,
|
|
9
|
+
export function registerScenarioSeed(scenario: EventScenario, seed: (source: string, events: object[]) => Promise<void>): void {
|
|
10
10
|
seeders.set(scenario, seed);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function seedScenario(scenario: EventScenario, source: string,
|
|
13
|
+
export function seedScenario(scenario: EventScenario, source: string, events: object[]): Promise<void> {
|
|
14
14
|
const seed = seeders.get(scenario);
|
|
15
15
|
if (!seed) throw new Error('EventScenario setup requires a constructed scenario.');
|
|
16
|
-
return seed(source,
|
|
16
|
+
return seed(source, events);
|
|
17
17
|
}
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import type { AppendResult } from '../eventSequences/AppendResult.js';
|
|
5
5
|
import type { EventScenario } from './EventScenario.js';
|
|
6
|
-
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
7
6
|
|
|
8
7
|
/** A single act-phase append; the plural form is reserved for atomic batches. */
|
|
9
8
|
export class EventScenarioWhen {
|
|
10
9
|
constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
|
|
11
10
|
async event(event: object): Promise<AppendResult> { return this._scenario.append(this._id, event); }
|
|
12
|
-
async events(...
|
|
13
|
-
|
|
14
|
-
'Even a one-event plural action uses kernel batch semantics; batch fixtures are not included.');
|
|
11
|
+
async events(...events: object[]): Promise<readonly AppendResult[]> {
|
|
12
|
+
return this._scenario.appendMany(this._id, events);
|
|
15
13
|
}
|
|
16
14
|
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { EventObservationState } from '@cratis/chronicle.contracts';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { AppendOperationsBroadcaster } from '../eventSequences/AppendOperationsBroadcaster.js';
|
|
7
|
+
import { prepareBatchAppend } from '../eventSequences/prepareBatchAppend.js';
|
|
8
|
+
import { createAppendNotification, mapAppendNotificationCausation } from '../eventSequences/createAppendNotification.js';
|
|
9
|
+
import { Guid, type Constructor } from '@cratis/fundamentals';
|
|
10
|
+
import { getEventTypeMetadata, getEventTypeFor } from '../events/eventTypeDecorator.js';
|
|
8
11
|
import { getTagsFor } from '../events/tagDecorator.js';
|
|
9
12
|
import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
10
13
|
import { Tag } from '../events/Tag.js';
|
|
@@ -31,7 +34,20 @@ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOpe
|
|
|
31
34
|
// JS trim() omits U+0085, which the kernel trims. Reject unproven non-ASCII whitespace and controls.
|
|
32
35
|
const unprovenFilterCharacters = /[\u007f-\u009f]|(?=[^\x00-\x7f])\p{White_Space}/u;
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
// Observers ReactorScenario registers to latch unsupported operations, keyed by sequence.
|
|
38
|
+
const unsupportedObservers = new WeakMap<object, (error: UnsupportedEventSequenceOperation) => void>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Observe every unsupported operation a scenario sequence reports, however the caller receives it.
|
|
42
|
+
* Internal to the testing harness; ReactorScenario uses it to fail deliveries that swallow such errors.
|
|
43
|
+
* @param sequence - The in-process sequence to observe.
|
|
44
|
+
* @param observer - Called with each unsupported-operation error the sequence creates.
|
|
45
|
+
*/
|
|
46
|
+
export function observeUnsupportedOperations(sequence: object, observer: (error: UnsupportedEventSequenceOperation) => void): void {
|
|
47
|
+
unsupportedObservers.set(sequence, observer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fixture-backed, scenario-local append sequence; no kernel or observer scheduler is started. */
|
|
35
51
|
export class InProcessEventSequence implements IEventSequence {
|
|
36
52
|
readonly id: EventSequenceId;
|
|
37
53
|
private readonly _catalog = new Map<Function, ReturnType<typeof getEventTypeMetadata>>();
|
|
@@ -42,8 +58,11 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
42
58
|
private readonly _correlationId?: () => string;
|
|
43
59
|
private _setup = false;
|
|
44
60
|
private _allowSeedAppend = false;
|
|
61
|
+
private _stagedSeeds?: AppendedEvent[];
|
|
62
|
+
private _stagedNotifications?: AppendedEventWithResult[][];
|
|
45
63
|
private _busy = false;
|
|
46
64
|
private readonly _results: AppendResult[] = [];
|
|
65
|
+
readonly appendOperations = new AppendOperationsBroadcaster<AppendedEventWithResult[]>();
|
|
47
66
|
|
|
48
67
|
constructor(options: EventScenarioOptions, eventTypes: Constructor[]) {
|
|
49
68
|
if (options.eventSequenceId && options.eventSequenceId.value !== EventSequenceId.eventLog.value) {
|
|
@@ -100,24 +119,35 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
100
119
|
get results(): readonly AppendResult[] { return Object.freeze([...this._results]); }
|
|
101
120
|
get appendedEvents(): readonly AppendedEvent[] { return Object.freeze(this._history.map(event => this.snapshot(event))); }
|
|
102
121
|
|
|
103
|
-
/**
|
|
104
|
-
async seed(source: string,
|
|
105
|
-
if (this._setup) throw this.unsupported('given.events', this.id.value, 'Overlapping setup calls are not fixture-backed.');
|
|
122
|
+
/** Validate and stage all setup appends, then commit and notify in single-append order. */
|
|
123
|
+
async seed(source: string, events: object[]): Promise<void> {
|
|
124
|
+
if (this._setup || this._busy) throw this.unsupported('given.events', this.id.value, 'Overlapping setup calls are not fixture-backed.');
|
|
106
125
|
this._setup = true;
|
|
107
|
-
this.
|
|
108
|
-
|
|
109
|
-
try { pending = this.append(source, event); }
|
|
110
|
-
finally { this._allowSeedAppend = false; }
|
|
126
|
+
this._stagedSeeds = [];
|
|
127
|
+
this._stagedNotifications = [];
|
|
111
128
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
this._allowSeedAppend = true;
|
|
131
|
+
let pending: Promise<AppendResult>;
|
|
132
|
+
try { pending = this.append(source, event); }
|
|
133
|
+
finally { this._allowSeedAppend = false; }
|
|
134
|
+
const result = await pending;
|
|
135
|
+
if (!result.isSuccess) throw new Error(`EventScenario given setup failed: ${JSON.stringify(result)}`);
|
|
136
|
+
}
|
|
137
|
+
for (const [index, stored] of this._stagedSeeds.entries()) {
|
|
138
|
+
this._history.push(stored);
|
|
139
|
+
this.appendOperations.publish(this._stagedNotifications[index]);
|
|
140
|
+
// Let an async subscriber observe this append before the next enters history.
|
|
141
|
+
await Promise.resolve();
|
|
142
|
+
}
|
|
114
143
|
} finally {
|
|
144
|
+
this._stagedSeeds = undefined;
|
|
145
|
+
this._stagedNotifications = undefined;
|
|
115
146
|
this._setup = false;
|
|
116
147
|
}
|
|
117
148
|
}
|
|
118
149
|
|
|
119
150
|
get transactional(): ITransactionalEventSequence { throw this.unsupported('transactional', this.id.value, 'Transactions require a kernel.'); }
|
|
120
|
-
get appendOperations(): AsyncIterable<AppendedEventWithResult[]> { throw this.unsupported('appendOperations', this.id.value, 'Append notifications are not fixture-backed.'); }
|
|
121
151
|
|
|
122
152
|
async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
|
|
123
153
|
if (this._busy || (this._setup && !this._allowSeedAppend)) {
|
|
@@ -168,7 +198,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
168
198
|
}
|
|
169
199
|
const canonical = Object.fromEntries(Object.entries(content).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
170
200
|
const hash = createHash('sha256').update(`${prepared.eventType.id.value}|${eventSourceId}|${JSON.stringify(canonical)}`).digest('base64');
|
|
171
|
-
const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length));
|
|
201
|
+
const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length + (this._stagedSeeds?.length ?? 0)));
|
|
172
202
|
const stored: AppendedEvent = {
|
|
173
203
|
eventType: prepared.eventType,
|
|
174
204
|
content,
|
|
@@ -182,12 +212,16 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
182
212
|
tags: prepared.tags.map(tag => new Tag(tag))
|
|
183
213
|
}
|
|
184
214
|
};
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
waitForCompletion: async () => { throw this.unsupported('waitForCompletion', event.constructor.name, 'No observers run in process.'); }
|
|
189
|
-
});
|
|
215
|
+
if (this._stagedSeeds) this._stagedSeeds.push(this.snapshot(stored));
|
|
216
|
+
else this._history.push(this.snapshot(stored));
|
|
217
|
+
const result = this.success(sequenceNumber, event.constructor.name);
|
|
190
218
|
if (!this._setup) this._results.push(result);
|
|
219
|
+
if (this._stagedNotifications || this.appendOperations.hasSubscribers) {
|
|
220
|
+
const notification = [createAppendNotification(eventSourceId, event, result,
|
|
221
|
+
prepared.correlationId.toString(), mapAppendNotificationCausation(prepared.causationChain), prepared.tags)];
|
|
222
|
+
if (this._stagedNotifications) this._stagedNotifications.push(notification);
|
|
223
|
+
else this.appendOperations.publish(notification);
|
|
224
|
+
}
|
|
191
225
|
return result;
|
|
192
226
|
} finally {
|
|
193
227
|
// Keep the append in flight until its promise crosses an async boundary.
|
|
@@ -196,20 +230,114 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
appendMany(
|
|
200
|
-
appendMany(
|
|
201
|
-
async appendMany(): Promise<AppendResult[]> {
|
|
202
|
-
throw this.unsupported('appendMany', this.id.value, '
|
|
233
|
+
appendMany(eventSourceId: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
234
|
+
appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
235
|
+
async appendMany(sourceOrEvents: string | EventForEventSourceId[], eventsOrOptions?: object[] | AppendOptions, options?: AppendOptions): Promise<AppendResult[]> {
|
|
236
|
+
if (this._busy || this._setup) throw this.unsupported('appendMany', this.id.value, 'Overlapping calls do not have proven ordering.');
|
|
237
|
+
this._busy = true;
|
|
238
|
+
try {
|
|
239
|
+
const entries = typeof sourceOrEvents === 'string' ? eventsOrOptions as object[] : sourceOrEvents;
|
|
240
|
+
const shared = typeof sourceOrEvents === 'string' ? options : eventsOrOptions as AppendOptions | undefined;
|
|
241
|
+
if (!Array.isArray(entries)) throw this.unsupported('appendMany.arguments', this.id.value, 'Events must be an array.');
|
|
242
|
+
if (!entries.length) throw this.unsupported('appendMany.empty', this.id.value, 'The kernel rejects an empty batch.');
|
|
243
|
+
this.validateBatchOptions(shared);
|
|
244
|
+
if (shared?.occurred !== undefined) this.checkedDate(shared.occurred, 'appendMany.occurred');
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (typeof sourceOrEvents === 'string') break;
|
|
247
|
+
if (!entry || typeof entry !== 'object' || Reflect.ownKeys(entry).some(key =>
|
|
248
|
+
!['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags'].includes(String(key)))) {
|
|
249
|
+
throw this.unsupported('appendMany.entry', this.id.value, 'Unrecognized per-entry metadata.');
|
|
250
|
+
}
|
|
251
|
+
const individual = entry as EventForEventSourceId;
|
|
252
|
+
if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
|
|
253
|
+
const tags = individual.tags;
|
|
254
|
+
if (tags && (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string'))) {
|
|
255
|
+
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
let prepared: ReturnType<typeof prepareBatchAppend>;
|
|
259
|
+
try {
|
|
260
|
+
const batchOptions: AppendOptions | undefined = shared?.correlationId === undefined && this._correlationId
|
|
261
|
+
? { correlationId: this._correlationId(), sourceType: shared?.sourceType, streamType: shared?.streamType,
|
|
262
|
+
streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags }
|
|
263
|
+
: shared;
|
|
264
|
+
if (batchOptions?.correlationId !== undefined) this.validateGuid(batchOptions.correlationId, 'appendMany.correlationId');
|
|
265
|
+
prepared = typeof sourceOrEvents === 'string'
|
|
266
|
+
? prepareBatchAppend(sourceOrEvents, entries as object[], batchOptions)
|
|
267
|
+
: prepareBatchAppend(sourceOrEvents, batchOptions);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof UnsupportedEventSequenceOperation) throw error;
|
|
270
|
+
throw this.unsupported('appendMany.serialization', this.id.value, `Batch preparation failed: ${String(error)}.`);
|
|
271
|
+
}
|
|
272
|
+
const { eventsForEventSourceIds, eventsToAppend, correlationId, batchCausationChain, identity } = prepared;
|
|
273
|
+
this.validateGuid(correlationId, 'appendMany.correlationId');
|
|
274
|
+
if (identity !== Identity.system || batchCausationChain.length !== 2 ||
|
|
275
|
+
Object.keys(batchCausationChain[0].properties).length !== 0 ||
|
|
276
|
+
batchCausationChain[1].type.name !== 'TypeScriptClient.AppendMany' ||
|
|
277
|
+
batchCausationChain[1].properties.count !== String(entries.length)) {
|
|
278
|
+
throw this.unsupported('appendMany.context', this.id.value, 'Ambient identity or causation metadata is not fixture-backed.');
|
|
279
|
+
}
|
|
280
|
+
const staged = eventsForEventSourceIds.map(({ eventSourceId, event }, index) => {
|
|
281
|
+
if (!event || typeof event !== 'object' || !this._catalog.has(event.constructor)) {
|
|
282
|
+
throw this.unsupported('appendMany.event', event?.constructor?.name ?? this.id.value, 'Event is not in the selected catalog.');
|
|
283
|
+
}
|
|
284
|
+
this.validateSource(eventSourceId, 'appendMany.source');
|
|
285
|
+
const wire = eventsToAppend[index];
|
|
286
|
+
for (const [name, value] of Object.entries({ sourceType: wire.EventSourceType, streamType: wire.EventStreamType,
|
|
287
|
+
streamId: wire.EventStreamId, subject: wire.Subject })) {
|
|
288
|
+
if (value !== undefined && (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value))) {
|
|
289
|
+
throw this.unsupported(`appendMany.${name}`, String(value), 'Only simple metadata identifiers are fixture-backed.');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (wire.Tags.some(tag => !/^[A-Za-z0-9_-]+$/.test(tag))) {
|
|
293
|
+
throw this.unsupported('appendMany.tags', event.constructor.name, 'Only simple string tags are fixture-backed.');
|
|
294
|
+
}
|
|
295
|
+
const occurred = wire.Occurred ? this.checkedDate(new Date(wire.Occurred.Value), 'appendMany.occurred')
|
|
296
|
+
: this.checkedDate(this._clock(), 'appendMany.clock');
|
|
297
|
+
const content = this.checkedContent(event, wire.Content, 'appendMany.content');
|
|
298
|
+
const canonical = Object.fromEntries(Object.entries(content).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
299
|
+
const hash = createHash('sha256').update(`${wire.EventType.Id}|${eventSourceId}|${JSON.stringify(canonical)}`).digest('base64');
|
|
300
|
+
const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length + index));
|
|
301
|
+
const eventType = getEventTypeFor(event.constructor);
|
|
302
|
+
const stored: AppendedEvent = { eventType, content, context: {
|
|
303
|
+
eventStore: this._store, namespace: this._namespace, sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
304
|
+
eventSourceType: wire.EventSourceType || 'Default', eventStreamType: wire.EventStreamType || 'All',
|
|
305
|
+
eventStreamId: wire.EventStreamId || 'Default', subject: wire.Subject, hash, causedBy: identity,
|
|
306
|
+
observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
|
|
307
|
+
causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
308
|
+
tags: wire.Tags.map(tag => new Tag(tag))
|
|
309
|
+
} };
|
|
310
|
+
return { stored, result: this.success(sequenceNumber, event.constructor.name) };
|
|
311
|
+
});
|
|
312
|
+
// Stage and validate every entry before committing any event or result.
|
|
313
|
+
this._history.push(...staged.map(item => this.snapshot(item.stored)));
|
|
314
|
+
const results = staged.map(item => item.result);
|
|
315
|
+
this._results.push(...results);
|
|
316
|
+
if (this.appendOperations.hasSubscribers) {
|
|
317
|
+
const occurredAt = new Date();
|
|
318
|
+
const causationEntries = mapAppendNotificationCausation(batchCausationChain);
|
|
319
|
+
this.appendOperations.publish(staged.map(({ result }, index) =>
|
|
320
|
+
createAppendNotification(eventsForEventSourceIds[index].eventSourceId, eventsForEventSourceIds[index].event, result,
|
|
321
|
+
correlationId.toString(), causationEntries, eventsToAppend[index].Tags, occurredAt)));
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
} finally {
|
|
325
|
+
await Promise.resolve();
|
|
326
|
+
this._busy = false;
|
|
327
|
+
}
|
|
203
328
|
}
|
|
204
329
|
|
|
205
330
|
async getNextSequenceNumber(): Promise<EventSequenceNumber> { return new EventSequenceNumber(BigInt(this._history.length)); }
|
|
206
331
|
|
|
207
332
|
async getTailSequenceNumber(source?: string, sourceType?: string, streamType?: string, streamId?: string, types?: Constructor[]): Promise<EventSequenceNumber> {
|
|
208
|
-
if (sourceType !== undefined || streamType !== undefined || streamId !== undefined || types?.length) {
|
|
209
|
-
throw this.unsupported('getTailSequenceNumber.filters', this.id.value, 'Route and event-type tail filters are not fixture-backed.');
|
|
210
|
-
}
|
|
211
333
|
if (source !== undefined) this.validateReadSource('getTailSequenceNumber.source', source);
|
|
212
|
-
|
|
334
|
+
this.validateRouteFilters('getTailSequenceNumber.filters', sourceType, streamType, streamId);
|
|
335
|
+
const ids = this.filterIds(types);
|
|
336
|
+
const last = [...this._history].reverse().find(event => (source === undefined || event.context.eventSourceId === source) &&
|
|
337
|
+
(sourceType === undefined || sourceType === 'Default' || event.context.eventSourceType === sourceType) &&
|
|
338
|
+
(streamType === undefined || streamType === 'All' || event.context.eventStreamType === streamType) &&
|
|
339
|
+
(streamId === undefined || streamId === 'Default' || event.context.eventStreamId === streamId) &&
|
|
340
|
+
(!ids.length || ids.includes(event.eventType.id.value)));
|
|
213
341
|
return last ? new EventSequenceNumber(last.context.sequenceNumber) : EventSequenceNumber.unset;
|
|
214
342
|
}
|
|
215
343
|
|
|
@@ -223,32 +351,102 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
223
351
|
}
|
|
224
352
|
|
|
225
353
|
async getForEventSourceIdAndEventTypes(source: string, types: Constructor[], streamType?: string, streamId?: string, sourceType?: string): Promise<AppendedEvent[]> {
|
|
226
|
-
if (
|
|
227
|
-
throw this.unsupported('getForEventSourceIdAndEventTypes.filters', source, 'Only explicit event-type/source filtering is fixture-backed.');
|
|
228
|
-
}
|
|
354
|
+
if (!types.length) throw this.unsupported('getForEventSourceIdAndEventTypes.filters', source, 'Explicit event types are required.');
|
|
229
355
|
this.validateReadSource('getForEventSourceIdAndEventTypes.source', source);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
356
|
+
this.validateRouteFilters('getForEventSourceIdAndEventTypes.filters', sourceType, streamType, streamId);
|
|
357
|
+
const ids = this.filterIds(types);
|
|
358
|
+
return this._history.filter(event => event.context.eventSourceId === source && ids.includes(event.eventType.id.value) &&
|
|
359
|
+
(sourceType === undefined || sourceType === 'Default' || event.context.eventSourceType === sourceType) &&
|
|
360
|
+
(streamType === undefined || streamType === 'All' || event.context.eventStreamType === streamType) &&
|
|
361
|
+
(streamId === undefined || streamId === 'Default' || event.context.eventStreamId === streamId)).map(event => this.snapshot(event));
|
|
236
362
|
}
|
|
237
363
|
|
|
238
364
|
async getFromSequenceNumber(sequence: EventSequenceNumber, source?: string, types?: Constructor[]): Promise<AppendedEvent[]> {
|
|
239
365
|
if (sequence.value < 0n || sequence.value > EventSequenceNumber.unset.value) {
|
|
240
366
|
throw this.unsupported('getFromSequenceNumber.sequenceNumber', sequence.value.toString(), 'Sequence numbers must fit unsigned 64-bit wire values.');
|
|
241
367
|
}
|
|
242
|
-
if (types?.length) throw this.unsupported('getFromSequenceNumber.filterEventTypes', this.id.value, 'Event-type filtering on sequence reads is not fixture-backed.');
|
|
243
368
|
if (source !== undefined) this.validateReadSource('getFromSequenceNumber.source', source);
|
|
369
|
+
const ids = this.filterIds(types);
|
|
244
370
|
return this._history.filter(event => event.context.sequenceNumber >= sequence.value &&
|
|
245
|
-
(source === undefined || event.context.eventSourceId === source)
|
|
371
|
+
(source === undefined || event.context.eventSourceId === source) &&
|
|
372
|
+
(!ids.length || ids.includes(event.eventType.id.value))).map(event => this.snapshot(event));
|
|
246
373
|
}
|
|
247
374
|
|
|
248
375
|
async redact(): Promise<void> { throw this.unsupported('redact', this.id.value, 'Redaction requires kernel storage.'); }
|
|
249
376
|
async redactForEventSource(): Promise<void> { throw this.unsupported('redactForEventSource', this.id.value, 'Redaction requires kernel storage.'); }
|
|
250
377
|
async completeStream(): Promise<CompleteStreamResult> { throw this.unsupported('completeStream', this.id.value, 'Stream completion requires a kernel.'); }
|
|
251
378
|
|
|
379
|
+
private filterIds(types?: Constructor[]): string[] {
|
|
380
|
+
return (types ?? []).map(type => {
|
|
381
|
+
const metadata = this._catalog.get(type);
|
|
382
|
+
if (!metadata) throw this.unsupported('read.eventTypes', type.name, 'Event type is not in the selected catalog.');
|
|
383
|
+
return metadata.eventType.id.value;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private validateRouteFilters(operation: string, ...filters: (string | undefined)[]): void {
|
|
388
|
+
for (const filter of filters) {
|
|
389
|
+
if (filter !== undefined && (typeof filter !== 'string' || !/^[A-Za-z0-9_-]+$/.test(filter))) {
|
|
390
|
+
throw this.unsupported(operation, String(filter), 'Only simple, nonblank route filters are fixture-backed.');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private validateBatchOptions(options?: AppendOptions): void {
|
|
396
|
+
if (options === undefined) return;
|
|
397
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
398
|
+
throw this.unsupported('appendMany.options', this.id.value, 'Append options must be an object.');
|
|
399
|
+
}
|
|
400
|
+
const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags'];
|
|
401
|
+
if (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
|
|
402
|
+
options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
|
|
403
|
+
throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
|
|
404
|
+
}
|
|
405
|
+
if (options.tags && (!Array.isArray(options.tags) || options.tags.some(tag => typeof tag !== 'string'))) {
|
|
406
|
+
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private validateGuid(value: string | Guid, operation: string): void {
|
|
411
|
+
try {
|
|
412
|
+
toContractsGuid(Guid.as(value));
|
|
413
|
+
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value.toString())) throw new Error('Invalid GUID.');
|
|
414
|
+
} catch { throw this.unsupported(operation, this.id.value, 'Correlation ID must be a valid GUID.'); }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private checkedDate(value: Date, operation: string): Date {
|
|
418
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime()) || value.getUTCFullYear() < 1 || value.getUTCFullYear() > 9999) {
|
|
419
|
+
throw this.unsupported(operation, this.id.value, 'Occurrence must be within UTC years 1–9999 at millisecond precision.');
|
|
420
|
+
}
|
|
421
|
+
return new Date(value);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private validateSource(source: string, operation: string): void {
|
|
425
|
+
if (typeof source !== 'string' || !/^[A-Za-z0-9_-]+$/.test(source)) {
|
|
426
|
+
throw this.unsupported(operation, String(source), 'Only simple source identifiers are fixture-backed.');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private checkedContent(event: object, serialized: string, operation: string): Record<string, unknown> {
|
|
431
|
+
let content: Record<string, unknown>;
|
|
432
|
+
try { content = JSON.parse(serialized) as Record<string, unknown>; }
|
|
433
|
+
catch { throw this.unsupported(operation, event.constructor.name, 'Content must be valid JSON.'); }
|
|
434
|
+
const properties = this._catalog.get(event.constructor)!.schema.properties!;
|
|
435
|
+
if (!content || typeof content !== 'object' || Array.isArray(content) ||
|
|
436
|
+
Object.keys(content).length !== Object.keys(properties).length ||
|
|
437
|
+
Object.entries(properties).some(([key, property]) => typeof content[key] !== property.type ||
|
|
438
|
+
(property.type === 'string' && !/^[\x20-\x21\x23-\x5b\x5d-\x7e\u00e9]*$/.test(content[key] as string)))) {
|
|
439
|
+
throw this.unsupported(operation, event.constructor.name, 'Content differs from the fixture-backed scalar schema.');
|
|
440
|
+
}
|
|
441
|
+
return content;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private success(sequenceNumber: EventSequenceNumber, artifact: string): AppendResult {
|
|
445
|
+
return Object.freeze({ sequenceNumber: Object.freeze(sequenceNumber), constraintViolations: Object.freeze([]),
|
|
446
|
+
errors: Object.freeze([]), isSuccess: true,
|
|
447
|
+
waitForCompletion: async () => { throw this.unsupported('waitForCompletion', artifact, 'No observers run in process.'); } });
|
|
448
|
+
}
|
|
449
|
+
|
|
252
450
|
private snapshot(event: AppendedEvent): AppendedEvent {
|
|
253
451
|
const eventType = new EventType(new EventTypeId(event.eventType.id.value),
|
|
254
452
|
new EventTypeGeneration(event.eventType.generation.value), event.eventType.tombstone);
|
|
@@ -270,6 +468,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
270
468
|
}
|
|
271
469
|
|
|
272
470
|
private unsupported(operation: string, artifact: string, reason: string): UnsupportedEventSequenceOperation {
|
|
273
|
-
|
|
471
|
+
const error = new UnsupportedEventSequenceOperation(operation, artifact, reason);
|
|
472
|
+
unsupportedObservers.get(this)?.(error);
|
|
473
|
+
return error;
|
|
274
474
|
}
|
|
275
475
|
}
|
|
@@ -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 type { EventContext } from '../events/EventContext.js';
|
|
5
|
+
|
|
6
|
+
/** Observable outcome of one ordered source-partition delivery (not a kernel checkpoint). */
|
|
7
|
+
export interface ReactorDeliveryResult {
|
|
8
|
+
readonly sourceId: string;
|
|
9
|
+
readonly handled: readonly EventContext[];
|
|
10
|
+
readonly skipped: readonly EventContext[];
|
|
11
|
+
readonly completed: boolean;
|
|
12
|
+
readonly error?: unknown;
|
|
13
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
5
|
+
import { chai, describe, it } from 'vitest';
|
|
6
|
+
import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
|
|
7
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
8
|
+
import { reactor } from '../reactors/reactor.js';
|
|
9
|
+
import { ReactorScenario } from './ReactorScenario.js';
|
|
10
|
+
import { UnsupportedReactorOperation } from './UnsupportedReactorOperation.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
@eventType('rejection-event')
|
|
14
|
+
class Registered {
|
|
15
|
+
@field(String) name: string;
|
|
16
|
+
constructor(name: string) { this.name = name; }
|
|
17
|
+
}
|
|
18
|
+
class Unregistered { constructor(readonly name: string) {} }
|
|
19
|
+
const options = { artifacts: { eventTypes: [Registered] }, constraints: 'disabled' as const };
|
|
20
|
+
|
|
21
|
+
async function rejected(operation: string, call: () => Promise<unknown>): Promise<void> {
|
|
22
|
+
await call().then(() => { throw new Error('expected rejection'); }, error => {
|
|
23
|
+
(error instanceof UnsupportedReactorOperation).should.be.true;
|
|
24
|
+
error.operation.should.equal(operation);
|
|
25
|
+
error.message.should.contain('Use a kernel-backed test.');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('unsupported reactor operations', () => {
|
|
30
|
+
it('rejects missing registration, custom sequence and commands during configuration', () => {
|
|
31
|
+
class Plain {}
|
|
32
|
+
(() => new ReactorScenario(Plain, options)).should.throw(UnsupportedReactorOperation, 'Use a kernel-backed test.');
|
|
33
|
+
@reactor('custom-sequence', 'custom') class Custom { registered() {} }
|
|
34
|
+
(() => new ReactorScenario(Custom, options)).should.throw(UnsupportedReactorOperation, 'Use a kernel-backed test.');
|
|
35
|
+
@reactor('commands') class Commands { registered() {} }
|
|
36
|
+
(() => new ReactorScenario(Commands, { ...options, commandTypes: [Unregistered] }))
|
|
37
|
+
.should.throw(UnsupportedReactorOperation, 'Use a kernel-backed test.');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects replay and redelivery asynchronously without mutation', async () => {
|
|
41
|
+
@reactor('replay-rejection') class R { registered() {} }
|
|
42
|
+
const scenario = new ReactorScenario(R, options);
|
|
43
|
+
await rejected('replay', () => scenario.replay());
|
|
44
|
+
await rejected('redeliver', () => scenario.redeliver());
|
|
45
|
+
await rejected('when.events', () => scenario.when.forEventSource('A').events());
|
|
46
|
+
scenario.results.length.should.equal(0);
|
|
47
|
+
scenario.sideEffects.length.should.equal(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects unsupported returned values instead of silently dropping them', async () => {
|
|
51
|
+
for (const value of [{ command: true }, [new Registered('ok'), { command: true }], [undefined]]) {
|
|
52
|
+
@reactor('unknown-result') class R { registered() { return value; } }
|
|
53
|
+
const scenario = new ReactorScenario(R, options);
|
|
54
|
+
await rejected('handler.return', () => scenario.when.forEventSource('A').events(new Registered('one')));
|
|
55
|
+
scenario.sideEffects.length.should.equal(0);
|
|
56
|
+
scenario.results[0].completed.should.be.false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rejects unsupported setup before invocation and without delivery results', async () => {
|
|
61
|
+
let called = 0;
|
|
62
|
+
@reactor('unsupported-input') class R { registered() { called++; } }
|
|
63
|
+
const scenario = new ReactorScenario(R, options);
|
|
64
|
+
await scenario.given.forEventSource('A').events(new Registered('ok'), new Unregistered('bad')).then(() => {
|
|
65
|
+
throw new Error('expected rejection');
|
|
66
|
+
}, error => { error.message.should.contain('Use a kernel-backed test.'); });
|
|
67
|
+
called.should.equal(0);
|
|
68
|
+
scenario.results.length.should.equal(0);
|
|
69
|
+
await scenario.when.forEventSource('A').events(new Registered('after'));
|
|
70
|
+
scenario.results[0].handled[0].sequenceNumber.should.equal(0n);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects missing read-model services instead of supplying fake data', async () => {
|
|
74
|
+
@reactor('read-model-rejection') class R {
|
|
75
|
+
registered(_event: Registered, _context: unknown, services: { readModels: { findInstanceById(): unknown } }) {
|
|
76
|
+
return services.readModels.findInstanceById();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const scenario = new ReactorScenario(R, options);
|
|
80
|
+
await rejected('services.readModels.findInstanceById', () => scenario.when.forEventSource('A').events(new Registered('one')));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('preserves processing-only, completion-only and activation failures', async () => {
|
|
84
|
+
@reactor('failure-rejection') class R { registered() { throw new Error('handler'); } }
|
|
85
|
+
const processing = new ReactorScenario(R, { ...options, artifactActivator: type => ({ instance: new type() }) });
|
|
86
|
+
await processing.when.forEventSource('A').events(new Registered('one')).then(() => { throw new Error('expected rejection'); },
|
|
87
|
+
error => { String(error).should.contain('handler'); });
|
|
88
|
+
processing.results[0].completed.should.be.false;
|
|
89
|
+
@reactor('completion-rejection') class Complete { registered() {} }
|
|
90
|
+
const completion = new ReactorScenario(Complete, { ...options, artifactActivator: type => ({ instance: new type(),
|
|
91
|
+
complete: () => { throw new Error('completion'); } }) });
|
|
92
|
+
await completion.when.forEventSource('A').events(new Registered('one')).then(() => { throw new Error('expected rejection'); },
|
|
93
|
+
error => { (error instanceof ArtifactCompletionFailed).should.be.true; chai.expect(error.processingError).to.be.undefined; });
|
|
94
|
+
completion.results[0].completed.should.be.false;
|
|
95
|
+
const activation = new ReactorScenario(Complete, { ...options, artifactActivator: () => { throw new Error('activation'); } });
|
|
96
|
+
await activation.when.forEventSource('A').events(new Registered('one')).then(() => { throw new Error('expected rejection'); },
|
|
97
|
+
error => { String(error).should.contain('activation'); });
|
|
98
|
+
activation.results[0].handled.length.should.equal(0);
|
|
99
|
+
});
|
|
100
|
+
});
|