Skip to content

Deferred Delivery

ctx.defer() acknowledges a message but promises the result later. It hands back a message key. act() is how you deliver against that key — from a webhook handler, a scheduler, a background job, or a process run started long after the one that deferred.

import { createService } from '@requence/service'
const service = createService('1.0.0', (ctx) => {
const messageKey = ctx.defer('waiting for external process')
saveMessageKey(ctx.taskId, messageKey)
})
// ...later, in a webhook handler or background job:
await service.act(messageKey, async (api) => {
api.send({ result: 'done' })
// or: api.sendToOutput('success', { result: 'done' })
// or: api.abort('something went wrong')
})

The actor may also return a value or an iterable directly, which behaves the same as calling send for each value.

Call Does
api.send(data) Send data to the default output
api.sendToOutput(name, data) Send data to a named output
api.abort(error) Abort the deferred message with an error
api.render(ui, props, options?) Draw or redraw the node’s surface

A deferring handler cannot register a connection-bound surface handler at all — they die the moment it returns. So a surface whose buttons are written inline has to be rendered from the actor, where they belong to the act() call and live exactly as long as it does. (A handler written in the component’s own defaults is durable and needs none of this.)

const ui = createUI(new URL('./dist/button.js', import.meta.url))
const service = createService('1.0.0', (ctx) => {
pending.save(ctx.defer('waiting for a click'))
})
await service.open()
// On boot AND after each defer — this loop is what survives a restart.
for (const key of await pending.all()) {
void attach(key)
}
async function attach(key: string) {
const clicks = asyncEventEmitter<{ timestamp: number }>()
await service.act(key, async (api) => {
api.render(ui, {
onClick: createCallback<{ timestamp: number }>((arg) => clicks.push(arg)),
})
for await (const click of clicks) {
api.send(click) // completes the deferred node
break
}
})
await pending.remove(key)
}

Restart the process, reload the keys, act() again: the new render — fresh connection, fresh callbacks — replaces the stale surface, and the browser needs to know nothing about it. Props are type-checked against createUI<Props> in TypeScript exactly as they are for ctx.render.

One instance per defer key — your lock, not ours

Section titled “One instance per defer key — your lock, not ours”

Requence does not arbitrate this: enforcing it would need cross-replica holder state plus a takeover path, so that a dead holder could not block a key forever. Instead the symptom is made loud — when a render replaces a surface that is still live on another connection, the server logs a surface takeover error naming both connections.

Take a lease on the key in your own store before acting (SELECT … FOR UPDATE SKIP LOCKED, a Redis SET NX, one designated attacher), and only act on keys you hold.

  • Python needs Service.start(...). act() is a method on the instance, and the blocking Service(...) constructor never returns one. See Starting & Stopping.
  • Python’s act() may be called from any thread — a web request handler, a scheduler, a durable surface handler. It publishes through the connection’s own thread and does not return until everything it published has actually gone out, so a script that calls act() and then exits does not lose the answer.