@cratis/chronicle 6.43.0 → 6.45.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/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +30 -21
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/modelBound/FromAllEventsMetadata.d.ts +8 -0
- package/dist/projections/modelBound/FromAllEventsMetadata.d.ts.map +1 -0
- package/dist/projections/modelBound/FromAllEventsMetadata.js +4 -0
- package/dist/projections/modelBound/FromAllEventsMetadata.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +4 -2
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +15 -8
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/projections/modelBound/fromAll.d.ts +5 -2
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +8 -3
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/projections/modelBound/fromAllEvents.d.ts +32 -0
- package/dist/projections/modelBound/fromAllEvents.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAllEvents.js +44 -0
- package/dist/projections/modelBound/fromAllEvents.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +2 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +1 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/reactors/ReactorDelivery.d.ts +23 -0
- package/dist/reactors/ReactorDelivery.d.ts.map +1 -0
- package/dist/reactors/ReactorDelivery.js +36 -0
- package/dist/reactors/ReactorDelivery.js.map +1 -0
- package/dist/reactors/ReactorDispatcher.d.ts +2 -1
- package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
- package/dist/reactors/ReactorDispatcher.js +2 -2
- package/dist/reactors/ReactorDispatcher.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +2 -1
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/for_Reactors/given/deliver_to_reactor.d.ts +6 -0
- package/dist/reactors/for_Reactors/given/deliver_to_reactor.d.ts.map +1 -0
- package/dist/reactors/for_Reactors/given/deliver_to_reactor.js +37 -0
- package/dist/reactors/for_Reactors/given/deliver_to_reactor.js.map +1 -0
- package/dist/reactors/index.d.ts +1 -0
- package/dist/reactors/index.d.ts.map +1 -1
- package/dist/reactors/index.js +1 -0
- package/dist/reactors/index.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +2 -1
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/modelBoundPropertyMetadata.js +1 -1
- package/dist/types/modelBoundPropertyMetadata.js.map +1 -1
- package/package.json +2 -2
- package/projections/ProjectionDefinitionCompiler.ts +28 -21
- package/projections/for_ProjectionDefinitionCompiler/against_a_kernel/when_subscribing_with_from_all_events.integration.spec.ts +202 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.spec.ts +235 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts +107 -0
- package/projections/modelBound/FromAllEventsMetadata.ts +10 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +27 -8
- package/projections/modelBound/fromAll.ts +8 -3
- package/projections/modelBound/fromAllEvents.ts +48 -0
- package/projections/modelBound/index.ts +2 -0
- package/reactors/ReactorDelivery.ts +32 -0
- package/reactors/ReactorDispatcher.ts +3 -2
- package/reactors/Reactors.ts +5 -1
- package/reactors/for_ReactorDelivery/when_comparing_deliveries.spec.ts +27 -0
- package/reactors/for_ReactorDelivery/when_rendering_a_receipt_key.spec.ts +33 -0
- package/reactors/for_Reactors/given/deliver_to_reactor.ts +47 -0
- package/reactors/for_Reactors/when_delivering_a_stable_identity.spec.ts +66 -0
- package/reactors/index.ts +1 -0
- package/testing/ReactorScenario.ts +5 -1
- package/testing/for_ReactorScenario/when_receiving_a_delivery_identity.spec.ts +68 -0
- package/types/modelBoundPropertyMetadata.ts +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { ReactorDelivery } from '../index.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when rendering a delivery as an application receipt key', () => {
|
|
10
|
+
let delivery: ReactorDelivery;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
delivery = new ReactorDelivery('Cratis.Chronicle.Reactors.for_ReactorDelivery.OrderConfirmations',
|
|
13
|
+
'orders', 'default', 'event-log', 'order-42', 7n);
|
|
14
|
+
});
|
|
15
|
+
it('should match the .NET v19.26.2 receipt format', () => {
|
|
16
|
+
delivery.id.should.equal('Cratis.Chronicle.Reactors.for_ReactorDelivery.OrderConfirmations#orders#default#event-log#order-42#7');
|
|
17
|
+
});
|
|
18
|
+
it('should render the same key for equal components in a new instance', () => {
|
|
19
|
+
new ReactorDelivery(delivery.reactor, delivery.eventStore, delivery.namespace,
|
|
20
|
+
delivery.eventSequence, delivery.partition, delivery.sequenceNumber).id.should.equal(delivery.id);
|
|
21
|
+
});
|
|
22
|
+
it('should preserve sequence numbers above the JavaScript safe integer range', () => {
|
|
23
|
+
new ReactorDelivery('orders', 'store', 'tenant', 'event-log', 'order-42', 18446744073709551615n)
|
|
24
|
+
.id.should.equal('orders#store#tenant#event-log#order-42#18446744073709551615');
|
|
25
|
+
});
|
|
26
|
+
it('should preserve .NET delimiter behavior without escaping or hashing', () => {
|
|
27
|
+
new ReactorDelivery('orders', '', 'tenant#one', 'event-log', 'order-42', 0n)
|
|
28
|
+
.id.should.equal('orders##tenant#one#event-log#order-42#0');
|
|
29
|
+
});
|
|
30
|
+
it('should keep the identity immutable during handling', () => {
|
|
31
|
+
Object.isFrozen(delivery).should.be.true;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import { ReplayState, type EventContext, type ReactorMessage } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
7
|
+
import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
|
|
8
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
9
|
+
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
10
|
+
import type { IEventLog } from '../../../eventSequences/IEventLog.js';
|
|
11
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
12
|
+
import { Reactors } from '../../index.js';
|
|
13
|
+
|
|
14
|
+
/** Deliver through the production observation stream without a kernel. */
|
|
15
|
+
export async function deliverToReactor(reactorType: Constructor, eventType: Constructor,
|
|
16
|
+
contexts: readonly EventContext[], artifactActivator?: ClientArtifactsActivator,
|
|
17
|
+
eventStore = 'orders', namespace = 'default'): Promise<ReactorMessage[]> {
|
|
18
|
+
const lifecycle = new ConnectionLifecycle();
|
|
19
|
+
const results: ReactorMessage[] = [];
|
|
20
|
+
let finish!: () => void;
|
|
21
|
+
const finished = new Promise<void>(resolve => { finish = resolve; });
|
|
22
|
+
async function* stream(queue: AsyncIterable<ReactorMessage>) {
|
|
23
|
+
const messages = queue[Symbol.asyncIterator]();
|
|
24
|
+
await messages.next();
|
|
25
|
+
try {
|
|
26
|
+
for (const context of contexts) {
|
|
27
|
+
yield { Events: [{ Content: '{}', Context: context }], Partition: context.EventSourceId,
|
|
28
|
+
ReplayState: ReplayState.REPLAY_STATE_None, InitialState: '' };
|
|
29
|
+
results.push((await messages.next()).value!);
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
await lifecycle.disconnected(error => { throw error; });
|
|
33
|
+
finish();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const reactors = new Reactors({ reactors: [reactorType], eventTypes: [eventType] } as unknown as IClientArtifactsProvider,
|
|
37
|
+
{ reactors: { observe: stream } } as unknown as ChronicleConnection,
|
|
38
|
+
eventStore, namespace, lifecycle, {} as IEventLog, undefined,
|
|
39
|
+
{ readModels: {} } as IEventStore, artifactActivator);
|
|
40
|
+
try {
|
|
41
|
+
await reactors.register();
|
|
42
|
+
await finished;
|
|
43
|
+
return results;
|
|
44
|
+
} finally {
|
|
45
|
+
reactors.dispose();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { EventContext as ContractEventContext, EventObservationState, EventType, ObservationState } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { EventContext } from '../../events/EventContext.js';
|
|
7
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
8
|
+
import type { ClientArtifactsActivator } from '../../artifacts/ClientArtifactsActivator.js';
|
|
9
|
+
import { handles, reactor, ReactorDelivery, replay, type ReactorServices } from '../index.js';
|
|
10
|
+
import { deliverToReactor } from './given/deliver_to_reactor.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
|
|
14
|
+
@eventType('delivery-order-placed')
|
|
15
|
+
class OrderPlaced {}
|
|
16
|
+
|
|
17
|
+
for (const activated of [false, true]) {
|
|
18
|
+
describe(`when a ${activated ? 'activated' : 'default'} reactor receives a retry and replay`, () => {
|
|
19
|
+
let deliveries: ReactorDelivery[];
|
|
20
|
+
let states: ObservationState[];
|
|
21
|
+
let contexts: EventContext[];
|
|
22
|
+
let services: ReactorServices[];
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
deliveries = [];
|
|
25
|
+
contexts = [];
|
|
26
|
+
services = [];
|
|
27
|
+
@reactor('order-confirmations', 'inbox-upstream')
|
|
28
|
+
class OrderConfirmations {
|
|
29
|
+
@handles(OrderPlaced)
|
|
30
|
+
confirm(_event: OrderPlaced, context: EventContext, reactorServices: ReactorServices, delivery: ReactorDelivery) {
|
|
31
|
+
contexts.push(context);
|
|
32
|
+
services.push(reactorServices);
|
|
33
|
+
deliveries.push(delivery);
|
|
34
|
+
if (deliveries.length === 1) throw new Error('failed after side effect');
|
|
35
|
+
}
|
|
36
|
+
@replay(OrderPlaced)
|
|
37
|
+
restore(event: OrderPlaced, context: EventContext, reactorServices: ReactorServices, delivery: ReactorDelivery) {
|
|
38
|
+
this.confirm(event, context, reactorServices, delivery);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const context = ContractEventContext.fromPartial({ EventType: EventType.fromPartial({ Id: 'delivery-order-placed', Generation: 1 }),
|
|
42
|
+
EventStore: 'source-store', Namespace: 'source-tenant', EventSourceId: 'order-42',
|
|
43
|
+
SequenceNumber: 9007199254740993n, ObservationState: EventObservationState.Initial });
|
|
44
|
+
const activator: ClientArtifactsActivator | undefined = activated ? type => ({ instance: new type() }) : undefined;
|
|
45
|
+
const results = await deliverToReactor(OrderConfirmations, OrderPlaced,
|
|
46
|
+
[context, { ...context }, { ...context, ObservationState: EventObservationState.Replay }], activator);
|
|
47
|
+
states = results.map(result => result.Content!.Value1!.State);
|
|
48
|
+
});
|
|
49
|
+
it('should preserve the identity through failed delivery, retry and a separate replay handler', () => {
|
|
50
|
+
deliveries.map(delivery => delivery.id).should.deep.equal(Array(3).fill(
|
|
51
|
+
'order-confirmations#source-store#source-tenant#inbox-upstream#order-42#9007199254740993'));
|
|
52
|
+
});
|
|
53
|
+
it('should expose all six components from the observed event and reactor', () => {
|
|
54
|
+
deliveries[0].should.deep.equal(new ReactorDelivery('order-confirmations', 'source-store', 'source-tenant',
|
|
55
|
+
'inbox-upstream', 'order-42', 9007199254740993n));
|
|
56
|
+
});
|
|
57
|
+
it('should preserve existing context and services arguments', () => {
|
|
58
|
+
contexts.map(context => context.observationState).should.deep.equal([
|
|
59
|
+
EventObservationState.Initial, EventObservationState.Initial, EventObservationState.Replay]);
|
|
60
|
+
services.every(service => service.eventStore.readModels === service.readModels).should.be.true;
|
|
61
|
+
});
|
|
62
|
+
it('should still report the handler failure and subsequent successes', () => {
|
|
63
|
+
states.should.deep.equal([ObservationState.Failed, ObservationState.Success, ObservationState.Success]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
package/reactors/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
export { ReactorId } from './ReactorId.js';
|
|
5
|
+
export { ReactorDelivery } from './ReactorDelivery.js';
|
|
5
6
|
export { reactor, getReactorMetadata, isReactor } from './reactor.js';
|
|
6
7
|
export { handles } from '../events/handles.js';
|
|
7
8
|
export { onceOnly } from './onceOnly.js';
|
|
@@ -25,6 +25,7 @@ import { createHandlerInstanceValidator } from '../observation/createHandlerInst
|
|
|
25
25
|
import { getReactorMetadata } from '../reactors/reactor.js';
|
|
26
26
|
import { normalizeReactorSideEffects } from '../reactors/ReactorSideEffects.js';
|
|
27
27
|
import type { ReactorServices } from '../reactors/ReactorServices.js';
|
|
28
|
+
import { ReactorDelivery } from '../reactors/ReactorDelivery.js';
|
|
28
29
|
import { EventScenario } from './EventScenario.js';
|
|
29
30
|
import type { ReactorDeliveryResult } from './ReactorDeliveryResult.js';
|
|
30
31
|
import type { ReactorScenarioOptions } from './ReactorScenarioOptions.js';
|
|
@@ -239,7 +240,10 @@ export class ReactorScenario {
|
|
|
239
240
|
// A swallowed unsupported call fails this event like a throwing handler: nothing is recorded.
|
|
240
241
|
this.throwIfViolated(delivery);
|
|
241
242
|
return this.record(result, event.context, selection.methodName!, deliveryIndex);
|
|
242
|
-
})
|
|
243
|
+
}, new ReactorDelivery(getReactorMetadata(this._reactor)!.id.value,
|
|
244
|
+
event.context.eventStore ?? this._options.eventStore ?? 'test-event-store',
|
|
245
|
+
event.context.namespace ?? this._options.namespace ?? 'default', 'event-log',
|
|
246
|
+
event.context.eventSourceId, event.context.sequenceNumber));
|
|
243
247
|
this.throwIfViolated(delivery);
|
|
244
248
|
handled.push(event.context);
|
|
245
249
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { EventContext as ContractEventContext, EventType as ContractEventType, ObservationState } from '@cratis/chronicle.contracts';
|
|
7
|
+
import type { EventContext } from '../../events/EventContext.js';
|
|
8
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
9
|
+
import { ReactorDelivery, reactor, type ReactorServices } from '../../reactors/index.js';
|
|
10
|
+
import { deliverToReactor } from '../../reactors/for_Reactors/given/deliver_to_reactor.js';
|
|
11
|
+
import { ReactorScenario } from '../index.js';
|
|
12
|
+
import type { ClientArtifactsActivator } from '../../artifacts/ClientArtifactsActivator.js';
|
|
13
|
+
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
@eventType('scenario-delivery-order-placed')
|
|
17
|
+
class OrderPlaced {
|
|
18
|
+
@field(String) readonly name: string;
|
|
19
|
+
constructor(name = '') { this.name = name; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const activated of [false, true]) {
|
|
23
|
+
describe(`when a ${activated ? 'activated' : 'default'} scenario reactor receives a delivery identity`, () => {
|
|
24
|
+
let scenarioDeliveries: ReactorDelivery[];
|
|
25
|
+
let productionDeliveries: ReactorDelivery[];
|
|
26
|
+
let contexts: EventContext[];
|
|
27
|
+
let states: ObservationState[];
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
let deliveries: ReactorDelivery[] = [];
|
|
30
|
+
contexts = [];
|
|
31
|
+
@reactor('order-confirmations')
|
|
32
|
+
class OrderConfirmations {
|
|
33
|
+
orderPlaced(_event: OrderPlaced, context: EventContext, services: ReactorServices, delivery: ReactorDelivery) {
|
|
34
|
+
(services.eventStore.readModels === services.readModels).should.be.true;
|
|
35
|
+
contexts.push(context);
|
|
36
|
+
deliveries.push(delivery);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const activator: ClientArtifactsActivator | undefined = activated ? type => ({ instance: new type() }) : undefined;
|
|
40
|
+
const scenario = new ReactorScenario(OrderConfirmations, {
|
|
41
|
+
artifacts: { eventTypes: [OrderPlaced] }, constraints: 'disabled', artifactActivator: activator });
|
|
42
|
+
await scenario.given.forEventSource('order-42').events(new OrderPlaced('first'));
|
|
43
|
+
await scenario.when.forEventSource('order-43').events(new OrderPlaced('second'));
|
|
44
|
+
scenarioDeliveries = deliveries;
|
|
45
|
+
const wireContexts = contexts.map(context => ContractEventContext.fromPartial({
|
|
46
|
+
EventType: ContractEventType.fromPartial({ Id: 'scenario-delivery-order-placed', Generation: 1 }),
|
|
47
|
+
EventStore: context.eventStore, Namespace: context.namespace,
|
|
48
|
+
EventSourceId: context.eventSourceId, SequenceNumber: context.sequenceNumber,
|
|
49
|
+
ObservationState: context.observationState
|
|
50
|
+
}));
|
|
51
|
+
deliveries = [];
|
|
52
|
+
const results = await deliverToReactor(OrderConfirmations, OrderPlaced, wireContexts, activator,
|
|
53
|
+
'test-event-store', 'default');
|
|
54
|
+
productionDeliveries = deliveries;
|
|
55
|
+
states = results.map(result => result.Content!.Value1!.State);
|
|
56
|
+
});
|
|
57
|
+
it('should supply the same identity as the production observation path', () => {
|
|
58
|
+
scenarioDeliveries.should.deep.equal(productionDeliveries);
|
|
59
|
+
scenarioDeliveries.map(delivery => delivery.id).should.deep.equal(productionDeliveries.map(delivery => delivery.id));
|
|
60
|
+
states.should.deep.equal([ObservationState.Success, ObservationState.Success]);
|
|
61
|
+
});
|
|
62
|
+
it('should use the fixture store, namespace, partition and committed sequence number', () => {
|
|
63
|
+
scenarioDeliveries.map(delivery => delivery.id).should.deep.equal([
|
|
64
|
+
'order-confirmations#test-event-store#default#event-log#order-42#0',
|
|
65
|
+
'order-confirmations#test-event-store#default#event-log#order-43#1']);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -12,7 +12,7 @@ const finalizedMetadata = new FinalizationRegistry<WeakRef<object>>(reference =>
|
|
|
12
12
|
|
|
13
13
|
export const modelBoundPropertyKeys = [
|
|
14
14
|
'setFrom', 'setFromContext', 'setValue', 'addFrom', 'subtractFrom',
|
|
15
|
-
'increment', 'decrement', 'count', 'childrenFrom', 'join', 'fromEvery', 'fromAll'
|
|
15
|
+
'increment', 'decrement', 'count', 'childrenFrom', 'join', 'fromEvery', 'fromAll', 'fromAllEvents'
|
|
16
16
|
].map(name => `chronicle:projection:${name}`);
|
|
17
17
|
|
|
18
18
|
/** Records a standard-decorated class with an event mapping on one of its fields. */
|