@warlock.js/scheduler 4.0.31 → 4.0.41

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/cjs/index.js CHANGED
@@ -1 +1,977 @@
1
- 'use strict';var cronParser=require('./cron-parser.js'),job=require('./job.js'),scheduler=require('./scheduler.js');exports.CronParser=cronParser.CronParser;exports.parseCron=cronParser.parseCron;exports.Job=job.Job;exports.job=job.job;exports.Scheduler=scheduler.Scheduler;exports.scheduler=scheduler.scheduler;//# sourceMappingURL=index.js.map
1
+ 'use strict';
2
+
3
+ var dayjs2 = require('dayjs');
4
+ var timezone = require('dayjs/plugin/timezone.js');
5
+ var utc = require('dayjs/plugin/utc.js');
6
+ var events = require('events');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var dayjs2__default = /*#__PURE__*/_interopDefault(dayjs2);
11
+ var timezone__default = /*#__PURE__*/_interopDefault(timezone);
12
+ var utc__default = /*#__PURE__*/_interopDefault(utc);
13
+
14
+ // ../../warlock.js/scheduler/src/cron-parser.ts
15
+ var CronParser = class {
16
+ /**
17
+ * Creates a new CronParser instance
18
+ *
19
+ * @param expression - Standard 5-field cron expression
20
+ * @throws Error if expression is invalid
21
+ */
22
+ constructor(_expression) {
23
+ this._expression = _expression;
24
+ this._fields = this._parse(_expression);
25
+ }
26
+ _fields;
27
+ /**
28
+ * Get the parsed cron fields
29
+ */
30
+ get fields() {
31
+ return this._fields;
32
+ }
33
+ /**
34
+ * Get the original expression
35
+ */
36
+ get expression() {
37
+ return this._expression;
38
+ }
39
+ /**
40
+ * Calculate the next run time from a given date
41
+ *
42
+ * @param from - Starting date (defaults to now)
43
+ * @returns Next run time as Dayjs
44
+ */
45
+ nextRun(from = dayjs2__default.default()) {
46
+ let date = from.add(1, "minute").second(0).millisecond(0);
47
+ const maxIterations = 366 * 24 * 60;
48
+ let iterations = 0;
49
+ while (iterations < maxIterations) {
50
+ iterations++;
51
+ if (!this._fields.months.includes(date.month() + 1)) {
52
+ date = date.add(1, "month").date(1).hour(0).minute(0);
53
+ continue;
54
+ }
55
+ if (!this._fields.daysOfMonth.includes(date.date())) {
56
+ date = date.add(1, "day").hour(0).minute(0);
57
+ continue;
58
+ }
59
+ if (!this._fields.daysOfWeek.includes(date.day())) {
60
+ date = date.add(1, "day").hour(0).minute(0);
61
+ continue;
62
+ }
63
+ if (!this._fields.hours.includes(date.hour())) {
64
+ date = date.add(1, "hour").minute(0);
65
+ continue;
66
+ }
67
+ if (!this._fields.minutes.includes(date.minute())) {
68
+ date = date.add(1, "minute");
69
+ continue;
70
+ }
71
+ return date;
72
+ }
73
+ throw new Error(`Could not find next run time for cron expression: ${this._expression}`);
74
+ }
75
+ /**
76
+ * Check if a given date matches the cron expression
77
+ *
78
+ * @param date - Date to check
79
+ * @returns true if the date matches
80
+ */
81
+ matches(date) {
82
+ return this._fields.minutes.includes(date.minute()) && this._fields.hours.includes(date.hour()) && this._fields.daysOfMonth.includes(date.date()) && this._fields.months.includes(date.month() + 1) && this._fields.daysOfWeek.includes(date.day());
83
+ }
84
+ /**
85
+ * Parse a cron expression into fields
86
+ */
87
+ _parse(expression) {
88
+ const parts = expression.trim().split(/\s+/);
89
+ if (parts.length !== 5) {
90
+ throw new Error(
91
+ `Invalid cron expression: "${expression}". Expected 5 fields (minute hour day month weekday).`
92
+ );
93
+ }
94
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
95
+ return {
96
+ minutes: this._parseField(minute, 0, 59),
97
+ hours: this._parseField(hour, 0, 23),
98
+ daysOfMonth: this._parseField(dayOfMonth, 1, 31),
99
+ months: this._parseField(month, 1, 12),
100
+ daysOfWeek: this._parseField(dayOfWeek, 0, 6)
101
+ };
102
+ }
103
+ /**
104
+ * Parse a single cron field
105
+ *
106
+ * @param field - Field value (e.g., "*", "5", "1-5", "* /2", "1,3,5")
107
+ * @param min - Minimum allowed value
108
+ * @param max - Maximum allowed value
109
+ * @returns Array of matching values
110
+ */
111
+ _parseField(field, min, max) {
112
+ const values = /* @__PURE__ */ new Set();
113
+ const parts = field.split(",");
114
+ for (const part of parts) {
115
+ const [range, stepStr] = part.split("/");
116
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
117
+ if (isNaN(step) || step < 1) {
118
+ throw new Error(`Invalid step value in cron field: "${field}"`);
119
+ }
120
+ let rangeStart;
121
+ let rangeEnd;
122
+ if (range === "*") {
123
+ rangeStart = min;
124
+ rangeEnd = max;
125
+ } else if (range.includes("-")) {
126
+ const [startStr, endStr] = range.split("-");
127
+ rangeStart = parseInt(startStr, 10);
128
+ rangeEnd = parseInt(endStr, 10);
129
+ if (isNaN(rangeStart) || isNaN(rangeEnd)) {
130
+ throw new Error(`Invalid range in cron field: "${field}"`);
131
+ }
132
+ if (rangeStart < min || rangeEnd > max || rangeStart > rangeEnd) {
133
+ throw new Error(`Range out of bounds in cron field: "${field}" (valid: ${min}-${max})`);
134
+ }
135
+ } else {
136
+ const value = parseInt(range, 10);
137
+ if (isNaN(value)) {
138
+ throw new Error(`Invalid value in cron field: "${field}"`);
139
+ }
140
+ if (value < min || value > max) {
141
+ throw new Error(`Value out of bounds in cron field: "${field}" (valid: ${min}-${max})`);
142
+ }
143
+ rangeStart = value;
144
+ rangeEnd = value;
145
+ }
146
+ for (let i = rangeStart; i <= rangeEnd; i += step) {
147
+ values.add(i);
148
+ }
149
+ }
150
+ return Array.from(values).sort((a, b) => a - b);
151
+ }
152
+ };
153
+ function parseCron(expression) {
154
+ return new CronParser(expression);
155
+ }
156
+ dayjs2__default.default.extend(utc__default.default);
157
+ dayjs2__default.default.extend(timezone__default.default);
158
+ var DAYS_OF_WEEK = [
159
+ "sunday",
160
+ "monday",
161
+ "tuesday",
162
+ "wednesday",
163
+ "thursday",
164
+ "friday",
165
+ "saturday"
166
+ ];
167
+ var Job = class {
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // Constructor
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+ /**
172
+ * Creates a new Job instance
173
+ *
174
+ * @param name - Unique identifier for the job
175
+ * @param callback - Function to execute when the job runs
176
+ */
177
+ constructor(name, _callback) {
178
+ this.name = name;
179
+ this._callback = _callback;
180
+ }
181
+ // ─────────────────────────────────────────────────────────────────────────────
182
+ // Private Properties
183
+ // ─────────────────────────────────────────────────────────────────────────────
184
+ /**
185
+ * Interval configuration for scheduling
186
+ */
187
+ _intervals = {};
188
+ /**
189
+ * Last execution timestamp
190
+ */
191
+ _lastRun = null;
192
+ /**
193
+ * Whether the job is currently executing
194
+ */
195
+ _isRunning = false;
196
+ /**
197
+ * Skip execution if job is already running
198
+ */
199
+ _skipIfRunning = false;
200
+ /**
201
+ * Retry configuration
202
+ */
203
+ _retryConfig = null;
204
+ /**
205
+ * Timezone for scheduling (defaults to system timezone)
206
+ */
207
+ _timezone = null;
208
+ /**
209
+ * Cron expression parser (mutually exclusive with interval config)
210
+ */
211
+ _cronParser = null;
212
+ /**
213
+ * Promise resolver for completion waiting
214
+ */
215
+ _completionResolver = null;
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+ // Public Properties
218
+ // ─────────────────────────────────────────────────────────────────────────────
219
+ /**
220
+ * Next scheduled execution time
221
+ */
222
+ nextRun = null;
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // Public Getters
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+ /**
227
+ * Returns true if the job is currently executing
228
+ */
229
+ get isRunning() {
230
+ return this._isRunning;
231
+ }
232
+ /**
233
+ * Returns the last execution timestamp
234
+ */
235
+ get lastRun() {
236
+ return this._lastRun;
237
+ }
238
+ /**
239
+ * Returns the current interval configuration (readonly)
240
+ */
241
+ get intervals() {
242
+ return this._intervals;
243
+ }
244
+ // ─────────────────────────────────────────────────────────────────────────────
245
+ // Interval Configuration Methods (Fluent API)
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ /**
248
+ * Set a custom interval for job execution
249
+ *
250
+ * @param value - Number of time units
251
+ * @param timeType - Type of time unit
252
+ * @returns this for chaining
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * job.every(5, "minute"); // Run every 5 minutes
257
+ * job.every(2, "hour"); // Run every 2 hours
258
+ * ```
259
+ */
260
+ every(value, timeType) {
261
+ this._intervals.every = { type: timeType, value };
262
+ return this;
263
+ }
264
+ /**
265
+ * Run job every second (use with caution - high frequency)
266
+ */
267
+ everySecond() {
268
+ return this.every(1, "second");
269
+ }
270
+ /**
271
+ * Run job every specified number of seconds
272
+ */
273
+ everySeconds(seconds) {
274
+ return this.every(seconds, "second");
275
+ }
276
+ /**
277
+ * Run job every minute
278
+ */
279
+ everyMinute() {
280
+ return this.every(1, "minute");
281
+ }
282
+ /**
283
+ * Run job every specified number of minutes
284
+ */
285
+ everyMinutes(minutes) {
286
+ return this.every(minutes, "minute");
287
+ }
288
+ /**
289
+ * Run job every hour
290
+ */
291
+ everyHour() {
292
+ return this.every(1, "hour");
293
+ }
294
+ /**
295
+ * Run job every specified number of hours
296
+ */
297
+ everyHours(hours) {
298
+ return this.every(hours, "hour");
299
+ }
300
+ /**
301
+ * Run job every day at midnight
302
+ */
303
+ everyDay() {
304
+ return this.every(1, "day");
305
+ }
306
+ /**
307
+ * Alias for everyDay()
308
+ */
309
+ daily() {
310
+ return this.everyDay();
311
+ }
312
+ /**
313
+ * Run job twice a day (every 12 hours)
314
+ */
315
+ twiceDaily() {
316
+ return this.every(12, "hour");
317
+ }
318
+ /**
319
+ * Run job every week
320
+ */
321
+ everyWeek() {
322
+ return this.every(1, "week");
323
+ }
324
+ /**
325
+ * Alias for everyWeek()
326
+ */
327
+ weekly() {
328
+ return this.everyWeek();
329
+ }
330
+ /**
331
+ * Run job every month
332
+ */
333
+ everyMonth() {
334
+ return this.every(1, "month");
335
+ }
336
+ /**
337
+ * Alias for everyMonth()
338
+ */
339
+ monthly() {
340
+ return this.everyMonth();
341
+ }
342
+ /**
343
+ * Run job every year
344
+ */
345
+ everyYear() {
346
+ return this.every(1, "year");
347
+ }
348
+ /**
349
+ * Alias for everyYear()
350
+ */
351
+ yearly() {
352
+ return this.everyYear();
353
+ }
354
+ /**
355
+ * Alias for everyMinute() - job runs continuously every minute
356
+ */
357
+ always() {
358
+ return this.everyMinute();
359
+ }
360
+ /**
361
+ * Schedule job using a cron expression
362
+ *
363
+ * Supports standard 5-field cron syntax:
364
+ * ```
365
+ * ┌───────────── minute (0-59)
366
+ * │ ┌───────────── hour (0-23)
367
+ * │ │ ┌───────────── day of month (1-31)
368
+ * │ │ │ ┌───────────── month (1-12)
369
+ * │ │ │ │ ┌───────────── day of week (0-6, Sunday = 0)
370
+ * │ │ │ │ │
371
+ * * * * * *
372
+ * ```
373
+ *
374
+ * Supports:
375
+ * - '*' - any value
376
+ * - '5' - specific value
377
+ * - '1,3,5' - list of values
378
+ * - '1-5' - range of values
379
+ * - 'x/5' - step values (every 5)
380
+ * - '1-10/2' - range with step
381
+ *
382
+ * @param expression - Standard 5-field cron expression
383
+ * @returns this for chaining
384
+ *
385
+ * @example
386
+ * ```typescript
387
+ * job.cron("0 9 * * 1-5"); // 9 AM weekdays
388
+ * job.cron("x/5 * * * *"); // Every 5 minutes
389
+ * job.cron("0 0 1 * *"); // First day of month at midnight
390
+ * job.cron("0 x/2 * * *"); // Every 2 hours
391
+ * ```
392
+ */
393
+ cron(expression) {
394
+ this._cronParser = new CronParser(expression);
395
+ this._intervals = {};
396
+ return this;
397
+ }
398
+ /**
399
+ * Get the cron expression if one is set
400
+ */
401
+ get cronExpression() {
402
+ return this._cronParser?.expression ?? null;
403
+ }
404
+ // ─────────────────────────────────────────────────────────────────────────────
405
+ // Day & Time Configuration Methods
406
+ // ─────────────────────────────────────────────────────────────────────────────
407
+ /**
408
+ * Schedule job on a specific day
409
+ *
410
+ * @param day - Day of week (string) or day of month (number 1-31)
411
+ * @returns this for chaining
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * job.on("monday"); // Run on Mondays
416
+ * job.on(15); // Run on the 15th of each month
417
+ * ```
418
+ */
419
+ on(day) {
420
+ if (typeof day === "number" && (day < 1 || day > 31)) {
421
+ throw new Error("Invalid day of the month. Must be between 1 and 31.");
422
+ }
423
+ this._intervals.day = day;
424
+ return this;
425
+ }
426
+ /**
427
+ * Schedule job at a specific time
428
+ *
429
+ * @param time - Time in HH:mm or HH:mm:ss format
430
+ * @returns this for chaining
431
+ *
432
+ * @example
433
+ * ```typescript
434
+ * job.daily().at("09:00"); // Run daily at 9 AM
435
+ * job.weekly().at("14:30"); // Run weekly at 2:30 PM
436
+ * ```
437
+ */
438
+ at(time) {
439
+ if (!/^\d{1,2}:\d{2}(:\d{2})?$/.test(time)) {
440
+ throw new Error("Invalid time format. Use HH:mm or HH:mm:ss.");
441
+ }
442
+ this._intervals.time = time;
443
+ return this;
444
+ }
445
+ /**
446
+ * Run task at the beginning of the specified time period
447
+ *
448
+ * @param type - Time type (day, month, year)
449
+ */
450
+ beginOf(type) {
451
+ const time = "00:00";
452
+ switch (type) {
453
+ case "day":
454
+ break;
455
+ case "month":
456
+ this.on(1);
457
+ break;
458
+ case "year":
459
+ this.on(1);
460
+ this.every(1, "year");
461
+ break;
462
+ default:
463
+ throw new Error(`Unsupported type for beginOf: ${type}`);
464
+ }
465
+ return this.at(time);
466
+ }
467
+ /**
468
+ * Run task at the end of the specified time period
469
+ *
470
+ * @param type - Time type (day, month, year)
471
+ */
472
+ endOf(type) {
473
+ const now = this._now();
474
+ const time = "23:59";
475
+ switch (type) {
476
+ case "day":
477
+ break;
478
+ case "month":
479
+ this.on(now.endOf("month").date());
480
+ break;
481
+ case "year":
482
+ this.on(31);
483
+ this.every(1, "year");
484
+ break;
485
+ default:
486
+ throw new Error(`Unsupported type for endOf: ${type}`);
487
+ }
488
+ return this.at(time);
489
+ }
490
+ // ─────────────────────────────────────────────────────────────────────────────
491
+ // Timezone Configuration
492
+ // ─────────────────────────────────────────────────────────────────────────────
493
+ /**
494
+ * Set the timezone for this job's scheduling
495
+ *
496
+ * @param tz - IANA timezone string (e.g., "America/New_York", "Europe/London")
497
+ * @returns this for chaining
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * job.daily().at("09:00").inTimezone("America/New_York");
502
+ * ```
503
+ */
504
+ inTimezone(tz) {
505
+ this._timezone = tz;
506
+ return this;
507
+ }
508
+ // ─────────────────────────────────────────────────────────────────────────────
509
+ // Execution Options
510
+ // ─────────────────────────────────────────────────────────────────────────────
511
+ /**
512
+ * Prevent overlapping executions of this job
513
+ *
514
+ * When enabled, if the job is already running when it's scheduled to run again,
515
+ * the new execution will be skipped.
516
+ *
517
+ * @param skip - Whether to skip if already running (default: true)
518
+ * @returns this for chaining
519
+ */
520
+ preventOverlap(skip = true) {
521
+ this._skipIfRunning = skip;
522
+ return this;
523
+ }
524
+ /**
525
+ * Configure automatic retry on failure
526
+ *
527
+ * @param maxRetries - Maximum number of retry attempts
528
+ * @param delay - Delay between retries in milliseconds
529
+ * @param backoffMultiplier - Optional multiplier for exponential backoff
530
+ * @returns this for chaining
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * job.retry(3, 1000); // Retry 3 times with 1s delay
535
+ * job.retry(5, 1000, 2); // Exponential backoff: 1s, 2s, 4s, 8s, 16s
536
+ * ```
537
+ */
538
+ retry(maxRetries, delay = 1e3, backoffMultiplier) {
539
+ this._retryConfig = {
540
+ maxRetries,
541
+ delay,
542
+ backoffMultiplier
543
+ };
544
+ return this;
545
+ }
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+ // Execution Control
548
+ // ─────────────────────────────────────────────────────────────────────────────
549
+ /**
550
+ * Terminate the job and clear all scheduling data
551
+ */
552
+ terminate() {
553
+ this._intervals = {};
554
+ this.nextRun = null;
555
+ this._lastRun = null;
556
+ this._isRunning = false;
557
+ return this;
558
+ }
559
+ /**
560
+ * Prepare the job by calculating the next run time
561
+ * Called by the scheduler when starting
562
+ */
563
+ prepare() {
564
+ this._determineNextRun();
565
+ }
566
+ /**
567
+ * Determine if the job should run now
568
+ *
569
+ * @returns true if the job should execute
570
+ */
571
+ shouldRun() {
572
+ if (this._skipIfRunning && this._isRunning) {
573
+ return false;
574
+ }
575
+ return this.nextRun !== null && this._now().isAfter(this.nextRun);
576
+ }
577
+ /**
578
+ * Execute the job
579
+ *
580
+ * @returns Promise resolving to the job result
581
+ */
582
+ async run() {
583
+ const startTime = Date.now();
584
+ this._isRunning = true;
585
+ try {
586
+ const result = await this._executeWithRetry();
587
+ this._lastRun = this._now();
588
+ this._determineNextRun();
589
+ return {
590
+ success: true,
591
+ duration: Date.now() - startTime,
592
+ retries: result.retries || 0
593
+ };
594
+ } catch (error) {
595
+ return {
596
+ success: false,
597
+ duration: Date.now() - startTime,
598
+ error,
599
+ retries: this._retryConfig?.maxRetries ?? 0
600
+ };
601
+ } finally {
602
+ this._isRunning = false;
603
+ if (this._completionResolver) {
604
+ this._completionResolver();
605
+ this._completionResolver = null;
606
+ }
607
+ }
608
+ }
609
+ /**
610
+ * Wait for the job to complete
611
+ * Useful for graceful shutdown
612
+ *
613
+ * @returns Promise that resolves when the job completes
614
+ */
615
+ waitForCompletion() {
616
+ if (!this._isRunning) {
617
+ return Promise.resolve();
618
+ }
619
+ return new Promise((resolve) => {
620
+ this._completionResolver = resolve;
621
+ });
622
+ }
623
+ // ─────────────────────────────────────────────────────────────────────────────
624
+ // Private Methods
625
+ // ─────────────────────────────────────────────────────────────────────────────
626
+ /**
627
+ * Get current time, respecting timezone if set
628
+ */
629
+ _now() {
630
+ return this._timezone ? dayjs2__default.default().tz(this._timezone) : dayjs2__default.default();
631
+ }
632
+ /**
633
+ * Execute the callback with retry logic
634
+ */
635
+ async _executeWithRetry() {
636
+ let lastError;
637
+ let attempts = 0;
638
+ const maxAttempts = (this._retryConfig?.maxRetries ?? 0) + 1;
639
+ while (attempts < maxAttempts) {
640
+ try {
641
+ await this._callback(this);
642
+ return { retries: attempts };
643
+ } catch (error) {
644
+ lastError = error;
645
+ attempts++;
646
+ if (attempts < maxAttempts && this._retryConfig) {
647
+ const delay = this._calculateRetryDelay(attempts);
648
+ await this._sleep(delay);
649
+ }
650
+ }
651
+ }
652
+ throw lastError;
653
+ }
654
+ /**
655
+ * Calculate retry delay with optional exponential backoff
656
+ */
657
+ _calculateRetryDelay(attempt) {
658
+ if (!this._retryConfig) return 0;
659
+ const { delay, backoffMultiplier } = this._retryConfig;
660
+ if (backoffMultiplier) {
661
+ return delay * Math.pow(backoffMultiplier, attempt - 1);
662
+ }
663
+ return delay;
664
+ }
665
+ /**
666
+ * Sleep for specified milliseconds
667
+ */
668
+ _sleep(ms) {
669
+ return new Promise((resolve) => setTimeout(resolve, ms));
670
+ }
671
+ /**
672
+ * Calculate the next run time based on interval or cron configuration
673
+ */
674
+ _determineNextRun() {
675
+ if (this._cronParser) {
676
+ const now = this._now();
677
+ this.nextRun = this._cronParser.nextRun(now);
678
+ return;
679
+ }
680
+ let date = this._lastRun ? this._lastRun.add(1, "second") : this._now();
681
+ if (this._intervals.every?.value && this._intervals.every?.type) {
682
+ date = date.add(this._intervals.every.value, this._intervals.every.type);
683
+ }
684
+ if (this._intervals.day !== void 0) {
685
+ if (typeof this._intervals.day === "number") {
686
+ date = date.date(this._intervals.day);
687
+ } else {
688
+ const targetDay = DAYS_OF_WEEK.indexOf(this._intervals.day);
689
+ if (targetDay !== -1) {
690
+ date = date.day(targetDay);
691
+ }
692
+ }
693
+ }
694
+ if (this._intervals.time) {
695
+ const parts = this._intervals.time.split(":").map(Number);
696
+ const [hour, minute, second = 0] = parts;
697
+ date = date.hour(hour).minute(minute).second(second).millisecond(0);
698
+ }
699
+ while (date.isBefore(this._now())) {
700
+ if (this._intervals.every?.value && this._intervals.every?.type) {
701
+ date = date.add(this._intervals.every.value, this._intervals.every.type);
702
+ } else {
703
+ date = date.add(1, "day");
704
+ }
705
+ }
706
+ this.nextRun = date;
707
+ }
708
+ };
709
+ function job(name, callback) {
710
+ return new Job(name, callback);
711
+ }
712
+ var Scheduler = class extends events.EventEmitter {
713
+ // ─────────────────────────────────────────────────────────────────────────────
714
+ // Private Properties
715
+ // ─────────────────────────────────────────────────────────────────────────────
716
+ /**
717
+ * List of registered jobs
718
+ */
719
+ _jobs = [];
720
+ /**
721
+ * Reference to the current timeout for stopping
722
+ */
723
+ _timeoutId = null;
724
+ /**
725
+ * Tick interval in milliseconds (how often to check for due jobs)
726
+ */
727
+ _tickInterval = 1e3;
728
+ /**
729
+ * Whether to run due jobs in parallel
730
+ */
731
+ _runInParallel = false;
732
+ /**
733
+ * Maximum concurrent jobs when running in parallel
734
+ */
735
+ _maxConcurrency = 10;
736
+ /**
737
+ * Flag indicating scheduler is shutting down
738
+ */
739
+ _isShuttingDown = false;
740
+ // ─────────────────────────────────────────────────────────────────────────────
741
+ // Public Getters
742
+ // ─────────────────────────────────────────────────────────────────────────────
743
+ /**
744
+ * Returns true if the scheduler is currently running
745
+ */
746
+ get isRunning() {
747
+ return this._timeoutId !== null;
748
+ }
749
+ /**
750
+ * Returns the number of registered jobs
751
+ */
752
+ get jobCount() {
753
+ return this._jobs.length;
754
+ }
755
+ // ─────────────────────────────────────────────────────────────────────────────
756
+ // Configuration Methods (Fluent API)
757
+ // ─────────────────────────────────────────────────────────────────────────────
758
+ /**
759
+ * Add a job to the scheduler
760
+ *
761
+ * @param job - Job instance to schedule
762
+ * @returns this for chaining
763
+ */
764
+ addJob(job2) {
765
+ this._jobs.push(job2);
766
+ return this;
767
+ }
768
+ /**
769
+ * Alias to create a new job directly and store it
770
+ */
771
+ newJob(name, jobCallback) {
772
+ const job2 = new Job(name, jobCallback);
773
+ this.addJob(job2);
774
+ return job2;
775
+ }
776
+ /**
777
+ * Add multiple jobs to the scheduler
778
+ *
779
+ * @param jobs - Array of Job instances
780
+ * @returns this for chaining
781
+ */
782
+ addJobs(jobs) {
783
+ this._jobs.push(...jobs);
784
+ return this;
785
+ }
786
+ /**
787
+ * Remove a job from the scheduler by name
788
+ *
789
+ * @param jobName - Name of the job to remove
790
+ * @returns true if job was found and removed
791
+ */
792
+ removeJob(jobName) {
793
+ const index = this._jobs.findIndex((j) => j.name === jobName);
794
+ if (index !== -1) {
795
+ this._jobs.splice(index, 1);
796
+ return true;
797
+ }
798
+ return false;
799
+ }
800
+ /**
801
+ * Get a job by name
802
+ *
803
+ * @param jobName - Name of the job to find
804
+ * @returns Job instance or undefined
805
+ */
806
+ getJob(jobName) {
807
+ return this._jobs.find((j) => j.name === jobName);
808
+ }
809
+ /**
810
+ * Get all registered jobs
811
+ *
812
+ * @returns Array of registered jobs (readonly)
813
+ */
814
+ list() {
815
+ return this._jobs;
816
+ }
817
+ /**
818
+ * Set the tick interval (how often to check for due jobs)
819
+ *
820
+ * @param ms - Interval in milliseconds (minimum 100ms)
821
+ * @returns this for chaining
822
+ */
823
+ runEvery(ms) {
824
+ if (ms < 100) {
825
+ throw new Error("Tick interval must be at least 100ms");
826
+ }
827
+ this._tickInterval = ms;
828
+ return this;
829
+ }
830
+ /**
831
+ * Configure whether jobs should run in parallel
832
+ *
833
+ * @param parallel - Enable parallel execution
834
+ * @param maxConcurrency - Maximum concurrent jobs (default: 10)
835
+ * @returns this for chaining
836
+ */
837
+ runInParallel(parallel, maxConcurrency = 10) {
838
+ this._runInParallel = parallel;
839
+ this._maxConcurrency = maxConcurrency;
840
+ return this;
841
+ }
842
+ // ─────────────────────────────────────────────────────────────────────────────
843
+ // Lifecycle Methods
844
+ // ─────────────────────────────────────────────────────────────────────────────
845
+ /**
846
+ * Start the scheduler
847
+ *
848
+ * @throws Error if scheduler is already running
849
+ */
850
+ start() {
851
+ if (this.isRunning) {
852
+ throw new Error("Scheduler is already running.");
853
+ }
854
+ if (this._jobs.length === 0) {
855
+ throw new Error("Cannot start scheduler with no jobs.");
856
+ }
857
+ for (const job2 of this._jobs) {
858
+ job2.prepare();
859
+ }
860
+ this._isShuttingDown = false;
861
+ this._scheduleTick();
862
+ this.emit("scheduler:started");
863
+ }
864
+ /**
865
+ * Stop the scheduler immediately
866
+ *
867
+ * Note: This does not wait for running jobs to complete.
868
+ * Use shutdown() for graceful termination.
869
+ */
870
+ stop() {
871
+ if (this._timeoutId) {
872
+ clearTimeout(this._timeoutId);
873
+ this._timeoutId = null;
874
+ }
875
+ this.emit("scheduler:stopped");
876
+ }
877
+ /**
878
+ * Gracefully shutdown the scheduler
879
+ *
880
+ * Stops scheduling new jobs and waits for currently running jobs to complete.
881
+ *
882
+ * @param timeout - Maximum time to wait for jobs (default: 30000ms)
883
+ * @returns Promise that resolves when shutdown is complete
884
+ */
885
+ async shutdown(timeout = 3e4) {
886
+ this._isShuttingDown = true;
887
+ this.stop();
888
+ const runningJobs = this._jobs.filter((j) => j.isRunning);
889
+ if (runningJobs.length > 0) {
890
+ await Promise.race([
891
+ Promise.all(runningJobs.map((j) => j.waitForCompletion())),
892
+ new Promise((resolve) => setTimeout(resolve, timeout))
893
+ ]);
894
+ }
895
+ }
896
+ // ─────────────────────────────────────────────────────────────────────────────
897
+ // Private Methods
898
+ // ─────────────────────────────────────────────────────────────────────────────
899
+ /**
900
+ * Schedule the next tick
901
+ */
902
+ _scheduleTick() {
903
+ if (this._isShuttingDown) return;
904
+ const startTime = Date.now();
905
+ this._timeoutId = setTimeout(async () => {
906
+ await this._tick();
907
+ const elapsed = Date.now() - startTime;
908
+ Math.max(this._tickInterval - elapsed, 0);
909
+ this._scheduleTick();
910
+ }, this._tickInterval);
911
+ }
912
+ /**
913
+ * Execute a scheduler tick - check and run due jobs
914
+ */
915
+ async _tick() {
916
+ this.emit("scheduler:tick", /* @__PURE__ */ new Date());
917
+ const dueJobs = this._jobs.filter((job2) => {
918
+ if (!job2.shouldRun()) return false;
919
+ if (job2.isRunning) {
920
+ this.emit("job:skip", job2.name, "Job is already running");
921
+ return false;
922
+ }
923
+ return true;
924
+ });
925
+ if (dueJobs.length === 0) return;
926
+ if (this._runInParallel) {
927
+ await this._runJobsInParallel(dueJobs);
928
+ } else {
929
+ await this._runJobsSequentially(dueJobs);
930
+ }
931
+ }
932
+ /**
933
+ * Run jobs sequentially
934
+ */
935
+ async _runJobsSequentially(jobs) {
936
+ for (const job2 of jobs) {
937
+ if (this._isShuttingDown) break;
938
+ await this._runJob(job2);
939
+ }
940
+ }
941
+ /**
942
+ * Run jobs in parallel with concurrency limit
943
+ */
944
+ async _runJobsInParallel(jobs) {
945
+ const batches = [];
946
+ for (let i = 0; i < jobs.length; i += this._maxConcurrency) {
947
+ batches.push(jobs.slice(i, i + this._maxConcurrency));
948
+ }
949
+ for (const batch of batches) {
950
+ if (this._isShuttingDown) break;
951
+ await Promise.allSettled(batch.map((job2) => this._runJob(job2)));
952
+ }
953
+ }
954
+ /**
955
+ * Run a single job and emit events
956
+ */
957
+ async _runJob(job2) {
958
+ this.emit("job:start", job2.name);
959
+ const result = await job2.run();
960
+ if (result.success) {
961
+ this.emit("job:complete", job2.name, result);
962
+ } else {
963
+ this.emit("job:error", job2.name, result.error);
964
+ }
965
+ return result;
966
+ }
967
+ };
968
+ var scheduler = new Scheduler();
969
+
970
+ exports.CronParser = CronParser;
971
+ exports.Job = Job;
972
+ exports.Scheduler = Scheduler;
973
+ exports.job = job;
974
+ exports.parseCron = parseCron;
975
+ exports.scheduler = scheduler;
976
+ //# sourceMappingURL=index.js.map
977
+ //# sourceMappingURL=index.js.map