diff --git a/.github/banner.png b/.github/banner.png new file mode 100644 index 0000000..3662ceb Binary files /dev/null and b/.github/banner.png differ diff --git a/README.md b/README.md index 027187e..82d5d82 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,232 @@ -
- +
-

Adonis Package Boilerplate

-

A easy way to create AdonisJS packages

+

Adonis Scheduler

+

Schedule tasks in AdonisJS with ease

-## **Usage** +
+ +[![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url] + +
+ + +## **Pre-requisites** +The `@verful/scheduler` package requires `@adonisjs/core >= 5.9.0` + +## **Setup** + +Install the package from the npm registry as follows. + +``` +npm i @verful/scheduler +# or +yarn add @verful/scheduler +``` + +Next, configure the package by running the following ace command. + +``` +node ace configure @verful/scheduler +``` + +## **Defining Scheduled Tasks** +You may define all of your scheduled tasks in the `start/tasks.ts` preloaded file. To get started, let's take a look at an example. In this example, we will schedule a closure to be called every day at midnight. Within the closure we will execute a database query to clear a table: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' +import Database from '@ioc:Adonis/Lucid/Database' + +Scheduler.call(async () => { + Database.from('recent_users').delete() +}).daily() + +``` + +### Scheduling Ace Commands + +In addition to scheduling closures, you may also schedule Ace commands and system commands. For example, you may use the command method to schedule an Ace command using the commands name. + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('queue:flush').everyFiveMinutes() +``` + +### Scheduling Shell Commands + +The `exec` method may be used to issue a command to the operating system: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.exec('node script.js').daily() +``` + +### Schedule Frequency Options + +We've already seen a few examples of how you may configure a task to run at specified intervals. However, there are many more task schedule frequencies that you may assign to a task: + +| Method | Description | +| ------------------------------- | ------------------------------------------------------- | +| `.cron('* * * * *')` | Run the task on a custom cron schedule | +| `.everySecond()` | Run the task every second | +| `.everyTwoSeconds()` | Run the task every two seconds | +| `.everyFiveSeconds()` | Run the task every five seconds | +| `.everyTenSeconds()` | Run the task every ten seconds | +| `.everyFifteenSeconds()` | Run the task every fifteen seconds | +| `.everyTwentySeconds()` | Run the task every twenty seconds | +| `.everyThirtySeconds()` | Run the task every thirty seconds | +| `.everyMinute()` | Run the task every minute | +| `.everyTwoMinutes()` | Run the task every two minutes | +| `.everyThreeMinutes()` | Run the task every three minutes | +| `.everyFourMinutes()` | Run the task every four minutes | +| `.everyFiveMinutes()` | Run the task every five minutes | +| `.everyTenMinutes()` | Run the task every ten minutes | +| `.everyFifteenMinutes()` | Run the task every fifteen minutes | +| `.everyThirtyMinutes()` | Run the task every thirty minutes | +| `.hourly()` | Run the task every hour | +| `.hourlyAt(17)` | Run the task every hour at 17 minutes past the hour | +| `.everyOddHour(minutes)` | Run the task every odd hour | +| `.everyTwoHours(minutes)` | Run the task every two hours | +| `.everyThreeHours(minutes)` | Run the task every three hours | +| `.everyFourHours(minutes)` | Run the task every four hours | +| `.everySixHours(minutes)` | Run the task every six hours | +| `.daily()` | Run the task every day at midnight | +| `.dailyAt('13:00')` | Run the task every day at 13:00 | +| `.twiceDaily(1, 13)` | Run the task daily at 1:00 & 13:00 | +| `.twiceDailyAt(1, 13, 15)` | Run the task daily at 1:15 & 13:15 | +| `.weekly()` | Run the task every Sunday at 00:00 | +| `.weeklyOn(1, '8:00')` | Run the task every week on Monday at 8:00 | +| `.monthly()` | Run the task on the first day of every month at 00:00 | +| `.monthlyOn(4, '15:00')` | Run the task every month on the 4th at 15:00 | +| `.twiceMonthly(1, 16, '13:00')` | Run the task monthly on the 1st and 16th at 13:00 | +| `.lastDayOfMonth('15:00')` | Run the task on the last day of the month at 15:00 | +| `.quarterly()` | Run the task on the first day of every quarter at 00:00 | +| `.quarterlyOn(4, '14:00')` | Run the task every quarter on the 4th at 14:00 | +| `.yearly()` | Run the task on the first day of every year at 00:00 | +| `.yearlyOn(6, 1, '17:00')` | Run the task every year on June 1st at 17:00 | + +These methods may be combined with additional constraints to create even more finely tuned schedules that only run on certain days of the week. For example, you may schedule a command to run weekly on Monday: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' -1. Press the "Use this template" button at the top of this page to create a new repository with the contents of this template. -2. Install the required dependencies using your preferred package manager -```bash -npm install -yarn install -pnpm install +// Run once per week on Monday at 1 PM... +Scheduler.call(() => { + // ... +}).weekly().mondays().at('13:00') + +// Run hourly from 8 AM to 5 PM on weekdays... +Scheduler.command('foo') + .weekdays() + .hourly() + .between('8:00', '17:00') ``` -3. Run the configuration script using your preffered package manager -```bash -npm run configure -yarn configure -pnpm configure + +A list of additional schedule constraints may be found below: + +| Method | Description | +| ----------------------------- | ----------------------------------------------------- | +| `.weekdays()` | Limit the task to weekdays | +| `.weekends()` | Limit the task to weekends | +| `.sundays()` | Limit the task to Sunday | +| `.mondays()` | Limit the task to Monday | +| `.tuesdays()` | Limit the task to Tuesday | +| `.wednesdays()` | Limit the task to Wednesday | +| `.thursdays()` | Limit the task to Thursday | +| `.fridays()` | Limit the task to Friday | +| `.saturdays()` | Limit the task to Saturday | +| `.days(days)` | Limit the task to specific days | +| `.between(start, end)` | Limit the task to run between start and end times | +| `.unlessBetween(start, end)` | Limit the task to not run between start and end times | +| `.when(Closure)` | Limit the task based on a truth test | +| `.environments(environments)` | Limit the task to specific environments | + + +#### Day Constraints + +The `days` method may be used to limit the execution of a task to specific days of the week. For example, you may schedule a command to run hourly on Sundays and Wednesdays: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .hourly() + .days([0, 3]) +``` + +#### Between Time Constraints + +The `between` method may be used to limit the execution of a task based on the time of day: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .hourly() + .between('7:00', '22:00') +``` + +Similarly, the `unlessBetween` method can be used to exclude the execution of a task for a period of time: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .hourly() + .unlessBetween('23:00', '4:00') +``` + +#### Truth Test Constraints + +The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other words, if the given closure returns `true`, the task will execute as long as no other constraining conditions prevent the task from running: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .daily() + .when(() => true); +``` + +The `skip` method may be seen as the inverse of `when`. If the `skip` method returns `true`, the scheduled task will not be executed: + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .daily() + .skip(() => true); +``` + +When using chained when methods, the scheduled command will only execute if all when conditions return true. + +#### Environment Constraints + +The environments method may be used to execute tasks only on the given environments (as defined by the NODE_ENV environment variable): + +```typescript +import Scheduler from '@ioc:Verful/Scheduler' + +Scheduler.command('emails:send') + .daily() + .environments(['staging', 'production']); ``` + +## Running the Scheduler + +Run the `scheduler:work` ace command, it doesn't need to be put into a cron job, as the scheduler will process the jobs as the time passes + +[npm-image]: https://img.shields.io/npm/v/@verful/scheduler.svg?style=for-the-badge&logo=**npm** +[npm-url]: https://npmjs.org/package/@verful/scheduler "npm" + +[license-image]: https://img.shields.io/npm/l/@verful/scheduler?color=blueviolet&style=for-the-badge +[license-url]: LICENSE.md "license" + +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript +[typescript-url]: "typescript" diff --git a/adonis-typings/scheduler.ts b/adonis-typings/scheduler.ts index 5a1f08d..1d911c0 100644 --- a/adonis-typings/scheduler.ts +++ b/adonis-typings/scheduler.ts @@ -51,8 +51,8 @@ declare module '@ioc:Verful/Scheduler' { rejects: Condition[] skip(condition: Condition): this when(condition: Condition): this - between(start: DateTime, end: DateTime): this - unlessBetween(start: DateTime, end: DateTime): this + between(start: Time, end: Time): this + unlessBetween(start: Time, end: Time): this environments(environments: Array<'production' | 'development' | 'staging' | 'test'>): this } diff --git a/package.json b/package.json index dd193e9..0e618a8 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,9 @@ "sinon": "^15.2.0", "typescript": "^5.2.2" }, + "peerDependencies": { + "@adonisjs/core": "^5.9.0" + }, "main": "./build/providers/AdonisScheduleProvider.js", "files": [ "build/adonis-typings", @@ -77,18 +80,14 @@ }, "adonisjs": { "instructionsMd": "./build/instructions.md", - "preloads": [], + "preloads": [ + "./start/tasks" + ], "templates": { - "config": [ - { - "src": "config.txt", - "dest": "@verful/adonis-scheduler" - } - ], - "contracts": [ + "start": [ { - "src": "contract.txt", - "dest": "@verful/adonis-scheduler" + "src": "tasks.txt", + "dest": "tasks" } ] }, diff --git a/src/schedule.ts b/src/schedule.ts index 210c7f8..2cd1014 100644 --- a/src/schedule.ts +++ b/src/schedule.ts @@ -1,7 +1,7 @@ import { DateTime } from 'luxon' import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { Condition, ScheduleContract, ScheduleHandler } from '@ioc:Verful/Scheduler' +import { Condition, ScheduleContract, ScheduleHandler, Time } from '@ioc:Verful/Scheduler' import ManagesFrequencies from './manages_frequencies' @@ -13,21 +13,36 @@ export default class Schedule extends ManagesFrequencies implements ScheduleCont super() } - protected inTimeInterval(startTime: DateTime, endTime: DateTime) { - const [now, start, end] = [ + protected inTimeInterval(startTime: Time, endTime: Time) { + const [startHours, startMinutes] = startTime.split(':').map(Number) + const [endHours, endMinutes] = endTime.split(':').map(Number) + + let [now, start, end] = [ DateTime.now().setZone(this.currentTimezone), - startTime.setZone(this.currentTimezone), - endTime.setZone(this.currentTimezone), + DateTime.now() + .set({ minute: startMinutes, hour: startHours, second: 0, millisecond: 0 }) + .setZone(this.currentTimezone), + DateTime.now() + .set({ minute: endMinutes, hour: endHours, second: 0, millisecond: 0 }) + .setZone(this.currentTimezone), ] + if (end < start) { + if (start > now) { + start = start.minus({ days: 1 }) + } else { + end = end.plus({ days: 1 }) + } + } + return () => now > start && now < end } - public between(start: DateTime, end: DateTime) { + public between(start: Time, end: Time) { return this.when(this.inTimeInterval(start, end)) } - public unlessBetween(start: DateTime, end: DateTime) { + public unlessBetween(start: Time, end: Time) { return this.skip(this.inTimeInterval(start, end)) } diff --git a/templates/config.txt b/templates/config.txt deleted file mode 100644 index e69de29..0000000 diff --git a/templates/contract.txt b/templates/contract.txt deleted file mode 100644 index e69de29..0000000 diff --git a/templates/tasks.txt b/templates/tasks.txt new file mode 100644 index 0000000..906bd18 --- /dev/null +++ b/templates/tasks.txt @@ -0,0 +1,41 @@ +import Scheduler from '@ioc:Verful/Scheduler' + +/* +|-------------------------------------------------------------------------- +| Scheduled tasks +|-------------------------------------------------------------------------- +| +| Scheduled tasks allow you to run recurrent tasks in the background of your +| application. Here you can define all your scheduled tasks. +| +| You can define a scheduled task using the `.call` method on the Scheduler object +| as shown in the following example +| +| ``` +| Scheduler.call(() => { +| console.log('I am a scheduled task') +| }).everyMinute() +| ``` +| +| The example above will print the message `I am a scheduled task` every minute. +| +| You can also schedule ace commands using the `.command` method on the Scheduler +| object as shown in the following example +| +| ``` +| Scheduler.command('greet').everyMinute() +| ``` +| +| The example above will run the `greet` command every minute. +| +| You can also schedule shell commands with arguments using the `.exec` method on the Scheduler +| object as shown in the following example +| +| ``` +| Scheduler.exec('node ace greet').everyMinute() +| ``` +| +| The example above will run the `node ace greet` command every minute. +| +| Happy scheduling! +*/ diff --git a/tests/schedule.spec.ts b/tests/schedule.spec.ts index e88936f..8ed64ba 100644 --- a/tests/schedule.spec.ts +++ b/tests/schedule.spec.ts @@ -7,7 +7,15 @@ import Schedule from '../src/schedule' test.group('Schedule', (group) => { group.each.setup(() => { // Stub DateTime.now() to a fixed value for consistent testing - sinon.stub(DateTime, 'now').returns(DateTime.fromMillis(1_627_651_200_000)) // July 31, 2021 + sinon.stub(DateTime, 'now').returns( + DateTime.fromObject({ + year: 2021, + month: 7, + day: 31, + hour: 7, + minute: 30, + }) + ) // July 31, 2021 7:30 return () => { // Restore the stubs @@ -18,8 +26,8 @@ test.group('Schedule', (group) => { test('can set between condition', ({ assert }) => { const schedule = new Schedule({} as any, () => {}) - const start = DateTime.fromObject({ year: 2021, month: 8, day: 1, hour: 12 }) - const end = DateTime.fromObject({ year: 2021, month: 8, day: 2, hour: 12 }) + const start = '7:00' + const end = '8:00' schedule.between(start, end) @@ -30,8 +38,8 @@ test.group('Schedule', (group) => { test('can set unlessBetween condition', ({ assert }) => { const schedule = new Schedule({} as any, () => {}) - const start = DateTime.fromObject({ year: 2021, month: 8, day: 1, hour: 12 }) - const end = DateTime.fromObject({ year: 2021, month: 8, day: 2, hour: 12 }) + const start = '7:00' + const end = '8:00' schedule.unlessBetween(start, end) @@ -39,6 +47,40 @@ test.group('Schedule', (group) => { assert.isFunction(schedule.rejects[0]) }) + test('can set between condition that wraps midnight', ({ assert }) => { + const schedule = new Schedule({} as any, () => {}) + + const start = '23:00' + const end = '1:00' + + schedule.between(start, end) + + assert.lengthOf(schedule.filters, 1) + assert.isFunction(schedule.filters[0]) + }) + + test('can set unlessBetween condition that wraps midnight', ({ assert }) => { + const schedule = new Schedule({} as any, () => {}) + + const start = '23:00' + const end = '1:00' + + schedule.unlessBetween(start, end) + + assert.lengthOf(schedule.filters, 1) + assert.isFunction(schedule.filters[0]) + }) + + test('time interval check is correct', ({ assert }) => { + const schedule = new Schedule({} as any, () => {}) + + assert.isTrue(schedule['inTimeInterval']('7:00', '8:00')()) + assert.isTrue(schedule['inTimeInterval']('23:00', '8:00')()) + + assert.isFalse(schedule['inTimeInterval']('6:00', '7:00')()) + assert.isFalse(schedule['inTimeInterval']('23:00', '1:00')()) + }).pin() + test('can set skip condition', ({ assert }) => { const schedule = new Schedule({} as any, () => {})