Skip to content

resonatehq/resonate-sdk-ts

resonate component banner

Resonate TypeScript SDK

cicd codecov dst License

About this component

The Resonate TypeScript SDK enables developers to build reliable and scalable cloud applications across a wide variety of use cases.

Quickstart

quickstart banner

  1. Install the Resonate Server & CLI
brew install resonatehq/tap/resonate
  1. Install the Resonate SDK
npm install @resonatehq/sdk
  1. Write your first Resonate Function

A countdown as a loop. Simple, but the function can run for minutes, hours, or days, despite restarts.

import { Resonate, type Context } from "@resonatehq/sdk";

function* countdown(context: Context, count: number, delay: number) {
  for (let i = count; i > 0; i--) {
    // Run a function, persist its result
    yield* context.run((context: Context) => console.log(`Countdown: ${i}`));
    // Sleep
    yield* context.sleep(delay * 1000);
  }
  console.log("Done!");
}
// Instantiate Resonate
const resonate = new Resonate({ url: "http://localhost:8001" });
// Register the function
resonate.register(countdown);

Clone a working example repo

  1. Start the server
resonate dev
  1. Start the worker
npx ts-node countdown.ts
  1. Run the function

Run the function with execution ID countdown.1:

resonate invoke countdown.1 --func countdown --arg 5 --arg 60

Result

You will see the countdown in the terminal

npx ts-node countdown.ts
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Done!

What to try

After starting the function, inspect the current state of the execution using the resonate tree command. The tree command visualizes the call graph of the function execution as a graph of durable promises.

resonate tree countdown.1

Now try killing the worker mid-countdown and restarting. The countdown picks up right where it left off without missing a beat.

Async/await

The SDK also ships an async/await engine: the same durable model, written with ordinary async functions instead of generators. Import it from @resonatehq/sdk/async:

import { Resonate, type Context } from "@resonatehq/sdk/async";

async function countdown(ctx: Context, count: number, delay: number) {
  for (let i = count; i > 0; i--) {
    // Run a function, persist its result
    await ctx.run((ctx: Context) => console.log(`Countdown: ${i}`));
    // Sleep
    await ctx.sleep(delay * 1000);
  }
  console.log("Done!");
}

// Instantiate Resonate
const resonate = new Resonate({ url: "http://localhost:8001" });
// Register the function
resonate.register(countdown);

Same server, same CLI, same durable promises — the rest of the quickstart is unchanged.

One difference to know about: operations are eager. Calling ctx.run(...) starts the work immediately and returns an awaitable handle, so fan-out is ordinary promise code:

async function checkout(ctx: Context) {
  const payment = ctx.run(chargeCard);     // starts now
  const inventory = ctx.run(reserveItems); // runs concurrently
  return await Promise.all([payment, inventory]);
}

Migrating from generators

Both engines live in the same package and speak the same protocol to the same server, so you can migrate one function at a time. The mechanical changes:

Generator engine Async engine
import { Resonate } from "@resonatehq/sdk" import { Resonate } from "@resonatehq/sdk/async"
function* (context: Context, ...) async function (ctx: Context, ...)
yield* context.run(...), yield* context.sleep(...) await ctx.run(...), await ctx.sleep(...)
yield context.beginRun(...), later yield future const p = ctx.run(...), later await p — every op is eager
resonate.run(id, func, ...args) → the result resonate.run(id, func, ...args) → a handle; await handle.result()
resonate.beginRun(id, func, ...args) → a handle same call — there are no begin* variants, run is begin-run

Two things to watch:

  • Retries are opt-in. The generator engine retries plain functions with exponential backoff by default. The async engine never retries by default — an async workflow and a plain async function are indistinguishable at runtime, so there is no safe blanket default. To keep retry behavior, pass a policy explicitly:

    import { Exponential } from "@resonatehq/sdk/async";
    
    await ctx.run(chargeCard, ctx.options({ retryPolicy: new Exponential() }));
  • Only await durable promises inside a workflow. A plain await on a timer or network call is invisible to the engine — the workflow may resume after its execution pass has ended and abort. Wrap side effects in ctx.run, the same rule as context.run in the generator engine.

About

Distributed Async Await — Durable Executions, Dead Simple

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

53 stars

Watchers

4 watching

Forks

Packages

 
 
 

Contributors

Languages