@namnguyen.repl.it/nn-scheduler 1.0.0 → 2.0.0
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/dist/scheduler.d.ts +45 -2
- package/dist/scheduler.js +222 -35
- package/dist/types/SchedulerType.d.ts +18 -2
- package/package.json +3 -1
package/dist/scheduler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IntervalTask, ScheduledTask, SchedulerConfig } from
|
|
1
|
+
import { IntervalTask, ScheduledTask, SchedulerConfig, JobRegistry, JobRecord } from "./types/SchedulerType";
|
|
2
2
|
/**
|
|
3
3
|
* Scheduler class for managing background tasks in Node.js, designed to run under PM2.
|
|
4
4
|
* Supports two types of tasks:
|
|
@@ -11,11 +11,54 @@ export declare class Scheduler {
|
|
|
11
11
|
private logHandler;
|
|
12
12
|
private cronJobs;
|
|
13
13
|
private intervalIds;
|
|
14
|
+
private db;
|
|
15
|
+
private jobHandlers;
|
|
16
|
+
private jobPollerInterval;
|
|
14
17
|
/**
|
|
15
18
|
* Creates a new Scheduler instance
|
|
16
|
-
* @param config Configuration object with optional logHandler
|
|
19
|
+
* @param config Configuration object with optional logHandler and dbPath
|
|
17
20
|
*/
|
|
18
21
|
constructor(config?: SchedulerConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Initializes the SQLite database schema for job persistence
|
|
24
|
+
*/
|
|
25
|
+
private initializeDatabase;
|
|
26
|
+
/**
|
|
27
|
+
* Registers job handlers that can be invoked by name
|
|
28
|
+
* @param jobs Object mapping job names to handler functions
|
|
29
|
+
*/
|
|
30
|
+
registerJobs(jobs: JobRegistry): void;
|
|
31
|
+
/**
|
|
32
|
+
* Schedules a one-time job to run at a specific time
|
|
33
|
+
* @param time Time to run in "HH:MM" format (24-hour)
|
|
34
|
+
* @param jobName Name of the registered job handler
|
|
35
|
+
* @param data Data to pass to the job handler (can be any JSON-serializable value)
|
|
36
|
+
* @returns The job ID
|
|
37
|
+
*/
|
|
38
|
+
job(time: string, jobName: string, data?: any): number;
|
|
39
|
+
/**
|
|
40
|
+
* Starts the internal job poller that checks for pending jobs every minute
|
|
41
|
+
*/
|
|
42
|
+
private startJobPoller;
|
|
43
|
+
/**
|
|
44
|
+
* Polls the database for pending jobs that are ready to execute
|
|
45
|
+
*/
|
|
46
|
+
private pollJobs;
|
|
47
|
+
/**
|
|
48
|
+
* Executes a single job and updates its status
|
|
49
|
+
*/
|
|
50
|
+
private executeJob;
|
|
51
|
+
/**
|
|
52
|
+
* Gets all pending jobs from the database
|
|
53
|
+
* @returns Array of pending job records
|
|
54
|
+
*/
|
|
55
|
+
getPendingJobs(): JobRecord[];
|
|
56
|
+
/**
|
|
57
|
+
* Cancels a scheduled job by ID
|
|
58
|
+
* @param jobId The job ID to cancel
|
|
59
|
+
* @returns True if job was cancelled, false if not found
|
|
60
|
+
*/
|
|
61
|
+
cancelJob(jobId: number): boolean;
|
|
19
62
|
/**
|
|
20
63
|
* Converts a time string and weekdays into a cron expression
|
|
21
64
|
* @param time Time in "HH:MM" format
|
package/dist/scheduler.js
CHANGED
|
@@ -41,17 +41,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
41
41
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
44
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
48
|
exports.Scheduler = void 0;
|
|
46
49
|
const cron = __importStar(require("node-cron"));
|
|
47
50
|
const fs = __importStar(require("fs"));
|
|
48
51
|
const path = __importStar(require("path"));
|
|
52
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
49
53
|
/** Writes log messages to a file named by date (e.g., "2025-03-24.log") in the current directory */
|
|
50
54
|
const defaultLogHandler = (message) => {
|
|
51
|
-
const date = new Date().toISOString().split(
|
|
55
|
+
const date = new Date().toISOString().split("T")[0];
|
|
52
56
|
const logFile = path.join(process.cwd(), `${date}.log`);
|
|
53
57
|
const logMessage = `${new Date().toISOString()} - ${message}\n`;
|
|
54
|
-
fs.appendFileSync(logFile, logMessage, { encoding:
|
|
58
|
+
fs.appendFileSync(logFile, logMessage, { encoding: "utf8" });
|
|
55
59
|
};
|
|
56
60
|
/**
|
|
57
61
|
* Scheduler class for managing background tasks in Node.js, designed to run under PM2.
|
|
@@ -64,14 +68,174 @@ const defaultLogHandler = (message) => {
|
|
|
64
68
|
class Scheduler {
|
|
65
69
|
/**
|
|
66
70
|
* Creates a new Scheduler instance
|
|
67
|
-
* @param config Configuration object with optional logHandler
|
|
71
|
+
* @param config Configuration object with optional logHandler and dbPath
|
|
68
72
|
*/
|
|
69
73
|
constructor(config = {}) {
|
|
70
74
|
this.cronJobs = [];
|
|
71
75
|
this.intervalIds = [];
|
|
76
|
+
this.jobHandlers = new Map();
|
|
77
|
+
this.jobPollerInterval = null;
|
|
72
78
|
this.logHandler = config.logHandler || defaultLogHandler;
|
|
79
|
+
const dbPath = config.dbPath || path.join(process.cwd(), "scheduler.db");
|
|
80
|
+
this.db = new better_sqlite3_1.default(dbPath);
|
|
81
|
+
this.initializeDatabase();
|
|
82
|
+
this.startJobPoller();
|
|
73
83
|
this.setupPM2();
|
|
74
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Initializes the SQLite database schema for job persistence
|
|
87
|
+
*/
|
|
88
|
+
initializeDatabase() {
|
|
89
|
+
this.db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
91
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
|
+
job_name TEXT NOT NULL,
|
|
93
|
+
scheduled_at TEXT NOT NULL,
|
|
94
|
+
data TEXT,
|
|
95
|
+
status TEXT DEFAULT 'pending',
|
|
96
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
+
error TEXT
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_jobs_status_scheduled ON jobs(status, scheduled_at)`);
|
|
101
|
+
this.logHandler("Database initialized");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Registers job handlers that can be invoked by name
|
|
105
|
+
* @param jobs Object mapping job names to handler functions
|
|
106
|
+
*/
|
|
107
|
+
registerJobs(jobs) {
|
|
108
|
+
for (const [name, handler] of Object.entries(jobs)) {
|
|
109
|
+
this.jobHandlers.set(name, handler);
|
|
110
|
+
this.logHandler(`Registered job handler: ${name}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Schedules a one-time job to run at a specific time
|
|
115
|
+
* @param time Time to run in "HH:MM" format (24-hour)
|
|
116
|
+
* @param jobName Name of the registered job handler
|
|
117
|
+
* @param data Data to pass to the job handler (can be any JSON-serializable value)
|
|
118
|
+
* @returns The job ID
|
|
119
|
+
*/
|
|
120
|
+
job(time, jobName, data) {
|
|
121
|
+
// Validate time format
|
|
122
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
123
|
+
if (isNaN(hours) ||
|
|
124
|
+
isNaN(minutes) ||
|
|
125
|
+
hours < 0 ||
|
|
126
|
+
hours > 23 ||
|
|
127
|
+
minutes < 0 ||
|
|
128
|
+
minutes > 59) {
|
|
129
|
+
throw new Error(`Invalid time format: ${time}`);
|
|
130
|
+
}
|
|
131
|
+
// Check if job handler exists
|
|
132
|
+
if (!this.jobHandlers.has(jobName)) {
|
|
133
|
+
throw new Error(`Job handler not registered: ${jobName}`);
|
|
134
|
+
}
|
|
135
|
+
// Calculate scheduled_at datetime
|
|
136
|
+
const now = new Date();
|
|
137
|
+
const scheduledAt = new Date(now);
|
|
138
|
+
scheduledAt.setHours(hours, minutes, 0, 0);
|
|
139
|
+
// If time has passed today, schedule for tomorrow
|
|
140
|
+
if (scheduledAt <= now) {
|
|
141
|
+
scheduledAt.setDate(scheduledAt.getDate() + 1);
|
|
142
|
+
}
|
|
143
|
+
const scheduledAtStr = scheduledAt.toISOString();
|
|
144
|
+
const dataStr = data !== undefined ? JSON.stringify(data) : null;
|
|
145
|
+
const stmt = this.db.prepare(`
|
|
146
|
+
INSERT INTO jobs (job_name, scheduled_at, data, status)
|
|
147
|
+
VALUES (?, ?, ?, 'pending')
|
|
148
|
+
`);
|
|
149
|
+
const result = stmt.run(jobName, scheduledAtStr, dataStr);
|
|
150
|
+
this.logHandler(`Scheduled job '${jobName}' for ${scheduledAtStr} (ID: ${result.lastInsertRowid})`);
|
|
151
|
+
return result.lastInsertRowid;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts the internal job poller that checks for pending jobs every minute
|
|
155
|
+
*/
|
|
156
|
+
startJobPoller() {
|
|
157
|
+
// Run immediately on start
|
|
158
|
+
this.pollJobs();
|
|
159
|
+
// Then run every minute
|
|
160
|
+
this.jobPollerInterval = setInterval(() => {
|
|
161
|
+
this.pollJobs();
|
|
162
|
+
}, 60000); // 60 seconds
|
|
163
|
+
this.logHandler("Job poller started (runs every minute)");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Polls the database for pending jobs that are ready to execute
|
|
167
|
+
*/
|
|
168
|
+
pollJobs() {
|
|
169
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
170
|
+
const now = new Date().toISOString();
|
|
171
|
+
const stmt = this.db.prepare(`
|
|
172
|
+
SELECT * FROM jobs
|
|
173
|
+
WHERE status = 'pending' AND scheduled_at <= ?
|
|
174
|
+
`);
|
|
175
|
+
const pendingJobs = stmt.all(now);
|
|
176
|
+
for (const job of pendingJobs) {
|
|
177
|
+
yield this.executeJob(job);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Executes a single job and updates its status
|
|
183
|
+
*/
|
|
184
|
+
executeJob(job) {
|
|
185
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
const handler = this.jobHandlers.get(job.job_name);
|
|
187
|
+
if (!handler) {
|
|
188
|
+
this.logHandler(`Job handler not found: ${job.job_name} (ID: ${job.id})`);
|
|
189
|
+
this.db
|
|
190
|
+
.prepare(`UPDATE jobs SET status = 'failed', error = ? WHERE id = ?`)
|
|
191
|
+
.run("Handler not found", job.id);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Mark as processing
|
|
195
|
+
this.db
|
|
196
|
+
.prepare(`UPDATE jobs SET status = 'processing' WHERE id = ?`)
|
|
197
|
+
.run(job.id);
|
|
198
|
+
try {
|
|
199
|
+
const data = job.data ? JSON.parse(job.data) : undefined;
|
|
200
|
+
this.logHandler(`Executing job '${job.job_name}' (ID: ${job.id})`);
|
|
201
|
+
yield handler(data);
|
|
202
|
+
// Mark as completed and delete
|
|
203
|
+
this.db.prepare(`DELETE FROM jobs WHERE id = ?`).run(job.id);
|
|
204
|
+
this.logHandler(`Job '${job.job_name}' completed and removed (ID: ${job.id})`);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
208
|
+
this.db
|
|
209
|
+
.prepare(`UPDATE jobs SET status = 'failed', error = ? WHERE id = ?`)
|
|
210
|
+
.run(errorMessage, job.id);
|
|
211
|
+
this.logHandler(`Job '${job.job_name}' failed (ID: ${job.id}): ${errorMessage}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Gets all pending jobs from the database
|
|
217
|
+
* @returns Array of pending job records
|
|
218
|
+
*/
|
|
219
|
+
getPendingJobs() {
|
|
220
|
+
return this.db
|
|
221
|
+
.prepare(`SELECT * FROM jobs WHERE status = 'pending' ORDER BY scheduled_at`)
|
|
222
|
+
.all();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Cancels a scheduled job by ID
|
|
226
|
+
* @param jobId The job ID to cancel
|
|
227
|
+
* @returns True if job was cancelled, false if not found
|
|
228
|
+
*/
|
|
229
|
+
cancelJob(jobId) {
|
|
230
|
+
const result = this.db
|
|
231
|
+
.prepare(`DELETE FROM jobs WHERE id = ? AND status = 'pending'`)
|
|
232
|
+
.run(jobId);
|
|
233
|
+
if (result.changes > 0) {
|
|
234
|
+
this.logHandler(`Cancelled job ID: ${jobId}`);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
75
239
|
/**
|
|
76
240
|
* Converts a time string and weekdays into a cron expression
|
|
77
241
|
* @param time Time in "HH:MM" format
|
|
@@ -80,11 +244,16 @@ class Scheduler {
|
|
|
80
244
|
* @throws Error if time format is invalid
|
|
81
245
|
*/
|
|
82
246
|
timeToCron(time, weekDays) {
|
|
83
|
-
const [hours, minutes] = time.split(
|
|
84
|
-
if (isNaN(hours) ||
|
|
247
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
248
|
+
if (isNaN(hours) ||
|
|
249
|
+
isNaN(minutes) ||
|
|
250
|
+
hours < 0 ||
|
|
251
|
+
hours > 23 ||
|
|
252
|
+
minutes < 0 ||
|
|
253
|
+
minutes > 59) {
|
|
85
254
|
throw new Error(`Invalid time format: ${time}`);
|
|
86
255
|
}
|
|
87
|
-
const weekDayStr = weekDays.includes(
|
|
256
|
+
const weekDayStr = weekDays.includes("*") ? "*" : weekDays.join(",");
|
|
88
257
|
return `${minutes} ${hours} * * ${weekDayStr}`;
|
|
89
258
|
}
|
|
90
259
|
// Converts a time string and matchPattern into a cron expression
|
|
@@ -93,30 +262,37 @@ class Scheduler {
|
|
|
93
262
|
// @returns Cron expression string
|
|
94
263
|
// @throws Error if time or pattern is invalid
|
|
95
264
|
patternToCron(time, matchPattern) {
|
|
96
|
-
const [hours, minutes] = time.split(
|
|
97
|
-
if (isNaN(hours) ||
|
|
265
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
266
|
+
if (isNaN(hours) ||
|
|
267
|
+
isNaN(minutes) ||
|
|
268
|
+
hours < 0 ||
|
|
269
|
+
hours > 23 ||
|
|
270
|
+
minutes < 0 ||
|
|
271
|
+
minutes > 59) {
|
|
98
272
|
throw new Error(`Invalid time format: ${time}`);
|
|
99
273
|
}
|
|
100
274
|
// Validate pattern format (yyyy/mm/dd with ** wildcards)
|
|
101
275
|
if (!matchPattern.match(/^\d{4}\/(\d{2}|\*\*)\/(\d{2}|\*\*)$/)) {
|
|
102
276
|
throw new Error(`Invalid pattern format: ${matchPattern}`);
|
|
103
277
|
}
|
|
104
|
-
const [year, month, day] = matchPattern.split(
|
|
278
|
+
const [year, month, day] = matchPattern.split("/");
|
|
105
279
|
const yearNum = parseInt(year, 10);
|
|
106
|
-
const monthNum = month ===
|
|
107
|
-
const dayNum = day ===
|
|
280
|
+
const monthNum = month === "**" ? "*" : parseInt(month, 10);
|
|
281
|
+
const dayNum = day === "**" ? "*" : parseInt(day, 10);
|
|
108
282
|
// Validate year (basic range check, assuming 1970-2099 for cron compatibility)
|
|
109
|
-
if (isNaN(yearNum) ||
|
|
283
|
+
if (isNaN(yearNum) ||
|
|
284
|
+
yearNum < new Date().getFullYear() ||
|
|
285
|
+
yearNum > 3000) {
|
|
110
286
|
throw new Error(`Invalid year in pattern: ${matchPattern}`);
|
|
111
287
|
}
|
|
112
288
|
// Validate month//
|
|
113
289
|
//@ts-ignore
|
|
114
|
-
if (month !==
|
|
290
|
+
if (month !== "**" && (isNaN(monthNum) || monthNum < 1 || monthNum > 12)) {
|
|
115
291
|
throw new Error(`Invalid month in pattern: ${matchPattern}`);
|
|
116
292
|
}
|
|
117
293
|
// Validate day
|
|
118
294
|
//@ts-ignore
|
|
119
|
-
if (day !==
|
|
295
|
+
if (day !== "**" && (isNaN(dayNum) || dayNum < 1 || dayNum > 31)) {
|
|
120
296
|
throw new Error(`Invalid day in pattern: ${matchPattern}`);
|
|
121
297
|
}
|
|
122
298
|
// Note: Cron doesn't support year directly, so we rely on pattern matching in schedule
|
|
@@ -130,7 +306,7 @@ class Scheduler {
|
|
|
130
306
|
*/
|
|
131
307
|
isMatchingWeekDay(weekDays) {
|
|
132
308
|
const currentDay = new Date().getDay();
|
|
133
|
-
return weekDays.includes(
|
|
309
|
+
return weekDays.includes("*") || weekDays.includes(currentDay);
|
|
134
310
|
}
|
|
135
311
|
//Checks if the current date matches the specified match signalling
|
|
136
312
|
//@param matchPattern Pattern (e.g., '2025/**/** ', '2025 /06 /01')
|
|
@@ -144,12 +320,14 @@ class Scheduler {
|
|
|
144
320
|
if (!matchPattern.match(/^\d{4}\/(\d{2}|\*\*)\/(\d{2}|\*\*)$/)) {
|
|
145
321
|
return false;
|
|
146
322
|
}
|
|
147
|
-
const [patternYear, patternMonth, patternDay] = matchPattern.split(
|
|
323
|
+
const [patternYear, patternMonth, patternDay] = matchPattern.split("/");
|
|
148
324
|
const yearNum = parseInt(patternYear, 10);
|
|
149
|
-
const monthNum = patternMonth ===
|
|
150
|
-
const dayNum = patternDay ===
|
|
325
|
+
const monthNum = patternMonth === "**" ? month : parseInt(patternMonth, 10);
|
|
326
|
+
const dayNum = patternDay === "**" ? day : parseInt(patternDay, 10);
|
|
151
327
|
// Validate parsed values
|
|
152
|
-
if (isNaN(yearNum) ||
|
|
328
|
+
if (isNaN(yearNum) ||
|
|
329
|
+
(patternMonth !== "**" && isNaN(monthNum)) ||
|
|
330
|
+
(patternDay !== "**" && isNaN(dayNum))) {
|
|
153
331
|
return false;
|
|
154
332
|
}
|
|
155
333
|
// Check year match
|
|
@@ -157,17 +335,19 @@ class Scheduler {
|
|
|
157
335
|
return false;
|
|
158
336
|
}
|
|
159
337
|
// Check month match
|
|
160
|
-
if (patternMonth !==
|
|
338
|
+
if (patternMonth !== "**" && monthNum !== month) {
|
|
161
339
|
return false;
|
|
162
340
|
}
|
|
163
341
|
// Check day match
|
|
164
|
-
if (patternDay !==
|
|
342
|
+
if (patternDay !== "**" && dayNum !== day) {
|
|
165
343
|
return false;
|
|
166
344
|
}
|
|
167
345
|
// Validate date (e.g., avoid Feb 30)
|
|
168
|
-
if (patternMonth !==
|
|
346
|
+
if (patternMonth !== "**" && patternDay !== "**") {
|
|
169
347
|
const date = new Date(yearNum, monthNum - 1, dayNum);
|
|
170
|
-
if (date.getFullYear() !== yearNum ||
|
|
348
|
+
if (date.getFullYear() !== yearNum ||
|
|
349
|
+
date.getMonth() + 1 !== monthNum ||
|
|
350
|
+
date.getDate() !== dayNum) {
|
|
171
351
|
return false;
|
|
172
352
|
}
|
|
173
353
|
}
|
|
@@ -180,7 +360,7 @@ class Scheduler {
|
|
|
180
360
|
schedule(tasks) {
|
|
181
361
|
for (const task of tasks) {
|
|
182
362
|
if (!task.time) {
|
|
183
|
-
this.logHandler(
|
|
363
|
+
this.logHandler("No valid time provided for scheduled task, skipping");
|
|
184
364
|
continue;
|
|
185
365
|
}
|
|
186
366
|
// Weekdays trigger
|
|
@@ -201,7 +381,7 @@ class Scheduler {
|
|
|
201
381
|
}
|
|
202
382
|
}));
|
|
203
383
|
this.cronJobs.push(cronJob);
|
|
204
|
-
this.logHandler(`Scheduled task at ${task.time} with weekdays ${task.weekDays.join(
|
|
384
|
+
this.logHandler(`Scheduled task at ${task.time} with weekdays ${task.weekDays.join(",")}`);
|
|
205
385
|
}
|
|
206
386
|
catch (error) {
|
|
207
387
|
this.logHandler(`Error processing task at ${task.time} for weekdays: ${error}`);
|
|
@@ -280,7 +460,7 @@ class Scheduler {
|
|
|
280
460
|
}
|
|
281
461
|
}), task.interval);
|
|
282
462
|
this.intervalIds.push(intervalId);
|
|
283
|
-
this.logHandler(`Started interval task every ${task.interval / 1000} seconds with weekdays ${task.weekDays.join(
|
|
463
|
+
this.logHandler(`Started interval task every ${task.interval / 1000} seconds with weekdays ${task.weekDays.join(",")} and patterns ${task.matchPattern ? task.matchPattern.join(",") : "none"}`);
|
|
284
464
|
}
|
|
285
465
|
}
|
|
286
466
|
/**
|
|
@@ -288,26 +468,33 @@ class Scheduler {
|
|
|
288
468
|
* Called when PM2 sends a shutdown signal
|
|
289
469
|
*/
|
|
290
470
|
stop() {
|
|
291
|
-
this.cronJobs.forEach(job => job.stop());
|
|
471
|
+
this.cronJobs.forEach((job) => job.stop());
|
|
292
472
|
this.cronJobs = [];
|
|
293
|
-
this.logHandler(
|
|
294
|
-
this.intervalIds.forEach(id => clearInterval(id));
|
|
473
|
+
this.logHandler("All scheduled tasks stopped");
|
|
474
|
+
this.intervalIds.forEach((id) => clearInterval(id));
|
|
295
475
|
this.intervalIds = [];
|
|
296
|
-
this.logHandler(
|
|
476
|
+
this.logHandler("All interval tasks stopped");
|
|
477
|
+
if (this.jobPollerInterval) {
|
|
478
|
+
clearInterval(this.jobPollerInterval);
|
|
479
|
+
this.jobPollerInterval = null;
|
|
480
|
+
this.logHandler("Job poller stopped");
|
|
481
|
+
}
|
|
482
|
+
this.db.close();
|
|
483
|
+
this.logHandler("Database connection closed");
|
|
297
484
|
}
|
|
298
485
|
/**
|
|
299
486
|
* Sets up integration with PM2 by handling shutdown signals
|
|
300
487
|
* PM2 sends SIGINT when stopping the process
|
|
301
488
|
*/
|
|
302
489
|
setupPM2() {
|
|
303
|
-
process.on(
|
|
304
|
-
this.logHandler(
|
|
490
|
+
process.on("SIGINT", () => {
|
|
491
|
+
this.logHandler("Received SIGINT from PM2, stopping scheduler...");
|
|
305
492
|
this.stop();
|
|
306
493
|
process.exit(0);
|
|
307
494
|
});
|
|
308
|
-
process.on(
|
|
309
|
-
if (msg ===
|
|
310
|
-
this.logHandler(
|
|
495
|
+
process.on("message", (msg) => {
|
|
496
|
+
if (msg === "shutdown") {
|
|
497
|
+
this.logHandler("Received shutdown message from PM2, stopping scheduler...");
|
|
311
498
|
this.stop();
|
|
312
499
|
process.exit(0);
|
|
313
500
|
}
|
|
@@ -2,12 +2,28 @@
|
|
|
2
2
|
export type Task = () => void | Promise<void>;
|
|
3
3
|
/** A log handler is a function that processes log messages */
|
|
4
4
|
export type LogHandler = (message: string) => void;
|
|
5
|
+
/** A job handler receives data of any type and processes it */
|
|
6
|
+
export type JobHandler<T = any> = (data: T) => void | Promise<void>;
|
|
7
|
+
/** Registry of job handlers keyed by job name */
|
|
8
|
+
export type JobRegistry = Record<string, JobHandler>;
|
|
9
|
+
/** A stored job record in the database */
|
|
10
|
+
export interface JobRecord {
|
|
11
|
+
id: number;
|
|
12
|
+
job_name: string;
|
|
13
|
+
scheduled_at: string;
|
|
14
|
+
data: string;
|
|
15
|
+
status: "pending" | "processing" | "completed" | "failed";
|
|
16
|
+
created_at: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
5
19
|
/**
|
|
6
20
|
* Configuration options for the Scheduler
|
|
7
21
|
*/
|
|
8
22
|
export interface SchedulerConfig {
|
|
9
23
|
/** Optional custom log handler. If not provided, logs are written to daily files (e.g., "2025-03-24.log") */
|
|
10
24
|
logHandler?: LogHandler;
|
|
25
|
+
/** Path to SQLite database file for job persistence. Defaults to 'scheduler.db' in current directory */
|
|
26
|
+
dbPath?: string;
|
|
11
27
|
}
|
|
12
28
|
/**
|
|
13
29
|
* Definition of a scheduled task with time, matchPattern, and weekday constraints
|
|
@@ -19,7 +35,7 @@ export interface ScheduledTask {
|
|
|
19
35
|
/** The task to execute */
|
|
20
36
|
action: Task;
|
|
21
37
|
/** Array of weekdays to run on (0-6 for Sun-Sat, or '*' for all days) */
|
|
22
|
-
weekDays: Array<number |
|
|
38
|
+
weekDays: Array<number | "*">;
|
|
23
39
|
}
|
|
24
40
|
/**
|
|
25
41
|
* Definition of an interval task with time, matchPattern, and weekday constraints
|
|
@@ -30,5 +46,5 @@ export interface IntervalTask {
|
|
|
30
46
|
/** The task to execute */
|
|
31
47
|
action: Task;
|
|
32
48
|
/** Array of weekdays to run on (0-6 for Sun-Sat, or '*' for all days) */
|
|
33
|
-
weekDays: Array<number |
|
|
49
|
+
weekDays: Array<number | "*">;
|
|
34
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namnguyen.repl.it/nn-scheduler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A scheduler module for NodeJS",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"better-sqlite3": "^11.0.0",
|
|
14
15
|
"node-cron": "^3.0.3"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
18
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
17
19
|
"@babel/core": "^7.26.10",
|
|
18
20
|
"@babel/preset-env": "^7.26.9",
|
|
19
21
|
"@babel/preset-typescript": "^7.26.0",
|