From c922d2494198586851856315131b9ce22d1aef6a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 16 Oct 2023 16:04:05 +0100 Subject: [PATCH 01/39] Adding simulate graphile job to easily debug graceful shutdown retries in docker image --- .../app/routes/admin.api.v1.simulate.ts | 34 +++++++++++++++++++ apps/webapp/app/services/worker.server.ts | 9 +++++ docker/dev-compose.yml | 4 ++- 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 apps/webapp/app/routes/admin.api.v1.simulate.ts diff --git a/apps/webapp/app/routes/admin.api.v1.simulate.ts b/apps/webapp/app/routes/admin.api.v1.simulate.ts new file mode 100644 index 000000000..2b7887175 --- /dev/null +++ b/apps/webapp/app/routes/admin.api.v1.simulate.ts @@ -0,0 +1,34 @@ +import { ActionArgs, json, redirect } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { workerQueue } from "~/services/worker.server"; + +export async function action({ request }: ActionArgs) { + // Next authenticate the request + const authenticationResult = await authenticateApiRequest(request); + + if (!authenticationResult) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const adminOrgMembers = await prisma.orgMember.findMany({ + where: { + organizationId: authenticationResult.environment.organizationId, + user: { + admin: true, + }, + }, + }); + + if (!adminOrgMembers.length) { + return json({ error: "You must be an admin to perform this action" }, { status: 403 }); + } + + const body: any = await request.json(); + + await workerQueue.enqueue("simulate", { + seconds: body.seconds, + }); + + return json({ success: true }); +} diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 9e048b123..069685b0a 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -76,6 +76,9 @@ const workerCatalog = { connectionCreated: z.object({ id: z.string(), }), + simulate: z.object({ + seconds: z.number(), + }), }; const executionWorkerCatalog = { @@ -313,6 +316,12 @@ function getWorkerQueue() { }); }, }, + simulate: { + maxAttempts: 5, + handler: async (payload, job) => { + await new Promise((resolve) => setTimeout(resolve, payload.seconds * 1000)); + }, + }, }, }); } diff --git a/docker/dev-compose.yml b/docker/dev-compose.yml index 3f0003a57..015139b68 100644 --- a/docker/dev-compose.yml +++ b/docker/dev-compose.yml @@ -9,7 +9,7 @@ networks: services: db: - container_name: db + container_name: devdb image: postgres:14 restart: always volumes: @@ -30,6 +30,8 @@ services: - 3030:3030 depends_on: - db + env_file: + - ../.env environment: DATABASE_URL: postgres://postgres:postgres@db:5432/postgres?schema=public DIRECT_URL: postgres://postgres:postgres@db:5432/postgres?schema=public From 4f317055fd0471964d984bb38cc99b0e67f145ef Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 16 Oct 2023 16:19:16 +0100 Subject: [PATCH 02/39] =?UTF-8?q?useDevEnvironment=20returns=20the=20curre?= =?UTF-8?q?nt=20user=E2=80=99s=20dev=20environment,=20not=20the=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/webapp/app/hooks/useEnvironments.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/hooks/useEnvironments.ts b/apps/webapp/app/hooks/useEnvironments.ts index 7756c2479..20601bcd9 100644 --- a/apps/webapp/app/hooks/useEnvironments.ts +++ b/apps/webapp/app/hooks/useEnvironments.ts @@ -1,5 +1,6 @@ import { RouteMatch } from "@remix-run/react"; import { MatchedProject, useOptionalProject } from "./useProject"; +import { useUser } from "./useUser"; export type ProjectJobEnvironment = MatchedProject["environments"][number]; @@ -11,10 +12,13 @@ export function useEnvironments(matches?: RouteMatch[]) { } export function useDevEnvironment(matches?: RouteMatch[]) { + const user = useUser(); const environments = useEnvironments(matches); if (!environments) return; - return environments.find((environment) => environment.type === "DEVELOPMENT"); + return environments.find( + (environment) => environment.type === "DEVELOPMENT" && environment.userId === user.id + ); } export function useProdEnvironment(matches?: RouteMatch[]) { From 0fb16a7324b1049d0db0af0a5520a9e04b047659 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Wed, 18 Oct 2023 01:21:47 +0530 Subject: [PATCH 03/39] framework icons --- docs/documentation/quickstarts/astro.mdx | 1 + docs/documentation/quickstarts/express.mdx | 1 + docs/documentation/quickstarts/fastify.mdx | 1 + docs/documentation/quickstarts/nestjs.mdx | 1 + docs/documentation/quickstarts/nextjs.mdx | 1 + docs/documentation/quickstarts/nuxt.mdx | 1 + docs/documentation/quickstarts/redwood.mdx | 1 + docs/documentation/quickstarts/remix.mdx | 1 + docs/documentation/quickstarts/sveltekit.mdx | 1 + 9 files changed, 9 insertions(+) diff --git a/docs/documentation/quickstarts/astro.mdx b/docs/documentation/quickstarts/astro.mdx index f1d0a9298..26c69228b 100644 --- a/docs/documentation/quickstarts/astro.mdx +++ b/docs/documentation/quickstarts/astro.mdx @@ -2,6 +2,7 @@ title: "Astro Quick Start" sidebarTitle: "Astro" description: "Start creating Jobs in 5 minutes in your Astro project." +icon: "rocket" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/express.mdx b/docs/documentation/quickstarts/express.mdx index 83648bf49..91622ea5a 100644 --- a/docs/documentation/quickstarts/express.mdx +++ b/docs/documentation/quickstarts/express.mdx @@ -2,6 +2,7 @@ title: "Express Quick Start" sidebarTitle: "Express" description: "Start creating Jobs in 5 minutes in your Express project." +icon: "node-js" --- diff --git a/docs/documentation/quickstarts/fastify.mdx b/docs/documentation/quickstarts/fastify.mdx index 8ffbbcc95..f9456ce19 100644 --- a/docs/documentation/quickstarts/fastify.mdx +++ b/docs/documentation/quickstarts/fastify.mdx @@ -2,6 +2,7 @@ title: "Fastify Quick Start" sidebarTitle: "Fastify" description: "Start creating Jobs in 5 minutes in your Fastify project." +icon: "paw-claws" --- diff --git a/docs/documentation/quickstarts/nestjs.mdx b/docs/documentation/quickstarts/nestjs.mdx index 46bc1b03b..77f858bc5 100644 --- a/docs/documentation/quickstarts/nestjs.mdx +++ b/docs/documentation/quickstarts/nestjs.mdx @@ -2,6 +2,7 @@ title: "NestJS Quick Start" sidebarTitle: "NestJS" description: "Start creating Jobs in 5 minutes in your NestJS project." +icon: "paw-claws" --- diff --git a/docs/documentation/quickstarts/nextjs.mdx b/docs/documentation/quickstarts/nextjs.mdx index 4e59b4624..3b21e9a15 100644 --- a/docs/documentation/quickstarts/nextjs.mdx +++ b/docs/documentation/quickstarts/nextjs.mdx @@ -2,6 +2,7 @@ title: "Next.js Quick Start" sidebarTitle: "Next.js" description: "Start creating Jobs in 5 minutes in your Next.js project." +icon: "N" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/nuxt.mdx b/docs/documentation/quickstarts/nuxt.mdx index c93cbf586..73f1f75c2 100644 --- a/docs/documentation/quickstarts/nuxt.mdx +++ b/docs/documentation/quickstarts/nuxt.mdx @@ -2,6 +2,7 @@ title: "Nuxt Quick Start" sidebarTitle: "Nuxt" description: "Start creating Jobs in 5 minutes in your Nuxt project." +icon: "triangle" --- diff --git a/docs/documentation/quickstarts/redwood.mdx b/docs/documentation/quickstarts/redwood.mdx index 831faf277..405679055 100644 --- a/docs/documentation/quickstarts/redwood.mdx +++ b/docs/documentation/quickstarts/redwood.mdx @@ -2,6 +2,7 @@ title: "Redwood Quick Start" sidebarTitle: "Redwood" description: "Start creating Jobs in 5 minutes in your Redwood project." +icon: "js" --- diff --git a/docs/documentation/quickstarts/remix.mdx b/docs/documentation/quickstarts/remix.mdx index dcdf7f6c5..b84679094 100644 --- a/docs/documentation/quickstarts/remix.mdx +++ b/docs/documentation/quickstarts/remix.mdx @@ -2,6 +2,7 @@ title: "Remix Quick Start" sidebarTitle: "Remix" description: "Start creating Jobs in 5 minutes in your Remix project." +icon: "r" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/sveltekit.mdx b/docs/documentation/quickstarts/sveltekit.mdx index 9a4a92956..0b20349e5 100644 --- a/docs/documentation/quickstarts/sveltekit.mdx +++ b/docs/documentation/quickstarts/sveltekit.mdx @@ -2,6 +2,7 @@ title: "SvelteKit Quick Start" sidebarTitle: "SvelteKit" description: "Start creating Jobs in 5 minutes in your SvelteKit project." +icon: "square-s" --- From d1ecd6b99cb491640a2d8f24579a436737104cb0 Mon Sep 17 00:00:00 2001 From: Digvijay Shelar <82649533+shelar1423@users.noreply.github.com> Date: Wed, 18 Oct 2023 02:30:56 +0530 Subject: [PATCH 04/39] framework icons (#635) --- docs/documentation/quickstarts/astro.mdx | 1 + docs/documentation/quickstarts/express.mdx | 1 + docs/documentation/quickstarts/fastify.mdx | 1 + docs/documentation/quickstarts/nestjs.mdx | 1 + docs/documentation/quickstarts/nextjs.mdx | 1 + docs/documentation/quickstarts/nuxt.mdx | 1 + docs/documentation/quickstarts/redwood.mdx | 1 + docs/documentation/quickstarts/remix.mdx | 1 + docs/documentation/quickstarts/sveltekit.mdx | 1 + 9 files changed, 9 insertions(+) diff --git a/docs/documentation/quickstarts/astro.mdx b/docs/documentation/quickstarts/astro.mdx index f1d0a9298..26c69228b 100644 --- a/docs/documentation/quickstarts/astro.mdx +++ b/docs/documentation/quickstarts/astro.mdx @@ -2,6 +2,7 @@ title: "Astro Quick Start" sidebarTitle: "Astro" description: "Start creating Jobs in 5 minutes in your Astro project." +icon: "rocket" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/express.mdx b/docs/documentation/quickstarts/express.mdx index 83648bf49..91622ea5a 100644 --- a/docs/documentation/quickstarts/express.mdx +++ b/docs/documentation/quickstarts/express.mdx @@ -2,6 +2,7 @@ title: "Express Quick Start" sidebarTitle: "Express" description: "Start creating Jobs in 5 minutes in your Express project." +icon: "node-js" --- diff --git a/docs/documentation/quickstarts/fastify.mdx b/docs/documentation/quickstarts/fastify.mdx index 8ffbbcc95..f9456ce19 100644 --- a/docs/documentation/quickstarts/fastify.mdx +++ b/docs/documentation/quickstarts/fastify.mdx @@ -2,6 +2,7 @@ title: "Fastify Quick Start" sidebarTitle: "Fastify" description: "Start creating Jobs in 5 minutes in your Fastify project." +icon: "paw-claws" --- diff --git a/docs/documentation/quickstarts/nestjs.mdx b/docs/documentation/quickstarts/nestjs.mdx index 46bc1b03b..77f858bc5 100644 --- a/docs/documentation/quickstarts/nestjs.mdx +++ b/docs/documentation/quickstarts/nestjs.mdx @@ -2,6 +2,7 @@ title: "NestJS Quick Start" sidebarTitle: "NestJS" description: "Start creating Jobs in 5 minutes in your NestJS project." +icon: "paw-claws" --- diff --git a/docs/documentation/quickstarts/nextjs.mdx b/docs/documentation/quickstarts/nextjs.mdx index 4e59b4624..3b21e9a15 100644 --- a/docs/documentation/quickstarts/nextjs.mdx +++ b/docs/documentation/quickstarts/nextjs.mdx @@ -2,6 +2,7 @@ title: "Next.js Quick Start" sidebarTitle: "Next.js" description: "Start creating Jobs in 5 minutes in your Next.js project." +icon: "N" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/nuxt.mdx b/docs/documentation/quickstarts/nuxt.mdx index c93cbf586..73f1f75c2 100644 --- a/docs/documentation/quickstarts/nuxt.mdx +++ b/docs/documentation/quickstarts/nuxt.mdx @@ -2,6 +2,7 @@ title: "Nuxt Quick Start" sidebarTitle: "Nuxt" description: "Start creating Jobs in 5 minutes in your Nuxt project." +icon: "triangle" --- diff --git a/docs/documentation/quickstarts/redwood.mdx b/docs/documentation/quickstarts/redwood.mdx index 831faf277..405679055 100644 --- a/docs/documentation/quickstarts/redwood.mdx +++ b/docs/documentation/quickstarts/redwood.mdx @@ -2,6 +2,7 @@ title: "Redwood Quick Start" sidebarTitle: "Redwood" description: "Start creating Jobs in 5 minutes in your Redwood project." +icon: "js" --- diff --git a/docs/documentation/quickstarts/remix.mdx b/docs/documentation/quickstarts/remix.mdx index dcdf7f6c5..b84679094 100644 --- a/docs/documentation/quickstarts/remix.mdx +++ b/docs/documentation/quickstarts/remix.mdx @@ -2,6 +2,7 @@ title: "Remix Quick Start" sidebarTitle: "Remix" description: "Start creating Jobs in 5 minutes in your Remix project." +icon: "r" --- This quick start guide will get you up and running with Trigger.dev. diff --git a/docs/documentation/quickstarts/sveltekit.mdx b/docs/documentation/quickstarts/sveltekit.mdx index 9a4a92956..0b20349e5 100644 --- a/docs/documentation/quickstarts/sveltekit.mdx +++ b/docs/documentation/quickstarts/sveltekit.mdx @@ -2,6 +2,7 @@ title: "SvelteKit Quick Start" sidebarTitle: "SvelteKit" description: "Start creating Jobs in 5 minutes in your SvelteKit project." +icon: "square-s" --- From b3b5852a88b3412ff47c8c7b0afff4c6b7e059de Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Thu, 19 Oct 2023 01:02:54 +0530 Subject: [PATCH 05/39] fixed icons --- docs/documentation/introduction.mdx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/documentation/introduction.mdx b/docs/documentation/introduction.mdx index a9f68d1bf..be19d0eff 100644 --- a/docs/documentation/introduction.mdx +++ b/docs/documentation/introduction.mdx @@ -54,7 +54,8 @@ We'd love to hear from you or give you a hand getting started. Here are some way + } href="https://twitter.com/triggerdotdev" color="#1DA1F2" > @@ -62,7 +63,15 @@ We'd love to hear from you or give you a hand getting started. Here are some way + + + + +} href="https://cal.com/team/triggerdotdev/founders-call" > Arrange a call with one of the founders. We can help answer questions, build API Integrations @@ -71,6 +80,7 @@ We'd love to hear from you or give you a hand getting started. Here are some way From bc7206d9815af15638b6c2faae5d9f29d8a25af3 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Thu, 19 Oct 2023 01:10:05 +0530 Subject: [PATCH 06/39] x --- docs/documentation/introduction.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/documentation/introduction.mdx b/docs/documentation/introduction.mdx index be19d0eff..606329c01 100644 --- a/docs/documentation/introduction.mdx +++ b/docs/documentation/introduction.mdx @@ -53,13 +53,13 @@ We'd love to hear from you or give you a hand getting started. Here are some way Trigger.dev. } href="https://twitter.com/triggerdotdev" color="#1DA1F2" > - Follow us on Twitter to get the latest updates and news. + Follow us on X(Twitter) to get the latest updates and news. Date: Thu, 19 Oct 2023 08:08:53 +0530 Subject: [PATCH 07/39] fix --- docs/documentation/introduction.mdx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/documentation/introduction.mdx b/docs/documentation/introduction.mdx index 606329c01..73141630c 100644 --- a/docs/documentation/introduction.mdx +++ b/docs/documentation/introduction.mdx @@ -53,25 +53,18 @@ We'd love to hear from you or give you a hand getting started. Here are some way Trigger.dev. } href="https://twitter.com/triggerdotdev" color="#1DA1F2" > - Follow us on X(Twitter) to get the latest updates and news. + Follow us on X (Twitter) to get the latest updates and news. - - - - -} + icon="phone" + iconType="solid" href="https://cal.com/team/triggerdotdev/founders-call" > Arrange a call with one of the founders. We can help answer questions, build API Integrations From c8aaea8ad0e484e7845d52ee8d1eced5818ea642 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 19 Oct 2023 11:18:19 +0100 Subject: [PATCH 08/39] Improvements: Gracefully shutdown to prevent locked jobs (#648) * WIP * Report ECS task info on startup and shutdown * Fixed lifecycle name * Re-add terminus * Require the build dir when http server is disabled * Remove unnecessary logs * Implement graceful shutdown in ZodWorker * Re-order some code * Increase the keepAliveTimeout to 65 seconds to prevent LB 502 errors --- apps/webapp/app/env.server.ts | 1 + apps/webapp/app/platform/zodWorker.server.ts | 32 + apps/webapp/app/services/worker.server.ts | 3 +- apps/webapp/package.json | 16 +- apps/webapp/server.ts | 3 + docker/scripts/entrypoint.sh | 4 +- pnpm-lock.yaml | 767 ++++++------------- 7 files changed, 279 insertions(+), 547 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 46f3f9e81..4e229834c 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({ EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), WORKER_ENABLED: z.string().default("true"), EXECUTION_WORKER_ENABLED: z.string().default("true"), + GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000), }); export type Environment = z.infer; diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index a275ad9e0..2a9e823ff 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -102,6 +102,7 @@ export type ZodWorkerOptions = { recurringTasks?: ZodRecurringTasks; cleanup?: ZodWorkerCleanupOptions; reporter?: ZodWorkerReporter; + shutdownTimeoutInMs?: number; }; export class ZodWorker { @@ -114,6 +115,8 @@ export class ZodWorker { #runner?: GraphileRunner; #cleanup: ZodWorkerCleanupOptions | undefined; #reporter?: ZodWorkerReporter; + #shutdownTimeoutInMs?: number; + #shuttingDown = false; constructor(options: ZodWorkerOptions) { this.#name = options.name; @@ -124,6 +127,7 @@ export class ZodWorker { this.#recurringTasks = options.recurringTasks; this.#cleanup = options.cleanup; this.#reporter = options.reporter; + this.#shutdownTimeoutInMs = options.shutdownTimeoutInMs ?? 60000; // default to 60 seconds } get graphileWorkerSchema() { @@ -143,6 +147,7 @@ export class ZodWorker { this.#runner = await graphileRun({ ...this.#runnerOptions, + noHandleSignals: true, taskList: this.#createTaskListFromTasks(), parsedCronItems, }); @@ -199,9 +204,36 @@ export class ZodWorker { this.#logDebug("stop"); }); + process.on("SIGTERM", this._handleSignal("SIGTERM").bind(this)); + process.on("SIGINT", this._handleSignal("SIGINT").bind(this)); + return true; } + private _handleSignal(signal: string) { + return () => { + if (this.#shuttingDown) { + return; + } + + this.#shuttingDown = true; + + if (this.#shutdownTimeoutInMs) { + setTimeout(() => { + this.#logDebug("Shutdown timeout reached, exiting process"); + + process.exit(0); + }, this.#shutdownTimeoutInMs); + } + + this.#logDebug(`Received ${signal}, shutting down zodWorker...`); + + this.stop().finally(() => { + this.#logDebug("zodWorker stopped"); + }); + }; + } + public async stop() { await this.#runner?.stop(); } diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index 069685b0a..cb9b4d6b0 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -12,7 +12,6 @@ import { DeliverEventService } from "./events/deliverEvent.server"; import { InvokeDispatcherService } from "./events/invokeDispatcher.server"; import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server"; import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server"; -import { logger } from "./logger.server"; import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server"; import { PerformRunExecutionV1Service } from "./runs/performRunExecutionV1.server"; import { PerformRunExecutionV2Service } from "./runs/performRunExecutionV2.server"; @@ -148,6 +147,7 @@ function getWorkerQueue() { schema: env.WORKER_SCHEMA, maxPoolSize: env.WORKER_CONCURRENCY, }, + shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT, schema: workerCatalog, recurringTasks: { // Run this every 5 minutes @@ -338,6 +338,7 @@ function getExecutionWorkerQueue() { schema: env.WORKER_SCHEMA, maxPoolSize: env.EXECUTION_WORKER_CONCURRENCY, }, + shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT, schema: executionWorkerCatalog, tasks: { performRunExecution: { diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 564a85867..5cf5eacef 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -55,11 +55,11 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.5", - "@remix-run/express": "1.19.2-pre.0", - "@remix-run/node": "1.19.2-pre.0", - "@remix-run/react": "1.19.2-pre.0", - "@remix-run/serve": "1.19.2-pre.0", - "@remix-run/server-runtime": "1.19.2-pre.0", + "@remix-run/express": "1.19.2", + "@remix-run/node": "1.19.2", + "@remix-run/react": "1.19.2", + "@remix-run/serve": "1.19.2", + "@remix-run/server-runtime": "1.19.2", "@team-plain/typescript-sdk": "^2.2.0", "@trigger.dev/companyicons": "^1.5.14", "@trigger.dev/core": "workspace:*", @@ -117,9 +117,9 @@ "zod-validation-error": "^1.5.0" }, "devDependencies": { - "@remix-run/dev": "1.19.2-pre.0", - "@remix-run/eslint-config": "1.19.2-pre.0", - "@remix-run/testing": "^1.19.2-pre.0", + "@remix-run/dev": "1.19.2", + "@remix-run/eslint-config": "1.19.2", + "@remix-run/testing": "^1.19.2", "@storybook/addon-backgrounds": "^7.0.7", "@storybook/addon-docs": "^7.0.12", "@storybook/addon-essentials": "^7.0.7", diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index 55c9fc530..e40b0a5bc 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -61,6 +61,8 @@ if (process.env.HTTP_SERVER_DISABLED !== "true") { console.log(`✅ app ready: http://localhost:${port}`); }); + server.keepAliveTimeout = 65 * 1000; + // Handle shutdowns gracefully createTerminus(server, { signals: ["SIGINT", "SIGTERM"], @@ -80,6 +82,7 @@ if (process.env.HTTP_SERVER_DISABLED !== "true") { }, }); } else { + require(BUILD_DIR); console.log(`✅ app ready (skipping http server)`); } diff --git a/docker/scripts/entrypoint.sh b/docker/scripts/entrypoint.sh index 449ee43be..fa33a8f10 100755 --- a/docker/scripts/entrypoint.sh +++ b/docker/scripts/entrypoint.sh @@ -14,4 +14,6 @@ cp node_modules/@prisma/engines/*.node apps/webapp/prisma/ pnpm --filter webapp db:seed cd /triggerdotdev/apps/webapp -exec dumb-init pnpm run start:local +# exec dumb-init pnpm run start:local +NODE_PATH='/triggerdotdev/node_modules/.pnpm/node_modules' exec dumb-init node --max-old-space-size=8192 ./build/server.js + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3fc4bb66..3738f06f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,14 +80,14 @@ importers: '@radix-ui/react-switch': ^1.0.3 '@radix-ui/react-tabs': ^1.0.3 '@radix-ui/react-tooltip': ^1.0.5 - '@remix-run/dev': 1.19.2-pre.0 - '@remix-run/eslint-config': 1.19.2-pre.0 - '@remix-run/express': 1.19.2-pre.0 - '@remix-run/node': 1.19.2-pre.0 - '@remix-run/react': 1.19.2-pre.0 - '@remix-run/serve': 1.19.2-pre.0 - '@remix-run/server-runtime': 1.19.2-pre.0 - '@remix-run/testing': ^1.19.2-pre.0 + '@remix-run/dev': 1.19.2 + '@remix-run/eslint-config': 1.19.2 + '@remix-run/express': 1.19.2 + '@remix-run/node': 1.19.2 + '@remix-run/react': 1.19.2 + '@remix-run/serve': 1.19.2 + '@remix-run/server-runtime': 1.19.2 + '@remix-run/testing': ^1.19.2 '@storybook/addon-backgrounds': ^7.0.7 '@storybook/addon-docs': ^7.0.12 '@storybook/addon-essentials': ^7.0.7 @@ -226,11 +226,11 @@ importers: '@radix-ui/react-switch': 1.0.3_daadhm4ohxobgnrt365as5bhny '@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-tooltip': 1.0.5_bwbutfx4xj25dewzmxso6o3wga - '@remix-run/express': 1.19.2-pre.0_express@4.18.2 - '@remix-run/node': 1.19.2-pre.0 - '@remix-run/react': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y - '@remix-run/serve': 1.19.2-pre.0 - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/express': 1.19.2_express@4.18.2 + '@remix-run/node': 1.19.2 + '@remix-run/react': 1.19.2_biqbaboplfbrettd7655fr4n2y + '@remix-run/serve': 1.19.2 + '@remix-run/server-runtime': 1.19.2 '@team-plain/typescript-sdk': 2.2.0 '@trigger.dev/companyicons': 1.5.14_biqbaboplfbrettd7655fr4n2y '@trigger.dev/core': link:../../packages/core @@ -268,11 +268,11 @@ importers: react-hotkeys-hook: 4.4.1_biqbaboplfbrettd7655fr4n2y react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y recharts: 2.8.0_v2m5e27vhdewzwhryxwfaorcca - remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y - remix-auth-email-link: 1.5.2_xmjsiulzsxcc3znmuhq3turs2q - remix-auth-github: 1.3.0_xmjsiulzsxcc3znmuhq3turs2q - remix-typedjson: 0.1.7_bhtgrpeaoe6kbm4hb4gzl6x7c4 - remix-utils: 6.0.0_7krs2yfztxu2oblf77wwc4rpoe + remix-auth: 3.4.0_ya7xgmpozdkweczbsj2asvumvm + remix-auth-email-link: 1.5.2_3kyxpfn573t2hovk377zjamxda + remix-auth-github: 1.3.0_3kyxpfn573t2hovk377zjamxda + remix-typedjson: 0.1.7_uyy3oyfmtzakxsapatosuzqil4 + remix-utils: 6.0.0_jtxmz5feu54a7fcxnhi2w5jyki semver: 7.5.0 simple-oauth2: 5.0.0 simplur: 3.0.1 @@ -287,9 +287,9 @@ importers: zod-error: 1.5.0 zod-validation-error: 1.5.0_zod@3.22.3 devDependencies: - '@remix-run/dev': 1.19.2-pre.0_36n2i74sizt32vwpdxc4husnkq - '@remix-run/eslint-config': 1.19.2-pre.0_ol4nhuzbuflsbzk2mijpqykzba - '@remix-run/testing': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y + '@remix-run/dev': 1.19.2_uvzmt2w75nvrltrkqo4r7vh6eq + '@remix-run/eslint-config': 1.19.2_ol4nhuzbuflsbzk2mijpqykzba + '@remix-run/testing': 1.19.3_biqbaboplfbrettd7655fr4n2y '@storybook/addon-backgrounds': 7.0.9_biqbaboplfbrettd7655fr4n2y '@storybook/addon-docs': 7.0.12_biqbaboplfbrettd7655fr4n2y '@storybook/addon-essentials': 7.0.9_biqbaboplfbrettd7655fr4n2y @@ -1723,11 +1723,6 @@ packages: '@babel/highlight': 7.22.13 chalk: 2.4.2 - /@babel/compat-data/7.21.7: - resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/compat-data/7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} @@ -1814,14 +1809,14 @@ packages: semver: 6.3.1 dev: true - /@babel/eslint-parser/7.21.8_mxgwiyfjuazeomuuf56n24ufpy: + /@babel/eslint-parser/7.21.8_6ukzja5aifaykyuthozjethhji: resolution: {integrity: sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': '>=7.11.0' eslint: ^7.5.0 || ^8.0.0 dependencies: - '@babel/core': 7.21.8 + '@babel/core': 7.22.17 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.31.0 eslint-visitor-keys: 2.1.0 @@ -1832,7 +1827,7 @@ packages: resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.17 '@jridgewell/gen-mapping': 0.3.2 '@jridgewell/trace-mapping': 0.3.19 jsesc: 2.5.2 @@ -1868,20 +1863,6 @@ packages: '@babel/types': 7.22.17 dev: true - /@babel/helper-compilation-targets/7.21.5_@babel+core@7.20.12: - resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.22.9 - '@babel/core': 7.20.12 - '@babel/helper-validator-option': 7.22.15 - browserslist: 4.21.10 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-compilation-targets/7.21.5_@babel+core@7.21.8: resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} engines: {node: '>=6.9.0'} @@ -1896,20 +1877,6 @@ packages: semver: 6.3.1 dev: true - /@babel/helper-compilation-targets/7.21.5_@babel+core@7.22.17: - resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.22.9 - '@babel/core': 7.22.17 - '@babel/helper-validator-option': 7.22.15 - browserslist: 4.21.10 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-compilation-targets/7.22.15: resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} engines: {node: '>=6.9.0'} @@ -2194,11 +2161,6 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-plugin-utils/7.21.5: - resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-plugin-utils/7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} @@ -2306,11 +2268,6 @@ packages: resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option/7.21.0: - resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-option/7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} engines: {node: '>=6.9.0'} @@ -2361,7 +2318,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.21.5 + '@babel/types': 7.22.17 dev: true /@babel/parser/7.22.16: @@ -2440,6 +2397,7 @@ packages: /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.20.12: resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2455,6 +2413,7 @@ packages: /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.21.8: resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2470,6 +2429,7 @@ packages: /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.22.17: resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2485,6 +2445,7 @@ packages: /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2498,6 +2459,7 @@ packages: /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2511,6 +2473,7 @@ packages: /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2524,6 +2487,7 @@ packages: /@babel/plugin-proposal-class-static-block/7.21.0_@babel+core@7.20.12: resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 dependencies: @@ -2538,6 +2502,7 @@ packages: /@babel/plugin-proposal-class-static-block/7.21.0_@babel+core@7.21.8: resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 dependencies: @@ -2552,6 +2517,7 @@ packages: /@babel/plugin-proposal-class-static-block/7.21.0_@babel+core@7.22.17: resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 dependencies: @@ -2566,6 +2532,7 @@ packages: /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2577,6 +2544,7 @@ packages: /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2588,6 +2556,7 @@ packages: /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2599,6 +2568,7 @@ packages: /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.12: resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2610,6 +2580,7 @@ packages: /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.21.8: resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2621,6 +2592,7 @@ packages: /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.22.17: resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2632,6 +2604,7 @@ packages: /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2643,6 +2616,7 @@ packages: /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2654,6 +2628,7 @@ packages: /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2665,6 +2640,7 @@ packages: /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.20.12: resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2676,6 +2652,7 @@ packages: /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.21.8: resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2687,6 +2664,7 @@ packages: /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.22.17: resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2698,6 +2676,7 @@ packages: /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2709,6 +2688,7 @@ packages: /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2720,6 +2700,7 @@ packages: /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2731,6 +2712,7 @@ packages: /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2742,6 +2724,7 @@ packages: /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2753,6 +2736,7 @@ packages: /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2764,6 +2748,7 @@ packages: /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.20.12: resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2778,6 +2763,7 @@ packages: /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.21.8: resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2792,6 +2778,7 @@ packages: /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.22.17: resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2806,6 +2793,7 @@ packages: /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2817,6 +2805,7 @@ packages: /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2828,6 +2817,7 @@ packages: /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2839,6 +2829,7 @@ packages: /@babel/plugin-proposal-optional-chaining/7.21.0_@babel+core@7.20.12: resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2851,6 +2842,7 @@ packages: /@babel/plugin-proposal-optional-chaining/7.21.0_@babel+core@7.21.8: resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2863,6 +2855,7 @@ packages: /@babel/plugin-proposal-optional-chaining/7.21.0_@babel+core@7.22.17: resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2875,6 +2868,7 @@ packages: /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2888,6 +2882,7 @@ packages: /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2901,6 +2896,7 @@ packages: /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2914,6 +2910,7 @@ packages: /@babel/plugin-proposal-private-property-in-object/7.21.0_@babel+core@7.20.12: resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2929,6 +2926,7 @@ packages: /@babel/plugin-proposal-private-property-in-object/7.21.0_@babel+core@7.21.8: resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2944,6 +2942,7 @@ packages: /@babel/plugin-proposal-private-property-in-object/7.21.0_@babel+core@7.22.17: resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2959,6 +2958,7 @@ packages: /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2970,6 +2970,7 @@ packages: /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.21.8: resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -2981,6 +2982,7 @@ packages: /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -3239,15 +3241,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx/7.18.6: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.12: resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} engines: {node: '>=6.9.0'} @@ -3268,36 +3261,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.22.17: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - - /@babel/plugin-syntax-jsx/7.21.4_@babel+core@7.21.8: - resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: true - - /@babel/plugin-syntax-jsx/7.21.4_@babel+core@7.22.17: - resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.21.5 - dev: true - /@babel/plugin-syntax-jsx/7.22.5: resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} @@ -3307,16 +3270,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx/7.22.5_@babel+core@7.21.8: - resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-jsx/7.22.5_@babel+core@7.22.17: resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} @@ -3548,16 +3501,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript/7.21.4_@babel+core@7.21.8: - resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - dev: true - /@babel/plugin-syntax-typescript/7.21.4_@babel+core@7.22.17: resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} engines: {node: '>=6.9.0'} @@ -3565,7 +3508,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 dev: true /@babel/plugin-transform-arrow-functions/7.21.5_@babel+core@7.20.12: @@ -4378,16 +4321,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.21.8: - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} engines: {node: '>=6.9.0'} @@ -4407,16 +4340,6 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.15 dev: true - /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.21.8: - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/plugin-transform-react-jsx': 7.22.15_@babel+core@7.21.8 - dev: true - /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} engines: {node: '>=6.9.0'} @@ -4427,19 +4350,6 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.15_@babel+core@7.22.17 dev: true - /@babel/plugin-transform-react-jsx/7.20.7: - resolution: {integrity: sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6 - '@babel/types': 7.20.7 - dev: true - /@babel/plugin-transform-react-jsx/7.20.7_@babel+core@7.20.12: resolution: {integrity: sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ==} engines: {node: '>=6.9.0'} @@ -4468,20 +4378,6 @@ packages: '@babel/types': 7.20.7 dev: true - /@babel/plugin-transform-react-jsx/7.20.7_@babel+core@7.22.17: - resolution: {integrity: sha512-Tfq7qqD+tRj3EoDhY00nn2uP2hsRxgYGi5mLQ5TimKav0a9Lrpd4deE+fcLXU8zFYRjlKPHZhpCvfEA6qnBxqQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.22.17 - '@babel/types': 7.20.7 - dev: true - /@babel/plugin-transform-react-jsx/7.22.15: resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} engines: {node: '>=6.9.0'} @@ -4495,20 +4391,6 @@ packages: '@babel/types': 7.22.17 dev: true - /@babel/plugin-transform-react-jsx/7.22.15_@babel+core@7.21.8: - resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx': 7.22.5_@babel+core@7.21.8 - '@babel/types': 7.22.17 - dev: true - /@babel/plugin-transform-react-jsx/7.22.15_@babel+core@7.22.17: resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} engines: {node: '>=6.9.0'} @@ -4532,17 +4414,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.21.8: - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} engines: {node: '>=6.9.0'} @@ -4770,21 +4641,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typescript/7.21.3_@babel+core@7.21.8: - resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.21.8_@babel+core@7.21.8 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.21.8 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-typescript/7.21.3_@babel+core@7.22.17: resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} @@ -4869,11 +4725,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.7 + '@babel/compat-data': 7.22.9 '@babel/core': 7.20.12 - '@babel/helper-compilation-targets': 7.21.5_@babel+core@7.20.12 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.20.12 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.20.12 '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.20.12 @@ -4940,7 +4796,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.21.5_@babel+core@7.20.12 '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12 '@babel/preset-modules': 0.1.5_@babel+core@7.20.12 - '@babel/types': 7.21.5 + '@babel/types': 7.22.17 babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.12 babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12 @@ -4956,11 +4812,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.7 + '@babel/compat-data': 7.22.9 '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.21.5_@babel+core@7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.21.8 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.21.8 '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.21.8 @@ -5027,7 +4883,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.21.5_@babel+core@7.21.8 '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.21.8 '@babel/preset-modules': 0.1.5_@babel+core@7.21.8 - '@babel/types': 7.21.5 + '@babel/types': 7.22.17 babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.21.8 babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.21.8 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.21.8 @@ -5043,11 +4899,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.7 + '@babel/compat-data': 7.22.9 '@babel/core': 7.22.17 - '@babel/helper-compilation-targets': 7.21.5_@babel+core@7.22.17 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.22.17 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.22.17 '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.22.17 @@ -5114,7 +4970,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.21.5_@babel+core@7.22.17 '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.22.17 '@babel/preset-modules': 0.1.5_@babel+core@7.22.17 - '@babel/types': 7.21.5 + '@babel/types': 7.22.17 babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.22.17 babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.22.17 babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.22.17 @@ -5192,29 +5048,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-transform-react-display-name': 7.18.6 - '@babel/plugin-transform-react-jsx': 7.20.7 + '@babel/plugin-transform-react-jsx': 7.22.15 '@babel/plugin-transform-react-jsx-development': 7.18.6 '@babel/plugin-transform-react-pure-annotations': 7.18.6 dev: true - /@babel/preset-react/7.18.6_@babel+core@7.21.8: - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.21.8 - '@babel/plugin-transform-react-jsx': 7.20.7_@babel+core@7.21.8 - '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.21.8 - '@babel/plugin-transform-react-pure-annotations': 7.18.6_@babel+core@7.21.8 - dev: true - /@babel/preset-react/7.18.6_@babel+core@7.22.17: resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} engines: {node: '>=6.9.0'} @@ -5222,30 +5063,14 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.22.17 - '@babel/plugin-transform-react-jsx': 7.20.7_@babel+core@7.22.17 + '@babel/plugin-transform-react-jsx': 7.22.15_@babel+core@7.22.17 '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.22.17 '@babel/plugin-transform-react-pure-annotations': 7.18.6_@babel+core@7.22.17 dev: true - /@babel/preset-typescript/7.21.5_@babel+core@7.21.8: - resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-syntax-jsx': 7.21.4_@babel+core@7.21.8 - '@babel/plugin-transform-modules-commonjs': 7.21.5_@babel+core@7.21.8 - '@babel/plugin-transform-typescript': 7.21.3_@babel+core@7.21.8 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/preset-typescript/7.21.5_@babel+core@7.22.17: resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==} engines: {node: '>=6.9.0'} @@ -5253,9 +5078,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-syntax-jsx': 7.21.4_@babel+core@7.22.17 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-syntax-jsx': 7.22.5_@babel+core@7.22.17 '@babel/plugin-transform-modules-commonjs': 7.21.5_@babel+core@7.22.17 '@babel/plugin-transform-typescript': 7.21.3_@babel+core@7.22.17 transitivePeerDependencies: @@ -5309,14 +5134,14 @@ packages: resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.5 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.22.15 '@babel/helper-environment-visitor': 7.21.5 '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.8 - '@babel/types': 7.21.5 + '@babel/parser': 7.22.16 + '@babel/types': 7.22.17 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: @@ -10658,28 +10483,28 @@ packages: engines: {node: '>=14.0.0'} dev: false - /@remix-run/dev/1.19.2-pre.0_36n2i74sizt32vwpdxc4husnkq: - resolution: {integrity: sha512-8s7g8jLueKcIr5Gb7qvZRJVzfHx2KqyApGYW55LrQ63kNwZwuKHglYSMoaQGB4xUhX2cfmzW7pIcunfbh3SNyA==} + /@remix-run/dev/1.19.2_uvzmt2w75nvrltrkqo4r7vh6eq: + resolution: {integrity: sha512-wAY3AN0xPJB01wi9RqHG1mV3W0M915XgudcLtJlWDbVppY3kGr7gP0/1sAz5onyEX8Z/+1vvVVU9jn59G8PIcA==} engines: {node: '>=14.0.0'} hasBin: true peerDependencies: - '@remix-run/serve': ^1.19.2-pre.0 + '@remix-run/serve': ^1.19.2 peerDependenciesMeta: '@remix-run/serve': optional: true dependencies: - '@babel/core': 7.21.8 - '@babel/generator': 7.21.5 - '@babel/parser': 7.21.8 - '@babel/plugin-syntax-jsx': 7.21.4_@babel+core@7.21.8 - '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.21.8 - '@babel/preset-env': 7.21.5_@babel+core@7.21.8 - '@babel/preset-typescript': 7.21.5_@babel+core@7.21.8 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 + '@babel/core': 7.22.17 + '@babel/generator': 7.22.15 + '@babel/parser': 7.22.16 + '@babel/plugin-syntax-jsx': 7.22.5_@babel+core@7.22.17 + '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.22.17 + '@babel/preset-env': 7.21.5_@babel+core@7.22.17 + '@babel/preset-typescript': 7.21.5_@babel+core@7.22.17 + '@babel/traverse': 7.22.17 + '@babel/types': 7.22.17 '@npmcli/package-json': 2.0.0 - '@remix-run/serve': 1.19.2-pre.0 - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/serve': 1.19.2 + '@remix-run/server-runtime': 1.19.2 '@vanilla-extract/integration': 6.2.1_@types+node@18.11.18 arg: 5.0.2 cacache: 15.3.0 @@ -10695,21 +10520,21 @@ packages: fs-extra: 10.1.0 get-port: 5.1.1 gunzip-maybe: 1.4.2 - inquirer: 8.2.5 + inquirer: 8.2.6 jsesc: 3.0.2 json5: 2.2.3 lodash: 4.17.21 lodash.debounce: 4.0.8 - minimatch: 9.0.0 + minimatch: 9.0.3 node-fetch: 2.6.12 ora: 5.4.1 picocolors: 1.0.0 picomatch: 2.3.1 pidtree: 0.6.0 - postcss: 8.4.27 - postcss-discard-duplicates: 5.1.0_postcss@8.4.27 - postcss-load-config: 4.0.1_xf4uokkt3yp55pilbqb2sbk4eu - postcss-modules: 6.0.0_postcss@8.4.27 + postcss: 8.4.29 + postcss-discard-duplicates: 5.1.0_postcss@8.4.29 + postcss-load-config: 4.0.1_ntcifew6f27c4l7dh5q6i7owpq + postcss-modules: 6.0.0_postcss@8.4.29 prettier: 2.8.8 pretty-ms: 7.0.1 proxy-agent: 6.3.0 @@ -10720,7 +10545,7 @@ packages: semver: 7.5.4 sort-package-json: 1.57.0 tar-fs: 2.1.1 - tsconfig-paths: 4.1.2 + tsconfig-paths: 4.2.0 ws: 7.5.9 xdm: 2.1.0 transitivePeerDependencies: @@ -10820,8 +10645,8 @@ packages: - utf-8-validate dev: true - /@remix-run/eslint-config/1.19.2-pre.0_ol4nhuzbuflsbzk2mijpqykzba: - resolution: {integrity: sha512-DnMiKi5B2ZWehcVjm0fY1KnE+/oqphnLb0Pq2ueac16WcBqW1S6HnrX4bv+6ZXuAqRVcv0eooOem5uJTx0DWfw==} + /@remix-run/eslint-config/1.19.2_ol4nhuzbuflsbzk2mijpqykzba: + resolution: {integrity: sha512-YvB3uulbSdwsvkcQSUPXtZG2AbVq1v49jVf5IsPj0xApNcRMtkk4Y/6cethbtHJOIpHhbAnJyPtoHOVvlRWQHQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^8.0.0 @@ -10831,9 +10656,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.21.8 - '@babel/eslint-parser': 7.21.8_mxgwiyfjuazeomuuf56n24ufpy - '@babel/preset-react': 7.18.6_@babel+core@7.21.8 + '@babel/core': 7.22.17 + '@babel/eslint-parser': 7.21.8_6ukzja5aifaykyuthozjethhji + '@babel/preset-react': 7.18.6_@babel+core@7.22.17 '@rushstack/eslint-patch': 1.2.0 '@typescript-eslint/eslint-plugin': 5.59.6_bhjpu5ld5qmewts22wsycix4mi '@typescript-eslint/parser': 5.59.6_iukboom6ndih5an6iafl45j2fe @@ -10892,13 +10717,13 @@ packages: - supports-color dev: true - /@remix-run/express/1.19.2-pre.0_express@4.18.2: - resolution: {integrity: sha512-TJij9DkC4fnls9q1dz8eJ+IjUKdrwd2edz7TLxTb/To/3YUyxJLz7pom3fOQHZ7L0Ynwi3TuEcmnSlkrkztWjw==} + /@remix-run/express/1.19.2_express@4.18.2: + resolution: {integrity: sha512-uaKv2Yxfl545QmE2Ou/EYkGUycKYJDZ98kCgI0+bBgX591IE2rzhmSeExCerLXI5rDTKO4YikPTyxiUqZncPiA==} engines: {node: '>=14.0.0'} peerDependencies: express: ^4.17.1 dependencies: - '@remix-run/node': 1.19.2-pre.0 + '@remix-run/node': 1.19.2 express: 4.18.2 /@remix-run/express/1.19.3_express@4.18.2: @@ -10910,11 +10735,11 @@ packages: '@remix-run/node': 1.19.3 express: 4.18.2 - /@remix-run/node/1.19.2-pre.0: - resolution: {integrity: sha512-/Isbggw/fSbFeTy0hoL8gRwS8FCmrFyi6q3zc8rV8s7UsV70VzB+9QzWQHanCMveZFFT1FtkoON8s/u0vT1Wvw==} + /@remix-run/node/1.19.2: + resolution: {integrity: sha512-ceoDsnG4SQHxqV6aq6ZTxiBr90mnAsQBL3D7kfNswsgPS/d0gERV1hIC9svDW2NuDeMI/U1CXCM08XZYrqLfRQ==} engines: {node: '>=14.0.0'} dependencies: - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/server-runtime': 1.19.2 '@remix-run/web-fetch': 4.3.6 '@remix-run/web-file': 3.0.3 '@remix-run/web-stream': 1.0.4 @@ -10938,8 +10763,8 @@ packages: source-map-support: 0.5.21 stream-slice: 0.1.2 - /@remix-run/react/1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-eO8HnotwHwqfazyXol71W1mMZZ2lE/zVt4xpAst7UjVvOhmXbolTUdK4Hru8Q0SDuaE53feux3KsFgBvug/qpg==} + /@remix-run/react/1.19.2_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-Q3abYOR02+8wg3mNGYHafGqZUwVv19Et+2ti/8CKTOvY+/rIRppEhDqPCYaiAvNJ7kMcvTdCYKbCi6bU9ElV0g==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8.0' @@ -10949,6 +10774,7 @@ packages: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-router-dom: 6.14.2_biqbaboplfbrettd7655fr4n2y + dev: false /@remix-run/react/1.19.3_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-iP37MZ+oG1n4kv4rX77pKT/knra51lNwKo5tinPPF0SuNJhF3+XjWo5nwEjvisKTXLZ/OHeicinhgX2JHHdDvA==} @@ -10961,7 +10787,6 @@ packages: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-router-dom: 6.14.2_biqbaboplfbrettd7655fr4n2y - dev: false /@remix-run/router/1.7.2: resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==} @@ -10972,16 +10797,17 @@ packages: engines: {node: '>=14.0.0'} dev: true - /@remix-run/serve/1.19.2-pre.0: - resolution: {integrity: sha512-5rVnhsNO5X1lKJCcWPmOYAl30XuNuUpbAFPpUC654YqMwuulFSnY879jAFPUzeIf4Rarmb50LLkEwizcRk8vuw==} + /@remix-run/serve/1.19.2: + resolution: {integrity: sha512-8ypS7zdK8hlAyjD+I8MNwqHV3m+4qek2UryqCFAJU6/nfjRmCNo1TTItR9NuQSqNiWzfW2J6TyA47NYtwGb7Dw==} engines: {node: '>=14.0.0'} hasBin: true dependencies: - '@remix-run/express': 1.19.2-pre.0_express@4.18.2 - '@remix-run/node': 1.19.2-pre.0 + '@remix-run/express': 1.19.2_express@4.18.2 + '@remix-run/node': 1.19.2 compression: 1.7.4 express: 4.18.2 morgan: 1.10.0 + source-map-support: 0.5.21 transitivePeerDependencies: - supports-color @@ -10999,15 +10825,15 @@ packages: transitivePeerDependencies: - supports-color - /@remix-run/server-runtime/1.19.2-pre.0: - resolution: {integrity: sha512-e7qZ5z/Yj8jYw7uJy5LqTV33RPGN96HymFYZpDwWuI9rffOmyO9tt5xcfc44g18G5lg36449pDjhQlSLHc1a8A==} + /@remix-run/server-runtime/1.19.2: + resolution: {integrity: sha512-d+oOaQlBPFHPum1nstJCntRgwgi4fzijW51yPCo3ljUxIrjlDjutX2GCQ72Apd6Ffpd11/6nEq+B90EY+Ala9Q==} engines: {node: '>=14.0.0'} dependencies: '@remix-run/router': 1.7.2 '@types/cookie': 0.4.1 '@web3-storage/multipart-parser': 1.0.0 cookie: 0.4.2 - set-cookie-parser: 2.5.1 + set-cookie-parser: 2.6.0 source-map: 0.7.4 /@remix-run/server-runtime/1.19.3: @@ -11040,14 +10866,14 @@ packages: typescript: 4.9.5 dev: true - /@remix-run/testing/1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-QK8nZKsgFOXZez0FmbINTgNfsbwKuBFLYQTUt2EtvJ/cNTfReiVyXxjG7U320Ghb98E5/WZS2pMpHbm3U+jcbg==} + /@remix-run/testing/1.19.3_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-qO485TWDxHUnWuWX8PotBdq4xa5W3WUQ+iYvoWNCyILw6iJG0WeBTgd2qN44GFEOE1Hd80RqDXVuAlTkG33rCA==} engines: {node: '>=14.0.0'} peerDependencies: react: ^17.0.0 || ^18.0.0 dependencies: - '@remix-run/node': 1.19.2-pre.0 - '@remix-run/react': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y + '@remix-run/node': 1.19.3 + '@remix-run/react': 1.19.3_biqbaboplfbrettd7655fr4n2y '@remix-run/router': 1.7.2 react: 18.2.0 react-router-dom: 6.14.2_biqbaboplfbrettd7655fr4n2y @@ -14668,7 +14494,7 @@ packages: cssesc: 3.0.0 csstype: 3.1.1 deep-object-diff: 1.1.9 - deepmerge: 4.2.2 + deepmerge: 4.3.1 media-query-parser: 2.0.2 outdent: 0.8.0 dev: true @@ -14676,8 +14502,8 @@ packages: /@vanilla-extract/integration/6.2.1: resolution: {integrity: sha512-+xYJz07G7TFAMZGrOqArOsURG+xcYvqctujEkANjw2McCBvGEK505RxQqOuNiA9Mi9hgGdNp2JedSa94f3eoLg==} dependencies: - '@babel/core': 7.21.8 - '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.21.8 + '@babel/core': 7.22.17 + '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.22.17 '@vanilla-extract/babel-plugin-debug-ids': 1.0.2 '@vanilla-extract/css': 1.11.0 esbuild: 0.17.6 @@ -14685,7 +14511,7 @@ packages: find-up: 5.0.0 javascript-stringify: 2.1.0 lodash: 4.17.21 - mlly: 1.1.1 + mlly: 1.4.2 outdent: 0.8.0 vite: 4.4.9 vite-node: 0.28.5 @@ -14703,8 +14529,8 @@ packages: /@vanilla-extract/integration/6.2.1_@types+node@18.11.18: resolution: {integrity: sha512-+xYJz07G7TFAMZGrOqArOsURG+xcYvqctujEkANjw2McCBvGEK505RxQqOuNiA9Mi9hgGdNp2JedSa94f3eoLg==} dependencies: - '@babel/core': 7.21.8 - '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.21.8 + '@babel/core': 7.22.17 + '@babel/plugin-syntax-typescript': 7.21.4_@babel+core@7.22.17 '@vanilla-extract/babel-plugin-debug-ids': 1.0.2 '@vanilla-extract/css': 1.11.0 esbuild: 0.17.6 @@ -14712,7 +14538,7 @@ packages: find-up: 5.0.0 javascript-stringify: 2.1.0 lodash: 4.17.21 - mlly: 1.1.1 + mlly: 1.4.2 outdent: 0.8.0 vite: 4.4.9_@types+node@18.11.18 vite-node: 0.28.5_@types+node@18.11.18 @@ -15284,12 +15110,12 @@ packages: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} dependencies: deep-equal: 2.2.0 + dev: true /aria-query/5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} dependencies: dequal: 2.0.3 - dev: true /arr-diff/1.1.0: resolution: {integrity: sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==} @@ -15665,16 +15491,10 @@ packages: - debug dev: false - /axobject-query/3.1.1: - resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} - dependencies: - deep-equal: 2.2.0 - /axobject-query/3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: dequal: 2.0.3 - dev: true /b4a/1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} @@ -17577,6 +17397,7 @@ packages: which-boxed-primitive: 1.0.2 which-collection: 1.0.1 which-typed-array: 1.1.9 + dev: true /deep-extend/0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} @@ -18035,6 +17856,7 @@ packages: dependencies: graceful-fs: 4.2.10 tapable: 2.2.1 + dev: true /enhanced-resolve/5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} @@ -18042,7 +17864,6 @@ packages: dependencies: graceful-fs: 4.2.10 tapable: 2.2.1 - dev: true /enquirer/2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -18147,6 +17968,7 @@ packages: is-set: 2.0.2 is-string: 1.0.7 isarray: 2.0.5 + dev: true /es-module-lexer/1.3.1: resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} @@ -18817,8 +18639,8 @@ packages: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: debug: 3.2.7 - is-core-module: 2.11.0 - resolve: 1.22.2 + is-core-module: 2.13.0 + resolve: 1.22.4 transitivePeerDependencies: - supports-color @@ -18830,13 +18652,13 @@ packages: eslint-plugin-import: '*' dependencies: debug: 4.3.4 - enhanced-resolve: 5.13.0 + enhanced-resolve: 5.15.0 eslint: 8.42.0 eslint-module-utils: 2.7.4_pxhjym6joapyaysotvqp5q6kau eslint-plugin-import: 2.27.5_dclhgbuu3rymxzhsuuyv72p2ym get-tsconfig: 4.5.0 globby: 13.1.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 synckit: 0.8.5 transitivePeerDependencies: @@ -18854,13 +18676,13 @@ packages: eslint-plugin-import: '*' dependencies: debug: 4.3.4 - enhanced-resolve: 5.13.0 + enhanced-resolve: 5.15.0 eslint: 8.45.0 eslint-module-utils: 2.7.4_mcdyrqs6peaairrn7md524c74a eslint-plugin-import: 2.27.5_xpt3ce3kmhzhmuk3dcuwe6u2pe get-tsconfig: 4.5.0 globby: 13.1.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 synckit: 0.8.5 transitivePeerDependencies: @@ -18877,13 +18699,13 @@ packages: eslint-plugin-import: '*' dependencies: debug: 4.3.4 - enhanced-resolve: 5.13.0 + enhanced-resolve: 5.15.0 eslint: 8.31.0 eslint-module-utils: 2.7.4_hhwxwo5vg2mzr5fq3eei7kqaye eslint-plugin-import: 2.27.5_4nmwrqeucdooykidvg7oplsu44 get-tsconfig: 4.5.0 globby: 13.1.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 synckit: 0.8.5 transitivePeerDependencies: @@ -18973,7 +18795,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 + '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 debug: 3.2.7 eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -19035,11 +18857,11 @@ packages: eslint-import-resolver-node: 0.3.7 eslint-module-utils: 2.7.4_hhwxwo5vg2mzr5fq3eei7kqaye has: 1.0.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 minimatch: 3.1.2 object.values: 1.1.6 - resolve: 1.22.2 + resolve: 1.22.4 semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: @@ -19058,7 +18880,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 + '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -19068,11 +18890,11 @@ packages: eslint-import-resolver-node: 0.3.7 eslint-module-utils: 2.7.4_pxhjym6joapyaysotvqp5q6kau has: 1.0.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 minimatch: 3.1.2 object.values: 1.1.6 - resolve: 1.22.2 + resolve: 1.22.4 semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: @@ -19101,11 +18923,11 @@ packages: eslint-import-resolver-node: 0.3.7 eslint-module-utils: 2.7.4_mcdyrqs6peaairrn7md524c74a has: 1.0.3 - is-core-module: 2.11.0 + is-core-module: 2.13.0 is-glob: 4.0.3 minimatch: 3.1.2 object.values: 1.1.6 - resolve: 1.22.2 + resolve: 1.22.4 semver: 6.3.1 tsconfig-paths: 3.14.1 transitivePeerDependencies: @@ -19186,12 +19008,12 @@ packages: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: '@babel/runtime': 7.22.5 - aria-query: 5.1.3 + aria-query: 5.3.0 array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 ast-types-flow: 0.0.7 axe-core: 4.6.2 - axobject-query: 3.1.1 + axobject-query: 3.2.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 8.31.0 @@ -19211,12 +19033,12 @@ packages: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: '@babel/runtime': 7.22.5 - aria-query: 5.1.3 + aria-query: 5.3.0 array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 ast-types-flow: 0.0.7 axe-core: 4.6.2 - axobject-query: 3.1.1 + axobject-query: 3.2.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 8.42.0 @@ -19236,12 +19058,12 @@ packages: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: '@babel/runtime': 7.22.5 - aria-query: 5.1.3 + aria-query: 5.3.0 array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 ast-types-flow: 0.0.7 axe-core: 4.6.2 - axobject-query: 3.1.1 + axobject-query: 3.2.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 8.45.0 @@ -19812,10 +19634,6 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true - /estree-walker/3.0.2: - resolution: {integrity: sha512-C03BvXCQIH/po+PNPONx/zSM9ziPr9weX8xNhYb/IJtdJ9z+L4z9VKPTB+UTHdmhnIopA2kc419ueyVyHVktwA==} - dev: true - /estree-walker/3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: @@ -21700,15 +21518,6 @@ packages: dependencies: safer-buffer: 2.1.2 - /icss-utils/5.1.0_postcss@8.4.27: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - postcss: 8.4.27 - dev: true - /icss-utils/5.1.0_postcss@8.4.29: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -22187,6 +21996,7 @@ packages: /is-map/2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true /is-nan/1.3.2: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} @@ -22296,6 +22106,7 @@ packages: /is-set/2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true /is-shared-array-buffer/1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} @@ -22358,6 +22169,7 @@ packages: /is-weakmap/2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: true /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} @@ -22369,6 +22181,7 @@ packages: dependencies: call-bind: 1.0.2 get-intrinsic: 1.1.3 + dev: true /is-whitespace/0.3.0: resolution: {integrity: sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==} @@ -22402,6 +22215,7 @@ packages: /isarray/2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true /isbot/3.6.13: resolution: {integrity: sha512-uoP4uK5Dc2CrabmK+Gue1jTL+scHiCc1c9rblRpJwG8CPxjLIv8jmGyyGRGkbPOweayhkskdZsEQXG6p+QCQrg==} @@ -24507,13 +24321,6 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch/9.0.0: - resolution: {integrity: sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - dev: true - /minimatch/9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -24635,15 +24442,6 @@ packages: engines: {node: '>=10'} hasBin: true - /mlly/1.1.1: - resolution: {integrity: sha512-Jnlh4W/aI4GySPo6+DyTN17Q75KKbLTyFK8BrGhjNP4rxuUjbRWhE6gHg3bs33URWAF44FRm7gdQA348i3XxRw==} - dependencies: - acorn: 8.10.0 - pathe: 1.1.1 - pkg-types: 1.0.3 - ufo: 1.3.0 - dev: true - /mlly/1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: @@ -25414,6 +25212,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 + dev: true /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -26112,13 +25911,6 @@ packages: /pend/1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - /periscopic/3.0.4: - resolution: {integrity: sha512-SFx68DxCv0Iyo6APZuw/AKewkkThGwssmU0QWtTlvov3VAtPX+QJ4CadwSaz8nrT5jPIuxdvJWB4PnD2KNDxQg==} - dependencies: - estree-walker: 3.0.3 - is-reference: 3.0.1 - dev: true - /periscopic/3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: @@ -26295,15 +26087,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /postcss-discard-duplicates/5.1.0_postcss@8.4.27: - resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - dependencies: - postcss: 8.4.27 - dev: true - /postcss-discard-duplicates/5.1.0_postcss@8.4.29: resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} engines: {node: ^10 || ^12 || >=14.0} @@ -26459,6 +26242,24 @@ packages: ts-node: 10.9.1_fodzh64fuekdilycyvke2qmf2e yaml: 2.3.1 + /postcss-load-config/4.0.1_ntcifew6f27c4l7dh5q6i7owpq: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.29 + ts-node: 10.9.1_fodzh64fuekdilycyvke2qmf2e + yaml: 2.3.1 + dev: true + /postcss-load-config/4.0.1_postcss@8.4.29: resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} @@ -26475,24 +26276,6 @@ packages: postcss: 8.4.29 yaml: 2.3.1 - /postcss-load-config/4.0.1_xf4uokkt3yp55pilbqb2sbk4eu: - resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 2.1.0 - postcss: 8.4.27 - ts-node: 10.9.1_fodzh64fuekdilycyvke2qmf2e - yaml: 2.3.1 - dev: true - /postcss-loader/7.3.0_postcss@8.4.21: resolution: {integrity: sha512-qLAFjvR2BFNz1H930P7mj1iuWJFjGey/nVhimfOAAQ1ZyPpcClAxP8+A55Sl8mBvM+K2a9Pjgdj10KpANWrNfw==} engines: {node: '>= 14.15.0'} @@ -26507,15 +26290,6 @@ packages: semver: 7.5.4 dev: true - /postcss-modules-extract-imports/3.0.0_postcss@8.4.27: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - postcss: 8.4.27 - dev: true - /postcss-modules-extract-imports/3.0.0_postcss@8.4.29: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} @@ -26525,18 +26299,6 @@ packages: postcss: 8.4.29 dev: true - /postcss-modules-local-by-default/4.0.0_postcss@8.4.27: - resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - icss-utils: 5.1.0_postcss@8.4.27 - postcss: 8.4.27 - postcss-selector-parser: 6.0.11 - postcss-value-parser: 4.2.0 - dev: true - /postcss-modules-local-by-default/4.0.0_postcss@8.4.29: resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} engines: {node: ^10 || ^12 || >= 14} @@ -26549,16 +26311,6 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope/3.0.0_postcss@8.4.27: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - postcss: 8.4.27 - postcss-selector-parser: 6.0.11 - dev: true - /postcss-modules-scope/3.0.0_postcss@8.4.29: resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} @@ -26569,16 +26321,6 @@ packages: postcss-selector-parser: 6.0.11 dev: true - /postcss-modules-values/4.0.0_postcss@8.4.27: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - icss-utils: 5.1.0_postcss@8.4.27 - postcss: 8.4.27 - dev: true - /postcss-modules-values/4.0.0_postcss@8.4.29: resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} @@ -26589,22 +26331,6 @@ packages: postcss: 8.4.29 dev: true - /postcss-modules/6.0.0_postcss@8.4.27: - resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==} - peerDependencies: - postcss: ^8.0.0 - dependencies: - generic-names: 4.0.0 - icss-utils: 5.1.0_postcss@8.4.27 - lodash.camelcase: 4.3.0 - postcss: 8.4.27 - postcss-modules-extract-imports: 3.0.0_postcss@8.4.27 - postcss-modules-local-by-default: 4.0.0_postcss@8.4.27 - postcss-modules-scope: 3.0.0_postcss@8.4.27 - postcss-modules-values: 4.0.0_postcss@8.4.27 - string-hash: 1.1.3 - dev: true - /postcss-modules/6.0.0_postcss@8.4.29: resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==} peerDependencies: @@ -26721,6 +26447,7 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: false /postcss/8.4.29: resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} @@ -27895,16 +27622,6 @@ packages: toml: 3.0.0 dev: true - /remark-parse/10.0.1: - resolution: {integrity: sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==} - dependencies: - '@types/mdast': 3.0.10 - mdast-util-from-markdown: 1.2.0 - unified: 10.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /remark-parse/10.0.2: resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} dependencies: @@ -27947,68 +27664,68 @@ packages: retext-smartypants: 5.2.0 unist-util-visit: 4.1.2 - /remix-auth-email-link/1.5.2_xmjsiulzsxcc3znmuhq3turs2q: + /remix-auth-email-link/1.5.2_3kyxpfn573t2hovk377zjamxda: resolution: {integrity: sha512-LCbaYkhsENbRK70O36Ul/jBtvNcNdBabjOIXYhrTNwx4qYA/fFkX8Sr3GMuDhoMzHnXQy7zHtsqbg/DTHocX6g==} peerDependencies: '@remix-run/server-runtime': ^1.1.1 remix-auth: ^3.2.1 dependencies: - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/server-runtime': 1.19.2 crypto-js: 4.1.1 - remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y + remix-auth: 3.4.0_ya7xgmpozdkweczbsj2asvumvm dev: false - /remix-auth-github/1.3.0_xmjsiulzsxcc3znmuhq3turs2q: + /remix-auth-github/1.3.0_3kyxpfn573t2hovk377zjamxda: resolution: {integrity: sha512-dqveIbXc1yXQXiB8UVjKSR3qgHFUZ0VJOu2/GTT5uMKZY0//FSnp1w8gK0btGlbc1HW97gig16uS84et6FBMiA==} peerDependencies: '@remix-run/server-runtime': ^1.0.0 remix-auth: ^3.4.0 dependencies: - '@remix-run/server-runtime': 1.19.2-pre.0 - remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y - remix-auth-oauth2: 1.5.0_xmjsiulzsxcc3znmuhq3turs2q + '@remix-run/server-runtime': 1.19.2 + remix-auth: 3.4.0_ya7xgmpozdkweczbsj2asvumvm + remix-auth-oauth2: 1.5.0_3kyxpfn573t2hovk377zjamxda transitivePeerDependencies: - supports-color dev: false - /remix-auth-oauth2/1.5.0_xmjsiulzsxcc3znmuhq3turs2q: + /remix-auth-oauth2/1.5.0_3kyxpfn573t2hovk377zjamxda: resolution: {integrity: sha512-vv/CX2bCeVf8vzACfS1Fae+i75Cw0/As3RLkp00+9n9IBEvKi07LhUxTbODNA51jRh+wInnmCRIwsHafKQDD6g==} peerDependencies: '@remix-run/server-runtime': ^1.0.0 remix-auth: ^3.2.2 dependencies: - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/server-runtime': 1.19.2 debug: 4.3.4 - remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y + remix-auth: 3.4.0_ya7xgmpozdkweczbsj2asvumvm uuid: 8.3.2 transitivePeerDependencies: - supports-color dev: false - /remix-auth/3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y: + /remix-auth/3.4.0_ya7xgmpozdkweczbsj2asvumvm: resolution: {integrity: sha512-VRliJo9VRAS4sSYgMjYbi7rYRixPWU2Tb8PFZb06OIj0nONK/1KRzHf2+Y6VGZeIacepLVgan6g2IUJjayE2rQ==} peerDependencies: '@remix-run/react': ^1.0.0 '@remix-run/server-runtime': ^1.0.0 dependencies: - '@remix-run/react': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/react': 1.19.2_biqbaboplfbrettd7655fr4n2y + '@remix-run/server-runtime': 1.19.2 uuid: 8.3.2 dev: false - /remix-typedjson/0.1.7_bhtgrpeaoe6kbm4hb4gzl6x7c4: + /remix-typedjson/0.1.7_uyy3oyfmtzakxsapatosuzqil4: resolution: {integrity: sha512-h7WoKbKBGsVdZZp3s79U5SDrMvQ3We7BjGaSIPxchnJOTS3GUDat0kBoy/gh9TVKRf5KYCee+K1UlFAYQmLQsA==} peerDependencies: '@remix-run/react': ^1.6.5 '@remix-run/server-runtime': ^1.6.5 react: ^17.0.2 || ^18.0.0 dependencies: - '@remix-run/react': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/react': 1.19.2_biqbaboplfbrettd7655fr4n2y + '@remix-run/server-runtime': 1.19.2 react: 18.2.0 dev: false - /remix-utils/6.0.0_7krs2yfztxu2oblf77wwc4rpoe: + /remix-utils/6.0.0_jtxmz5feu54a7fcxnhi2w5jyki: resolution: {integrity: sha512-S7Xec0YHZxGFEDawWpIbU7HQAZC0j51FmAvcCyBRuxjo71aAIMdmez47dgF8T91yxpHV1xlIKPL7LhBzmYsOZw==} engines: {node: '>=14'} peerDependencies: @@ -28017,8 +27734,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 zod: ^3.19.1 dependencies: - '@remix-run/react': 1.19.2-pre.0_biqbaboplfbrettd7655fr4n2y - '@remix-run/server-runtime': 1.19.2-pre.0 + '@remix-run/react': 1.19.2_biqbaboplfbrettd7655fr4n2y + '@remix-run/server-runtime': 1.19.2 intl-parse-accept-language: 1.0.0 is-ip: 3.1.0 react: 18.2.0 @@ -28644,6 +28361,7 @@ packages: /set-cookie-parser/2.5.1: resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + dev: true /set-cookie-parser/2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} @@ -30503,15 +30221,6 @@ packages: minimist: 1.2.7 strip-bom: 3.0.0 - /tsconfig-paths/4.1.2: - resolution: {integrity: sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==} - engines: {node: '>=6'} - dependencies: - json5: 2.2.3 - minimist: 1.2.7 - strip-bom: 3.0.0 - dev: true - /tsconfig-paths/4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -31286,14 +30995,6 @@ packages: unist-util-visit-parents: 3.1.1 dev: true - /unist-util-visit/4.1.1: - resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==} - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 5.1.1 - unist-util-visit-parents: 5.1.1 - dev: true - /unist-util-visit/4.1.2: resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} dependencies: @@ -31619,15 +31320,6 @@ packages: '@types/unist': 2.0.6 unist-util-stringify-position: 3.0.2 - /vfile/5.3.6: - resolution: {integrity: sha512-ADBsmerdGBs2WYckrLBEmuETSPyTD4TuLxTrw0DvjirxW1ra4ZwkbzG8ndsv3Q57smvHxo677MHaQrY9yxH8cA==} - dependencies: - '@types/unist': 2.0.6 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.2 - vfile-message: 3.1.3 - dev: true - /vfile/5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} dependencies: @@ -32309,6 +32001,7 @@ packages: is-set: 2.0.2 is-weakmap: 2.0.1 is-weakset: 2.0.2 + dev: true /which-module/2.0.0: resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} @@ -32513,22 +32206,22 @@ packages: astring: 1.8.4 estree-util-build-jsx: 2.2.2 estree-util-is-identifier-name: 2.0.1 - estree-walker: 3.0.2 + estree-walker: 3.0.3 got: 11.8.6 hast-util-to-estree: 2.1.0 loader-utils: 2.0.4 markdown-extensions: 1.1.1 mdast-util-mdx: 1.1.0 micromark-extension-mdxjs: 1.0.0 - periscopic: 3.0.4 - remark-parse: 10.0.1 + periscopic: 3.1.0 + remark-parse: 10.0.2 remark-rehype: 9.1.0 source-map: 0.7.4 unified: 10.1.2 unist-util-position-from-estree: 1.1.1 unist-util-stringify-position: 3.0.2 - unist-util-visit: 4.1.1 - vfile: 5.3.6 + unist-util-visit: 4.1.2 + vfile: 5.3.7 optionalDependencies: deasync: 0.1.28 transitivePeerDependencies: From 50e3192453500be1f7b985dfb9a7f8871f7945ee Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:41:08 +0530 Subject: [PATCH 09/39] feat: add ability to use custom tunnel in dev cmd (#597) * feat: add ability to use custom tunnel in dev cmd * Added a short flag -t for the tunnel flag * skip framework url resolution if tunnel-url is provided * remove type annotation --------- Co-authored-by: Eric Allam Co-authored-by: Matt Aitken --- .changeset/orange-cows-tease.md | 5 ++ packages/cli/src/cli/index.ts | 4 + packages/cli/src/commands/dev.ts | 140 +++++++++++++++++++++---------- 3 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 .changeset/orange-cows-tease.md diff --git a/.changeset/orange-cows-tease.md b/.changeset/orange-cows-tease.md new file mode 100644 index 000000000..a37f2ee7a --- /dev/null +++ b/.changeset/orange-cows-tease.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +add ability to use custom tunnel in dev command diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index cc3bc8713..b55243de4 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -59,6 +59,10 @@ program "The URI path to the API handler function to use for this project.", "/api/trigger" ) + .option( + "-t, --tunnel ", + "An optional custom tunnel URL. Use only if you already have an open tunnel to your local dev server." + ) .version(getVersion(), "-v, --version", "Display the version number") .action(async (path, options) => { try { diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 3224c24d3..0d92526e1 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -28,6 +28,11 @@ export const DevCommandOptionsSchema = z.object({ envFile: z.string().optional(), handlerPath: z.string(), clientId: z.string().optional(), + tunnel: z + .string() + .url() + .regex(/^(http|https).+/, "only http/https URLs are accepted") + .optional(), }); export type DevCommandOptions = z.infer; @@ -43,6 +48,29 @@ const formattedDate = new Intl.DateTimeFormat("en", { let runtime: JsRuntime; +type TunnelUrl = { + type: "tunnel"; + url: string; +}; + +type ResolvedUrl = { + type: "resolved"; + hostname: string; + port: number; +}; + +type ServerUrl = TunnelUrl | ResolvedUrl; + +type TunnelEndpoint = TunnelUrl & { + handlerPath: string; +}; + +type ResolvedEndpoint = ResolvedUrl & { + handlerPath: string; +}; + +type ServerEndpoint = TunnelEndpoint | ResolvedEndpoint; + export async function devCommand(path: string, anyOptions: any) { telemetryClient.dev.started(path, anyOptions); @@ -90,23 +118,23 @@ export async function devCommand(path: string, anyOptions: any) { logger.error( `✖ [trigger.dev] Your endpoint couldn't be verified. Make sure your app is running and try again. ${resolvedOptions.handlerPath}` ); - logger.info(` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port.`); + logger.info( + ` [trigger.dev] You can use -H to specify a hostname, or -p to specify a port, or -t to specify the tunnel-url pointing to the local dev server.` + ); telemetryClient.dev.failed("no_server_found", resolvedOptions); return; } - const { hostname, port, handlerPath } = verifiedEndpoint; - telemetryClient.dev.serverRunning(path, resolvedOptions); // Setup tunnel - const endpointUrl = await resolveEndpointUrl(apiUrl, port, hostname); + const endpointUrl = await resolveEndpointUrl(apiUrl, verifiedEndpoint); if (!endpointUrl) { telemetryClient.dev.failed("failed_to_create_tunnel", resolvedOptions); return; } - const endpointHandlerUrl = `${endpointUrl}${handlerPath}`; + const endpointHandlerUrl = `${endpointUrl}${verifiedEndpoint.handlerPath}`; telemetryClient.dev.tunnelRunning(path, resolvedOptions); // Watch for changes to files and refresh endpoints @@ -306,6 +334,7 @@ async function resolveOptions( envFile: unresolvedOptions.envFile ?? ".env", handlerPath: unresolvedOptions.handlerPath, clientId: unresolvedOptions.clientId, + tunnel: unresolvedOptions.tunnel, }; } @@ -318,6 +347,7 @@ async function resolveOptions( envFile: unresolvedOptions.envFile ?? envName ?? ".env", handlerPath: unresolvedOptions.handlerPath, clientId: unresolvedOptions.clientId, + tunnel: unresolvedOptions.tunnel, }; } @@ -327,40 +357,15 @@ async function verifyEndpoint( apiKey: string, framework?: Framework ) { - //create list of hostnames to try - const hostnames = []; - if (resolvedOptions.hostname) { - hostnames.push(resolvedOptions.hostname); - } - if (framework) { - hostnames.push(...framework.defaultHostnames); - } else { - hostnames.push("localhost"); - } + const serverUrls = findServerUrls(resolvedOptions, framework); - //create list of ports to try - const ports = []; - if (resolvedOptions.port) { - ports.push(resolvedOptions.port); - } - if (framework) { - ports.push(...framework.defaultPorts); - } else { - ports.push(3000); - } - - //create list of urls to try - const urls: { hostname: string; port: number }[] = []; - for (const hostname of hostnames) { - for (const port of ports) { - urls.push({ hostname, port }); - } - } - - //try each hostname - for (const url of urls) { - const { hostname, port } = url; - const localEndpointHandlerUrl = `http://${hostname}:${port}${resolvedOptions.handlerPath}`; + //try each url + for (const serverUrl of serverUrls) { + const url = + serverUrl.type === "tunnel" + ? serverUrl.url + : `http://${serverUrl.hostname}:${serverUrl.port}`; + const localEndpointHandlerUrl = `${url}${resolvedOptions.handlerPath}`; const spinner = ora( `[trigger.dev] Looking for your trigger endpoint: ${localEndpointHandlerUrl}` @@ -384,7 +389,8 @@ async function verifyEndpoint( } spinner.succeed(`[trigger.dev] Found your trigger endpoint: ${localEndpointHandlerUrl}`); - return { hostname, port, handlerPath: resolvedOptions.handlerPath }; + + return { ...serverUrl, handlerPath: resolvedOptions.handlerPath }; } catch (err) { spinner.fail(`[trigger.dev] No server found (${localEndpointHandlerUrl}).`); } @@ -399,17 +405,61 @@ export function getEndpointId(runtime: JsRuntime, clientId?: string) { } else return runtime.getEndpointId(); } -async function resolveEndpointUrl(apiUrl: string, port: number, hostname: string) { +function findServerUrls(resolvedOptions: ResolvedOptions, framework?: Framework): ServerUrl[] { + if (resolvedOptions.tunnel) { + logger.info(` Using provided tunnel URL: ${resolvedOptions.tunnel}`); + return [{ type: "tunnel", url: resolvedOptions.tunnel }]; + } + + //create list of hostnames to try + const hostnames = []; + if (resolvedOptions.hostname) { + hostnames.push(resolvedOptions.hostname); + } + if (framework) { + hostnames.push(...framework.defaultHostnames); + } else { + hostnames.push("localhost"); + } + + //create list of ports to try + const ports = []; + if (resolvedOptions.port) { + ports.push(resolvedOptions.port); + } + if (framework) { + ports.push(...framework.defaultPorts); + } else { + ports.push(3000); + } + + //create list of urls to try + const urls: ResolvedUrl[] = []; + for (const hostname of hostnames) { + for (const port of ports) { + urls.push({ type: "resolved", hostname, port }); + } + } + + return urls; +} + +async function resolveEndpointUrl(apiUrl: string, endpoint: ServerEndpoint) { + // use tunnel URL if provided + if (endpoint.type === "tunnel") { + return endpoint.url; + } + const apiURL = new URL(apiUrl); - //if the API is localhost and the hostname is localhost - if (apiURL.hostname === "localhost" && hostname === "localhost") { - return `http://${hostname}:${port}`; + // if the API is localhost and the hostname is localhost + if (apiURL.hostname === "localhost" && endpoint.hostname === "localhost") { + return `http://${endpoint.hostname}:${endpoint.port}`; } // Setup tunnel const tunnelSpinner = ora(`🚇 Creating tunnel`).start(); - const tunnelUrl = await createTunnel(hostname, port, tunnelSpinner); + const tunnelUrl = await createTunnel(endpoint.hostname, endpoint.port, tunnelSpinner); if (tunnelUrl) { tunnelSpinner.succeed(`🚇 Created tunnel: ${tunnelUrl}`); @@ -442,7 +492,7 @@ async function createTunnel(hostname: string, port: number, spinner: Ora) { error.message.includes("connect ECONNREFUSED 127.0.0.1:4041") ) { spinner.fail( - `Ngrok failed to create a tunnel for port ${port} because ngrok is already running` + `Ngrok failed to create a tunnel for port ${port} because ngrok is already running.\n You may want to use -t flag to use an existing URL that points to the local dev server.` ); return; } From 513d0f071d46c52c12947e6944dec0108fd02c9d Mon Sep 17 00:00:00 2001 From: vimode <39148877+vimode@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:57:21 +0530 Subject: [PATCH 10/39] fix: broken links in documentation (#602) * fix: broken links at what is triggerdev docs * fix: broken links for apikey * fix: broken sdk link * fix: broken link in concepts/triggers * fix: broken link for zod guide at concepts/triggers/events * fix: broken links in title for projects * Shouldn't have /docs at the start * Use backgroundFetch path --------- Co-authored-by: Matt Aitken --- docs/documentation/concepts/client-adaptors.mdx | 2 +- docs/documentation/concepts/projects.mdx | 2 +- docs/documentation/concepts/runs.mdx | 2 +- docs/documentation/concepts/triggers/events.mdx | 2 +- docs/documentation/concepts/triggers/webhooks.mdx | 2 +- docs/documentation/concepts/what-is-triggerdotdev.mdx | 8 ++++---- docs/sdk/triggerclient/overview.mdx | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/documentation/concepts/client-adaptors.mdx b/docs/documentation/concepts/client-adaptors.mdx index 7cd7c3a90..5d0201559 100644 --- a/docs/documentation/concepts/client-adaptors.mdx +++ b/docs/documentation/concepts/client-adaptors.mdx @@ -5,7 +5,7 @@ description: "The Client is how you interact with the API, through an Adaptor." ## Client -A Client is used to connect to a specific [Project](/documentation/concepts/projects) by using an [API Key](/documentation/concepts/environments-apikeys). +A Client is used to connect to a specific [Project](/documentation/concepts/projects) by using an [API Key](/documentation/concepts/environments-endpoints). Clients are created using the `TriggerClient` class. diff --git a/docs/documentation/concepts/projects.mdx b/docs/documentation/concepts/projects.mdx index 83f81ac6b..f20fc6b7f 100644 --- a/docs/documentation/concepts/projects.mdx +++ b/docs/documentation/concepts/projects.mdx @@ -1,6 +1,6 @@ --- title: "Projects" -description: "Projects are a group of [Jobs](/documentation/concepts/jobs-runs-tasks) with [Environments](/documentation/concepts/environments-apikeys)" +description: "Projects are a group of [Jobs](/documentation/concepts/jobs) with [Environments](/documentation/concepts/environments-endpoints)" --- User's are members of one or more Organizations. Each Organization can have many Projects. diff --git a/docs/documentation/concepts/runs.mdx b/docs/documentation/concepts/runs.mdx index d45a88689..e65ebb870 100644 --- a/docs/documentation/concepts/runs.mdx +++ b/docs/documentation/concepts/runs.mdx @@ -56,7 +56,7 @@ A few things you can do with `io`: - Use [Integrations](/documentation/concepts/integrations). - Add [delays](/documentation/concepts/delays) (that can be longer than your server timeout). - Log messages to the [Run log](/documentation/guides/viewing-runs). -- Perform [background fetch requests](/documentation/sdk/io) (that can be longer than your server timeout). +- Perform [background fetch requests](/sdk/io/backgroundfetch) (that can be longer than your server timeout). - [Send events](/documentation/concepts/triggers/events) to Trigger other Jobs. - Create a [Task](/documentation/concepts/tasks) manually by wrapping code in `io.runTask`. diff --git a/docs/documentation/concepts/triggers/events.mdx b/docs/documentation/concepts/triggers/events.mdx index 4373d0235..f2da21a63 100644 --- a/docs/documentation/concepts/triggers/events.mdx +++ b/docs/documentation/concepts/triggers/events.mdx @@ -18,7 +18,7 @@ Event triggers take a [Zod](https://github.com/colinhacks/zod) schema. This is u It also means that inside your run function the payload will be typed correctly. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way. -You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation. See our [Zod guide](/guides/zod) for more information. +You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation. See our [Zod guide](/documentation/guides/zod) for more information. ## Example diff --git a/docs/documentation/concepts/triggers/webhooks.mdx b/docs/documentation/concepts/triggers/webhooks.mdx index 76579a8a5..397b67f6a 100644 --- a/docs/documentation/concepts/triggers/webhooks.mdx +++ b/docs/documentation/concepts/triggers/webhooks.mdx @@ -17,7 +17,7 @@ Webhooks can be difficult to work with, especially when developing locally. We m There are two ways to use webhooks with Trigger.dev: -1. Use one of our built-in Integrations, such as [GitHub](/integrations/github). We'll take care of registering the webhook for you. +1. Use one of our built-in Integrations, such as [GitHub](/integrations/apis/github). We'll take care of registering the webhook for you. 2. [Create your own Integration](/integrations/create) that registers for webhooks, this is useful if you want to use a service that we don't have an Integration for. ## Example diff --git a/docs/documentation/concepts/what-is-triggerdotdev.mdx b/docs/documentation/concepts/what-is-triggerdotdev.mdx index a456f017b..7793e5090 100644 --- a/docs/documentation/concepts/what-is-triggerdotdev.mdx +++ b/docs/documentation/concepts/what-is-triggerdotdev.mdx @@ -10,7 +10,7 @@ It can be used from _any_ Node.js (support versions) or TypeScript backend appli ## What we take care of for you: - We make it possible to run long-running Jobs on serverless platforms that have short timeouts (e.g. 30 seconds). -- We provide an SDK for building Jobs in your codebase, triggered by various sources such as [events](/triggers/events), [scheduled events](/triggers/scheduled-events), and [webhooks](/triggers/webhooks). +- We provide an SDK for building Jobs in your codebase, triggered by various sources such as [events](/documentation/concepts/triggers/events), [scheduled events](/documentation/concepts/triggers/scheduled), and [webhooks](/documentation/concepts/triggers/webhooks). - We provide an orchestration platform for running Jobs in your codebase. - We provide out-of-the-box Integrations with popular services such as [Slack](/integrations/apis/slack), [OpenAI](/integrations/apis/openai), [GitHub](/integrations/apis/github) and [more](/integrations), which vastly simplifies the process interacting with 3rd-party services. - We handle OAuth for you @@ -73,16 +73,16 @@ client.defineJob({ This code lives in a file inside your project repo. -It is listening for the [issueEvent](/integrations/apis/github/triggers) GitHub webhook, and when it receives one, we will take care of calling the `run` function supplied to the `Job` constructor with the webhook payload. This gives you the following advantages over traditional webhooks: +It is listening for the [issueEvent](/integrations/apis/github-triggers#onissue) GitHub webhook, and when it receives one, we will take care of calling the `run` function supplied to the `Job` constructor with the webhook payload. This gives you the following advantages over traditional webhooks: - We will automatically register the webhook with GitHub for you, and verify the payload signature. - We provide a nicely typed `event` payload to your `run` function, so you don't have to setup webhook payload types. - If your server isn't running, we will wait until it's back online before attempting to run the Job. -- It is very easy to test your Job locally using our [Test Run](/guides/running-tests) feature. +- It is very easy to test your Job locally using our [Test Run](/documentation/guides/testing-jobs) feature. As you can see the above Job also makes a call to our Slack [postMessage](/integrations/apis/slack) function, which provides the following advantages over using the raw Slack API: -- We automatically handle the OAuth flow for you, so you don't have to worry about setting up a Slack app and dealing with credentials in your code (see our [Authentication](/concepts/authentication) guide for more details). +- We automatically handle the OAuth flow for you, so you don't have to worry about setting up a Slack app and dealing with credentials in your code (see our [Authentication](/documentation/concepts/integrations#authentication) guide for more details). - We will automatically retry the request if the Slack API returns an error. - We provide a nicely typed `response` object from your `postMessage` function, so you don't have to setup Slack API types. diff --git a/docs/sdk/triggerclient/overview.mdx b/docs/sdk/triggerclient/overview.mdx index 6d263dd0b..44b9e3311 100644 --- a/docs/sdk/triggerclient/overview.mdx +++ b/docs/sdk/triggerclient/overview.mdx @@ -4,7 +4,7 @@ sidebarTitle: "Overview" description: "TriggerClient is used to create a client that connects to the Trigger.dev platform" --- -A [TriggerClient](/documentation/concepts/client-adaptors) is used to connect to a specific [Project](/documentation/concepts/projects) by using an [API Key](/documentation/concepts/environments-apikeys). +A [TriggerClient](/documentation/concepts/client-adaptors) is used to connect to a specific [Project](/documentation/concepts/projects) by using an [API Key](/documentation/concepts/environments-endpoints). ```ts Example export const client = new TriggerClient({ From 51f2cd6b3f1a8e72b6dd3a11891d6966e79fadd9 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 19 Oct 2023 16:34:41 +0100 Subject: [PATCH 11/39] Updated posthog to 1.83.0 (#608) --- apps/webapp/package.json | 4 ++-- pnpm-lock.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 5cf5eacef..b9fcc6f95 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -87,7 +87,7 @@ "morgan": "^1.10.0", "nanoid": "^3.3.4", "postcss-import": "^14.1.0", - "posthog-js": "^1.69.0", + "posthog-js": "^1.83.0", "posthog-node": "^3.1.1", "prism-react-renderer": "^1.3.5", "prismjs": "^1.29.0", @@ -180,4 +180,4 @@ "engines": { "node": ">=16.0.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3738f06f9..870043aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,7 +159,7 @@ importers: npm-run-all: ^4.1.5 postcss: ^8.4.21 postcss-import: ^14.1.0 - posthog-js: ^1.69.0 + posthog-js: ^1.83.0 posthog-node: ^3.1.1 prettier: ^2.8.8 prettier-plugin-tailwindcss: ^0.3.0 @@ -258,7 +258,7 @@ importers: morgan: 1.10.0 nanoid: 3.3.4 postcss-import: 14.1.0_postcss@8.4.21 - posthog-js: 1.69.0 + posthog-js: 1.83.0 posthog-node: 3.1.1 prism-react-renderer: 1.3.5_react@18.2.0 prismjs: 1.29.0 @@ -26479,8 +26479,8 @@ packages: xtend: 4.0.2 dev: false - /posthog-js/1.69.0: - resolution: {integrity: sha512-VaeKxbwCBGG3cN1UdFOdb9DBnTHAT3ZoDVubUV3irr0kjB8jIQjWYlnBLkdg++RLPQwAFfsiAGam7EFvXJrZiw==} + /posthog-js/1.83.0: + resolution: {integrity: sha512-3dp/yNbRCYsOgvJovFUMCLv9/KxnwmGBy5Ft27Q7/rbW++iJXVR64liX7i0NrXkudjoL9j1GW1LGh84rV7kv8Q==} dependencies: fflate: 0.4.8 dev: false From cf33396dc0ed956e95be6456a441e5e44a0f1690 Mon Sep 17 00:00:00 2001 From: Rutam21 Date: Fri, 20 Oct 2023 02:45:46 +0530 Subject: [PATCH 12/39] [TRI-1425] Improve the Plain integration documentation --- docs/integrations/apis/plain-tasks.mdx | 213 +++++++++++++++++++++++++ docs/integrations/apis/plain.mdx | 150 +++-------------- docs/mint.json | 8 +- 3 files changed, 242 insertions(+), 129 deletions(-) create mode 100644 docs/integrations/apis/plain-tasks.mdx diff --git a/docs/integrations/apis/plain-tasks.mdx b/docs/integrations/apis/plain-tasks.mdx new file mode 100644 index 000000000..224a35bdb --- /dev/null +++ b/docs/integrations/apis/plain-tasks.mdx @@ -0,0 +1,213 @@ +--- +title: Plain tasks +sidebarTitle: Tasks +--- + +Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want. + +--- + +## All tasks + +### `getCustomerById` + +Gets a customer using their id. [Official Plain Docs](https://plain-typescript-sdk-docs.vercel.app/classes/PlainClient.html#getCustomerById) + +```ts example.ts +const getCustomer = await io.plain.getCustomerById("get-customer", { + customerId: customer.id, // The customer's unique identifier, likely an ID. + }); +``` + +### `upsertCustomer` + +Creates or updates a customer. [Official Plain Docs](https://plain-typescript-sdk-docs.vercel.app/classes/PlainClient.html#upsertCustomer) + +```ts example.ts +run: async (payload, io, ctx) => { + // Inside this function, a new customer record is being upserted (inserted or updated). + + // Using object destructuring to extract the 'customer' object from the result of the asynchronous 'upsertCustomer' function. + const { customer } = await io.plain.upsertCustomer("upsert-customer", { + // Configuration for upserting a customer record: + + // Unique identifier for the customer record, in this case, based on the email address. + identifier: { + emailAddress: "rick.astley@gmail.com", + }, + + // 'onCreate' block defines what to do when a new customer gets created with the given identifier. + onCreate: { + email: { + email: "rick.astley@gmail.com", // Setting the email address. + isVerified: true, // Marking the email as verified. + }, + fullName: "Rick Astley", // Setting the full name. + externalId: "u_123", // Assigning an external identifier. + }, + + // 'onUpdate' block defines what to do when a customer with the given identifier already exists. + onUpdate: { + fullName: { + value: "Rick Astley", // Updating the full name. + }, + externalId: { + value: "u_123", // Updating the external identifier. + }, + }, + }); +} +``` + + +### `upsertCustomTimelineEntry` + +Creates or updates a timeline entry. [Official Plain Docs](https://plain-typescript-sdk-docs.vercel.app/classes/PlainClient.html#upsertCustomTimelineEntry) + +```ts example.ts +// Declaring a constant 'timelineEntry' and using 'await' to asynchronously execute the 'upsertCustomTimelineEntry' function. +const timelineEntry = await io.plain.upsertCustomTimelineEntry("upsert-timeline-entry", { + // Specifying properties for the timeline entry: + customerId: customer.id, // Assigning the 'customer.id' to the 'customerId' property. + title: "My timeline entry", // Assigning the title for the timeline entry. + + // Defining an array of 'components' that make up the timeline entry. + components: [ + { + // First component: A text component. + componentText: { + text: `This is a nice title`, // The text to display in this component. + }, + }, + { + // Second component: A divider component. + componentDivider: { + dividerSpacingSize: ComponentDividerSpacingSize.M, // Configuring the size of the divider. + }, + }, + { + // Third component: Another text component with additional properties. + componentText: { + textSize: ComponentTextSize.S, // Configuring the text size. + textColor: ComponentTextColor.Muted, // Configuring the text color. + text: "External id", // The text to display in this component. + }, + }, + { + // Fourth component: A text component that displays the 'externalId' property. + componentText: { + text: foundCustomer?.externalId ?? "", // Displaying the 'externalId' if available; otherwise, an empty string. + }, + }, + ], +}); + +``` + +## Using the underlying client + +You can also use the underlying client to do anything [@team-plain/typescript-sdk](https://github.com/team-plain/typescript-sdk) supports by using runTask: + +```ts example.ts +import { Plain } from "@trigger.dev/plain"; + +// Creating a new instance of the 'Plain' class and exporting it as 'plain'. +export const plain = new Plain({ + id: "plain", // Unique identifier for this 'Plain' instance. + apiKey: process.env.PLAIN_API_KEY!, // API key obtained from an environment variable. +}); + +// Defining a job using the 'defineJob' method on the 'client' object. +client.defineJob({ + id: "plain-client", // Unique identifier for this job. + name: "Plain Client", // Setting a name for the job. + version: "0.1.0", // Version number for this job. + integrations: { plain }, // Integrating the 'plain' object into this job. + trigger: eventTrigger({ + name: "plain.client", // Setting an event trigger with a name. + }), + run: async (payload, io, ctx) => { + // Inside the 'run' function, a task is being executed asynchronously. + + // Executing a task using the 'io.plain.runTask' method. + const result = await io.plain.runTask( + "create-issue", // Task name. + async (client) => + client.createIssue({ + customerId: "abcdefghij", + issueTypeId: "123456", + }), + { name: "Create issue" } + ); + }, +}); + +``` + +## Example usage + +In this example we'll create a task that updates or creates customer information based on an identifier. + +```ts example.ts +import { Job, TriggerClient, eventTrigger } from "@trigger.dev/sdk"; +import { + ComponentDividerSpacingSize, + ComponentTextColor, + ComponentTextSize, + Plain, +} from "@trigger.dev/plain"; + +// Creating an instance of the TriggerClient with a unique identifier "jobs-showcase." +const client = new TriggerClient({ id: "jobs-showcase" }); + +// Creating an instance of the Plain client. It uses an API key from an environment variable. +export const plain = new Plain({ + id: "plain", + apiKey: process.env.PLAIN_API_KEY!, +}); + +// Defining a job for updating customer information. +client.defineJob({ + id: "plain-update-customer", // Unique identifier for the job. + name: "Plain: update customer", // A specific name for the job. + version: "1.0.0", // Version number for the job. + integrations: { + plain, // Integrating the Plain client into this job. + }, + trigger: eventTrigger({ + name: "plain.update.customer", // Setting an event trigger with a specific name. + }), + run: async (payload, io, ctx) => { + // Inside the 'run' function, an operation is performed to update customer information. + + // Retrieving the 'customer' object using the 'io.plain.upsertCustomer' method. + const { customer } = await io.plain.upsertCustomer("upsert-customer", { + identifier: { + emailAddress: "rick.astley@gmail.com", + }, + // If the customer isn't found, they will be created with the following details. + onCreate: { + email: { + email: "rick.astley@gmail.com", + isVerified: true, + }, + fullName: "Rick Astley", + externalId: "u_123", + }, + // If the customer is found, their details will be updated. + onUpdate: { + fullName: { + value: "Rick Astley", + }, + externalId: { + value: "u_123", + }, + }, + }); + }, +}); + +// If you're not using Express, you can remove these lines. +import { createExpressServer } from "@trigger.dev/express"; +createExpressServer(client); +``` diff --git a/docs/integrations/apis/plain.mdx b/docs/integrations/apis/plain.mdx index a98f215a4..ee20c32f8 100644 --- a/docs/integrations/apis/plain.mdx +++ b/docs/integrations/apis/plain.mdx @@ -1,11 +1,23 @@ --- -title: Plain -description: Plain is customer support for developer tools +title: Plain overview & authentication +sidebarTitle: Overview & authentication --- - +## Overview -## Installation +Plain is the customer support tool for technical teams and products. +It aims to bring engineering and customer service teams together by creating a modern opinionated platform that's fantastic to build with. + + + + Check out pre-built Plain jobs in our showcase. + + +## Installing the Plain packages @@ -36,130 +48,12 @@ export const plain = new Plain({ }); ``` -## Create customers and timeline entries - -The Plain Integration allows you to create/update customers and add timeline entries. - -```ts -import { client } from "@/trigger"; -import { Job } from "@trigger.dev/sdk"; -import { Plain } from "@trigger.dev/plain"; - -export const plain = new Plain({ - id: "plain", - apiKey: process.env.PLAIN_API_KEY!, -}); - -client.defineJob({ - id: "plain-playground", - name: "Plain Playground", - version: "0.1.1", - integrations: { - plain, - }, - trigger: eventTrigger({ - name: "plain.playground", - }), - run: async (payload, io, ctx) => { - const { customer } = await io.plain.upsertCustomer("upsert-customer", { - identifier: { - emailAddress: "rick.astley@gmail.com", - }, - onCreate: { - email: { - email: "rick.astley@gmail.com", - isVerified: true, - }, - fullName: "Rick Astley", - externalId: "u_123", - }, - onUpdate: { - fullName: { - value: "Rick Astley", - }, - externalId: { - value: "u_123", - }, - }, - }); - - const foundCustomer = await io.plain.getCustomerById("get-customer", { - customerId: customer.id, - }); - - const timelineEntry = await io.plain.upsertCustomTimelineEntry("upsert-timeline-entry", { - customerId: customer.id, - title: "My timeline entry", - components: [ - { - componentText: { - text: `This is a nice title`, - }, - }, - { - componentDivider: { - dividerSpacingSize: ComponentDividerSpacingSize.M, - }, - }, - { - componentText: { - textSize: ComponentTextSize.S, - textColor: ComponentTextColor.Muted, - text: "External id", - }, - }, - { - componentText: { - text: foundCustomer?.externalId ?? "", - }, - }, - ], - }); - }, -}); -``` - ## Tasks -## All tasks +Once you have set up a Plain client, you can use it to create tasks. -| Function Name | Description | -| --------------------------- | ----------------------------------- | -| `getCustomerById` | Gets a customer using their id | -| `upsertCustomer` | Creates or updates a customer | -| `upsertCustomTimelineEntry` | Creates or updates a timeline entry | - -## Using the underlying client - -You can use the underlying client to do anything [@team-plain/typescript-sdk](https://github.com/team-plain/typescript-sdk) supports by using runTask: - -```ts -import { Plain } from "@trigger.dev/plain"; - -//create client -export const plain = new Plain({ - id: "plain", - apiKey: process.env.PLAIN_API_KEY!, -}); - -client.defineJob({ - id: "plain-client", - name: "Plain Client", - version: "0.1.0", - integrations: { plain }, - trigger: eventTrigger({ - name: "plain.client", - }), - run: async (payload, io, ctx) => { - const result = await io.plain.runTask( - "create-issue", - async (client) => - client.createIssue({ - customerId: "abcdefghij", - issueTypeId: "123456", - }), - { name: "Create issue" } - ); - }, -}); -``` + + + Perform tasks such as creating/updating customers and adding timeline entries. + + diff --git a/docs/mint.json b/docs/mint.json index 5abf8aa9e..8c1399778 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -248,7 +248,13 @@ }, "integrations/apis/linear", "integrations/apis/openai", - "integrations/apis/plain", + { + "group": "Plain", + "pages": [ + "integrations/apis/plain", + "integrations/apis/plain-tasks" + ] + }, "integrations/apis/replicate", "integrations/apis/resend", "integrations/apis/sendgrid", From 0adf41c7f063f72ba81ccfa8192fda5b6c01882b Mon Sep 17 00:00:00 2001 From: Alexandre Costa Date: Fri, 20 Oct 2023 06:16:44 -0300 Subject: [PATCH 13/39] Feat: add `maxDuration` setting for API/Trigger route in the CLI Init Command for Next.js (#617) * add cli init maxDuration to the api/trigger route for nextjs * refactor createTriggerRoute * move boxen log to createTriggerRoute function * refine detectNextVersion and versionNumberPattern regex to match the latest nextjs version * add tests for the detectNextVersion function * add changeset file * If the regex doesn't match, it returns null instead of throwing * Tweaked message about the max duration --------- Co-authored-by: Matt Aitken --- .changeset/twenty-mangos-hide.md | 5 + packages/cli/src/frameworks/nextjs/index.ts | 91 ++++++++++++------- .../cli/src/frameworks/nextjs/nextjs.test.ts | 31 ++++++- .../cli/src/templates/nextjs/appApiRoute.js | 3 + .../nextjs/pagesApiRouteWithConfigObject.js | 15 +++ 5 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 .changeset/twenty-mangos-hide.md create mode 100644 packages/cli/src/templates/nextjs/pagesApiRouteWithConfigObject.js diff --git a/.changeset/twenty-mangos-hide.md b/.changeset/twenty-mangos-hide.md new file mode 100644 index 000000000..32ee48b9d --- /dev/null +++ b/.changeset/twenty-mangos-hide.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/cli": patch +--- + +Added Next.js `maxDuration` commented out to the api/trigger file using CLI init diff --git a/packages/cli/src/frameworks/nextjs/index.ts b/packages/cli/src/frameworks/nextjs/index.ts index d40710753..ffb1b625a 100644 --- a/packages/cli/src/frameworks/nextjs/index.ts +++ b/packages/cli/src/frameworks/nextjs/index.ts @@ -1,5 +1,6 @@ import fs from "fs/promises"; import pathModule from "path"; +import boxen from "boxen"; import { Framework } from ".."; import { templatesPath } from "../../paths"; import { InstallPackage } from "../../utils/addDependencies"; @@ -50,17 +51,46 @@ export class NextJs implements Framework { } const nextJsDir = await detectPagesOrAppDir(path); + const nextJsVersion = await detectNextVersion(path); const routeDir = pathModule.join(path, usesSrcDir ? "src" : ""); const pathAlias = await getPathAlias({ projectPath: path, isTypescriptProject: options.typescript, extraDirectories: usesSrcDir ? ["src"] : undefined, }); + const fileExtension = options.typescript ? ".ts" : ".js"; if (nextJsDir === "pages") { - await createTriggerPageRoute(routeDir, options.endpointSlug, options.typescript, pathAlias); + const apiRoutePath = pathModule.join(routeDir, "pages", "api", `trigger${fileExtension}`); + if (nextJsVersion && nextJsVersion !== 'latest' && nextJsVersion < "13.5") { + await createTriggerRoute({ + path: routeDir, + apiRoutePath, + template: "pagesApiRoute.js", + fileExtension, + endpointSlug: options.endpointSlug, + pathAlias + }); + } else { + await createTriggerRoute({ + path: routeDir, + apiRoutePath, + template: "pagesApiRouteWithConfigObject.js", + fileExtension, + endpointSlug: options.endpointSlug, + pathAlias + }); + } } else { - await createTriggerAppRoute(routeDir, options.endpointSlug, options.typescript, pathAlias); + const apiRoutePath = pathModule.join(routeDir, "app", "api", "trigger", `route${fileExtension}`); + await createTriggerRoute({ + path: routeDir, + apiRoutePath, + template: "appApiRoute.js", + fileExtension, + endpointSlug: options.endpointSlug, + pathAlias + }); } } @@ -144,45 +174,34 @@ export async function detectPagesOrAppDir(path: string): Promise<"pages" | "app" return "pages"; } -async function createTriggerPageRoute( - path: string, - endpointSlug: string, - isTypescriptProject: boolean, - pathAlias: string | undefined -) { - const templatesDir = pathModule.join(templatesPath(), "nextjs"); - const fileExtension = isTypescriptProject ? ".ts" : ".js"; - - //pages/api/trigger.js or src/pages/api/trigger.js - const apiRoutePath = pathModule.join(path, "pages", "api", `trigger${fileExtension}`); - const apiRouteResult = await createFileFromTemplate({ - templatePath: pathModule.join(templatesDir, "pagesApiRoute.js"), - replacements: { - routePathPrefix: pathAlias ? pathAlias + "/" : "../../", - }, - outputPath: apiRoutePath, - }); - if (!apiRouteResult.success) { - throw new Error("Failed to create API route file"); +export async function detectNextVersion(path: string) { + const packageJsonContent = await readPackageJson(path); + if (!packageJsonContent) { + return null; } - logger.success(`✔ Created API route at ${apiRoutePath}`); - await createJobsAndTriggerFile(path, endpointSlug, fileExtension, pathAlias, templatesDir); + const versionNumberPattern = /[\d.]+|latest/; + if (packageJsonContent.dependencies?.next !== undefined) + return packageJsonContent.dependencies?.next?.match(versionNumberPattern)?.at(0) ?? null; + if (packageJsonContent.devDependencies?.next !== undefined) + return packageJsonContent.devDependencies?.next?.match(versionNumberPattern)?.at(0) ?? null; + + return null; } -async function createTriggerAppRoute( +async function createTriggerRoute(options: { path: string, + apiRoutePath: string, + template: string, + fileExtension: string, endpointSlug: string, - isTypescriptProject: boolean, pathAlias: string | undefined -) { - const templatesDir = pathModule.join(templatesPath(), "nextjs"); - const fileExtension = isTypescriptProject ? ".ts" : ".js"; +}) { + const { path, apiRoutePath, template, pathAlias, endpointSlug, fileExtension } = options; - //app/api/trigger/route.js or src/app/api/trigger/route.js - const apiRoutePath = pathModule.join(path, "app", "api", "trigger", `route${fileExtension}`); + const templatesDir = pathModule.join(templatesPath(), "nextjs"); const apiRouteResult = await createFileFromTemplate({ - templatePath: pathModule.join(templatesDir, "appApiRoute.js"), + templatePath: pathModule.join(templatesDir, template), replacements: { routePathPrefix: pathAlias ? pathAlias + "/" : "../../", }, @@ -192,6 +211,14 @@ async function createTriggerAppRoute( throw new Error("Failed to create API route file"); } logger.success(`✔ Created API route at ${apiRoutePath}`); + logger.info( + boxen(`If you're deploying to Vercel, configure your max duration in ${apiRoutePath}`, { + padding: 1, + margin: 1, + borderStyle: "double", + borderColor: "magenta", + }) + ); await createJobsAndTriggerFile(path, endpointSlug, fileExtension, pathAlias, templatesDir); } diff --git a/packages/cli/src/frameworks/nextjs/nextjs.test.ts b/packages/cli/src/frameworks/nextjs/nextjs.test.ts index 38342637b..4af4263e2 100644 --- a/packages/cli/src/frameworks/nextjs/nextjs.test.ts +++ b/packages/cli/src/frameworks/nextjs/nextjs.test.ts @@ -1,5 +1,5 @@ import mock from "mock-fs"; -import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir } from "."; +import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir, detectNextVersion } from "."; import { getFramework } from ".."; import { pathExists } from "../../utils/fileSystem"; import { detectMiddlewareUsage } from "./middleware"; @@ -57,6 +57,35 @@ describe("Next project detection", () => { }); }); +describe("Next version detection", () => { + test("detect Nextjs latest version", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { next: "latest" } }), + }); + + const nextJsVersion = await detectNextVersion(""); + expect(nextJsVersion).toEqual("latest"); + }); + + test("detect Nextjs 13.0.0 version", async () => { + mock({ + "package.json": JSON.stringify({ dependencies: { next: "13.0.0" } }), + }); + + const nextJsVersion = await detectNextVersion(""); + expect(nextJsVersion).toEqual("13.0.0"); + }); + + test("detect Nextjs version as a dev dependency", async () => { + mock({ + "package.json": JSON.stringify({ devDependencies: { next: "^12.0.0" } }), + }); + + const nextJsVersion = await detectNextVersion(""); + expect(nextJsVersion).toEqual("12.0.0"); + }); +}); + describe("src directory", () => { test("has src directory", async () => { mock({ diff --git a/packages/cli/src/templates/nextjs/appApiRoute.js b/packages/cli/src/templates/nextjs/appApiRoute.js index 546336103..6c81a628c 100644 --- a/packages/cli/src/templates/nextjs/appApiRoute.js +++ b/packages/cli/src/templates/nextjs/appApiRoute.js @@ -5,3 +5,6 @@ import "${routePathPrefix}jobs"; //this route is used to send and receive data with Trigger.dev export const { POST, dynamic } = createAppRoute(client); + +//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration +//export const maxDuration = 60; \ No newline at end of file diff --git a/packages/cli/src/templates/nextjs/pagesApiRouteWithConfigObject.js b/packages/cli/src/templates/nextjs/pagesApiRouteWithConfigObject.js new file mode 100644 index 000000000..64d1c54cd --- /dev/null +++ b/packages/cli/src/templates/nextjs/pagesApiRouteWithConfigObject.js @@ -0,0 +1,15 @@ +import { createPagesRoute } from "@trigger.dev/nextjs"; +import { client } from "${routePathPrefix}trigger"; + +import "${routePathPrefix}jobs"; + +//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration +//export const config = { +// maxDuration: 60, +//}; + +//this route is used to send and receive data with Trigger.dev +const { handler, config } = createPagesRoute(client); +export { config }; + +export default handler; From 84af64165c36f93df8ce6744c44c73a02b2f8e3c Mon Sep 17 00:00:00 2001 From: Bhargav Shirin Nalamati Date: Fri, 20 Oct 2023 14:52:13 +0530 Subject: [PATCH 14/39] changed the title for contributors (#633) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 609dfd8ab..67aacddea 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ We provide an official trigger.dev docker image you can use to easily self-host To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md). -## 🙏 to our contributors +## Meet the Amazing People Behind This Project 🚀 From abc9737a4f327107ca2c646b2c361d0b1fa149ff Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 20 Oct 2023 11:41:45 +0100 Subject: [PATCH 15/39] Tabler icons added to NamedIcon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 13ed268f1d6732ef28c6e4a9eeb927c1af822032 Author: Matt Aitken Date: Fri Oct 20 11:38:22 2023 +0100 Added all the tabler-icons to Storybook commit 62738d71a40f17c45c0b29332359b2b670233a97 Author: Matt Aitken Date: Fri Oct 20 11:38:09 2023 +0100 Use the no-stroke version of the tabler-sprite so we can control the stroke width commit a1b7edd57f8b2d3f2a5f4d96cbc9eb0a887f3442 Author: Matt Aitken Date: Fri Oct 20 11:37:54 2023 +0100 Set the default width to 1.5, so they’re a bit thinner commit 69ba38802b849376611678537f7d8e248b60086b Author: Matt Aitken Date: Fri Oct 20 10:55:34 2023 +0100 Added a tabler icon to an events job catalog job commit eb3c5f649d06adcc9ce0641f50e082ecdeead7a2 Author: Matt Aitken Date: Fri Oct 20 10:53:40 2023 +0100 Moved the tabler sprite to the app and use an import. This means it’ll be cached and we can move it commit c24d76e6df9233b001a0242f8806076562ca2e90 Author: Chaturved Degloorkar <48447253+chaturrved@users.noreply.github.com> Date: Fri Oct 20 15:22:59 2023 +0530 feat: Add support for tabler-icons when using the icon for Tasks (#629) * Add support for tabler-icons * Changed the core update type from minor to a patch --------- Co-authored-by: Matt Aitken --- .changeset/slimy-moles-smash.md | 5 + .../app/components/primitives/NamedIcon.tsx | 16 + .../components/primitives/tabler-sprite.svg | 1 + .../components/stories/NamedIcon.stories.tsx | 40 +- apps/webapp/app/utils/icon.ts | 3 +- apps/webapp/app/utils/tablerIcons.ts | 4822 +++++++++++++++++ docs/sdk/io/runtask.mdx | 3 +- packages/core/src/schemas/api.ts | 2 +- references/job-catalog/src/events.ts | 14 +- 9 files changed, 4887 insertions(+), 19 deletions(-) create mode 100644 .changeset/slimy-moles-smash.md create mode 100644 apps/webapp/app/components/primitives/tabler-sprite.svg create mode 100644 apps/webapp/app/utils/tablerIcons.ts diff --git a/.changeset/slimy-moles-smash.md b/.changeset/slimy-moles-smash.md new file mode 100644 index 000000000..8dea53715 --- /dev/null +++ b/.changeset/slimy-moles-smash.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Updated icon documentation in runTasks diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx index e06656e90..e0b7a5c0a 100644 --- a/apps/webapp/app/components/primitives/NamedIcon.tsx +++ b/apps/webapp/app/components/primitives/NamedIcon.tsx @@ -68,6 +68,8 @@ import { Spinner } from "./Spinner"; import { SaplingIcon } from "~/assets/icons/SaplingIcon"; import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon"; import { OneTreeIcon } from "~/assets/icons/OneTreeIcon"; +import { tablerIcons } from "~/utils/tablerIcons"; +import tablerSpritePath from "./tabler-sprite.svg"; const icons = { account: (className: string) => , @@ -215,6 +217,12 @@ export function NamedIcon({ ); } + if (tablerIcons.has("tabler-" + name)) { + return ; + } else if (name.startsWith("tabler-") && tablerIcons.has(name)) { + return ; + } + console.log(`Icon ${name} not found`); if (fallback) { @@ -247,3 +255,11 @@ export function NamedIconInBox({ ); } + +export function TablerIcon({ name, className }: { name: string; className?: string }) { + return ( + + + + ); +} diff --git a/apps/webapp/app/components/primitives/tabler-sprite.svg b/apps/webapp/app/components/primitives/tabler-sprite.svg new file mode 100644 index 000000000..1587c16d4 --- /dev/null +++ b/apps/webapp/app/components/primitives/tabler-sprite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/webapp/app/components/stories/NamedIcon.stories.tsx b/apps/webapp/app/components/stories/NamedIcon.stories.tsx index 2d7c6a4ba..658b6802e 100644 --- a/apps/webapp/app/components/stories/NamedIcon.stories.tsx +++ b/apps/webapp/app/components/stories/NamedIcon.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { withDesign } from "storybook-addon-designs"; import { NamedIcon, iconNames } from "../primitives/NamedIcon"; +import { tablerIcons } from "~/utils/tablerIcons"; +import { Header1 } from "../primitives/Headers"; const meta: Meta = { title: "Icons", @@ -25,17 +27,35 @@ export const Basic: Story = { function NamedIcons() { return ( -
- {iconNames - .sort((a, b) => a.localeCompare(b)) - .map((iconName) => ( -
-
- +
+
+ Internal +
+ {iconNames + .sort((a, b) => a.localeCompare(b)) + .map((iconName) => ( +
+
+ +
+ {iconName} +
+ ))} +
+
+
+ Tabler +
+ {Array.from(tablerIcons).map((iconName) => ( +
+
+ +
+ {iconName}
- {iconName} -
- ))} + ))} +
+
); } diff --git a/apps/webapp/app/utils/icon.ts b/apps/webapp/app/utils/icon.ts index d3ee34bcc..ac36bd1cb 100644 --- a/apps/webapp/app/utils/icon.ts +++ b/apps/webapp/app/utils/icon.ts @@ -1,9 +1,10 @@ import { hasIcon } from "@trigger.dev/companyicons"; import { iconNames as namedIcons } from "~/components/primitives/NamedIcon"; +import { tablerIcons } from "~/utils/tablerIcons"; export const isValidIcon = (icon?: string): boolean => { if (!icon) { return false; } - return namedIcons.includes(icon) || hasIcon(icon); + return namedIcons.includes(icon) || hasIcon(icon) || tablerIcons.has(icon); }; diff --git a/apps/webapp/app/utils/tablerIcons.ts b/apps/webapp/app/utils/tablerIcons.ts new file mode 100644 index 000000000..559f08356 --- /dev/null +++ b/apps/webapp/app/utils/tablerIcons.ts @@ -0,0 +1,4822 @@ +const tablerIconNames = [ + "tabler-12-hours", + "tabler-123", + "tabler-24-hours", + "tabler-2fa", + "tabler-360-view", + "tabler-360", + "tabler-3d-cube-sphere-off", + "tabler-3d-cube-sphere", + "tabler-3d-rotate", + "tabler-a-b-2", + "tabler-a-b-off", + "tabler-a-b", + "tabler-abacus-off", + "tabler-abacus", + "tabler-abc", + "tabler-access-point-off", + "tabler-access-point", + "tabler-accessible-off-filled", + "tabler-accessible-off", + "tabler-accessible", + "tabler-activity-heartbeat", + "tabler-activity", + "tabler-ad-2", + "tabler-ad-circle-filled", + "tabler-ad-circle-off", + "tabler-ad-circle", + "tabler-ad-filled", + "tabler-ad-off", + "tabler-ad", + "tabler-address-book-off", + "tabler-address-book", + "tabler-adjustments-alt", + "tabler-adjustments-bolt", + "tabler-adjustments-cancel", + "tabler-adjustments-check", + "tabler-adjustments-code", + "tabler-adjustments-cog", + "tabler-adjustments-dollar", + "tabler-adjustments-down", + "tabler-adjustments-exclamation", + "tabler-adjustments-filled", + "tabler-adjustments-heart", + "tabler-adjustments-horizontal", + "tabler-adjustments-minus", + "tabler-adjustments-off", + "tabler-adjustments-pause", + "tabler-adjustments-pin", + "tabler-adjustments-plus", + "tabler-adjustments-question", + "tabler-adjustments-search", + "tabler-adjustments-share", + "tabler-adjustments-star", + "tabler-adjustments-up", + "tabler-adjustments-x", + "tabler-adjustments", + "tabler-aerial-lift", + "tabler-affiliate-filled", + "tabler-affiliate", + "tabler-air-balloon", + "tabler-air-conditioning-disabled", + "tabler-air-conditioning", + "tabler-air-traffic-control", + "tabler-alarm-average", + "tabler-alarm-filled", + "tabler-alarm-minus-filled", + "tabler-alarm-minus", + "tabler-alarm-off", + "tabler-alarm-plus-filled", + "tabler-alarm-plus", + "tabler-alarm-snooze-filled", + "tabler-alarm-snooze", + "tabler-alarm", + "tabler-album-off", + "tabler-album", + "tabler-alert-circle-filled", + "tabler-alert-circle-off", + "tabler-alert-circle", + "tabler-alert-hexagon-filled", + "tabler-alert-hexagon-off", + "tabler-alert-hexagon", + "tabler-alert-octagon-filled", + "tabler-alert-octagon", + "tabler-alert-small-off", + "tabler-alert-small", + "tabler-alert-square-filled", + "tabler-alert-square-rounded-filled", + "tabler-alert-square-rounded-off", + "tabler-alert-square-rounded", + "tabler-alert-square", + "tabler-alert-triangle-filled", + "tabler-alert-triangle-off", + "tabler-alert-triangle", + "tabler-alien-filled", + "tabler-alien", + "tabler-align-box-bottom-center-filled", + "tabler-align-box-bottom-center", + "tabler-align-box-bottom-left-filled", + "tabler-align-box-bottom-left", + "tabler-align-box-bottom-right-filled", + "tabler-align-box-bottom-right", + "tabler-align-box-center-bottom", + "tabler-align-box-center-middle-filled", + "tabler-align-box-center-middle", + "tabler-align-box-center-stretch", + "tabler-align-box-center-top", + "tabler-align-box-left-bottom-filled", + "tabler-align-box-left-bottom", + "tabler-align-box-left-middle-filled", + "tabler-align-box-left-middle", + "tabler-align-box-left-stretch", + "tabler-align-box-left-top-filled", + "tabler-align-box-left-top", + "tabler-align-box-right-bottom-filled", + "tabler-align-box-right-bottom", + "tabler-align-box-right-middle-filled", + "tabler-align-box-right-middle", + "tabler-align-box-right-stretch", + "tabler-align-box-right-top-filled", + "tabler-align-box-right-top", + "tabler-align-box-top-center-filled", + "tabler-align-box-top-center", + "tabler-align-box-top-left-filled", + "tabler-align-box-top-left", + "tabler-align-box-top-right-filled", + "tabler-align-box-top-right", + "tabler-align-center", + "tabler-align-justified", + "tabler-align-left", + "tabler-align-right", + "tabler-alpha", + "tabler-alphabet-cyrillic", + "tabler-alphabet-greek", + "tabler-alphabet-latin", + "tabler-alt", + "tabler-ambulance", + "tabler-ampersand", + "tabler-analyze-filled", + "tabler-analyze-off", + "tabler-analyze", + "tabler-anchor-off", + "tabler-anchor", + "tabler-angle", + "tabler-ankh", + "tabler-antenna-bars-1", + "tabler-antenna-bars-2", + "tabler-antenna-bars-3", + "tabler-antenna-bars-4", + "tabler-antenna-bars-5", + "tabler-antenna-bars-off", + "tabler-antenna-off", + "tabler-antenna", + "tabler-aperture-off", + "tabler-aperture", + "tabler-api-app-off", + "tabler-api-app", + "tabler-api-off", + "tabler-api", + "tabler-app-window-filled", + "tabler-app-window", + "tabler-apple", + "tabler-apps-filled", + "tabler-apps-off", + "tabler-apps", + "tabler-archery-arrow", + "tabler-archive-filled", + "tabler-archive-off", + "tabler-archive", + "tabler-armchair-2-off", + "tabler-armchair-2", + "tabler-armchair-off", + "tabler-armchair", + "tabler-arrow-autofit-content-filled", + "tabler-arrow-autofit-content", + "tabler-arrow-autofit-down", + "tabler-arrow-autofit-height", + "tabler-arrow-autofit-left", + "tabler-arrow-autofit-right", + "tabler-arrow-autofit-up", + "tabler-arrow-autofit-width", + "tabler-arrow-back-up-double", + "tabler-arrow-back-up", + "tabler-arrow-back", + "tabler-arrow-badge-down-filled", + "tabler-arrow-badge-down", + "tabler-arrow-badge-left-filled", + "tabler-arrow-badge-left", + "tabler-arrow-badge-right-filled", + "tabler-arrow-badge-right", + "tabler-arrow-badge-up-filled", + "tabler-arrow-badge-up", + "tabler-arrow-bar-both", + "tabler-arrow-bar-down", + "tabler-arrow-bar-left", + "tabler-arrow-bar-right", + "tabler-arrow-bar-to-down", + "tabler-arrow-bar-to-left", + "tabler-arrow-bar-to-right", + "tabler-arrow-bar-to-up", + "tabler-arrow-bar-up", + "tabler-arrow-bear-left-2", + "tabler-arrow-bear-left", + "tabler-arrow-bear-right-2", + "tabler-arrow-bear-right", + "tabler-arrow-big-down-filled", + "tabler-arrow-big-down-line-filled", + "tabler-arrow-big-down-line", + "tabler-arrow-big-down-lines-filled", + "tabler-arrow-big-down-lines", + "tabler-arrow-big-down", + "tabler-arrow-big-left-filled", + "tabler-arrow-big-left-line-filled", + "tabler-arrow-big-left-line", + "tabler-arrow-big-left-lines-filled", + "tabler-arrow-big-left-lines", + "tabler-arrow-big-left", + "tabler-arrow-big-right-filled", + "tabler-arrow-big-right-line-filled", + "tabler-arrow-big-right-line", + "tabler-arrow-big-right-lines-filled", + "tabler-arrow-big-right-lines", + "tabler-arrow-big-right", + "tabler-arrow-big-up-filled", + "tabler-arrow-big-up-line-filled", + "tabler-arrow-big-up-line", + "tabler-arrow-big-up-lines-filled", + "tabler-arrow-big-up-lines", + "tabler-arrow-big-up", + "tabler-arrow-bounce", + "tabler-arrow-capsule", + "tabler-arrow-curve-left", + "tabler-arrow-curve-right", + "tabler-arrow-down-bar", + "tabler-arrow-down-circle", + "tabler-arrow-down-left-circle", + "tabler-arrow-down-left", + "tabler-arrow-down-rhombus", + "tabler-arrow-down-right-circle", + "tabler-arrow-down-right", + "tabler-arrow-down-square", + "tabler-arrow-down-tail", + "tabler-arrow-down", + "tabler-arrow-elbow-left", + "tabler-arrow-elbow-right", + "tabler-arrow-fork", + "tabler-arrow-forward-up-double", + "tabler-arrow-forward-up", + "tabler-arrow-forward", + "tabler-arrow-guide", + "tabler-arrow-iteration", + "tabler-arrow-left-bar", + "tabler-arrow-left-circle", + "tabler-arrow-left-rhombus", + "tabler-arrow-left-right", + "tabler-arrow-left-square", + "tabler-arrow-left-tail", + "tabler-arrow-left", + "tabler-arrow-loop-left-2", + "tabler-arrow-loop-left", + "tabler-arrow-loop-right-2", + "tabler-arrow-loop-right", + "tabler-arrow-merge-alt-left", + "tabler-arrow-merge-alt-right", + "tabler-arrow-merge-both", + "tabler-arrow-merge-left", + "tabler-arrow-merge-right", + "tabler-arrow-merge", + "tabler-arrow-move-down", + "tabler-arrow-move-left", + "tabler-arrow-move-right", + "tabler-arrow-move-up", + "tabler-arrow-narrow-down", + "tabler-arrow-narrow-left", + "tabler-arrow-narrow-right", + "tabler-arrow-narrow-up", + "tabler-arrow-ramp-left-2", + "tabler-arrow-ramp-left-3", + "tabler-arrow-ramp-left", + "tabler-arrow-ramp-right-2", + "tabler-arrow-ramp-right-3", + "tabler-arrow-ramp-right", + "tabler-arrow-right-bar", + "tabler-arrow-right-circle", + "tabler-arrow-right-rhombus", + "tabler-arrow-right-square", + "tabler-arrow-right-tail", + "tabler-arrow-right", + "tabler-arrow-rotary-first-left", + "tabler-arrow-rotary-first-right", + "tabler-arrow-rotary-last-left", + "tabler-arrow-rotary-last-right", + "tabler-arrow-rotary-left", + "tabler-arrow-rotary-right", + "tabler-arrow-rotary-straight", + "tabler-arrow-roundabout-left", + "tabler-arrow-roundabout-right", + "tabler-arrow-sharp-turn-left", + "tabler-arrow-sharp-turn-right", + "tabler-arrow-up-bar", + "tabler-arrow-up-circle", + "tabler-arrow-up-left-circle", + "tabler-arrow-up-left", + "tabler-arrow-up-rhombus", + "tabler-arrow-up-right-circle", + "tabler-arrow-up-right", + "tabler-arrow-up-square", + "tabler-arrow-up-tail", + "tabler-arrow-up", + "tabler-arrow-wave-left-down", + "tabler-arrow-wave-left-up", + "tabler-arrow-wave-right-down", + "tabler-arrow-wave-right-up", + "tabler-arrow-zig-zag", + "tabler-arrows-cross", + "tabler-arrows-diagonal-2", + "tabler-arrows-diagonal-minimize-2", + "tabler-arrows-diagonal-minimize", + "tabler-arrows-diagonal", + "tabler-arrows-diff", + "tabler-arrows-double-ne-sw", + "tabler-arrows-double-nw-se", + "tabler-arrows-double-se-nw", + "tabler-arrows-double-sw-ne", + "tabler-arrows-down-up", + "tabler-arrows-down", + "tabler-arrows-exchange-2", + "tabler-arrows-exchange", + "tabler-arrows-horizontal", + "tabler-arrows-join-2", + "tabler-arrows-join", + "tabler-arrows-left-down", + "tabler-arrows-left-right", + "tabler-arrows-left", + "tabler-arrows-maximize", + "tabler-arrows-minimize", + "tabler-arrows-move-horizontal", + "tabler-arrows-move-vertical", + "tabler-arrows-move", + "tabler-arrows-random", + "tabler-arrows-right-down", + "tabler-arrows-right-left", + "tabler-arrows-right", + "tabler-arrows-shuffle-2", + "tabler-arrows-shuffle", + "tabler-arrows-sort", + "tabler-arrows-split-2", + "tabler-arrows-split", + "tabler-arrows-transfer-down", + "tabler-arrows-transfer-up", + "tabler-arrows-up-down", + "tabler-arrows-up-left", + "tabler-arrows-up-right", + "tabler-arrows-up", + "tabler-arrows-vertical", + "tabler-artboard-filled", + "tabler-artboard-off", + "tabler-artboard", + "tabler-article-filled-filled", + "tabler-article-off", + "tabler-article", + "tabler-aspect-ratio-filled", + "tabler-aspect-ratio-off", + "tabler-aspect-ratio", + "tabler-assembly-off", + "tabler-assembly", + "tabler-asset", + "tabler-asterisk-simple", + "tabler-asterisk", + "tabler-at-off", + "tabler-at", + "tabler-atom-2-filled", + "tabler-atom-2", + "tabler-atom-off", + "tabler-atom", + "tabler-augmented-reality-2", + "tabler-augmented-reality-off", + "tabler-augmented-reality", + "tabler-automatic-gearbox", + "tabler-award-filled", + "tabler-award-off", + "tabler-award", + "tabler-axe", + "tabler-axis-x", + "tabler-axis-y", + "tabler-baby-bottle", + "tabler-baby-carriage", + "tabler-backhoe", + "tabler-backpack-off", + "tabler-backpack", + "tabler-backslash", + "tabler-backspace-filled", + "tabler-backspace", + "tabler-badge-3d", + "tabler-badge-4k", + "tabler-badge-8k", + "tabler-badge-ad", + "tabler-badge-ar", + "tabler-badge-cc", + "tabler-badge-filled", + "tabler-badge-hd", + "tabler-badge-off", + "tabler-badge-sd", + "tabler-badge-tm", + "tabler-badge-vo", + "tabler-badge-vr", + "tabler-badge-wc", + "tabler-badge", + "tabler-badges-filled", + "tabler-badges-off", + "tabler-badges", + "tabler-baguette", + "tabler-ball-american-football-off", + "tabler-ball-american-football", + "tabler-ball-baseball", + "tabler-ball-basketball", + "tabler-ball-bowling", + "tabler-ball-football-off", + "tabler-ball-football", + "tabler-ball-tennis", + "tabler-ball-volleyball", + "tabler-balloon-filled", + "tabler-balloon-off", + "tabler-balloon", + "tabler-ballpen-filled", + "tabler-ballpen-off", + "tabler-ballpen", + "tabler-ban", + "tabler-bandage-filled", + "tabler-bandage-off", + "tabler-bandage", + "tabler-barbell-off", + "tabler-barbell", + "tabler-barcode-off", + "tabler-barcode", + "tabler-barrel-off", + "tabler-barrel", + "tabler-barrier-block-off", + "tabler-barrier-block", + "tabler-baseline-density-large", + "tabler-baseline-density-medium", + "tabler-baseline-density-small", + "tabler-baseline", + "tabler-basket-bolt", + "tabler-basket-cancel", + "tabler-basket-check", + "tabler-basket-code", + "tabler-basket-cog", + "tabler-basket-discount", + "tabler-basket-dollar", + "tabler-basket-down", + "tabler-basket-exclamation", + "tabler-basket-filled", + "tabler-basket-heart", + "tabler-basket-minus", + "tabler-basket-off", + "tabler-basket-pause", + "tabler-basket-pin", + "tabler-basket-plus", + "tabler-basket-question", + "tabler-basket-search", + "tabler-basket-share", + "tabler-basket-star", + "tabler-basket-up", + "tabler-basket-x", + "tabler-basket", + "tabler-bat", + "tabler-bath-filled", + "tabler-bath-off", + "tabler-bath", + "tabler-battery-1-filled", + "tabler-battery-1", + "tabler-battery-2-filled", + "tabler-battery-2", + "tabler-battery-3-filled", + "tabler-battery-3", + "tabler-battery-4-filled", + "tabler-battery-4", + "tabler-battery-automotive", + "tabler-battery-charging-2", + "tabler-battery-charging", + "tabler-battery-eco", + "tabler-battery-filled", + "tabler-battery-off", + "tabler-battery", + "tabler-beach-off", + "tabler-beach", + "tabler-bed-filled", + "tabler-bed-flat", + "tabler-bed-off", + "tabler-bed", + "tabler-beer-filled", + "tabler-beer-off", + "tabler-beer", + "tabler-bell-bolt", + "tabler-bell-cancel", + "tabler-bell-check", + "tabler-bell-code", + "tabler-bell-cog", + "tabler-bell-dollar", + "tabler-bell-down", + "tabler-bell-exclamation", + "tabler-bell-filled", + "tabler-bell-heart", + "tabler-bell-minus-filled", + "tabler-bell-minus", + "tabler-bell-off", + "tabler-bell-pause", + "tabler-bell-pin", + "tabler-bell-plus-filled", + "tabler-bell-plus", + "tabler-bell-question", + "tabler-bell-ringing-2-filled", + "tabler-bell-ringing-2", + "tabler-bell-ringing-filled", + "tabler-bell-ringing", + "tabler-bell-school", + "tabler-bell-search", + "tabler-bell-share", + "tabler-bell-star", + "tabler-bell-up", + "tabler-bell-x-filled", + "tabler-bell-x", + "tabler-bell-z-filled", + "tabler-bell-z", + "tabler-bell", + "tabler-beta", + "tabler-bible", + "tabler-bike-off", + "tabler-bike", + "tabler-binary-off", + "tabler-binary-tree-2", + "tabler-binary-tree", + "tabler-binary", + "tabler-biohazard-off", + "tabler-biohazard", + "tabler-blade-filled", + "tabler-blade", + "tabler-bleach-chlorine", + "tabler-bleach-no-chlorine", + "tabler-bleach-off", + "tabler-bleach", + "tabler-blender", + "tabler-blockquote", + "tabler-bluetooth-connected", + "tabler-bluetooth-off", + "tabler-bluetooth-x", + "tabler-bluetooth", + "tabler-blur-off", + "tabler-blur", + "tabler-bmp", + "tabler-body-scan", + "tabler-bold-off", + "tabler-bold", + "tabler-bolt-off", + "tabler-bolt", + "tabler-bomb-filled", + "tabler-bomb", + "tabler-bone-off", + "tabler-bone", + "tabler-bong-off", + "tabler-bong", + "tabler-book-2", + "tabler-book-download", + "tabler-book-filled", + "tabler-book-off", + "tabler-book-upload", + "tabler-book", + "tabler-bookmark-ai", + "tabler-bookmark-edit", + "tabler-bookmark-filled", + "tabler-bookmark-minus", + "tabler-bookmark-off", + "tabler-bookmark-plus", + "tabler-bookmark-question", + "tabler-bookmark", + "tabler-bookmarks-filled", + "tabler-bookmarks-off", + "tabler-bookmarks", + "tabler-books-off", + "tabler-books", + "tabler-border-all", + "tabler-border-bottom", + "tabler-border-corners", + "tabler-border-horizontal", + "tabler-border-inner", + "tabler-border-left", + "tabler-border-none", + "tabler-border-outer", + "tabler-border-radius", + "tabler-border-right", + "tabler-border-sides", + "tabler-border-style-2", + "tabler-border-style", + "tabler-border-top", + "tabler-border-vertical", + "tabler-bottle-filled", + "tabler-bottle-off", + "tabler-bottle", + "tabler-bounce-left-filled", + "tabler-bounce-left", + "tabler-bounce-right-filled", + "tabler-bounce-right", + "tabler-bow", + "tabler-bowl-filled", + "tabler-bowl", + "tabler-box-align-bottom-filled", + "tabler-box-align-bottom-left-filled", + "tabler-box-align-bottom-left", + "tabler-box-align-bottom-right-filled", + "tabler-box-align-bottom-right", + "tabler-box-align-bottom", + "tabler-box-align-left-filled", + "tabler-box-align-left", + "tabler-box-align-right-filled", + "tabler-box-align-right", + "tabler-box-align-top-filled", + "tabler-box-align-top-left-filled", + "tabler-box-align-top-left", + "tabler-box-align-top-right-filled", + "tabler-box-align-top-right", + "tabler-box-align-top", + "tabler-box-margin", + "tabler-box-model-2-off", + "tabler-box-model-2", + "tabler-box-model-off", + "tabler-box-model", + "tabler-box-multiple-0", + "tabler-box-multiple-1", + "tabler-box-multiple-2", + "tabler-box-multiple-3", + "tabler-box-multiple-4", + "tabler-box-multiple-5", + "tabler-box-multiple-6", + "tabler-box-multiple-7", + "tabler-box-multiple-8", + "tabler-box-multiple-9", + "tabler-box-multiple", + "tabler-box-off", + "tabler-box-padding", + "tabler-box-seam", + "tabler-box", + "tabler-braces-off", + "tabler-braces", + "tabler-brackets-angle-off", + "tabler-brackets-angle", + "tabler-brackets-contain-end", + "tabler-brackets-contain-start", + "tabler-brackets-contain", + "tabler-brackets-off", + "tabler-brackets", + "tabler-braille", + "tabler-brain", + "tabler-brand-4chan", + "tabler-brand-abstract", + "tabler-brand-adobe", + "tabler-brand-adonis-js", + "tabler-brand-airbnb", + "tabler-brand-airtable", + "tabler-brand-algolia", + "tabler-brand-alipay", + "tabler-brand-alpine-js", + "tabler-brand-amazon", + "tabler-brand-amd", + "tabler-brand-amigo", + "tabler-brand-among-us", + "tabler-brand-android", + "tabler-brand-angular", + "tabler-brand-ansible", + "tabler-brand-ao3", + "tabler-brand-appgallery", + "tabler-brand-apple-arcade", + "tabler-brand-apple-podcast", + "tabler-brand-apple", + "tabler-brand-appstore", + "tabler-brand-asana", + "tabler-brand-auth0", + "tabler-brand-aws", + "tabler-brand-azure", + "tabler-brand-backbone", + "tabler-brand-badoo", + "tabler-brand-baidu", + "tabler-brand-bandcamp", + "tabler-brand-bandlab", + "tabler-brand-beats", + "tabler-brand-behance", + "tabler-brand-bilibili", + "tabler-brand-binance", + "tabler-brand-bing", + "tabler-brand-bitbucket", + "tabler-brand-blackberry", + "tabler-brand-blender", + "tabler-brand-blogger", + "tabler-brand-booking", + "tabler-brand-bootstrap", + "tabler-brand-bulma", + "tabler-brand-bumble", + "tabler-brand-bunpo", + "tabler-brand-c-sharp", + "tabler-brand-cake", + "tabler-brand-cakephp", + "tabler-brand-campaignmonitor", + "tabler-brand-carbon", + "tabler-brand-cashapp", + "tabler-brand-chrome", + "tabler-brand-cinema-4d", + "tabler-brand-citymapper", + "tabler-brand-cloudflare", + "tabler-brand-codecov", + "tabler-brand-codepen", + "tabler-brand-codesandbox", + "tabler-brand-cohost", + "tabler-brand-coinbase", + "tabler-brand-comedy-central", + "tabler-brand-coreos", + "tabler-brand-couchdb", + "tabler-brand-couchsurfing", + "tabler-brand-cpp", + "tabler-brand-craft", + "tabler-brand-crunchbase", + "tabler-brand-css3", + "tabler-brand-ctemplar", + "tabler-brand-cucumber", + "tabler-brand-cupra", + "tabler-brand-cypress", + "tabler-brand-d3", + "tabler-brand-databricks", + "tabler-brand-days-counter", + "tabler-brand-dcos", + "tabler-brand-debian", + "tabler-brand-deezer", + "tabler-brand-deliveroo", + "tabler-brand-deno", + "tabler-brand-denodo", + "tabler-brand-deviantart", + "tabler-brand-digg", + "tabler-brand-dingtalk", + "tabler-brand-discord-filled", + "tabler-brand-discord", + "tabler-brand-disney", + "tabler-brand-disqus", + "tabler-brand-django", + "tabler-brand-docker", + "tabler-brand-doctrine", + "tabler-brand-dolby-digital", + "tabler-brand-douban", + "tabler-brand-dribbble-filled", + "tabler-brand-dribbble", + "tabler-brand-drops", + "tabler-brand-drupal", + "tabler-brand-edge", + "tabler-brand-elastic", + "tabler-brand-electronic-arts", + "tabler-brand-ember", + "tabler-brand-envato", + "tabler-brand-etsy", + "tabler-brand-evernote", + "tabler-brand-facebook-filled", + "tabler-brand-facebook", + "tabler-brand-feedly", + "tabler-brand-figma", + "tabler-brand-filezilla", + "tabler-brand-finder", + "tabler-brand-firebase", + "tabler-brand-firefox", + "tabler-brand-fiverr", + "tabler-brand-flickr", + "tabler-brand-flightradar24", + "tabler-brand-flipboard", + "tabler-brand-flutter", + "tabler-brand-fortnite", + "tabler-brand-foursquare", + "tabler-brand-framer-motion", + "tabler-brand-framer", + "tabler-brand-funimation", + "tabler-brand-gatsby", + "tabler-brand-git", + "tabler-brand-github-copilot", + "tabler-brand-github-filled", + "tabler-brand-github", + "tabler-brand-gitlab", + "tabler-brand-gmail", + "tabler-brand-golang", + "tabler-brand-google-analytics", + "tabler-brand-google-big-query", + "tabler-brand-google-drive", + "tabler-brand-google-fit", + "tabler-brand-google-home", + "tabler-brand-google-maps", + "tabler-brand-google-one", + "tabler-brand-google-photos", + "tabler-brand-google-play", + "tabler-brand-google-podcasts", + "tabler-brand-google", + "tabler-brand-grammarly", + "tabler-brand-graphql", + "tabler-brand-gravatar", + "tabler-brand-grindr", + "tabler-brand-guardian", + "tabler-brand-gumroad", + "tabler-brand-hbo", + "tabler-brand-headlessui", + "tabler-brand-hexo", + "tabler-brand-hipchat", + "tabler-brand-html5", + "tabler-brand-inertia", + "tabler-brand-instagram", + "tabler-brand-intercom", + "tabler-brand-itch", + "tabler-brand-javascript", + "tabler-brand-juejin", + "tabler-brand-kbin", + "tabler-brand-kick", + "tabler-brand-kickstarter", + "tabler-brand-kotlin", + "tabler-brand-laravel", + "tabler-brand-lastfm", + "tabler-brand-leetcode", + "tabler-brand-letterboxd", + "tabler-brand-line", + "tabler-brand-linkedin", + "tabler-brand-linktree", + "tabler-brand-linqpad", + "tabler-brand-loom", + "tabler-brand-mailgun", + "tabler-brand-mantine", + "tabler-brand-mastercard", + "tabler-brand-mastodon", + "tabler-brand-matrix", + "tabler-brand-mcdonalds", + "tabler-brand-medium", + "tabler-brand-meetup", + "tabler-brand-mercedes", + "tabler-brand-messenger", + "tabler-brand-meta", + "tabler-brand-minecraft", + "tabler-brand-miniprogram", + "tabler-brand-mixpanel", + "tabler-brand-monday", + "tabler-brand-mongodb", + "tabler-brand-my-oppo", + "tabler-brand-mysql", + "tabler-brand-national-geographic", + "tabler-brand-nem", + "tabler-brand-netbeans", + "tabler-brand-netease-music", + "tabler-brand-netflix", + "tabler-brand-nexo", + "tabler-brand-nextcloud", + "tabler-brand-nextjs", + "tabler-brand-nodejs", + "tabler-brand-nord-vpn", + "tabler-brand-notion", + "tabler-brand-npm", + "tabler-brand-nuxt", + "tabler-brand-nytimes", + "tabler-brand-oauth", + "tabler-brand-office", + "tabler-brand-ok-ru", + "tabler-brand-onedrive", + "tabler-brand-onlyfans", + "tabler-brand-open-source", + "tabler-brand-openai", + "tabler-brand-openvpn", + "tabler-brand-opera", + "tabler-brand-pagekit", + "tabler-brand-parsinta", + "tabler-brand-patreon-filled", + "tabler-brand-patreon", + "tabler-brand-paypal-filled", + "tabler-brand-paypal", + "tabler-brand-paypay", + "tabler-brand-peanut", + "tabler-brand-pepsi", + "tabler-brand-php", + "tabler-brand-picsart", + "tabler-brand-pinterest", + "tabler-brand-planetscale", + "tabler-brand-pocket", + "tabler-brand-polymer", + "tabler-brand-powershell", + "tabler-brand-prisma", + "tabler-brand-producthunt", + "tabler-brand-pushbullet", + "tabler-brand-pushover", + "tabler-brand-python", + "tabler-brand-qq", + "tabler-brand-radix-ui", + "tabler-brand-react-native", + "tabler-brand-react", + "tabler-brand-reason", + "tabler-brand-reddit", + "tabler-brand-redhat", + "tabler-brand-redux", + "tabler-brand-revolut", + "tabler-brand-rumble", + "tabler-brand-rust", + "tabler-brand-safari", + "tabler-brand-samsungpass", + "tabler-brand-sass", + "tabler-brand-sentry", + "tabler-brand-sharik", + "tabler-brand-shazam", + "tabler-brand-shopee", + "tabler-brand-sketch", + "tabler-brand-skype", + "tabler-brand-slack", + "tabler-brand-snapchat", + "tabler-brand-snapseed", + "tabler-brand-snowflake", + "tabler-brand-socket-io", + "tabler-brand-solidjs", + "tabler-brand-soundcloud", + "tabler-brand-spacehey", + "tabler-brand-speedtest", + "tabler-brand-spotify", + "tabler-brand-stackoverflow", + "tabler-brand-stackshare", + "tabler-brand-steam", + "tabler-brand-storj", + "tabler-brand-storybook", + "tabler-brand-storytel", + "tabler-brand-strava", + "tabler-brand-stripe", + "tabler-brand-sublime-text", + "tabler-brand-sugarizer", + "tabler-brand-supabase", + "tabler-brand-superhuman", + "tabler-brand-supernova", + "tabler-brand-surfshark", + "tabler-brand-svelte", + "tabler-brand-swift", + "tabler-brand-symfony", + "tabler-brand-tabler", + "tabler-brand-tailwind", + "tabler-brand-taobao", + "tabler-brand-teams", + "tabler-brand-ted", + "tabler-brand-telegram", + "tabler-brand-terraform", + "tabler-brand-tether", + "tabler-brand-threads", + "tabler-brand-threejs", + "tabler-brand-tidal", + "tabler-brand-tiktok-filled", + "tabler-brand-tiktok", + "tabler-brand-tinder", + "tabler-brand-topbuzz", + "tabler-brand-torchain", + "tabler-brand-toyota", + "tabler-brand-trello", + "tabler-brand-tripadvisor", + "tabler-brand-tumblr", + "tabler-brand-twilio", + "tabler-brand-twitch", + "tabler-brand-twitter-filled", + "tabler-brand-twitter", + "tabler-brand-typescript", + "tabler-brand-uber", + "tabler-brand-ubuntu", + "tabler-brand-unity", + "tabler-brand-unsplash", + "tabler-brand-upwork", + "tabler-brand-valorant", + "tabler-brand-vercel", + "tabler-brand-vimeo", + "tabler-brand-vinted", + "tabler-brand-visa", + "tabler-brand-visual-studio", + "tabler-brand-vite", + "tabler-brand-vivaldi", + "tabler-brand-vk", + "tabler-brand-vlc", + "tabler-brand-volkswagen", + "tabler-brand-vsco", + "tabler-brand-vscode", + "tabler-brand-vue", + "tabler-brand-walmart", + "tabler-brand-waze", + "tabler-brand-webflow", + "tabler-brand-wechat", + "tabler-brand-weibo", + "tabler-brand-whatsapp", + "tabler-brand-wikipedia", + "tabler-brand-windows", + "tabler-brand-windy", + "tabler-brand-wish", + "tabler-brand-wix", + "tabler-brand-wordpress", + "tabler-brand-x-filled", + "tabler-brand-x", + "tabler-brand-xamarin", + "tabler-brand-xbox", + "tabler-brand-xdeep", + "tabler-brand-xing", + "tabler-brand-yahoo", + "tabler-brand-yandex", + "tabler-brand-yatse", + "tabler-brand-ycombinator", + "tabler-brand-youtube-filled", + "tabler-brand-youtube-kids", + "tabler-brand-youtube", + "tabler-brand-zalando", + "tabler-brand-zapier", + "tabler-brand-zeit", + "tabler-brand-zhihu", + "tabler-brand-zoom", + "tabler-brand-zulip", + "tabler-brand-zwift", + "tabler-bread-off", + "tabler-bread", + "tabler-briefcase-2", + "tabler-briefcase-filled", + "tabler-briefcase-off", + "tabler-briefcase", + "tabler-brightness-2", + "tabler-brightness-down-filled", + "tabler-brightness-down", + "tabler-brightness-half", + "tabler-brightness-off", + "tabler-brightness-up-filled", + "tabler-brightness-up", + "tabler-brightness", + "tabler-broadcast-off", + "tabler-broadcast", + "tabler-browser-check", + "tabler-browser-off", + "tabler-browser-plus", + "tabler-browser-x", + "tabler-browser", + "tabler-brush-off", + "tabler-brush", + "tabler-bucket-droplet", + "tabler-bucket-off", + "tabler-bucket", + "tabler-bug-filled", + "tabler-bug-off", + "tabler-bug", + "tabler-building-arch", + "tabler-building-bank", + "tabler-building-bridge-2", + "tabler-building-bridge", + "tabler-building-broadcast-tower", + "tabler-building-carousel", + "tabler-building-castle", + "tabler-building-church", + "tabler-building-circus", + "tabler-building-community", + "tabler-building-cottage", + "tabler-building-estate", + "tabler-building-factory-2", + "tabler-building-factory", + "tabler-building-fortress", + "tabler-building-hospital", + "tabler-building-lighthouse", + "tabler-building-monument", + "tabler-building-mosque", + "tabler-building-pavilion", + "tabler-building-skyscraper", + "tabler-building-stadium", + "tabler-building-store", + "tabler-building-tunnel", + "tabler-building-warehouse", + "tabler-building-wind-turbine", + "tabler-building", + "tabler-bulb-filled", + "tabler-bulb-off", + "tabler-bulb", + "tabler-bulldozer", + "tabler-burger", + "tabler-bus-off", + "tabler-bus-stop", + "tabler-bus", + "tabler-businessplan", + "tabler-butterfly", + "tabler-cactus-filled", + "tabler-cactus-off", + "tabler-cactus", + "tabler-cake-off", + "tabler-cake", + "tabler-calculator-filled", + "tabler-calculator-off", + "tabler-calculator", + "tabler-calendar-bolt", + "tabler-calendar-cancel", + "tabler-calendar-check", + "tabler-calendar-code", + "tabler-calendar-cog", + "tabler-calendar-dollar", + "tabler-calendar-down", + "tabler-calendar-due", + "tabler-calendar-event", + "tabler-calendar-exclamation", + "tabler-calendar-filled", + "tabler-calendar-heart", + "tabler-calendar-minus", + "tabler-calendar-off", + "tabler-calendar-pause", + "tabler-calendar-pin", + "tabler-calendar-plus", + "tabler-calendar-question", + "tabler-calendar-repeat", + "tabler-calendar-search", + "tabler-calendar-share", + "tabler-calendar-star", + "tabler-calendar-stats", + "tabler-calendar-time", + "tabler-calendar-up", + "tabler-calendar-x", + "tabler-calendar", + "tabler-camera-bolt", + "tabler-camera-cancel", + "tabler-camera-check", + "tabler-camera-code", + "tabler-camera-cog", + "tabler-camera-dollar", + "tabler-camera-down", + "tabler-camera-exclamation", + "tabler-camera-filled", + "tabler-camera-heart", + "tabler-camera-minus", + "tabler-camera-off", + "tabler-camera-pause", + "tabler-camera-pin", + "tabler-camera-plus", + "tabler-camera-question", + "tabler-camera-rotate", + "tabler-camera-search", + "tabler-camera-selfie", + "tabler-camera-share", + "tabler-camera-star", + "tabler-camera-up", + "tabler-camera-x", + "tabler-camera", + "tabler-camper", + "tabler-campfire-filled", + "tabler-campfire", + "tabler-candle-filled", + "tabler-candle", + "tabler-candy-off", + "tabler-candy", + "tabler-cane", + "tabler-cannabis", + "tabler-capsule-filled", + "tabler-capsule-horizontal-filled", + "tabler-capsule-horizontal", + "tabler-capsule", + "tabler-capture-filled", + "tabler-capture-off", + "tabler-capture", + "tabler-car-crane", + "tabler-car-crash", + "tabler-car-garage", + "tabler-car-off", + "tabler-car-suv", + "tabler-car-turbine", + "tabler-car", + "tabler-caravan", + "tabler-cardboards-off", + "tabler-cardboards", + "tabler-cards-filled", + "tabler-cards", + "tabler-caret-down-filled", + "tabler-caret-down", + "tabler-caret-left-filled", + "tabler-caret-left-right-filled", + "tabler-caret-left-right", + "tabler-caret-left", + "tabler-caret-right-filled", + "tabler-caret-right", + "tabler-caret-up-down-filled", + "tabler-caret-up-down", + "tabler-caret-up-filled", + "tabler-caret-up", + "tabler-carousel-horizontal-filled", + "tabler-carousel-horizontal", + "tabler-carousel-vertical-filled", + "tabler-carousel-vertical", + "tabler-carrot-off", + "tabler-carrot", + "tabler-cash-banknote-off", + "tabler-cash-banknote", + "tabler-cash-off", + "tabler-cash", + "tabler-cast-off", + "tabler-cast", + "tabler-cat", + "tabler-category-2", + "tabler-category-filled", + "tabler-category", + "tabler-ce-off", + "tabler-ce", + "tabler-cell-signal-1", + "tabler-cell-signal-2", + "tabler-cell-signal-3", + "tabler-cell-signal-4", + "tabler-cell-signal-5", + "tabler-cell-signal-off", + "tabler-cell", + "tabler-certificate-2-off", + "tabler-certificate-2", + "tabler-certificate-off", + "tabler-certificate", + "tabler-chair-director", + "tabler-chalkboard-off", + "tabler-chalkboard", + "tabler-charging-pile", + "tabler-chart-arcs-3", + "tabler-chart-arcs", + "tabler-chart-area-filled", + "tabler-chart-area-line-filled", + "tabler-chart-area-line", + "tabler-chart-area", + "tabler-chart-arrows-vertical", + "tabler-chart-arrows", + "tabler-chart-bar-off", + "tabler-chart-bar", + "tabler-chart-bubble-filled", + "tabler-chart-bubble", + "tabler-chart-candle-filled", + "tabler-chart-candle", + "tabler-chart-circles", + "tabler-chart-donut-2", + "tabler-chart-donut-3", + "tabler-chart-donut-4", + "tabler-chart-donut-filled", + "tabler-chart-donut", + "tabler-chart-dots-2", + "tabler-chart-dots-3", + "tabler-chart-dots-filled", + "tabler-chart-dots", + "tabler-chart-grid-dots-filled", + "tabler-chart-grid-dots", + "tabler-chart-histogram", + "tabler-chart-infographic", + "tabler-chart-line", + "tabler-chart-pie-2", + "tabler-chart-pie-3", + "tabler-chart-pie-4", + "tabler-chart-pie-filled", + "tabler-chart-pie-off", + "tabler-chart-pie", + "tabler-chart-ppf", + "tabler-chart-radar", + "tabler-chart-sankey", + "tabler-chart-treemap", + "tabler-check", + "tabler-checkbox", + "tabler-checklist", + "tabler-checks", + "tabler-checkup-list", + "tabler-cheese", + "tabler-chef-hat-off", + "tabler-chef-hat", + "tabler-cherry-filled", + "tabler-cherry", + "tabler-chess-bishop-filled", + "tabler-chess-bishop", + "tabler-chess-filled", + "tabler-chess-king-filled", + "tabler-chess-king", + "tabler-chess-knight-filled", + "tabler-chess-knight", + "tabler-chess-queen-filled", + "tabler-chess-queen", + "tabler-chess-rook-filled", + "tabler-chess-rook", + "tabler-chess", + "tabler-chevron-compact-down", + "tabler-chevron-compact-left", + "tabler-chevron-compact-right", + "tabler-chevron-compact-up", + "tabler-chevron-down-left", + "tabler-chevron-down-right", + "tabler-chevron-down", + "tabler-chevron-left-pipe", + "tabler-chevron-left", + "tabler-chevron-right-pipe", + "tabler-chevron-right", + "tabler-chevron-up-left", + "tabler-chevron-up-right", + "tabler-chevron-up", + "tabler-chevrons-down-left", + "tabler-chevrons-down-right", + "tabler-chevrons-down", + "tabler-chevrons-left", + "tabler-chevrons-right", + "tabler-chevrons-up-left", + "tabler-chevrons-up-right", + "tabler-chevrons-up", + "tabler-chisel", + "tabler-christmas-tree-off", + "tabler-christmas-tree", + "tabler-circle-0-filled", + "tabler-circle-1-filled", + "tabler-circle-2-filled", + "tabler-circle-3-filled", + "tabler-circle-4-filled", + "tabler-circle-5-filled", + "tabler-circle-6-filled", + "tabler-circle-7-filled", + "tabler-circle-8-filled", + "tabler-circle-9-filled", + "tabler-circle-arrow-down-filled", + "tabler-circle-arrow-down-left-filled", + "tabler-circle-arrow-down-left", + "tabler-circle-arrow-down-right-filled", + "tabler-circle-arrow-down-right", + "tabler-circle-arrow-down", + "tabler-circle-arrow-left-filled", + "tabler-circle-arrow-left", + "tabler-circle-arrow-right-filled", + "tabler-circle-arrow-right", + "tabler-circle-arrow-up-filled", + "tabler-circle-arrow-up-left-filled", + "tabler-circle-arrow-up-left", + "tabler-circle-arrow-up-right-filled", + "tabler-circle-arrow-up-right", + "tabler-circle-arrow-up", + "tabler-circle-caret-down", + "tabler-circle-caret-left", + "tabler-circle-caret-right", + "tabler-circle-caret-up", + "tabler-circle-check-filled", + "tabler-circle-check", + "tabler-circle-chevron-down", + "tabler-circle-chevron-left", + "tabler-circle-chevron-right", + "tabler-circle-chevron-up", + "tabler-circle-chevrons-down", + "tabler-circle-chevrons-left", + "tabler-circle-chevrons-right", + "tabler-circle-chevrons-up", + "tabler-circle-dashed-number-0", + "tabler-circle-dashed-number-1", + "tabler-circle-dashed-number-2", + "tabler-circle-dashed-number-3", + "tabler-circle-dashed-number-4", + "tabler-circle-dashed-number-5", + "tabler-circle-dashed-number-6", + "tabler-circle-dashed-number-7", + "tabler-circle-dashed-number-8", + "tabler-circle-dashed-number-9", + "tabler-circle-dashed-x", + "tabler-circle-dashed", + "tabler-circle-dot-filled", + "tabler-circle-dot", + "tabler-circle-dotted", + "tabler-circle-filled", + "tabler-circle-half-2", + "tabler-circle-half-vertical", + "tabler-circle-half", + "tabler-circle-key-filled", + "tabler-circle-key", + "tabler-circle-letter-a", + "tabler-circle-letter-b", + "tabler-circle-letter-c", + "tabler-circle-letter-d", + "tabler-circle-letter-e", + "tabler-circle-letter-f", + "tabler-circle-letter-g", + "tabler-circle-letter-h", + "tabler-circle-letter-i", + "tabler-circle-letter-j", + "tabler-circle-letter-k", + "tabler-circle-letter-l", + "tabler-circle-letter-m", + "tabler-circle-letter-n", + "tabler-circle-letter-o", + "tabler-circle-letter-p", + "tabler-circle-letter-q", + "tabler-circle-letter-r", + "tabler-circle-letter-s", + "tabler-circle-letter-t", + "tabler-circle-letter-u", + "tabler-circle-letter-v", + "tabler-circle-letter-w", + "tabler-circle-letter-x", + "tabler-circle-letter-y", + "tabler-circle-letter-z", + "tabler-circle-minus-2", + "tabler-circle-minus", + "tabler-circle-number-0", + "tabler-circle-number-1", + "tabler-circle-number-2", + "tabler-circle-number-3", + "tabler-circle-number-4", + "tabler-circle-number-5", + "tabler-circle-number-6", + "tabler-circle-number-7", + "tabler-circle-number-8", + "tabler-circle-number-9", + "tabler-circle-off", + "tabler-circle-plus-2", + "tabler-circle-plus", + "tabler-circle-rectangle-off", + "tabler-circle-rectangle", + "tabler-circle-square", + "tabler-circle-triangle", + "tabler-circle-x-filled", + "tabler-circle-x", + "tabler-circle", + "tabler-circles-filled", + "tabler-circles-relation", + "tabler-circles", + "tabler-circuit-ammeter", + "tabler-circuit-battery", + "tabler-circuit-bulb", + "tabler-circuit-capacitor-polarized", + "tabler-circuit-capacitor", + "tabler-circuit-cell-plus", + "tabler-circuit-cell", + "tabler-circuit-changeover", + "tabler-circuit-diode-zener", + "tabler-circuit-diode", + "tabler-circuit-ground-digital", + "tabler-circuit-ground", + "tabler-circuit-inductor", + "tabler-circuit-motor", + "tabler-circuit-pushbutton", + "tabler-circuit-resistor", + "tabler-circuit-switch-closed", + "tabler-circuit-switch-open", + "tabler-circuit-voltmeter", + "tabler-clear-all", + "tabler-clear-formatting", + "tabler-click", + "tabler-clipboard-check", + "tabler-clipboard-copy", + "tabler-clipboard-data", + "tabler-clipboard-heart", + "tabler-clipboard-list", + "tabler-clipboard-off", + "tabler-clipboard-plus", + "tabler-clipboard-text", + "tabler-clipboard-typography", + "tabler-clipboard-x", + "tabler-clipboard", + "tabler-clock-12", + "tabler-clock-2", + "tabler-clock-24", + "tabler-clock-bolt", + "tabler-clock-cancel", + "tabler-clock-check", + "tabler-clock-code", + "tabler-clock-cog", + "tabler-clock-dollar", + "tabler-clock-down", + "tabler-clock-edit", + "tabler-clock-exclamation", + "tabler-clock-filled", + "tabler-clock-heart", + "tabler-clock-hour-1", + "tabler-clock-hour-10", + "tabler-clock-hour-11", + "tabler-clock-hour-12", + "tabler-clock-hour-2", + "tabler-clock-hour-3", + "tabler-clock-hour-4", + "tabler-clock-hour-5", + "tabler-clock-hour-6", + "tabler-clock-hour-7", + "tabler-clock-hour-8", + "tabler-clock-hour-9", + "tabler-clock-minus", + "tabler-clock-off", + "tabler-clock-pause", + "tabler-clock-pin", + "tabler-clock-play", + "tabler-clock-plus", + "tabler-clock-question", + "tabler-clock-record", + "tabler-clock-search", + "tabler-clock-share", + "tabler-clock-shield", + "tabler-clock-star", + "tabler-clock-stop", + "tabler-clock-up", + "tabler-clock-x", + "tabler-clock", + "tabler-clothes-rack-off", + "tabler-clothes-rack", + "tabler-cloud-bolt", + "tabler-cloud-cancel", + "tabler-cloud-check", + "tabler-cloud-code", + "tabler-cloud-cog", + "tabler-cloud-computing", + "tabler-cloud-data-connection", + "tabler-cloud-dollar", + "tabler-cloud-down", + "tabler-cloud-download", + "tabler-cloud-exclamation", + "tabler-cloud-filled", + "tabler-cloud-fog", + "tabler-cloud-heart", + "tabler-cloud-lock-open", + "tabler-cloud-lock", + "tabler-cloud-minus", + "tabler-cloud-network", + "tabler-cloud-off", + "tabler-cloud-pause", + "tabler-cloud-pin", + "tabler-cloud-plus", + "tabler-cloud-question", + "tabler-cloud-rain", + "tabler-cloud-search", + "tabler-cloud-share", + "tabler-cloud-snow", + "tabler-cloud-star", + "tabler-cloud-storm", + "tabler-cloud-up", + "tabler-cloud-upload", + "tabler-cloud-x", + "tabler-cloud", + "tabler-clover-2", + "tabler-clover", + "tabler-clubs-filled", + "tabler-clubs", + "tabler-code-asterix", + "tabler-code-circle-2", + "tabler-code-circle", + "tabler-code-dots", + "tabler-code-minus", + "tabler-code-off", + "tabler-code-plus", + "tabler-code", + "tabler-coffee-off", + "tabler-coffee", + "tabler-coffin", + "tabler-coin-bitcoin-filled", + "tabler-coin-bitcoin", + "tabler-coin-euro-filled", + "tabler-coin-euro", + "tabler-coin-filled", + "tabler-coin-monero-filled", + "tabler-coin-monero", + "tabler-coin-off", + "tabler-coin-pound-filled", + "tabler-coin-pound", + "tabler-coin-rupee-filled", + "tabler-coin-rupee", + "tabler-coin-taka-filled", + "tabler-coin-taka", + "tabler-coin-yen-filled", + "tabler-coin-yen", + "tabler-coin-yuan-filled", + "tabler-coin-yuan", + "tabler-coin", + "tabler-coins", + "tabler-color-filter", + "tabler-color-picker-off", + "tabler-color-picker", + "tabler-color-swatch-off", + "tabler-color-swatch", + "tabler-column-insert-left", + "tabler-column-insert-right", + "tabler-column-remove", + "tabler-columns-1", + "tabler-columns-2", + "tabler-columns-3", + "tabler-columns-off", + "tabler-columns", + "tabler-comet", + "tabler-command-off", + "tabler-command", + "tabler-compass-filled", + "tabler-compass-off", + "tabler-compass", + "tabler-components-off", + "tabler-components", + "tabler-cone-2", + "tabler-cone-off", + "tabler-cone-plus", + "tabler-cone", + "tabler-confetti-off", + "tabler-confetti", + "tabler-confucius", + "tabler-container-off", + "tabler-container", + "tabler-contrast-2-off", + "tabler-contrast-2", + "tabler-contrast-off", + "tabler-contrast", + "tabler-cooker", + "tabler-cookie-man", + "tabler-cookie-off", + "tabler-cookie", + "tabler-copy-off", + "tabler-copy", + "tabler-copyleft-filled", + "tabler-copyleft-off", + "tabler-copyleft", + "tabler-copyright-filled", + "tabler-copyright-off", + "tabler-copyright", + "tabler-corner-down-left-double", + "tabler-corner-down-left", + "tabler-corner-down-right-double", + "tabler-corner-down-right", + "tabler-corner-left-down-double", + "tabler-corner-left-down", + "tabler-corner-left-up-double", + "tabler-corner-left-up", + "tabler-corner-right-down-double", + "tabler-corner-right-down", + "tabler-corner-right-up-double", + "tabler-corner-right-up", + "tabler-corner-up-left-double", + "tabler-corner-up-left", + "tabler-corner-up-right-double", + "tabler-corner-up-right", + "tabler-cpu-2", + "tabler-cpu-off", + "tabler-cpu", + "tabler-crane-off", + "tabler-crane", + "tabler-creative-commons-by", + "tabler-creative-commons-nc", + "tabler-creative-commons-nd", + "tabler-creative-commons-off", + "tabler-creative-commons-sa", + "tabler-creative-commons-zero", + "tabler-creative-commons", + "tabler-credit-card-filled", + "tabler-credit-card-off", + "tabler-credit-card", + "tabler-cricket", + "tabler-crop", + "tabler-cross-filled", + "tabler-cross-off", + "tabler-cross", + "tabler-crosshair", + "tabler-crown-off", + "tabler-crown", + "tabler-crutches-off", + "tabler-crutches", + "tabler-crystal-ball", + "tabler-csv", + "tabler-cube-off", + "tabler-cube-plus", + "tabler-cube-send", + "tabler-cube-unfolded", + "tabler-cube", + "tabler-cup-off", + "tabler-cup", + "tabler-curling", + "tabler-curly-loop", + "tabler-currency-afghani", + "tabler-currency-bahraini", + "tabler-currency-baht", + "tabler-currency-bitcoin", + "tabler-currency-cent", + "tabler-currency-dinar", + "tabler-currency-dirham", + "tabler-currency-dogecoin", + "tabler-currency-dollar-australian", + "tabler-currency-dollar-brunei", + "tabler-currency-dollar-canadian", + "tabler-currency-dollar-guyanese", + "tabler-currency-dollar-off", + "tabler-currency-dollar-singapore", + "tabler-currency-dollar-zimbabwean", + "tabler-currency-dollar", + "tabler-currency-dong", + "tabler-currency-dram", + "tabler-currency-ethereum", + "tabler-currency-euro-off", + "tabler-currency-euro", + "tabler-currency-florin", + "tabler-currency-forint", + "tabler-currency-frank", + "tabler-currency-guarani", + "tabler-currency-hryvnia", + "tabler-currency-iranian-rial", + "tabler-currency-kip", + "tabler-currency-krone-czech", + "tabler-currency-krone-danish", + "tabler-currency-krone-swedish", + "tabler-currency-lari", + "tabler-currency-leu", + "tabler-currency-lira", + "tabler-currency-litecoin", + "tabler-currency-lyd", + "tabler-currency-manat", + "tabler-currency-monero", + "tabler-currency-naira", + "tabler-currency-nano", + "tabler-currency-off", + "tabler-currency-paanga", + "tabler-currency-peso", + "tabler-currency-pound-off", + "tabler-currency-pound", + "tabler-currency-quetzal", + "tabler-currency-real", + "tabler-currency-renminbi", + "tabler-currency-ripple", + "tabler-currency-riyal", + "tabler-currency-rubel", + "tabler-currency-rufiyaa", + "tabler-currency-rupee-nepalese", + "tabler-currency-rupee", + "tabler-currency-shekel", + "tabler-currency-solana", + "tabler-currency-som", + "tabler-currency-taka", + "tabler-currency-tenge", + "tabler-currency-tugrik", + "tabler-currency-won", + "tabler-currency-yen-off", + "tabler-currency-yen", + "tabler-currency-yuan", + "tabler-currency-zloty", + "tabler-currency", + "tabler-current-location-off", + "tabler-current-location", + "tabler-cursor-off", + "tabler-cursor-text", + "tabler-cut", + "tabler-cylinder-off", + "tabler-cylinder-plus", + "tabler-cylinder", + "tabler-dashboard-off", + "tabler-dashboard", + "tabler-database-cog", + "tabler-database-dollar", + "tabler-database-edit", + "tabler-database-exclamation", + "tabler-database-export", + "tabler-database-heart", + "tabler-database-import", + "tabler-database-leak", + "tabler-database-minus", + "tabler-database-off", + "tabler-database-plus", + "tabler-database-search", + "tabler-database-share", + "tabler-database-star", + "tabler-database-x", + "tabler-database", + "tabler-decimal", + "tabler-deer", + "tabler-delta", + "tabler-dental-broken", + "tabler-dental-off", + "tabler-dental", + "tabler-deselect", + "tabler-details-off", + "tabler-details", + "tabler-device-airpods-case", + "tabler-device-airpods", + "tabler-device-airtag", + "tabler-device-analytics", + "tabler-device-audio-tape", + "tabler-device-camera-phone", + "tabler-device-cctv-off", + "tabler-device-cctv", + "tabler-device-computer-camera-off", + "tabler-device-computer-camera", + "tabler-device-desktop-analytics", + "tabler-device-desktop-bolt", + "tabler-device-desktop-cancel", + "tabler-device-desktop-check", + "tabler-device-desktop-code", + "tabler-device-desktop-cog", + "tabler-device-desktop-dollar", + "tabler-device-desktop-down", + "tabler-device-desktop-exclamation", + "tabler-device-desktop-heart", + "tabler-device-desktop-minus", + "tabler-device-desktop-off", + "tabler-device-desktop-pause", + "tabler-device-desktop-pin", + "tabler-device-desktop-plus", + "tabler-device-desktop-question", + "tabler-device-desktop-search", + "tabler-device-desktop-share", + "tabler-device-desktop-star", + "tabler-device-desktop-up", + "tabler-device-desktop-x", + "tabler-device-desktop", + "tabler-device-floppy", + "tabler-device-gamepad-2", + "tabler-device-gamepad-3", + "tabler-device-gamepad", + "tabler-device-heart-monitor-filled", + "tabler-device-heart-monitor", + "tabler-device-imac-bolt", + "tabler-device-imac-cancel", + "tabler-device-imac-check", + "tabler-device-imac-code", + "tabler-device-imac-cog", + "tabler-device-imac-dollar", + "tabler-device-imac-down", + "tabler-device-imac-exclamation", + "tabler-device-imac-heart", + "tabler-device-imac-minus", + "tabler-device-imac-off", + "tabler-device-imac-pause", + "tabler-device-imac-pin", + "tabler-device-imac-plus", + "tabler-device-imac-question", + "tabler-device-imac-search", + "tabler-device-imac-share", + "tabler-device-imac-star", + "tabler-device-imac-up", + "tabler-device-imac-x", + "tabler-device-imac", + "tabler-device-ipad-bolt", + "tabler-device-ipad-cancel", + "tabler-device-ipad-check", + "tabler-device-ipad-code", + "tabler-device-ipad-cog", + "tabler-device-ipad-dollar", + "tabler-device-ipad-down", + "tabler-device-ipad-exclamation", + "tabler-device-ipad-heart", + "tabler-device-ipad-horizontal-bolt", + "tabler-device-ipad-horizontal-cancel", + "tabler-device-ipad-horizontal-check", + "tabler-device-ipad-horizontal-code", + "tabler-device-ipad-horizontal-cog", + "tabler-device-ipad-horizontal-dollar", + "tabler-device-ipad-horizontal-down", + "tabler-device-ipad-horizontal-exclamation", + "tabler-device-ipad-horizontal-heart", + "tabler-device-ipad-horizontal-minus", + "tabler-device-ipad-horizontal-off", + "tabler-device-ipad-horizontal-pause", + "tabler-device-ipad-horizontal-pin", + "tabler-device-ipad-horizontal-plus", + "tabler-device-ipad-horizontal-question", + "tabler-device-ipad-horizontal-search", + "tabler-device-ipad-horizontal-share", + "tabler-device-ipad-horizontal-star", + "tabler-device-ipad-horizontal-up", + "tabler-device-ipad-horizontal-x", + "tabler-device-ipad-horizontal", + "tabler-device-ipad-minus", + "tabler-device-ipad-off", + "tabler-device-ipad-pause", + "tabler-device-ipad-pin", + "tabler-device-ipad-plus", + "tabler-device-ipad-question", + "tabler-device-ipad-search", + "tabler-device-ipad-share", + "tabler-device-ipad-star", + "tabler-device-ipad-up", + "tabler-device-ipad-x", + "tabler-device-ipad", + "tabler-device-landline-phone", + "tabler-device-laptop-off", + "tabler-device-laptop", + "tabler-device-mobile-bolt", + "tabler-device-mobile-cancel", + "tabler-device-mobile-charging", + "tabler-device-mobile-check", + "tabler-device-mobile-code", + "tabler-device-mobile-cog", + "tabler-device-mobile-dollar", + "tabler-device-mobile-down", + "tabler-device-mobile-exclamation", + "tabler-device-mobile-filled", + "tabler-device-mobile-heart", + "tabler-device-mobile-message", + "tabler-device-mobile-minus", + "tabler-device-mobile-off", + "tabler-device-mobile-pause", + "tabler-device-mobile-pin", + "tabler-device-mobile-plus", + "tabler-device-mobile-question", + "tabler-device-mobile-rotated", + "tabler-device-mobile-search", + "tabler-device-mobile-share", + "tabler-device-mobile-star", + "tabler-device-mobile-up", + "tabler-device-mobile-vibration", + "tabler-device-mobile-x", + "tabler-device-mobile", + "tabler-device-nintendo-off", + "tabler-device-nintendo", + "tabler-device-projector", + "tabler-device-remote", + "tabler-device-sd-card", + "tabler-device-sim-1", + "tabler-device-sim-2", + "tabler-device-sim-3", + "tabler-device-sim", + "tabler-device-speaker-off", + "tabler-device-speaker", + "tabler-device-tablet-bolt", + "tabler-device-tablet-cancel", + "tabler-device-tablet-check", + "tabler-device-tablet-code", + "tabler-device-tablet-cog", + "tabler-device-tablet-dollar", + "tabler-device-tablet-down", + "tabler-device-tablet-exclamation", + "tabler-device-tablet-filled", + "tabler-device-tablet-heart", + "tabler-device-tablet-minus", + "tabler-device-tablet-off", + "tabler-device-tablet-pause", + "tabler-device-tablet-pin", + "tabler-device-tablet-plus", + "tabler-device-tablet-question", + "tabler-device-tablet-search", + "tabler-device-tablet-share", + "tabler-device-tablet-star", + "tabler-device-tablet-up", + "tabler-device-tablet-x", + "tabler-device-tablet", + "tabler-device-tv-off", + "tabler-device-tv-old", + "tabler-device-tv", + "tabler-device-usb", + "tabler-device-vision-pro", + "tabler-device-watch-bolt", + "tabler-device-watch-cancel", + "tabler-device-watch-check", + "tabler-device-watch-code", + "tabler-device-watch-cog", + "tabler-device-watch-dollar", + "tabler-device-watch-down", + "tabler-device-watch-exclamation", + "tabler-device-watch-heart", + "tabler-device-watch-minus", + "tabler-device-watch-off", + "tabler-device-watch-pause", + "tabler-device-watch-pin", + "tabler-device-watch-plus", + "tabler-device-watch-question", + "tabler-device-watch-search", + "tabler-device-watch-share", + "tabler-device-watch-star", + "tabler-device-watch-stats-2", + "tabler-device-watch-stats", + "tabler-device-watch-up", + "tabler-device-watch-x", + "tabler-device-watch", + "tabler-devices-2", + "tabler-devices-bolt", + "tabler-devices-cancel", + "tabler-devices-check", + "tabler-devices-code", + "tabler-devices-cog", + "tabler-devices-dollar", + "tabler-devices-down", + "tabler-devices-exclamation", + "tabler-devices-heart", + "tabler-devices-minus", + "tabler-devices-off", + "tabler-devices-pause", + "tabler-devices-pc-off", + "tabler-devices-pc", + "tabler-devices-pin", + "tabler-devices-plus", + "tabler-devices-question", + "tabler-devices-search", + "tabler-devices-share", + "tabler-devices-star", + "tabler-devices-up", + "tabler-devices-x", + "tabler-devices", + "tabler-diabolo-off", + "tabler-diabolo-plus", + "tabler-diabolo", + "tabler-dialpad-filled", + "tabler-dialpad-off", + "tabler-dialpad", + "tabler-diamond-filled", + "tabler-diamond-off", + "tabler-diamond", + "tabler-diamonds-filled", + "tabler-diamonds", + "tabler-dice-1-filled", + "tabler-dice-1", + "tabler-dice-2-filled", + "tabler-dice-2", + "tabler-dice-3-filled", + "tabler-dice-3", + "tabler-dice-4-filled", + "tabler-dice-4", + "tabler-dice-5-filled", + "tabler-dice-5", + "tabler-dice-6-filled", + "tabler-dice-6", + "tabler-dice-filled", + "tabler-dice", + "tabler-dimensions", + "tabler-direction-horizontal", + "tabler-direction-sign-filled", + "tabler-direction-sign-off", + "tabler-direction-sign", + "tabler-direction", + "tabler-directions-off", + "tabler-directions", + "tabler-disabled-2", + "tabler-disabled-off", + "tabler-disabled", + "tabler-disc-golf", + "tabler-disc-off", + "tabler-disc", + "tabler-discount-2-off", + "tabler-discount-2", + "tabler-discount-check-filled", + "tabler-discount-check", + "tabler-discount-off", + "tabler-discount", + "tabler-divide", + "tabler-dna-2-off", + "tabler-dna-2", + "tabler-dna-off", + "tabler-dna", + "tabler-dog-bowl", + "tabler-dog", + "tabler-door-enter", + "tabler-door-exit", + "tabler-door-off", + "tabler-door", + "tabler-dots-circle-horizontal", + "tabler-dots-diagonal-2", + "tabler-dots-diagonal", + "tabler-dots-vertical", + "tabler-dots", + "tabler-download-off", + "tabler-download", + "tabler-drag-drop-2", + "tabler-drag-drop", + "tabler-drone-off", + "tabler-drone", + "tabler-drop-circle", + "tabler-droplet-bolt", + "tabler-droplet-cancel", + "tabler-droplet-check", + "tabler-droplet-code", + "tabler-droplet-cog", + "tabler-droplet-dollar", + "tabler-droplet-down", + "tabler-droplet-exclamation", + "tabler-droplet-filled", + "tabler-droplet-half-2-filled", + "tabler-droplet-half-2", + "tabler-droplet-half-filled", + "tabler-droplet-half", + "tabler-droplet-heart", + "tabler-droplet-minus", + "tabler-droplet-off", + "tabler-droplet-pause", + "tabler-droplet-pin", + "tabler-droplet-plus", + "tabler-droplet-question", + "tabler-droplet-search", + "tabler-droplet-share", + "tabler-droplet-star", + "tabler-droplet-up", + "tabler-droplet-x", + "tabler-droplet", + "tabler-droplets", + "tabler-dual-screen", + "tabler-e-passport", + "tabler-ear-off", + "tabler-ear", + "tabler-ease-in-control-point", + "tabler-ease-in-out-control-points", + "tabler-ease-in-out", + "tabler-ease-in", + "tabler-ease-out-control-point", + "tabler-ease-out", + "tabler-edit-circle-off", + "tabler-edit-circle", + "tabler-edit-off", + "tabler-edit", + "tabler-egg-cracked", + "tabler-egg-filled", + "tabler-egg-fried", + "tabler-egg-off", + "tabler-egg", + "tabler-eggs", + "tabler-elevator-off", + "tabler-elevator", + "tabler-emergency-bed", + "tabler-empathize-off", + "tabler-empathize", + "tabler-emphasis", + "tabler-engine-off", + "tabler-engine", + "tabler-equal-double", + "tabler-equal-not", + "tabler-equal", + "tabler-eraser-off", + "tabler-eraser", + "tabler-error-404-off", + "tabler-error-404", + "tabler-escalator-down", + "tabler-escalator-up", + "tabler-escalator", + "tabler-exchange-off", + "tabler-exchange", + "tabler-exclamation-circle", + "tabler-exclamation-mark-off", + "tabler-exclamation-mark", + "tabler-explicit-off", + "tabler-explicit", + "tabler-exposure-0", + "tabler-exposure-minus-1", + "tabler-exposure-minus-2", + "tabler-exposure-off", + "tabler-exposure-plus-1", + "tabler-exposure-plus-2", + "tabler-exposure", + "tabler-external-link-off", + "tabler-external-link", + "tabler-eye-bolt", + "tabler-eye-cancel", + "tabler-eye-check", + "tabler-eye-closed", + "tabler-eye-code", + "tabler-eye-cog", + "tabler-eye-discount", + "tabler-eye-dollar", + "tabler-eye-down", + "tabler-eye-edit", + "tabler-eye-exclamation", + "tabler-eye-filled", + "tabler-eye-heart", + "tabler-eye-minus", + "tabler-eye-off", + "tabler-eye-pause", + "tabler-eye-pin", + "tabler-eye-plus", + "tabler-eye-question", + "tabler-eye-search", + "tabler-eye-share", + "tabler-eye-star", + "tabler-eye-table", + "tabler-eye-up", + "tabler-eye-x", + "tabler-eye", + "tabler-eyeglass-2", + "tabler-eyeglass-off", + "tabler-eyeglass", + "tabler-face-id-error", + "tabler-face-id", + "tabler-face-mask-off", + "tabler-face-mask", + "tabler-fall", + "tabler-feather-off", + "tabler-feather", + "tabler-fence-off", + "tabler-fence", + "tabler-fidget-spinner", + "tabler-file-3d", + "tabler-file-alert", + "tabler-file-analytics", + "tabler-file-arrow-left", + "tabler-file-arrow-right", + "tabler-file-barcode", + "tabler-file-broken", + "tabler-file-certificate", + "tabler-file-chart", + "tabler-file-check", + "tabler-file-code-2", + "tabler-file-code", + "tabler-file-cv", + "tabler-file-database", + "tabler-file-delta", + "tabler-file-description", + "tabler-file-diff", + "tabler-file-digit", + "tabler-file-dislike", + "tabler-file-dollar", + "tabler-file-dots", + "tabler-file-download", + "tabler-file-euro", + "tabler-file-export", + "tabler-file-filled", + "tabler-file-function", + "tabler-file-horizontal", + "tabler-file-import", + "tabler-file-infinity", + "tabler-file-info", + "tabler-file-invoice", + "tabler-file-lambda", + "tabler-file-like", + "tabler-file-minus", + "tabler-file-music", + "tabler-file-off", + "tabler-file-orientation", + "tabler-file-pencil", + "tabler-file-percent", + "tabler-file-phone", + "tabler-file-plus", + "tabler-file-power", + "tabler-file-report", + "tabler-file-rss", + "tabler-file-scissors", + "tabler-file-search", + "tabler-file-settings", + "tabler-file-shredder", + "tabler-file-signal", + "tabler-file-spreadsheet", + "tabler-file-stack", + "tabler-file-star", + "tabler-file-symlink", + "tabler-file-text-ai", + "tabler-file-text", + "tabler-file-time", + "tabler-file-type-bmp", + "tabler-file-type-css", + "tabler-file-type-csv", + "tabler-file-type-doc", + "tabler-file-type-docx", + "tabler-file-type-html", + "tabler-file-type-jpg", + "tabler-file-type-js", + "tabler-file-type-jsx", + "tabler-file-type-pdf", + "tabler-file-type-php", + "tabler-file-type-png", + "tabler-file-type-ppt", + "tabler-file-type-rs", + "tabler-file-type-sql", + "tabler-file-type-svg", + "tabler-file-type-ts", + "tabler-file-type-tsx", + "tabler-file-type-txt", + "tabler-file-type-vue", + "tabler-file-type-xls", + "tabler-file-type-xml", + "tabler-file-type-zip", + "tabler-file-typography", + "tabler-file-unknown", + "tabler-file-upload", + "tabler-file-vector", + "tabler-file-x-filled", + "tabler-file-x", + "tabler-file-zip", + "tabler-file", + "tabler-files-off", + "tabler-files", + "tabler-filter-bolt", + "tabler-filter-cancel", + "tabler-filter-check", + "tabler-filter-code", + "tabler-filter-cog", + "tabler-filter-discount", + "tabler-filter-dollar", + "tabler-filter-down", + "tabler-filter-edit", + "tabler-filter-exclamation", + "tabler-filter-filled", + "tabler-filter-heart", + "tabler-filter-minus", + "tabler-filter-off", + "tabler-filter-pause", + "tabler-filter-pin", + "tabler-filter-plus", + "tabler-filter-question", + "tabler-filter-search", + "tabler-filter-share", + "tabler-filter-star", + "tabler-filter-up", + "tabler-filter-x", + "tabler-filter", + "tabler-filters", + "tabler-fingerprint-off", + "tabler-fingerprint-scan", + "tabler-fingerprint", + "tabler-fire-extinguisher", + "tabler-fire-hydrant-off", + "tabler-fire-hydrant", + "tabler-firetruck", + "tabler-first-aid-kit-off", + "tabler-first-aid-kit", + "tabler-fish-bone", + "tabler-fish-christianity", + "tabler-fish-hook-off", + "tabler-fish-hook", + "tabler-fish-off", + "tabler-fish", + "tabler-flag-2-filled", + "tabler-flag-2-off", + "tabler-flag-2", + "tabler-flag-3-filled", + "tabler-flag-3", + "tabler-flag-bolt", + "tabler-flag-cancel", + "tabler-flag-check", + "tabler-flag-code", + "tabler-flag-cog", + "tabler-flag-discount", + "tabler-flag-dollar", + "tabler-flag-down", + "tabler-flag-exclamation", + "tabler-flag-filled", + "tabler-flag-heart", + "tabler-flag-minus", + "tabler-flag-off", + "tabler-flag-pause", + "tabler-flag-pin", + "tabler-flag-plus", + "tabler-flag-question", + "tabler-flag-search", + "tabler-flag-share", + "tabler-flag-star", + "tabler-flag-up", + "tabler-flag-x", + "tabler-flag", + "tabler-flame-off", + "tabler-flame", + "tabler-flare", + "tabler-flask-2-filled", + "tabler-flask-2-off", + "tabler-flask-2", + "tabler-flask-filled", + "tabler-flask-off", + "tabler-flask", + "tabler-flip-flops", + "tabler-flip-horizontal", + "tabler-flip-vertical", + "tabler-float-center", + "tabler-float-left", + "tabler-float-none", + "tabler-float-right", + "tabler-flower-off", + "tabler-flower", + "tabler-focus-2", + "tabler-focus-auto", + "tabler-focus-centered", + "tabler-focus", + "tabler-fold-down", + "tabler-fold-up", + "tabler-fold", + "tabler-folder-bolt", + "tabler-folder-cancel", + "tabler-folder-check", + "tabler-folder-code", + "tabler-folder-cog", + "tabler-folder-dollar", + "tabler-folder-down", + "tabler-folder-exclamation", + "tabler-folder-filled", + "tabler-folder-heart", + "tabler-folder-minus", + "tabler-folder-off", + "tabler-folder-open", + "tabler-folder-pause", + "tabler-folder-pin", + "tabler-folder-plus", + "tabler-folder-question", + "tabler-folder-search", + "tabler-folder-share", + "tabler-folder-star", + "tabler-folder-symlink", + "tabler-folder-up", + "tabler-folder-x", + "tabler-folder", + "tabler-folders-off", + "tabler-folders", + "tabler-forbid-2-filled", + "tabler-forbid-2", + "tabler-forbid-filled", + "tabler-forbid", + "tabler-forklift", + "tabler-forms", + "tabler-fountain-filled", + "tabler-fountain-off", + "tabler-fountain", + "tabler-frame-off", + "tabler-frame", + "tabler-free-rights", + "tabler-freeze-column", + "tabler-freeze-row-column", + "tabler-freeze-row", + "tabler-fridge-off", + "tabler-fridge", + "tabler-friends-off", + "tabler-friends", + "tabler-frustum-off", + "tabler-frustum-plus", + "tabler-frustum", + "tabler-function-filled", + "tabler-function-off", + "tabler-function", + "tabler-galaxy", + "tabler-garden-cart-off", + "tabler-garden-cart", + "tabler-gas-station-off", + "tabler-gas-station", + "tabler-gauge-filled", + "tabler-gauge-off", + "tabler-gauge", + "tabler-gavel", + "tabler-gender-agender", + "tabler-gender-androgyne", + "tabler-gender-bigender", + "tabler-gender-demiboy", + "tabler-gender-demigirl", + "tabler-gender-epicene", + "tabler-gender-female", + "tabler-gender-femme", + "tabler-gender-genderfluid", + "tabler-gender-genderless", + "tabler-gender-genderqueer", + "tabler-gender-hermaphrodite", + "tabler-gender-intergender", + "tabler-gender-male", + "tabler-gender-neutrois", + "tabler-gender-third", + "tabler-gender-transgender", + "tabler-gender-trasvesti", + "tabler-geometry", + "tabler-ghost-2-filled", + "tabler-ghost-2", + "tabler-ghost-3", + "tabler-ghost-filled", + "tabler-ghost-off", + "tabler-ghost", + "tabler-gif", + "tabler-gift-card-filled", + "tabler-gift-card", + "tabler-gift-filled", + "tabler-gift-off", + "tabler-gift", + "tabler-git-branch-deleted", + "tabler-git-branch", + "tabler-git-cherry-pick", + "tabler-git-commit", + "tabler-git-compare", + "tabler-git-fork", + "tabler-git-merge", + "tabler-git-pull-request-closed", + "tabler-git-pull-request-draft", + "tabler-git-pull-request", + "tabler-gizmo", + "tabler-glass-full-filled", + "tabler-glass-full", + "tabler-glass-off", + "tabler-glass", + "tabler-globe-filled", + "tabler-globe-off", + "tabler-globe", + "tabler-go-game", + "tabler-golf-off", + "tabler-golf", + "tabler-gps", + "tabler-gradienter", + "tabler-grain", + "tabler-graph-filled", + "tabler-graph-off", + "tabler-graph", + "tabler-grave-2", + "tabler-grave", + "tabler-grid-3x3", + "tabler-grid-4x4", + "tabler-grid-dots", + "tabler-grid-goldenratio", + "tabler-grid-pattern", + "tabler-grid-scan", + "tabler-grill-fork", + "tabler-grill-off", + "tabler-grill-spatula", + "tabler-grill", + "tabler-grip-horizontal", + "tabler-grip-vertical", + "tabler-growth", + "tabler-guitar-pick-filled", + "tabler-guitar-pick", + "tabler-h-1", + "tabler-h-2", + "tabler-h-3", + "tabler-h-4", + "tabler-h-5", + "tabler-h-6", + "tabler-hammer-off", + "tabler-hammer", + "tabler-hand-click", + "tabler-hand-finger-off", + "tabler-hand-finger", + "tabler-hand-grab", + "tabler-hand-little-finger", + "tabler-hand-middle-finger", + "tabler-hand-move", + "tabler-hand-off", + "tabler-hand-ring-finger", + "tabler-hand-rock", + "tabler-hand-sanitizer", + "tabler-hand-stop", + "tabler-hand-three-fingers", + "tabler-hand-two-fingers", + "tabler-hanger-2", + "tabler-hanger-off", + "tabler-hanger", + "tabler-hash", + "tabler-haze-moon", + "tabler-haze", + "tabler-hdr", + "tabler-heading-off", + "tabler-heading", + "tabler-headphones-filled", + "tabler-headphones-off", + "tabler-headphones", + "tabler-headset-off", + "tabler-headset", + "tabler-health-recognition", + "tabler-heart-bolt", + "tabler-heart-broken", + "tabler-heart-cancel", + "tabler-heart-check", + "tabler-heart-code", + "tabler-heart-cog", + "tabler-heart-discount", + "tabler-heart-dollar", + "tabler-heart-down", + "tabler-heart-exclamation", + "tabler-heart-filled", + "tabler-heart-handshake", + "tabler-heart-minus", + "tabler-heart-off", + "tabler-heart-pause", + "tabler-heart-pin", + "tabler-heart-plus", + "tabler-heart-question", + "tabler-heart-rate-monitor", + "tabler-heart-search", + "tabler-heart-share", + "tabler-heart-star", + "tabler-heart-up", + "tabler-heart-x", + "tabler-heart", + "tabler-heartbeat", + "tabler-hearts-off", + "tabler-hearts", + "tabler-helicopter-landing", + "tabler-helicopter", + "tabler-helmet-off", + "tabler-helmet", + "tabler-help-circle-filled", + "tabler-help-circle", + "tabler-help-hexagon-filled", + "tabler-help-hexagon", + "tabler-help-octagon-filled", + "tabler-help-octagon", + "tabler-help-off", + "tabler-help-small", + "tabler-help-square-filled", + "tabler-help-square-rounded-filled", + "tabler-help-square-rounded", + "tabler-help-square", + "tabler-help-triangle-filled", + "tabler-help-triangle", + "tabler-help", + "tabler-hemisphere-off", + "tabler-hemisphere-plus", + "tabler-hemisphere", + "tabler-hexagon-0-filled", + "tabler-hexagon-1-filled", + "tabler-hexagon-2-filled", + "tabler-hexagon-3-filled", + "tabler-hexagon-3d", + "tabler-hexagon-4-filled", + "tabler-hexagon-5-filled", + "tabler-hexagon-6-filled", + "tabler-hexagon-7-filled", + "tabler-hexagon-8-filled", + "tabler-hexagon-9-filled", + "tabler-hexagon-filled", + "tabler-hexagon-letter-a", + "tabler-hexagon-letter-b", + "tabler-hexagon-letter-c", + "tabler-hexagon-letter-d", + "tabler-hexagon-letter-e", + "tabler-hexagon-letter-f", + "tabler-hexagon-letter-g", + "tabler-hexagon-letter-h", + "tabler-hexagon-letter-i", + "tabler-hexagon-letter-j", + "tabler-hexagon-letter-k", + "tabler-hexagon-letter-l", + "tabler-hexagon-letter-m", + "tabler-hexagon-letter-n", + "tabler-hexagon-letter-o", + "tabler-hexagon-letter-p", + "tabler-hexagon-letter-q", + "tabler-hexagon-letter-r", + "tabler-hexagon-letter-s", + "tabler-hexagon-letter-t", + "tabler-hexagon-letter-u", + "tabler-hexagon-letter-v", + "tabler-hexagon-letter-w", + "tabler-hexagon-letter-x", + "tabler-hexagon-letter-y", + "tabler-hexagon-letter-z", + "tabler-hexagon-minus-2", + "tabler-hexagon-minus", + "tabler-hexagon-number-0", + "tabler-hexagon-number-1", + "tabler-hexagon-number-2", + "tabler-hexagon-number-3", + "tabler-hexagon-number-4", + "tabler-hexagon-number-5", + "tabler-hexagon-number-6", + "tabler-hexagon-number-7", + "tabler-hexagon-number-8", + "tabler-hexagon-number-9", + "tabler-hexagon-off", + "tabler-hexagon-plus-2", + "tabler-hexagon-plus", + "tabler-hexagon", + "tabler-hexagonal-prism-off", + "tabler-hexagonal-prism-plus", + "tabler-hexagonal-prism", + "tabler-hexagonal-pyramid-off", + "tabler-hexagonal-pyramid-plus", + "tabler-hexagonal-pyramid", + "tabler-hexagons-off", + "tabler-hexagons", + "tabler-hierarchy-2", + "tabler-hierarchy-3", + "tabler-hierarchy-off", + "tabler-hierarchy", + "tabler-highlight-off", + "tabler-highlight", + "tabler-history-off", + "tabler-history-toggle", + "tabler-history", + "tabler-home-2", + "tabler-home-bolt", + "tabler-home-cancel", + "tabler-home-check", + "tabler-home-cog", + "tabler-home-dollar", + "tabler-home-dot", + "tabler-home-down", + "tabler-home-eco", + "tabler-home-edit", + "tabler-home-exclamation", + "tabler-home-hand", + "tabler-home-heart", + "tabler-home-infinity", + "tabler-home-link", + "tabler-home-minus", + "tabler-home-move", + "tabler-home-off", + "tabler-home-plus", + "tabler-home-question", + "tabler-home-ribbon", + "tabler-home-search", + "tabler-home-share", + "tabler-home-shield", + "tabler-home-signal", + "tabler-home-star", + "tabler-home-stats", + "tabler-home-up", + "tabler-home-x", + "tabler-home", + "tabler-horse-toy", + "tabler-horse", + "tabler-horseshoe", + "tabler-hotel-service", + "tabler-hourglass-empty", + "tabler-hourglass-filled", + "tabler-hourglass-high", + "tabler-hourglass-low", + "tabler-hourglass-off", + "tabler-hourglass", + "tabler-html", + "tabler-http-connect", + "tabler-http-delete", + "tabler-http-get", + "tabler-http-head", + "tabler-http-options", + "tabler-http-patch", + "tabler-http-post", + "tabler-http-put", + "tabler-http-que", + "tabler-http-trace", + "tabler-ice-cream-2", + "tabler-ice-cream-off", + "tabler-ice-cream", + "tabler-ice-skating", + "tabler-icons-off", + "tabler-icons", + "tabler-id-badge-2", + "tabler-id-badge-off", + "tabler-id-badge", + "tabler-id-off", + "tabler-id", + "tabler-inbox-off", + "tabler-inbox", + "tabler-indent-decrease", + "tabler-indent-increase", + "tabler-infinity-off", + "tabler-infinity", + "tabler-info-circle-filled", + "tabler-info-circle", + "tabler-info-hexagon-filled", + "tabler-info-hexagon", + "tabler-info-octagon-filled", + "tabler-info-octagon", + "tabler-info-small", + "tabler-info-square-filled", + "tabler-info-square-rounded-filled", + "tabler-info-square-rounded", + "tabler-info-square", + "tabler-info-triangle-filled", + "tabler-info-triangle", + "tabler-inner-shadow-bottom-filled", + "tabler-inner-shadow-bottom-left-filled", + "tabler-inner-shadow-bottom-left", + "tabler-inner-shadow-bottom-right-filled", + "tabler-inner-shadow-bottom-right", + "tabler-inner-shadow-bottom", + "tabler-inner-shadow-left-filled", + "tabler-inner-shadow-left", + "tabler-inner-shadow-right-filled", + "tabler-inner-shadow-right", + "tabler-inner-shadow-top-filled", + "tabler-inner-shadow-top-left-filled", + "tabler-inner-shadow-top-left", + "tabler-inner-shadow-top-right-filled", + "tabler-inner-shadow-top-right", + "tabler-inner-shadow-top", + "tabler-input-ai", + "tabler-input-check", + "tabler-input-search", + "tabler-input-x", + "tabler-ironing-1", + "tabler-ironing-2", + "tabler-ironing-3", + "tabler-ironing-off", + "tabler-ironing-steam-off", + "tabler-ironing-steam", + "tabler-ironing", + "tabler-irregular-polyhedron-off", + "tabler-irregular-polyhedron-plus", + "tabler-irregular-polyhedron", + "tabler-italic", + "tabler-jacket", + "tabler-jetpack", + "tabler-jewish-star-filled", + "tabler-jewish-star", + "tabler-jpg", + "tabler-json", + "tabler-jump-rope", + "tabler-karate", + "tabler-kayak", + "tabler-kering", + "tabler-key-off", + "tabler-key", + "tabler-keyboard-hide", + "tabler-keyboard-off", + "tabler-keyboard-show", + "tabler-keyboard", + "tabler-keyframe-align-center-filled", + "tabler-keyframe-align-center", + "tabler-keyframe-align-horizontal-filled", + "tabler-keyframe-align-horizontal", + "tabler-keyframe-align-vertical-filled", + "tabler-keyframe-align-vertical", + "tabler-keyframe-filled", + "tabler-keyframe", + "tabler-keyframes-filled", + "tabler-keyframes", + "tabler-ladder-off", + "tabler-ladder", + "tabler-ladle", + "tabler-lambda", + "tabler-lamp-2", + "tabler-lamp-off", + "tabler-lamp", + "tabler-lane", + "tabler-language-hiragana", + "tabler-language-katakana", + "tabler-language-off", + "tabler-language", + "tabler-lasso-off", + "tabler-lasso-polygon", + "tabler-lasso", + "tabler-layers-difference", + "tabler-layers-intersect-2", + "tabler-layers-intersect", + "tabler-layers-linked", + "tabler-layers-off", + "tabler-layers-subtract", + "tabler-layers-union", + "tabler-layout-2", + "tabler-layout-align-bottom", + "tabler-layout-align-center", + "tabler-layout-align-left", + "tabler-layout-align-middle", + "tabler-layout-align-right", + "tabler-layout-align-top", + "tabler-layout-board-split", + "tabler-layout-board", + "tabler-layout-bottombar-collapse-filled", + "tabler-layout-bottombar-collapse", + "tabler-layout-bottombar-expand-filled", + "tabler-layout-bottombar-expand", + "tabler-layout-bottombar-filled", + "tabler-layout-bottombar", + "tabler-layout-cards", + "tabler-layout-collage", + "tabler-layout-columns", + "tabler-layout-dashboard", + "tabler-layout-distribute-horizontal", + "tabler-layout-distribute-vertical", + "tabler-layout-grid-add", + "tabler-layout-grid-remove", + "tabler-layout-grid", + "tabler-layout-kanban", + "tabler-layout-list", + "tabler-layout-navbar-collapse-filled", + "tabler-layout-navbar-collapse", + "tabler-layout-navbar-expand-filled", + "tabler-layout-navbar-expand", + "tabler-layout-navbar-filled", + "tabler-layout-navbar", + "tabler-layout-off", + "tabler-layout-rows", + "tabler-layout-sidebar-left-collapse-filled", + "tabler-layout-sidebar-left-collapse", + "tabler-layout-sidebar-left-expand-filled", + "tabler-layout-sidebar-left-expand", + "tabler-layout-sidebar-right-collapse-filled", + "tabler-layout-sidebar-right-collapse", + "tabler-layout-sidebar-right-expand-filled", + "tabler-layout-sidebar-right-expand", + "tabler-layout-sidebar-right", + "tabler-layout-sidebar", + "tabler-layout", + "tabler-leaf-off", + "tabler-leaf", + "tabler-lego-off", + "tabler-lego", + "tabler-lemon-2", + "tabler-lemon", + "tabler-letter-a-small", + "tabler-letter-a", + "tabler-letter-b-small", + "tabler-letter-b", + "tabler-letter-c-small", + "tabler-letter-c", + "tabler-letter-case-lower", + "tabler-letter-case-toggle", + "tabler-letter-case-upper", + "tabler-letter-case", + "tabler-letter-d-small", + "tabler-letter-d", + "tabler-letter-e-small", + "tabler-letter-e", + "tabler-letter-f-small", + "tabler-letter-f", + "tabler-letter-g-small", + "tabler-letter-g", + "tabler-letter-h-small", + "tabler-letter-h", + "tabler-letter-i-small", + "tabler-letter-i", + "tabler-letter-j-small", + "tabler-letter-j", + "tabler-letter-k-small", + "tabler-letter-k", + "tabler-letter-l-small", + "tabler-letter-l", + "tabler-letter-m-small", + "tabler-letter-m", + "tabler-letter-n-small", + "tabler-letter-n", + "tabler-letter-o-small", + "tabler-letter-o", + "tabler-letter-p-small", + "tabler-letter-p", + "tabler-letter-q-small", + "tabler-letter-q", + "tabler-letter-r-small", + "tabler-letter-r", + "tabler-letter-s-small", + "tabler-letter-s", + "tabler-letter-spacing", + "tabler-letter-t-small", + "tabler-letter-t", + "tabler-letter-u-small", + "tabler-letter-u", + "tabler-letter-v-small", + "tabler-letter-v", + "tabler-letter-w-small", + "tabler-letter-w", + "tabler-letter-x-small", + "tabler-letter-x", + "tabler-letter-y-small", + "tabler-letter-y", + "tabler-letter-z-small", + "tabler-letter-z", + "tabler-license-off", + "tabler-license", + "tabler-lifebuoy-off", + "tabler-lifebuoy", + "tabler-lighter", + "tabler-line-dashed", + "tabler-line-dotted", + "tabler-line-height", + "tabler-line-scan", + "tabler-line", + "tabler-link-minus", + "tabler-link-off", + "tabler-link-plus", + "tabler-link", + "tabler-list-check", + "tabler-list-details", + "tabler-list-letters", + "tabler-list-numbers", + "tabler-list-search", + "tabler-list-tree", + "tabler-list", + "tabler-live-photo-off", + "tabler-live-photo", + "tabler-live-view", + "tabler-load-balancer", + "tabler-loader-2", + "tabler-loader-3", + "tabler-loader-quarter", + "tabler-loader", + "tabler-location-bolt", + "tabler-location-broken", + "tabler-location-cancel", + "tabler-location-check", + "tabler-location-code", + "tabler-location-cog", + "tabler-location-discount", + "tabler-location-dollar", + "tabler-location-down", + "tabler-location-exclamation", + "tabler-location-filled", + "tabler-location-heart", + "tabler-location-minus", + "tabler-location-off", + "tabler-location-pause", + "tabler-location-pin", + "tabler-location-plus", + "tabler-location-question", + "tabler-location-search", + "tabler-location-share", + "tabler-location-star", + "tabler-location-up", + "tabler-location-x", + "tabler-location", + "tabler-lock-access-off", + "tabler-lock-access", + "tabler-lock-bolt", + "tabler-lock-cancel", + "tabler-lock-check", + "tabler-lock-code", + "tabler-lock-cog", + "tabler-lock-dollar", + "tabler-lock-down", + "tabler-lock-exclamation", + "tabler-lock-heart", + "tabler-lock-minus", + "tabler-lock-off", + "tabler-lock-open-off", + "tabler-lock-open", + "tabler-lock-pause", + "tabler-lock-pin", + "tabler-lock-plus", + "tabler-lock-question", + "tabler-lock-search", + "tabler-lock-share", + "tabler-lock-square-rounded-filled", + "tabler-lock-square-rounded", + "tabler-lock-square", + "tabler-lock-star", + "tabler-lock-up", + "tabler-lock-x", + "tabler-lock", + "tabler-logic-and", + "tabler-logic-buffer", + "tabler-logic-nand", + "tabler-logic-nor", + "tabler-logic-not", + "tabler-logic-or", + "tabler-logic-xnor", + "tabler-logic-xor", + "tabler-login-2", + "tabler-login", + "tabler-logout-2", + "tabler-logout", + "tabler-lollipop-off", + "tabler-lollipop", + "tabler-luggage-off", + "tabler-luggage", + "tabler-lungs-off", + "tabler-lungs", + "tabler-macro-off", + "tabler-macro", + "tabler-magnet-off", + "tabler-magnet", + "tabler-magnetic", + "tabler-mail-ai", + "tabler-mail-bolt", + "tabler-mail-cancel", + "tabler-mail-check", + "tabler-mail-code", + "tabler-mail-cog", + "tabler-mail-dollar", + "tabler-mail-down", + "tabler-mail-exclamation", + "tabler-mail-fast", + "tabler-mail-filled", + "tabler-mail-forward", + "tabler-mail-heart", + "tabler-mail-minus", + "tabler-mail-off", + "tabler-mail-opened-filled", + "tabler-mail-opened", + "tabler-mail-pause", + "tabler-mail-pin", + "tabler-mail-plus", + "tabler-mail-question", + "tabler-mail-search", + "tabler-mail-share", + "tabler-mail-star", + "tabler-mail-up", + "tabler-mail-x", + "tabler-mail", + "tabler-mailbox-off", + "tabler-mailbox", + "tabler-man", + "tabler-manual-gearbox", + "tabler-map-2", + "tabler-map-bolt", + "tabler-map-cancel", + "tabler-map-check", + "tabler-map-code", + "tabler-map-cog", + "tabler-map-discount", + "tabler-map-dollar", + "tabler-map-down", + "tabler-map-east", + "tabler-map-exclamation", + "tabler-map-heart", + "tabler-map-minus", + "tabler-map-north", + "tabler-map-off", + "tabler-map-pause", + "tabler-map-pin-2", + "tabler-map-pin-bolt", + "tabler-map-pin-cancel", + "tabler-map-pin-check", + "tabler-map-pin-code", + "tabler-map-pin-cog", + "tabler-map-pin-dollar", + "tabler-map-pin-down", + "tabler-map-pin-exclamation", + "tabler-map-pin-filled", + "tabler-map-pin-heart", + "tabler-map-pin-minus", + "tabler-map-pin-off", + "tabler-map-pin-pause", + "tabler-map-pin-pin", + "tabler-map-pin-plus", + "tabler-map-pin-question", + "tabler-map-pin-search", + "tabler-map-pin-share", + "tabler-map-pin-star", + "tabler-map-pin-up", + "tabler-map-pin-x", + "tabler-map-pin", + "tabler-map-pins", + "tabler-map-plus", + "tabler-map-question", + "tabler-map-route", + "tabler-map-search", + "tabler-map-share", + "tabler-map-south", + "tabler-map-star", + "tabler-map-up", + "tabler-map-west", + "tabler-map-x", + "tabler-map", + "tabler-markdown-off", + "tabler-markdown", + "tabler-marquee-2", + "tabler-marquee-off", + "tabler-marquee", + "tabler-mars", + "tabler-mask-off", + "tabler-mask", + "tabler-masks-theater-off", + "tabler-masks-theater", + "tabler-massage", + "tabler-matchstick", + "tabler-math-1-divide-2", + "tabler-math-1-divide-3", + "tabler-math-avg", + "tabler-math-equal-greater", + "tabler-math-equal-lower", + "tabler-math-function-off", + "tabler-math-function-y", + "tabler-math-function", + "tabler-math-greater", + "tabler-math-integral-x", + "tabler-math-integral", + "tabler-math-integrals", + "tabler-math-lower", + "tabler-math-max", + "tabler-math-min", + "tabler-math-not", + "tabler-math-off", + "tabler-math-pi-divide-2", + "tabler-math-pi", + "tabler-math-symbols", + "tabler-math-x-divide-2", + "tabler-math-x-divide-y-2", + "tabler-math-x-divide-y", + "tabler-math-x-minus-x", + "tabler-math-x-minus-y", + "tabler-math-x-plus-x", + "tabler-math-x-plus-y", + "tabler-math-xy", + "tabler-math-y-minus-y", + "tabler-math-y-plus-y", + "tabler-math", + "tabler-maximize-off", + "tabler-maximize", + "tabler-meat-off", + "tabler-meat", + "tabler-medal-2", + "tabler-medal", + "tabler-medical-cross-circle", + "tabler-medical-cross-filled", + "tabler-medical-cross-off", + "tabler-medical-cross", + "tabler-medicine-syrup", + "tabler-meeple", + "tabler-melon", + "tabler-menorah", + "tabler-menu-2", + "tabler-menu-deep", + "tabler-menu-order", + "tabler-menu", + "tabler-message-2-bolt", + "tabler-message-2-cancel", + "tabler-message-2-check", + "tabler-message-2-code", + "tabler-message-2-cog", + "tabler-message-2-dollar", + "tabler-message-2-down", + "tabler-message-2-exclamation", + "tabler-message-2-heart", + "tabler-message-2-minus", + "tabler-message-2-off", + "tabler-message-2-pause", + "tabler-message-2-pin", + "tabler-message-2-plus", + "tabler-message-2-question", + "tabler-message-2-search", + "tabler-message-2-share", + "tabler-message-2-star", + "tabler-message-2-up", + "tabler-message-2-x", + "tabler-message-2", + "tabler-message-bolt", + "tabler-message-cancel", + "tabler-message-chatbot", + "tabler-message-check", + "tabler-message-circle-2-filled", + "tabler-message-circle-2", + "tabler-message-circle-bolt", + "tabler-message-circle-cancel", + "tabler-message-circle-check", + "tabler-message-circle-code", + "tabler-message-circle-cog", + "tabler-message-circle-dollar", + "tabler-message-circle-down", + "tabler-message-circle-exclamation", + "tabler-message-circle-heart", + "tabler-message-circle-minus", + "tabler-message-circle-off", + "tabler-message-circle-pause", + "tabler-message-circle-pin", + "tabler-message-circle-plus", + "tabler-message-circle-question", + "tabler-message-circle-search", + "tabler-message-circle-share", + "tabler-message-circle-star", + "tabler-message-circle-up", + "tabler-message-circle-x", + "tabler-message-circle", + "tabler-message-code", + "tabler-message-cog", + "tabler-message-dollar", + "tabler-message-dots", + "tabler-message-down", + "tabler-message-exclamation", + "tabler-message-forward", + "tabler-message-heart", + "tabler-message-language", + "tabler-message-minus", + "tabler-message-off", + "tabler-message-pause", + "tabler-message-pin", + "tabler-message-plus", + "tabler-message-question", + "tabler-message-report", + "tabler-message-search", + "tabler-message-share", + "tabler-message-star", + "tabler-message-up", + "tabler-message-x", + "tabler-message", + "tabler-messages-off", + "tabler-messages", + "tabler-meteor-off", + "tabler-meteor", + "tabler-michelin-bib-gourmand", + "tabler-michelin-star-green", + "tabler-michelin-star", + "tabler-mickey-filled", + "tabler-mickey", + "tabler-microphone-2-off", + "tabler-microphone-2", + "tabler-microphone-off", + "tabler-microphone", + "tabler-microscope-off", + "tabler-microscope", + "tabler-microwave-off", + "tabler-microwave", + "tabler-military-award", + "tabler-military-rank", + "tabler-milk-off", + "tabler-milk", + "tabler-milkshake", + "tabler-minimize", + "tabler-minus-vertical", + "tabler-minus", + "tabler-mist-off", + "tabler-mist", + "tabler-mobiledata-off", + "tabler-mobiledata", + "tabler-moneybag", + "tabler-mood-angry", + "tabler-mood-annoyed-2", + "tabler-mood-annoyed", + "tabler-mood-boy", + "tabler-mood-check", + "tabler-mood-cog", + "tabler-mood-confuzed-filled", + "tabler-mood-confuzed", + "tabler-mood-crazy-happy", + "tabler-mood-cry", + "tabler-mood-dollar", + "tabler-mood-edit", + "tabler-mood-empty-filled", + "tabler-mood-empty", + "tabler-mood-happy-filled", + "tabler-mood-happy", + "tabler-mood-heart", + "tabler-mood-kid-filled", + "tabler-mood-kid", + "tabler-mood-look-left", + "tabler-mood-look-right", + "tabler-mood-minus", + "tabler-mood-nerd", + "tabler-mood-nervous", + "tabler-mood-neutral-filled", + "tabler-mood-neutral", + "tabler-mood-off", + "tabler-mood-pin", + "tabler-mood-plus", + "tabler-mood-sad-2", + "tabler-mood-sad-dizzy", + "tabler-mood-sad-filled", + "tabler-mood-sad-squint", + "tabler-mood-sad", + "tabler-mood-search", + "tabler-mood-share", + "tabler-mood-sick", + "tabler-mood-silence", + "tabler-mood-sing", + "tabler-mood-smile-beam", + "tabler-mood-smile-dizzy", + "tabler-mood-smile-filled", + "tabler-mood-smile", + "tabler-mood-suprised", + "tabler-mood-tongue-wink-2", + "tabler-mood-tongue-wink", + "tabler-mood-tongue", + "tabler-mood-unamused", + "tabler-mood-up", + "tabler-mood-wink-2", + "tabler-mood-wink", + "tabler-mood-wrrr", + "tabler-mood-x", + "tabler-mood-xd", + "tabler-moon-2", + "tabler-moon-filled", + "tabler-moon-off", + "tabler-moon-stars", + "tabler-moon", + "tabler-moped", + "tabler-motorbike", + "tabler-mountain-off", + "tabler-mountain", + "tabler-mouse-2", + "tabler-mouse-filled", + "tabler-mouse-off", + "tabler-mouse", + "tabler-moustache", + "tabler-movie-off", + "tabler-movie", + "tabler-mug-off", + "tabler-mug", + "tabler-multiplier-0-5x", + "tabler-multiplier-1-5x", + "tabler-multiplier-1x", + "tabler-multiplier-2x", + "tabler-mushroom-filled", + "tabler-mushroom-off", + "tabler-mushroom", + "tabler-music-bolt", + "tabler-music-cancel", + "tabler-music-check", + "tabler-music-code", + "tabler-music-cog", + "tabler-music-discount", + "tabler-music-dollar", + "tabler-music-down", + "tabler-music-exclamation", + "tabler-music-heart", + "tabler-music-minus", + "tabler-music-off", + "tabler-music-pause", + "tabler-music-pin", + "tabler-music-plus", + "tabler-music-question", + "tabler-music-search", + "tabler-music-share", + "tabler-music-star", + "tabler-music-up", + "tabler-music-x", + "tabler-music", + "tabler-navigation-bolt", + "tabler-navigation-cancel", + "tabler-navigation-check", + "tabler-navigation-code", + "tabler-navigation-cog", + "tabler-navigation-discount", + "tabler-navigation-dollar", + "tabler-navigation-down", + "tabler-navigation-east", + "tabler-navigation-exclamation", + "tabler-navigation-filled", + "tabler-navigation-heart", + "tabler-navigation-minus", + "tabler-navigation-north", + "tabler-navigation-off", + "tabler-navigation-pause", + "tabler-navigation-pin", + "tabler-navigation-plus", + "tabler-navigation-question", + "tabler-navigation-search", + "tabler-navigation-share", + "tabler-navigation-south", + "tabler-navigation-star", + "tabler-navigation-top", + "tabler-navigation-up", + "tabler-navigation-west", + "tabler-navigation-x", + "tabler-navigation", + "tabler-needle-thread", + "tabler-needle", + "tabler-network-off", + "tabler-network", + "tabler-new-section", + "tabler-news-off", + "tabler-news", + "tabler-nfc-off", + "tabler-nfc", + "tabler-no-copyright", + "tabler-no-creative-commons", + "tabler-no-derivatives", + "tabler-north-star", + "tabler-note-off", + "tabler-note", + "tabler-notebook-off", + "tabler-notebook", + "tabler-notes-off", + "tabler-notes", + "tabler-notification-off", + "tabler-notification", + "tabler-number-0-small", + "tabler-number-0", + "tabler-number-1-small", + "tabler-number-1", + "tabler-number-10-small", + "tabler-number-11-small", + "tabler-number-12-small", + "tabler-number-13-small", + "tabler-number-14-small", + "tabler-number-15-small", + "tabler-number-16-small", + "tabler-number-17-small", + "tabler-number-18-small", + "tabler-number-19-small", + "tabler-number-2-small", + "tabler-number-2", + "tabler-number-20-small", + "tabler-number-21-small", + "tabler-number-22-small", + "tabler-number-23-small", + "tabler-number-24-small", + "tabler-number-25-small", + "tabler-number-26-small", + "tabler-number-27-small", + "tabler-number-28-small", + "tabler-number-29-small", + "tabler-number-3-small", + "tabler-number-3", + "tabler-number-4-small", + "tabler-number-4", + "tabler-number-5-small", + "tabler-number-5", + "tabler-number-6-small", + "tabler-number-6", + "tabler-number-7-small", + "tabler-number-7", + "tabler-number-8-small", + "tabler-number-8", + "tabler-number-9-small", + "tabler-number-9", + "tabler-number", + "tabler-numbers", + "tabler-nurse", + "tabler-nut", + "tabler-octagon-filled", + "tabler-octagon-minus-2", + "tabler-octagon-minus", + "tabler-octagon-off", + "tabler-octagon-plus-2", + "tabler-octagon-plus", + "tabler-octagon", + "tabler-octahedron-off", + "tabler-octahedron-plus", + "tabler-octahedron", + "tabler-old", + "tabler-olympics-off", + "tabler-olympics", + "tabler-om", + "tabler-omega", + "tabler-outbound", + "tabler-outlet", + "tabler-oval-filled", + "tabler-oval-vertical-filled", + "tabler-oval-vertical", + "tabler-oval", + "tabler-overline", + "tabler-package-export", + "tabler-package-import", + "tabler-package-off", + "tabler-package", + "tabler-packages", + "tabler-pacman", + "tabler-page-break", + "tabler-paint-filled", + "tabler-paint-off", + "tabler-paint", + "tabler-palette-off", + "tabler-palette", + "tabler-panorama-horizontal-off", + "tabler-panorama-horizontal", + "tabler-panorama-vertical-off", + "tabler-panorama-vertical", + "tabler-paper-bag-off", + "tabler-paper-bag", + "tabler-paperclip", + "tabler-parachute-off", + "tabler-parachute", + "tabler-parentheses-off", + "tabler-parentheses", + "tabler-parking-off", + "tabler-parking", + "tabler-password-fingerprint", + "tabler-password-mobile-phone", + "tabler-password-user", + "tabler-password", + "tabler-paw-filled", + "tabler-paw-off", + "tabler-paw", + "tabler-pdf", + "tabler-peace", + "tabler-pencil-bolt", + "tabler-pencil-cancel", + "tabler-pencil-check", + "tabler-pencil-code", + "tabler-pencil-cog", + "tabler-pencil-discount", + "tabler-pencil-dollar", + "tabler-pencil-down", + "tabler-pencil-exclamation", + "tabler-pencil-heart", + "tabler-pencil-minus", + "tabler-pencil-off", + "tabler-pencil-pause", + "tabler-pencil-pin", + "tabler-pencil-plus", + "tabler-pencil-question", + "tabler-pencil-search", + "tabler-pencil-share", + "tabler-pencil-star", + "tabler-pencil-up", + "tabler-pencil-x", + "tabler-pencil", + "tabler-pennant-2-filled", + "tabler-pennant-2", + "tabler-pennant-filled", + "tabler-pennant-off", + "tabler-pennant", + "tabler-pentagon-filled", + "tabler-pentagon-number-0", + "tabler-pentagon-number-1", + "tabler-pentagon-number-2", + "tabler-pentagon-number-3", + "tabler-pentagon-number-4", + "tabler-pentagon-number-5", + "tabler-pentagon-number-6", + "tabler-pentagon-number-7", + "tabler-pentagon-number-8", + "tabler-pentagon-number-9", + "tabler-pentagon-off", + "tabler-pentagon-plus", + "tabler-pentagon-x", + "tabler-pentagon", + "tabler-pentagram", + "tabler-pepper-off", + "tabler-pepper", + "tabler-percentage", + "tabler-perfume", + "tabler-perspective-off", + "tabler-perspective", + "tabler-phone-call", + "tabler-phone-calling", + "tabler-phone-check", + "tabler-phone-filled", + "tabler-phone-incoming", + "tabler-phone-off", + "tabler-phone-outgoing", + "tabler-phone-pause", + "tabler-phone-plus", + "tabler-phone-x", + "tabler-phone", + "tabler-photo-ai", + "tabler-photo-bolt", + "tabler-photo-cancel", + "tabler-photo-check", + "tabler-photo-circle-minus", + "tabler-photo-circle-plus", + "tabler-photo-circle", + "tabler-photo-code", + "tabler-photo-cog", + "tabler-photo-dollar", + "tabler-photo-down", + "tabler-photo-edit", + "tabler-photo-exclamation", + "tabler-photo-filled", + "tabler-photo-heart", + "tabler-photo-hexagon", + "tabler-photo-minus", + "tabler-photo-off", + "tabler-photo-pause", + "tabler-photo-pentagon", + "tabler-photo-pin", + "tabler-photo-plus", + "tabler-photo-question", + "tabler-photo-scan", + "tabler-photo-search", + "tabler-photo-sensor-2", + "tabler-photo-sensor-3", + "tabler-photo-sensor", + "tabler-photo-share", + "tabler-photo-shield", + "tabler-photo-square-rounded", + "tabler-photo-star", + "tabler-photo-up", + "tabler-photo-video", + "tabler-photo-x", + "tabler-photo", + "tabler-physotherapist", + "tabler-piano", + "tabler-pick", + "tabler-picture-in-picture-off", + "tabler-picture-in-picture-on", + "tabler-picture-in-picture-top", + "tabler-picture-in-picture", + "tabler-pig-money", + "tabler-pig-off", + "tabler-pig", + "tabler-pilcrow", + "tabler-pill-off", + "tabler-pill", + "tabler-pills", + "tabler-pin-filled", + "tabler-pin", + "tabler-ping-pong", + "tabler-pinned-filled", + "tabler-pinned-off", + "tabler-pinned", + "tabler-pizza-off", + "tabler-pizza", + "tabler-placeholder", + "tabler-plane-arrival", + "tabler-plane-departure", + "tabler-plane-inflight", + "tabler-plane-off", + "tabler-plane-tilt", + "tabler-plane", + "tabler-planet-off", + "tabler-planet", + "tabler-plant-2-off", + "tabler-plant-2", + "tabler-plant-off", + "tabler-plant", + "tabler-play-basketball", + "tabler-play-card-off", + "tabler-play-card", + "tabler-play-football", + "tabler-play-handball", + "tabler-play-volleyball", + "tabler-player-eject-filled", + "tabler-player-eject", + "tabler-player-pause-filled", + "tabler-player-pause", + "tabler-player-play-filled", + "tabler-player-play", + "tabler-player-record-filled", + "tabler-player-record", + "tabler-player-skip-back-filled", + "tabler-player-skip-back", + "tabler-player-skip-forward-filled", + "tabler-player-skip-forward", + "tabler-player-stop-filled", + "tabler-player-stop", + "tabler-player-track-next-filled", + "tabler-player-track-next", + "tabler-player-track-prev-filled", + "tabler-player-track-prev", + "tabler-playlist-add", + "tabler-playlist-off", + "tabler-playlist-x", + "tabler-playlist", + "tabler-playstation-circle", + "tabler-playstation-square", + "tabler-playstation-triangle", + "tabler-playstation-x", + "tabler-plug-connected-x", + "tabler-plug-connected", + "tabler-plug-off", + "tabler-plug-x", + "tabler-plug", + "tabler-plus-equal", + "tabler-plus-minus", + "tabler-plus", + "tabler-png", + "tabler-podium-off", + "tabler-podium", + "tabler-point-filled", + "tabler-point-off", + "tabler-point", + "tabler-pointer-bolt", + "tabler-pointer-cancel", + "tabler-pointer-check", + "tabler-pointer-code", + "tabler-pointer-cog", + "tabler-pointer-dollar", + "tabler-pointer-down", + "tabler-pointer-exclamation", + "tabler-pointer-filled", + "tabler-pointer-heart", + "tabler-pointer-minus", + "tabler-pointer-off", + "tabler-pointer-pause", + "tabler-pointer-pin", + "tabler-pointer-plus", + "tabler-pointer-question", + "tabler-pointer-search", + "tabler-pointer-share", + "tabler-pointer-star", + "tabler-pointer-up", + "tabler-pointer-x", + "tabler-pointer", + "tabler-pokeball-off", + "tabler-pokeball", + "tabler-poker-chip", + "tabler-polaroid-filled", + "tabler-polaroid", + "tabler-polygon-off", + "tabler-polygon", + "tabler-poo", + "tabler-pool-off", + "tabler-pool", + "tabler-power", + "tabler-pray", + "tabler-premium-rights", + "tabler-prescription", + "tabler-presentation-analytics", + "tabler-presentation-off", + "tabler-presentation", + "tabler-printer-off", + "tabler-printer", + "tabler-prism-off", + "tabler-prism-plus", + "tabler-prism", + "tabler-prison", + "tabler-progress-alert", + "tabler-progress-bolt", + "tabler-progress-check", + "tabler-progress-down", + "tabler-progress-help", + "tabler-progress-x", + "tabler-progress", + "tabler-prompt", + "tabler-propeller-off", + "tabler-propeller", + "tabler-pumpkin-scary", + "tabler-puzzle-2", + "tabler-puzzle-filled", + "tabler-puzzle-off", + "tabler-puzzle", + "tabler-pyramid-off", + "tabler-pyramid-plus", + "tabler-pyramid", + "tabler-qrcode-off", + "tabler-qrcode", + "tabler-question-mark", + "tabler-quote-off", + "tabler-quote", + "tabler-quotes", + "tabler-radar-2", + "tabler-radar-off", + "tabler-radar", + "tabler-radio-off", + "tabler-radio", + "tabler-radioactive-filled", + "tabler-radioactive-off", + "tabler-radioactive", + "tabler-radius-bottom-left", + "tabler-radius-bottom-right", + "tabler-radius-top-left", + "tabler-radius-top-right", + "tabler-rainbow-off", + "tabler-rainbow", + "tabler-rating-12-plus", + "tabler-rating-14-plus", + "tabler-rating-16-plus", + "tabler-rating-18-plus", + "tabler-rating-21-plus", + "tabler-razor-electric", + "tabler-razor", + "tabler-receipt-2", + "tabler-receipt-off", + "tabler-receipt-refund", + "tabler-receipt-tax", + "tabler-receipt", + "tabler-recharging", + "tabler-record-mail-off", + "tabler-record-mail", + "tabler-rectangle-filled", + "tabler-rectangle-rounded-bottom", + "tabler-rectangle-rounded-top", + "tabler-rectangle-vertical-filled", + "tabler-rectangle-vertical", + "tabler-rectangle", + "tabler-rectangular-prism-off", + "tabler-rectangular-prism-plus", + "tabler-rectangular-prism", + "tabler-recycle-off", + "tabler-recycle", + "tabler-refresh-alert", + "tabler-refresh-dot", + "tabler-refresh-off", + "tabler-refresh", + "tabler-regex-off", + "tabler-regex", + "tabler-registered", + "tabler-relation-many-to-many", + "tabler-relation-one-to-many", + "tabler-relation-one-to-one", + "tabler-reload", + "tabler-reorder", + "tabler-repeat-off", + "tabler-repeat-once", + "tabler-repeat", + "tabler-replace-filled", + "tabler-replace-off", + "tabler-replace", + "tabler-report-analytics", + "tabler-report-medical", + "tabler-report-money", + "tabler-report-off", + "tabler-report-search", + "tabler-report", + "tabler-reserved-line", + "tabler-resize", + "tabler-restore", + "tabler-rewind-backward-10", + "tabler-rewind-backward-15", + "tabler-rewind-backward-20", + "tabler-rewind-backward-30", + "tabler-rewind-backward-40", + "tabler-rewind-backward-5", + "tabler-rewind-backward-50", + "tabler-rewind-backward-60", + "tabler-rewind-forward-10", + "tabler-rewind-forward-15", + "tabler-rewind-forward-20", + "tabler-rewind-forward-30", + "tabler-rewind-forward-40", + "tabler-rewind-forward-5", + "tabler-rewind-forward-50", + "tabler-rewind-forward-60", + "tabler-ribbon-health", + "tabler-rings", + "tabler-ripple-off", + "tabler-ripple", + "tabler-road-off", + "tabler-road-sign", + "tabler-road", + "tabler-robot-face", + "tabler-robot-off", + "tabler-robot", + "tabler-rocket-off", + "tabler-rocket", + "tabler-roller-skating", + "tabler-rollercoaster-off", + "tabler-rollercoaster", + "tabler-rosette-filled", + "tabler-rosette-number-0", + "tabler-rosette-number-1", + "tabler-rosette-number-2", + "tabler-rosette-number-3", + "tabler-rosette-number-4", + "tabler-rosette-number-5", + "tabler-rosette-number-6", + "tabler-rosette-number-7", + "tabler-rosette-number-8", + "tabler-rosette-number-9", + "tabler-rosette", + "tabler-rotate-2", + "tabler-rotate-360", + "tabler-rotate-clockwise-2", + "tabler-rotate-clockwise", + "tabler-rotate-dot", + "tabler-rotate-rectangle", + "tabler-rotate", + "tabler-route-2", + "tabler-route-alt-left", + "tabler-route-alt-right", + "tabler-route-off", + "tabler-route-scan", + "tabler-route-square-2", + "tabler-route-square", + "tabler-route-x-2", + "tabler-route-x", + "tabler-route", + "tabler-router-off", + "tabler-router", + "tabler-row-insert-bottom", + "tabler-row-insert-top", + "tabler-row-remove", + "tabler-rss", + "tabler-rubber-stamp-off", + "tabler-rubber-stamp", + "tabler-ruler-2-off", + "tabler-ruler-2", + "tabler-ruler-3", + "tabler-ruler-measure", + "tabler-ruler-off", + "tabler-ruler", + "tabler-run", + "tabler-rv-truck", + "tabler-s-turn-down", + "tabler-s-turn-left", + "tabler-s-turn-right", + "tabler-s-turn-up", + "tabler-sailboat-2", + "tabler-sailboat-off", + "tabler-sailboat", + "tabler-salad", + "tabler-salt", + "tabler-satellite-off", + "tabler-satellite", + "tabler-sausage", + "tabler-scale-off", + "tabler-scale-outline-off", + "tabler-scale-outline", + "tabler-scale", + "tabler-scan-eye", + "tabler-scan", + "tabler-schema-off", + "tabler-schema", + "tabler-school-bell", + "tabler-school-off", + "tabler-school", + "tabler-scissors-off", + "tabler-scissors", + "tabler-scooter-electric", + "tabler-scooter", + "tabler-scoreboard", + "tabler-screen-share-off", + "tabler-screen-share", + "tabler-screenshot", + "tabler-scribble-off", + "tabler-scribble", + "tabler-script-minus", + "tabler-script-plus", + "tabler-script-x", + "tabler-script", + "tabler-scuba-mask-off", + "tabler-scuba-mask", + "tabler-sdk", + "tabler-search-off", + "tabler-search", + "tabler-section-sign", + "tabler-section", + "tabler-seeding-off", + "tabler-seeding", + "tabler-select-all", + "tabler-select", + "tabler-selector", + "tabler-send-off", + "tabler-send", + "tabler-seo", + "tabler-separator-horizontal", + "tabler-separator-vertical", + "tabler-separator", + "tabler-server-2", + "tabler-server-bolt", + "tabler-server-cog", + "tabler-server-off", + "tabler-server", + "tabler-servicemark", + "tabler-settings-2", + "tabler-settings-automation", + "tabler-settings-bolt", + "tabler-settings-cancel", + "tabler-settings-check", + "tabler-settings-code", + "tabler-settings-cog", + "tabler-settings-dollar", + "tabler-settings-down", + "tabler-settings-exclamation", + "tabler-settings-filled", + "tabler-settings-heart", + "tabler-settings-minus", + "tabler-settings-off", + "tabler-settings-pause", + "tabler-settings-pin", + "tabler-settings-plus", + "tabler-settings-question", + "tabler-settings-search", + "tabler-settings-share", + "tabler-settings-star", + "tabler-settings-up", + "tabler-settings-x", + "tabler-settings", + "tabler-shadow-off", + "tabler-shadow", + "tabler-shape-2", + "tabler-shape-3", + "tabler-shape-off", + "tabler-shape", + "tabler-share-2", + "tabler-share-3", + "tabler-share-off", + "tabler-share", + "tabler-shi-jumping", + "tabler-shield-bolt", + "tabler-shield-cancel", + "tabler-shield-check-filled", + "tabler-shield-check", + "tabler-shield-checkered-filled", + "tabler-shield-checkered", + "tabler-shield-chevron", + "tabler-shield-code", + "tabler-shield-cog", + "tabler-shield-dollar", + "tabler-shield-down", + "tabler-shield-exclamation", + "tabler-shield-filled", + "tabler-shield-half-filled", + "tabler-shield-half", + "tabler-shield-heart", + "tabler-shield-lock-filled", + "tabler-shield-lock", + "tabler-shield-minus", + "tabler-shield-off", + "tabler-shield-pause", + "tabler-shield-pin", + "tabler-shield-plus", + "tabler-shield-question", + "tabler-shield-search", + "tabler-shield-share", + "tabler-shield-star", + "tabler-shield-up", + "tabler-shield-x", + "tabler-shield", + "tabler-ship-off", + "tabler-ship", + "tabler-shirt-filled", + "tabler-shirt-off", + "tabler-shirt-sport", + "tabler-shirt", + "tabler-shoe-off", + "tabler-shoe", + "tabler-shopping-bag-check", + "tabler-shopping-bag-discount", + "tabler-shopping-bag-edit", + "tabler-shopping-bag-exclamation", + "tabler-shopping-bag-minus", + "tabler-shopping-bag-plus", + "tabler-shopping-bag-search", + "tabler-shopping-bag-x", + "tabler-shopping-bag", + "tabler-shopping-cart-bolt", + "tabler-shopping-cart-cancel", + "tabler-shopping-cart-check", + "tabler-shopping-cart-code", + "tabler-shopping-cart-cog", + "tabler-shopping-cart-copy", + "tabler-shopping-cart-discount", + "tabler-shopping-cart-dollar", + "tabler-shopping-cart-down", + "tabler-shopping-cart-exclamation", + "tabler-shopping-cart-filled", + "tabler-shopping-cart-heart", + "tabler-shopping-cart-minus", + "tabler-shopping-cart-off", + "tabler-shopping-cart-pause", + "tabler-shopping-cart-pin", + "tabler-shopping-cart-plus", + "tabler-shopping-cart-question", + "tabler-shopping-cart-search", + "tabler-shopping-cart-share", + "tabler-shopping-cart-star", + "tabler-shopping-cart-up", + "tabler-shopping-cart-x", + "tabler-shopping-cart", + "tabler-shovel", + "tabler-shredder", + "tabler-sign-left-filled", + "tabler-sign-left", + "tabler-sign-right-filled", + "tabler-sign-right", + "tabler-signal-2g", + "tabler-signal-3g", + "tabler-signal-4g-plus", + "tabler-signal-4g", + "tabler-signal-5g", + "tabler-signal-6g", + "tabler-signal-e", + "tabler-signal-g", + "tabler-signal-h-plus", + "tabler-signal-h", + "tabler-signal-lte", + "tabler-signature-off", + "tabler-signature", + "tabler-sitemap-off", + "tabler-sitemap", + "tabler-skateboard-off", + "tabler-skateboard", + "tabler-skateboarding", + "tabler-skull", + "tabler-slash", + "tabler-slashes", + "tabler-sleigh", + "tabler-slice", + "tabler-slideshow", + "tabler-smart-home-off", + "tabler-smart-home", + "tabler-smoking-no", + "tabler-smoking", + "tabler-snowflake-off", + "tabler-snowflake", + "tabler-snowman", + "tabler-soccer-field", + "tabler-social-off", + "tabler-social", + "tabler-sock", + "tabler-sofa-off", + "tabler-sofa", + "tabler-solar-electricity", + "tabler-solar-panel-2", + "tabler-solar-panel", + "tabler-sort-0-9", + "tabler-sort-9-0", + "tabler-sort-a-z", + "tabler-sort-ascending-2", + "tabler-sort-ascending-letters", + "tabler-sort-ascending-numbers", + "tabler-sort-ascending", + "tabler-sort-descending-2", + "tabler-sort-descending-letters", + "tabler-sort-descending-numbers", + "tabler-sort-descending", + "tabler-sort-z-a", + "tabler-sos", + "tabler-soup-off", + "tabler-soup", + "tabler-source-code", + "tabler-space-off", + "tabler-space", + "tabler-spacing-horizontal", + "tabler-spacing-vertical", + "tabler-spade-filled", + "tabler-spade", + "tabler-sparkles", + "tabler-speakerphone", + "tabler-speedboat", + "tabler-sphere-off", + "tabler-sphere-plus", + "tabler-sphere", + "tabler-spider", + "tabler-spiral-off", + "tabler-spiral", + "tabler-sport-billard", + "tabler-spray", + "tabler-spy-off", + "tabler-spy", + "tabler-sql", + "tabler-square-0-filled", + "tabler-square-1-filled", + "tabler-square-2-filled", + "tabler-square-3-filled", + "tabler-square-4-filled", + "tabler-square-5-filled", + "tabler-square-6-filled", + "tabler-square-7-filled", + "tabler-square-8-filled", + "tabler-square-9-filled", + "tabler-square-arrow-down-filled", + "tabler-square-arrow-down", + "tabler-square-arrow-left-filled", + "tabler-square-arrow-left", + "tabler-square-arrow-right-filled", + "tabler-square-arrow-right", + "tabler-square-arrow-up-filled", + "tabler-square-arrow-up", + "tabler-square-asterisk-filled", + "tabler-square-asterisk", + "tabler-square-check-filled", + "tabler-square-check", + "tabler-square-chevron-down-filled", + "tabler-square-chevron-down", + "tabler-square-chevron-left-filled", + "tabler-square-chevron-left", + "tabler-square-chevron-right-filled", + "tabler-square-chevron-right", + "tabler-square-chevron-up-filled", + "tabler-square-chevron-up", + "tabler-square-chevrons-down-filled", + "tabler-square-chevrons-down", + "tabler-square-chevrons-left-filled", + "tabler-square-chevrons-left", + "tabler-square-chevrons-right-filled", + "tabler-square-chevrons-right", + "tabler-square-chevrons-up-filled", + "tabler-square-chevrons-up", + "tabler-square-dot-filled", + "tabler-square-dot", + "tabler-square-f0-filled", + "tabler-square-f0", + "tabler-square-f1-filled", + "tabler-square-f1", + "tabler-square-f2-filled", + "tabler-square-f2", + "tabler-square-f3-filled", + "tabler-square-f3", + "tabler-square-f4-filled", + "tabler-square-f4", + "tabler-square-f5-filled", + "tabler-square-f5", + "tabler-square-f6-filled", + "tabler-square-f6", + "tabler-square-f7-filled", + "tabler-square-f7", + "tabler-square-f8-filled", + "tabler-square-f8", + "tabler-square-f9-filled", + "tabler-square-f9", + "tabler-square-filled", + "tabler-square-forbid-2", + "tabler-square-forbid", + "tabler-square-half", + "tabler-square-key", + "tabler-square-letter-a", + "tabler-square-letter-b", + "tabler-square-letter-c", + "tabler-square-letter-d", + "tabler-square-letter-e", + "tabler-square-letter-f", + "tabler-square-letter-g", + "tabler-square-letter-h", + "tabler-square-letter-i", + "tabler-square-letter-j", + "tabler-square-letter-k", + "tabler-square-letter-l", + "tabler-square-letter-m", + "tabler-square-letter-n", + "tabler-square-letter-o", + "tabler-square-letter-p", + "tabler-square-letter-q", + "tabler-square-letter-r", + "tabler-square-letter-s", + "tabler-square-letter-t", + "tabler-square-letter-u", + "tabler-square-letter-v", + "tabler-square-letter-w", + "tabler-square-letter-x", + "tabler-square-letter-y", + "tabler-square-letter-z", + "tabler-square-minus-filled", + "tabler-square-minus", + "tabler-square-number-0", + "tabler-square-number-1", + "tabler-square-number-2", + "tabler-square-number-3", + "tabler-square-number-4", + "tabler-square-number-5", + "tabler-square-number-6", + "tabler-square-number-7", + "tabler-square-number-8", + "tabler-square-number-9", + "tabler-square-off", + "tabler-square-plus-2", + "tabler-square-plus", + "tabler-square-root-2", + "tabler-square-root", + "tabler-square-rotated-filled", + "tabler-square-rotated-forbid-2", + "tabler-square-rotated-forbid", + "tabler-square-rotated-off", + "tabler-square-rotated", + "tabler-square-rounded-arrow-down-filled", + "tabler-square-rounded-arrow-down", + "tabler-square-rounded-arrow-left-filled", + "tabler-square-rounded-arrow-left", + "tabler-square-rounded-arrow-right-filled", + "tabler-square-rounded-arrow-right", + "tabler-square-rounded-arrow-up-filled", + "tabler-square-rounded-arrow-up", + "tabler-square-rounded-check-filled", + "tabler-square-rounded-check", + "tabler-square-rounded-chevron-down-filled", + "tabler-square-rounded-chevron-down", + "tabler-square-rounded-chevron-left-filled", + "tabler-square-rounded-chevron-left", + "tabler-square-rounded-chevron-right-filled", + "tabler-square-rounded-chevron-right", + "tabler-square-rounded-chevron-up-filled", + "tabler-square-rounded-chevron-up", + "tabler-square-rounded-chevrons-down-filled", + "tabler-square-rounded-chevrons-down", + "tabler-square-rounded-chevrons-left-filled", + "tabler-square-rounded-chevrons-left", + "tabler-square-rounded-chevrons-right-filled", + "tabler-square-rounded-chevrons-right", + "tabler-square-rounded-chevrons-up-filled", + "tabler-square-rounded-chevrons-up", + "tabler-square-rounded-filled", + "tabler-square-rounded-letter-a", + "tabler-square-rounded-letter-b", + "tabler-square-rounded-letter-c", + "tabler-square-rounded-letter-d", + "tabler-square-rounded-letter-e", + "tabler-square-rounded-letter-f", + "tabler-square-rounded-letter-g", + "tabler-square-rounded-letter-h", + "tabler-square-rounded-letter-i", + "tabler-square-rounded-letter-j", + "tabler-square-rounded-letter-k", + "tabler-square-rounded-letter-l", + "tabler-square-rounded-letter-m", + "tabler-square-rounded-letter-n", + "tabler-square-rounded-letter-o", + "tabler-square-rounded-letter-p", + "tabler-square-rounded-letter-q", + "tabler-square-rounded-letter-r", + "tabler-square-rounded-letter-s", + "tabler-square-rounded-letter-t", + "tabler-square-rounded-letter-u", + "tabler-square-rounded-letter-v", + "tabler-square-rounded-letter-w", + "tabler-square-rounded-letter-x", + "tabler-square-rounded-letter-y", + "tabler-square-rounded-letter-z", + "tabler-square-rounded-minus-2", + "tabler-square-rounded-minus-filled", + "tabler-square-rounded-minus", + "tabler-square-rounded-number-0-filled", + "tabler-square-rounded-number-0", + "tabler-square-rounded-number-1-filled", + "tabler-square-rounded-number-1", + "tabler-square-rounded-number-2-filled", + "tabler-square-rounded-number-2", + "tabler-square-rounded-number-3-filled", + "tabler-square-rounded-number-3", + "tabler-square-rounded-number-4-filled", + "tabler-square-rounded-number-4", + "tabler-square-rounded-number-5-filled", + "tabler-square-rounded-number-5", + "tabler-square-rounded-number-6-filled", + "tabler-square-rounded-number-6", + "tabler-square-rounded-number-7-filled", + "tabler-square-rounded-number-7", + "tabler-square-rounded-number-8-filled", + "tabler-square-rounded-number-8", + "tabler-square-rounded-number-9-filled", + "tabler-square-rounded-number-9", + "tabler-square-rounded-plus-2", + "tabler-square-rounded-plus-filled", + "tabler-square-rounded-plus", + "tabler-square-rounded-x-filled", + "tabler-square-rounded-x", + "tabler-square-rounded", + "tabler-square-toggle-horizontal", + "tabler-square-toggle", + "tabler-square-x-filled", + "tabler-square-x", + "tabler-square", + "tabler-squares-diagonal", + "tabler-squares-filled", + "tabler-stack-2", + "tabler-stack-3", + "tabler-stack-pop", + "tabler-stack-push", + "tabler-stack", + "tabler-stairs-down", + "tabler-stairs-up", + "tabler-stairs", + "tabler-star-filled", + "tabler-star-half-filled", + "tabler-star-half", + "tabler-star-off", + "tabler-star", + "tabler-stars-filled", + "tabler-stars-off", + "tabler-stars", + "tabler-status-change", + "tabler-steam", + "tabler-steering-wheel-off", + "tabler-steering-wheel", + "tabler-step-into", + "tabler-step-out", + "tabler-stereo-glasses", + "tabler-stethoscope-off", + "tabler-stethoscope", + "tabler-sticker", + "tabler-storm-off", + "tabler-storm", + "tabler-stretching-2", + "tabler-stretching", + "tabler-strikethrough", + "tabler-submarine", + "tabler-subscript", + "tabler-subtask", + "tabler-sum-off", + "tabler-sum", + "tabler-sun-electricity", + "tabler-sun-filled", + "tabler-sun-high", + "tabler-sun-low", + "tabler-sun-moon", + "tabler-sun-off", + "tabler-sun-wind", + "tabler-sun", + "tabler-sunglasses", + "tabler-sunrise", + "tabler-sunset-2", + "tabler-sunset", + "tabler-superscript", + "tabler-svg", + "tabler-swimming", + "tabler-swipe", + "tabler-switch-2", + "tabler-switch-3", + "tabler-switch-horizontal", + "tabler-switch-vertical", + "tabler-switch", + "tabler-sword-off", + "tabler-sword", + "tabler-swords", + "tabler-table-alias", + "tabler-table-column", + "tabler-table-down", + "tabler-table-export", + "tabler-table-filled", + "tabler-table-heart", + "tabler-table-import", + "tabler-table-minus", + "tabler-table-off", + "tabler-table-options", + "tabler-table-plus", + "tabler-table-row", + "tabler-table-share", + "tabler-table-shortcut", + "tabler-table", + "tabler-tag-off", + "tabler-tag-starred", + "tabler-tag", + "tabler-tags-off", + "tabler-tags", + "tabler-tallymark-1", + "tabler-tallymark-2", + "tabler-tallymark-3", + "tabler-tallymark-4", + "tabler-tallymarks", + "tabler-tank", + "tabler-target-arrow", + "tabler-target-off", + "tabler-target", + "tabler-teapot", + "tabler-telescope-off", + "tabler-telescope", + "tabler-temperature-celsius", + "tabler-temperature-fahrenheit", + "tabler-temperature-minus", + "tabler-temperature-off", + "tabler-temperature-plus", + "tabler-temperature", + "tabler-template-off", + "tabler-template", + "tabler-tent-off", + "tabler-tent", + "tabler-terminal-2", + "tabler-terminal", + "tabler-test-pipe-2", + "tabler-test-pipe-off", + "tabler-test-pipe", + "tabler-tex", + "tabler-text-caption", + "tabler-text-color", + "tabler-text-decrease", + "tabler-text-direction-ltr", + "tabler-text-direction-rtl", + "tabler-text-increase", + "tabler-text-orientation", + "tabler-text-plus", + "tabler-text-recognition", + "tabler-text-resize", + "tabler-text-scan-2", + "tabler-text-size", + "tabler-text-spellcheck", + "tabler-text-wrap-disabled", + "tabler-text-wrap", + "tabler-texture", + "tabler-theater", + "tabler-thermometer", + "tabler-thumb-down-filled", + "tabler-thumb-down-off", + "tabler-thumb-down", + "tabler-thumb-up-filled", + "tabler-thumb-up-off", + "tabler-thumb-up", + "tabler-tic-tac", + "tabler-ticket-off", + "tabler-ticket", + "tabler-tie", + "tabler-tilde", + "tabler-tilt-shift-off", + "tabler-tilt-shift", + "tabler-time-duration-0", + "tabler-time-duration-10", + "tabler-time-duration-15", + "tabler-time-duration-30", + "tabler-time-duration-45", + "tabler-time-duration-5", + "tabler-time-duration-60", + "tabler-time-duration-90", + "tabler-time-duration-off", + "tabler-timeline-event-exclamation", + "tabler-timeline-event-filled", + "tabler-timeline-event-minus", + "tabler-timeline-event-plus", + "tabler-timeline-event-text", + "tabler-timeline-event-x", + "tabler-timeline-event", + "tabler-timeline", + "tabler-tir", + "tabler-toggle-left", + "tabler-toggle-right", + "tabler-toilet-paper-off", + "tabler-toilet-paper", + "tabler-toml", + "tabler-tool", + "tabler-tools-kitchen-2-off", + "tabler-tools-kitchen-2", + "tabler-tools-kitchen-off", + "tabler-tools-kitchen", + "tabler-tools-off", + "tabler-tools", + "tabler-tooltip", + "tabler-topology-bus", + "tabler-topology-complex", + "tabler-topology-full-hierarchy", + "tabler-topology-full", + "tabler-topology-ring-2", + "tabler-topology-ring-3", + "tabler-topology-ring", + "tabler-topology-star-2", + "tabler-topology-star-3", + "tabler-topology-star-ring-2", + "tabler-topology-star-ring-3", + "tabler-topology-star-ring", + "tabler-topology-star", + "tabler-torii", + "tabler-tornado", + "tabler-tournament", + "tabler-tower-off", + "tabler-tower", + "tabler-track", + "tabler-tractor", + "tabler-trademark", + "tabler-traffic-cone-off", + "tabler-traffic-cone", + "tabler-traffic-lights-off", + "tabler-traffic-lights", + "tabler-train", + "tabler-transfer-in", + "tabler-transfer-out", + "tabler-transfer-vertical", + "tabler-transfer", + "tabler-transform-filled", + "tabler-transform", + "tabler-transition-bottom", + "tabler-transition-left", + "tabler-transition-right", + "tabler-transition-top", + "tabler-trash-filled", + "tabler-trash-off", + "tabler-trash-x-filled", + "tabler-trash-x", + "tabler-trash", + "tabler-treadmill", + "tabler-tree", + "tabler-trees", + "tabler-trekking", + "tabler-trending-down-2", + "tabler-trending-down-3", + "tabler-trending-down", + "tabler-trending-up-2", + "tabler-trending-up-3", + "tabler-trending-up", + "tabler-triangle-filled", + "tabler-triangle-inverted-filled", + "tabler-triangle-inverted", + "tabler-triangle-minus-2", + "tabler-triangle-minus", + "tabler-triangle-off", + "tabler-triangle-plus-2", + "tabler-triangle-plus", + "tabler-triangle-square-circle-filled", + "tabler-triangle-square-circle", + "tabler-triangle", + "tabler-triangles", + "tabler-trident", + "tabler-trolley", + "tabler-trophy-filled", + "tabler-trophy-off", + "tabler-trophy", + "tabler-trowel", + "tabler-truck-delivery", + "tabler-truck-loading", + "tabler-truck-off", + "tabler-truck-return", + "tabler-truck", + "tabler-txt", + "tabler-typography-off", + "tabler-typography", + "tabler-ufo-off", + "tabler-ufo", + "tabler-umbrella-filled", + "tabler-umbrella-off", + "tabler-umbrella", + "tabler-underline", + "tabler-universe", + "tabler-unlink", + "tabler-upload", + "tabler-urgent", + "tabler-usb", + "tabler-user-bolt", + "tabler-user-cancel", + "tabler-user-check", + "tabler-user-circle", + "tabler-user-code", + "tabler-user-cog", + "tabler-user-dollar", + "tabler-user-down", + "tabler-user-edit", + "tabler-user-exclamation", + "tabler-user-filled", + "tabler-user-heart", + "tabler-user-hexagon", + "tabler-user-minus", + "tabler-user-off", + "tabler-user-pause", + "tabler-user-pentagon", + "tabler-user-pin", + "tabler-user-plus", + "tabler-user-question", + "tabler-user-scan", + "tabler-user-search", + "tabler-user-share", + "tabler-user-shield", + "tabler-user-square-rounded", + "tabler-user-square", + "tabler-user-star", + "tabler-user-up", + "tabler-user-x", + "tabler-user", + "tabler-users-group", + "tabler-users-minus", + "tabler-users-plus", + "tabler-users", + "tabler-uv-index", + "tabler-ux-circle", + "tabler-vaccine-bottle-off", + "tabler-vaccine-bottle", + "tabler-vaccine-off", + "tabler-vaccine", + "tabler-vacuum-cleaner", + "tabler-variable-minus", + "tabler-variable-off", + "tabler-variable-plus", + "tabler-variable", + "tabler-vector-bezier-2", + "tabler-vector-bezier-arc", + "tabler-vector-bezier-circle", + "tabler-vector-bezier", + "tabler-vector-off", + "tabler-vector-spline", + "tabler-vector-triangle-off", + "tabler-vector-triangle", + "tabler-vector", + "tabler-venus", + "tabler-versions-filled", + "tabler-versions-off", + "tabler-versions", + "tabler-video-minus", + "tabler-video-off", + "tabler-video-plus", + "tabler-video", + "tabler-view-360-off", + "tabler-view-360", + "tabler-viewfinder-off", + "tabler-viewfinder", + "tabler-viewport-narrow", + "tabler-viewport-wide", + "tabler-vinyl", + "tabler-vip-off", + "tabler-vip", + "tabler-virus-off", + "tabler-virus-search", + "tabler-virus", + "tabler-vocabulary-off", + "tabler-vocabulary", + "tabler-volcano", + "tabler-volume-2", + "tabler-volume-3", + "tabler-volume-off", + "tabler-volume", + "tabler-vs", + "tabler-walk", + "tabler-wall-off", + "tabler-wall", + "tabler-wallet-off", + "tabler-wallet", + "tabler-wallpaper-off", + "tabler-wallpaper", + "tabler-wand-off", + "tabler-wand", + "tabler-wash-dry-1", + "tabler-wash-dry-2", + "tabler-wash-dry-3", + "tabler-wash-dry-a", + "tabler-wash-dry-dip", + "tabler-wash-dry-f", + "tabler-wash-dry-flat", + "tabler-wash-dry-hang", + "tabler-wash-dry-off", + "tabler-wash-dry-p", + "tabler-wash-dry-shade", + "tabler-wash-dry-w", + "tabler-wash-dry", + "tabler-wash-dryclean-off", + "tabler-wash-dryclean", + "tabler-wash-eco", + "tabler-wash-gentle", + "tabler-wash-hand", + "tabler-wash-machine", + "tabler-wash-off", + "tabler-wash-press", + "tabler-wash-temperature-1", + "tabler-wash-temperature-2", + "tabler-wash-temperature-3", + "tabler-wash-temperature-4", + "tabler-wash-temperature-5", + "tabler-wash-temperature-6", + "tabler-wash-tumble-dry", + "tabler-wash-tumble-off", + "tabler-wash", + "tabler-waterpolo", + "tabler-wave-saw-tool", + "tabler-wave-sine", + "tabler-wave-square", + "tabler-waves-electricity", + "tabler-webhook-off", + "tabler-webhook", + "tabler-weight", + "tabler-wheel", + "tabler-wheelchair-off", + "tabler-wheelchair", + "tabler-whirl", + "tabler-wifi-0", + "tabler-wifi-1", + "tabler-wifi-2", + "tabler-wifi-off", + "tabler-wifi", + "tabler-wind-electricity", + "tabler-wind-off", + "tabler-wind", + "tabler-windmill-filled", + "tabler-windmill-off", + "tabler-windmill", + "tabler-window-maximize", + "tabler-window-minimize", + "tabler-window-off", + "tabler-window", + "tabler-windsock", + "tabler-wiper-wash", + "tabler-wiper", + "tabler-woman", + "tabler-wood", + "tabler-world-bolt", + "tabler-world-cancel", + "tabler-world-check", + "tabler-world-code", + "tabler-world-cog", + "tabler-world-dollar", + "tabler-world-down", + "tabler-world-download", + "tabler-world-exclamation", + "tabler-world-heart", + "tabler-world-latitude", + "tabler-world-longitude", + "tabler-world-minus", + "tabler-world-off", + "tabler-world-pause", + "tabler-world-pin", + "tabler-world-plus", + "tabler-world-question", + "tabler-world-search", + "tabler-world-share", + "tabler-world-star", + "tabler-world-up", + "tabler-world-upload", + "tabler-world-www", + "tabler-world-x", + "tabler-world", + "tabler-wrecking-ball", + "tabler-writing-off", + "tabler-writing-sign-off", + "tabler-writing-sign", + "tabler-writing", + "tabler-x", + "tabler-xbox-a", + "tabler-xbox-b", + "tabler-xbox-x", + "tabler-xbox-y", + "tabler-xd", + "tabler-xxx", + "tabler-yin-yang-filled", + "tabler-yin-yang", + "tabler-yoga", + "tabler-zeppelin-off", + "tabler-zeppelin", + "tabler-zip", + "tabler-zodiac-aquarius", + "tabler-zodiac-aries", + "tabler-zodiac-cancer", + "tabler-zodiac-capricorn", + "tabler-zodiac-gemini", + "tabler-zodiac-leo", + "tabler-zodiac-libra", + "tabler-zodiac-pisces", + "tabler-zodiac-sagittarius", + "tabler-zodiac-scorpio", + "tabler-zodiac-taurus", + "tabler-zodiac-virgo", + "tabler-zoom-cancel", + "tabler-zoom-check-filled", + "tabler-zoom-check", + "tabler-zoom-code", + "tabler-zoom-exclamation", + "tabler-zoom-filled", + "tabler-zoom-in-area-filled", + "tabler-zoom-in-area", + "tabler-zoom-in-filled", + "tabler-zoom-in", + "tabler-zoom-money", + "tabler-zoom-out-area", + "tabler-zoom-out-filled", + "tabler-zoom-out", + "tabler-zoom-pan", + "tabler-zoom-question", + "tabler-zoom-replace", + "tabler-zoom-reset", + "tabler-zoom-scan", + "tabler-zzz-off", + "tabler-zzz", +]; + +export const tablerIcons = new Set(tablerIconNames); diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index eca598386..1b619e0eb 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -60,8 +60,7 @@ The wrappers at `io.integration.runTask()` expose the underlying Integration cli The icon for the Task, it will appear in the logs. You can use the name of a - company in lowercase, e.g. "github". Or any icon name that [Font - Awesome](https://fontawesome.com/icons) supports. + company in lowercase, e.g. "github". Or any icon name that [Tabler Icons](https://tabler-icons.io/) supports. A description of the Task. diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index 4e16cbfc5..d95ebf65e 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -664,7 +664,7 @@ export const RunTaskOptionsSchema = z.object({ retry: RetryOptionsSchema.optional(), /** The icon for the Task, it will appear in the logs. * You can use the name of a company in lowercase, e.g. "github". - * Or any icon name that [Font Awesome](https://fontawesome.com/icons) supports. */ + * Or any icon name that [Tabler Icons](https://tabler-icons.io/) supports. */ icon: z.string().optional(), /** The key for the Task that you want to appear in the logs */ displayKey: z.string().optional(), diff --git a/references/job-catalog/src/events.ts b/references/job-catalog/src/events.ts index 7bd9d9a08..8e4cb660b 100644 --- a/references/job-catalog/src/events.ts +++ b/references/job-catalog/src/events.ts @@ -19,11 +19,15 @@ client.defineJob({ name: "event.example", }), run: async (payload, io, ctx) => { - await io.runTask("task-example-1", async () => { - return { - message: "Hello World", - }; - }); + await io.runTask( + "task-example-1", + async () => { + return { + message: "Hello World", + }; + }, + { icon: "360" } + ); await io.wait("wait-1", 1); From 6d3b761cd7ce398b8e050bb86b8b03893ddcdf34 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:30:17 +0530 Subject: [PATCH 16/39] fix: set correct next/prev cursors in run-list-presenter (#642) * fix: set correct next/prev cursors in run-list-presenter * set runs to return --- apps/webapp/app/presenters/RunListPresenter.server.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/presenters/RunListPresenter.server.ts b/apps/webapp/app/presenters/RunListPresenter.server.ts index bace18a9c..63a1df2b5 100644 --- a/apps/webapp/app/presenters/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/RunListPresenter.server.ts @@ -100,21 +100,24 @@ export class RunListPresenter { let previous: string | undefined; switch (direction) { case "forward": + previous = cursor ? runs.at(0)?.id : undefined; if (hasMore) { next = runs[PAGE_SIZE - 1]?.id; } - previous = cursor ? runs.at(1)?.id : undefined; break; case "backward": if (hasMore) { - next = runs[PAGE_SIZE - 1]?.id; + previous = runs[1]?.id; } - previous = runs.at(1)?.id; + next = runs[PAGE_SIZE - 1]?.id; break; } + const runsToReturn = + direction === "backward" && hasMore ? runs.slice(1, PAGE_SIZE + 1) : runs.slice(0, PAGE_SIZE); + return { - runs: runs.slice(0, PAGE_SIZE).map((run) => ({ + runs: runsToReturn.map((run) => ({ id: run.id, number: run.number, startedAt: run.startedAt, From 34bb3acc235fdc1ea74bda93b2e1952281a49a40 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Fri, 20 Oct 2023 12:50:27 +0100 Subject: [PATCH 17/39] Removed idempotencyKey line from GH tasks --- docs/integrations/apis/github-tasks.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/integrations/apis/github-tasks.mdx b/docs/integrations/apis/github-tasks.mdx index 2dbc2e33d..2cf9c9a31 100644 --- a/docs/integrations/apis/github-tasks.mdx +++ b/docs/integrations/apis/github-tasks.mdx @@ -259,5 +259,3 @@ client.defineJob({ }, }); ``` - -Make sure to pass the `idempotencyKey` to the underlying client to ensure that the API call is only executed once. This is only needed for mutating API calls. From e5307133b3f6c730fbf6313f47b3c037be799dc4 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Fri, 20 Oct 2023 17:55:07 +0530 Subject: [PATCH 18/39] airtable docs --- docs/integrations/apis/airtable-tasks.mdx | 83 ++++++++++++++++++ docs/integrations/apis/airtable.mdx | 102 ++++------------------ docs/mint.json | 24 +++-- 3 files changed, 120 insertions(+), 89 deletions(-) create mode 100644 docs/integrations/apis/airtable-tasks.mdx diff --git a/docs/integrations/apis/airtable-tasks.mdx b/docs/integrations/apis/airtable-tasks.mdx new file mode 100644 index 000000000..14199b65a --- /dev/null +++ b/docs/integrations/apis/airtable-tasks.mdx @@ -0,0 +1,83 @@ +--- +title: Airtable tasks +sidebarTitle: Tasks +--- + +Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want. + +--- + +## All tasks + +### `base.table.getRecords` + +Gets multiple records for a table. You can filter. [Official Airtable Docs](https://airtable.com/developers/web/api/update-multiple-records) + +```ts example.ts + //gets multiple records from the table. Here we only get the Status fields (columns) + const records = await table.getRecords("muliple records", { fields: ["Status"] }); + +``` + +### `base.table.getRecord` + +Gets a single record (using the id) for a table. [Official Airtable Docs](https://airtable.com/developers/web/api/get-record) + +```ts example.ts +// Get a single record using the ID of the first record in the fetched records. +const aRecord = await table.getRecord("single", records[0].id); +``` + + +### `base.table.createRecords` + +Create one or more records in a table.[Official Airtable Docs](https://airtable.com/developers/web/api/create-records) + +```ts example.ts +// Create newRecords by adding a record to the table. +const newRecords = await table.createRecords("create records", [ + { + // Define the new record to be created with field values for "Launch goals" and "Status." + fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" }, + }, +]); +``` + + +### `base.table.updateRecords` + +Update one or more records in a table.[Official Airtable Docs](https://airtable.com/developers/web/api/update-record) + +```ts example.ts +// Update the record that was just created. +const updatedRecords = await table.updateRecords( + // Specify the table's name as "update records." + "update records", + // Use the `map` method to create an array of records to be updated. + newRecords.map((record) => ({ + // For each record, specify the ID and the field to update ("Status" to "At risk"). + id: record.id, + fields: { Status: "At risk" }, + })) +); +``` + + +### `base.table.deleteRecords` + +Delete one or more records in a table (using ids).[Official Airtable Docs](https://airtable.com/developers/web/api/delete-record) + +```ts example.ts +// Delete records from the table. +const deletedRecords = await table.deleteRecords( +// Specify the table's name as "delete records." + "delete records", +); +``` + + +### `runTask` + +Do anything that’s possible with the official Airtable Node SDK. + + diff --git a/docs/integrations/apis/airtable.mdx b/docs/integrations/apis/airtable.mdx index 563de2e37..530acf69f 100644 --- a/docs/integrations/apis/airtable.mdx +++ b/docs/integrations/apis/airtable.mdx @@ -1,12 +1,19 @@ --- -title: Airtable +title: Airtable overview & authentication +sidebarTitle: Overview & authentication --- -Use the Airtable integration to create, read, update, and delete records in your Airtable bases. +## Overview -Coming soon: subscribing to changes in your Airtable bases using triggers. +With Airtable, your organization can move fast while moving together. Airtable allows you to run your most important business processes—from tracking global marketing campaigns to managing your product roadmap—all on a single platform. - + + Check out pre-built Airtable jobs in our showcase. + ## Installation @@ -57,85 +64,12 @@ const airtable = new Airtable({ token: process.env["AIRTABLE_TOKEN"], }); ``` - -## Usage - -Include the Airtable integration in your Trigger.dev job. - -You can optionally add the types for your Table – this gives you nice type inference and errors when writing your code. - -```ts -//this is the type definition for my Table -type LaunchGoalsAndOkRs = { - "Launch goals"?: string; - DRI?: Collaborator; - Team?: string; - Status?: "On track" | "In progress" | "At risk"; - "Key results"?: Array; - "Features (from 💻 Features table)"?: Array; - "Status (from 💻 Features)": Array< - "Live" | "Complete" | "In progress" | "Planning" | "In reviews" - >; -}; - -client.defineJob({ - id: "airtable-example-1", - name: "Airtable Example 1: getRecords", - version: "0.1.0", - trigger: eventTrigger({ - name: "airtable.example", - }), - integrations: { - //make sure to add the integration here - airtable, - }, - run: async (payload, io, ctx) => { - //adding the type to table("") gives you nice type inference and errors - //you can leave it out as well table("") - const table = io.airtable.base("").table(""); - - //gets multiple records from the table. Here we only get the Status fields (columns) - const records = await table.getRecords("muliple records", { fields: ["Status"] }); - await io.logger.log(records[0].fields.Status ?? "no status"); - - //Get a single record - const aRecord = await table.getRecord("single", records[0].id); - - //create a new record - const newRecords = await table.createRecords("create records", [ - { - fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" }, - }, - ]); - - //update the record we just created - const updatedRecords = await table.updateRecords( - "update records", - newRecords.map((record) => ({ - id: record.id, - fields: { Status: "At risk" }, - })) - ); - - //wait 5 seconds - await io.wait("5 secs", 5); - - //delete the record we just created + updated - const deletedRecords = await table.deleteRecords( - "delete records", - updatedRecords.map((record) => record.id) - ); - }, -}); -``` - ## Tasks -| Function Name | Description | -| -------------------------- | --------------------------------------------------------------- | -| `base.table.getRecords` | Gets multiple records for a table. You can filter | -| `base.table.getRecord` | Gets a single record (using the id) for a table. | -| `base.table.createRecords` | Create one or more records in a table. | -| `base.table.updateRecords` | Update one or more records in a table. | -| `base.table.deleteRecords` | Delete one or more records in a table (using ids). | -| `runTask` | Do anything that's possible with the official Airtable Node SDK | +Once you have set up a Airtable client, you can use it to create tasks. + + + + Perform tasks such as creating/updating single or multiple records in table. + + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 8c1399778..102e0d266 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -237,7 +237,13 @@ { "group": "Integrations", "pages": [ - "integrations/apis/airtable", + { + "group": "Airtable", + "pages": [ + "integrations/apis/airtable", + "integrations/apis/airtable-tasks" + ] + }, { "group": "GitHub", "pages": [ @@ -325,7 +331,10 @@ "sdk/dynamictrigger/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"] + "pages": [ + "sdk/dynamictrigger/register", + "sdk/dynamictrigger/unregister" + ] } ] }, @@ -336,7 +345,10 @@ "sdk/dynamicschedule/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"] + "pages": [ + "sdk/dynamicschedule/register", + "sdk/dynamicschedule/unregister" + ] } ] }, @@ -357,7 +369,9 @@ }, { "group": "Overview", - "pages": ["examples/introduction"] + "pages": [ + "examples/introduction" + ] } ], "footerSocials": { @@ -370,4 +384,4 @@ "apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW" } } -} +} \ No newline at end of file From a6c9cac26cc73842719c141b71876b02bf02d051 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Fri, 20 Oct 2023 18:30:03 +0530 Subject: [PATCH 19/39] slack docs --- docs/integrations/apis/slack-tasks.mdx | 26 +++++++++++++ docs/integrations/apis/slack.mdx | 51 +++++++++++--------------- docs/mint.json | 24 +++++++++--- 3 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 docs/integrations/apis/slack-tasks.mdx diff --git a/docs/integrations/apis/slack-tasks.mdx b/docs/integrations/apis/slack-tasks.mdx new file mode 100644 index 000000000..a23f8b2eb --- /dev/null +++ b/docs/integrations/apis/slack-tasks.mdx @@ -0,0 +1,26 @@ +--- +title: Slack tasks +sidebarTitle: Tasks +--- + +Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want. + +--- + +## All tasks + +### `postMessage` + +Post a message to a channel. [Official Slack Docs](https://api.slack.com/methods/chat.postMessage) + +```ts example.ts +// Send a Slack message using the io.slack.postMessage function +const response = await io.slack.postMessage("post message", { + // Specify the target channel by providing its ID + channel: "C04GWUTDC3W", + // Set the text content of the message + text: "My first Slack message", +}); + +``` + diff --git a/docs/integrations/apis/slack.mdx b/docs/integrations/apis/slack.mdx index 53e6cbadf..848aed47c 100644 --- a/docs/integrations/apis/slack.mdx +++ b/docs/integrations/apis/slack.mdx @@ -1,8 +1,20 @@ --- -title: Slack +title: Plain overview & authentication +sidebarTitle: Overview & authentication --- - +## Overview + +The Slack platform allows you to extend and automate your workspaces to cultivate conversation, inspire action, and synergize services. + + + + Check out pre-built Plain jobs in our showcase. + ## Installation @@ -33,32 +45,13 @@ const slack = new Slack({ id: "slack", }); ``` - -## Example - -```ts -client.defineJob({ - id: "slack-test", - name: "Slack test", - version: "0.0.1", - trigger: eventTrigger({ - name: "slack.test", - schema: z.object({}), - }), - integrations: { - slack, - }, - run: async (payload, io, ctx) => { - const response = await io.slack.postMessage("post message", { - channel: "C04GWUTDC3W", - text: "My first Slack message", - }); - }, -}); -``` - ## Tasks -| Function Name | Description | -| ------------- | --------------------------- | -| `postMessage` | Post a message to a channel | +Once you have set up a Slack client, you can use it to create tasks. + + + + Perform tasks such as posting message to a channel. + + + diff --git a/docs/mint.json b/docs/mint.json index 8c1399778..a402df803 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -258,7 +258,13 @@ "integrations/apis/replicate", "integrations/apis/resend", "integrations/apis/sendgrid", - "integrations/apis/slack", + { + "group": "Slack", + "pages": [ + "integrations/apis/slack", + "integrations/apis/slack-tasks" + ] + }, "integrations/apis/stripe", { "group": "Supabase", @@ -325,7 +331,10 @@ "sdk/dynamictrigger/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"] + "pages": [ + "sdk/dynamictrigger/register", + "sdk/dynamictrigger/unregister" + ] } ] }, @@ -336,7 +345,10 @@ "sdk/dynamicschedule/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"] + "pages": [ + "sdk/dynamicschedule/register", + "sdk/dynamicschedule/unregister" + ] } ] }, @@ -357,7 +369,9 @@ }, { "group": "Overview", - "pages": ["examples/introduction"] + "pages": [ + "examples/introduction" + ] } ], "footerSocials": { @@ -370,4 +384,4 @@ "apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW" } } -} +} \ No newline at end of file From fde3f17a251b78de4f37e853358202dc99f6a41f Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Fri, 20 Oct 2023 19:00:41 +0530 Subject: [PATCH 20/39] fixed typos --- docs/integrations/apis/slack.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/apis/slack.mdx b/docs/integrations/apis/slack.mdx index 848aed47c..23aedd31d 100644 --- a/docs/integrations/apis/slack.mdx +++ b/docs/integrations/apis/slack.mdx @@ -1,5 +1,5 @@ --- -title: Plain overview & authentication +title: Slack overview & authentication sidebarTitle: Overview & authentication --- @@ -13,7 +13,7 @@ The Slack platform allows you to extend and automate your workspaces to cultivat icon="rocket" href="https://trigger.dev/showcase?tags=&integrations=slack" > - Check out pre-built Plain jobs in our showcase. + Check out pre-built Slack jobs in our showcase. ## Installation From dce5578327cd7b13175dd5846d8b4def5219994b Mon Sep 17 00:00:00 2001 From: D-K-P Date: Fri, 20 Oct 2023 14:35:28 +0100 Subject: [PATCH 21/39] Improved the GitHub overview copy --- docs/integrations/apis/github.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/integrations/apis/github.mdx b/docs/integrations/apis/github.mdx index 4b00021be..a3ee2e175 100644 --- a/docs/integrations/apis/github.mdx +++ b/docs/integrations/apis/github.mdx @@ -5,7 +5,11 @@ sidebarTitle: Overview & authentication ## Overview -Our GitHub integration allows you to create triggers and tasks that interact with GitHub. For examples of some of the things you can do with it, check out our Jobs Showcase: +Our GitHub integration allows you to create triggers and tasks that interact with GitHub. + +Trigger jobs when events happen, such as when a new issue is added to a repo, a commit is pushed, or a pull request is opened. You can also use the integration to perform tasks such as creating issues, getting information about a repo, adding comments, and much more. + +For examples of some of the things you can do with it, check out our Jobs Showcase: Date: Fri, 20 Oct 2023 16:12:17 +0100 Subject: [PATCH 22/39] Made the app work very basically on mobile devices (#668) * Removed the no-mobile blocker overlay so you can use the site on a phone * Removed mobile dropdown from nav * The app works on mobile at a width of 1024px --- .../app/components/navigation/NavBar.tsx | 81 ------------------- apps/webapp/app/root.tsx | 2 +- apps/webapp/app/routes/_app/route.tsx | 2 - 3 files changed, 1 insertion(+), 84 deletions(-) diff --git a/apps/webapp/app/components/navigation/NavBar.tsx b/apps/webapp/app/components/navigation/NavBar.tsx index 6529cb4f2..9f09cd72b 100644 --- a/apps/webapp/app/components/navigation/NavBar.tsx +++ b/apps/webapp/app/components/navigation/NavBar.tsx @@ -17,7 +17,6 @@ export function NavBar() { -
@@ -46,83 +45,3 @@ export function BreadcrumbLink({ title, to }: { title: string; to: string }) { ); } - -function MobileDropdownMenu() { - return ( - - - {({ open }) => } - - - - - - - - {/* - - Documentation - - - - Send us feedback - - - Logout - */} - - - - - ); -} - -function MobileNavIcon({ open }: { open: boolean }) { - return ( - - ); -} diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 589878a20..9819685cb 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -25,7 +25,7 @@ export const links: LinksFunction = () => { export const meta: TypedMetaFunction = ({ data }) => ({ title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`, charset: "utf-8", - viewport: "width=device-width,initial-scale=1", + viewport: "width=1024, initial-scale=1", }); export const loader = async ({ request }: LoaderArgs) => { diff --git a/apps/webapp/app/routes/_app/route.tsx b/apps/webapp/app/routes/_app/route.tsx index 44d8970e9..582008cb4 100644 --- a/apps/webapp/app/routes/_app/route.tsx +++ b/apps/webapp/app/routes/_app/route.tsx @@ -60,7 +60,6 @@ export default function App() { return ( <> {impersonationId && } - @@ -72,7 +71,6 @@ export default function App() { export function ErrorBoundary() { return ( <> - From e7768881afe355c265c332e0ef107e336ab9ec3a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 20 Oct 2023 16:53:01 +0100 Subject: [PATCH 23/39] Admin page now full screen with now extra UI --- apps/webapp/app/routes/admin._index.tsx | 18 +-- apps/webapp/app/routes/admin.tsx | 167 +----------------------- 2 files changed, 10 insertions(+), 175 deletions(-) diff --git a/apps/webapp/app/routes/admin._index.tsx b/apps/webapp/app/routes/admin._index.tsx index 77f30bbe4..3b98900e6 100644 --- a/apps/webapp/app/routes/admin._index.tsx +++ b/apps/webapp/app/routes/admin._index.tsx @@ -30,9 +30,8 @@ export async function action({ request }: ActionArgs) { }); } -const headerClassName = - "py-3 px-2 pr-3 text-xs font-semibold leading-tight text-slate-900 text-left"; -const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-slate-500"; +const headerClassName = "py-3 px-2 pr-3 text-xs font-semibold leading-tight text-bright text-left"; +const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-bright"; export default function AdminDashboardRoute() { const { users } = useTypedLoaderData(); @@ -46,8 +45,8 @@ export default function AdminDashboardRoute() { >

Accounts ({users.length})

- - +
+ - + {users.map((user) => { return ( - +
Email @@ -69,10 +68,10 @@ export default function AdminDashboardRoute() {
{user.email}
- - {/* Secondary column (hidden on smaller screens) */} - ); } diff --git a/apps/webapp/app/routes/admin.tsx b/apps/webapp/app/routes/admin.tsx index 9b83ce228..97091a28d 100644 --- a/apps/webapp/app/routes/admin.tsx +++ b/apps/webapp/app/routes/admin.tsx @@ -1,15 +1,8 @@ -import { Dialog, Transition } from "@headlessui/react"; -import { HomeIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { UserCircleIcon } from "@heroicons/react/24/solid"; +import { HomeIcon } from "@heroicons/react/24/outline"; import { Outlet } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/server-runtime"; -import { Fragment, useState } from "react"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; -import type { User } from "~/models/user.server"; import { getUser, requireUserId } from "~/services/session.server"; -import { cn } from "~/utils/cn"; - -const navigation = [{ name: "Home", href: "/admin", icon: HomeIcon }]; export async function loader({ request }: LoaderArgs) { await requireUserId(request); @@ -27,162 +20,10 @@ export async function loader({ request }: LoaderArgs) { export default function Page() { const data = useTypedLoaderData(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); return ( - <> -
- - - - - - ); -} - -function UserProfilePhoto({ user, className }: { user: User; className?: string }) { - return user.avatarUrl ? ( - {user.name - ) : ( - +
+ +
); } From 044d38e3903f8f61dbcf5fb5a36f01d74f45725a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 20 Oct 2023 17:44:10 +0100 Subject: [PATCH 24/39] Improvement: Auto Execution Yielding (#612) * Auto-yield run execution to help prevent duplicate task executions * Add auto-yield config to endpoints * Refactor run execution with buffer and limits Introduced constants RUN_CHUNK_EXECUTION_BUFFER and MAX_RUN_CHUNK_EXECUTION_LIMIT. Adjusted PerformRunExecutionV2Service to use the new constants to fine-tune execution timings and buffers. * Add endpoint probing functionality Added new `RESPONSE_TIMEOUT_STATUS_CODES` in `consts.ts` to manage timeout responses. Additional functions `detectResponseIsTimeout(response: Response)` was added in `endpoint.server.ts` to detect if a response was a timeout based on the status codes from `RESPONSE_TIMEOUT_STATUS_CODES`. Update actions to use new endpoint probing endpoint service. This allows for the early probing of endpoints to determine if they're up and running. A new class `ProbeEndpointService` was created in `probeEndpoint.server.ts` which makes HTTP requests to a given endpoint and updates its properties based on the result. Finally, `detectResponseIsTimeout(response)` is used in `performRunExecutionV2.server.ts` for marking the execution as succeeded when facing a timeout. * Refactored probe method in EndpointApi class The probe method of the EndpointApi class has been refactored to remove the error handling part and it now takes a timeout sent from the client directly. The corresponding changes were also made in the ProbeEndpointService and TriggerClient objects to reflect the alterations in the probe method. The error handling related to the timeout has been removed and the responsibility of handling the timeout has been shifted to the client. Thus, the probe method has been greatly simplified. The `probeEndpoint.server.ts` file was also changed to accommodate the change in behavior of the probe result. In the `triggerClient.ts` the timeout for probe is now read from the incoming request object. For backward compatibility, if no timeout is provided in the request, the default value of 15 minutes is used. * Remove performRunExecution v1 enqueue function * Better document limits and add docs on increasing function timeouts * Upgrade webapp docker container to use 18.18.2 * force clients to yield when a run is executing in a gracefully shutting down worker * Renamed task `key` to `cacheKey` and added more task documentation * Index the `@trigger.dev/sdk` version on Endpoints --- .changeset/cyan-brooms-cheat.md | 6 + apps/webapp/app/consts.ts | 3 + apps/webapp/app/models/endpoint.server.ts | 12 + .../app/models/jobRunExecution.server.ts | 21 - apps/webapp/app/models/task.server.ts | 5 +- apps/webapp/app/platform/zodWorker.server.ts | 34 +- .../routes/api.v1.runs.$runId.statuses.$id.ts | 9 +- .../api.v1.runs.$runId.tasks.$id.complete.ts | 9 +- .../api.v1.runs.$runId.tasks.$id.fail.ts | 1 + .../app/routes/api.v1.runs.$runId.tasks.ts | 1 + ...nvironmentParam.endpoint.$endpointParam.ts | 16 +- .../webapp/app/services/endpointApi.server.ts | 22 + .../endpoints/performEndpointIndexService.ts | 9 +- .../endpoints/probeEndpoint.server.ts | 64 ++ .../runs/forceYieldCoordinator.server.ts | 47 ++ .../runs/performRunExecutionV1.server.ts | 771 ------------------ .../runs/performRunExecutionV2.server.ts | 643 ++++++++++----- apps/webapp/app/services/worker.server.ts | 28 +- apps/webapp/package.json | 1 - apps/webapp/server.ts | 26 +- docker/Dockerfile | 6 +- docker/services-compose.yml | 65 ++ docs/_snippets/stable-key-param.mdx | 4 +- docs/documentation/concepts/limitations.mdx | 31 - docs/documentation/concepts/limits.mdx | 116 +++ docs/documentation/concepts/resumability.mdx | 19 +- docs/documentation/concepts/tasks.mdx | 270 +++++- .../concepts/what-is-triggerdotdev.mdx | 16 + docs/documentation/guides/create-a-job.mdx | 270 ------ .../documentation/guides/platforms/nextjs.mdx | 8 + .../guides/writing-jobs-step-by-step.mdx | 382 +++++++++ docs/images/task.png | Bin 0 -> 51335 bytes docs/mint.json | 36 +- docs/sdk/io/overview.mdx | 12 +- docs/sdk/io/runtask.mdx | 47 +- packages/core/src/schemas/api.ts | 43 + packages/core/src/schemas/tasks.ts | 1 + .../migration.sql | 2 + .../migration.sql | 14 + .../migration.sql | 8 + .../migration.sql | 2 + .../migration.sql | 5 + .../migration.sql | 2 + .../migration.sql | 2 + packages/database/prisma/schema.prisma | 24 + packages/trigger-sdk/src/errors.ts | 30 +- packages/trigger-sdk/src/io.ts | 168 +++- packages/trigger-sdk/src/triggerClient.ts | 86 +- pnpm-lock.yaml | 17 +- references/job-catalog/package.json | 3 +- references/job-catalog/src/auto-yield.ts | 83 ++ 51 files changed, 1975 insertions(+), 1525 deletions(-) create mode 100644 .changeset/cyan-brooms-cheat.md create mode 100644 apps/webapp/app/services/endpoints/probeEndpoint.server.ts create mode 100644 apps/webapp/app/services/runs/forceYieldCoordinator.server.ts delete mode 100644 apps/webapp/app/services/runs/performRunExecutionV1.server.ts create mode 100644 docker/services-compose.yml delete mode 100644 docs/documentation/concepts/limitations.mdx create mode 100644 docs/documentation/concepts/limits.mdx delete mode 100644 docs/documentation/guides/create-a-job.mdx create mode 100644 docs/documentation/guides/writing-jobs-step-by-step.mdx create mode 100644 docs/images/task.png create mode 100644 packages/database/prisma/migrations/20231011104302_add_run_chunk_column/migration.sql create mode 100644 packages/database/prisma/migrations/20231011134840_add_auto_yielded_executions/migration.sql create mode 100644 packages/database/prisma/migrations/20231011141302_add_location_to_auto_yield_executions/migration.sql create mode 100644 packages/database/prisma/migrations/20231011145406_change_run_chunk_execution_limit_default/migration.sql create mode 100644 packages/database/prisma/migrations/20231011213532_add_auto_yield_threshold_settings_to_endpoints/migration.sql create mode 100644 packages/database/prisma/migrations/20231019123406_add_force_yield_immediately_to_runs/migration.sql create mode 100644 packages/database/prisma/migrations/20231020145127_add_sdk_version_to_endpoints/migration.sql create mode 100644 references/job-catalog/src/auto-yield.ts diff --git a/.changeset/cyan-brooms-cheat.md b/.changeset/cyan-brooms-cheat.md new file mode 100644 index 000000000..5d3b310fa --- /dev/null +++ b/.changeset/cyan-brooms-cheat.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/sdk": patch +"@trigger.dev/core": patch +--- + +Auto-yield run execution to help prevent duplicate task executions diff --git a/apps/webapp/app/consts.ts b/apps/webapp/app/consts.ts index 51ab2cc44..f81d96109 100644 --- a/apps/webapp/app/consts.ts +++ b/apps/webapp/app/consts.ts @@ -6,3 +6,6 @@ export const MAX_CONCURRENT_RUNS_LIMIT = 20; export const PREPROCESS_RETRY_LIMIT = 2; export const EXECUTE_JOB_RETRY_LIMIT = 10; export const MAX_RUN_YIELDED_EXECUTIONS = 100; +export const RUN_CHUNK_EXECUTION_BUFFER = 350; +export const MAX_RUN_CHUNK_EXECUTION_LIMIT = 120000; // 2 minutes +export const RESPONSE_TIMEOUT_STATUS_CODES = [408, 504]; diff --git a/apps/webapp/app/models/endpoint.server.ts b/apps/webapp/app/models/endpoint.server.ts index 0581d87ca..25c7aa945 100644 --- a/apps/webapp/app/models/endpoint.server.ts +++ b/apps/webapp/app/models/endpoint.server.ts @@ -1,3 +1,4 @@ +import { RESPONSE_TIMEOUT_STATUS_CODES } from "~/consts"; import { prisma } from "~/db.server"; import { Prettify } from "~/lib.es5"; @@ -18,3 +19,14 @@ export async function findEndpoint(id: string) { }, }); } + +export function detectResponseIsTimeout(response?: Response) { + if (!response) { + return false; + } + + return ( + RESPONSE_TIMEOUT_STATUS_CODES.includes(response.status) || + response.headers.get("x-vercel-error") === "FUNCTION_INVOCATION_TIMEOUT" + ); +} diff --git a/apps/webapp/app/models/jobRunExecution.server.ts b/apps/webapp/app/models/jobRunExecution.server.ts index 933463c6a..0a4422e07 100644 --- a/apps/webapp/app/models/jobRunExecution.server.ts +++ b/apps/webapp/app/models/jobRunExecution.server.ts @@ -2,27 +2,6 @@ import { JobRun, JobRunExecution } from "@trigger.dev/database"; import { PrismaClientOrTransaction } from "~/db.server"; import { executionWorker } from "~/services/worker.server"; -export async function enqueueRunExecutionV1( - execution: JobRunExecution, - queueId: string, - concurrency: number, - tx: PrismaClientOrTransaction, - runAt?: Date -) { - const job = await executionWorker.enqueue( - "performRunExecution", - { - id: execution.id, - }, - { - queueName: `job:queue:${queueId}`, - tx, - runAt, - jobKey: `execution:${execution.runId}`, - } - ); -} - export type EnqueueRunExecutionV2Options = { runAt?: Date; resumeTaskId?: string; diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index b674dd0a4..f2df0a060 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -1,7 +1,7 @@ -import type { Task, TaskAttempt } from "@trigger.dev/database"; +import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database"; import { CachedTask, ServerTask } from "@trigger.dev/core"; -export type TaskWithAttempts = Task & { attempts: TaskAttempt[] }; +export type TaskWithAttempts = Task & { attempts: TaskAttempt[]; run: JobRun }; export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask { return { @@ -24,6 +24,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask idempotencyKey: task.idempotencyKey, operation: task.operation, callbackUrl: task.callbackUrl, + forceYield: task.run.forceYieldImmediately, }; } diff --git a/apps/webapp/app/platform/zodWorker.server.ts b/apps/webapp/app/platform/zodWorker.server.ts index 2a9e823ff..d4480286d 100644 --- a/apps/webapp/app/platform/zodWorker.server.ts +++ b/apps/webapp/app/platform/zodWorker.server.ts @@ -204,34 +204,32 @@ export class ZodWorker { this.#logDebug("stop"); }); - process.on("SIGTERM", this._handleSignal("SIGTERM").bind(this)); - process.on("SIGINT", this._handleSignal("SIGINT").bind(this)); + process.on("SIGTERM", this._handleSignal.bind(this)); + process.on("SIGINT", this._handleSignal.bind(this)); return true; } private _handleSignal(signal: string) { - return () => { - if (this.#shuttingDown) { - return; - } + if (this.#shuttingDown) { + return; + } - this.#shuttingDown = true; + this.#shuttingDown = true; - if (this.#shutdownTimeoutInMs) { - setTimeout(() => { - this.#logDebug("Shutdown timeout reached, exiting process"); + if (this.#shutdownTimeoutInMs) { + setTimeout(() => { + this.#logDebug("Shutdown timeout reached, exiting process"); - process.exit(0); - }, this.#shutdownTimeoutInMs); - } + process.exit(0); + }, this.#shutdownTimeoutInMs); + } - this.#logDebug(`Received ${signal}, shutting down zodWorker...`); + this.#logDebug(`Received ${signal}, shutting down zodWorker...`); - this.stop().finally(() => { - this.#logDebug("zodWorker stopped"); - }); - }; + this.stop().finally(() => { + this.#logDebug("zodWorker stopped"); + }); } public async stop() { diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts index e54753423..ca8bf9fb2 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts @@ -1,10 +1,7 @@ import type { ActionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; -import { TaskStatus } from "@trigger.dev/database"; import { - RunTaskBodyOutput, - RunTaskBodyOutputSchema, - ServerTask, + JobRunStatusRecordSchema, StatusHistory, StatusHistorySchema, StatusUpdate, @@ -14,12 +11,8 @@ import { } from "@trigger.dev/core"; import { z } from "zod"; import { $transaction, PrismaClient, prisma } from "~/db.server"; -import { taskWithAttemptsToServerTask } from "~/models/task.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; -import { ulid } from "~/services/ulid.server"; -import { workerQueue } from "~/services/worker.server"; -import { JobRunStatusRecordSchema } from "@trigger.dev/core"; const ParamsSchema = z.object({ runId: z.string(), diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts index f116261cb..c84a7af3d 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts @@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime"; import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core"; import { CompleteTaskBodyInputSchema } from "@trigger.dev/core"; import { z } from "zod"; -import { PrismaClient, prisma } from "~/db.server"; +import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; import { taskWithAttemptsToServerTask } from "~/models/task.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; @@ -72,9 +72,9 @@ export async function action({ request, params }: ActionArgs) { } export class CompleteRunTaskService { - #prismaClient: PrismaClient; + #prismaClient: PrismaClientOrTransaction; - constructor(prismaClient: PrismaClient = prisma) { + constructor(prismaClient: PrismaClientOrTransaction = prisma) { this.#prismaClient = prismaClient; } @@ -86,7 +86,7 @@ export class CompleteRunTaskService { ): Promise { // Using a transaction, we'll first check to see if the task already exists and return if if it does // If it doesn't exist, we'll create it and return it - const task = await this.#prismaClient.$transaction(async (tx) => { + const task = await $transaction(this.#prismaClient, async (tx) => { const existingTask = await tx.task.findUnique({ where: { id, @@ -152,6 +152,7 @@ export class CompleteRunTaskService { }, include: { attempts: true, + run: true, }, }); }); diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts index 8bdfb4f44..56bef530e 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts @@ -152,6 +152,7 @@ export class FailRunTaskService { }, include: { attempts: true, + run: true, }, }); }); diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 871a48d92..e19796b18 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -184,6 +184,7 @@ export class RunTaskService { }, include: { attempts: true, + run: true, }, }); diff --git a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts index df8fcf927..b4033d9dd 100644 --- a/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts +++ b/apps/webapp/app/routes/resources.environments.$environmentParam.endpoint.$endpointParam.ts @@ -1,20 +1,26 @@ import { ActionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server"; -import { requireUserId } from "~/services/session.server"; +import { workerQueue } from "~/services/worker.server"; const ParamsSchema = z.object({ environmentParam: z.string(), endpointParam: z.string(), }); -export async function action({ request, params }: ActionArgs) { - const userId = await requireUserId(request); - const { environmentParam, endpointParam } = ParamsSchema.parse(params); +export async function action({ params }: ActionArgs) { + const { endpointParam } = ParamsSchema.parse(params); try { const service = new IndexEndpointService(); - const result = await service.call(endpointParam, "MANUAL"); + await service.call(endpointParam, "MANUAL"); + + // Enqueue the endpoint to be probed in 10 seconds + await workerQueue.enqueue( + "probeEndpoint", + { id: endpointParam }, + { jobKey: `probe:${endpointParam}`, runAt: new Date(Date.now() + 10000) } + ); return json({ success: true }); } catch (e) { diff --git a/apps/webapp/app/services/endpointApi.server.ts b/apps/webapp/app/services/endpointApi.server.ts index 708a62ee7..af2c04d02 100644 --- a/apps/webapp/app/services/endpointApi.server.ts +++ b/apps/webapp/app/services/endpointApi.server.ts @@ -97,6 +97,7 @@ export class EndpointApi { return { ...pongResponse.data, triggerVersion: headers.data["trigger-version"], + triggerSdkVersion: headers.data["trigger-sdk-version"], }; } @@ -308,6 +309,27 @@ export class EndpointApi { return validateResponse.data; } + + async probe(timeout: number) { + const startTimeInMs = performance.now(); + + const response = await safeFetch(this.url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-trigger-api-key": this.apiKey, + "x-trigger-action": "PROBE_EXECUTION_TIMEOUT", + }, + body: JSON.stringify({ + timeout, + }), + }); + + return { + response, + durationInMs: Math.floor(performance.now() - startTimeInMs), + }; + } } async function safeFetch(url: string, options: RequestInit) { diff --git a/apps/webapp/app/services/endpoints/performEndpointIndexService.ts b/apps/webapp/app/services/endpoints/performEndpointIndexService.ts index ca1c0bcd1..3457fa466 100644 --- a/apps/webapp/app/services/endpoints/performEndpointIndexService.ts +++ b/apps/webapp/app/services/endpoints/performEndpointIndexService.ts @@ -127,16 +127,21 @@ export class PerformEndpointIndexService { } const { jobs, sources, dynamicTriggers, dynamicSchedules } = bodyResult.data; - const { "trigger-version": triggerVersion } = headerResult.data; + const { "trigger-version": triggerVersion, "trigger-sdk-version": triggerSdkVersion } = + headerResult.data; const { endpoint } = endpointIndex; - if (triggerVersion && triggerVersion !== endpoint.version) { + if ( + (triggerVersion && triggerVersion !== endpoint.version) || + (triggerSdkVersion && triggerSdkVersion !== endpoint.sdkVersion) + ) { await this.#prismaClient.endpoint.update({ where: { id: endpoint.id, }, data: { version: triggerVersion, + sdkVersion: triggerSdkVersion, }, }); } diff --git a/apps/webapp/app/services/endpoints/probeEndpoint.server.ts b/apps/webapp/app/services/endpoints/probeEndpoint.server.ts new file mode 100644 index 000000000..ad8726d74 --- /dev/null +++ b/apps/webapp/app/services/endpoints/probeEndpoint.server.ts @@ -0,0 +1,64 @@ +import { MAX_RUN_CHUNK_EXECUTION_LIMIT, RESPONSE_TIMEOUT_STATUS_CODES } from "~/consts"; +import { prisma, PrismaClient } from "~/db.server"; +import { EndpointApi } from "../endpointApi.server"; +import { logger } from "../logger.server"; +import { detectResponseIsTimeout } from "~/models/endpoint.server"; + +export class ProbeEndpointService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(id: string) { + const endpoint = await this.#prismaClient.endpoint.findUnique({ + where: { + id, + }, + include: { + environment: true, + }, + }); + + if (!endpoint) { + return; + } + + logger.debug(`Probing endpoint`, { + id, + }); + + const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url); + + const { response, durationInMs } = await client.probe(MAX_RUN_CHUNK_EXECUTION_LIMIT); + + if (!response) { + return; + } + + logger.debug(`Probing endpoint complete`, { + id, + durationInMs, + response: { + status: response.status, + headers: Object.fromEntries(response.headers.entries()), + }, + }); + + // If the response is a 200, or it was a timeout, we can assume the endpoint is up and update the runChunkExecutionLimit + if (response.status === 200 || detectResponseIsTimeout(response)) { + await this.#prismaClient.endpoint.update({ + where: { + id, + }, + data: { + runChunkExecutionLimit: Math.min( + Math.max(durationInMs, 10000), + MAX_RUN_CHUNK_EXECUTION_LIMIT + ), + }, + }); + } + } +} diff --git a/apps/webapp/app/services/runs/forceYieldCoordinator.server.ts b/apps/webapp/app/services/runs/forceYieldCoordinator.server.ts new file mode 100644 index 000000000..c6565424d --- /dev/null +++ b/apps/webapp/app/services/runs/forceYieldCoordinator.server.ts @@ -0,0 +1,47 @@ +import { PrismaClient } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; +import { logger } from "../logger.server"; + +class ForceYieldCoordinator { + private inFlightRuns: Set = new Set(); + private prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient) { + this.prismaClient = prismaClient; + + process.on("SIGTERM", this.handleForceYield); + } + + // Add a run to the in-flight set + public registerRun(runId: string): void { + this.inFlightRuns.add(runId); + } + + // Remove a run from the in-flight set + public deregisterRun(runId: string): void { + this.inFlightRuns.delete(runId); + } + + // Handle forced yield on SIGTERM + private handleForceYield = async (): Promise => { + const runIds = Array.from(this.inFlightRuns); + + const results = await this.prismaClient.jobRun.updateMany({ + where: { + id: { + in: runIds, + }, + forceYieldImmediately: false, + }, + data: { + forceYieldImmediately: true, + }, + }); + + logger.debug( + `ForceYieldCoordinator: ${results.count}/${runIds.length} runs set to immediately force yield` + ); + }; +} + +export const forceYieldCoordinator = new ForceYieldCoordinator(prisma); diff --git a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts b/apps/webapp/app/services/runs/performRunExecutionV1.server.ts deleted file mode 100644 index c67a9dd18..000000000 --- a/apps/webapp/app/services/runs/performRunExecutionV1.server.ts +++ /dev/null @@ -1,771 +0,0 @@ -import { - CachedTaskSchema, - RunJobError, - RunJobInvalidPayloadError, - RunJobResumeWithTask, - RunJobRetryWithTask, - RunJobSuccess, - RunJobUnresolvedAuthError, - RunSourceContextSchema, -} from "@trigger.dev/core"; -import type { Task } from "@trigger.dev/database"; -import { generateErrorMessage } from "zod-error"; -import { eventRecordToApiJson } from "~/api.server"; -import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts"; -import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server"; -import { enqueueRunExecutionV1 } from "~/models/jobRunExecution.server"; -import { resolveRunConnections } from "~/models/runConnection.server"; -import { formatError } from "~/utils/formatErrors.server"; -import { safeJsonZodParse } from "~/utils/json"; -import { EndpointApi } from "../endpointApi.server"; -import { logger } from "../logger.server"; - -type FoundRunExecution = NonNullable>>; - -export class PerformRunExecutionV1Service { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call(id: string) { - const runExecution = await findRunExecution(this.#prismaClient, id); - - if (!runExecution) { - return; - } - - switch (runExecution.reason) { - case "PREPROCESS": { - await this.#executePreprocessing(runExecution); - break; - } - case "EXECUTE_JOB": { - await this.#executeJob(runExecution); - break; - } - } - } - - // Execute the preprocessing step of a run, which will send the payload to the endpoint and give the job - // an opportunity to generate run properties based on the payload. - // If the endpoint is not available, or the response is not ok, - // the run execution will be marked as failed and the run will start - async #executePreprocessing(execution: FoundRunExecution) { - const { run } = execution; - - const client = new EndpointApi(run.environment.apiKey, run.endpoint.url); - const event = eventRecordToApiJson(run.event); - const startedAt = new Date(); - - await this.#prismaClient.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "STARTED", - startedAt, - }, - }); - - const { response, parser } = await client.preprocessRunRequest({ - event, - job: { - id: run.version.job.slug, - version: run.version.version, - }, - run: { - id: run.id, - isTest: run.isTest, - }, - environment: { - id: run.environment.id, - slug: run.environment.slug, - type: run.environment.type, - }, - organization: { - id: run.organization.id, - slug: run.organization.slug, - title: run.organization.title, - }, - account: run.externalAccount - ? { - id: run.externalAccount.identifier, - metadata: run.externalAccount.metadata, - } - : undefined, - }); - - if (!response) { - return await this.#failRunExecutionWithRetry(execution, { - message: "Could not connect to the endpoint", - }); - } - - if (!response.ok) { - return await this.#failRunExecutionWithRetry(execution, { - message: `Endpoint responded with ${response.status} status code`, - }); - } - - const rawBody = await response.text(); - const safeBody = safeJsonZodParse(parser, rawBody); - - if (!safeBody) { - return await this.#failRunExecution(this.#prismaClient, execution, { - message: "Endpoint responded with invalid JSON", - }); - } - - if (!safeBody.success) { - return await this.#failRunExecution(this.#prismaClient, execution, { - message: generateErrorMessage(safeBody.error.issues), - }); - } - - if (safeBody.data.abort) { - return this.#failRunExecution( - this.#prismaClient, - execution, - { message: "Endpoint aborted the run" }, - "ABORTED" - ); - } else { - await $transaction(this.#prismaClient, async (tx) => { - await tx.jobRun.update({ - where: { - id: run.id, - }, - data: { - status: "STARTED", - startedAt: new Date(), - properties: safeBody.data.properties, - }, - }); - - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "SUCCESS", - completedAt: new Date(), - }, - }); - - const runExecution = await tx.jobRunExecution.create({ - data: { - runId: run.id, - reason: "EXECUTE_JOB", - status: "PENDING", - retryLimit: EXECUTE_JOB_RETRY_LIMIT, - }, - }); - - await enqueueRunExecutionV1(runExecution, run.queue.id, run.queue.maxJobs, tx); - }); - } - } - async #executeJob(execution: FoundRunExecution) { - const { run, isRetry } = execution; - - if (run.status === "CANCELED") { - await this.#cancelExecution(execution); - return; - } - - const client = new EndpointApi(run.environment.apiKey, run.endpoint.url); - const event = eventRecordToApiJson(run.event); - - const startedAt = new Date(); - - await this.#prismaClient.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "STARTED", - startedAt, - run: { - update: { - status: run.status === "QUEUED" ? "STARTED" : run.status, - startedAt: run.startedAt ?? new Date(), - }, - }, - }, - }); - - const connections = await resolveRunConnections(run.runConnections); - - if (!connections.success) { - return this.#failRunExecutionWithRetry(execution, { - message: `Could not resolve all connections for run ${run.id}, attempting to retry`, - }); - } - - let resumedTask: Task | undefined; - - if (execution.resumeTaskId) { - resumedTask = - (await this.#prismaClient.task.findUnique({ - where: { - id: execution.resumeTaskId, - }, - })) ?? undefined; - - if (resumedTask) { - resumedTask = await this.#prismaClient.task.update({ - where: { - id: execution.resumeTaskId, - }, - data: { - status: resumedTask.noop ? "COMPLETED" : "RUNNING", - completedAt: resumedTask.noop ? new Date() : undefined, - }, - }); - } - } - - const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext); - - const { response, parser, errorParser } = await client.executeJobRequest({ - event, - job: { - id: run.version.job.slug, - version: run.version.version, - }, - run: { - id: run.id, - isTest: run.isTest, - startedAt, - isRetry, - }, - environment: { - id: run.environment.id, - slug: run.environment.slug, - type: run.environment.type, - }, - organization: { - id: run.organization.id, - slug: run.organization.slug, - title: run.organization.title, - }, - account: run.externalAccount - ? { - id: run.externalAccount.identifier, - metadata: run.externalAccount.metadata, - } - : undefined, - connections: connections.auth, - source: sourceContext.success ? sourceContext.data : undefined, - tasks: [run.tasks, resumedTask] - .flat() - .filter(Boolean) - .map((t) => CachedTaskSchema.parse(t)), - yieldedExecutions: run.yieldedExecutions, - }); - - if (!response) { - return await this.#failRunExecutionWithRetry(execution, { - message: `Connection could not be established to the endpoint (${run.endpoint.url})`, - }); - } - - const rawBody = await response.text(); - - if (!response.ok) { - logger.debug("Endpoint responded with non-200 status code", { - status: response.status, - runId: run.id, - endpoint: run.endpoint.url, - }); - - const errorBody = safeJsonZodParse(errorParser, rawBody); - - if (errorBody && errorBody.success) { - // Only retry if the error isn't a 4xx - if (response.status >= 400 && response.status <= 499) { - return await this.#failRunExecution(this.#prismaClient, execution, errorBody.data); - } else { - return await this.#failRunExecutionWithRetry(execution, errorBody.data); - } - } - - // Only retry if the error isn't a 4xx - if (response.status >= 400 && response.status <= 499) { - return await this.#failRunExecution(this.#prismaClient, execution, { - message: `Endpoint responded with ${response.status} status code`, - }); - } else { - return await this.#failRunExecutionWithRetry(execution, { - message: `Endpoint responded with ${response.status} status code`, - }); - } - } - - const safeBody = safeJsonZodParse(parser, rawBody); - - if (!safeBody) { - return await this.#failRunExecution(this.#prismaClient, execution, { - message: "Endpoint responded with invalid JSON", - }); - } - - if (!safeBody.success) { - return await this.#failRunExecution(this.#prismaClient, execution, { - message: generateErrorMessage(safeBody.error.issues), - }); - } - - const status = safeBody.data.status; - - switch (status) { - case "SUCCESS": { - await this.#completeRunWithSuccess(execution, safeBody.data); - - break; - } - case "RESUME_WITH_TASK": { - await this.#resumeRunWithTask(execution, safeBody.data); - - break; - } - case "ERROR": { - await this.#failRunWithError(execution, safeBody.data); - - break; - } - case "RETRY_WITH_TASK": { - await this.#retryRunWithTask(execution, safeBody.data); - - break; - } - case "CANCELED": { - await this.#cancelExecution(execution); - break; - } - case "UNRESOLVED_AUTH_ERROR": { - await this.#failRunWithUnresolvedAuthError(execution, safeBody.data); - - break; - } - case "INVALID_PAYLOAD": { - await this.#failRunWithInvalidPayloadError(execution, safeBody.data); - - break; - } - case "YIELD_EXECUTION": { - await this.#resumeYieldedExecution(execution, safeBody.data.key); - - break; - } - default: { - const _exhaustiveCheck: never = status; - throw new Error(`Non-exhaustive match for value: ${status}`); - } - } - } - - async #completeRunWithSuccess(execution: FoundRunExecution, data: RunJobSuccess) { - const { run } = execution; - - return await $transaction(this.#prismaClient, async (tx) => { - await tx.jobRun.update({ - where: { id: run.id }, - data: { - completedAt: new Date(), - status: "SUCCESS", - output: data.output ?? undefined, - queue: { - update: { - jobCount: { - decrement: 1, - }, - }, - }, - }, - }); - - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "SUCCESS", - completedAt: new Date(), - }, - }); - }); - } - - async #resumeYieldedExecution(execution: FoundRunExecution, key: string) { - const { run } = execution; - - return await $transaction(this.#prismaClient, async (tx) => { - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "SUCCESS", - completedAt: new Date(), - run: { - update: { - yieldedExecutions: { - push: key, - }, - }, - }, - }, - }); - - const newJobExecution = await tx.jobRunExecution.create({ - data: { - runId: run.id, - reason: "EXECUTE_JOB", - status: "PENDING", - retryLimit: EXECUTE_JOB_RETRY_LIMIT, - }, - }); - - await enqueueRunExecutionV1(newJobExecution, run.queue.id, run.queue.maxJobs, tx); - }); - } - - async #resumeRunWithTask(execution: FoundRunExecution, data: RunJobResumeWithTask) { - const { run } = execution; - - return await $transaction(this.#prismaClient, async (tx) => { - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "SUCCESS", - completedAt: new Date(), - }, - }); - - // If the task has an operation, then the next performRunExecution will occur - // when that operation has finished - // Tasks with callbacks enabled will also get processed separately, i.e. when - // they time out, or on valid requests to their callbackUrl - if (!data.task.operation && !data.task.callbackUrl) { - const newJobExecution = await tx.jobRunExecution.create({ - data: { - runId: run.id, - reason: "EXECUTE_JOB", - status: "PENDING", - retryLimit: EXECUTE_JOB_RETRY_LIMIT, - resumeTaskId: data.task.id, - }, - }); - - await enqueueRunExecutionV1( - newJobExecution, - run.queue.id, - run.queue.maxJobs, - tx, - data.task.delayUntil ?? undefined - ); - } - }); - } - - async #failRunWithError(execution: FoundRunExecution, data: RunJobError) { - return await $transaction(this.#prismaClient, async (tx) => { - if (data.task) { - await tx.task.update({ - where: { - id: data.task.id, - }, - data: { - status: "ERRORED", - completedAt: new Date(), - output: data.error ?? undefined, - }, - }); - } - - await this.#failRunExecution(tx, execution, data.error ?? undefined); - }); - } - - async #failRunWithUnresolvedAuthError( - execution: FoundRunExecution, - data: RunJobUnresolvedAuthError - ) { - return await $transaction(this.#prismaClient, async (tx) => { - await this.#failRunExecution(tx, execution, data.issues, "UNRESOLVED_AUTH"); - }); - } - - async #failRunWithInvalidPayloadError( - execution: FoundRunExecution, - data: RunJobInvalidPayloadError - ) { - return await $transaction(this.#prismaClient, async (tx) => { - await this.#failRunExecution(tx, execution, data.errors, "INVALID_PAYLOAD"); - }); - } - - async #retryRunWithTask(execution: FoundRunExecution, data: RunJobRetryWithTask) { - const { run } = execution; - - return await $transaction(this.#prismaClient, async (tx) => { - // We need to check for an existing task attempt - const existingAttempt = await tx.taskAttempt.findFirst({ - where: { - taskId: data.task.id, - status: "PENDING", - }, - orderBy: { - number: "desc", - }, - }); - - if (existingAttempt) { - await tx.taskAttempt.update({ - where: { - id: existingAttempt.id, - }, - data: { - status: "ERRORED", - error: formatError(data.error), - }, - }); - } - - // We need to create a new task attempt - await tx.taskAttempt.create({ - data: { - taskId: data.task.id, - number: existingAttempt ? existingAttempt.number + 1 : 1, - status: "PENDING", - runAt: data.retryAt, - }, - }); - - await tx.task.update({ - where: { - id: data.task.id, - }, - data: { - status: "WAITING", - }, - }); - - // Now we need to create a new job execution - const newJobExecution = await tx.jobRunExecution.create({ - data: { - runId: run.id, - reason: "EXECUTE_JOB", - status: "PENDING", - retryLimit: EXECUTE_JOB_RETRY_LIMIT, - resumeTaskId: data.task.id, - }, - }); - - await enqueueRunExecutionV1( - newJobExecution, - run.queue.id, - run.queue.maxJobs, - tx, - data.retryAt - ); - }); - } - - async #failRunExecutionWithRetry( - execution: FoundRunExecution, - output: Record - ): Promise { - await $transaction(this.#prismaClient, async (tx) => { - if (execution.retryCount + 1 > execution.retryLimit) { - // We've reached the retry limit, so we need to fail the execution and stop retrying - return await this.#failRunExecution(tx, execution, output); - } - - // We need to retry execution - const retryCount = execution.retryCount + 1; - // Use an exponential backoff strategy with the exponent being 1.5 - // So when retryCount is 1, retryDelayInMs is 500ms - // When retryCount is 2, retryDelayInMs is 750ms - // When retryCount is 3, retryDelayInMs is 1125ms - // When retryCount is 4, retryDelayInMs is 1687ms - // When retryCount is 5, retryDelayInMs is 2531ms - // When retryCount is 6, retryDelayInMs is 3796ms - // When retryCount is 7, retryDelayInMs is 5694ms - // When retryCount is 8, retryDelayInMs is 8541ms - // When retryCount is 9, retryDelayInMs is 12812ms - // When retryCount is 10, retryDelayInMs is 19218ms - const retryDelayInMs = Math.round(500 * Math.pow(1.5, retryCount - 1)); - - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - retryCount, - retryDelayInMs, - error: JSON.stringify(output), - }, - }); - - const runAt = new Date(Date.now() + retryDelayInMs); - - await enqueueRunExecutionV1( - execution, - execution.run.queue.id, - execution.run.queue.maxJobs, - tx, - runAt - ); - }); - } - - async #failRunExecution( - prisma: PrismaClientOrTransaction, - execution: FoundRunExecution, - output: Record, - status: "FAILURE" | "ABORTED" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE" - ): Promise { - const { run } = execution; - - await $transaction(prisma, async (tx) => { - switch (execution.reason) { - case "EXECUTE_JOB": { - // If the execution is an EXECUTE_JOB reason, we need to fail the run - await tx.jobRun.update({ - where: { id: run.id }, - data: { - completedAt: new Date(), - status, - output, - queue: { - update: { - jobCount: { - decrement: 1, - }, - }, - }, - }, - }); - - break; - } - case "PREPROCESS": { - // If the status is ABORTED, we need to fail the run - if (status === "ABORTED") { - await tx.jobRun.update({ - where: { id: run.id }, - data: { - completedAt: new Date(), - status, - output, - queue: { - update: { - jobCount: { - decrement: 1, - }, - }, - }, - }, - }); - - break; - } - - await tx.jobRun.update({ - where: { - id: run.id, - }, - data: { - status: "STARTED", - startedAt: new Date(), - }, - }); - - const runExecution = await tx.jobRunExecution.create({ - data: { - runId: run.id, - reason: "EXECUTE_JOB", - status: "PENDING", - retryLimit: EXECUTE_JOB_RETRY_LIMIT, - }, - }); - - await enqueueRunExecutionV1(runExecution, run.queue.id, run.queue.maxJobs, tx); - - break; - } - } - - await tx.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "FAILURE", - completedAt: new Date(), - error: JSON.stringify(output), - }, - }); - }); - } - - async #cancelExecution(execution: FoundRunExecution) { - await this.#prismaClient.jobRunExecution.update({ - where: { - id: execution.id, - }, - data: { - status: "FAILURE", - completedAt: new Date(), - error: "This never ran because it was canceled by the user.", - }, - }); - } -} - -async function findRunExecution(prisma: PrismaClientOrTransaction, id: string) { - return await prisma.jobRunExecution.findUnique({ - where: { id }, - include: { - run: { - include: { - environment: true, - endpoint: true, - organization: true, - externalAccount: true, - queue: true, - runConnections: { - include: { - integration: true, - connection: { - include: { - dataReference: true, - }, - }, - }, - }, - tasks: { - where: { - status: { - in: ["COMPLETED"], - }, - }, - }, - event: true, - version: { - include: { - job: true, - organization: true, - }, - }, - }, - }, - }, - }); -} diff --git a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts index f22d13edb..ec9318181 100644 --- a/apps/webapp/app/services/runs/performRunExecutionV2.server.ts +++ b/apps/webapp/app/services/runs/performRunExecutionV2.server.ts @@ -3,6 +3,7 @@ import { BloomFilter, ConnectionAuth, EndpointHeadersSchema, + RunJobAutoYieldWithCompletedTaskExecutionError, RunJobError, RunJobInvalidPayloadError, RunJobResumeWithTask, @@ -24,9 +25,17 @@ import { safeJsonZodParse } from "~/utils/json"; import { EndpointApi } from "../endpointApi.server"; import { logger } from "../logger.server"; import { prepareTasksForCaching, prepareTasksForCachingLegacy } from "~/models/task.server"; -import { MAX_RUN_YIELDED_EXECUTIONS } from "~/consts"; +import { + MAX_RUN_CHUNK_EXECUTION_LIMIT, + MAX_RUN_YIELDED_EXECUTIONS, + RESPONSE_TIMEOUT_STATUS_CODES, + RUN_CHUNK_EXECUTION_BUFFER, +} from "~/consts"; import { ApiEventLog } from "@trigger.dev/core"; import { RunJobBody } from "@trigger.dev/core"; +import { CompleteRunTaskService } from "~/routes/api.v1.runs.$runId.tasks.$id.complete"; +import { detectResponseIsTimeout } from "~/models/endpoint.server"; +import { forceYieldCoordinator } from "./forceYieldCoordinator.server"; type FoundRun = NonNullable>>; type FoundTask = FoundRun["tasks"][number]; @@ -148,6 +157,7 @@ export class PerformRunExecutionV2Service { status: "STARTED", startedAt: new Date(), properties: safeBody.data.properties, + forceYieldImmediately: false, }, }); @@ -158,257 +168,291 @@ export class PerformRunExecutionV2Service { } } async #executeJob(run: FoundRun, input: PerformRunExecutionV2Input) { - const { isRetry, resumeTaskId } = input; - - if (run.status === "CANCELED") { - await this.#cancelExecution(run); - return; - } - try { - if ( - typeof process.env.BLOCKED_ORGS === "string" && - process.env.BLOCKED_ORGS.includes(run.organizationId) - ) { - logger.debug("Skipping execution for blocked org", { - orgId: run.organizationId, - }); - - await this.#prismaClient.jobRun.update({ - where: { - id: run.id, - }, - data: { - status: "CANCELED", - completedAt: new Date(), - }, - }); + const { isRetry, resumeTaskId } = input; + if (run.status === "CANCELED") { + await this.#cancelExecution(run); return; } - } catch (e) {} - const client = new EndpointApi(run.environment.apiKey, run.endpoint.url); - const event = eventRecordToApiJson(run.event); + try { + if ( + typeof process.env.BLOCKED_ORGS === "string" && + process.env.BLOCKED_ORGS.includes(run.organizationId) + ) { + logger.debug("Skipping execution for blocked org", { + orgId: run.organizationId, + }); - const startedAt = new Date(); + await this.#prismaClient.jobRun.update({ + where: { + id: run.id, + }, + data: { + status: "CANCELED", + completedAt: new Date(), + }, + }); - const { executionCount } = await this.#prismaClient.jobRun.update({ - where: { - id: run.id, - }, - data: { - status: run.status === "QUEUED" ? "STARTED" : run.status, - startedAt: run.startedAt ?? new Date(), - executionCount: { - increment: 1, + return; + } + } catch (e) {} + + const client = new EndpointApi(run.environment.apiKey, run.endpoint.url); + const event = eventRecordToApiJson(run.event); + + const startedAt = new Date(); + + const { executionCount } = await this.#prismaClient.jobRun.update({ + where: { + id: run.id, }, - }, - select: { - executionCount: true, - }, - }); - - const connections = await resolveRunConnections(run.runConnections); - - if (!connections.success) { - return this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, { - message: `Could not resolve all connections for run ${run.id}. This should not happen`, - }); - } - - let resumedTask: Task | undefined; - - if (resumeTaskId) { - resumedTask = - (await this.#prismaClient.task.findUnique({ - where: { - id: resumeTaskId, + data: { + status: run.status === "QUEUED" ? "STARTED" : run.status, + startedAt: run.startedAt ?? new Date(), + executionCount: { + increment: 1, }, - })) ?? undefined; + }, + select: { + executionCount: true, + }, + }); - if (resumedTask) { - resumedTask = await this.#prismaClient.task.update({ + const connections = await resolveRunConnections(run.runConnections); + + if (!connections.success) { + return this.#failRunExecution(this.#prismaClient, "EXECUTE_JOB", run, { + message: `Could not resolve all connections for run ${run.id}. This should not happen`, + }); + } + + let resumedTask: Task | undefined; + + if (resumeTaskId) { + resumedTask = + (await this.#prismaClient.task.findUnique({ + where: { + id: resumeTaskId, + }, + })) ?? undefined; + + if (resumedTask) { + resumedTask = await this.#prismaClient.task.update({ + where: { + id: resumeTaskId, + }, + data: { + status: resumedTask.noop ? "COMPLETED" : "RUNNING", + completedAt: resumedTask.noop ? new Date() : undefined, + }, + }); + } + } + + const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext); + + const executionBody = await this.#createExecutionBody( + run, + [run.tasks, resumedTask].flat().filter(Boolean), + startedAt, + isRetry, + connections.auth, + event, + sourceContext.success ? sourceContext.data : undefined + ); + + forceYieldCoordinator.registerRun(run.id); + + const { response, parser, errorParser, durationInMs } = await client.executeJobRequest( + executionBody + ); + + forceYieldCoordinator.deregisterRun(run.id); + + if (!response) { + return await this.#failRunExecutionWithRetry({ + message: `Connection could not be established to the endpoint (${run.endpoint.url})`, + }); + } + + // Update the endpoint version if it has changed + const rawHeaders = Object.fromEntries(response.headers.entries()); + const headers = EndpointHeadersSchema.safeParse(rawHeaders); + + if ( + headers.success && + headers.data["trigger-version"] && + headers.data["trigger-version"] !== run.endpoint.version + ) { + await this.#prismaClient.endpoint.update({ where: { - id: resumeTaskId, + id: run.endpoint.id, }, data: { - status: resumedTask.noop ? "COMPLETED" : "RUNNING", - completedAt: resumedTask.noop ? new Date() : undefined, + version: headers.data["trigger-version"], }, }); } - } - const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext); + const rawBody = await response.text(); - const executionBody = await this.#createExecutionBody( - run, - [run.tasks, resumedTask].flat().filter(Boolean), - startedAt, - isRetry, - connections.auth, - event, - sourceContext.success ? sourceContext.data : undefined - ); + if (!response.ok) { + logger.debug("Endpoint responded with non-200 status code", { + status: response.status, + runId: run.id, + endpoint: run.endpoint.url, + }); - const { response, parser, errorParser, durationInMs } = await client.executeJobRequest( - executionBody - ); + const errorBody = safeJsonZodParse(errorParser, rawBody); - if (!response) { - return await this.#failRunExecutionWithRetry({ - message: `Connection could not be established to the endpoint (${run.endpoint.url})`, - }); - } + if (errorBody && errorBody.success) { + // Only retry if the error isn't a 4xx + if (response.status >= 400 && response.status <= 499) { + return await this.#failRunExecution( + this.#prismaClient, + "EXECUTE_JOB", + run, + errorBody.data + ); + } else { + return await this.#failRunExecutionWithRetry(errorBody.data); + } + } - // Update the endpoint version if it has changed - const rawHeaders = Object.fromEntries(response.headers.entries()); - const headers = EndpointHeadersSchema.safeParse(rawHeaders); - - if ( - headers.success && - headers.data["trigger-version"] && - headers.data["trigger-version"] !== run.endpoint.version - ) { - await this.#prismaClient.endpoint.update({ - where: { - id: run.endpoint.id, - }, - data: { - version: headers.data["trigger-version"], - }, - }); - } - - const rawBody = await response.text(); - - if (!response.ok) { - logger.debug("Endpoint responded with non-200 status code", { - status: response.status, - runId: run.id, - endpoint: run.endpoint.url, - }); - - const errorBody = safeJsonZodParse(errorParser, rawBody); - - if (errorBody && errorBody.success) { // Only retry if the error isn't a 4xx - if (response.status >= 400 && response.status <= 499) { + if (response.status >= 400 && response.status <= 499 && response.status !== 408) { return await this.#failRunExecution( this.#prismaClient, "EXECUTE_JOB", run, - errorBody.data + { + message: `Endpoint responded with ${response.status} status code`, + }, + "FAILURE", + durationInMs ); } else { - return await this.#failRunExecutionWithRetry(errorBody.data); + // If the error is a timeout, we should mark this execution as succeeded (by not throwing an error) and enqueue a new execution + if (detectResponseIsTimeout(response)) { + return await this.#resumeRunExecutionAfterTimeout( + this.#prismaClient, + run, + input, + durationInMs, + executionCount + ); + } else { + return await this.#failRunExecutionWithRetry({ + message: `Endpoint responded with ${response.status} status code`, + }); + } } } - // Only retry if the error isn't a 4xx - if (response.status >= 400 && response.status <= 499 && response.status !== 408) { + const safeBody = safeJsonZodParse(parser, rawBody); + + if (!safeBody) { return await this.#failRunExecution( this.#prismaClient, "EXECUTE_JOB", run, { - message: `Endpoint responded with ${response.status} status code`, + message: "Endpoint responded with invalid JSON", }, "FAILURE", durationInMs ); - } else { - // If the error is a 504 timeout, we should mark this execution as succeeded (by not throwing an error) and enqueue a new execution - if (response.status === 504) { - return await this.#resumeRunExecutionAfterTimeout( - this.#prismaClient, + } + + if (!safeBody.success) { + return await this.#failRunExecution( + this.#prismaClient, + "EXECUTE_JOB", + run, + { + message: generateErrorMessage(safeBody.error.issues), + }, + "FAILURE", + durationInMs + ); + } + + const status = safeBody.data.status; + + switch (status) { + case "SUCCESS": { + await this.#completeRunWithSuccess(run, safeBody.data, durationInMs); + + break; + } + case "RESUME_WITH_TASK": { + await this.#resumeRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount); + + break; + } + case "ERROR": { + await this.#failRunWithError(run, safeBody.data, durationInMs); + + break; + } + case "RETRY_WITH_TASK": { + await this.#retryRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount); + + break; + } + case "CANCELED": { + await this.#cancelExecution(run); + break; + } + case "UNRESOLVED_AUTH_ERROR": { + await this.#failRunWithUnresolvedAuthError(run, safeBody.data, durationInMs); + + break; + } + case "INVALID_PAYLOAD": { + await this.#failRunWithInvalidPayloadError(run, safeBody.data, durationInMs); + + break; + } + case "YIELD_EXECUTION": { + await this.#resumeYieldedRun( run, - input, + safeBody.data.key, + isRetry, durationInMs, executionCount ); - } else { - return await this.#failRunExecutionWithRetry({ - message: `Endpoint responded with ${response.status} status code`, - }); + break; + } + case "AUTO_YIELD_EXECUTION": { + await this.#resumeAutoYieldedRun( + run, + safeBody.data, + isRetry, + durationInMs, + executionCount + ); + break; + } + case "AUTO_YIELD_EXECUTION_WITH_COMPLETED_TASK": { + await this.#resumeAutoYieldedRunWithCompletedTask( + run, + safeBody.data, + isRetry, + durationInMs, + executionCount + ); + break; + } + default: { + const _exhaustiveCheck: never = status; + throw new Error(`Non-exhaustive match for value: ${status}`); } } - } - - const safeBody = safeJsonZodParse(parser, rawBody); - - if (!safeBody) { - return await this.#failRunExecution( - this.#prismaClient, - "EXECUTE_JOB", - run, - { - message: "Endpoint responded with invalid JSON", - }, - "FAILURE", - durationInMs - ); - } - - if (!safeBody.success) { - return await this.#failRunExecution( - this.#prismaClient, - "EXECUTE_JOB", - run, - { - message: generateErrorMessage(safeBody.error.issues), - }, - "FAILURE", - durationInMs - ); - } - - const status = safeBody.data.status; - - switch (status) { - case "SUCCESS": { - await this.#completeRunWithSuccess(run, safeBody.data, durationInMs); - - break; - } - case "RESUME_WITH_TASK": { - await this.#resumeRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount); - - break; - } - case "ERROR": { - await this.#failRunWithError(run, safeBody.data, durationInMs); - - break; - } - case "RETRY_WITH_TASK": { - await this.#retryRunWithTask(run, safeBody.data, isRetry, durationInMs, executionCount); - - break; - } - case "CANCELED": { - await this.#cancelExecution(run); - break; - } - case "UNRESOLVED_AUTH_ERROR": { - await this.#failRunWithUnresolvedAuthError(run, safeBody.data, durationInMs); - - break; - } - case "INVALID_PAYLOAD": { - await this.#failRunWithInvalidPayloadError(run, safeBody.data, durationInMs); - - break; - } - case "YIELD_EXECUTION": { - await this.#resumeYieldedRun(run, safeBody.data.key, isRetry, durationInMs, executionCount); - break; - } - default: { - const _exhaustiveCheck: never = status; - throw new Error(`Non-exhaustive match for value: ${status}`); - } + } finally { + forceYieldCoordinator.deregisterRun(run.id); } } @@ -458,6 +502,13 @@ export class PerformRunExecutionV2Service { cachedTaskCursor: preparedTasks.cursor, noopTasksSet: prepareNoOpTasksBloomFilter(tasks), yieldedExecutions: run.yieldedExecutions, + runChunkExecutionLimit: run.endpoint.runChunkExecutionLimit - RUN_CHUNK_EXECUTION_BUFFER, + autoYieldConfig: { + startTaskThreshold: run.endpoint.startTaskThreshold, + beforeExecuteTaskThreshold: run.endpoint.beforeExecuteTaskThreshold, + beforeCompleteTaskThreshold: run.endpoint.beforeCompleteTaskThreshold, + afterCompleteTaskThreshold: run.endpoint.afterCompleteTaskThreshold, + }, }; } @@ -639,6 +690,7 @@ export class PerformRunExecutionV2Service { yieldedExecutions: { push: key, }, + forceYieldImmediately: false, }, select: { yieldedExecutions: true, @@ -654,6 +706,101 @@ export class PerformRunExecutionV2Service { }); } + async #resumeAutoYieldedRun( + run: FoundRun, + data: { location: string; timeRemaining: number; timeElapsed: number; limit?: number }, + isRetry: boolean, + durationInMs: number, + executionCount: number + ) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.jobRun.update({ + where: { + id: run.id, + }, + data: { + executionDuration: { + increment: durationInMs, + }, + executionCount: { + increment: 1, + }, + autoYieldExecution: { + create: [ + { + location: data.location, + timeRemaining: data.timeRemaining, + timeElapsed: data.timeElapsed, + limit: data.limit ?? 0, + }, + ], + }, + forceYieldImmediately: false, + }, + select: { + executionCount: true, + }, + }); + + await enqueueRunExecutionV2(run, tx, { + isRetry, + skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + executionCount, + }); + }); + } + + async #resumeAutoYieldedRunWithCompletedTask( + run: FoundRun, + data: RunJobAutoYieldWithCompletedTaskExecutionError, + isRetry: boolean, + durationInMs: number, + executionCount: number + ) { + await $transaction(this.#prismaClient, async (tx) => { + await tx.jobRun.update({ + where: { + id: run.id, + }, + data: { + executionDuration: { + increment: durationInMs, + }, + executionCount: { + increment: 1, + }, + autoYieldExecution: { + create: [ + { + location: data.data.location, + timeRemaining: data.data.timeRemaining, + timeElapsed: data.data.timeElapsed, + limit: data.data.limit ?? 0, + }, + ], + }, + forceYieldImmediately: false, + }, + select: { + executionCount: true, + }, + }); + + const service = new CompleteRunTaskService(tx); + + await service.call(run.environment, run.id, data.id, { + properties: data.properties, + output: data.output, + }); + + await enqueueRunExecutionV2(run, tx, { + isRetry, + skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT, + executionCount, + }); + }); + } + async #retryRunWithTask( run: FoundRun, data: RunJobRetryWithTask, @@ -748,6 +895,54 @@ export class PerformRunExecutionV2Service { return; } + const runWithLatestTask = await tx.jobRun.findUniqueOrThrow({ + where: { + id: run.id, + }, + select: { + tasks: { + select: { + id: true, + name: true, + status: true, + displayKey: true, + }, + take: 1, + orderBy: { createdAt: "desc" }, + }, + _count: { + select: { + tasks: true, + }, + }, + }, + }); + + if (runWithLatestTask._count.tasks === run._count.tasks) { + const latestTask = runWithLatestTask.tasks[0]; + + const cause = + latestTask?.status === "RUNNING" + ? `This is likely caused by task "${ + latestTask.displayKey ?? latestTask.name + }" execution exceeding the function timeout` + : "This is likely caused by executing code outside of a task that exceeded the function timeout"; + + await this.#failRunExecution( + tx, + "EXECUTE_JOB", + run, + { + message: `Function timeout detected in ${ + durationInMs / 1000.0 + }s without any task creation. This is unexpected behavior and could lead to an infinite execution error because the run will never finish. ${cause}`, + }, + "TIMED_OUT", + durationInMs + ); + return; + } + await tx.jobRun.update({ where: { id: run.id, @@ -756,6 +951,16 @@ export class PerformRunExecutionV2Service { executionDuration: { increment: durationInMs, }, + endpoint: { + update: { + // Never allow the execution limit to be less than 10 seconds or more than MAX_RUN_CHUNK_EXECUTION_LIMIT + runChunkExecutionLimit: Math.min( + Math.max(durationInMs, 10000), + MAX_RUN_CHUNK_EXECUTION_LIMIT + ), + }, + }, + forceYieldImmediately: false, }, }); @@ -794,6 +999,20 @@ export class PerformRunExecutionV2Service { executionDuration: { increment: durationInMs, }, + tasks: { + updateMany: { + where: { + status: { + in: ["WAITING", "RUNNING", "PENDING"], + }, + }, + data: { + status: status === "TIMED_OUT" ? "CANCELED" : "ERRORED", + completedAt: new Date(), + }, + }, + }, + forceYieldImmediately: false, }, }); @@ -855,7 +1074,12 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) { return await prisma.jobRun.findUnique({ where: { id }, include: { - environment: true, + environment: { + include: { + project: true, + organization: true, + }, + }, endpoint: true, organization: true, externalAccount: true, @@ -894,6 +1118,11 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) { organization: true, }, }, + _count: { + select: { + tasks: true, + }, + }, }, }); } diff --git a/apps/webapp/app/services/worker.server.ts b/apps/webapp/app/services/worker.server.ts index cb9b4d6b0..15f2534a2 100644 --- a/apps/webapp/app/services/worker.server.ts +++ b/apps/webapp/app/services/worker.server.ts @@ -13,7 +13,6 @@ import { InvokeDispatcherService } from "./events/invokeDispatcher.server"; import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server"; import { IntegrationConnectionCreatedService } from "./externalApis/integrationConnectionCreated.server"; import { MissingConnectionCreatedService } from "./runs/missingConnectionCreated.server"; -import { PerformRunExecutionV1Service } from "./runs/performRunExecutionV1.server"; import { PerformRunExecutionV2Service } from "./runs/performRunExecutionV2.server"; import { StartRunService } from "./runs/startRun.server"; import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent.server"; @@ -21,6 +20,7 @@ import { ActivateSourceService } from "./sources/activateSource.server"; import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server"; import { PerformTaskOperationService } from "./tasks/performTaskOperation.server"; import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout"; +import { ProbeEndpointService } from "./endpoints/probeEndpoint.server"; const workerCatalog = { indexEndpoint: z.object({ @@ -75,15 +75,15 @@ const workerCatalog = { connectionCreated: z.object({ id: z.string(), }), + probeEndpoint: z.object({ + id: z.string(), + }), simulate: z.object({ seconds: z.number(), }), }; const executionWorkerCatalog = { - performRunExecution: z.object({ - id: z.string(), - }), performRunExecutionV2: z.object({ id: z.string(), reason: z.enum(["EXECUTE_JOB", "PREPROCESS"]), @@ -316,6 +316,15 @@ function getWorkerQueue() { }); }, }, + probeEndpoint: { + priority: 10, + maxAttempts: 1, + handler: async (payload, job) => { + const service = new ProbeEndpointService(); + + await service.call(payload.id); + }, + }, simulate: { maxAttempts: 5, handler: async (payload, job) => { @@ -341,17 +350,6 @@ function getExecutionWorkerQueue() { shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT, schema: executionWorkerCatalog, tasks: { - performRunExecution: { - priority: 0, // smaller number = higher priority - maxAttempts: 1, - handler: async (payload, job) => { - // This is a legacy task that we don't use anymore, but needs to be here for backwards compatibility - // TODO: remove this once all performRunExecution tasks have been processed - const service = new PerformRunExecutionV1Service(); - - await service.call(payload.id); - }, - }, performRunExecutionV2: { priority: 0, // smaller number = higher priority maxAttempts: 12, diff --git a/apps/webapp/package.json b/apps/webapp/package.json index b9fcc6f95..675b49e86 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -40,7 +40,6 @@ "@codemirror/view": "^6.5.0", "@conform-to/react": "^0.6.1", "@conform-to/zod": "^0.6.1", - "@godaddy/terminus": "^4.12.1", "@headlessui/react": "^1.7.8", "@heroicons/react": "^2.0.12", "@highlight-run/node": "^3.1.0", diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index e40b0a5bc..ac31b4ee2 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -3,7 +3,6 @@ import express from "express"; import compression from "compression"; import morgan from "morgan"; import { createRequestHandler } from "@remix-run/express"; -import { createTerminus } from "@godaddy/terminus"; const app = express(); @@ -63,23 +62,14 @@ if (process.env.HTTP_SERVER_DISABLED !== "true") { server.keepAliveTimeout = 65 * 1000; - // Handle shutdowns gracefully - createTerminus(server, { - signals: ["SIGINT", "SIGTERM"], - timeout: process.env.GRACEFUL_SHUTDOWN_TIMEOUT - ? Number(process.env.GRACEFUL_SHUTDOWN_TIMEOUT) - : 5000, - onSignal: async () => { - console.log("[terminus] onSignal: starting cleanup"); - }, - onShutdown: async () => { - console.log("[terminus] onShutdown: cleanup finished, server is shutting down"); - }, - onSendFailureDuringShutdown: async () => { - console.log( - "[terminus] onSendFailureDuringShutdown: cleanup finished, server is shutting down" - ); - }, + process.on("SIGTERM", () => { + server.close((err) => { + if (err) { + console.error("Error closing express server:", err); + } else { + console.log("Express server closed gracefully."); + } + }); }); } else { require(BUILD_DIR); diff --git a/docker/Dockerfile b/docker/Dockerfile index cf12ecd6a..ce185f9ea 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.16.1-bullseye-slim AS pruner +FROM node:18.18.2-bullseye-slim@sha256:a50436b44fe3eb2ff72696fb394ad3cae45f06cb2a7468c5ef3f012d23d9888f AS pruner WORKDIR /triggerdotdev @@ -7,7 +7,7 @@ RUN npx -q turbo@1.10.9 prune --scope=webapp --docker RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' + # Base strategy to have layer caching -FROM node:18.16.1-bullseye-slim AS base +FROM node:18.18.2-bullseye-slim@sha256:a50436b44fe3eb2ff72696fb394ad3cae45f06cb2a7468c5ef3f012d23d9888f AS base RUN apt-get update && apt-get install -y openssl dumb-init WORKDIR /triggerdotdev COPY --chown=node:node .gitignore .gitignore @@ -50,7 +50,7 @@ RUN pnpm run generate RUN pnpm run build --filter=webapp... # Runner -FROM node:18.16.1-bullseye-slim AS runner +FROM node:18.18.2-bullseye-slim@sha256:a50436b44fe3eb2ff72696fb394ad3cae45f06cb2a7468c5ef3f012d23d9888f AS runner RUN apt-get update && apt-get install -y openssl WORKDIR /triggerdotdev RUN corepack enable diff --git a/docker/services-compose.yml b/docker/services-compose.yml new file mode 100644 index 000000000..b2ec2bdb7 --- /dev/null +++ b/docker/services-compose.yml @@ -0,0 +1,65 @@ +version: "3" + +volumes: + database-data: + +networks: + app_network: + external: false + +services: + db: + container_name: devdb + image: postgres:14 + restart: always + volumes: + - database-data:/var/lib/postgresql/data/ + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + networks: + - app_network + ports: + - 5432:5432 + app: + build: + context: ../ + dockerfile: ./docker/Dockerfile + ports: + - 3030:3030 + depends_on: + - db + env_file: + - ../.env + environment: + DATABASE_URL: postgres://postgres:postgres@db:5432/postgres?schema=public + DIRECT_URL: postgres://postgres:postgres@db:5432/postgres?schema=public + SESSION_SECRET: secret123 + MAGIC_LINK_SECRET: secret123 + ENCRYPTION_KEY: secret123 + REMIX_APP_PORT: 3030 + PORT: 3030 + WORKER_ENABLED: "false" + EXECUTION_WORKER_ENABLED: "false" + networks: + - app_network + worker: + build: + context: ../ + dockerfile: ./docker/Dockerfile + depends_on: + - db + env_file: + - ../.env + environment: + DATABASE_URL: postgres://postgres:postgres@db:5432/postgres?schema=public + DIRECT_URL: postgres://postgres:postgres@db:5432/postgres?schema=public + SESSION_SECRET: secret123 + MAGIC_LINK_SECRET: secret123 + ENCRYPTION_KEY: secret123 + REMIX_APP_PORT: 3030 + PORT: 3030 + HTTP_SERVER_DISABLED: "true" + networks: + - app_network diff --git a/docs/_snippets/stable-key-param.mdx b/docs/_snippets/stable-key-param.mdx index 1053571c7..37227f417 100644 --- a/docs/_snippets/stable-key-param.mdx +++ b/docs/_snippets/stable-key-param.mdx @@ -1,4 +1,4 @@ - - Should be a stable and unique key inside the `run()`. See + + Should be a stable and unique cache key inside the `run()`. See [resumability](/documentation/concepts/resumability) for more information. diff --git a/docs/documentation/concepts/limitations.mdx b/docs/documentation/concepts/limitations.mdx deleted file mode 100644 index ab9b80a8c..000000000 --- a/docs/documentation/concepts/limitations.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Limitations" ---- - -There are a few limitations that are important to understand. - -In the latest version: - -- Runs on localhost are limited to 5 minutes. -- On long-running servers (not serverless) Runs can be retried erroneously. -- Compute intensive jobs are not well supported. - -## Runs on localhost are limited to 5 minutes - -When developing locally the [CLI dev command](/documentation/guides/cli#dev-command) uses [ngrok](https://ngrok.com/) so messages can be sent to your machine. - -Ngrok has a timeout of 5 minutes on a Request/Response cycle. so, if a localhost Run takes longer than 5 minutes to complete, the Run will fail. - -This limitation will be removed in the future by adding an alternative run strategy that works well on localhost and long-running servers. This won't use the request/response cycle. - -## On long-running servers (not serverless) Runs can be retried erroneously - -Currently the only way that Runs are performed is by a Request/Response cycle when `run` is called on a Job. This is optimized for serverless functions (where you have to use a Request/Response cycle), but not for long-running servers. - -This limitation will be removed in the future by adding an alternative mode so Jobs works well on localhost and long-running servers. This won't use the request/response cycle. - -## Compute intensive jobs are not well supported - -Currently the only way that Runs are performed is inside a Request/Response cycle when `run` is called on a Job. This is not a good way to perform compute intensive jobs. - -In the future we will add good support for compute intensive jobs. diff --git a/docs/documentation/concepts/limits.mdx b/docs/documentation/concepts/limits.mdx new file mode 100644 index 000000000..1bb7b0d0f --- /dev/null +++ b/docs/documentation/concepts/limits.mdx @@ -0,0 +1,116 @@ +--- +title: "Limits" +--- + +## General Limits + +The following limits apply to the Trigger.dev Cloud service and users of the self-hosted version of Trigger.dev. + +| | Hobby | Team | Self-hosted / Enterprise | +| ----------------------------------------------------------------------- | --------- | ----------- | ------------------------- | +| Team Members | Up to 2 | Up to 5 | Custom | +| Projects | 1 | Up to 5 | Custom | +| Jobs per Project | Up to 10 | Up to 50 | Custom | +| Runs (per Month) | 5,000 | Up to 1m | Custom | +| Run Log retention | 24 hours | 7 days | Custom | +| Connected Integrations | Up to 50 | Up to 1000 | Custom | +| [Tasks per Run](#tasks-per-runs) | Up to 250 | Up to 1000 | Custom | +| [Concurrent Run Executions](#concurrent-run-executions) | Up to 10 | Up to 10 | Custom | +| [Maximum Task Duration](#maximum-task-duration) | < 2m | < 2m | < Deployment Grace Period | +| [Maximum Run Execution Duration](#maximum-total-run-execution-duration) | up to 15m | up to 2 hrs | Custom | +| [Yielded Executions per Run](#yielded-executions-per-run) | Up to 100 | Up to 100 | Custom | + +### Tasks per Run + +For any individual Job Run, the number of Tasks that can be executed is limited to 250 for Hobby and 1000 for Team plans. This limit is enforced to prevent runaway Jobs from consuming excessive resources. + +#### What is a Task? + +Tasks are the fundamental building blocks on which the Trigger.dev service is constructed. You can create and run a task using [io.runTask()](/sdk/io/runtask): + +```ts +client.defineJob({ + id: "task-example", + name: "Task Example", + version: "1.0.0", + trigger: eventTrigger({ name: "task.example" }), + run: async (payload, io, ctx) => { + const response = await io.runTask("task-1", async (task) => { + // Do some work here + return { foo: "bar" }; + }); + }, +}); +``` + +Tasks power the following features as well: + +- [io.wait()](/sdk/io/wait) +- [io.sendEvent()](/sdk/io/sendevent) +- [io.backgroundFetch()](/sdk/io/backgroundfetch) +- [io.logger](/sdk/io/logger) + +Our integration clients are also built on top of Tasks, so any time you call an integration client method, you are creating a Task. e.g.: + +```ts +client.defineJob({ + id: "send-resend-email", + name: "Send Resend Email", + version: "0.1.0", + trigger: eventTrigger({ + name: "send.email", + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + // This creates a Task + await io.resend.sendEmail("send-email", { + to: payload.to, + subject: payload.subject, + text: payload.text, + from: "Trigger.dev ", + }); + }, +}); +``` + +Anything that shows up as an item on the [Run Log](/documentation/guides/viewing-runs#run-page) is a Task: + +![Task](/images/task.png) + +### Concurrent Run Executions + +A Run Execution is a single HTTP request from the Trigger.dev server to your endpoint to execute a run. The number of concurrent Run Executions is limited to 10 for Hobby and Team plans. + +This does not include runs that are waiting for a [io.wait()](/sdk/io/wait) to complete, so you could in theory have 1000s of "In Progress" jobs at a given time with no current run executions. + +Going over this limit does not abort or cancel runs, but it will prevent new run executions until the number of concurrent executions drops below the limit. + +### Maximum Task Duration + +The Maximum Task Duration is the maximum amount of time a single Task can run for. This limit is partly enforced by the Trigger.dev server, but also by the execution runtime of your deployed serverless function. + +For example, if you're deploying to Vercel and using their Node.js Serverless functions, the maximum execution time is anywhere from 1 second to 5 minutes. If you have a single task that can run for longer than your maximum function execution time, it will never complete. + +We will retry tasks that never complete due to a timeout, but if the task continues to not complete, it will be marked as cancelled and the run will be timed out with an output like the following: + +```json +{ + "message": "Function timeout detected in 10s without any task creation. This is unexpected behavior and could lead to an infinite execution error because the run will never finish. This is likely caused by task \"initial-long-task\" execution exceeding the function timeout" +} +``` + +See our Next.js section on [Deployment](/documentation/guides/platforms/nextjs#deployment) for more information on how to configure your function timeout. + +Additionally, the Trigger.dev enforces a soft-cap of 2 minutes. Tasks that take longer than 2 minutes will be allowed to complete but we cannot guarentee that they won't be retried erroneously or cause your run execution to be locked for up to 4 hours. This is because of a current limitation of [Graphile Worker](https://github.com/graphile/worker) and our deployment platform. + +### Maximum Total Run Execution Duration + +The Maximum Total Run Execution Duration is the maximum amount of time a single run can execute for. Runs are completed over 1 or more executions, depending on many factors like the number of tasks, the serverless function timeout, task errors and delays. The Trigger.dev measures the total time spent across all run executions and will cancel the run if it exceeds the limit. + +Hobby plans have a limit of 15 minutes, Team plans have a limit of 2 hours, and Enterprise plans can set a custom limit. + +### Yielded Executions per Run + +You can manually yield a run execution using `io.yield()`, which will exit the current run execution and schedule a new run execution to continue the run. You do this at most 100 times per run. diff --git a/docs/documentation/concepts/resumability.mdx b/docs/documentation/concepts/resumability.mdx index e6e9b5c84..675cfc0f8 100644 --- a/docs/documentation/concepts/resumability.mdx +++ b/docs/documentation/concepts/resumability.mdx @@ -8,14 +8,14 @@ description: "Runs are resumable by returning Task stored data" ## How does this work? 1. When a Run is created, it is given a unique ID. This ID is used to identify the Run. -2. [Tasks](/documentation/concepts/tasks) have a `key` which is a string and is the first parameter. This should be stable and unique inside that `run` function. +2. [Tasks](/documentation/concepts/tasks) have a `cacheKey` which is a string and is the first parameter. This should be stable and unique inside that `run` function. 3. When a Task is completed, its output is stored. 4. If a Run exceeds the timeout, or your server restarts, the Run will be "replayed". 5. The second+ time it is run, Tasks that have already successfully completed will immediately return their first output. The code inside them won't re-run. -## How to use keys +## How to use cache keys -Like we mentioned above, we use Task keys to determine which Tasks have already been executed. They are defined by you inside your `run` function, for example when you call `io.slack.postMessage`: +Like we mentioned above, we use Task cache keys to determine which Tasks have already been executed. They are defined by you inside your `run` function, for example when you call `io.slack.postMessage`: ```ts await io.slack.postMessage("⭐️ New Star", { @@ -24,9 +24,9 @@ await io.slack.postMessage("⭐️ New Star", { }); ``` -In this example, the key is the string `"⭐️ New Star"`. This means that if the Job is interrupted and then resumed, the `slack.postMessage` Task will be skipped because it has already been executed. +In this example, the cache key is the string `"⭐️ New Star"`. This means that if the Job is interrupted and then resumed, the `slack.postMessage` Task will be skipped because it has already been executed. -If you make multiple calls to `slack.postMessage`, you should use different keys for each call. For example: +If you make multiple calls to `slack.postMessage`, you should use different cache keys for each call. For example: ```ts await io.slack.postMessage("⭐️ New Star", { @@ -42,7 +42,9 @@ await io.slack.postMessage("🚨 Critical Issue", { If you are calling a Task multiple times with the same key, it will only be executed once. For example, if you call `slack.postMessage` with the key `"⭐️ New Star"` twice, it will only be executed once. -## How to use keys with loops +See our [Task concept guide](/documentation/concepts/tasks) for more information about tasks and how they are crucial to the resumability of your Jobs. + +## How to use cache keys with loops If you are using a loop, you should use the loop index as the key. For example: @@ -56,3 +58,8 @@ for (let i = 0; i < 10; i++) { }); } ``` + + + We don't currently support running tasks in parallel so `Promise.all` will not work correctly. + This is something on our roadmap that we hope to support soon. + diff --git a/docs/documentation/concepts/tasks.mdx b/docs/documentation/concepts/tasks.mdx index 288872f60..31539a6d9 100644 --- a/docs/documentation/concepts/tasks.mdx +++ b/docs/documentation/concepts/tasks.mdx @@ -3,17 +3,24 @@ title: "Tasks" description: "Tasks are individual building blocks of a Run." --- -> A Task is a resumable unit of a Run that can be retried, resumed and is logged. +A [Task](/documentation/concepts/tasks) is a cached unit of work in a Job Run that are logged to the Trigger.dev UI. -## Tasks vs regular code + + Any interaction with an external service (database or API) should be wrapped in a Task. Failing to + do so could result in repeated work when runs are resumed. + -In the `run()` function you can use regular code and you can use Tasks. +## Why do you need tasks? + +Tasks are a key building block of how Trigger.dev works, and failing to use them will result in unpredictable results. Tasks allow bits of work inside a Job Run to be cached and the results of those tasks to be reused. + +This is **very important** because for a Job Run to be resumable (e.g. after a serverless function timeout, or because of a call to `io.wait()`), we need to call the `Job.run` function multiple times. If we didn't cache the results of Tasks, then we would be repeating work on each run. ```ts client.defineJob({ id: "new-user", name: "Run when a new user signs up", - version: "0.0.1", + version: "1.0.0", trigger: eventTrigger({ name: "new.user", schema: z.object({ @@ -24,26 +31,24 @@ client.defineJob({ resend, }, run: async (payload, io, ctx) => { - // regular code, not a Task - // the inputs/outputs of this function are not sent to the Trigger.dev platform - const user = await prisma.user.findUnique({ + // This code will run twice. Once when the run first starts, and once after the wait + const user = await prisma.user.findUniqueOrThrow({ where: { id: payload.userId }, select: { email: true, name: true }, }); - if (!user) throw new Error(`User not found: ${payload.userId}`); - // Integration functions are Tasks - await io.resend.sendEmail("Welcome email", { + // This code will run once, because the resend integration creates a task with the "welcome-email" cacheKey + await io.resend.sendEmail("welcome-email", { to: user.email, from: "jane@acme.inc", subject: "Welcome!", html: welcomeEmail(user.name), }); - // built-in io functions are Tasks + // This code will run once, because io.wait creates a task with the "wait" cacheKey await io.wait("wait", 60 * 60 * 3); // wait for 3 hours - // You can wrap your own code in a Task, for retrying, resumability and logging + // This code will run once, because we're manually creating a task with the "my-task" cacheKey const response = await io.runTask( "my-task", async () => { @@ -57,21 +62,246 @@ client.defineJob({ }); ``` -## The benefits of Tasks +As well as powering the resumable nature of Trigger.dev, Tasks also provide: -Tasks are a powerful concept that gives you a lot of benefits: - -- **Resumability** – Runs can exceed the maximum timeout on serverless platforms. If a Run exceeds this limit, it will be re-run. When it is re-run, any completed Tasks return their original output and they aren't re-run. Read more about [Resumability](/documentation/concepts/resumability). -- **Retryable** – If a Task fails, it will be retried. You can configure how (or if) a Task is retried. Full details in the [io SDK reference](/sdk/io). +- **Retryable** – If a Task fails, it can be retried. You can configure how (or if) a Task is retried. Full details in the [io.runTask() SDK reference](/sdk/io/runtask). - **Logging** – Tasks are logged, so you can see what happened in a Run. Find out more about [viewing runs](/documentation/guides/viewing-runs). +## Task Cache Keys + +The first param of all Tasks is a `cacheKey`. This is a unique identifier for the Task inside that Run. It is used for storing the cached result of a task. It is also used to identify the Task in the [Viewing Runs Dashboard](/documentation/guides/viewing-runs). + +It's important that cacheKey's are unique inside an individual Job Run. + +## Creating Tasks + +There are **3** ways of using tasks in your code: + +- Using the [io.runTask()](/sdk/io/runtask) function +- Using one of our [Integration packages](/documentation/concepts/integrations) and calling the `io.integration.runTask()` wrapper +- Using one of our [Integration packages](/documentation/concepts/integrations) and calling a task wrapper function, such as [io.slack.postMessage()](/integrations/apis/slack) + +### Using `io.runTask()` + +The `io.runTask()` function allows you to run a Task manually. It takes a `cacheKey` and a function to run. The function will only be run if the Task is not already cached. + +```ts +const response = await io.runTask("my-task", async (task) => { + return await longRunningCode(payload.userId); +}); +``` + +The callback function is passed a `task` object, which can be useful for providing an idempotency key to an external service. For example, Stripe: + + + Our [Stripe Integration](integrations/apis/stripe) handles this for you automatically, this is + just for documentation purposes + + +```ts +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: "2020-08-27", +}); + +await io.runTask("create-customer", async (task) => { + await stripe.customers.create( + { + email: "eric@trigger.dev", + }, + { + idempotencyKey: task.idempotencyKey, + } + ); +}); +``` + +`runTask` also takes an optional 3rd argument, which allows you to customize how the Task is displayed and run. For example, you can supply a name and some properties to be displayed in the Viewing Runs Dashboard: + +```ts +const response = await io.runTask( + "my-task", + async (task) => { + return await longRunningCode(payload.userId); + }, + { + name: "My Task", + properties: [ + { + label: "User ID", + value: payload.userId, + }, + ], + icon: "user", + } +); +``` + +See the [io.runTask() SDK reference](/sdk/io/runtask) for more information. + +### Using `io.integration.runTask()` + +All of our [Integration packages](/documentation/concepts/integrations) expose a `runTask()` function. The main differences between this and `io.runTask()` are: + +- Adds an additional callback parameter which provides the underlying authenticated integration client +- Automatically sets the `icon` property on the Task. +- Configures sensible defaults for retries and error handling. + +An example here demonstrates using the GitHub integration's `runTask` function to create a project card when a new user signs up: + +```ts +import { Github } from "@trigger.dev/github"; + +const github = new Github({ + id: "github", +}); + +client.defineJob({ + id: "create-project-card", + name: "Create Project Card", + version: "1.0.0", + trigger: eventTrigger({ + name: "new.user", + }), + integrations: { + github, + }, + run: async (payload, io, ctx) => { + await io.github.runTask( + "create-card", + async (client, task) => { + // client is an authenticated GitHub client (https://github.com/octokit/octokit.js) + return client.rest.projects.createCard({ + column_id: process.env.GITHUB_PROJECT_COLUMN_ID, + note: `New User ${payload.user.name} signed up!`, + }); + }, + { name: "Create card" } + ); + }, +}); +``` + +### Using an Integration Task Wrapper Function + +Our [Integration packages](/documentation/concepts/integrations) also expose a number of task wrapper functions. These are functions that wrap a common task for that integration. For example, the [Slack integration](/integrations/apis/slack) exposes a `postMessage()` function: + +```ts +import { Slack } from "@trigger.dev/slack"; + +const slack = new Slack({ + id: "slack", +}); + +client.defineJob({ + id: "send-welcome-message", + name: "Send welcome message", + version: "1.0.0", + trigger: eventTrigger({ + name: "new.user", + }), + integrations: { + slack, + }, + run: async (payload, io, ctx) => { + await io.slack.postMessage("send-message", { + channel: process.env.SLACK_CHANNEL_ID, + text: `New user ${payload.user.name} signed up!`, + }); + }, +}); +``` + +All task wrapper functions take a `cacheKey` as the first argument, because they are Tasks under the hood. Think of them as a convenience wrapper around `io.runTask()`. + +We strive to document all of the task wrapper functions in our [Integration packages](/documentation/concepts/integrations). For example, checkout our [GitHub integration task](/integrations/apis/github-tasks) docs. + ## Subtasks -A Task can have multiple subtasks, and so on. This is useful for breaking down a large Task into smaller Tasks. We currently support nesting 5 levels deep. +You can break up a task into multiple subtasks. This is useful for breaking up a long-running task into smaller chunks, while consolidating the logging into a single task in the dashboard with children. -## Task Keys +We currently support nesting up to 5 levels -The first param of all Tasks is a `key`. This is a unique identifier for the Task inside that Run. It is used for resumability and logging. It is also used to identify the Task in the [Viewing Runs Dashboard](/documentation/guides/viewing-runs). +```ts +const response = await io.runTask("parent-task", async (task) => { + await io.runTask("child-1", async () => { + // do something + }); + + await io.runTask("child-2", async () => { + // do something + }); +}); +``` + +Task cacheKey's are automatically scoped to the parent task. So for example, you can reuse a cacheKey inside a parent task and it will not conflict with another top-level task. + +```ts +const response = await io.runTask("parent-task", async (task) => { + await io.runTask("child-1", async () => { + // do something + }); + + await io.runTask("child-2", async () => { + // do something + }); +}); + +// This will not conflict with the child-1 task above +const response = await io.runTask("child-1", async (task) => { + // do something +}); +``` + +### Extracting Common Tasks + +Subtasks allow you to DRY up any repeating task code into a single function. For example, if you have a common task that sends a welcome email, you can extract that into a function: + +```ts +const sendWelcomeEmail = async (cacheKey: string, io: IO, resend: Resend, userId: string) => { + return await io.runTask(cacheKey, async () => { + const user = await io.runTask("fetch-user", async () => { + return prisma.user.findUniqueOrThrow({ + where: { id: userId }, + select: { email: true, name: true }, + }); + }); + + await io.resend.sendEmail("📧", { + to: user.email, + from: "eric@trigger.dev", + subject: "Welcome!", + html: welcomeEmail(user.name), + }); + }); +}; + +client.defineJob({ + id: "new-user", + name: "Run when a new user signs up", + version: "1.0.0", + trigger: eventTrigger({ + name: "new.user", + schema: z.object({ + userId: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + await sendWelcomeEmail("🫡", io, io.resend, payload.userId); + }, +}); +``` + + + Always make sure you are allow passing a unique cacheKey to the `runTask` function, so the tasks + inside the function are not accidentally reused. + + +## Limitations + +A single task has an upper-bound on it's execution duration, which must be less than the serverless function execution timeout of your deployed platform. For more information see our [Limits docs](/documentation/concepts/limits#maximum-task-duration) ## References diff --git a/docs/documentation/concepts/what-is-triggerdotdev.mdx b/docs/documentation/concepts/what-is-triggerdotdev.mdx index 7793e5090..4be93be5b 100644 --- a/docs/documentation/concepts/what-is-triggerdotdev.mdx +++ b/docs/documentation/concepts/what-is-triggerdotdev.mdx @@ -103,3 +103,19 @@ Below is a simplified architecture diagram of how Trigger.dev works: ![Architecture](/images/architecture.png) As you can see above, we communicate between your code and the Trigger.dev platform. This allows us to send events to your code, and receive tasks from your code. + +## Limitations + +There are a few limitations that are important to understand. + +In the latest version the following are not supported: + +### Long-running servers + +Currently Trigger.dev is optimized for deployment to serverless functions, but not for long-running servers. + +This limitation will be removed in the future by adding an alternative mode so Jobs works well on localhost and long-running servers. + +### Compute intensive tasks + +Because Trigger.dev is optimized for serverless functions, it is not well suited for compute intensive jobs as each individual task is limited to the [Maximum Run Chunk Execution Duration](/placeholder) diff --git a/docs/documentation/guides/create-a-job.mdx b/docs/documentation/guides/create-a-job.mdx deleted file mode 100644 index c89c05dfd..000000000 --- a/docs/documentation/guides/create-a-job.mdx +++ /dev/null @@ -1,270 +0,0 @@ ---- -title: "Create a Job" -description: "How to create a Job in your codebase" ---- - -> Jobs are the core of the system. They allow you to run code when some event occurs. They are built using a combination of Triggers and Tasks. - -### Pre-requisites - -Make sure your Project is set up with Trigger.dev. We recommend [using the CLI](/documentation/quickstart) to do this. - -## How to write a Job in code - -### 1. Create a Job file in your Project - -This is where you will write your Job code. E.g. `my-job.ts`. - -```ts -//this path might be different depending on your project -import { client } from "@/trigger"; - -client.defineJob({ - // This is the unique ID for your Job's end-point - id: "your-job-id", - // This is the name of your Job - name: "Your Job name", - // This is the version of our SDK you are using - version: "0.0.1", - ... -``` - -The `id` and `name` are important because they are used to create and identify your Job in the app. - - - This Job must be imported in the `trigger` file in order to be registered when the CLI dev command - is run. If you're using Next.js, this can be found in either the `app/api/trigger/route.ts` file - for projects using the App Router, or `pages/api/trigger.ts` if you're using the Pages Router. - - -### 2. Choose a Trigger - -This is what kicks-off a Job. There are a few different types of Triggers you can use: - - - - Run a Job on a repeating schedule, using [intervalTrigger](/documentation/concepts/triggers/scheduled#interval) - ```ts - client.defineJob({ - ... - trigger: intervalTrigger({ - seconds: 60, - }), - ... - ``` - Or with CRON syntax, using [cronTrigger](/documentation/concepts/triggers/scheduled#using-cron-syntax): - ```ts - client.defineJob({ - ... - trigger: cronTrigger({ - cron: "30 14 * * 1", - }), - ... - ``` - - - - Start your Jobs when an event happens in another API. You'll need to use [Integrations](/integrations) to do this. - - Here's an example with the GitHub integration. - - ```ts - client.defineJob({ - ... - //E.g. When a GitHub issue is modified on the triggerdotdev/trigger.dev repo - trigger: github.triggers.repo({ - event: events.onIssue, - owner: "triggerdotdev", - repo: "trigger.dev", - }), - ... - ``` - - - - The [eventTrigger](/documentation/concepts/triggers/events) allows you to define an event that your Job listens for. - - When you [send an event](/documentation/concepts/triggers/events#sending-events) with the same name the Job will run. - - ``` ts - client.defineJob({ - ... - //E.g. when a user is created in your app (you send the event) - trigger: eventTrigger({ - name: "user.created", - schema: z.object({ - name: z.string(), - email: z.string(), - paidPlan: z.boolean(), - }), - ... - ``` - - - - These are advanced features that allows you to attach dynamic triggers to a Job. Full information [here](/documentation/concepts/triggers/dynamic). - - - -### 3. Create the Job Tasks - -> A Task is a resumable unit of a Run that can be retried, resumed and is logged. - - - You can use just regular code in your Jobs. But you don't get the benefits of retrying, logging - and resumability. More info on [Tasks vs regular - code](/documentation/concepts/tasks#tasks-vs-regular-code). - - -You can string together multiple Tasks and regular code in any order you want. - -**Useful built-in Tasks:** - -| Task | Description | Task code | -| ------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| [Delay](/documentation/concepts/delays) | Wait for a period of time | `await io.wait("wait", 60);` | -| [Log](/sdk/io/logger) | Log a message | `await io.logger.log("Hello");` | -| [Send Event](/sdk/io/sendevent) | Send an event (for eventTrigger) | `await io.sendEvent("my-event", { name: "my.event", payload: { hello: "world" } });` | -| [Run task](/sdk/io/runtask) | Wrap your own code in this to create a Task | `await io.runTask("My Task", async () => { console.log("Hello"); });` | -| [Background fetch](/sdk/io/backgroundfetch) | Fetch data from a URL that can take longer that the serverless timeout. | `await io.backgroundFetch("fetch-some-data", { url: "https://example.com" });` | - -For a full list of built-in Tasks, see the [io SDK reference](/sdk/io). - -**Integration Task examples:** - - - To use our integrations you will need to set them up in the app first. Our guide is - [here](/documentation/guides/using-integrations). - - - - - -**Task:** [backgroundCreateCompletion](/integrations/apis/openai) - -```ts -await io.openai.backgroundCreateChatCompletion("background-chat-completion", { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: "Create a good programming joke about background jobs", - }, - ], -}); -``` - -View more OpenAI tasks [here](/integrations/apis/openai). - - - - -**Task:** [addIssueLabels](/integrations/apis/github-tasks) - -```ts -await io.github.addIssueLabels("add label", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - labels: ["bug"], -}); -``` - -View more GitHub tasks [here](/integrations/apis/github-tasks). - - - - -**Task:** [sendEmail](/integrations/apis/resend) - -```ts -await io.resend.sendEmail("send-email", { - to: payload.to, - subject: payload.subject, - text: payload.text, - from: "Trigger.dev ", -}); -``` - - - - -**Task:** [postMessage](/integrations/apis/slack) - -```ts -await io.slack.postMessage("post message", { - channel: "C04GWUTDC3W", - text: "My first Slack message", -}); -``` - -View more Slack tasks [here](/integrations/apis/slack). - - - - -These are just a few examples of Integration Tasks. For many more, browse our [Integrations section](/integrations/). - -### 4. Register your Jobs - -While your app is running, open a **new terminal window or tab** and run: - - - -```bash npm -npx @trigger.dev/cli@latest dev -``` - -```bash pnpm -pnpm dlx @trigger.dev/cli@latest dev -``` - -```bash yarn -yarn dlx @trigger.dev/cli@latest dev -``` - - - -This will register all of your Jobs, they should appear in your dashboard. - - - Not seeing your Job in the web app? It might be because you forgot to import it. This will need to - be either in `app/api/trigger/route.ts` file if you're using the Next,js App Router, or - `pages/api/trigger.ts` if you're using the Next,js Pages Router. - - -If you are having trouble getting your job running, please reach out to us and we will help you fix any issues: - -- [Join our Discord](https://discord.gg/kA47vcd8P6) -- [Email us](mailto:help@trigger.dev) - ---- - -## Next steps - -We recommend exploring all of the below sections to fully understand how to create and run Jobs using Trigger.dev. - - - - A guide for how to run your Jobs. Triggering a test Run and Triggering your Job for real - - - - - View example Jobs / the example jobs repo. These are a great starting - point for creating your own Jobs. - - - How to use the SDK. This includes all the available Tasks, triggers and - actions you can use. - - - - Integrations make it easy to authenticate and use APIs. -Learn how to use and create integrations. - - diff --git a/docs/documentation/guides/platforms/nextjs.mdx b/docs/documentation/guides/platforms/nextjs.mdx index 06685f8fe..dfca5a234 100644 --- a/docs/documentation/guides/platforms/nextjs.mdx +++ b/docs/documentation/guides/platforms/nextjs.mdx @@ -17,6 +17,14 @@ View our [guide for writing Jobs](/documentation/guides/create-a-job). View our [deployment guide](/documentation/guides/deployment) to learn how to deploy your Jobs. +### Serverless function timeouts + +If you are deploying your Next.js app to Vercel, you may need to configure a larger max function duration. By default, Vercel has a max function duration of 10 seconds. As outlined in our [Limits docs](/documentation/concepts/limits#maximum-task-duration), the max function duration effects the maximum [Task](/documentation/concepts/tasks) duration. + +So if you have any tasks that may take longer than 10 seconds to run (or close to 10 seconds), you should increase the max function duration to a higher value (only available to paid Vercel plans). + +See the [Vercel docs](https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration) for more information on increasing the `maxDuration` for your Vercel Serverless Functions. + ## Middleware Next.js Middleware allows you to run code before a request is completed, and if you are using it currently in your Next.js project (or you add it later), you might need to guard against altering requests to the `/api/trigger` endpoint, which needs to be exposed to the Trigger.dev installation (either your self-hosted one or the Trigger.dev Cloud). diff --git a/docs/documentation/guides/writing-jobs-step-by-step.mdx b/docs/documentation/guides/writing-jobs-step-by-step.mdx new file mode 100644 index 000000000..438049da0 --- /dev/null +++ b/docs/documentation/guides/writing-jobs-step-by-step.mdx @@ -0,0 +1,382 @@ +--- +title: "Writing Jobs - Step by Step" +description: "Best practices for writing well-behaving Jobs in your codebase" +--- + +## Pre-requisites + +This guide assumes you already have a project setup and working with Trigger.dev. If not, head over to our [Quick Start guides](/documentation/quickstarts/introduction) to get up and running in a few minutes. + +## 1. Define your Job + +A Job is a collection of Tasks that are run in a specific order. You can think of it as a function that you can run on a schedule, or when an event happens. Jobs are defined by calling the `TriggerClient.defineJob` function + +```ts +//this path might be different depending on your project +import { client } from "@/trigger"; + +client.defineJob({ + // ... job definition +}); +``` + + + If you aren't seeing defined jobs in your Trigger.dev dashboard, it might be because the job file + isn't being imported in your app. + + +## 2. Choose a name and ID + +Each job must have a unique and stable `id` and `name`. The `id` is used to identify the Job in the database, and the `name` is used to identify the Job in the UI. + +```ts +client.defineJob({ + id: "my-job", + name: "My Job", + // ... job definition +}); +``` + +We will pass the value of the `id` property through a slugifier because we use it in URLs in our Dashboard. This means you can use any characters you want, but we recommend using only lowercase letters, numbers and dashes. + +## 3. Set the current version + +The `version` property is used to track changes to your Job. It's required to be a [semantic version](https://semver.org/) string. You can track changes to your Job by incrementing the version number, and we will display in the Dashboard which version each Job Run was created with. + +```ts +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + // ... job definition +}); +``` + +## 4. Choose a Trigger + +The trigger you choose determines how and when a job will run. See our [Triggers guide](/documentation/concepts/triggers/introduction) for more information. The Trigger you choose also defines the type of the run `payload` argument (more in this below) + +```ts +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + trigger: eventTrigger({ + name: "my.event", + }), + // ... job definition +}); +``` + +## 5. Add integrations + +Integrations provide a convienent way to create and run tasks against authenticated APIs inside your Job's run function. You'll need to pass them in the `integrations` option when defining your Job. + +```ts +import { Slack } from "@trigger.dev/slack"; + +const slack = new Slack({ id: "slack" }); + +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + trigger: eventTrigger({ + name: "my.event", + }), + integrations: { slack }, + // ... job definition +}); +``` + +## 6. Implement the run function + +The `run` function implements your custom code that will be executed when your Job is run. It's an async function that takes three arguments: + +- The run `payload` - The type of the `payload` argument is determined by the Trigger you choose. +- An instance of `IO`, which exposes built-in tasks and allows you to create your own, as well as interact with integrations. +- A `context` object, which contains information about the current run, such as the run ID, the Job ID, and the Job version. [Context reference](/sdk/context) + +```ts +import { Slack } from "@trigger.dev/slack"; + +const slack = new Slack({ id: "slack" }); + +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + trigger: eventTrigger({ + name: "my.event", + }), + integrations: { slack }, + run: async (payload, io, context) => { + // ... your code + }, +}); +``` + + + We do not compile and ship your code to run on the Trigger.dev server. It runs exactly where + you've deployed your code (e.g. Vercel). + + +The `run` function you define is like a normal JavaScript function in all respects except one: it will be called one or more times to complete a single Job Run. + +This means that you can't rely on any state that is not persisted between runs, and you must **create tasks** to ensure that work is not repeated. + + + The run function is called multiple times to ensure that your Job is resilient to failure and can finish running even if it is interrupted. There are many reasons why a run could be interrupted and resumed later, including: + +- The serverless function times out +- A task fails and needs to be retried +- A wait task is used to delay continuing the run until a later time +- A run yields execution to prevent the serverless function from timing out +- Waiting for a [backgroundFetch](/sdk/io/backgroundfetch) to complete +- Waiting for a task callback to be called (like the ones used in our [Replicate integration](/integrations/apis/replicate#predictions)) +- Waiting for [another event](https://github.com/triggerdotdev/trigger.dev/issues/472) to fire. + + + +Tasks are so important that we've dedicated a whole section to them. See our [Tasks guide](/documentation/concepts/tasks) for more information. + +### Built in tasks + +We provide some built-in tasks that you can use in your run function: + +| Task | Description | Task code | +| ------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| [Delay](/documentation/concepts/delays) | Wait for a period of time | `await io.wait("wait", 60);` | +| [Log](/sdk/io/logger) | Log a message | `await io.logger.log("Hello");` | +| [Send Event](/sdk/io/sendevent) | Send an event (for eventTrigger) | `await io.sendEvent("my-event", { name: "my.event", payload: { hello: "world" } });` | +| [Background fetch](/sdk/io/backgroundfetch) | Fetch data from a URL that can take longer that the serverless timeout. | `await io.backgroundFetch("fetch-some-data", { url: "https://example.com" });` | + +For a full list of built-in Tasks, see the [io SDK reference](/sdk/io). The below example makes use a few of these built-in tasks: + +```ts +import { Slack } from "@trigger.dev/slack"; + +const slack = new Slack({ id: "slack" }); + +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + trigger: eventTrigger({ + name: "my.event", + }), + integrations: { slack }, + run: async (payload, io, context) => { + await io.logger.info("Received the my.event event", { payload }); + await io.sendEvent("send-event", { + name: "other.event", + payload: { hello: "world" }, + }); + + await io.wait("wait for 60 seconds", 60); + + await io.backgroundFetch("fetch-some-data", { + url: "https://example.com", + }); + }, +}); +``` + +### Create your own tasks + +You can also create your own tasks or use tasks provided by our integration packages. See [Creating Tasks](/documentation/concepts/tasks#creating-tasks) for more information. The example below demonstrates creating tasks in 3 different ways: + +```ts +import { Slack } from "@trigger.dev/slack"; + +const slack = new Slack({ id: "slack" }); + +client.defineJob({ + id: "my-job", + name: "My Job", + version: "1.0.0", + trigger: eventTrigger({ + name: "my.event", + }), + integrations: { slack }, + run: async (payload, io, context) => { + // Use runTask with the "get-user" cacheKey, and return the user + const user = await io.runTask("get-user", async () => { + return prisma.user.findUniqueOrThrow({ + where: { + id: payload.id, + }, + }); + }); + + // Use the Slack integration to create a task using the "post-message" cacheKey + const message = await io.slack.postMessage("post-message", { + channel: process.env.SLACK_CHANNEL_ID, + message: `Hello ${user.name}`, + }); + + await io.wait("wait for 10 seconds", 10); + + // Use the Slack integration's runTask method to add a reaction to the message + await io.slack.runTask("add-reaction", async (client) => { + // client here is an authenticated instance of the Slack SDK + await client.reactions.add({ + channel: process.env.SLACK_CHANNEL_ID, + name: "thumbsup", + timestamp: message.ts, + }); + }); + }, +}); +``` + +## 7. Handling errors + +If your run function throws an error, the Job Run will fail and the error will be displayed in the Dashboard. If you'd like to retry on an error, you can do so using tasks and the `retry` option. + +```ts +const user = await io.runTask( + "get-user", + async () => { + return prisma.user.findUniqueOrThrow({ + where: { + id: payload.id, + }, + }); + }, + { + retry: { + limit: 3, + factor: 2, + minTimeoutInMs: 1000, + }, + } +); +``` + +See the [retry options](/sdk/io/runtask) for more information. + +## 8. Skip catching internal errors + +We will throw some errors internally to interrupt run execution so they can be resumed later. If you put a `try/catch` block in your run code and catch these errors, your job will not work correctly. You can check if an error is an internal error using [isTriggerError()](/sdk/istriggererror): + +```ts +client.defineJob({ + run: async (payload, io, context) => { + try { + // Use runTask with the "get-user" cacheKey, and return the user + const user = await io.runTask("get-user", async () => { + return prisma.user.findUniqueOrThrow({ + where: { + id: payload.id, + }, + }); + }); + } catch (error) { + if (isTriggerError(error)) throw error; + + // do something with your error here + } + }, +}); +``` + +Alternatively, you can use the [io.try()](/sdk/io/try) function: + +```ts +client.defineJob({ + run: async (payload, io, context) => { + const result = io.try( + () => { + return io.runTask("get-user", async () => { + return prisma.user.findUniqueOrThrow({ + where: { + id: payload.id, + }, + }); + }); + }, + async (error) => { + //you can return data from the error handler, + //if you wish to elegantly deal with errors + return { + success: false as const, + error, + }; + } + ); + }, +}); +``` + +## 9. Creating tasks in a loop + +If you want to create tasks in a loop, you should use `for const ... of` to ensure that the tasks are created in the correct order. + +```ts +client.defineJob({ + run: async (payload, io, context) => { + for (const user of payload.users) { + await io.runTask(`update-user-${user.id}`, async () => { + return prisma.user.update({ + where: { + id: user.id, + }, + data: { + name: user.name, + }, + }); + }); + } + }, +}); +``` + + + We don't currently support running tasks in parallel so `Promise.all` will not work correctly. + This is something on our roadmap that we hope to support soon. + + +## 10. Return data from your Job + +Anything you return from the `run` function will be automatically set as the run output and displayed in the Dashboard. + +```ts +client.defineJob({ + run: async (payload, io, context) => { + return { + success: true, + message: "Hello world", + }; + }, +}); +``` + +## Next steps + +We recommend exploring all of the below sections to fully understand how to create and run Jobs using Trigger.dev. + + + + A guide for how to run your Jobs. Triggering a test Run and Triggering your Job for real + + + + + View example Jobs / the example jobs repo. These are a great starting + point for creating your own Jobs. + + + How to use the SDK. This includes all the available Tasks, triggers and + actions you can use. + + + + Integrations make it easy to authenticate and use APIs. +Learn how to use and create integrations. + + diff --git a/docs/images/task.png b/docs/images/task.png new file mode 100644 index 0000000000000000000000000000000000000000..e45068d3f910b0cc13e54d30d6ed0cc24dbdef0a GIT binary patch literal 51335 zcmeFYbzD^2+c%5|f*>L7&>&qR-K8Lngn*Rv&?4QSAUSk5A|Ob2qf*k{C7na}FwDC- z=Xc-t@yPq<`##Sf_k3p9o4xj4YhCN=@3nTYijoW#1_%QQ2?62lL&gyS~%*;V)tX3+_g8uo86MgN?+fscwGG^L_9{968&MDNu*XNLnWF zuGYutmIRlHJ2W;Wlk!#9QwW35dl3fTP?3T^@_*3$))#nAh2%=8`IsHWmvOZ(mmuUb z$*WAdk9Q;+RYEFOYUM(>R$NPZ=|$sF?S+u!n90KVF_FgPBlC#uD>6S?inWDHri-Kd zg2;_BuC8Q-gGQ*UKl0K9 zoBRxv`C7CFeDlutNWY@Uo=h!a6`S_jM|H1_~#t(GiV=(c`uPTp@y$jsPa>4kGgk znn3HeXf2DYHQdg_fbZAuyb2lB$Oyica`bSJBy&^?=zI%T{{;`2I-owhdIQxBXZ7YM z-oZ2YsX#KkNc=bur!Sxr2R(Lx+@ph-zKzLIGzbH3ylQM8!pQO3Fe2sf=zUHjQyV+# zbNi4oS0ifO$ah}CYU(8991-{&{fqw6J;zT=DixYW#7mI@7B5C($%N^4|5 zf9p$Pa!0gO^NAE~w26qME97qbFXf=~%r~y7ErkgpbK`PE{PpQ;qbN308ZJ$BYKk`gkdH>Y4vP=Mw7gFK=l## zLlAGR=MeNijDK|hB|P#kH>v=na^a2<2HV7A{nqB0pp-o@yKG8JJB zKYF2|qcLGfXi@Cb8zDR%w&+>$(Zl<6;F209JoyoB9n98-{l>f$e^OuveH?v5i)Gy3 z>5WW|tE-bpd{HsAU0J#7yR7hEl-D2A3%O%Z!>huZ!VOlu0s9i~xiQnih_a7{=r&$gg61H+nGA!N z8$6YyUVhkbkkmRHpWydW7{6g<#gK1*%E`%#$pliLb-<6a3;N1?ght5g-X>_9?{u9g%7z8RS$XJ zVP4SG1zsh6k|_J+sn?~J!U=i_x&Vdp zI9sHSxaP_ZSB^dyU9sq&m@RNqJ$i_hoiwDy&2B?sLkcA*k_r7{n$tesJ;XdQojsO6 zR#KD?{gn1aM593!_QkmdJJ&eBb-ZgtyP(2gy}m#1;YZp;+H+bW$QM$3w>FQtsCw^u z&$*{_?sHc48OIy$e(rVdJtv@(-Oc68>PyZ`wPU`mjNv`fe4b&e$1WyQKjE}?q?z}s z@0Hyf!Kwy1k~|;*J-SC4@?hdoI` zk2b6}p~v3%cVlqzgS&`glwt;QU*+DzAV5R`e+zqRq=f- zn$2?CGQ~#6!ea=#%Ao>((x!A9^qgdsQ;xsDeH6=s&?DC?%Bu)|7mYmdZ6IEtbYMbu zpG;o3zUXD|hFy4ZxLf!P*g*RZ%Pme+$nTw!PTvJqtgu8kkJ07tf z&+c6$Bs6IjPq3Gm8y9sPcG{A{e$I@hEH||`%mnVn>{d+W%}lfos$G zOd}tYT4(z_H}^I;PQ_Lb>8oAIU4j(8s3>T$sE+q6@6n?a<1|v5fMN;NNk=JnK*A#D zuISW{Sxwx2VpUV8^b{MO1=a`lKd`}3yB~_FLwWjSmRFZ-9?wtOU%D`?mek@QQwG^N7R7CoUZEb zU*aFqlcjy(jp8~G7o-+p^3uE{RwBqt=8F8@|D}6ly}Z_{mc!M%QKM16na>9mVUg>y z7Se03J+IAD;a=fY!BP%GVg2s@{cCDus-nQ2L-sqD73?*-)vA81Ibr>D82o!~6A}RQaS7 zp|dl*&S*k9yLGxgt0X&qpZ+VE78Hc4PQhzt_NKCLR^M|2&LzM9=zrcHI?i_as)I$y zU{_23<+UlN%fW>~=T-gb#CN(RdBKEwwz=A{T7}x3%XbT_LYm-0M*Rb@Xx5Q|PQ}+kd+ov&EUUNeI2Nv*0^iD<^-rcjd3Jet z8Xh>a6+O42t7>W(aW_$UIany3bwjF@~+ zD9UN+F*3M?;2*bdWfy(p~QID0>^}4mAup z+{u|#nr#{u@f-)Q-eK+NFi-XNr0GHrLotf3fLEN?$&ad^R(cco#S4v5&e<^fy|oG* zw;KI9_Imh5e`!NSB{V6mG;T-zdkdUS39?C$*P0Y;4V{r>4}-1?>n8?Kv5z z%qffS_YgBPAv z`jid0gU!Ls*3r}H5^ReN!i%|V>*id55NBN@IM>>^Pn)t4e9?_ioe(S zudBd9i(&|K{5R7?F_d4<+{hi7#WQ7f;0Q?BU!MWsFC*}~IRZc4H^YM#M!=C(_L+pb z>z$oBmsARkCbVA$7@LI1D9Cb{O&I!uccKco9~S%iGQJpM(X}8(u?WE!{^}b`MgF-> zQVktNk`RyTm|m3t`M&@0y9F-$4i~;XTV9xL`JQe1g=cSXs(`~X9Aal;PFrf1YGw_+h{AU_MTj`?JkgoSq7NEOqN+mUkqK$NWLJwTTaI@ZBgCyvAyRkOxlxw(HPL>+l zA3P`4m{XGHDP59dX%8UQ*Wy0w6uZjm_qP9b?@uxF3sBE|{-J{*-Id1uY*USARi2}X zpaEKMH3qFzziV)Pm|(n7H9^$Zbh-7l>_|z75XO6pc(LvPDn@^Wi~>;0p4U0 zfrl~=)^u!xP2;t5X}}B3UfIT-;q7HiYPoG~!T6!@LrAA|co-$(ymR$=Q9T>jTEm-| z;SY@k-$iBvaq~L2XzDp>SXUl&Jdf+)XkhIZh3y!%#Pgv*s;(T5A%BdYNrW(@H4CE6wX^S{esqua7iw1+9j$`kn@~rf|1w)*w*0isObKmx;Ppw zu80QN^oStV^RzE)JnVMhfwfXz}TU8L*YooiSd^ zD45zEbS(I;*}9>R_n=>BZ>7P0M3Jw3db?0<2Zvmc z+uQzfv)y}oW3Is^j1lr?XE;axm8i{hrB;ojlKY7O8!(%b8RZk@^DgD3>KR-46sP!0 zr<9QdL9A)&KSZ2Lg%+8Il$$<-TkeVC{L{BxbQN|CubwvS|G+ffF7dA7kJV38Dp6VQ zp+p?#X&D9(9?sRCPFpP>XCN9U2l#jwJdbw9){na#-Bgmo`3Si64`voRa#L)R`J#$^ zE_w_)cr*?4@>mxvf}2-twDJG6;a}+dlCeSS_r^*LT592^GseGEKIG|i3Q@0A@ge{GA-c^D3dokgUJc=Ru7iGgR0eny`6 z*?DuxG#$S_PO)o#q%3lxKC71e({v>}qW>iW#}`WOwbj*aQrlW;C6U$G!%qU$I-fWk zxC{QvmHlIx#j(&w9S3W@nY4s=YnP2q#xx_a0VW22^&U)ADnPh9<$S!+?Fg}J*HiMJ z_RAe(`NOf92aAD};d>J)jk!v+i23|#j<{)c=|6kVtU|Tq1uHD*HU4^!nN0R%Ei&$X zhvQDe0^FAD^aG_j8ER5S%D4qh;s+ej+J5xTTolOW?1+*OTXD;7edW}Ks-gkNW>Vr7 z=`4Dz5+h-ptmr2@JJ8ulq^2vmL zPk0|RW9`_~)<#h27W#NlET4iEVro7Is~yszdIyrddy)3?<3(9UoLS_&*V)|BT%yjP zmipD1bp1GICk1H_Ta9j-R3KtQ#(uBq%*+&64sy8)Ll@{YkGDZ`SgtTGxtO=7^{eDy zK}A8X5_z@DrzJK8%72dhMgo1*WKc^?xN*CqOa&Ia$>pG2T*msn8$3e$&Knvb5=%jZ8wA>g5ZSgrJ@49NN zR9bMqQp#@}R<$u;uF4Pl!+ZFOTVVo2yN(XYbe=VzHT_uYAhioqQxdHM^sRczEQNpX z>S)`uvrHIFChYolRvUxqDA>fTFPTsXTB+8qnX-)*zn<=(#Hr&1&pa5@G=QYZLs#T6 z==i2xmp%D(@*30iWXU{Px_+~thtUo1U z$uF3&8aU6`)VOZ86IXny1B_)#G)@G`L-^$hOY#ewJUfXso6mRIh7y*L|KFU$JxCXW z;^lCWPL049dgXN8GT{#wHUDS3T}6^ zShn+l>FlV1yplM^PU-AAZ(nTI(oD`inlv+w{)D4`5k7V{R9^@iOJS*`E|(+t0m+j$ z;g&Jn_3{IM2;I3w+5CNvPbFpw!M0Iqk~)LIDl9ecS6raZ(hC3))KHR zwO1i5MiqU$Ed7e47Bnf`)0_qcoAUD?d2R_hUgK%Kkuc`ro2#;#U~SZKsv28b9_Aw& zDeSjvJdmzPCgXqmrtp;|L(`VUuAH9D2!8S<$ zz9Rtku+HPQh2{Wq3b^^_z}w8UK9D;cMZ7?}Ei!2aa)i^2CrZ{iWv&pq zIg;Bp$`m6vGeQhLTeku{X@{-IWvpZe1?F+C?k#TQ+Yk09PTg&Z!UrR$MEw0VnPu`d z=|kC?h$T0rBw;2qg$xjrb{db>tCzMP{uF#9zb#Zj$BoD)+1Y%DgpKJUkD*^}_cPi? zyE2GjM^?toD3Ce#V|IwiPRfY@YS>7w5*~Yh3Bwn?Ta^6-MRlY0MZ)g$U;J)M=YYK$ z*c!8(nj^vi^~gL`y1c4L)CaYLZ;4IC-+ae(WQW-9%vZQLFJ6e0mjSusv0{fP9LV3XwTN4*SELbUpDRt+dP_i4!gw?qW69f&l|pC7I8m; zY>v=(`X5mCwI(Tkoi>s%&Ul^>5>74@Fp-;buY~9p>x9Y_*&-_9uoiZHynLo}HOxlD zeM(xYFkUy%5n*$nWO<9VyJ5QfnI5Z^$*c_GOLLh!8#-v)Z<6j0Ii?!a2W%$vq^q9e z7Jg%@h!55xmFb22DOTbhgt*+*6AatUx2zDW_#KI5`BD_x-UCxg0TczQWFNb4Yn|vcAPj^ALz^s)WXq5I>{m?X0aowZS7 zxp0%yd#3@@`mr&bQUQY$#?H%V=Cq-Ui1fOJH#SOdvampTiWHRiVzAE|lv)wmRCNE_-8dXx z758qYytXcejM;BP0--`KyJR9T4z6`SBG!xPaG|#985{hs#r1Nmb!~W2l6gQL_E_Ho ze&TAqVr;*vG11nW#HF(zg4vu;CwJxqcbYIE_TI>PXuoA4mXW74>)rzyDlr(5i4SKE zaFU^L>lN0^=3zlB;<&WEGq^Gu4+v$ie+wy|lr0p#FT;VW3mrV^2tV&dc_AfyRrgBEJVnRY+%S5GT2> z>`vTQ&(46nqNBgTGkaqsJK{lvsMn>5;Sqo`7{t!}JGinmpjs6Z+C7G785cQ~_hUCK z$CDggZM@p9%`CffY5Bfmbmf(f4WWZan!7hD#KLi&yk>#u?mgC5vvZXeBTEL9-Xv=D z%E9f#l(R}HD1j0A{B;|Z#RLzH?X4YM2$WFpo!th@{tCGnt2aSz%L13eF80izDF~@BJ%Ga5V zBQ|MyMh&+1Z3c4B_pOVqfyCwy9zA`m+9W4mkn*FWBmRPrJKbehyX<7#po=|CKUDN0 zc4Xz$#|C+okYym0{#YlF;79`(aL ztdItTaG|RWe}FZUwUt%RVGkxK1=foK>5Frq_}wFb$^C5PG8|^yXUHo;m6N$SZJQ3J zC(nF8RCq}k@a($KFtaiRrk-fCz(EpW0Dc4)q zYm9IRU^~f648Z}Rz4y@rk&}jjP$M`;5pgJM`HM{rXoB&%6i&M$-`vxQ`xfI`#wX_s z2$4qa{4V11iDog_PKE1XSKe|MmycfZ&m4nL&+M`~TF}|~+HTo;`a-WR_T@Eo*`!fu zyAR@wqHL2s#biOubGL4Nso2mb43PS~C-5!JnsD;!1>SKTySeD+39jpDE=$Q)#So9( zrjjpe7RE(Y<6)H7zXr9;l1z<;p2K9^J2qj-HF45~`aW<7A$bqXrMIlJ9YBLLxY@w0 zjQ;`|{dlQMTPIu~)CN<$>E4HB=kUG>nF(9H<-EMKq?4wy%rciv!fis$*8=ed_Bk7) zWon-1i(<>03>2aF@kOo5V5eJ-cVn5j3&pk_jlUb_TsnAoxqAa>jq_ob7?9h?p+X>i z#dF`F#O(&>Kgx6VMCL(y^}Q6-+GOWv(wDi5OnKL=ALHIv9?}KG42T|?yo#j<%f>|R zl=mpDYJ@=R6o0+@ez6{}FGZ<3%CYcbLTvXOe<`)XCyj8RsOZq)2s-<<4#0aH&6gp3 z-N`~brHEim4q=XzIUzeSgFHLh^mp9@OG}~s5p30y!W_jF&3nx)ZHVjhFM_5Pd735q z_Rv8VAWH8Izm0_!184J+#zRX_hX^|jvd!^tMa1n)v-ODP26<}DflsUf_Dr=q zL)kJf<9Sc#&0B7K$)`eLa4M&(e@}l_xNI|)bF9` zblkVu_Z^3koPDbY^9_4I^slxQ+Eb#^Z6#=)H1tReVG#yfSuW5+fnecQuV!*KF?#@J zgTRH?w0jglQiYZZz7_|&EO?YK+F8>ExcXd==uNqh0gy<+@k<$m0nthhJSQGR!u0L6 zZQbgyEs?v8{^`Tv!%R+cx#Y|6;}A*guXdky-I7?pCkHz)9NCN#g3NPY;y_GHxNx!sfG{e|lazO! zy5_{mm1G!bh6&CY4>WD{Qn7w54Zau7KoWbU-fuUha(g~Mg^Q_ocgKDEC}>K zZmuJJH68FX1f51v?M}mM~*ORJ=5W$*RPpQJwy*-)SL&wJJ zay=$%;q5}Mkfj?(`r_gwRJwk7rN?MY!@V+{fJ`w^Ha*!Dnaud8gpcHN7H~twpwScAI#KHy?uD+=2#`cwH z<{e>L*Kg9>cXu&#eDk||^ht307<4?Qi37ppsQ;|=HlBIz8kQ2ecgK6(_wI6jo|*C! zwLHJ!es7qUP`JS5&%*Olq1`Z`Rw6LNjsqG2cQ}pDy$QGa9FmuMH8Kg{z~v78B4<~% zOEsFNGZt+|mmS8h^Nwmr4&wxxMG`ew;SO*oDPw5WM7Q%-EYJ}zsg7OcfmsetPQN!) zV2lL^#1ascujH1QtZ(YGnUkPqpSGIRA`%7Zx9+c@ahcw<${9)%>zO9Km|V6Wln5lS zNjOs;)GD3)SDXZud}AxI9GsW_P((hXQ~sjcU=QXXeEbW}nKS>UJpMf{H!qbo6WBV_ zvVExz5Icsxy-d!%Ng&1{UJ%>(X8j}D^VravwMgvz*N4fnV~Ih6x2rvB%Hgb7Y0!8m zh4*;`sX`U8fzbi~f&X%a`zp18+o0BFgWrWy_lNEjyE-0^CP8xf3g!j)x}kQ}!E$G3 zZi-EkjppV0muzpcg(;tf9Thi9=CLC6c;$OB;j68IEMq-Ayz?fZiTw6P%{mMK)c#%g zct2=KtJ!{`$){hRPZ>t)>9#B&9hiH_km1S=J2koNU!FC?q^vw1&e?Z{YFV@s()Rsq z`hJh}*+j9PD%X_tuZ8(Dp(*D}nL#a;KUPTTW`!;d<{3{Xty-3v)qb5HOhzn?Y3heX z(SGa{6LW3he+B&rsOEP-8FKx@Hp?-Y!K^LqH_>U8uZCX_?vupVN**ECVHbiN$X7aoTONwKMx)phTBu6>b6Y!D`L zHrudtk^gqP#F`L@1Du^3lQYW%Xz!0aC&fH0SBksN2Wl$m3)iRfW$P3RtO2DTKwi89 zqC(6lkh;cy0jS^wWB_L#nJcL~tuGrX_IS474PXD}dIXtRD(Kie))6ACK4zTJU!Mu~~iuvkS%EUzXQfJp5s4ebXkjyLxMDZ-;@DMLApJ4_Zr z6}U73ce-QF0A~I2hZo>@!XaTSARR#=viuVv#02wR4tq4R%mCmSS8pq-Q6X5RJOsd^ z&(KnOF}k1arrGS3Z|@#lZrgEBE5SdE-GemAQ8Ia1ue54cIKa#nr_9qH z&Dg@m3T-_efO<08Z@liT*O|ykUA8C6mrQ$X3e?|0pUgQ7ZX#}WSo^9`o?i1KdG;zt z-M6P4%h@;xjZVXXrn3kRn^HqwzD|3Y>&*VLwH-jNRIKCxxL+|Fq&9b$lb;P*ml<^Z zb%q^88=jaZo;Q&>vONf4qoweEa(R*h?}k1yPbZWLyk`IEJruCs9!afZG6U=gkV!os zQDf$Hdyi`5Xk#p%MnkT#~HHvJcAUVo7$@HYQ^!C0x+_9`Z#vtvwC zH&yW(sOp?)c{H6EPt3K%_1!%#xfJ#+FTTmswHolb(@Kb9QjAY2oh`3UL@Qm=&zxf@qSuX1G_<`1($i ze$4J@zC&5Y(*zcF0gicB_}HhyI#R%8t}vqpVfE3^&Ax;j(Sj9fVvv6j5Ull_A1p5q z$xyO*i5&!NXtx2>=mRC$>)58NLl(}Z zC>c4!dCaocn<>5)L@Pik+pN>PH}RoV^{ikAgfs(M^K3Bj$DoYdNMYVSH`^wN=|v-g z!w16QeSlfE8cFSB*01DLa37==c{XVrk!J?dyS^y#X}`~KwH|lTL9f$AT3|N52~^k_ zn#;%Hwbd-tm-iQO+P%&YJiv~P10bC3Gj2s(zI*sISMGgBShv{k+<%jW`yv}(S=8$k z+_ta)yxqk4?pYd4>BEpSY)!xH`^OZMY~miAT+RHEun}Xi%hJZ$sTp9AHrD9Ch3Yj- zfK+P^CQ5_!J%>=sfC?!0TNkI$l4f`C)n%c>7SXjwiKn_@J4VhAfFxHMN7kexT}#Wy=l=Jf~FI9gvGZ%$bauN%BI{G3@_ zwwjn70r;dVv-ZqwU`LV+nfoN7?IzL83y#MCsb#a22=n(}o;)f8(4Dx`>x$1=O+W#4 zJ>5m(vv&E2s+b8(8Tzs2(2j~%H+wfK7@wiT$EMcehg=-d)FuoWUIzy|A4pVgpNzPF_i;+0X(_U9+X<_J?} za?68qo^dS^QgVL#rO!KKGeA~imtIl^Z1QUF{(vTPMlomHn@C(S5xeaJvri{Y3&f9S z&Dq^9s<9>NXgcg>$rNsoQ|lDw@*$dk}J(TqQE%7*WcWZ>=2!Po4Ls`09^0H0zyupGOEtI!bo$B%Meuj6{fnl|n?ml)HJ2mWpCqEZ)&n}-iV#P;jS4TIfH33parZXz zzXF}=z#ne>SzAEzs|G5-*06y=^4rLF0y_OFhD?4N6{kQo!v^uF{_ekp`i2|qK&K}% znYV4moxZ7!L&c-SIXm{%>QV4KvuWc3e@_0Mjjeqwiy3kBF?eam*tJIH;&erev6 z{@`1L27LvPw%bZ5fCVuFwGgg6H4iC zv z%O(qPDnN19@WyXjEN&e@#jFJxZmFYEMFOkkaR0XbI4H@$-g9f-H2mKKt-ojT6IiV} z|9|CUe=F4gvhCmE^?!nGde1tQ(u|{?*FWp*PrKQGtx>5il-Hjsz-6YCK{1l62m!p& zgi`16XARV=ah@HfjVbl{T9 zCS_1gRq-Gn%SNU7Rnsh`v3Z+9jwqFz|h zl5TIalEJ*UF*_Zc6MmQd%p4RH$Praiv)V+HdC3ltiRM+$L~=67QXA*>R~X=A9ABU; zSoazdy{}rR--}^K5p$^dT_4$4slcgdgP#7H?t1QqNsM%;N}D`e@u0EV4WQluvMMI0 z^trNc+@Jjta{0BmS(>%^94|^=no?jXD-po8U)jx_@CD5`xoZkDa9qJ=?AmASnv$T! z+^(xymYWisZig$bhX+w^aRTGOZnqE!-wcglYb=pmqwI3J7IfGqyrJ|ytxWV!=E!IqUG5Lt%|FZSb5(Z4EwAkK0;isi!CSR zWrQhrM%e9NUG3Qnf0jKM#gE|8Dv?hpE?ewKG4ZjAh`(OzPPp3JT}{wu)jRrdDCJa* zR?=wsfc4~{k550sr}5})Nh_JrYNRia0ET4~MscDLM#)#4)ym1?t=HfjikJp3-=-lNnJnX6yBK<2hxJceJzDOYX-|=^bf)iBK9xiKaVShcUbKL-Pg=y{N1mH~#l*I! z1$@$LplRvoKK_!Xn2=kzv+9=vh)8o>T+7u`V(o4+OQ>LHy6d~+tFxftPo?u6Rwd1N zQUDPCD*1f}y!i}n4O`pxAs&eEuGWW6HO%;UtxtXifQgC4f$OmWQY;~6-a%|k`1n^u z(Yjb@`@IK2AFyFBk2i*BIhe;c_bks1_9D=BTWT1;wnV+(zYCNp9=tZ2x(qCbuggS) z0Pt+}QrXoW8~N38T21FNEoHt|i2>sJVA%B(%mfum|Qb9o*Rt6noeBi;~R`7hpTYR)tm0W?n;e<~$_xwvAJ3Wk7)}I70MF zGu3DOk;AdXZ=s*%%QxQlAP5{JBm{ye#2I*3Q5@;!y|JA%v{~gb%FdS4`Et3gpm_S| z9Svs!)hPAoPvv;!d^w*!y?E56&-73`m=V%KG+cf*f4%OI-tXjnR@bs{q=VD4ejuQq zzR{5jF8Ey+zFUd#St-f7Q2YM zET9al87+utkp}0B6&@CyYU^mM?TI7}i=izhCeeb7J5HOis4AYgnE+S_@6SgrTPJTu>kqPnZXY9>t4CjOj1!C_kaHR0T=`BWbxzG+1Kjam|d^S?Gs8~Plk2f^-S5?)9sSuI-BtWQuY?d&0%?{JJ!yp zmG1Ybt0BT}Cp^Do#;}F1_VOrKCNlW87#o_rJRIXj@|0&}PpW95MJ*2BI@BGkMc4PX z24Z*ZOTbrV>l{R#-sJ3vtgbc-o|q2o>K%6MQy)}wy+?WB``gzjO1%ETBgYEtoePGo zK6!~O&MgHFSwCVaaN}XY`$P2^=Sw>8v*NU;mTmj491GI>i3bNVhj5qe4@P&gKB?7c zJ_hyyAD(Ekn0}v~61_+V9~J6)DN+l#9|t{vE->h)i3W3I6JkK3Fd@MEC-J9g`q9=? z2D3i&zeIUh<_w4B9P7Dk*V<<Kv3+gBSeeh;LG0)TjpPDM|j!mHEm z0nwSgd=**C(Sqz{QG+K2uhNA9xEq?A3TTO&}n5ov}l_4JAdOZ&9R8> z)+EV&KD7#Zbxo33dH+5n>NJ>*`;!uS;gR_O_n1%%0f?MHm+B7QKff;|Q5d;D4r&t^ z&;a=MzM&5HT5p_ux>r_=xR!7GL`n&Eu=#U;TMQZSGf~oF|4FmoXGvd4V|0<{2XvQ`#<{&oIbNL!$C!MD7zUKQT1w}SXef&Z zgV;Ov!9h$U&LU=`q|-mjM);;7k}kz zf_GSzd(E`aYpX~u&nAsjojh*i3_8SJhvM2~ajJSd&y{CkKfkhmNC6$V?!im)D@&nlp5F{~Vc~j@<;M%B1D8nU zOoTnpBNr%wpMl#3cg6>*Crp*E_r1Qmw=<6&t5}?e2z&1&F1T}aVhNAaijBh_fO2uj z&$<2WT2)$)EGM1%b?a6;_gZ2sC>1dho1jPJkL{WPY^eirKim#~=LoCwVg#YBEICTa zn_044!A%k>UCZk0)A>rdW~_Z6ol~P3ZdR1fyhQ|wI&=V?E=Y3wcYu!pA??@j<`Ht* z&iAsZhlNXKH=xxi{YKQ|NCI>f@Pn)b#^z`2%_NJBRX^Wy0hPlV z>u{Epi%1wKhQ2SA_jw-HE3m)as2~=|vL!n4+sc>(oV+FGyB4=C?2PjJ6I&Plcju)F zN#}m}Y$!Ir6CV`CH?KLbGA`uco10aE22t_vN$tS5Mjqq!gQw>D|O1ndRy~&vHnhtj)bJ&;b3QSG7-(R}x$>~e9EvwZVxp%EWWtW)pMbE$mQqH zi6vI1vaVD+Y49`ME*-lVv}Dn+=U)AbvzX7pZEbQK&7+Dqn4N8kcFlt734~#TU+~?p zzDF7=5EuhiC#k&G?r&!5fS&;yiMVhL1F<$}&vB))hK|X6WPdlp(Rg<-Ak@dN3WGUv z|3aJUcHyaHy*c8+bkgL`K)^p;yz|It{a2hA`QNBACseN zyt1@=7hl6T)YfCv{rKDGM~T9B4M(@TL*9A3VUdg3dk2G58N-bZBF4+GIY~beNY-|0 zoAZ3;b%d4&L7UzY|le)2mw=UH{E+M6obu{$frXU9tcXeIQz81q9V@AV%32Y zt6`k!yEr&iZkm9#P_Q_tw)|0wwn<>YI#jx?wZ6vTO!IheuLVv+k`B2a^PUj&@}6hv zt0Mc7{<{-sXJbVi&qIhtVurra`Q88W)g$BDo$yuYsXqffWWw(A)vT%XUupp`0{qoO zdOJ2>wcE>St0#S~VFj`%HQUhY^t%zigBK)=OaPMFh~Eg}pjHRdVM)KKjNWd4DQ8W_ zvmPWQxN-sx;RxT)qR%Fotw|UmIzq<5WKmGHUuwH=o}!&O2!8SS+HR%u(oLYzE`@Bm zg{#o8b#aHfaJzi%CyVwwB9*CDe9~;ULb0JW?Yhfk$%DcNb1*ZMh7V{sKllCMseSYm z>us5$BJkH+W?j)5Po%g`7esoe1r|mNM-m@&VB?~4&OS0c_<^cgWL6aEFTLWi+paV{ zzSafbngx2 z^ImRYiy^RkWXnI@7;yd{Psa50fX+Xp@bi8vf3unr3;l8X3pUbhre+j8Q4C@x<_M9C z%K7hv2JBJKGuZa0v@sx}{CG}u(Z=wd{N?iHy^JcA=r53CHU73_Pt+34)-PP=vWsGZ z$(@3h-`^T(U_O-I% zJ0zB}`w2DZ=B!c~`96eEOmla(yGe?;vkxgiTE+0U(Bux@3+BJVF}0{0sa}q8T%Pn4 z-QFeCVl|>6QNY0eX=nV{$e6o5!QVduRJDbUatGz?i3%;Z8qN)X&R8>kk+yXcmp7&Mp)#@xX~ESVhkKrh(UH$tDW*EnA_M~Qe0tS%5-yZef0&g>XZ0!rTKY!!iA%NhtBN9zfH(boD<6D1Fyg-Yn+^qCZI0w6 zD9yQV!C`V(cM!cGWLumx>DNAY@Ymifk+}rj6N}Y8O+S=SEdGDkd&{7@wq^_TK+xcU z;BJ8c!CeCcmjJ=t-QC@SyF+jS1cJM}ySux)YZAhcW%8u@8_*bQJZ9EuC;p2 zo^y2f7(I1rSVuRR*RDnB_(|qXAE%G!>jZ?FS>R>h|6vqTFN0`&&k4$ zw^cDVs;V3PL+i>c5prKy^Ti0k*5zL9#;9pTI|{ZWd)w=93YbsY*P&cquD%FK6mW=KudPh5><;LZWjja`##PO(YoC9 zfLE2O)s>Snr6%+1p7Ze3IsVhCeBfoGFChTvP4Mj-er68iTK;TquK6N(SJ$8BS)-WL z-#24khQiH6X; zj>dFQTfBCFuq#4F9x!Ov0H-=>j|&@rb_>zi>A-A-;fPaORHQtKf45UF=Er||>|m*v zy9^@vZQw9LBPJbU8f<~w;mZI%U?e7gp-O1fp4uNcAlR5Z5E?cD>{ImrM328ew|0>L z`vt`GpE=|b?3Wq87MIS0-Bds~EJ+AF=R7}&pwJEV6zt25?jFmO#QgUteCw#+Mlr!R zL^>0Y3&%uB%8cN@VEA|Q+0_mq2W=`vRFCd5yCtKC<>lXs|I4G43A{XtJIXJEuWzZ+ z$38xdmw>KfxmffvT82C}*s(%2nfe?ANP9T^yY*a^ta|>d7Ys6awEVK=nn;@fBUqtWzS_}`48j?3>ppf3w+7}Z;5V-G_M!4 zgqdVsGLt@WSzN=In0>kf1$)ib8h|;7onPNl?d#u=(vg1z@+^Z{JGAahE;`zze}`|s zY(@J1Wzf|yK{wHY8P4J__Xsut@1mw51y`WprS}U=b3kU48kpC4C3}+b>A1eotXrz|j<$ zgDDw=OuCF}5`b@SoGXQIDj^9Q`Q;KnG9~}C+mo%>eN-q>n|l{l(WVQm>Ck9GWFp_p z<`~LtcccLGnfe$f{T=rI@(~`uP-jWEVZnlK))?3bQ7EX5u^-Z>DX|Y$3ZA3jV;o*r zpl6XW8H~ispKlG2yd+UBc1Bd0)s!wVPK<~0`bDA`{DCZ(l*8d74G?41enw6pCVsIo z5Vd3FNuDN~WAl@Cc58wAcWY6o&ohPLutw*nn^C@#PLG|p$5yZG&XLa0*d9tDr<`ml zQP+&JoOUldz-RkbGg+)GNn2w&Bco8F;yyOt`SqVukgyIJZ~}g5;2q6Q3BG3#not?D zNNCwIf(@g=H>Zx^z5cn`pDsC^#+%~;1`FU%kh#XS zvfX9xG2Ax=ADgKm#r*PRKOrU@g|K7geBUaQoKZ}J9?ecO+Q!pjuKQkr`^A=xVb>2T zLvvGChJ6@tcJ4qYiiEn0_4s_LG{R3IXwv{;Q#zJ)+3g{#s!v^}NP%Yadch*f)rhR^)dq8pzlq z;{CRUK#HxxS}f}g8VVt~SU;1Elx!ihepf+kckFG(``klbCqbY$1*xWcl|uaucJai8 z^dG?se-Xvp-2)mSUpQc4wFpbef9GX)p97o1GDn!Bh0(tqd#w{Fm>W{nW(Dj1-ylpWf3j z<;hw%IGn_E1R{w7ZDBSm{58c!nmg7RPAxnCJcZ7?_FwPij3{y!vNL%!ahDU99});a zyt;D!;YReIUY5EgV5yU>7gWJop)`c4x+8Cz?Z`cv2khbC`Mrs2wxA@H&4d5CU~M>w zK|g-6u6&@b!jNq0F6R7Tc;bBS+Ra8{yf5jZ-$w=^qWd}4P+Ba_%~SNdkMX8m|7G-h zd6T&&$CksT2G#TJVH)LfgD7QNo;VFqz{1eB4aBc|req2*ox+6U2k?)hi2wPP-bFkO zgX^_hn@h*Tfw6eij8JIoN%O;Uejpu+=#`0l4xhJMx%&g+-Sq+jgXL2`kMSZE>*8nr z{uSGrSZe1OTy7T$56~5B65SaIyZwG8=l6BgA7z5Kk zJUNU9`O9=Zh1*3~qo{jkBf2RxMm#XF!CL=t>|tDX(0o#AB7 zKyovgUva>As)ROyCZWz2L*(IlM9_Zsl6eLV?be;zZfL6fx%H#+#g&S3wOekv6VKrU zv8O=(P;>i_4%5=G%RXI&LymAoea;Z zLC~-R9@rp9vA1jd5jI|@wr~ezXx1EtV#bX){F!;zcAek z|Iaa&j?swcP_<2T2ggvu5A32Ji;;{A+0n+u+?qqQ|bhs%x<;b!VZ z)L3!PLi%lqL|5#_DjXrPlu??)Vk&cvr;+G7=M>`IKsP!Q0!!O%n%HHK<`ra5)lXOe z)CtQObs{<`Gwa0fYfNv_ALE5VW?C=K)@)6yiuYHWFH})D4$!c24&idx`MELaCu>Nj zu>2TIrB{Xxy-PbXSo>xJcWN_}Uak^s;J}xDvy4ZEd3=BfG**fdPT|cI>{(e#+04vC z5(`8;itVCXM7|C96p|nkf$#1n*pEJxLizFM51t;Ne)>TG>ShAH%V9W;(@_)z73O}P zbVTcTM2f^U+FH!ff-DQjct{X%rX#*abgy+l6$Jad=h#YtR1!zRWRn17=(<^*8KXwm zZ(%K4vQuN0sc(y3HLkcksC#P)bIyWV9FAO!N56$PDbvzwG=6EF@=Idm9q0e9T;sVW z{)UGP>%W%a_+QA=vHh)PdznFF=2SScN_?#-;9Sj%ckITq-xN|mX9h?RaJVU5AMsKs zsznKx=W)Knd15CELDYEXTJucORV^Lwqeub9V6W1b%3Or@!NGTPI7abKl9NdF`CeR_ z!nW&eT^e2n=d*u0ImL486xDqdy{+4G~bfHULP+}?ZpwyN?O^RF~cNsjfBtct)y+;@@4ppffS0Qi^($pfrglb# zyAs+bV{|ybr(DgXY|7QxfW+{b3Z?3 z1TuW@PAc^O2=D9h^Ni&Dq-Ei%8Yp-6rq0QcSuS(AG}#~Qq!QIzWS-USaIj6s zDaQ@?VD-O2Z6_M;kjjI~Etk|Ft>^ zf%F|8P+8BW;VAmjG?FCP4HG8fcsWaN{JAAK-3aPnMpILb=W$hishZpMS}ZL23i$}) z-pKB=)jwV!VKB-IDhl+cY6ikI-$&V=>hMt`i1v$qF_Vpje=6N0xGWW)v+{$l z;U5%SpFWw}SXrh9z13!HA5N!H^yrJu=Igyf6$kjZ-+~`OH@s)_7gzhaKH8(FP0-#W zc!W&+R)V!fW@I!5@}Hkjgd14|d)g8R-5BP<+A$w#+HbO8&F8s@qSMxd{Dp2`<38=<@Z6bcS6odTZKhaG9~@Q3X)jdM zvaRRznZZRx!Gy)U3yXQvpHEcGVLD?>ihOkFbzNh8k%P{Mv$*m_6)oUBlTK=OhO&zSo&(@oA3n?=3Cg zyri{i`TvggBmI6AMJj-G&<6#3=E-Gk>k6le%S0+wUJ7PNyfSC(jb^=_lMwb~NX2`f zTCZnJF=qt?ve*gXP0t3ozIjHnDYJ3tt;_A)Xd-hlGP1mAs`A_@u`Wh2@5MP%a~BDy z7%`Ca5&VsxK8auVldt=yNWoV&SZKJ%Wr$3M(s&7Yp3jF$DH>*KZ20GI8#QEehmdWi zURJ*{lw`Uu^^4r{8&zb&;fLp4>(X|O=AVq-`5_4@H4|03tYdL|!&Adx&?M(?XwkqF zb-|+MvrYTC_#TWj=nC}%Wcj=a-H4F zdFBB-+<7o6F^Ebm^PBSN3E0UD_6pu#sjVRMIHWGvPOLNxr0xPZR#Wgb{xK>}%@2S7tbRFUk=5x4olsw(+M z&9{D6DFjKbyiY<(K16&<_nQ;6daaSHsA*kWvL9=ETyQfkY@AkD*egBrCuT50aKinI z+0Vt|Qz5^^S--jz_|B=)!?DlAKLNHi@>6U|M zSQ2wbY!6)RjXK%QN^r0HT&~wgeK6zsPMYVcSkeCcK8Q$GW2pZK9UH@qL5^;6L36i+ zyjb}s&|edih>3XC>~V|nEE6VdZz?dXRKZ=ld>@Hq_-mEcEaD8R^ngb=9AWsl6X%eIe_Uc+P;hH4oa7{q^oy^y11l3$9EQ35%8^1xFi0 z_shgGm-Cbqi!amZ1l`K64twJQbfyk;L}-~~)f5PMW(x@h6#Tj!LPM8Uj&jicLIOVj zl{63gwU<(>o%!?Y;%g)q^|JIKwoyE^=&M=zx#w>HI!-Xq=~_~T~fJNw*>p7)!t=c>nIJDB-Mw!QKf7l zc`PP6Dfq+5x|GAkx&fw+3&)8(nqg8>2XG}81v%9IMfK|s`d29{bC=sGC%q3F?{=?7 zFoxp*a)+^aPk5S!!?+U-u0ll{&OyDEzmVLDWs7-q9#Yk5p-^|2tExp)t{zwB7-N|* zsX=Ri*K6;&0nZ#^biB7;M}rPRu&|%*b~JlCMdcpf7H!m6F04u1@@LW$zPBL#pX2}_ z1E2RRGA;umc`R`rtfixn_}lSJkeuwn@_Q8}KQiJc-QIlk5ZSMXoNoL) z>JRy5E3Rx7~km<7UG*Jkq_7bp%F;bcr$BtI>J3zs06!e@!mhW;U=jpmk)$)xz_W!1Zz0VA=CsksL23 zc5=&VRsW5~2WGSTRCH_^4(cRAmi6aoea*gBB9aZo{=}a!r!SRw=u1C8@*H~9u+|9O zwED2v=dq0-2mX!ju+2I{RX9JUK_mE;5E=s4FGzI(K@KPG2$yFkdY|FC2}sB>+DJ)Y zh6v2bM|f8hXmz)Zl>V9roA$;maRu;r5{r4B#FJ6sGH%J}?PLti)?R+bsJ@nh*tanU)ViG5dJ_iZrz(Y$?ZT#>@VX4mOC(5P~^w*Cr#(t}#yEp|`y zP5h$60?qv^(F=6x{t@2ieL3MaAH}}Sz7x0r$&Rm+gN}Q-{M@jb!dg%(*K)DsI=S8AfyrAnH)5 zG9kKI!r6g-tF!h~yY#>g$V)^^Z;wP4D_88=!+#HeB=Zx8|L$=SwkF%?MBg-o4%P7% zyF!0}JaLjtAmH-85wz+ns6g3Z;t-<5NiNkkQS}8jOc_Y6JA*JA~rrkhq-+;b$|Bx%XYgM4oO`?Ib zZsq=X#xfi_3wz~Bm}%X+>LL3>MSlqTq0(kb(FN*6AQ16@?TP5`j!fvSMD|iBP0xHG zPefBGBKD>1yuNBjTpOif>bCb&6{&3Kp+Xub@t7%TDtASqCc?`4_}iTKghZKrmoZ6b zUBxLIi#!-m@VM^*^gk^MU^MCscMjE&db9J^5lS_f#J>i5p>9~m)?#$za5HBUiz@r66k*CKy;y2hb20+F|?{u}9r^`2UEzsR9 z?+7kDSG$0p2N?jhpvE5Cc*$Wf)jNj!%8G}h0d zs0M?HG$3er`{}yBQ-FYkRGG+25~_*+jLhbY3}R8j1r`=?wMQjs5u@@33<0-;nbPcc ztmPZ@IoPOO#hTA)cr?feD^BhOXjBT5@9@_#0c;VbUL13|;<7EsX@48&=lP574*sT+R zODSf}ReRQmK3tpIF9C4�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 HGY|Pc39NDF literal 0 HcmV?d00001 diff --git a/docs/mint.json b/docs/mint.json index 8c1399778..597903b83 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -58,6 +58,14 @@ { "source": "/documentation/guides/manual", "destination": "/documentation/guides/manual/nextjs" + }, + { + "source": "/documentation/concepts/limitations", + "destination": "/documentation/concepts/limits" + }, + { + "source": "/documentation/guides/create-a-job", + "destination": "/documentation/guides/writing-jobs-step-by-step" } ], "navigation": [ @@ -86,7 +94,7 @@ "documentation/quickstarts/supabase" ] }, - "documentation/guides/create-a-job", + "documentation/guides/writing-jobs-step-by-step", "documentation/guides/video-walkthrough" ] }, @@ -94,7 +102,7 @@ "group": "Concepts", "pages": [ "documentation/concepts/what-is-triggerdotdev", - "documentation/concepts/limitations", + "documentation/concepts/limits", "documentation/concepts/projects", "documentation/concepts/client-adaptors", { @@ -301,12 +309,12 @@ "group": "IO", "pages": [ "sdk/io/overview", - "sdk/io/logger", - "sdk/io/wait", - "sdk/io/sendevent", "sdk/io/runtask", - "sdk/io/try", + "sdk/io/wait", + "sdk/io/logger", + "sdk/io/sendevent", "sdk/io/backgroundfetch", + "sdk/io/try", "sdk/io/registerinterval", "sdk/io/unregisterinterval", "sdk/io/registercron", @@ -325,7 +333,10 @@ "sdk/dynamictrigger/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"] + "pages": [ + "sdk/dynamictrigger/register", + "sdk/dynamictrigger/unregister" + ] } ] }, @@ -336,7 +347,10 @@ "sdk/dynamicschedule/constructor", { "group": "Instance methods", - "pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"] + "pages": [ + "sdk/dynamicschedule/register", + "sdk/dynamicschedule/unregister" + ] } ] }, @@ -357,7 +371,9 @@ }, { "group": "Overview", - "pages": ["examples/introduction"] + "pages": [ + "examples/introduction" + ] } ], "footerSocials": { @@ -370,4 +386,4 @@ "apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW" } } -} +} \ No newline at end of file diff --git a/docs/sdk/io/overview.mdx b/docs/sdk/io/overview.mdx index c44a34fc6..ae9058883 100644 --- a/docs/sdk/io/overview.mdx +++ b/docs/sdk/io/overview.mdx @@ -22,6 +22,10 @@ Used to send log messages to the [Run log](/documentation/guides/viewing-runs). ## Instance methods +### [runTask()](/sdk/io/runtask) + +`io.runTask()` allows you to run a [Task](/documentation/concepts/tasks) from inside a Job run. A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. + ### [wait()](/sdk/io/wait) Waits for a certain amount of time before continuing the Job. Delays works even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay. @@ -32,18 +36,14 @@ Waits for a certain amount of time before continuing the Job. Delays works even If you want to send an event from outside a run (e.g. just from your backend) you can use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent). -### [runTask()](/sdk/io/runtask) +### [backgroundFetch()](/sdk/io/backgroundfetch) -`io.runTask()` allows you to run a [Task](/documentation/concepts/tasks) from inside a Job run. A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. +`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints. ### [try()](/sdk/io/try) `io.try()` allows you to run Tasks and catch any errors that are thrown, it's similar to a normal `try/catch` block but works with [io.runTask()](/sdk/io/runtask). -### [backgroundFetch()](/sdk/io/backgroundfetch) - -`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints. - ### [registerInterval()](/sdk/io/registerinterval) `io.registerInterval()` allows you to register a [DynamicSchedule](/sdk/dynamicschedule) that will run on a regular interval. diff --git a/docs/sdk/io/runtask.mdx b/docs/sdk/io/runtask.mdx index 1b619e0eb..27e862e17 100644 --- a/docs/sdk/io/runtask.mdx +++ b/docs/sdk/io/runtask.mdx @@ -4,9 +4,12 @@ sidebarTitle: "runTask()" description: "Creates and runs a Task inside a Run." --- -A [Task](/documentation/concepts/tasks) is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions. +A [Task](/documentation/concepts/tasks) is a cached unit of work in a Job Run that are logged to the Trigger.dev UI. You can use `io.runTask()` to create and run a Task inside a Run. -The wrappers at `io.integration.runTask()` expose the underlying Integration client as the first callback parameter (see examples on the right). They will have defaults set for options and `onError` handlers, but should otherwise be considered identical to raw `io.runTask()`. + + Any interaction with an external service (database or API) should be wrapped in a Task. Failing to + do so could result in repeated work when runs are resumed. + ## Parameters @@ -159,32 +162,30 @@ If the remote callback feature `options.callback` is enabled, the Promise will i client.defineJob({ id: "alert-on-new-github-issues", name: "Alert on new GitHub issues", - version: "0.1.1", + version: "1.0.0", trigger: github.triggers.repo({ event: events.onIssueOpened, owner: "triggerdotdev", repo: "trigger.dev", }), - integrations: { - github, - }, run: async (payload, io, ctx) => { - //runTask - const response = await io.github.runTask( - "create-card", - async (client) => { - //create a project card using the underlying GitHub Integration client - return client.rest.projects.createCard({ - column_id: 123, - note: "test", + const record = await io.runTask( + "sync-github-issue", + async (task) => { + return prisma.githubIssues.create({ + data: { + number: payload.issue.number, + title: payload.issue.title, + body: payload.issue.body, + url: payload.issue.html_url, + repo: payload.repository.full_name, + owner: payload.repository.owner.login, + }, }); }, //this is optional - { name: "Create card", icon: "github" } + { name: "Sync GitHub Issue", icon: "github" } ); - - //log the url of the created card - await io.logger.info(response.data.url); }, }); ``` @@ -227,14 +228,12 @@ client.defineJob({ name: "Remote Callback example", version: "0.1.1", trigger: eventTrigger({ name: "predict" }), - integrations: { replicate }, run: async (payload, io, ctx) => { - //runTask - const prediction = await io.replicate.runTask( + const prediction = await io.runTask( "create-and-await-prediction", - async (client, task) => { - //create a prediction using the underlying Replicate Integration client - await client.predictions.create({ + async (task) => { + //create a prediction using a Replicate SDK instance + await replicate.predictions.create({ ...payload, webhook: task.callbackUrl ?? "", webhook_events_filter: ["completed"], diff --git a/packages/core/src/schemas/api.ts b/packages/core/src/schemas/api.ts index d95ebf65e..57dd99843 100644 --- a/packages/core/src/schemas/api.ts +++ b/packages/core/src/schemas/api.ts @@ -177,12 +177,14 @@ export type HttpSourceRequestHeaders = z.output; +export const AutoYieldConfigSchema = z.object({ + startTaskThreshold: z.number(), + beforeExecuteTaskThreshold: z.number(), + beforeCompleteTaskThreshold: z.number(), + afterCompleteTaskThreshold: z.number(), +}); + +export type AutoYieldConfig = z.infer; + export const RunJobBodySchema = z.object({ event: ApiEventLogSchema, job: z.object({ @@ -485,6 +497,8 @@ export const RunJobBodySchema = z.object({ noopTasksSet: z.string().optional(), connections: z.record(ConnectionAuthSchema).optional(), yieldedExecutions: z.string().array().optional(), + runChunkExecutionLimit: z.number().optional(), + autoYieldConfig: AutoYieldConfigSchema.optional(), }); export type RunJobBody = z.infer; @@ -504,6 +518,33 @@ export const RunJobYieldExecutionErrorSchema = z.object({ export type RunJobYieldExecutionError = z.infer; +export const RunJobAutoYieldExecutionErrorSchema = z.object({ + status: z.literal("AUTO_YIELD_EXECUTION"), + location: z.string(), + timeRemaining: z.number(), + timeElapsed: z.number(), + limit: z.number().optional(), +}); + +export type RunJobAutoYieldExecutionError = z.infer; + +export const RunJobAutoYieldWithCompletedTaskExecutionErrorSchema = z.object({ + status: z.literal("AUTO_YIELD_EXECUTION_WITH_COMPLETED_TASK"), + id: z.string(), + properties: z.array(DisplayPropertySchema).optional(), + output: z.any(), + data: z.object({ + location: z.string(), + timeRemaining: z.number(), + timeElapsed: z.number(), + limit: z.number().optional(), + }), +}); + +export type RunJobAutoYieldWithCompletedTaskExecutionError = z.infer< + typeof RunJobAutoYieldWithCompletedTaskExecutionErrorSchema +>; + export const RunJobInvalidPayloadErrorSchema = z.object({ status: z.literal("INVALID_PAYLOAD"), errors: z.array(SchemaErrorSchema), @@ -549,6 +590,8 @@ export const RunJobSuccessSchema = z.object({ export type RunJobSuccess = z.infer; export const RunJobResponseSchema = z.discriminatedUnion("status", [ + RunJobAutoYieldExecutionErrorSchema, + RunJobAutoYieldWithCompletedTaskExecutionErrorSchema, RunJobYieldExecutionErrorSchema, RunJobErrorSchema, RunJobUnresolvedAuthErrorSchema, diff --git a/packages/core/src/schemas/tasks.ts b/packages/core/src/schemas/tasks.ts index 559dc4bab..15744bfc2 100644 --- a/packages/core/src/schemas/tasks.ts +++ b/packages/core/src/schemas/tasks.ts @@ -37,6 +37,7 @@ export const TaskSchema = z.object({ export const ServerTaskSchema = TaskSchema.extend({ idempotencyKey: z.string(), attempts: z.number(), + forceYield: z.boolean().optional().nullable(), }); export type ServerTask = z.infer; diff --git a/packages/database/prisma/migrations/20231011104302_add_run_chunk_column/migration.sql b/packages/database/prisma/migrations/20231011104302_add_run_chunk_column/migration.sql new file mode 100644 index 000000000..3e28ece9a --- /dev/null +++ b/packages/database/prisma/migrations/20231011104302_add_run_chunk_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Endpoint" ADD COLUMN "runChunkExecutionLimit" INTEGER NOT NULL DEFAULT 60; diff --git a/packages/database/prisma/migrations/20231011134840_add_auto_yielded_executions/migration.sql b/packages/database/prisma/migrations/20231011134840_add_auto_yielded_executions/migration.sql new file mode 100644 index 000000000..be62509c2 --- /dev/null +++ b/packages/database/prisma/migrations/20231011134840_add_auto_yielded_executions/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "JobRunAutoYieldExecution" ( + "id" TEXT NOT NULL, + "runId" TEXT NOT NULL, + "timeRemaining" INTEGER NOT NULL, + "timeElapsed" INTEGER NOT NULL, + "limit" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "JobRunAutoYieldExecution_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "JobRunAutoYieldExecution" ADD CONSTRAINT "JobRunAutoYieldExecution_runId_fkey" FOREIGN KEY ("runId") REFERENCES "JobRun"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/migrations/20231011141302_add_location_to_auto_yield_executions/migration.sql b/packages/database/prisma/migrations/20231011141302_add_location_to_auto_yield_executions/migration.sql new file mode 100644 index 000000000..f8f1775b4 --- /dev/null +++ b/packages/database/prisma/migrations/20231011141302_add_location_to_auto_yield_executions/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `location` to the `JobRunAutoYieldExecution` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "JobRunAutoYieldExecution" ADD COLUMN "location" TEXT NOT NULL; diff --git a/packages/database/prisma/migrations/20231011145406_change_run_chunk_execution_limit_default/migration.sql b/packages/database/prisma/migrations/20231011145406_change_run_chunk_execution_limit_default/migration.sql new file mode 100644 index 000000000..bd19a6bb1 --- /dev/null +++ b/packages/database/prisma/migrations/20231011145406_change_run_chunk_execution_limit_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Endpoint" ALTER COLUMN "runChunkExecutionLimit" SET DEFAULT 60000; diff --git a/packages/database/prisma/migrations/20231011213532_add_auto_yield_threshold_settings_to_endpoints/migration.sql b/packages/database/prisma/migrations/20231011213532_add_auto_yield_threshold_settings_to_endpoints/migration.sql new file mode 100644 index 000000000..260f0fcdf --- /dev/null +++ b/packages/database/prisma/migrations/20231011213532_add_auto_yield_threshold_settings_to_endpoints/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Endpoint" ADD COLUMN "afterCompleteTaskThreshold" INTEGER NOT NULL DEFAULT 750, +ADD COLUMN "beforeCompleteTaskThreshold" INTEGER NOT NULL DEFAULT 750, +ADD COLUMN "beforeExecuteTaskThreshold" INTEGER NOT NULL DEFAULT 1500, +ADD COLUMN "startTaskThreshold" INTEGER NOT NULL DEFAULT 750; diff --git a/packages/database/prisma/migrations/20231019123406_add_force_yield_immediately_to_runs/migration.sql b/packages/database/prisma/migrations/20231019123406_add_force_yield_immediately_to_runs/migration.sql new file mode 100644 index 000000000..4ede43f10 --- /dev/null +++ b/packages/database/prisma/migrations/20231019123406_add_force_yield_immediately_to_runs/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "JobRun" ADD COLUMN "forceYieldImmediately" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/database/prisma/migrations/20231020145127_add_sdk_version_to_endpoints/migration.sql b/packages/database/prisma/migrations/20231020145127_add_sdk_version_to_endpoints/migration.sql new file mode 100644 index 000000000..3180c6531 --- /dev/null +++ b/packages/database/prisma/migrations/20231020145127_add_sdk_version_to_endpoints/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Endpoint" ADD COLUMN "sdkVersion" TEXT NOT NULL DEFAULT 'unknown'; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 7f1d21918..302be3bf8 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -368,6 +368,13 @@ model Endpoint { indexingHookIdentifier String? version String @default("unknown") + sdkVersion String @default("unknown") + + runChunkExecutionLimit Int @default(60000) + startTaskThreshold Int @default(750) + beforeExecuteTaskThreshold Int @default(1500) + beforeCompleteTaskThreshold Int @default(750) + afterCompleteTaskThreshold Int @default(750) jobVersions JobVersion[] jobRuns JobRun[] @@ -726,11 +733,14 @@ model JobRun { yieldedExecutions String[] + forceYieldImmediately Boolean @default(false) + tasks Task[] runConnections RunConnection[] missingConnections MissingConnection[] executions JobRunExecution[] statuses JobRunStatusRecord[] + autoYieldExecution JobRunAutoYieldExecution[] } enum JobRunStatus { @@ -748,6 +758,20 @@ enum JobRunStatus { INVALID_PAYLOAD } +model JobRunAutoYieldExecution { + id String @id @default(cuid()) + + run JobRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runId String + + timeRemaining Int + timeElapsed Int + limit Int + location String + + createdAt DateTime @default(now()) +} + model JobRunExecution { id String @id @default(cuid()) diff --git a/packages/trigger-sdk/src/errors.ts b/packages/trigger-sdk/src/errors.ts index 9e2aeec0e..d8fcf404d 100644 --- a/packages/trigger-sdk/src/errors.ts +++ b/packages/trigger-sdk/src/errors.ts @@ -1,3 +1,4 @@ +import { DisplayProperty } from "@trigger.dev/core"; import { ErrorWithStack, SchemaError, ServerTask } from "@trigger.dev/core"; export class ResumeWithTaskError { @@ -20,6 +21,23 @@ export class YieldExecutionError { constructor(public key: string) {} } +export class AutoYieldExecutionError { + constructor( + public location: string, + public timeRemaining: number, + public timeElapsed: number + ) {} +} + +export class AutoYieldWithCompletedTaskExecutionError { + constructor( + public id: string, + public properties: DisplayProperty[] | undefined, + public output: any, + public data: { location: string; timeRemaining: number; timeElapsed: number } + ) {} +} + export class ParsedPayloadSchemaError { constructor(public schemaErrors: SchemaError[]) {} } @@ -32,11 +50,19 @@ export class ParsedPayloadSchemaError { */ export function isTriggerError( err: unknown -): err is ResumeWithTaskError | RetryWithTaskError | CanceledWithTaskError { +): err is + | ResumeWithTaskError + | RetryWithTaskError + | CanceledWithTaskError + | YieldExecutionError + | AutoYieldExecutionError + | AutoYieldWithCompletedTaskExecutionError { return ( err instanceof ResumeWithTaskError || err instanceof RetryWithTaskError || err instanceof CanceledWithTaskError || - err instanceof YieldExecutionError + err instanceof YieldExecutionError || + err instanceof AutoYieldExecutionError || + err instanceof AutoYieldWithCompletedTaskExecutionError ); } diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index b4dacfc5e..de9a77531 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -23,6 +23,8 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { webcrypto } from "node:crypto"; import { ApiClient } from "./apiClient"; import { + AutoYieldExecutionError, + AutoYieldWithCompletedTaskExecutionError, CanceledWithTaskError, ResumeWithTaskError, RetryWithTaskError, @@ -45,6 +47,7 @@ export type IOOptions = { apiClient: ApiClient; client: TriggerClient; context: TriggerContext; + timeOrigin: number; logger?: Logger; logLevel?: LogLevel; jobLogger?: Logger; @@ -54,6 +57,7 @@ export type IOOptions = { yieldedExecutions?: Array; noopTasksSet?: string; serverVersion?: string | null; + executionTimeout?: number; }; type JsonPrimitive = string | number | boolean | null | undefined | Date | symbol; @@ -96,6 +100,8 @@ export class IO { private _noopTasksBloomFilter: BloomFilter | undefined; private _stats: IOStats; private _serverVersion: string; + private _timeOrigin: number; + private _executionTimeout?: number; get stats() { return this._stats; @@ -109,6 +115,8 @@ export class IO { this._cachedTasks = new Map(); this._jobLogger = options.jobLogger; this._jobLogLevel = options.jobLogLevel; + this._timeOrigin = options.timeOrigin; + this._executionTimeout = options.executionTimeout; this._stats = { initialCachedTasks: 0, @@ -205,11 +213,11 @@ export class IO { } /** `io.wait()` waits for the specified amount of time before continuing the Job. Delays work even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](https://trigger.dev/docs/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param seconds The number of seconds to wait. This can be very long, serverless timeouts are not an issue. */ - async wait(key: string | any[], seconds: number) { - return await this.runTask(key, async (task) => {}, { + async wait(cacheKey: string | any[], seconds: number) { + return await this.runTask(cacheKey, async (task) => {}, { name: "wait", icon: "clock", params: { seconds }, @@ -220,7 +228,7 @@ export class IO { } /** `io.createStatus()` allows you to set a status with associated data during the Run. Statuses can be used by your UI using the react package - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param initialStatus The initial status you want this status to have. You can update it during the rub using the returned object. * @returns a TriggerStatus object that you can call `update()` on, to update the status. * @example @@ -252,17 +260,17 @@ export class IO { * ``` */ async createStatus( - key: IntegrationTaskKey, + cacheKey: IntegrationTaskKey, initialStatus: InitialStatusUpdate ): Promise { - const id = typeof key === "string" ? key : key.join("-"); + const id = typeof cacheKey === "string" ? cacheKey : cacheKey.join("-"); const status = new TriggerStatus(id, this); - await status.update(key, initialStatus); + await status.update(cacheKey, initialStatus); return status; } /** `io.backgroundFetch()` fetches data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param url The URL to fetch from. * @param requestInit The options for the request * @param retry The options for retrying the request if it fails @@ -273,7 +281,7 @@ export class IO { * - Wildcards: 2xx, 3xx, 4xx, 5xx */ async backgroundFetch( - key: string | any[], + cacheKey: string | any[], url: string, requestInit?: FetchRequestInit, retry?: FetchRetryOptions @@ -281,7 +289,7 @@ export class IO { const urlObject = new URL(url); return (await this.runTask( - key, + cacheKey, async (task) => { return task.output; }, @@ -311,13 +319,13 @@ export class IO { } /** `io.sendEvent()` allows you to send an event from inside a Job run. The sent even will trigger any Jobs that are listening for that event (based on the name). - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param event The event to send. The event name must match the name of the event that your Jobs are listening for. * @param options Options for sending the event. */ - async sendEvent(key: string | any[], event: SendEvent, options?: SendEventOptions) { + async sendEvent(cacheKey: string | any[], event: SendEvent, options?: SendEventOptions) { return await this.runTask( - key, + cacheKey, async (task) => { return await this._triggerClient.sendEvent(event, options); }, @@ -335,9 +343,9 @@ export class IO { ); } - async getEvent(key: string | any[], id: string) { + async getEvent(cacheKey: string | any[], id: string) { return await this.runTask( - key, + cacheKey, async (task) => { return await this._triggerClient.getEvent(id); }, @@ -355,13 +363,13 @@ export class IO { } /** `io.cancelEvent()` allows you to cancel an event that was previously sent with `io.sendEvent()`. This will prevent any Jobs from running that are listening for that event if the event was sent with a delay - * @param key + * @param cacheKey * @param eventId * @returns */ - async cancelEvent(key: string | any[], eventId: string) { + async cancelEvent(cacheKey: string | any[], eventId: string) { return await this.runTask( - key, + cacheKey, async (task) => { return await this._triggerClient.cancelEvent(eventId); }, @@ -380,9 +388,12 @@ export class IO { ); } - async updateSource(key: string | any[], options: { key: string } & UpdateTriggerSourceBodyV2) { + async updateSource( + cacheKey: string | any[], + options: { key: string } & UpdateTriggerSourceBodyV2 + ) { return this.runTask( - key, + cacheKey, async (task) => { return await this._apiClient.updateSource(this._triggerClient.id, options.key, options); }, @@ -404,7 +415,7 @@ export class IO { } /** `io.registerInterval()` allows you to register a [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) that will trigger any jobs it's attached to on a regular interval. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param dynamicSchedule The [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) to register a new schedule on. * @param id A unique id for the interval. This is used to identify and unregister the interval later. * @param options The options for the interval. @@ -412,13 +423,13 @@ export class IO { * @deprecated Use `DynamicSchedule.register` instead. */ async registerInterval( - key: string | any[], + cacheKey: string | any[], dynamicSchedule: DynamicSchedule, id: string, options: IntervalOptions ) { return await this.runTask( - key, + cacheKey, async (task) => { return dynamicSchedule.register(id, { type: "interval", @@ -438,14 +449,14 @@ export class IO { } /** `io.unregisterInterval()` allows you to unregister a [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) that was previously registered with `io.registerInterval()`. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param dynamicSchedule The [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) to unregister a schedule on. * @param id A unique id for the interval. This is used to identify and unregister the interval later. * @deprecated Use `DynamicSchedule.unregister` instead. */ - async unregisterInterval(key: string | any[], dynamicSchedule: DynamicSchedule, id: string) { + async unregisterInterval(cacheKey: string | any[], dynamicSchedule: DynamicSchedule, id: string) { return await this.runTask( - key, + cacheKey, async (task) => { return dynamicSchedule.unregister(id); }, @@ -460,20 +471,20 @@ export class IO { } /** `io.registerCron()` allows you to register a [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) that will trigger any jobs it's attached to on a regular CRON schedule. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param dynamicSchedule The [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) to register a new schedule on. * @param id A unique id for the schedule. This is used to identify and unregister the schedule later. * @param options The options for the CRON schedule. * @deprecated Use `DynamicSchedule.register` instead. */ async registerCron( - key: string | any[], + cacheKey: string | any[], dynamicSchedule: DynamicSchedule, id: string, options: CronOptions ) { return await this.runTask( - key, + cacheKey, async (task) => { return dynamicSchedule.register(id, { type: "cron", @@ -493,14 +504,14 @@ export class IO { } /** `io.unregisterCron()` allows you to unregister a [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) that was previously registered with `io.registerCron()`. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param dynamicSchedule The [DynamicSchedule](https://trigger.dev/docs/sdk/dynamicschedule) to unregister a schedule on. * @param id A unique id for the interval. This is used to identify and unregister the interval later. * @deprecated Use `DynamicSchedule.unregister` instead. */ - async unregisterCron(key: string | any[], dynamicSchedule: DynamicSchedule, id: string) { + async unregisterCron(cacheKey: string | any[], dynamicSchedule: DynamicSchedule, id: string) { return await this.runTask( - key, + cacheKey, async (task) => { return dynamicSchedule.unregister(id); }, @@ -515,7 +526,7 @@ export class IO { } /** `io.registerTrigger()` allows you to register a [DynamicTrigger](https://trigger.dev/docs/sdk/dynamictrigger) with the specified trigger params. - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param trigger The [DynamicTrigger](https://trigger.dev/docs/sdk/dynamictrigger) to register. * @param id A unique id for the trigger. This is used to identify and unregister the trigger later. * @param params The params for the trigger. @@ -524,13 +535,13 @@ export class IO { async registerTrigger< TTrigger extends DynamicTrigger, ExternalSource>, >( - key: string | any[], + cacheKey: string | any[], trigger: TTrigger, id: string, params: ExternalSourceParams ): Promise<{ id: string; key: string } | undefined> { return await this.runTask( - key, + cacheKey, async (task) => { const registration = await this.runTask( "register-source", @@ -558,13 +569,13 @@ export class IO { ); } - async getAuth(key: string | any[], clientId?: string): Promise { + async getAuth(cacheKey: string | any[], clientId?: string): Promise { if (!clientId) { return; } return this.runTask( - key, + cacheKey, async (task) => { return await this._triggerClient.getAuth(clientId); }, @@ -574,29 +585,33 @@ export class IO { /** `io.runTask()` allows you to run a [Task](https://trigger.dev/docs/documentation/concepts/tasks) from inside a Job run. A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](https://trigger.dev/docs/integrations) use Tasks internally to perform their actions. * - * @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. + * @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information. * @param callback The callback that will be called when the Task is run. The callback receives the Task and the IO as parameters. * @param options The options of how you'd like to run and log the Task. * @param onError The callback that will be called when the Task fails. The callback receives the error, the Task and the IO as parameters. If you wish to retry then return an object with a `retryAt` property. * @returns A Promise that resolves with the returned value of the callback. */ async runTask | void>( - key: string | any[], + cacheKey: string | any[], callback: (task: ServerTask, io: IO) => Promise, options?: RunTaskOptions, onError?: RunTaskErrorCallback ): Promise { + this.#detectAutoYield("start_task", 500); + const parentId = this._taskStorage.getStore()?.taskId; if (parentId) { this._logger.debug("Using parent task", { parentId, - key, + cacheKey, options, }); } - const idempotencyKey = await generateIdempotencyKey([this._id, parentId ?? "", key].flat()); + const idempotencyKey = await generateIdempotencyKey( + [this._id, parentId ?? "", cacheKey].flat() + ); const cachedTask = this._cachedTasks.get(idempotencyKey); @@ -626,7 +641,7 @@ export class IO { this._id, { idempotencyKey, - displayKey: typeof key === "string" ? key : undefined, + displayKey: typeof cacheKey === "string" ? cacheKey : undefined, noop: false, ...(options ?? {}), parentId, @@ -641,6 +656,14 @@ export class IO { ? response.body.task : response.body; + if (task.forceYield) { + this._logger.debug("Forcing yield after run task", { + idempotencyKey, + }); + + this.#forceYield("after_run_task"); + } + if (response.version === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) { this._cachedTasksCursor = response.body.cachedTasks?.cursor; @@ -694,6 +717,8 @@ export class IO { throw new Error(task.error ?? task?.output ? JSON.stringify(task.output) : "Task errored"); } + this.#detectAutoYield("before_execute_task", 1500); + const executeTask = async () => { try { const result = await callback(task, this); @@ -713,17 +738,29 @@ export class IO { task, }); + this.#detectAutoYield("before_complete_task", 500, task, output); + const completedTask = await this._apiClient.completeTask(this._id, task.id, { output: output ?? undefined, properties: task.outputProperties ?? undefined, }); + if (completedTask.forceYield) { + this._logger.debug("Forcing yield after task completed", { + idempotencyKey, + }); + + this.#forceYield("after_complete_task"); + } + this._stats.executedTasks++; if (completedTask.status === "CANCELED") { throw new CanceledWithTaskError(completedTask); } + this.#detectAutoYield("after_complete_task", 500); + return output; } catch (error) { if (isTriggerError(error)) { @@ -818,7 +855,7 @@ export class IO { /** * `io.yield()` allows you to yield execution of the current run and resume it in a new function execution. Similar to `io.wait()` but does not create a task and resumes execution immediately. */ - yield(key: string) { + yield(cacheKey: string) { if (!supportsFeature("yieldExecution", this._serverVersion)) { console.warn( "[trigger.dev] io.yield() is not support by the version of the Trigger.dev server you are using, you will need to upgrade your self-hosted Trigger.dev instance." @@ -827,11 +864,11 @@ export class IO { return; } - if (this._yieldedExecutions.includes(key)) { + if (this._yieldedExecutions.includes(cacheKey)) { return; } - throw new YieldExecutionError(key); + throw new YieldExecutionError(cacheKey); } /** @@ -863,6 +900,47 @@ export class IO { #addToCachedTasks(task: ServerTask) { this._cachedTasks.set(task.idempotencyKey, task); } + + #detectAutoYield(location: string, threshold: number = 1500, task?: ServerTask, output?: any) { + const timeRemaining = this.#getRemainingTimeInMillis(); + + if (timeRemaining && timeRemaining < threshold) { + if (task) { + throw new AutoYieldWithCompletedTaskExecutionError( + task.id, + task.outputProperties ?? [], + output, + { + location, + timeRemaining, + timeElapsed: this.#getTimeElapsed(), + } + ); + } else { + throw new AutoYieldExecutionError(location, timeRemaining, this.#getTimeElapsed()); + } + } + } + + #forceYield(location: string) { + const timeRemaining = this.#getRemainingTimeInMillis(); + + if (timeRemaining) { + throw new AutoYieldExecutionError(location, timeRemaining, this.#getTimeElapsed()); + } + } + + #getTimeElapsed() { + return performance.now() - this._timeOrigin; + } + + #getRemainingTimeInMillis() { + if (this._executionTimeout) { + return this._executionTimeout - (performance.now() - this._timeOrigin); + } + + return undefined; + } } // Generate a stable idempotency key for the key material, using a stable json stringification diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index b1766f164..bbb5e8b21 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -33,6 +33,8 @@ import { } from "@trigger.dev/core"; import { ApiClient } from "./apiClient"; import { + AutoYieldExecutionError, + AutoYieldWithCompletedTaskExecutionError, CanceledWithTaskError, ParsedPayloadSchemaError, ResumeWithTaskError, @@ -63,6 +65,8 @@ const registerSourceEvent: EventSpecification = { parsePayload: RegisterSourceEventSchemaV2.parse, }; +import * as packageJson from "../package.json"; + export type TriggerClientOptions = { /** The `id` property is used to uniquely identify the client. */ @@ -132,7 +136,10 @@ export class TriggerClient { ]); } - async handleRequest(request: Request): Promise { + async handleRequest( + request: Request, + timeOrigin: number = performance.now() + ): Promise { this.#internalLogger.debug("handling request", { url: request.url, headers: Object.fromEntries(request.headers.entries()), @@ -154,7 +161,7 @@ export class TriggerClient { body: { message: "Unauthorized: client missing apiKey", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "missing-header": { @@ -163,7 +170,7 @@ export class TriggerClient { body: { message: "Unauthorized: missing x-trigger-api-key header", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "unauthorized": { @@ -172,7 +179,7 @@ export class TriggerClient { body: { message: `Forbidden: client apiKey mismatch: Make sure you are using the correct API Key for your environment`, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } } @@ -183,7 +190,7 @@ export class TriggerClient { body: { message: "Method not allowed (only POST is allowed)", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } @@ -195,7 +202,7 @@ export class TriggerClient { body: { message: "Missing x-trigger-action header", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } @@ -210,7 +217,7 @@ export class TriggerClient { ok: false, error: "Missing endpoint ID", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } @@ -221,7 +228,7 @@ export class TriggerClient { ok: false, error: `Endpoint ID mismatch error. Expected ${this.id}, got ${endpointId}`, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } @@ -230,7 +237,7 @@ export class TriggerClient { body: { ok: true, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "INDEX_ENDPOINT": { @@ -255,7 +262,7 @@ export class TriggerClient { return { status: 200, body, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "INITIALIZE_TRIGGER": { @@ -285,7 +292,7 @@ export class TriggerClient { return { status: 200, body: dynamicTrigger.registeredTriggerForParams(body.data.params), - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "EXECUTE_JOB": { @@ -312,12 +319,12 @@ export class TriggerClient { }; } - const results = await this.#executeJob(execution.data, job, triggerVersion); + const results = await this.#executeJob(execution.data, job, timeOrigin, triggerVersion); return { status: 200, body: results, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "PREPROCESS_RUN": { @@ -352,7 +359,7 @@ export class TriggerClient { abort: results.abort, properties: results.properties, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "DELIVER_HTTP_SOURCE_REQUEST": { @@ -418,7 +425,7 @@ export class TriggerClient { response, metadata, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } case "VALIDATE": { @@ -428,7 +435,22 @@ export class TriggerClient { ok: true, endpointId: this.id, }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), + }; + } + case "PROBE_EXECUTION_TIMEOUT": { + const json = await request.json(); + // Keep this request open for max 15 minutes so the server can detect when the function execution limit is exceeded + const timeout = json?.timeout ?? 15 * 60 * 1000; + + await new Promise((resolve) => setTimeout(resolve, timeout)); + + return { + status: 200, + body: { + ok: true, + }, + headers: this.#standardResponseHeaders(timeOrigin), }; } } @@ -438,7 +460,7 @@ export class TriggerClient { body: { message: "Method not allowed", }, - headers: this.#standardResponseHeaders, + headers: this.#standardResponseHeaders(timeOrigin), }; } @@ -690,6 +712,7 @@ export class TriggerClient { async #executeJob( body: RunJobBody, job: Job, Record>, + timeOrigin: number, triggerVersion: string | null ): Promise { this.#internalLogger.debug("executing job", { @@ -716,6 +739,8 @@ export class TriggerClient { ? new Logger(job.id, job.logLevel ?? this.#options.logLevel ?? "info") : undefined, serverVersion: triggerVersion, + timeOrigin, + executionTimeout: body.runChunkExecutionLimit, }); const resolvedConnections = await this.#resolveConnections( @@ -756,6 +781,29 @@ export class TriggerClient { this.#logIOStats(io.stats); } + if (error instanceof AutoYieldExecutionError) { + return { + status: "AUTO_YIELD_EXECUTION", + location: error.location, + timeRemaining: error.timeRemaining, + timeElapsed: error.timeElapsed, + limit: body.runChunkExecutionLimit, + }; + } + + if (error instanceof AutoYieldWithCompletedTaskExecutionError) { + return { + status: "AUTO_YIELD_EXECUTION_WITH_COMPLETED_TASK", + id: error.id, + properties: error.properties, + output: error.output, + data: { + ...error.data, + limit: body.runChunkExecutionLimit, + }, + }; + } + if (error instanceof YieldExecutionError) { return { status: "YIELD_EXECUTION", key: error.key }; } @@ -1158,9 +1206,11 @@ export class TriggerClient { }); } - get #standardResponseHeaders() { + #standardResponseHeaders(start: number) { return { "Trigger-Version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS, + "Trigger-SDK-Version": packageJson.version, + "X-Trigger-Request-Timing": `dur=${performance.now() - start / 1000.0}`, }; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 870043aab..6d4c913c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,6 @@ importers: '@codemirror/view': ^6.5.0 '@conform-to/react': ^0.6.1 '@conform-to/zod': ^0.6.1 - '@godaddy/terminus': ^4.12.1 '@headlessui/react': ^1.7.8 '@heroicons/react': ^2.0.12 '@highlight-run/node': ^3.1.0 @@ -211,7 +210,6 @@ importers: '@codemirror/view': 6.7.2 '@conform-to/react': 0.6.1_react@18.2.0 '@conform-to/zod': 0.6.1_zod@3.22.3 - '@godaddy/terminus': 4.12.1 '@headlessui/react': 1.7.8_biqbaboplfbrettd7655fr4n2y '@heroicons/react': 2.0.13_react@18.2.0 '@highlight-run/node': 3.1.0 @@ -6906,12 +6904,6 @@ packages: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: true - /@godaddy/terminus/4.12.1: - resolution: {integrity: sha512-Tm+wVu1/V37uZXcT7xOhzdpFoovQReErff8x3y82k6YyWa1gzxWBjTyrx4G2enjEqoXPnUUmJ3MOmwH+TiP6Sw==} - dependencies: - stoppable: 1.1.0 - dev: false - /@graphile/logger/0.2.0: resolution: {integrity: sha512-jjcWBokl9eb1gVJ85QmoaQ73CQ52xAaOCF29ukRbYNl6lY+ts0ErTaDYOBlejcbUs2OpaiqYLO5uDhyLFzWw4w==} dev: false @@ -18795,7 +18787,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 debug: 3.2.7 eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -18880,7 +18872,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6_eslint@8.42.0 + '@typescript-eslint/parser': 5.59.6_binxsscxvozjxebftqdoazsxm4 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -28918,11 +28910,6 @@ packages: dependencies: bl: 5.1.0 - /stoppable/1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - dev: false - /store2/2.14.2: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true diff --git a/references/job-catalog/package.json b/references/job-catalog/package.json index 0fcca5422..0059ee0e2 100644 --- a/references/job-catalog/package.json +++ b/references/job-catalog/package.json @@ -27,6 +27,7 @@ "redacted": "nodemon --watch src/redacted.ts -r tsconfig-paths/register -r dotenv/config src/redacted.ts", "replicate": "nodemon --watch src/replicate.ts -r tsconfig-paths/register -r dotenv/config src/replicate.ts", "misconfigured": "nodemon --watch src/misconfigured.ts -r tsconfig-paths/register -r dotenv/config src/misconfigured.ts", + "auto-yield": "nodemon --watch src/auto-yield.ts -r tsconfig-paths/register -r dotenv/config src/auto-yield.ts", "dev:trigger": "trigger-cli dev --port 8080" }, "dependencies": { @@ -61,4 +62,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^3.14.1" } -} +} \ No newline at end of file diff --git a/references/job-catalog/src/auto-yield.ts b/references/job-catalog/src/auto-yield.ts new file mode 100644 index 000000000..b097979af --- /dev/null +++ b/references/job-catalog/src/auto-yield.ts @@ -0,0 +1,83 @@ +import { createExpressServer } from "@trigger.dev/express"; +import { TriggerClient, eventTrigger } from "@trigger.dev/sdk"; + +export const client = new TriggerClient({ + id: "job-catalog", + apiKey: process.env["TRIGGER_API_KEY"], + apiUrl: process.env["TRIGGER_API_URL"], + verbose: true, + ioLogLocalEnabled: true, +}); + +client.defineJob({ + id: "auto-yield-1", + name: "Auto Yield 1", + version: "1.0.0", + trigger: eventTrigger({ + name: "auto.yield.1", + }), + run: async (payload, io, ctx) => { + await io.runTask("initial-long-task", async (task) => { + await new Promise((resolve) => setTimeout(resolve, 51000)); // 51 seconds + + return { + message: "initial-long-task", + }; + }); + + for (let i = 0; i < payload.iterations; i++) { + await io.runTask(`task.${i}`, async (task) => { + // Create a random number between 250 and 1250 + const random = Math.floor(Math.random() * 1000) + 250; + + await new Promise((resolve) => setTimeout(resolve, random)); + + await fetch(payload.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: `task.${i}`, + random, + idempotencyKey: task.idempotencyKey, + runId: ctx.run.id, + }), + }); + + return { + message: `task.${i}`, + random, + }; + }); + } + }, +}); + +client.defineJob({ + id: "auto-yield-2", + name: "Auto Yield 2", + version: "1.0.0", + trigger: eventTrigger({ + name: "auto.yield.2", + }), + run: async (payload, io, ctx) => { + await io.runTask("long-task-1", async (task) => { + await new Promise((resolve) => setTimeout(resolve, 10000)); + + return { + message: "long-task-1", + }; + }); + + await io.runTask("long-task-2", async (task) => { + await new Promise((resolve) => setTimeout(resolve, 10000)); + + return { + message: "long-task-2", + }; + }); + }, +}); + +createExpressServer(client); From 7ef61222ed2f83fac6b74e97ef2fd5e7acf02373 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Fri, 20 Oct 2023 22:26:38 +0530 Subject: [PATCH 25/39] slack editing --- docs/integrations/apis/slack.mdx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/integrations/apis/slack.mdx b/docs/integrations/apis/slack.mdx index 23aedd31d..218d3861d 100644 --- a/docs/integrations/apis/slack.mdx +++ b/docs/integrations/apis/slack.mdx @@ -5,8 +5,11 @@ sidebarTitle: Overview & authentication ## Overview -The Slack platform allows you to extend and automate your workspaces to cultivate conversation, inspire action, and synergize services. +Our Slack integration provides a powerful way to streamline communication and workflows within your organization, making it easier to stay informed, collaborate efficiently, and automate routine tasks. +You can tailor triggers and tasks to your specific needs, ensuring that your team and processes are more productive and responsive. + +For examples of some of the things you can do with it, check out our Jobs Showcase: Date: Fri, 20 Oct 2023 22:32:48 +0530 Subject: [PATCH 26/39] Oauth and overview modifications --- docs/integrations/apis/airtable.mdx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/integrations/apis/airtable.mdx b/docs/integrations/apis/airtable.mdx index 530acf69f..690186935 100644 --- a/docs/integrations/apis/airtable.mdx +++ b/docs/integrations/apis/airtable.mdx @@ -5,7 +5,11 @@ sidebarTitle: Overview & authentication ## Overview -With Airtable, your organization can move fast while moving together. Airtable allows you to run your most important business processes—from tracking global marketing campaigns to managing your product roadmap—all on a single platform. +Our Airtable integration provides a versatile way to enhance data management, collaboration, and workflow automation. + + You can customize triggers and tasks to fit your unique use cases and business processes, helping your team become more efficient and productive. + +For examples of some of the things you can do with it, check out our Jobs Showcase: Date: Fri, 20 Oct 2023 23:11:41 +0530 Subject: [PATCH 27/39] slack fixes --- docs/integrations/apis/slack-tasks.mdx | 24 +++++++++++++++++++++++- docs/integrations/apis/slack.mdx | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/integrations/apis/slack-tasks.mdx b/docs/integrations/apis/slack-tasks.mdx index a23f8b2eb..04ebd85f6 100644 --- a/docs/integrations/apis/slack-tasks.mdx +++ b/docs/integrations/apis/slack-tasks.mdx @@ -17,10 +17,32 @@ Post a message to a channel. [Official Slack Docs](https://api.slack.com/methods // Send a Slack message using the io.slack.postMessage function const response = await io.slack.postMessage("post message", { // Specify the target channel by providing its ID - channel: "C04GWUTDC3W", + channel: "< your-channel-id >", // Set the text content of the message text: "My first Slack message", }); ``` +## Example Usage + +```ts +client.defineJob({ + id: "slack-test", + name: "Slack test", + version: "0.0.1", + trigger: eventTrigger({ + name: "slack.test", + schema: z.object({}), + }), + integrations: { + slack, + }, + run: async (payload, io, ctx) => { + const response = await io.slack.postMessage("post message", { + channel: "C04GWUTDC3W", + text: "My first Slack message", + }); + }, +}); +``` diff --git a/docs/integrations/apis/slack.mdx b/docs/integrations/apis/slack.mdx index 218d3861d..8b0102022 100644 --- a/docs/integrations/apis/slack.mdx +++ b/docs/integrations/apis/slack.mdx @@ -43,7 +43,7 @@ Slack supports OAuth ## OAuth -To use OAuth you can connect to Slack via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, and configure Slack with the ID you want to use in your job and the required [scopes](https://api.slack.com/legacy/oauth-scopes). +To use OAuth you can connect to Slack via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, and configure Slack with the ID you want to use in your job and the required [scopes](https://api.slack.com/scopes). ```ts import { Slack } from "@trigger.dev/slack"; From 8718b28949c04420e86af54d791cc670316f99c4 Mon Sep 17 00:00:00 2001 From: Rutam21 Date: Sat, 21 Oct 2023 04:55:12 +0530 Subject: [PATCH 28/39] [TRI-1430] : Improve the OpenAI integration documentation --- docs/integrations/apis/openai-tasks.mdx | 511 ++++++++++++++++++++++++ docs/integrations/apis/openai.mdx | 294 +------------- docs/mint.json | 8 +- 3 files changed, 536 insertions(+), 277 deletions(-) create mode 100644 docs/integrations/apis/openai-tasks.mdx diff --git a/docs/integrations/apis/openai-tasks.mdx b/docs/integrations/apis/openai-tasks.mdx new file mode 100644 index 000000000..a4864dc95 --- /dev/null +++ b/docs/integrations/apis/openai-tasks.mdx @@ -0,0 +1,511 @@ +--- +title: OpenAI tasks +sidebarTitle: Tasks +--- + +Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want. + +--- + +## All tasks + +### `createCompletion` + +Generates text completions as per given prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code demonstrates using OpenAI's text completion with the "davinci" model. + // It generates text based on the given prompt. + await io.openai.createCompletion("completion", { + model: "davinci", + prompt: "Once upon a time", + }); +}, +``` + +### `backgroundCreateCompletion` + +Generates text completions in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code showcases background text completion using the "gpt-3.5-turbo" model. + // It generates text based on the provided programming task and logs the result. + const programmingTask = `Create a function that checks if a string is a palindrome.`; + + const response = await io.openai.backgroundCreateCompletion("background-completion", { + model: "gpt-3.5-turbo", + prompt: `Coding task: ${programmingTask}\n\n`, + }); + + await io.logger.info("codeSnippet", response.choices[0]?.text); +}, +``` + +### `createChatCompletion` + +Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code demonstrates chat completion with the "gpt-3.5-turbo" model. + // It simulates a conversation by providing messages and receiving a chat response. + await io.openai.createChatCompletion("chat-completion", { + model: "gpt-3.5-turbo", + messages: [ + { + role: "user", + content: "Create a good programming joke about background jobs", + }, + ], + }); +}, +``` + +### `backgroundCreateChatCompletion` + +Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code showcases background chat completion using the "gpt-3.5-turbo" model. + // It simulates a conversation with a user message and logs the response choices. + const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", { + model: "gpt-3.5-turbo", + messages: [ + { + role: "user", + content: "Create a good programming joke about background jobs", + }, + ], + }); + + await io.logger.info("choices", response.choices); +}, +``` + +### `retrieveModel` + +Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models) + +```ts example.ts +run: async (payload, io, ctx) => { + // In this code snippet, we retrieve detailed information about a specific OpenAI model. + + // Specify the ID of the model you want to retrieve. Replace 'your_model_id' with the actual model ID. + const modelIdToRetrieve = "your_model_id"; + + try { + // Retrieve the model information using the OpenAI API + const retrievedModel = await io.openai.retrieveModel("get-model", { + model: modelIdToRetrieve, + }); + + // Log the detailed model information + await io.logger.info("retrievedModel", retrievedModel); + } catch (error) { + // Handle errors, such as if the model with the provided ID does not exist. + await io.logger.error("Error retrieving model:", error.message); + } +}, +``` + +### `listModels` + +Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code lists available models without retrieving detailed information. + const models = await io.openai.listModels("list-models"); +}, +``` + +### `createEdit` + +Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code snippet demonstrates using the OpenAI API to create an edit task. + + // Specify the task parameters: + const editTaskParams = { + model: "text-davinci-edit-001", // Replace with the desired model + input: "Thsi is ridddled with erors", // Replace with the input text + instruction: "Fix the spelling errors", // Replace with the editing instruction + }; + + try { + // Create an edit task using the OpenAI API + const editResponse = await io.openai.createEdit("edit", editTaskParams); + + // Log the response + await io.logger.info("editResponse", editResponse); + } catch (error) { + // Handle any potential errors that may occur during the API request. + await io.logger.error("Error creating edit task:", error.message); + } +}, +``` + +### `createImage` + +Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) + +```ts example.ts + run: async (payload, io, ctx) => { + const imageResults = await io.openai.createImage("image", { + prompt: "A hedgehog wearing a party hat", + n: 2, + size: "256x256", + response_format: "url", + }); +``` + +### `createImageEdit` + +Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the parameters for the image edit + const imageEditParams = { + style: "data:image/png;base64,base64_encoded_style_image", + content: "data:image/png;base64,base64_encoded_content_image", + }; + + // Create the image edit using the OpenAI API + const imageEditResponse = await io.openai.createImageEdit(imageEditParams); + + // Log the response + await io.logger.info("imageEditResponse", imageEditResponse); +}, +``` + +### `createImageVariation` + +Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) + +```ts example.ts + run: async (payload, io, ctx) => { + // Specify the parameters for creating an image variation + const imageVariationParams = { + image: "data:image/png;base64,base64_encoded_image", + variation: "brightness(1.2) contrast(0.8) rotate(45deg)", + }; + + // Create the image variation using the OpenAI API + const imageVariationResponse = await io.openai.createImageVariation(imageVariationParams); + + // Log the response + await io.logger.info("imageVariationResponse", imageVariationResponse); + }, +``` + +### `createEmbedding` + +Generates embeddings for a given text. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/embeddings/object) + +```ts example.ts +run: async (payload, io, ctx) => { + // This code snippet demonstrates using the OpenAI API to create a text embedding. + + // Specify the task parameters: + const embeddingTaskParams = { + model: "text-embedding-ada-002", // Replace with the desired model + input: "The food was delicious and the waiter...", // Replace with the input text + }; + + try { + // Create a text embedding using the OpenAI API + const embeddingResponse = await io.openai.createEmbedding("embedding", embeddingTaskParams); + + // Log the response + await io.logger.info("embeddingResponse", embeddingResponse); + } catch (error) { + // Handle any potential errors that may occur during the API request. + await io.logger.error("Error creating text embedding:", error.message); + } +}, +``` + +### `createFile` + +Uploads a file to the OpenAI API. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the parameters for creating a file + const fileParams = { + name: "example.txt", + content: "This is the content of the file.", + }; + + // Create the file using the OpenAI API + const fileResponse = await io.openai.createFile(fileParams); + + // Log the response + await io.logger.info("fileResponse", fileResponse); +}, +``` + +### `listFiles` + +Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object) + +```ts example.ts +run: async (payload, io, ctx) => { + // List the files available in your OpenAI account + const fileListResponse = await io.openai.listFiles(); + + // Log the list of files + await io.logger.info("fileListResponse", fileListResponse); +}, +``` + +### `createFineTuneFile` + +Uploads a file for fine-tuning a model. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the parameters for creating a fine-tune file + const fineTuneFileParams = { + model: "text-davinci-002", + prompt: "Translate English to French: 'Hello, world.'", + language: "en", + description: "Fine-tune file for translation task", + }; + + // Create the fine-tune file using the OpenAI API + const fineTuneFileResponse = await io.openai.createFineTuneFile(fineTuneFileParams); + + // Log the response + await io.logger.info("fineTuneFileResponse", fineTuneFileResponse); +}, +``` + +### `createFineTune` + +Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the parameters for creating a fine-tune task + const fineTuneParams = { + model: "text-davinci-002", + dataset: "your_dataset_id", + description: "Fine-tune task for custom dataset", + }; + + // Create the fine-tune task using the OpenAI API + const fineTuneResponse = await io.openai.createFineTune(fineTuneParams); + + // Log the response + await io.logger.info("fineTuneResponse", fineTuneResponse); +}, +``` + +### `listFineTunes` + +Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // List the fine-tunes available in your OpenAI account + const fineTunesListResponse = await io.openai.listFineTunes(); + + // Log the list of fine-tunes + await io.logger.info("fineTunesListResponse", fineTunesListResponse); +}, +``` + +### `retrieveFineTune` + +Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the ID of the fine-tune you want to retrieve + const fineTuneId = "your_fine_tune_id"; // Replace with the actual fine-tune ID + + // Retrieve the fine-tune using the OpenAI API + const retrievedFineTune = await io.openai.retrieveFineTune(fineTuneId); + + // Log the retrieved fine-tune + await io.logger.info("retrievedFineTune", retrievedFineTune); +}, +``` + +### `cancelFineTune` + +Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the ID of the fine-tune you want to cancel + const fineTuneIdToCancel = "your_fine_tune_id"; // Replace with the actual fine-tune ID + + // Cancel the specified fine-tune using the OpenAI API + const cancellationResponse = await io.openai.cancelFineTune(fineTuneIdToCancel); + + // Log the cancellation response + await io.logger.info("cancellationResponse", cancellationResponse); +}, +``` + +### `createFineTuningJob` + +Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the parameters for creating a fine-tuning job + const fineTuningJobParams = { + fineTuneId: "your_fine_tune_id", // Replace with the actual fine-tune ID + datasetId: "your_dataset_id", // Replace with the ID of your dataset + model: "text-davinci-002", // Replace with the model for fine-tuning + n_examples: 100, // Replace with the number of examples + }; + + // Create the fine-tuning job using the OpenAI API + const fineTuningJobResponse = await io.openai.createFineTuningJob(fineTuningJobParams); + + // Log the response + await io.logger.info("fineTuningJobResponse", fineTuningJobResponse); +}, +``` + +### `retrieveFineTuningJob` + +Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the ID of the fine-tuning job you want to retrieve + const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID + + // Retrieve the fine-tuning job using the OpenAI API + const retrievedJob = await io.openai.retrieveFineTuningJob(fineTuningJobId); + + // Log the retrieved job + await io.logger.info("retrievedJob", retrievedJob); +}, +``` + +### `cancelFineTuningJob` + +Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the ID of the fine-tuning job you want to cancel + const fineTuningJobIdToCancel = "your_fine_tuning_job_id"; // Replace with the actual job ID + + // Cancel the specified fine-tuning job using the OpenAI API + const cancellationResponse = await io.openai.cancelFineTuningJob(fineTuningJobIdToCancel); + + // Log the cancellation response + await io.logger.info("cancellationResponse", cancellationResponse); +}, +``` + +### `listFineTuningJobEvents` + +List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // Specify the ID of the fine-tuning job for which you want to list events + const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID + + // List events for the specified fine-tuning job using the OpenAI API + const eventsListResponse = await io.openai.listFineTuningJobEvents(fineTuningJobId); + + // Log the list of events + await io.logger.info("eventsListResponse", eventsListResponse); +}, +``` + +### `listFineTuningJobs` + +List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) + +```ts example.ts +run: async (payload, io, ctx) => { + // List the fine-tuning jobs available in your OpenAI account + const jobsListResponse = await io.openai.listFineTuningJobs(); + + // Log the list of fine-tuning jobs + await io.logger.info("jobsListResponse", jobsListResponse); +}, +``` + +## Example usage + +In this example we'll create a task that generates a random joke using OpenAI GPT 3.5 . + +```ts example.ts +import { TriggerClient, eventTrigger } from "@trigger.dev/sdk"; +import { OpenAI } from "@trigger.dev/openai"; +import { z } from "zod"; + +// Initialize a TriggerClient with the ID "jobs-showcase" +const client = new TriggerClient({ id: "jobs-showcase" }); + +// Create an instance of the OpenAI client and provide the OpenAI API key from environment variables +const openai = new OpenAI({ + id: "openai", + apiKey: process.env.OPENAI_API_KEY!, // Replace with your actual OpenAI API key +}); + +// Define a job that uses OpenAI GPT-3.5 Turbo to tell jokes +client.defineJob({ + id: "openai-tell-me-a-joke", + name: "OpenAI: tell me a joke", + version: "1.0.0", + trigger: eventTrigger({ + name: "openai.tasks", // Define the trigger event name + schema: z.object({ + jokePrompt: z.string(), // Expect a joke prompt as input + }), + }), + integrations: { + openai, // Use the OpenAI integration for this job + }, + run: async (payload, io, ctx) => { + // Retrieve information about the GPT-3.5 Turbo model + await io.openai.retrieveModel("get-model", { + model: "gpt-3.5-turbo", + }); + + // List available models (optional, for reference) + const models = await io.openai.listModels("list-models"); + + // Generate a joke in the background using the chat conversation format + const jokeResult = await io.openai.backgroundCreateChatCompletion( + "background-chat-completion", + { + model: "gpt-3.5-turbo", + messages: [ + { + role: "user", + content: payload.jokePrompt, // User-provided joke prompt + }, + ], + } + ); + + // Return the generated joke as the result + return { + joke: jokeResult.choices[0]?.message?.content, + }; + }, +}); + +// These lines are specific to the Express framework and can be removed if not needed +import { createExpressServer } from "@trigger.dev/express"; +createExpressServer(client); +``` diff --git a/docs/integrations/apis/openai.mdx b/docs/integrations/apis/openai.mdx index c78cf41c5..989246657 100644 --- a/docs/integrations/apis/openai.mdx +++ b/docs/integrations/apis/openai.mdx @@ -1,17 +1,23 @@ --- -title: OpenAI +title: OpenAI overview & authentication +sidebarTitle: Overview & authentication --- +## Overview + Trigger.dev has a seamless integration with OpenAI, enabling developers to harness the power of AI language models in their serverless applications. With Trigger.dev's background tasks, long-running OpenAI completions become possible, even within the constraints of serverless timeouts. - + + Check out pre-built OpenAI jobs in our showcase. + -## Installation - -To get started with the OpenAI integration on Trigger.dev, you need to install the `@trigger.dev/openai` package. -You can do this using npm, pnpm, or yarn: +## Installing the OpenAI packages @@ -43,276 +49,12 @@ const openai = new OpenAI({ }); ``` -## Usage - -Include the OpenAI integration in your Trigger.dev job: - -```ts -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - // Now you can access the OpenAI tasks through the io object - await io.openai.createCompletion("completion", { - model: "davinci", - prompt: "Once upon a time", - }); - }, -}); -``` - ## Tasks -Tasks that are marked as "long-running" can last longer than your serverless timeout – they are performed on one of our background workers. +Once you have set up a OpenAI client, you can use it to create tasks. -| Function Name | Description | Long-running? | -| -------------------------------- | ------------------------------------------------------------------------- | ------------- | -| `createCompletion` | Generates text completions given a prompt. | -| `backgroundCreateCompletion` | Generates text completions in the background. | ✔ | -| `createChatCompletion` | Generates text completions in a conversational context. | -| `backgroundCreateChatCompletion` | Generates text completions in a conversational context in the background. | ✔ | -| `retrieveModel` | Retrieves a specific model by ID. | -| `listModels` | Lists the available models. | -| `createEdit` | Edits a given text prompt. | -| `createImage` | Generates images from textual descriptions. | -| `createImageEdit` | Creates an edited or extended image given an original image and a prompt | -| `createImageVariation` | Creates a variation of a given image. | -| `createEmbedding` | Generates embeddings for a given text. | -| `createFile` | Uploads a file to the OpenAI API. | -| `listFiles` | Lists the uploaded files. | -| `createFineTuneFile` | Uploads a file for fine-tuning a model. | -| `createFineTune` | Fine-tunes a model on a given task. | -| `listFineTunes` | Lists the available fine-tunes. | -| `retrieveFineTune` | Retrieves a specific fine-tune by ID. | -| `cancelFineTune` | Cancels a specific fine-tune by ID. | -| `createFineTuningJob` | Creates a job that fine-tunes a specified model from a given dataset. | -| `retrieveFineTuningJob` | Get info about a fine-tuning job. | -| `cancelFineTuningJob` | Cancel a fine-tuning job | -| `listFineTuningJobEvents` | List events for a fine-tuning job | -| `listFineTuningJobs` | List fine tuning jobs | - -## Examples - -### Generate a joke - -Here's an example of how to use the OpenAI integration in a Trigger.dev job. -In this example, we'll create a background task to generate a programming joke. - -```ts -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: "Create a good programming joke about background jobs", - }, - ], - }); - - await io.logger.info("choices", response.choices); - }, -}); -``` - -### Generate Code Snippets - -In this example, we'll leverage Trigger.dev's background task to generate code snippets for -a given programming task: - -```ts -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const programmingTask = `Create a function that checks if a string is a palindrome.`; - - const response = await io.openai.backgroundCreateCompletion("background-completion", { - model: "gpt-3.5-turbo", - prompt: `Coding task: ${programmingTask}\n\n`, - }); - - await io.logger.info("codeSnippet", response.choices[0]?.text); - }, -}); -``` - -### Summarize Text - -We'll use Trigger.dev's background task to summarize a lengthy article: - -```ts -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const articleToSummarize = `Lorem ipsum. olor sit amet, consectetur adipiscing elit. - Sed nec aliquet sapien. Pellentesque vitae nisi id purus luctus tincidunt. - Proin condimentum malesuada turpis, eget tincidunt mauris viverra in.`; - - const response = await io.openai.backgroundCreateCompletion("background-completion", { - model: "gpt-3.5-turbo", - prompt: `Please summarize the following article:\n\n${articleToSummarize}`, - }); - - await io.logger.info("summary", response.choices[0]?.text); - }, -}); -``` - -### Draft Email Response - -we'll use Trigger.dev's background task to draft an email response based on a given email content: - -```ts -client.defineJob({ - id: "openai-tasks", - name: "OpenAI Tasks", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.tasks", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const emailContent = `Dear John, - - Thank you for your inquiry. We appreciate your interest in our products. - I have reviewed your request, and I'm pleased to inform you that we can - accommodate your requirements. Please find the attached proposal for your - reference. If you have any further questions, feel free to ask. - - Best regards, - Jane Doe`; - - const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", { - model: "gpt-3.5-turbo", - messages: [ - { - role: "user", - content: emailContent, - }, - { - role: "assistant", - content: "Draft a suitable response to the email above.", - }, - ], - }); - - await io.logger.info("draftedEmailResponse", response.choices[0]?.text); - }, -}); -``` - -### Chatbot Counseling Session - -This job represents a simulated AI counseling session. Leveraging OpenAI's ability to understand context and generate human-like text, it forms empathetic responses to user inputs. Such a system could be part of a mental wellness app. - -```ts -client.defineJob({ - id: "openai-chatbot-counseling", - name: "Chatbot Counseling Session", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.startCounselingSession", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const response = await io.openai.backgroundCreateChatCompletion( - "background-counseling-chat-completion", - { - model: "gpt-3.5-turbo", - messages: [ - { - role: "system", - content: "You are a helpful and empathetic AI counselor.", - }, - { - role: "user", - content: "I've been feeling really stressed out lately.", - }, - ], - } - ); - await io.logger.info("counseling session", response.choices); - }, -}); -``` - -### AI Roleplay Game Session - -This job creates a fantasy AI role-playing game. It could be fun for interactive storytelling or game development contexts. - -```ts -client.defineJob({ - id: "openai-roleplay-game-session", - name: "AI Roleplay Game Session", - version: "0.0.1", - trigger: eventTrigger({ - name: "openai.startRoleplayGameSession", - schema: z.object({}), - }), - integrations: { - openai, - }, - run: async (payload, io, ctx) => { - const response = await io.openai.backgroundCreateChatCompletion( - "background-roleplay-game-session-chat-completion", - { - model: "gpt-3.5-turbo", - messages: [ - { - role: "system", - content: "You are an intelligent guide in a fantasy role-playing game.", - }, - { - role: "user", - content: "I embark on a quest for the enchanted crown. What's the first step?", - }, - ], - } - ); - await io.logger.info("roleplay game session", response.choices); - }, -}); -``` + + + Perform different AI-powered tasks using OpenAI. + + diff --git a/docs/mint.json b/docs/mint.json index 597903b83..054142f8b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -255,7 +255,13 @@ ] }, "integrations/apis/linear", - "integrations/apis/openai", + { + "group": "OpenAI", + "pages": [ + "integrations/apis/openai", + "integrations/apis/openai-tasks" + ] + }, { "group": "Plain", "pages": [ From 01fc1c6634e54d25b60d879e0f20fd3dc71e7574 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Mon, 23 Oct 2023 12:17:29 +0100 Subject: [PATCH 29/39] Slack integration docs copy updates --- docs/integrations/apis/slack-tasks.mdx | 60 +++++++++++++++++++++----- docs/integrations/apis/slack.mdx | 14 +++--- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/docs/integrations/apis/slack-tasks.mdx b/docs/integrations/apis/slack-tasks.mdx index 04ebd85f6..863dc4a04 100644 --- a/docs/integrations/apis/slack-tasks.mdx +++ b/docs/integrations/apis/slack-tasks.mdx @@ -15,23 +15,23 @@ Post a message to a channel. [Official Slack Docs](https://api.slack.com/methods ```ts example.ts // Send a Slack message using the io.slack.postMessage function -const response = await io.slack.postMessage("post message", { +await io.slack.postMessage("post message", { // Specify the target channel by providing its ID - channel: "< your-channel-id >", + channel: "", // Set the text content of the message - text: "My first Slack message", + text: "", }); - ``` + ## Example Usage ```ts client.defineJob({ - id: "slack-test", - name: "Slack test", - version: "0.0.1", + id: "send-slack-message", + name: "Send a Slack message", + version: "1.0.0", trigger: eventTrigger({ - name: "slack.test", + name: "send.slack.message", schema: z.object({}), }), integrations: { @@ -39,10 +39,50 @@ client.defineJob({ }, run: async (payload, io, ctx) => { const response = await io.slack.postMessage("post message", { - channel: "C04GWUTDC3W", - text: "My first Slack message", + channel: "", + text: "", }); }, }); ``` +## How to post rich messages to Slack + +Use their [Block kit builder](https://api.slack.com/block-kit), and then use the `blocks` property to send the message. + +To see this in action, check out our 'Daily Slack alert for Linear issues' [example job](https://trigger.dev/showcase/jobs/linearIssuesDailySlackAlert). + +```ts linearIssuesDailySlackAlert.ts +... +await io.slack.postMessage("post message", { + channel: process.env.SLACK_CHANNEL_ID!, + // Include text for notifications and blocks to get a rich Slack message in the channel + text: `You have ${inProgressIssues.nodes.length} 'In Progress' issues in Linear!`, + // Create rich Slack messages with the Block Kit builder https://app.slack.com/block-kit-builder/ + blocks: inProgressIssues.nodes.flatMap((issue) => [ + { + type: "section", + text: { + type: "mrkdwn", + text: `⏳ *${issue.title}*`, + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "View issue", + emoji: true, + }, + value: "click_me_123", + url: issue.url, + action_id: "button-action", + }, + }, + { + type: "divider", + }, + ]), + }); + }, +}); +``` diff --git a/docs/integrations/apis/slack.mdx b/docs/integrations/apis/slack.mdx index 8b0102022..93e98d3c5 100644 --- a/docs/integrations/apis/slack.mdx +++ b/docs/integrations/apis/slack.mdx @@ -5,11 +5,9 @@ sidebarTitle: Overview & authentication ## Overview -Our Slack integration provides a powerful way to streamline communication and workflows within your organization, making it easier to stay informed, collaborate efficiently, and automate routine tasks. +Our Slack integration allows you to connect to the Slack API and post messages to Slack. -You can tailor triggers and tasks to your specific needs, ensuring that your team and processes are more productive and responsive. - -For examples of some of the things you can do with it, check out our Jobs Showcase: +For examples of some of the things you can do with Slack, check out our Jobs Showcase: - Perform tasks such as posting message to a channel. + Perform tasks such as posting messages to a channel. - From 7bca3d9c15972de1c97199cfb5b0f9f42bfc6912 Mon Sep 17 00:00:00 2001 From: dhselar1423 Date: Mon, 23 Oct 2023 16:57:52 +0530 Subject: [PATCH 30/39] made suggested changes --- docs/integrations/apis/airtable-tasks.mdx | 97 ++++++++++++++++++++--- docs/integrations/apis/airtable.mdx | 4 +- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/docs/integrations/apis/airtable-tasks.mdx b/docs/integrations/apis/airtable-tasks.mdx index 14199b65a..184ca7e49 100644 --- a/docs/integrations/apis/airtable-tasks.mdx +++ b/docs/integrations/apis/airtable-tasks.mdx @@ -14,8 +14,9 @@ Tasks are executed after the job is triggered and are the main building blocks o Gets multiple records for a table. You can filter. [Official Airtable Docs](https://airtable.com/developers/web/api/update-multiple-records) ```ts example.ts - //gets multiple records from the table. Here we only get the Status fields (columns) - const records = await table.getRecords("muliple records", { fields: ["Status"] }); +//gets multiple records from the table. +// Here we only get the Status fields (columns) + await table.getRecords("muliple records", { fields: ["Status"] }); ``` @@ -25,7 +26,7 @@ Gets a single record (using the id) for a table. [Official Airtable Docs](https: ```ts example.ts // Get a single record using the ID of the first record in the fetched records. -const aRecord = await table.getRecord("single", records[0].id); +await table.getRecord("single", records[0].id); ``` @@ -35,10 +36,12 @@ Create one or more records in a table.[Official Airtable Docs](https://airtable. ```ts example.ts // Create newRecords by adding a record to the table. -const newRecords = await table.createRecords("create records", [ +await table.createRecords("create records", [ { - // Define the new record to be created with field values for "Launch goals" and "Status." - fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" }, + // Define the new record to be created + // with field values for "Launch goals" and "Status." + fields: { "Launch goals": "Created from Trigger.dev", + Status: "In progress" }, }, ]); ``` @@ -50,12 +53,13 @@ Update one or more records in a table.[Official Airtable Docs](https://airtable. ```ts example.ts // Update the record that was just created. -const updatedRecords = await table.updateRecords( - // Specify the table's name as "update records." +await table.updateRecords( +// Specify the table's name as "update records." "update records", - // Use the `map` method to create an array of records to be updated. +// Use the `map` method to create an array of records to be updated. newRecords.map((record) => ({ - // For each record, specify the ID and the field to update ("Status" to "At risk"). +/*For each record, specify the : +ID and the field to update ("Status" to "At risk").*/ id: record.id, fields: { Status: "At risk" }, })) @@ -69,7 +73,7 @@ Delete one or more records in a table (using ids).[Official Airtable Docs](https ```ts example.ts // Delete records from the table. -const deletedRecords = await table.deleteRecords( +await table.deleteRecords( // Specify the table's name as "delete records." "delete records", ); @@ -80,4 +84,75 @@ const deletedRecords = await table.deleteRecords( Do anything that’s possible with the official Airtable Node SDK. +## Usage + +Include the Airtable integration in your Trigger.dev job. + +You can optionally add the types for your Table – this gives you nice type inference and errors when writing your code. + +```ts +//this is the type definition for my Table +type LaunchGoalsAndOkRs = { + "Launch goals"?: string; + DRI?: Collaborator; + Team?: string; + Status?: "On track" | "In progress" | "At risk"; + "Key results"?: Array; + "Features (from 💻 Features table)"?: Array; + "Status (from 💻 Features)": Array< + "Live" | "Complete" | "In progress" | "Planning" | "In reviews" + >; +}; + +client.defineJob({ + id: "airtable-example-1", + name: "Airtable Example 1: getRecords", + version: "0.1.0", + trigger: eventTrigger({ + name: "airtable.example", + }), + integrations: { + //make sure to add the integration here + airtable, + }, + run: async (payload, io, ctx) => { + //adding the type to table("") gives you nice type inference and errors + //you can leave it out as well table("") + const table = io.airtable.base("").table(""); + + //gets multiple records from the table. Here we only get the Status fields (columns) + const records = await table.getRecords("muliple records", { fields: ["Status"] }); + await io.logger.log(records[0].fields.Status ?? "no status"); + + //Get a single record + const aRecord = await table.getRecord("single", records[0].id); + + //create a new record + const newRecords = await table.createRecords("create records", [ + { + fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" }, + }, + ]); + + //update the record we just created + const updatedRecords = await table.updateRecords( + "update records", + newRecords.map((record) => ({ + id: record.id, + fields: { Status: "At risk" }, + })) + ); + + //wait 5 seconds + await io.wait("5 secs", 5); + + //delete the record we just created + updated + const deletedRecords = await table.deleteRecords( + "delete records", + updatedRecords.map((record) => record.id) + ); + }, +}); +``` + diff --git a/docs/integrations/apis/airtable.mdx b/docs/integrations/apis/airtable.mdx index 690186935..3e913c913 100644 --- a/docs/integrations/apis/airtable.mdx +++ b/docs/integrations/apis/airtable.mdx @@ -5,9 +5,7 @@ sidebarTitle: Overview & authentication ## Overview -Our Airtable integration provides a versatile way to enhance data management, collaboration, and workflow automation. - - You can customize triggers and tasks to fit your unique use cases and business processes, helping your team become more efficient and productive. +Our Airtable integration allows you to easily connect to the Airtable API and perform tasks such as creating/updating/deleting single or multiple records in your tables. For examples of some of the things you can do with it, check out our Jobs Showcase: From 627c767c644fb08315532762dedeac89d58867f2 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 23 Oct 2023 15:24:35 +0100 Subject: [PATCH 31/39] fix: fix trigger sources getting repeatedly registered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a bug where trigger sources would get re-registered on every index, no matter if they were already properly registered or not. This was causing a lot of extra “internal” runs and being overly chatty with client endpoints --- .../app/components/primitives/NamedIcon.tsx | 4 +- .../sources/registerSourceV2.server.ts | 52 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx index e0b7a5c0a..523fade79 100644 --- a/apps/webapp/app/components/primitives/NamedIcon.tsx +++ b/apps/webapp/app/components/primitives/NamedIcon.tsx @@ -223,7 +223,9 @@ export function NamedIcon({ return ; } - console.log(`Icon ${name} not found`); + if (name === "supabase-management") { + return ; + } if (fallback) { return fallback; diff --git a/apps/webapp/app/services/sources/registerSourceV2.server.ts b/apps/webapp/app/services/sources/registerSourceV2.server.ts index 4b6bea252..f13e435ef 100644 --- a/apps/webapp/app/services/sources/registerSourceV2.server.ts +++ b/apps/webapp/app/services/sources/registerSourceV2.server.ts @@ -6,6 +6,7 @@ import type { AuthenticatedEnvironment } from "../apiAuth.server"; import { workerQueue } from "../worker.server"; import { generateSecret } from "./utils.server"; import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server"; +import { logger } from "../logger.server"; export class RegisterSourceServiceV2 { #prismaClient: PrismaClientOrTransaction; @@ -211,16 +212,22 @@ export class RegisterSourceServiceV2 { // Collect the options that are no longer being used so we can remove them const newOptions = metadata.options; - const orphanedOptions: Record = {}; - for (const event of triggerSource.options) { - const values = newOptions[event.name]; - if (values === undefined) { - orphanedOptions[event.name] = [event.value]; + const orphanedOptions: Record> = {}; + for (const option of triggerSource.options) { + const newValues = newOptions[option.name]; + + // initialize the set + if (!orphanedOptions[option.name]) { + orphanedOptions[option.name] = new Set(); + } + + if (newValues === undefined) { + orphanedOptions[option.name] = new Set([...orphanedOptions[option.name], option.value]); continue; } - if (values!.includes(event.value)) { - orphanedOptions[event.name] = [...values, event.value]; + if (!newValues.includes(option.value)) { + orphanedOptions[option.name] = new Set([...orphanedOptions[option.name], option.value]); } } @@ -250,9 +257,26 @@ export class RegisterSourceServiceV2 { }); } + // Delete the orphaned options + for (const [name, values] of Object.entries(orphanedOptions)) { + for (const value of values) { + await tx.triggerSourceOption.delete({ + where: { + name_value_sourceId: { + name, + value, + sourceId: triggerSource.id, + }, + }, + }); + } + } + return { id: triggerSource.id, - orphanedOptions, + orphanedOptions: Object.fromEntries( + Object.entries(orphanedOptions).map(([name, values]) => [name, Array.from(values)]) + ), }; }, { timeout: 15000 } @@ -284,9 +308,19 @@ export class RegisterSourceServiceV2 { } const triggerIsActive = triggerSource.active; - const triggerHasOrphanedEvents = Object.keys(orphanedOptions).length > 0; + const triggerHasOrphanedEvents = Object.values(orphanedOptions).some( + (values) => values.length > 0 + ); const triggerHasUnregisteredEvents = triggerSource.options.some((option) => !option.registered); + logger.debug("Deciding whether to activate source", { + triggerIsActive, + triggerHasOrphanedEvents, + triggerHasUnregisteredEvents, + orphanedOptions, + options: triggerSource.options, + }); + if (!triggerIsActive || triggerHasOrphanedEvents || triggerHasUnregisteredEvents) { // We need to re-activate the source, and there could be orphaned events await workerQueue.enqueue("activateSource", { From 0c4aea03382628ad6454aee8d4c00b21ba3a696c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 23 Oct 2023 16:55:00 +0100 Subject: [PATCH 32/39] Add execution duration for background fetch --- .../tasks/performTaskOperation.server.ts | 17 ++++++++++++++--- references/job-catalog/src/background-fetch.ts | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/services/tasks/performTaskOperation.server.ts b/apps/webapp/app/services/tasks/performTaskOperation.server.ts index 987cb56fa..9dfbfd3a9 100644 --- a/apps/webapp/app/services/tasks/performTaskOperation.server.ts +++ b/apps/webapp/app/services/tasks/performTaskOperation.server.ts @@ -35,7 +35,7 @@ export class PerformTaskOperationService { } if (!task.operation) { - return await this.#resumeTask(task, null); + return await this.#resumeTask(task, null, 0); } logger.debug("PerformTaskOperationService.call", { task }); @@ -53,12 +53,16 @@ export class PerformTaskOperationService { const { url, requestInit, retry } = fetchOperation.data; + const startTimeInMs = performance.now(); + const response = await fetch(url, { method: requestInit?.method ?? "GET", headers: normalizeHeaders(requestInit?.headers ?? {}), body: requestInit?.body, }); + const durationInMs = Math.floor(performance.now() - startTimeInMs); + const jsonBody = await safeJsonFromResponse(response); logger.debug("PerformTaskOperationService.call.fetch", { @@ -91,7 +95,7 @@ export class PerformTaskOperationService { } } - return await this.#resumeTask(task, jsonBody); + return await this.#resumeTask(task, jsonBody, durationInMs); } default: { await this.#resumeTaskWithError(task, { @@ -218,7 +222,7 @@ export class PerformTaskOperationService { }); } - async #resumeTask(task: NonNullable, output: any) { + async #resumeTask(task: NonNullable, output: any, durationInMs: number) { await $transaction(this.#prismaClient, async (tx) => { await tx.taskAttempt.updateMany({ where: { @@ -236,6 +240,13 @@ export class PerformTaskOperationService { status: "COMPLETED", completedAt: new Date(), output: output ? output : undefined, + run: { + update: { + executionDuration: { + increment: durationInMs, + }, + }, + }, }, }); diff --git a/references/job-catalog/src/background-fetch.ts b/references/job-catalog/src/background-fetch.ts index 523686464..80c908e8f 100644 --- a/references/job-catalog/src/background-fetch.ts +++ b/references/job-catalog/src/background-fetch.ts @@ -15,7 +15,7 @@ client.defineJob({ name: "Test background fetch retry", version: "0.0.1", trigger: eventTrigger({ - name: "test.background-fetch", + name: "test.background-fetch.retry", schema: z.object({ url: z.string(), method: z.string().optional(), @@ -38,4 +38,19 @@ client.defineJob({ }, }); +client.defineJob({ + id: "test-background-fetch", + name: "Test background fetch", + version: "0.0.1", + trigger: eventTrigger({ + name: "test.background-fetch", + schema: z.object({ + url: z.string(), + }), + }), + run: async (payload, io, ctx) => { + return await io.backgroundFetch("fetch", payload.url); + }, +}); + createExpressServer(client); From b85983756497425441f673720fefc6cad1486fc0 Mon Sep 17 00:00:00 2001 From: D-K-P Date: Mon, 23 Oct 2023 17:19:19 +0100 Subject: [PATCH 33/39] Edited the OpenAI ovrerview and added correct links for the tasks --- docs/integrations/apis/openai-tasks.mdx | 38 ++++++++++++------------- docs/integrations/apis/openai.mdx | 4 +-- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/docs/integrations/apis/openai-tasks.mdx b/docs/integrations/apis/openai-tasks.mdx index a4864dc95..d372bba8f 100644 --- a/docs/integrations/apis/openai-tasks.mdx +++ b/docs/integrations/apis/openai-tasks.mdx @@ -45,7 +45,7 @@ run: async (payload, io, ctx) => { ### `createChatCompletion` -Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) +Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create) ```ts example.ts run: async (payload, io, ctx) => { @@ -65,7 +65,7 @@ run: async (payload, io, ctx) => { ### `backgroundCreateChatCompletion` -Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat) +Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/object) ```ts example.ts run: async (payload, io, ctx) => { @@ -87,7 +87,7 @@ run: async (payload, io, ctx) => { ### `retrieveModel` -Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models) +Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/retrieve) ```ts example.ts run: async (payload, io, ctx) => { @@ -113,7 +113,7 @@ run: async (payload, io, ctx) => { ### `listModels` -Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models) +Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list) ```ts example.ts run: async (payload, io, ctx) => { @@ -124,7 +124,7 @@ run: async (payload, io, ctx) => { ### `createEdit` -Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits) +Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits/create) ```ts example.ts run: async (payload, io, ctx) => { @@ -152,7 +152,7 @@ run: async (payload, io, ctx) => { ### `createImage` -Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) +Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create) ```ts example.ts run: async (payload, io, ctx) => { @@ -166,7 +166,7 @@ Generates images from textual descriptions. [Official OpenAI Docs](https://platf ### `createImageEdit` -Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) +Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit) ```ts example.ts run: async (payload, io, ctx) => { @@ -186,7 +186,7 @@ run: async (payload, io, ctx) => { ### `createImageVariation` -Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images) +Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation) ```ts example.ts run: async (payload, io, ctx) => { @@ -206,7 +206,7 @@ Creates a variation of a given image. [Official OpenAI Docs](https://platform.op ### `createEmbedding` -Generates embeddings for a given text. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/embeddings/object) +Generates embeddings for a given text. [Official OpenAI Docs](hhttps://platform.openai.com/docs/api-reference/embeddings/object) ```ts example.ts run: async (payload, io, ctx) => { @@ -253,7 +253,7 @@ run: async (payload, io, ctx) => { ### `listFiles` -Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object) +Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list) ```ts example.ts run: async (payload, io, ctx) => { @@ -289,7 +289,7 @@ run: async (payload, io, ctx) => { ### `createFineTune` -Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create) ```ts example.ts run: async (payload, io, ctx) => { @@ -310,7 +310,7 @@ run: async (payload, io, ctx) => { ### `listFineTunes` -Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list) ```ts example.ts run: async (payload, io, ctx) => { @@ -324,7 +324,7 @@ run: async (payload, io, ctx) => { ### `retrieveFineTune` -Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve) ```ts example.ts run: async (payload, io, ctx) => { @@ -341,7 +341,7 @@ run: async (payload, io, ctx) => { ### `cancelFineTune` -Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel) ```ts example.ts run: async (payload, io, ctx) => { @@ -358,7 +358,7 @@ run: async (payload, io, ctx) => { ### `createFineTuningJob` -Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create) ```ts example.ts run: async (payload, io, ctx) => { @@ -380,7 +380,7 @@ run: async (payload, io, ctx) => { ### `retrieveFineTuningJob` -Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve) ```ts example.ts run: async (payload, io, ctx) => { @@ -397,7 +397,7 @@ run: async (payload, io, ctx) => { ### `cancelFineTuningJob` -Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel) ```ts example.ts run: async (payload, io, ctx) => { @@ -414,7 +414,7 @@ run: async (payload, io, ctx) => { ### `listFineTuningJobEvents` -List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events) ```ts example.ts run: async (payload, io, ctx) => { @@ -431,7 +431,7 @@ run: async (payload, io, ctx) => { ### `listFineTuningJobs` -List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning) +List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list) ```ts example.ts run: async (payload, io, ctx) => { diff --git a/docs/integrations/apis/openai.mdx b/docs/integrations/apis/openai.mdx index 989246657..9e19ef187 100644 --- a/docs/integrations/apis/openai.mdx +++ b/docs/integrations/apis/openai.mdx @@ -5,9 +5,7 @@ sidebarTitle: Overview & authentication ## Overview -Trigger.dev has a seamless integration with OpenAI, enabling developers to harness the power of AI -language models in their serverless applications. With Trigger.dev's background tasks, long-running -OpenAI completions become possible, even within the constraints of serverless timeouts. +Our OpenAI integration allows you to easily perform AI-powered tasks, such as summarizing text, answering questions, generating images, fine tuning and much more. Date: Tue, 24 Oct 2023 09:11:57 +0100 Subject: [PATCH 34/39] Added link to the api reference repo to the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67aacddea..22e80d7ad 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag! - Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag. -- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag. +- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest), [/api-reference](https://github.com/triggerdotdev/api-reference/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%8E%83hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag. - Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved in with the community. _New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._ From 07d28ee4f514e85763ce9fee1795d707f3a7ebb5 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Oct 2023 09:19:38 +0100 Subject: [PATCH 35/39] Add custom baseURL and idempotency key support to OpenAI integration (#682) This commit introduces the ability to set custom base URLs, which allows for the use of different APIs such as Perplexity. It also ensures that any OpenAI-related task now includes an idempotency key when making requests. Works for background tasks as well. --- .changeset/rude-kids-joke.md | 5 +++ integrations/openai/src/chat.ts | 14 ++++++-- integrations/openai/src/completions.ts | 14 ++++++-- integrations/openai/src/edits.ts | 2 +- integrations/openai/src/embeddings.ts | 4 ++- integrations/openai/src/files.ts | 10 ++++-- integrations/openai/src/fineTunes.ts | 8 ++--- integrations/openai/src/images.ts | 40 +++++++++++++---------- integrations/openai/src/index.ts | 4 ++- integrations/openai/src/types.ts | 2 ++ references/job-catalog/src/openai.ts | 45 +++++++++++++++++++++++--- 11 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 .changeset/rude-kids-joke.md diff --git a/.changeset/rude-kids-joke.md b/.changeset/rude-kids-joke.md new file mode 100644 index 000000000..8ab083e2f --- /dev/null +++ b/.changeset/rude-kids-joke.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/openai": patch +--- + +Add ability to set baseURL to use different APIs (like Perplexity) diff --git a/integrations/openai/src/chat.ts b/integrations/openai/src/chat.ts index 2cabd4c2a..6ded6ec8a 100644 --- a/integrations/openai/src/chat.ts +++ b/integrations/openai/src/chat.ts @@ -14,7 +14,9 @@ export class Chat { return this.runTask( key, async (client, task) => { - const response = await client.chat.completions.create(params); + const response = await client.chat.completions.create(params, { + idempotencyKey: task.idempotencyKey, + }); task.outputProperties = createTaskUsageProperties(response.usage); return response; }, @@ -38,15 +40,23 @@ export class Chat { return this.runTask( key, async (client, task, io) => { + let baseURL = client.baseURL ?? "https://api.openai.com/v1"; + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } + + const chatCompletionsURL = `${baseURL}/chat/completions`; + const response = await io.backgroundFetch( "background", - "https://api.openai.com/v1/chat/completions", + chatCompletionsURL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: redactString`Bearer ${client.apiKey}`, ...(client.organization ? { "OpenAI-Organization": client.organization } : {}), + "Idempotency-Key": task.idempotencyKey, }, body: JSON.stringify(params), }, diff --git a/integrations/openai/src/completions.ts b/integrations/openai/src/completions.ts index 952fcbac5..8f27ea971 100644 --- a/integrations/openai/src/completions.ts +++ b/integrations/openai/src/completions.ts @@ -17,7 +17,9 @@ export class Completions { return this.runTask( key, async (client, task) => { - const response = await client.completions.create(params); + const response = await client.completions.create(params, { + idempotencyKey: task.idempotencyKey, + }); task.outputProperties = createTaskUsageProperties(response.usage); return response; }, @@ -41,15 +43,23 @@ export class Completions { return this.runTask( key, async (client, task, io) => { + let baseURL = client.baseURL ?? "https://api.openai.com/v1"; + if (baseURL.endsWith("/")) { + baseURL = baseURL.slice(0, -1); + } + + const chatCompletionsURL = `${baseURL}/chat/completions`; + const response = await io.backgroundFetch( "background", - "https://api.openai.com/v1/completions", + chatCompletionsURL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: redactString`Bearer ${client.apiKey}`, ...(client.organization ? { "OpenAI-Organization": client.organization } : {}), + "Idempotency-Key": task.idempotencyKey, }, body: JSON.stringify(params), }, diff --git a/integrations/openai/src/edits.ts b/integrations/openai/src/edits.ts index 63c28d0fe..dd2ee7928 100644 --- a/integrations/openai/src/edits.ts +++ b/integrations/openai/src/edits.ts @@ -37,7 +37,7 @@ export class Edits { return this.runTask( key, async (client, task) => { - const response = await client.edits.create(params); + const response = await client.edits.create(params, { idempotencyKey: task.idempotencyKey }); task.outputProperties = createTaskUsageProperties(response.usage); return response; }, diff --git a/integrations/openai/src/embeddings.ts b/integrations/openai/src/embeddings.ts index 357bffc6d..0594095ac 100644 --- a/integrations/openai/src/embeddings.ts +++ b/integrations/openai/src/embeddings.ts @@ -17,7 +17,9 @@ export class Embeddings { return this.runTask( key, async (client, task) => { - const response = await client.embeddings.create(params); + const response = await client.embeddings.create(params, { + idempotencyKey: task.idempotencyKey, + }); task.outputProperties = createTaskUsageProperties(response.usage); return response; }, diff --git a/integrations/openai/src/files.ts b/integrations/openai/src/files.ts index bc6f03d34..e15556a3f 100644 --- a/integrations/openai/src/files.ts +++ b/integrations/openai/src/files.ts @@ -36,7 +36,10 @@ export class Files { file = params.file; } - return client.files.create({ file, purpose: params.purpose }); + return client.files.create( + { file, purpose: params.purpose }, + { idempotencyKey: task.idempotencyKey } + ); }, { name: "Create file", @@ -81,7 +84,10 @@ export class Files { params.fileName ); - return client.files.create({ file, purpose: "fine-tune" }); + return client.files.create( + { file, purpose: "fine-tune" }, + { idempotencyKey: task.idempotencyKey } + ); }, { name: "Create fine tune file", diff --git a/integrations/openai/src/fineTunes.ts b/integrations/openai/src/fineTunes.ts index 5f89c669b..6b8917ca1 100644 --- a/integrations/openai/src/fineTunes.ts +++ b/integrations/openai/src/fineTunes.ts @@ -41,7 +41,7 @@ export class FineTunes { return this.runTask( key, async (client, task) => { - return client.fineTunes.create(params); + return client.fineTunes.create(params, { idempotencyKey: task.idempotencyKey }); }, { name: "Create fine tune", @@ -94,7 +94,7 @@ export class FineTunes { return this.runTask( key, async (client, task) => { - return client.fineTunes.cancel(params.fineTuneId); + return client.fineTunes.cancel(params.fineTuneId, { idempotencyKey: task.idempotencyKey }); }, { name: "Cancel fine tune", @@ -168,7 +168,7 @@ export class FineTunes { return this.runTask( key, async (client, task) => { - return client.fineTuning.jobs.create(params); + return client.fineTuning.jobs.create(params, { idempotencyKey: task.idempotencyKey }); }, { name: "Create Fine Tuning Job", @@ -207,7 +207,7 @@ export class FineTunes { return this.runTask( key, async (client, task) => { - return client.fineTuning.jobs.cancel(params.id); + return client.fineTuning.jobs.cancel(params.id, { idempotencyKey: task.idempotencyKey }); }, { name: "Cancel Fine Tuning Job", diff --git a/integrations/openai/src/images.ts b/integrations/openai/src/images.ts index 57ec44cbd..ce87876cf 100644 --- a/integrations/openai/src/images.ts +++ b/integrations/openai/src/images.ts @@ -64,7 +64,7 @@ export class Images { return this.runTask( key, async (client, task) => { - return client.images.generate(params); + return client.images.generate(params, { idempotencyKey: task.idempotencyKey }); }, { name: "Create image", @@ -121,15 +121,18 @@ export class Images { typeof params.image === "string" ? await fileFromUrl(params.image) : params.image; const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask; - const response = await client.images.edit({ - image: file, - prompt: params.prompt, - mask: mask, - n: params.n, - size: params.size, - response_format: params.response_format, - user: params.user, - }); + const response = await client.images.edit( + { + image: file, + prompt: params.prompt, + mask: mask, + n: params.n, + size: params.size, + response_format: params.response_format, + user: params.user, + }, + { idempotencyKey: task.idempotencyKey } + ); return response; }, @@ -182,13 +185,16 @@ export class Images { const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image; - const response = await client.images.createVariation({ - image: file, - n: params.n, - size: params.size, - response_format: params.response_format, - user: params.user, - }); + const response = await client.images.createVariation( + { + image: file, + n: params.n, + size: params.size, + response_format: params.response_format, + user: params.user, + }, + { idempotencyKey: task.idempotencyKey } + ); return response; }, diff --git a/integrations/openai/src/index.ts b/integrations/openai/src/index.ts index 4f3e2283c..89fe9dc21 100644 --- a/integrations/openai/src/index.ts +++ b/integrations/openai/src/index.ts @@ -55,6 +55,7 @@ export class OpenAI implements TriggerIntegration { this.native = new OpenAIApi({ apiKey: options.apiKey, organization: options.organization, + baseURL: options.baseURL, }); } @@ -77,6 +78,7 @@ export class OpenAI implements TriggerIntegration { openai._client = new OpenAIApi({ apiKey, organization: this._options.organization, + baseURL: this._options.baseURL ?? "https://api.openai.com/v1", }); return openai; } @@ -104,7 +106,7 @@ export class OpenAI implements TriggerIntegration { return callback(this._client, task, io); }, { - icon: "openai", + icon: this._options.icon ?? "openai", retry: retry.standardBackoff, ...(options ?? {}), connectionKey: this._connectionKey, diff --git a/integrations/openai/src/types.ts b/integrations/openai/src/types.ts index 481bf224d..2dad32871 100644 --- a/integrations/openai/src/types.ts +++ b/integrations/openai/src/types.ts @@ -2,6 +2,8 @@ export type OpenAIIntegrationOptions = { id: string; apiKey?: string; organization?: string; + baseURL?: string; + icon?: string; }; export type OpenAIIntegrationAuth = Omit; diff --git a/references/job-catalog/src/openai.ts b/references/job-catalog/src/openai.ts index 35be1fa49..222e5b11d 100644 --- a/references/job-catalog/src/openai.ts +++ b/references/job-catalog/src/openai.ts @@ -54,11 +54,6 @@ client.defineJob({ ], }); - await io.openai.backgroundCreateCompletion("background-completion", { - model: "text-davinci-003", - prompt: "Create a good programming joke about Tasks", - }); - await io.openai.createCompletion("completion", { model: "text-davinci-003", prompt: "Create a good programming joke about Tasks", @@ -77,4 +72,44 @@ client.defineJob({ }, }); +const perplexity = new OpenAI({ + id: "perplexity", + apiKey: process.env["PERPLEXITY_API_KEY"]!, + baseURL: "https://api.perplexity.ai", + icon: "brand-open-source", +}); + +client.defineJob({ + id: "perplexity-tasks", + name: "Perplexity Tasks", + version: "0.0.1", + trigger: eventTrigger({ + name: "perplexity.tasks", + }), + integrations: { + perplexity, + }, + run: async (payload, io, ctx) => { + await io.perplexity.createChatCompletion("chat-completion", { + model: "mistral-7b-instruct", + messages: [ + { + role: "user", + content: "Create a good programming joke about background jobs", + }, + ], + }); + + await io.perplexity.backgroundCreateChatCompletion("background-chat-completion", { + model: "mistral-7b-instruct", + messages: [ + { + role: "user", + content: "If you were a programming language, what would you be and why?", + }, + ], + }); + }, +}); + createExpressServer(client); From cc44aff24b2de919029ca62eafc1427d70d31132 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Oct 2023 09:35:20 +0100 Subject: [PATCH 36/39] Added some docs about using the OpenAI integration as a universal client --- docs/images/perplexity.png | Bin 0 -> 394005 bytes docs/integrations/apis/openai.mdx | 57 +++++++++++++++++++++++++++ references/job-catalog/src/openai.ts | 4 +- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 docs/images/perplexity.png diff --git a/docs/images/perplexity.png b/docs/images/perplexity.png new file mode 100644 index 0000000000000000000000000000000000000000..557c8fa44012369a5765b81fbb19f54faa4c7dd3 GIT binary patch literal 394005 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®)uc67rM@^8W- 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 z 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%0D^T_hXk1<*}ucGk% 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 + +## Usage as Universal Client + +You can use our OpenAI integration as a universal client to interact with other OpenAI-compatible APIs, such as [Perplexity.ai](https://docs.perplexity.ai/) + +```ts +import { OpenAI } from "@trigger.dev/openai"; + +const perplexity = new OpenAI({ + id: "perplexity", + apiKey: process.env["PERPLEXITY_API_KEY"]!, + baseURL: "https://api.perplexity.ai", + icon: "brand-open-source", +}); +``` + +Since [Perplexity.ai](https://docs.perplexity.ai/) is compatible with OpenAI, you can use the same tasks as with OpenAI. + +```ts +client.defineJob({ + id: "perplexity-tasks", + name: "Perplexity Tasks", + version: "0.0.1", + trigger: eventTrigger({ + name: "perplexity.tasks", + }), + integrations: { + perplexity, + }, + run: async (payload, io, ctx) => { + await io.perplexity.chat.completions.create("chat-completion", { + model: "mistral-7b-instruct", + messages: [ + { + role: "user", + content: "Create a good programming joke about background jobs", + }, + ], + }); + + // Run this in the background + await io.perplexity.chat.completions.backgroundCreate("background-chat-completion", { + model: "mistral-7b-instruct", + messages: [ + { + role: "user", + content: "If you were a programming language, what would you be and why?", + }, + ], + }); + }, +}); +``` + +And you'll get the same experience in the Run Dashboard when viewing the logs: + +![Perplexity.ai logs](/images/perplexity.png) diff --git a/references/job-catalog/src/openai.ts b/references/job-catalog/src/openai.ts index 222e5b11d..e8e96f5f6 100644 --- a/references/job-catalog/src/openai.ts +++ b/references/job-catalog/src/openai.ts @@ -90,7 +90,7 @@ client.defineJob({ perplexity, }, run: async (payload, io, ctx) => { - await io.perplexity.createChatCompletion("chat-completion", { + await io.perplexity.chat.completions.create("chat-completion", { model: "mistral-7b-instruct", messages: [ { @@ -100,7 +100,7 @@ client.defineJob({ ], }); - await io.perplexity.backgroundCreateChatCompletion("background-chat-completion", { + await io.perplexity.chat.completions.backgroundCreate("background-chat-completion", { model: "mistral-7b-instruct", messages: [ { From 76224568f466e55fa6b99627260f4cda049d890e Mon Sep 17 00:00:00 2001 From: D-K-P Date: Tue, 24 Oct 2023 11:31:01 +0100 Subject: [PATCH 37/39] Airtable copy updates --- docs/integrations/apis/airtable-tasks.mdx | 85 ++++++++++++----------- docs/integrations/apis/airtable.mdx | 8 +-- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/integrations/apis/airtable-tasks.mdx b/docs/integrations/apis/airtable-tasks.mdx index 184ca7e49..a9c9ff2b8 100644 --- a/docs/integrations/apis/airtable-tasks.mdx +++ b/docs/integrations/apis/airtable-tasks.mdx @@ -11,87 +11,85 @@ Tasks are executed after the job is triggered and are the main building blocks o ### `base.table.getRecords` -Gets multiple records for a table. You can filter. [Official Airtable Docs](https://airtable.com/developers/web/api/update-multiple-records) +Gets multiple records for a table. You can filter. [Official Airtable Docs](https://airtable.com/developers/web/api/update-multiple-records). ```ts example.ts -//gets multiple records from the table. +// Gets multiple records from the table. // Here we only get the Status fields (columns) - await table.getRecords("muliple records", { fields: ["Status"] }); - +await table.getRecords("multiple records", { + fields: ["Status"], +}); ``` ### `base.table.getRecord` -Gets a single record (using the id) for a table. [Official Airtable Docs](https://airtable.com/developers/web/api/get-record) +Gets a single record (using the id) for a table. [Official Airtable Docs](https://airtable.com/developers/web/api/get-record). ```ts example.ts // Get a single record using the ID of the first record in the fetched records. -await table.getRecord("single", records[0].id); +await table.getRecord("single", records[0].id); ``` - ### `base.table.createRecords` -Create one or more records in a table.[Official Airtable Docs](https://airtable.com/developers/web/api/create-records) +Create one or more records in a table. [Official Airtable Docs](https://airtable.com/developers/web/api/create-records). ```ts example.ts // Create newRecords by adding a record to the table. await table.createRecords("create records", [ { // Define the new record to be created - // with field values for "Launch goals" and "Status." - fields: { "Launch goals": "Created from Trigger.dev", - Status: "In progress" }, + // with field values for "Launch goals" and "Status." + fields: { + "Launch goals": "Created from Trigger.dev", + Status: "In progress", + }, }, ]); ``` - ### `base.table.updateRecords` -Update one or more records in a table.[Official Airtable Docs](https://airtable.com/developers/web/api/update-record) +Update one or more records in a table. [Official Airtable Docs](https://airtable.com/developers/web/api/update-record). ```ts example.ts // Update the record that was just created. await table.updateRecords( -// Specify the table's name as "update records." + // Specify the table's name as "update records." "update records", -// Use the `map` method to create an array of records to be updated. + // Use the `map` method to create an array of records to be updated. newRecords.map((record) => ({ -/*For each record, specify the : -ID and the field to update ("Status" to "At risk").*/ + // For each record, specify the: ID and the field to update ("Status" to "At risk"). id: record.id, - fields: { Status: "At risk" }, + fields: { + Status: "At risk", + }, })) ); ``` - ### `base.table.deleteRecords` -Delete one or more records in a table (using ids).[Official Airtable Docs](https://airtable.com/developers/web/api/delete-record) +Delete one or more records in a table (using ids). [Official Airtable Docs](https://airtable.com/developers/web/api/delete-record). ```ts example.ts // Delete records from the table. await table.deleteRecords( -// Specify the table's name as "delete records." - "delete records", + // Specify the table's name as "delete records." + "delete records" ); ``` - ### `runTask` Do anything that’s possible with the official Airtable Node SDK. -## Usage - -Include the Airtable integration in your Trigger.dev job. +## Example Usage You can optionally add the types for your Table – this gives you nice type inference and errors when writing your code. ```ts -//this is the type definition for my Table +// This is the type definition for my Table type LaunchGoalsAndOkRs = { "Launch goals"?: string; DRI?: Collaborator; @@ -105,36 +103,41 @@ type LaunchGoalsAndOkRs = { }; client.defineJob({ - id: "airtable-example-1", - name: "Airtable Example 1: getRecords", + id: "airtable-example", + name: "Airtable Example: getRecords", version: "0.1.0", trigger: eventTrigger({ name: "airtable.example", }), integrations: { - //make sure to add the integration here + // Make sure to add the integration here airtable, }, run: async (payload, io, ctx) => { - //adding the type to table("") gives you nice type inference and errors - //you can leave it out as well table("") + // Adding the type to table("") gives you nice type inference and errors + // You can leave it out as well table("") const table = io.airtable.base("").table(""); - //gets multiple records from the table. Here we only get the Status fields (columns) - const records = await table.getRecords("muliple records", { fields: ["Status"] }); + // Gets multiple records from the table. Here we only get the Status fields (columns) + const records = await table.getRecords("multiple records", { + fields: ["Status"], + }); await io.logger.log(records[0].fields.Status ?? "no status"); - //Get a single record + // Get a single record const aRecord = await table.getRecord("single", records[0].id); - //create a new record + // Create a new record const newRecords = await table.createRecords("create records", [ { - fields: { "Launch goals": "Created from Trigger.dev", Status: "In progress" }, + fields: { + "Launch goals": "Created from Trigger.dev", + Status: "In progress", + }, }, ]); - //update the record we just created + // Update the record we just created const updatedRecords = await table.updateRecords( "update records", newRecords.map((record) => ({ @@ -143,10 +146,10 @@ client.defineJob({ })) ); - //wait 5 seconds + // Wait for 5 seconds await io.wait("5 secs", 5); - //delete the record we just created + updated + // Delete the record we just created + updated const deletedRecords = await table.deleteRecords( "delete records", updatedRecords.map((record) => record.id) @@ -154,5 +157,3 @@ client.defineJob({ }, }); ``` - - diff --git a/docs/integrations/apis/airtable.mdx b/docs/integrations/apis/airtable.mdx index 3e913c913..5603bbc27 100644 --- a/docs/integrations/apis/airtable.mdx +++ b/docs/integrations/apis/airtable.mdx @@ -5,7 +5,7 @@ sidebarTitle: Overview & authentication ## Overview -Our Airtable integration allows you to easily connect to the Airtable API and perform tasks such as creating/updating/deleting single or multiple records in your tables. +Our Airtable integration allows you to easily connect to the Airtable API and perform tasks such as creating / updating / deleting single or multiple records in your tables. For examples of some of the things you can do with it, check out our Jobs Showcase: @@ -46,7 +46,6 @@ To use the Airtable API with Trigger.dev, you can either use OAuth or a Personal To use OAuth you can connect to Airtable via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, and configure Airtable with the ID you want to use in your job and the required [scopes](https://airtable.com/developers/web/api/scopes). - ```ts import { Airtable } from "@trigger.dev/airtable"; @@ -69,12 +68,13 @@ const airtable = new Airtable({ token: process.env["AIRTABLE_TOKEN"], }); ``` + ## Tasks Once you have set up a Airtable client, you can use it to create tasks. - Perform tasks such as creating/updating single or multiple records in table. + Perform tasks such as creating / updating / deleting single or multiple records in table. - \ No newline at end of file + From ea052c44c79295b86ad4d33b81cd857daac42f3d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Oct 2023 11:38:35 +0100 Subject: [PATCH 38/39] ignore: Add durationinMs to log --- apps/webapp/app/services/tasks/performTaskOperation.server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/webapp/app/services/tasks/performTaskOperation.server.ts b/apps/webapp/app/services/tasks/performTaskOperation.server.ts index 9dfbfd3a9..7b4577c17 100644 --- a/apps/webapp/app/services/tasks/performTaskOperation.server.ts +++ b/apps/webapp/app/services/tasks/performTaskOperation.server.ts @@ -72,6 +72,7 @@ export class PerformTaskOperationService { statusCode: response.status, headers: Object.fromEntries(response.headers.entries()), jsonBody, + durationInMs, }); if (!response.ok) { From 7e100b8362c0f7379457cb095c983e28ea1c15a9 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Oct 2023 12:14:52 +0100 Subject: [PATCH 39/39] ignore: adding more logs to perform task operation --- apps/webapp/app/services/tasks/performTaskOperation.server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/services/tasks/performTaskOperation.server.ts b/apps/webapp/app/services/tasks/performTaskOperation.server.ts index 7b4577c17..eaf74b197 100644 --- a/apps/webapp/app/services/tasks/performTaskOperation.server.ts +++ b/apps/webapp/app/services/tasks/performTaskOperation.server.ts @@ -123,6 +123,8 @@ export class PerformTaskOperationService { logger.debug("Calculating retry at for strategy", { strategy, + status: response.status, + retry, }); switch (strategy.strategy) {