@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
- Basic Job
- Extra properties
- Common options
- Errors and retries
- Overlaps
- Options flexibility
- Current job access
- Dynamic jobs
- Pure dependency injection
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'
Install the package along with peer dependencies.
yarn add @reflet/cron cron && yarn add -D @types/cron @types/node
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' }) } }
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
: readonlyboolean
that helps us determine if the job is actually being executed. Because the original node-cronrunning
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 withjob.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 (plusnumber
).
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())