Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Documentation / Examples - [x] Make sure the linting passes
- Loading branch information
Showing
19 changed files
with
502 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env.local | ||
.env.development.local | ||
.env.test.local | ||
.env.production.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# compiled files | ||
temporal/lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
16 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Temporal + Next.js Example | ||
|
||
This is a starter project for creating resilient Next.js applications with [Temporal](https://temporal.io/). Whenever our [API routes](https://nextjs.org/docs/api-routes/introduction) need to do any of the following, we can greatly increase our code's fault tolerance by using Temporal: | ||
|
||
- Perform a series of network requests (to a database, another function, an internal service, or an external API), any of which may fail. Temporal will automatically set timeouts and retry, as well as remember the state of execution in the event of power loss. | ||
- Long-running tasks | ||
- Cron jobs | ||
|
||
The starter project has this logic flow: | ||
|
||
- Load [`localhost:3000`](http://localhost:3000) | ||
- Click the “Create order” button | ||
- The click handler POSTs a new order object to `/api/orders` | ||
- The serverless function at `pages/api/orders/index.ts`: | ||
- Parses the order object | ||
- Tells Temporal Server to start a new Order Workflow, and passes the user ID and order info | ||
- Waits for the Workflow result and returns it to the client | ||
- Temporal Server puts Workflow tasks on the `orders` task queue | ||
- The Worker executes chunks of Workflow and Activity code. The Activity code logs: | ||
|
||
``` | ||
Reserving 2 of item B102 | ||
Charging user 123 for 2 of item B102 | ||
``` | ||
|
||
- The Workflow completes and Temporal Server sends the result back to the serverless function, which returns it to the client, which alerts the result. | ||
|
||
Here is the Temporal code: | ||
|
||
- The Workflow: `temporal/src/workflows/order.ts` | ||
- The Activites: `temporal/src/activities/{payment|inventory}.ts` | ||
|
||
There are three parts of this starter project that are left unimplemented: | ||
|
||
- Authentication (currently, the client sends their user ID in the authorization header): `pages/api/orders/index.ts` | ||
- Doing database queries to check and alter inventory levels: `temporal/src/activities/inventory.ts` | ||
- Communicating with a payments service (or function) to charge the user: `temporal/src/activities/payment.ts` | ||
|
||
## Deploy your own | ||
|
||
### Web server | ||
|
||
The Next.js server can be deployed using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): | ||
|
||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-temporal&project-name=with-temporal&repository-name=with-temporal) | ||
|
||
### Worker | ||
|
||
One or more instances of the worker (`temporal/src/worker/`) can be deployed to a PaaS (each worker is a long-running Node process, so it can't run on a FaaS/serverless platform). | ||
|
||
### Temporal Server | ||
|
||
Temporal Server is a cluster of internal services, a database of record, and a search database. It can be run locally with Docker Compose and [deployed](https://docs.temporal.io/docs/server/production-deployment) with a container orchestration service like Kubernetes or ECS. | ||
|
||
## How to use | ||
|
||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example with-temporal next-temporal-app | ||
# or | ||
yarn create next-app --example with-temporal next-temporal-app | ||
``` | ||
|
||
The Temporal Node SDK requires [Node `>= 14`, `node-gyp`, and Temporal Server](https://docs.temporal.io/docs/node/getting-started#step-0-prerequisites). Once you have everything installed, you can develop locally with the below commands in four different shells: | ||
|
||
In the Temporal Server docker directory: | ||
|
||
```bash | ||
docker-compose up | ||
``` | ||
|
||
In the `next-temporal-app/` directory: | ||
|
||
```bash | ||
npm run dev | ||
``` | ||
|
||
```bash | ||
npm run build-worker.watch | ||
``` | ||
|
||
```bash | ||
npm run start-worker | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import React, { ReactNode } from 'react' | ||
import Link from 'next/link' | ||
import Head from 'next/head' | ||
|
||
type Props = { | ||
children?: ReactNode | ||
title?: string | ||
} | ||
|
||
const Layout = ({ children, title = 'This is the default title' }: Props) => ( | ||
<div> | ||
<Head> | ||
<title>{title}</title> | ||
</Head> | ||
<header> | ||
<nav> | ||
<Link href="/"> | ||
<a>Home</a> | ||
</Link>{' '} | ||
|{' '} | ||
<Link href="/about"> | ||
<a>About</a> | ||
</Link>{' '} | ||
</nav> | ||
</header> | ||
{children} | ||
<footer> | ||
<hr /> | ||
<span>I'm here to stay (Footer)</span> | ||
</footer> | ||
</div> | ||
) | ||
|
||
export default Layout |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/types/global" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"private": true, | ||
"scripts": { | ||
"dev": "next", | ||
"build": "next build", | ||
"start": "next start", | ||
"type-check": "tsc", | ||
"build-worker": "tsc --build temporal/src/worker/tsconfig.json", | ||
"build-worker.watch": "tsc --build --watch temporal/src/worker/tsconfig.json", | ||
"start-worker": "nodemon -w temporal/lib/ -x 'node temporal/lib/worker || (sleep 5; touch temporal/lib/worker/index.js)'" | ||
}, | ||
"dependencies": { | ||
"next": "latest", | ||
"react": "^17.0.2", | ||
"react-dom": "^17.0.2", | ||
"temporalio": "^0.3.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.15.0", | ||
"@tsconfig/node14": "^1.0.0", | ||
"@types/node": "^12.12.21", | ||
"@types/react": "^17.0.2", | ||
"@types/react-dom": "^17.0.1", | ||
"nodemon": "^2.0.12", | ||
"typescript": "^4.3.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { NextPage } from 'next' | ||
import Link from 'next/link' | ||
import Layout from '../components/Layout' | ||
|
||
const AboutPage: NextPage = () => ( | ||
<Layout title="About | Next.js + Temporal Example"> | ||
<h1>About</h1> | ||
<p>This is the about page</p> | ||
<p> | ||
<Link href="/"> | ||
<a>Go home</a> | ||
</Link> | ||
</p> | ||
</Layout> | ||
) | ||
|
||
export default AboutPage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next' | ||
import { Connection, WorkflowClient } from '@temporalio/client' | ||
import { Order } from '../../../temporal/src/interfaces/workflows' | ||
|
||
export type Data = { | ||
result: string | ||
} | ||
|
||
function getUserId(token: string): string { | ||
// TODO if the token is a JWT, decode & verify it. If it's a session ID, | ||
// look up the user's ID in the session store. | ||
return 'user-id-123' | ||
} | ||
|
||
export default async function handler( | ||
req: NextApiRequest, | ||
res: NextApiResponse<Data> | ||
) { | ||
if (req.method !== 'POST') { | ||
res.send({ result: 'Error code 405: use POST' }) | ||
return | ||
} | ||
|
||
const userId: string = getUserId(req.headers.authorization) | ||
const { itemId, quantity } = JSON.parse(req.body) | ||
|
||
// Connect to our Temporal Server running locally in Docker | ||
const connection = new Connection() | ||
const client = new WorkflowClient(connection.service) | ||
const example = client.stub<Order>('order', { taskQueue: 'orders' }) | ||
|
||
// Execute the Order workflow and wait for it to finish | ||
const result = await example.execute(userId, itemId, quantity) | ||
return res.status(200).json({ result }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import Link from 'next/link' | ||
import Layout from '../components/Layout' | ||
import { Data } from './api/orders' | ||
|
||
const IndexPage = () => ( | ||
<Layout title="Home | Next.js + Temporal Example"> | ||
<h1>Hello Next.js 👋</h1> | ||
|
||
<button | ||
onClick={async () => { | ||
const newOrder = { itemId: 'B102', quantity: 2 } | ||
const response = await fetch('/api/orders', { | ||
method: 'POST', | ||
headers: { Authorization: 'session-id-or-jwt' }, | ||
body: JSON.stringify(newOrder), | ||
}) | ||
const data: Data = await response.json() | ||
console.log(data) | ||
alert(data.result) | ||
}} | ||
> | ||
Create order | ||
</button> | ||
|
||
<p> | ||
<Link href="/about"> | ||
<a>About</a> | ||
</Link> | ||
</p> | ||
</Layout> | ||
) | ||
|
||
export default IndexPage |
28 changes: 28 additions & 0 deletions
28
examples/with-temporal/temporal/src/activities/inventory.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export async function checkAndDecrementInventory( | ||
itemId: string, | ||
quantity: number | ||
): Promise<boolean> { | ||
// TODO a database request that—in a single operation or transaction—checks | ||
// whether there are `quantity` items remaining, and if so, decreases the | ||
// total. Something like: | ||
// const result = await db.collection('items').updateOne( | ||
// { _id: itemId, numAvailable: { $gte: quantity } }, | ||
// { $inc: { numAvailable: -quantity } } | ||
// ) | ||
// return result.modifiedCount === 1 | ||
console.log(`Reserving ${quantity} of item ${itemId}`) | ||
return true | ||
} | ||
|
||
export async function incrementInventory( | ||
itemId: string, | ||
quantity: number | ||
): Promise<boolean> { | ||
// TODO increment inventory: | ||
// const result = await db.collection('items').updateOne( | ||
// { _id: itemId }, | ||
// { $inc: { numAvailable: quantity } } | ||
// ) | ||
// return result.modifiedCount === 1 | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export type ChargeResult = { | ||
success: boolean | ||
errorMessage?: string | ||
} | ||
|
||
export async function chargeUser( | ||
userId: string, | ||
itemId: string, | ||
quantity: number | ||
): Promise<ChargeResult> { | ||
// TODO send request to the payments service that looks up the user's saved | ||
// payment info and the cost of the item and attempts to charge their payment | ||
// method. | ||
console.log(`Charging user ${userId} for ${quantity} of item ${itemId}`) | ||
return { success: true } | ||
// return { success: false, errorMessage: 'Card expired' } | ||
} |
21 changes: 21 additions & 0 deletions
21
examples/with-temporal/temporal/src/activities/tsconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"extends": "@tsconfig/node14/tsconfig.json", | ||
"version": "4.2.2", | ||
"compilerOptions": { | ||
"emitDecoratorMetadata": false, | ||
"experimentalDecorators": false, | ||
"declaration": true, | ||
"declarationMap": true, | ||
"sourceMap": true, | ||
"composite": true, | ||
"incremental": true, | ||
"rootDir": ".", | ||
"outDir": "../../lib/activities" | ||
}, | ||
"include": ["./**/*.ts"], | ||
"references": [ | ||
{ | ||
"path": "../interfaces/tsconfig.json" | ||
} | ||
] | ||
} |
16 changes: 16 additions & 0 deletions
16
examples/with-temporal/temporal/src/interfaces/tsconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"extends": "@tsconfig/node14/tsconfig.json", | ||
"version": "4.2.2", | ||
"compilerOptions": { | ||
"emitDecoratorMetadata": false, | ||
"experimentalDecorators": false, | ||
"declaration": true, | ||
"declarationMap": true, | ||
"sourceMap": true, | ||
"composite": true, | ||
"incremental": true, | ||
"rootDir": ".", | ||
"outDir": "../../lib/interfaces" | ||
}, | ||
"include": ["./**/*.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Workflow } from '@temporalio/workflow' | ||
|
||
export interface Order extends Workflow { | ||
main(userId: string, itemId: string, quantity: number): Promise<string> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Worker } from '@temporalio/worker' | ||
|
||
async function run() { | ||
// https://docs.temporal.io/docs/node/workers/ | ||
const worker = await Worker.create({ | ||
workDir: __dirname, | ||
nodeModulesPath: `${__dirname}/../../../node_modules`, | ||
taskQueue: 'orders', | ||
}) | ||
|
||
// Start accepting tasks on the `orders` queue | ||
await worker.run() | ||
} | ||
|
||
run().catch((err) => { | ||
console.error(err) | ||
process.exit(1) | ||
}) |
Oops, something went wrong.