Skip to content

Commit

Permalink
feat!: rework utcOffset parameter (kelektiv#699)
Browse files Browse the repository at this point in the history
## Breaking changes
BREAKING CHANGE: `utcOffset` parameter no longer accepts a string
BREAKING CHANGE: `utcOffset` values between -60 and 60 are no longer
treated as hours
BREAKING CHANGE: providing both `timeZone` and `utcOffset` parameters
now throws an error
  • Loading branch information
sheerlox committed Sep 29, 2023
1 parent 163dda8 commit 96c6f46
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 119 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ Parameter Based
- `onTick` - [REQUIRED] - The function to fire at the specified time. If an `onComplete` callback was provided, `onTick` will receive it as an argument. `onTick` may call `onComplete` when it has finished its work.
- `onComplete` - [OPTIONAL] - A function that will fire when the job is stopped with `job.stop()`, and may also be called by `onTick` at the end of each run.
- `start` - [OPTIONAL] - Specifies whether to start the job just before exiting the constructor. By default this is set to false. If left at default you will need to call `job.start()` in order to start the job (assuming `job` is the variable you set the cronjob to). This does not immediately fire your `onTick` function, it just gives you more control over the behavior of your jobs.
- `timeZone` - [OPTIONAL] - Specify the time zone for the execution. This will modify the actual time relative to your time zone. If the time zone is invalid, an error is thrown. By default (if this is omitted) the local time zone will be used. You can check the various time zones format accepted in the [Luxon documentation](https://github.com/moment/luxon/blob/master/docs/zones.md#specifying-a-zone). Note: This parameter supports minutes offsets, e.g. `UTC+5:30`. **Warning**: Probably don't use both `timeZone` and `utcOffset` together or weird things may happen.
- `timeZone` - [OPTIONAL] - Specify the time zone for the execution. This will modify the actual time relative to your time zone. If the time zone is invalid, an error is thrown. By default (if this is omitted) the local time zone will be used. You can check the various time zones format accepted in the [Luxon documentation](https://github.com/moment/luxon/blob/master/docs/zones.md#specifying-a-zone). Note: This parameter supports minutes offsets, e.g. `UTC+5:30`. **Note**: Cannot be used together with `utcOffset`.
- `context` - [OPTIONAL] - The context within which to execute the onTick method. This defaults to the cronjob itself allowing you to call `this.stop()`. However, if you change this you'll have access to the functions and values within your context object.
- `runOnInit` - [OPTIONAL] - 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.
- `utcOffset` - [OPTIONAL] - This allows you to specify the offset of your time zone rather than using the `timeZone` param. This should be an integer representing the number of minutes offset (like `120` for +2 hours or `-90` for -1.5 hours). **Warning**: Minutes offsets < 60 and >-60 will be treated as an offset in hours. This means a minute offset of `30` means an offset of +30 hours. Use the `timeZone` param in this case. This behavior [is planned to be removed in V3](https://github.com/kelektiv/node-cron/pull/685#issuecomment-1676417917). **Warning**: Probably don't use both `timeZone` and `utcOffset` together or weird things may happen.
- `utcOffset` - [OPTIONAL] - This allows you to specify the offset of your time zone rather than using the `timeZone` param. This should be an integer representing the number of minutes offset (like `120` for +2 hours or `-90` for -1.5 hours). **Note**: Cannot be used together with `timeZone`.
- `unrefTimeout` - [OPTIONAL] - 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 [timers#timers_timeout_unref](https://nodejs.org/api/timers.html#timers_timeout_unref) from the NodeJS docs.
- `from` (static) - Create a new CronJob object providing arguments as an object. See argument names and descriptions above.
- `start` - Runs your job.
Expand Down
5 changes: 5 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ExclusiveParametersError extends Error {
constructor(param1: string, param2: string) {
super(`You can't specify both ${param1} and ${param2}`);
}
}
91 changes: 79 additions & 12 deletions src/job.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn } from 'child_process';
import { ExclusiveParametersError } from './errors';
import { CronTime } from './time';
import { CronCommand, CronJobParams } from './types/cron.types';

Expand All @@ -16,6 +17,28 @@ export class CronJob {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _callbacks: ((...args: any) => void)[] = [];

constructor(
cronTime: CronJobParams['cronTime'],
onTick: CronJobParams['onTick'],
onComplete?: CronJobParams['onComplete'],
start?: CronJobParams['start'],
timeZone?: CronJobParams['timeZone'],
context?: CronJobParams['context'],
runOnInit?: CronJobParams['runOnInit'],
utcOffset?: null,
unrefTimeout?: CronJobParams['unrefTimeout']
);
constructor(
cronTime: CronJobParams['cronTime'],
onTick: CronJobParams['onTick'],
onComplete?: CronJobParams['onComplete'],
start?: CronJobParams['start'],
timeZone?: null,
context?: CronJobParams['context'],
runOnInit?: CronJobParams['runOnInit'],
utcOffset?: CronJobParams['utcOffset'],
unrefTimeout?: CronJobParams['unrefTimeout']
);
constructor(
cronTime: CronJobParams['cronTime'],
onTick: CronJobParams['onTick'],
Expand All @@ -28,7 +51,19 @@ export class CronJob {
unrefTimeout?: CronJobParams['unrefTimeout']
) {
this.context = context || this;
this.cronTime = new CronTime(cronTime, timeZone, utcOffset);

// runtime check for JS users
if (timeZone != null && utcOffset != null) {
throw new ExclusiveParametersError('timeZone', 'utcOffset');
}

if (timeZone != null) {
this.cronTime = new CronTime(cronTime, timeZone, null);
} else if (utcOffset != null) {
this.cronTime = new CronTime(cronTime, null, utcOffset);
} else {
this.cronTime = new CronTime(cronTime, timeZone, utcOffset);
}

if (unrefTimeout != null) {
this.unrefTimeout = unrefTimeout;
Expand All @@ -53,17 +88,49 @@ export class CronJob {
}

static from(params: CronJobParams) {
return new CronJob(
params.cronTime,
params.onTick,
params.onComplete,
params.start,
params.timeZone,
params.context,
params.runOnInit,
params.utcOffset,
params.unrefTimeout
);
// runtime check for JS users
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (params.timeZone != null && params.utcOffset != null) {
throw new ExclusiveParametersError('timeZone', 'utcOffset');
}

if (params.timeZone != null) {
return new CronJob(
params.cronTime,
params.onTick,
params.onComplete,
params.start,
params.timeZone,
params.context,
params.runOnInit,
params.utcOffset,
params.unrefTimeout
);
} else if (params.utcOffset != null) {
return new CronJob(
params.cronTime,
params.onTick,
params.onComplete,
params.start,
null,
params.context,
params.runOnInit,
params.utcOffset,
params.unrefTimeout
);
} else {
return new CronJob(
params.cronTime,
params.onTick,
params.onComplete,
params.start,
params.timeZone,
params.context,
params.runOnInit,
params.utcOffset,
params.unrefTimeout
);
}
}

private _fnWrap(cmd: CronCommand | string) {
Expand Down
74 changes: 38 additions & 36 deletions src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
TIME_UNITS_LEN,
TIME_UNITS_MAP
} from './constants';
import { ExclusiveParametersError } from './errors';
import {
CronJobParams,
DayOfMonthRange,
MonthRange,
Ranges,
Expand All @@ -23,8 +25,8 @@ import { getRecordKeys } from './utils';

export class CronTime {
source: string | DateTime;
zone?: string;
utcOffset?: number | string;
timeZone?: string;
utcOffset?: number;
realDate = false;

private second: TimeUnitField<'second'> = {};
Expand All @@ -35,17 +37,32 @@ export class CronTime {
private dayOfWeek: TimeUnitField<'dayOfWeek'> = {};

constructor(
source: string | Date | DateTime,
zone?: string | null,
utcOffset?: string | number | null
source: CronJobParams['cronTime'],
timeZone?: CronJobParams['timeZone'],
utcOffset?: null
);
constructor(
source: CronJobParams['cronTime'],
timeZone?: null,
utcOffset?: CronJobParams['utcOffset']
);
constructor(
source: CronJobParams['cronTime'],
timeZone?: CronJobParams['timeZone'],
utcOffset?: CronJobParams['utcOffset']
) {
if (zone) {
const dt = DateTime.fromObject({}, { zone });
// runtime check for JS users
if (timeZone != null && utcOffset != null) {
throw new ExclusiveParametersError('timeZone', 'utcOffset');
}

if (timeZone) {
const dt = DateTime.fromObject({}, { zone: timeZone });
if (!dt.isValid) {
throw new Error('Invalid timezone.');
}

this.zone = zone;
this.timeZone = timeZone;
}

if (utcOffset != null) {
Expand Down Expand Up @@ -122,35 +139,20 @@ export class CronTime {
this.realDate && this.source instanceof DateTime
? this.source
: DateTime.local();
if (this.zone) {
date = date.setZone(this.zone);
if (this.timeZone) {
date = date.setZone(this.timeZone);
}

if (this.utcOffset != null) {
const offsetHours = parseInt(
// @ts-expect-error old undocumented behavior going to be removed in V3
this.utcOffset >= 60 || this.utcOffset <= -60
? // @ts-expect-error old undocumented behavior going to be removed in V3
this.utcOffset / 60
: this.utcOffset
);
if (this.utcOffset !== undefined) {
const sign = this.utcOffset < 0 ? '-' : '+';

const offsetMins =
// @ts-expect-error old undocumented behavior going to be removed in V3
this.utcOffset >= 60 || this.utcOffset <= -60
? // @ts-expect-error old undocumented behavior going to be removed in V3
Math.abs(this.utcOffset - offsetHours * 60)
: 0;
const offsetMinsStr = offsetMins >= 10 ? offsetMins : `0${offsetMins}`;
const offsetHours = Math.trunc(this.utcOffset / 60);
const offsetHoursStr = String(Math.abs(offsetHours)).padStart(2, '0');

let utcZone = 'UTC';
const offsetMins = Math.abs(this.utcOffset - offsetHours * 60);
const offsetMinsStr = String(offsetMins).padStart(2, '0');

// @ts-expect-error old undocumented behavior going to be removed in V3
if (parseInt(this.utcOffset) < 0) {
utcZone += `${offsetHours === 0 ? '-0' : offsetHours}:${offsetMinsStr}`;
} else {
utcZone += `+${offsetHours}:${offsetMinsStr}`;
}
const utcZone = `UTC${sign}${offsetHoursStr}:${offsetMinsStr}`;

date = date.setZone(utcZone);

Expand Down Expand Up @@ -222,14 +224,14 @@ export class CronTime {
* - Check that the chosen time does not equal the current execution.
* - Return the selected date object.
*/
getNextDateFrom(start: Date | DateTime, zone?: string | Zone) {
getNextDateFrom(start: Date | DateTime, timeZone?: string | Zone) {
if (start instanceof Date) {
start = DateTime.fromJSDate(start);
}
let date = start;
const firstDate = start.toMillis();
if (zone) {
date = date.setZone(zone);
if (timeZone) {
date = date.setZone(timeZone);
}
if (!this.realDate) {
if (date.millisecond > 0) {
Expand Down Expand Up @@ -260,7 +262,7 @@ export class CronTime {
`Something went wrong. No execution date was found in the next 8 years.
Please provide the following string if you would like to help debug:
Time Zone: ${
zone?.toString() ?? '""'
timeZone?.toString() ?? '""'
} - Cron String: ${this.source.toString()} - UTC offset: ${
date.offset
} - current Date: ${DateTime.local().toString()}`
Expand Down
17 changes: 14 additions & 3 deletions src/types/cron.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@ import { CONSTRAINTS, TIME_UNITS_MAP } from '../constants';
import { CronJob } from '../job';
import { IntRange } from './utils';

export interface CronJobParams {
interface BaseCronJobParams {
cronTime: string | Date | DateTime;
onTick: CronCommand;
onComplete?: CronCommand | null;
start?: boolean | null;
timeZone?: string | null;
context?: unknown | null;
runOnInit?: boolean | null;
utcOffset?: string | number | null;
unrefTimeout?: boolean | null;
}

export type CronJobParams =
| BaseCronJobParams &
(
| {
timeZone?: string | null;
utcOffset?: never;
}
| {
timeZone?: never;
utcOffset?: number | null;
}
);

export type CronCommand =
/**
* TODO: find out how to type the context correctly, based on
Expand Down
Loading

0 comments on commit 96c6f46

Please sign in to comment.