Skip to content

Commit

Permalink
Feature: Task Scheduling (#79)
Browse files Browse the repository at this point in the history
* fea: add schedule module

* fix: missing `this` scope to callback

* enh: create an example of how to use it

* fea: teardown scheduled listeners

* fea: add test case

* fix: update example

* fea: add CronExpression enum

* enh: isolate logic to register cron jobs

* fea: add @interval

* fea: add @timeout

* enh: mock Deno.cron to unit test schedule module

* enh: add tests for @timeout

* enh: add tests for @interval

* ref: reuse registration logic

* ref: use SetMetadata utils

* fix: add license to enum file
  • Loading branch information
marco-souza authored Feb 21, 2024
1 parent 8e7c930 commit cc251f0
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 0 deletions.
57 changes: 57 additions & 0 deletions example/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Cron,
DanetApplication,
Interval,
IntervalExpression,
Module,
ScheduleModule,
Timeout,
} from '../mod.ts';

class TaskScheduler {
getNow() {
return {
now: new Date(),
};
}

@Timeout(IntervalExpression.SECOND)
runOnce() {
console.log('run once after 1s', this.getNow());
}

@Interval(IntervalExpression.SECOND)
runEachSecond() {
console.log('1 sec', this.getNow());
}

@Cron('*/1 * * * *')
runEachMinute() {
console.log('1 minute', this.getNow());
}

@Cron('*/2 * * * *')
runEach2Min() {
console.log('2 minutes', this.getNow());
}

@Cron('*/3 * * * *')
runEach3Min() {
console.log('3 minutes', this.getNow());
}
}

@Module({
imports: [ScheduleModule],
injectables: [TaskScheduler],
})
class AppModule {}

const app = new DanetApplication();
await app.init(AppModule);

let port = Number(Deno.env.get('PORT'));
if (isNaN(port)) {
port = 3000;
}
app.listen(port);
110 changes: 110 additions & 0 deletions spec/schedule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
Cron,
CronExpression,
DanetApplication,
Interval,
IntervalExpression,
Module,
ScheduleModule,
Timeout,
} from '../mod.ts';
import { assertEquals } from '../src/deps_test.ts';
import { assertSpyCallArg, FakeTime, spy } from '../src/deps_test.ts';

Deno.test('Schedule Module', async (t) => {
const cron = Deno.cron;
// @ts-ignore:next-line
Deno.cron = spy();

class TestListener {
@Cron(CronExpression.EVERY_MINUTE)
runEachMinute() {}
}

@Module({
imports: [ScheduleModule],
injectables: [TestListener],
})
class TestModule {}

const application = new DanetApplication();
await application.init(TestModule);
await application.listen(0);

await t.step('cronjob was called', () => {
// @ts-ignore:next-line
assertSpyCallArg(Deno.cron, 0, 0, 'runEachMinute');
// @ts-ignore:next-line
assertSpyCallArg(Deno.cron, 0, 1, CronExpression.EVERY_MINUTE);
});

await application.close();
Deno.cron = cron;
});

Deno.test('Timeout Module', async (t) => {
const time = new FakeTime();
const cb = spy();

class TestListener {
@Timeout(IntervalExpression.MILISECOND)
runEachMinute() {
cb();
}
}

@Module({
imports: [ScheduleModule],
injectables: [TestListener],
})
class TestModule {}

const application = new DanetApplication();
await application.init(TestModule);
await application.listen(0);

await t.step('should be called once after tick', async () => {
await time.tickAsync(IntervalExpression.MILISECOND);
assertEquals(cb.calls.length, 1);

await time.tickAsync(IntervalExpression.MILISECOND);
assertEquals(cb.calls.length, 1);
});

await application.close();
});

Deno.test('Interval Module', async (t) => {
const time = new FakeTime();
const cb = spy();

class TestListener {
@Interval(IntervalExpression.MILISECOND)
runEachMinute() {
cb();
}
}

@Module({
imports: [ScheduleModule],
injectables: [TestListener],
})
class TestModule {}

const application = new DanetApplication();
await application.init(TestModule);
await application.listen(0);

await t.step('should be called once after tick', async () => {
await time.tickAsync(IntervalExpression.MILISECOND);
assertEquals(cb.calls.length, 1);

await time.tickAsync(IntervalExpression.MILISECOND);
assertEquals(cb.calls.length, 2);

await time.tickAsync(IntervalExpression.MILISECOND);
assertEquals(cb.calls.length, 3);
});

await application.close();
});
2 changes: 2 additions & 0 deletions src/deps_test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export {
assertSpyCall,
assertSpyCallArg,
assertSpyCalls,
spy,
} from 'https://deno.land/[email protected]/testing/mock.ts';
export { FakeTime } from 'https://deno.land/[email protected]/testing/time.ts';
export {
assertEquals,
assertInstanceOf,
Expand Down
1 change: 1 addition & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './injector/mod.ts';
export * from './guard/mod.ts';
export * from './logger.ts';
export * from './events/mod.ts';
export * from './schedule/mod.ts';
3 changes: 3 additions & 0 deletions src/schedule/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const scheduleMetadataKey = 'task-scheduler';
export const intervalMetadataKey = 'interval';
export const timeoutMetadataKey = 'timeout';
16 changes: 16 additions & 0 deletions src/schedule/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SetMetadata } from '../metadata/decorator.ts';
import {
intervalMetadataKey,
scheduleMetadataKey,
timeoutMetadataKey,
} from './constants.ts';
import { CronString } from './types.ts';

export const Cron = (cron: CronString): MethodDecorator =>
SetMetadata(scheduleMetadataKey, { cron });

export const Interval = (interval: number): MethodDecorator =>
SetMetadata(intervalMetadataKey, { interval });

export const Timeout = (timeout: number): MethodDecorator =>
SetMetadata(timeoutMetadataKey, { timeout });
91 changes: 91 additions & 0 deletions src/schedule/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2017-2024 Kamil Mysliwiec MIT

export enum CronExpression {
EVERY_MINUTE = '*/1 * * * *',
EVERY_5_MINUTES = '*/5 * * * *',
EVERY_10_MINUTES = '*/10 * * * *',
EVERY_30_MINUTES = '*/30 * * * *',
EVERY_HOUR = '0 0-23/1 * * *',
EVERY_2_HOURS = '0 0-23/2 * * *',
EVERY_3_HOURS = '0 0-23/3 * * *',
EVERY_4_HOURS = '0 0-23/4 * * *',
EVERY_5_HOURS = '0 0-23/5 * * *',
EVERY_6_HOURS = '0 0-23/6 * * *',
EVERY_7_HOURS = '0 0-23/7 * * *',
EVERY_8_HOURS = '0 0-23/8 * * *',
EVERY_9_HOURS = '0 0-23/9 * * *',
EVERY_10_HOURS = '0 0-23/10 * * *',
EVERY_11_HOURS = '0 0-23/11 * * *',
EVERY_12_HOURS = '0 0-23/12 * * *',
EVERY_DAY_AT_1AM = '0 01 * * *',
EVERY_DAY_AT_2AM = '0 02 * * *',
EVERY_DAY_AT_3AM = '0 03 * * *',
EVERY_DAY_AT_4AM = '0 04 * * *',
EVERY_DAY_AT_5AM = '0 05 * * *',
EVERY_DAY_AT_6AM = '0 06 * * *',
EVERY_DAY_AT_7AM = '0 07 * * *',
EVERY_DAY_AT_8AM = '0 08 * * *',
EVERY_DAY_AT_9AM = '0 09 * * *',
EVERY_DAY_AT_10AM = '0 10 * * *',
EVERY_DAY_AT_11AM = '0 11 * * *',
EVERY_DAY_AT_NOON = '0 12 * * *',
EVERY_DAY_AT_1PM = '0 13 * * *',
EVERY_DAY_AT_2PM = '0 14 * * *',
EVERY_DAY_AT_3PM = '0 15 * * *',
EVERY_DAY_AT_4PM = '0 16 * * *',
EVERY_DAY_AT_5PM = '0 17 * * *',
EVERY_DAY_AT_6PM = '0 18 * * *',
EVERY_DAY_AT_7PM = '0 19 * * *',
EVERY_DAY_AT_8PM = '0 20 * * *',
EVERY_DAY_AT_9PM = '0 21 * * *',
EVERY_DAY_AT_10PM = '0 22 * * *',
EVERY_DAY_AT_11PM = '0 23 * * *',
EVERY_DAY_AT_MIDNIGHT = '0 0 * * *',
EVERY_WEEK = '0 0 * * 0',
EVERY_WEEKDAY = '0 0 * * 1-5',
EVERY_WEEKEND = '0 0 * * 6,0',
EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT = '0 0 1 * *',
EVERY_1ST_DAY_OF_MONTH_AT_NOON = '0 12 1 * *',
EVERY_2ND_HOUR = '0 */2 * * *',
EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM = '0 1-23/2 * * *',
EVERY_2ND_MONTH = '0 0 1 */2 *',
EVERY_QUARTER = '0 0 1 */3 *',
EVERY_6_MONTHS = '0 0 1 */6 *',
EVERY_YEAR = '0 0 1 0 *',
EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM = '0 */30 9-17 * * *',
EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM = '0 */30 9-18 * * *',
EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM = '0 */30 10-19 * * *',
MONDAY_TO_FRIDAY_AT_1AM = '0 0 01 * * 1-5',
MONDAY_TO_FRIDAY_AT_2AM = '0 0 02 * * 1-5',
MONDAY_TO_FRIDAY_AT_3AM = '0 0 03 * * 1-5',
MONDAY_TO_FRIDAY_AT_4AM = '0 0 04 * * 1-5',
MONDAY_TO_FRIDAY_AT_5AM = '0 0 05 * * 1-5',
MONDAY_TO_FRIDAY_AT_6AM = '0 0 06 * * 1-5',
MONDAY_TO_FRIDAY_AT_7AM = '0 0 07 * * 1-5',
MONDAY_TO_FRIDAY_AT_8AM = '0 0 08 * * 1-5',
MONDAY_TO_FRIDAY_AT_9AM = '0 0 09 * * 1-5',
MONDAY_TO_FRIDAY_AT_09_30AM = '0 30 09 * * 1-5',
MONDAY_TO_FRIDAY_AT_10AM = '0 0 10 * * 1-5',
MONDAY_TO_FRIDAY_AT_11AM = '0 0 11 * * 1-5',
MONDAY_TO_FRIDAY_AT_11_30AM = '0 30 11 * * 1-5',
MONDAY_TO_FRIDAY_AT_12PM = '0 0 12 * * 1-5',
MONDAY_TO_FRIDAY_AT_1PM = '0 0 13 * * 1-5',
MONDAY_TO_FRIDAY_AT_2PM = '0 0 14 * * 1-5',
MONDAY_TO_FRIDAY_AT_3PM = '0 0 15 * * 1-5',
MONDAY_TO_FRIDAY_AT_4PM = '0 0 16 * * 1-5',
MONDAY_TO_FRIDAY_AT_5PM = '0 0 17 * * 1-5',
MONDAY_TO_FRIDAY_AT_6PM = '0 0 18 * * 1-5',
MONDAY_TO_FRIDAY_AT_7PM = '0 0 19 * * 1-5',
MONDAY_TO_FRIDAY_AT_8PM = '0 0 20 * * 1-5',
MONDAY_TO_FRIDAY_AT_9PM = '0 0 21 * * 1-5',
MONDAY_TO_FRIDAY_AT_10PM = '0 0 22 * * 1-5',
MONDAY_TO_FRIDAY_AT_11PM = '0 0 23 * * 1-5',
}

export enum IntervalExpression {
MILISECOND = 1,
SECOND = 1000,
MINUTE = 1000 * 60,
HOUR = 1000 * 60 * 60,
DAY = 1000 * 60 * 60 * 24,
}
3 changes: 3 additions & 0 deletions src/schedule/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './decorator.ts';
export * from './module.ts';
export * from './enum.ts';
Loading

0 comments on commit cc251f0

Please sign in to comment.