From 05979b82e7347f72cae758357d819ed3c3919143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 14:59:32 -0400 Subject: [PATCH 01/20] Copy with-typescript --- examples/with-temporal/.gitignore | 34 +++++++++++ examples/with-temporal/README.md | 47 ++++++++++++++ examples/with-temporal/components/Layout.tsx | 41 +++++++++++++ examples/with-temporal/components/List.tsx | 19 ++++++ .../with-temporal/components/ListDetail.tsx | 16 +++++ .../with-temporal/components/ListItem.tsx | 18 ++++++ examples/with-temporal/interfaces/index.ts | 10 +++ examples/with-temporal/next-env.d.ts | 6 ++ examples/with-temporal/package.json | 20 ++++++ examples/with-temporal/pages/about.tsx | 16 +++++ .../with-temporal/pages/api/users/index.ts | 16 +++++ examples/with-temporal/pages/index.tsx | 15 +++++ examples/with-temporal/pages/users/[id].tsx | 61 +++++++++++++++++++ examples/with-temporal/pages/users/index.tsx | 37 +++++++++++ examples/with-temporal/tsconfig.json | 19 ++++++ examples/with-temporal/utils/sample-data.ts | 9 +++ 16 files changed, 384 insertions(+) create mode 100644 examples/with-temporal/.gitignore create mode 100644 examples/with-temporal/README.md create mode 100644 examples/with-temporal/components/Layout.tsx create mode 100644 examples/with-temporal/components/List.tsx create mode 100644 examples/with-temporal/components/ListDetail.tsx create mode 100644 examples/with-temporal/components/ListItem.tsx create mode 100644 examples/with-temporal/interfaces/index.ts create mode 100644 examples/with-temporal/next-env.d.ts create mode 100644 examples/with-temporal/package.json create mode 100644 examples/with-temporal/pages/about.tsx create mode 100644 examples/with-temporal/pages/api/users/index.ts create mode 100644 examples/with-temporal/pages/index.tsx create mode 100644 examples/with-temporal/pages/users/[id].tsx create mode 100644 examples/with-temporal/pages/users/index.tsx create mode 100644 examples/with-temporal/tsconfig.json create mode 100644 examples/with-temporal/utils/sample-data.ts diff --git a/examples/with-temporal/.gitignore b/examples/with-temporal/.gitignore new file mode 100644 index 000000000000000..1437c53f70bc211 --- /dev/null +++ b/examples/with-temporal/.gitignore @@ -0,0 +1,34 @@ +# 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 diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md new file mode 100644 index 000000000000000..3cb3ae36fc759a7 --- /dev/null +++ b/examples/with-temporal/README.md @@ -0,0 +1,47 @@ +# TypeScript Next.js example + +This is a really simple project that shows the usage of Next.js with TypeScript. + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-typescript) + +## Deploy your own + +Deploy the example 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-typescript&project-name=with-typescript&repository-name=with-typescript) + +## How to use it? + +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-typescript with-typescript-app +# or +yarn create next-app --example with-typescript with-typescript-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +## Notes + +This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. + +``` +npm install --save-dev typescript +``` + +To enable TypeScript's features, we install the type declarations for React and Node. + +``` +npm install --save-dev @types/react @types/react-dom @types/node +``` + +When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. + +Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. + +A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. diff --git a/examples/with-temporal/components/Layout.tsx b/examples/with-temporal/components/Layout.tsx new file mode 100644 index 000000000000000..8f111e1ccd2e639 --- /dev/null +++ b/examples/with-temporal/components/Layout.tsx @@ -0,0 +1,41 @@ +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) => ( +
+ + {title} + + + +
+ +
+ {children} +
+
+ I'm here to stay (Footer) +
+
+) + +export default Layout diff --git a/examples/with-temporal/components/List.tsx b/examples/with-temporal/components/List.tsx new file mode 100644 index 000000000000000..5fe5ef21580732c --- /dev/null +++ b/examples/with-temporal/components/List.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' +import ListItem from './ListItem' +import { User } from '../interfaces' + +type Props = { + items: User[] +} + +const List = ({ items }: Props) => ( + +) + +export default List diff --git a/examples/with-temporal/components/ListDetail.tsx b/examples/with-temporal/components/ListDetail.tsx new file mode 100644 index 000000000000000..fa3f9c3dc8d9d52 --- /dev/null +++ b/examples/with-temporal/components/ListDetail.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +import { User } from '../interfaces' + +type ListDetailProps = { + item: User +} + +const ListDetail = ({ item: user }: ListDetailProps) => ( +
+

Detail for {user.name}

+

ID: {user.id}

+
+) + +export default ListDetail diff --git a/examples/with-temporal/components/ListItem.tsx b/examples/with-temporal/components/ListItem.tsx new file mode 100644 index 000000000000000..b10b9c83d9645c1 --- /dev/null +++ b/examples/with-temporal/components/ListItem.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import Link from 'next/link' + +import { User } from '../interfaces' + +type Props = { + data: User +} + +const ListItem = ({ data }: Props) => ( + + + {data.id}: {data.name} + + +) + +export default ListItem diff --git a/examples/with-temporal/interfaces/index.ts b/examples/with-temporal/interfaces/index.ts new file mode 100644 index 000000000000000..68528c55121ee5c --- /dev/null +++ b/examples/with-temporal/interfaces/index.ts @@ -0,0 +1,10 @@ +// You can include shared interfaces/types in a separate file +// and then use them in any component by importing them. For +// example, to import the interface below do: +// +// import { User } from 'path/to/interfaces'; + +export type User = { + id: number + name: string +} diff --git a/examples/with-temporal/next-env.d.ts b/examples/with-temporal/next-env.d.ts new file mode 100644 index 000000000000000..9bc3dd46b9d9b21 --- /dev/null +++ b/examples/with-temporal/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json new file mode 100644 index 000000000000000..6c6b04565982e65 --- /dev/null +++ b/examples/with-temporal/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start", + "type-check": "tsc" + }, + "dependencies": { + "next": "latest", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@types/node": "^12.12.21", + "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.1", + "typescript": "4.0" + } +} diff --git a/examples/with-temporal/pages/about.tsx b/examples/with-temporal/pages/about.tsx new file mode 100644 index 000000000000000..4e6f301fe8ebac8 --- /dev/null +++ b/examples/with-temporal/pages/about.tsx @@ -0,0 +1,16 @@ +import Link from 'next/link' +import Layout from '../components/Layout' + +const AboutPage = () => ( + +

About

+

This is the about page

+

+ + Go home + +

+
+) + +export default AboutPage diff --git a/examples/with-temporal/pages/api/users/index.ts b/examples/with-temporal/pages/api/users/index.ts new file mode 100644 index 000000000000000..4efdba6f1a4e799 --- /dev/null +++ b/examples/with-temporal/pages/api/users/index.ts @@ -0,0 +1,16 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { sampleUserData } from '../../../utils/sample-data' + +const handler = (_req: NextApiRequest, res: NextApiResponse) => { + try { + if (!Array.isArray(sampleUserData)) { + throw new Error('Cannot find user data') + } + + res.status(200).json(sampleUserData) + } catch (err) { + res.status(500).json({ statusCode: 500, message: err.message }) + } +} + +export default handler diff --git a/examples/with-temporal/pages/index.tsx b/examples/with-temporal/pages/index.tsx new file mode 100644 index 000000000000000..57f913a9b58818b --- /dev/null +++ b/examples/with-temporal/pages/index.tsx @@ -0,0 +1,15 @@ +import Link from 'next/link' +import Layout from '../components/Layout' + +const IndexPage = () => ( + +

Hello Next.js 👋

+

+ + About + +

+
+) + +export default IndexPage diff --git a/examples/with-temporal/pages/users/[id].tsx b/examples/with-temporal/pages/users/[id].tsx new file mode 100644 index 000000000000000..61720da5a7a5dd3 --- /dev/null +++ b/examples/with-temporal/pages/users/[id].tsx @@ -0,0 +1,61 @@ +import { GetStaticProps, GetStaticPaths } from 'next' + +import { User } from '../../interfaces' +import { sampleUserData } from '../../utils/sample-data' +import Layout from '../../components/Layout' +import ListDetail from '../../components/ListDetail' + +type Props = { + item?: User + errors?: string +} + +const StaticPropsDetail = ({ item, errors }: Props) => { + if (errors) { + return ( + +

+ Error: {errors} +

+
+ ) + } + + return ( + + {item && } + + ) +} + +export default StaticPropsDetail + +export const getStaticPaths: GetStaticPaths = async () => { + // Get the paths we want to pre-render based on users + const paths = sampleUserData.map((user) => ({ + params: { id: user.id.toString() }, + })) + + // We'll pre-render only these paths at build time. + // { fallback: false } means other routes should 404. + return { paths, fallback: false } +} + +// This function gets called at build time on server-side. +// It won't be called on client-side, so you can even do +// direct database queries. +export const getStaticProps: GetStaticProps = async ({ params }) => { + try { + const id = params?.id + const item = sampleUserData.find((data) => data.id === Number(id)) + // By returning { props: item }, the StaticPropsDetail component + // will receive `item` as a prop at build time + return { props: { item } } + } catch (err) { + return { props: { errors: err.message } } + } +} diff --git a/examples/with-temporal/pages/users/index.tsx b/examples/with-temporal/pages/users/index.tsx new file mode 100644 index 000000000000000..7b3ee883b3a6754 --- /dev/null +++ b/examples/with-temporal/pages/users/index.tsx @@ -0,0 +1,37 @@ +import { GetStaticProps } from 'next' +import Link from 'next/link' + +import { User } from '../../interfaces' +import { sampleUserData } from '../../utils/sample-data' +import Layout from '../../components/Layout' +import List from '../../components/List' + +type Props = { + items: User[] +} + +const WithStaticProps = ({ items }: Props) => ( + +

Users List

+

+ Example fetching data from inside getStaticProps(). +

+

You are currently on: /users

+ +

+ + Go home + +

+
+) + +export const getStaticProps: GetStaticProps = async () => { + // Example for including static props in a Next.js function component page. + // Don't forget to include the respective types for any props passed into + // the component. + const items: User[] = sampleUserData + return { props: { items } } +} + +export default WithStaticProps diff --git a/examples/with-temporal/tsconfig.json b/examples/with-temporal/tsconfig.json new file mode 100644 index 000000000000000..93a83a407c40c84 --- /dev/null +++ b/examples/with-temporal/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/with-temporal/utils/sample-data.ts b/examples/with-temporal/utils/sample-data.ts new file mode 100644 index 000000000000000..1dd38ecc424ddba --- /dev/null +++ b/examples/with-temporal/utils/sample-data.ts @@ -0,0 +1,9 @@ +import { User } from '../interfaces' + +/** Dummy user data. */ +export const sampleUserData: User[] = [ + { id: 101, name: 'Alice' }, + { id: 102, name: 'Bob' }, + { id: 103, name: 'Caroline' }, + { id: 104, name: 'Dave' }, +] From ca9ee216dd523b3de7ab4713944d4300175d9be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 15:04:04 -0400 Subject: [PATCH 02/20] Add peer dep --- examples/with-temporal/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index 6c6b04565982e65..65fb84f8543304c 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -7,6 +7,7 @@ "type-check": "tsc" }, "dependencies": { + "@babel/core": "^7.15.0", "next": "latest", "react": "^17.0.2", "react-dom": "^17.0.2" From 66400852a45685bcce93411542fb7891832aeefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 15:05:01 -0400 Subject: [PATCH 03/20] Upgrade TypeScript --- examples/with-temporal/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index 65fb84f8543304c..c056aa53dddf5d8 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -16,6 +16,6 @@ "@types/node": "^12.12.21", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", - "typescript": "4.0" + "typescript": "^4.3.5" } } From 04f429e73cc5725cb3fbeec71322772307fc0831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 15:05:17 -0400 Subject: [PATCH 04/20] Add minimum Node version --- examples/with-temporal/.nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/with-temporal/.nvmrc diff --git a/examples/with-temporal/.nvmrc b/examples/with-temporal/.nvmrc new file mode 100644 index 000000000000000..8351c19397f4fcd --- /dev/null +++ b/examples/with-temporal/.nvmrc @@ -0,0 +1 @@ +14 From 63c8a51d82de22e3e5c9d275c5a7ccb2ad781fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 17:11:09 -0400 Subject: [PATCH 05/20] Add Temporal --- examples/with-temporal/.gitignore | 3 ++ examples/with-temporal/package.json | 11 +++-- .../temporal/src/activities/greeter.ts | 3 ++ .../temporal/src/activities/tsconfig.json | 21 +++++++++ .../temporal/src/interfaces/tsconfig.json | 16 +++++++ .../temporal/src/interfaces/workflows.ts | 7 +++ .../temporal/src/worker/index.ts | 23 ++++++++++ .../temporal/src/worker/tsconfig.json | 27 +++++++++++ .../temporal/src/workflows/example.ts | 10 ++++ .../temporal/src/workflows/tsconfig.json | 46 +++++++++++++++++++ 10 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 examples/with-temporal/temporal/src/activities/greeter.ts create mode 100644 examples/with-temporal/temporal/src/activities/tsconfig.json create mode 100644 examples/with-temporal/temporal/src/interfaces/tsconfig.json create mode 100644 examples/with-temporal/temporal/src/interfaces/workflows.ts create mode 100644 examples/with-temporal/temporal/src/worker/index.ts create mode 100644 examples/with-temporal/temporal/src/worker/tsconfig.json create mode 100644 examples/with-temporal/temporal/src/workflows/example.ts create mode 100644 examples/with-temporal/temporal/src/workflows/tsconfig.json diff --git a/examples/with-temporal/.gitignore b/examples/with-temporal/.gitignore index 1437c53f70bc211..aff5690c8eec733 100644 --- a/examples/with-temporal/.gitignore +++ b/examples/with-temporal/.gitignore @@ -32,3 +32,6 @@ yarn-error.log* # vercel .vercel + +# compiled files +temporal/lib \ No newline at end of file diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index c056aa53dddf5d8..8b2469a3d874641 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -4,18 +4,23 @@ "dev": "next", "build": "next build", "start": "next start", - "type-check": "tsc" + "type-check": "tsc", + "build-temporal": "tsc --build temporal/src/worker/tsconfig.json", + "build-temporal.watch": "tsc --build --watch temporal/src/worker/tsconfig.json", + "start-worker": "node temporal/lib/worker" }, "dependencies": { "@babel/core": "^7.15.0", "next": "latest", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "temporalio": "^0.3.0" }, "devDependencies": { "@types/node": "^12.12.21", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "@tsconfig/node14": "^1.0.0" } } diff --git a/examples/with-temporal/temporal/src/activities/greeter.ts b/examples/with-temporal/temporal/src/activities/greeter.ts new file mode 100644 index 000000000000000..28297c60cbe9821 --- /dev/null +++ b/examples/with-temporal/temporal/src/activities/greeter.ts @@ -0,0 +1,3 @@ +export async function greet(name: string): Promise { + return `Hello, ${name}!` +} diff --git a/examples/with-temporal/temporal/src/activities/tsconfig.json b/examples/with-temporal/temporal/src/activities/tsconfig.json new file mode 100644 index 000000000000000..e0f04b08e389675 --- /dev/null +++ b/examples/with-temporal/temporal/src/activities/tsconfig.json @@ -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" + } + ] +} diff --git a/examples/with-temporal/temporal/src/interfaces/tsconfig.json b/examples/with-temporal/temporal/src/interfaces/tsconfig.json new file mode 100644 index 000000000000000..be3e36444feb064 --- /dev/null +++ b/examples/with-temporal/temporal/src/interfaces/tsconfig.json @@ -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"] +} diff --git a/examples/with-temporal/temporal/src/interfaces/workflows.ts b/examples/with-temporal/temporal/src/interfaces/workflows.ts new file mode 100644 index 000000000000000..c3a04470140e766 --- /dev/null +++ b/examples/with-temporal/temporal/src/interfaces/workflows.ts @@ -0,0 +1,7 @@ +import { Workflow } from '@temporalio/workflow' + +// Extend the generic Workflow interface to check that Example is a valid workflow interface +// Workflow interfaces are useful for generating type safe workflow clients +export interface Example extends Workflow { + main(name: string): Promise +} diff --git a/examples/with-temporal/temporal/src/worker/index.ts b/examples/with-temporal/temporal/src/worker/index.ts new file mode 100644 index 000000000000000..e67dfd6d260c046 --- /dev/null +++ b/examples/with-temporal/temporal/src/worker/index.ts @@ -0,0 +1,23 @@ +import { Worker } from '@temporalio/worker' + +async function run() { + // Automatically locate and register Activities and Workflows relative to __dirname + // (assuming package was bootstrapped with `npm init @temporalio`). + // Worker connects to localhost by default and uses console error for logging. + // Customize the Worker by passing more options to create(). + // create() tries to connect to the server and will throw if a connection could not be established. + // You may create multiple Workers in a single process in order to poll on multiple task queues. + // In order to configure the server connection parameters and other global options, + // use the Core.install() method to configure the Rust Core SDK singleton. + const worker = await Worker.create({ + workDir: __dirname, + taskQueue: 'tutorial', + }) + // Start accepting tasks on the `tutorial` queue + await worker.run() +} + +run().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/with-temporal/temporal/src/worker/tsconfig.json b/examples/with-temporal/temporal/src/worker/tsconfig.json new file mode 100644 index 000000000000000..978ea3e9eba3575 --- /dev/null +++ b/examples/with-temporal/temporal/src/worker/tsconfig.json @@ -0,0 +1,27 @@ +{ + "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/worker" + }, + "include": ["./**/*.ts"], + "references": [ + { + "path": "../interfaces/tsconfig.json" + }, + { + "path": "../activities/tsconfig.json" + }, + { + "path": "../workflows/tsconfig.json" + } + ] +} diff --git a/examples/with-temporal/temporal/src/workflows/example.ts b/examples/with-temporal/temporal/src/workflows/example.ts new file mode 100644 index 000000000000000..792214721970068 --- /dev/null +++ b/examples/with-temporal/temporal/src/workflows/example.ts @@ -0,0 +1,10 @@ +import { Example } from '../interfaces/workflows' +import { greet } from '@activities/greeter' + +// A workflow that simply calls an activity +async function main(name: string): Promise { + return greet(name) +} + +// Declare the workflow's type to be checked by the Typescript compiler +export const workflow: Example = { main } diff --git a/examples/with-temporal/temporal/src/workflows/tsconfig.json b/examples/with-temporal/temporal/src/workflows/tsconfig.json new file mode 100644 index 000000000000000..ca7d68b67ac1a1b --- /dev/null +++ b/examples/with-temporal/temporal/src/workflows/tsconfig.json @@ -0,0 +1,46 @@ +{ + "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": ".", + "lib": [ + "es5", + "es2015.core", + "es2015.generator", + "es2015.iterable", + "es2015.promise", + "es2015.proxy", + "es2015.reflect", + "es2015.symbol", + "es2015.symbol.wellknown", + "es2016.array.include", + "es2017.object", + "es2017.intl", + "es2017.sharedmemory", + "es2017.string", + "es2017.typedarrays" + ], + "typeRoots": ["."], + "outDir": "../../lib/workflows", + "paths": { + "@activities": ["../activities"], + "@activities/*": ["../activities/*"] + } + }, + "include": ["./**/*.ts"], + "references": [ + { + "path": "../activities/tsconfig.json" + }, + { + "path": "../interfaces/tsconfig.json" + } + ] +} From b8632f4a0ac247f3490f1bce7574d16a5def8a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Sun, 15 Aug 2021 23:44:00 -0400 Subject: [PATCH 06/20] Start Workflow --- examples/with-temporal/pages/about.tsx | 3 ++- examples/with-temporal/pages/api/order.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 examples/with-temporal/pages/api/order.ts diff --git a/examples/with-temporal/pages/about.tsx b/examples/with-temporal/pages/about.tsx index 4e6f301fe8ebac8..6c80ded5bf6d87a 100644 --- a/examples/with-temporal/pages/about.tsx +++ b/examples/with-temporal/pages/about.tsx @@ -1,7 +1,8 @@ +import type { NextPage } from 'next' import Link from 'next/link' import Layout from '../components/Layout' -const AboutPage = () => ( +const AboutPage: NextPage = () => (

About

This is the about page

diff --git a/examples/with-temporal/pages/api/order.ts b/examples/with-temporal/pages/api/order.ts new file mode 100644 index 000000000000000..8d74326defe8889 --- /dev/null +++ b/examples/with-temporal/pages/api/order.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { Connection, WorkflowClient } from '@temporalio/client' +import { Example } from '../../temporal/src/interfaces/workflows' + +type Data = { + result: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const connection = new Connection() + const client = new WorkflowClient(connection.service) + const example = client.stub('example', { taskQueue: 'tutorial' }) + const result = await example.execute('Temporal') + console.log(result) // Hello, Temporal! + res.status(200).json({ result }) +} From 910d2033232a7e29f70a68f25a400fe8f9f0e165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Mon, 16 Aug 2021 13:43:25 -0400 Subject: [PATCH 07/20] Update README --- examples/with-temporal/README.md | 52 ++++++++++++++++------------- examples/with-temporal/package.json | 4 +-- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md index 3cb3ae36fc759a7..e0d51352a10cf39 100644 --- a/examples/with-temporal/README.md +++ b/examples/with-temporal/README.md @@ -1,47 +1,51 @@ -# TypeScript Next.js example +# Temporal Next.js example -This is a really simple project that shows the usage of Next.js with TypeScript. +[Temporal](https://temporal.io/) is a runtime for orchestrating microservices or serverless functions. TODO -## Preview +## Deploy your own -Preview the example live on [StackBlitz](http://stackblitz.com/): +### Web server -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-typescript) +The web server and `pages/api/` serverless functions can be deployed using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): -## Deploy your own +[![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-typescript&project-name=with-typescript&repository-name=with-typescript) -Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): +### Worker -[![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-typescript&project-name=with-typescript&repository-name=with-typescript) +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 FaaS/serverless). -## How to use it? +### Temporal Server + +Temporal Server is a cluster of internal services, a database of record, and a search database. It can be [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-typescript with-typescript-app +npx create-next-app --example with-temporal next-temporal-app # or -yarn create next-app --example with-typescript with-typescript-app +yarn create next-app --example with-temporal next-temporal-app ``` -Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). +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: -## Notes +In the Temporal Server docker directory: -This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. - -``` -npm install --save-dev typescript +```bash +docker-compose up ``` -To enable TypeScript's features, we install the type declarations for React and Node. +In the root Next.js directory: +```bash +npm run dev ``` -npm install --save-dev @types/react @types/react-dom @types/node -``` - -When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. -Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. +```bash +npm run build-worker.watch +``` -A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. +```bash +npm run start-worker +``` diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index 8b2469a3d874641..57ba59c422ac801 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -5,8 +5,8 @@ "build": "next build", "start": "next start", "type-check": "tsc", - "build-temporal": "tsc --build temporal/src/worker/tsconfig.json", - "build-temporal.watch": "tsc --build --watch temporal/src/worker/tsconfig.json", + "build-worker": "tsc --build temporal/src/worker/tsconfig.json", + "build-worker.watch": "tsc --build --watch temporal/src/worker/tsconfig.json", "start-worker": "node temporal/lib/worker" }, "dependencies": { From ca7393005611e794544438b15f2ae65952579f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Thu, 19 Aug 2021 20:11:01 -0400 Subject: [PATCH 08/20] Finish app and README --- examples/with-temporal/README.md | 49 +++++++++++++-- examples/with-temporal/components/Layout.tsx | 5 -- examples/with-temporal/package.json | 7 ++- examples/with-temporal/pages/about.tsx | 2 +- examples/with-temporal/pages/api/order.ts | 19 ------ .../with-temporal/pages/api/orders/index.ts | 29 +++++++++ .../with-temporal/pages/api/users/index.ts | 16 ----- examples/with-temporal/pages/index.tsx | 20 +++++- examples/with-temporal/pages/users/[id].tsx | 61 ------------------- examples/with-temporal/pages/users/index.tsx | 37 ----------- .../temporal/src/activities/greeter.ts | 3 - .../temporal/src/activities/inventory.ts | 28 +++++++++ .../temporal/src/activities/payment.ts | 17 ++++++ .../temporal/src/interfaces/workflows.ts | 6 +- .../temporal/src/worker/index.ts | 15 ++--- .../temporal/src/workflows/example.ts | 10 --- .../temporal/src/workflows/order.ts | 30 +++++++++ 17 files changed, 178 insertions(+), 176 deletions(-) delete mode 100644 examples/with-temporal/pages/api/order.ts create mode 100644 examples/with-temporal/pages/api/orders/index.ts delete mode 100644 examples/with-temporal/pages/api/users/index.ts delete mode 100644 examples/with-temporal/pages/users/[id].tsx delete mode 100644 examples/with-temporal/pages/users/index.tsx delete mode 100644 examples/with-temporal/temporal/src/activities/greeter.ts create mode 100644 examples/with-temporal/temporal/src/activities/inventory.ts create mode 100644 examples/with-temporal/temporal/src/activities/payment.ts delete mode 100644 examples/with-temporal/temporal/src/workflows/example.ts create mode 100644 examples/with-temporal/temporal/src/workflows/order.ts diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md index e0d51352a10cf39..46a0a3af01fd1ed 100644 --- a/examples/with-temporal/README.md +++ b/examples/with-temporal/README.md @@ -1,22 +1,59 @@ # Temporal Next.js example -[Temporal](https://temporal.io/) is a runtime for orchestrating microservices or serverless functions. TODO +> “Temporal does to backend and infra what React did to frontend. If you're in the React world, you've forgotten about manually adding and removing DOM elements, updating attributes and their quirks, hooking up event listeners… It's not only been a boost in developer experience, but most importantly in consistency and reliability. In the backend world, this reliability problem is absurdly amplified as monoliths break into SaaS services, functions, containers… You have to carefully manage and create queues to capture each side effect, ensure everything gets retried, state is scattered all over the place. Temporal's engine is quite complex, much like React's, but the surface exposed to the developer is a beautiful "render()" function to organize your backend workflows.” +> —[Guillermo Rauch](https://twitter.com/rauchg/status/1316808665370820609) + +**Table of Contents** + +- [Starter project](#starter-project) +- [Deploy your own](#deploy-your-own) + - [Web server](#web-server) + - [Worker](#worker) + - [Temporal Server](#temporal-server) +- [How to use](#how-to-use) + +## Starter project + +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 below, 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) +- Do something that takes longer than 5 or 30 seconds (Vercel's serverless function execution [time limit](https://vercel.com/docs/platform/limits) for Hobby or Enterprise accounts, respectively) +- Do something after a certain amount of time, like an hour or a month (or periodically—once an hour, or once a month) + +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 code is: `temporal/src/workflows/order.ts` and the Activites code is: `temporal/src/activities/{payment|inventory}.ts`. ## Deploy your own ### Web server -The web server and `pages/api/` serverless functions can be deployed using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): +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-typescript&project-name=with-typescript&repository-name=with-typescript) +[![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 FaaS/serverless). +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 [deployed](https://docs.temporal.io/docs/server/production-deployment) with a container orchestration service like Kubernetes or ECS. +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 @@ -36,7 +73,7 @@ In the Temporal Server docker directory: docker-compose up ``` -In the root Next.js directory: +In the `next-temporal-app/` directory: ```bash npm run dev diff --git a/examples/with-temporal/components/Layout.tsx b/examples/with-temporal/components/Layout.tsx index 8f111e1ccd2e639..11bee6fb589dd21 100644 --- a/examples/with-temporal/components/Layout.tsx +++ b/examples/with-temporal/components/Layout.tsx @@ -23,11 +23,6 @@ const Layout = ({ children, title = 'This is the default title' }: Props) => ( About {' '} - |{' '} - - Users List - {' '} - | Users API {children} diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index 57ba59c422ac801..86edf941dbdf4ce 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -7,7 +7,7 @@ "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": "node temporal/lib/worker" + "start-worker": "nodemon -w temporal/lib/ -x 'node temporal/lib/worker || (sleep 5; touch temporal/lib/worker/index.js)'" }, "dependencies": { "@babel/core": "^7.15.0", @@ -17,10 +17,11 @@ "temporalio": "^0.3.0" }, "devDependencies": { + "@tsconfig/node14": "^1.0.0", "@types/node": "^12.12.21", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", - "typescript": "^4.3.5", - "@tsconfig/node14": "^1.0.0" + "nodemon": "^2.0.12", + "typescript": "^4.3.5" } } diff --git a/examples/with-temporal/pages/about.tsx b/examples/with-temporal/pages/about.tsx index 6c80ded5bf6d87a..bbcb05858a2ee54 100644 --- a/examples/with-temporal/pages/about.tsx +++ b/examples/with-temporal/pages/about.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import Layout from '../components/Layout' const AboutPage: NextPage = () => ( - +

About

This is the about page

diff --git a/examples/with-temporal/pages/api/order.ts b/examples/with-temporal/pages/api/order.ts deleted file mode 100644 index 8d74326defe8889..000000000000000 --- a/examples/with-temporal/pages/api/order.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { Connection, WorkflowClient } from '@temporalio/client' -import { Example } from '../../temporal/src/interfaces/workflows' - -type Data = { - result: string -} - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const connection = new Connection() - const client = new WorkflowClient(connection.service) - const example = client.stub('example', { taskQueue: 'tutorial' }) - const result = await example.execute('Temporal') - console.log(result) // Hello, Temporal! - res.status(200).json({ result }) -} diff --git a/examples/with-temporal/pages/api/orders/index.ts b/examples/with-temporal/pages/api/orders/index.ts new file mode 100644 index 000000000000000..3227eda0691ce46 --- /dev/null +++ b/examples/with-temporal/pages/api/orders/index.ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { Connection, WorkflowClient } from '@temporalio/client' +import { Order } from '../../../temporal/src/interfaces/workflows' + +export type Data = { + result: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + res.send({ result: 'Error code 405: use POST' }) + return + } + + const userId: string = req.headers.authorization // FIXME insecure 😄 + 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', { taskQueue: 'orders' }) + + // Execute the Order workflow and wait for it to finish + const result = await example.execute(userId, itemId, quantity) + res.status(200).json({ result }) +} diff --git a/examples/with-temporal/pages/api/users/index.ts b/examples/with-temporal/pages/api/users/index.ts deleted file mode 100644 index 4efdba6f1a4e799..000000000000000 --- a/examples/with-temporal/pages/api/users/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { sampleUserData } from '../../../utils/sample-data' - -const handler = (_req: NextApiRequest, res: NextApiResponse) => { - try { - if (!Array.isArray(sampleUserData)) { - throw new Error('Cannot find user data') - } - - res.status(200).json(sampleUserData) - } catch (err) { - res.status(500).json({ statusCode: 500, message: err.message }) - } -} - -export default handler diff --git a/examples/with-temporal/pages/index.tsx b/examples/with-temporal/pages/index.tsx index 57f913a9b58818b..6826a581bb8d5ea 100644 --- a/examples/with-temporal/pages/index.tsx +++ b/examples/with-temporal/pages/index.tsx @@ -1,9 +1,27 @@ import Link from 'next/link' import Layout from '../components/Layout' +import { Data } from './api/orders' const IndexPage = () => ( - +

Hello Next.js 👋

+ + +

About diff --git a/examples/with-temporal/pages/users/[id].tsx b/examples/with-temporal/pages/users/[id].tsx deleted file mode 100644 index 61720da5a7a5dd3..000000000000000 --- a/examples/with-temporal/pages/users/[id].tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { GetStaticProps, GetStaticPaths } from 'next' - -import { User } from '../../interfaces' -import { sampleUserData } from '../../utils/sample-data' -import Layout from '../../components/Layout' -import ListDetail from '../../components/ListDetail' - -type Props = { - item?: User - errors?: string -} - -const StaticPropsDetail = ({ item, errors }: Props) => { - if (errors) { - return ( - -

- Error: {errors} -

-
- ) - } - - return ( - - {item && } - - ) -} - -export default StaticPropsDetail - -export const getStaticPaths: GetStaticPaths = async () => { - // Get the paths we want to pre-render based on users - const paths = sampleUserData.map((user) => ({ - params: { id: user.id.toString() }, - })) - - // We'll pre-render only these paths at build time. - // { fallback: false } means other routes should 404. - return { paths, fallback: false } -} - -// This function gets called at build time on server-side. -// It won't be called on client-side, so you can even do -// direct database queries. -export const getStaticProps: GetStaticProps = async ({ params }) => { - try { - const id = params?.id - const item = sampleUserData.find((data) => data.id === Number(id)) - // By returning { props: item }, the StaticPropsDetail component - // will receive `item` as a prop at build time - return { props: { item } } - } catch (err) { - return { props: { errors: err.message } } - } -} diff --git a/examples/with-temporal/pages/users/index.tsx b/examples/with-temporal/pages/users/index.tsx deleted file mode 100644 index 7b3ee883b3a6754..000000000000000 --- a/examples/with-temporal/pages/users/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { GetStaticProps } from 'next' -import Link from 'next/link' - -import { User } from '../../interfaces' -import { sampleUserData } from '../../utils/sample-data' -import Layout from '../../components/Layout' -import List from '../../components/List' - -type Props = { - items: User[] -} - -const WithStaticProps = ({ items }: Props) => ( - -

Users List

-

- Example fetching data from inside getStaticProps(). -

-

You are currently on: /users

- -

- - Go home - -

-
-) - -export const getStaticProps: GetStaticProps = async () => { - // Example for including static props in a Next.js function component page. - // Don't forget to include the respective types for any props passed into - // the component. - const items: User[] = sampleUserData - return { props: { items } } -} - -export default WithStaticProps diff --git a/examples/with-temporal/temporal/src/activities/greeter.ts b/examples/with-temporal/temporal/src/activities/greeter.ts deleted file mode 100644 index 28297c60cbe9821..000000000000000 --- a/examples/with-temporal/temporal/src/activities/greeter.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function greet(name: string): Promise { - return `Hello, ${name}!` -} diff --git a/examples/with-temporal/temporal/src/activities/inventory.ts b/examples/with-temporal/temporal/src/activities/inventory.ts new file mode 100644 index 000000000000000..e166bb5a5d7374b --- /dev/null +++ b/examples/with-temporal/temporal/src/activities/inventory.ts @@ -0,0 +1,28 @@ +export async function checkAndDecrementInventory( + itemId: string, + quantity: number +): Promise { + // 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 { + // TODO increment inventory: + // const result = await db.collection('items').updateOne( + // { _id: itemId }, + // { $inc: { numAvailable: quantity } } + // ) + // return result.modifiedCount === 1 + return true +} diff --git a/examples/with-temporal/temporal/src/activities/payment.ts b/examples/with-temporal/temporal/src/activities/payment.ts new file mode 100644 index 000000000000000..1872c26a67222fd --- /dev/null +++ b/examples/with-temporal/temporal/src/activities/payment.ts @@ -0,0 +1,17 @@ +export type ChargeResult = { + success: boolean + errorMessage?: string +} + +export async function chargeUser( + userId: string, + itemId: string, + quantity: number +): Promise { + // 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' } +} diff --git a/examples/with-temporal/temporal/src/interfaces/workflows.ts b/examples/with-temporal/temporal/src/interfaces/workflows.ts index c3a04470140e766..352522450455ac8 100644 --- a/examples/with-temporal/temporal/src/interfaces/workflows.ts +++ b/examples/with-temporal/temporal/src/interfaces/workflows.ts @@ -1,7 +1,5 @@ import { Workflow } from '@temporalio/workflow' -// Extend the generic Workflow interface to check that Example is a valid workflow interface -// Workflow interfaces are useful for generating type safe workflow clients -export interface Example extends Workflow { - main(name: string): Promise +export interface Order extends Workflow { + main(userId: string, itemId: string, quantity: number): Promise } diff --git a/examples/with-temporal/temporal/src/worker/index.ts b/examples/with-temporal/temporal/src/worker/index.ts index e67dfd6d260c046..98fced059a39eae 100644 --- a/examples/with-temporal/temporal/src/worker/index.ts +++ b/examples/with-temporal/temporal/src/worker/index.ts @@ -1,19 +1,14 @@ import { Worker } from '@temporalio/worker' async function run() { - // Automatically locate and register Activities and Workflows relative to __dirname - // (assuming package was bootstrapped with `npm init @temporalio`). - // Worker connects to localhost by default and uses console error for logging. - // Customize the Worker by passing more options to create(). - // create() tries to connect to the server and will throw if a connection could not be established. - // You may create multiple Workers in a single process in order to poll on multiple task queues. - // In order to configure the server connection parameters and other global options, - // use the Core.install() method to configure the Rust Core SDK singleton. + // https://docs.temporal.io/docs/node/workers/ const worker = await Worker.create({ workDir: __dirname, - taskQueue: 'tutorial', + nodeModulesPath: `${__dirname}/../../../node_modules`, + taskQueue: 'orders', }) - // Start accepting tasks on the `tutorial` queue + + // Start accepting tasks on the `orders` queue await worker.run() } diff --git a/examples/with-temporal/temporal/src/workflows/example.ts b/examples/with-temporal/temporal/src/workflows/example.ts deleted file mode 100644 index 792214721970068..000000000000000 --- a/examples/with-temporal/temporal/src/workflows/example.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Example } from '../interfaces/workflows' -import { greet } from '@activities/greeter' - -// A workflow that simply calls an activity -async function main(name: string): Promise { - return greet(name) -} - -// Declare the workflow's type to be checked by the Typescript compiler -export const workflow: Example = { main } diff --git a/examples/with-temporal/temporal/src/workflows/order.ts b/examples/with-temporal/temporal/src/workflows/order.ts new file mode 100644 index 000000000000000..37290cfb0365b3e --- /dev/null +++ b/examples/with-temporal/temporal/src/workflows/order.ts @@ -0,0 +1,30 @@ +import { Order } from '../interfaces/workflows' +import { + checkAndDecrementInventory, + incrementInventory, +} from '@activities/inventory' +import { chargeUser, ChargeResult } from '@activities/payment' + +async function main( + userId: string, + itemId: string, + quantity: number +): Promise { + const haveEnoughInventory: boolean = await checkAndDecrementInventory( + itemId, + quantity + ) + if (haveEnoughInventory) { + const result: ChargeResult = await chargeUser(userId, itemId, quantity) + if (result.success) { + return `Order successful!` + } else { + await incrementInventory(itemId, quantity) + return `Unable to complete payment. Error: ${result.errorMessage}` + } + } else { + return `Sorry, we don't have enough items in stock to fulfill your order.` + } +} + +export const workflow: Order = { main } From 3c6be372aeb269530a63d96c835060d85a64d94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Thu, 19 Aug 2021 20:18:31 -0400 Subject: [PATCH 09/20] Clarify README logic flow --- examples/with-temporal/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md index 46a0a3af01fd1ed..5d2f6b1d9307228 100644 --- a/examples/with-temporal/README.md +++ b/examples/with-temporal/README.md @@ -37,7 +37,12 @@ Reserving 2 of item B102 Charging user 123 for 2 of item B102 ``` -The Workflow code is: `temporal/src/workflows/order.ts` and the Activites code is: `temporal/src/activities/{payment|inventory}.ts`. +- 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` ## Deploy your own From 9fd452162ed88000a978d64877ff4775e2e827ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Fri, 20 Aug 2021 12:37:48 -0400 Subject: [PATCH 10/20] Use Node 16 --- examples/with-temporal/.nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-temporal/.nvmrc b/examples/with-temporal/.nvmrc index 8351c19397f4fcd..b6a7d89c68e0ca6 100644 --- a/examples/with-temporal/.nvmrc +++ b/examples/with-temporal/.nvmrc @@ -1 +1 @@ -14 +16 From c7e1b565c17b929fa032b7c99a943077cbf56a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=E2=98=BA=EF=B8=8F?= <251288+lorensr@users.noreply.github.com> Date: Fri, 20 Aug 2021 14:18:48 -0400 Subject: [PATCH 11/20] Simplify Temporal use cases Co-authored-by: Lee Robinson --- examples/with-temporal/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md index 5d2f6b1d9307228..98ef620fdd314e4 100644 --- a/examples/with-temporal/README.md +++ b/examples/with-temporal/README.md @@ -14,11 +14,11 @@ ## Starter project -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 below, we can greatly increase our code's fault tolerance by using Temporal: +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) -- Do something that takes longer than 5 or 30 seconds (Vercel's serverless function execution [time limit](https://vercel.com/docs/platform/limits) for Hobby or Enterprise accounts, respectively) -- Do something after a certain amount of time, like an hour or a month (or periodically—once an hour, or once a month) +- 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: From 0057b94e87622d74f5dd81bee09ee607be4f6751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=E2=98=BA=EF=B8=8F?= <251288+lorensr@users.noreply.github.com> Date: Fri, 20 Aug 2021 14:19:05 -0400 Subject: [PATCH 12/20] Remove Temporal quote and ToC Co-authored-by: Lee Robinson --- examples/with-temporal/README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/with-temporal/README.md b/examples/with-temporal/README.md index 98ef620fdd314e4..f324c9bf28d8190 100644 --- a/examples/with-temporal/README.md +++ b/examples/with-temporal/README.md @@ -1,18 +1,4 @@ -# Temporal Next.js example - -> “Temporal does to backend and infra what React did to frontend. If you're in the React world, you've forgotten about manually adding and removing DOM elements, updating attributes and their quirks, hooking up event listeners… It's not only been a boost in developer experience, but most importantly in consistency and reliability. In the backend world, this reliability problem is absurdly amplified as monoliths break into SaaS services, functions, containers… You have to carefully manage and create queues to capture each side effect, ensure everything gets retried, state is scattered all over the place. Temporal's engine is quite complex, much like React's, but the surface exposed to the developer is a beautiful "render()" function to organize your backend workflows.” -> —[Guillermo Rauch](https://twitter.com/rauchg/status/1316808665370820609) - -**Table of Contents** - -- [Starter project](#starter-project) -- [Deploy your own](#deploy-your-own) - - [Web server](#web-server) - - [Worker](#worker) - - [Temporal Server](#temporal-server) -- [How to use](#how-to-use) - -## Starter project +# 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: From c51f232512363d651d7f5a69138abe9fd6058e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=E2=98=BA=EF=B8=8F?= <251288+lorensr@users.noreply.github.com> Date: Fri, 20 Aug 2021 14:24:23 -0400 Subject: [PATCH 13/20] Remove unnecessary meta tags from Layout Co-authored-by: Lee Robinson --- examples/with-temporal/components/Layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/with-temporal/components/Layout.tsx b/examples/with-temporal/components/Layout.tsx index 11bee6fb589dd21..8645161767c9abc 100644 --- a/examples/with-temporal/components/Layout.tsx +++ b/examples/with-temporal/components/Layout.tsx @@ -11,8 +11,6 @@ const Layout = ({ children, title = 'This is the default title' }: Props) => (
{title} - -