Skip to main content

resonate-basic-durable-world-usage-typescript

The Resonate TypeScript SDK's Context API reference for durable generator functions — ctx.run, ctx.rpc, ctx.sleep, ctx.promise, determinism rules, and structured concurrency. Use once you've decided to use Resonate and are writing code inside function* bodies. For the conceptual decision of whether to use durable execution at all, see the durable-execution skill.

Ir a la instalación

Datos de origen

Repositorio
resonatehq/resonate-skills
Última actividad en el origen
21 de agosto de 2026 a las 17:03
Idioma detectado de SKILL.md
inglés
Estrellas
6
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
resonate-basic-durable-world-usage-typescript
description
The Resonate TypeScript SDK's Context API reference for durable generator functions — ctx.run, ctx.rpc, ctx.sleep, ctx.promise, determinism rules, and structured concurrency. Use once you've decided to use Resonate and are writing code inside function* bodies. For the conceptual decision of whether to use durable execution at all, see the durable-execution skill.
license
Apache-2.0
# Resonate Basic Durable World Usage > **SDK version:** This skill reflects `@resonatehq/sdk` v0.11.4 (current on npm). > > **Two execution engines (v0.11.0+):** The SDK now ships two engines. This skill documents the **generator engine** (imported from `@resonatehq/sdk`), which uses `function*` / `yield*` and is the basis of all existing Resonate examples. An **async/await engine** (imported from `@resonatehq/sdk/async`) was added in v0.11.0 and is documented in `resonate-async-await-engine-typescript`. ## Overview The **Durable World** is inside generator functions where you write durable, recoverable execution logic using the Context object. Every operation checkpoints progress, enabling automatic recovery after failures. **Key distinction:** Durable World code uses `ctx` (Context). Ephemeral World code uses `resonate` (Client). ## Mental Model ``` Generator Function (function*) ↓ yield* ctx.run() ← Checkpoint ↓ yield* ctx.sleep() ← Checkpoint + Suspend ↓ yield* ctx.rpc() ← Checkpoint + Cross Process ↓ return result ← Final Checkpoint ``` **Every `yield*` is a durable checkpoint.** If the process crashes, the execution replays from the last checkpoint using stored results. ## Core Syntax Rules ### 1. Use `function*` (Generator Functions) ```ts // ✅ CORRECT - Generator function function* myWorkflow(ctx: Context, arg: string) { // Durable code here } // ❌ WRONG - Async function async function myWorkflow(ctx: Context, arg: string) { // Not durable! } // ❌ WRONG - Async generator async function* myWorkflow(ctx: Context, arg: string) { // Invalid syntax for Resonate } ``` ### 2. Always `yield*` Context Calls ```ts function* myWorkflow(ctx: Context) { // ✅ CORRECT const result = yield* ctx.run(someFunc, "arg"); // ❌ WRONG - Missing yield* const result = ctx.run(someFunc, "arg"); // ❌ WRONG - Using await const result = await ctx.run(someFunc, "arg"); } ``` ### 3. First Parameter is Always Context ```ts // ✅ CORRECT function* myWorkflow(ctx: Context, userId: string, data: any) { // ... } // ❌ WRONG - Missing Context function* myWorkflow(userId: string) { // Can't call Context APIs! } ``` ## Context.run() - Local Invocation **Signature:** `ctx.run(func, ...args) → yield* → result` **Behavior:** - Executes in same process - Synchronous (blocks until complete) - Arguments can be in-memory (no serialization required) - Return value must serialize (stored for replay) ### Basic Usage ```ts function* workflow(ctx: Context, userId: string) { // Call synchronously, get result immediately const user = yield* ctx.run(fetchUser, userId); const orders = yield* ctx.run(fetchOrders, userId); return { user, orders }; } async function fetchUser(_ctx: Context, userId: string) { // Regular async function - can throw, use await, etc. return { id: userId, name: "Alice" }; } ``` ### With Non-Serializable Arguments ```ts function* workflow(ctx: Context) { const db = ctx.getDependency("db"); // DB connection object // ✅ OK - db object doesn't need to serialize const user = yield* ctx.run(queryDatabase, db, "123"); return user; } async function queryDatabase(_ctx: Context, db: any, userId: string) { return await db.query("SELECT * FROM users WHERE id = $1", [userId]); } ``` ### Wrapping Side Effects ```ts function* workflow(ctx: Context, email: string) { // ✅ CORRECT - Wrap side effects in ctx.run yield* ctx.run(sendEmail, email); // ❌ WRONG - Side effect outside ctx.run await sendEmailDirect(email); // Will re-send on replay! } async function sendEmail(_ctx: Context, email: string) { // Side effect happens here, checkpointed result prevents replay await emailService.send(email); } ``` ## Context.beginRun() - Non-Blocking Local **Signature:** `ctx.beginRun(func, ...args) → yield* → Future` **Use when:** - Starting multiple operations concurrently - Following fork-join pattern ### Fork-Join Concurrency ```ts function* workflow(ctx: Context, userId: string) { // Fork: Start all operations const userFuture = yield* ctx.beginRun(fetchUser, userId); const ordersFuture = yield* ctx.beginRun(fetchOrders, userId); const invoicesFuture = yield* ctx.beginRun(fetchInvoices, userId); // Join: Await all results const user = yield* userFuture; const orders = yield* ordersFuture; const invoices = yield* invoicesFuture; return { user, orders, invoices }; } ``` **Pattern:** Always fork first, then join. Don't interleave. ### Anti-Pattern: Interleaved Begin/Await ```ts // ❌ WRONG - Hard to read, unclear concurrency function* workflow(ctx: Context, id: string) { const f1 = yield* ctx.beginRun(step1, id); const r1 = yield* f1; const f2 = yield* ctx.beginRun(step2, r1); const r2 = yield* f2; return r2; } // ✅ CORRECT - Sequential operations should use run function* workflow(ctx: Context, id: string) { const r1 = yield* ctx.run(step1, id); const r2 = yield* ctx.run(step2, r1); return r2; } ``` ## Context.rpc() - Remote Invocation **Signature:** `ctx.rpc(funcName, ...args, options?) → yield* → result` **Behavior:** - Executes in different process/group - Blocks until remote completes - All arguments must serialize (crossing process boundary) - Return value must serialize ### Basic Usage ```ts // Process A function* workflow(ctx: Context, userId: string) { // Call function in process B const score = yield* ctx.rpc( "computeScore", userId, { model: "v2" }, ctx.options({ target: "poll://any@scorers" }) ); return score; } // Process B (different worker group "scorers") function* computeScore(ctx: Context, userId: string, options: any) { // Expensive computation return 850; } ``` ### Target Specification ```ts ctx.options({ target: "poll://any@workers" // Any worker in "workers" group }) ctx.options({ target: "poll://any@gpu-workers" // Specific worker group }) ``` ### Serialization Rules ```ts function* workflow(ctx: Context) { const db = ctx.getDependency("db"); // ❌ WRONG - DB connection can't serialize yield* ctx.rpc("processData", db); // ✅ CORRECT - Only pass serializable data const data = yield* ctx.run(fetchData, db); yield* ctx.rpc("processData", data); } ``` **Serializable:** strings, numbers, booleans, plain objects, arrays, null **Not serializable:** functions, class instances, DB connections, file handles ## Context.beginRpc() - Non-Blocking Remote ```ts function* workflow(ctx: Context, userId: string) { // Fork: Start remote operations const scoreFuture = yield* ctx.beginRpc( "computeScore", userId, ctx.options({ target: "poll://any@scorers" }) ); const riskFuture = yield* ctx.beginRpc( "assessRisk", userId, ctx.options({ target: "poll://any@risk-workers" }) ); // Join: Await results const score = yield* scoreFuture; const risk = yield* riskFuture; return { score, risk }; } ``` ## Context.detached() - Fire and Forget ```ts function* workflow(ctx: Context, orderId: string) { // Start analytics tracking but don't wait yield* ctx.detached( "trackOrder", orderId, ctx.options({ target: "poll://any@analytics" }) ); // Continue immediately return processOrder(orderId); } ``` **Important:** Detached calls are NOT implicitly awaited, even with structured concurrency. ## Context.sleep() - Durable Sleep **Signature:** `ctx.sleep(ms | options) → yield* → void` ### Sleep for Duration ```ts function* workflow(ctx: Context) { yield* ctx.sleep(5000); // Sleep 5 seconds yield* ctx.sleep({ for: 60_000 }); // Sleep 1 minute } ``` ### Sleep Until Specific Time ```ts function* workflow(ctx: Context) { const tomorrow8am = new Date(); tomorrow8am.setDate(tomorrow8am.getDate() + 1); tomorrow8am.setHours(8, 0, 0, 0); yield* ctx.sleep({ until: tomorrow8am }); // Resumes at exactly 8am tomorrow } ``` **Key behaviors:** - No limit on sleep duration - Activation terminates during sleep - Resumes in new activation when time expires - Sleep is durable - survives crashes ## Context.promise() - External Promises **Signature:** `ctx.promise(options?) → yield* → Future` **Use for:** Human-in-the-loop, webhooks, external triggers ### Promise IDs are always auto-generated **IMPORTANT:** `ctx.promise()` does not take an `id`. Resonate generates a deterministic ID from the call tree, and that is the only supported mechanism — there is no way to pin a custom ID from inside a durable function. The signature accepts `timeout`, `data` and `tags` and nothing else: ```ts promise<T>(): RFI<T>; promise<T>({ timeout, data, tags }: { timeout?: number; data?: any; tags?: { [key: string]: string }; }): RFI<T>; ``` Passing `{ id }` is a TypeScript excess-property error, and if you force it through, the runtime destructures only `{ timeout, data, tags }` and silently discards it — the promise still gets the sequence-generated ID. **So how does an external resolver learn the ID?** Read it back off the promise and send it. That is the pattern below: create the promise first, then hand `promise.id` to whatever needs to resolve it. ```ts const promise = yield* ctx.promise<T>(); const result = yield* promise; ``` `ctx.options()` excludes `id` for the same reason — `options(opts?: Partial<Omit<Options, "id">>)`. ### Basic HITL Pattern ```ts function* approvalWorkflow(ctx: Context, orderId: string) { // ID is auto-generated; read it back to give the external resolver const approvalPromise = yield* ctx.promise<Decision>(); // Send notification with promise ID yield* ctx.run(sendApprovalEmail, approvalPromise.id); // Block until human approves (via external resolve) const decision = yield* approvalPromise; if (decision.approved) { yield* ctx.run(processOrder, orderId); } return decision; } ``` ### External Resolution (Ephemeral World) ```ts // Webhook handler or UI callback app.post("/approve/:promiseId", async (req, res) => { // Base64 encode data before sending to Resonate server const response = { approved: true, approver: req.body.userId }; const encodedData = Buffer.from(JSON.stringify(response)).toString('base64'); await resonate.promises.resolve(req.params.promiseId, { data: encodedData, }); res.json({ status: "approved" }); }); ``` **Promise ID determinism is the SDK's job, not yours** Replay only works if a promise gets the same ID every time the function runs. Because `ctx.promise()` takes no `id`, you cannot get this wrong: the ID comes from a per-context sequence counter that advances in call order, so the same code path always produces the same IDs. ```ts function* approvalLoop(ctx: Context, orderId: string) { while (true) { // Each iteration gets the next sequence ID — deterministic across replays const promise = yield* ctx.promise<Decision>(); yield* ctx.run(sendApprovalEmail, orderId, promise.id); const result = yield* promise; if (result.approved) break; } } ``` **What you still have to get right:** the sequence is positional, so the *code path* must be deterministic even though the IDs are. Branching on `Date.now()`, `Math.random()`, or anything else that varies between runs can change how many promises get created before a given one, which shifts every subsequent ID. Use `ctx.date.now()` and `ctx.math.random()` — they replay the recorded value — and keep non-deterministic input out of control flow. ```ts // ❌ BAD — a wall-clock branch changes the call sequence between replays if (Date.now() % 2) { yield* ctx.promise<Decision>(); } // ✅ GOOD — recorded and replayed, so the branch is stable const now = yield* ctx.date.now(); if (now % 2) { yield* ctx.promise<Decision>(); } ```
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub