rschmukler/agenda

View on GitHub
src/utils/nextRunAt.ts

Summary

Maintainability
A
3 hrs
Test Coverage
/* eslint-disable import/first */
import { DateTime } from 'luxon';
import * as date from 'date.js';
import * as debug from 'debug';
import { parseExpression } from 'cron-parser';
import humanInterval = require('human-interval');
import { isValidDate } from './isValidDate';
import type { IJobParameters } from '../types/JobParameters';

const log = debug('agenda:nextRunAt');

const dateForTimezone = (timezoneDate: Date, timezone?: string): DateTime =>
    DateTime.fromJSDate(timezoneDate, { zone: timezone });

export function isValidHumanInterval(value: unknown): value is string {
    const transformedValue = humanInterval(value as string);
    return typeof transformedValue === 'number' && Number.isNaN(transformedValue) === false;
}

/**
 * Internal method that computes the interval
 */
export const computeFromInterval = (attrs: IJobParameters<any>): Date => {
    const previousNextRunAt = attrs.nextRunAt || new Date();
    log('[%s:%s] computing next run via interval [%s]', attrs.name, attrs._id, attrs.repeatInterval);

    const lastRun = dateForTimezone(attrs.lastRunAt || new Date(), attrs.repeatTimezone);

    const cronOptions = {
        currentDate: lastRun.toJSDate(),
        tz: attrs.repeatTimezone
    };

    let nextRunAt: Date | null = null;

    let error;
    if (typeof attrs.repeatInterval === 'string') {
        try {
            let cronTime = parseExpression(attrs.repeatInterval, cronOptions);
            let nextDate = cronTime.next().toDate();
            if (
                nextDate.valueOf() === lastRun.valueOf() ||
                nextDate.valueOf() <= previousNextRunAt.valueOf()
            ) {
                // Handle cronTime giving back the same date for the next run time
                cronOptions.currentDate = new Date(lastRun.valueOf() + 1000);
                cronTime = parseExpression(attrs.repeatInterval, cronOptions);
                nextDate = cronTime.next().toDate();
            }

            nextRunAt = nextDate;

            // eslint-disable-next-line no-empty
        } catch (err) {
            error = err;
        }
    }

    if (isValidHumanInterval(attrs.repeatInterval)) {
        if (!attrs.lastRunAt) {
            nextRunAt = new Date(lastRun.valueOf());
        } else {
            const intervalValue = humanInterval(attrs.repeatInterval) as number;
            nextRunAt = new Date(lastRun.valueOf() + intervalValue);
        }
    }

    if (!isValidDate(nextRunAt)) {
        log(
            '[%s:%s] failed to calculate nextRunAt due to invalid repeat interval',
            attrs.name,
            attrs._id
        );
        throw new Error(
            `failed to calculate nextRunAt due to invalid repeat interval (${attrs.repeatInterval}): ${
                error || 'no readable human interval'
            }`
        );
    }

    return nextRunAt;
};

/**
 * Internal method to compute next run time from the repeat string
 * @returns {undefined}
 */
export function computeFromRepeatAt(attrs: IJobParameters<any>): Date {
    const lastRun = attrs.lastRunAt || new Date();
    const nextDate = date(attrs.repeatAt).valueOf();

    // If you do not specify offset date for below test it will fail for ms
    const offset = Date.now();

    if (offset === date(attrs.repeatAt, offset).valueOf()) {
        log('[%s:%s] failed to calculate repeatAt due to invalid format', attrs.name, attrs._id);
        // this.attrs.nextRunAt = undefined;
        // this.fail('failed to calculate repeatAt time due to invalid format');
        throw new Error('failed to calculate repeatAt time due to invalid format');
    }

    if (nextDate.valueOf() === lastRun.valueOf()) {
        return date('tomorrow at ', attrs.repeatAt);
    }

    return date(attrs.repeatAt);
}