✍️ Pushing events
Modifying the state of your application (i.e. pushing new events to your event stores) is done by executing commands. They typically consist in:
- Fetching the required aggregates (if not the initial event of a new aggregate)
- Validating that the modification is acceptable
- Pushing new events with incremented versions
import { Command, tuple } from '@castore/core';
type Input = { name: string; level: number };
type Output = { pokemonId: string };
type Context = { generateUuid: () => string };
const catchPokemonCommand = new Command({
commandId: 'CATCH_POKEMON',
// 👇 "tuple" is needed to keep ordering in inferred type
requiredEventStores: tuple(pokemonsEventStore, trainersEventStore),
// 👇 Code to execute
handler: async (
commandInput: Input,
[pokemonsEventStore, trainersEventStore],
// 👇 Additional context arguments can be provided
{ generateUuid }: Context,
): Promise<Output> => {
const { name, level } = commandInput;
const pokemonId = generateUuid();
await pokemonsEventStore.pushEvent({
aggregateId: pokemonId,
version: 1,
type: 'POKEMON_CAUGHT',
payload: { name, level },
});
return { pokemonId };
},
});
Note that we only provided TS types for Input
and Output
properties. That is because, as stated in the core design, Castore is meant to be as flexible as possible, and that includes the validation library you want to use (if any): The Command
class can be used directly if no validation is required, or implemented by other classes which will add run-time validation methods to it 👍
Commands
handlers should NOT use read models when validating that a modification is acceptable. Read models are like cache: They are not the source of truth, and may not represent the freshest state.
Fetching and pushing events non-simultaneously exposes your application to race conditions. To counter that, commands are designed to be retried when an EventAlreadyExistsError
is triggered (which is part of the EventStorageAdapter
interface).
Finally, Command handlers should be, as much as possible, pure functions. If they depend on impure functions like functions with unpredictable outputs (e.g. id generation), mutating effects, side effects or state dependency (e.g. external data fetching), you should pass them through the additional context arguments rather than directly importing and using them. This will make them easier to test and to re-use in different contexts, such as in the React Visualizer.
🔧 Reference
Constructor:
commandId (string)
: A string identifying the commandhandler ((input: Input, requiredEventsStores: EventStore[]) => Promise<Output>)
: The code to executerequiredEventStores (EventStore[])
: A tuple ofEventStores
that are required by the command for read/write purposes. In TS, you should use thetuple
util to preserve tuple ordering in the handler (tuple
doesn't mute its inputs, it simply returns them)eventAlreadyExistsRetries (?number = 2)
: Number of handler execution retries before breaking out of the retry loop (See section above on race conditions)onEventAlreadyExists (?(error: EventAlreadyExistsError, context: ContextObj) => Promise<void>)
: Optional callback to execute when anEventAlreadyExistsError
is raised.The
EventAlreadyExistsError
class contains the following properties:eventStoreId (?string)
: TheeventStoreId
of the aggregate on which thepushEvent
attempt failedaggregateId (string)
: TheaggregateId
of the aggregateversion (number)
: Theversion
of the aggregate The `ContextObj` contains the following properties:attemptNumber (?number)
: The number of handler execution attempts in the retry loopretriesLeft (?number)
: The number of retries left before breaking out of the retry loop
import { Command, tuple } from '@castore/core';
const doSomethingCommand = new Command({
commandId: 'DO_SOMETHING',
requiredEventStores: tuple(eventStore1, eventStore2),
handler: async (commandInput, [eventStore1, eventStore2]) => {
// ...do something here
},
});
Properties:
commandId (string)
: The command id
const commandId = doSomethingCommand.commandId;
// => 'DO_SOMETHING'
requiredEventStores (EventStore[])
: The required event stores
const requiredEventStores = doSomethingCommand.requiredEventStores;
// => [eventStore1, eventStore2]
handler ((input: Input, requiredEventsStores: EventStore[]) => Promise<Output>)
: Function to invoke the command
const output = await doSomethingCommand.handler(input, [
eventStore1,
eventStore2,
]);