TypeScript SDK API guidance
Welcome to the Resonate Typescript SDK guide! This SDK makes it possible to write Distributed Async Await applications with Typescript. This guide covers installation and features that the SDK offers.
Looking for the API reference?
The Resonate TypeScript SDK API reference is available here.
Installation
How to install the Resonate Typescript SDK into your project.
To install the Resonate Typescript SDK, you can use any of your favorite package managers.
- Bun
- npm
- Yarn
bun add @resonatehq/sdk
npm install @resonatehq/sdk
yarn add @resonatehq/sdk
Initialization
How to initialize a Resonate Client.
There are two ways to initialize Resonate, local and remote.
Local initialization
Local initialization means that Resonate uses local memory for promise storage.
This is ideal for getting started quickly or for integrating Resonate into an existing application without relying on dependencies.
The drawback is that you can't use Resonate's Async RPC or recover executions from process / worker crashes.
import { Resonate } from "@resonatehq/sdk";
const resonate = Resonate.local();
Temporal, Restate, and DBOS require a server or database to get started.
Resonate enables you to get started with a local worker that stores promises in memory, no server or database required. This makes it easy to incrementally adopt Resonate into your existing TypeScript application.
Remote initialization
Remote initialization means that promises are stored remotely, and that the worker receives messages from a remote source. This is how Resonate enables Async RPC for building distributed applications that are reliable and scalable.
The quickest way to get started with Remote initialization is for the worker to use Resonate's default Poller message transport to receive messages directly from a Resonate Server.
import { Resonate } from "@resonatehq/sdk";
// default connection to local development server
const resonate = Resonate.remote();
// connection to a custom endpoint
const resonate = Resonate.remote({ url: "https://my-resonate.com" });
// declaring which group it is part of
const resonate = Resonate.remote({ group: "worker-group-x" });
Resonate workers can receive messages from many different transports, such as HTTP, RabbitMQ, RedPanda, etc... The Poller is a great starting place as it will long-poll for messages from the Resonate Server without any additional setup.
Client APIs
Registration
How to register a function with Resonate in the TypeScript SDK.
.register()
function foo(ctx: Context, ...args: any[]) {
// ...
return result;
}
resonate.register(foo);
// or alternatively
resonate.register("foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
.setDependency()
Resonate's .setDependency()
method allows you to set a dependency for the Application Node.
You can then access the dependency in the function using the .getDependency()
method.
Dependencies can only be added in the ephemeral world.
resonate.setDependency("dependency-name", dependency);
The dependency can be accessed from any function in the Call Graph on that Application Node. This is useful for things like database connections or other resources that you want to share across functions.
How to invoke a function in the ephemeral world with the Resonate Class.
To move from the ephemeral world to the durable world you use the Resonate Class to invoke functions.
There are two methods that you can use: .run()
and .rpc()
.
.run()
Resonate's .run()
method invokes a function in the same process and returns the result.
You can think of it as a "run right here" invocation.
After invocation, the function is considered durable and will recover in another process if required.
const result = await resonate.run("invocation-id", foo, ...args);
.beginRun()
Similar to .run()
but instead of returning the result, returns a handle so the result can be awaited later.
const handle = await resonate.beginRun("invocation-id", foo, ...args);
const result = await handle.result();
.rpc()
Resonate's .rpc()
method (Remote Procedure Call) invokes a function in a remote process and returns the result.
You can think of it as a "run somewhere else" invocation (Asynchronous Remote Procedure Call).
After invocation, the function is considered durable and will recover in another process if required.
// worker.ts
resonate.register("foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
// client.ts
const result = await resonate.rpc(
"invocation-id",
"foo",
...args,
resonate.options({
target: "poll://any@workers",
})
);
.beginRpc()
Similar to .rpc()
but instead of returning the result, returns a handle so the result can be awaited later.
const handle = await resonate.beginRpc(
"invocation-id",
"foo",
...args,
resonate.options({
target: "poll://any@workers",
})
);
const result = await handle.result();
.schedule()
Resonate's .schedule()
method allows you to schedule a function to be invoked on a specified cron schedule.
The scheduled function will be invoked until the schedule is deleted.
const schedule = await resonate.schedule(
"scheduled-foo",
"0 * * * *", // every hour
foo,
...args
);
.options()
Options can be used with .run()
, .beginRun()
, .rpc()
, .beginRpc()
, and .schedule()
as the final argument.
await resonate.run(
"invocation-id",
foo,
...args,
resonate.options({
timeout: 60_000, // 1 minute in ms
target: "poll://any@workers",
tags: { key: "value" },
})
);
.get()
Resonate's .get()
method allows you to subscribe to a function invocation.
If the function invocation does not exist, an error will be thrown.
const handle = await resonate.get("invocation-id");
const result = await handle.result();
.promises.get()
Resonate's .promises.get()
method allows you to get a promise by ID.
const p = await resonate.promises.get("promise-id");
.promises.create()
Resonate's .promises.create()
method allows you to create a promise.
await resonate.promises.create(
"promise-id",
Date.now() + 30000 // 30 seconds in the future
);
.promises.get()
Resonate's .promises.get()
method allows you to get a promise by ID.
const p = await resonate.promises.get("promise-id");
.promises.resolve()
Resonate's .promises.resolve()
method allows you to resolve a promise by ID.
This is useful for HITL use cases where you want to wait for a human to approve or reject a function execution.
It works well in conjunction with the .promise()
method.
await resonate.promises.resolve("promise-id");
.promises.reject()
Resonate's .promises.reject()
method allows you to reject a promise by ID.
await resonate.promises.reject("promise-id");
Context APIs
How to use the Resonate Context object in the TypeScript SDK.
Resonate's Context object enables you to invoke functions from inside a Durable Function.
This is how you extend the Call Graph and create a world of Durable Functions.
Inside a Durable Function you use the yield*
keyword to interact with the Context object.
.getDependency()
Context's .getDependency()
method allows you to get a dependency that was set in the ephemeral world using the .setDependency()
method and use it the Durable World.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const dependency = ctx.getDependency("dependency-name");
// do something with the dependency
// ...
});
.run()
Context's .run()
method invokes a function in the same process in a synchronous manner.
That is — the calling function blocks until the invoked function returns.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const result = yield* ctx.run(bar, ...args);
// do more stuff
// ...
});
function bar(ctx: Context, ...args: any[]) {
// ...
return;
}
.beginRun()
Context's .beginRun()
method invokes a function in the same process in an asynchronous manner.
That is — the invocation returns a promise which can be awaited later.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.beginRun(bar, ...args);
// do more stuff
const result = yield* promise;
// ...
});
function bar(ctx: Context, ...args: any[]) {
// ...
return;
}
.rpc()
Context's .rpc()
method invokes a function in a remote process in a synchronous manner.
That is — the calling function blocks until the invoked function returns.
// process a
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const result = yield* ctx.rpc(
"bar",
...args,
ctx.options({ target: "poll://any@workers" })
);
// do more stuff
// ...
});
// process b
resonate.register("bar", function (ctx: Context, ...args: any[]) {
// ...
return;
});
.beginRpc()
Context's .beginRpc()
method invokes a function in a remote process in an asynchronous manner.
That is — the invocation returns a promise which can be awaited on later.
// process a
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.beginRpc(
"bar",
...args,
ctx.options({ target: "poll://any@workers" })
);
// do more stuff
const result = yield* promise;
// ...
});
// process b
resonate.register("bar", function (ctx: Context, ...args: any[]) {
// ...
return;
});
.detached()
Context's .detached()
method invokes a function in a remote process in an asynchronous manner
but unlike .beginRpc()
, the promise is not implictly awaited.
Use .detached()
when you want to fire-and-forget a function invocation.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
yield* ctx.detached("bar", ...args);
// do more stuff
});
.options()
Options can be used with .run()
, .beginRun()
, .rpc()
, .beginRpc()
, and .detached()
as the final argument.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
yield* ctx.run(
bar,
...args,
ctx.options({
id: "custom-id",
timeout: 60_000, // 1 minute in ms
target: "poll://any@workers",
tags: { key: "value" },
})
);
});
.promise()
Context's .promise()
method allows you to get or create a promise that can be awaited on.
If no ID is provided, one is generated and a new promise is created. If an ID is provided and a promise already exists with that ID, then the existing promise is returned.
This is very useful for HITL (Human-In-The-Loop) use cases where you want to block progress until a human has taken an action or provided data.
It works well in conjunction with the .promises.resolve()
method.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.promise({ id: "promise-id" });
// do more stuff
const result = yield* promise;
// ...
});
You can also pass custom data into the promise.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.promise({ data: { key: "value" } });
// do more stuff
const result = yield* promise;
// ...
});
.sleep()
Context's .sleep()
method allows you to sleep inside a function.
There is no limit to how long you can sleep — even if the original process crashes or is terminated, the sleep will continue and the function will resume in a new process.
The sleep method accepts a number value in milliseconds.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
yield* ctx.sleep(5000); // sleep for 5 seconds
// do more stuff
// ...
});
.date.now()
Context's .date.now()
method allows you to deterministically get the time in milliseconds since the epoch.
If your function execution is recovered after the time has been retrieved, the same time will be returned.
This is helpful for ensuring the same code path is taken in the event of a recovery.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const time = yield* ctx.date.now();
// do something with time
// ...
});
.math.random()
Context's .math.random()
method allows you to generate a deterministic random number.
If your function execution is recovered after the random number has been generated, the same number will be returned.
This is helpful for ensuring the same code path is taken in the event of a recovery.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const rand = yield* ctx.math.random();
// do something with rand
// ...
});