Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91afa5ebbf | |||
| 65262dc3d7 | |||
| 9105701ae0 | |||
| 9b35cc484b | |||
| 30a04a5a06 | |||
| 493315af48 | |||
| 8db1da69e9 | |||
| cd7a45101e | |||
| 29d107dc0a | |||
| cf7dc8d719 | |||
| 9ced599b19 | |||
| 67592ec2b4 | |||
| f7bf7bc268 | |||
| bb57426a0d | |||
| 3ab7eb9c7a | |||
| 2892efad04 | |||
| 979ba51d2f | |||
| 364ea565ed | |||
| 58252728f6 | |||
| 5eaad0577e | |||
| 00f1103deb | |||
| c37622e7b6 | |||
| d67023aa8f | |||
| d9bfe55a8c | |||
| f4a18feca0 |
@@ -16,7 +16,7 @@ const variations = {
|
||||
"flex items-center h-[1.5rem] gap-x-1.5 rounded hover:bg-tertiary disabled:hover:bg-transparent pr-1 py-[0.1rem] pl-1.5 transition focus-custom disabled:hover:text-charcoal-400 disabled:opacity-50 text-charcoal-400 hover:text-charcoal-200 disabled:hover:cursor-not-allowed hover:cursor-pointer",
|
||||
root: "h-3 w-6",
|
||||
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs transition",
|
||||
text: "text-xs",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
|
||||
type CheckBatchCompletionDialogProps = {
|
||||
batchId: string;
|
||||
redirectPath: string;
|
||||
};
|
||||
|
||||
export function CheckBatchCompletionDialog({
|
||||
batchId,
|
||||
redirectPath,
|
||||
}: CheckBatchCompletionDialogProps) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const formAction = `/resources/batches/${batchId}/check-completion`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
|
||||
return (
|
||||
<DialogContent key="check-completion">
|
||||
<DialogHeader>Try and resume batch</DialogHeader>
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<Paragraph>
|
||||
In rare cases, parent runs don't continue after child runs have completed.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
If this doesn't help, please get in touch. We are working on a permanent fix for this.
|
||||
</Paragraph>
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Form action={`/resources/batches/${batchId}/check-completion`} method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
name="redirectUrl"
|
||||
value={redirectPath}
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : undefined}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Attempting resume..." : "Attempt resume"}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
cancelButton={
|
||||
<DialogClose asChild>
|
||||
<Button variant={"tertiary/medium"}>Cancel</Button>
|
||||
</DialogClose>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export const TaskRunListSearchFilters = z.object({
|
||||
bulkId: z.string().optional(),
|
||||
from: z.coerce.number().optional(),
|
||||
to: z.coerce.number().optional(),
|
||||
showChildTasks: z.coerce.boolean().optional(),
|
||||
rootOnly: z.coerce.boolean().optional(),
|
||||
batchId: z.string().optional(),
|
||||
runId: z.string().optional(),
|
||||
scheduleId: z.string().optional(),
|
||||
@@ -119,6 +119,7 @@ type RunFiltersProps = {
|
||||
type: BulkActionType;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
rootOnlyDefault: boolean;
|
||||
hasFilters: boolean;
|
||||
};
|
||||
|
||||
@@ -141,16 +142,12 @@ export function RunsFilters(props: RunFiltersProps) {
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<FilterMenu {...props} />
|
||||
<ShowChildTasksToggle />
|
||||
<RootOnlyToggle defaultValue={props.rootOnlyDefault} />
|
||||
<AppliedFilters {...props} />
|
||||
{hasFilters && (
|
||||
<Form className="h-6">
|
||||
{searchParams.has("showChildTasks") && (
|
||||
<input
|
||||
type="hidden"
|
||||
name="showChildTasks"
|
||||
value={searchParams.get("showChildTasks") as string}
|
||||
/>
|
||||
{searchParams.has("rootOnly") && (
|
||||
<input type="hidden" name="rootOnly" value={searchParams.get("rootOnly") as string} />
|
||||
)}
|
||||
<Button variant="minimal/small" LeadingIcon={TrashIcon}>
|
||||
Clear all
|
||||
@@ -707,26 +704,27 @@ function AppliedTagsFilter() {
|
||||
);
|
||||
}
|
||||
|
||||
function ShowChildTasksToggle() {
|
||||
const { value, replace } = useSearchParams();
|
||||
|
||||
const showChildTasks = value("showChildTasks") === "true";
|
||||
function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) {
|
||||
const { value, values, replace } = useSearchParams();
|
||||
const searchValue = value("rootOnly");
|
||||
const rootOnly = searchValue !== undefined ? searchValue === "true" : defaultValue;
|
||||
|
||||
const batchId = value("batchId");
|
||||
const runId = value("runId");
|
||||
const scheduleId = value("scheduleId");
|
||||
const tasks = values("tasks");
|
||||
|
||||
const disabled = !!batchId || !!runId || !!scheduleId;
|
||||
const disabled = !!batchId || !!runId || !!scheduleId || tasks.length > 0;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
variant="small"
|
||||
label="Show child runs"
|
||||
checked={disabled ? true : showChildTasks}
|
||||
label="Root only"
|
||||
checked={disabled ? false : rootOnly}
|
||||
onCheckedChange={(checked) => {
|
||||
replace({
|
||||
showChildTasks: checked ? "true" : undefined,
|
||||
rootOnly: checked ? "true" : "false",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -1023,7 +1021,7 @@ function ScheduleIdDropdown({
|
||||
>
|
||||
<div className="flex flex-col gap-4 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>Batch ID</Label>
|
||||
<Label>Schedule ID</Label>
|
||||
<Input
|
||||
placeholder="sched_"
|
||||
value={scheduleId ?? ""}
|
||||
|
||||
@@ -142,6 +142,10 @@ const EnvironmentSchema = z.object({
|
||||
CONTAINER_REGISTRY_PASSWORD: z.string().optional(),
|
||||
DEPLOY_REGISTRY_HOST: z.string().optional(),
|
||||
DEPLOY_REGISTRY_NAMESPACE: z.string().default("trigger"),
|
||||
DEPLOY_TIMEOUT_MS: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.default(60 * 1000 * 8), // 8 minutes
|
||||
OBJECT_STORE_BASE_URL: z.string().optional(),
|
||||
OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),
|
||||
OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { RetrieveBatchResponse } from "@trigger.dev/core/v3";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export class ApiRetrieveBatchPresenter extends BasePresenter {
|
||||
public async call(
|
||||
friendlyId: string,
|
||||
env: AuthenticatedEnvironment
|
||||
): Promise<RetrieveBatchResponse | undefined> {
|
||||
return this.traceWithEnv<RetrieveBatchResponse | undefined>("call", env, async (span) => {
|
||||
const batch = await this._replica.batchTaskRun.findFirst({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!batch) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
id: batch.friendlyId,
|
||||
status: batch.status,
|
||||
idempotencyKey: batch.idempotencyKey ?? undefined,
|
||||
createdAt: batch.createdAt,
|
||||
updatedAt: batch.updatedAt,
|
||||
runCount: batch.runCount,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import assertNever from "assert-never";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { generatePresignedUrl } from "~/v3/r2.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { $replica, prisma } from "~/db.server";
|
||||
|
||||
// Build 'select' object
|
||||
const commonRunSelect = {
|
||||
@@ -59,48 +59,46 @@ type CommonRelatedRun = Prisma.Result<
|
||||
"findFirstOrThrow"
|
||||
>;
|
||||
|
||||
type FoundRun = NonNullable<Awaited<ReturnType<typeof ApiRetrieveRunPresenter.findRun>>>;
|
||||
|
||||
export class ApiRetrieveRunPresenter extends BasePresenter {
|
||||
public static async findRun(friendlyId: string, env: AuthenticatedEnvironment) {
|
||||
return $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
lockedToVersion: true,
|
||||
schedule: true,
|
||||
tags: true,
|
||||
batch: {
|
||||
select: {
|
||||
id: true,
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
parentTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
rootTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
childRuns: {
|
||||
select: {
|
||||
...commonRunSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async call(
|
||||
friendlyId: string,
|
||||
taskRun: FoundRun,
|
||||
env: AuthenticatedEnvironment
|
||||
): Promise<RetrieveRunResponse | undefined> {
|
||||
return this.traceWithEnv("call", env, async (span) => {
|
||||
const taskRun = await this._replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
lockedToVersion: true,
|
||||
schedule: true,
|
||||
tags: true,
|
||||
batch: {
|
||||
select: {
|
||||
id: true,
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
parentTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
rootTaskRun: {
|
||||
select: commonRunSelect,
|
||||
},
|
||||
childRuns: {
|
||||
select: {
|
||||
...commonRunSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskRun) {
|
||||
logger.debug("Task run not found", { friendlyId, envId: env.id });
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let $payload: any;
|
||||
let $payloadPresignedUrl: string | undefined;
|
||||
let $output: any;
|
||||
|
||||
@@ -108,6 +108,7 @@ export class DeploymentPresenter {
|
||||
},
|
||||
},
|
||||
sdkVersion: true,
|
||||
cliVersion: true,
|
||||
},
|
||||
},
|
||||
triggeredBy: {
|
||||
@@ -145,6 +146,7 @@ export class DeploymentPresenter {
|
||||
},
|
||||
deployedBy: deployment.triggeredBy,
|
||||
sdkVersion: deployment.worker?.sdkVersion,
|
||||
cliVersion: deployment.worker?.cliVersion,
|
||||
imageReference: deployment.imageReference,
|
||||
externalBuildData:
|
||||
externalBuildData && externalBuildData.success ? externalBuildData.data : undefined,
|
||||
|
||||
@@ -183,7 +183,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
}
|
||||
|
||||
//show all runs if we are filtering by batchId or runId
|
||||
if (batchId || runId || scheduleId) {
|
||||
if (batchId || runId || scheduleId || tasks?.length) {
|
||||
rootOnly = false;
|
||||
}
|
||||
|
||||
|
||||
+70
-7
@@ -1,6 +1,10 @@
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
ArrowPathRoundedSquareIcon,
|
||||
ArrowRightIcon,
|
||||
ExclamationCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { useLocation, useNavigation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -8,16 +12,19 @@ import { ListPagination } from "~/components/ListPagination";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { PopoverMenuItem } from "~/components/primitives/Popover";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
@@ -29,12 +36,17 @@ import {
|
||||
BatchStatusCombo,
|
||||
descriptionForBatchStatus,
|
||||
} from "~/components/runs/v3/BatchStatus";
|
||||
import { CheckBatchCompletionDialog } from "~/components/runs/v3/CheckBatchCompletionDialog";
|
||||
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithErrorMessage } from "~/models/message.server";
|
||||
import { findProjectBySlug } from "~/models/project.server";
|
||||
import { BatchList, BatchListPresenter } from "~/presenters/v3/BatchListPresenter.server";
|
||||
import {
|
||||
BatchList,
|
||||
BatchListItem,
|
||||
BatchListPresenter,
|
||||
} from "~/presenters/v3/BatchListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, ProjectParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder";
|
||||
|
||||
@@ -150,11 +162,14 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
<TableHeaderCell>Duration</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Finished</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<span className="sr-only">Go to batch</span>
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{batches.length === 0 && !hasFilters ? (
|
||||
<TableBlankRow colSpan={7}>
|
||||
<TableBlankRow colSpan={8}>
|
||||
{!isLoading && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Paragraph className="w-auto">No batches</Paragraph>
|
||||
@@ -162,7 +177,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
)}
|
||||
</TableBlankRow>
|
||||
) : batches.length === 0 ? (
|
||||
<TableBlankRow colSpan={7}>
|
||||
<TableBlankRow colSpan={8}>
|
||||
<div className="flex items-center justify-center">
|
||||
<Paragraph className="w-auto">No batches match these filters</Paragraph>
|
||||
</div>
|
||||
@@ -215,13 +230,14 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
<TableCell to={path}>
|
||||
{batch.finishedAt ? <DateTime date={batch.finishedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<BatchActionsCell batch={batch} path={path} />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{isLoading && (
|
||||
<TableBlankRow
|
||||
colSpan={7}
|
||||
colSpan={8}
|
||||
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-charcoal-900/90"
|
||||
>
|
||||
<Spinner /> <span className="text-text-dimmed">Loading…</span>
|
||||
@@ -231,3 +247,50 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function BatchActionsCell({ batch, path }: { batch: BatchListItem; path: string }) {
|
||||
const location = useLocation();
|
||||
|
||||
if (batch.hasFinished || batch.environment.type === "DEVELOPMENT") {
|
||||
return <TableCell to={path}>{""}</TableCell>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
popoverContent={
|
||||
<>
|
||||
<PopoverMenuItem
|
||||
to={path}
|
||||
icon={ArrowRightIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
title="View batch"
|
||||
/>
|
||||
{!batch.hasFinished && (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
asChild
|
||||
className="size-6 rounded-sm p-1 text-text-dimmed transition hover:bg-charcoal-700 hover:text-text-bright"
|
||||
>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={ArrowPathRoundedSquareIcon}
|
||||
leadingIconClassName="text-success"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
className="w-full px-1.5 py-[0.9rem]"
|
||||
>
|
||||
Try and resume
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<CheckBatchCompletionDialog
|
||||
batchId={batch.id}
|
||||
redirectPath={`${location.pathname}${location.search}`}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -151,6 +151,10 @@ export default function Page() {
|
||||
<Property.Label>SDK Version</Property.Label>
|
||||
<Property.Value>{deployment.sdkVersion ? deployment.sdkVersion : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>CLI Version</Property.Label>
|
||||
<Property.Value>{deployment.cliVersion ? deployment.cliVersion : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Started at</Property.Label>
|
||||
<Property.Value>
|
||||
|
||||
+32
-9
@@ -35,9 +35,13 @@ import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
|
||||
import { BULK_ACTION_RUN_LIMIT } from "~/consts";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { findProjectBySlug } from "~/models/project.server";
|
||||
import { RunListPresenter } from "~/presenters/v3/RunListPresenter.server";
|
||||
import {
|
||||
getRootOnlyFilterPreference,
|
||||
setRootOnlyFilterPreference,
|
||||
uiPreferencesStorage,
|
||||
} from "~/services/preferences/uiPreferences.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
@@ -54,6 +58,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
let rootOnlyValue = false;
|
||||
if (url.searchParams.has("rootOnly")) {
|
||||
rootOnlyValue = url.searchParams.get("rootOnly") === "true";
|
||||
} else {
|
||||
rootOnlyValue = await getRootOnlyFilterPreference(request);
|
||||
}
|
||||
|
||||
const s = {
|
||||
cursor: url.searchParams.get("cursor") ?? undefined,
|
||||
direction: url.searchParams.get("direction") ?? undefined,
|
||||
@@ -65,7 +77,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
tags: url.searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
|
||||
from: url.searchParams.get("from") ?? undefined,
|
||||
to: url.searchParams.get("to") ?? undefined,
|
||||
showChildTasks: url.searchParams.get("showChildTasks") === "true",
|
||||
rootOnly: rootOnlyValue,
|
||||
runId: url.searchParams.get("runId") ?? undefined,
|
||||
batchId: url.searchParams.get("batchId") ?? undefined,
|
||||
scheduleId: url.searchParams.get("scheduleId") ?? undefined,
|
||||
@@ -82,7 +94,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
to,
|
||||
cursor,
|
||||
direction,
|
||||
showChildTasks,
|
||||
rootOnly,
|
||||
runId,
|
||||
batchId,
|
||||
scheduleId,
|
||||
@@ -110,22 +122,32 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
batchId,
|
||||
runId,
|
||||
scheduleId,
|
||||
rootOnly: !showChildTasks,
|
||||
rootOnly,
|
||||
direction: direction,
|
||||
cursor: cursor,
|
||||
});
|
||||
|
||||
return typeddefer({
|
||||
data: list,
|
||||
});
|
||||
const session = await setRootOnlyFilterPreference(rootOnlyValue, request);
|
||||
const cookieValue = await uiPreferencesStorage.commitSession(session);
|
||||
|
||||
return typeddefer(
|
||||
{
|
||||
data: list,
|
||||
rootOnlyDefault: rootOnlyValue,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": cookieValue,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { data } = useTypedLoaderData<typeof loader>();
|
||||
const { data, rootOnlyDefault } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -184,6 +206,7 @@ export default function Page() {
|
||||
possibleTasks={list.possibleTasks}
|
||||
bulkActions={list.bulkActions}
|
||||
hasFilters={list.hasFilters}
|
||||
rootOnlyDefault={rootOnlyDefault}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { ApiRetrieveBatchPresenter } from "~/presenters/v3/ApiRetrieveBatchPresenter.server";
|
||||
import { $replica } from "~/db.server";
|
||||
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
@@ -12,20 +12,28 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: (params, auth) => {
|
||||
return $replica.batchTaskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.batchId,
|
||||
runtimeEnvironmentId: auth.environment.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (params) => ({ batch: params.batchId }),
|
||||
resource: (batch) => ({ batch: batch.friendlyId }),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
async ({ params, authentication }) => {
|
||||
const presenter = new ApiRetrieveBatchPresenter();
|
||||
const result = await presenter.call(params.batchId, authentication.environment);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Batch not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json(result);
|
||||
async ({ resource: batch }) => {
|
||||
return json({
|
||||
id: batch.friendlyId,
|
||||
status: batch.status,
|
||||
idempotencyKey: batch.idempotencyKey ?? undefined,
|
||||
createdAt: batch.createdAt,
|
||||
updatedAt: batch.updatedAt,
|
||||
runCount: batch.runCount,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
|
||||
},
|
||||
async ({ params, authentication }) => {
|
||||
const filename = params["*"];
|
||||
|
||||
@@ -61,8 +61,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return json({ error: "An unknown error occurred" }, { status: 500 });
|
||||
}
|
||||
|
||||
const run = await ApiRetrieveRunPresenter.findRun(
|
||||
updatedRun.friendlyId,
|
||||
authenticationResult.environment
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
return json({ error: "Run not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const presenter = new ApiRetrieveRunPresenter();
|
||||
const result = await presenter.call(updatedRun.friendlyId, authenticationResult.environment);
|
||||
const result = await presenter.call(run, authenticationResult.environment);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Run not found" }, { status: 404 });
|
||||
|
||||
@@ -12,9 +12,10 @@ export const loader = createLoaderApiRoute(
|
||||
corsStrategy: "all",
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (_, searchParams) => ({ tasks: searchParams["filter[taskIdentifier]"] }),
|
||||
resource: (_, __, searchParams) => ({ tasks: searchParams["filter[taskIdentifier]"] }),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
|
||||
},
|
||||
async ({ searchParams, authentication }) => {
|
||||
const presenter = new ApiRunListPresenter();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { generateJWT as internal_generateJWT, TriggerTaskRequestBody } from "@tr
|
||||
import { TaskRun } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
@@ -33,7 +33,7 @@ const { action, loader } = createActionApiRoute(
|
||||
allowJWT: true,
|
||||
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
|
||||
authorization: {
|
||||
action: "write",
|
||||
action: "trigger",
|
||||
resource: (params) => ({ tasks: params.taskId }),
|
||||
superScopes: ["write:tasks", "admin"],
|
||||
},
|
||||
@@ -59,6 +59,8 @@ const { action, loader } = createActionApiRoute(
|
||||
? { traceparent, tracestate }
|
||||
: undefined;
|
||||
|
||||
const oneTimeUseToken = await getOneTimeUseToken(authentication);
|
||||
|
||||
logger.debug("Triggering task", {
|
||||
taskId: params.taskId,
|
||||
idempotencyKey,
|
||||
@@ -78,6 +80,7 @@ const { action, loader } = createActionApiRoute(
|
||||
triggerVersion: triggerVersion ?? undefined,
|
||||
traceContext,
|
||||
spanParentAsLink: spanParentAsLink === 1,
|
||||
oneTimeUseToken,
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
import { BatchTriggerV2Service } from "~/v3/services/batchTriggerV2.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
@@ -22,7 +22,7 @@ const { action, loader } = createActionApiRoute(
|
||||
allowJWT: true,
|
||||
maxContentLength: env.BATCH_TASK_PAYLOAD_MAXIMUM_SIZE,
|
||||
authorization: {
|
||||
action: "write",
|
||||
action: "batchTrigger",
|
||||
resource: (_, __, ___, body) => ({
|
||||
tasks: Array.from(new Set(body.items.map((i) => i.task))),
|
||||
}),
|
||||
@@ -56,6 +56,8 @@ const { action, loader } = createActionApiRoute(
|
||||
tracestate,
|
||||
} = headers;
|
||||
|
||||
const oneTimeUseToken = await getOneTimeUseToken(authentication);
|
||||
|
||||
logger.debug("Batch trigger request", {
|
||||
idempotencyKey,
|
||||
idempotencyKeyTTL,
|
||||
@@ -86,6 +88,7 @@ const { action, loader } = createActionApiRoute(
|
||||
triggerVersion: triggerVersion ?? undefined,
|
||||
traceContext,
|
||||
spanParentAsLink: spanParentAsLink === 1,
|
||||
oneTimeUseToken,
|
||||
});
|
||||
|
||||
const $responseHeaders = await responseHeaders(
|
||||
@@ -133,7 +136,7 @@ async function responseHeaders(
|
||||
const claims = {
|
||||
sub: environment.id,
|
||||
pub: true,
|
||||
scopes: [`read:batch:${batch.id}`].concat(batch.runs.map((r) => `read:runs:${r.id}`)),
|
||||
scopes: [`read:batch:${batch.id}`],
|
||||
};
|
||||
|
||||
const jwt = await generateJWT({
|
||||
|
||||
@@ -12,15 +12,23 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: (params, auth) => {
|
||||
return ApiRetrieveRunPresenter.findRun(params.runId, auth.environment);
|
||||
},
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (params) => ({ runs: params.runId }),
|
||||
resource: (run) => ({
|
||||
runs: run.friendlyId,
|
||||
tags: run.runTags,
|
||||
batch: run.batch?.friendlyId,
|
||||
tasks: run.taskIdentifier,
|
||||
}),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
async ({ params, authentication }) => {
|
||||
async ({ authentication, resource }) => {
|
||||
const presenter = new ApiRetrieveRunPresenter();
|
||||
const result = await presenter.call(params.runId, authentication.environment);
|
||||
const result = await presenter.call(resource, authentication.environment);
|
||||
|
||||
if (!result) {
|
||||
return json(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { $replica } from "~/db.server";
|
||||
import { realtimeClient } from "~/services/realtimeClientGlobal.server";
|
||||
@@ -13,24 +12,21 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: (params, auth) => {
|
||||
return $replica.batchTaskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.batchId,
|
||||
runtimeEnvironmentId: auth.environment.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (params) => ({ batch: params.batchId }),
|
||||
resource: (batch) => ({ batch: batch.friendlyId }),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
async ({ params, authentication, request }) => {
|
||||
const batchRun = await $replica.batchTaskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.batchId,
|
||||
runtimeEnvironmentId: authentication.environment.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!batchRun) {
|
||||
return json({ error: "Batch not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
async ({ authentication, request, resource: batchRun }) => {
|
||||
return realtimeClient.streamBatch(
|
||||
request.url,
|
||||
authentication.environment,
|
||||
|
||||
@@ -13,24 +13,33 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: async (params, authentication) => {
|
||||
return $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.runId,
|
||||
runtimeEnvironmentId: authentication.environment.id,
|
||||
},
|
||||
include: {
|
||||
batch: {
|
||||
select: {
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (params) => ({ runs: params.runId }),
|
||||
resource: (run) => ({
|
||||
runs: run.friendlyId,
|
||||
tags: run.runTags,
|
||||
batch: run.batch?.friendlyId,
|
||||
tasks: run.taskIdentifier,
|
||||
}),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
async ({ params, authentication, request }) => {
|
||||
const run = await $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.runId,
|
||||
runtimeEnvironmentId: authentication.environment.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return json({ error: "Run not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
async ({ authentication, request, resource: run }) => {
|
||||
return realtimeClient.streamRun(
|
||||
request.url,
|
||||
authentication.environment,
|
||||
|
||||
@@ -16,9 +16,10 @@ export const loader = createLoaderApiRoute(
|
||||
searchParams: SearchParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: async () => 1, // This is a dummy value, it's not used
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (_, searchParams) => searchParams,
|
||||
resource: (_, __, searchParams) => searchParams,
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,24 +24,33 @@ export const loader = createLoaderApiRoute(
|
||||
params: ParamsSchema,
|
||||
allowJWT: true,
|
||||
corsStrategy: "all",
|
||||
findResource: async (params, auth) => {
|
||||
return $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.runId,
|
||||
runtimeEnvironmentId: auth.environment.id,
|
||||
},
|
||||
include: {
|
||||
batch: {
|
||||
select: {
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
authorization: {
|
||||
action: "read",
|
||||
resource: (params) => ({ runs: params.runId }),
|
||||
resource: (run) => ({
|
||||
runs: run.friendlyId,
|
||||
tags: run.runTags,
|
||||
batch: run.batch?.friendlyId,
|
||||
tasks: run.taskIdentifier,
|
||||
}),
|
||||
superScopes: ["read:runs", "read:all", "admin"],
|
||||
},
|
||||
},
|
||||
async ({ params, authentication, request }) => {
|
||||
const run = await $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.runId,
|
||||
runtimeEnvironmentId: authentication.environment.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return new Response("Run not found", { status: 404 });
|
||||
}
|
||||
|
||||
async ({ params, request, resource: run }) => {
|
||||
return realtimeStreams.streamResponse(run.friendlyId, params.streamId, request.signal);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ActionFunction, json } from "@remix-run/node";
|
||||
import { assertExhaustive } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server";
|
||||
|
||||
export const checkCompletionSchema = z.object({
|
||||
redirectUrl: z.string(),
|
||||
});
|
||||
|
||||
const ParamSchema = z.object({
|
||||
batchId: z.string(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const { batchId } = ParamSchema.parse(params);
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: checkCompletionSchema });
|
||||
|
||||
if (!submission.value) {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
const resumeBatchRunService = new ResumeBatchRunService();
|
||||
const resumeResult = await resumeBatchRunService.call(batchId);
|
||||
|
||||
let message: string | undefined;
|
||||
|
||||
switch (resumeResult) {
|
||||
case "ERROR": {
|
||||
throw "Unknown error during batch completion check";
|
||||
}
|
||||
case "ALREADY_COMPLETED": {
|
||||
message = "Batch already completed.";
|
||||
break;
|
||||
}
|
||||
case "COMPLETED": {
|
||||
message = "Batch completed and parent tasks resumed.";
|
||||
break;
|
||||
}
|
||||
case "PENDING": {
|
||||
message = "Child runs still in progress. Please try again later.";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(resumeResult);
|
||||
}
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(submission.value.redirectUrl, request, message);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Failed to check batch completion", {
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
return redirectWithErrorMessage(submission.value.redirectUrl, request, error.message);
|
||||
} else {
|
||||
logger.error("Failed to check batch completion", { error });
|
||||
return redirectWithErrorMessage(submission.value.redirectUrl, request, "Unknown error");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,8 @@ import { isPublicJWT, validatePublicJwtKey } from "./realtime/jwtAuth.server";
|
||||
|
||||
const ClaimsSchema = z.object({
|
||||
scopes: z.array(z.string()).optional(),
|
||||
// One-time use token
|
||||
otu: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Optional<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;
|
||||
@@ -39,6 +41,7 @@ export type ApiAuthenticationResultSuccess = {
|
||||
type: "PUBLIC" | "PRIVATE" | "PUBLIC_JWT";
|
||||
environment: AuthenticatedEnvironment;
|
||||
scopes?: string[];
|
||||
oneTimeUse?: boolean;
|
||||
};
|
||||
|
||||
export type ApiAuthenticationResultFailure = {
|
||||
@@ -146,6 +149,7 @@ export async function authenticateApiKey(
|
||||
...result,
|
||||
environment: validationResults.environment,
|
||||
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
|
||||
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -227,6 +231,7 @@ export async function authenticateApiKeyWithFailure(
|
||||
...result,
|
||||
environment: validationResults.environment,
|
||||
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
|
||||
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -531,3 +536,20 @@ function calculateJWTExpiration() {
|
||||
|
||||
return (Date.now() + DEFAULT_JWT_EXPIRATION_IN_MS) / 1000;
|
||||
}
|
||||
|
||||
export async function getOneTimeUseToken(
|
||||
auth: ApiAuthenticationResultSuccess
|
||||
): Promise<string | undefined> {
|
||||
if (auth.type !== "PUBLIC_JWT") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!auth.oneTimeUse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash the API key to make it unique
|
||||
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(auth.apiKey));
|
||||
|
||||
return Buffer.from(hash).toString("hex");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AuthorizationAction = "read" | "write"; // Add more actions as needed
|
||||
export type AuthorizationAction = "read" | "write" | string; // Add more actions as needed
|
||||
|
||||
const ResourceTypes = ["tasks", "tags", "runs", "batch"] as const;
|
||||
|
||||
@@ -88,34 +88,26 @@ export function checkAuthorization(
|
||||
for (const [resourceType, resourceValue] of Object.entries(filteredResource)) {
|
||||
const resourceValues = Array.isArray(resourceValue) ? resourceValue : [resourceValue];
|
||||
|
||||
let resourceAuthorized = false;
|
||||
for (const value of resourceValues) {
|
||||
// Check for specific resource permission
|
||||
const specificPermission = `${action}:${resourceType}:${value}`;
|
||||
// Check for general resource type permission
|
||||
const generalPermission = `${action}:${resourceType}`;
|
||||
|
||||
// If any permission matches, return authorized
|
||||
if (entity.scopes.includes(specificPermission) || entity.scopes.includes(generalPermission)) {
|
||||
resourceAuthorized = true;
|
||||
break;
|
||||
return { authorized: true };
|
||||
}
|
||||
}
|
||||
|
||||
// If any resource is not authorized, return false
|
||||
if (!resourceAuthorized) {
|
||||
return {
|
||||
authorized: false,
|
||||
reason: `Public Access Token is missing required permissions. Permissions required for ${resourceValues
|
||||
.map((v) => `'${action}:${resourceType}:${v}'`)
|
||||
.join(", ")} but token has the following permissions: ${entity.scopes
|
||||
.map((s) => `'${s}'`)
|
||||
.join(
|
||||
", "
|
||||
)}. See https://trigger.dev/docs/frontend/overview#authentication for more information.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// All resources are authorized
|
||||
return { authorized: true };
|
||||
// No matching permissions found
|
||||
return {
|
||||
authorized: false,
|
||||
reason: `Public Access Token is missing required permissions. Token has the following permissions: ${entity.scopes
|
||||
.map((s) => `'${s}'`)
|
||||
.join(
|
||||
", "
|
||||
)}. See https://trigger.dev/docs/frontend/overview#authentication for more information.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,3 +27,18 @@ export async function setUsefulLinksPreference(show: boolean, request: Request)
|
||||
session.set("showUsefulLinks", show);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function getRootOnlyFilterPreference(request: Request): Promise<boolean> {
|
||||
const session = await getUiPreferencesSession(request);
|
||||
const rootOnly = session.get("rootOnly");
|
||||
if (rootOnly === undefined) {
|
||||
return false;
|
||||
}
|
||||
return rootOnly;
|
||||
}
|
||||
|
||||
export async function setRootOnlyFilterPreference(rootOnly: boolean, request: Request) {
|
||||
const session = await getUiPreferencesSession(request);
|
||||
session.set("rootOnly", rootOnly);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -21,16 +21,22 @@ import { safeJsonParse } from "~/utils/json";
|
||||
type ApiKeyRouteBuilderOptions<
|
||||
TParamsSchema extends z.AnyZodObject | undefined = undefined,
|
||||
TSearchParamsSchema extends z.AnyZodObject | undefined = undefined,
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined,
|
||||
TResource = never
|
||||
> = {
|
||||
params?: TParamsSchema;
|
||||
searchParams?: TSearchParamsSchema;
|
||||
headers?: THeadersSchema;
|
||||
allowJWT?: boolean;
|
||||
corsStrategy?: "all" | "none";
|
||||
findResource: (
|
||||
params: TParamsSchema extends z.AnyZodObject ? z.infer<TParamsSchema> : undefined,
|
||||
authentication: ApiAuthenticationResultSuccess
|
||||
) => Promise<TResource | undefined>;
|
||||
authorization?: {
|
||||
action: AuthorizationAction;
|
||||
resource: (
|
||||
resource: NonNullable<TResource>,
|
||||
params: TParamsSchema extends z.AnyZodObject ? z.infer<TParamsSchema> : undefined,
|
||||
searchParams: TSearchParamsSchema extends z.AnyZodObject
|
||||
? z.infer<TSearchParamsSchema>
|
||||
@@ -44,7 +50,8 @@ type ApiKeyRouteBuilderOptions<
|
||||
type ApiKeyHandlerFunction<
|
||||
TParamsSchema extends z.AnyZodObject | undefined,
|
||||
TSearchParamsSchema extends z.AnyZodObject | undefined,
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined,
|
||||
TResource = never
|
||||
> = (args: {
|
||||
params: TParamsSchema extends z.AnyZodObject ? z.infer<TParamsSchema> : undefined;
|
||||
searchParams: TSearchParamsSchema extends z.AnyZodObject
|
||||
@@ -53,15 +60,17 @@ type ApiKeyHandlerFunction<
|
||||
headers: THeadersSchema extends z.AnyZodObject ? z.infer<THeadersSchema> : undefined;
|
||||
authentication: ApiAuthenticationResultSuccess;
|
||||
request: Request;
|
||||
resource: NonNullable<TResource>;
|
||||
}) => Promise<Response>;
|
||||
|
||||
export function createLoaderApiRoute<
|
||||
TParamsSchema extends z.AnyZodObject | undefined = undefined,
|
||||
TSearchParamsSchema extends z.AnyZodObject | undefined = undefined,
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined
|
||||
THeadersSchema extends z.AnyZodObject | undefined = undefined,
|
||||
TResource = never
|
||||
>(
|
||||
options: ApiKeyRouteBuilderOptions<TParamsSchema, TSearchParamsSchema, THeadersSchema>,
|
||||
handler: ApiKeyHandlerFunction<TParamsSchema, TSearchParamsSchema, THeadersSchema>
|
||||
options: ApiKeyRouteBuilderOptions<TParamsSchema, TSearchParamsSchema, THeadersSchema, TResource>,
|
||||
handler: ApiKeyHandlerFunction<TParamsSchema, TSearchParamsSchema, THeadersSchema, TResource>
|
||||
) {
|
||||
return async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const {
|
||||
@@ -71,6 +80,7 @@ export function createLoaderApiRoute<
|
||||
allowJWT = false,
|
||||
corsStrategy = "none",
|
||||
authorization,
|
||||
findResource,
|
||||
} = options;
|
||||
|
||||
if (corsStrategy !== "none" && request.method.toUpperCase() === "OPTIONS") {
|
||||
@@ -146,13 +156,29 @@ export function createLoaderApiRoute<
|
||||
parsedHeaders = headers.data;
|
||||
}
|
||||
|
||||
// Find the resource
|
||||
const resource = await findResource(parsedParams, authenticationResult);
|
||||
|
||||
if (!resource) {
|
||||
return await wrapResponse(
|
||||
request,
|
||||
json({ error: "Not found" }, { status: 404 }),
|
||||
corsStrategy !== "none"
|
||||
);
|
||||
}
|
||||
|
||||
if (authorization) {
|
||||
const { action, resource, superScopes } = authorization;
|
||||
const $resource = resource(parsedParams, parsedSearchParams, parsedHeaders);
|
||||
const { action, resource: authResource, superScopes } = authorization;
|
||||
const $authResource = authResource(
|
||||
resource,
|
||||
parsedParams,
|
||||
parsedSearchParams,
|
||||
parsedHeaders
|
||||
);
|
||||
|
||||
logger.debug("Checking authorization", {
|
||||
action,
|
||||
resource: $resource,
|
||||
resource: $authResource,
|
||||
superScopes,
|
||||
scopes: authenticationResult.scopes,
|
||||
});
|
||||
@@ -160,7 +186,7 @@ export function createLoaderApiRoute<
|
||||
const authorizationResult = checkAuthorization(
|
||||
authenticationResult,
|
||||
action,
|
||||
$resource,
|
||||
$authResource,
|
||||
superScopes
|
||||
);
|
||||
|
||||
@@ -187,6 +213,7 @@ export function createLoaderApiRoute<
|
||||
headers: parsedHeaders,
|
||||
authentication: authenticationResult,
|
||||
request,
|
||||
resource,
|
||||
});
|
||||
return await wrapResponse(request, result, corsStrategy !== "none");
|
||||
} catch (error) {
|
||||
@@ -543,10 +570,25 @@ export function createActionApiRoute<
|
||||
scopes: authenticationResult.scopes,
|
||||
});
|
||||
|
||||
if (!checkAuthorization(authenticationResult, action, $resource, superScopes)) {
|
||||
const authorizationResult = checkAuthorization(
|
||||
authenticationResult,
|
||||
action,
|
||||
$resource,
|
||||
superScopes
|
||||
);
|
||||
|
||||
if (!authorizationResult.authorized) {
|
||||
return await wrapResponse(
|
||||
request,
|
||||
json({ error: "Unauthorized" }, { status: 403 }),
|
||||
json(
|
||||
{
|
||||
error: `Unauthorized: ${authorizationResult.reason}`,
|
||||
code: "unauthorized",
|
||||
param: "access_token",
|
||||
type: "authorization",
|
||||
},
|
||||
{ status: 403 }
|
||||
),
|
||||
corsStrategy !== "none"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ function getWorkerQueue() {
|
||||
handler: async (payload, job) => {
|
||||
const service = new ResumeBatchRunService();
|
||||
|
||||
return await service.call(payload.batchRunId);
|
||||
await service.call(payload.batchRunId);
|
||||
},
|
||||
},
|
||||
"v3.resumeTaskDependency": {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
packetRequiresOffloading,
|
||||
parsePacket,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { BatchTaskRun, TaskRunAttempt } from "@trigger.dev/database";
|
||||
import { BatchTaskRun, Prisma, TaskRunAttempt } from "@trigger.dev/database";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { batchTaskRunItemStatusForRunStatus } from "~/models/taskRun.server";
|
||||
@@ -48,6 +48,7 @@ export type BatchTriggerTaskServiceOptions = {
|
||||
triggerVersion?: string;
|
||||
traceContext?: Record<string, string | undefined>;
|
||||
spanParentAsLink?: boolean;
|
||||
oneTimeUseToken?: string;
|
||||
};
|
||||
|
||||
export class BatchTriggerV2Service extends BaseService {
|
||||
@@ -56,255 +57,288 @@ export class BatchTriggerV2Service extends BaseService {
|
||||
body: BatchTriggerTaskV2RequestBody,
|
||||
options: BatchTriggerTaskServiceOptions = {}
|
||||
): Promise<BatchTriggerTaskV2Response> {
|
||||
return await this.traceWithEnv<BatchTriggerTaskV2Response>(
|
||||
"call()",
|
||||
environment,
|
||||
async (span) => {
|
||||
const existingBatch = options.idempotencyKey
|
||||
? await this._prisma.batchTaskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (existingBatch) {
|
||||
if (
|
||||
existingBatch.idempotencyKeyExpiresAt &&
|
||||
existingBatch.idempotencyKeyExpiresAt < new Date()
|
||||
) {
|
||||
logger.debug("[BatchTriggerV2][call] Idempotency key has expired", {
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
batch: {
|
||||
id: existingBatch.id,
|
||||
friendlyId: existingBatch.friendlyId,
|
||||
runCount: existingBatch.runCount,
|
||||
idempotencyKeyExpiresAt: existingBatch.idempotencyKeyExpiresAt,
|
||||
idempotencyKey: existingBatch.idempotencyKey,
|
||||
},
|
||||
});
|
||||
|
||||
// Update the existing batch to remove the idempotency key
|
||||
await this._prisma.batchTaskRun.update({
|
||||
where: { id: existingBatch.id },
|
||||
data: { idempotencyKey: null },
|
||||
});
|
||||
|
||||
// Don't return, just continue with the batch trigger
|
||||
} else {
|
||||
span.setAttribute("batchId", existingBatch.friendlyId);
|
||||
|
||||
return this.#respondWithExistingBatch(existingBatch, environment);
|
||||
}
|
||||
}
|
||||
|
||||
const batchId = generateFriendlyId("batch");
|
||||
|
||||
span.setAttribute("batchId", batchId);
|
||||
|
||||
const dependentAttempt = body?.dependentAttempt
|
||||
? await this._prisma.taskRunAttempt.findUnique({
|
||||
where: { friendlyId: body.dependentAttempt },
|
||||
include: {
|
||||
taskRun: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
dependentAttempt &&
|
||||
(isFinalAttemptStatus(dependentAttempt.status) ||
|
||||
isFinalRunStatus(dependentAttempt.taskRun.status))
|
||||
) {
|
||||
logger.debug("[BatchTriggerV2][call] Dependent attempt or run is in a terminal state", {
|
||||
dependentAttempt: dependentAttempt,
|
||||
batchId,
|
||||
});
|
||||
|
||||
throw new ServiceValidationError(
|
||||
"Cannot process batch as the parent run is already in a terminal state"
|
||||
);
|
||||
}
|
||||
|
||||
if (environment.type !== "DEVELOPMENT") {
|
||||
const result = await getEntitlement(environment.organizationId);
|
||||
if (result && result.hasAccess === false) {
|
||||
throw new OutOfEntitlementError();
|
||||
}
|
||||
}
|
||||
|
||||
const idempotencyKeys = body.items.map((i) => i.options?.idempotencyKey).filter(Boolean);
|
||||
|
||||
const cachedRuns =
|
||||
idempotencyKeys.length > 0
|
||||
? await this._prisma.taskRun.findMany({
|
||||
try {
|
||||
return await this.traceWithEnv<BatchTriggerTaskV2Response>(
|
||||
"call()",
|
||||
environment,
|
||||
async (span) => {
|
||||
const existingBatch = options.idempotencyKey
|
||||
? await this._prisma.batchTaskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: {
|
||||
in: body.items.map((i) => i.options?.idempotencyKey).filter(Boolean),
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
friendlyId: true,
|
||||
idempotencyKey: true,
|
||||
idempotencyKeyExpiresAt: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
: undefined;
|
||||
|
||||
if (cachedRuns.length) {
|
||||
logger.debug("[BatchTriggerV2][call] Found cached runs", {
|
||||
cachedRuns,
|
||||
batchId,
|
||||
});
|
||||
}
|
||||
|
||||
// Now we need to create an array of all the run IDs, in order
|
||||
// If we have a cached run, that isn't expired, we should use that run ID
|
||||
// If we have a cached run, that is expired, we should generate a new run ID and save that cached run ID to a set of expired run IDs
|
||||
// If we don't have a cached run, we should generate a new run ID
|
||||
const expiredRunIds = new Set<string>();
|
||||
let cachedRunCount = 0;
|
||||
|
||||
const runs = body.items.map((item) => {
|
||||
const cachedRun = cachedRuns.find(
|
||||
(r) => r.idempotencyKey === item.options?.idempotencyKey
|
||||
);
|
||||
|
||||
if (cachedRun) {
|
||||
if (existingBatch) {
|
||||
if (
|
||||
cachedRun.idempotencyKeyExpiresAt &&
|
||||
cachedRun.idempotencyKeyExpiresAt < new Date()
|
||||
existingBatch.idempotencyKeyExpiresAt &&
|
||||
existingBatch.idempotencyKeyExpiresAt < new Date()
|
||||
) {
|
||||
expiredRunIds.add(cachedRun.friendlyId);
|
||||
logger.debug("[BatchTriggerV2][call] Idempotency key has expired", {
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
batch: {
|
||||
id: existingBatch.id,
|
||||
friendlyId: existingBatch.friendlyId,
|
||||
runCount: existingBatch.runCount,
|
||||
idempotencyKeyExpiresAt: existingBatch.idempotencyKeyExpiresAt,
|
||||
idempotencyKey: existingBatch.idempotencyKey,
|
||||
},
|
||||
});
|
||||
|
||||
// Update the existing batch to remove the idempotency key
|
||||
await this._prisma.batchTaskRun.update({
|
||||
where: { id: existingBatch.id },
|
||||
data: { idempotencyKey: null },
|
||||
});
|
||||
|
||||
// Don't return, just continue with the batch trigger
|
||||
} else {
|
||||
span.setAttribute("batchId", existingBatch.friendlyId);
|
||||
|
||||
return this.#respondWithExistingBatch(existingBatch, environment);
|
||||
}
|
||||
}
|
||||
|
||||
const batchId = generateFriendlyId("batch");
|
||||
|
||||
span.setAttribute("batchId", batchId);
|
||||
|
||||
const dependentAttempt = body?.dependentAttempt
|
||||
? await this._prisma.taskRunAttempt.findUnique({
|
||||
where: { friendlyId: body.dependentAttempt },
|
||||
include: {
|
||||
taskRun: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
dependentAttempt &&
|
||||
(isFinalAttemptStatus(dependentAttempt.status) ||
|
||||
isFinalRunStatus(dependentAttempt.taskRun.status))
|
||||
) {
|
||||
logger.debug("[BatchTriggerV2][call] Dependent attempt or run is in a terminal state", {
|
||||
dependentAttempt: dependentAttempt,
|
||||
batchId,
|
||||
});
|
||||
|
||||
throw new ServiceValidationError(
|
||||
"Cannot process batch as the parent run is already in a terminal state"
|
||||
);
|
||||
}
|
||||
|
||||
if (environment.type !== "DEVELOPMENT") {
|
||||
const result = await getEntitlement(environment.organizationId);
|
||||
if (result && result.hasAccess === false) {
|
||||
throw new OutOfEntitlementError();
|
||||
}
|
||||
}
|
||||
|
||||
const idempotencyKeys = body.items.map((i) => i.options?.idempotencyKey).filter(Boolean);
|
||||
|
||||
const cachedRuns =
|
||||
idempotencyKeys.length > 0
|
||||
? await this._prisma.taskRun.findMany({
|
||||
where: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: {
|
||||
in: body.items.map((i) => i.options?.idempotencyKey).filter(Boolean),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
friendlyId: true,
|
||||
idempotencyKey: true,
|
||||
idempotencyKeyExpiresAt: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
if (cachedRuns.length) {
|
||||
logger.debug("[BatchTriggerV2][call] Found cached runs", {
|
||||
cachedRuns,
|
||||
batchId,
|
||||
});
|
||||
}
|
||||
|
||||
// Now we need to create an array of all the run IDs, in order
|
||||
// If we have a cached run, that isn't expired, we should use that run ID
|
||||
// If we have a cached run, that is expired, we should generate a new run ID and save that cached run ID to a set of expired run IDs
|
||||
// If we don't have a cached run, we should generate a new run ID
|
||||
const expiredRunIds = new Set<string>();
|
||||
let cachedRunCount = 0;
|
||||
|
||||
const runs = body.items.map((item) => {
|
||||
const cachedRun = cachedRuns.find(
|
||||
(r) => r.idempotencyKey === item.options?.idempotencyKey
|
||||
);
|
||||
|
||||
if (cachedRun) {
|
||||
if (
|
||||
cachedRun.idempotencyKeyExpiresAt &&
|
||||
cachedRun.idempotencyKeyExpiresAt < new Date()
|
||||
) {
|
||||
expiredRunIds.add(cachedRun.friendlyId);
|
||||
|
||||
return {
|
||||
id: generateFriendlyId("run"),
|
||||
isCached: false,
|
||||
idempotencyKey: item.options?.idempotencyKey ?? undefined,
|
||||
taskIdentifier: item.task,
|
||||
};
|
||||
}
|
||||
|
||||
cachedRunCount++;
|
||||
|
||||
return {
|
||||
id: generateFriendlyId("run"),
|
||||
isCached: false,
|
||||
id: cachedRun.friendlyId,
|
||||
isCached: true,
|
||||
idempotencyKey: item.options?.idempotencyKey ?? undefined,
|
||||
taskIdentifier: item.task,
|
||||
};
|
||||
}
|
||||
|
||||
cachedRunCount++;
|
||||
|
||||
return {
|
||||
id: cachedRun.friendlyId,
|
||||
isCached: true,
|
||||
id: generateFriendlyId("run"),
|
||||
isCached: false,
|
||||
idempotencyKey: item.options?.idempotencyKey ?? undefined,
|
||||
taskIdentifier: item.task,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: generateFriendlyId("run"),
|
||||
isCached: false,
|
||||
idempotencyKey: item.options?.idempotencyKey ?? undefined,
|
||||
taskIdentifier: item.task,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate how many new runs we need to create
|
||||
const newRunCount = body.items.length - cachedRunCount;
|
||||
|
||||
if (newRunCount === 0) {
|
||||
logger.debug("[BatchTriggerV2][call] All runs are cached", {
|
||||
batchId,
|
||||
});
|
||||
|
||||
await this._prisma.batchTaskRun.create({
|
||||
data: {
|
||||
friendlyId: batchId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
idempotencyKeyExpiresAt: options.idempotencyKeyExpiresAt,
|
||||
dependentTaskAttemptId: dependentAttempt?.id,
|
||||
runCount: body.items.length,
|
||||
runIds: runs.map((r) => r.id),
|
||||
status: "COMPLETED",
|
||||
batchVersion: "v2",
|
||||
// Calculate how many new runs we need to create
|
||||
const newRunCount = body.items.length - cachedRunCount;
|
||||
|
||||
if (newRunCount === 0) {
|
||||
logger.debug("[BatchTriggerV2][call] All runs are cached", {
|
||||
batchId,
|
||||
});
|
||||
|
||||
await this._prisma.batchTaskRun.create({
|
||||
data: {
|
||||
friendlyId: batchId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
idempotencyKeyExpiresAt: options.idempotencyKeyExpiresAt,
|
||||
dependentTaskAttemptId: dependentAttempt?.id,
|
||||
runCount: body.items.length,
|
||||
runIds: runs.map((r) => r.id),
|
||||
status: "COMPLETED",
|
||||
batchVersion: "v2",
|
||||
oneTimeUseToken: options.oneTimeUseToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: batchId,
|
||||
isCached: false,
|
||||
idempotencyKey: options.idempotencyKey ?? undefined,
|
||||
runs,
|
||||
};
|
||||
}
|
||||
|
||||
const queueSizeGuard = await guardQueueSizeLimitsForEnv(environment, marqs, newRunCount);
|
||||
|
||||
logger.debug("Queue size guard result", {
|
||||
newRunCount,
|
||||
queueSizeGuard,
|
||||
environment: {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
organization: environment.organization,
|
||||
project: environment.project,
|
||||
},
|
||||
});
|
||||
|
||||
if (!queueSizeGuard.isWithinLimits) {
|
||||
throw new ServiceValidationError(
|
||||
`Cannot trigger ${newRunCount} tasks as the queue size limit for this environment has been reached. The maximum size is ${queueSizeGuard.maximumSize}`
|
||||
);
|
||||
}
|
||||
|
||||
// Expire the cached runs that are no longer valid
|
||||
if (expiredRunIds.size) {
|
||||
logger.debug("Expiring cached runs", {
|
||||
expiredRunIds: Array.from(expiredRunIds),
|
||||
batchId,
|
||||
});
|
||||
|
||||
// TODO: is there a limit to the number of items we can update in a single query?
|
||||
await this._prisma.taskRun.updateMany({
|
||||
where: { friendlyId: { in: Array.from(expiredRunIds) } },
|
||||
data: { idempotencyKey: null },
|
||||
});
|
||||
}
|
||||
|
||||
// Upload to object store
|
||||
const payloadPacket = await this.#handlePayloadPacket(
|
||||
body.items,
|
||||
`batch/${batchId}`,
|
||||
environment
|
||||
);
|
||||
|
||||
const batch = await this.#createAndProcessBatchTaskRun(
|
||||
batchId,
|
||||
runs,
|
||||
payloadPacket,
|
||||
newRunCount,
|
||||
environment,
|
||||
body,
|
||||
options,
|
||||
dependentAttempt ?? undefined
|
||||
);
|
||||
|
||||
if (!batch) {
|
||||
throw new Error("Failed to create batch");
|
||||
}
|
||||
|
||||
return {
|
||||
id: batchId,
|
||||
id: batch.friendlyId,
|
||||
isCached: false,
|
||||
idempotencyKey: options.idempotencyKey ?? undefined,
|
||||
idempotencyKey: batch.idempotencyKey ?? undefined,
|
||||
runs,
|
||||
};
|
||||
}
|
||||
|
||||
const queueSizeGuard = await guardQueueSizeLimitsForEnv(environment, marqs, newRunCount);
|
||||
|
||||
logger.debug("Queue size guard result", {
|
||||
newRunCount,
|
||||
queueSizeGuard,
|
||||
environment: {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
organization: environment.organization,
|
||||
project: environment.project,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
// Detect a prisma transaction Unique constraint violation
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
logger.debug("BatchTriggerV2: Prisma transaction error", {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
meta: error.meta,
|
||||
});
|
||||
|
||||
if (!queueSizeGuard.isWithinLimits) {
|
||||
throw new ServiceValidationError(
|
||||
`Cannot trigger ${newRunCount} tasks as the queue size limit for this environment has been reached. The maximum size is ${queueSizeGuard.maximumSize}`
|
||||
);
|
||||
if (error.code === "P2002") {
|
||||
const target = error.meta?.target;
|
||||
|
||||
if (
|
||||
Array.isArray(target) &&
|
||||
target.length > 0 &&
|
||||
typeof target[0] === "string" &&
|
||||
target[0].includes("oneTimeUseToken")
|
||||
) {
|
||||
throw new ServiceValidationError(
|
||||
"Cannot batch trigger with a one-time use token as it has already been used."
|
||||
);
|
||||
} else {
|
||||
throw new ServiceValidationError(
|
||||
"Cannot batch trigger as it has already been triggered with the same idempotency key."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Expire the cached runs that are no longer valid
|
||||
if (expiredRunIds.size) {
|
||||
logger.debug("Expiring cached runs", {
|
||||
expiredRunIds: Array.from(expiredRunIds),
|
||||
batchId,
|
||||
});
|
||||
|
||||
// TODO: is there a limit to the number of items we can update in a single query?
|
||||
await this._prisma.taskRun.updateMany({
|
||||
where: { friendlyId: { in: Array.from(expiredRunIds) } },
|
||||
data: { idempotencyKey: null },
|
||||
});
|
||||
}
|
||||
|
||||
// Upload to object store
|
||||
const payloadPacket = await this.#handlePayloadPacket(
|
||||
body.items,
|
||||
`batch/${batchId}`,
|
||||
environment
|
||||
);
|
||||
|
||||
const batch = await this.#createAndProcessBatchTaskRun(
|
||||
batchId,
|
||||
runs,
|
||||
payloadPacket,
|
||||
newRunCount,
|
||||
environment,
|
||||
body,
|
||||
options,
|
||||
dependentAttempt ?? undefined
|
||||
);
|
||||
|
||||
if (!batch) {
|
||||
throw new Error("Failed to create batch");
|
||||
}
|
||||
|
||||
return {
|
||||
id: batch.friendlyId,
|
||||
isCached: false,
|
||||
idempotencyKey: batch.idempotencyKey ?? undefined,
|
||||
runs,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async #createAndProcessBatchTaskRun(
|
||||
@@ -336,6 +370,7 @@ export class BatchTriggerV2Service extends BaseService {
|
||||
payloadType: payloadPacket.dataType,
|
||||
options,
|
||||
batchVersion: "v2",
|
||||
oneTimeUseToken: options.oneTimeUseToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -413,6 +448,7 @@ export class BatchTriggerV2Service extends BaseService {
|
||||
payloadType: payloadPacket.dataType,
|
||||
options,
|
||||
batchVersion: "v2",
|
||||
oneTimeUseToken: options.oneTimeUseToken,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -64,9 +64,13 @@ export class FinalizeTaskRunService extends BaseService {
|
||||
completedAt,
|
||||
});
|
||||
|
||||
// I moved the error update here for two reasons:
|
||||
// - A single update is more efficient than two
|
||||
// - If the status updates to a final status, realtime will receive that status and then shut down the stream
|
||||
// before the error is updated, which would cause the error to be lost
|
||||
const run = await this._prisma.taskRun.update({
|
||||
where: { id },
|
||||
data: { status, expiredAt, completedAt },
|
||||
data: { status, expiredAt, completedAt, error: error ? sanitizeError(error) : undefined },
|
||||
...(include ? { include } : {}),
|
||||
});
|
||||
|
||||
@@ -78,10 +82,6 @@ export class FinalizeTaskRunService extends BaseService {
|
||||
await this.finalizeAttempt({ attemptStatus, error, run });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
await this.finalizeRunError(run, error);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#finalizeBatch(run);
|
||||
} catch (finalizeBatchError) {
|
||||
@@ -211,15 +211,6 @@ export class FinalizeTaskRunService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async finalizeRunError(run: TaskRun, error: TaskRunError) {
|
||||
await this._prisma.taskRun.update({
|
||||
where: { id: run.id },
|
||||
data: {
|
||||
error: sanitizeError(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async finalizeAttempt({
|
||||
attemptStatus,
|
||||
error,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class IndexDeploymentService extends BaseService {
|
||||
deployment.id,
|
||||
"DEPLOYING",
|
||||
"Could not index deployment in time",
|
||||
new Date(Date.now() + 180_000)
|
||||
new Date(Date.now() + env.DEPLOY_TIMEOUT_MS)
|
||||
);
|
||||
|
||||
const responses = await socketIo.providerNamespace.timeout(30_000).emitWithAck("INDEX", {
|
||||
|
||||
@@ -64,7 +64,7 @@ export class InitializeDeploymentService extends BaseService {
|
||||
deployment.id,
|
||||
"BUILDING",
|
||||
"Building timed out",
|
||||
new Date(Date.now() + 180_000) // 3 minutes
|
||||
new Date(Date.now() + env.DEPLOY_TIMEOUT_MS)
|
||||
);
|
||||
|
||||
const imageTag = `${payload.namespace ?? env.DEPLOY_REGISTRY_NAMESPACE}/${
|
||||
|
||||
@@ -35,7 +35,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
batchRunId,
|
||||
}
|
||||
);
|
||||
return;
|
||||
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
if (batchRun.status === "COMPLETED") {
|
||||
@@ -46,7 +47,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
status: batchRun.status,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
if (batchRun.items.some((item) => !finishedBatchRunStatuses.includes(item.status))) {
|
||||
@@ -57,7 +59,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
status: batchRun.status,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
return "PENDING";
|
||||
}
|
||||
|
||||
// If we are in development, or there is no dependent attempt, we can just mark the batch as completed and return
|
||||
@@ -71,7 +74,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
return "COMPLETED";
|
||||
}
|
||||
|
||||
const dependentTaskAttempt = await this._prisma.taskRunAttempt.findFirst({
|
||||
@@ -98,12 +102,11 @@ export class ResumeBatchRunService extends BaseService {
|
||||
dependentTaskAttemptId: batchRun.dependentTaskAttemptId,
|
||||
});
|
||||
|
||||
return;
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
// This batch has a dependent attempt and just finalized, we should resume that attempt
|
||||
const environment = batchRun.runtimeEnvironment;
|
||||
|
||||
const dependentRun = dependentTaskAttempt.taskRun;
|
||||
|
||||
if (dependentTaskAttempt.status === "PAUSED" && batchRun.checkpointEventId) {
|
||||
@@ -115,11 +118,13 @@ export class ResumeBatchRunService extends BaseService {
|
||||
|
||||
// We need to update the batchRun status so we don't resume it again
|
||||
const wasUpdated = await this.#setBatchToCompletedOnce(batchRun.id);
|
||||
|
||||
if (wasUpdated) {
|
||||
logger.debug("ResumeBatchRunService: Resuming dependent run with checkpoint", {
|
||||
batchRunId: batchRun.id,
|
||||
dependentTaskAttemptId: dependentTaskAttempt.id,
|
||||
});
|
||||
|
||||
await marqs?.enqueueMessage(
|
||||
environment,
|
||||
dependentRun.queue,
|
||||
@@ -136,6 +141,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
},
|
||||
dependentRun.concurrencyKey ?? undefined
|
||||
);
|
||||
|
||||
return "COMPLETED";
|
||||
} else {
|
||||
logger.debug("ResumeBatchRunService: with checkpoint was already completed", {
|
||||
batchRunId: batchRun.id,
|
||||
@@ -143,6 +150,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
checkpointEventId: batchRun.checkpointEventId,
|
||||
hasCheckpointEvent: !!batchRun.checkpointEventId,
|
||||
});
|
||||
|
||||
return "ALREADY_COMPLETED";
|
||||
}
|
||||
} else {
|
||||
logger.debug("ResumeBatchRunService: attempt is not paused or there's no checkpoint event", {
|
||||
@@ -161,11 +170,13 @@ export class ResumeBatchRunService extends BaseService {
|
||||
checkpointEventId: batchRun.checkpointEventId,
|
||||
hasCheckpointEvent: !!batchRun.checkpointEventId,
|
||||
});
|
||||
return;
|
||||
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
// We need to update the batchRun status so we don't resume it again
|
||||
const wasUpdated = await this.#setBatchToCompletedOnce(batchRun.id);
|
||||
|
||||
if (wasUpdated) {
|
||||
logger.debug("ResumeBatchRunService: Resuming dependent run without checkpoint", {
|
||||
batchRunId: batchRun.id,
|
||||
@@ -173,6 +184,7 @@ export class ResumeBatchRunService extends BaseService {
|
||||
checkpointEventId: batchRun.checkpointEventId,
|
||||
hasCheckpointEvent: !!batchRun.checkpointEventId,
|
||||
});
|
||||
|
||||
await marqs?.replaceMessage(dependentRun.id, {
|
||||
type: "RESUME",
|
||||
completedAttemptIds: batchRun.items.map((item) => item.taskRunAttemptId).filter(Boolean),
|
||||
@@ -183,6 +195,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
environmentId: environment.id,
|
||||
environmentType: environment.type,
|
||||
});
|
||||
|
||||
return "COMPLETED";
|
||||
} else {
|
||||
logger.debug("ResumeBatchRunService: without checkpoint was already completed", {
|
||||
batchRunId: batchRun.id,
|
||||
@@ -190,6 +204,8 @@ export class ResumeBatchRunService extends BaseService {
|
||||
checkpointEventId: batchRun.checkpointEventId,
|
||||
hasCheckpointEvent: !!batchRun.checkpointEventId,
|
||||
});
|
||||
|
||||
return "ALREADY_COMPLETED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ExpireEnqueuedRunService } from "./expireEnqueuedRun.server";
|
||||
import { guardQueueSizeLimitsForEnv } from "../queueSizeLimits.server";
|
||||
import { clampMaxDuration } from "../utils/maxDuration";
|
||||
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
import { Prisma } from "@trigger.dev/database";
|
||||
|
||||
export type TriggerTaskServiceOptions = {
|
||||
idempotencyKey?: string;
|
||||
@@ -38,6 +39,7 @@ export type TriggerTaskServiceOptions = {
|
||||
customIcon?: string;
|
||||
runId?: string;
|
||||
skipChecks?: boolean;
|
||||
oneTimeUseToken?: string;
|
||||
};
|
||||
|
||||
export class OutOfEntitlementError extends Error {
|
||||
@@ -275,191 +277,217 @@ export class TriggerTaskService extends BaseService {
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return await eventRepository.traceEvent(
|
||||
taskId,
|
||||
{
|
||||
context: options.traceContext,
|
||||
spanParentAsLink: options.spanParentAsLink,
|
||||
parentAsLinkType: options.parentAsLinkType,
|
||||
kind: "SERVER",
|
||||
environment,
|
||||
taskSlug: taskId,
|
||||
attributes: {
|
||||
properties: {
|
||||
[SemanticInternalAttributes.SHOW_ACTIONS]: true,
|
||||
try {
|
||||
return await eventRepository.traceEvent(
|
||||
taskId,
|
||||
{
|
||||
context: options.traceContext,
|
||||
spanParentAsLink: options.spanParentAsLink,
|
||||
parentAsLinkType: options.parentAsLinkType,
|
||||
kind: "SERVER",
|
||||
environment,
|
||||
taskSlug: taskId,
|
||||
attributes: {
|
||||
properties: {
|
||||
[SemanticInternalAttributes.SHOW_ACTIONS]: true,
|
||||
},
|
||||
style: {
|
||||
icon: options.customIcon ?? "task",
|
||||
},
|
||||
runIsTest: body.options?.test ?? false,
|
||||
batchId: options.batchId,
|
||||
idempotencyKey,
|
||||
},
|
||||
style: {
|
||||
icon: options.customIcon ?? "task",
|
||||
},
|
||||
runIsTest: body.options?.test ?? false,
|
||||
batchId: options.batchId,
|
||||
idempotencyKey,
|
||||
incomplete: true,
|
||||
immediate: true,
|
||||
},
|
||||
incomplete: true,
|
||||
immediate: true,
|
||||
},
|
||||
async (event, traceContext, traceparent) => {
|
||||
const run = await autoIncrementCounter.incrementInTransaction(
|
||||
`v3-run:${environment.id}:${taskId}`,
|
||||
async (num, tx) => {
|
||||
const lockedToBackgroundWorker = body.options?.lockToVersion
|
||||
? await tx.backgroundWorker.findUnique({
|
||||
where: {
|
||||
projectId_runtimeEnvironmentId_version: {
|
||||
projectId: environment.projectId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
version: body.options?.lockToVersion,
|
||||
async (event, traceContext, traceparent) => {
|
||||
const run = await autoIncrementCounter.incrementInTransaction(
|
||||
`v3-run:${environment.id}:${taskId}`,
|
||||
async (num, tx) => {
|
||||
const lockedToBackgroundWorker = body.options?.lockToVersion
|
||||
? await tx.backgroundWorker.findUnique({
|
||||
where: {
|
||||
projectId_runtimeEnvironmentId_version: {
|
||||
projectId: environment.projectId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
version: body.options?.lockToVersion,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
let queueName = sanitizeQueueName(
|
||||
await this.#getQueueName(taskId, environment, body.options?.queue?.name)
|
||||
);
|
||||
let queueName = sanitizeQueueName(
|
||||
await this.#getQueueName(taskId, environment, body.options?.queue?.name)
|
||||
);
|
||||
|
||||
// Check that the queuename is not an empty string
|
||||
if (!queueName) {
|
||||
queueName = sanitizeQueueName(`task/${taskId}`);
|
||||
}
|
||||
// Check that the queuename is not an empty string
|
||||
if (!queueName) {
|
||||
queueName = sanitizeQueueName(`task/${taskId}`);
|
||||
}
|
||||
|
||||
event.setAttribute("queueName", queueName);
|
||||
span.setAttribute("queueName", queueName);
|
||||
event.setAttribute("queueName", queueName);
|
||||
span.setAttribute("queueName", queueName);
|
||||
|
||||
//upsert tags
|
||||
let tagIds: string[] = [];
|
||||
const bodyTags =
|
||||
typeof body.options?.tags === "string" ? [body.options.tags] : body.options?.tags;
|
||||
if (bodyTags && bodyTags.length > 0) {
|
||||
for (const tag of bodyTags) {
|
||||
const tagRecord = await createTag({
|
||||
tag,
|
||||
projectId: environment.projectId,
|
||||
});
|
||||
if (tagRecord) {
|
||||
tagIds.push(tagRecord.id);
|
||||
//upsert tags
|
||||
let tagIds: string[] = [];
|
||||
const bodyTags =
|
||||
typeof body.options?.tags === "string" ? [body.options.tags] : body.options?.tags;
|
||||
if (bodyTags && bodyTags.length > 0) {
|
||||
for (const tag of bodyTags) {
|
||||
const tagRecord = await createTag({
|
||||
tag,
|
||||
projectId: environment.projectId,
|
||||
});
|
||||
if (tagRecord) {
|
||||
tagIds.push(tagRecord.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const depth = dependentAttempt
|
||||
? dependentAttempt.taskRun.depth + 1
|
||||
: parentAttempt
|
||||
? parentAttempt.taskRun.depth + 1
|
||||
: dependentBatchRun?.dependentTaskAttempt
|
||||
? dependentBatchRun.dependentTaskAttempt.taskRun.depth + 1
|
||||
: 0;
|
||||
const depth = dependentAttempt
|
||||
? dependentAttempt.taskRun.depth + 1
|
||||
: parentAttempt
|
||||
? parentAttempt.taskRun.depth + 1
|
||||
: dependentBatchRun?.dependentTaskAttempt
|
||||
? dependentBatchRun.dependentTaskAttempt.taskRun.depth + 1
|
||||
: 0;
|
||||
|
||||
const taskRun = await tx.taskRun.create({
|
||||
data: {
|
||||
status: delayUntil ? "DELAYED" : "PENDING",
|
||||
number: num,
|
||||
friendlyId: runFriendlyId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
projectId: environment.projectId,
|
||||
idempotencyKey,
|
||||
idempotencyKeyExpiresAt: idempotencyKey ? idempotencyKeyExpiresAt : undefined,
|
||||
taskIdentifier: taskId,
|
||||
payload: payloadPacket.data ?? "",
|
||||
payloadType: payloadPacket.dataType,
|
||||
context: body.context,
|
||||
traceContext: traceContext,
|
||||
traceId: event.traceId,
|
||||
spanId: event.spanId,
|
||||
parentSpanId:
|
||||
options.parentAsLinkType === "replay" ? undefined : traceparent?.spanId,
|
||||
lockedToVersionId: lockedToBackgroundWorker?.id,
|
||||
taskVersion: lockedToBackgroundWorker?.version,
|
||||
sdkVersion: lockedToBackgroundWorker?.sdkVersion,
|
||||
cliVersion: lockedToBackgroundWorker?.cliVersion,
|
||||
concurrencyKey: body.options?.concurrencyKey,
|
||||
queue: queueName,
|
||||
isTest: body.options?.test ?? false,
|
||||
delayUntil,
|
||||
queuedAt: delayUntil ? undefined : new Date(),
|
||||
maxAttempts: body.options?.maxAttempts,
|
||||
ttl,
|
||||
tags:
|
||||
tagIds.length === 0
|
||||
? undefined
|
||||
: {
|
||||
connect: tagIds.map((id) => ({ id })),
|
||||
},
|
||||
parentTaskRunId:
|
||||
dependentAttempt?.taskRun.id ??
|
||||
parentAttempt?.taskRun.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.id,
|
||||
parentTaskRunAttemptId:
|
||||
dependentAttempt?.id ??
|
||||
parentAttempt?.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.id,
|
||||
rootTaskRunId:
|
||||
dependentAttempt?.taskRun.rootTaskRunId ??
|
||||
dependentAttempt?.taskRun.id ??
|
||||
parentAttempt?.taskRun.rootTaskRunId ??
|
||||
parentAttempt?.taskRun.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.rootTaskRunId ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.id,
|
||||
batchId: dependentBatchRun?.id ?? parentBatchRun?.id,
|
||||
resumeParentOnCompletion: !!(dependentAttempt ?? dependentBatchRun),
|
||||
depth,
|
||||
metadata: metadataPacket?.data,
|
||||
metadataType: metadataPacket?.dataType,
|
||||
seedMetadata: metadataPacket?.data,
|
||||
seedMetadataType: metadataPacket?.dataType,
|
||||
maxDurationInSeconds: body.options?.maxDuration
|
||||
? clampMaxDuration(body.options.maxDuration)
|
||||
: undefined,
|
||||
runTags: bodyTags,
|
||||
},
|
||||
});
|
||||
|
||||
event.setAttribute("runId", taskRun.friendlyId);
|
||||
span.setAttribute("runId", taskRun.friendlyId);
|
||||
|
||||
if (dependentAttempt) {
|
||||
await tx.taskRunDependency.create({
|
||||
const taskRun = await tx.taskRun.create({
|
||||
data: {
|
||||
taskRunId: taskRun.id,
|
||||
dependentAttemptId: dependentAttempt.id,
|
||||
},
|
||||
});
|
||||
} else if (dependentBatchRun) {
|
||||
await tx.taskRunDependency.create({
|
||||
data: {
|
||||
taskRunId: taskRun.id,
|
||||
dependentBatchRunId: dependentBatchRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (body.options?.queue) {
|
||||
const concurrencyLimit =
|
||||
typeof body.options.queue.concurrencyLimit === "number"
|
||||
? Math.max(0, body.options.queue.concurrencyLimit)
|
||||
: undefined;
|
||||
|
||||
let taskQueue = await tx.taskQueue.findFirst({
|
||||
where: {
|
||||
status: delayUntil ? "DELAYED" : "PENDING",
|
||||
number: num,
|
||||
friendlyId: runFriendlyId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
name: queueName,
|
||||
projectId: environment.projectId,
|
||||
idempotencyKey,
|
||||
idempotencyKeyExpiresAt: idempotencyKey ? idempotencyKeyExpiresAt : undefined,
|
||||
taskIdentifier: taskId,
|
||||
payload: payloadPacket.data ?? "",
|
||||
payloadType: payloadPacket.dataType,
|
||||
context: body.context,
|
||||
traceContext: traceContext,
|
||||
traceId: event.traceId,
|
||||
spanId: event.spanId,
|
||||
parentSpanId:
|
||||
options.parentAsLinkType === "replay" ? undefined : traceparent?.spanId,
|
||||
lockedToVersionId: lockedToBackgroundWorker?.id,
|
||||
taskVersion: lockedToBackgroundWorker?.version,
|
||||
sdkVersion: lockedToBackgroundWorker?.sdkVersion,
|
||||
cliVersion: lockedToBackgroundWorker?.cliVersion,
|
||||
concurrencyKey: body.options?.concurrencyKey,
|
||||
queue: queueName,
|
||||
isTest: body.options?.test ?? false,
|
||||
delayUntil,
|
||||
queuedAt: delayUntil ? undefined : new Date(),
|
||||
maxAttempts: body.options?.maxAttempts,
|
||||
ttl,
|
||||
tags:
|
||||
tagIds.length === 0
|
||||
? undefined
|
||||
: {
|
||||
connect: tagIds.map((id) => ({ id })),
|
||||
},
|
||||
parentTaskRunId:
|
||||
dependentAttempt?.taskRun.id ??
|
||||
parentAttempt?.taskRun.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.id,
|
||||
parentTaskRunAttemptId:
|
||||
dependentAttempt?.id ??
|
||||
parentAttempt?.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.id,
|
||||
rootTaskRunId:
|
||||
dependentAttempt?.taskRun.rootTaskRunId ??
|
||||
dependentAttempt?.taskRun.id ??
|
||||
parentAttempt?.taskRun.rootTaskRunId ??
|
||||
parentAttempt?.taskRun.id ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.rootTaskRunId ??
|
||||
dependentBatchRun?.dependentTaskAttempt?.taskRun.id,
|
||||
batchId: dependentBatchRun?.id ?? parentBatchRun?.id,
|
||||
resumeParentOnCompletion: !!(dependentAttempt ?? dependentBatchRun),
|
||||
depth,
|
||||
metadata: metadataPacket?.data,
|
||||
metadataType: metadataPacket?.dataType,
|
||||
seedMetadata: metadataPacket?.data,
|
||||
seedMetadataType: metadataPacket?.dataType,
|
||||
maxDurationInSeconds: body.options?.maxDuration
|
||||
? clampMaxDuration(body.options.maxDuration)
|
||||
: undefined,
|
||||
runTags: bodyTags,
|
||||
oneTimeUseToken: options.oneTimeUseToken,
|
||||
},
|
||||
});
|
||||
|
||||
const existingConcurrencyLimit =
|
||||
typeof taskQueue?.concurrencyLimit === "number"
|
||||
? taskQueue.concurrencyLimit
|
||||
: undefined;
|
||||
event.setAttribute("runId", taskRun.friendlyId);
|
||||
span.setAttribute("runId", taskRun.friendlyId);
|
||||
|
||||
if (taskQueue) {
|
||||
if (existingConcurrencyLimit !== concurrencyLimit) {
|
||||
taskQueue = await tx.taskQueue.update({
|
||||
where: {
|
||||
id: taskQueue.id,
|
||||
},
|
||||
if (dependentAttempt) {
|
||||
await tx.taskRunDependency.create({
|
||||
data: {
|
||||
taskRunId: taskRun.id,
|
||||
dependentAttemptId: dependentAttempt.id,
|
||||
},
|
||||
});
|
||||
} else if (dependentBatchRun) {
|
||||
await tx.taskRunDependency.create({
|
||||
data: {
|
||||
taskRunId: taskRun.id,
|
||||
dependentBatchRunId: dependentBatchRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (body.options?.queue) {
|
||||
const concurrencyLimit =
|
||||
typeof body.options.queue.concurrencyLimit === "number"
|
||||
? Math.max(0, body.options.queue.concurrencyLimit)
|
||||
: undefined;
|
||||
|
||||
let taskQueue = await tx.taskQueue.findFirst({
|
||||
where: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
name: queueName,
|
||||
},
|
||||
});
|
||||
|
||||
const existingConcurrencyLimit =
|
||||
typeof taskQueue?.concurrencyLimit === "number"
|
||||
? taskQueue.concurrencyLimit
|
||||
: undefined;
|
||||
|
||||
if (taskQueue) {
|
||||
if (existingConcurrencyLimit !== concurrencyLimit) {
|
||||
taskQueue = await tx.taskQueue.update({
|
||||
where: {
|
||||
id: taskQueue.id,
|
||||
},
|
||||
data: {
|
||||
concurrencyLimit:
|
||||
typeof concurrencyLimit === "number" ? concurrencyLimit : null,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof taskQueue.concurrencyLimit === "number") {
|
||||
await marqs?.updateQueueConcurrencyLimits(
|
||||
environment,
|
||||
taskQueue.name,
|
||||
taskQueue.concurrencyLimit
|
||||
);
|
||||
} else {
|
||||
await marqs?.removeQueueConcurrencyLimits(environment, taskQueue.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const queueId = generateFriendlyId("queue");
|
||||
|
||||
taskQueue = await tx.taskQueue.create({
|
||||
data: {
|
||||
concurrencyLimit:
|
||||
typeof concurrencyLimit === "number" ? concurrencyLimit : null,
|
||||
friendlyId: queueId,
|
||||
name: queueName,
|
||||
concurrencyLimit,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
projectId: environment.projectId,
|
||||
type: "NAMED",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -469,106 +497,113 @@ export class TriggerTaskService extends BaseService {
|
||||
taskQueue.name,
|
||||
taskQueue.concurrencyLimit
|
||||
);
|
||||
} else {
|
||||
await marqs?.removeQueueConcurrencyLimits(environment, taskQueue.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const queueId = generateFriendlyId("queue");
|
||||
}
|
||||
|
||||
taskQueue = await tx.taskQueue.create({
|
||||
data: {
|
||||
friendlyId: queueId,
|
||||
name: queueName,
|
||||
concurrencyLimit,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
projectId: environment.projectId,
|
||||
type: "NAMED",
|
||||
},
|
||||
});
|
||||
if (taskRun.delayUntil) {
|
||||
await workerQueue.enqueue(
|
||||
"v3.enqueueDelayedRun",
|
||||
{ runId: taskRun.id },
|
||||
{ tx, runAt: delayUntil, jobKey: `v3.enqueueDelayedRun.${taskRun.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof taskQueue.concurrencyLimit === "number") {
|
||||
await marqs?.updateQueueConcurrencyLimits(
|
||||
environment,
|
||||
taskQueue.name,
|
||||
taskQueue.concurrencyLimit
|
||||
);
|
||||
if (!taskRun.delayUntil && taskRun.ttl) {
|
||||
const expireAt = parseNaturalLanguageDuration(taskRun.ttl);
|
||||
|
||||
if (expireAt) {
|
||||
await ExpireEnqueuedRunService.enqueue(taskRun.id, expireAt, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (taskRun.delayUntil) {
|
||||
await workerQueue.enqueue(
|
||||
"v3.enqueueDelayedRun",
|
||||
{ runId: taskRun.id },
|
||||
{ tx, runAt: delayUntil, jobKey: `v3.enqueueDelayedRun.${taskRun.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
if (!taskRun.delayUntil && taskRun.ttl) {
|
||||
const expireAt = parseNaturalLanguageDuration(taskRun.ttl);
|
||||
|
||||
if (expireAt) {
|
||||
await ExpireEnqueuedRunService.enqueue(taskRun.id, expireAt, tx);
|
||||
}
|
||||
}
|
||||
|
||||
return taskRun;
|
||||
},
|
||||
async (_, tx) => {
|
||||
const counter = await tx.taskRunNumberCounter.findUnique({
|
||||
where: {
|
||||
taskIdentifier_environmentId: {
|
||||
taskIdentifier: taskId,
|
||||
environmentId: environment.id,
|
||||
},
|
||||
},
|
||||
select: { lastNumber: true },
|
||||
});
|
||||
|
||||
return counter?.lastNumber;
|
||||
},
|
||||
this._prisma
|
||||
);
|
||||
|
||||
//release the concurrency for the env and org, if part of a (batch)triggerAndWait
|
||||
if (dependentAttempt) {
|
||||
const isSameTask = dependentAttempt.taskRun.taskIdentifier === taskId;
|
||||
await marqs?.releaseConcurrency(dependentAttempt.taskRun.id, isSameTask);
|
||||
}
|
||||
if (dependentBatchRun?.dependentTaskAttempt) {
|
||||
const isSameTask =
|
||||
dependentBatchRun.dependentTaskAttempt.taskRun.taskIdentifier === taskId;
|
||||
await marqs?.releaseConcurrency(
|
||||
dependentBatchRun.dependentTaskAttempt.taskRun.id,
|
||||
isSameTask
|
||||
);
|
||||
}
|
||||
|
||||
if (!run) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to enqueue the task run into the appropriate queue. This is done after the tx completes to prevent a race condition where the task run hasn't been created yet by the time we dequeue.
|
||||
if (run.status === "PENDING") {
|
||||
await marqs?.enqueueMessage(
|
||||
environment,
|
||||
run.queue,
|
||||
run.id,
|
||||
{
|
||||
type: "EXECUTE",
|
||||
taskIdentifier: taskId,
|
||||
projectId: environment.projectId,
|
||||
environmentId: environment.id,
|
||||
environmentType: environment.type,
|
||||
return taskRun;
|
||||
},
|
||||
body.options?.concurrencyKey
|
||||
);
|
||||
}
|
||||
async (_, tx) => {
|
||||
const counter = await tx.taskRunNumberCounter.findUnique({
|
||||
where: {
|
||||
taskIdentifier_environmentId: {
|
||||
taskIdentifier: taskId,
|
||||
environmentId: environment.id,
|
||||
},
|
||||
},
|
||||
select: { lastNumber: true },
|
||||
});
|
||||
|
||||
return run;
|
||||
return counter?.lastNumber;
|
||||
},
|
||||
this._prisma
|
||||
);
|
||||
|
||||
//release the concurrency for the env and org, if part of a (batch)triggerAndWait
|
||||
if (dependentAttempt) {
|
||||
const isSameTask = dependentAttempt.taskRun.taskIdentifier === taskId;
|
||||
await marqs?.releaseConcurrency(dependentAttempt.taskRun.id, isSameTask);
|
||||
}
|
||||
if (dependentBatchRun?.dependentTaskAttempt) {
|
||||
const isSameTask =
|
||||
dependentBatchRun.dependentTaskAttempt.taskRun.taskIdentifier === taskId;
|
||||
await marqs?.releaseConcurrency(
|
||||
dependentBatchRun.dependentTaskAttempt.taskRun.id,
|
||||
isSameTask
|
||||
);
|
||||
}
|
||||
|
||||
if (!run) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to enqueue the task run into the appropriate queue. This is done after the tx completes to prevent a race condition where the task run hasn't been created yet by the time we dequeue.
|
||||
if (run.status === "PENDING") {
|
||||
await marqs?.enqueueMessage(
|
||||
environment,
|
||||
run.queue,
|
||||
run.id,
|
||||
{
|
||||
type: "EXECUTE",
|
||||
taskIdentifier: taskId,
|
||||
projectId: environment.projectId,
|
||||
environmentId: environment.id,
|
||||
environmentType: environment.type,
|
||||
},
|
||||
body.options?.concurrencyKey
|
||||
);
|
||||
}
|
||||
|
||||
return run;
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Detect a prisma transaction Unique constraint violation
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
logger.debug("TriggerTask: Prisma transaction error", {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
meta: error.meta,
|
||||
});
|
||||
|
||||
if (error.code === "P2002") {
|
||||
const target = error.meta?.target;
|
||||
|
||||
if (
|
||||
Array.isArray(target) &&
|
||||
target.length > 0 &&
|
||||
typeof target[0] === "string" &&
|
||||
target[0].includes("oneTimeUseToken")
|
||||
) {
|
||||
throw new ServiceValidationError(
|
||||
`Cannot trigger ${taskId} with a one-time use token as it has already been used.`
|
||||
);
|
||||
} else {
|
||||
throw new ServiceValidationError(
|
||||
`Cannot trigger ${taskId} as it has already been triggered with the same idempotency key.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ describe("checkAuthorization", () => {
|
||||
scopes: ["read:runs:run_1234", "read:tasks", "read:tags:tag_5678"],
|
||||
};
|
||||
const publicJwtEntityNoPermissions: AuthorizationEntity = { type: "PUBLIC_JWT" };
|
||||
const publicJwtEntityWithTaskWritePermissions: AuthorizationEntity = {
|
||||
type: "PUBLIC_JWT",
|
||||
scopes: ["write:tasks:task-1"],
|
||||
};
|
||||
|
||||
describe("PRIVATE entity", () => {
|
||||
it("should always return authorized regardless of action or resource", () => {
|
||||
@@ -49,6 +53,28 @@ describe("checkAuthorization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUBLIC_JWT entity with task write scope", () => {
|
||||
it("should return authorized for specific resource scope", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithTaskWritePermissions, "write", {
|
||||
tasks: "task-1",
|
||||
});
|
||||
expect(result.authorized).toBe(true);
|
||||
expect(result).not.toHaveProperty("reason");
|
||||
});
|
||||
|
||||
it("should return unauthorized with reason for unauthorized specific resources", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithTaskWritePermissions, "write", {
|
||||
tasks: "task-2",
|
||||
});
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Token has the following permissions: 'write:tasks:task-1'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUBLIC_JWT entity with scope", () => {
|
||||
it("should return authorized for specific resource scope", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithPermissions, "read", {
|
||||
@@ -65,7 +91,7 @@ describe("checkAuthorization", () => {
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Permissions required for 'read:runs:run_5678' but token has the following permissions: 'read:runs:run_1234', 'read:tasks', 'read:tags:tag_5678'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
"Public Access Token is missing required permissions. Token has the following permissions: 'read:runs:run_1234', 'read:tasks', 'read:tags:tag_5678'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -97,8 +123,8 @@ describe("checkAuthorization", () => {
|
||||
// @ts-expect-error
|
||||
nonexistent: "resource",
|
||||
});
|
||||
expect(result.authorized).toBe(true);
|
||||
expect(result).not.toHaveProperty("reason");
|
||||
expect(result.authorized).toBe(false);
|
||||
expect(result).toHaveProperty("reason");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,29 +193,26 @@ describe("checkAuthorization", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return unauthorized if any resource is not authorized", () => {
|
||||
it("should return authorized if any resource is authorized", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithPermissions, "read", {
|
||||
runs: "run_1234", // This is authorized
|
||||
tasks: "task_5678", // This is authorized (general permission)
|
||||
tags: "tag_3456", // This is not authorized
|
||||
});
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Permissions required for 'read:tags:tag_3456' but token has the following permissions: 'read:runs:run_1234', 'read:tasks', 'read:tags:tag_5678'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return authorized only if all resources are authorized", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithPermissions, "read", {
|
||||
runs: "run_1234", // This is authorized
|
||||
tasks: "task_5678", // This is authorized (general permission)
|
||||
tags: "tag_5678", // This is authorized
|
||||
});
|
||||
expect(result.authorized).toBe(true);
|
||||
expect(result).not.toHaveProperty("reason");
|
||||
});
|
||||
|
||||
it("should return unauthorized only if no resources are authorized", () => {
|
||||
const result = checkAuthorization(publicJwtEntityWithPermissions, "read", {
|
||||
runs: "run_5678", // Not authorized
|
||||
tags: "tag_3456", // Not authorized
|
||||
});
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toContain("Public Access Token is missing required permissions");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Super scope", () => {
|
||||
@@ -244,7 +267,7 @@ describe("checkAuthorization", () => {
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Permissions required for 'read:tasks:task_1234' but token has the following permissions: 'read:all'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
"Public Access Token is missing required permissions. Token has the following permissions: 'read:all'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -281,7 +304,7 @@ describe("checkAuthorization", () => {
|
||||
expect(result2.authorized).toBe(false);
|
||||
if (!result2.authorized) {
|
||||
expect(result2.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Permissions required for 'read:runs:run_5678' but token has the following permissions: 'read:tasks', 'read:tags'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
"Public Access Token is missing required permissions. Token has the following permissions: 'read:tasks', 'read:tags'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -314,7 +337,7 @@ describe("checkAuthorization", () => {
|
||||
expect(result.authorized).toBe(false);
|
||||
if (!result.authorized) {
|
||||
expect(result.reason).toBe(
|
||||
"Public Access Token is missing required permissions. Permissions required for 'read:runs:run_5678' but token has the following permissions: 'read:tasks'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
"Public Access Token is missing required permissions. Token has the following permissions: 'read:tasks'. See https://trigger.dev/docs/frontend/overview#authentication for more information."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,18 +11,18 @@ You can use our [React hooks](/frontend/react-hooks) in your frontend applicatio
|
||||
To create a Public Access Token, you can use the `auth.createPublicToken` function in your **backend** code:
|
||||
|
||||
```tsx
|
||||
const publicToken = await auth.createPublicToken();
|
||||
const publicToken = await auth.createPublicToken(); // 👈 this public access token has no permissions, so is pretty useless!
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
By default a Public Access Token has limited permissions. You can specify the scopes you need when creating a Public Access Token:
|
||||
By default a Public Access Token has no permissions. You must specify the scopes you need when creating a Public Access Token:
|
||||
|
||||
```ts
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: true,
|
||||
runs: true, // ❌ this token can read all runs, possibly useful for debugging/testing
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -34,7 +34,7 @@ This will allow the token to read all runs, which is probably not what you want.
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: ["run_1234", "run_5678"],
|
||||
runs: ["run_1234", "run_5678"], // ✅ this token can read only these runs
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -46,7 +46,7 @@ You can scope the token to only read certain tasks:
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
tasks: ["my-task-1", "my-task-2"],
|
||||
tasks: ["my-task-1", "my-task-2"], // 👈 this token can read all runs of these tasks
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -58,7 +58,7 @@ Or tags:
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
tags: ["my-tag-1", "my-tag-2"],
|
||||
tags: ["my-tag-1", "my-tag-2"], // 👈 this token can read all runs with these tags
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -70,13 +70,13 @@ Or a specific batch of runs:
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
batch: "batch_1234",
|
||||
batch: "batch_1234", // 👈 this token can read all runs in this batch
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also combine scopes. For example, to read only certain tasks and tags:
|
||||
You can also combine scopes. For example, to read runs with specific tags and for specific tasks:
|
||||
|
||||
```ts
|
||||
const publicToken = await auth.createPublicToken({
|
||||
@@ -89,22 +89,6 @@ const publicToken = await auth.createPublicToken({
|
||||
});
|
||||
```
|
||||
|
||||
### Write scopes
|
||||
|
||||
You can also specify write scopes, which is required for triggering tasks from your frontend application:
|
||||
|
||||
```ts
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
write: {
|
||||
tasks: ["my-task-1", "my-task-2"],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This will allow the token to trigger the specified tasks. `tasks` is the only write scope available at the moment.
|
||||
|
||||
### Expiration
|
||||
|
||||
By default, Public Access Token's expire after 15 minutes. You can specify a different expiration time when creating a Public Access Token:
|
||||
@@ -133,7 +117,7 @@ const handle = await tasks.trigger("my-task", { some: "data" });
|
||||
console.log(handle.publicAccessToken);
|
||||
```
|
||||
|
||||
By default, tokens returned from the `trigger` function expire after 15 minutes and have a read scope for that specific run, and any tags associated with it. You can customize the expiration of the auto-generated tokens by passing a `publicTokenOptions` object to the `trigger` function:
|
||||
By default, tokens returned from the `trigger` function expire after 15 minutes and have a read scope for that specific run. You can customize the expiration of the auto-generated tokens by passing a `publicTokenOptions` object to the `trigger` function:
|
||||
|
||||
```ts
|
||||
const handle = await tasks.trigger(
|
||||
|
||||
@@ -1,797 +0,0 @@
|
||||
---
|
||||
title: React hooks
|
||||
sidebarTitle: React hooks
|
||||
description: Using the Trigger.dev v3 API from your React application.
|
||||
---
|
||||
|
||||
Our react hooks package provides a set of hooks that make it easy to interact with the Trigger.dev API from your React application, using our [frontend API](/frontend/overview). You can use these hooks to fetch runs, batches, and subscribe to real-time updates.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the `@trigger.dev/react-hooks` package in your project:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn install @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
All hooks accept an optional last argument `options` that accepts an `accessToken` param, which should be a valid Public Access Token. Learn more about [generating tokens in the frontend guide](/frontend/overview).
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken, // This is required
|
||||
baseURL: "https://your-trigger-dev-instance.com", // optional, only needed if you are self-hosting Trigger.dev
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use our `TriggerAuthContext` provider
|
||||
|
||||
```tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function SetupTrigger({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider value={{ accessToken: publicAccessToken }}>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now children components can use the hooks to interact with the Trigger.dev API. If you are self-hosting Trigger.dev, you can provide the `baseURL` to the `TriggerAuthContext` provider.
|
||||
|
||||
```tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function SetupTrigger({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider
|
||||
value={{
|
||||
accessToken: publicAccessToken,
|
||||
baseURL: "https://your-trigger-dev-instance.com",
|
||||
}}
|
||||
>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js and client components
|
||||
|
||||
If you are using Next.js with the App Router, you have to make sure the component that uses the `TriggerAuthContext` is a client component. So for example, the following code will not work:
|
||||
|
||||
```tsx app/page.tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<TriggerAuthContext.Provider value={{ accessToken: "your-access-token" }}>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
That's because `Page` is a server component and the `TriggerAuthContext.Provider` uses client-only react code. To fix this, wrap the `TriggerAuthContext.Provider` in a client component:
|
||||
|
||||
```ts components/TriggerProvider.tsx
|
||||
"use client";
|
||||
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function TriggerProvider({
|
||||
accessToken,
|
||||
children,
|
||||
}: {
|
||||
accessToken: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider
|
||||
value={{
|
||||
accessToken,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Passing the token to the frontend
|
||||
|
||||
Techniques for passing the token to the frontend vary depending on your setup. Here are a few ways to do it for different setups:
|
||||
|
||||
#### Next.js App Router
|
||||
|
||||
If you are using Next.js with the App Router and you are triggering a task from a server action, you can use cookies to store and pass the token to the frontend.
|
||||
|
||||
```tsx actions/trigger.ts
|
||||
"use server";
|
||||
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { exampleTask } from "@/trigger/example";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function startRun() {
|
||||
const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
|
||||
|
||||
// Set the auto-generated publicAccessToken in a cookie
|
||||
cookies().set("publicAccessToken", handle.publicAccessToken);
|
||||
|
||||
redirect(`/runs/${handle.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
Then in the `/runs/[id].tsx` page, you can read the token from the cookie and pass it to the `TriggerProvider`.
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
|
||||
export default function RunPage({ params }: { params: { id: string } }) {
|
||||
const publicAccessToken = cookies().get("publicAccessToken");
|
||||
|
||||
return (
|
||||
<TriggerProvider accessToken={publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Instead of a cookie, you could also use a query parameter to pass the token to the frontend:
|
||||
|
||||
```tsx actions/trigger.ts
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { exampleTask } from "@/trigger/example";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function startRun() {
|
||||
const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
|
||||
|
||||
redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`);
|
||||
}
|
||||
```
|
||||
|
||||
And then in the `/runs/[id].tsx` page:
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
|
||||
export default function RunPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { id: string };
|
||||
searchParams: { publicAccessToken: string };
|
||||
}) {
|
||||
return (
|
||||
<TriggerProvider accessToken={searchParams.publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Another alternative would be to use a server-side rendered page to fetch the token and pass it to the frontend:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
import { generatePublicAccessToken } from "@/trigger/auth";
|
||||
|
||||
export default async function RunPage({ params }: { params: { id: string } }) {
|
||||
// This will be executed on the server only
|
||||
const publicAccessToken = await generatePublicAccessToken(params.id);
|
||||
|
||||
return (
|
||||
<TriggerProvider accessToken={publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx trigger/auth.ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export async function generatePublicAccessToken(runId: string) {
|
||||
return auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: [runId],
|
||||
},
|
||||
},
|
||||
expirationTime: "1h",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## SWR vs Realtime hooks
|
||||
|
||||
We offer two "styles" of hooks: SWR and Realtime. The SWR hooks use the [swr](https://swr.vercel.app/) library to fetch data once and cache it. The Realtime hooks use [Trigger.dev realtime](/realtime) to subscribe to updates in real-time.
|
||||
|
||||
<Note>
|
||||
It can be a little confusing which one to use because [swr](https://swr.vercel.app/) can also be
|
||||
configured to poll for updates. But because of rate-limits and the way the Trigger.dev API works,
|
||||
we recommend using the Realtime hooks for most use-cases.
|
||||
</Note>
|
||||
|
||||
All hooks named `useRealtime*` are Realtime hooks, and all hooks named `use*` are SWR hooks.
|
||||
|
||||
## Realtime hooks
|
||||
|
||||
### useRealtimeRun
|
||||
|
||||
The `useRealtimeRun` hook allows you to subscribe to a run by its ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
To correctly type the run's payload and output, you can provide the type of your task to the `useRealtimeRun` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun<typeof myTask>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now run.payload and run.output are correctly typed
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
See our [Realtime documentation](/realtime) for more information about the type of the run object and more.
|
||||
|
||||
### useRealtimeRunsWithTag
|
||||
|
||||
The `useRealtimeRunsWithTag` hook allows you to subscribe to multiple runs with a specific tag.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
To correctly type the runs payload and output, you can provide the type of your task to the `useRealtimeRunsWithTag` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag<typeof myTask>(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now runs[i].payload and runs[i].output are correctly typed
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If `useRealtimeRunsWithTag` could return multiple different types of tasks, you can pass a union of all the task types to the hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
import type { myTask1, myTask2 } from "@/trigger/myTasks";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag<typeof myTask1 | typeof myTask2>(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// You can narrow down the type of the run based on the taskIdentifier
|
||||
for (const run of runs) {
|
||||
if (run.taskIdentifier === "my-task-1") {
|
||||
// run is correctly typed as myTask1
|
||||
} else if (run.taskIdentifier === "my-task-2") {
|
||||
// run is correctly typed as myTask2
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See our [Realtime documentation](/realtime) for more information.
|
||||
|
||||
### useRealtimeBatch
|
||||
|
||||
The `useRealtimeBatch` hook allows you to subscribe to a batch of runs by its the batch ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeBatch } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ batchId }: { batchId: string }) {
|
||||
const { runs, error } = useRealtimeBatch(batchId);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See our [Realtime documentation](/realtime) for more information.
|
||||
|
||||
### useRealtimeRunWithStreams
|
||||
|
||||
The `useRealtimeRunWithStreams` hook allows you to subscribe to a run by its ID and also receive any streams that are emitted by the task. See our [Realtime documentation](/realtime#streams) for more information about emitting streams from a task.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>
|
||||
{Object.keys(streams).map((stream) => (
|
||||
<div key={stream}>Stream: {stream}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can provide the type of the streams to the `useRealtimeRunWithStreams` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above, each stream is an array of the type you provided, keyed by the stream name. If instead of a pure text stream you have a stream of objects, you can provide the type of the object:
|
||||
|
||||
```tsx
|
||||
import type { TextStreamPart } from "ai";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = { openai: TextStreamPart<{}> };
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai
|
||||
?.filter((stream) => stream.type === "text-delta")
|
||||
?.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Common options
|
||||
|
||||
#### enabled
|
||||
|
||||
You can pass the `enabled` option to the Realtime hooks to enable or disable the subscription.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
enabled,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
enabled,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to conditionally disable using the hook based on some state.
|
||||
|
||||
#### id
|
||||
|
||||
You can pass the `id` option to the Realtime hooks to change the ID of the subscription.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
id,
|
||||
runId,
|
||||
publicAccessToken,
|
||||
enabled,
|
||||
}: {
|
||||
id: string;
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
enabled,
|
||||
id,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to change the ID of the subscription based on some state. Passing in a different ID will unsubscribe from the current subscription and subscribe to the new one (and remove any cached data).
|
||||
|
||||
#### experimental_throttleInMs
|
||||
|
||||
The `*withStreams` variants of the Realtime hooks accept an `experimental_throttleInMs` option to throttle the updates from the server. This can be useful if you are getting too many updates and want to reduce the number of updates.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { runs, error } = useRealtimeRunsWithStreams(tag, {
|
||||
accessToken: publicAccessToken,
|
||||
experimental_throttleInMs: 1000, // Throttle updates to once per second
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## SWR Hooks
|
||||
|
||||
### useRun
|
||||
|
||||
The `useRun` hook allows you to fetch a run by its ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun(runId);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
The `run` object returned is the same as the [run object](/management/runs/retrieve) returned by the Trigger.dev API. To correctly type the run's payload and output, you can provide the type of your task to the `useRun` hook:
|
||||
|
||||
```tsx
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun<typeof myTask>(runId, {
|
||||
refreshInterval: 0, // Disable polling
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now run.payload and run.output are correctly typed
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common options
|
||||
|
||||
You can pass the following options to the all SWR hooks:
|
||||
|
||||
<ParamField path="revalidateOnFocus" type="boolean">
|
||||
Revalidate the data when the window regains focus.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="revalidateOnReconnect" type="boolean">
|
||||
Revalidate the data when the browser regains a network connection.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="refreshInterval" type="number">
|
||||
Poll for updates at the specified interval (in milliseconds). Polling is not recommended for most
|
||||
use-cases. Use the Realtime hooks instead.
|
||||
</ParamField>
|
||||
|
||||
### Common return values
|
||||
|
||||
<ResponseField name="error" type="Error">
|
||||
An error object if an error occurred while fetching the data.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isLoading" type="boolean">
|
||||
A boolean indicating if the data is currently being fetched.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isValidating" type="boolean">
|
||||
A boolean indicating if the data is currently being revalidated.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isError" type="boolean">
|
||||
A boolean indicating if an error occurred while fetching the data.
|
||||
</ResponseField>
|
||||
|
||||
## Trigger Hooks
|
||||
|
||||
We provide a set of hooks that can be used to trigger tasks from your frontend application. You'll need to generate a Public Access Token with `write` permissions to use these hooks. See our [frontend guide](/frontend/overview#write-scopes) for more information.
|
||||
|
||||
### useTaskTrigger
|
||||
|
||||
The `useTaskTrigger` hook allows you to trigger a task from your frontend application.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, handle, error, isLoading } = useTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
return <div>Run ID: {handle.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useRealtimeTaskTrigger
|
||||
|
||||
The `useRealtimeTaskTrigger` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime:
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, error, isLoading } = useRealtimeTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
// This is the realtime run object, which will automatically update when the run changes
|
||||
if (run) {
|
||||
return <div>Run ID: {run.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useRealtimeTaskTriggerWithStreams
|
||||
|
||||
The `useRealtimeTaskTriggerWithStreams` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime, and also receive any streams that are emitted by the task.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTriggerWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, streams, error, isLoading } = useRealtimeTaskTriggerWithStreams<
|
||||
typeof myTask,
|
||||
STREAMS
|
||||
>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (streams && run) {
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run ID: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,333 @@
|
||||
---
|
||||
title: Overview
|
||||
sidebarTitle: Overview
|
||||
description: Using the Trigger.dev v3 API from your React application.
|
||||
---
|
||||
|
||||
Our react hooks package provides a set of hooks that make it easy to interact with the Trigger.dev API from your React application, using our [frontend API](/frontend/overview). You can use these hooks to fetch runs, and subscribe to real-time updates, and trigger tasks from your frontend application.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the `@trigger.dev/react-hooks` package in your project:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn install @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
All hooks accept an optional last argument `options` that accepts an `accessToken` param, which should be a valid Public Access Token. Learn more about [generating tokens in the frontend guide](/frontend/overview).
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken, // This is required
|
||||
baseURL: "https://your-trigger-dev-instance.com", // optional, only needed if you are self-hosting Trigger.dev
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use our `TriggerAuthContext` provider
|
||||
|
||||
```tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function SetupTrigger({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider value={{ accessToken: publicAccessToken }}>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now children components can use the hooks to interact with the Trigger.dev API. If you are self-hosting Trigger.dev, you can provide the `baseURL` to the `TriggerAuthContext` provider.
|
||||
|
||||
```tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function SetupTrigger({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider
|
||||
value={{
|
||||
accessToken: publicAccessToken,
|
||||
baseURL: "https://your-trigger-dev-instance.com",
|
||||
}}
|
||||
>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js and client components
|
||||
|
||||
If you are using Next.js with the App Router, you have to make sure the component that uses the `TriggerAuthContext` is a client component. So for example, the following code will not work:
|
||||
|
||||
```tsx app/page.tsx
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<TriggerAuthContext.Provider value={{ accessToken: "your-access-token" }}>
|
||||
<MyComponent />
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
That's because `Page` is a server component and the `TriggerAuthContext.Provider` uses client-only react code. To fix this, wrap the `TriggerAuthContext.Provider` in a client component:
|
||||
|
||||
```ts components/TriggerProvider.tsx
|
||||
"use client";
|
||||
|
||||
import { TriggerAuthContext } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function TriggerProvider({
|
||||
accessToken,
|
||||
children,
|
||||
}: {
|
||||
accessToken: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TriggerAuthContext.Provider
|
||||
value={{
|
||||
accessToken,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TriggerAuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Passing the token to the frontend
|
||||
|
||||
Techniques for passing the token to the frontend vary depending on your setup. Here are a few ways to do it for different setups:
|
||||
|
||||
#### Next.js App Router
|
||||
|
||||
If you are using Next.js with the App Router and you are triggering a task from a server action, you can use cookies to store and pass the token to the frontend.
|
||||
|
||||
```tsx actions/trigger.ts
|
||||
"use server";
|
||||
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { exampleTask } from "@/trigger/example";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function startRun() {
|
||||
const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
|
||||
|
||||
// Set the auto-generated publicAccessToken in a cookie
|
||||
cookies().set("publicAccessToken", handle.publicAccessToken); // ✅ this token only has access to read this run
|
||||
|
||||
redirect(`/runs/${handle.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
Then in the `/runs/[id].tsx` page, you can read the token from the cookie and pass it to the `TriggerProvider`.
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
|
||||
export default function RunPage({ params }: { params: { id: string } }) {
|
||||
const publicAccessToken = cookies().get("publicAccessToken");
|
||||
|
||||
return (
|
||||
<TriggerProvider accessToken={publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Instead of a cookie, you could also use a query parameter to pass the token to the frontend:
|
||||
|
||||
```tsx actions/trigger.ts
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { exampleTask } from "@/trigger/example";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function startRun() {
|
||||
const handle = await tasks.trigger<typeof exampleTask>("example", { foo: "bar" });
|
||||
|
||||
redirect(`/runs/${handle.id}?publicAccessToken=${handle.publicAccessToken}`);
|
||||
}
|
||||
```
|
||||
|
||||
And then in the `/runs/[id].tsx` page:
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
|
||||
export default function RunPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { id: string };
|
||||
searchParams: { publicAccessToken: string };
|
||||
}) {
|
||||
return (
|
||||
<TriggerProvider accessToken={searchParams.publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Another alternative would be to use a server-side rendered page to fetch the token and pass it to the frontend:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```tsx pages/runs/[id].tsx
|
||||
import { TriggerProvider } from "@/components/TriggerProvider";
|
||||
import { generatePublicAccessToken } from "@/trigger/auth";
|
||||
|
||||
export default async function RunPage({ params }: { params: { id: string } }) {
|
||||
// This will be executed on the server only
|
||||
const publicAccessToken = await generatePublicAccessToken(params.id);
|
||||
|
||||
return (
|
||||
<TriggerProvider accessToken={publicAccessToken}>
|
||||
<RunDetails id={params.id} />
|
||||
</TriggerProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx trigger/auth.ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export async function generatePublicAccessToken(runId: string) {
|
||||
return auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: [runId],
|
||||
},
|
||||
},
|
||||
expirationTime: "1h",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## SWR vs Realtime hooks
|
||||
|
||||
We offer two "styles" of hooks: SWR and Realtime. The SWR hooks use the [swr](https://swr.vercel.app/) library to fetch data once and cache it. The Realtime hooks use [Trigger.dev realtime](/realtime) to subscribe to updates in real-time.
|
||||
|
||||
<Note>
|
||||
It can be a little confusing which one to use because [swr](https://swr.vercel.app/) can also be
|
||||
configured to poll for updates. But because of rate-limits and the way the Trigger.dev API works,
|
||||
we recommend using the Realtime hooks for most use-cases.
|
||||
</Note>
|
||||
|
||||
## SWR Hooks
|
||||
|
||||
### useRun
|
||||
|
||||
The `useRun` hook allows you to fetch a run by its ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun(runId);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
The `run` object returned is the same as the [run object](/management/runs/retrieve) returned by the Trigger.dev API. To correctly type the run's payload and output, you can provide the type of your task to the `useRun` hook:
|
||||
|
||||
```tsx
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun<typeof myTask>(runId, {
|
||||
refreshInterval: 0, // Disable polling
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now run.payload and run.output are correctly typed
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common options
|
||||
|
||||
You can pass the following options to the all SWR hooks:
|
||||
|
||||
<ParamField path="revalidateOnFocus" type="boolean">
|
||||
Revalidate the data when the window regains focus.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="revalidateOnReconnect" type="boolean">
|
||||
Revalidate the data when the browser regains a network connection.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="refreshInterval" type="number">
|
||||
Poll for updates at the specified interval (in milliseconds). Polling is not recommended for most
|
||||
use-cases. Use the Realtime hooks instead.
|
||||
</ParamField>
|
||||
|
||||
### Common return values
|
||||
|
||||
<ResponseField name="error" type="Error">
|
||||
An error object if an error occurred while fetching the data.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isLoading" type="boolean">
|
||||
A boolean indicating if the data is currently being fetched.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isValidating" type="boolean">
|
||||
A boolean indicating if the data is currently being revalidated.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isError" type="boolean">
|
||||
A boolean indicating if an error occurred while fetching the data.
|
||||
</ResponseField>
|
||||
|
||||
## Realtime hooks
|
||||
|
||||
See our [Realtime hooks documentation](/frontend/react-hooks/realtime) for more information.
|
||||
|
||||
## Trigger Hooks
|
||||
|
||||
See our [Trigger hooks documentation](/frontend/react-hooks/triggering) for more information.
|
||||
@@ -0,0 +1,391 @@
|
||||
---
|
||||
title: Realtime hooks
|
||||
sidebarTitle: Realtime
|
||||
description: Get live updates from the Trigger.dev API in your frontend application.
|
||||
---
|
||||
|
||||
These hooks allow you to subscribe to runs, batches, and streams using [Trigger.dev realtime](/realtime). Before reading this guide:
|
||||
|
||||
- Read our [Realtime documentation](/realtime) to understand how the Trigger.dev realtime API works.
|
||||
- Read how to [setup and authenticate](/frontend/overview) using the `@trigger.dev/react-hooks` package.
|
||||
|
||||
## Hooks
|
||||
|
||||
### useRealtimeRun
|
||||
|
||||
The `useRealtimeRun` hook allows you to subscribe to a run by its ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
To correctly type the run's payload and output, you can provide the type of your task to the `useRealtimeRun` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun<typeof myTask>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now run.payload and run.output are correctly typed
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
See our [Realtime documentation](/realtime) for more information about the type of the run object and more.
|
||||
|
||||
### useRealtimeRunsWithTag
|
||||
|
||||
The `useRealtimeRunsWithTag` hook allows you to subscribe to multiple runs with a specific tag.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
To correctly type the runs payload and output, you can provide the type of your task to the `useRealtimeRunsWithTag` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag<typeof myTask>(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now runs[i].payload and runs[i].output are correctly typed
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If `useRealtimeRunsWithTag` could return multiple different types of tasks, you can pass a union of all the task types to the hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
|
||||
import type { myTask1, myTask2 } from "@/trigger/myTasks";
|
||||
|
||||
export function MyComponent({ tag }: { tag: string }) {
|
||||
const { runs, error } = useRealtimeRunsWithTag<typeof myTask1 | typeof myTask2>(tag);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// You can narrow down the type of the run based on the taskIdentifier
|
||||
for (const run of runs) {
|
||||
if (run.taskIdentifier === "my-task-1") {
|
||||
// run is correctly typed as myTask1
|
||||
} else if (run.taskIdentifier === "my-task-2") {
|
||||
// run is correctly typed as myTask2
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useRealtimeBatch
|
||||
|
||||
The `useRealtimeBatch` hook allows you to subscribe to a batch of runs by its the batch ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeBatch } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ batchId }: { batchId: string }) {
|
||||
const { runs, error } = useRealtimeBatch(batchId);
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See our [Realtime documentation](/realtime) for more information.
|
||||
|
||||
### useRealtimeRunWithStreams
|
||||
|
||||
The `useRealtimeRunWithStreams` hook allows you to subscribe to a run by its ID and also receive any streams that are emitted by the task. See our [Realtime documentation](/realtime#streams) for more information about emitting streams from a task.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>
|
||||
{Object.keys(streams).map((stream) => (
|
||||
<div key={stream}>Stream: {stream}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can provide the type of the streams to the `useRealtimeRunWithStreams` hook:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above, each stream is an array of the type you provided, keyed by the stream name. If instead of a pure text stream you have a stream of objects, you can provide the type of the object:
|
||||
|
||||
```tsx
|
||||
import type { TextStreamPart } from "ai";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = { openai: TextStreamPart<{}> };
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai
|
||||
?.filter((stream) => stream.type === "text-delta")
|
||||
?.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Common options
|
||||
|
||||
### accessToken & baseURL
|
||||
|
||||
You can pass the `accessToken` option to the Realtime hooks to authenticate the subscription.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
baseURL: "https://my-self-hosted-trigger.com", // Optional if you are using a self-hosted Trigger.dev instance
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### enabled
|
||||
|
||||
You can pass the `enabled` option to the Realtime hooks to enable or disable the subscription.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
enabled,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
enabled,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to conditionally disable using the hook based on some state.
|
||||
|
||||
### id
|
||||
|
||||
You can pass the `id` option to the Realtime hooks to change the ID of the subscription.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
id,
|
||||
runId,
|
||||
publicAccessToken,
|
||||
enabled,
|
||||
}: {
|
||||
id: string;
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const { run, error } = useRealtimeRun(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
enabled,
|
||||
id,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to change the ID of the subscription based on some state. Passing in a different ID will unsubscribe from the current subscription and subscribe to the new one (and remove any cached data).
|
||||
|
||||
### experimental_throttleInMs
|
||||
|
||||
The `*withStreams` variants of the Realtime hooks accept an `experimental_throttleInMs` option to throttle the updates from the server. This can be useful if you are getting too many updates and want to reduce the number of updates.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunsWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { runs, error } = useRealtimeRunsWithStreams(tag, {
|
||||
accessToken: publicAccessToken,
|
||||
experimental_throttleInMs: 1000, // Throttle updates to once per second
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id}>Run: {run.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
title: Trigger hooks
|
||||
sidebarTitle: Triggering
|
||||
description: Triggering tasks from your frontend application.
|
||||
---
|
||||
|
||||
We provide a set of hooks that can be used to trigger tasks from your frontend application.
|
||||
|
||||
## Demo
|
||||
|
||||
We've created a [Demo application](https://github.com/triggerdotdev/realtime-llm-battle) that demonstrates how to use our React hooks to trigger tasks in a Next.js application. The application uses the `@trigger.dev/react-hooks` package to trigger a task and subscribe to the run in real-time.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the `@trigger.dev/react-hooks` package in your project:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn install @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
To authenticate a trigger hook, you must provide a special one-time use "trigger" token. These tokens are very similar to [Public Access Tokens](/frontend/overview#authentication), but they can only be used once to trigger a task. You can generate a trigger token using the `auth.createTriggerPublicToken` function in your backend code:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task");
|
||||
```
|
||||
|
||||
These tokens also expire, with the default expiration time being 15 minutes. You can specify a custom expiration time by passing a `expirationTime` parameter:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task", {
|
||||
expirationTime: "24hr",
|
||||
});
|
||||
```
|
||||
|
||||
You can also pass multiple tasks to the `createTriggerPublicToken` function to create a token that can trigger multiple tasks:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken(["my-task-1", "my-task-2"]);
|
||||
```
|
||||
|
||||
You can also pass the `multipleUse` parameter to create a token that can be used multiple times:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task", {
|
||||
multipleUse: true, // ❌ Use this with caution!
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
After generating the trigger token in your backend, you must pass it to your frontend application.
|
||||
We have a guide on how to do this in the [React hooks
|
||||
overview](/frontend/react-hooks/overview#passing-the-token-to-the-frontend).
|
||||
</Note>
|
||||
|
||||
## Hooks
|
||||
|
||||
### useTaskTrigger
|
||||
|
||||
The `useTaskTrigger` hook allows you to trigger a task from your frontend application.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
// 👆 This is the type of your task
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
// pass the type of your task here 👇
|
||||
const { submit, handle, error, isLoading } = useTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken, // 👈 this is the "trigger" token
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
return <div>Run ID: {handle.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useTaskTrigger` returns an object with the following properties:
|
||||
|
||||
- `submit`: A function that triggers the task. It takes the payload of the task as an argument.
|
||||
- `handle`: The run handle object. This object contains the ID of the run that was triggered, along with a Public Access Token that can be used to access the run.
|
||||
- `isLoading`: A boolean that indicates whether the task is currently being triggered.
|
||||
- `error`: An error object that contains any errors that occurred while triggering the task.
|
||||
|
||||
The `submit` function triggers the task with the specified payload. You can additionally pass an optional [options](/triggering#options) argument to the `submit` function:
|
||||
|
||||
```tsx
|
||||
submit({ foo: "bar" }, { tags: ["tag1", "tag2"] });
|
||||
```
|
||||
|
||||
#### Using the handle object
|
||||
|
||||
You can use the `handle` object to initiate a subsequent [realtime hook](/frontend/react-hooks/realtime#userealtimerun) to subscribe to the run.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useTaskTrigger, useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
// 👆 This is the type of your task
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
// pass the type of your task here 👇
|
||||
const { submit, handle, error, isLoading } = useTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken, // 👈 this is the "trigger" token
|
||||
});
|
||||
|
||||
// use the handle object to preserve type-safety 👇
|
||||
const { run, error: realtimeError } = useRealtimeRun(handle, {
|
||||
accessToken: handle?.publicAccessToken,
|
||||
enabled: !!handle, // Only subscribe to the run if the handle is available
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
return <div>Run ID: {handle.id}</div>;
|
||||
}
|
||||
|
||||
if (realtimeError) {
|
||||
return <div>Error: {realtimeError.message}</div>;
|
||||
}
|
||||
|
||||
if (run) {
|
||||
return <div>Run ID: {run.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We've also created some additional hooks that allow you to trigger tasks and subscribe to the run in one step:
|
||||
|
||||
### useRealtimeTaskTrigger
|
||||
|
||||
The `useRealtimeTaskTrigger` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime:
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, error, isLoading } = useRealtimeTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
// This is the realtime run object, which will automatically update when the run changes
|
||||
if (run) {
|
||||
return <div>Run ID: {run.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useRealtimeTaskTriggerWithStreams
|
||||
|
||||
The `useRealtimeTaskTriggerWithStreams` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime, and also receive any streams that are emitted by the task.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTriggerWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, streams, error, isLoading } = useRealtimeTaskTriggerWithStreams<
|
||||
typeof myTask,
|
||||
STREAMS
|
||||
>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (streams && run) {
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run ID: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -8,7 +8,16 @@ import LocalDevelopment from "/snippets/local-development-extensions.mdx";
|
||||
import ScrapingWarning from "/snippets/web-scraping-warning.mdx";
|
||||
|
||||
<div className="w-full h-full aspect-video">
|
||||
<iframe width="100%" height="100%" src="https://www.youtube.com/embed/6azvzrZITKY?si=muKtsBiS9TJGGKWg" title="YouTube video player" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src="https://www.youtube.com/embed/6azvzrZITKY?si=muKtsBiS9TJGGKWg"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Overview
|
||||
@@ -125,12 +134,9 @@ export const summarizeHackerNews = schedules.task({
|
||||
.batchTriggerAndWait(
|
||||
articles.map((article) => ({
|
||||
payload: { title: article.title!, link: article.link! },
|
||||
idempotencyKey: article.link,
|
||||
}))
|
||||
)
|
||||
.then((batch) =>
|
||||
batch.runs.filter((run) => run.ok).map((run) => run.output)
|
||||
);
|
||||
.then((batch) => batch.runs.filter((run) => run.ok).map((run) => run.output));
|
||||
|
||||
// Send email using Resend
|
||||
await resend.emails.send({
|
||||
@@ -165,11 +171,7 @@ export const scrapeAndSummarizeArticle = task({
|
||||
// Prevent all assets from loading, images, stylesheets etc
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
["script", "stylesheet", "image", "media", "font"].includes(
|
||||
request.resourceType()
|
||||
)
|
||||
) {
|
||||
if (["script", "stylesheet", "image", "media", "font"].includes(request.resourceType())) {
|
||||
request.abort();
|
||||
} else {
|
||||
request.continue();
|
||||
@@ -218,16 +220,7 @@ To prevent the main example from becoming too cluttered, we'll create a separate
|
||||
Notice how this file is imported into the main task code and passed to Resend to send the email.
|
||||
|
||||
```tsx summarize-hn-email.tsx
|
||||
import {
|
||||
Html,
|
||||
Head,
|
||||
Body,
|
||||
Container,
|
||||
Section,
|
||||
Heading,
|
||||
Text,
|
||||
Link,
|
||||
} from "@react-email/components";
|
||||
import { Html, Head, Body, Container, Section, Heading, Text, Link } from "@react-email/components";
|
||||
|
||||
interface Article {
|
||||
title: string;
|
||||
@@ -235,9 +228,7 @@ interface Article {
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
export const HNSummaryEmail: React.FC<{ articles: Article[] }> = ({
|
||||
articles,
|
||||
}) => (
|
||||
export const HNSummaryEmail: React.FC<{ articles: Article[] }> = ({ articles }) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={{ fontFamily: "Arial, sans-serif", padding: "20px" }}>
|
||||
|
||||
+53
-13
@@ -5,11 +5,17 @@ description: "An API call or operation is “idempotent” if it has the same re
|
||||
|
||||
We currently support idempotency at the task level, meaning that if you trigger a task with the same `idempotencyKey` twice, the second request will not create a new task run.
|
||||
|
||||
<Warning>
|
||||
In version 3.3.0 and later, the `idempotencyKey` option is not available when using
|
||||
`triggerAndWait` or `batchTriggerAndWait`, due to a bug that would sometimes cause the parent task
|
||||
to become stuck. We are working on a fix for this issue.
|
||||
</Warning>
|
||||
|
||||
## `idempotencyKey` option
|
||||
|
||||
You can provide an `idempotencyKey` to ensure that a task is only triggered once with the same key. This is useful if you are triggering a task within another task that might be retried:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { idempotencyKeys, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
@@ -18,13 +24,14 @@ export const myTask = task({
|
||||
maxAttempts: 4,
|
||||
},
|
||||
run: async (payload: any) => {
|
||||
// By default, idempotency keys generated are unique to the run, to prevent retries from duplicating child tasks
|
||||
// This idempotency key will be unique to this task run, meaning the childTask will only be triggered once across all retries
|
||||
const idempotencyKey = await idempotencyKeys.create("my-task-key");
|
||||
|
||||
// childTask will only be triggered once with the same idempotency key
|
||||
await childTask.triggerAndWait(payload, { idempotencyKey });
|
||||
await childTask.trigger({ foo: "bar" }, { idempotencyKey });
|
||||
|
||||
// Do something else, that may throw an error and cause the task to be retried
|
||||
throw new Error("Something went wrong");
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -33,7 +40,7 @@ You can use the `idempotencyKeys.create` SDK function to create an idempotency k
|
||||
|
||||
We automatically inject the run ID when generating the idempotency key when running inside a task by default. You can turn it off by passing the `scope` option to `idempotencyKeys.create`:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { idempotencyKeys, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
@@ -42,21 +49,18 @@ export const myTask = task({
|
||||
maxAttempts: 4,
|
||||
},
|
||||
run: async (payload: any) => {
|
||||
// This idempotency key will be the same for all runs of this task
|
||||
// This idempotency key will be globally unique, meaning only a single task run will be triggered with this key
|
||||
const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" });
|
||||
|
||||
// childTask will only be triggered once with the same idempotency key
|
||||
await childTask.triggerAndWait(payload, { idempotencyKey });
|
||||
|
||||
// This is the same as the above
|
||||
await childTask.triggerAndWait(payload, { idempotencyKey: "my-task-key" });
|
||||
await childTask.trigger({ foo: "bar" }, { idempotencyKey });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you are triggering a task from your backend code, you can use the `idempotencyKeys.create` SDK function to create an idempotency key.
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { idempotencyKeys, tasks } from "@trigger.dev/sdk/v3";
|
||||
|
||||
// You can also pass an array of strings to create a idempotency key
|
||||
@@ -66,7 +70,7 @@ await tasks.trigger("my-task", { some: "data" }, { idempotencyKey });
|
||||
|
||||
You can also pass a string to the `idempotencyKey` option, without first creating it with `idempotencyKeys.create`.
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { myTask } from "./trigger/myTasks";
|
||||
|
||||
// You can also pass an array of strings to create a idempotency key
|
||||
@@ -77,7 +81,7 @@ await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id });
|
||||
|
||||
You can pass the `idempotencyKey` when calling `batchTrigger` as well:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
|
||||
await tasks.batchTrigger("my-task", [
|
||||
@@ -88,11 +92,47 @@ await tasks.batchTrigger("my-task", [
|
||||
]);
|
||||
```
|
||||
|
||||
## `idempotencyKeyTTL` option
|
||||
|
||||
By default idempotency keys are stored for 30 days. You can change this by passing the `idempotencyKeyTTL` option when triggering a task:
|
||||
|
||||
```ts
|
||||
import { idempotencyKeys, task, wait } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
retry: {
|
||||
maxAttempts: 4,
|
||||
},
|
||||
run: async (payload: any) => {
|
||||
const idempotencyKey = await idempotencyKeys.create("my-task-key");
|
||||
|
||||
// The idempotency key will expire after 60 seconds
|
||||
await childTask.trigger({ foo: "bar" }, { idempotencyKey, idempotencyKeyTTL: "60s" });
|
||||
|
||||
await wait.for({ seconds: 61 });
|
||||
|
||||
// The idempotency key will have expired, so the childTask will be triggered again
|
||||
await childTask.trigger({ foo: "bar" }, { idempotencyKey });
|
||||
|
||||
// Do something else, that may throw an error and cause the task to be retried
|
||||
throw new Error("Something went wrong");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can use the following units for the `idempotencyKeyTTL` option:
|
||||
|
||||
- `s` for seconds (e.g. `60s`)
|
||||
- `m` for minutes (e.g. `5m`)
|
||||
- `h` for hours (e.g. `2h`)
|
||||
- `d` for days (e.g. `3d`)
|
||||
|
||||
## Payload-based idempotency
|
||||
|
||||
We don't currently support payload-based idempotency, but you can implement it yourself by hashing the payload and using the hash as the idempotency key.
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { idempotencyKeys, task } from "@trigger.dev/sdk/v3";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
|
||||
+48
-39
@@ -3,15 +3,15 @@ title: "Limits"
|
||||
description: "There are some hard and soft limits that you might hit."
|
||||
---
|
||||
|
||||
import RateLimitHitUseBatchTrigger from '/snippets/rate-limit-hit-use-batchtrigger.mdx';
|
||||
import RateLimitHitUseBatchTrigger from "/snippets/rate-limit-hit-use-batchtrigger.mdx";
|
||||
|
||||
## Concurrency limits
|
||||
|
||||
| Pricing tier | Limit |
|
||||
|:---------------- |:-------------------- |
|
||||
| Free | 5 concurrent runs |
|
||||
| Hobby | 25 concurrent runs |
|
||||
| Pro | 100+ concurrent runs |
|
||||
| Pricing tier | Limit |
|
||||
| :----------- | :------------------- |
|
||||
| Free | 5 concurrent runs |
|
||||
| Hobby | 25 concurrent runs |
|
||||
| Pro | 100+ concurrent runs |
|
||||
|
||||
If you need more than 100 concurrent runs on the Pro tier, you can request more by contacting us via [email](https://trigger.dev/contact) or [Discord](https://trigger.dev/discord).
|
||||
|
||||
@@ -20,28 +20,28 @@ If you need more than 100 concurrent runs on the Pro tier, you can request more
|
||||
Generally speaking each SDK call is an API call.
|
||||
|
||||
| Limit | Details |
|
||||
|:----- |:------------------------- |
|
||||
| :---- | :------------------------ |
|
||||
| API | 1,500 requests per minute |
|
||||
|
||||
<RateLimitHitUseBatchTrigger/>
|
||||
<RateLimitHitUseBatchTrigger />
|
||||
|
||||
## Queued tasks
|
||||
|
||||
The number of queued tasks by environment.
|
||||
|
||||
| Limit | Details |
|
||||
|:------- |:------------------ |
|
||||
| :------ | :----------------- |
|
||||
| Dev | At most 500 |
|
||||
| Staging | At most 10 million |
|
||||
| Prod | At most 10 million |
|
||||
|
||||
## Schedules
|
||||
|
||||
| Pricing tier | Limit |
|
||||
|:---------------- |:-------------------- |
|
||||
| Free | 5 per project |
|
||||
| Hobby | 100 per project |
|
||||
| Pro | 1,000+ per project |
|
||||
| Pricing tier | Limit |
|
||||
| :----------- | :----------------- |
|
||||
| Free | 5 per project |
|
||||
| Hobby | 100 per project |
|
||||
| Pro | 1,000+ per project |
|
||||
|
||||
When attaching schedules to tasks we strongly recommend you add them [in our dashboard](/tasks/scheduled#attaching-schedules-in-the-dashboard) if they're "static". That way you can control them easily per environment.
|
||||
|
||||
@@ -49,15 +49,29 @@ If you add them [dynamically using code](/management/schedules/create) make sure
|
||||
|
||||
If you're creating schedules for your user you will definitely need to request more schedules from us.
|
||||
|
||||
## Task payloads and outputs
|
||||
|
||||
| Limit | Details |
|
||||
| :--------------------- | :-------------------------------------------- |
|
||||
| Single trigger payload | Must not exceed 3MB |
|
||||
| Batch trigger payload | The total of all payloads must not exceed 1MB |
|
||||
| Task outputs | Must not exceed 10MB |
|
||||
|
||||
Payloads and outputs that exceed 512KB will be offloaded to object storage and a presigned URL will be provided to download the data when calling `runs.retrieve`. You don't need to do anything to handle this in your tasks however, as we will transparently upload/download these during operation.
|
||||
|
||||
## Batch size
|
||||
|
||||
A single batch can have a maximum of 500 items.
|
||||
|
||||
<SoftLimit />
|
||||
|
||||
## Log retention
|
||||
|
||||
| Pricing tier | Limit |
|
||||
|:---------------- |:--------- |
|
||||
| Free | 1 day |
|
||||
| Hobby | 7 days |
|
||||
| Pro | 30 days |
|
||||
| Pricing tier | Limit |
|
||||
| :----------- | :------ |
|
||||
| Free | 1 day |
|
||||
| Hobby | 7 days |
|
||||
| Pro | 30 days |
|
||||
|
||||
## Log size
|
||||
|
||||
@@ -66,25 +80,30 @@ We limit the size of logs to prevent oversized data potentially causing issues.
|
||||
<Expandable title="log limits">
|
||||
|
||||
#### Attribute Limits
|
||||
|
||||
- Span Attribute Count Limit: 256
|
||||
- Log Attribute Count Limit: 256
|
||||
- Span Attribute Value Length Limit: 1028 characters
|
||||
- Log Attribute Value Length Limit: 1028 characters
|
||||
|
||||
#### Event and Link Limits
|
||||
|
||||
- Span Event Count Limit: 10
|
||||
- Link Count Limit: 2
|
||||
- Attributes per Link Limit: 10
|
||||
- Attributes per Event Limit: 10
|
||||
|
||||
#### I/O Packet Length Limit
|
||||
|
||||
128 KB (131,072 bytes)
|
||||
|
||||
#### Attribute Clipping Behavior
|
||||
|
||||
- Attributes exceeding the value length limit (1028 characters) are discarded.
|
||||
- If the total number of attributes exceeds 256, additional attributes are not included.
|
||||
|
||||
#### Attribute Value Size Calculation
|
||||
|
||||
- Strings: Actual length of the string
|
||||
- Numbers: 8 bytes
|
||||
- Booleans: 4 bytes
|
||||
@@ -93,25 +112,15 @@ We limit the size of logs to prevent oversized data potentially causing issues.
|
||||
|
||||
</Expandable>
|
||||
|
||||
## Task payloads and outputs
|
||||
|
||||
| Limit | Details |
|
||||
|:--- |:--- |
|
||||
| Single trigger payload | Must not exceed 10MB |
|
||||
| Batch trigger payload | The total of all payloads must not exceed 10MB |
|
||||
| Task outputs | Must not exceed 10MB |
|
||||
|
||||
Payloads and outputs that exceed 512KB will be offloaded to object storage and a presigned URL will be provided to download the data when calling `runs.retrieve`. You don't need to do anything to handle this in your tasks however, as we will transparently upload/download these during operation.
|
||||
|
||||
## Alerts
|
||||
|
||||
An alert destination is a single email address, Slack channel, or webhook URL that you want to send alerts to. If you're on the Pro and need more than 100 alert destinations, you can request more by contacting us via [email](https://trigger.dev/contact) or [Discord](https://trigger.dev/discord).
|
||||
|
||||
| Pricing tier | Limit |
|
||||
|:---------------- |:----------------------- |
|
||||
| Free | 1 alert destination |
|
||||
| Hobby | 3 alert destinations |
|
||||
| Pro | 100+ alert destinations |
|
||||
| Pricing tier | Limit |
|
||||
| :----------- | :---------------------- |
|
||||
| Free | 1 alert destination |
|
||||
| Hobby | 3 alert destinations |
|
||||
| Pro | 100+ alert destinations |
|
||||
|
||||
## Machines
|
||||
|
||||
@@ -121,8 +130,8 @@ See the [machine configurations](/machines#machine-configurations) for more deta
|
||||
|
||||
## Team members
|
||||
|
||||
| Pricing tier | Limit |
|
||||
|:---------------- |:----------------- |
|
||||
| Free | 5 team members |
|
||||
| Hobby | 5 team members |
|
||||
| Pro | 25+ team members |
|
||||
| Pricing tier | Limit |
|
||||
| :----------- | :--------------- |
|
||||
| Free | 5 team members |
|
||||
| Hobby | 5 team members |
|
||||
| Pro | 25+ team members |
|
||||
|
||||
+14
-2
@@ -110,6 +110,10 @@
|
||||
{
|
||||
"source": "/runs-and-attempts",
|
||||
"destination": "/runs"
|
||||
},
|
||||
{
|
||||
"source": "/frontend/react-hooks",
|
||||
"destination": "/frontend/react-hooks/overview"
|
||||
}
|
||||
],
|
||||
"anchors": [
|
||||
@@ -207,7 +211,14 @@
|
||||
"group": "Frontend usage",
|
||||
"pages": [
|
||||
"frontend/overview",
|
||||
"frontend/react-hooks"
|
||||
{
|
||||
"group": "React hooks",
|
||||
"pages": [
|
||||
"frontend/react-hooks/overview",
|
||||
"frontend/react-hooks/realtime",
|
||||
"frontend/react-hooks/triggering"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -217,7 +228,8 @@
|
||||
"realtime/streams",
|
||||
"realtime/react-hooks",
|
||||
"realtime/subscribe-to-run",
|
||||
"realtime/subscribe-to-runs-with-tag"
|
||||
"realtime/subscribe-to-runs-with-tag",
|
||||
"realtime/subscribe-to-batch"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: runs.subscribeToBatch
|
||||
sidebarTitle: subscribeToBatch
|
||||
description: Subscribes to all changes for runs in a batch.
|
||||
---
|
||||
|
||||
import RunObject from "/snippets/realtime/run-object.mdx";
|
||||
|
||||
<RequestExample>
|
||||
|
||||
```ts Example
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
for await (const run of runs.subscribeToBatch("batch_1234")) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
</RequestExample>
|
||||
|
||||
This function subscribes to all changes for runs in a batch. It returns an async iterator that yields the a run object whenever a run in the batch is updated. The iterator does not complete on it's own, you must manually `break` the loop when you want to stop listening for updates.
|
||||
|
||||
### Authentication
|
||||
|
||||
This function supports both server-side and client-side authentication. For server-side authentication, use your API key. For client-side authentication, you must generate a public access token with one of the following scopes:
|
||||
|
||||
- `read:batch:<batchId>`
|
||||
- `read:runs` will provide access to all runs (not recommended for production use)
|
||||
|
||||
To generate a public access token, use the `auth.createPublicToken` function:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
batch: ["batch_1234"],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
The AsyncIterator yields an object with the following properties:
|
||||
|
||||
<RunObject />
|
||||
+128
-20
@@ -98,19 +98,21 @@ At this point, the run will have either an output (if successful) or an error (i
|
||||
|
||||
When triggering a task, you can provide an idempotency key to ensure the task is executed only once, even if triggered multiple times. This is useful for preventing duplicate executions in distributed systems.
|
||||
|
||||
```javascript
|
||||
yourTask.trigger({ foo: "bar" }, { idempotencyKey: "unique-key" });
|
||||
```ts
|
||||
await yourTask.trigger({ foo: "bar" }, { idempotencyKey: "unique-key" });
|
||||
```
|
||||
|
||||
- If a run with the same idempotency key is already in progress, the new trigger will be ignored.
|
||||
- If the run has already finished, the previous output or error will be returned.
|
||||
|
||||
See our [Idempotency docs](/idempotency) for more information.
|
||||
|
||||
### Canceling runs
|
||||
|
||||
You can cancel an in-progress run using the API or the dashboard:
|
||||
|
||||
```ts
|
||||
runs.cancel(runId);
|
||||
await runs.cancel(runId);
|
||||
```
|
||||
|
||||
When a run is canceled:
|
||||
@@ -128,7 +130,7 @@ When a run is canceled:
|
||||
You can set a TTL when triggering a run:
|
||||
|
||||
```ts
|
||||
yourTask.trigger({ foo: "bar" }, { ttl: "10m" });
|
||||
await yourTask.trigger({ foo: "bar" }, { ttl: "10m" });
|
||||
```
|
||||
|
||||
If the run hasn't started within the specified TTL, it will automatically expire. This is useful for time-sensitive tasks. Note that dev runs automatically have a 10-minute TTL.
|
||||
@@ -140,7 +142,7 @@ If the run hasn't started within the specified TTL, it will automatically expire
|
||||
You can schedule a run to start after a specified delay:
|
||||
|
||||
```ts
|
||||
yourTask.trigger({ foo: "bar" }, { delay: "1h" });
|
||||
await yourTask.trigger({ foo: "bar" }, { delay: "1h" });
|
||||
```
|
||||
|
||||
This is useful for tasks that need to be executed at a specific time in the future.
|
||||
@@ -152,7 +154,7 @@ This is useful for tasks that need to be executed at a specific time in the futu
|
||||
You can create a new run with the same payload as a previous run:
|
||||
|
||||
```ts
|
||||
runs.replay(runId);
|
||||
await runs.replay(runId);
|
||||
```
|
||||
|
||||
This is useful for re-running a task with the same input, especially for debugging or recovering from failures. The new run will use the latest version of the task.
|
||||
@@ -175,37 +177,143 @@ Similar to `triggerAndWait()`, the `batchTriggerAndWait()` function lets you bat
|
||||
|
||||
### Runs API
|
||||
|
||||
The runs API provides methods to interact with and manage runs:
|
||||
#### runs.list()
|
||||
|
||||
List runs in a specific environment. You can filter the runs by status, created at, task identifier, version, and more:
|
||||
|
||||
```ts
|
||||
// List all runs
|
||||
runs.list();
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
// Get a specific run by ID
|
||||
runs.retrieve(runId);
|
||||
// Get the first page of runs, returning up to 20 runs
|
||||
let page = await runs.list({ limit: 20 });
|
||||
|
||||
// Replay a run
|
||||
runs.replay(runId);
|
||||
for (const run of page.data) {
|
||||
console.log(run);
|
||||
}
|
||||
|
||||
// Reschedule a run
|
||||
runs.reschedule(runId, delay);
|
||||
|
||||
// Cancel a run
|
||||
runs.cancel(runId);
|
||||
// Keep getting the next page until there are no more runs
|
||||
while (page.hasNextPage()) {
|
||||
page = await page.getNextPage();
|
||||
// Do something with the next page of runs
|
||||
}
|
||||
```
|
||||
|
||||
These methods allow you to access detailed information about runs and their attempts, including payloads, outputs, parent runs, and child runs.
|
||||
You can also use an Async Iterator to get all runs:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
for await (const run of runs.list({ limit: 20 })) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
You can provide multiple filters to the `list()` function to narrow down the results:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
const response = await runs.list({
|
||||
status: ["QUEUED", "EXECUTING"], // Filter by status
|
||||
taskIdentifier: ["my-task", "my-other-task"], // Filter by task identifier
|
||||
from: new Date("2024-04-01T00:00:00Z"), // Filter by created at
|
||||
to: new Date(),
|
||||
version: "20241127.2", // Filter by deployment version,
|
||||
tag: ["tag1", "tag2"], // Filter by tags
|
||||
batch: "batch_1234", // Filter by batch ID
|
||||
schedule: "sched_1234", // Filter by schedule ID
|
||||
});
|
||||
```
|
||||
|
||||
#### runs.retrieve()
|
||||
|
||||
Fetch a single run by it's ID:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
const run = await runs.retrieve(runId);
|
||||
```
|
||||
|
||||
You can provide the type of the task to correctly type the `run.payload` and `run.output`:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
import type { myTask } from "./trigger/myTask";
|
||||
|
||||
const run = await runs.retrieve<typeof myTask>(runId);
|
||||
|
||||
console.log(run.payload.foo); // string
|
||||
console.log(run.output.bar); // string
|
||||
```
|
||||
|
||||
If you have just triggered a run, you can pass the entire response object to `retrieve()` and the response will already be typed:
|
||||
|
||||
```ts
|
||||
import { runs, tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { myTask } from "./trigger/myTask";
|
||||
|
||||
const response = await tasks.trigger<typeof myTask>({ foo: "bar" });
|
||||
const run = await runs.retrieve(response);
|
||||
|
||||
console.log(run.payload.foo); // string
|
||||
console.log(run.output.bar); // string
|
||||
```
|
||||
|
||||
#### runs.cancel()
|
||||
|
||||
Cancel a run:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
await runs.cancel(runId);
|
||||
```
|
||||
|
||||
#### runs.replay()
|
||||
|
||||
Replay a run:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
await runs.replay(runId);
|
||||
```
|
||||
|
||||
#### runs.reschedule()
|
||||
|
||||
Updates a delayed run with a new delay. Only valid when the run is in the DELAYED state.
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
await runs.reschedule(runId, { delay: "1h" });
|
||||
```
|
||||
|
||||
### Real-time updates
|
||||
|
||||
You can subscribe to run updates in real-time using the `subscribeToRun()` function:
|
||||
Subscribe to changes to a specific run in real-time:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
for await (const run of runs.subscribeToRun(runId)) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
Similar to `runs.retrieve()`, you can provide the type of the task to correctly type the `run.payload` and `run.output`:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk/v3";
|
||||
import type { myTask } from "./trigger/myTask";
|
||||
|
||||
for await (const run of runs.subscribeToRun<typeof myTask>(runId)) {
|
||||
console.log(run.payload.foo); // string
|
||||
console.log(run.output?.bar); // string | undefined
|
||||
}
|
||||
```
|
||||
|
||||
For more on real-time updates, see the [Realtime](/realtime) documentation.
|
||||
|
||||
### Triggering runs for undeployed tasks
|
||||
|
||||
+337
-108
@@ -3,44 +3,38 @@ title: "Triggering"
|
||||
description: "Tasks need to be triggered in order to run."
|
||||
---
|
||||
|
||||
## Trigger functions
|
||||
|
||||
Trigger tasks **from your backend**:
|
||||
|
||||
| Function | This works | What it does |
|
||||
| :----------------------- | :--------- | :-------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `tasks.trigger()` | Anywhere | Triggers a task and gets a handle you can use to fetch and manage the run. [Read more](#tasks-trigger) |
|
||||
| `tasks.batchTrigger()` | Anywhere | Triggers a task multiple times and gets a handle you can use to fetch and manage the runs. [Read more](#tasks-batchtrigger) |
|
||||
| `tasks.triggerAndPoll()` | Anywhere | Triggers a task and then polls the run until it’s complete. [Read more](#tasks-triggerandpoll) |
|
||||
| Function | What it does | |
|
||||
| :----------------------- | :----------------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `tasks.trigger()` | Triggers a task and returns a handle you can use to fetch and manage the run. | [Docs](#tasks-trigger) |
|
||||
| `tasks.batchTrigger()` | Triggers a single task in a batch and returns a handle you can use to fetch and manage the runs. | [Docs](#tasks-batchtrigger) |
|
||||
| `tasks.triggerAndPoll()` | Triggers a task and then polls the run until it’s complete. | [Docs](#tasks-triggerandpoll) |
|
||||
| `batch.trigger()` | Similar to `tasks.batchTrigger` but allows running multiple different tasks | [Docs](#batch-trigger) |
|
||||
|
||||
Trigger tasks **from inside a run**:
|
||||
Trigger tasks **from inside a another task**:
|
||||
|
||||
| Function | This works | What it does |
|
||||
| :------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `yourTask.trigger()` | Anywhere | Triggers a task and gets a handle you can use to monitor and manage the run. It does not wait for the result. [Read more](#yourtask-trigger) |
|
||||
| `yourTask.batchTrigger()` | Anywhere | Triggers a task multiple times and gets a handle you can use to monitor and manage the runs. It does not wait for the results. [Read more](#yourtask-batchtrigger) |
|
||||
| `yourTask.triggerAndWait()` | Inside task | Triggers a task and then waits until it's complete. You get the result data to continue with. [Read more](#yourtask-triggerandwait) |
|
||||
| `yourTask.batchTriggerAndWait()` | Inside task | Triggers a task multiple times in parallel and then waits until they're all complete. You get the resulting data to continue with. [Read more](#yourtask-batchtriggerandwait) |
|
||||
|
||||
Additionally, [scheduled tasks](/tasks/scheduled) get **automatically** triggered on their schedule and webhooks when receiving a webhook.
|
||||
|
||||
## Scheduled tasks
|
||||
|
||||
You should attach one or more schedules to your `schedules.task()` to trigger it on a recurring schedule. [Read the scheduled tasks docs](/tasks/scheduled).
|
||||
|
||||
## Authentication
|
||||
|
||||
When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard. [More info on API keys](/apikeys).
|
||||
| Function | What it does | |
|
||||
| :------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| `yourTask.trigger()` | Triggers a task and gets a handle you can use to monitor and manage the run. It does not wait for the result. | [Docs](#yourtask-trigger) |
|
||||
| `yourTask.batchTrigger()` | Triggers a task multiple times and gets a handle you can use to monitor and manage the runs. It does not wait for the results. | [Docs](#yourtask-batchtrigger) |
|
||||
| `yourTask.triggerAndWait()` | Triggers a task and then waits until it's complete. You get the result data to continue with. | [Docs](#yourtask-triggerandwait) |
|
||||
| `yourTask.batchTriggerAndWait()` | Triggers a task multiple times in parallel and then waits until they're all complete. You get the resulting data to continue with. | [Docs](#yourtask-batchtriggerandwait) |
|
||||
| `batch.triggerAndWait()` | Similar to `batch.trigger` but will wait on the triggered tasks to finish and return the results. | [Docs](#batch-triggerandwait) |
|
||||
| `batch.triggerByTask()` | Similar to `batch.trigger` but allows passing in task instances instead of task IDs. | [Docs](#batch-triggerbytask) |
|
||||
| `batch.triggerByTaskAndWait()` | Similar to `batch.triggerbyTask` but will wait on the triggered tasks to finish and return the results. | [Docs](#batch-triggerbytaskandwait) |
|
||||
|
||||
## Triggering from your backend
|
||||
|
||||
You can trigger any task from your backend code using the `tasks.trigger()` or `tasks.batchTrigger()` SDK functions.
|
||||
When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard. [More info on API keys](/apikeys).
|
||||
|
||||
<Note>
|
||||
Do not trigger tasks directly from your frontend. If you do, you will leak your private
|
||||
Trigger.dev API key.
|
||||
If you are using Next.js Server Actions [you'll need to be careful with
|
||||
bundling](/guides/frameworks/nextjs#triggering-your-task-in-next-js).
|
||||
</Note>
|
||||
|
||||
You can use Next.js Server Actions but [you need to be careful with bundling](/guides/frameworks/nextjs#triggering-your-task-in-next-js).
|
||||
|
||||
### tasks.trigger()
|
||||
|
||||
Triggers a single run of a task with the payload you pass in, and any options you specify, without needing to import the task.
|
||||
@@ -51,9 +45,7 @@ Triggers a single run of a task with the payload you pass in, and any options yo
|
||||
application.
|
||||
</Note>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts Next.js API route
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
// 👆 **type-only** import
|
||||
@@ -74,45 +66,37 @@ export async function POST(request: Request) {
|
||||
}
|
||||
```
|
||||
|
||||
```ts Remix
|
||||
You can pass in options to the task using the second argument:
|
||||
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
// 👆 **type-only** import
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (request.method !== "POST") {
|
||||
throw new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
//app/email/route.ts
|
||||
export async function POST(request: Request) {
|
||||
//get the JSON from the request
|
||||
const data = await request.json();
|
||||
|
||||
// Pass the task type to `trigger()` as a generic argument, giving you full type checking
|
||||
const handle = await tasks.trigger<typeof emailSequence>("email-sequence", {
|
||||
to: data.email,
|
||||
name: data.name,
|
||||
});
|
||||
const handle = await tasks.trigger<typeof emailSequence>(
|
||||
"email-sequence",
|
||||
{
|
||||
to: data.email,
|
||||
name: data.name,
|
||||
},
|
||||
{ delay: "1h" } // 👈 Pass in the options here
|
||||
);
|
||||
|
||||
//return a success response with the handle
|
||||
return json(handle);
|
||||
return Response.json(handle);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### tasks.batchTrigger()
|
||||
|
||||
Triggers multiple runs of a task with the payloads you pass in, and any options you specify, without needing to import the task.
|
||||
Triggers multiple runs of a single task with the payloads you pass in, and any options you specify, without needing to import the task.
|
||||
|
||||
<Note>
|
||||
By using `tasks.batchTrigger()`, you can pass in the task type as a generic argument, giving you
|
||||
full type checking. Make sure you use a `type` import so that your task code is not imported into
|
||||
your application.
|
||||
</Note>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts Next.js API route
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
// 👆 **type-only** import
|
||||
@@ -133,44 +117,62 @@ export async function POST(request: Request) {
|
||||
}
|
||||
```
|
||||
|
||||
```ts Remix
|
||||
You can pass in options to the `batchTrigger` function using the second argument:
|
||||
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (request.method !== "POST") {
|
||||
throw new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
//app/email/route.ts
|
||||
export async function POST(request: Request) {
|
||||
//get the JSON from the request
|
||||
const data = await request.json();
|
||||
|
||||
// Pass the task type to `batchTrigger()` as a generic argument, giving you full type checking
|
||||
const batchHandle = await tasks.batchTrigger<typeof emailSequence>(
|
||||
"email-sequence",
|
||||
data.users.map((u) => ({ payload: { to: u.email, name: u.name } }))
|
||||
data.users.map((u) => ({ payload: { to: u.email, name: u.name } })),
|
||||
{ idempotencyKey: "my-idempotency-key" } // 👈 Pass in the options here
|
||||
);
|
||||
|
||||
//return a success response with the handle
|
||||
return json(batchHandle);
|
||||
return Response.json(batchHandle);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
You can also pass in options for each run in the batch:
|
||||
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
|
||||
//app/email/route.ts
|
||||
export async function POST(request: Request) {
|
||||
//get the JSON from the request
|
||||
const data = await request.json();
|
||||
|
||||
// Pass the task type to `batchTrigger()` as a generic argument, giving you full type checking
|
||||
const batchHandle = await tasks.batchTrigger<typeof emailSequence>(
|
||||
"email-sequence",
|
||||
data.users.map((u) => ({ payload: { to: u.email, name: u.name }, options: { delay: "1h" } })) // 👈 Pass in options to each item like so
|
||||
);
|
||||
|
||||
//return a success response with the handle
|
||||
return Response.json(batchHandle);
|
||||
}
|
||||
```
|
||||
|
||||
### tasks.triggerAndPoll()
|
||||
|
||||
Triggers a single run of a task with the payload you pass in, and any options you specify, and then polls the run until it's complete.
|
||||
|
||||
<Note>
|
||||
By using `tasks.triggerAndPoll()`, you can pass in the task type as a generic argument, giving you
|
||||
full type checking. Make sure you use a `type` import so that your task code is not imported into
|
||||
your application.
|
||||
</Note>
|
||||
<Warning>
|
||||
We don't recommend using `triggerAndPoll()`, especially inside a web request, as it will block the
|
||||
request until the run is complete. Please see our [Realtime docs](/realtime) for a better way to
|
||||
handle this.
|
||||
</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts Next.js API route
|
||||
```ts Your backend
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
|
||||
@@ -194,71 +196,83 @@ export async function POST(request: Request) {
|
||||
}
|
||||
```
|
||||
|
||||
```ts Remix
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
### batch.trigger()
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (request.method !== "POST") {
|
||||
throw new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
Triggers multiple runs of different tasks with the payloads you pass in, and any options you specify. This is useful when you need to trigger multiple tasks at once.
|
||||
|
||||
```ts Your backend
|
||||
import { batch } from "@trigger.dev/sdk/v3";
|
||||
import type { myTask1, myTask2 } from "~/trigger/myTasks";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
//get the JSON from the request
|
||||
const data = await request.json();
|
||||
|
||||
// Pass the task type to `triggerAndPoll()` as a generic argument, giving you full type checking
|
||||
const result = await tasks.triggerAndPoll<typeof emailSequence>(
|
||||
"email-sequence",
|
||||
{
|
||||
to: data.email,
|
||||
name: data.name,
|
||||
},
|
||||
{ pollIntervalMs: 5000 }
|
||||
);
|
||||
// Pass a union of the tasks to `trigger()` as a generic argument, giving you full type checking
|
||||
const result = await batch.trigger<typeof myTask1 | typeof myTask2>([
|
||||
// Because we're using a union, we can pass in multiple tasks by ID
|
||||
{ id: "my-task-1", payload: { some: data.some } },
|
||||
{ id: "my-task-2", payload: { other: data.other } },
|
||||
]);
|
||||
|
||||
//return a success response with the result
|
||||
return json(result);
|
||||
return Response.json(result);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
## Triggering from inside another task
|
||||
|
||||
<Note>
|
||||
The above code is just a demonstration of the API and is not recommended to use in an API route
|
||||
this way as it will block the request until the task is complete.
|
||||
</Note>
|
||||
The following functions should only be used when running inside a task, for one of the following reasons:
|
||||
|
||||
## Triggering from inside a run
|
||||
|
||||
Task instance methods are available on the `Task` object you receive when you define a task. We recommend you use these methods inside another task to trigger subtasks.
|
||||
- You need to **wait** for the result of the triggered task.
|
||||
- You need to import the task instance. Importing a task instance from your backend code is not recommended, as it can pull in a lot of unnecessary code and dependencies.
|
||||
|
||||
### yourTask.trigger()
|
||||
|
||||
Triggers a single run of a task with the payload you pass in, and any options you specify. It does NOT wait for the result.
|
||||
Triggers a single run of a task with the payload you pass in, and any options you specify.
|
||||
|
||||
If called from within a task, you can use the `AndWait` version to pause execution until the triggered run is complete.
|
||||
<Note>
|
||||
If you need to call `trigger()` on a task in a loop, use
|
||||
[`batchTrigger()`](#yourTask-batchtrigger) instead which will trigger up to 500 runs in a single
|
||||
call.
|
||||
</Note>
|
||||
|
||||
If you need to call `trigger()` on a task in a loop, use [`batchTrigger()`](/triggering#task-batchtrigger) instead which will trigger up to 100 tasks in a single call.
|
||||
|
||||
```ts /trigger/my-task.ts
|
||||
import { myOtherTask } from "~/trigger/my-other-task";
|
||||
```ts ./trigger/my-task.ts
|
||||
import { myOtherTask, runs } from "~/trigger/my-other-task";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const handle = await myOtherTask.trigger("some data");
|
||||
const handle = await myOtherTask.trigger({ foo: "some data" });
|
||||
|
||||
//...do other stuff
|
||||
const run = await runs.retrieve(handle);
|
||||
// Do something with the run
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
To pass options to the triggered task, you can use the second argument:
|
||||
|
||||
```ts ./trigger/my-task.ts
|
||||
import { myOtherTask, runs } from "~/trigger/my-other-task";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const handle = await myOtherTask.trigger({ foo: "some data" }, { delay: "1h" });
|
||||
|
||||
const run = await runs.retrieve(handle);
|
||||
// Do something with the run
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### yourTask.batchTrigger()
|
||||
|
||||
Triggers multiple runs of a task with the payloads you pass in, and any options you specify. It does NOT wait for the result.
|
||||
Triggers multiple runs of a single task with the payloads you pass in, and any options you specify.
|
||||
|
||||
```ts /trigger/my-task.ts
|
||||
import { myOtherTask } from "~/trigger/my-other-task";
|
||||
import { myOtherTask, batch } from "~/trigger/my-other-task";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
@@ -266,6 +280,43 @@ export const myTask = task({
|
||||
const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]);
|
||||
|
||||
//...do other stuff
|
||||
const batch = await batch.retrieve(batchHandle.id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you need to pass options to `batchTrigger`, you can use the second argument:
|
||||
|
||||
```ts /trigger/my-task.ts
|
||||
import { myOtherTask, batch } from "~/trigger/my-other-task";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }], {
|
||||
idempotencyKey: "my-task-key",
|
||||
});
|
||||
|
||||
//...do other stuff
|
||||
const batch = await batch.retrieve(batchHandle.id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also pass in options for each run in the batch:
|
||||
|
||||
```ts /trigger/my-task.ts
|
||||
import { myOtherTask, batch } from "~/trigger/my-other-task";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const batchHandle = await myOtherTask.batchTrigger([
|
||||
{ payload: "some data", options: { delay: "1h" } },
|
||||
]);
|
||||
|
||||
//...do other stuff
|
||||
const batch = await batch.retrieve(batchHandle.id);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -504,6 +555,152 @@ export const batchParentTask = task({
|
||||
error.
|
||||
</Warning>
|
||||
|
||||
### batch.triggerAndWait()
|
||||
|
||||
You can batch trigger multiple different tasks and wait for all the results:
|
||||
|
||||
```ts /trigger/batch.ts
|
||||
import { batch, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const parentTask = task({
|
||||
id: "parent-task",
|
||||
run: async (payload: string) => {
|
||||
// 👇 Pass a union of all the tasks you want to trigger
|
||||
const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
|
||||
{ id: "child-task-1", payload: { foo: "World" } }, // 👈 The payload is typed correctly based on the task `id`
|
||||
{ id: "child-task-2", payload: { bar: 42 } }, // 👈 The payload is typed correctly based on the task `id`
|
||||
]);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.ok) {
|
||||
// 👇 Narrow the type of the result based on the taskIdentifier
|
||||
switch (result.taskIdentifier) {
|
||||
case "child-task-1":
|
||||
console.log("Child task 1 output", result.output); // 👈 result.output is typed as a string
|
||||
break;
|
||||
case "child-task-2":
|
||||
console.log("Child task 2 output", result.output); // 👈 result.output is typed as a number
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.error("Error", result.error); // 👈 result.error is the error that caused the run to fail
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask1 = task({
|
||||
id: "child-task-1",
|
||||
run: async (payload: { foo: string }) => {
|
||||
return `Hello ${payload}`;
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask2 = task({
|
||||
id: "child-task-2",
|
||||
run: async (payload: { bar: number }) => {
|
||||
return bar + 1;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### batch.triggerByTask()
|
||||
|
||||
You can batch trigger multiple different tasks by passing in the task instances. This function is especially useful when you have a static set of tasks you want to trigger:
|
||||
|
||||
```ts /trigger/batch.ts
|
||||
import { batch, task, runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const parentTask = task({
|
||||
id: "parent-task",
|
||||
run: async (payload: string) => {
|
||||
const results = await batch.triggerByTask([
|
||||
{ task: childTask1, payload: { foo: "World" } }, // 👈 The payload is typed correctly based on the task instance
|
||||
{ task: childTask2, payload: { bar: 42 } }, // 👈 The payload is typed correctly based on the task instance
|
||||
]);
|
||||
|
||||
// 👇 results.runs is a tuple, allowing you to get type safety without needing to narrow
|
||||
const run1 = await runs.retrieve(results.runs[0]); // 👈 run1 is typed as the output of childTask1
|
||||
const run2 = await runs.retrieve(results.runs[1]); // 👈 run2 is typed as the output of childTask2
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask1 = task({
|
||||
id: "child-task-1",
|
||||
run: async (payload: { foo: string }) => {
|
||||
return `Hello ${payload}`;
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask2 = task({
|
||||
id: "child-task-2",
|
||||
run: async (payload: { bar: number }) => {
|
||||
return bar + 1;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### batch.triggerByTaskAndWait()
|
||||
|
||||
You can batch trigger multiple different tasks by passing in the task instances, and wait for all the results. This function is especially useful when you have a static set of tasks you want to trigger:
|
||||
|
||||
```ts /trigger/batch.ts
|
||||
import { batch, task, runs } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const parentTask = task({
|
||||
id: "parent-task",
|
||||
run: async (payload: string) => {
|
||||
const { runs } = await batch.triggerByTaskAndWait([
|
||||
{ task: childTask1, payload: { foo: "World" } }, // 👈 The payload is typed correctly based on the task instance
|
||||
{ task: childTask2, payload: { bar: 42 } }, // 👈 The payload is typed correctly based on the task instance
|
||||
]);
|
||||
|
||||
if (runs[0].ok) {
|
||||
console.log("Child task 1 output", runs[0].output); // 👈 runs[0].output is typed as the output of childTask1
|
||||
}
|
||||
|
||||
if (runs[1].ok) {
|
||||
console.log("Child task 2 output", runs[1].output); // 👈 runs[1].output is typed as the output of childTask2
|
||||
}
|
||||
|
||||
// 💭 A nice alternative syntax is to destructure the runs array:
|
||||
const {
|
||||
runs: [run1, run2],
|
||||
} = await batch.triggerByTaskAndWait([
|
||||
{ task: childTask1, payload: { foo: "World" } }, // 👈 The payload is typed correctly based on the task instance
|
||||
{ task: childTask2, payload: { bar: 42 } }, // 👈 The payload is typed correctly based on the task instance
|
||||
]);
|
||||
|
||||
if (run1.ok) {
|
||||
console.log("Child task 1 output", run1.output); // 👈 run1.output is typed as the output of childTask1
|
||||
}
|
||||
|
||||
if (run2.ok) {
|
||||
console.log("Child task 2 output", run2.output); // 👈 run2.output is typed as the output of childTask2
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask1 = task({
|
||||
id: "child-task-1",
|
||||
run: async (payload: { foo: string }) => {
|
||||
return `Hello ${payload}`;
|
||||
},
|
||||
});
|
||||
|
||||
export const childTask2 = task({
|
||||
id: "child-task-2",
|
||||
run: async (payload: { bar: number }) => {
|
||||
return bar + 1;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Triggering from your frontend
|
||||
|
||||
If you want to trigger a task directly from a frontend application, you can use our [React
|
||||
hooks](/frontend/react-hooks/triggering).
|
||||
|
||||
## Options
|
||||
|
||||
All of the above functions accept an options object:
|
||||
@@ -623,7 +820,39 @@ export const myTask = task({
|
||||
const idempotencyKey = await idempotencyKeys.create("my-task-key");
|
||||
|
||||
// childTask will only be triggered once with the same idempotency key
|
||||
await childTask.triggerAndWait(payload, { idempotencyKey });
|
||||
await childTask.trigger(payload, { idempotencyKey });
|
||||
|
||||
// Do something else, that may throw an error and cause the task to be retried
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For more information, see our [Idempotency](/idempotency) documentation.
|
||||
|
||||
<Warning>
|
||||
In version 3.3.0 and later, the `idempotencyKey` option is not available when using
|
||||
`triggerAndWait` or `batchTriggerAndWait`, due to a bug that would sometimes cause the parent task
|
||||
to become stuck. We are working on a fix for this issue.
|
||||
</Warning>
|
||||
|
||||
### `idempotencyKeyTTL`
|
||||
|
||||
Idempotency keys automatically expire after 30 days, but you can set a custom TTL for an idempotency key when triggering a task:
|
||||
|
||||
```typescript
|
||||
import { idempotencyKeys, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
retry: {
|
||||
maxAttempts: 4,
|
||||
},
|
||||
run: async (payload: any) => {
|
||||
// By default, idempotency keys generated are unique to the run, to prevent retries from duplicating child tasks
|
||||
const idempotencyKey = await idempotencyKeys.create("my-task-key");
|
||||
|
||||
// childTask will only be triggered once with the same idempotency key
|
||||
await childTask.trigger(payload, { idempotencyKey, idempotencyKeyTTL: "1h" });
|
||||
|
||||
// Do something else, that may throw an error and cause the task to be retried
|
||||
},
|
||||
@@ -827,4 +1056,4 @@ export const myTask = task({
|
||||
|
||||
### Batch Triggering
|
||||
|
||||
When using `batchTrigger` or `batchTriggerAndWait`, the total size of all payloads cannot exceed 10MB. This means if you are doing a batch of 100 runs, each payload should be less than 100KB.
|
||||
When using triggering a batch, the total size of all payloads cannot exceed 1MB. This means if you are doing a batch of 100 runs, each payload should be less than 100KB. The max batch size is 500 runs.
|
||||
|
||||
@@ -753,7 +753,7 @@ paths:
|
||||
import { runs, configure } from "@trigger.dev/sdk/v3";
|
||||
|
||||
configure({
|
||||
secretKey: "tr_pat_1234" // always use an environment variable for this
|
||||
accessToken: "tr_pat_1234" // always use an environment variable for this
|
||||
});
|
||||
|
||||
// Get the first page of runs
|
||||
@@ -781,7 +781,7 @@ paths:
|
||||
import { runs, configure } from "@trigger.dev/sdk/v3";
|
||||
|
||||
configure({
|
||||
secretKey: "tr_pat_1234" // always use an environment variable for this
|
||||
accessToken: "tr_pat_1234" // always use an environment variable for this
|
||||
});
|
||||
|
||||
const response = await runs.list("proj_1234", {
|
||||
@@ -1503,7 +1503,7 @@ components:
|
||||
```typescript
|
||||
import { configure } from "@trigger.dev/sdk/v3";
|
||||
|
||||
configure({ secretKey: "tr_dev_1234" });
|
||||
configure({ accessToken: "tr_dev_1234" });
|
||||
```
|
||||
|
||||
personalAccessToken:
|
||||
@@ -1517,7 +1517,7 @@ components:
|
||||
```typescript
|
||||
import { configure } from "@trigger.dev/sdk/v3";
|
||||
|
||||
configure({ secretKey: "tr_pat_1234" });
|
||||
configure({ accessToken: "tr_pat_1234" });
|
||||
```
|
||||
schemas:
|
||||
TriggerTaskResponse:
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "BatchTaskRun" ADD COLUMN "oneTimeUseToken" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TaskRun" ADD COLUMN "oneTimeUseToken" TEXT;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[oneTimeUseToken]` on the table `TaskRun` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "TaskRun_oneTimeUseToken_key" ON "TaskRun"("oneTimeUseToken");
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[oneTimeUseToken]` on the table `BatchTaskRun` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "BatchTaskRun_oneTimeUseToken_key" ON "BatchTaskRun"("oneTimeUseToken");
|
||||
@@ -1722,6 +1722,9 @@ model TaskRun {
|
||||
expiredAt DateTime?
|
||||
maxAttempts Int?
|
||||
|
||||
/// optional token that can be used to authenticate the task run
|
||||
oneTimeUseToken String?
|
||||
|
||||
batchItems BatchTaskRunItem[]
|
||||
dependency TaskRunDependency?
|
||||
CheckpointRestoreEvent CheckpointRestoreEvent[]
|
||||
@@ -1787,6 +1790,7 @@ model TaskRun {
|
||||
|
||||
maxDurationInSeconds Int?
|
||||
|
||||
@@unique([oneTimeUseToken])
|
||||
@@unique([runtimeEnvironmentId, taskIdentifier, idempotencyKey])
|
||||
// Finding child runs
|
||||
@@index([parentTaskRunId])
|
||||
@@ -2161,6 +2165,9 @@ model BatchTaskRun {
|
||||
options Json?
|
||||
batchVersion String @default("v1")
|
||||
|
||||
/// optional token that can be used to authenticate the task run
|
||||
oneTimeUseToken String?
|
||||
|
||||
///all the below properties are engine v1 only
|
||||
items BatchTaskRunItem[]
|
||||
taskIdentifier String?
|
||||
@@ -2170,6 +2177,7 @@ model BatchTaskRun {
|
||||
dependentTaskAttemptId String?
|
||||
runDependencies TaskRunDependency[] @relation("dependentBatchRun")
|
||||
|
||||
@@unique([oneTimeUseToken])
|
||||
///this is used for all engine versions
|
||||
@@unique([runtimeEnvironmentId, idempotencyKey])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# @trigger.dev/build
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.5`
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.4`
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.3`
|
||||
|
||||
## 3.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.2`
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.1`
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/build",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "trigger.dev build extensions",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -65,7 +65,7 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:3.3.0",
|
||||
"@trigger.dev/core": "workspace:3.3.5",
|
||||
"pkg-types": "^1.1.3",
|
||||
"tinyglobby": "^0.2.2",
|
||||
"tsconfck": "3.1.3"
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# trigger.dev
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.5`
|
||||
- `@trigger.dev/build@3.3.5`
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix SDK version in build manifest for out-of-sync detection ([#1530](https://github.com/triggerdotdev/trigger.dev/pull/1530))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/build@3.3.4`
|
||||
- `@trigger.dev/core@3.3.4`
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.3`
|
||||
- `@trigger.dev/build@3.3.3`
|
||||
|
||||
## 3.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/build@3.3.2`
|
||||
- `@trigger.dev/core@3.3.2`
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/build@3.3.1`
|
||||
- `@trigger.dev/core@3.3.1`
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -87,8 +87,8 @@
|
||||
"@opentelemetry/sdk-trace-base": "1.25.1",
|
||||
"@opentelemetry/sdk-trace-node": "1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "1.25.1",
|
||||
"@trigger.dev/build": "workspace:3.3.0",
|
||||
"@trigger.dev/core": "workspace:3.3.0",
|
||||
"@trigger.dev/build": "workspace:3.3.5",
|
||||
"@trigger.dev/core": "workspace:3.3.5",
|
||||
"c12": "^1.11.1",
|
||||
"chalk": "^5.2.0",
|
||||
"cli-table3": "^0.6.3",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { isWindows } from "std-env";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { SdkVersionExtractor } from "./plugins.js";
|
||||
|
||||
export type BuildWorkerEventListener = {
|
||||
onBundleStart?: () => void;
|
||||
@@ -61,6 +62,8 @@ export async function buildWorker(options: BuildWorkerOptions) {
|
||||
await notifyExtensionOnBuildStart(buildContext);
|
||||
const pluginsFromExtensions = resolvePluginsForContext(buildContext);
|
||||
|
||||
const sdkVersionExtractor = new SdkVersionExtractor();
|
||||
|
||||
options.listener?.onBundleStart?.();
|
||||
|
||||
const bundleResult = await bundleWorker({
|
||||
@@ -69,7 +72,7 @@ export async function buildWorker(options: BuildWorkerOptions) {
|
||||
destination: options.destination,
|
||||
watch: false,
|
||||
resolvedConfig,
|
||||
plugins: [...pluginsFromExtensions],
|
||||
plugins: [sdkVersionExtractor.plugin, ...pluginsFromExtensions],
|
||||
jsxFactory: resolvedConfig.build.jsx.factory,
|
||||
jsxFragment: resolvedConfig.build.jsx.fragment,
|
||||
jsxAutomatic: resolvedConfig.build.jsx.automatic,
|
||||
@@ -81,7 +84,7 @@ export async function buildWorker(options: BuildWorkerOptions) {
|
||||
contentHash: bundleResult.contentHash,
|
||||
runtime: resolvedConfig.runtime ?? DEFAULT_RUNTIME,
|
||||
environment: options.environment,
|
||||
packageVersion: CORE_VERSION,
|
||||
packageVersion: sdkVersionExtractor.sdkVersion ?? CORE_VERSION,
|
||||
cliPackageVersion: VERSION,
|
||||
target: "deploy",
|
||||
files: bundleResult.files,
|
||||
|
||||
@@ -4,6 +4,10 @@ import { ResolvedConfig } from "@trigger.dev/core/v3/build";
|
||||
import { configPlugin } from "../config.js";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { bunPlugin } from "../runtimes/bun.js";
|
||||
import { resolvePathSync as esmResolveSync } from "mlly";
|
||||
import { readPackageJSON, resolvePackageJSON } from "pkg-types";
|
||||
import { dirname } from "node:path";
|
||||
import { readJSONFile } from "../utilities/fileSystem.js";
|
||||
|
||||
export async function buildPlugins(
|
||||
target: BuildTarget,
|
||||
@@ -87,3 +91,94 @@ export function polyshedPlugin(): esbuild.Plugin {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class SdkVersionExtractor {
|
||||
private _sdkVersion: string | undefined;
|
||||
private _ranOnce = false;
|
||||
|
||||
get sdkVersion() {
|
||||
return this._sdkVersion;
|
||||
}
|
||||
|
||||
get plugin(): esbuild.Plugin {
|
||||
return {
|
||||
name: "sdk-version",
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /^@trigger\.dev\/sdk\// }, async (args) => {
|
||||
if (this._ranOnce) {
|
||||
return undefined;
|
||||
} else {
|
||||
this._ranOnce = true;
|
||||
}
|
||||
|
||||
logger.debug("[SdkVersionExtractor] Extracting SDK version", { args });
|
||||
|
||||
try {
|
||||
const resolvedPath = esmResolveSync(args.path, {
|
||||
url: args.resolveDir,
|
||||
});
|
||||
|
||||
logger.debug("[SdkVersionExtractor] Resolved SDK module path", { resolvedPath });
|
||||
|
||||
const packageJsonPath = await resolvePackageJSON(dirname(resolvedPath), {
|
||||
test: async (filePath) => {
|
||||
try {
|
||||
const candidate = await readJSONFile(filePath);
|
||||
|
||||
// Exclude esm type markers
|
||||
return Object.keys(candidate).length > 1 || !candidate.type;
|
||||
} catch (error) {
|
||||
logger.debug("[SdkVersionExtractor] Error during package.json test", {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!packageJsonPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
logger.debug("[SdkVersionExtractor] Found package.json", { packageJsonPath });
|
||||
|
||||
const packageJson = await readPackageJSON(packageJsonPath);
|
||||
|
||||
if (!packageJson.name || packageJson.name !== "@trigger.dev/sdk") {
|
||||
logger.debug("[SdkVersionExtractor] No match for SDK package name", {
|
||||
packageJsonPath,
|
||||
packageJson,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!packageJson.version) {
|
||||
logger.debug("[SdkVersionExtractor] No version found in package.json", {
|
||||
packageJsonPath,
|
||||
packageJson,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._sdkVersion = packageJson.version;
|
||||
|
||||
logger.debug("[SdkVersionExtractor] Found SDK version", {
|
||||
args,
|
||||
packageJsonPath,
|
||||
sdkVersion: this._sdkVersion,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
logger.debug("[SdkVersionExtractor] Failed to extract SDK version", { error });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# internal-platform
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix an issue that caused errors when using realtime with a run that is cancelled ([#1533](https://github.com/triggerdotdev/trigger.dev/pull/1533))
|
||||
|
||||
## 3.3.4
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Multiple streams can now be consumed simultaneously ([#1522](https://github.com/triggerdotdev/trigger.dev/pull/1522))
|
||||
|
||||
## 3.3.2
|
||||
|
||||
## 3.3.1
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/core",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "Core code used across the Trigger.dev SDK and platform",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -214,9 +214,7 @@ export class ApiClient {
|
||||
secretKey: this.accessToken,
|
||||
payload: {
|
||||
...claims,
|
||||
scopes: [`read:runs:${data.id}`].concat(
|
||||
body.options?.tags ? Array.from(body.options?.tags).map((t) => `read:tags:${t}`) : []
|
||||
),
|
||||
scopes: [`read:runs:${data.id}`],
|
||||
},
|
||||
expirationTime: requestOptions?.publicAccessToken?.expirationTime ?? "1h",
|
||||
});
|
||||
@@ -255,7 +253,7 @@ export class ApiClient {
|
||||
secretKey: this.accessToken,
|
||||
payload: {
|
||||
...claims,
|
||||
scopes: [`read:batch:${data.id}`].concat(data.runs.map((r) => `read:runs:${r.id}`)),
|
||||
scopes: [`read:batch:${data.id}`],
|
||||
},
|
||||
expirationTime: requestOptions?.publicAccessToken?.expirationTime ?? "1h",
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DeserializedJson } from "../../schemas/json.js";
|
||||
import { createJsonErrorObject } from "../errors.js";
|
||||
import { RunStatus, SubscribeRunRawShape } from "../schemas/api.js";
|
||||
import { SerializedError } from "../schemas/common.js";
|
||||
import { AnyRunTypes, AnyTask, InferRunTypes } from "../types/tasks.js";
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
IOPacket,
|
||||
parsePacket,
|
||||
} from "../utils/ioSerialization.js";
|
||||
import { ApiError } from "./errors.js";
|
||||
import { ApiClient } from "./index.js";
|
||||
import { AsyncIterableStream, createAsyncIterableStream, zodShapeStream } from "./stream.js";
|
||||
import { EventSourceParserStream } from "eventsource-parser/stream";
|
||||
@@ -97,7 +99,7 @@ export function runShapeStream<TRunTypes extends AnyRunTypes>(
|
||||
|
||||
// First, define interfaces for the stream handling
|
||||
export interface StreamSubscription {
|
||||
subscribe(onChunk: (chunk: unknown) => Promise<void>): Promise<() => void>;
|
||||
subscribe(): Promise<ReadableStream<unknown>>;
|
||||
}
|
||||
|
||||
export interface StreamSubscriptionFactory {
|
||||
@@ -111,33 +113,38 @@ export class SSEStreamSubscription implements StreamSubscription {
|
||||
private options: { headers?: Record<string, string>; signal?: AbortSignal }
|
||||
) {}
|
||||
|
||||
async subscribe(onChunk: (chunk: unknown) => Promise<void>): Promise<() => void> {
|
||||
const response = await fetch(this.url, {
|
||||
async subscribe(): Promise<ReadableStream<unknown>> {
|
||||
return fetch(this.url, {
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
...this.options.headers,
|
||||
},
|
||||
signal: this.options.signal,
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw ApiError.generate(
|
||||
response.status,
|
||||
{},
|
||||
"Could not subscribe to stream",
|
||||
Object.fromEntries(response.headers)
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body");
|
||||
}
|
||||
|
||||
return response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new EventSourceParserStream())
|
||||
.pipeThrough(
|
||||
new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(safeParseJSON(chunk.data));
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body");
|
||||
}
|
||||
|
||||
const reader = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new EventSourceParserStream())
|
||||
.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
await onChunk(safeParseJSON(value.data));
|
||||
}
|
||||
|
||||
return () => reader.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,13 +261,31 @@ export class RunSubscription<TRunTypes extends AnyRunTypes> {
|
||||
this.options.client?.baseUrl
|
||||
);
|
||||
|
||||
await subscription.subscribe(async (chunk) => {
|
||||
controller.enqueue({
|
||||
type: streamKey,
|
||||
chunk: chunk as TStreams[typeof streamKey],
|
||||
run,
|
||||
} as StreamPartResult<RunShape<TRunTypes>, TStreams>);
|
||||
});
|
||||
const stream = await subscription.subscribe();
|
||||
|
||||
// Create the pipeline and start it
|
||||
stream
|
||||
.pipeThrough(
|
||||
new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue({
|
||||
type: streamKey,
|
||||
chunk: chunk as TStreams[typeof streamKey],
|
||||
run,
|
||||
} as StreamPartResult<RunShape<TRunTypes>, TStreams>);
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error(`Error in stream ${streamKey}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +348,7 @@ export class RunSubscription<TRunTypes extends AnyRunTypes> {
|
||||
startedAt: row.startedAt ?? undefined,
|
||||
delayedUntil: row.delayUntil ?? undefined,
|
||||
queuedAt: row.queuedAt ?? undefined,
|
||||
error: row.error ?? undefined,
|
||||
error: row.error ? createJsonErrorObject(row.error) : undefined,
|
||||
isTest: row.isTest,
|
||||
metadata,
|
||||
} as RunShape<TRunTypes>;
|
||||
|
||||
@@ -250,7 +250,7 @@ export class StandardMetadataManager implements RunMetadataManager {
|
||||
return streamInstance;
|
||||
} catch (error) {
|
||||
// Clean up metadata key if stream creation fails
|
||||
this.deleteKey(`$$stream.${key}`);
|
||||
this.removeFromKey(`$$streams`, key);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,7 @@ export class StandardMetadataManager implements RunMetadataManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = Array.from(this.activeStreams.values());
|
||||
const promises = Array.from(this.activeStreams.values()).map((stream) => stream.wait());
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { DeserializedJsonSchema } from "../../schemas/json.js";
|
||||
import { SerializedError } from "./common.js";
|
||||
import { SerializedError, TaskRunError } from "./common.js";
|
||||
import { BackgroundWorkerMetadata } from "./resources.js";
|
||||
import { QueueOptions } from "./schemas.js";
|
||||
|
||||
@@ -708,7 +708,7 @@ export const SubscribeRunRawShape = z.object({
|
||||
output: z.string().nullish(),
|
||||
outputType: z.string().nullish(),
|
||||
runTags: z.array(z.string()).nullish().default([]),
|
||||
error: SerializedError.nullish(),
|
||||
error: TaskRunError.nullish(),
|
||||
});
|
||||
|
||||
export type SubscribeRunRawShape = z.infer<typeof SubscribeRunRawShape>;
|
||||
|
||||
@@ -9,17 +9,23 @@ import {
|
||||
import type { SubscribeRunRawShape } from "../src/v3/schemas/api.js";
|
||||
|
||||
// Test implementations
|
||||
// Update TestStreamSubscription to return a ReadableStream
|
||||
class TestStreamSubscription implements StreamSubscription {
|
||||
constructor(private chunks: unknown[]) {}
|
||||
|
||||
async subscribe(onChunk: (chunk: unknown) => Promise<void>): Promise<() => void> {
|
||||
for (const chunk of this.chunks) {
|
||||
await onChunk(chunk);
|
||||
}
|
||||
return () => {};
|
||||
async subscribe(): Promise<ReadableStream<unknown>> {
|
||||
return new ReadableStream({
|
||||
start: async (controller) => {
|
||||
for (const chunk of this.chunks) {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamSubscriptionFactory can remain the same
|
||||
class TestStreamSubscriptionFactory implements StreamSubscriptionFactory {
|
||||
private streams = new Map<string, unknown[]>();
|
||||
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/react-hooks
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.5`
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add trigger options to all trigger hooks ([#1528](https://github.com/triggerdotdev/trigger.dev/pull/1528))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.4`
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.3`
|
||||
|
||||
## 3.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.2`
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Public access token scopes with just tags or just a batch can now access runs that have those tags or are in the batch. Previously, the only way to access a run was to have a specific scope for that exact run. ([#1511](https://github.com/triggerdotdev/trigger.dev/pull/1511))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.1`
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/react-hooks",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "trigger.dev react hooks",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:^3.3.0",
|
||||
"@trigger.dev/core": "workspace:^3.3.5",
|
||||
"swr": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
makeIdempotencyKey,
|
||||
RunHandleFromTypes,
|
||||
stringifyIO,
|
||||
TriggerOptions,
|
||||
type TriggerOptions,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { useApiClient, UseApiClientOptions } from "./useApiClient.js";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
*/
|
||||
export interface TriggerInstance<TTask extends AnyTask> {
|
||||
/** Function to submit the task with a payload */
|
||||
submit: (payload: TaskPayload<TTask>) => void;
|
||||
submit: (payload: TaskPayload<TTask>, options?: TriggerOptions) => void;
|
||||
/** Whether the task is currently being submitted */
|
||||
isLoading: boolean;
|
||||
/** The handle returned after successful task submission */
|
||||
@@ -94,9 +94,9 @@ export function useTaskTrigger<TTask extends AnyTask>(
|
||||
const mutation = useSWRMutation(id as string, triggerTask);
|
||||
|
||||
return {
|
||||
submit: (payload) => {
|
||||
submit: (payload, options) => {
|
||||
// trigger the task with the given payload
|
||||
mutation.trigger({ payload });
|
||||
mutation.trigger({ payload, options });
|
||||
},
|
||||
isLoading: mutation.isMutating,
|
||||
handle: mutation.data as RunHandleFromTypes<InferRunTypes<TTask>>,
|
||||
@@ -118,7 +118,7 @@ export type RealtimeTriggerInstanceWithStreams<
|
||||
TTask extends AnyTask,
|
||||
TStreams extends Record<string, any> = Record<string, any>,
|
||||
> = UseRealtimeRunWithStreamsInstance<TTask, TStreams> & {
|
||||
submit: (payload: TaskPayload<TTask>) => void;
|
||||
submit: (payload: TaskPayload<TTask>, options?: TriggerOptions) => void;
|
||||
isLoading: boolean;
|
||||
handle?: RunHandleFromTypes<InferRunTypes<TTask>>;
|
||||
};
|
||||
@@ -166,7 +166,7 @@ export function useRealtimeTaskTriggerWithStreams<
|
||||
}
|
||||
|
||||
export type RealtimeTriggerInstance<TTask extends AnyTask> = UseRealtimeRunInstance<TTask> & {
|
||||
submit: (payload: TaskPayload<TTask>) => void;
|
||||
submit: (payload: TaskPayload<TTask>, options?: TriggerOptions) => void;
|
||||
isLoading: boolean;
|
||||
handle?: RunHandleFromTypes<InferRunTypes<TTask>>;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# @trigger.dev/rsc
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.5`
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.4`
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.3`
|
||||
|
||||
## 3.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.2`
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.1`
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/rsc",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "trigger.dev rsc",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -37,14 +37,14 @@
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:^3.3.0",
|
||||
"@trigger.dev/core": "workspace:^3.3.5",
|
||||
"mlly": "^1.7.1",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react-dom": "19.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.15.4",
|
||||
"@trigger.dev/build": "workspace:^3.3.0",
|
||||
"@trigger.dev/build": "workspace:^3.3.5",
|
||||
"@types/node": "^20.14.14",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# @trigger.dev/sdk
|
||||
|
||||
## 3.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.5`
|
||||
|
||||
## 3.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.4`
|
||||
|
||||
## 3.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.3`
|
||||
|
||||
## 3.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Add one-time use public tokens to trigger and batch trigger ([#1515](https://github.com/triggerdotdev/trigger.dev/pull/1515))
|
||||
- Fix for waiting for realtime streams to finish ([#1520](https://github.com/triggerdotdev/trigger.dev/pull/1520))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.2`
|
||||
|
||||
## 3.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fixed the missing icons in trigger spans ([#1506](https://github.com/triggerdotdev/trigger.dev/pull/1506))
|
||||
- Public access token scopes with just tags or just a batch can now access runs that have those tags or are in the batch. Previously, the only way to access a run was to have a specific scope for that exact run. ([#1511](https://github.com/triggerdotdev/trigger.dev/pull/1511))
|
||||
- Updated dependencies:
|
||||
- `@trigger.dev/core@3.3.1`
|
||||
|
||||
## 3.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sdk",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.5",
|
||||
"description": "trigger.dev Node.JS SDK",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/api-logs": "0.52.1",
|
||||
"@opentelemetry/semantic-conventions": "1.25.1",
|
||||
"@trigger.dev/core": "workspace:3.3.0",
|
||||
"@trigger.dev/core": "workspace:3.3.5",
|
||||
"chalk": "^5.2.0",
|
||||
"cronstrue": "^2.21.0",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -25,11 +25,14 @@ export function configure(options: ApiClientConfiguration) {
|
||||
export const auth = {
|
||||
configure,
|
||||
createPublicToken,
|
||||
createTriggerPublicToken,
|
||||
createBatchTriggerPublicToken,
|
||||
withAuth,
|
||||
withPublicToken,
|
||||
withTriggerPublicToken,
|
||||
withBatchTriggerPublicToken,
|
||||
};
|
||||
|
||||
type PublicTokenPermissionAction = "read" | "write"; // Add more actions as needed
|
||||
|
||||
type PublicTokenPermissionProperties = {
|
||||
/**
|
||||
* Grant access to specific tasks
|
||||
@@ -53,7 +56,26 @@ type PublicTokenPermissionProperties = {
|
||||
};
|
||||
|
||||
export type PublicTokenPermissions = {
|
||||
[key in PublicTokenPermissionAction]?: PublicTokenPermissionProperties;
|
||||
read?: PublicTokenPermissionProperties;
|
||||
|
||||
/**
|
||||
* @deprecated use trigger instead
|
||||
*/
|
||||
write?: PublicTokenPermissionProperties;
|
||||
|
||||
/**
|
||||
* Use auth.createTriggerPublicToken
|
||||
*/
|
||||
trigger?: {
|
||||
tasks: string | string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Use auth.createBatchTriggerPublicToken
|
||||
*/
|
||||
batchTrigger?: {
|
||||
tasks: string | string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CreatePublicTokenOptions = {
|
||||
@@ -120,6 +142,180 @@ async function createPublicToken(options?: CreatePublicTokenOptions): Promise<st
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with a public token, providing temporary access permissions.
|
||||
*
|
||||
* @param options - Options for creating the public token.
|
||||
* @param fn - The asynchronous function to be executed with the public token.
|
||||
*/
|
||||
async function withPublicToken(options: CreatePublicTokenOptions, fn: () => Promise<void>) {
|
||||
const token = await createPublicToken(options);
|
||||
|
||||
await withAuth({ accessToken: token }, fn);
|
||||
}
|
||||
|
||||
export type CreateTriggerTokenOptions = {
|
||||
/**
|
||||
* The expiration time for the token. This can be a number representing the time in milliseconds, a `Date` object, or a string.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```typescript
|
||||
* expirationTime: "1h"
|
||||
* ```
|
||||
*/
|
||||
expirationTime?: number | Date | string;
|
||||
|
||||
/**
|
||||
* Whether the token can be used multiple times. By default trigger tokens are one-time use.
|
||||
* @default false
|
||||
*/
|
||||
multipleUse?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a one-time use token to trigger a specific task.
|
||||
*
|
||||
* @param task - The task ID or an array of task IDs that the token should allow triggering.
|
||||
* @param options - Options for creating the one-time use token.
|
||||
* @returns A promise that resolves to a string representing the generated one-time use token.
|
||||
*
|
||||
* @example
|
||||
* Create a one-time use public token that allows triggering a specific task:
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createTriggerPublicToken("my-task");
|
||||
* ```
|
||||
*
|
||||
* @example You can also create a one-time use token that allows triggering multiple tasks:
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createTriggerPublicToken(["task1", "task2"]);
|
||||
* ```
|
||||
*
|
||||
* @example You can also create a one-time use token that allows triggering a task with a specific expiration time:
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createTriggerPublicToken("my-task", { expirationTime: "1h" });
|
||||
* ```
|
||||
*/
|
||||
async function createTriggerPublicToken(
|
||||
task: string | string[],
|
||||
options?: CreateTriggerTokenOptions
|
||||
): Promise<string> {
|
||||
const apiClient = apiClientManager.clientOrThrow();
|
||||
|
||||
const claims = await apiClient.generateJWTClaims();
|
||||
|
||||
return await internal_generateJWT({
|
||||
secretKey: apiClient.accessToken,
|
||||
payload: {
|
||||
...claims,
|
||||
otu: typeof options?.multipleUse === "boolean" ? !options.multipleUse : true,
|
||||
scopes: flattenScopes({
|
||||
trigger: {
|
||||
tasks: task,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expirationTime: options?.expirationTime,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with a one-time use token that allows triggering a specific task.
|
||||
*
|
||||
* @param task - The task ID or an array of task IDs that the token should allow triggering.
|
||||
* @param options - Options for creating the one-time use token.
|
||||
* @param fn - The asynchronous function to be executed with the one-time use token.
|
||||
*/
|
||||
async function withTriggerPublicToken(
|
||||
task: string | string[],
|
||||
options: CreateTriggerTokenOptions = {},
|
||||
fn: () => Promise<void>
|
||||
) {
|
||||
const token = await createTriggerPublicToken(task, options);
|
||||
|
||||
await withAuth({ accessToken: token }, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a one-time use token to batch trigger a specific task or tasks.
|
||||
*
|
||||
* @param task - The task ID or an array of task IDs that the token should allow triggering.
|
||||
* @param options - Options for creating the one-time use token.
|
||||
* @returns A promise that resolves to a string representing the generated one-time use token.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createBatchTriggerPublicToken("my-task");
|
||||
* ```
|
||||
*
|
||||
* @example You can also create a one-time use token that allows batch triggering multiple tasks:
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createBatchTriggerPublicToken(["task1", "task2"]);
|
||||
* ```
|
||||
*
|
||||
* @example You can also create a one-time use token that allows batch triggering a task with a specific expiration time:
|
||||
*
|
||||
* ```ts
|
||||
* import { auth } from "@trigger.dev/sdk/v3";
|
||||
*
|
||||
* const token = await auth.createBatchTriggerPublicToken("my-task", { expirationTime: "1h" });
|
||||
* ```
|
||||
*/
|
||||
async function createBatchTriggerPublicToken(
|
||||
task: string | string[],
|
||||
options?: CreateTriggerTokenOptions
|
||||
): Promise<string> {
|
||||
const apiClient = apiClientManager.clientOrThrow();
|
||||
|
||||
const claims = await apiClient.generateJWTClaims();
|
||||
|
||||
return await internal_generateJWT({
|
||||
secretKey: apiClient.accessToken,
|
||||
payload: {
|
||||
...claims,
|
||||
otu: typeof options?.multipleUse === "boolean" ? !options.multipleUse : true,
|
||||
scopes: flattenScopes({
|
||||
batchTrigger: {
|
||||
tasks: task,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expirationTime: options?.expirationTime,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with a one-time use token that allows triggering a specific task.
|
||||
*
|
||||
* @param task - The task ID or an array of task IDs that the token should allow triggering.
|
||||
* @param options - Options for creating the one-time use token.
|
||||
* @param fn - The asynchronous function to be executed with the one-time use token.
|
||||
*/
|
||||
async function withBatchTriggerPublicToken(
|
||||
task: string | string[],
|
||||
options: CreateTriggerTokenOptions = {},
|
||||
fn: () => Promise<void>
|
||||
) {
|
||||
const token = await createBatchTriggerPublicToken(task, options);
|
||||
|
||||
await withAuth({ accessToken: token }, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided asynchronous function with a specified API client configuration.
|
||||
*
|
||||
|
||||
@@ -817,6 +817,9 @@ export async function batchTriggerByIdAndWait<TTask extends AnyTask>(
|
||||
},
|
||||
{
|
||||
kind: SpanKind.PRODUCER,
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "trigger",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1146,6 +1149,9 @@ export async function batchTriggerAndWaitTasks<TTasks extends readonly AnyTask[]
|
||||
},
|
||||
{
|
||||
kind: SpanKind.PRODUCER,
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "trigger",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1351,6 +1357,7 @@ async function triggerAndWait_internal<TIdentifier extends string, TPayload, TOu
|
||||
{
|
||||
kind: SpanKind.PRODUCER,
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "trigger",
|
||||
...accessoryAttributes({
|
||||
items: [
|
||||
{
|
||||
@@ -1445,6 +1452,7 @@ async function batchTriggerAndWait_internal<TIdentifier extends string, TPayload
|
||||
{
|
||||
kind: SpanKind.PRODUCER,
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "trigger",
|
||||
...accessoryAttributes({
|
||||
items: [
|
||||
{
|
||||
|
||||
Generated
+7
-7
@@ -1015,7 +1015,7 @@ importers:
|
||||
packages/build:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:3.3.0
|
||||
specifier: workspace:3.3.5
|
||||
version: link:../core
|
||||
pkg-types:
|
||||
specifier: ^1.1.3
|
||||
@@ -1094,10 +1094,10 @@ importers:
|
||||
specifier: 1.25.1
|
||||
version: 1.25.1
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:3.3.0
|
||||
specifier: workspace:3.3.5
|
||||
version: link:../build
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:3.3.0
|
||||
specifier: workspace:3.3.5
|
||||
version: link:../core
|
||||
c12:
|
||||
specifier: ^1.11.1
|
||||
@@ -1390,7 +1390,7 @@ importers:
|
||||
packages/react-hooks:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:^3.3.0
|
||||
specifier: workspace:^3.3.5
|
||||
version: link:../core
|
||||
react:
|
||||
specifier: '>=18 || >=19.0.0-beta'
|
||||
@@ -1430,7 +1430,7 @@ importers:
|
||||
packages/rsc:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:^3.3.0
|
||||
specifier: workspace:^3.3.5
|
||||
version: link:../core
|
||||
mlly:
|
||||
specifier: ^1.7.1
|
||||
@@ -1446,7 +1446,7 @@ importers:
|
||||
specifier: ^0.15.4
|
||||
version: 0.15.4
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:^3.3.0
|
||||
specifier: workspace:^3.3.5
|
||||
version: link:../build
|
||||
'@types/node':
|
||||
specifier: ^20.14.14
|
||||
@@ -1482,7 +1482,7 @@ importers:
|
||||
specifier: 1.25.1
|
||||
version: 1.25.1
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:3.3.0
|
||||
specifier: workspace:3.3.5
|
||||
version: link:../core
|
||||
chalk:
|
||||
specifier: ^5.2.0
|
||||
|
||||
@@ -6,23 +6,7 @@ import { ImageUploadDropzone } from "@/components/ImageUploadButton";
|
||||
import { auth } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export default async function Home() {
|
||||
const publicAccessToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
write: {
|
||||
tasks: ["openai-streaming"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const readAll = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log({ publicAccessToken, readAll });
|
||||
const publicAccessToken = await auth.createTriggerPublicToken("openai-streaming");
|
||||
|
||||
return (
|
||||
<main className="grid grid-rows-[1fr_auto] min-h-screen items-center justify-center w-full bg-gray-900">
|
||||
|
||||
@@ -25,11 +25,16 @@ export default function TriggerButton({ accessToken }: { accessToken: string })
|
||||
disabled={isLoading}
|
||||
className="p-0 bg-transparent hover:bg-transparent hover:text-gray-200 text-gray-400"
|
||||
onClick={() => {
|
||||
submit({
|
||||
model: "gpt-4o-mini",
|
||||
prompt:
|
||||
"Based on the temperature, will I need to wear extra clothes today in San Fransico? Please be detailed.",
|
||||
});
|
||||
submit(
|
||||
{
|
||||
model: "gpt-4o-mini",
|
||||
prompt:
|
||||
"Based on the temperature, will I need to wear extra clothes today in San Fransico? Please be detailed.",
|
||||
},
|
||||
{
|
||||
tags: ["user:1234"],
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Triggering..." : "Trigger Task"}
|
||||
|
||||
@@ -15,7 +15,12 @@ export const exampleTask = schemaTask({
|
||||
|
||||
metadata.set("status", { type: "started", progress: 0.1 });
|
||||
|
||||
await setTimeout(2000);
|
||||
if (Math.random() < 0.9) {
|
||||
// Simulate a failure
|
||||
throw new Error("Random failure");
|
||||
}
|
||||
|
||||
await setTimeout(20000);
|
||||
|
||||
metadata.set("status", { type: "processing", progress: 0.5 });
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"queues": "ts-node -r dotenv/config -r tsconfig-paths/register ./src/queues.ts",
|
||||
"build:client": "tsup-node ./src/clientUsage.ts --format esm,cjs",
|
||||
"client": "tsx -r dotenv/config ./src/clientUsage.ts",
|
||||
"triggerWithLargePayload": "ts-node -r dotenv/config -r tsconfig-paths/register ./src/triggerWithLargePayload.ts",
|
||||
"triggerWithLargePayload": "tsx -r dotenv/config ./src/triggerWithLargePayload.ts",
|
||||
"generate:prisma": "prisma generate --sql"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure, envvars, runs, schedules, batch } from "@trigger.dev/sdk/v3";
|
||||
import { configure, envvars, runs, schedules, batch, auth } from "@trigger.dev/sdk/v3";
|
||||
import dotenv from "dotenv";
|
||||
import { unfriendlyIdTask } from "./trigger/other.js";
|
||||
import { spamRateLimiter, taskThatErrors } from "./trigger/retries.js";
|
||||
@@ -271,9 +271,154 @@ async function doBatchTrigger() {
|
||||
console.log("batch runs", $runs.data);
|
||||
}
|
||||
|
||||
async function doRescheduleRun() {
|
||||
const run = await simpleChildTask.trigger({ message: "Hello, World!" }, { delay: "1h" });
|
||||
|
||||
console.log("run", run);
|
||||
|
||||
const reschedule = await runs.reschedule(run.id, {
|
||||
delay: "1s",
|
||||
});
|
||||
|
||||
console.log("reschedule", reschedule);
|
||||
|
||||
const rescheduledRun = await waitForRunToComplete(reschedule.id);
|
||||
|
||||
console.log("rescheduled run", rescheduledRun);
|
||||
}
|
||||
|
||||
async function doOneTimeUseTrigger() {
|
||||
console.log("Testing with one-time use token");
|
||||
|
||||
try {
|
||||
await auth.withTriggerPublicToken(simpleChildTask.id, {}, async () => {
|
||||
const run1 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run1", run1);
|
||||
|
||||
const run2 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run2", run2);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing with deprecated public token");
|
||||
|
||||
try {
|
||||
await auth.withPublicToken(
|
||||
{
|
||||
scopes: {
|
||||
write: {
|
||||
tasks: simpleChildTask.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const run1 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing with trigger public token");
|
||||
|
||||
try {
|
||||
await auth.withPublicToken(
|
||||
{
|
||||
scopes: {
|
||||
trigger: {
|
||||
tasks: simpleChildTask.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const run1 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run1", run1);
|
||||
|
||||
const run2 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run2", run2);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing with a one-time use token for the wrong task");
|
||||
|
||||
try {
|
||||
await auth.withTriggerPublicToken("wrong-task-id", {}, async () => {
|
||||
const run1 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run1", run1);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing with a public token for the wrong task");
|
||||
|
||||
try {
|
||||
await auth.withPublicToken(
|
||||
{
|
||||
scopes: {
|
||||
write: {
|
||||
tasks: "wrong-task-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const run1 = await simpleChildTask.trigger({ message: "Hello, World!" });
|
||||
|
||||
console.log("run1", run1);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing batch trigger with one-time use token");
|
||||
|
||||
try {
|
||||
await auth.withBatchTriggerPublicToken(simpleChildTask.id, {}, async () => {
|
||||
const batch1 = await batch.triggerByTask([
|
||||
{ task: simpleChildTask, payload: { message: "Hello, World!" } },
|
||||
]);
|
||||
|
||||
console.log("batch1", batch1);
|
||||
|
||||
const batch2 = await batch.triggerByTask([
|
||||
{ task: simpleChildTask, payload: { message: "Hello, World!" } },
|
||||
]);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log("Testing batch trigger with a trigger token");
|
||||
|
||||
try {
|
||||
await auth.withTriggerPublicToken(simpleChildTask.id, {}, async () => {
|
||||
const batch1 = await batch.triggerByTask([
|
||||
{ task: simpleChildTask, payload: { message: "Hello, World!" } },
|
||||
]);
|
||||
|
||||
console.log("batch1", batch1);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// doRuns().catch(console.error);
|
||||
// doListRuns().catch(console.error);
|
||||
// doScheduleLists().catch(console.error);
|
||||
doBatchTrigger().catch(console.error);
|
||||
// doBatchTrigger().catch(console.error);
|
||||
// doEnvVars().catch(console.error);
|
||||
// doTriggerUnfriendlyTaskId().catch(console.error);
|
||||
// doRescheduleRun().catch(console.error);
|
||||
doOneTimeUseTrigger().catch(console.error);
|
||||
|
||||
@@ -36,9 +36,17 @@ else
|
||||
fi
|
||||
|
||||
# Run your commands
|
||||
|
||||
# Run changeset version command and capture its output
|
||||
echo "Running: pnpm exec changeset version --snapshot $version"
|
||||
pnpm exec changeset version --snapshot $version
|
||||
if output=$(pnpm exec changeset version --snapshot $version 2>&1); then
|
||||
if echo "$output" | grep -q "No unreleased changesets found"; then
|
||||
echo "No unreleased changesets found. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Error running changeset version command"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running: pnpm run build --filter \"@trigger.dev/*\" --filter \"trigger.dev\""
|
||||
pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
Reference in New Issue
Block a user