If you’re looking for a good open-source framework to manage server state, Temporal Typescript SDK is a great one to try. If you’re not familiar with Temporal, here’s an excellent introduction video, and in this post, I’ll walk you through a simple workflow to show you how it works.
If you’d like to try it yourself, you can clone my repo and follow the steps in the README.
Reviewing our Requirements
Our workflow is for a fictional Uber-like ride-share service. The requirements are as follows:
-
The client can request a ride
-
A driver can accept the request, which transitions you to the
"driver is on their way"
state -
The system will timeout after 30 seconds and transition you to the
"no drivers found"
state
Setting up the TypeScript Project
First, I used the temporal package initializer to generate a hello-world example project. This gave me a simple, but working project to start from.
Here’s a quick overview of the most important pieces:
-
workflows.ts
- This is where we’ll define the main flow of our app’s business logic. Workflows are “just functions”, but their code must be deterministic. -
activities.ts
- Activities are actions like calling another service, transcoding a file, or sending an email. We can think of them as smaller steps within a workflow that don’t have to be deterministic. -
client.ts
- The client represents an external system that connects to the Temporal Server to start workflows and, perhaps, process the results. -
worker.ts
- Workers execute our workflows and activities. The Temporal Server feeds them tasks via a set of queues which makes them extremely scaleable. For this post we will focus on the previous three files.
Note: Why does Temporal care about determinism in workflows? It’s so that the workflows can be restored exactly as they were at any point during execution. If Temporal crashes right in the middle of a workflow, it can pick up right where it left off - no sweat! You can find more details in the docs.
Creating the Activity
I implemented the requestARide()
activity in activities.ts.
// activities.ts
export async function requestARide(): Promise{
console.log('Requesting a ride from the ride-share api...');
}
It’s just a placeholder right now that logs a message to the console, but it will help illustrate how activities are called from workflows.
Creating the Workflow
The next step was to implement the rideshareWorkflow()
in workflows.ts.
// workflows.ts
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
const { requestARide } = wf.proxyActivities({
startToCloseTimeout: '5s'
});
export const driverAcceptedSignal = wf.defineSignal('driverAcceptedSignal');
export async function rideshareWorkflow(requestARideTimeout: string | number): Promise {
await requestARide();
let driverHasAccepted = false;
wf.setHandler(driverAcceptedSignal, () => void (driverHasAccepted = true));
if (await wf.condition(() => driverHasAccepted === true, requestARideTimeout)) {
// reach here if predicate function is true
return 'driver is on their way';
} else {
// reach here if timeout happens first
return 'no drivers found';
}
}
Let’s take note of a few things here:
-
We’re setting up our
requestARide()
activity withwf.proxyActivities<>()
so it can be scheduled for execution by the Temporal Server (rather than being executed directly). -
We’re using
wf.defineSignal()
andwf.setHandler()
so that drivers will be able to “signal” into this workflow to indicate that they have accepted the request. -
We’re using
wf.condition()
to wait for either thedriverAcceptedSignal
, or therequestARideTimeout
- whichever happens first. It’s quite a nifty helper. See the docs for more details.
Kicking off the Workflow
With our workflow in place, we can now use a client to run it. Let’s take a quick look at client.ts.
// client.ts
import { Connection, WorkflowClient } from '@temporalio/client';
import { rideshareWorkflow } from './workflows';
async function run() {
const connection = new Connection({});
const client = new WorkflowClient(connection.service, {});
const handle = await client.start(rideshareWorkflow, {
args: ['30s'],
taskQueue: 'rideshare-task-queue',
workflowId: 'wf-id-' + Math.floor(Math.random() * 1000),
});
console.log(`Started workflow ${handle.workflowId}`);
console.log(await handle.result());
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
This client code represents what might be run on the end-user’s phone as they are requesting their ride. Notice how it connects to the Temporal Server and then start()
s a rideshareWorkflow
.
We’re also logging the workflow result to the console with handle.result()
which returns a promise (see the docs). In our case, that promise will resolve just as soon as a driver signals that they’ve accepted the ride request, or the timeout occurs - whichever comes first.
If we just run the client.ts script above, we’ll see how the workflow can end after a timeout. Now, to cover the other possibility: when a driver accepts the ride request.
Signaling into the Workflow
Drivers need to be able to “signal” into the workflow to indicate that they have accepted the ride request. Let’s take a look at how we did this in driver-accepts-request.ts. Think of this next example as a client made specifically for the driver.
// driver-accepts-request.ts
import { Connection, WorkflowClient } from '@temporalio/client';
import { driverAcceptedSignal } from './workflows';
async function run() {
const workflowId = process.argv
?.find(arg => arg.includes('--workflow'))
?.split('=')
[1];
const connection = new Connection({});
const client = new WorkflowClient(connection.service, {});
if (workflowId){
const handle = client.getHandle(workflowId);
await handle.signal(driverAcceptedSignal);
console.log('signal has been sent');
return;
}
throw new Error('workflowId was not provided');
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
It’s almost identical to the client.ts
script, except for 2 major differences:
-
Our
driver-accepts-requests.ts
script is designed to be run from the command line, so we’ll parse theworkflowId
from the command line args. If this were a real app, the driver would pick aworkflowId
by reviewing a list of available rides. Our app uses command line args to keep the focus on Temporal. -
Instead of starting a new workflow, we’re going to use the
workflowId
to retrieve ahandle
for the existing one and thensignal()
into it.
What’s Next?
And there we are - we’ve implemented a workflow that meets our requirements!
If you want even more detail, you can check out this project’s README. From there you can run it yourself, and explore the details of your workflow executions using the Temporal Web UI.
And as always, if you have questions about this workflow, don’t hesitate to reach out on our Community Discord. We’re always around to talk shop.