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.
Deferring, then acting
Section titled “Deferring, then acting”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')})from requence.service import Service
def handler(ctx): message_key = ctx.defer("waiting for external process") save_to_db(ctx.task_id, message_key)
# act() is a method, so the service must be STARTED, not constructed —# see Starting & Stopping.service = Service.start("1.0.0", handler)# ...later, in a webhook handler or background job:def actor(api): api["send"]({"result": "done"}) # or: api["send_to_output"]("success", {"result": "done"}) # or: api["abort"]("something went wrong")
service.act(message_key, actor)The actor may also return a value or an iterable directly, which behaves the same as
calling send for each value.
What the actor is given
Section titled “What the actor is given”| 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 |
The actor receives an api dict, read by key:
| Call | Does |
|---|---|
api["send"](data) |
Send data to the default output |
api["send_to_output"](name, data) |
Send data to a named output |
api["abort"](error) |
Abort the deferred message with an error string or exception |
api["render"](ui, props, surface_id=…) |
Draw or redraw the node’s surface |
Rendering from act()
Section titled “Rendering from act()”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)}def on_submit(arg): # Durable: module scope, so any instance of this version can run it. The # defer key went out in the props and comes back in the argument. def actor(api): api["render"](ui, {"submitted": True}) # before the send: act() settles it api["send"]({"answer": arg["answer"]})
service.act(arg["deferKey"], actor)
ui = create_ui(HERE / "dist" / "ask.js", {"onSubmit": on_submit})
def handler(ctx): # Defer FIRST: there is no key until the message is parked, and deferring # is what keeps the task running so the button stays reachable. ctx.render(ui, {"question": "ship it?", "deferKey": ctx.defer("waiting")})
service = Service.start({"version": "1.0.0"}, handler)threading.Event().wait()Kill the process afterwards and the form still works: answering it runs the durable handler in whichever instance of the version is free at the time.
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.
Language differences
Section titled “Language differences”- Python needs
Service.start(...).act()is a method on the instance, and the blockingService(...)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 callsact()and then exits does not lose the answer.