Resonate TypeScript SDK quickstart tutorial
This quickstart tutorial introduces you to the Resonate TypeScript SDK by building a mock summarization service using Resonate. The first part of the tutorial showcases how Resonate provides Durable Execution for application-level failures using the SDK's local promise storage.
Durable Execution for application-level failures means that you get promise resolution timeouts, transparent function execution retries, and retry rate limits without adding an additional supervisor service.
You will create an HTTP service with a single route handler.
The handler will use resonate.run()
to run the downloadAndSummarize()
function.
Resonate will manage the execution of these functions, ensuring that the result of download()
is passed as input to summarize()
.
If any of the functions throw an error or reject a promise, Resonate will automatically retry the function execution.
Prerequisites
This tutorial assumes that you have NodeJS and npm installed.
Part 1 Incremental adoption
Create a project folder.
mkdir resonate-quickstart && cd resonate-quickstart
Install the dev dependencies.
npm init -y && npm install typescript @types/node --save-dev
Install the app dependencies.
npm install @resonatehq/sdk express @types/express
Create a file named app.ts and copy and paste the minimal distributed async/await application below:
docs/get-started/typescript-quickstart/code/part-1/src/app.ts
import { Context } from "@resonatehq/sdk";
// downloadAndSummarize is the top level function that awaits on the download and summarize functions.
export async function downloadAndSummarize(ctx: Context, url: string) {
// Download the content from the provided URL
console.log("Downloading and summarizing content from", url);
let content = await ctx.run(download, url);
// Summarize the downloaded content
let summary = await ctx.run(summarize, content);
// Return the summary of the content
return summary;
}
// download simulates downloading a page from the internet.
// This function has a 50% chance of failing.
async function download(ctx: Context, url: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 50% chance to fail
if (Math.random() < 0.5) {
console.log("download failed");
reject("download failed");
} else {
console.log("download successful");
resolve("This is the text of the page");
}
}, 2500);
});
}
// summarize simulates summarizing the downloaded content
// This function has a 50% chance of failing.
async function summarize(ctx: Context, text: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 50% chance to fail
if (Math.random() < 0.5) {
console.log("summarization failed");
reject("summarization failed");
} else {
console.log("summarization successful");
resolve("This is a summary of the text");
}
}, 2500);
});
}
The code that makes up app.ts
is a simple mock application that simulates downloading a page from the internet and then summarizing the content of that page.
It represents the "business logic" of the service.
You have your downloadAndSummarize()
main function that orchestrates the steps of your application (download()
then summarize()
).
Both the download()
and summarize()
functions include a 2.5-second timeout to simulate the time required for downloading and summarizing content from a URL.
These functions are invoked using ctx.run
, which wraps the function call in a promise, adds it to the call graph, and introduces durability.
In this example, download()
and summarize()
are mock functions designed to “fail” 50% of the time.
When they return a “rejected” promise, Resonate automatically retries them until they succeed.
Next, create a file named gateway.ts
and paste in the following express server code that uses Resonate:
docs/get-started/typescript-quickstart/code/part-1/src/gateway.ts
import express, { Request, Response } from "express";
import { Resonate, Context } from "@resonatehq/sdk";
import { downloadAndSummarize } from "./app";
// Initialize a Resonate application.
const resonate = new Resonate();
// Register a function as a Resonate function
resonate.register(
"downloadAndSummarize", // function name
downloadAndSummarize, // function pointer
resonate.options({ timeout: 20000 }) // set a total execution timeout of 20 seconds
);
// Start the Resonate application
resonate.start();
// Initialize an Express application.
const app = express().use(express.json());
// Register a function as an Express endpoint
app.post("/summarize", async (req: Request, res: Response) => {
const url = req.body?.url;
try {
// Call the resonate function
let summary = await resonate.run(
"downloadAndSummarize", // function name
`summarize-${url}`, // promise ID
url // function argument
);
res.send(summary);
} catch (e) {
res.status(500).send("An error occurred.");
}
});
// Start the Express application
app.listen(3000, () => {
console.log("Listening on port 3000");
});
The gateway.ts
file contains an Express server that listens on port 3000 and includes a single route handler that leverages Resonate to run the downloadAndSummarize()
function.
To set it up, you instantiate a Resonate object, register the top-level function via resonate.run()
, call resonate.start()
, and create an Express HTTP handler for the /summarize
route before starting the server on port 3000.
When calling resonate.run()
, you must provide the function name, a unique identifier, and the function’s arguments.
The identifier serves as the promise ID for the function execution.
If you use the same identifier for multiple calls to resonate.run()
, you will receive the result from the initial execution without re-running the function.
To get a new result on each call, you need to supply a different promise ID.
Lastly, make sure to update your package.json
file to include the following scripts:
{
"scripts": {
"dev": "npx tsx src/gateway.ts"
}
}
To start your summarization service run npm start
.
npm run dev
Then, from another terminal send a POST
request to the /summarize
endpoint.
curl -X POST http://localhost:3000/summarize -H "Content-Type: application/json" -d '{"url": "http://example.com"}'
Watch the log output of your service. There is a good chance that you will see either "download failed" or "summarization failed" or both one or more times.
However, even if you see those failures logged, eventually you should get the text response, "This is a summary of the text", back to where you made POST request.
After the request for example.com succeeds, try running it again. Notice how on subsequent requests with the same URL, Resonate does not start the executions again but returns the same root-level promise immediately.
This is because the url makes up part of the root-level promise ID. The Resonate TypeScript SDK stores that promise locally and ensures that if the same ID is used in subsequent calls, the result of the execution associated with that promise is returned.
However, if you restart the service or provide a different url, the service will execute the functions again.
In the next part of the tutorial, you will connect to a Resonate Server which stores the promise remotely, so that even if your HTTP server crashes, the function executions will be able to eventually complete.
Part 2 Crash recovery
Now it is time to connect your service to a Resonate Server to enable recovery from platform-level failures, otherwise known as Durable Execution.
To simulate a platform-level failure, you will kill the HTTP server process in the middle of the execution of the downloadAndSummarize()
function.
Follow the steps in the Resonate Server install guide to install and run the server on your machine.
By default, the Server runs on localhost port 8001.
After the server is running, you can update your service code.
There are three places where you need to update your code for this nex part of the tutorial.
First, in your gateway.ts
file, update the new Resonate()
call and provide it with the URL of the server, in this case http://localhost:8001
.
docs/get-started/typescript-quickstart/code/part-2/src/gateway.ts
// Initialize a Resonate application.
const resonate = new Resonate({ url: "http://localhost:8001" });
Then, extend the top-level promise resolution timeout to a full minute.
docs/get-started/typescript-quickstart/code/part-2/src/gateway.ts
// Register a function as a Resonate function
resonate.register(
"downloadAndSummarize", // function name
downloadAndSummarize, // function pointer
resonate.options({ timeout: 60000 }) // set a total execution timeout of 1 minute
);
You are increasing the top-level promise resolution timeout to a full minute so that you have time to kill the service and bring it back up again.
And finally, in your app.ts
file add a 10 second sleep to the downloadAndSummarize()
function between download()
and summarize()
.
docs/get-started/typescript-quickstart/code/part-2/src/app.ts
export async function downloadAndSummarize(ctx: Context, url: string) {
console.log("Downloading and summarizing content from", url);
// Download the content from the provided URL
let content = await ctx.run(download, url);
// Sleep for 10 seconds
await ctx.sleep(10000);
// Summarize the downloaded content
let summary = await ctx.run(summarize, content);
// Return the summary of the content
return summary;
}
You are adding a 10 second sleep so that you have time between the download()
step and the summarize()
step so that you have time to kill the service before the summarize()
step starts.
Now that you have your code updated, it is time to simulate a service outage.
If you haven't already, restart your HTTP service with the updated code.
npm start
After your service is running with the updated code, send the POST
request to your service.
curl -X POST http://localhost:3000/summarize -H "Content-Type: application/json" -d '{"url": "http://example.com"}'
Let it run it until you see that the log "download successful", then kill (Ctrl-c) the service.
Because you are using cURL from another terminal, you will get "received". In Part 1 of the tutorial, killing the HTTP service would have resulted in the loss of the state of the executions. However, because the service is connected to the Resonate Server, when you bring the service back up, the execution sequence continues from where it left off.
Start the service again.
Assuming that you stopped the service after the download()
function ran, you should now see the summarize()
function logs and the application complete its execution.
Lets, look at the sequence diagram to understand what happened.
In the diagram above, you can map function 1 to downloadAndSummarize()
, function 2 to download()
, and function 3 to summarize()
.
Notice how function 1 gets the result of function 2 from the Durable Promise after the process restarts.
That is because the result of function 2 was stored in promise 2 before the process crashed.
This effectively resumes the execution of function 1 from where it left off.
Next, you will inspect the promise in the Resonate Server to see that it resolved successfully.
You can inspect the promises stored in the Resonate Server via the Resonate CLI.
resonate promise get summarize-http://example.com
You should see output similar to the following:
Id: summarize-http://example.com
State: RESOLVED
Timeout: 9008909898871320
Idempotency Key (create): summarize-http://example.com
Idempotency Key (complete): summarize-http://example.com
Param:
Headers:
Data:
{"func":"downloadAndSummarize","args":["http://example.com"]}
Value:
Headers:
Data:
"This is a summary of the text"
Tags:
resonate:invocation: true
Try out your app again with another url (to create a new promise with a different ID). Then, kill the HTTP service halfway through execution. Query for the promise in the Resonate Server and check out the status.
You should see that the promise is marked PENDING
.
Bring your HTTP service back up and and then query for the promise again.
It should be marked RESOLVED
.
So, now you should have a good understanding of how to use the Resonate SDK and the Resonate Server to build a distributed async await applications!