UNPKG

expresser

Version:

A ready to use Node.js web app wrapper, built on top of Express.

293 lines (241 loc) 11.9 kB
# EXPRESSER CRON # ----------------------------------------------------------------------------- # Handle scheduled cron jobs. You can use intervals (seconds) or specific # times to trigger jobs, and the module will take care of setting the proper timers. # Jobs are added using "job" objects with id, schedule, callback and other options. # <!-- # @example Sample job object, alerts user every minute (60 seconds). # var myJob = { # id: "alertJob", # schedule: 60, # callback: function(job) { alertUser(mydata); } # } # @example Sample job object, sends email every day at 10AM and 5PM. # var otherJob = { # id: "my mail job", # schedule: ["10:00:00", "17:00:00"], # callback: function(job) { mail.send(something); } # } # @see Settings.cron # --> class Cron events = require "./events.coffee" fs = require "fs" lodash = require "lodash" logger = require "./logger.coffee" moment = require "moment" path = require "path" settings = require "./settings.coffee" utils = require "./utils.coffee" # @property [Array] The jobs collection, please do not edit this object manually! jobs: [] # CONSTRUCTOR AND INIT # ------------------------------------------------------------------------- # Cron constructor. constructor: -> @setEvents() if settings.events.enabled # Bind event listeners. setEvents: => events.on "Cron.start", @start events.on "Cron.stop", @stop events.on "Cron.add", @add events.on "Cron.remove", @remove # Init the cron manager. If `loadOnInit` setting is true, the `cron.json` file will be parsed # and loaded straight away (if there's one). init: (options) => logger.debug "Cron.init", options lodash.assign settings.cron, options if options? @load true if settings.cron.loadOnInit # Load jobs from the `cron.json` file. If `autoStart` is true, it will automatically # call the `start` method after loading. # @param [String] filename Path to the JSON file containing jobs, optional, default is "cron.json". # @param [Object] options Options to be passed when loading cron jobs. # @option options [String] basePath Sets the base path of modules when requiring them. # @option options [Boolean] autoStart If true, call "start" after loading. load: (filename, options) => logger.debug "Cron.load", filename, options # Set default options. options = {} if not options? options = lodash.defaults options, {autoStart: false, basePath: ""} if lodash.isBoolean filename filename = null options.autoStart = filename if not filename? or filename is false or filename is "" filename = "cron.json" doNotWarn = true # Get full path to the passed json file. filepath = utils.getFilePath filename basename = path.basename filepath # Found the cron.json file? Read it. if filepath? cronJson = fs.readFileSync filepath, {encoding: settings.general.encoding} cronJson = utils.minifyJson cronJson # Iterate jobs, but do not add if job's `enabled` is false. for key, data of cronJson module = require(options.basePath + key) # Only proceed if the cronDisabled flag is not present on the module. if module.cronDisabled isnt true for d in data if not d.enabled? or d.enabled cb = module[d.callback] job = d job.module = key job.id = key + "." + d.callback job.callback = cb @add job else logger.debug "Cron.load", filename, key, d.callback, "Enabled is false. Skip!" else logger.debug "Cron.load", filename, "Module has cronDisabled set. Skip!" # Start all jobs automatically if `autoStart` is true. @start() if options.autoStart logger.info "Cron.load", "#{basename} loaded." else if not doNotWarn logger.warn "Cron.load", "#{basename} not found." # METHODS # ------------------------------------------------------------------------- # Start the specified cron job. If no `id` is specified, all jobs will be started. # A filter can also be passed as an object. For example to start all jobs for # the module "email", use start({module: "email"}). # @param [String] idOrFilter The job id or filter, optional (if not specified, start everything). start: (idOrFilter) => if not idOrFilter? logger.info "Cron.start", "All jobs" arr = @jobs if lodash.isString idOrFilter or lodash.isNumber idOrFilter logger.info "Cron.start", idOrFilter arr = lodash.find @jobs, {id: idOrFilter.toString()} else logger.info "Cron.start", idOrFilter arr = lodash.find @jobs, idOrFilter if not arr? or arr.length < 1 logger.debug "Cron.start", "Job #{idOrFilter} does not exist. Abort!" else for job in arr clearTimeout job.timer if job.timer? setTimer job # Stop the specified cron job. If no `id` is specified, all jobs will be stopped. # A filter can also be passed as an object. For example to stop all jobs for # the module "mymodule", use stop({module: "mymodule"}). # @param [String] idOrFilter The job id or filter, optional (if not specified, stop everything). stop: (idOrFilter) => if not idOrFilter? logger.info "Cron.stop", "All jobs" arr = @jobs if lodash.isString idOrFilter or lodash.isNumber idOrFilter logger.info "Cron.stop", idOrFilter arr = lodash.find @jobs, {id: idOrFilter.toString()} else logger.info "Cron.stop", idOrFilter arr = lodash.find @jobs, idOrFilter if not arr? or arr.length < 1 logger.debug "Cron.stop", "Job #{idOrFilter} does not exist. Abort!" else for job in arr clearTimeout job.timer if job.timer? job.timer = null # Add a scheduled job to the cron, passing an `id` and `job`. # You can also pass only the `job` if it has an id property. # @param [String] id The job ID, optional, overrides job.id in case it has one. # @param [Object] job The job object. # @option job [String] id The job ID, optional. # @option job [Integer, Array] schedule If a number assume it's the interval in seconds, otherwise a times array. # @option job [Method] callback The callback (job) to be triggered. # @option job [Boolean] once If true, the job will be triggered only once no matter which schedule it has. # @return [Object] Returns {error, job}, where job is the job object and error is the error message (if any). add: (job) => logger.debug "Cron.add", job # Throw error if no `id` was provided. if not job.id? or job.id is "" throw new Error "Job must have an ID. Please set the job.id property." # Throw error if job callback is not a valid function. if not lodash.isFunction job.callback throw new Error "Job callback is not a valid, please set job.callback as a valid Function." # Find existing job. existing = lodash.find @jobs, {id: job.id} # Handle existing jobs. if existing? if settings.cron.allowReplacing clearTimeout existing.timer if existing.timer? existing.timer = null else errorMsg = "Job #{job.id} already exists and 'allowReplacing' is false. Abort!" logger.error "Cron.add", errorMsg return {error: errorMsg} # Set `startTime` and `endTime` if not set. job.startTime = moment 0 if not job.startTime? job.endTime = moment 0 if not job.endTime? # Only create the timer if `autoStart` is not false, add to the jobs list. setTimer job if job.autoStart isnt false @jobs.push job return {job: job} # Remove and stop a current job. If job does not exist, a warning will be logged. # @param [String] id The job ID. remove: (id) => existing = lodash.find @jobs, {id: id} # Job exists? if not existing? logger.debug "Cron.remove", "Job #{id} does not exist. Abort!" return false # Clear timer and remove job from array. clearTimeout existing.timer if existing.timer? @jobs.splice existing # HELPERS # ------------------------------------------------------------------------- # Helper to get the timeout value (ms) to the next job callback. getTimeout = (job) -> now = moment() nextDate = moment() # If `schedule` is not an array, parse it as integer / seconds. if lodash.isNumber job.schedule or lodash.isString job.schedule timeout = moment().add(job.schedule, "s").valueOf() - now.valueOf() else minTime = "99:99:99" nextTime = "99:99:99" # Get the next and minimum times from `schedule`. for sc in job.schedule minTime = sc if sc < minTime nextTime = sc if sc < nextTime and sc > nextDate.format("HH:mm:ss") # If no times were found for today then set for tomorrow, minimum time. if nextTime is "99:99:99" nextDate = nextDate.add 1, "d" nextTime = minTime # Return the timeout. arr = nextTime.split ":" dateValue = [nextDate.year(), nextDate.month(), nextDate.date(), parseInt(arr[0]), parseInt(arr[1]), parseInt(arr[2])] timeout = moment(dateValue).valueOf() - now.valueOf() return timeout # Helper to prepare and get a job callback function. getCallback = (job) -> callback = -> logger.debug "Cron", "Job #{job.id} trigger." job.timer = null job.startTime = moment() job.endTime = moment() try job.callback job catch ex logger.error "Cron.getCallback", "Could not run jub.", ex.message, ex.stack # Only reset timer if once is not true. setTimer job if not job.once # Return generated callback. return callback # Helper to get a timer / interval based on the defined options. setTimer = (job) -> logger.debug "Cron.setTimer", job callback = getCallback job # Get the correct schedule / timeout value. schedule = job.schedule schedule = moment.duration(schedule).asMilliseconds() if not lodash.isNumber schedule # Make sure timer is not running. clearTimeout job.timer if job.timer? # Set the timeout based on the defined schedule. timeout = getTimeout job job.timer = setTimeout callback, timeout job.nextRun = moment().add timeout, "ms" # Singleton implementation. # ----------------------------------------------------------------------------- Cron.getInstance = -> @instance = new Cron() if not @instance? return @instance module.exports = exports = Cron.getInstance()