Resonate TypeScript quickstart tutorial • Part 1
In the first part of this tutorial you will build a mock summarization service using Resonate. This 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.
Set up the project
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/part-1/code/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/part-1/code/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"
}
}
Run the application
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.
Up next
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.