| id | sagas |
|---|---|
| title | Sagas |
A saga describes a long running process as a sequence of events.
You can define a saga as a set of event handler functions. Each function runs in response to a specific event and can do the following:
- Send a command to an aggregate
- Schedule a command at the specified moment in time
- Store intermediate data in a persistent storage
- Trigger a side effect
This functionality allows you to organize branched chains of events and side effects to describe processes of any complexity. For example, the code below demonstrates a saga that structures a web site's user registration process:
export default {
handlers: {
Init: async ({ store }) => {
await store.defineTable('users', {
indexes: { id: 'string' },
fields: ['mail']
})
},
USER_CREATED: async ({ store, sideEffects }, event) => {
await store.insert('users', {
id: event.aggregateId,
mail: event.payload.mail
})
await sideEffects.executeCommand({
aggregateName: 'User',
aggregateId: event.aggregateId,
type: 'requestConfirmUser',
payload: event.payload
})
},
USER_CONFIRM_REQUESTED: async ({ sideEffects }, event) => {
await sideEffects.sendEmail(event.payload.mail, 'Confirm mail')
await sideEffects.scheduleCommand(
event.timestamp + 1000 * 60 * 60 * 24 * 7,
{
aggregateName: 'User',
aggregateId: event.aggregateId,
type: 'forgetUser',
payload: {}
}
)
},
USER_FORGOTTEN: async ({ store }, event) => {
await store.delete('users', {
id: event.aggregateId
})
}
},
sideEffects: {
sendEmail: async (mail, content) => {
...
}
}
}The saga requests that a new user confirms his/her email address. If the user does not confirm the address within one week, the saga cancels the registration.
You can define a saga in one of the following ways:
-
In one source file as an object that contains the
handlersandsideEffectsobjects.export default { handlers: { // Event handlers implementation } sideEffects: { // Side effects implementation } }
-
In two separate files.
export default { // Event handlers implementation }
export default { // Side effects implementation }
Every saga should define an Init function that initializes the saga's persistent storage:
Init: async ({ store }) => {
await store.defineTable('users', {
indexes: { id: 'string' },
fields: ['mail']
})
},An event handler function runs for each occurrence of a specific event. It has the following general structure:
EVENT_NAME: async (
{ executeCommand, scheduleCommand, store, sideEffects },
event
) => {
// Event handler logic
}As a first argument, an event handler receives an object that provides access to the following API:
store- Provides access to the saga's persistent store (similar to the Read Model store).sideEffects- Provides access to the saga's side effect functions.
You should define all functions that have side effects in the sideEffects object.
sideEffects: {
sendEmail: async (mail, content) => {
...
}
}You can trigger the defined side effects from an event handler as shown below:
await sideEffects.sendEmail(event.payload.mail, 'Confirm mail')The following side effect functions are available by default:
executeCommand- Sends a command with the specified payload to an aggregate.scheduleCommand- Similar toexecuteCommand, but delays command execution until a specified moment in time.
Use the executeCommand side effect function to send aggregate commands as shown below:
await sideEffects.executeCommand({
aggregateName: 'User',
aggregateId: event.aggregateId,
type: 'requestConfirmUser',
payload: event.payload
})The code sample below demonstrates how the command executes at a specified point in time.
await sideEffects.scheduleCommand(
event.timestamp + 1000 * 60 * 60 * 24 * 7,
{
aggregateName: 'User',
aggregateId: event.aggregateId,
type: 'forgetUser',
payload: {}
}
)To use a saga in your application, you need to register it in the application's configuration file. If a saga is defined in a single file, you can register it as shown below:
sagas: [
{
name: 'UserConfirmation',
source: 'common/sagas/user-confirmation.saga.js',
connectorName: 'default',
schedulerName: 'scheduler'
}
]If a saga is split between two files, register it as follows:
sagas: [
{
name: 'UserConfirmation',
handlers: 'common/sagas/user-confirmation.handlers.js',
sideEffects: 'common/sagas/user-confirmation.side-effects.js',
connectorName: 'default',
schedulerName: 'scheduler'
}
]The connectorName option defines a Read Model storage used to store the saga's persistent data.
The schedulerName option specifies the scheduler that should be used to schedule command execution. Define a scheduler in the schedulers configuration section:
schedulers: {
scheduler: {
adapter: {
module: 'resolve-scheduler-local',
options: {}
},
connectorName: 'default'
}
},