webspresso 0.0.98 → 0.0.99
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/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +1 -1
- package/.agents/AUTH.md +169 -0
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +73 -5
- package/.agents/QUEUE.md +84 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +8 -0
- package/core/orm/model.js +4 -0
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +82 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/index.d.ts +528 -0
- package/index.js +44 -0
- package/package.json +1 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +6 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/src/api/api-loader.js +1 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +137 -100
- package/src/helpers.js +9 -0
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Native Cron Expression Parser & Matcher
|
|
3
|
+
* Pure Node.js standard 5-part cron implementation (zero external dependencies)
|
|
4
|
+
* @module core/queue/cron
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const MONTH_NAMES = {
|
|
10
|
+
jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
|
|
11
|
+
jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DAY_NAMES = {
|
|
15
|
+
sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CRON_ALIASES = {
|
|
19
|
+
'@yearly': '0 0 1 1 *',
|
|
20
|
+
'@annually': '0 0 1 1 *',
|
|
21
|
+
'@monthly': '0 0 1 * *',
|
|
22
|
+
'@weekly': '0 0 * * 0',
|
|
23
|
+
'@daily': '0 0 * * *',
|
|
24
|
+
'@midnight': '0 0 * * *',
|
|
25
|
+
'@hourly': '0 * * * *',
|
|
26
|
+
'@every_minute': '* * * * *',
|
|
27
|
+
'@minutely': '* * * * *',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves alias string (e.g. '@daily', '@every 5m') into a 5-part cron expression.
|
|
32
|
+
* @param {string} str
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function resolveCronAlias(str) {
|
|
36
|
+
const trimmed = str.trim();
|
|
37
|
+
const lower = trimmed.toLowerCase();
|
|
38
|
+
|
|
39
|
+
if (CRON_ALIASES[lower]) {
|
|
40
|
+
return CRON_ALIASES[lower];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Support '@every 5m', '@every 15m', '@every 2h'
|
|
44
|
+
const everyMatch = lower.match(/^@every\s+(\d+)\s*([mh])$/);
|
|
45
|
+
if (everyMatch) {
|
|
46
|
+
const amount = parseInt(everyMatch[1], 10);
|
|
47
|
+
const unit = everyMatch[2];
|
|
48
|
+
if (unit === 'm' && amount >= 1 && amount <= 59) {
|
|
49
|
+
return `*/${amount} * * * *`;
|
|
50
|
+
}
|
|
51
|
+
if (unit === 'h' && amount >= 1 && amount <= 23) {
|
|
52
|
+
return `0 */${amount} * * *`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse a single cron field with ranges, steps, and lists.
|
|
61
|
+
* @param {string} field
|
|
62
|
+
* @param {number} min
|
|
63
|
+
* @param {number} max
|
|
64
|
+
* @param {Record<string, number>} [nameMap]
|
|
65
|
+
* @returns {Set<number>}
|
|
66
|
+
*/
|
|
67
|
+
function parseField(field, min, max, nameMap = null) {
|
|
68
|
+
let str = field.trim().toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (nameMap) {
|
|
71
|
+
for (const [name, val] of Object.entries(nameMap)) {
|
|
72
|
+
str = str.replace(new RegExp(`\\b${name}\\b`, 'g'), String(val));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const values = new Set();
|
|
77
|
+
const parts = str.split(',');
|
|
78
|
+
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
if (!part) {
|
|
81
|
+
throw new Error(`Invalid empty cron segment in '${field}'`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let step = 1;
|
|
85
|
+
let rangeStr = part;
|
|
86
|
+
|
|
87
|
+
if (part.includes('/')) {
|
|
88
|
+
const sub = part.split('/');
|
|
89
|
+
if (sub.length !== 2) {
|
|
90
|
+
throw new Error(`Invalid step syntax in '${part}'`);
|
|
91
|
+
}
|
|
92
|
+
rangeStr = sub[0];
|
|
93
|
+
step = parseInt(sub[1], 10);
|
|
94
|
+
if (isNaN(step) || step <= 0) {
|
|
95
|
+
throw new Error(`Invalid step value '${sub[1]}' in '${part}'`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let start;
|
|
100
|
+
let end;
|
|
101
|
+
|
|
102
|
+
if (rangeStr === '*' || rangeStr === '') {
|
|
103
|
+
start = min;
|
|
104
|
+
end = max;
|
|
105
|
+
} else if (rangeStr.includes('-')) {
|
|
106
|
+
const sub = rangeStr.split('-');
|
|
107
|
+
if (sub.length !== 2) {
|
|
108
|
+
throw new Error(`Invalid range syntax in '${rangeStr}'`);
|
|
109
|
+
}
|
|
110
|
+
start = parseInt(sub[0], 10);
|
|
111
|
+
end = parseInt(sub[1], 10);
|
|
112
|
+
if (isNaN(start) || isNaN(end) || start > end) {
|
|
113
|
+
throw new Error(`Invalid range boundary '${rangeStr}' in '${part}'`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
start = parseInt(rangeStr, 10);
|
|
117
|
+
end = start;
|
|
118
|
+
if (isNaN(start)) {
|
|
119
|
+
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Day of week special case: 7 is Sunday (same as 0)
|
|
124
|
+
if (max === 6 && (start === 7 || end === 7)) {
|
|
125
|
+
if (start === 7) start = 0;
|
|
126
|
+
if (end === 7) end = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (start < min || end > max) {
|
|
130
|
+
throw new Error(`Cron value out of range [${min}-${max}]: '${part}'`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (let i = start; i <= end; i += step) {
|
|
134
|
+
values.add(i === 7 && max === 6 ? 0 : i);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return values;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class CronExpression {
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} expression - Standard 5-field cron or alias
|
|
144
|
+
* @param {Object} [options]
|
|
145
|
+
* @param {boolean} [options.utc=false] - Evaluate dates in UTC instead of local time
|
|
146
|
+
*/
|
|
147
|
+
constructor(expression, options = {}) {
|
|
148
|
+
if (typeof expression !== 'string' || !expression.trim()) {
|
|
149
|
+
throw new Error('Cron expression must be a non-empty string');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.rawExpression = expression;
|
|
153
|
+
this.resolvedExpression = resolveCronAlias(expression);
|
|
154
|
+
this.utc = !!options.utc;
|
|
155
|
+
|
|
156
|
+
const parts = this.resolvedExpression.trim().split(/\s+/);
|
|
157
|
+
if (parts.length !== 5) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Invalid cron expression '${expression}': expected 5 fields (minute hour day-of-month month day-of-week), got ${parts.length}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.domAny = parts[2] === '*';
|
|
164
|
+
this.dowAny = parts[4] === '*';
|
|
165
|
+
|
|
166
|
+
this.minutes = parseField(parts[0], 0, 59);
|
|
167
|
+
this.hours = parseField(parts[1], 0, 23);
|
|
168
|
+
this.doms = parseField(parts[2], 1, 31);
|
|
169
|
+
this.months = parseField(parts[3], 1, 12, MONTH_NAMES);
|
|
170
|
+
this.dows = parseField(parts[4], 0, 6, DAY_NAMES);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a given Date matches the cron expression
|
|
175
|
+
* @param {Date} [date=new Date()]
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
match(date = new Date()) {
|
|
179
|
+
const minute = this.utc ? date.getUTCMinutes() : date.getMinutes();
|
|
180
|
+
const hour = this.utc ? date.getUTCHours() : date.getHours();
|
|
181
|
+
const month = (this.utc ? date.getUTCMonth() : date.getMonth()) + 1;
|
|
182
|
+
const dom = this.utc ? date.getUTCDate() : date.getDate();
|
|
183
|
+
const dow = this.utc ? date.getUTCDay() : date.getDay();
|
|
184
|
+
|
|
185
|
+
if (!this.minutes.has(minute)) return false;
|
|
186
|
+
if (!this.hours.has(hour)) return false;
|
|
187
|
+
if (!this.months.has(month)) return false;
|
|
188
|
+
|
|
189
|
+
// Standard POSIX / crontab union behavior:
|
|
190
|
+
// If either dom or dow is '*', the other specifies matching day.
|
|
191
|
+
// If both are specified (neither is '*'), either match satisfies.
|
|
192
|
+
if (this.domAny && this.dowAny) return true;
|
|
193
|
+
if (!this.domAny && this.dowAny) return this.doms.has(dom);
|
|
194
|
+
if (this.domAny && !this.dowAny) return this.dows.has(dow);
|
|
195
|
+
return this.doms.has(dom) || this.dows.has(dow);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculate next matching Date after fromDate (default: now)
|
|
200
|
+
* Uses jumping skips to avoid long iteration loops.
|
|
201
|
+
*
|
|
202
|
+
* @param {Date} [fromDate=new Date()]
|
|
203
|
+
* @returns {Date|null}
|
|
204
|
+
*/
|
|
205
|
+
getNextDate(fromDate = new Date()) {
|
|
206
|
+
const d = new Date(fromDate.getTime());
|
|
207
|
+
|
|
208
|
+
if (this.utc) {
|
|
209
|
+
d.setUTCSeconds(0, 0);
|
|
210
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
211
|
+
} else {
|
|
212
|
+
d.setSeconds(0, 0);
|
|
213
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const startYear = this.utc ? d.getUTCFullYear() : d.getFullYear();
|
|
217
|
+
const maxYear = startYear + 5;
|
|
218
|
+
|
|
219
|
+
while ((this.utc ? d.getUTCFullYear() : d.getFullYear()) <= maxYear) {
|
|
220
|
+
const month = (this.utc ? d.getUTCMonth() : d.getMonth()) + 1;
|
|
221
|
+
if (!this.months.has(month)) {
|
|
222
|
+
if (this.utc) {
|
|
223
|
+
d.setUTCMonth(d.getUTCMonth() + 1, 1);
|
|
224
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
225
|
+
} else {
|
|
226
|
+
d.setMonth(d.getMonth() + 1, 1);
|
|
227
|
+
d.setHours(0, 0, 0, 0);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const dom = this.utc ? d.getUTCDate() : d.getDate();
|
|
233
|
+
const dow = this.utc ? d.getUTCDay() : d.getDay();
|
|
234
|
+
|
|
235
|
+
const dayMatch = (this.domAny && this.dowAny)
|
|
236
|
+
? true
|
|
237
|
+
: (!this.domAny && this.dowAny)
|
|
238
|
+
? this.doms.has(dom)
|
|
239
|
+
: (this.domAny && !this.dowAny)
|
|
240
|
+
? this.dows.has(dow)
|
|
241
|
+
: (this.doms.has(dom) || this.dows.has(dow));
|
|
242
|
+
|
|
243
|
+
if (!dayMatch) {
|
|
244
|
+
if (this.utc) {
|
|
245
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
246
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
247
|
+
} else {
|
|
248
|
+
d.setDate(d.getDate() + 1);
|
|
249
|
+
d.setHours(0, 0, 0, 0);
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const hour = this.utc ? d.getUTCHours() : d.getHours();
|
|
255
|
+
if (!this.hours.has(hour)) {
|
|
256
|
+
if (this.utc) {
|
|
257
|
+
d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0);
|
|
258
|
+
} else {
|
|
259
|
+
d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const minute = this.utc ? d.getUTCMinutes() : d.getMinutes();
|
|
265
|
+
if (!this.minutes.has(minute)) {
|
|
266
|
+
if (this.utc) {
|
|
267
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
268
|
+
} else {
|
|
269
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return d;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Return the next N matching Dates
|
|
282
|
+
* @param {number} count
|
|
283
|
+
* @param {Date} [fromDate=new Date()]
|
|
284
|
+
* @returns {Date[]}
|
|
285
|
+
*/
|
|
286
|
+
getNextDates(count = 5, fromDate = new Date()) {
|
|
287
|
+
const dates = [];
|
|
288
|
+
let current = fromDate;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < count; i++) {
|
|
291
|
+
const next = this.getNextDate(current);
|
|
292
|
+
if (!next) break;
|
|
293
|
+
dates.push(next);
|
|
294
|
+
current = next;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return dates;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Static helper to validate a cron expression string
|
|
302
|
+
* @param {string} expression
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
static isValid(expression) {
|
|
306
|
+
try {
|
|
307
|
+
new CronExpression(expression);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (_) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse expression into a CronExpression instance
|
|
316
|
+
* @param {string} expression
|
|
317
|
+
* @param {Object} [options]
|
|
318
|
+
* @returns {CronExpression}
|
|
319
|
+
*/
|
|
320
|
+
static parse(expression, options) {
|
|
321
|
+
return new CronExpression(expression, options);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
CronExpression,
|
|
327
|
+
resolveCronAlias,
|
|
328
|
+
isValidCron: CronExpression.isValid,
|
|
329
|
+
parseCron: CronExpression.parse,
|
|
330
|
+
CRON_ALIASES,
|
|
331
|
+
};
|
package/core/queue/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Webspresso Core Background Job Queue Engine
|
|
2
|
+
* Webspresso Core Background Job Queue & Task Scheduler Engine
|
|
3
3
|
* @module core/queue
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -10,6 +10,8 @@ const { QueueManager, createQueueManager } = require('./manager');
|
|
|
10
10
|
const { MemoryQueueAdapter } = require('./adapters/memory');
|
|
11
11
|
const { DatabaseQueueAdapter } = require('./adapters/database');
|
|
12
12
|
const { RedisQueueAdapter } = require('./adapters/redis');
|
|
13
|
+
const { CronExpression, isValidCron, parseCron, resolveCronAlias } = require('./cron');
|
|
14
|
+
const { TaskScheduler } = require('./scheduler');
|
|
13
15
|
|
|
14
16
|
module.exports = {
|
|
15
17
|
Job,
|
|
@@ -18,4 +20,9 @@ module.exports = {
|
|
|
18
20
|
MemoryQueueAdapter,
|
|
19
21
|
DatabaseQueueAdapter,
|
|
20
22
|
RedisQueueAdapter,
|
|
23
|
+
CronExpression,
|
|
24
|
+
isValidCron,
|
|
25
|
+
parseCron,
|
|
26
|
+
resolveCronAlias,
|
|
27
|
+
TaskScheduler,
|
|
21
28
|
};
|
package/core/queue/manager.js
CHANGED
|
@@ -10,6 +10,8 @@ const path = require('path');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const { Job } = require('./job');
|
|
12
12
|
const { MemoryQueueAdapter } = require('./adapters/memory');
|
|
13
|
+
const { CronExpression } = require('./cron');
|
|
14
|
+
const { TaskScheduler } = require('./scheduler');
|
|
13
15
|
|
|
14
16
|
class QueueManager extends EventEmitter {
|
|
15
17
|
/**
|
|
@@ -78,6 +80,12 @@ class QueueManager extends EventEmitter {
|
|
|
78
80
|
this.startCleanup();
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
// Initialize TaskScheduler for recurring cron tasks
|
|
84
|
+
this.scheduler = new TaskScheduler(this, {
|
|
85
|
+
autoStart: options.autoStart !== false,
|
|
86
|
+
...(options.scheduler || {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
81
89
|
if (this.jobsDir && fs.existsSync(this.jobsDir)) {
|
|
82
90
|
this.loadJobsFromDirectory(this.jobsDir);
|
|
83
91
|
}
|
|
@@ -181,6 +189,9 @@ class QueueManager extends EventEmitter {
|
|
|
181
189
|
this.isPaused = false;
|
|
182
190
|
this._currentPollInterval = this.pollInterval;
|
|
183
191
|
this._scheduleNextTick(0);
|
|
192
|
+
if (this.scheduler) {
|
|
193
|
+
this.scheduler.start();
|
|
194
|
+
}
|
|
184
195
|
this.emit('start');
|
|
185
196
|
return this;
|
|
186
197
|
}
|
|
@@ -194,6 +205,9 @@ class QueueManager extends EventEmitter {
|
|
|
194
205
|
clearTimeout(this._timer);
|
|
195
206
|
this._timer = null;
|
|
196
207
|
}
|
|
208
|
+
if (this.scheduler) {
|
|
209
|
+
this.scheduler.stop();
|
|
210
|
+
}
|
|
197
211
|
this.emit('pause');
|
|
198
212
|
return this;
|
|
199
213
|
}
|
|
@@ -206,6 +220,9 @@ class QueueManager extends EventEmitter {
|
|
|
206
220
|
this.isPaused = false;
|
|
207
221
|
this._currentPollInterval = this.pollInterval;
|
|
208
222
|
this._scheduleNextTick(0);
|
|
223
|
+
if (this.scheduler) {
|
|
224
|
+
this.scheduler.start();
|
|
225
|
+
}
|
|
209
226
|
this.emit('resume');
|
|
210
227
|
return this;
|
|
211
228
|
}
|
|
@@ -218,6 +235,9 @@ class QueueManager extends EventEmitter {
|
|
|
218
235
|
async drain(timeoutMs = 10000) {
|
|
219
236
|
this.isDraining = true;
|
|
220
237
|
this.pause();
|
|
238
|
+
if (this.scheduler) {
|
|
239
|
+
this.scheduler.stop();
|
|
240
|
+
}
|
|
221
241
|
|
|
222
242
|
if (this.runningCount === 0) {
|
|
223
243
|
this.isDraining = false;
|
|
@@ -245,10 +265,58 @@ class QueueManager extends EventEmitter {
|
|
|
245
265
|
async stop() {
|
|
246
266
|
this.pause();
|
|
247
267
|
this.isRunning = false;
|
|
268
|
+
if (this.scheduler) {
|
|
269
|
+
this.scheduler.stop();
|
|
270
|
+
}
|
|
248
271
|
this.stopCleanup();
|
|
249
272
|
this.emit('stop');
|
|
250
273
|
}
|
|
251
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Schedule a recurring cron job
|
|
277
|
+
* @param {string|CronExpression} cronExpression - Cron pattern or alias (e.g. '0 0 * * *', '@daily')
|
|
278
|
+
* @param {string} name - Registered job name
|
|
279
|
+
* @param {any} [data={}] - Job data payload
|
|
280
|
+
* @param {Object} [options={}] - Schedule options (overlap, maxRuns, utc, runImmediately)
|
|
281
|
+
* @returns {Object} Registered schedule object
|
|
282
|
+
*/
|
|
283
|
+
schedule(cronExpression, name, data = {}, options = {}) {
|
|
284
|
+
return this.scheduler.schedule(cronExpression, name, data, options);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Alias for schedule()
|
|
289
|
+
*/
|
|
290
|
+
cron(cronExpression, name, data, options) {
|
|
291
|
+
return this.schedule(cronExpression, name, data, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Unschedule a recurring job by ID or job name
|
|
296
|
+
* @param {string} idOrJobName
|
|
297
|
+
* @returns {boolean}
|
|
298
|
+
*/
|
|
299
|
+
unschedule(idOrJobName) {
|
|
300
|
+
return this.scheduler.unschedule(idOrJobName);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get all registered schedules
|
|
305
|
+
* @returns {Array<Object>}
|
|
306
|
+
*/
|
|
307
|
+
getSchedules() {
|
|
308
|
+
return this.scheduler.getSchedules();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get single schedule by ID or job name
|
|
313
|
+
* @param {string} idOrJobName
|
|
314
|
+
* @returns {Object|null}
|
|
315
|
+
*/
|
|
316
|
+
getSchedule(idOrJobName) {
|
|
317
|
+
return this.scheduler.getSchedule(idOrJobName);
|
|
318
|
+
}
|
|
319
|
+
|
|
252
320
|
/**
|
|
253
321
|
* Delete completed or failed jobs older than a cutoff date
|
|
254
322
|
* @param {Object} [options={}]
|
|
@@ -371,7 +439,10 @@ class QueueManager extends EventEmitter {
|
|
|
371
439
|
if (entry.isDirectory()) {
|
|
372
440
|
readRecursive(fullPath, basePrefix ? `${basePrefix}.${entry.name}` : entry.name);
|
|
373
441
|
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
|
|
374
|
-
const
|
|
442
|
+
const isCronFile = entry.name.endsWith('.cron.js') || entry.name.endsWith('.cron.cjs');
|
|
443
|
+
const nameWithoutExt = isCronFile
|
|
444
|
+
? entry.name.replace(/\.cron\.(c)?js$/, '')
|
|
445
|
+
: entry.name.replace(/\.(c)?js$/, '');
|
|
375
446
|
const jobName = basePrefix ? `${basePrefix}.${nameWithoutExt}` : nameWithoutExt;
|
|
376
447
|
|
|
377
448
|
try {
|
|
@@ -381,6 +452,16 @@ class QueueManager extends EventEmitter {
|
|
|
381
452
|
|
|
382
453
|
if (typeof handler === 'function') {
|
|
383
454
|
this.define(jobName, handler, options);
|
|
455
|
+
|
|
456
|
+
const cronExpr = mod.cron || mod.schedule || (isCronFile ? (mod.expression || `@${nameWithoutExt}`) : null);
|
|
457
|
+
if (cronExpr) {
|
|
458
|
+
this.schedule(
|
|
459
|
+
cronExpr,
|
|
460
|
+
jobName,
|
|
461
|
+
mod.data || {},
|
|
462
|
+
{ ...options, ...(mod.scheduleOptions || {}) }
|
|
463
|
+
);
|
|
464
|
+
}
|
|
384
465
|
}
|
|
385
466
|
} catch (err) {
|
|
386
467
|
console.error(`[QueueManager] Failed to load job from ${fullPath}:`, err.message);
|