Do Work in the Background
The shop emails a receipt when an order is approved. The customer should not wait for the mail provider, and an approval should not fail because the provider is away. Hand the email over as a task: a call nobody waits for.
§Hand the work over
notifications is a component with an effect send that sends an email, given to and text; Call an External Service writes it. approve is the transition of store.orders that charges the customer’s wallet, billing.wallets.debit with the order’s id as ref, and approves the order. It calls send with task: true beside the rest of the request:
// components/store.orders/operations/approve.ts
import type { Context, State } from '../types/index.d.ts'
export async function transition (input: null, entry: State, context: Context) {
if (entry.status !== 'pending') return new Error('NOT_PENDING')
const reply = await context.remote.billing.wallets.debit({
query: { id: entry.customer },
input: { amount: entry.total, ref: entry.id }
})
if (reply instanceof Error) return reply
entry.status = 'approved'
await context.remote.notifications.send({
input: { to: entry.customer, text: `Order ${entry.id} is approved` },
task: true
})
return entry
} The call answers null as soon as the task is stored, and approve goes on. send runs later, in
whichever process of notifications is free, even if that component was down when the task
was made.
The task is handed over when that line runs, not when the order is stored. If approve is run
again because the order changed underneath it, the task is handed over again; if storing the order
fails, the task has been made anyway. send has to tolerate both. Where the work is a consequence
of a stored change and must follow it exactly, react to the event instead; Call, Event, or Task compares the three.
Nothing is declared for it. send is an ordinary operation, and the same operation can be called
both ways. query works as in any call:
await context.remote.store.orders.archive({ query: { id }, task: true }) §Give a task to your own component
Answer now and do the slow part afterwards, with context.local:
// components/store.orders/operations/cancel.ts
import type { CancelInput, Context, State } from '../types/index.d.ts'
export async function transition (input: CancelInput, entry: State, context: Context) {
if (entry.status !== 'pending') return new Error('NOT_PENDING')
entry.status = 'cancelled'
entry.TRAILERS.reason = input.reason
await context.local.refund({ query: { id: entry.id }, task: true })
return entry
} cancel is the transition Announce a Change writes, which takes a reason and leaves it in TRAILERS; the task is the line added to it.
§Try it
Restart the application and approve a pending order:
$ npm run restart
$ npx toa call store.orders.approve "{ query: { id: '7a1c5e0c2b8f4d0e9a3b6c1d2e4f5a6b' } }" The approved order is printed at once. Stop notifications and approve another order: the
approval still succeeds, and the email is sent when the component is started again.
A task can be made from the command line too:
$ npx toa call notifications.send "{ input: { to: 'ann@example.com', text: 'Hello' }, task: true }" §Getting the result
The call answers null, which says the task was accepted and nothing about how it will end. A
caller that needs the outcome gets it as it would get any other fact about another component: it
reads state later, or receives an event.
If the call throws, the task may or may not have been taken. Let the exception through.
§What the task’s operation must handle
It can run more than once. An operation that throws is run again later, from the top, and a process that stops halfway hands the task to another. Write the operation so that running it twice leaves the same state; Handle Repeated Calls shows how. For an effect that makes several outside calls in a row, see Write a Multi-Step Workflow.
A returned error ends it, and nobody reads it. The task is finished and is not tried again. If
a refusal has to be visible, write it where somebody will read it. credit of billing.wallets (A Wallet and Its Ledger) puts money back into the
customer’s wallet. It declares once, so refund is an effect, which has no write of its own to
lose, and not a transition that retries:
// components/store.orders/operations/refund.ts
import type { Context, Entity } from '../types/index.d.ts'
export async function effect (input: null, entry: Entity, context: Context) {
const reply = await context.remote.billing.wallets.credit({
query: { id: entry.customer },
input: { amount: entry.total, type: 'refund', ref: entry.id }
})
await context.local.assign({
query: { id: entry.id },
input: { refund: reply instanceof Error ? 'failed' : 'done' }
})
} Do not throw to refuse. A thrown refusal is retried several times and then set aside for an operator, as though something were broken; see Operating.
It has no deadline. A task may run long after it was made: after an outage, after a
deployment. Work that can stop being worth doing checks the state it reads — a refund for an
order that is no longer cancelled does nothing.
§Calls that cannot be tasks
Each of these throws where the call is made:
| A task | Why |
|---|---|
with timeout or signal | nobody waits, so there is nothing to give up |
| to a stateful operation | it is served by one named process |
| that carries a stream | the stream exists only while its caller writes |
to a transition, assignment, effect or unmanaged operation, from an observation or a computation, or under a GET request | what may only read may not hand over a write |
A task that names an operation its target does not have — a caller deployed ahead of the component it calls — is accepted, and set aside for an operator when the component takes it.
§Slow tasks delay the others
All tasks of a component wait in one line, whichever operation they are for. An operation that takes minutes delays every other task of its component. Where that matters, move the slow operation to a component of its own.
§Summary
| You want | You write |
|---|---|
| hand work to another component | a context.remote call with task: true |
| hand work to your own component | a context.local call with task: true |
| know it was accepted | the call answered null |
| know how it ended | read state, or receive an event |
| survive a repeat | a repeat-safe operation |
When a task fits and when an event does is discussed in Tasks.