Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90f52de147 | |||
| 28b05a82d8 | |||
| b9ed7e2ced | |||
| 5e651d84af | |||
| 6a992a1995 | |||
| 81e886a1ba | |||
| ab9e4a989c | |||
| e350659e24 | |||
| f888a49555 | |||
| 421c249e50 | |||
| a8a6f51387 | |||
| 12e73eef22 | |||
| 2e33fcb16b | |||
| 5912cdd11c | |||
| cc016b3ae3 | |||
| 7760e09462 | |||
| 3ca4456c88 | |||
| 618b7f22da | |||
| a12c7c3b0a | |||
| a42e94c75f |
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://trigger.dev/discord
|
||||
|
||||
@@ -57,12 +57,12 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
|
||||
<RemixLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
|
||||
<AstroLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
|
||||
<NuxtLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
|
||||
@@ -272,6 +272,21 @@ export function HowToUseApiKeysAndEndpoints() {
|
||||
you should use the Test feature to trigger any scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Staging</span>
|
||||
<EnvironmentLabel environment={{ type: "STAGING" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
|
||||
environment, meant to mirror your production environment.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
|
||||
@@ -119,11 +119,11 @@ export function ProjectSideMenu() {
|
||||
data-action="onboarding"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Homepage"
|
||||
icon="external-link"
|
||||
to="https://trigger.dev"
|
||||
name="Changelog"
|
||||
icon="list"
|
||||
to="https://trigger.dev/changelog"
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="onboarding"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
|
||||
@@ -131,6 +131,7 @@ const icons = {
|
||||
"clipboard-checked": (className: string) => (
|
||||
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
|
||||
@@ -5,3 +5,4 @@ export const DEFAULT_MAX_CONCURRENT_RUNS = 10;
|
||||
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;
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
|
||||
rethrowPrismaErrors?: boolean;
|
||||
swallowPrismaErrors?: boolean;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
@@ -55,11 +55,9 @@ export async function $transaction<R>(
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
if (options?.rethrowPrismaErrors) {
|
||||
throw error;
|
||||
if (options?.swallowPrismaErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -124,6 +122,10 @@ function getClient() {
|
||||
emit: "stdout",
|
||||
level: "warn",
|
||||
},
|
||||
// {
|
||||
// emit: "stdout",
|
||||
// level: "query",
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import { customAlphabet } from "nanoid";
|
||||
import slug from "slug";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { createProject } from "./project.server";
|
||||
|
||||
export type { Organization };
|
||||
@@ -176,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "staging";
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "preview";
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function createProject(
|
||||
|
||||
// Create the dev and prod environments
|
||||
await createEnvironment(organization, project, "PRODUCTION");
|
||||
await createEnvironment(organization, project, "STAGING");
|
||||
|
||||
for (const member of project.organization.members) {
|
||||
await createEnvironment(organization, project, "DEVELOPMENT", member);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import { ServerTask } from "@trigger.dev/core";
|
||||
import { CachedTask, ServerTask } from "@trigger.dev/core";
|
||||
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
|
||||
|
||||
@@ -23,5 +23,90 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
|
||||
attempts: task.attempts.length,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
operation: task.operation,
|
||||
callbackUrl: task.callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export type TaskForCaching = Pick<
|
||||
Task,
|
||||
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId"
|
||||
>;
|
||||
|
||||
export function prepareTasksForCaching(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const task of tasks) {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
const size = calculateCachedTaskSize(cachedTask);
|
||||
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(cachedTask);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareTasksForCachingLegacy(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
|
||||
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTaskForCaching(task: TaskForCaching): CachedTask {
|
||||
return {
|
||||
id: task.idempotencyKey, // We should eventually move this back to task.id
|
||||
status: task.status,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
noop: task.noop,
|
||||
output: task.output as any,
|
||||
parentId: task.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import {
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "../../../../packages/database/src";
|
||||
import { env } from "~/env.server";
|
||||
} from "@trigger.dev/database";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
endpoints: {
|
||||
DEVELOPMENT: ClientEndpoint;
|
||||
PRODUCTION: ClientEndpoint;
|
||||
STAGING?: ClientEndpoint;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,6 +133,8 @@ export class EnvironmentsPresenter {
|
||||
throw new Error("Development environment not found, this should not happen");
|
||||
}
|
||||
|
||||
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
|
||||
|
||||
const productionEnvironment = filtered.find(
|
||||
(environment) => environment.type === "PRODUCTION"
|
||||
);
|
||||
@@ -151,6 +153,9 @@ export class EnvironmentsPresenter {
|
||||
state: "unconfigured",
|
||||
environment: productionEnvironment,
|
||||
},
|
||||
STAGING: stagingEnvironment
|
||||
? { state: "unconfigured", environment: stagingEnvironment }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -161,6 +166,16 @@ export class EnvironmentsPresenter {
|
||||
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
|
||||
}
|
||||
|
||||
if (stagingEnvironment) {
|
||||
const stagingEndpoint = stagingEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
if (stagingEndpoint) {
|
||||
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const prodEndpoint = productionEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
+2
-1
@@ -104,6 +104,7 @@ export default function Page() {
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<HelpTrigger title="Example Jobs and inspiration" />
|
||||
</div>
|
||||
@@ -160,7 +161,7 @@ function ExampleJobs() {
|
||||
height="250"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
className="mb-4 w-full border-b border-slate-800"
|
||||
className="mb-4 border-b border-slate-800"
|
||||
/>
|
||||
<Header2 spacing>How to create a Job</Header2>
|
||||
<Paragraph variant="small" spacing>
|
||||
|
||||
+15
-3
@@ -85,8 +85,8 @@ export default function Page() {
|
||||
const client = clients.find((c) => c.slug === selected.client);
|
||||
if (!client) return undefined;
|
||||
|
||||
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
|
||||
throw new Error("PREVIEW/STAGING is not yet supported");
|
||||
if (selected.type === "PREVIEW") {
|
||||
throw new Error("PREVIEW is not yet supported");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,6 +195,18 @@ export default function Page() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
{client.endpoints.STAGING && (
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.STAGING}
|
||||
type="STAGING"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "STAGING",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
@@ -218,7 +230,7 @@ export default function Page() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
{selectedEndpoint && selectedEndpoint.endpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
|
||||
+3
@@ -44,6 +44,9 @@ export default function SetUpAstro() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<AstroLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
+4
@@ -1,5 +1,6 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -36,6 +37,9 @@ export default function Page() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<ExpressLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
+4
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<NextjsLogo className="w-56" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
|
||||
|
||||
+4
@@ -27,6 +27,7 @@ import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { RemixLogo } from "~/assets/logos/RemixLogo";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
|
||||
@@ -43,6 +44,9 @@ export default function SetUpRemix() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<RemixLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
id: z.string(),
|
||||
secret: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const { runId, id } = ParamsSchema.parse(params);
|
||||
|
||||
// Parse body as JSON (no schema parsing)
|
||||
const body = await request.json();
|
||||
|
||||
const service = new CallbackRunTaskService();
|
||||
|
||||
try {
|
||||
// Complete task with request body as output
|
||||
await service.call(runId, id, body, request.url);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error while processing task callback:", { error });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export class CallbackRunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise<void> {
|
||||
const task = await findTask(prisma, id);
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.runId !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.status !== "WAITING") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.callbackUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) {
|
||||
logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("CallbackRunTaskService.call()", { task });
|
||||
|
||||
await this.#resumeTask(task, taskBody);
|
||||
}
|
||||
|
||||
async #resumeTask(task: NonNullable<FoundTask>, output: any) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.taskAttempt.updateMany({
|
||||
where: {
|
||||
taskId: task.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
completedAt: new Date(),
|
||||
output: output ? output : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#resumeRunExecution(task, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
|
||||
await enqueueRunExecutionV2(task.run, prisma, {
|
||||
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type FoundTask = Awaited<ReturnType<typeof findTask>>;
|
||||
|
||||
async function findTask(prisma: PrismaClientOrTransaction, id: string) {
|
||||
return prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
queue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// 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 (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class FailRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// 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 (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class FailRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
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 } from "@trigger.dev/core";
|
||||
import {
|
||||
API_VERSIONS,
|
||||
RunTaskBodyOutput,
|
||||
RunTaskBodyOutputSchema,
|
||||
RunTaskResponseWithCachedTasksBody,
|
||||
ServerTask,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { prepareTasksForCaching, 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 { generateSecret } from "~/services/sources/utils.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
@@ -16,6 +24,8 @@ const ParamsSchema = z.object({
|
||||
|
||||
const HeadersSchema = z.object({
|
||||
"idempotency-key": z.string(),
|
||||
"trigger-version": z.string().optional().nullable(),
|
||||
"x-cached-tasks-cursor": z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
@@ -37,7 +47,11 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Invalid or Missing idempotency key" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { "idempotency-key": idempotencyKey } = headers.data;
|
||||
const {
|
||||
"idempotency-key": idempotencyKey,
|
||||
"trigger-version": triggerVersion,
|
||||
"x-cached-tasks-cursor": cachedTasksCursor,
|
||||
} = headers.data;
|
||||
|
||||
const { runId } = ParamsSchema.parse(params);
|
||||
|
||||
@@ -48,6 +62,8 @@ export async function action({ request, params }: ActionArgs) {
|
||||
body: anyBody,
|
||||
runId,
|
||||
idempotencyKey,
|
||||
triggerVersion,
|
||||
cachedTasksCursor,
|
||||
});
|
||||
|
||||
const body = RunTaskBodyOutputSchema.safeParse(anyBody);
|
||||
@@ -71,6 +87,26 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
|
||||
if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) {
|
||||
const requestMigration = new ChangeRequestLazyLoadedCachedTasks();
|
||||
|
||||
const responseBody = await requestMigration.call(runId, task, cachedTasksCursor);
|
||||
|
||||
logger.debug(
|
||||
"RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks",
|
||||
{
|
||||
responseBody,
|
||||
cachedTasksCursor,
|
||||
}
|
||||
);
|
||||
|
||||
return json(responseBody, {
|
||||
headers: {
|
||||
"trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return json(task);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -81,6 +117,51 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChangeRequestLazyLoadedCachedTasks {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
runId: string,
|
||||
task: ServerTask,
|
||||
cursor?: string | null
|
||||
): Promise<RunTaskResponseWithCachedTasksBody> {
|
||||
if (!cursor) {
|
||||
return {
|
||||
task,
|
||||
};
|
||||
}
|
||||
|
||||
// We need to limit the cached tasks to not be too large >2MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
|
||||
|
||||
const nextTasks = await this.#prismaClient.task.findMany({
|
||||
where: {
|
||||
runId,
|
||||
status: "COMPLETED",
|
||||
noop: false,
|
||||
},
|
||||
take: 250,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
task,
|
||||
cachedTasks: preparedTasks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -106,10 +187,13 @@ export class RunTaskService {
|
||||
},
|
||||
});
|
||||
|
||||
const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now();
|
||||
const callbackEnabled = taskBody.callback?.enabled;
|
||||
|
||||
if (existingTask) {
|
||||
if (existingTask.status === "CANCELED") {
|
||||
const existingTaskStatus =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
@@ -154,16 +238,21 @@ export class RunTaskService {
|
||||
status = "CANCELED";
|
||||
} else {
|
||||
status =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
: "RUNNING";
|
||||
}
|
||||
|
||||
const taskId = ulid();
|
||||
const callbackUrl = callbackEnabled
|
||||
? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}`
|
||||
: undefined;
|
||||
|
||||
const task = await tx.task.create({
|
||||
data: {
|
||||
id: ulid(),
|
||||
id: taskId,
|
||||
idempotencyKey,
|
||||
displayKey: taskBody.displayKey,
|
||||
runConnection: taskBody.connectionKey
|
||||
@@ -194,6 +283,7 @@ export class RunTaskService {
|
||||
properties: taskBody.properties ?? undefined,
|
||||
redact: taskBody.redact ?? undefined,
|
||||
operation: taskBody.operation,
|
||||
callbackUrl,
|
||||
style: taskBody.style ?? { style: "normal" },
|
||||
attempts: {
|
||||
create: {
|
||||
@@ -217,6 +307,17 @@ export class RunTaskService {
|
||||
},
|
||||
{ tx, runAt: task.delayUntil ?? undefined }
|
||||
);
|
||||
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
|
||||
if (taskBody.callback.timeoutInSeconds > 0) {
|
||||
// We need to schedule the callback timeout
|
||||
await workerQueue.enqueue(
|
||||
"processCallbackTimeout",
|
||||
{
|
||||
id: task.id,
|
||||
},
|
||||
{ tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
@@ -2,28 +2,15 @@ import { parse } from "@conform-to/zod";
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
CreateEndpointError,
|
||||
CreateEndpointService,
|
||||
} from "~/services/endpoints/createEndpoint.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core";
|
||||
import { env } from "process";
|
||||
import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server";
|
||||
import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
export const bodySchema = z.object({
|
||||
environmentId: z.string(),
|
||||
url: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectId } = ParamsSchema.parse(params);
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: bodySchema });
|
||||
|
||||
@@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
|
||||
const service = new ValidateCreateEndpointService();
|
||||
const result = await service.call({
|
||||
await service.call({
|
||||
url: submission.value.url,
|
||||
environment,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ApiEventLog,
|
||||
DeliverEventResponseSchema,
|
||||
DeserializedJson,
|
||||
EndpointHeadersSchema,
|
||||
ErrorWithStackSchema,
|
||||
HttpSourceRequest,
|
||||
HttpSourceResponseSchema,
|
||||
@@ -89,6 +91,15 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...pongResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return pongResponse.data;
|
||||
}
|
||||
|
||||
@@ -129,41 +140,15 @@ export class EndpointApi {
|
||||
const anyBody = await response.json();
|
||||
|
||||
const data = IndexEndpointResponseSchema.parse(anyBody);
|
||||
const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data,
|
||||
headers,
|
||||
} as const;
|
||||
}
|
||||
|
||||
async deliverEvent(event: ApiEventLog) {
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-trigger-api-key": this.apiKey,
|
||||
"x-trigger-action": "DELIVER_EVENT",
|
||||
},
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
logger.debug("deliverEvent() response from endpoint", {
|
||||
body: anyBody,
|
||||
});
|
||||
|
||||
return DeliverEventResponseSchema.parse(anyBody);
|
||||
}
|
||||
|
||||
async executeJobRequest(options: RunJobBody) {
|
||||
const startTimeInMs = performance.now();
|
||||
|
||||
@@ -338,6 +323,15 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...validateResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return validateResponse.data;
|
||||
}
|
||||
}
|
||||
@@ -359,6 +353,7 @@ function addStandardRequestOptions(options: RequestInit) {
|
||||
headers: {
|
||||
...options.headers,
|
||||
"user-agent": "triggerdotdev-server/2.0.0",
|
||||
"x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ export class CreateEndpointService {
|
||||
slug: id,
|
||||
url: endpointUrl,
|
||||
indexingHookIdentifier: indexingHookIdentifier(),
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
update: {
|
||||
url: endpointUrl,
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export class IndexEndpointService {
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data;
|
||||
const { "trigger-version": triggerVersion } = indexResponse.headers;
|
||||
|
||||
logger.debug("Indexing endpoint", {
|
||||
endpointId: endpoint.id,
|
||||
@@ -48,6 +49,7 @@ export class IndexEndpointService {
|
||||
endpointSlug: endpoint.slug,
|
||||
source: source,
|
||||
sourceData: sourceData,
|
||||
triggerVersion,
|
||||
stats: {
|
||||
jobs: jobs.length,
|
||||
sources: sources.length,
|
||||
@@ -56,6 +58,17 @@ export class IndexEndpointService {
|
||||
},
|
||||
});
|
||||
|
||||
if (triggerVersion && triggerVersion !== endpoint.version) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: triggerVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const indexStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
|
||||
@@ -58,9 +58,11 @@ export class ValidateCreateEndpointService {
|
||||
slug: validationResult.endpointId,
|
||||
url: endpointUrl,
|
||||
indexingHookIdentifier: indexingHookIdentifier(),
|
||||
version: validationResult.triggerVersion,
|
||||
},
|
||||
update: {
|
||||
url: endpointUrl,
|
||||
version: validationResult.triggerVersion,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -34,77 +34,55 @@ export class IngestSendEvent {
|
||||
try {
|
||||
const deliverAt = this.#calculateDeliverAt(options);
|
||||
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Create a new event in the database
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
connect: {
|
||||
id: environment.projectId,
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
eventId: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp ?? new Date(),
|
||||
payload: event.payload ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccount: externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: externalAccount.id,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
// Create a new event in the database
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
environmentId: environment.id,
|
||||
eventId: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp ?? new Date(),
|
||||
payload: event.payload ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccountId: externalAccount ? externalAccount.id : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.deliverEvents) {
|
||||
// Produce a message to the event bus
|
||||
await workerQueue.enqueue(
|
||||
"deliverEvent",
|
||||
{
|
||||
id: eventLog.id,
|
||||
},
|
||||
});
|
||||
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
if (this.deliverEvents) {
|
||||
// Produce a message to the event bus
|
||||
await workerQueue.enqueue(
|
||||
"deliverEvent",
|
||||
{
|
||||
id: eventLog.id,
|
||||
},
|
||||
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
return eventLog;
|
||||
},
|
||||
{ rethrowPrismaErrors: true }
|
||||
);
|
||||
return eventLog;
|
||||
});
|
||||
} catch (error) {
|
||||
const prismaError = PrismaErrorSchema.safeParse(error);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { github } from "./integrations/github";
|
||||
import { linear } from "./integrations/linear";
|
||||
import { openai } from "./integrations/openai";
|
||||
import { plain } from "./integrations/plain";
|
||||
import { replicate } from "./integrations/replicate";
|
||||
import { resend } from "./integrations/resend";
|
||||
import { sendgrid } from "./integrations/sendgrid";
|
||||
import { slack } from "./integrations/slack";
|
||||
@@ -37,6 +38,7 @@ export const integrationCatalog = new IntegrationCatalog({
|
||||
linear,
|
||||
openai,
|
||||
plain,
|
||||
replicate,
|
||||
resend,
|
||||
slack,
|
||||
stripe,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { HelpSample, Integration } from "../types";
|
||||
|
||||
function usageSample(hasApiKey: boolean): HelpSample {
|
||||
const apiKeyPropertyName = "apiKey";
|
||||
|
||||
return {
|
||||
title: "Using the client",
|
||||
code: `
|
||||
import { Replicate } from "@trigger.dev/replicate";
|
||||
|
||||
const replicate = new Replicate({
|
||||
id: "__SLUG__",${hasApiKey ? `,\n ${apiKeyPropertyName}: process.env.REPLICATE_API_KEY!` : ""}
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "replicate-create-prediction",
|
||||
name: "Replicate - Create Prediction",
|
||||
version: "0.1.0",
|
||||
integrations: { replicate },
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.predict",
|
||||
schema: z.object({
|
||||
prompt: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return io.replicate.predictions.createAndAwait("await-prediction", {
|
||||
version: payload.version,
|
||||
input: { prompt: payload.prompt },
|
||||
});
|
||||
},
|
||||
});
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
export const replicate: Integration = {
|
||||
identifier: "replicate",
|
||||
name: "Replicate",
|
||||
packageName: "@trigger.dev/replicate@latest",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [usageSample(true)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -42,29 +42,32 @@ export class CreateRunService {
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
// Get the current max number for the given jobId
|
||||
const currentMaxNumber = await tx.jobRun.aggregate({
|
||||
const latestJob = await tx.jobRun.findFirst({
|
||||
where: { jobId: job.id },
|
||||
_max: { number: true },
|
||||
orderBy: { id: "desc" },
|
||||
select: {
|
||||
number: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Increment the number for the new execution
|
||||
const newNumber = (currentMaxNumber._max.number ?? 0) + 1;
|
||||
const newNumber = (latestJob?.number ?? 0) + 1;
|
||||
|
||||
// Create the new execution with the incremented number
|
||||
const run = await tx.jobRun.create({
|
||||
data: {
|
||||
number: newNumber,
|
||||
preprocess: version.preprocessRuns,
|
||||
job: { connect: { id: job.id } },
|
||||
version: { connect: { id: version.id } },
|
||||
event: { connect: { id: eventId } },
|
||||
environment: { connect: { id: environment.id } },
|
||||
organization: { connect: { id: environment.organizationId } },
|
||||
project: { connect: { id: environment.projectId } },
|
||||
endpoint: { connect: { id: endpoint.id } },
|
||||
queue: { connect: { id: jobQueue.id } },
|
||||
externalAccount: eventRecord.externalAccountId
|
||||
? { connect: { id: eventRecord.externalAccountId } }
|
||||
jobId: job.id,
|
||||
versionId: version.id,
|
||||
eventId: eventId,
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
endpointId: endpoint.id,
|
||||
queueId: jobQueue.id,
|
||||
externalAccountId: eventRecord.externalAccountId
|
||||
? eventRecord.externalAccountId
|
||||
: undefined,
|
||||
isTest: eventRecord.isTest,
|
||||
},
|
||||
|
||||
@@ -263,6 +263,7 @@ export class PerformRunExecutionV1Service {
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.map((t) => CachedTaskSchema.parse(t)),
|
||||
yieldedExecutions: run.yieldedExecutions,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@@ -354,6 +355,11 @@ export class PerformRunExecutionV1Service {
|
||||
|
||||
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}`);
|
||||
@@ -393,6 +399,40 @@ export class PerformRunExecutionV1Service {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -409,7 +449,9 @@ export class PerformRunExecutionV1Service {
|
||||
|
||||
// If the task has an operation, then the next performRunExecution will occur
|
||||
// when that operation has finished
|
||||
if (!data.task.operation) {
|
||||
// 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,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
CachedTask,
|
||||
API_VERSIONS,
|
||||
BloomFilter,
|
||||
ConnectionAuth,
|
||||
EndpointHeadersSchema,
|
||||
RunJobError,
|
||||
RunJobInvalidPayloadError,
|
||||
RunJobResumeWithTask,
|
||||
RunJobRetryWithTask,
|
||||
RunJobSuccess,
|
||||
RunJobUnresolvedAuthError,
|
||||
RunSourceContext,
|
||||
RunSourceContextSchema,
|
||||
supportsFeature,
|
||||
} from "@trigger.dev/core";
|
||||
import { RuntimeEnvironmentType, type Task } from "@trigger.dev/database";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
@@ -18,10 +23,17 @@ import { formatError } from "~/utils/formatErrors.server";
|
||||
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 { ApiEventLog } from "@trigger.dev/core";
|
||||
import { RunJobBody } from "@trigger.dev/core";
|
||||
|
||||
type FoundRun = NonNullable<Awaited<ReturnType<typeof findRun>>>;
|
||||
type FoundTask = FoundRun["tasks"][number];
|
||||
|
||||
// We need to limit the cached tasks to not be too large >3.5MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000;
|
||||
|
||||
export type PerformRunExecutionV2Input = {
|
||||
id: string;
|
||||
reason: "PREPROCESS" | "EXECUTE_JOB";
|
||||
@@ -230,38 +242,19 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext);
|
||||
|
||||
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest({
|
||||
const executionBody = await this.#createExecutionBody(
|
||||
run,
|
||||
[run.tasks, resumedTask].flat().filter(Boolean),
|
||||
startedAt,
|
||||
isRetry,
|
||||
connections.auth,
|
||||
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: prepareTasksForRun([run.tasks, resumedTask].flat().filter(Boolean)),
|
||||
});
|
||||
sourceContext.success ? sourceContext.data : undefined
|
||||
);
|
||||
|
||||
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest(
|
||||
executionBody
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
return await this.#failRunExecutionWithRetry({
|
||||
@@ -269,6 +262,25 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -389,6 +401,10 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
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}`);
|
||||
@@ -396,6 +412,91 @@ export class PerformRunExecutionV2Service {
|
||||
}
|
||||
}
|
||||
|
||||
async #createExecutionBody(
|
||||
run: FoundRun,
|
||||
tasks: FoundTask[],
|
||||
startedAt: Date,
|
||||
isRetry: boolean,
|
||||
connections: Record<string, ConnectionAuth>,
|
||||
event: ApiEventLog,
|
||||
source?: RunSourceContext
|
||||
): Promise<RunJobBody> {
|
||||
if (supportsFeature("lazyLoadedCachedTasks", run.endpoint.version)) {
|
||||
const preparedTasks = prepareTasksForCaching(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
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,
|
||||
source,
|
||||
tasks: preparedTasks.tasks,
|
||||
cachedTaskCursor: preparedTasks.cursor,
|
||||
noopTasksSet: prepareNoOpTasksBloomFilter(tasks),
|
||||
yieldedExecutions: run.yieldedExecutions,
|
||||
};
|
||||
}
|
||||
|
||||
const preparedTasks = prepareTasksForCachingLegacy(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
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,
|
||||
source,
|
||||
tasks: preparedTasks.tasks,
|
||||
};
|
||||
}
|
||||
|
||||
async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess, durationInMs: number) {
|
||||
await this.#prismaClient.jobRun.update({
|
||||
where: { id: run.id },
|
||||
@@ -429,7 +530,9 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
// If the task has an operation, then the next performRunExecution will occur
|
||||
// when that operation has finished
|
||||
if (!data.task.operation) {
|
||||
// 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) {
|
||||
await enqueueRunExecutionV2(run, tx, {
|
||||
runAt: data.task.delayUntil ?? undefined,
|
||||
resumeTaskId: data.task.id,
|
||||
@@ -501,6 +604,56 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeYieldedRun(
|
||||
run: FoundRun,
|
||||
key: string,
|
||||
isRetry: boolean,
|
||||
durationInMs: number,
|
||||
executionCount: number
|
||||
) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
if (run.yieldedExecutions.length + 1 > MAX_RUN_YIELDED_EXECUTIONS) {
|
||||
return await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: `Run has yielded too many times, the maximum is ${MAX_RUN_YIELDED_EXECUTIONS}`,
|
||||
},
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
}
|
||||
|
||||
await tx.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
executionCount: {
|
||||
increment: 1,
|
||||
},
|
||||
yieldedExecutions: {
|
||||
push: key,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
yieldedExecutions: true,
|
||||
executionCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueRunExecutionV2(run, tx, {
|
||||
isRetry,
|
||||
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
executionCount,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(
|
||||
run: FoundRun,
|
||||
data: RunJobRetryWithTask,
|
||||
@@ -686,69 +839,16 @@ export class PerformRunExecutionV2Service {
|
||||
}
|
||||
}
|
||||
|
||||
function prepareTasksForRun(possibleTasks: FoundTask[]): CachedTask[] {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
|
||||
function prepareNoOpTasksBloomFilter(possibleTasks: FoundTask[]): string {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && task.noop);
|
||||
|
||||
// We need to limit the cached tasks to not be too large >3.5MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000;
|
||||
const filter = new BloomFilter(BloomFilter.NOOP_TASK_SET_SIZE);
|
||||
|
||||
const cachedTasks = new Map<string, CachedTask>(); // Cache for prepared tasks
|
||||
const cachedTaskSizes = new Map<string, number>(); // Cache for calculated task sizes
|
||||
|
||||
// Helper function to get the cached prepared task, or prepare and cache if not already cached
|
||||
function getCachedTask(task: FoundTask): CachedTask {
|
||||
const taskId = task.id;
|
||||
if (!cachedTasks.has(taskId)) {
|
||||
cachedTasks.set(taskId, prepareTaskForRun(task));
|
||||
}
|
||||
return cachedTasks.get(taskId)!;
|
||||
for (const task of tasks) {
|
||||
filter.add(task.idempotencyKey);
|
||||
}
|
||||
|
||||
// Helper function to get the cached task size, or calculate and cache if not already cached
|
||||
function getCachedTaskSize(task: CachedTask): number {
|
||||
const taskId = task.id;
|
||||
if (!cachedTaskSizes.has(taskId)) {
|
||||
cachedTaskSizes.set(taskId, calculateCachedTaskSize(task));
|
||||
}
|
||||
return cachedTaskSizes.get(taskId)!;
|
||||
}
|
||||
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = getCachedTask(task);
|
||||
return { task: cachedTask, size: getCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = TOTAL_CACHED_TASK_BYTE_LIMIT;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return tasksToRun;
|
||||
}
|
||||
|
||||
function prepareTaskForRun(task: FoundTask): CachedTask {
|
||||
return {
|
||||
id: task.idempotencyKey, // We should eventually move this back to task.id
|
||||
status: task.status,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
noop: task.noop,
|
||||
output: task.output as any,
|
||||
parentId: task.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
return filter.serialize();
|
||||
}
|
||||
|
||||
async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
@@ -783,6 +883,9 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
output: true,
|
||||
parentId: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
version: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export function generateSecret(): string {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
export function generateSecret(sizeInBytes = 32): string {
|
||||
return crypto.randomBytes(sizeInBytes).toString("hex");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
import { logger } from "../logger.server";
|
||||
|
||||
type FoundTask = Awaited<ReturnType<typeof findTask>>;
|
||||
|
||||
export class ProcessCallbackTimeoutService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string) {
|
||||
const task = await findTask(this.#prismaClient, id);
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.status !== "WAITING" || !task.callbackUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("ProcessCallbackTimeoutService.call", { task });
|
||||
|
||||
return await this.#failTask(task, "Remote callback timeout - no requests received");
|
||||
}
|
||||
|
||||
async #failTask(task: NonNullable<FoundTask>, error: string) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.taskAttempt.updateMany({
|
||||
where: {
|
||||
taskId: task.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
completedAt: new Date(),
|
||||
output: error,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#resumeRunExecution(task, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
|
||||
await enqueueRunExecutionV2(task.run, prisma, {
|
||||
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function findTask(prisma: PrismaClient, id: string) {
|
||||
return prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
queue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent.
|
||||
import { ActivateSourceService } from "./sources/activateSource.server";
|
||||
import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server";
|
||||
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
|
||||
import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout";
|
||||
import { addMissingVersionField } from "@trigger.dev/core";
|
||||
|
||||
const workerCatalog = {
|
||||
@@ -30,6 +31,9 @@ const workerCatalog = {
|
||||
}),
|
||||
scheduleEmail: DeliverEmailSchema,
|
||||
startRun: z.object({ id: z.string() }),
|
||||
processCallbackTimeout: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
performTaskOperation: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
@@ -161,7 +165,8 @@ function getWorkerQueue() {
|
||||
tasks: {
|
||||
"events.invokeDispatcher": {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
maxAttempts: 6,
|
||||
queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially
|
||||
handler: async (payload, job) => {
|
||||
const service = new InvokeDispatcherService();
|
||||
|
||||
@@ -239,6 +244,15 @@ function getWorkerQueue() {
|
||||
await service.call(payload.id);
|
||||
},
|
||||
},
|
||||
processCallbackTimeout: {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
handler: async (payload, job) => {
|
||||
const service = new ProcessCallbackTimeoutService();
|
||||
|
||||
await service.call(payload.id);
|
||||
},
|
||||
},
|
||||
performTaskOperation: {
|
||||
priority: 0, // smaller number = higher priority
|
||||
queueName: (payload) => `tasks:${payload.id}`,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { integrationCatalog } from "../app/services/externalApis/integrationCatalog.server";
|
||||
import { seedCloud } from "./seedCloud";
|
||||
import { prisma } from "../app/db.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
|
||||
async function seedIntegrationAuthMethods() {
|
||||
for (const [_, integration] of Object.entries(integrationCatalog.getIntegrations())) {
|
||||
@@ -67,12 +68,78 @@ async function seedIntegrationAuthMethods() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runDataMigrations() {
|
||||
await runStagingEnvironmentMigration();
|
||||
}
|
||||
|
||||
async function runStagingEnvironmentMigration() {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const existingDataMigration = await tx.dataMigration.findUnique({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
if (existingDataMigration) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tx.dataMigration.create({
|
||||
data: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Running data migration 2023-09-27-AddStagingEnvironments");
|
||||
|
||||
const projectsWithoutStagingEnvironments = await tx.project.findMany({
|
||||
where: {
|
||||
environments: {
|
||||
none: {
|
||||
type: "STAGING",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const project of projectsWithoutStagingEnvironments) {
|
||||
try {
|
||||
console.log(
|
||||
`Creating staging environment for project ${project.slug} on org ${project.organization.slug}`
|
||||
);
|
||||
|
||||
await createEnvironment(project.organization, project, "STAGING", undefined, tx);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
await tx.dataMigration.update({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
data: {
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
await seedIntegrationAuthMethods();
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.SEED_CLOUD === "enabled") {
|
||||
await seedCloud(prisma);
|
||||
}
|
||||
|
||||
await runDataMigrations();
|
||||
}
|
||||
|
||||
seed()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./node18.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"paths": {
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
|
||||
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"]
|
||||
},
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"baseUrl": ".",
|
||||
"stripInternal": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
|
||||
## Install and initial setup
|
||||
|
||||
`npm install`
|
||||
`pnpm install`
|
||||
|
||||
## Running the app
|
||||
|
||||
`npm run dev`
|
||||
`pnpm run dev --filter docs`
|
||||
|
||||
## View the app locally
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<Card title="React hooks" icon="fishing-rod" href="/documentation/guides/react-hooks">
|
||||
Show the live status of Job Runs in your React app
|
||||
</Card>
|
||||
@@ -27,6 +27,10 @@ The `DEV` environment should only be used for local development. It's where you
|
||||
|
||||
<Snippet file="scheduled-dev-warning.mdx" />
|
||||
|
||||
### Staging
|
||||
|
||||
The `STAGING` environment is useful for testing your Jobs against your staging server, if you have one. STAGING works identically to PROD.
|
||||
|
||||
### Production
|
||||
|
||||
The `PROD` environment is where your Jobs will run in production. It's where you can run your Jobs against real data.
|
||||
|
||||
@@ -48,38 +48,110 @@ This guide assumes that your project is already setup and you have a Job running
|
||||
</Accordion>
|
||||
|
||||
</Step>
|
||||
<Step title="Add the env var to your project">
|
||||
Add the `NEXT_PUBLIC_TRIGGER_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
<Step title="Setting up environment variables">
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
Add the `NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
Your private API key should already be in there.
|
||||
Your private API key should already be in there.
|
||||
|
||||
`NEXT_PUBLIC_` is a special prefix that exposes the environment variable to your users' web browsers.
|
||||
</Tab>
|
||||
|
||||
<Tab title="Remix/React">
|
||||
Add the `TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
```sh .env
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
You will need to pass this value from the server to the client. We recommend you do this in your Root loader.
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add the <TriggerProvider> component">
|
||||
|
||||
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
|
||||
|
||||
Generally you'll want to add this to the root of your app, so that it's available everywhere. However, you can add it lower in the hierarchy but it must be above any of the hooks.
|
||||
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Remix">
|
||||
|
||||
```tsx app/root.tsx
|
||||
//return the public key env var from the loader so it's available in the browser
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
//...other code
|
||||
|
||||
const triggerPublicApiKey = env.TRIGGER_PUBLIC_API_KEY!;
|
||||
|
||||
return json({
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
});
|
||||
}
|
||||
|
||||
//Your default export, i.e. the page component
|
||||
export default function App() {
|
||||
const {
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{/* wrap your outlet in this */}
|
||||
<TriggerProvider publicApiKey={triggerPublicApiKey}>
|
||||
<Outlet />
|
||||
</TriggerProvider>
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -87,3 +87,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
<CardGroup cols={2}>
|
||||
<Snippet file="card-react-hooks.mdx" />
|
||||
</CardGroup>
|
||||
|
||||
@@ -84,3 +84,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
<CardGroup cols={2}>
|
||||
<Snippet file="card-react-hooks.mdx" />
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: Replicate
|
||||
description: "Run machine learning tasks easily at scale"
|
||||
---
|
||||
|
||||
<Snippet file="integration-getting-started.mdx" />
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with the Replicate integration on Trigger.dev, you need to install the `@trigger.dev/replicate` package.
|
||||
You can do this using npm, pnpm, or yarn:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
To use the Replicate API with Trigger.dev, you have to provide an API Key.
|
||||
|
||||
### API Key
|
||||
|
||||
You can create an API Key in your [Account Settings](https://replicate.com/account/api-tokens).
|
||||
|
||||
```ts
|
||||
import { Replicate } from "@trigger.dev/replicate";
|
||||
|
||||
//this will use the passed in API key (defined in your environment variables)
|
||||
const replicate = new Replicate({
|
||||
id: "replicate",
|
||||
apiKey: process.env["REPLICATE_API_KEY"],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Include the Replicate integration in your Trigger.dev job.
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "replicate-cinematic-prompt",
|
||||
name: "Replicate - Cinematic Prompt",
|
||||
version: "0.1.0",
|
||||
integrations: { replicate },
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.cinematic",
|
||||
schema: z.object({
|
||||
prompt: z.string().default("rick astley riding a harley through post-apocalyptic miami"),
|
||||
version: z
|
||||
.string()
|
||||
.default("af1a68a271597604546c09c64aabcd7782c114a63539a4a8d14d1eeda5630c33"),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wait for prediction completion (uses remote callbacks internally)
|
||||
const prediction = await io.replicate.predictions.createAndAwait("await-prediction", {
|
||||
version: payload.version,
|
||||
input: {
|
||||
prompt: `${payload.prompt}, cinematic, 70mm, anamorphic, bokeh`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
});
|
||||
return prediction.output;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
You can paginate responses:
|
||||
|
||||
- Using the `getAll` helper
|
||||
- Using the `paginate` helper
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "replicate-pagination",
|
||||
name: "Replicate Pagination",
|
||||
version: "0.1.0",
|
||||
integrations: {
|
||||
replicate,
|
||||
},
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.paginate",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// getAll - returns an array of all results (uses paginate internally)
|
||||
const all = await io.replicate.getAll(io.replicate.predictions.list, "get-all");
|
||||
|
||||
// paginate - returns an async generator, useful to process one page at a time
|
||||
for await (const predictions of io.replicate.paginate(
|
||||
io.replicate.predictions.list,
|
||||
"paginate-all"
|
||||
)) {
|
||||
await io.logger.info("stats", {
|
||||
total: predictions.length,
|
||||
versions: predictions.map((p) => p.version),
|
||||
});
|
||||
}
|
||||
|
||||
return { count: all.length };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### Collections
|
||||
|
||||
| Function Name | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------- |
|
||||
| `collections.get` | Gets a collection. |
|
||||
| `collections.list` | Returns the first page of all collections. Use with pagination helper. |
|
||||
|
||||
### Deployments
|
||||
|
||||
| Function Name | Description |
|
||||
| ---------------------------------------- | --------------------------------------------------------- |
|
||||
| `deployments.predictions.create` | Creates a new prediction with a deployment. |
|
||||
| `deployments.predictions.createAndAwait` | Creates and waits for a new prediction with a deployment. |
|
||||
|
||||
### Models
|
||||
|
||||
| Function Name | Description |
|
||||
| ----------------- | ------------------------ |
|
||||
| `models.get` | Gets a model. |
|
||||
| `models.versions` | Gets a model version. |
|
||||
| `models.versions` | Gets all model versions. |
|
||||
|
||||
### Predictions
|
||||
|
||||
| Function Name | Description |
|
||||
| ---------------------------- | ---------------------------------------------------------------------- |
|
||||
| `predictions.cancel` | Cancels a prediction. |
|
||||
| `predictions.create` | Creates a prediction. |
|
||||
| `predictions.createAndAwait` | Creates and waits for a prediction. |
|
||||
| `predictions.get` | Gets a prediction. |
|
||||
| `predictions.list` | Returns the first page of all predictions. Use with pagination helper. |
|
||||
|
||||
### Trainings
|
||||
|
||||
| Function Name | Description |
|
||||
| -------------------------- | -------------------------------------------------------------------- |
|
||||
| `trainings.cancel` | Cancels a training. |
|
||||
| `trainings.create` | Creates a training. |
|
||||
| `trainings.createAndAwait` | Creates and waits for a training. |
|
||||
| `trainings.get` | Gets a training. |
|
||||
| `trainings.list` | Returns the first page of all trainings. Use with pagination helper. |
|
||||
|
||||
### Misc
|
||||
|
||||
| Function Name | Description |
|
||||
| ------------- | --------------------------------------------------- |
|
||||
| `getAll` | Pagination helper that returns an array of results. |
|
||||
| `paginate` | Pagination helper that returns an async generator. |
|
||||
| `request` | Sends authenticated requests to the Replicate API. |
|
||||
| `run` | Creates and waits for a prediction. |
|
||||
@@ -24,7 +24,7 @@ export class Github implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -30,14 +30,15 @@ description: "Integrations make it easy to authenticate and use APIs."
|
||||
|
||||
Navigate the menu or select Integrations from the table below.
|
||||
|
||||
| API | Description | Webhooks | Tasks |
|
||||
| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- |
|
||||
| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ |
|
||||
| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ |
|
||||
| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ |
|
||||
| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ |
|
||||
| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ |
|
||||
| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | 🕘 | ✅ |
|
||||
| [Slack](/integrations/apis/slack) | Send Slack messages | 🕘 | ✅ |
|
||||
| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | ✅ | ✅ |
|
||||
| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | ✅ | ✅ |
|
||||
| API | Description | Webhooks | Tasks |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------- | -------- | ----- |
|
||||
| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ |
|
||||
| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ |
|
||||
| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ |
|
||||
| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ |
|
||||
| [Replicate](/integrations/apis/replicate) | Run machine learning tasks easily at scale | N/A | ✅ |
|
||||
| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ |
|
||||
| [SendGrid](/integrations/apis/sendgrid) | Send emails using SendGrid | 🕘 | ✅ |
|
||||
| [Slack](/integrations/apis/slack) | Send Slack messages | 🕘 | ✅ |
|
||||
| [Supabase](/integrations/apis/supabase) | Interact with your projects and databases | ✅ | ✅ |
|
||||
| [Typeform](/integrations/apis/typeform) | Interact with the Typeform API and get notified of new responses | ✅ | ✅ |
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
"integrations/apis/linear",
|
||||
"integrations/apis/openai",
|
||||
"integrations/apis/plain",
|
||||
"integrations/apis/replicate",
|
||||
"integrations/apis/resend",
|
||||
"integrations/apis/sendgrid",
|
||||
"integrations/apis/slack",
|
||||
@@ -276,6 +277,7 @@
|
||||
"pages": [
|
||||
"sdk/triggerclient/instancemethods/sendevent",
|
||||
"sdk/triggerclient/instancemethods/getevent",
|
||||
"sdk/triggerclient/instancemethods/cancel-event",
|
||||
"sdk/triggerclient/instancemethods/getruns",
|
||||
"sdk/triggerclient/instancemethods/getrun",
|
||||
"sdk/triggerclient/instancemethods/define-job",
|
||||
|
||||
+62
-3
@@ -6,6 +6,8 @@ description: "`io.runTask()` allows you to run a [Task](/documentation/concepts/
|
||||
|
||||
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.
|
||||
|
||||
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()`.
|
||||
|
||||
## Parameters
|
||||
|
||||
<Snippet file="stable-key-param.mdx" />
|
||||
@@ -112,6 +114,22 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged.
|
||||
</Expandable>
|
||||
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="callback" type="object">
|
||||
An optional object that exposes settings for the remote callback feature.
|
||||
|
||||
Enabling this feature will expose a `callbackUrl` property on the callback's Task parameter. Additionally, `io.runTask()` will now return a Promise that resolves with the body of the first request sent to that URL.
|
||||
|
||||
<Expandable title="property fields">
|
||||
<ResponseField name="enabled" type="boolean" required>
|
||||
Whether to enable the remote callback feature.
|
||||
</ResponseField>
|
||||
<ResponseField name="timeoutInSeconds" type="number" required>
|
||||
The value of the property.
|
||||
</ResponseField>
|
||||
</Expandable>
|
||||
</ResponseField>
|
||||
|
||||
</Expandable>
|
||||
</ResponseField>
|
||||
|
||||
@@ -133,6 +151,8 @@ A Task is a resumable unit of a Run that can be retried, resumed and is logged.
|
||||
|
||||
A Promise that resolves with the returned value of the callback.
|
||||
|
||||
If the remote callback feature `options.callback` is enabled, the Promise will instead resolve with the body of the first request sent to `task.callbackUrl`.
|
||||
|
||||
<RequestExample>
|
||||
|
||||
```typescript Run a task
|
||||
@@ -150,11 +170,11 @@ client.defineJob({
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
//runTask
|
||||
const response = await io.runTask(
|
||||
const response = await io.github.runTask(
|
||||
"create-card",
|
||||
async () => {
|
||||
async (client) => {
|
||||
//create a project card using the underlying GitHub Integration client
|
||||
return io.github.client.rest.projects.createCard({
|
||||
return client.rest.projects.createCard({
|
||||
column_id: 123,
|
||||
note: "test",
|
||||
});
|
||||
@@ -201,4 +221,43 @@ client.defineJob({
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Remote callbacks
|
||||
client.defineJob({
|
||||
id: "remote-callback-example",
|
||||
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(
|
||||
"create-and-await-prediction",
|
||||
async (client, task) => {
|
||||
//create a prediction using the underlying Replicate Integration client
|
||||
await client.predictions.create({
|
||||
...payload,
|
||||
webhook: task.callbackUrl ?? "",
|
||||
webhook_events_filter: ["completed"],
|
||||
});
|
||||
//the actual return value will be the data sent to callbackUrl
|
||||
//cast to the exact data type you expect to receive or `any` if unsure
|
||||
return {} as Prediction;
|
||||
},
|
||||
{
|
||||
name: "Create and await Prediction",
|
||||
icon: "replicate",
|
||||
//remote callback settings
|
||||
callback: {
|
||||
enabled: true,
|
||||
timeoutInSeconds: 300,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//log the prediction output
|
||||
await io.logger.info(prediction.output);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</RequestExample>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: "TriggerClient: cancelEvent() instance method"
|
||||
sidebarTitle: "cancelEvent()"
|
||||
description: "The `cancelEvent()` instance method will cancel an event that is scheduled to be delivered in the future."
|
||||
---
|
||||
|
||||
If you've scheduled an event to be delivered in the future, you can cancel it using the `cancelEvent()` instance method, passing in the ID of the event you want to cancel. This will prevent any jobs listening for that event from being triggered.
|
||||
|
||||
```ts
|
||||
// Sending an event that will be delivered in 24 hours
|
||||
const event = await client.sendEvent(
|
||||
{
|
||||
id: "event_12345",
|
||||
name: "my.event",
|
||||
payload: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours
|
||||
}
|
||||
);
|
||||
|
||||
// Sometime later, cancel the event by ID
|
||||
await client.cancelEvent(event.id);
|
||||
```
|
||||
|
||||
<Note>
|
||||
Cancelling an event after it has already triggered a job run does not cancel the job run.
|
||||
Cancelling events only prevent the event from triggering future job runs.
|
||||
</Note>
|
||||
@@ -12,6 +12,11 @@ export const client = new TriggerClient({
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The `TriggerClient` should only ever be used in a server-side environment. It is not safe to use
|
||||
in a browser environment because it exposes your API Key.
|
||||
</Warning>
|
||||
|
||||
## Constructor
|
||||
|
||||
### [TriggerClient()](/sdk/triggerclient/constructor)
|
||||
@@ -36,6 +41,10 @@ You can call this function from anywhere in your code to send an event. The othe
|
||||
|
||||
The `getEvent()` method gets the event details for a given eventId.
|
||||
|
||||
#### [cancelEvent()](/sdk/triggerclient/instancemethods/cancel-event)
|
||||
|
||||
The `cancelEvent()` method cancels an event that is scheduled to be delivered in the future.
|
||||
|
||||
#### [getRuns()](/sdk/triggerclient/instancemethods/getruns)
|
||||
|
||||
The `getRuns()` method gets runs for a Job.
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for airtable",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@ export class Airtable implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official GitHub integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -29,8 +29,8 @@
|
||||
"@octokit/request": "^6.2.5",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/webhooks": "^10.4.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"octokit": "^2.0.14",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
|
||||
@@ -138,7 +138,7 @@ export class Github implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- 81e886a1: Fix `getAll` helper and search function params
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/linear",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for @linear/sdk",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@linear/sdk": "^8.0.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -158,7 +158,7 @@ export class Linear implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
@@ -182,7 +182,7 @@ export class Linear implements TriggerIntegration {
|
||||
>(
|
||||
task: TTask,
|
||||
key: IntegrationTaskKey,
|
||||
params: Nullable<QueryVariables> = {}
|
||||
params: Parameters<TTask>[1] = {}
|
||||
): Promise<Awaited<ReturnType<TTask>>["nodes"]> {
|
||||
const boundTask = task.bind(this as any);
|
||||
|
||||
@@ -695,7 +695,7 @@ export class Linear implements TriggerIntegration {
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
term: string;
|
||||
variables?: L.SearchDocumentsQueryVariables;
|
||||
variables?: Parameters<LinearClient["searchDocuments"]>[1];
|
||||
}
|
||||
): LinearReturnType<DocumentSearchPayload> {
|
||||
return this.runTask(
|
||||
@@ -862,7 +862,7 @@ export class Linear implements TriggerIntegration {
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
term: string;
|
||||
variables?: L.SearchIssuesQueryVariables;
|
||||
variables?: Parameters<LinearClient["searchIssues"]>[1];
|
||||
}
|
||||
): LinearReturnType<IssueSearchPayload> {
|
||||
return this.runTask(
|
||||
@@ -1273,7 +1273,7 @@ export class Linear implements TriggerIntegration {
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
term: string;
|
||||
variables?: L.SearchProjectsQueryVariables;
|
||||
variables?: Parameters<LinearClient["searchProjects"]>[1];
|
||||
}
|
||||
): LinearReturnType<ProjectSearchPayload> {
|
||||
return this.runTask(
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official OpenAI integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"openai": "^4.2.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6"
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/plain",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official Plain.com integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -24,8 +24,8 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"@team-plain/typescript-sdk": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# @trigger.dev/replicate
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
@@ -0,0 +1 @@
|
||||
# @trigger.dev/replicate
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@trigger.dev/replicate",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for replicate",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.d.ts",
|
||||
"dist/index.js.map"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/node": "16.x",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsup": "7.1.x",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && npm run build:tsup",
|
||||
"build:tsup": "tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"replicate": "^0.18.1",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Page, Collection } from "replicate";
|
||||
|
||||
import { ReplicateRunTask } from "./index";
|
||||
import { ReplicateReturnType } from "./types";
|
||||
|
||||
export class Collections {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Fetch a model collection. */
|
||||
get(key: IntegrationTaskKey, params: { slug: string }): ReplicateReturnType<Collection> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.collections.get(params.slug);
|
||||
},
|
||||
{
|
||||
name: "Get Collection",
|
||||
params,
|
||||
properties: [{ label: "Collection Slug", text: params.slug }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch a list of model collections. */
|
||||
list(key: IntegrationTaskKey): ReplicateReturnType<Page<Collection>> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.collections.list();
|
||||
},
|
||||
{
|
||||
name: "List Collections",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import ReplicateClient, { Prediction } from "replicate";
|
||||
|
||||
import { ReplicateRunTask } from "./index";
|
||||
import { callbackProperties, createDeploymentProperties } from "./utils";
|
||||
import { CallbackTimeout, ReplicateReturnType } from "./types";
|
||||
|
||||
export class Deployments {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
get predictions() {
|
||||
return new Predictions(this.runTask);
|
||||
}
|
||||
}
|
||||
|
||||
class Predictions {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Create a new prediction with a deployment. */
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
deployment_owner: string;
|
||||
deployment_name: string;
|
||||
} & Parameters<ReplicateClient["deployments"]["predictions"]["create"]>[2]
|
||||
): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
const { deployment_owner, deployment_name, ...options } = params;
|
||||
|
||||
return client.deployments.predictions.create(deployment_owner, deployment_name, options);
|
||||
},
|
||||
{
|
||||
name: "Create Prediction With Deployment",
|
||||
params,
|
||||
properties: createDeploymentProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a new prediction with a deployment and await the result. */
|
||||
createAndAwait(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
deployment_owner: string;
|
||||
deployment_name: string;
|
||||
} & Omit<
|
||||
Parameters<ReplicateClient["deployments"]["predictions"]["create"]>[2],
|
||||
"webhook" | "webhook_events_filter"
|
||||
>,
|
||||
options: CallbackTimeout = { timeoutInSeconds: 3600 }
|
||||
): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client, task) => {
|
||||
const { deployment_owner, deployment_name, ...options } = params;
|
||||
|
||||
return client.deployments.predictions.create(deployment_owner, deployment_name, {
|
||||
...options,
|
||||
webhook: task.callbackUrl ?? "",
|
||||
webhook_events_filter: ["completed"],
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create And Await Prediction With Deployment",
|
||||
params,
|
||||
properties: [...createDeploymentProperties(params), ...callbackProperties(options)],
|
||||
callback: {
|
||||
enabled: true,
|
||||
timeoutInSeconds: options.timeoutInSeconds,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
TriggerIntegration,
|
||||
RunTaskOptions,
|
||||
IO,
|
||||
IOTask,
|
||||
IntegrationTaskKey,
|
||||
RunTaskErrorCallback,
|
||||
Json,
|
||||
retry,
|
||||
ConnectionAuth,
|
||||
} from "@trigger.dev/sdk";
|
||||
import ReplicateClient, { Page, Prediction } from "replicate";
|
||||
|
||||
import { Predictions } from "./predictions";
|
||||
import { Models } from "./models";
|
||||
import { Trainings } from "./trainings";
|
||||
import { Collections } from "./collections";
|
||||
import { ReplicateReturnType } from "./types";
|
||||
import { Deployments } from "./deployments";
|
||||
|
||||
export type ReplicateIntegrationOptions = {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type ReplicateRunTask = InstanceType<typeof Replicate>["runTask"];
|
||||
|
||||
export class Replicate implements TriggerIntegration {
|
||||
private _options: ReplicateIntegrationOptions;
|
||||
private _client?: any;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(private options: ReplicateIntegrationOptions) {
|
||||
if (Object.keys(options).includes("apiKey") && !options.apiKey) {
|
||||
throw `Can't create Replicate integration (${options.id}) as apiKey was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
return "LOCAL" as const;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "replicate", name: "Replicate" };
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const replicate = new Replicate(this._options);
|
||||
replicate._io = io;
|
||||
replicate._connectionKey = connectionKey;
|
||||
replicate._client = this.createClient(auth);
|
||||
return replicate;
|
||||
}
|
||||
|
||||
createClient(auth?: ConnectionAuth) {
|
||||
return new ReplicateClient({
|
||||
auth: this._options.apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: ReplicateClient, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "replicate",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback ?? onError
|
||||
);
|
||||
}
|
||||
|
||||
get collections() {
|
||||
return new Collections(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get deployments() {
|
||||
return new Deployments(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get models() {
|
||||
return new Models(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get predictions() {
|
||||
return new Predictions(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
get trainings() {
|
||||
return new Trainings(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
/** Paginate through a list of results. */
|
||||
async *paginate<T>(
|
||||
task: (key: string) => Promise<Page<T>>,
|
||||
key: IntegrationTaskKey,
|
||||
counter: number = 0
|
||||
): AsyncGenerator<T[]> {
|
||||
const boundTask = task.bind(this as any);
|
||||
|
||||
const page = await boundTask(`${key}-${counter}`);
|
||||
yield page.results;
|
||||
|
||||
if (page.next) {
|
||||
const nextStep = counter++;
|
||||
|
||||
const nextPage = () => {
|
||||
return this.request<Page<T>>(`${key}-${nextStep}`, {
|
||||
route: page.next!,
|
||||
options: { method: "GET" },
|
||||
});
|
||||
};
|
||||
|
||||
yield* this.paginate(nextPage, key, nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
/** Auto-paginate and return all results. */
|
||||
async getAll<T>(
|
||||
task: (key: string) => Promise<Page<T>>,
|
||||
key: IntegrationTaskKey
|
||||
): ReplicateReturnType<T[]> {
|
||||
const allResults: T[] = [];
|
||||
|
||||
for await (const results of this.paginate(task, key)) {
|
||||
allResults.push(...results);
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
/** Make a request to the Replicate API. */
|
||||
request<T = any>(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
route: string | URL;
|
||||
options: Parameters<ReplicateClient["request"]>[1];
|
||||
}
|
||||
): ReplicateReturnType<T> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const response = await client.request(params.route, params.options);
|
||||
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
name: "Send Request",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Route", text: params.route.toString() },
|
||||
...(params.options.method ? [{ label: "Method", text: params.options.method }] : []),
|
||||
],
|
||||
callback: { enabled: true },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Run a model and await the result. */
|
||||
run(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
identifier: Parameters<ReplicateClient["run"]>[0];
|
||||
} & Omit<
|
||||
Parameters<ReplicateClient["run"]>[1],
|
||||
"webhook" | "webhook_events_filter" | "wait" | "signal"
|
||||
>
|
||||
): ReplicateReturnType<Prediction> {
|
||||
const { identifier, ...paramsWithoutIdentifier } = params;
|
||||
|
||||
// see: https://github.com/replicate/replicate-javascript/blob/4b0d9cb0e226fab3d3d31de5b32261485acf5626/index.js#L102
|
||||
|
||||
const namePattern = /[a-zA-Z0-9]+(?:(?:[._]|__|[-]*)[a-zA-Z0-9]+)*/;
|
||||
const pattern = new RegExp(
|
||||
`^(?<owner>${namePattern.source})/(?<name>${namePattern.source}):(?<version>[0-9a-fA-F]+)$`
|
||||
);
|
||||
|
||||
const match = identifier.match(pattern);
|
||||
|
||||
if (!match || !match.groups) {
|
||||
throw new Error('Invalid version. It must be in the format "owner/name:version"');
|
||||
}
|
||||
|
||||
const { version } = match.groups;
|
||||
|
||||
return this.predictions.createAndAwait(key, { ...paramsWithoutIdentifier, version });
|
||||
}
|
||||
|
||||
// TODO: wait(prediction) - needs polling
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly request: Request,
|
||||
readonly response: Response
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
function isReplicateApiError(error: unknown): error is ApiError {
|
||||
if (typeof error !== "object" || error === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const apiError = error as ApiError;
|
||||
|
||||
return (
|
||||
apiError.name === "ApiError" &&
|
||||
apiError.request instanceof Request &&
|
||||
apiError.response instanceof Response
|
||||
);
|
||||
}
|
||||
|
||||
function shouldRetry(method: string, status: number) {
|
||||
return status === 429 || (method === "GET" && status >= 500);
|
||||
}
|
||||
|
||||
export function onError(error: unknown): ReturnType<RunTaskErrorCallback> {
|
||||
if (!isReplicateApiError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRetry(error.request.method, error.response.status)) {
|
||||
return {
|
||||
skipRetrying: true,
|
||||
};
|
||||
}
|
||||
|
||||
// see: https://github.com/replicate/replicate-javascript/blob/4b0d9cb0e226fab3d3d31de5b32261485acf5626/lib/util.js#L43
|
||||
|
||||
const retryAfter = error.response.headers.get("retry-after");
|
||||
|
||||
if (retryAfter) {
|
||||
const resetDate = new Date(retryAfter);
|
||||
|
||||
if (!Number.isNaN(resetDate.getTime())) {
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rateLimitRemaining = error.response.headers.get("ratelimit-remaining");
|
||||
const rateLimitReset = error.response.headers.get("ratelimit-reset");
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
if (!Number.isNaN(resetDate.getTime())) {
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Model, ModelVersion } from "replicate";
|
||||
|
||||
import { ReplicateRunTask } from "./index";
|
||||
import { modelProperties } from "./utils";
|
||||
import { ReplicateReturnType } from "./types";
|
||||
|
||||
export class Models {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Get information about a model. */
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): ReplicateReturnType<Model> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.get(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "Get Model",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return new Versions(this.runTask);
|
||||
}
|
||||
}
|
||||
|
||||
class Versions {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Get a specific model version. */
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
}
|
||||
): ReplicateReturnType<ModelVersion> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.get(params.model_owner, params.model_name, params.version_id);
|
||||
},
|
||||
{
|
||||
name: "Get Model Version",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** List model versions. */
|
||||
list(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): ReplicateReturnType<ModelVersion[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.list(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "List Models",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import ReplicateClient, { Page, Prediction } from "replicate";
|
||||
|
||||
import { ReplicateRunTask } from "./index";
|
||||
import { CallbackTimeout, ReplicateReturnType } from "./types";
|
||||
import { callbackProperties, createPredictionProperties } from "./utils";
|
||||
|
||||
export class Predictions {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Cancel a prediction. */
|
||||
cancel(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.predictions.cancel(params.id);
|
||||
},
|
||||
{
|
||||
name: "Cancel Prediction",
|
||||
params,
|
||||
properties: [{ label: "Prediction ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a new prediction. */
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: Parameters<ReplicateClient["predictions"]["create"]>[0]
|
||||
): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.predictions.create(params);
|
||||
},
|
||||
{
|
||||
name: "Create Prediction",
|
||||
params,
|
||||
properties: createPredictionProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a new prediction and await the result. */
|
||||
createAndAwait(
|
||||
key: IntegrationTaskKey,
|
||||
params: Omit<
|
||||
Parameters<ReplicateClient["predictions"]["create"]>[0],
|
||||
"webhook" | "webhook_events_filter"
|
||||
>,
|
||||
options: CallbackTimeout = { timeoutInSeconds: 3600 }
|
||||
): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client, task) => {
|
||||
return client.predictions.create({
|
||||
...params,
|
||||
webhook: task.callbackUrl ?? "",
|
||||
webhook_events_filter: ["completed"],
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create And Await Prediction",
|
||||
params,
|
||||
properties: [...createPredictionProperties(params), ...callbackProperties(options)],
|
||||
callback: {
|
||||
enabled: true,
|
||||
timeoutInSeconds: options.timeoutInSeconds,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch a prediction. */
|
||||
get(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType<Prediction> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.predictions.get(params.id);
|
||||
},
|
||||
{
|
||||
name: "Get Prediction",
|
||||
params,
|
||||
properties: [{ label: "Prediction ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** List all predictions. */
|
||||
list(key: IntegrationTaskKey): ReplicateReturnType<Page<Prediction>> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.predictions.list();
|
||||
},
|
||||
{
|
||||
name: "List Predictions",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import ReplicateClient, { Page, Training } from "replicate";
|
||||
|
||||
import { ReplicateRunTask } from "./index";
|
||||
import { CallbackTimeout, ReplicateReturnType } from "./types";
|
||||
import { callbackProperties, modelProperties } from "./utils";
|
||||
|
||||
export class Trainings {
|
||||
constructor(private runTask: ReplicateRunTask) {}
|
||||
|
||||
/** Cancel a training. */
|
||||
cancel(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType<Training> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.trainings.cancel(params.id);
|
||||
},
|
||||
{
|
||||
name: "Cancel Training",
|
||||
params,
|
||||
properties: [{ label: "Training ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a new training. */
|
||||
create(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
} & Parameters<ReplicateClient["trainings"]["create"]>[3]
|
||||
): ReplicateReturnType<Training> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
const { model_owner, model_name, version_id, ...options } = params;
|
||||
|
||||
return client.trainings.create(model_owner, model_name, version_id, options);
|
||||
},
|
||||
{
|
||||
name: "Create Training",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a new training and await the result. */
|
||||
createAndAwait(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
} & Omit<
|
||||
Parameters<ReplicateClient["trainings"]["create"]>[3],
|
||||
"webhook" | "webhook_events_filter"
|
||||
>,
|
||||
options: CallbackTimeout = { timeoutInSeconds: 3600 }
|
||||
): ReplicateReturnType<Training> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client, task) => {
|
||||
const { model_owner, model_name, version_id, ...options } = params;
|
||||
|
||||
return client.trainings.create(model_owner, model_name, version_id, {
|
||||
...options,
|
||||
webhook: task.callbackUrl ?? "",
|
||||
webhook_events_filter: ["completed"],
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create And Await Training",
|
||||
params,
|
||||
properties: [...modelProperties(params), ...callbackProperties(options)],
|
||||
callback: {
|
||||
enabled: true,
|
||||
timeoutInSeconds: options.timeoutInSeconds,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch a training. */
|
||||
get(key: IntegrationTaskKey, params: { id: string }): ReplicateReturnType<Training> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.trainings.get(params.id);
|
||||
},
|
||||
{
|
||||
name: "Get Training",
|
||||
params,
|
||||
properties: [{ label: "Training ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** List all trainings. */
|
||||
list(key: IntegrationTaskKey): ReplicateReturnType<Page<Training>> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
return client.trainings.list();
|
||||
},
|
||||
{
|
||||
name: "List Trainings",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CallbackTimeout = { timeoutInSeconds?: number };
|
||||
|
||||
export type ReplicateReturnType<T> = Promise<T>;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CallbackTimeout } from "./types";
|
||||
|
||||
export const createPredictionProperties = (
|
||||
params: Partial<{
|
||||
version: string;
|
||||
stream: boolean;
|
||||
}>
|
||||
) => {
|
||||
return [
|
||||
...(params.version ? [{ label: "Model Version", text: params.version }] : []),
|
||||
...streamingProperty(params),
|
||||
];
|
||||
};
|
||||
|
||||
export const createDeploymentProperties = (
|
||||
params: Partial<{
|
||||
deployment_owner: string;
|
||||
deployment_name: string;
|
||||
stream: boolean;
|
||||
}>
|
||||
) => {
|
||||
return [
|
||||
...(params.deployment_owner
|
||||
? [{ label: "Deployment Owner", text: params.deployment_owner }]
|
||||
: []),
|
||||
...(params.deployment_name ? [{ label: "Deployment Name", text: params.deployment_name }] : []),
|
||||
...streamingProperty(params),
|
||||
];
|
||||
};
|
||||
|
||||
export const modelProperties = (
|
||||
params: Partial<{
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
destination: string;
|
||||
}>
|
||||
) => {
|
||||
return [
|
||||
...(params.model_owner ? [{ label: "Model Owner", text: params.model_owner }] : []),
|
||||
...(params.model_name ? [{ label: "Model Name", text: params.model_name }] : []),
|
||||
...(params.version_id ? [{ label: "Model Version", text: params.version_id }] : []),
|
||||
...(params.destination ? [{ label: "Destination Model", text: params.destination }] : []),
|
||||
];
|
||||
};
|
||||
|
||||
export const streamingProperty = (params: { stream?: boolean }) => {
|
||||
return [{ label: "Streaming Enabled", text: String(!!params.stream) }];
|
||||
};
|
||||
|
||||
export const callbackProperties = (options: CallbackTimeout) => {
|
||||
return [
|
||||
{
|
||||
label: "Callback Timeout",
|
||||
text: options.timeoutInSeconds ? `${options.timeoutInSeconds}s` : "default",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/integration.json",
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"],
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: "main",
|
||||
entry: ["./src/index.ts"],
|
||||
outDir: "./dist",
|
||||
platform: "node",
|
||||
format: ["cjs"],
|
||||
legacyOutput: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
dts: true,
|
||||
treeshake: {
|
||||
preset: "smallest",
|
||||
},
|
||||
esbuildPlugins: [],
|
||||
external: ["http", "https", "util", "events", "tty", "os", "timers"],
|
||||
},
|
||||
]);
|
||||
@@ -1,5 +1,23 @@
|
||||
# @trigger.dev/resend
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/resend",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official Resend.com integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -24,8 +24,8 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"resend": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -100,7 +100,7 @@ export class Resend implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @trigger.dev/sendgrid
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sendgrid",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for @sendgrid/mail",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6"
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -70,7 +70,7 @@ export class SendGrid implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a992a19: First release of `@trigger.dev/replicate` integration with remote callback support.
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/slack",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official Slack integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/web-api": "^6.8.1",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -92,7 +92,7 @@ export class Slack implements TriggerIntegration {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
return this._io.runTask(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @trigger.dev/stripe
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/stripe",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for stripe",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"stripe": "^12.14.0",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @trigger.dev/supabase
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/supabase",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "Trigger.dev integration for @supabase/supabase-js",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.26.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"supabase-management-js": "^0.1.4",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @trigger.dev/typeform
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
- @trigger.dev/integration-kit@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@2.1.7
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/typeform",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The official Typeform integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@typeform/api-client": "^1.8.0",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.6",
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8",
|
||||
"@trigger.dev/integration-kit": "workspace:^2.1.8",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @trigger.dev/astro
|
||||
|
||||
## 2.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ab9e4a98: Send client version back to the server via headers
|
||||
- Updated dependencies [6a992a19]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- Updated dependencies [ab9e4a98]
|
||||
- @trigger.dev/sdk@2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@2.1.7
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@trigger.dev/astro",
|
||||
"description": "An Astro-native integration for Trigger.dev background jobs platform",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
@@ -20,7 +20,7 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@trigger.dev/sdk": "workspace:^2.1.6"
|
||||
"@trigger.dev/sdk": "workspace:^2.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^3.0.12",
|
||||
|
||||
@@ -42,6 +42,7 @@ export function createAstroRoute(client: TriggerClient) {
|
||||
// execution's response body
|
||||
return new Response(JSON.stringify(response.body), {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# create-trigger
|
||||
|
||||
## 2.1.8
|
||||
|
||||
## 2.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7760e094: Improved CLI init Next.js middleware detection
|
||||
|
||||
## 2.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/cli",
|
||||
"version": "2.1.6",
|
||||
"version": "2.1.8",
|
||||
"description": "The Trigger.dev CLI",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getEnvFilename,
|
||||
setApiKeyEnvironmentVariable,
|
||||
setApiUrlEnvironmentVariable,
|
||||
setPublicApiKeyEnvironmentVariable,
|
||||
} from "../utils/env";
|
||||
import { readJSONFile } from "../utils/fileSystem";
|
||||
import { PackageManager, getUserPackageManager } from "../utils/getUserPkgManager";
|
||||
@@ -114,6 +115,12 @@ export const initCommand = async (options: InitCommandOptions) => {
|
||||
}
|
||||
await setApiKeyEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiKey);
|
||||
await setApiUrlEnvironmentVariable(resolvedPath, envName, resolvedOptions.apiUrl);
|
||||
await setPublicApiKeyEnvironmentVariable(
|
||||
resolvedPath,
|
||||
envName,
|
||||
framework.publicKeyEnvName,
|
||||
authorizedKey.pkApiKey
|
||||
);
|
||||
|
||||
const installOptions = {
|
||||
typescript: isTypescriptProject,
|
||||
|
||||
@@ -27,6 +27,9 @@ export interface Framework {
|
||||
/** Priority list of env filenames, e.g. ".env" */
|
||||
possibleEnvFilenames(): string[];
|
||||
|
||||
/** Defaults to TRIGGER_PUBLIC_API_KEY */
|
||||
publicKeyEnvName?: string;
|
||||
|
||||
/** Install the required files */
|
||||
install(path: string, options: ProjectInstallOptions): Promise<void>;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { logger } from "../../utils/logger";
|
||||
import { getPathAlias } from "../../utils/pathAlias";
|
||||
import { readPackageJson } from "../../utils/readPackageJson";
|
||||
import { standardWatchFilePaths } from "../watchConfig";
|
||||
import { telemetryClient } from "../../telemetry/telemetry";
|
||||
import { detectMiddlewareUsage } from "./middleware";
|
||||
|
||||
export class NextJs implements Framework {
|
||||
@@ -37,6 +38,8 @@ export class NextJs implements Framework {
|
||||
return [".env.local", ".env"];
|
||||
}
|
||||
|
||||
publicKeyEnvName = "NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY";
|
||||
|
||||
async install(
|
||||
path: string,
|
||||
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
|
||||
@@ -65,7 +68,27 @@ export class NextJs implements Framework {
|
||||
path: string,
|
||||
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
|
||||
): Promise<void> {
|
||||
await detectMiddlewareUsage(path);
|
||||
const result = await detectMiddlewareUsage(path, options.typescript);
|
||||
if (result.hasMiddleware) {
|
||||
switch (result.conflict) {
|
||||
case "possible": {
|
||||
logger.warn(
|
||||
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
break;
|
||||
}
|
||||
case "likely": {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultHostnames = ["localhost"];
|
||||
|
||||
@@ -4,66 +4,87 @@ import pathModule from "path";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { telemetryClient } from "../../telemetry/telemetry";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
import { detectUseOfSrcDir } from ".";
|
||||
|
||||
export async function detectMiddlewareUsage(path: string, usesSrcDir = false) {
|
||||
const middlewarePath = pathModule.join(path, usesSrcDir ? "src" : "", "middleware.ts");
|
||||
type Result =
|
||||
| {
|
||||
hasMiddleware: false;
|
||||
}
|
||||
| {
|
||||
hasMiddleware: true;
|
||||
conflict: "unlikely" | "possible" | "likely";
|
||||
middlewarePath: string;
|
||||
};
|
||||
|
||||
const middlewareExists = await pathExists(middlewarePath);
|
||||
export async function detectMiddlewareUsage(path: string, typescript: boolean): Promise<Result> {
|
||||
const usesSrcDir = await detectUseOfSrcDir(path);
|
||||
const middlewarePath = pathModule.join(
|
||||
path,
|
||||
usesSrcDir ? "src" : "",
|
||||
`middleware.${typescript ? "ts" : "js"}`
|
||||
);
|
||||
|
||||
if (!middlewareExists) {
|
||||
return;
|
||||
try {
|
||||
return await detectMiddleware(path, typescript, middlewarePath);
|
||||
} catch (e) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: pathModule.relative(process.cwd(), middlewarePath),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function detectMiddleware(
|
||||
path: string,
|
||||
typescript: boolean,
|
||||
middlewarePath: string
|
||||
): Promise<Result> {
|
||||
const middlewareExists = await pathExists(middlewarePath);
|
||||
if (!middlewareExists) {
|
||||
return { hasMiddleware: false };
|
||||
}
|
||||
|
||||
const middlewareRelativeFilePath = pathModule.relative(process.cwd(), middlewarePath);
|
||||
|
||||
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
|
||||
|
||||
if (!matcher || matcher.length === 0) {
|
||||
logger.warn(
|
||||
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
return;
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (matcher.length === 0) {
|
||||
return;
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "unlikely",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof matcher === "string") {
|
||||
const matcherRegex = pathToRegexp(matcher);
|
||||
|
||||
// Check to see if /api/trigger matches the regex, if it does, then we need to output a warning with a link to the docs to fix it
|
||||
if (matcherRegex.test("/api/trigger")) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict_api_trigger", { projectPath: path });
|
||||
}
|
||||
} else if (Array.isArray(matcher) && matcher.every((m) => typeof m === "string")) {
|
||||
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
|
||||
|
||||
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
|
||||
logger.warn(
|
||||
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
|
||||
process.cwd(),
|
||||
middlewarePath
|
||||
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
|
||||
);
|
||||
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
|
||||
}
|
||||
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
|
||||
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "likely",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasMiddleware: true,
|
||||
conflict: "possible",
|
||||
middlewarePath: middlewareRelativeFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
|
||||
const fileContent = await fs.readFile(path, "utf-8");
|
||||
|
||||
const regex = /matcher:\s*(\[.*\]|".*")/s;
|
||||
const regex = /matcher:\s*(\[.*\]|["'].*["'])/g;
|
||||
let match = regex.exec(fileContent);
|
||||
|
||||
if (!match) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user