@arikajs/scheduler 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/Contracts/Task.d.ts +14 -0
- package/dist/Contracts/Task.js +3 -0
- package/dist/Contracts/Task.js.map +1 -0
- package/dist/Event.d.ts +44 -0
- package/dist/Event.js +140 -0
- package/dist/Event.js.map +1 -0
- package/dist/Schedule.d.ts +24 -0
- package/dist/Schedule.js +48 -0
- package/dist/Schedule.js.map +1 -0
- package/dist/Scheduler.d.ts +27 -0
- package/dist/Scheduler.js +170 -0
- package/dist/Scheduler.js.map +1 -0
- package/dist/Worker.d.ts +11 -0
- package/dist/Worker.js +41 -0
- package/dist/Worker.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/Contracts/Task.ts +17 -0
- package/src/Event.ts +151 -0
- package/src/Schedule.ts +50 -0
- package/src/Scheduler.ts +189 -0
- package/src/Worker.ts +44 -0
- package/src/index.ts +6 -0
- package/tests/Scheduler.test.ts +105 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Scheduler = void 0;
|
|
4
|
+
const logging_1 = require("@arikajs/logging");
|
|
5
|
+
const Schedule_1 = require("./Schedule");
|
|
6
|
+
class Scheduler {
|
|
7
|
+
container;
|
|
8
|
+
schedule;
|
|
9
|
+
constructor(container) {
|
|
10
|
+
this.container = container;
|
|
11
|
+
this.schedule = new Schedule_1.Schedule();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Define the schedule.
|
|
15
|
+
*/
|
|
16
|
+
define(callback) {
|
|
17
|
+
callback(this.schedule);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run the scheduled tasks.
|
|
22
|
+
*/
|
|
23
|
+
async run(date = new Date()) {
|
|
24
|
+
const config = this.container.make('config');
|
|
25
|
+
const timezone = config.get('app.timezone', 'UTC');
|
|
26
|
+
const dueEvents = this.schedule.dueEvents(date, timezone);
|
|
27
|
+
if (dueEvents.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Leader Election (Prevent multiple instances from running the same schedule)
|
|
31
|
+
if (await this.shouldSkipBecauseAnotherInstanceIsRunning()) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
logging_1.Log.info(`Running ${dueEvents.length} scheduled tasks in parallel...`);
|
|
35
|
+
// Run events in parallel
|
|
36
|
+
await Promise.allSettled(dueEvents.map(event => this.runEvent(event)));
|
|
37
|
+
}
|
|
38
|
+
async shouldSkipBecauseAnotherInstanceIsRunning() {
|
|
39
|
+
if (!this.container.has('cache'))
|
|
40
|
+
return false;
|
|
41
|
+
const cache = this.container.make('cache');
|
|
42
|
+
const lockKey = 'framework/scheduler-leader-lock';
|
|
43
|
+
const isLeader = await cache.add(lockKey, true, 55); // Lock for 55 seconds
|
|
44
|
+
if (!isLeader) {
|
|
45
|
+
logging_1.Log.debug('Another instance is already running the scheduler. Skipping...');
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
async runEvent(event) {
|
|
51
|
+
const name = this.getEventName(event);
|
|
52
|
+
try {
|
|
53
|
+
await this.dispatchLifecycleEvent('TaskStarting', event);
|
|
54
|
+
// Check for overlapping
|
|
55
|
+
if (event.shouldSkipOverlapping()) {
|
|
56
|
+
const locked = await this.isLocked(event);
|
|
57
|
+
if (locked) {
|
|
58
|
+
logging_1.Log.debug(`Skipping task [${name}] as it is still running.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await this.lock(event);
|
|
62
|
+
}
|
|
63
|
+
logging_1.Log.info(`Running scheduled task: [${name}]`);
|
|
64
|
+
// Handle execution with Retries and Timeout
|
|
65
|
+
await this.executeWithRetries(event);
|
|
66
|
+
logging_1.Log.info(`Task [${name}] completed successfully.`);
|
|
67
|
+
await this.dispatchLifecycleEvent('TaskFinished', event);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
logging_1.Log.error(`Task [${name}] failed: ${e.message}`);
|
|
71
|
+
await this.dispatchLifecycleEvent('TaskFailed', event, { error: e });
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (event.shouldSkipOverlapping()) {
|
|
75
|
+
await this.unlock(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async executeWithRetries(event) {
|
|
80
|
+
const maxRetries = event.getRetries();
|
|
81
|
+
let attempt = 0;
|
|
82
|
+
while (attempt <= maxRetries) {
|
|
83
|
+
try {
|
|
84
|
+
await this.executeWithTimeout(event);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
attempt++;
|
|
89
|
+
if (attempt > maxRetries)
|
|
90
|
+
throw e;
|
|
91
|
+
const delay = event.getRetryDelay();
|
|
92
|
+
if (delay > 0) {
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, delay * 1000));
|
|
94
|
+
}
|
|
95
|
+
logging_1.Log.warning(`Retrying task [${this.getEventName(event)}] (Attempt ${attempt}/${maxRetries})...`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async executeWithTimeout(event) {
|
|
100
|
+
const timeoutSeconds = event.getTimeout();
|
|
101
|
+
if (timeoutSeconds <= 0) {
|
|
102
|
+
return this.actuallyRun(event);
|
|
103
|
+
}
|
|
104
|
+
return new Promise(async (resolve, reject) => {
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
reject(new Error(`Task timed out after ${timeoutSeconds} seconds.`));
|
|
107
|
+
}, timeoutSeconds * 1000);
|
|
108
|
+
try {
|
|
109
|
+
await this.actuallyRun(event);
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
resolve();
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
reject(e);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async actuallyRun(event) {
|
|
120
|
+
if (typeof event.command === 'string') {
|
|
121
|
+
const { CommandRegistry } = await import('@arikajs/console');
|
|
122
|
+
const registry = this.container.make(CommandRegistry);
|
|
123
|
+
await registry.run([event.command]);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await event.run();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async dispatchLifecycleEvent(type, event, extra = {}) {
|
|
130
|
+
// Only if @arikajs/events is available
|
|
131
|
+
try {
|
|
132
|
+
const { Event: Emitter } = await import('@arikajs/events');
|
|
133
|
+
await Emitter.dispatch({
|
|
134
|
+
type: `scheduler.${type}`,
|
|
135
|
+
task: this.getEventName(event),
|
|
136
|
+
expression: event.expression(),
|
|
137
|
+
...extra
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
// Events package might not be installed, ignore silently
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
getEventName(event) {
|
|
145
|
+
return event.getDescription() || (typeof event.command === 'string' ? event.command : 'closure');
|
|
146
|
+
}
|
|
147
|
+
async isLocked(event) {
|
|
148
|
+
if (!this.container.has('cache'))
|
|
149
|
+
return false;
|
|
150
|
+
const cache = this.container.make('cache');
|
|
151
|
+
return await cache.has(this.getMutexName(event));
|
|
152
|
+
}
|
|
153
|
+
async lock(event) {
|
|
154
|
+
if (!this.container.has('cache'))
|
|
155
|
+
return;
|
|
156
|
+
const cache = this.container.make('cache');
|
|
157
|
+
await cache.put(this.getMutexName(event), true, event.mutexExpiration());
|
|
158
|
+
}
|
|
159
|
+
async unlock(event) {
|
|
160
|
+
if (!this.container.has('cache'))
|
|
161
|
+
return;
|
|
162
|
+
const cache = this.container.make('cache');
|
|
163
|
+
await cache.forget(this.getMutexName(event));
|
|
164
|
+
}
|
|
165
|
+
getMutexName(event) {
|
|
166
|
+
return `framework/schedule-${Buffer.from(this.getEventName(event) + event.expression()).toString('base64')}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.Scheduler = Scheduler;
|
|
170
|
+
//# sourceMappingURL=Scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Scheduler.js","sourceRoot":"","sources":["../src/Scheduler.ts"],"names":[],"mappings":"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"}
|
package/dist/Worker.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Scheduler } from './Scheduler';
|
|
2
|
+
export declare class Worker {
|
|
3
|
+
protected scheduler: Scheduler;
|
|
4
|
+
protected stopped: boolean;
|
|
5
|
+
protected running: boolean;
|
|
6
|
+
constructor(scheduler: Scheduler);
|
|
7
|
+
/**
|
|
8
|
+
* Start the scheduler worker.
|
|
9
|
+
*/
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
}
|
package/dist/Worker.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Worker = void 0;
|
|
4
|
+
const logging_1 = require("@arikajs/logging");
|
|
5
|
+
class Worker {
|
|
6
|
+
scheduler;
|
|
7
|
+
stopped = false;
|
|
8
|
+
running = false;
|
|
9
|
+
constructor(scheduler) {
|
|
10
|
+
this.scheduler = scheduler;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Start the scheduler worker.
|
|
14
|
+
*/
|
|
15
|
+
async start() {
|
|
16
|
+
logging_1.Log.info('Scheduler worker started. Press Ctrl+C to stop.');
|
|
17
|
+
// Graceful shutdown
|
|
18
|
+
const shutdown = () => {
|
|
19
|
+
this.stopped = true;
|
|
20
|
+
logging_1.Log.info('Stopping scheduler worker gracefully...');
|
|
21
|
+
};
|
|
22
|
+
process.on('SIGINT', shutdown);
|
|
23
|
+
process.on('SIGTERM', shutdown);
|
|
24
|
+
while (!this.stopped) {
|
|
25
|
+
const now = new Date();
|
|
26
|
+
// Round down to the minute
|
|
27
|
+
now.setSeconds(0, 0);
|
|
28
|
+
this.running = true;
|
|
29
|
+
await this.scheduler.run(now);
|
|
30
|
+
this.running = false;
|
|
31
|
+
if (this.stopped)
|
|
32
|
+
break;
|
|
33
|
+
// Wait until the next minute
|
|
34
|
+
const waitTime = 60000 - (Date.now() % 60000);
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
36
|
+
}
|
|
37
|
+
logging_1.Log.info('Scheduler worker stopped.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.Worker = Worker;
|
|
41
|
+
//# sourceMappingURL=Worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Worker.js","sourceRoot":"","sources":["../src/Worker.ts"],"names":[],"mappings":";;;AAEA,8CAAuC;AAEvC,MAAa,MAAM;IAIO;IAHZ,OAAO,GAAY,KAAK,CAAC;IACzB,OAAO,GAAY,KAAK,CAAC;IAEnC,YAAsB,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAI,CAAC;IAE/C;;OAEG;IACI,KAAK,CAAC,KAAK;QACd,aAAG,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;QAE5D,oBAAoB;QACpB,MAAM,QAAQ,GAAG,GAAG,EAAE;YAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,aAAG,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;QACxD,CAAC,CAAC;QAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEhC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,2BAA2B;YAC3B,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAErB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;YAErB,IAAI,IAAI,CAAC,OAAO;gBAAE,MAAM;YAExB,6BAA6B;YAC7B,MAAM,QAAQ,GAAG,KAAK,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;YAC9C,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QAChE,CAAC;QAED,aAAG,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAC1C,CAAC;CACJ;AAvCD,wBAuCC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./Scheduler"), exports);
|
|
18
|
+
__exportStar(require("./Schedule"), exports);
|
|
19
|
+
__exportStar(require("./Event"), exports);
|
|
20
|
+
__exportStar(require("./Worker"), exports);
|
|
21
|
+
__exportStar(require("./Contracts/Task"), exports);
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,8CAA4B;AAC5B,6CAA2B;AAC3B,0CAAwB;AACxB,2CAAyB;AACzB,mDAAiC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arikajs/scheduler",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Task scheduling for ArikaJS.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"test": "npx tsx --test tests/**/*.test.ts",
|
|
12
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@arikajs/foundation": "*",
|
|
16
|
+
"@arikajs/logging": "*",
|
|
17
|
+
"@arikajs/queue": "*",
|
|
18
|
+
"@arikajs/console": "*",
|
|
19
|
+
"@arikajs/cache": "*",
|
|
20
|
+
"@arikajs/events": "*",
|
|
21
|
+
"cron-parser": "^4.9.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.11.24",
|
|
25
|
+
"typescript": "^5.3.3",
|
|
26
|
+
"tsx": "^4.7.1"
|
|
27
|
+
},
|
|
28
|
+
"author": "Prakash Tank",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/ArikaJs/arikajs.git",
|
|
32
|
+
"directory": "packages/scheduler"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/ArikaJs/arikajs/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/ArikaJs/arikajs/tree/main/packages/scheduler#readme"
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
export interface Task {
|
|
3
|
+
/**
|
|
4
|
+
* Run the scheduled task.
|
|
5
|
+
*/
|
|
6
|
+
run(): Promise<void> | void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the cron expression for the task.
|
|
10
|
+
*/
|
|
11
|
+
expression(): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Determine if the task is due to run.
|
|
15
|
+
*/
|
|
16
|
+
isDue(date: Date, timezone?: string): boolean;
|
|
17
|
+
}
|
package/src/Event.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
import { Task } from './Contracts/Task';
|
|
3
|
+
import cronParser from 'cron-parser';
|
|
4
|
+
|
|
5
|
+
export class Event implements Task {
|
|
6
|
+
protected cronExpression: string = '* * * * *';
|
|
7
|
+
protected timezoneVal?: string;
|
|
8
|
+
protected withoutOverlappingVal: boolean = false;
|
|
9
|
+
protected expiresAt: number = 1440; // 24 hours default for overlap lock
|
|
10
|
+
protected successCallbacks: Function[] = [];
|
|
11
|
+
protected failureCallbacks: (Function)[] = [];
|
|
12
|
+
protected timeoutVal: number = 0;
|
|
13
|
+
protected retriesVal: number = 0;
|
|
14
|
+
protected retryDelayVal: number = 0;
|
|
15
|
+
protected descriptionVal: string = '';
|
|
16
|
+
|
|
17
|
+
constructor(public readonly command: string | Function) { }
|
|
18
|
+
|
|
19
|
+
public name(description: string): this {
|
|
20
|
+
this.descriptionVal = description;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public timeout(seconds: number): this {
|
|
25
|
+
this.timeoutVal = seconds;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public retry(times: number, delay: number = 0): this {
|
|
30
|
+
this.retriesVal = times;
|
|
31
|
+
this.retryDelayVal = delay;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public cron(expression: string): this {
|
|
36
|
+
this.cronExpression = expression;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public timezone(timezone: string): this {
|
|
41
|
+
this.timezoneVal = timezone;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public withoutOverlapping(expiresAt: number = 1440): this {
|
|
46
|
+
this.withoutOverlappingVal = true;
|
|
47
|
+
this.expiresAt = expiresAt;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public onSuccess(callback: Function): this {
|
|
52
|
+
this.successCallbacks.push(callback);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public onFailure(callback: Function): this {
|
|
57
|
+
this.failureCallbacks.push(callback);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Frequencies
|
|
62
|
+
public everySecond(): this { return this.cron('* * * * * *'); }
|
|
63
|
+
public everyMinute(): this { return this.cron('* * * * *'); }
|
|
64
|
+
public everyFiveMinutes(): this { return this.cron('*/5 * * * *'); }
|
|
65
|
+
public everyTenMinutes(): this { return this.cron('*/10 * * * *'); }
|
|
66
|
+
public hourly(): this { return this.cron('0 * * * *'); }
|
|
67
|
+
public hourlyAt(minute: number): this { return this.cron(`${minute} * * * *`); }
|
|
68
|
+
public daily(): this { return this.cron('0 0 * * *'); }
|
|
69
|
+
public dailyAt(time: string): this {
|
|
70
|
+
const [hour, minute] = time.split(':');
|
|
71
|
+
return this.cron(`${minute} ${hour} * * *`);
|
|
72
|
+
}
|
|
73
|
+
public weekly(): this { return this.cron('0 0 * * 0'); }
|
|
74
|
+
public weeklyOn(day: number, time: string = '00:00'): this {
|
|
75
|
+
const [hour, minute] = time.split(':');
|
|
76
|
+
return this.cron(`${minute} ${hour} * * ${day}`);
|
|
77
|
+
}
|
|
78
|
+
public monthly(): this { return this.cron('0 0 1 * *'); }
|
|
79
|
+
public monthlyOn(day: number, time: string = '00:00'): this {
|
|
80
|
+
const [hour, minute] = time.split(':');
|
|
81
|
+
return this.cron(`${minute} ${hour} ${day} * *`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public expression(): string {
|
|
85
|
+
return this.cronExpression;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public isDue(date: Date, timezone?: string): boolean {
|
|
89
|
+
const options = {
|
|
90
|
+
currentDate: new Date(date.getTime() - 1000),
|
|
91
|
+
tz: this.timezoneVal || timezone
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const interval = cronParser.parseExpression(this.cronExpression, options);
|
|
96
|
+
const next = interval.next().toDate();
|
|
97
|
+
|
|
98
|
+
return next.getMinutes() === date.getMinutes() &&
|
|
99
|
+
next.getHours() === date.getHours() &&
|
|
100
|
+
next.getDate() === date.getDate() &&
|
|
101
|
+
next.getMonth() === date.getMonth() &&
|
|
102
|
+
next.getFullYear() === date.getFullYear();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public async run(): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof this.command === 'function') {
|
|
111
|
+
await this.command();
|
|
112
|
+
} else {
|
|
113
|
+
// For commands, we would usually use the CommandRegistry
|
|
114
|
+
// But this will be handled by the Scheduler runner
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const callback of this.successCallbacks) {
|
|
118
|
+
await callback();
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
for (const callback of this.failureCallbacks) {
|
|
122
|
+
await callback(e);
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public shouldSkipOverlapping(): boolean {
|
|
129
|
+
return this.withoutOverlappingVal;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public mutexExpiration(): number {
|
|
133
|
+
return this.expiresAt;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public getTimeout(): number {
|
|
137
|
+
return this.timeoutVal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public getRetries(): number {
|
|
141
|
+
return this.retriesVal;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public getRetryDelay(): number {
|
|
145
|
+
return this.retryDelayVal;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public getDescription(): string {
|
|
149
|
+
return this.descriptionVal;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/Schedule.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
import { Event } from './Event';
|
|
3
|
+
|
|
4
|
+
export class Schedule {
|
|
5
|
+
protected events: Event[] = [];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add a new callback event to the schedule.
|
|
9
|
+
*/
|
|
10
|
+
public call(callback: Function): Event {
|
|
11
|
+
const event = new Event(callback);
|
|
12
|
+
this.events.push(event);
|
|
13
|
+
return event;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add a new command event to the schedule.
|
|
18
|
+
*/
|
|
19
|
+
public command(command: string): Event {
|
|
20
|
+
const event = new Event(command);
|
|
21
|
+
this.events.push(event);
|
|
22
|
+
return event;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add a new job event to the schedule.
|
|
27
|
+
*/
|
|
28
|
+
public job(job: any): Event {
|
|
29
|
+
const event = new Event(async () => {
|
|
30
|
+
const { Queue } = await import('@arikajs/queue');
|
|
31
|
+
await Queue.dispatch(job);
|
|
32
|
+
});
|
|
33
|
+
this.events.push(event);
|
|
34
|
+
return event;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get all events.
|
|
39
|
+
*/
|
|
40
|
+
public allEvents(): Event[] {
|
|
41
|
+
return this.events;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get due events.
|
|
46
|
+
*/
|
|
47
|
+
public dueEvents(date: Date, timezone?: string): Event[] {
|
|
48
|
+
return this.events.filter(event => event.isDue(date, timezone));
|
|
49
|
+
}
|
|
50
|
+
}
|