Deadlines and timers
Schedule a typed event back to the same instance. Deadlines survive deploys and land on the lane like any other message.
A deadline is a message, delivered later
import { actor } from 'experimental-a2/actor'
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
interface Escrow {
state: { held: Record<string, number> }
events: {
hold: { offerId: string; amount: number }
accept: { offerId: string }
release: { offerId: string }
}
}
export const escrow = actor<Escrow>({
name: 'escrow',
state: { held: {} },
scheduler: vercelQueues(),
handlers: {
hold: async (ctx, input) => {
ctx.state.held[input.offerId] = input.amount
await ctx.schedule.release({ offerId: input.offerId }, { delay: '1h' })
},
accept: (ctx, input) => {
if (ctx.state.held[input.offerId] === undefined) return
delete ctx.state.held[input.offerId]
// your payout; ctx.id is the idempotency key
},
release: (ctx, input) => {
if (ctx.state.held[input.offerId] === undefined) return
delete ctx.state.held[input.offerId]
// your refund; ctx.id is the idempotency key
},
},
})ctx.schedule mirrors ctx.send, one method per declared event, with
a timing argument: exactly one of { delay: '1h' } or { at: date }.
It requires the scheduler option. The deadline outlives the
invocation that armed it and any deploys in between, and delivery lands
on the instance's lane like any other message.
Adapters, delay limits, and delivery semantics are the session timers underneath.
Stale timers are no-ops
When accept wins, the release timer still fires an hour later, meets
the held[offerId] === undefined guard, and does nothing. When the
timer wins, a late accept meets the same guard. There is no
cancellation API, and the log records that the timer fired: let the
event land and return when it no longer applies.
Identity and retries
A timer's name defaults to the target event's name, scoped to the triggering message, so a re-run of a crashed handler addresses the same timer instead of arming a second. One handler run gets one timer per target event; to arm several toward the same event, name them apart:
// inside a handler:
await ctx.schedule.release(
{ offerId: 'offer-42' },
{ delay: '30m', name: 'first-nudge' },
)Duplicate deliveries fold into one append, and relative delays anchor
to the triggering message's createdAt; see
Identity and retries.
Scheduling is immediate
From outside a handler
// anywhere on the server:
import { escrow } from '@/escrow'
await escrow.actor('offer-42').session.schedule(
'expire-offer',
{ at: new Date('2030-01-01T00:00:00Z') },
{ type: 'release', payload: { offerId: 'offer-42' } },
)Outside handler contexts, schedule through the instance's session with
an explicit name, timing, and event. Root scheduling semantics apply
({ at } for retry-stable targets); see
Schedule from ordinary server code.