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.

Zur Installation springen

Quellinformationen

Repository
resonatehq/resonate-skills
Letzte Quellaktivität
21. August 2026 um 17:03
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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>(); } ```
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen