From fdbbb3dc451792cc6249c0dd3d1069f100d8d822 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 21 Oct 2024 21:37:58 +0100 Subject: [PATCH 01/10] WIP realtime/frontend docs --- docs/frontend/overview.mdx | 7 + docs/frontend/react-hooks.mdx | 99 +++++++ docs/mint.json | 77 +++++- docs/tasks/schemaTask.mdx | 261 ++++++++++++++++++ pnpm-lock.yaml | 127 +++++++++ references/v3-catalog/package.json | 8 + .../v3-catalog/src/trigger/taskTypes.ts | 145 +++++++++- 7 files changed, 701 insertions(+), 23 deletions(-) create mode 100644 docs/frontend/overview.mdx create mode 100644 docs/frontend/react-hooks.mdx create mode 100644 docs/tasks/schemaTask.mdx diff --git a/docs/frontend/overview.mdx b/docs/frontend/overview.mdx new file mode 100644 index 0000000000..058069858e --- /dev/null +++ b/docs/frontend/overview.mdx @@ -0,0 +1,7 @@ +--- +title: Overview & Authentication +sidebarTitle: Overview & Auth +description: Using the Trigger.dev v3 API from your frontend application. +--- + +Install the preview package: `0.0.0-realtime-20241021152316`. diff --git a/docs/frontend/react-hooks.mdx b/docs/frontend/react-hooks.mdx new file mode 100644 index 0000000000..94819e327d --- /dev/null +++ b/docs/frontend/react-hooks.mdx @@ -0,0 +1,99 @@ +--- +title: React hooks +sidebarTitle: React hooks +description: Using the Trigger.dev v3 API from your React application. +--- + +## Installation + +Install the preview package: `0.0.0-realtime-20241021152316`. + +```bash +npm install @trigger.dev/react-hooks@0.0.0-realtime-20241021152316 +``` + +## Authentication + +You need to use the `TriggerAuthContext` provider to authenticate your requests. + +```tsx +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +const App = () => { + return ( + + + + ); +}; +``` + +## Usage + +### useRun + +The `useRun` hook allows you to fetch a run by its ID. + +```tsx +import { useRun } from "@trigger.dev/react-hooks"; + +const MyComponent = ({ runId }) => { + const { data, error, isLoading } = useRun(runId); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Run: {data.id}
; +}; +``` + +### useRealtimeRun + +The `useRealtimeRun` hook allows you to subscribe to a run by its ID. + +```tsx +import { useRealtimeRun } from "@trigger.dev/react-hooks"; + +const MyComponent = ({ runId }) => { + const { data, error, isLoading } = useRealtimeRun(runId); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Run: {data.id}
; +}; +``` + +### useRealtimeRunsWithTag + +The `useRealtimeRunsWithTag` hook allows you to subscribe to runs with a specific tag. + +```tsx +import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks"; + +const MyComponent = ({ tag }) => { + const { data, error, isLoading } = useRealtimeRunsWithTag(tag); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Runs: {data.map((run) => run.id).join(", ")}
; +}; +``` + +### useRealtimeBatch + +The `useRealtimeBatch` hook allows you to subscribe to a batch by its ID. + +```tsx +import { useRealtimeBatch } from "@trigger.dev/react-hooks"; + +const MyComponent = ({ batchId }) => { + const { data, error, isLoading } = useRealtimeBatch(batchId); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Batch: {data.id}
; +}; +``` diff --git a/docs/mint.json b/docs/mint.json index edb804ec66..187c4f9dc7 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1,7 +1,10 @@ { "$schema": "https://mintlify.com/schema.json", "name": "Trigger.dev", - "openapi": ["/openapi.yml", "/v3-openapi.yaml"], + "openapi": [ + "/openapi.yml", + "/v3-openapi.yaml" + ], "api": { "playground": { "mode": "simple" @@ -111,27 +114,43 @@ "navigation": [ { "group": "Getting Started", - "pages": ["introduction", "quick-start", "how-it-works", "upgrading-beta", "limits"] + "pages": [ + "introduction", + "quick-start", + "how-it-works", + "upgrading-beta", + "limits" + ] }, { "group": "Fundamentals", "pages": [ { "group": "Tasks", - "pages": ["tasks/overview", "tasks/scheduled"] + "pages": [ + "tasks/overview", + "tasks/schemaTask", + "tasks/scheduled" + ] }, "triggering", "runs-and-attempts", "apikeys", { "group": "Configuration", - "pages": ["config/config-file", "config/extensions/overview"] + "pages": [ + "config/config-file", + "config/extensions/overview" + ] } ] }, { "group": "Development", - "pages": ["cli-dev", "run-tests"] + "pages": [ + "cli-dev", + "run-tests" + ] }, { "group": "Deployment", @@ -141,7 +160,9 @@ "github-actions", { "group": "Deployment integrations", - "pages": ["vercel-integration"] + "pages": [ + "vercel-integration" + ] } ] }, @@ -153,7 +174,13 @@ "errors-retrying", { "group": "Wait", - "pages": ["wait", "wait-for", "wait-until", "wait-for-event", "wait-for-request"] + "pages": [ + "wait", + "wait-for", + "wait-until", + "wait-for-event", + "wait-for-request" + ] }, "queue-concurrency", "versioning", @@ -167,13 +194,23 @@ "context" ] }, + { + "group": "Frontend usage", + "pages": [ + "frontend/overview", + "frontend/react-hooks" + ] + }, { "group": "API reference", "pages": [ "management/overview", { "group": "Tasks API", - "pages": ["management/tasks/trigger", "management/tasks/batch-trigger"] + "pages": [ + "management/tasks/trigger", + "management/tasks/batch-trigger" + ] }, { "group": "Runs API", @@ -212,7 +249,9 @@ }, { "group": "Projects API", - "pages": ["management/projects/runs"] + "pages": [ + "management/projects/runs" + ] } ] }, @@ -258,11 +297,17 @@ }, { "group": "Help", - "pages": ["community", "help-slack", "help-email"] + "pages": [ + "community", + "help-slack", + "help-email" + ] }, { "group": "", - "pages": ["guides/introduction"] + "pages": [ + "guides/introduction" + ] }, { "group": "Frameworks", @@ -322,11 +367,15 @@ }, { "group": "Dashboard", - "pages": ["guides/dashboard/creating-a-project"] + "pages": [ + "guides/dashboard/creating-a-project" + ] }, { "group": "Migrations", - "pages": ["guides/use-cases/upgrading-from-v2"] + "pages": [ + "guides/use-cases/upgrading-from-v2" + ] } ], "footerSocials": { @@ -334,4 +383,4 @@ "github": "https://github.com/triggerdotdev", "linkedin": "https://www.linkedin.com/company/triggerdotdev" } -} +} \ No newline at end of file diff --git a/docs/tasks/schemaTask.mdx b/docs/tasks/schemaTask.mdx new file mode 100644 index 0000000000..d87cf39875 --- /dev/null +++ b/docs/tasks/schemaTask.mdx @@ -0,0 +1,261 @@ +--- +title: "schemaTask" +sidebarTitle: "Schema task" +description: "Define tasks with a runtime payload schema and validate the payload before running the task." +--- + +The `schemaTask` function allows you to define a task with a runtime payload schema. This schema is used to validate the payload before running the task or when triggering a task directly. If the payload does not match the schema, the task will not execute. + +## Usage + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +const myTask = schemaTask({ + id: "my-task", + schema: z.object({ + name: z.string(), + age: z.number(), + }), + run: async (payload) => { + console.log(payload.name, payload.age); + }, +}); +``` + +`schemaTask` takes all the same options as [task](/tasks/overview), with the addition of a `schema` field. The `schema` field is a schema parser function from a schema library or or a custom parser function. + + + We will probably eventually combine `task` and `schemaTask` into a single function, but because + that would be a breaking change, we are keeping them separate for now. + + +When you trigger the task directly, the payload will be validated against the schema before the [run](/runs-and-attempts) is created: + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; +import { myTask } from "./trigger/myTasks"; + +// This will call the schema parser function and validate the payload +await myTask.trigger({ name: "Alice", age: "oops" }); // this will throw an error + +// This will NOT call the schema parser function +await tasks.trigger("my-task", { name: "Alice", age: "oops" }); // this will not throw an error +``` + +The error thrown when the payload does not match the schema will be the same as the error thrown by the schema parser function. For example, if you are using Zod, the error will be a `ZodError`. + +We will also validate the payload every time before the task is run, so you can be sure that the payload is always valid. In the example above, the task would fail with a `TaskPayloadParsedError` error and skip retrying if the payload does not match the schema. + +## Input/output schemas + +Certain schema libraries, like Zod, split their type inference into "schema in" and "schema out". This means that you can define a single schema that will produce different types when triggering the task and when running the task. For example, you can define a schema that has a default value for a field, or a string coerced into a date: + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +const myTask = schemaTask({ + id: "my-task", + schema: z.object({ + name: z.string().default("John"), + age: z.number(), + dob: z.coerce.date(), + }), + run: async (payload) => { + console.log(payload.name, payload.age); + }, +}); +``` + +In this case, the trigger payload type is `{ name?: string, age: number; dob: string }`, but the run payload type is `{ name: string, age: number; dob: Date }`. So you can trigger the task with a payload like this: + +```ts +await myTask.trigger({ age: 30, dob: "2020-01-01" }); // this is valid +await myTask.trigger({ name: "Alice", age: 30, dob: "2020-01-01" }); // this is also valid +``` + +## Supported schema types + +### Zod + +You can use the [Zod](https://zod.dev) schema library to define your schema. The schema will be validated using Zod's `parse` function. + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +export const zodTask = schemaTask({ + id: "types/zod", + schema: z.object({ + bar: z.string(), + baz: z.string().default("foo"), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### Yup + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import * as yup from "yup"; + +export const yupTask = schemaTask({ + id: "types/yup", + schema: yup.object({ + bar: yup.string().required(), + baz: yup.string().default("foo"), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### Superstruct + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { object, string } from "superstruct"; + +export const superstructTask = schemaTask({ + id: "types/superstruct", + schema: object({ + bar: string(), + baz: string(), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### ArkType + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { type } from "arktype"; + +export const arktypeTask = schemaTask({ + id: "types/arktype", + schema: type({ + bar: "string", + baz: "string", + }).assert, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### @effect/schema + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import * as Schema from "@effect/schema/Schema"; + +// For some funny typescript reason, you cannot pass the Schema.decodeUnknownSync directly to schemaTask +const effectSchemaParser = Schema.decodeUnknownSync( + Schema.Struct({ bar: Schema.String, baz: Schema.String }) +); + +export const effectTask = schemaTask({ + id: "types/effect", + schema: effectSchemaParser, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### runtypes + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import * as T from "runtypes"; + +export const runtypesTask = schemaTask({ + id: "types/runtypes", + schema: T.Record({ + bar: T.String, + baz: T.String, + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### valibot + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; + +import * as v from "valibot"; + +// For some funny typescript reason, you cannot pass the v.parser directly to schemaTask +const valibotParser = v.parser( + v.object({ + bar: v.string(), + baz: v.string(), + }) +); + +export const valibotTask = schemaTask({ + id: "types/valibot", + schema: valibotParser, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### typebox + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { Type } from "@sinclair/typebox"; +import { wrap } from "@typeschema/typebox"; + +export const typeboxTask = schemaTask({ + id: "types/typebox", + schema: wrap( + Type.Object({ + bar: Type.String(), + baz: Type.String(), + }) + ), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` + +### Custom parser function + +You can also define a custom parser function that will be called with the payload before the task is run. The parser function should return the parsed payload or throw an error if the payload is invalid. + +```ts +import { schemaTask } from "@trigger.dev/sdk/v3"; + +export const customParserTask = schemaTask({ + id: "types/custom-parser", + schema: (data: unknown) => { + // This is a custom parser, and should do actual parsing (not just casting) + if (typeof data !== "object") { + throw new Error("Invalid data"); + } + + const { bar, baz } = data as { bar: string; baz: string }; + + return { bar, baz }; + }, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); +``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cea754f9fb..c07b073139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1638,6 +1638,9 @@ importers: references/v3-catalog: dependencies: + '@effect/schema': + specifier: ^0.75.5 + version: 0.75.5(effect@3.9.2) '@infisical/sdk': specifier: ^2.1.9 version: 2.3.5 @@ -1656,6 +1659,9 @@ importers: '@sentry/esbuild-plugin': specifier: ^2.22.2 version: 2.22.2 + '@sinclair/typebox': + specifier: ^0.33.17 + version: 0.33.17 '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -1671,9 +1677,15 @@ importers: '@trigger.dev/sdk': specifier: workspace:* version: link:../../packages/trigger-sdk + '@typeschema/typebox': + specifier: ^0.14.0 + version: 0.14.0(@sinclair/typebox@0.33.17) ai: specifier: ^3.3.24 version: 3.3.24(openai@4.56.0)(react@19.0.0-rc.0)(svelte@4.2.19)(vue@3.4.38)(zod@3.22.3) + arktype: + specifier: 2.0.0-rc.17 + version: 2.0.0-rc.17 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -1713,21 +1725,33 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.14 + runtypes: + specifier: ^6.7.0 + version: 6.7.0 server-only: specifier: ^0.0.1 version: 0.0.1 stripe: specifier: ^12.14.0 version: 12.18.0 + superstruct: + specifier: ^2.0.2 + version: 2.0.2 typeorm: specifier: ^0.3.20 version: 0.3.20(pg@8.11.5)(ts-node@10.9.2) + valibot: + specifier: ^0.42.1 + version: 0.42.1(typescript@5.5.4) wrangler: specifier: 3.70.0 version: 3.70.0 yt-dlp-wrap: specifier: ^2.3.12 version: 2.3.12 + yup: + specifier: ^1.4.0 + version: 1.4.0 zod: specifier: 3.22.3 version: 3.22.3 @@ -2010,6 +2034,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@ark/schema@0.19.0: + resolution: {integrity: sha512-m+NLBrfewxH1IieXxK7i2cjJtMksZm2PlobbZDcsNDpYKjuDAzWimXZb+GaxawKMBV9rhO3XY2Lnvf2TLq+JTQ==} + dependencies: + '@ark/util': 0.18.0 + dev: false + + /@ark/util@0.18.0: + resolution: {integrity: sha512-TpHY532LKQwwYHui5NN/eO/6eSiSMvf652YNt1BsV7fya7RzXL27IiU9x4bm7jTFZxLQGYDQTB7nw41TqeuF4g==} + dev: false + /@aws-crypto/crc32@3.0.0: resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} dependencies: @@ -4773,6 +4807,15 @@ packages: fast-check: 3.22.0 dev: false + /@effect/schema@0.75.5(effect@3.9.2): + resolution: {integrity: sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw==} + peerDependencies: + effect: ^3.9.2 + dependencies: + effect: 3.9.2 + fast-check: 3.22.0 + dev: false + /@electric-sql/client@0.4.0: resolution: {integrity: sha512-YVYSqHitqVIDC1RBTfmHMfAfqDNAKMK9/AFVTDFQQxN3Q85dIQS49zThAuJVecYiuYRJvTiqf40c4n39jZSNrQ==} optionalDependencies: @@ -13897,6 +13940,10 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@sinclair/typebox@0.33.17: + resolution: {integrity: sha512-75232GRx3wp3P7NP+yc4nRK3XUAnaQShxTAzapgmQrgs0QvSq0/mOJGoZXRpH15cFCKyys+4laCPbBselqJ5Ag==} + dev: false + /@sindresorhus/is@0.14.0: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} @@ -15440,6 +15487,29 @@ packages: dev: false optional: true + /@typeschema/core@0.14.0: + resolution: {integrity: sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==} + peerDependencies: + '@types/json-schema': ^7.0.15 + peerDependenciesMeta: + '@types/json-schema': + optional: true + dev: false + + /@typeschema/typebox@0.14.0(@sinclair/typebox@0.33.17): + resolution: {integrity: sha512-+Td4CHkWQ17T60gEtA2SzeFp382CHEwsI7aWiqBq9YeqAwbkTrluGh6R9MNFHJzOLaYL+AG60b8fX9Rbcex0Tg==} + peerDependencies: + '@sinclair/typebox': ^0.33.7 + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + dependencies: + '@sinclair/typebox': 0.33.17 + '@typeschema/core': 0.14.0 + transitivePeerDependencies: + - '@types/json-schema' + dev: false + /@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.2.2): resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -16525,6 +16595,13 @@ packages: dependencies: dequal: 2.0.3 + /arktype@2.0.0-rc.17: + resolution: {integrity: sha512-1m1VG9ZGcGx8OIbeA4ghw8n1QVpu7MYcel3My2Tob17mMBaLy6+M116RRwx9GvaCyGpHhgu1RK5XfhP4wX17ug==} + dependencies: + '@ark/schema': 0.19.0 + '@ark/util': 0.18.0 + dev: false + /array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -18815,6 +18892,10 @@ packages: resolution: {integrity: sha512-pV7l1+LSZFvVObj4zuy4nYiBaC7qZOfrKV6s/Ef4p3KueiQwZFgamazklwyZ+x7Nyj2etRDFvHE/xkThTfQD1w==} dev: false + /effect@3.9.2: + resolution: {integrity: sha512-1sx/v1HTWHTodXfzWxAFg+SCF+ACgpJVruaAMIh/NmDVvrUsf0x9PzpXvkgJUbQ1fMdmKYK//FqxeHSQ+Zxv/Q==} + dev: false + /electron-to-chromium@1.4.433: resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==} dev: true @@ -25496,6 +25577,10 @@ packages: mkdirp: 1.0.4 dev: true + /property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + dev: false + /property-information@6.2.0: resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} dev: true @@ -26953,6 +27038,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /runtypes@6.7.0: + resolution: {integrity: sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA==} + dev: false + /rusha@0.8.14: resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} dev: false @@ -27945,6 +28034,11 @@ packages: copy-anything: 3.0.5 dev: false + /superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + dev: false + /supertest@7.0.0: resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} engines: {node: '>=14.18.0'} @@ -28444,6 +28538,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false + /tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + dev: false + /tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: @@ -28574,6 +28672,10 @@ packages: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} dev: true + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /tough-cookie@2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} @@ -28993,6 +29095,11 @@ packages: engines: {node: '>=10'} dev: false + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + /type-fest@4.10.3: resolution: {integrity: sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==} engines: {node: '>=16'} @@ -29605,6 +29712,17 @@ packages: engines: {node: '>=0.10.0'} dev: false + /valibot@0.42.1(typescript@5.5.4): + resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.5.4 + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -30832,6 +30950,15 @@ packages: resolution: {integrity: sha512-P8fJ+6M1YjukyJENCTviNLiZ8mokxprR54ho3DsSKPWDcac489OjRiStGEARJr6un6ETS6goTn4CWl/b/rM3aA==} dev: false + /yup@1.4.0: + resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==} + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + dev: false + /zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} diff --git a/references/v3-catalog/package.json b/references/v3-catalog/package.json index ceedc89f3f..3e9896329d 100644 --- a/references/v3-catalog/package.json +++ b/references/v3-catalog/package.json @@ -16,18 +16,22 @@ "generate:prisma": "prisma generate --sql" }, "dependencies": { + "@effect/schema": "^0.75.5", "@infisical/sdk": "^2.1.9", "@opentelemetry/api": "1.4.1", "@prisma/client": "5.19.0", "@react-email/components": "0.0.24", "@react-email/render": "1.0.1", "@sentry/esbuild-plugin": "^2.22.2", + "@sinclair/typebox": "^0.33.17", "@sindresorhus/slugify": "^2.2.1", "@t3-oss/env-core": "^0.11.0", "@t3-oss/env-nextjs": "^0.10.1", "@traceloop/instrumentation-openai": "^0.10.0", "@trigger.dev/sdk": "workspace:*", + "@typeschema/typebox": "^0.14.0", "ai": "^3.3.24", + "arktype": "2.0.0-rc.17", "dotenv": "^16.4.5", "email-reply-parser": "^1.8.0", "execa": "^8.0.1", @@ -41,11 +45,15 @@ "react": "19.0.0-rc.0", "react-email": "^3.0.1", "reflect-metadata": "^0.1.13", + "runtypes": "^6.7.0", "server-only": "^0.0.1", "stripe": "^12.14.0", + "superstruct": "^2.0.2", "typeorm": "^0.3.20", + "valibot": "^0.42.1", "wrangler": "3.70.0", "yt-dlp-wrap": "^2.3.12", + "yup": "^1.4.0", "zod": "3.22.3" }, "devDependencies": { diff --git a/references/v3-catalog/src/trigger/taskTypes.ts b/references/v3-catalog/src/trigger/taskTypes.ts index 28aafa068d..06e0104a55 100644 --- a/references/v3-catalog/src/trigger/taskTypes.ts +++ b/references/v3-catalog/src/trigger/taskTypes.ts @@ -1,4 +1,4 @@ -import { task, schemaTask } from "@trigger.dev/sdk/v3"; +import { task, schemaTask, type TaskPayload } from "@trigger.dev/sdk/v3"; import { z } from "zod"; export const task1 = task({ @@ -8,16 +8,143 @@ export const task1 = task({ }, }); -const Task2Payload = z.object({ - bar: z.string(), +export const zodTask = schemaTask({ + id: "types/zod", + schema: z.object({ + bar: z.string(), + baz: z.string().default("foo"), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type ZodPayload = TaskPayload; + +import * as yup from "yup"; + +export const yupTask = schemaTask({ + id: "types/yup", + schema: yup.object({ + bar: yup.string().required(), + baz: yup.string().default("foo"), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type YupPayload = TaskPayload; + +import { object, string } from "superstruct"; + +export const superstructTask = schemaTask({ + id: "types/superstruct", + schema: object({ + bar: string(), + baz: string(), + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type SuperstructPayload = TaskPayload; + +import { type } from "arktype"; + +export const arktypeTask = schemaTask({ + id: "types/arktype", + schema: type({ + bar: "string", + baz: "string", + }).assert, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type ArktypePayload = TaskPayload; + +import * as Schema from "@effect/schema/Schema"; + +const effectSchemaParser = Schema.decodeUnknownSync( + Schema.Struct({ bar: Schema.String, baz: Schema.String }) +); + +export const effectTask = schemaTask({ + id: "types/effect", + schema: effectSchemaParser, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type EffectPayload = TaskPayload; + +import * as T from "runtypes"; + +export const runtypesTask = schemaTask({ + id: "types/runtypes", + schema: T.Record({ + bar: T.String, + baz: T.String, + }), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +type RuntypesPayload = TaskPayload; + +import * as v from "valibot"; + +const valibotParser = v.parser( + v.object({ + bar: v.string(), + baz: v.string(), + }) +); + +export const valibotTask = schemaTask({ + id: "types/valibot", + schema: valibotParser, + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, +}); + +import { Type } from "@sinclair/typebox"; +import { wrap } from "@typeschema/typebox"; + +export const typeboxTask = schemaTask({ + id: "types/typebox", + schema: wrap( + Type.Object({ + bar: Type.String(), + baz: Type.String(), + }) + ), + run: async (payload) => { + console.log(payload.bar, payload.baz); + }, }); -export const task2 = schemaTask({ - id: "types/task-2", - schema: Task2Payload, - run: async (payload, { ctx }) => { - console.log(ctx.run.idempotencyKey); +export const customParserTask = schemaTask({ + id: "types/custom-parser", + schema: (data: unknown) => { + // This is a custom parser, and should do actual parsing (not just casting) + if (typeof data !== "object") { + throw new Error("Invalid data"); + } + + const { bar, baz } = data as { bar: string; baz: string }; - return { goodbye: "world" as const }; + return { bar, baz }; + }, + run: async (payload) => { + console.log(payload.bar, payload.baz); }, }); + +type CustomParserPayload = TaskPayload; From 8ccba7a8e204cb869e3cb6b9ce14591675a52ee6 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 22 Oct 2024 22:21:37 +0100 Subject: [PATCH 02/10] WIP --- docs/frontend/overview.mdx | 239 ++++++++++++++++++++++- docs/management/overview.mdx | 9 +- references/v3-catalog/src/clientUsage.ts | 16 +- 3 files changed, 256 insertions(+), 8 deletions(-) diff --git a/docs/frontend/overview.mdx b/docs/frontend/overview.mdx index 058069858e..5819f23699 100644 --- a/docs/frontend/overview.mdx +++ b/docs/frontend/overview.mdx @@ -1,7 +1,244 @@ --- title: Overview & Authentication sidebarTitle: Overview & Auth -description: Using the Trigger.dev v3 API from your frontend application. +description: Using the Trigger.dev SDK from your frontend application. --- Install the preview package: `0.0.0-realtime-20241021152316`. + +You can use certain SDK functions in your frontend application to interact with the Trigger.dev API. This guide will show you how to authenticate your requests and use the SDK in your frontend application. + +## Authentication + +You must authenticate your requests using a "Public Access Token" when using the SDK in your frontend application. To create a Public Access Token, you can use the `auth.createPublicToken` function in your backend code: + +```tsx +const publicToken = await auth.createPublicToken(); +``` + +To use a Public Access Token in your frontend application, you can call the `auth.configure` function or the `auth.withAuth` function: + +```ts +import { auth } from "@trigger.dev/sdk/v3"; + +auth.configure({ + accessToken: publicToken, +}); + +// or +await auth.withAuth({ accessToken: publicToken }, async () => { + // Your code here will use the public token +}); +``` + +### Scopes + +By default a Public Access Token has limited permissions. You can specify the scopes you need when creating a Public Access Token: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + runs: true, + }, + }, +}); +``` + +This will allow the token to read all runs, which is probably not what you want. You can specify only certain runs by passing an array of run IDs: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + runs: ["run_1234", "run_5678"], + }, + }, +}); +``` + +You can scope the token to only read certain tasks: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + tasks: ["my-task-1", "my-task-2"], + }, + }, +}); +``` + +Or tags: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + tags: ["my-tag-1", "my-tag-2"], + }, + }, +}); +``` + +Or a specific batch of runs: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + batch: "batch_1234", + }, + }, +}); +``` + +You can also combine scopes. For example, to read only certain tasks and tags: + +```ts +const publicToken = await auth.createPublicToken({ + scopes: { + read: { + tasks: ["my-task-1", "my-task-2"], + tags: ["my-tag-1", "my-tag-2"], + }, + }, +}); +``` + +### Expiration + +By default, Public Access Token's expire after 15 minutes. You can specify a different expiration time when creating a Public Access Token: + +```ts +const publicToken = await auth.createPublicToken({ + expirationTime: "1hr", +}); +``` + +- If `expirationTime` is a string, it will be treated as a time span +- If `expirationTime` is a number, it will be treated as a Unix timestamp +- If `expirationTime` is a `Date`, it will be treated as a date + +The format used for a time span is the same as the [jose package](https://github.com/panva/jose), which is a number followed by a unit. Valid units are: "sec", "secs", "second", "seconds", "s", "minute", "minutes", "min", "mins", "m", "hour", "hours", "hr", "hrs", "h", "day", "days", "d", "week", "weeks", "w", "year", "years", "yr", "yrs", and "y". It is not possible to specify months. 365.25 days is used as an alias for a year. If the string is suffixed with "ago", or prefixed with a "-", the resulting time span gets subtracted from the current unix timestamp. A "from now" suffix can also be used for readability when adding to the current unix timestamp. + +## Auto-generated tokens + +When triggering a task from your backend, the `handle` received from the `trigger` function now includes a `publicAccessToken` field. This token can be used to authenticate requests in your frontend application: + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; + +const handle = await tasks.trigger("my-task", { some: "data" }); + +console.log(handle.publicAccessToken); +``` + +By default, tokens returned from the `trigger` function expire after 15 minutes and have a read scope for that specific run, and any tags associated with it. You can customize the expiration of the auto-generated tokens by passing a `publicTokenOptions` object to the `trigger` function: + +```ts +const handle = await tasks.trigger( + "my-task", + { some: "data" }, + { + tags: ["my-tag"], + }, + { + publicAccessToken: { + expirationTime: "1hr", + }, + } +); +``` + +You will also get back a Public Access Token when using the `batchTrigger` function: + +```ts +import { tasks } from "@trigger.dev/sdk/v3"; + +const handle = await tasks.batchTrigger("my-task", [ + { payload: { some: "data" } }, + { payload: { some: "data" } }, + { payload: { some: "data" } }, +]); + +console.log(handle.publicAccessToken); +``` + +## Available SDK functions + +Currently the following functions are available in the frontend SDK: + +### runs.retrieve + +The `runs.retrieve` function allows you to retrieve a run by its ID. + +```ts +import { runs, auth } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +const handle = await tasks.trigger("my-task", { some: "data" }); + +// In your frontend code +auth.configure({ + accessToken: handle.publicAccessToken, +}); + +const run = await runs.retrieve(handle.id); +``` + +Learn more about the `runs.retrieve` function in the [runs.retrieve doc](/management/runs/retrieve). + +### runs.subscribeToRun + +The `runs.subscribeToRun` function allows you to subscribe to a run by its ID, and receive updates in real-time when the run changes. + +```ts +import { runs, auth } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +const handle = await tasks.trigger("my-task", { some: "data" }); + +// In your frontend code +auth.configure({ + accessToken: handle.publicAccessToken, +}); + +for await (const run of runs.subscribeToRun(handle.id)) { + // This will log the run every time it changes + console.log(run); +} +``` + +See the [Realtime doc](/realtime) for more information. + +### runs.subscribeToRunsWithTag + +The `runs.subscribeToRunsWithTag` function allows you to subscribe to runs with a specific tag, and receive updates in real-time when the runs change. + +```ts +import { runs, auth } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +const handle = await tasks.trigger("my-task", { some: "data" }, { tags: ["my-tag"] }); + +// In your frontend code +auth.configure({ + accessToken: handle.publicAccessToken, +}); + +for await (const run of runs.subscribeToRunsWithTag("my-tag")) { + // This will log the run every time it changes + console.log(run); +} +``` + +See the [Realtime doc](/realtime) for more information. + +## React hooks + +We also provide React hooks to make it easier to use the SDK in your React application. See our [React hooks](/frontend/react-hooks) documentation for more information. + +## Triggering tasks + +We don't currently support triggering tasks from the frontend SDK. If this is something you need, please let us know by [upvoting the feature](https://feedback.trigger.dev/p/ability-to-trigger-tasks-from-frontend). diff --git a/docs/management/overview.mdx b/docs/management/overview.mdx index 433463a362..d4a8d3b60d 100644 --- a/docs/management/overview.mdx +++ b/docs/management/overview.mdx @@ -50,10 +50,11 @@ main().catch(console.error); There are two methods of authenticating with the management API: using a secret key associated with a specific environment in a project (`secretKey`), or using a personal access token (`personalAccessToken`). Both methods should only be used in a backend server, as they provide full access to the project. - - Support for client-side authentication is coming soon to v3 but is not available at the time of - writing. - + + There is a separate authentication strategy when making requests from your frontend application. + See the [Frontend guide](/frontend/overview) for more information. This guide is for backend usage + only. + Certain API functions work with both authentication methods, but require different arguments depending on the method used. For example, the `runs.list` function can be called using either a `secretKey` or a `personalAccessToken`, but the `projectRef` argument is required when using a `personalAccessToken`: diff --git a/references/v3-catalog/src/clientUsage.ts b/references/v3-catalog/src/clientUsage.ts index 936583524a..9cb0b38e1b 100644 --- a/references/v3-catalog/src/clientUsage.ts +++ b/references/v3-catalog/src/clientUsage.ts @@ -1,5 +1,5 @@ import { auth, runs, tasks } from "@trigger.dev/sdk/v3"; -import type { task1, task2 } from "./trigger/taskTypes.js"; +import type { task1, zodTask } from "./trigger/taskTypes.js"; import { randomUUID } from "crypto"; async function main() { @@ -22,8 +22,18 @@ async function main() { console.log("Auto JWT", anyHandle.publicAccessToken); + const publicToken = await auth.createPublicToken({ + scopes: { + read: { + runs: true, + }, + }, + }); + await auth.withAuth({ accessToken: anyHandle.publicAccessToken }, async () => { - const subscription = runs.subscribeToRunsWithTag(`user:${userId}`); + const subscription = runs.subscribeToRunsWithTag( + `user:${userId}` + ); for await (const run of subscription) { switch (run.taskIdentifier) { @@ -33,7 +43,7 @@ async function main() { console.log("Payload:", run.payload); break; } - case "types/task-2": { + case "types/zod": { console.log("Run update:", run); console.log("Output:", run.output); console.log("Payload:", run.payload); From a710af3210b39c60b4e74ddaf6360d6f11b8cde4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 24 Oct 2024 11:27:01 +0100 Subject: [PATCH 03/10] Deleted old examples --- references/nextjs-realtime/src/app/page.tsx | 2 +- references/prisma-catalog/package.json | 18 ------------- .../migration.sql | 20 -------------- .../prisma/migrations/migration_lock.toml | 3 --- .../prisma-catalog/prisma/schema.prisma | 26 ------------------- .../prisma/sql/getUsersWithPosts.sql | 10 ------- references/prisma-catalog/src/db.ts | 6 ----- .../prisma-catalog/src/trigger/dbTasks.ts | 21 --------------- references/prisma-catalog/trigger.config.ts | 26 ------------------- references/prisma-catalog/tsconfig.json | 15 ----------- 10 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 references/prisma-catalog/package.json delete mode 100644 references/prisma-catalog/prisma/migrations/20240919122925_add_initial_schema/migration.sql delete mode 100644 references/prisma-catalog/prisma/migrations/migration_lock.toml delete mode 100644 references/prisma-catalog/prisma/schema.prisma delete mode 100644 references/prisma-catalog/prisma/sql/getUsersWithPosts.sql delete mode 100644 references/prisma-catalog/src/db.ts delete mode 100644 references/prisma-catalog/src/trigger/dbTasks.ts delete mode 100644 references/prisma-catalog/trigger.config.ts delete mode 100644 references/prisma-catalog/tsconfig.json diff --git a/references/nextjs-realtime/src/app/page.tsx b/references/nextjs-realtime/src/app/page.tsx index d23f0bc285..ccdd67283e 100644 --- a/references/nextjs-realtime/src/app/page.tsx +++ b/references/nextjs-realtime/src/app/page.tsx @@ -4,7 +4,7 @@ import { ImageUploadDropzone } from "@/components/ImageUploadButton"; export default function Home() { return ( -
+
diff --git a/references/prisma-catalog/package.json b/references/prisma-catalog/package.json deleted file mode 100644 index 74f9f3943c..0000000000 --- a/references/prisma-catalog/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "references-prisma-catalog", - "private": true, - "type": "module", - "devDependencies": { - "trigger.dev": "workspace:*", - "@trigger.dev/build": "workspace:*", - "typescript": "^5.5.4", - "prisma": "5.19.0" - }, - "dependencies": { - "@trigger.dev/sdk": "workspace:*", - "@prisma/client": "5.19.0" - }, - "scripts": { - "generate:prisma": "prisma generate --sql" - } -} \ No newline at end of file diff --git a/references/prisma-catalog/prisma/migrations/20240919122925_add_initial_schema/migration.sql b/references/prisma-catalog/prisma/migrations/20240919122925_add_initial_schema/migration.sql deleted file mode 100644 index 4af85373f9..0000000000 --- a/references/prisma-catalog/prisma/migrations/20240919122925_add_initial_schema/migration.sql +++ /dev/null @@ -1,20 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Post" ( - "id" SERIAL NOT NULL, - "title" TEXT NOT NULL, - "content" TEXT NOT NULL, - "authorId" INTEGER NOT NULL, - - CONSTRAINT "Post_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/references/prisma-catalog/prisma/migrations/migration_lock.toml b/references/prisma-catalog/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c2b..0000000000 --- a/references/prisma-catalog/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/references/prisma-catalog/prisma/schema.prisma b/references/prisma-catalog/prisma/schema.prisma deleted file mode 100644 index b05278b729..0000000000 --- a/references/prisma-catalog/prisma/schema.prisma +++ /dev/null @@ -1,26 +0,0 @@ -generator client { - provider = "prisma-client-js" - previewFeatures = ["typedSql"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_DATABASE_URL") -} - -// user.prisma -model User { - id Int @id @default(autoincrement()) - name String - posts Post[] -} - -// post.prisma -model Post { - id Int @id @default(autoincrement()) - title String - content String - authorId Int - author User @relation(fields: [authorId], references: [id]) -} diff --git a/references/prisma-catalog/prisma/sql/getUsersWithPosts.sql b/references/prisma-catalog/prisma/sql/getUsersWithPosts.sql deleted file mode 100644 index 8f0cb3576d..0000000000 --- a/references/prisma-catalog/prisma/sql/getUsersWithPosts.sql +++ /dev/null @@ -1,10 +0,0 @@ -SELECT - u.id, - u.name, - COUNT(p.id) as "postCount" -FROM - "User" u - LEFT JOIN "Post" p ON u.id = p."authorId" -GROUP BY - u.id, - u.name; \ No newline at end of file diff --git a/references/prisma-catalog/src/db.ts b/references/prisma-catalog/src/db.ts deleted file mode 100644 index 5e029ca062..0000000000 --- a/references/prisma-catalog/src/db.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { getUsersWithPosts } from "@prisma/client/sql"; - -export const prisma = new PrismaClient(); - -export { getUsersWithPosts }; diff --git a/references/prisma-catalog/src/trigger/dbTasks.ts b/references/prisma-catalog/src/trigger/dbTasks.ts deleted file mode 100644 index 7edb466019..0000000000 --- a/references/prisma-catalog/src/trigger/dbTasks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getUsersWithPosts, prisma } from "../db.js"; -import { logger, task } from "@trigger.dev/sdk/v3"; - -export const prismaTask = task({ - id: "prisma-task", - run: async () => { - const users = await prisma.user.findMany(); - - await prisma.user.create({ - data: { - name: "Alice", - }, - }); - - const usersWithPosts = await prisma.$queryRawTyped(getUsersWithPosts()); - - logger.info("Users with posts", { usersWithPosts }); - - return users; - }, -}); diff --git a/references/prisma-catalog/trigger.config.ts b/references/prisma-catalog/trigger.config.ts deleted file mode 100644 index 1bbf0eb2cd..0000000000 --- a/references/prisma-catalog/trigger.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { prismaExtension } from "@trigger.dev/build/extensions/prisma"; -import { defineConfig } from "@trigger.dev/sdk/v3"; - -export default defineConfig({ - runtime: "node", - project: "proj_mpzmrzygzbvmfjnnpcsk", - retries: { - enabledInDev: false, - default: { - maxAttempts: 3, - minTimeoutInMs: 5_000, - maxTimeoutInMs: 30_000, - factor: 2, - randomize: true, - }, - }, - build: { - extensions: [ - prismaExtension({ - schema: "prisma/schema.prisma", - directUrlEnvVarName: "DIRECT_DATABASE_URL", - typedSql: true, - }), - ], - }, -}); diff --git a/references/prisma-catalog/tsconfig.json b/references/prisma-catalog/tsconfig.json deleted file mode 100644 index 9a5ee0b9d6..0000000000 --- a/references/prisma-catalog/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2023", - "module": "Node16", - "moduleResolution": "Node16", - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "customConditions": ["@triggerdotdev/source"], - "jsx": "preserve", - "lib": ["DOM", "DOM.Iterable"], - "noEmit": true - }, - "include": ["./src/**/*.ts", "trigger.config.ts"] -} From 82e24e9aed57aa1fb261fabda601a4aac6b50e5d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 24 Oct 2024 14:45:09 +0100 Subject: [PATCH 04/10] Improved styling --- .../batches/[id]/ClientBatchRunDetails.tsx | 76 ++++++++++--------- .../src/app/batches/[id]/page.tsx | 2 +- references/nextjs-realtime/src/app/layout.tsx | 2 +- references/nextjs-realtime/src/app/page.tsx | 11 ++- .../src/app/runs/[id]/ClientRunDetails.tsx | 12 +-- .../src/app/runs/[id]/page.tsx | 2 +- .../app/uploads/[id]/ClientUploadDetails.tsx | 12 +-- .../src/app/uploads/[id]/page.tsx | 2 +- .../src/components/BatchRunButton.tsx | 6 +- .../src/components/HandleUploadFooter.tsx | 51 ++++++++----- .../src/components/ImageUploadButton.tsx | 1 + .../src/components/RunButton.tsx | 6 +- .../src/components/RunDetails.tsx | 34 ++++++--- .../src/components/UploadImageDisplay.tsx | 52 +++++++------ 14 files changed, 155 insertions(+), 114 deletions(-) diff --git a/references/nextjs-realtime/src/app/batches/[id]/ClientBatchRunDetails.tsx b/references/nextjs-realtime/src/app/batches/[id]/ClientBatchRunDetails.tsx index 8d11cb641d..6f4fc4d1d1 100644 --- a/references/nextjs-realtime/src/app/batches/[id]/ClientBatchRunDetails.tsx +++ b/references/nextjs-realtime/src/app/batches/[id]/ClientBatchRunDetails.tsx @@ -46,71 +46,73 @@ const ProgressBar = ({ run }: { run: AnyRunShape }) => { const StatusBadge = ({ run }: { run: AnyRunShape }) => { switch (run.status) { case "WAITING_FOR_DEPLOY": { - return {run.status}; + return {run.status}; } case "DELAYED": { - return {run.status}; + return {run.status}; } case "EXPIRED": { - return {run.status}; + return {run.status}; } case "QUEUED": { - return {run.status}; + return {run.status}; } case "FROZEN": case "REATTEMPTING": case "EXECUTING": { - return {run.status}; + return {run.status}; } case "COMPLETED": { - return {run.status}; + return {run.status}; } case "TIMED_OUT": case "SYSTEM_FAILURE": case "INTERRUPTED": case "CRASHED": case "FAILED": { - return {run.status}; + return {run.status}; } case "CANCELED": { - return {run.status}; + return {run.status}; } default: { - return {run.status}; + return {run.status}; } } }; export function BackgroundRunsTable({ runs }: { runs: TaskRunShape[] }) { return ( - - A list of your recent background runs. - - - Run ID / Task - Status - Payload ID - Progress - - - - {runs.map((run) => ( - - -
{run.id}
-
{run.taskIdentifier}
-
- - - - {run.payload.id} - - - +
+

Recent Background Runs

+
+ + + Run ID / Task + Status + Payload ID + Progress - ))} - -
+ + + {runs.map((run) => ( + + +
{run.id}
+
{run.taskIdentifier}
+
+ + + + {run.payload.id} + + + +
+ ))} +
+ +
); } @@ -132,7 +134,7 @@ function BatchRunTableWrapper({ batchId }: { batchId: string }) { } return ( -
+
); diff --git a/references/nextjs-realtime/src/app/batches/[id]/page.tsx b/references/nextjs-realtime/src/app/batches/[id]/page.tsx index 8a3f3c61d9..b9df3faafe 100644 --- a/references/nextjs-realtime/src/app/batches/[id]/page.tsx +++ b/references/nextjs-realtime/src/app/batches/[id]/page.tsx @@ -11,7 +11,7 @@ export default async function DetailsPage({ params }: { params: { id: string } } } return ( -
+
); diff --git a/references/nextjs-realtime/src/app/layout.tsx b/references/nextjs-realtime/src/app/layout.tsx index 04e352d9c9..dceb68d946 100644 --- a/references/nextjs-realtime/src/app/layout.tsx +++ b/references/nextjs-realtime/src/app/layout.tsx @@ -28,7 +28,7 @@ export default function RootLayout({ }>) { return ( - + -
+
+
+

+ Thumbnail Generator Demo +

+ +
+
-
); diff --git a/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx b/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx index 7fc7063205..a315ece862 100644 --- a/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx +++ b/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx @@ -10,8 +10,8 @@ function RunDetailsWrapper({ runId }: { runId: string }) { if (error) { return ( -
- +
+

Error: {error.message}

@@ -22,10 +22,10 @@ function RunDetailsWrapper({ runId }: { runId: string }) { if (!run) { return ( -
- +
+ -

Loading run details...

+

Loading run details…

@@ -33,7 +33,7 @@ function RunDetailsWrapper({ runId }: { runId: string }) { } return ( -
+
); diff --git a/references/nextjs-realtime/src/app/runs/[id]/page.tsx b/references/nextjs-realtime/src/app/runs/[id]/page.tsx index e74e709202..46a41e6a1a 100644 --- a/references/nextjs-realtime/src/app/runs/[id]/page.tsx +++ b/references/nextjs-realtime/src/app/runs/[id]/page.tsx @@ -11,7 +11,7 @@ export default async function DetailsPage({ params }: { params: { id: string } } } return ( -
+
); diff --git a/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx b/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx index 7c9dd8a3bd..1a2e150d7d 100644 --- a/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx +++ b/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx @@ -11,8 +11,8 @@ function UploadDetailsWrapper({ fileId }: { fileId: string }) { if (error) { return ( -
- +
+

Error: {error.message}

@@ -23,10 +23,10 @@ function UploadDetailsWrapper({ fileId }: { fileId: string }) { if (!run) { return ( -
- +
+ -

Loading run details...

+

Loading run details…

@@ -45,7 +45,7 @@ function UploadDetailsWrapper({ fileId }: { fileId: string }) { ); return ( -
+
+
); diff --git a/references/nextjs-realtime/src/components/BatchRunButton.tsx b/references/nextjs-realtime/src/components/BatchRunButton.tsx index 4679d0ac2c..9c10ae0633 100644 --- a/references/nextjs-realtime/src/components/BatchRunButton.tsx +++ b/references/nextjs-realtime/src/components/BatchRunButton.tsx @@ -9,7 +9,11 @@ function SubmitButton() { const { pending } = useFormStatus(); return ( - ); diff --git a/references/nextjs-realtime/src/components/HandleUploadFooter.tsx b/references/nextjs-realtime/src/components/HandleUploadFooter.tsx index bbd884a203..20139adcf7 100644 --- a/references/nextjs-realtime/src/components/HandleUploadFooter.tsx +++ b/references/nextjs-realtime/src/components/HandleUploadFooter.tsx @@ -3,7 +3,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { AnyRunShape, TaskRunShape } from "@trigger.dev/sdk/v3"; -import { ExternalLink } from "lucide-react"; +import { ChevronLeft, ExternalLink } from "lucide-react"; import type { handleUpload } from "@/trigger/images"; interface HandleUploadFooterProps { @@ -26,27 +26,36 @@ export function HandleUploadFooter({ run, viewRunUrl }: HandleUploadFooterProps) }; return ( -
-
-
- Run ID: {run.id} - Processing {run.payload.name} - - {run.status} - -
- +
+ +
+ Run ID: {run.id} + | + Processing {run.payload.name} + + {run.status} +
+
); } diff --git a/references/nextjs-realtime/src/components/ImageUploadButton.tsx b/references/nextjs-realtime/src/components/ImageUploadButton.tsx index 6020f8740c..df06858e0c 100644 --- a/references/nextjs-realtime/src/components/ImageUploadButton.tsx +++ b/references/nextjs-realtime/src/components/ImageUploadButton.tsx @@ -47,6 +47,7 @@ export function ImageUploadDropzone() { // Do something with the error. console.error(`ERROR! ${error.message}`); }} + className="border-gray-600" /> ); } diff --git a/references/nextjs-realtime/src/components/RunButton.tsx b/references/nextjs-realtime/src/components/RunButton.tsx index b33c934c97..59f2223f54 100644 --- a/references/nextjs-realtime/src/components/RunButton.tsx +++ b/references/nextjs-realtime/src/components/RunButton.tsx @@ -9,7 +9,11 @@ function SubmitButton() { const { pending } = useFormStatus(); return ( - ); diff --git a/references/nextjs-realtime/src/components/RunDetails.tsx b/references/nextjs-realtime/src/components/RunDetails.tsx index 7e1d4fc42c..1f8b094865 100644 --- a/references/nextjs-realtime/src/components/RunDetails.tsx +++ b/references/nextjs-realtime/src/components/RunDetails.tsx @@ -1,8 +1,9 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; -import type { RetrieveRunResult } from "@trigger.dev/sdk/v3"; import { exampleTask } from "@/trigger/example"; +import type { RetrieveRunResult } from "@trigger.dev/sdk/v3"; +import { AlertTriangleIcon, CheckCheckIcon, XIcon } from "lucide-react"; function formatDate(date: Date | undefined) { return date ? new Date(date).toLocaleString() : "N/A"; @@ -10,7 +11,7 @@ function formatDate(date: Date | undefined) { function JsonDisplay({ data }: { data: any }) { return ( - +
{JSON.stringify(data, null, 2)}
); @@ -18,7 +19,7 @@ function JsonDisplay({ data }: { data: any }) { export default function RunDetails({ record }: { record: RetrieveRunResult }) { return ( - + Run Details @@ -40,9 +41,16 @@ export default function RunDetails({ record }: { record: RetrieveRunResult

Is Test

- - {record.isTest ? "Yes" : "No"} - + {record.isTest ? ( + + + Yes + + ) : ( + + No + + )}
{record.idempotencyKey && (
@@ -121,14 +129,16 @@ export default function RunDetails({ record }: { record: RetrieveRunResult -

Error

- +

+ Error +

+ -

{record.error.name}

-

{record.error.message}

+

{record.error.name}

+

{record.error.message}

{record.error.stackTrace && ( -
{record.error.stackTrace}
+
{record.error.stackTrace}
)}
diff --git a/references/nextjs-realtime/src/components/UploadImageDisplay.tsx b/references/nextjs-realtime/src/components/UploadImageDisplay.tsx index 252164ce9b..4358aec2b0 100644 --- a/references/nextjs-realtime/src/components/UploadImageDisplay.tsx +++ b/references/nextjs-realtime/src/components/UploadImageDisplay.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Image from "next/image"; import { Card, CardContent } from "@/components/ui/card"; -import { LoaderPinwheel } from "lucide-react"; +import { LoaderCircleIcon, LoaderPinwheel } from "lucide-react"; type PendingGridImage = { status: "pending"; @@ -35,8 +35,9 @@ export default function ImageDisplay({ return (
{/* Main uploaded image */} -
-
+
+

Original

+
{uploadedCaption} {gridImages.map((image, index) => ( - - -
- {image.status === "completed" ? ( - {image.caption} - ) : ( -
- -

{image.message}

-
+
+

Style {index + 1}

+ + +
+ {image.status === "completed" ? ( + {image.caption} + ) : ( +
+ +

Processing: {image.message}

+
+ )} +
+ {image.status === "completed" && ( +

+ {image.caption} +

)} -
- {image.status === "completed" && ( -

{image.caption}

- )} - - + + +
))}
From e3659e805dd315d22e4d7131250f9cece48a9b46 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 24 Oct 2024 15:21:59 +0100 Subject: [PATCH 05/10] Improved the generated example styles --- .../nextjs-realtime/src/trigger/images.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/references/nextjs-realtime/src/trigger/images.ts b/references/nextjs-realtime/src/trigger/images.ts index 8e97f3dca5..b3a47e6f53 100644 --- a/references/nextjs-realtime/src/trigger/images.ts +++ b/references/nextjs-realtime/src/trigger/images.ts @@ -17,12 +17,9 @@ export const handleUpload = schemaTask({ const results = await runFalModel.batchTriggerAndWait([ { payload: { - model: "fal-ai/image-preprocessors/canny", + model: "fal-ai/image-preprocessors/lineart", url: file.url, - input: { - low_threshold: 100, - high_threshold: 200, - }, + input: {}, }, options: { tags: ctx.run.tags, @@ -30,9 +27,16 @@ export const handleUpload = schemaTask({ }, { payload: { - model: "fal-ai/aura-sr", + model: "fal-ai/omni-zero", url: file.url, - input: {}, + input: { + prompt: "Turn the image into a cartoon", + image_url: file.url, + composition_image_url: file.url, + style_image_url: + "https://storage.googleapis.com/falserverless/model_tests/omni_zero/style.jpg", + identity_image_url: file.url, + }, }, options: { tags: ctx.run.tags }, }, From fbd4612ad057df434450f3b2f8bff6ca97f44ed6 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 24 Oct 2024 15:22:04 +0100 Subject: [PATCH 06/10] More style improvements --- .../src/app/runs/[id]/ClientRunDetails.tsx | 2 +- .../src/app/uploads/[id]/ClientUploadDetails.tsx | 4 ++-- .../src/components/UploadImageDisplay.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx b/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx index a315ece862..486ca238e0 100644 --- a/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx +++ b/references/nextjs-realtime/src/app/runs/[id]/ClientRunDetails.tsx @@ -23,7 +23,7 @@ function RunDetailsWrapper({ runId }: { runId: string }) { if (!run) { return (
- +

Loading run details…

diff --git a/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx b/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx index 1a2e150d7d..9191f6861a 100644 --- a/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx +++ b/references/nextjs-realtime/src/app/uploads/[id]/ClientUploadDetails.tsx @@ -23,8 +23,8 @@ function UploadDetailsWrapper({ fileId }: { fileId: string }) { if (!run) { return ( -
- +
+

Loading run details…

diff --git a/references/nextjs-realtime/src/components/UploadImageDisplay.tsx b/references/nextjs-realtime/src/components/UploadImageDisplay.tsx index 4358aec2b0..aae3aa9014 100644 --- a/references/nextjs-realtime/src/components/UploadImageDisplay.tsx +++ b/references/nextjs-realtime/src/components/UploadImageDisplay.tsx @@ -37,7 +37,7 @@ export default function ImageDisplay({ {/* Main uploaded image */}

Original

-
+
{uploadedCaption} (

Style {index + 1}

- +
) : (
- -

Processing: {image.message}

+ +

Model: {image.message}

)}
{image.status === "completed" && ( -

+

{image.caption}

)} From 9b5bd4ffc6f2a780d189db63400bd09d8146c6d0 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 25 Oct 2024 22:47:56 +0100 Subject: [PATCH 07/10] More realtime docs --- docs/frontend/react-hooks.mdx | 401 ++++++++++++++++++-- docs/mint.json | 10 + docs/realtime/overview.mdx | 244 ++++++++++++ pnpm-lock.yaml | 22 -- references/nextjs-realtime/src/app/page.tsx | 2 +- 5 files changed, 629 insertions(+), 50 deletions(-) create mode 100644 docs/realtime/overview.mdx diff --git a/docs/frontend/react-hooks.mdx b/docs/frontend/react-hooks.mdx index 94819e327d..828979cada 100644 --- a/docs/frontend/react-hooks.mdx +++ b/docs/frontend/react-hooks.mdx @@ -4,47 +4,303 @@ sidebarTitle: React hooks description: Using the Trigger.dev v3 API from your React application. --- +Our react hooks package provides a set of hooks that make it easy to interact with the Trigger.dev API from your React application, using our [frontend API](/frontend/overview). You can use these hooks to fetch runs, batches, and subscribe to real-time updates. + ## Installation -Install the preview package: `0.0.0-realtime-20241021152316`. +Install the `@trigger.dev/react-hooks` package in your project: + + + +```bash npm +npm add @trigger.dev/react-hooks +``` + +```bash pnpm +pnpm add @trigger.dev/react-hooks +``` -```bash -npm install @trigger.dev/react-hooks@0.0.0-realtime-20241021152316 +```bash yarn +yarn install @trigger.dev/react-hooks ``` + + ## Authentication -You need to use the `TriggerAuthContext` provider to authenticate your requests. +Before you can use the hooks, you need to provide a public access token to the `TriggerAuthContext` provider. Learn more about [authentication in the frontend guide](/frontend/overview). ```tsx import { TriggerAuthContext } from "@trigger.dev/react-hooks"; -const App = () => { +export function SetupTrigger() { + return ( + + + + ); +} +``` + +Now children components can use the hooks to interact with the Trigger.dev API. If you are self-hosting Trigger.dev, you can provide the `baseURL` to the `TriggerAuthContext` provider. + +```tsx +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +export function SetupTrigger() { + return ( + + + + ); +} +``` + +### Next.js and client components + +If you are using Next.js with the App Router, you have to make sure the component that uses the `TriggerAuthContext` is a client component. So for example, the following code will not work: + +```tsx app/page.tsx +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +export default function Page() { return ( ); -}; +} +``` + +That's because `Page` is a server component and the `TriggerAuthContext.Provider` uses client-only react code. To fix this, wrap the `TriggerAuthContext.Provider` in a client component: + +```ts components/TriggerProvider.tsx +"use client"; + +import { TriggerAuthContext } from "@trigger.dev/react-hooks"; + +export function TriggerProvider({ + accessToken, + children, +}: { + accessToken: string; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} +``` + +### Passing the token to the frontend + +Techniques for passing the token to the frontend vary depending on your setup. Here are a few ways to do it for different setups: + +#### Next.js App Router + +If you are using Next.js with the App Router and you are triggering a task from a server action, you can use cookies to store and pass the token to the frontend. + +```tsx actions/trigger.ts +"use server"; + +import { tasks } from "@trigger.dev/sdk/v3"; +import type { exampleTask } from "@/trigger/example"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; + +export async function startRun() { + const handle = await tasks.trigger("example", { foo: "bar" }); + + // Set the auto-generated publicAccessToken in a cookie + cookies().set("publicAccessToken", handle.publicAccessToken); + + redirect(`/runs/${handle.id}`); +} +``` + +Then in the `/runs/[id].tsx` page, you can read the token from the cookie and pass it to the `TriggerProvider`. + +```tsx pages/runs/[id].tsx +import { TriggerProvider } from "@/components/TriggerProvider"; + +export default function RunPage({ params }: { params: { id: string } }) { + const publicAccessToken = cookies().get("publicAccessToken"); + + return ( + + + + ); +} +``` + +Instead of a cookie, you could also use a query parameter to pass the token to the frontend: + +```tsx actions/trigger.ts +import { tasks } from "@trigger.dev/sdk/v3"; +import type { exampleTask } from "@/trigger/example"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; + +export async function startRun() { + const handle = await tasks.trigger("example", { foo: "bar" }); + + redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`); +} +``` + +And then in the `/runs/[id].tsx` page: + +```tsx pages/runs/[id].tsx +import { TriggerProvider } from "@/components/TriggerProvider"; + +export default function RunPage({ + params, + searchParams, +}: { + params: { id: string }; + searchParams: { publicAccessToken: string }; +}) { + return ( + + + + ); +} +``` + +Another alternative would be to use a server-side rendered page to fetch the token and pass it to the frontend: + + + +```tsx pages/runs/[id].tsx +import { TriggerProvider } from "@/components/TriggerProvider"; +import { generatePublicAccessToken } from "@/trigger/auth"; + +export default async function RunPage({ params }: { params: { id: string } }) { + // This will be executed on the server only + const publicAccessToken = await generatePublicAccessToken(params.id); + + return ( + + + + ); +} +``` + +```tsx trigger/auth.ts +import { auth } from "@trigger.dev/sdk/v3"; + +export async function generatePublicAccessToken(runId: string) { + return auth.createPublicToken({ + scopes: { + read: { + runs: [runId], + }, + }, + expirationTime: "1h", + }); +} ``` + + ## Usage +### SWR vs Realtime hooks + +We offer two "styles" of hooks: SWR and Realtime. The SWR hooks use the [swr](https://swr.vercel.app/) library to fetch data once and cache it. The Realtime hooks use [Trigger.dev realtime](/realtime) to subscribe to updates in real-time. + + + It can be a little confusing which one to use because [swr](https://swr.vercel.app/) can also be + configured to poll for updates. But because of rate-limits and the way the Trigger.dev API works, + we recommend using the Realtime hooks for most use-cases. + + +All hooks named `useRealtime*` are Realtime hooks, and all hooks named `use*` are SWR hooks. + +#### Common SWR hook options + +You can pass the following options to the all SWR hooks: + + + Revalidate the data when the window regains focus. + + + + Revalidate the data when the browser regains a network connection. + + + + Poll for updates at the specified interval (in milliseconds). Polling is not recommended for most + use-cases. Use the Realtime hooks instead. + + +#### Common SWR hook return values + + + An error object if an error occurred while fetching the data. + + + + A boolean indicating if the data is currently being fetched. + + + + A boolean indicating if the data is currently being revalidated. + + + + A boolean indicating if an error occurred while fetching the data. + + ### useRun The `useRun` hook allows you to fetch a run by its ID. ```tsx +"use client"; // This is needed for Next.js App Router or other RSC frameworks + import { useRun } from "@trigger.dev/react-hooks"; -const MyComponent = ({ runId }) => { - const { data, error, isLoading } = useRun(runId); +export function MyComponent({ runId }: { runId: string }) { + const { run, error, isLoading } = useRun(runId); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - return
Run: {data.id}
; -}; + return
Run: {run.id}
; +} +``` + +The `run` object returned is the same as the [run object](/management/runs/retrieve) returned by the Trigger.dev API. To correctly type the run's payload and output, you can provide the type of your task to the `useRun` hook: + +```tsx +import { useRun } from "@trigger.dev/react-hooks"; +import type { myTask } from "@/trigger/myTask"; + +export function MyComponent({ runId }: { runId: string }) { + const { run, error, isLoading } = useRun(runId); + + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; + + // Now run.payload and run.output are correctly typed + + return
Run: {run.id}
; +} ``` ### useRealtimeRun @@ -52,48 +308,139 @@ const MyComponent = ({ runId }) => { The `useRealtimeRun` hook allows you to subscribe to a run by its ID. ```tsx +"use client"; // This is needed for Next.js App Router or other RSC frameworks + import { useRealtimeRun } from "@trigger.dev/react-hooks"; -const MyComponent = ({ runId }) => { - const { data, error, isLoading } = useRealtimeRun(runId); +export function MyComponent({ runId }: { runId: string }) { + const { run, error } = useRealtimeRun(runId); + + if (error) return
Error: {error.message}
; + + return
Run: {run.id}
; +} +``` + +To correctly type the run's payload and output, you can provide the type of your task to the `useRealtimeRun` hook: + +```tsx +import { useRealtimeRun } from "@trigger.dev/react-hooks"; +import type { myTask } from "@/trigger/myTask"; + +export function MyComponent({ runId }: { runId: string }) { + const { run, error } = useRealtimeRun(runId); - if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - return
Run: {data.id}
; -}; + // Now run.payload and run.output are correctly typed + + return
Run: {run.id}
; +} ``` +See our [Realtime documentation](/realtime) for more information. + ### useRealtimeRunsWithTag -The `useRealtimeRunsWithTag` hook allows you to subscribe to runs with a specific tag. +The `useRealtimeRunsWithTag` hook allows you to subscribe to multiple runs with a specific tag. ```tsx +"use client"; // This is needed for Next.js App Router or other RSC frameworks + import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks"; -const MyComponent = ({ tag }) => { - const { data, error, isLoading } = useRealtimeRunsWithTag(tag); +export function MyComponent({ tag }: { tag: string }) { + const { runs, error } = useRealtimeRunsWithTag(tag); - if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - return
Runs: {data.map((run) => run.id).join(", ")}
; -}; + return ( +
+ {runs.map((run) => ( +
Run: {run.id}
+ ))} +
+ ); +} +``` + +To correctly type the runs payload and output, you can provide the type of your task to the `useRealtimeRunsWithTag` hook: + +```tsx +import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks"; +import type { myTask } from "@/trigger/myTask"; + +export function MyComponent({ tag }: { tag: string }) { + const { runs, error } = useRealtimeRunsWithTag(tag); + + if (error) return
Error: {error.message}
; + + // Now runs[i].payload and runs[i].output are correctly typed + + return ( +
+ {runs.map((run) => ( +
Run: {run.id}
+ ))} +
+ ); +} ``` +If `useRealtimeRunsWithTag` could return multiple different types of tasks, you can pass a union of all the task types to the hook: + +```tsx +import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks"; +import type { myTask1, myTask2 } from "@/trigger/myTasks"; + +export function MyComponent({ tag }: { tag: string }) { + const { runs, error } = useRealtimeRunsWithTag(tag); + + if (error) return
Error: {error.message}
; + + // You can narrow down the type of the run based on the taskIdentifier + for (const run of runs) { + if (run.taskIdentifier === "my-task-1") { + // run is correctly typed as myTask1 + } else if (run.taskIdentifier === "my-task-2") { + // run is correctly typed as myTask2 + } + } + + return ( +
+ {runs.map((run) => ( +
Run: {run.id}
+ ))} +
+ ); +} +``` + +See our [Realtime documentation](/realtime) for more information. + ### useRealtimeBatch -The `useRealtimeBatch` hook allows you to subscribe to a batch by its ID. +The `useRealtimeBatch` hook allows you to subscribe to a batch of runs by its the batch ID. ```tsx +"use client"; // This is needed for Next.js App Router or other RSC frameworks + import { useRealtimeBatch } from "@trigger.dev/react-hooks"; -const MyComponent = ({ batchId }) => { - const { data, error, isLoading } = useRealtimeBatch(batchId); +export function MyComponent({ batchId }: { batchId: string }) { + const { runs, error } = useRealtimeBatch(batchId); - if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - return
Batch: {data.id}
; -}; + return ( +
+ {runs.map((run) => ( +
Run: {run.id}
+ ))} +
+ ); +} ``` + +See our [Realtime documentation](/realtime) for more information. diff --git a/docs/mint.json b/docs/mint.json index 187c4f9dc7..9df916edc4 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -102,6 +102,10 @@ { "source": "/examples/:slug*", "destination": "/guides/examples/:slug*" + }, + { + "source": "/realtime", + "destination": "/realtime/overview" } ], "anchors": [ @@ -201,6 +205,12 @@ "frontend/react-hooks" ] }, + { + "group": "Realtime API", + "pages": [ + "realtime/overview" + ] + }, { "group": "API reference", "pages": [ diff --git a/docs/realtime/overview.mdx b/docs/realtime/overview.mdx new file mode 100644 index 0000000000..1dce37d56e --- /dev/null +++ b/docs/realtime/overview.mdx @@ -0,0 +1,244 @@ +--- +title: Realtime overview +sidebarTitle: Overview +description: Using the Trigger.dev v3 realtime API +--- + +Trigger.dev Realtime is a set of APIs that allow you to subscribe to runs and get real-time updates on the run status. This is useful for monitoring runs, updating UIs, and building realtime dashboards. + +## How it works + +The Realtime API is built on top of [Electric SQL](https://electric-sql.com/), an open-source PostgreSQL syncing engine. The Trigger.dev API wraps Electric SQL and provides a simple API to subscribe to runs and get real-time updates. + +## Usage + +After you trigger a task, you can subscribe to the run using the `runs.subscribeToRun` function. This function returns an async iterator that you can use to get updates on the run status. + +```ts +import { runs, tasks } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +async function myBackend() { + const handle = await tasks.trigger("my-task", { some: "data" }); + + for await (const run of runs.subscribeToRun(handle.id)) { + // This will log the run every time it changes + console.log(run); + } +} +``` + +Every time the run changes, the async iterator will yield the updated run. You can use this to update your UI, log the run status, or take any other action. + +Alternatively, you can subscribe to changes to any run that includes a specific tag (or tags) using the `runs.subscribeToRunsWithTag` function. + +```ts +import { runs } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +for await (const run of runs.subscribeToRunsWithTag("user:1234")) { + // This will log the run every time it changes, for all runs with the tag "user:1234" + console.log(run); +} +``` + +If you've used `batchTrigger` to trigger multiple runs, you can also subscribe to changes to all the runs triggered in the batch using the `runs.subscribeToBatch` function. + +```ts +import { runs } from "@trigger.dev/sdk/v3"; + +// Somewhere in your backend code +for await (const run of runs.subscribeToBatch("batch-id")) { + // This will log the run every time it changes, for all runs in the batch with the ID "batch-id" + console.log(run); +} +``` + +## Run changes + +You will receive updates whenever a run changes for the following reasons: + +- The run moves to a new state. See our [run lifecycle docs](/runs-and-attempts#the-run-lifecycle) for more information. +- [Run tags](/tags) are added or removed. +- [Run metadata](/runs/metadata) is updated. + +## Run object + +The run object returned by the async iterator is NOT the same as the run object returned by the `runs.retrieve` function. This is because Electric SQL streams changes from a single PostgreSQL table, and the run object returned by `runs.retrieve` is a combination of multiple tables. + +The run object returned by the async iterator has the following fields: + + + The run ID. + + + + The task identifier. + + + + The input payload for the run. + + + + The output result of the run. + + + + Timestamp when the run was created. + + + + Timestamp when the run was last updated. + + + + Sequential number assigned to the run. + + + + Current status of the run. + + + +| Status | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------- | +| `WAITING_FOR_DEPLOY` | Task hasn't been deployed yet but is waiting to be executed | +| `QUEUED` | Run is waiting to be executed by a worker | +| `EXECUTING` | Run is currently being executed by a worker | +| `REATTEMPTING` | Run has failed and is waiting to be retried | +| `FROZEN` | Run has been paused by the system, and will be resumed by the system | +| `COMPLETED` | Run has been completed successfully | +| `CANCELED` | Run has been canceled by the user | +| `FAILED` | Run has been completed with errors | +| `CRASHED` | Run has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage | +| `INTERRUPTED` | Run was interrupted during execution, mostly this happens in development environments | +| `SYSTEM_FAILURE` | Run has failed to complete, due to an error in the system | +| `DELAYED` | Run has been scheduled to run at a specific time | +| `EXPIRED` | Run has expired and won't be executed | +| `TIMED_OUT` | Run has reached it's maxDuration and has been stopped | + + + + + + Duration of the run in milliseconds. + + + + Total cost of the run in cents. + + + + Base cost of the run in cents before any additional charges. + + + + Array of tags associated with the run. + + + + Key used to ensure idempotent execution. + + + + Timestamp when the run expired. + + + + Time-to-live duration for the run. + + + + Timestamp when the run finished. + + + + Timestamp when the run started. + + + + Timestamp until which the run is delayed. + + + + Timestamp when the run was queued. + + + + Additional metadata associated with the run. + + + + Error information if the run failed. + + + + Indicates whether this is a test run. + + +## Type-safety + +You can infer the types of the run's payload and output by passing the type of the task to the `subscribeToRun` function. This will give you type-safe access to the run's payload and output. + +```ts +import { runs, tasks } from "@trigger.dev/sdk/v3"; +import type { myTask } from "./trigger/my-task"; + +// Somewhere in your backend code +async function myBackend() { + const handle = await tasks.trigger("my-task", { some: "data" }); + + for await (const run of runs.subscribeToRun(handle.id)) { + // This will log the run every time it changes + console.log(run.payload.some); + + if (run.output) { + // This will log the output if it exists + console.log(run.output.some); + } + } +} +``` + +When using `subscribeToRunsWithTag`, you can pass a union of task types for all the possible tasks that can have the tag. + +```ts +import { runs } from "@trigger.dev/sdk/v3"; +import type { myTask, myOtherTask } from "./trigger/my-task"; + +// Somewhere in your backend code +for await (const run of runs.subscribeToRunsWithTag("my-tag")) { + // You can narrow down the type based on the taskIdentifier + switch (run.taskIdentifier) { + case "my-task": { + console.log("Run output:", run.output.foo); // This will be type-safe + break; + } + case "my-other-task": { + console.log("Run output:", run.output.bar); // This will be type-safe + break; + } + } +} +``` + +## Run metadata + +The run metadata API gives you the ability to add or update custom metadata on a run, which will cause the run to be updated. This allows you to extend the realtime API with custom data attached to a run that can be used for various purposes. Some common use cases include: + +- Adding a link to a related resource +- Adding a reference to a user or organization +- Adding a custom status with progress information + +See our [run metadata docs](/runs/metadata) for more on how to use this feature. + +### Using w/Realtime & React hooks + +We suggest combining run metadata with the realtime API and our [React hooks](/frontend/react-hooks) to bridge the gap between your trigger.dev tasks and your UI. This allows you to update your UI in real-time based on changes to the run metadata. As a simple example, you could add a custom status to a run with a progress value, and update your UI based on that progress. + +We have a full demo app repo available [here](https://github.com/triggerdotdev/nextjs-realtime-simple-demo) + +## Limits + +The Realtime API in the Trigger.dev Cloud limits the number of concurrent subscriptions, depending on your plan. If you exceed the limit, you will receive an error when trying to subscribe to a run. For more information, see our [pricing page](https://trigger.dev/pricing). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c07b073139..debeecc5c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1614,28 +1614,6 @@ importers: specifier: ^5 version: 5.5.4 - references/prisma-catalog: - dependencies: - '@prisma/client': - specifier: 5.19.0 - version: 5.19.0(prisma@5.19.0) - '@trigger.dev/sdk': - specifier: workspace:* - version: link:../../packages/trigger-sdk - devDependencies: - '@trigger.dev/build': - specifier: workspace:* - version: link:../../packages/build - prisma: - specifier: 5.19.0 - version: 5.19.0 - trigger.dev: - specifier: workspace:* - version: link:../../packages/cli-v3 - typescript: - specifier: ^5.5.4 - version: 5.5.4 - references/v3-catalog: dependencies: '@effect/schema': diff --git a/references/nextjs-realtime/src/app/page.tsx b/references/nextjs-realtime/src/app/page.tsx index e8ca8ca035..498712adbc 100644 --- a/references/nextjs-realtime/src/app/page.tsx +++ b/references/nextjs-realtime/src/app/page.tsx @@ -7,7 +7,7 @@ export default function Home() {

- Thumbnail Generator Demo + Trigger.dev Realtime + UploadThing + fal.ai

From 5f0b81c17ce72e96e2747831895bd01e34b3e6fd Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 26 Oct 2024 07:38:04 +0100 Subject: [PATCH 08/10] Couple doc tweeks --- docs/mint.json | 6 ++- docs/realtime/overview.mdx | 2 +- docs/{runs-and-attempts.mdx => runs.mdx} | 63 +++++++++++++++++------- 3 files changed, 51 insertions(+), 20 deletions(-) rename docs/{runs-and-attempts.mdx => runs.mdx} (77%) diff --git a/docs/mint.json b/docs/mint.json index 9df916edc4..0c577d6494 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -106,6 +106,10 @@ { "source": "/realtime", "destination": "/realtime/overview" + }, + { + "source": "/runs-and-attempts", + "destination": "/runs" } ], "anchors": [ @@ -138,7 +142,7 @@ ] }, "triggering", - "runs-and-attempts", + "runs", "apikeys", { "group": "Configuration", diff --git a/docs/realtime/overview.mdx b/docs/realtime/overview.mdx index 1dce37d56e..2ed4bbf6ab 100644 --- a/docs/realtime/overview.mdx +++ b/docs/realtime/overview.mdx @@ -8,7 +8,7 @@ Trigger.dev Realtime is a set of APIs that allow you to subscribe to runs and ge ## How it works -The Realtime API is built on top of [Electric SQL](https://electric-sql.com/), an open-source PostgreSQL syncing engine. The Trigger.dev API wraps Electric SQL and provides a simple API to subscribe to runs and get real-time updates. +The Realtime API is built on top of [Electric SQL](https://electric-sql.com/), an open-source PostgreSQL syncing engine. The Trigger.dev API wraps Electric SQL and provides a simple API to subscribe to [runs](/runs) and get real-time updates. ## Usage diff --git a/docs/runs-and-attempts.mdx b/docs/runs.mdx similarity index 77% rename from docs/runs-and-attempts.mdx rename to docs/runs.mdx index c00f4131e6..45eb9f5c17 100644 --- a/docs/runs-and-attempts.mdx +++ b/docs/runs.mdx @@ -1,6 +1,6 @@ --- -title: "Runs & attempts" -description: "Understanding the lifecycle of task execution in Trigger.dev" +title: "Runs" +description: "Understanding the lifecycle of task run execution in Trigger.dev" --- In Trigger.dev, the concepts of runs and attempts are fundamental to understanding how tasks are executed and managed. This article explains these concepts in detail and provides insights into the various states a run can go through during its lifecycle. @@ -24,37 +24,52 @@ Runs can also find themselves in lots of other states depending on what's happen ### Initial States - **Waiting for deploy**: If a task is triggered before it has been deployed, the run enters this state and waits for the task to be deployed. + **Waiting for deploy**: +If a task is triggered before it has been deployed, the run enters this state and waits for the task +to be deployed. - **Delayed**: When a run is triggered with a delay, it enters this state until the specified delay period has passed. + **Delayed**: When a run is triggered +with a delay, it enters this state until the specified delay period has passed. - **Queued**: The run is ready to be executed and is waiting in the queue. + **Queued**: The run is ready +to be executed and is waiting in the queue. ### Execution States - **Executing**: The task is currently running. + **Executing**: The task is +currently running. - **Reattempting**: The task has failed and is being retried. + **Reattempting**: The task has +failed and is being retried. - **Frozen**: Task has been frozen and is waiting to be resumed. + **Frozen**: Task has been frozen +and is waiting to be resumed. ### Final States - **Completed**: The task has successfully finished execution. + **Completed**: The task has successfully +finished execution. - **Canceled**: The run was manually canceled by the user. + **Canceled**: The run was manually canceled +by the user. - **Failed**: The task has failed to complete successfully. + **Failed**: The task has failed +to complete successfully. - **Timed out**: Task has failed because it exceeded its `maxDuration`. + **Timed out**: Task has +failed because it exceeded its `maxDuration`. - **Crashed**: The worker process crashed during execution (likely due to an Out of Memory error). + **Crashed**: The worker process crashed +during execution (likely due to an Out of Memory error). - **Interrupted**: In development mode, when the CLI is disconnected. + **Interrupted**: In development +mode, when the CLI is disconnected. - **System failure**: An unrecoverable system error has occurred. + **System failure**: An unrecoverable system +error has occurred. - **Expired**: The run's Time-to-Live (TTL) has passed before it could start executing. + **Expired**: The run's Time-to-Live +(TTL) has passed before it could start executing. ## Attempts @@ -150,13 +165,13 @@ You can also replay runs from the dashboard using the same or different payload. The `triggerAndWait()` function triggers a task and then lets you wait for the result before continuing. [Learn more about triggerAndWait()](/triggering#yourtask-triggerandwait). -![Run with triggerAndWait](/images/run-with-triggerAndWait().png) +![Run with triggerAndWait]() #### batchTriggerAndWait() Similar to `triggerAndWait()`, the `batchTriggerAndWait()` function lets you batch trigger a task and wait for all the results [Learn more about batchTriggerAndWait()](/triggering#yourtask-batchtriggerandwait). -![Run with batchTriggerAndWait](/images/run-with-batchTriggerAndWait().png) +![Run with batchTriggerAndWait]() ### Runs API @@ -181,6 +196,18 @@ runs.cancel(runId); These methods allow you to access detailed information about runs and their attempts, including payloads, outputs, parent runs, and child runs. +### Real-time updates + +You can subscribe to run updates in real-time using the `subscribeToRun()` function: + +```ts +for await (const run of runs.subscribeToRun(runId)) { + console.log(run); +} +``` + +For more on real-time updates, see the [Realtime](/realtime) documentation. + ### Triggering runs for undeployed tasks It's possible to trigger a run for a task that hasn't been deployed yet. The run will enter the "Waiting for deploy" state until the task is deployed. Once deployed, the run will be queued and executed normally. From b77f22a882381755eb141b45527dac6650871c9d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 28 Oct 2024 14:56:28 +0000 Subject: [PATCH 09/10] runs-and-attempts -> runs --- docs/realtime/overview.mdx | 2 +- docs/tasks/schemaTask.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/realtime/overview.mdx b/docs/realtime/overview.mdx index 2ed4bbf6ab..8d463ca1c8 100644 --- a/docs/realtime/overview.mdx +++ b/docs/realtime/overview.mdx @@ -58,7 +58,7 @@ for await (const run of runs.subscribeToBatch("batch-id")) { You will receive updates whenever a run changes for the following reasons: -- The run moves to a new state. See our [run lifecycle docs](/runs-and-attempts#the-run-lifecycle) for more information. +- The run moves to a new state. See our [run lifecycle docs](/runs#the-run-lifecycle) for more information. - [Run tags](/tags) are added or removed. - [Run metadata](/runs/metadata) is updated. diff --git a/docs/tasks/schemaTask.mdx b/docs/tasks/schemaTask.mdx index d87cf39875..5f28ebe2cd 100644 --- a/docs/tasks/schemaTask.mdx +++ b/docs/tasks/schemaTask.mdx @@ -31,7 +31,7 @@ const myTask = schemaTask({ that would be a breaking change, we are keeping them separate for now. -When you trigger the task directly, the payload will be validated against the schema before the [run](/runs-and-attempts) is created: +When you trigger the task directly, the payload will be validated against the schema before the [run](/runs) is created: ```ts import { tasks } from "@trigger.dev/sdk/v3"; From 38845038dac90ec0d618cc5232130b6994dfd93a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 28 Oct 2024 15:46:48 +0000 Subject: [PATCH 10/10] Remove the prerelease package version --- docs/frontend/overview.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/frontend/overview.mdx b/docs/frontend/overview.mdx index 5819f23699..4f48d9695a 100644 --- a/docs/frontend/overview.mdx +++ b/docs/frontend/overview.mdx @@ -4,8 +4,6 @@ sidebarTitle: Overview & Auth description: Using the Trigger.dev SDK from your frontend application. --- -Install the preview package: `0.0.0-realtime-20241021152316`. - You can use certain SDK functions in your frontend application to interact with the Trigger.dev API. This guide will show you how to authenticate your requests and use the SDK in your frontend application. ## Authentication