durable-execution-orpc-utils
Version:
oRPC utilities for durable-execution to create a separate server process for durable execution
202 lines (148 loc) • 6.84 kB
Markdown
[](https://www.npmjs.com/package/durable-execution-orpc-utils)
[](https://github.com/gpahal/durable-execution/blob/main/LICENSE)
oRPC utilities for [durable-execution](https://gpahal.github.io/durable-execution) to create a
separate server process for durable execution.
The usecase of this package is to help create a long-running durable executor server process that
runs separately from a client web app. The durable executor server process manages the tasks and
the state of task executions. The tasks can be local tasks or remote calls to the web application.
The web application no longer needs to be long running - it can be serverless like a Next.js app
with all the business logic. You get durability, resilience and persistence with the durable
executor server process that orchestrated the business logic as a thin layer. The following
diagram shows the flow of a task execution.
```mermaid
sequenceDiagram
participant A as Web app (serverless)
participant B as Durable execution server
Note right of B: Tasks defined before start of<br/>durable executor. The tasks<br/> might be api calls to the<br/>web app
A->>B: enqueueTask(task1)
B->>A: executionId
Note over A,B: Start of task execution
B-->>A: task.run(webAppClient.call(...))
A-->>B: taskResult
Note over A,B: Task execution completed
A->>B: getTaskExecution(executionId)
B->>A: execution(status=completed, ...)
```
This package exposes:
- Server-side oRPC procedures to enqueue tasks and fetch task executions
- Clients for the server-side procedures. When a task is enqueued or execution is fetched, an API
call is made to the durable executor server internally
- A utility to wrap a client oRPC procedure as a task. When this task is executed on the server,
the client oRPC procedure is called. This is useful when all the business logic is in the client
web app and you want to use the durable executor server process just to manage the state of task
executions.
## Installation
- npm
```bash
npm install durable-execution durable-execution-orpc-utils @orpc/client @orpc/contract @orpc/server
```
- pnpm
```bash
pnpm add durable-execution durable-execution-orpc-utils @orpc/client @orpc/contract @orpc/server
```
## Usage
### Expose tasks router from the durable executor server
```ts
// durable-executor-server.ts
import { os } from '@orpc/server'
import { DurableExecutor, InMemoryStorage } from 'durable-execution'
import { createTasksRouter } from 'durable-execution-orpc-utils/server'
// Create executor (use any Storage implementation)
const executor = new DurableExecutor(new InMemoryStorage())
// Register tasks
const add1 = executor.task({
id: 'add1',
timeoutMs: 5000,
run: async (_, input: { n: number }) => ({ n: input.n + 1 }),
})
const add2 = executor.task({
id: 'add2',
timeoutMs: 5000,
run: async (_, input: { n: number }) => ({ n: input.n + 2 }),
})
// Tasks to expose to the client
const tasks = { add1, add2 }
// Build oRPC router to enqueue tasks and fetch task executions
export const tasksRouter = createTasksRouter(os, executor)
async function server() {
// ... start the long-running server (see oRPC server docs for more details)
}
// Start the durable executor background processes
executor.startBackgroundProcesses()
// Run the server
await server()
// Shutdown the durable executor when the app is done
await executor.shutdown()
```
```ts
// web-app.ts
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { RouterClient } from '@orpc/server'
import { tasks, type tasksRouter } from './durable-executor-server'
// Create a client for the tasks router. This will be used to enqueue tasks.
const tasksRouterLink = new RPCLink({
url: 'http://localhost:3000/rpc',
headers: () => ({
authorization: 'TOKEN',
}),
})
const tasksRouterClient: RouterClient<typeof tasksRouter> = createORPCClient(tasksRouterLink)
// Enqueue a task and get execution
const executionId = await tasksRouterClient.enqueueTask({ taskId: 'add1', input: { n: 0 } })
const execution = await tasksRouterClient.getTaskExecution({ taskId: 'add1', executionId })
// Create handles for the tasks for type-safe enqueue and get execution
const handles = createTaskClientHandles(tasksRouterClient, tasks)
const executionId = await handles.add1.enqueue({ n: 0 })
const execution = await handles.add1.getExecution(executionId)
```
On the client web app, define the procedures as usual and create an oRPC router and expose it using
an oRPC server.
```ts
// web-app.ts
import { os } from '@orpc/server'
// Define a client procedure
const add1 = os
.input(type<{ n: number }>())
.output(type<{ n: number }>())
.handler(({ input }) => ({ n: input.n + 1 }))
const webAppRouter = { add1 }
// ... expose the client web app router using an oRPC server
```
On the server, create a client for the web app router and use the
`convertClientProcedureToTask` utility to wrap the client procedures as tasks.
```ts
// durable-executor-server.ts
import { convertClientProcedureToTask } from 'durable-execution-orpc-utils/server'
import { type webAppRouter } from './web-app'
// Create a client for the web app router
const webAppRouterLink = new RPCLink({
url: 'http://localhost:3000/rpc',
headers: () => ({
authorization: 'TOKEN',
}),
})
const webAppRouterClient: RouterClient<typeof webAppRouter> = createORPCClient(webAppRouterLink)
const clientAdd1 = convertClientProcedureToTask(executor, { id: 'add1', timeoutMs: 5000 }, webAppRouterClient.add1)
export const tasks = { add1, add2, clientAdd1 }
```
Enqueue tasks and query execution state on the client web app. This runs the client procedure on the
web app itself but the execution state is managed by the durable executor server.
```ts
// web-app.ts
import { tasks, type tasksRouter } from './durable-executor-server'
// ... create a client for the tasks router like above
// Create handles for the tasks for type-safe enqueue and get execution
const handles = createTaskClientHandles(tasksRouterClient, tasks)
const executionId = await handles.clientAdd1.enqueue({ n: 0 })
const execution = await handles.clientAdd1.getExecution(executionId)
```
- Durable Execution docs: <https://gpahal.github.io/durable-execution>
- Repository: <https://github.com/gpahal/durable-execution>
This project is licensed under the MIT License. See the
[](https://github.com/gpahal/durable-execution/blob/main/LICENSE) file for details.