Resonate TypeScript SDK
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.
โก๏ธ Resonate TypeScript SDK API reference
If you are new to Resonate, we recommend following the Resonate TypeScript SDK quickstart tutorial to get started learning how to use Resonate. ๐
Installationโ
How to install and use the TypeScript SDK in your project.
To install the SDK in your project you can use npm or yarn.
npm
npm install @resonatehq/sdk
yarn
yarn add @resonatehq/sdk
Then, create a top-level Resonate object in your project file.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
After that, register your top-level function with the Resonate object using resonate.register()
, provide a function unique identifier (UID) and a function pointer and then call resonate.start()
.
You can then invoke the registered function using resonate.run()
by passing the function's UID, a UID for corresponding promise, and any required arguments.
import { Resonate, Context } from "@resonatehq/sdk";
const resonate = new Resonate();
resonate.register("your-function-name", yourFunctionPointer);
resonate.start();
resonate.run("your-function-name", "unique-promise-id", args);
With resonate.run()
, your code execution will complete even in the presence of hardware or software failures.
Any part of your application that calls resonate.run()
is considered a Resonate Application Node, and maintans its own call graph.
Promise storage modesโ
Resonate offers two promise storage modes: Local and Remote.
Local storageโ
How to use local storage for Durable Promises.
When using Local storage (Local mode), which is the default, Resonate utilizes a volatile in-memory promise store.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
Promises are stored in memory and are not durable to hard crashes. This mode provides out-of-the-box features like automatic retries, tracing, and logging without requiring any additional infrastructure.
Remote storageโ
How to enable remote storage for Durable Promises.
Remote storage ensures promises get stored in the Resonate Server. To enable Remote storage, pass the Resonate Server's address when initializing Resonate:
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate({
url: "http://localhost:8001",
});
Durable Promise Timeoutโ
Set timeouts for Durable Promises to detect failures.
Application Node defaultโ
You can configure a default Durable Promise Timeout for all function executions in the Application Node via top-level Resonate object:
import { Resonate, Retry } from "@resonatehq/sdk";
const resonate = new Resonate({
// ...
// The new default Durable Promise Timeout for this Application Node
timeout: 5000, // Set in ms. If not set, defaults to 1000 ms.
// ...
});
Per registered functionโ
You can override the Application Node default when you register a function with resonate.register()
:
resonate.register(
"your-function-name",
yourFunc,
resonate.options({
timeout: 3000, // Set in ms. If not set, defaults to the Application Node default.
// ...
})
);
Per function invocationโ
Additionally, you can set the timeout for specific functions using ctx.run()
:
ctx.run(yourFunc, arg1, arg2, resonate.options({ timeout: 2000 }));
A specific Durable Promise Timeout cannot be longer than the Timeout of the parent promise. If it is longer, the minimum will take precedence.
Trigger a retryโ
If you want a function to retry in an attempt to recover from an application-level issue, you need to either throw an error, or return a rejected promise. You cannot return a custom object that contains error information.
โ Throw an error:
try {
return "success";
} catch (e) {
let errorMessage = "An unknown error occurred";
if (e instanceof Error) {
errorMessage = e.message;
}
throw errorMessage;
}
โ Reject a promise:
return new Promise((resolve, reject) => {
try {
resolve("success");
} catch (e) {
let errorMessage = "An unknown error occurred";
if (e instanceof Error) {
errorMessage = e.message;
}
reject(errorMessage);
}
});
โ Don't return a custom object:
try {
return { success: true };
} catch (e) {
let errorMessage = "An unknown error occurred";
if (e instanceof Error) {
errorMessage = e.message;
}
return { success: false, error: errorMessage };
}
}
Set a Retry Policyโ
Set Retry Policies to automatically retry function executions.
Application Node defaultโ
You can configure a default Retry Policy for all function executions in the Application Node via the top-level Resonate object:
import { Resonate, Retry } from "@resonatehq/sdk";
const resonate = new Resonate({
// ...
retry: Retry.exponential(
100, // initial delay (in ms)
2, // backoff factor
Infinity, // max attempts
60000 // max delay (in ms, 1 minute)
),
// ...
});
Function-specific Configurationโ
When registering functions with resonate.register()
, provide function-specific options:
resonate.register(
"your-function-name",
yourFunc,
resonate.options({
// ...
retry: Retry.linear(), // Overrides the default retry policy.
// ...
})
);
Per function invocationโ
You can override the default Retry Policy for specific function invocations using ctx.run()
:
ctx.run(yourFunc, args, resonate.options({ retry: Retry.linear() }));
Durable Promise metadataโ
You can add metadata to Durable Promises using the tags field in the options object.
Application Node defaultโ
resonate.register(
"downloadAndSummarize",
downloadAndSummarize,
resonate.options({
// ...
tags: { bar: "baz" }, // Additional tags to add to the Durable Promise.
})
);
Function-specific Configurationโ
Per function invocationโ
Schedulesโ
The Resonate TypeScript SDK enables you to schedule function executions.
Instead of calling resonate.run()
, use resonate.schedule()
to schedule the function execution on a repeating basis.
You can create a Schedule that is stored in local memory, or you can create a Schedule that is stored in the Resonate Server.
If the application node connects to a Resonate Server, then the Schedule will automatically be stored there. Otherwise, the Schedule will persist in local memory and disappear when the process does.
Create a Scheduleโ
How to schedule function executions with the Resonate TypeScript SDK.
You can execute your function periodically with a cron expression.
import { Resonate, Context } from "@resonatehq/sdk";
// flow is the top-level function that awaits on step1 and step 2
export async function flow(ctx: Context) {
console.log("Starting the flow");
// Call function 2
await ctx.run(step1);
// Call function 3
await ctx.run(step2);
return;
}
// step1 function
async function step1() {
console.log("Executing step 1");
// ...
return;
}
// step2 function
async function step2() {
console.log("Executing step 2");
// ...
return;
}
// Initialize a Resonate application.
const resonate = new Resonate({
url: "http://localhost:8001",
});
// Schedule the execution of "flow" for every minute
const sched = await resonate.schedule("flow-schedule", "* * * * *", flow);
console.log(sched);
// Start the Resonate application
resonate.start();
console.log("Running");
If the above process crashes, you can restart it and the interval of the function executions will pick back up.
Another acceptable syntax is to register the function first and then refer to the function by its name:
resonate.register("flow", flow);
// ...
const sched = await resonate.schedule("flow-schedule", "* * * * *", "flow");
Consider this pattern for creating schedules on one Application Node, and having the functions execute on another.
In the example above, an idempotency key is generated from the schedule ID and the function name. This enables you to run this Application Node in multiple places without creating a new Schedule each time. It also enables you to restart a process that crashes.
However, if you try to create a Schedule with an ID that already exists with a different function you will get an error.
message: 'A schedule with this identifier already exists'
Get a Scheduleโ
How to get Schedule details using the TypeScript SDK.
You can get the details for a Schedule with the Schedule ID used to create the schedule.
const sched = await resonate.schedules.get("flow-schedule");
You can also use the CLI to get a Schedule. See the CLI reference for getting a Schedule for details.
Search for Schedulesโ
How to search for Schedules.
You can search for Schedules using a combination of tags and ID wildcards.
Using the CLI:
resonate schedules search "flow-*"
See the CLI reference for searching for Schedules for details.
Delete a Scheduleโ
How to delete a Schedule stored in the Resonate Server.
Currently there isn't an API exposed in the TypeScript SDK to delete a Schedule.
Instead you can use the Server CLI.
resonate schedules delete <schedule-id>
See the CLI reference for deleting a Schedule for details.
Resolving promisesโ
How to manually resolve a Durable Promise from another process.
Promises and executions always come in pairs. However an execution could be any "step". Often the "step" is a function execution but it could be a human completing a task for example.
Let's say you have a multi step flow defined in a function like this:
import { Resonate, Context } from "@resonatehq/sdk";
// flow blocks on the resolution of "step2"
export async function flow(ctx: Context) {
// Await on step 1
// step 1 resolves when the function completes
await ctx.run(step1);
// Await on step2
// step2 gets resolved from another process
console.log("awaiting on step 2 completion");
await ctx.run("step2");
// Await on step 3
// step 3 resolves when the function completes
await ctx.run(step3);
return;
}
async function step1() {
console.log("executing step 1");
// ...
return;
}
async function step3() {
console.log("executing step 3");
// ...
return;
}
// Initialize a Resonate application.
// Connect to a Resonate Server for coordination
const resonate = new Resonate({ url: "http://localhost:8001" });
// Register a function as a Resonate function
resonate.register(
"flow", // function name
flow, // function pointer
resonate.options({ timeout: 1200000 }) // Timeout of 60 seconds
);
// Start the Resonate application
resonate.start();
// Call the function1
await resonate.run("flow", `flow-promise-2`);
console.log("Complete");
In the previous code example, the line await ctx.run("step2");
blocks the rest of the function execution on promise id "step2".
This is because we are passing the string "step2" which creates a promise with an Id of "step2" that is not directly attached to a function execution.
In the other await steps a function is called instead, i.e. await ctx.run(step1);
and await ctx.run(step2);
, and promises are created that are directly paired with those function exeuctions.
In the case of await ctx.run("step2");
a promise is created that is not directly associated with an execution.
Now we must resolve that promise "manually" and we can do this from any process that is connected to the Resonate Server. For example:
import { Resonate, Context, ResonatePromises } from "@resonatehq/sdk";
// Initialize a Resonate application.
const resonate = new Resonate({ url: "http://localhost:8001" });
// ...
// Unblock flow by resolving step 2
console.log("executing step 2");
resonate.promises.resolve("step2", "description of resolution");
// ...
What's happening under the hood?
Here is a sequence diagram that attempts to show how the code above plays out.
The diagram assumes that the flow function starts first and blocks on "step2", then while the flow function awaits on the "step2" promise the unblocking process runs and "completes" whatever happened in step2 using resonate.promises.resolve()
.
Resonate Contextโ
Interactions with the runtime occur through the Resonate Context
, which provides methods like ctx.run()
and ctx.sleep()
. These methods offer automatic retries, recoverability, task distribution, and more. All top-level functions (invoked by resonate.run()
) and intermediary functions (invoked by ctx.run()
) must accept a Resonate context as their first argument.
async function purchase(ctx: Context, user: User, song: Song): Promise<Status> {
const charged = await ctx.run(charge, user, song);
const granted = await ctx.run(access, user, song);
return { charged, granted };
}
async function charge(ctx: Context, user: User, song: Song): Promise<boolean> {
console.log(`Charged user:${user.id} $${song.price}.`);
return true;
}
async function access(ctx: Context, user: User, song: Song): Promise<boolean> {
console.log(`Granted user:${user.id} access to song:${song.id}.`);
return true;
}
In-Processโ
In-process execution enables durable execution of functions within the same process by passing a function pointer to a local function followed by its arguments.
const result = ctx.run(download, arg1, arg2, ...);
Sleepingโ
Resonate keeps track of timers and executes them, even across failures and restarts. To sleep in a Resonate application for a whole day, do the following:
await ctx.sleep(86_400_000);
Versioningโ
You can register multiple versions of a function with resonate.register()
:
// Register `downloadAndSummarize` function with a version number of 2,
// a function pointer to a local function,
// and optionals configurations.
resonate.register(
"downloadAndSummarize",
downloadAndSummarize,
resonate.options({
version: 2,
})
);
You can specify which version to run as an option on run. By default the function registered with the greatest (latest) version will be chosen.
resonate.run("downloadAndSummarize", "uid", resonate.options({ version: 2 }));
Additionally, your function has access to context.version
, telling it the version this execution was started with.
async function charge(ctx: Context, user: User, song: Song): Promise<boolean> {
if (ctx.version == 1) {
console.log(`Charged user:${user.id} $${song.price} with version 1`);
} else {
console.log(`Charged user:${user.id} $${song.price} with version 2`);
}
return true;
}