State
A workflow's state tracks it as it progresses. It's stored in the configured persistence, and can be used to find the workflow a message is for. This page covers reading, updating and discarding it, and testing workflow handlers.
Defining the state
The state is a class that extends WorkflowState, with a $name that's unique among your workflow states:
import { WorkflowState } from '@node-ts/bus-core'
export class FulfilmentWorkflowState extends WorkflowState {
// Unique among all of your workflow states
static NAME = 'my-app/store/fulfilment-workflow-state'
$name = FulfilmentWorkflowState.NAME
// The workflow's own fields
itemId: string
customerId: string
status: 'shipping-item' | 'emailing-receipt' | 'complete'
shippedAt?: Date
}The $workflowId, $version, $status and $name fields are managed by the bus. Values a handler returns for them are ignored.
Reading and updating the state
The state is the second parameter of every workflow handler. It's read-only: a handler changes it by returning the fields to update, which are merged into the state and saved. Returning nothing saves no changes.
export const fulfilmentWorkflow = defineWorkflow(FulfilmentWorkflowState)
.startedBy(ItemPurchased, ({ itemId, customerId }) => ({
itemId,
customerId,
status: 'shipping-item' as const
}))
// The state is the second parameter of every handler
.when(ItemShipped, (event, state) => {
console.log('Shipped', { itemId: state.itemId, status: state.status })
// Return the changes to save
return { status: 'emailing-receipt' as const, shippedAt: event.shippedAt }
})export class FulfilmentWorkflow extends Workflow<FulfilmentWorkflowState> {
configureWorkflow(
mapper: WorkflowMapper<FulfilmentWorkflowState, FulfilmentWorkflow>
): void {
mapper
.withState(FulfilmentWorkflowState)
.startedBy(ItemPurchased, 'start')
.when(ItemShipped, 'shipped')
}
start({ itemId, customerId }: ItemPurchased) {
return { itemId, customerId, status: 'shipping-item' as const }
}
// The state is the second parameter of every handler
shipped(event: ItemShipped, state: FulfilmentWorkflowState) {
console.log('Shipped', { itemId: state.itemId, status: state.status })
// Return the changes to save
return { status: 'emailing-receipt' as const, shippedAt: event.shippedAt }
}
}What a handler returns is type checked against the state: a field of the wrong type, or one that isn't in the state at any depth, doesn't compile. A handler with an annotated return type is only checked against its annotation, so leave the return type off to have its fields checked.
Discarding state
Sometimes a handler's changes shouldn't be saved. This is particularly useful when a workflow should only start in some circumstances. Return ctx.discard(), or this.discardWorkflow() in a class workflow, to save nothing.
For example, this workflow is started by a DocumentUploaded event, but only for documents uploaded under documents/:
export const documentWorkflow = defineWorkflow(DocumentWorkflowState).startedBy(
DocumentUploaded,
async ({ key }, _state, ctx) => {
if (!key.startsWith('documents/')) {
// Ignore this upload, and don't save a new workflow
return ctx.discard()
}
await ctx.send(new ReadDocument(key))
return { key }
}
)Testing a workflow handler
Function workflow handlers are plain functions. Get one from the workflow with startedByHandler(Message) or whenHandler(Message), and call it with a context from workflowContext(). The context sends and publishes nothing, unless you pass your own functions, and its complete and discard return what the bus expects.
// In a test, with any test runner
const sent: Command[] = []
const ctx = workflowContext<DocumentWorkflowState>({
send: async command => {
sent.push(command)
}
})
const result = await documentWorkflow.startedByHandler(DocumentUploaded)(
new DocumentUploaded('documents/invoice.pdf'),
new DocumentWorkflowState(),
ctx
)
deepStrictEqual(result, { key: 'documents/invoice.pdf' })
deepStrictEqual(sent, [new ReadDocument('documents/invoice.pdf')])See also
- Completing a workflow
WorkflowStateandworkflowContextin the API reference