Schedule limits and timezone support (#1165)
* Added maximumScheduleInstancesLimit column to Org, default to 20 * Docs on the schedule limits and improved soft-limit communication * Added limit info to the schedules list page * Created a task that creates schedules, useful for testing * Make deduplicationKey required when creating/updating a schedule using the SDK * New schedule button shows an alert if you’re over the limit * Added timezone to the form and db * WIP on the timezone dropdown for the create/edit schedule form * Use the new filter search for timezones * Made the timezone dropdown faster by fixing the virtualization * The preview table is working and added a nice message about daylight savings * Created a page where you can view the full list of timezones The URL is included in the error message if you send an invalid time using the SDK * Creating tasks with the timezone * Added timezone support the the scheduler and the schedules list * Added timezone support to more of the schedules UI * The timezone comes through to scheduled runs with nice JSDocs * Allow setting the timezone from the SDK * Always have a timezone on a schedule * Updated jsdocs * Updated catalog example * Changed the column to be a string, not null. Added the timezone across the SDK * API endpoint for getting the timezones * Added an SDK function to get the list of timezones * Added timezones to the docs * Changeset: Added timezone support to schedules * Added support for testing timezone * Tidied up imports * Imports * Imports * Update limits.mdx * Fixed a couple type issues and use the already exported zodfetch --------- Co-authored-by: Eric Allam <eallam@icloud.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Make deduplicationKey required when creating/updating a schedule
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added timezone support to schedules
|
||||
@@ -6,6 +6,7 @@ type DateTimeProps = {
|
||||
timeZone?: string;
|
||||
includeSeconds?: boolean;
|
||||
includeTime?: boolean;
|
||||
showTimezone?: boolean;
|
||||
};
|
||||
|
||||
export const DateTime = ({
|
||||
@@ -13,6 +14,7 @@ export const DateTime = ({
|
||||
timeZone,
|
||||
includeSeconds = true,
|
||||
includeTime = true,
|
||||
showTimezone = false,
|
||||
}: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
@@ -42,7 +44,12 @@ export const DateTime = ({
|
||||
);
|
||||
}, [locales, includeSeconds, realDate]);
|
||||
|
||||
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
|
||||
return (
|
||||
<Fragment>
|
||||
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
|
||||
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export function formatDateTime(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { MatchSorterOptions, matchSorter } from "match-sorter";
|
||||
|
||||
const sizes = {
|
||||
small: {
|
||||
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
|
||||
showHeading?: boolean;
|
||||
items?: TItem[] | Section<TItem>[];
|
||||
empty?: React.ReactNode;
|
||||
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
|
||||
filter?:
|
||||
| boolean
|
||||
| MatchSorterOptions<TItem>
|
||||
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((
|
||||
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
|
||||
if (!items) return [];
|
||||
if (!searchValue || !filter) return items;
|
||||
|
||||
if (typeof filter === "function") {
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) =>
|
||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||
),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||
}
|
||||
|
||||
if (typeof filter === "boolean" && filter) {
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: matchSorter(section.items, searchValue),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return matchSorter(items, searchValue);
|
||||
}
|
||||
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) =>
|
||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||
),
|
||||
items: matchSorter(section.items, searchValue, filter),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||
return matchSorter(items, searchValue, filter);
|
||||
}, [searchValue, items]);
|
||||
|
||||
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useRef } from "react";
|
||||
import { SelectItem } from "../primitives/Select";
|
||||
|
||||
export function TimezoneList({ timezones }: { timezones: string[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: timezones.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 28,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="max-h-[calc(min(480px,var(--popover-available-height))-2.35rem)] overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
|
||||
<TimezoneCell
|
||||
key={virtualItem.key}
|
||||
size={virtualItem.size}
|
||||
start={virtualItem.start}
|
||||
timezone={timezones[virtualItem.index]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TimezoneCell({
|
||||
timezone,
|
||||
size,
|
||||
start,
|
||||
}: {
|
||||
timezone: string;
|
||||
size: number;
|
||||
start: number;
|
||||
}) {
|
||||
return (
|
||||
<SelectItem
|
||||
value={timezone}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${size}px`,
|
||||
transform: `translateY(${start}px)`,
|
||||
}}
|
||||
>
|
||||
{timezone}
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
type EditScheduleOptions = {
|
||||
userId: string;
|
||||
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
|
||||
return {
|
||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||
possibleEnvironments,
|
||||
possibleTimezones: getTimezones(),
|
||||
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
|
||||
};
|
||||
}
|
||||
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
|
||||
externalId: true,
|
||||
deduplicationKey: true,
|
||||
userProvidedDeduplicationKey: true,
|
||||
timezone: true,
|
||||
taskIdentifier: true,
|
||||
instances: {
|
||||
select: {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
|
||||
userProvidedDeduplicationKey: boolean;
|
||||
cron: string;
|
||||
cronDescription: string;
|
||||
timezone: string;
|
||||
externalId: string | null;
|
||||
nextRun: Date;
|
||||
lastRun: Date | undefined;
|
||||
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
|
||||
export type ScheduleListAppliedFilters = ScheduleList["filters"];
|
||||
|
||||
export class ScheduleListPresenter extends BasePresenter {
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectId,
|
||||
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
},
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
select: {
|
||||
maximumSchedulesLimit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
//get all possible scheduled tasks
|
||||
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
|
||||
distinct: ["slug"],
|
||||
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
userProvidedDeduplicationKey: true,
|
||||
generatorExpression: true,
|
||||
generatorDescription: true,
|
||||
timezone: true,
|
||||
externalId: true,
|
||||
instances: {
|
||||
select: {
|
||||
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
|
||||
cron: schedule.generatorExpression,
|
||||
cronDescription: schedule.generatorDescription,
|
||||
timezone: schedule.timezone,
|
||||
active: schedule.active,
|
||||
externalId: schedule.externalId,
|
||||
lastRun: latestRun?.createdAt,
|
||||
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression),
|
||||
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
|
||||
environments: schedule.instances.map((instance) => {
|
||||
const environment = project.environments.find((env) => env.id === instance.environmentId);
|
||||
if (!environment) {
|
||||
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
return displayableEnvironment(environment, userId);
|
||||
}),
|
||||
hasFilters,
|
||||
limits: {
|
||||
used: schedulesCount,
|
||||
limit: project.organization.maximumSchedulesLimit,
|
||||
},
|
||||
filters: {
|
||||
tasks,
|
||||
environments,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TaskTriggerSource,
|
||||
} from "@trigger.dev/database";
|
||||
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
type TestTaskOptions = {
|
||||
@@ -37,6 +38,7 @@ export type TestTask =
|
||||
| {
|
||||
triggerSource: "SCHEDULED";
|
||||
task: Task;
|
||||
possibleTimezones: string[];
|
||||
runs: ScheduledRun[];
|
||||
};
|
||||
|
||||
@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
|
||||
timestamp: Date;
|
||||
lastTimestamp?: Date;
|
||||
externalId?: string;
|
||||
timezone: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -168,9 +171,11 @@ export class TestTaskPresenter {
|
||||
),
|
||||
};
|
||||
case "SCHEDULED":
|
||||
const possibleTimezones = getTimezones();
|
||||
return {
|
||||
triggerSource: "SCHEDULED",
|
||||
task: taskWithEnvironment,
|
||||
possibleTimezones,
|
||||
runs: (
|
||||
await Promise.all(
|
||||
latestRuns.map(async (r) => {
|
||||
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
|
||||
|
||||
async function getScheduleTaskRunPayload(run: RawRun) {
|
||||
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
|
||||
if (!payload.timezone) {
|
||||
payload.timezone = "UTC";
|
||||
}
|
||||
const parsed = ScheduledTaskPayload.safeParse(payload);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ScheduleObject } from "@trigger.dev/core/v3";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
|
||||
import { RunListPresenter } from "./RunListPresenter.server";
|
||||
import { ScheduleObject } from "@trigger.dev/core/v3";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
|
||||
type ViewScheduleOptions = {
|
||||
userId?: string;
|
||||
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
|
||||
friendlyId: true,
|
||||
generatorExpression: true,
|
||||
generatorDescription: true,
|
||||
timezone: true,
|
||||
externalId: true,
|
||||
deduplicationKey: true,
|
||||
userProvidedDeduplicationKey: true,
|
||||
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
|
||||
}
|
||||
|
||||
const nextRuns = schedule.active
|
||||
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5)
|
||||
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
|
||||
: [];
|
||||
|
||||
const runPresenter = new RunListPresenter(this.#prismaClient);
|
||||
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
|
||||
return {
|
||||
schedule: {
|
||||
...schedule,
|
||||
timezone: schedule.timezone,
|
||||
cron: schedule.generatorExpression,
|
||||
cronDescription: schedule.generatorDescription,
|
||||
nextRuns,
|
||||
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
|
||||
expression: result.schedule.cron,
|
||||
description: result.schedule.cronDescription,
|
||||
},
|
||||
timezone: result.schedule.timezone,
|
||||
externalId: result.schedule.externalId ?? undefined,
|
||||
deduplicationKey: result.schedule.userProvidedDeduplicationKey
|
||||
? result.schedule.deduplicationKey ?? undefined
|
||||
|
||||
+9
-5
@@ -185,7 +185,8 @@ export default function Page() {
|
||||
const location = useLocation();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
|
||||
const isUtc = schedule.timezone === "UTC";
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||
@@ -210,6 +211,7 @@ export default function Page() {
|
||||
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Timezone">{schedule.timezone}</Property>
|
||||
<Property label="Environments">
|
||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||
</Property>
|
||||
@@ -245,19 +247,21 @@ export default function Page() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
<TableHeaderCell>Local time</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{schedule.nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
{!isUtc && (
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone={schedule.timezone} />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DateTime date={run} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
||||
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -29,6 +29,7 @@ export default function Page() {
|
||||
schedule={schedule}
|
||||
possibleTasks={possibleTasks}
|
||||
possibleEnvironments={possibleEnvironments}
|
||||
possibleTimezones={possibleTimezones}
|
||||
showGenerateField={showGenerateField}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-1
@@ -20,7 +20,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
||||
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -29,6 +29,7 @@ export default function Page() {
|
||||
possibleTasks={possibleTasks}
|
||||
possibleEnvironments={possibleEnvironments}
|
||||
showGenerateField={showGenerateField}
|
||||
possibleTimezones={possibleTimezones}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+82
-22
@@ -4,12 +4,21 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, 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,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -78,6 +87,7 @@ export default function Page() {
|
||||
possibleEnvironments,
|
||||
hasFilters,
|
||||
filters,
|
||||
limits,
|
||||
currentPage,
|
||||
totalPages,
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
@@ -107,15 +117,43 @@ export default function Page() {
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
LeadingIcon={PlusIcon}
|
||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</LinkButton>
|
||||
{limits.used >= limits.limit ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
LeadingIcon={PlusIcon}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>You've exceeded your limit</DialogHeader>
|
||||
<DialogDescription>
|
||||
You've used {limits.used}/{limits.limit} of your schedules. You can request more
|
||||
schedules.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Feedback
|
||||
button={<Button variant="primary/medium">Request more</Button>}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<LinkButton
|
||||
LeadingIcon={PlusIcon}
|
||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</LinkButton>
|
||||
)}
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
@@ -142,7 +180,21 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
|
||||
<div className="mt-2 justify-end">
|
||||
<div className="mt-2 justify-between">
|
||||
<Paragraph variant="extra-small" className="mt-3">
|
||||
<span className={limits.used >= limits.limit ? "text-warning" : ""}>
|
||||
You've used {limits.used}/{limits.limit} of your schedules.
|
||||
</span>{" "}
|
||||
<Feedback
|
||||
button={
|
||||
<button className=" text-secondary transition hover:text-indigo-400">
|
||||
Request more
|
||||
</button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
.
|
||||
</Paragraph>
|
||||
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,12 +288,13 @@ function SchedulesTable({
|
||||
<TableRow>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell>External ID</TableHeaderCell>
|
||||
<TableHeaderCell>CRON</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
|
||||
<TableHeaderCell>External ID</TableHeaderCell>
|
||||
<TableHeaderCell>Timezone</TableHeaderCell>
|
||||
<TableHeaderCell>Next run</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell>Deduplication key</TableHeaderCell>
|
||||
<TableHeaderCell>Next run (UTC)</TableHeaderCell>
|
||||
<TableHeaderCell>Last run (UTC)</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell>Enabled</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -262,6 +315,9 @@ function SchedulesTable({
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.taskIdentifier}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.cron}
|
||||
</TableCell>
|
||||
@@ -269,17 +325,21 @@ function SchedulesTable({
|
||||
{schedule.cronDescription}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
{schedule.timezone}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<DateTime date={schedule.nextRun} timeZone={schedule.timezone} />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.lastRun ? (
|
||||
<DateTime date={schedule.lastRun} timeZone={schedule.timezone} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<DateTime date={schedule.nextRun} timeZone="utc" />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<EnvironmentLabels environments={schedule.environments} size="small" />
|
||||
</TableCell>
|
||||
|
||||
+53
-3
@@ -26,8 +26,10 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { Select } from "~/components/primitives/Select";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import {
|
||||
ScheduledRun,
|
||||
@@ -95,7 +97,13 @@ export default function Page() {
|
||||
return <StandardTaskForm task={result.task} runs={result.runs} />;
|
||||
}
|
||||
case "SCHEDULED": {
|
||||
return <ScheduledTaskForm task={result.task} runs={result.runs} />;
|
||||
return (
|
||||
<ScheduledTaskForm
|
||||
task={result.task}
|
||||
runs={result.runs}
|
||||
possibleTimezones={result.possibleTimezones}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,12 +223,21 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: ScheduledRun[] }) {
|
||||
function ScheduledTaskForm({
|
||||
task,
|
||||
runs,
|
||||
possibleTimezones,
|
||||
}: {
|
||||
task: TestTask["task"];
|
||||
runs: ScheduledRun[];
|
||||
possibleTimezones: string[];
|
||||
}) {
|
||||
const lastSubmission = useActionData();
|
||||
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
|
||||
const [timestampValue, setTimestampValue] = useState<Date | undefined>();
|
||||
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
|
||||
const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
|
||||
const [timezoneValue, setTimezoneValue] = useState<string>("UTC");
|
||||
|
||||
//set initial values
|
||||
useEffect(() => {
|
||||
@@ -233,11 +250,20 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
||||
setTimestampValue(initialRun.payload.timestamp);
|
||||
setLastTimestampValue(initialRun.payload.lastTimestamp);
|
||||
setExternalIdValue(initialRun.payload.externalId);
|
||||
setTimezoneValue(initialRun.payload.timezone);
|
||||
}, [selectedCodeSampleId]);
|
||||
|
||||
const [
|
||||
form,
|
||||
{ timestamp, lastTimestamp, externalId, triggerSource, taskIdentifier, environmentId },
|
||||
{
|
||||
timestamp,
|
||||
lastTimestamp,
|
||||
externalId,
|
||||
triggerSource,
|
||||
taskIdentifier,
|
||||
environmentId,
|
||||
timezone,
|
||||
},
|
||||
] = useForm({
|
||||
id: "test-task-scheduled",
|
||||
// TODO: type this
|
||||
@@ -314,6 +340,30 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
||||
</Hint>
|
||||
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||
<Select
|
||||
{...conform.select(timezone)}
|
||||
placeholder="Select a timezone"
|
||||
defaultValue={timezoneValue}
|
||||
value={timezoneValue}
|
||||
setValue={(e) => {
|
||||
if (Array.isArray(e)) return;
|
||||
setTimezoneValue(e);
|
||||
}}
|
||||
items={possibleTimezones}
|
||||
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => <TimezoneList timezones={matches} />}
|
||||
</Select>
|
||||
<Hint>
|
||||
The Timestamp and Last timestamp are in UTC so this just changes the timezone
|
||||
string that comes through in the payload.
|
||||
</Hint>
|
||||
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label required={false} htmlFor={externalId.id}>
|
||||
External ID
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { LogoIcon } from "~/components/LogoIcon";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
return typedjson({
|
||||
timezones: getTimezones(),
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { timezones } = useTypedLoaderData<typeof loader>();
|
||||
return (
|
||||
<div className="grid grid-rows-[2.5rem,1fr]">
|
||||
<div className="flex items-center border-b border-b-grid-dimmed px-3">
|
||||
<Link to="/">
|
||||
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-8 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Header1 spacing>Supported timezones</Header1>
|
||||
<Paragraph spacing>We support these timezones when creating a schedule.</Paragraph>
|
||||
<ul className="">
|
||||
{timezones.map((timezone) => (
|
||||
<li key={timezone}>{timezone}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -79,9 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
friendlyId: parsedParams.data.scheduleId,
|
||||
taskIdentifier: body.data.task,
|
||||
cron: body.data.cron,
|
||||
timezone: body.data.timezone,
|
||||
environments: [authenticationResult.environment.id],
|
||||
externalId: body.data.externalId,
|
||||
deduplicationKey: body.data.deduplicationKey,
|
||||
};
|
||||
|
||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||
@@ -95,6 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
externalId: schedule.externalId ?? undefined,
|
||||
deduplicationKey: schedule.deduplicationKey,
|
||||
environments: schedule.environments,
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
environments: [authenticationResult.environment.id],
|
||||
externalId: body.data.externalId,
|
||||
deduplicationKey: body.data.deduplicationKey,
|
||||
timezone: body.data.timezone,
|
||||
};
|
||||
|
||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
externalId: schedule.externalId ?? undefined,
|
||||
deduplicationKey: schedule.deduplicationKey,
|
||||
environments: schedule.environments,
|
||||
@@ -111,6 +113,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
deduplicationKey: schedule.userProvidedDeduplicationKey
|
||||
? schedule.deduplicationKey
|
||||
: undefined,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
excludeUtc: z.preprocess((value) => value === "true", z.boolean()).default(false),
|
||||
});
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
}
|
||||
|
||||
const rawSearchParams = new URL(request.url).searchParams;
|
||||
const params = SearchParamsSchema.safeParse(Object.fromEntries(rawSearchParams.entries()));
|
||||
|
||||
if (!params.success) {
|
||||
return apiCors(
|
||||
request,
|
||||
json({ error: "Invalid request parameters", issues: params.error.issues }, { status: 400 })
|
||||
);
|
||||
}
|
||||
|
||||
const timezones = getTimezones(!params.data.excludeUtc);
|
||||
return apiCors(request, json({ timezones }));
|
||||
}
|
||||
+53
-15
@@ -3,9 +3,10 @@ import { parse } from "@conform-to/zod";
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { parseExpression } from "cron-parser";
|
||||
import cronstrue from "cronstrue";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
environmentTextClassName,
|
||||
environmentTitle,
|
||||
@@ -42,6 +43,7 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
|
||||
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
|
||||
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
|
||||
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
|
||||
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||
|
||||
const cronFormat = `* * * * *
|
||||
┬ ┬ ┬ ┬ ┬
|
||||
@@ -117,9 +119,12 @@ export function UpsertScheduleForm({
|
||||
schedule,
|
||||
possibleTasks,
|
||||
possibleEnvironments,
|
||||
possibleTimezones,
|
||||
showGenerateField,
|
||||
}: EditableScheduleElements & { showGenerateField: boolean }) {
|
||||
const lastSubmission = useActionData();
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
|
||||
const isUtc = selectedTimezone === "UTC";
|
||||
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
@@ -127,18 +132,20 @@ export function UpsertScheduleForm({
|
||||
const project = useProject();
|
||||
const location = useLocation();
|
||||
|
||||
const [form, { taskIdentifier, cron, externalId, environments, deduplicationKey }] = useForm({
|
||||
id: "create-schedule",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: UpsertSchedule });
|
||||
},
|
||||
});
|
||||
const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
|
||||
useForm({
|
||||
id: "create-schedule",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: UpsertSchedule });
|
||||
},
|
||||
});
|
||||
|
||||
let cronPatternResult: CronPatternResult | undefined = undefined;
|
||||
let nextRuns: Date[] | undefined = undefined;
|
||||
|
||||
if (cronPattern !== "") {
|
||||
const result = CronPattern.safeParse(cronPattern);
|
||||
|
||||
@@ -149,7 +156,10 @@ export function UpsertScheduleForm({
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
const expression = parseExpression(cronPattern, { utc: true });
|
||||
const expression = parseExpression(
|
||||
cronPattern,
|
||||
isUtc ? { utc: true } : { tz: selectedTimezone }
|
||||
);
|
||||
cronPatternResult = {
|
||||
isValid: true,
|
||||
description: cronstrue.toString(cronPattern),
|
||||
@@ -195,6 +205,7 @@ export function UpsertScheduleForm({
|
||||
items={possibleTasks}
|
||||
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => (
|
||||
<>
|
||||
@@ -241,25 +252,52 @@ export function UpsertScheduleForm({
|
||||
<ValidCronMessage isValid={false} message={cronPatternResult.error} />
|
||||
)}
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||
<Select
|
||||
{...conform.select(timezone)}
|
||||
placeholder="Select a timezone"
|
||||
defaultValue={selectedTimezone}
|
||||
value={selectedTimezone}
|
||||
setValue={(e) => {
|
||||
if (Array.isArray(e)) return;
|
||||
setSelectedTimezone(e);
|
||||
}}
|
||||
items={possibleTimezones}
|
||||
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => <TimezoneList timezones={matches} />}
|
||||
</Select>
|
||||
<Hint>
|
||||
{isUtc
|
||||
? "UTC will not change with daylight savings time."
|
||||
: "This will automatically adjust for daylight savings time."}
|
||||
</Hint>
|
||||
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||
</InputGroup>
|
||||
{nextRuns !== undefined && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Next 5 runs</Header3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
<TableHeaderCell>Local time</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
{!isUtc && (
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone={selectedTimezone} />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DateTime date={run} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -150,6 +150,7 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
|
||||
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
|
||||
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
|
||||
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
|
||||
"/api/v1/timezones",
|
||||
],
|
||||
log: {
|
||||
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function getTimezones(includeUtc = true) {
|
||||
const possibleTimezones = Intl.supportedValuesOf("timeZone").sort();
|
||||
if (includeUtc) {
|
||||
possibleTimezones.unshift("UTC");
|
||||
}
|
||||
return possibleTimezones;
|
||||
}
|
||||
@@ -55,6 +55,7 @@ export const UpsertSchedule = z.object({
|
||||
),
|
||||
externalId: z.string().optional(),
|
||||
deduplicationKey: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpsertSchedule = z.infer<typeof UpsertSchedule>;
|
||||
|
||||
@@ -20,6 +20,7 @@ export class RegisterNextTaskScheduleInstanceService extends BaseService {
|
||||
|
||||
const nextScheduledTimestamp = calculateNextScheduledTimestamp(
|
||||
instance.taskSchedule.generatorExpression,
|
||||
instance.taskSchedule.timezone,
|
||||
instance.lastScheduledTimestamp ?? new Date()
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export class TestTaskService extends BaseService {
|
||||
scheduleId: "sched_1234",
|
||||
timestamp: data.timestamp,
|
||||
lastTimestamp: data.lastTimestamp,
|
||||
timezone: data.timezone,
|
||||
externalId: data.externalId,
|
||||
upcoming: [],
|
||||
};
|
||||
|
||||
@@ -96,8 +96,10 @@ export class TriggerScheduledTaskService extends BaseService {
|
||||
timestamp: instance.nextScheduledTimestamp,
|
||||
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
|
||||
externalId: instance.taskSchedule.externalId ?? undefined,
|
||||
timezone: instance.taskSchedule.timezone,
|
||||
upcoming: nextScheduledTimestamps(
|
||||
instance.taskSchedule.generatorExpression,
|
||||
instance.taskSchedule.timezone,
|
||||
instance.nextScheduledTimestamp!,
|
||||
10
|
||||
),
|
||||
|
||||
@@ -8,6 +8,8 @@ import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
||||
import cronstrue from "cronstrue";
|
||||
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
|
||||
|
||||
@@ -62,6 +64,48 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
);
|
||||
}
|
||||
|
||||
//if creating a schedule, check they're under the limits
|
||||
if (!schedule.friendlyId) {
|
||||
//check they're within their limit
|
||||
const limits = await this._prisma.organization.findFirst({
|
||||
select: {
|
||||
maximumSchedulesLimit: true,
|
||||
},
|
||||
where: {
|
||||
projects: {
|
||||
some: {
|
||||
id: projectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!limits) {
|
||||
throw new ServiceValidationError("Organization not found");
|
||||
}
|
||||
|
||||
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (schedulesCount >= limits.maximumSchedulesLimit) {
|
||||
throw new ServiceValidationError(
|
||||
`You have created ${schedulesCount}/${limits.maximumSchedulesLimit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule.timezone) {
|
||||
const possibleTimezones = getTimezones();
|
||||
if (!possibleTimezones.includes(schedule.timezone)) {
|
||||
throw new ServiceValidationError(
|
||||
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await $transaction(this._prisma, async (tx) => {
|
||||
const deduplicationKey =
|
||||
typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== ""
|
||||
@@ -115,6 +159,7 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
options.deduplicationKey !== undefined && options.deduplicationKey !== "",
|
||||
generatorExpression: options.cron,
|
||||
generatorDescription: cronstrue.toString(options.cron),
|
||||
timezone: options.timezone ?? "UTC",
|
||||
externalId: options.externalId ? options.externalId : undefined,
|
||||
},
|
||||
});
|
||||
@@ -164,6 +209,7 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
data: {
|
||||
generatorExpression: options.cron,
|
||||
generatorDescription: cronstrue.toString(options.cron),
|
||||
timezone: options.timezone ?? "UTC",
|
||||
externalId: options.externalId ? options.externalId : null,
|
||||
},
|
||||
});
|
||||
@@ -280,7 +326,11 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
: undefined,
|
||||
cron: taskSchedule.generatorExpression,
|
||||
cronDescription: taskSchedule.generatorDescription,
|
||||
nextRun: calculateNextScheduledTimestamp(taskSchedule.generatorExpression),
|
||||
timezone: taskSchedule.timezone,
|
||||
nextRun: calculateNextScheduledTimestamp(
|
||||
taskSchedule.generatorExpression,
|
||||
taskSchedule.timezone
|
||||
),
|
||||
environments: instances.map((instance) => ({
|
||||
id: instance.environment.id,
|
||||
shortcode: instance.environment.shortcode,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const TestTaskData = z
|
||||
(val) => (val === "" ? undefined : val),
|
||||
z.coerce.date().optional()
|
||||
),
|
||||
timezone: z.string(),
|
||||
externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -2,21 +2,23 @@ import { parseExpression } from "cron-parser";
|
||||
|
||||
export function calculateNextScheduledTimestamp(
|
||||
schedule: string,
|
||||
timezone: string | null,
|
||||
lastScheduledTimestamp: Date = new Date()
|
||||
) {
|
||||
let nextStep = calculateNextStep(schedule, lastScheduledTimestamp);
|
||||
let nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp);
|
||||
|
||||
while (nextStep.getTime() < Date.now()) {
|
||||
nextStep = calculateNextStep(schedule, nextStep);
|
||||
nextStep = calculateNextStep(schedule, timezone, nextStep);
|
||||
}
|
||||
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
function calculateNextStep(schedule: string, currentDate: Date) {
|
||||
function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) {
|
||||
return parseExpression(schedule, {
|
||||
currentDate,
|
||||
utc: true,
|
||||
utc: timezone === null,
|
||||
tz: timezone ?? undefined,
|
||||
})
|
||||
.next()
|
||||
.toDate();
|
||||
@@ -24,6 +26,7 @@ function calculateNextStep(schedule: string, currentDate: Date) {
|
||||
|
||||
export function nextScheduledTimestamps(
|
||||
cron: string,
|
||||
timezone: string | null,
|
||||
lastScheduledTimestamp: Date,
|
||||
count: number = 1
|
||||
) {
|
||||
@@ -31,7 +34,11 @@ export function nextScheduledTimestamps(
|
||||
let nextScheduledTimestamp = lastScheduledTimestamp;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
nextScheduledTimestamp = calculateNextScheduledTimestamp(cron, nextScheduledTimestamp);
|
||||
nextScheduledTimestamp = calculateNextScheduledTimestamp(
|
||||
cron,
|
||||
timezone,
|
||||
nextScheduledTimestamp
|
||||
);
|
||||
|
||||
result.push(nextScheduledTimestamp);
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare namespace Intl {
|
||||
type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit";
|
||||
|
||||
function supportedValuesOf(input: Key): string[];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"exclude": ["./cypress", "./cypress.config.ts"],
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Accordion title="How to increase these limits?">
|
||||
These are soft-limits and can be increased. Before we introduce paid plans in July you can request
|
||||
more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact).
|
||||
If you increase these defaults you may have to subscribe to a paid plan when we introduce them.
|
||||
For more details on the v3 Cloud pricing see the [pricing
|
||||
details](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
|
||||
</Accordion>
|
||||
+2
-1
@@ -190,7 +190,8 @@
|
||||
"v3/management/schedules/update",
|
||||
"v3/management/schedules/delete",
|
||||
"v3/management/schedules/deactivate",
|
||||
"v3/management/schedules/activate"
|
||||
"v3/management/schedules/activate",
|
||||
"v3/management/schedules/timezones"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+506
-16
@@ -5,7 +5,7 @@ info:
|
||||
description: "The REST API lets you trigger and manage runs on Trigger.dev. You
|
||||
can trigger a run, get the status of a run, and get the results of a run. "
|
||||
version: 2024-04
|
||||
license:
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
servers:
|
||||
@@ -48,6 +48,8 @@ paths:
|
||||
const schedule = await schedules.create({
|
||||
task: 'my-task',
|
||||
cron: '0 0 * * *'
|
||||
deduplicationKey: 'my-schedule',
|
||||
timezone: 'America/New_York'
|
||||
});
|
||||
|
||||
get:
|
||||
@@ -138,7 +140,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/CreateScheduleOptions"
|
||||
"$ref": "#/components/schemas/UpdateScheduleOptions"
|
||||
responses:
|
||||
"200":
|
||||
description: Schedule updated successfully
|
||||
@@ -265,6 +267,34 @@ paths:
|
||||
|
||||
const schedule = await schedules.activate(scheduleId);
|
||||
|
||||
"/api/v1/timezones":
|
||||
get:
|
||||
operationId: get_timezones_v1
|
||||
summary: Get all supported timezones
|
||||
description: Get all supported timezones that schedule tasks support.
|
||||
parameters:
|
||||
- in: query
|
||||
name: excludeUtc
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
description: Defaults to false. Whether to include UTC in the results or not.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/GetTimezonesResult"
|
||||
tags:
|
||||
- schedules
|
||||
x-codeSamples:
|
||||
- lang: typescript
|
||||
source: |-
|
||||
import { schedules } from "@trigger.dev/sdk/v3";
|
||||
|
||||
const { timezones } = await schedules.timezones();
|
||||
|
||||
"/api/v1/runs/{runId}/replay":
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/runId"
|
||||
@@ -401,7 +431,7 @@ paths:
|
||||
operationId: retrieve_run_v1
|
||||
summary: Retrieve a run
|
||||
description: |
|
||||
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
|
||||
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
|
||||
responses:
|
||||
"200":
|
||||
description: Successful request
|
||||
@@ -619,8 +649,6 @@ paths:
|
||||
for (const run of response.data) {
|
||||
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
"/api/v1/projects/{projectRef}/envvars/{env}":
|
||||
parameters:
|
||||
@@ -792,7 +820,7 @@ paths:
|
||||
format: binary
|
||||
override:
|
||||
type: boolean
|
||||
required:
|
||||
required:
|
||||
- variables
|
||||
responses:
|
||||
"200":
|
||||
@@ -851,7 +879,7 @@ paths:
|
||||
source: |-
|
||||
import { envvars } from "@trigger.dev/sdk/v3";
|
||||
import { createReadStream } from "node:fs";
|
||||
|
||||
|
||||
// Import variables in dotenv format from a file
|
||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||
variables: createReadStream(".env"),
|
||||
@@ -861,7 +889,7 @@ paths:
|
||||
label: Import variables from a response
|
||||
source: |-
|
||||
import { envvars } from "@trigger.dev/sdk/v3";
|
||||
|
||||
|
||||
// Import variables in dotenv format from a response
|
||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||
variables: await fetch("https://example.com/.env"),
|
||||
@@ -871,7 +899,7 @@ paths:
|
||||
label: Import variables from a Buffer
|
||||
source: |-
|
||||
import { envvars } from "@trigger.dev/sdk/v3";
|
||||
|
||||
|
||||
// Import variables in dotenv format from a buffer
|
||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
|
||||
@@ -881,14 +909,13 @@ paths:
|
||||
label: Import variables from a File
|
||||
source: |-
|
||||
import { envvars } from "@trigger.dev/sdk/v3";
|
||||
|
||||
|
||||
// Import variables in dotenv format from a file
|
||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
|
||||
override: false
|
||||
});
|
||||
|
||||
|
||||
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/projectRef"
|
||||
@@ -951,7 +978,7 @@ paths:
|
||||
console.log(`Value: ${variable.value}`);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
delete:
|
||||
operationId: delete_project_envvar_v1
|
||||
summary: Delete environment variable
|
||||
@@ -1068,7 +1095,6 @@ paths:
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
components:
|
||||
parameters:
|
||||
@@ -1471,11 +1497,11 @@ components:
|
||||
payload:
|
||||
type: object
|
||||
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
|
||||
example: {"foo": "bar"}
|
||||
example: { "foo": "bar" }
|
||||
output:
|
||||
type: object
|
||||
description: The output of the run. Will be omitted if the request was made with a Public API key
|
||||
example: {"foo": "bar"}
|
||||
example: { "foo": "bar" }
|
||||
idempotencyKey:
|
||||
type: string
|
||||
description: The idempotency key used to prevent creating duplicate runs, if provided
|
||||
@@ -1580,6 +1606,27 @@ components:
|
||||
type: string
|
||||
externalId:
|
||||
type: string
|
||||
timezone:
|
||||
type: string
|
||||
example: "America/New_York"
|
||||
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||
required:
|
||||
- task
|
||||
- cron
|
||||
- deduplicationKey
|
||||
UpdateScheduleOptions:
|
||||
type: object
|
||||
properties:
|
||||
task:
|
||||
type: string
|
||||
cron:
|
||||
type: string
|
||||
externalId:
|
||||
type: string
|
||||
timezone:
|
||||
type: string
|
||||
example: "America/New_York"
|
||||
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||
required:
|
||||
- task
|
||||
- cron
|
||||
@@ -1623,6 +1670,10 @@ components:
|
||||
type: string
|
||||
description: The description of the generator in plain english
|
||||
example: Every day at midnight
|
||||
timezone:
|
||||
type: string
|
||||
example: "America/New_York"
|
||||
description: Defaults to UTC. In IANA format, if set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||
nextRun:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -1648,6 +1699,445 @@ components:
|
||||
type: integer
|
||||
count:
|
||||
type: integer
|
||||
GetTimezonesResult:
|
||||
type: object
|
||||
properties:
|
||||
timezones:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"UTC",
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
"Africa/Asmera",
|
||||
"Africa/Bamako",
|
||||
"Africa/Bangui",
|
||||
"Africa/Banjul",
|
||||
"Africa/Bissau",
|
||||
"Africa/Blantyre",
|
||||
"Africa/Brazzaville",
|
||||
"Africa/Bujumbura",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/Conakry",
|
||||
"Africa/Dakar",
|
||||
"Africa/Dar_es_Salaam",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Douala",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Freetown",
|
||||
"Africa/Gaborone",
|
||||
"Africa/Harare",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Kampala",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Kigali",
|
||||
"Africa/Kinshasa",
|
||||
"Africa/Lagos",
|
||||
"Africa/Libreville",
|
||||
"Africa/Lome",
|
||||
"Africa/Luanda",
|
||||
"Africa/Lubumbashi",
|
||||
"Africa/Lusaka",
|
||||
"Africa/Malabo",
|
||||
"Africa/Maputo",
|
||||
"Africa/Maseru",
|
||||
"Africa/Mbabane",
|
||||
"Africa/Mogadishu",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Niamey",
|
||||
"Africa/Nouakchott",
|
||||
"Africa/Ouagadougou",
|
||||
"Africa/Porto-Novo",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Anguilla",
|
||||
"America/Antigua",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Aruba",
|
||||
"America/Asuncion",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Buenos_Aires",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Catamarca",
|
||||
"America/Cayenne",
|
||||
"America/Cayman",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Ciudad_Juarez",
|
||||
"America/Coral_Harbour",
|
||||
"America/Cordoba",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Dominica",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Grenada",
|
||||
"America/Guadeloupe",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indianapolis",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Jujuy",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Kralendijk",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Louisville",
|
||||
"America/Lower_Princes",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Marigot",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Mendoza",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Montserrat",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nipigon",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Pangnirtung",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rainy_River",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Santa_Isabel",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Sitka",
|
||||
"America/St_Barthelemy",
|
||||
"America/St_Johns",
|
||||
"America/St_Kitts",
|
||||
"America/St_Lucia",
|
||||
"America/St_Thomas",
|
||||
"America/St_Vincent",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Thunder_Bay",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Tortola",
|
||||
"America/Vancouver",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"America/Yellowknife",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/McMurdo",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Arctic/Longyearbyen",
|
||||
"Asia/Aden",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Chita",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Colombo",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Hebron",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Katmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Muscat",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Phnom_Penh",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qostanay",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Rangoon",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Saigon",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vientiane",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faeroe",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/St_Helena",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Currie",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Perth",
|
||||
"Australia/Sydney",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Athens",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Bratislava",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Busingen",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Guernsey",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Isle_of_Man",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Jersey",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Lisbon",
|
||||
"Europe/Ljubljana",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Mariehamn",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Podgorica",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/San_Marino",
|
||||
"Europe/Sarajevo",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Skopje",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Vaduz",
|
||||
"Europe/Vatican",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zagreb",
|
||||
"Europe/Zaporozhye",
|
||||
"Europe/Zurich",
|
||||
"Indian/Antananarivo",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Comoro",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Johnston",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Midway",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Ponape",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Saipan",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Truk",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis",
|
||||
]
|
||||
ScheduleEnvironment:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1659,7 +2149,7 @@ components:
|
||||
type: string
|
||||
SerializedError:
|
||||
type: object
|
||||
required:
|
||||
required:
|
||||
- message
|
||||
properties:
|
||||
message:
|
||||
|
||||
+16
-2
@@ -7,13 +7,13 @@ description: "There are some hard and soft limits in v3 that you might hit."
|
||||
|
||||
These are the default limits on a free account.
|
||||
|
||||
Before we introduce paid plans in July you can request more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact). If you increase these defaults you may have to subscribe to a paid plan when we introduce them. For more details on the v3 Cloud pricing see the [pricing page](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
|
||||
|
||||
| Limit | Details |
|
||||
| ------------ | ------------------ |
|
||||
| Organization | 10 concurrent runs |
|
||||
| Environment | 5 concurrent runs |
|
||||
|
||||
<Snippet file="v3/soft-limit.mdx" />
|
||||
|
||||
## Rate limits
|
||||
|
||||
| Limit | Details |
|
||||
@@ -23,3 +23,17 @@ Before we introduce paid plans in July you can request more [on Discord](https:/
|
||||
Generally speaking each SDK call is an API call.
|
||||
|
||||
The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call.
|
||||
|
||||
## Schedules
|
||||
|
||||
| Limit | Details |
|
||||
| --------- | ------------- |
|
||||
| Schedules | 5 per project |
|
||||
|
||||
When attaching schedules to tasks we strongly recommend you add them in our dashboard if they're "static". That way you can control them easily per environment.
|
||||
|
||||
If you add them dynamically using code make sure you add a `deduplicationKey` so you don't add the same schedule to a task multiple times. If you don't your task will get triggered multiple times, it will cost you more, and you will hit the limit.
|
||||
|
||||
If you're creating schedules for your user you will definitely need to request more schedules from us.
|
||||
|
||||
<Snippet file="v3/soft-limit.mdx" />
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get timezones"
|
||||
openapi: "v3-openapi GET /api/v1/timezones"
|
||||
---
|
||||
+42
-11
@@ -29,6 +29,16 @@ export const firstScheduledTask = schedules.task({
|
||||
//this can be undefined if it's never been run
|
||||
console.log(payload.lastTimestamp); //is a Date object or undefined
|
||||
|
||||
//the timezone the schedule was registered with, defaults to "UTC"
|
||||
//this is in IANA format, e.g. "America/New_York"
|
||||
//See the full list here: https://cloud.trigger.dev/timezones
|
||||
console.log(payload.timezone); //is a string
|
||||
|
||||
//If you want to output the time in the user's timezone do this:
|
||||
const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||
timeZone: payload.timezone,
|
||||
});
|
||||
|
||||
//the schedule id (you can have many schedules for the same task)
|
||||
//using this you can remove the schedule, update it, etc
|
||||
console.log(payload.scheduleId); //is a string
|
||||
@@ -46,8 +56,9 @@ export const firstScheduledTask = schedules.task({
|
||||
|
||||
You can see from the comments that the payload has several useful properties:
|
||||
|
||||
- `timestamp` - the time the task was scheduled to run
|
||||
- `lastTimestamp` - the time the task was last run
|
||||
- `timestamp` - the time the task was scheduled to run, as a UTC date.
|
||||
- `lastTimestamp` - the time the task was last run, as a UTC date.
|
||||
- `timezone` - the timezone the schedule was registered with, defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||
- `scheduleId` - the id of the schedule that triggered the task
|
||||
- `externalId` - the external id you (optionally) provided when creating the schedule
|
||||
- `upcoming` - the next 5 times the task is scheduled to run
|
||||
@@ -103,6 +114,7 @@ These are the options when creating a schedule:
|
||||
| ----------------- | --------------------------------------------------------------------------------------------- |
|
||||
| Task | The id of the task you want to attach to. |
|
||||
| CRON pattern | The schedule in CRON format. |
|
||||
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
|
||||
| External id | An optional external id, usually you'd use a userId. |
|
||||
| Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. |
|
||||
| Environments | The environments this schedule will run in. |
|
||||
@@ -121,6 +133,8 @@ const createdSchedule = await schedules.create({
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in CRON format.
|
||||
cron: "0 0 * * *",
|
||||
//this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
|
||||
deduplicationKey: "my-deduplication-key",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -136,17 +150,21 @@ const createdSchedule = await schedules.create({
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in CRON format.
|
||||
cron: "0 0 * * *",
|
||||
// Optional, it defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||
// In this case, the task will run at midnight every day in New York time.
|
||||
// If you specify a timezone it will automatically work with daylight saving time.
|
||||
timezone: "America/New_York",
|
||||
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
||||
//This allows you to have per-user CRON tasks.
|
||||
externalId: "user_123456",
|
||||
//(Optional) You can only create one schedule with this key.
|
||||
//You can only create one schedule with this key.
|
||||
//If you use it twice, the second call will update the schedule.
|
||||
//This is useful if you don't want to create duplicate schedules for a user.
|
||||
//This is useful because you don't want to create duplicate schedules for a user.
|
||||
deduplicationKey: "user_123456-todo_reminder",
|
||||
});
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-create-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/create) for full details.
|
||||
|
||||
### Dynamic schedules (or multi-tenant schedules)
|
||||
|
||||
@@ -189,6 +207,8 @@ export async function POST(request: Request) {
|
||||
task: reminderTask.id,
|
||||
//8am every day
|
||||
cron: "0 8 * * *",
|
||||
//the user's timezone
|
||||
timezone: data.timezone,
|
||||
//the user id
|
||||
externalId: data.userId,
|
||||
//this makes it impossible to have two reminder schedules for the same user
|
||||
@@ -228,7 +248,7 @@ You can test a scheduled task in the dashboard. Note that the `scheduleId` will
|
||||
const retrievedSchedule = await schedules.retrieve(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-retrieve-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/retrieve) for full details.
|
||||
|
||||
### Listing schedules
|
||||
|
||||
@@ -236,7 +256,7 @@ See [the SDK reference](/v3/management-retrieve-schedule) for full details.
|
||||
const allSchedules = await schedules.list();
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-list-schedules) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/list) for full details.
|
||||
|
||||
### Updating a schedule
|
||||
|
||||
@@ -245,10 +265,11 @@ const updatedSchedule = await schedules.update(scheduleId, {
|
||||
task: firstScheduledTask.id,
|
||||
cron: "0 0 1 * *",
|
||||
externalId: "ext_1234444",
|
||||
deduplicationKey: "my-deduplication-key",
|
||||
});
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-update-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/update) for full details.
|
||||
|
||||
### Deactivating a schedule
|
||||
|
||||
@@ -256,7 +277,7 @@ See [the SDK reference](/v3/management-update-schedule) for full details.
|
||||
const deactivatedSchedule = await schedules.deactivate(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-deactivate-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/deactivate) for full details.
|
||||
|
||||
### Activating a schedule
|
||||
|
||||
@@ -264,7 +285,7 @@ See [the SDK reference](/v3/management-deactivate-schedule) for full details.
|
||||
const activatedSchedule = await schedules.activate(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-activate-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/activate) for full details.
|
||||
|
||||
### Deleting a schedule
|
||||
|
||||
@@ -272,4 +293,14 @@ See [the SDK reference](/v3/management-activate-schedule) for full details.
|
||||
const deletedSchedule = await schedules.del(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management-delete-schedule) for full details.
|
||||
See [the SDK reference](/v3/management/schedules/delete) for full details.
|
||||
|
||||
### Getting possible timezones
|
||||
|
||||
You might want to show a dropdown menu in your UI so your users can select their timezone. You can get a list of all possible timezones using the SDK:
|
||||
|
||||
```ts
|
||||
const timezones = await schedules.timezones();
|
||||
```
|
||||
|
||||
See [the SDK reference](/v3/management/schedules/timezones) for full details.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from "./apiClient";
|
||||
export * from "./apiClient/types";
|
||||
export * from "./apiClient/pagination";
|
||||
export type { ApiPromise } from "./apiClient/core";
|
||||
export type { ApiPromise, OffsetLimitPagePromise, CursorPagePromise } from "./apiClient/core";
|
||||
export * from "./apiClient/errors";
|
||||
export * from "./clock-api";
|
||||
export * from "./errors";
|
||||
|
||||
@@ -222,15 +222,26 @@ export const ScheduledTaskPayload = z.object({
|
||||
You can use this to remove the schedule, update it, etc */
|
||||
scheduleId: z.string(),
|
||||
/** When the task was scheduled to run.
|
||||
* Note this will be slightly different from `new Date()` because it takes a few ms to run the task. */
|
||||
* Note this will be slightly different from `new Date()` because it takes a few ms to run the task.
|
||||
*
|
||||
* This date is UTC. To output it as a string with a timezone you would do this:
|
||||
* ```ts
|
||||
* const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||
timeZone: payload.timezone,
|
||||
});
|
||||
``` */
|
||||
timestamp: z.date(),
|
||||
/** When the task was last run (it has been).
|
||||
This can be undefined if it's never been run */
|
||||
This can be undefined if it's never been run. This date is UTC. */
|
||||
lastTimestamp: z.date().optional(),
|
||||
/** You can optionally provide an external id when creating the schedule.
|
||||
Usually you would use a userId or some other unique identifier.
|
||||
This defaults to undefined if you didn't provide one. */
|
||||
externalId: z.string().optional(),
|
||||
/** The IANA timezone the schedule is set to. The default is UTC.
|
||||
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
|
||||
*/
|
||||
timezone: z.string(),
|
||||
/** The next 5 dates this task is scheduled to run */
|
||||
upcoming: z.array(z.date()),
|
||||
});
|
||||
@@ -257,20 +268,31 @@ export const CreateScheduleOptions = z.object({
|
||||
|
||||
*/
|
||||
cron: z.string(),
|
||||
/** (Optional) You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
|
||||
/** You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
|
||||
*
|
||||
* This is useful if you don't want to create duplicate schedules for a user. */
|
||||
deduplicationKey: z.string().optional(),
|
||||
* This is required to prevent you from creating duplicate schedules. */
|
||||
deduplicationKey: z.string(),
|
||||
/** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
||||
*
|
||||
* This allows you to have per-user CRON tasks.
|
||||
*/
|
||||
externalId: z.string().optional(),
|
||||
/** Optionally, you can specify a timezone in the IANA format. If unset it will use UTC.
|
||||
* If specified then the CRON will be evaluated in that timezone and will respect daylight savings.
|
||||
*
|
||||
* If you set the CRON to `0 0 * * *` and the timezone to `America/New_York` then the task will run at midnight in New York time, no matter whether it's daylight savings or not.
|
||||
*
|
||||
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
|
||||
*
|
||||
* @example "America/New_York", "Europe/London", "Asia/Tokyo", "Africa/Cairo"
|
||||
*
|
||||
*/
|
||||
timezone: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>;
|
||||
|
||||
export const UpdateScheduleOptions = CreateScheduleOptions;
|
||||
export const UpdateScheduleOptions = CreateScheduleOptions.omit({ deduplicationKey: true });
|
||||
|
||||
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
|
||||
|
||||
@@ -289,6 +311,7 @@ export const ScheduleObject = z.object({
|
||||
deduplicationKey: z.string().nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
generator: ScheduleGenerator,
|
||||
timezone: z.string(),
|
||||
nextRun: z.coerce.date().nullish(),
|
||||
environments: z.array(
|
||||
z.object({
|
||||
@@ -325,6 +348,12 @@ export const ListScheduleOptions = z.object({
|
||||
|
||||
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
|
||||
|
||||
export const TimezonesResult = z.object({
|
||||
timezones: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type TimezonesResult = z.infer<typeof TimezonesResult>;
|
||||
|
||||
export const RunStatus = z.enum([
|
||||
/// Task hasn't been deployed yet but is waiting to be executed
|
||||
"WAITING_FOR_DEPLOY",
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Organization" ADD COLUMN "maximumScheduleInstancesLimit" INTEGER NOT NULL DEFAULT 20;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `maximumScheduleInstancesLimit` on the `Organization` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Organization" DROP COLUMN "maximumScheduleInstancesLimit",
|
||||
ADD COLUMN "maximumSchedulesLimit" INTEGER NOT NULL DEFAULT 5;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `timezone` on the `TaskSchedule` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "TaskSchedule" DROP COLUMN "timezone";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';
|
||||
@@ -111,6 +111,7 @@ model Organization {
|
||||
|
||||
maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes
|
||||
maximumConcurrencyLimit Int @default(10)
|
||||
maximumSchedulesLimit Int @default(5)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -2208,8 +2209,12 @@ model TaskSchedule {
|
||||
generatorExpression String
|
||||
generatorDescription String @default("")
|
||||
generatorType ScheduleGeneratorType @default(CRON)
|
||||
|
||||
/// These are IANA format string, or the default "UTC". E.g. "America/New_York"
|
||||
timezone String @default("UTC")
|
||||
|
||||
///Can be provided by the user then accessed inside a run
|
||||
externalId String?
|
||||
externalId String?
|
||||
|
||||
///Instances of the schedule that are active
|
||||
instances TaskScheduleInstance[]
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
ReplayRunResponse,
|
||||
RetrieveRunResponse,
|
||||
apiClientManager,
|
||||
CursorPagePromise,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3";
|
||||
import { apiClientMissingError } from "./shared";
|
||||
import { CursorPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
||||
|
||||
export type RetrieveRunResult = RetrieveRunResponse;
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@ import {
|
||||
ApiPromise,
|
||||
DeletedScheduleObject,
|
||||
InitOutput,
|
||||
OffsetLimitPagePromise,
|
||||
ScheduleObject,
|
||||
TimezonesResult,
|
||||
apiClientManager,
|
||||
taskCatalog,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
|
||||
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
|
||||
import * as SchedulesAPI from "./api";
|
||||
import { OffsetLimitPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
||||
|
||||
export function task<TOutput, TInitOutput extends InitOutput>(
|
||||
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
|
||||
@@ -27,6 +29,7 @@ export function task<TOutput, TInitOutput extends InitOutput>(
|
||||
* @param options
|
||||
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
||||
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
||||
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
|
||||
* @param options.externalId - An optional external identifier for the schedule
|
||||
* @param options.deduplicationKey - An optional deduplication key for the schedule
|
||||
* @returns The created schedule
|
||||
@@ -62,6 +65,7 @@ export function retrieve(scheduleId: string): ApiPromise<ScheduleObject> {
|
||||
* @param options - The updated schedule options
|
||||
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
||||
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
||||
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
|
||||
* @param options.externalId - An optional external identifier for the schedule
|
||||
* @returns The updated schedule
|
||||
*/
|
||||
@@ -138,3 +142,26 @@ export function list(
|
||||
|
||||
return apiClient.listSchedules(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the possible timezones we support
|
||||
* @param excludeUtc - By default "UTC" is included and is first. If true, "UTC" will be excluded.
|
||||
*/
|
||||
export function timezones(options?: { excludeUtc?: boolean }) {
|
||||
const baseUrl = apiClientManager.baseURL;
|
||||
|
||||
if (!baseUrl) {
|
||||
throw apiClientMissingError();
|
||||
}
|
||||
|
||||
return zodfetch(
|
||||
TimezonesResult,
|
||||
`${baseUrl}/api/v1/timezones${options?.excludeUtc === true ? "?excludeUtc=true" : ""}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ async function doTriggerUnfriendlyTaskId() {
|
||||
}
|
||||
|
||||
// doRuns().catch(console.error);
|
||||
// doListRuns().catch(console.error);
|
||||
doListRuns().catch(console.error);
|
||||
// doScheduleLists().catch(console.error);
|
||||
// doSchedules().catch(console.error);
|
||||
// doEnvVars().catch(console.error);
|
||||
doTriggerUnfriendlyTaskId().catch(console.error);
|
||||
// doTriggerUnfriendlyTaskId().catch(console.error);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger, schedules } from "@trigger.dev/sdk/v3";
|
||||
import { logger, schedules, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const firstScheduledTask = schedules.task({
|
||||
id: "first-scheduled-task",
|
||||
@@ -6,6 +6,50 @@ export const firstScheduledTask = schedules.task({
|
||||
const distanceInMs =
|
||||
payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime();
|
||||
|
||||
logger.log(payload.timezone);
|
||||
|
||||
logger.log("First scheduled tasks", { payload, distanceInMs });
|
||||
|
||||
const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||
timeZone: payload.timezone,
|
||||
});
|
||||
|
||||
logger.log(formatted);
|
||||
},
|
||||
});
|
||||
|
||||
export const manageSchedules = task({
|
||||
id: "manage-schedules",
|
||||
run: async (payload) => {
|
||||
const createdSchedule = await schedules.create({
|
||||
//The id of the scheduled task you want to attach to.
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in CRON format.
|
||||
cron: "* * * * *",
|
||||
deduplicationKey: `create-schedule-1718277290717`,
|
||||
timezone: "Asia/Tokyo",
|
||||
});
|
||||
logger.log("Created schedule", createdSchedule);
|
||||
|
||||
const editedSchedule = await schedules.update(createdSchedule.id, {
|
||||
//The id of the scheduled task you want to attach to.
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in CRON format.
|
||||
cron: "* * * * *",
|
||||
timezone: "Europe/Athens",
|
||||
});
|
||||
logger.log("Edited schedule", editedSchedule);
|
||||
|
||||
const sched = await schedules.retrieve(createdSchedule.id);
|
||||
logger.log("Retrieved schedule", sched);
|
||||
|
||||
const allSchedules = await schedules.list();
|
||||
logger.log("All schedules", { allSchedules });
|
||||
|
||||
const { timezones } = await schedules.timezones();
|
||||
logger.log("Timezones", { timezones });
|
||||
|
||||
const withoutUtc = await schedules.timezones({ excludeUtc: true });
|
||||
logger.log("Timezones without UTC", { withoutUtc });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user