npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@reflet/cron

v1.3.1

Published

Well-defined and well-typed cron decorators

Downloads

1,931

Readme

@reflet/cron 🌠

The best decorators for node-cron. Have a look at Reflet's philosophy.

Getting started

    • Enable them in your TypeScript compiler options.

      "experimentalDecorators": true,
      "emitDecoratorMetadata": true,
    • Install reflect-metadata shim.

      yarn add reflect-metadata
    • Import the shim in your program before everything else.

      import 'reflect-metadata'
  1. Install the package along with peer dependencies.

    yarn add @reflet/cron cron && yarn add -D @types/cron @types/node
  2. Create cron jobs.

    // jobs.ts
    import { Cron, Expression } from '@reflet/cron'
    
    @Cron.TimeZone('Europe/Paris')
    export class Jobs {
      @Cron(Expression.EVERY_SECOND)
      async logMessage() {
        console.log('You will see this message every second');
      }
    
      @Cron(Expression.EVERY_DAY_AT_MIDNIGHT)
      async sendEmailToAdmin() {
        await emailClient.send({ to: '[email protected]', content: 'This is fine' })
      }
    }
  3. Initialize and start cron jobs.

    // main.ts
    import { initCronJobs } from '@reflet/cron'
    import { Jobs } from './jobs.ts'
    
    const jobs = initCronJobs(Jobs)
    jobs.startAll()

Basic Job

🔦 @Cron(time) 🔦 initCronJobs(class) 💫 Related node-cron constructor: CronJob

import { Cron, initCronJobs } from '@reflet/cron'

class Jobs {
  @Cron('*/1 * * * *')
  logFoo() {
    console.log('foo')
  }
}

const jobs = initCronJobs(Jobs) // returns a Map of your jobs, with the method names as keys.

// You can start jobs individually
jobs.get('logFoo').start()

// Or all at once
jobs.startAll()

// Same with stop
jobs.stopAll()

💡 Use Expression enum to avoid using cron syntax:

import { Cron, Expression } from '@reflet/cron'

class Jobs {
  @Cron(Expression.EVERY_MINUTE)
  logFoo() {
    console.log('foo')
  }
}

Init encapsulation

Wrap initCronJobs into a static method to encapsulate initialization as well:

import { initCronJobs, Cron } from '@reflet/cron'

class Jobs {
  static init(...deps: ConstructorParameters<typeof Jobs>) {
    return initCronJobs(new Jobs(...deps))
  }

  @Cron(Expression.EVERY_MINUTE)
  logFoo() {
    console.log('foo')
  }
}

const jobs = Jobs.init()

💡 As a convenience, you can inherit from the abstract class Initializer which already have the init static method:

import { Initializer } from '@reflet/cron'

// You must pass the child class as type parameter to properly infer its constructor parameters.
class Jobs extends Initializer<typeof Jobs> {
  @Cron(Expression.EVERY_MINUTE)
  logFoo() {
    console.log('foo')
  }
}

const jobs = Jobs.init()

Extra properties

Compared to node-cron, Reflet adds 2 properties to jobs' instances:

  • firing: readonly boolean that helps us determine if the job is actually being executed. Because the original node-cron running property defines whether the job has been started or stopped. This property is used by the PreventOverlap decorator.
  • name: string in the following format: class.method, to help distinguish jobs.

Common options

🔦 @Cron.Start, @Cron.RunOnInit, @Cron.OnComplete(fn), @Cron.TimeZone(tz), @Cron.UtcOffset(offset), @Cron.UnrefTimeout 💫 Related node-cron parameters: start, runOnInit, onComplete, timezone, utcOffset, unrefTimeout

  • @Cron.Start: Starts the job after init (does not immediately fire its function). Can be used with or without invokation.
  • @Cron.RunOnInit: Fire the job's function on init. Can be used with or without invokation.
  • @Cron.OnComplete(fn): A function that will fire when the job is stopped with job.stop().
  • @Cron.TimeZone(tz): Specify the timezone for the execution. Type is narrowed to an union of all available timezones.
  • @Cron.UtcOffset(offset): Specify the offset of the timezone instead of the timezone directly. Type is narrowed to an union of all available offsets (plus number).

Please refer to the node-cron repository for more details.

import { Cron, Expression } from '@reflet/cron'

class Jobs {
  @Cron.Start 
  @Cron.RunOnInit
  @Cron.TimeZone('Europe/Paris')
  @Cron(Expression.EVERY_SECOND)
  doSomething() {}

  @Cron.Start()
  @Cron.RunOnInit()
  @Cron.UtcOffset('+01:00')
  @Cron.OnComplete(() => {}) 
  @Cron(Expression.EVERY_SECOND)
  doSomethingElse() {}
}

Errors and retries

Errors happening in a cron job are automatically logged to stderr instead of crashing the server.

Catch

🔦 @Cron.Catch(errorHandler)

This decorator allows you to do something else than logging with your errors.

import { Cron, Expression } from '@reflet/cron'

class Jobs {
  @Cron.Catch(async (err) => {
    console.error(err)
    await db.insert(err)
  })
  @Cron(Expression.EVERY_SECOND)
  doSomething() {}
}

Retry

🔦 @Cron.Retry(options)

If your job throw an error, you can retry it with a specify number of attempts, and specify backoff behavior.

  • attempts: Number of retry attempts.
  • delay: Delay between retry attemps in milliseconds.
  • delayFactor: Increases each time the previous delay by a multiplicative factor.
  • delayMax: Caps the maximum delay in milliseconds.
  • condition: Filter function with the error as parameter (so you can retry on specific errors only).
import { Cron, Expression } from '@reflet/cron'

class Jobs {
  @Cron.Retry({ attempts: 3, delay: 100, delayFactor: 2, delayMax: 1000 })
  @Cron(Expression.EVERY_HOUR)
  doSomething() {}
}

If all the attempts failed, then the error is logged to stderr (or handled by Cron.Catch).

Overlaps

🔦 @Cron.PreventOverlap

Prevents the job from firing if the previous occurence is not finished. Useful for potentially long jobs firing every second.Can be used with or without invokation.

import { Cron, Expression } from '@reflet/cron'

class Jobs {
  @Cron.PreventOverlap
  @Cron(Expression.EVERY_SECOND)
  doSomething() {}
}

Distributed overlaps

🔦 @Cron.PreventOverlap.RedisLock

If your application hosting your cron jobs is running amongst multiple instances, you want to make sure a single job is firing once.With the help of Redis and the node-redlock package, you can achieve cron locks easily.

You obviously need to install a Redis server, a nodejs Redis client, and node-redlock.

import * as redis from 'redis'
import * as Redlock from 'redlock'
import { Cron, Expression } from '@reflet/cron'

const redisClient = redis.createClient()

@Cron.PreventOverlap.RedisLock((job) => {
  const redlock = new Redlock([redisClient], { retryCount: 0 })
  return redlock.lock(`lock:${job.name}`, 1000)
  // use each job's name as a unique resource ("Jobs.doSomething" in this case).
})
class Jobs {
  @Cron(Expression.EVERY_MINUTE)
  doSomething() {}
}

Reflet will handle the unlocking, once the job is over.If @Cron.Retry is also applied, the lock will be reacquired on retries with the original ttl and the eventual retry delay.

🗣️ By default, redlock retryCount is set to 10, the documentation recommends to set retryCount to 0 for most tasks (especially for high frequency jobs).

In that regard, if you want to have a look at failed locks, you can simply .catch your lock promise:

return redlock.lock(`lock:${job.name}`, 1000).catch(console.warn)

Go to node-redlock documentation for more details.

Options flexibility

Single decorator

🔦 @Cron.Options(allOptions)

If you prefer, you can use @Cron.Options to group all your options in a single decorator:

import { Cron } from '@reflet/cron'

class Jobs {
  @Cron.Options({
    start: true,
    runOnInit: true,
    retry: { attempts: 3, delay: 200 },
    catchError: async (err) => {
      await db.insert(err)
    }
  })
  @Cron('* * * * *')
  doSomething() {}
}

Share and override

Attach any option decorator to the class to share it with all jobs. You can override it by reattaching it to the method.

Boolean option decorators (@Cron.Start, @Cron.RunOnInit, @Cron.UnrefTimeout, @Cron.PreventOverlap) can be turned to false with their special sub decorator: @Cron.XXX.Dont (can be used with or without invokation)

@Cron.Start
@Cron.PreventOverlap
@Cron.Retry({ maxRetries: 3, delay: 200 })
class Jobs {
  @Cron(Expression.EVERY_SECOND)
  doSomething() {}

  @Cron.Start.Dont
  @Cron(Expression.EVERY_SECOND)
  doSomethingElse() {}
}

Current job access

🔦 @CurrentJob

You might need to access the current job instance from its own onTick function, for example to simply stop it.Can be used with or without invokation.

import { Cron, Expression, CurrentJob } from '@reflet/cron'

class Jobs {
  @Cron(Expression.EVERY_SECOND)
  doSomething(@CurrentJob job: Job) {
    job.stop()
  }
}

Dynamic jobs

You can add dynamic cron jobs like so:

import { Cron, Expression, initCronJobs } from '@reflet/cron'

@Cron.Start
class Jobs {
  @Cron(Expression.EVERY_SECOND)
  doSomething() {}
}

const jobs = initCronJobs(Jobs)

jobs.set('doSomethingElse', {
  cronTime: Expression.EVERY_HOUR,
  onTick() {
    console.log("I'm a dynamic job")
  },
  runOnInit: true,
  preventOverlap: true
})

Dynamic jobs inherit shared class options.

Pure dependency injection

If you want to go full OOP and your job classes has constructor dependencies, Reflet will enforce passing them as instances (along with their dependencies) instead of classes.

import { Cron, Expression, initCronJobs } from '@reflet/cron'

class Service {
  user = 'Jeremy'
}

class Jobs {
  static init(...deps: ConstructorParameters<typeof Jobs>) {
    return initCronJobs(new Jobs(...deps))
  }

  constructor(private service: Service) {}

  @Cron(Expression.EVERY_10_MINUTES)
  doSomething() {
    console.log(this.service.user)
  }
}

const jobs = Jobs.init(new Service())