diff --git a/cli/tests/unit/cron_test.ts b/cli/tests/unit/cron_test.ts index 79918ed2f2..2a146bcfa5 100644 --- a/cli/tests/unit/cron_test.ts +++ b/cli/tests/unit/cron_test.ts @@ -1,5 +1,9 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertThrows } from "./test_util.ts"; +import { + formatToCronSchedule, + parseScheduleToString, +} from "../../../ext/cron/01_cron.ts"; const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); @@ -170,6 +174,31 @@ Deno.test(async function basicTest() { } }); +Deno.test(async function basicTestWithJsonFormatScheduleExpression() { + Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100"); + + let count = 0; + const { promise, resolve } = Promise.withResolvers(); + const ac = new AbortController(); + const c = Deno.cron( + "abc", + { minute: { every: 20 } }, + { signal: ac.signal }, + () => { + count++; + if (count > 5) { + resolve(); + } + }, + ); + try { + await promise; + } finally { + ac.abort(); + await c; + } +}); + Deno.test(async function multipleCrons() { Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100"); @@ -284,3 +313,67 @@ Deno.test(async function retriesWithBackoffScheduleOldApi() { // The cron should have executed 3 times (1st attempt and 2 retries). assertEquals(count, 3); }); + +Deno.test("formatToCronSchedule - undefined value", () => { + const result = formatToCronSchedule(); + assertEquals(result, "*"); +}); + +Deno.test("formatToCronSchedule - number value", () => { + const result = formatToCronSchedule(5); + assertEquals(result, "5"); +}); + +Deno.test("formatToCronSchedule - exact array value", () => { + const result = formatToCronSchedule({ exact: [1, 2, 3] }); + assertEquals(result, "1,2,3"); +}); + +Deno.test("formatToCronSchedule - exact number value", () => { + const result = formatToCronSchedule({ exact: 5 }); + assertEquals(result, "5"); +}); + +Deno.test("formatToCronSchedule - start, end, every values", () => { + const result = formatToCronSchedule({ start: 1, end: 10, every: 2 }); + assertEquals(result, "1-10/2"); +}); + +Deno.test("formatToCronSchedule - start, end values", () => { + const result = formatToCronSchedule({ start: 1, end: 10 }); + assertEquals(result, "1-10"); +}); + +Deno.test("formatToCronSchedule - start, every values", () => { + const result = formatToCronSchedule({ start: 1, every: 2 }); + assertEquals(result, "1/2"); +}); + +Deno.test("formatToCronSchedule - start value", () => { + const result = formatToCronSchedule({ start: 1 }); + assertEquals(result, "1/1"); +}); + +Deno.test("formatToCronSchedule - end, every values", () => { + assertThrows( + () => formatToCronSchedule({ end: 10, every: 2 }), + TypeError, + "Invalid cron schedule", + ); +}); + +Deno.test("Parse CronSchedule to string", () => { + const result = parseScheduleToString({ + minute: { exact: [1, 2, 3] }, + hour: { start: 1, end: 10, every: 2 }, + dayOfMonth: { exact: 5 }, + month: { start: 1, end: 10 }, + dayOfWeek: { start: 1, every: 2 }, + }); + assertEquals(result, "1,2,3 1-10/2 5 1-10 1/2"); +}); + +Deno.test("Parse schedule to string - string", () => { + const result = parseScheduleToString("* * * * *"); + assertEquals(result, "* * * * *"); +}); diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 5cba27a5e2..895b7c0c5c 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1317,11 +1317,41 @@ declare namespace Deno { */ export function openKv(path?: string): Promise; + /** **UNSTABLE**: New API, yet to be vetted. + * + * CronScheduleExpression is used as the type of `minute`, `hour`, + * `dayOfMonth`, `month`, and `dayOfWeek` in {@linkcode CronSchedule}. + * @category Cron + */ + type CronScheduleExpression = number | { exact: number | number[] } | { + start?: number; + end?: number; + every?: number; + }; + + /** **UNSTABLE**: New API, yet to be vetted. + * + * CronSchedule is the interface used for JSON format + * cron `schedule`. + * @category Cron + */ + export interface CronSchedule { + minute?: CronScheduleExpression; + hour?: CronScheduleExpression; + dayOfMonth?: CronScheduleExpression; + month?: CronScheduleExpression; + dayOfWeek?: CronScheduleExpression; + } + /** **UNSTABLE**: New API, yet to be vetted. * * Create a cron job that will periodically execute the provided handler * callback based on the specified schedule. * + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified + * using UTC time zone. + * * ```ts * Deno.cron("sample cron", "20 * * * *", () => { * console.log("cron job executed"); @@ -1339,7 +1369,7 @@ declare namespace Deno { */ export function cron( name: string, - schedule: string, + schedule: string | CronSchedule, handler: () => Promise | void, options: { backoffSchedule?: number[]; signal?: AbortSignal }, ): Promise; @@ -1355,14 +1385,15 @@ declare namespace Deno { * }); * ``` * - * `schedule` is a Unix cron format expression, where time is specified + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified * using UTC time zone. * * @category Cron */ export function cron( name: string, - schedule: string, + schedule: string | CronSchedule, handler: () => Promise | void, ): Promise; @@ -1379,7 +1410,8 @@ declare namespace Deno { * }); * ``` * - * `schedule` is a Unix cron format expression, where time is specified + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified * using UTC time zone. * * `backoffSchedule` option can be used to specify the retry policy for failed @@ -1392,7 +1424,7 @@ declare namespace Deno { */ export function cron( name: string, - schedule: string, + schedule: string | CronSchedule, options: { backoffSchedule?: number[]; signal?: AbortSignal }, handler: () => Promise | void, ): Promise; diff --git a/ext/cron/01_cron.ts b/ext/cron/01_cron.ts index 30343905c5..4a52506180 100644 --- a/ext/cron/01_cron.ts +++ b/ext/cron/01_cron.ts @@ -3,9 +3,73 @@ // @ts-ignore internal api const core = Deno.core; +export function formatToCronSchedule( + value?: number | { exact: number | number[] } | { + start?: number; + end?: number; + every?: number; + }, +): string { + if (value === undefined) { + return "*"; + } else if (typeof value === "number") { + return value.toString(); + } else { + const { exact } = value as { exact: number | number[] }; + if (exact === undefined) { + const { start, end, every } = value as { + start?: number; + end?: number; + every?: number; + }; + if (start !== undefined && end !== undefined && every !== undefined) { + return start + "-" + end + "/" + every; + } else if (start !== undefined && end !== undefined) { + return start + "-" + end; + } else if (start !== undefined && every !== undefined) { + return start + "/" + every; + } else if (start !== undefined) { + return start + "/1"; + } else if (end === undefined && every !== undefined) { + return "*/" + every; + } else { + throw new TypeError("Invalid cron schedule"); + } + } else { + if (typeof exact === "number") { + return exact.toString(); + } else { + return exact.join(","); + } + } + } +} + +export function parseScheduleToString( + schedule: string | Deno.CronSchedule, +): string { + if (typeof schedule === "string") { + return schedule; + } else { + const { + minute, + hour, + dayOfMonth, + month, + dayOfWeek, + } = schedule; + + return formatToCronSchedule(minute) + + " " + formatToCronSchedule(hour) + + " " + formatToCronSchedule(dayOfMonth) + + " " + formatToCronSchedule(month) + + " " + formatToCronSchedule(dayOfWeek); + } +} + function cron( name: string, - schedule: string, + schedule: string | Deno.CronSchedule, handlerOrOptions1: | (() => Promise | void) | ({ backoffSchedule?: number[]; signal?: AbortSignal }), @@ -20,6 +84,8 @@ function cron( throw new TypeError("Deno.cron requires a valid schedule"); } + schedule = parseScheduleToString(schedule); + let handler: () => Promise | void; let options: { backoffSchedule?: number[]; signal?: AbortSignal } | undefined;