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, () => {})