@warlock.js/scheduler 4.0.171 → 4.1.1

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.
Files changed (69) hide show
  1. package/README.md +60 -1
  2. package/cjs/index.cjs +1132 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/cron-parser.d.mts +129 -0
  5. package/esm/cron-parser.d.mts.map +1 -0
  6. package/esm/cron-parser.mjs +210 -0
  7. package/esm/cron-parser.mjs.map +1 -0
  8. package/esm/index.d.mts +5 -0
  9. package/esm/index.mjs +5 -0
  10. package/esm/job.d.mts +386 -0
  11. package/esm/job.d.mts.map +1 -0
  12. package/esm/job.mjs +633 -0
  13. package/esm/job.mjs.map +1 -0
  14. package/esm/scheduler.d.mts +193 -0
  15. package/esm/scheduler.d.mts.map +1 -0
  16. package/esm/scheduler.mjs +261 -0
  17. package/esm/scheduler.mjs.map +1 -0
  18. package/esm/types.d.mts +70 -0
  19. package/esm/types.d.mts.map +1 -0
  20. package/llms-full.txt +888 -0
  21. package/llms.txt +15 -0
  22. package/package.json +40 -28
  23. package/skills/configure-retry-and-overlap/SKILL.md +137 -0
  24. package/skills/observe-scheduler/SKILL.md +153 -0
  25. package/skills/overview/SKILL.md +92 -0
  26. package/skills/pin-schedule-timezone/SKILL.md +114 -0
  27. package/skills/schedule-fluently/SKILL.md +141 -0
  28. package/skills/schedule-with-cron/SKILL.md +123 -0
  29. package/skills/scheduler-basics/SKILL.md +94 -0
  30. package/cjs/cron-parser.d.ts +0 -98
  31. package/cjs/cron-parser.d.ts.map +0 -1
  32. package/cjs/cron-parser.js +0 -193
  33. package/cjs/cron-parser.js.map +0 -1
  34. package/cjs/index.d.ts +0 -44
  35. package/cjs/index.d.ts.map +0 -1
  36. package/cjs/index.js +0 -1
  37. package/cjs/index.js.map +0 -1
  38. package/cjs/job.d.ts +0 -332
  39. package/cjs/job.d.ts.map +0 -1
  40. package/cjs/job.js +0 -616
  41. package/cjs/job.js.map +0 -1
  42. package/cjs/scheduler.d.ts +0 -182
  43. package/cjs/scheduler.d.ts.map +0 -1
  44. package/cjs/scheduler.js +0 -316
  45. package/cjs/scheduler.js.map +0 -1
  46. package/cjs/types.d.ts +0 -63
  47. package/cjs/types.d.ts.map +0 -1
  48. package/cjs/utils.d.ts +0 -3
  49. package/cjs/utils.d.ts.map +0 -1
  50. package/esm/cron-parser.d.ts +0 -98
  51. package/esm/cron-parser.d.ts.map +0 -1
  52. package/esm/cron-parser.js +0 -193
  53. package/esm/cron-parser.js.map +0 -1
  54. package/esm/index.d.ts +0 -44
  55. package/esm/index.d.ts.map +0 -1
  56. package/esm/index.js +0 -1
  57. package/esm/index.js.map +0 -1
  58. package/esm/job.d.ts +0 -332
  59. package/esm/job.d.ts.map +0 -1
  60. package/esm/job.js +0 -616
  61. package/esm/job.js.map +0 -1
  62. package/esm/scheduler.d.ts +0 -182
  63. package/esm/scheduler.d.ts.map +0 -1
  64. package/esm/scheduler.js +0 -316
  65. package/esm/scheduler.js.map +0 -1
  66. package/esm/types.d.ts +0 -63
  67. package/esm/types.d.ts.map +0 -1
  68. package/esm/utils.d.ts +0 -3
  69. package/esm/utils.d.ts.map +0 -1
package/cjs/index.cjs ADDED
@@ -0,0 +1,1132 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let dayjs = require("dayjs");
30
+ dayjs = __toESM(dayjs, 1);
31
+ let dayjs_plugin_isSameOrAfter_js = require("dayjs/plugin/isSameOrAfter.js");
32
+ dayjs_plugin_isSameOrAfter_js = __toESM(dayjs_plugin_isSameOrAfter_js, 1);
33
+ let dayjs_plugin_timezone_js = require("dayjs/plugin/timezone.js");
34
+ dayjs_plugin_timezone_js = __toESM(dayjs_plugin_timezone_js, 1);
35
+ let dayjs_plugin_utc_js = require("dayjs/plugin/utc.js");
36
+ dayjs_plugin_utc_js = __toESM(dayjs_plugin_utc_js, 1);
37
+ let node_events = require("node:events");
38
+
39
+ //#region ../../@warlock.js/scheduler/src/cron-parser.ts
40
+ /**
41
+ * Cron expression parser
42
+ *
43
+ * Supports standard 5-field cron expressions:
44
+ * ```
45
+ * ┌───────────── minute (0-59)
46
+ * │ ┌───────────── hour (0-23)
47
+ * │ │ ┌───────────── day of month (1-31)
48
+ * │ │ │ ┌───────────── month (1-12)
49
+ * │ │ │ │ ┌───────────── day of week (0-6, Sunday = 0)
50
+ * │ │ │ │ │
51
+ * * * * * *
52
+ * ```
53
+ *
54
+ * Supports:
55
+ * - `*` - any value
56
+ * - `5` - specific value
57
+ * - `1,3,5` - list of values
58
+ * - `1-5` - range of values
59
+ * - `*‍/5` - step values (every 5)
60
+ * - `1-10/2` - range with step
61
+ *
62
+ * **Day-of-month / day-of-week semantics:** follows Vixie cron — when both
63
+ * fields are restricted (neither is `*`), a date matches if it satisfies
64
+ * EITHER constraint. When only one is restricted, the other is ignored.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const parser = new CronParser("0 9 * * 1-5"); // 9 AM weekdays
69
+ * const nextRun = parser.nextRun();
70
+ * ```
71
+ */
72
+ var CronParser = class {
73
+ /**
74
+ * Creates a new CronParser instance
75
+ *
76
+ * @param expression - Standard 5-field cron expression
77
+ * @throws Error if expression is invalid
78
+ */
79
+ constructor(_expression) {
80
+ this._expression = _expression;
81
+ const parts = _expression.trim().split(/\s+/);
82
+ if (parts.length !== 5) throw new Error(`Invalid cron expression: "${_expression}". Expected 5 fields (minute hour day month weekday).`);
83
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
84
+ this._isDayOfMonthRestricted = dayOfMonth.trim() !== "*";
85
+ this._isDayOfWeekRestricted = dayOfWeek.trim() !== "*";
86
+ this._fields = {
87
+ minutes: this._parseField(minute, 0, 59),
88
+ hours: this._parseField(hour, 0, 23),
89
+ daysOfMonth: this._parseField(dayOfMonth, 1, 31),
90
+ months: this._parseField(month, 1, 12),
91
+ daysOfWeek: this._parseField(dayOfWeek, 0, 6)
92
+ };
93
+ this._assertSatisfiable();
94
+ }
95
+ /**
96
+ * Get the parsed cron fields
97
+ */
98
+ get fields() {
99
+ return this._fields;
100
+ }
101
+ /**
102
+ * Get the original expression
103
+ */
104
+ get expression() {
105
+ return this._expression;
106
+ }
107
+ /**
108
+ * Calculate the next run time from a given date
109
+ *
110
+ * @param from - Starting date (defaults to now)
111
+ * @returns Next run time as Dayjs
112
+ */
113
+ nextRun(from = (0, dayjs.default)()) {
114
+ let date = from.add(1, "minute").second(0).millisecond(0);
115
+ const maxIterations = 366 * 24 * 60;
116
+ let iterations = 0;
117
+ while (iterations < maxIterations) {
118
+ iterations++;
119
+ if (!this._fields.months.includes(date.month() + 1)) {
120
+ date = date.add(1, "month").date(1).hour(0).minute(0);
121
+ continue;
122
+ }
123
+ if (!this._dayMatches(date)) {
124
+ date = date.add(1, "day").hour(0).minute(0);
125
+ continue;
126
+ }
127
+ if (!this._fields.hours.includes(date.hour())) {
128
+ date = date.add(1, "hour").minute(0);
129
+ continue;
130
+ }
131
+ if (!this._fields.minutes.includes(date.minute())) {
132
+ date = date.add(1, "minute");
133
+ continue;
134
+ }
135
+ return date;
136
+ }
137
+ throw new Error(`Could not find next run time for cron expression: ${this._expression}`);
138
+ }
139
+ /**
140
+ * Check if a given date matches the cron expression
141
+ *
142
+ * @param date - Date to check
143
+ * @returns true if the date matches
144
+ */
145
+ matches(date) {
146
+ return this._fields.minutes.includes(date.minute()) && this._fields.hours.includes(date.hour()) && this._fields.months.includes(date.month() + 1) && this._dayMatches(date);
147
+ }
148
+ /**
149
+ * Day-of-month / day-of-week match using Vixie cron semantics.
150
+ *
151
+ * When both fields are restricted (neither was `*`), a date matches if
152
+ * EITHER the day-of-month or the day-of-week matches. When only one is
153
+ * restricted, only that one needs to match (the other is the full set
154
+ * by construction, so AND degenerates to the restricted one).
155
+ */
156
+ _dayMatches(date) {
157
+ const dayOfMonthMatches = this._fields.daysOfMonth.includes(date.date());
158
+ const dayOfWeekMatches = this._fields.daysOfWeek.includes(date.day());
159
+ if (this._isDayOfMonthRestricted && this._isDayOfWeekRestricted) return dayOfMonthMatches || dayOfWeekMatches;
160
+ return dayOfMonthMatches && dayOfWeekMatches;
161
+ }
162
+ /**
163
+ * Reject expressions whose day-of-month / month combination can never occur
164
+ * (e.g. `0 0 30 2 *` — February never has a 30th).
165
+ *
166
+ * Without this guard, `nextRun()` would scan its full ~1-year iteration
167
+ * budget (527,040 passes) before throwing — seconds of synchronous CPU that
168
+ * stalls the event loop. Catching it here makes the failure eager and cheap,
169
+ * consistent with every other validation throw in the constructor.
170
+ *
171
+ * Only applies when the day-of-week field is unrestricted (`*`). Under Vixie
172
+ * OR semantics, a restricted day-of-week keeps the schedule satisfiable via
173
+ * the weekday path even when no listed day-of-month fits any listed month, so
174
+ * such expressions must NOT be rejected.
175
+ */
176
+ _assertSatisfiable() {
177
+ if (this._isDayOfWeekRestricted) return;
178
+ const smallestDay = this._fields.daysOfMonth[0];
179
+ if (!this._fields.months.some((month) => smallestDay <= this._maxDaysInMonth(month))) throw new Error(`Impossible cron expression: "${this._expression}". No day-of-month in [${this._fields.daysOfMonth.join(", ")}] ever occurs in month(s) [${this._fields.months.join(", ")}].`);
180
+ }
181
+ /**
182
+ * Largest possible day number for a 1-based month, taking leap years into
183
+ * account so February resolves to 29 (a date that does occur, just not every
184
+ * year). Used by satisfiability checking, never for matching a concrete date.
185
+ */
186
+ _maxDaysInMonth(month) {
187
+ if (month === 2) return 29;
188
+ if ([
189
+ 4,
190
+ 6,
191
+ 9,
192
+ 11
193
+ ].includes(month)) return 30;
194
+ return 31;
195
+ }
196
+ /**
197
+ * Parse a single cron field
198
+ *
199
+ * @param field - Field value (e.g., "*", "5", "1-5", "*‍/2", "1,3,5")
200
+ * @param min - Minimum allowed value
201
+ * @param max - Maximum allowed value
202
+ * @returns Array of matching values
203
+ */
204
+ _parseField(field, min, max) {
205
+ const values = /* @__PURE__ */ new Set();
206
+ const parts = field.split(",");
207
+ for (const part of parts) {
208
+ const [range, stepStr] = part.split("/");
209
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
210
+ if (isNaN(step) || step < 1) throw new Error(`Invalid step value in cron field: "${field}"`);
211
+ let rangeStart;
212
+ let rangeEnd;
213
+ if (range === "*") {
214
+ rangeStart = min;
215
+ rangeEnd = max;
216
+ } else if (range.includes("-")) {
217
+ const [startStr, endStr] = range.split("-");
218
+ rangeStart = parseInt(startStr, 10);
219
+ rangeEnd = parseInt(endStr, 10);
220
+ if (isNaN(rangeStart) || isNaN(rangeEnd)) throw new Error(`Invalid range in cron field: "${field}"`);
221
+ if (rangeStart < min || rangeEnd > max || rangeStart > rangeEnd) throw new Error(`Range out of bounds in cron field: "${field}" (valid: ${min}-${max})`);
222
+ } else {
223
+ const value = parseInt(range, 10);
224
+ if (isNaN(value)) throw new Error(`Invalid value in cron field: "${field}"`);
225
+ if (value < min || value > max) throw new Error(`Value out of bounds in cron field: "${field}" (valid: ${min}-${max})`);
226
+ rangeStart = value;
227
+ rangeEnd = value;
228
+ }
229
+ for (let i = rangeStart; i <= rangeEnd; i += step) values.add(i);
230
+ }
231
+ return Array.from(values).sort((a, b) => a - b);
232
+ }
233
+ };
234
+ /**
235
+ * Parse a cron expression string
236
+ *
237
+ * @param expression - Cron expression (5 fields)
238
+ * @returns CronParser instance
239
+ */
240
+ function parseCron(expression) {
241
+ return new CronParser(expression);
242
+ }
243
+
244
+ //#endregion
245
+ //#region ../../@warlock.js/scheduler/src/job.ts
246
+ dayjs.default.extend(dayjs_plugin_utc_js.default);
247
+ dayjs.default.extend(dayjs_plugin_timezone_js.default);
248
+ dayjs.default.extend(dayjs_plugin_isSameOrAfter_js.default);
249
+ /**
250
+ * Days of week mapping (lowercase for consistency with Day type)
251
+ */
252
+ const DAYS_OF_WEEK = [
253
+ "sunday",
254
+ "monday",
255
+ "tuesday",
256
+ "wednesday",
257
+ "thursday",
258
+ "friday",
259
+ "saturday"
260
+ ];
261
+ /**
262
+ * Validate a `HH:mm` or `HH:mm:ss` string and return its parts.
263
+ * Throws when the format is malformed or any part is out of range.
264
+ */
265
+ function parseTimeString(time) {
266
+ if (!/^\d{1,2}:\d{2}(:\d{2})?$/.test(time)) throw new Error("Invalid time format. Use HH:mm or HH:mm:ss.");
267
+ const [hour, minute, second = 0] = time.split(":").map(Number);
268
+ if (hour < 0 || hour > 23) throw new Error(`Invalid hour in time "${time}". Must be between 0 and 23.`);
269
+ if (minute < 0 || minute > 59) throw new Error(`Invalid minute in time "${time}". Must be between 0 and 59.`);
270
+ if (second < 0 || second > 59) throw new Error(`Invalid second in time "${time}". Must be between 0 and 59.`);
271
+ return {
272
+ hour,
273
+ minute,
274
+ second
275
+ };
276
+ }
277
+ /**
278
+ * Job class represents a scheduled task with configurable timing and execution options.
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * const job = new Job("cleanup", async () => {
283
+ * await cleanupOldFiles();
284
+ * })
285
+ * .everyDay()
286
+ * .at("03:00")
287
+ * .inTimezone("America/New_York")
288
+ * .preventOverlap()
289
+ * .retry(3, 1000);
290
+ * ```
291
+ */
292
+ var Job = class {
293
+ /**
294
+ * Creates a new Job instance
295
+ *
296
+ * @param name - Unique identifier for the job
297
+ * @param callback - Function to execute when the job runs
298
+ */
299
+ constructor(name, _callback) {
300
+ this.name = name;
301
+ this._callback = _callback;
302
+ this._intervals = {};
303
+ this._lastRun = null;
304
+ this._isRunning = false;
305
+ this._skipIfRunning = false;
306
+ this._retryConfig = null;
307
+ this._timezone = "UTC";
308
+ this._cronParser = null;
309
+ this._completionResolvers = [];
310
+ this.nextRun = null;
311
+ }
312
+ /**
313
+ * Returns true if the job is currently executing
314
+ */
315
+ get isRunning() {
316
+ return this._isRunning;
317
+ }
318
+ /**
319
+ * Returns the last execution timestamp (success OR failure).
320
+ */
321
+ get lastRun() {
322
+ return this._lastRun;
323
+ }
324
+ /**
325
+ * Returns the current interval configuration (readonly)
326
+ */
327
+ get intervals() {
328
+ return this._intervals;
329
+ }
330
+ /**
331
+ * Set a custom interval for job execution
332
+ *
333
+ * @param value - Number of time units (must be > 0)
334
+ * @param timeType - Type of time unit
335
+ * @returns this for chaining
336
+ * @throws Error if `value` is not a positive finite number
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * job.every(5, "minute"); // Run every 5 minutes
341
+ * job.every(2, "hour"); // Run every 2 hours
342
+ * ```
343
+ */
344
+ every(value, timeType) {
345
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`Invalid interval value: ${value}. Must be a positive finite number.`);
346
+ this._intervals.every = {
347
+ type: timeType,
348
+ value
349
+ };
350
+ this._determineNextRun();
351
+ return this;
352
+ }
353
+ /**
354
+ * Run job every second (use with caution - high frequency)
355
+ */
356
+ everySecond() {
357
+ return this.every(1, "second");
358
+ }
359
+ /**
360
+ * Run job every specified number of seconds
361
+ */
362
+ everySeconds(seconds) {
363
+ return this.every(seconds, "second");
364
+ }
365
+ /**
366
+ * Run job every minute
367
+ */
368
+ everyMinute() {
369
+ return this.every(1, "minute");
370
+ }
371
+ /**
372
+ * Run job every specified number of minutes
373
+ */
374
+ everyMinutes(minutes) {
375
+ return this.every(minutes, "minute");
376
+ }
377
+ /**
378
+ * Run job every hour
379
+ */
380
+ everyHour() {
381
+ return this.every(1, "hour");
382
+ }
383
+ /**
384
+ * Run job every specified number of hours
385
+ */
386
+ everyHours(hours) {
387
+ return this.every(hours, "hour");
388
+ }
389
+ /**
390
+ * Run job every day at midnight
391
+ */
392
+ everyDay() {
393
+ return this.every(1, "day");
394
+ }
395
+ /**
396
+ * Alias for everyDay()
397
+ */
398
+ daily() {
399
+ return this.everyDay();
400
+ }
401
+ /**
402
+ * Run job twice a day (every 12 hours)
403
+ */
404
+ twiceDaily() {
405
+ return this.every(12, "hour");
406
+ }
407
+ /**
408
+ * Run job every week
409
+ */
410
+ everyWeek() {
411
+ return this.every(1, "week");
412
+ }
413
+ /**
414
+ * Alias for everyWeek()
415
+ */
416
+ weekly() {
417
+ return this.everyWeek();
418
+ }
419
+ /**
420
+ * Run job every month
421
+ */
422
+ everyMonth() {
423
+ return this.every(1, "month");
424
+ }
425
+ /**
426
+ * Alias for everyMonth()
427
+ */
428
+ monthly() {
429
+ return this.everyMonth();
430
+ }
431
+ /**
432
+ * Run job every year
433
+ */
434
+ everyYear() {
435
+ return this.every(1, "year");
436
+ }
437
+ /**
438
+ * Alias for everyYear()
439
+ */
440
+ yearly() {
441
+ return this.everyYear();
442
+ }
443
+ /**
444
+ * Alias for everyMinute() - job runs continuously every minute
445
+ */
446
+ always() {
447
+ return this.everyMinute();
448
+ }
449
+ /**
450
+ * Schedule job using a cron expression
451
+ *
452
+ * Supports standard 5-field cron syntax:
453
+ * ```
454
+ * ┌───────────── minute (0-59)
455
+ * │ ┌───────────── hour (0-23)
456
+ * │ │ ┌───────────── day of month (1-31)
457
+ * │ │ │ ┌───────────── month (1-12)
458
+ * │ │ │ │ ┌───────────── day of week (0-6, Sunday = 0)
459
+ * │ │ │ │ │
460
+ * * * * * *
461
+ * ```
462
+ *
463
+ * Supports:
464
+ * - '*' - any value
465
+ * - '5' - specific value
466
+ * - '1,3,5' - list of values
467
+ * - '1-5' - range of values
468
+ * - '*‍/5' - step values (every 5)
469
+ * - '1-10/2' - range with step
470
+ *
471
+ * @param expression - Standard 5-field cron expression
472
+ * @returns this for chaining
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * job.cron("0 9 * * 1-5"); // 9 AM weekdays
477
+ * job.cron("*‍/5 * * * *"); // Every 5 minutes
478
+ * job.cron("0 0 1 * *"); // First day of month at midnight
479
+ * job.cron("0 *‍/2 * * *"); // Every 2 hours
480
+ * ```
481
+ */
482
+ cron(expression) {
483
+ this._cronParser = new CronParser(expression);
484
+ this._intervals = {};
485
+ this._determineNextRun();
486
+ return this;
487
+ }
488
+ /**
489
+ * Get the cron expression if one is set
490
+ */
491
+ get cronExpression() {
492
+ return this._cronParser?.expression ?? null;
493
+ }
494
+ /**
495
+ * Schedule job on a specific day
496
+ *
497
+ * @param day - Day of week (string) or day of month (number 1-31)
498
+ * @returns this for chaining
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * job.on("monday"); // Run on Mondays
503
+ * job.on(15); // Run on the 15th of each month
504
+ * ```
505
+ */
506
+ on(day) {
507
+ if (typeof day === "number" && (day < 1 || day > 31)) throw new Error("Invalid day of the month. Must be between 1 and 31.");
508
+ this._intervals.day = day;
509
+ if (typeof day === "number") this._intervals.dayOfMonthMode = "specific";
510
+ this._determineNextRun();
511
+ return this;
512
+ }
513
+ /**
514
+ * Schedule job at a specific time
515
+ *
516
+ * @param time - Time in HH:mm or HH:mm:ss format
517
+ * @returns this for chaining
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * job.daily().at("09:00"); // Run daily at 9 AM
522
+ * job.weekly().at("14:30"); // Run weekly at 2:30 PM
523
+ * ```
524
+ */
525
+ at(time) {
526
+ parseTimeString(time);
527
+ this._intervals.time = time;
528
+ this._determineNextRun();
529
+ return this;
530
+ }
531
+ /**
532
+ * Run task at the beginning of the specified time period.
533
+ *
534
+ * - `"day"` → 00:00 every day
535
+ * - `"month"` → 1st of every month at 00:00
536
+ * - `"year"` → January 1st at 00:00 every year
537
+ *
538
+ * @param type - Time type (day, month, year)
539
+ */
540
+ beginOf(type) {
541
+ switch (type) {
542
+ case "day":
543
+ this._intervals.every = {
544
+ type: "day",
545
+ value: 1
546
+ };
547
+ break;
548
+ case "month":
549
+ this._intervals.day = 1;
550
+ this._intervals.dayOfMonthMode = "specific";
551
+ this._intervals.every = {
552
+ type: "month",
553
+ value: 1
554
+ };
555
+ break;
556
+ case "year":
557
+ this._intervals.month = 1;
558
+ this._intervals.day = 1;
559
+ this._intervals.dayOfMonthMode = "specific";
560
+ this._intervals.every = {
561
+ type: "year",
562
+ value: 1
563
+ };
564
+ break;
565
+ default: throw new Error(`Unsupported type for beginOf: ${type}`);
566
+ }
567
+ return this.at("00:00");
568
+ }
569
+ /**
570
+ * Run task at the end of the specified time period.
571
+ *
572
+ * - `"day"` → 23:59 every day
573
+ * - `"month"` → last day of every month at 23:59 (recomputed each cycle —
574
+ * correct in February vs. March)
575
+ * - `"year"` → December 31st at 23:59 every year
576
+ *
577
+ * @param type - Time type (day, month, year)
578
+ */
579
+ endOf(type) {
580
+ switch (type) {
581
+ case "day":
582
+ this._intervals.every = {
583
+ type: "day",
584
+ value: 1
585
+ };
586
+ break;
587
+ case "month":
588
+ this._intervals.dayOfMonthMode = "last";
589
+ this._intervals.every = {
590
+ type: "month",
591
+ value: 1
592
+ };
593
+ break;
594
+ case "year":
595
+ this._intervals.month = 12;
596
+ this._intervals.day = 31;
597
+ this._intervals.dayOfMonthMode = "specific";
598
+ this._intervals.every = {
599
+ type: "year",
600
+ value: 1
601
+ };
602
+ break;
603
+ default: throw new Error(`Unsupported type for endOf: ${type}`);
604
+ }
605
+ return this.at("23:59");
606
+ }
607
+ /**
608
+ * Set the timezone for this job's scheduling
609
+ *
610
+ * @param tz - IANA timezone string (e.g., "America/New_York", "Europe/London")
611
+ * @returns this for chaining
612
+ *
613
+ * @example
614
+ * ```typescript
615
+ * job.daily().at("09:00").inTimezone("America/New_York");
616
+ * ```
617
+ */
618
+ inTimezone(tz) {
619
+ this._timezone = tz;
620
+ this._determineNextRun();
621
+ return this;
622
+ }
623
+ /**
624
+ * Prevent overlapping executions of this job.
625
+ *
626
+ * When enabled, if the job is already running when it's scheduled to run again,
627
+ * the new execution will be skipped.
628
+ *
629
+ * @param skip - Whether to skip if already running (default: true)
630
+ * @returns this for chaining
631
+ */
632
+ preventOverlap(skip = true) {
633
+ this._skipIfRunning = skip;
634
+ return this;
635
+ }
636
+ /**
637
+ * Configure automatic retry on failure
638
+ *
639
+ * @param maxRetries - Maximum number of retry attempts (must be ≥ 0)
640
+ * @param delay - Delay between retries in milliseconds (must be ≥ 0)
641
+ * @param backoffMultiplier - Optional multiplier for exponential backoff (must be > 0)
642
+ * @returns this for chaining
643
+ *
644
+ * @example
645
+ * ```typescript
646
+ * job.retry(3, 1000); // Retry 3 times with 1s delay
647
+ * job.retry(5, 1000, 2); // Exponential backoff: 1s, 2s, 4s, 8s, 16s
648
+ * ```
649
+ */
650
+ retry(maxRetries, delay = 1e3, backoffMultiplier) {
651
+ if (!Number.isFinite(maxRetries) || maxRetries < 0) throw new Error(`Invalid maxRetries: ${maxRetries}. Must be ≥ 0.`);
652
+ if (!Number.isFinite(delay) || delay < 0) throw new Error(`Invalid retry delay: ${delay}. Must be ≥ 0.`);
653
+ if (backoffMultiplier !== void 0 && (!Number.isFinite(backoffMultiplier) || backoffMultiplier <= 0)) throw new Error(`Invalid backoffMultiplier: ${backoffMultiplier}. Must be > 0.`);
654
+ this._retryConfig = {
655
+ maxRetries,
656
+ delay,
657
+ backoffMultiplier
658
+ };
659
+ return this;
660
+ }
661
+ /**
662
+ * Terminate the job and clear all scheduling data
663
+ */
664
+ terminate() {
665
+ this._intervals = {};
666
+ this._cronParser = null;
667
+ this.nextRun = null;
668
+ this._lastRun = null;
669
+ this._isRunning = false;
670
+ return this;
671
+ }
672
+ /**
673
+ * Prepare the job by calculating the next run time
674
+ * Called by the scheduler when starting
675
+ */
676
+ prepare() {
677
+ this._determineNextRun();
678
+ }
679
+ /**
680
+ * Returns true if the job's `nextRun` has arrived, regardless of whether
681
+ * it is currently running. Used by the scheduler to decide whether a
682
+ * tick is "due" before checking overlap state.
683
+ */
684
+ isDue() {
685
+ return this.nextRun !== null && this._now().isSameOrAfter(this.nextRun);
686
+ }
687
+ /**
688
+ * Determine if the job should run now.
689
+ *
690
+ * Combines `isDue()` with the overlap-prevention rule: when
691
+ * `preventOverlap()` is on and the job is already running, this returns
692
+ * false even if the next-run time has arrived.
693
+ *
694
+ * @returns true if the job should execute
695
+ */
696
+ shouldRun() {
697
+ if (this._skipIfRunning && this._isRunning) return false;
698
+ return this.isDue();
699
+ }
700
+ /**
701
+ * Execute the job once.
702
+ *
703
+ * Always advances `lastRun` and recalculates `nextRun` (success OR failure)
704
+ * so a permanently failing job does not re-fire on every scheduler tick.
705
+ *
706
+ * @returns Promise resolving to the job result
707
+ */
708
+ async run() {
709
+ const startTime = Date.now();
710
+ this._isRunning = true;
711
+ let result;
712
+ let executedRetries = 0;
713
+ try {
714
+ executedRetries = (await this._executeWithRetry()).retries;
715
+ result = {
716
+ success: true,
717
+ duration: Date.now() - startTime,
718
+ retries: executedRetries
719
+ };
720
+ } catch (error) {
721
+ result = {
722
+ success: false,
723
+ duration: Date.now() - startTime,
724
+ error,
725
+ retries: this._retryConfig?.maxRetries ?? 0
726
+ };
727
+ } finally {
728
+ this._lastRun = this._now();
729
+ this._determineNextRun();
730
+ this._isRunning = false;
731
+ const resolvers = this._completionResolvers.splice(0);
732
+ for (const resolve of resolvers) resolve();
733
+ }
734
+ return result;
735
+ }
736
+ /**
737
+ * Wait for the currently executing run to complete.
738
+ *
739
+ * Multiple concurrent waiters are all resolved when the run finishes.
740
+ * Useful for graceful shutdown.
741
+ *
742
+ * @returns Promise that resolves when the job completes
743
+ */
744
+ waitForCompletion() {
745
+ if (!this._isRunning) return Promise.resolve();
746
+ return new Promise((resolve) => {
747
+ this._completionResolvers.push(resolve);
748
+ });
749
+ }
750
+ /**
751
+ * Get current time, respecting the configured timezone.
752
+ */
753
+ _now() {
754
+ return (0, dayjs.default)().tz(this._timezone);
755
+ }
756
+ /**
757
+ * Execute the callback with retry logic
758
+ */
759
+ async _executeWithRetry() {
760
+ let lastError;
761
+ let attempts = 0;
762
+ const maxAttempts = (this._retryConfig?.maxRetries ?? 0) + 1;
763
+ while (attempts < maxAttempts) try {
764
+ await this._callback(this);
765
+ return { retries: attempts };
766
+ } catch (error) {
767
+ lastError = error;
768
+ attempts++;
769
+ if (attempts < maxAttempts && this._retryConfig) {
770
+ const delay = this._calculateRetryDelay(attempts);
771
+ await this._sleep(delay);
772
+ }
773
+ }
774
+ throw lastError;
775
+ }
776
+ /**
777
+ * Calculate retry delay with optional exponential backoff
778
+ */
779
+ _calculateRetryDelay(attempt) {
780
+ if (!this._retryConfig) return 0;
781
+ const { delay, backoffMultiplier } = this._retryConfig;
782
+ if (backoffMultiplier) return delay * Math.pow(backoffMultiplier, attempt - 1);
783
+ return delay;
784
+ }
785
+ /**
786
+ * Sleep for specified milliseconds
787
+ */
788
+ _sleep(ms) {
789
+ return new Promise((resolve) => setTimeout(resolve, ms));
790
+ }
791
+ /**
792
+ * Apply month / day-of-month / day-of-week / time constraints to a Dayjs.
793
+ *
794
+ * Re-applied after every interval advance inside `_determineNextRun` so
795
+ * dynamic constraints (`dayOfMonthMode: "last"` recomputed per month, month
796
+ * lock for `beginOf/endOf("year")`) stay correct as the candidate moves
797
+ * forward.
798
+ */
799
+ _applyConstraints(date) {
800
+ let result = date;
801
+ if (this._intervals.month !== void 0) result = result.month(this._intervals.month - 1);
802
+ if (this._intervals.dayOfMonthMode === "last") result = result.date(result.daysInMonth());
803
+ else if (this._intervals.day !== void 0) if (typeof this._intervals.day === "number") result = result.date(this._intervals.day);
804
+ else {
805
+ const targetDay = DAYS_OF_WEEK.indexOf(this._intervals.day);
806
+ if (targetDay !== -1) result = result.day(targetDay);
807
+ }
808
+ if (this._intervals.time) {
809
+ const { hour, minute, second } = parseTimeString(this._intervals.time);
810
+ result = result.hour(hour).minute(minute).second(second).millisecond(0);
811
+ }
812
+ return result;
813
+ }
814
+ /**
815
+ * Calculate the next run time based on interval or cron configuration.
816
+ *
817
+ * Strategy: apply all constraints (month, day, time) ONCE before the
818
+ * advance loop, then advance by `every` until the candidate is in the
819
+ * future. Static constraints (numeric day, fixed month, fixed time)
820
+ * survive `dayjs.add()` automatically. The only dynamic constraint —
821
+ * `dayOfMonthMode: "last"` — is re-applied inside the loop so the
822
+ * candidate always lands on the *new* month's last day after each
823
+ * advance.
824
+ *
825
+ * Re-applying time/day inside the loop would deadlock: with
826
+ * `twiceDaily().at("06:00")` we'd advance 06:00 → 18:00 → snap back to
827
+ * 06:00 → 18:00 → ... forever.
828
+ */
829
+ _determineNextRun() {
830
+ if (this._cronParser) {
831
+ const now = this._now();
832
+ this.nextRun = this._cronParser.nextRun(now);
833
+ return;
834
+ }
835
+ const intervalValue = this._intervals.every?.value;
836
+ const intervalType = this._intervals.every?.type;
837
+ const hasInterval = !!(intervalValue && intervalType);
838
+ let date;
839
+ if (this._lastRun && hasInterval) date = this._lastRun.add(intervalValue, intervalType);
840
+ else if (this._lastRun) date = this._lastRun.add(1, "second");
841
+ else date = this._now();
842
+ date = this._applyConstraints(date);
843
+ while (date.isBefore(this._now())) {
844
+ if (hasInterval) date = date.add(intervalValue, intervalType);
845
+ else date = date.add(1, "day");
846
+ if (this._intervals.dayOfMonthMode === "last") date = date.date(date.daysInMonth());
847
+ }
848
+ this.nextRun = date;
849
+ }
850
+ };
851
+ /**
852
+ * Factory function to create a new Job instance
853
+ *
854
+ * @param name - Unique identifier for the job
855
+ * @param callback - Function to execute when the job runs
856
+ * @returns New Job instance
857
+ *
858
+ * @example
859
+ * ```typescript
860
+ * const cleanupJob = job("cleanup", async () => {
861
+ * await db.deleteExpiredTokens();
862
+ * }).daily().at("03:00");
863
+ * ```
864
+ */
865
+ function job(name, callback) {
866
+ return new Job(name, callback);
867
+ }
868
+
869
+ //#endregion
870
+ //#region ../../@warlock.js/scheduler/src/scheduler.ts
871
+ /**
872
+ * Scheduler class manages and executes scheduled jobs.
873
+ *
874
+ * Features:
875
+ * - Event-based observability
876
+ * - Parallel or sequential job execution
877
+ * - Drift compensation for accurate timing
878
+ * - Graceful shutdown with job draining
879
+ *
880
+ * @example
881
+ * ```typescript
882
+ * const scheduler = new Scheduler();
883
+ *
884
+ * scheduler.on('job:error', (jobName, error) => {
885
+ * logger.error(`Job ${jobName} failed:`, error);
886
+ * });
887
+ *
888
+ * scheduler
889
+ * .addJob(cleanupJob)
890
+ * .addJob(reportJob)
891
+ * .runInParallel(true)
892
+ * .start();
893
+ *
894
+ * // Graceful shutdown
895
+ * process.on('SIGTERM', () => scheduler.shutdown());
896
+ * ```
897
+ */
898
+ var Scheduler = class extends node_events.EventEmitter {
899
+ constructor(..._args) {
900
+ super(..._args);
901
+ this._jobs = [];
902
+ this._timeoutId = null;
903
+ this._tickInterval = 1e3;
904
+ this._runInParallel = false;
905
+ this._maxConcurrency = 10;
906
+ this._isShuttingDown = false;
907
+ }
908
+ /**
909
+ * Returns true if the scheduler is currently running
910
+ */
911
+ get isRunning() {
912
+ return this._timeoutId !== null;
913
+ }
914
+ /**
915
+ * Returns the number of registered jobs
916
+ */
917
+ get jobCount() {
918
+ return this._jobs.length;
919
+ }
920
+ /**
921
+ * Add a job to the scheduler
922
+ *
923
+ * @param job - Job instance to schedule
924
+ * @returns this for chaining
925
+ */
926
+ addJob(job) {
927
+ this._jobs.push(job);
928
+ if (this.isRunning) job.prepare();
929
+ return this;
930
+ }
931
+ /**
932
+ * Alias to create a new job directly and store it
933
+ */
934
+ newJob(name, jobCallback) {
935
+ const job = new Job(name, jobCallback);
936
+ this.addJob(job);
937
+ return job;
938
+ }
939
+ /**
940
+ * Add multiple jobs to the scheduler.
941
+ *
942
+ * If the scheduler is already running, every newly-added job is prepared
943
+ * (its initial `nextRun` is computed) so it begins firing on the next tick.
944
+ *
945
+ * @param jobs - Array of Job instances
946
+ * @returns this for chaining
947
+ */
948
+ addJobs(jobs) {
949
+ this._jobs.push(...jobs);
950
+ if (this.isRunning) for (const job of jobs) job.prepare();
951
+ return this;
952
+ }
953
+ /**
954
+ * Remove a job from the scheduler by name
955
+ *
956
+ * @param jobName - Name of the job to remove
957
+ * @returns true if job was found and removed
958
+ */
959
+ removeJob(jobName) {
960
+ const index = this._jobs.findIndex((j) => j.name === jobName);
961
+ if (index !== -1) {
962
+ this._jobs.splice(index, 1);
963
+ return true;
964
+ }
965
+ return false;
966
+ }
967
+ /**
968
+ * Get a job by name
969
+ *
970
+ * @param jobName - Name of the job to find
971
+ * @returns Job instance or undefined
972
+ */
973
+ getJob(jobName) {
974
+ return this._jobs.find((j) => j.name === jobName);
975
+ }
976
+ /**
977
+ * Get all registered jobs
978
+ *
979
+ * @returns Array of registered jobs (readonly)
980
+ */
981
+ list() {
982
+ return this._jobs;
983
+ }
984
+ /**
985
+ * Set the tick interval (how often to check for due jobs)
986
+ *
987
+ * @param ms - Interval in milliseconds (minimum 100ms)
988
+ * @returns this for chaining
989
+ */
990
+ runEvery(ms) {
991
+ if (ms < 100) throw new Error("Tick interval must be at least 100ms");
992
+ this._tickInterval = ms;
993
+ return this;
994
+ }
995
+ /**
996
+ * Configure whether jobs should run in parallel
997
+ *
998
+ * @param parallel - Enable parallel execution
999
+ * @param maxConcurrency - Maximum concurrent jobs (default: 10)
1000
+ * @returns this for chaining
1001
+ */
1002
+ runInParallel(parallel, maxConcurrency = 10) {
1003
+ this._runInParallel = parallel;
1004
+ this._maxConcurrency = maxConcurrency;
1005
+ return this;
1006
+ }
1007
+ /**
1008
+ * Start the scheduler
1009
+ *
1010
+ * @throws Error if scheduler is already running
1011
+ */
1012
+ start() {
1013
+ if (this.isRunning) throw new Error("Scheduler is already running.");
1014
+ if (this._jobs.length === 0) throw new Error("Cannot start scheduler with no jobs.");
1015
+ for (const job of this._jobs) job.prepare();
1016
+ this._isShuttingDown = false;
1017
+ this._scheduleTick(this._tickInterval);
1018
+ this.emit("scheduler:started");
1019
+ }
1020
+ /**
1021
+ * Stop the scheduler immediately.
1022
+ *
1023
+ * No-op if the scheduler isn't running. Does not wait for in-flight jobs —
1024
+ * use `shutdown()` for graceful termination.
1025
+ */
1026
+ stop() {
1027
+ if (!this._timeoutId) return;
1028
+ clearTimeout(this._timeoutId);
1029
+ this._timeoutId = null;
1030
+ this.emit("scheduler:stopped");
1031
+ }
1032
+ /**
1033
+ * Gracefully shutdown the scheduler
1034
+ *
1035
+ * Stops scheduling new jobs and waits for currently running jobs to complete.
1036
+ *
1037
+ * @param timeout - Maximum time to wait for jobs (default: 30000ms)
1038
+ * @returns Promise that resolves when shutdown is complete
1039
+ */
1040
+ async shutdown(timeout = 3e4) {
1041
+ this._isShuttingDown = true;
1042
+ this.stop();
1043
+ const runningJobs = this._jobs.filter((j) => j.isRunning);
1044
+ if (runningJobs.length > 0) await Promise.race([Promise.all(runningJobs.map((j) => j.waitForCompletion())), new Promise((resolve) => setTimeout(resolve, timeout))]);
1045
+ }
1046
+ /**
1047
+ * Schedule the next tick.
1048
+ *
1049
+ * Drift-compensated: after each tick, the next delay is `tickInterval -
1050
+ * elapsed-tick-time` (clamped to 0). A tick that takes 600 ms on a 1 000 ms
1051
+ * interval will be followed by a 400 ms delay so the *period* between tick
1052
+ * starts averages `tickInterval` instead of `tickInterval + work-time`.
1053
+ */
1054
+ _scheduleTick(delay) {
1055
+ if (this._isShuttingDown) return;
1056
+ this._timeoutId = setTimeout(async () => {
1057
+ const tickStart = Date.now();
1058
+ await this._tick();
1059
+ const elapsed = Date.now() - tickStart;
1060
+ const nextDelay = Math.max(this._tickInterval - elapsed, 0);
1061
+ this._scheduleTick(nextDelay);
1062
+ }, delay);
1063
+ }
1064
+ /**
1065
+ * Execute a scheduler tick - check and run due jobs
1066
+ */
1067
+ async _tick() {
1068
+ this.emit("scheduler:tick", /* @__PURE__ */ new Date());
1069
+ const dueJobs = this._jobs.filter((job) => {
1070
+ if (!job.isDue()) return false;
1071
+ if (job.isRunning) {
1072
+ this.emit("job:skip", job.name, "Job is already running");
1073
+ return false;
1074
+ }
1075
+ return true;
1076
+ });
1077
+ if (dueJobs.length === 0) return;
1078
+ if (this._runInParallel) await this._runJobsInParallel(dueJobs);
1079
+ else await this._runJobsSequentially(dueJobs);
1080
+ }
1081
+ /**
1082
+ * Run jobs sequentially
1083
+ */
1084
+ async _runJobsSequentially(jobs) {
1085
+ for (const job of jobs) {
1086
+ if (this._isShuttingDown) break;
1087
+ await this._runJob(job);
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Run jobs in parallel with concurrency limit
1092
+ */
1093
+ async _runJobsInParallel(jobs) {
1094
+ const batches = [];
1095
+ for (let i = 0; i < jobs.length; i += this._maxConcurrency) batches.push(jobs.slice(i, i + this._maxConcurrency));
1096
+ for (const batch of batches) {
1097
+ if (this._isShuttingDown) break;
1098
+ await Promise.allSettled(batch.map((job) => this._runJob(job)));
1099
+ }
1100
+ }
1101
+ /**
1102
+ * Run a single job and emit events
1103
+ */
1104
+ async _runJob(job) {
1105
+ this.emit("job:start", job.name);
1106
+ const result = await job.run();
1107
+ if (result.success) this.emit("job:complete", job.name, result);
1108
+ else this.emit("job:error", job.name, result.error);
1109
+ return result;
1110
+ }
1111
+ };
1112
+ /**
1113
+ * Default scheduler instance for simple use cases
1114
+ *
1115
+ * @example
1116
+ * ```typescript
1117
+ * import { scheduler, job } from "@warlock.js/scheduler";
1118
+ *
1119
+ * scheduler.addJob(job("cleanup", cleanupFn).daily());
1120
+ * scheduler.start();
1121
+ * ```
1122
+ */
1123
+ const scheduler = new Scheduler();
1124
+
1125
+ //#endregion
1126
+ exports.CronParser = CronParser;
1127
+ exports.Job = Job;
1128
+ exports.Scheduler = Scheduler;
1129
+ exports.job = job;
1130
+ exports.parseCron = parseCron;
1131
+ exports.scheduler = scheduler;
1132
+ //# sourceMappingURL=index.cjs.map