diff --git a/__tests__/CronJob_tests.res b/__tests__/CronJob_tests.res index d21ce9f..f871e02 100644 --- a/__tests__/CronJob_tests.res +++ b/__tests__/CronJob_tests.res @@ -175,4 +175,50 @@ describe("changing CronJobs", () => { expect(LuxonDateTime.toMillis(nextTick))->toEqual(LuxonDateTime.toMillis(nextAssumedTick)) }) + + test("nextDate returns the next single date", () => { + let onTick = _ => () + + RescriptJestDateMock.advanceTo(Date.fromString("2010-01-01T12:00:00.000Z")) + + let job = RescriptCron.CronJob.make(#JsDate(Date.fromString(futureDate)), onTick, ()) + let time = RescriptCron.CronTime.make(#CronString("0 0 12 20 jan *"), ()) + + let nextAssumedTick = + LuxonDateTime.fromISO("2010-01-01T12:00:00.000Z")->LuxonDateTime.set({day: 20, hour: 12}) + + RescriptCron.setTime(job, time) + + let nextTick = RescriptCron.nextDate(job) + + expect(LuxonDateTime.toMillis(nextTick))->toEqual(LuxonDateTime.toMillis(nextAssumedTick)) + }) + + test("isActive reflects job start and stop state", () => { + let onTick = _ => () + + RescriptJestDateMock.advanceTo(Date.fromString(pastDate)) + + let job = RescriptCron.CronJob.make(#JsDate(Date.fromString(futureDate)), onTick, ()) + + expect(RescriptCron.isActive(job))->toEqual(false)->ignore + + RescriptCron.start(job) + expect(RescriptCron.isActive(job))->toEqual(true)->ignore + + RescriptCron.stop(job) + expect(RescriptCron.isActive(job))->toEqual(false) + }) +}) + +describe("validateCronExpression", () => { + test("returns valid for a correct cron expression", () => { + let result = RescriptCron.validateCronExpression("0 0 * * *") + expect(result.valid)->toEqual(true) + }) + + test("returns invalid for a malformed cron expression", () => { + let result = RescriptCron.validateCronExpression("not-a-cron") + expect(result.valid)->toEqual(false) + }) }) diff --git a/src/RescriptCron.res b/src/RescriptCron.res index febfa20..3b03b60 100644 --- a/src/RescriptCron.res +++ b/src/RescriptCron.res @@ -12,6 +12,10 @@ module CronJob = { Nullable.t, Nullable.t, Nullable.t, + Nullable.t, + Nullable.t unit>, + Nullable.t, + Nullable.t, ) => t = "CronJob" let make = ( @@ -22,8 +26,12 @@ module CronJob = { ~timezone: option=?, ~context: option<{..}>=?, ~runOnInit: option=?, - ~utcOffset: option=?, // Could also be string, but that complicates + ~utcOffset: option=?, ~unrefTimeout: option=?, + ~waitForCompletion: option=?, + ~errorHandler: option unit>=?, + ~name: option=?, + ~threshold: option=?, (), ) => make( @@ -36,6 +44,10 @@ module CronJob = { Nullable.fromOption(runOnInit), Nullable.fromOption(utcOffset), Nullable.fromOption(unrefTimeout), + Nullable.fromOption(waitForCompletion), + Nullable.fromOption(errorHandler), + Nullable.fromOption(name), + Nullable.fromOption(threshold), ) } @@ -78,3 +90,17 @@ let nextJsDates = (~numberOfDates=1, job: CronJob.t) => @send external addCallback: (CronJob.t, (unit => unit) => unit) => unit = "addCallback" + +@send external nextDate: CronJob.t => LuxonDateTime.t = "nextDate" + +@get external isActive: CronJob.t => bool = "isActive" + +@get external isCallbackRunning: CronJob.t => bool = "isCallbackRunning" + +type validateCronResult = { + valid: bool, + error?: JsExn.t, +} + +@module("cron") @val +external validateCronExpression: string => validateCronResult = "validateCronExpression" diff --git a/src/RescriptCron.resi b/src/RescriptCron.resi index 0ceb2a8..2fdf772 100644 --- a/src/RescriptCron.resi +++ b/src/RescriptCron.resi @@ -16,6 +16,10 @@ module CronJob: { @param runOnInit This will immediately fire your [onTick] function as soon as the requisite initialization has happened. This option is set to [false] by default for backwards compatibility. @param utcOffset Set a UTC offset for your timezone instead of using the [timezone] parameter. Do not use [utcOffset] and [timezone] parameter together. @param unrefTimeout If you have code that keeps the event loop running and want to stop the node process when that finishes regardless of the state of your cronjob, you can do so making use of this parameter. This is off by default and cron will run as if it needs to control the event loop. For more information take a look at {{: https://nodejs.org/api/timers.html#timers_timeout_unref } timers#timers_timeout_unref } from the NodeJS docs. + @param waitForCompletion If set to [true], the job will wait for the previous execution to complete before starting a new one. + @param errorHandler A function called with the error if [onTick] throws. Defaults to [None]. + @param name An optional name for the job, useful for debugging. + @param threshold An optional threshold in milliseconds. If the job is overdue by more than this value it will not fire. ") let make: ( [#CronString(string) | #JsDate(Date.t)], @@ -27,6 +31,10 @@ module CronJob: { ~runOnInit: bool=?, ~utcOffset: int=?, ~unrefTimeout: bool=?, + ~waitForCompletion: bool=?, + ~errorHandler: JsExn.t => unit=?, + ~name: string=?, + ~threshold: float=?, unit, ) => t } @@ -104,3 +112,32 @@ external fireOnTick: CronJob.t => unit = "fireOnTick" ) @send external addCallback: (CronJob.t, (unit => unit) => unit) => unit = "addCallback" + +@ocaml.doc( + " [nextDate(cronJob)] returns the next {!type:LuxonDateTime.t} that will trigger an [onTick] " +) +@send +external nextDate: CronJob.t => LuxonDateTime.t = "nextDate" + +@ocaml.doc(" [isActive(cronJob)] returns [true] if the job is currently running ") +@get +external isActive: CronJob.t => bool = "isActive" + +@ocaml.doc(" [isCallbackRunning(cronJob)] returns [true] if an [onTick] callback is currently executing ") +@get +external isCallbackRunning: CronJob.t => bool = "isCallbackRunning" + +@ocaml.doc( + " Result type returned by {!val:validateCronExpression}. [valid] is [true] if the expression is valid, [error] is set if it is not. " +) +type validateCronResult = { + valid: bool, + error?: JsExn.t, +} + +@ocaml.doc( + " [validateCronExpression(expr)] validates a cron expression string. Returns a {!type:validateCronResult} with [valid] set to [true] if the expression is valid. " +) +@module("cron") +@val +external validateCronExpression: string => validateCronResult = "validateCronExpression"