@stacksjs/scheduler 0.58.51 → 0.58.52
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/index.js +682 -152
- package/package.json +7 -4
- package/src/constants.ts +81 -0
- package/src/errors.ts +7 -0
- package/src/index.ts +15 -317
- package/src/job.ts +315 -0
- package/src/schedule.ts +111 -0
- package/src/time.ts +823 -0
- package/src/types/cron.ts +98 -0
- package/src/types/utils.ts +14 -0
- package/src/utils.ts +19 -0
package/src/job.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Many thanks to https://github.com/kelektiv/node-cron for the inspiration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { CronError, ExclusiveParametersError } from './errors'
|
|
7
|
+
import { CronTime } from './time'
|
|
8
|
+
import type {
|
|
9
|
+
CronCallback,
|
|
10
|
+
CronCommand,
|
|
11
|
+
CronContext,
|
|
12
|
+
CronJobParams,
|
|
13
|
+
CronOnCompleteCallback,
|
|
14
|
+
CronOnCompleteCommand,
|
|
15
|
+
WithOnComplete,
|
|
16
|
+
} from './types/cron'
|
|
17
|
+
import { getTimeZoneAndOffset } from './utils'
|
|
18
|
+
|
|
19
|
+
export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
|
|
20
|
+
cronTime: CronTime
|
|
21
|
+
running = false
|
|
22
|
+
unrefTimeout = false
|
|
23
|
+
lastExecution: Date | null = null
|
|
24
|
+
runOnce = false
|
|
25
|
+
context: CronContext<C>
|
|
26
|
+
onComplete?: WithOnComplete<OC> extends true
|
|
27
|
+
? CronOnCompleteCallback
|
|
28
|
+
: undefined
|
|
29
|
+
|
|
30
|
+
private _timeout?: NodeJS.Timeout
|
|
31
|
+
private _callbacks: CronCallback<C, WithOnComplete<OC>>[] = []
|
|
32
|
+
private _errorHandler?: (error: Error) => void
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
cronTime: CronJobParams<OC, C>['cronTime'],
|
|
36
|
+
onTick: CronJobParams<OC, C>['onTick'],
|
|
37
|
+
onComplete?: CronJobParams<OC, C>['onComplete'],
|
|
38
|
+
start?: CronJobParams<OC, C>['start'],
|
|
39
|
+
timeZone?: CronJobParams<OC, C>['timeZone'],
|
|
40
|
+
context?: CronJobParams<OC, C>['context'],
|
|
41
|
+
runOnInit?: CronJobParams<OC, C>['runOnInit'],
|
|
42
|
+
utcOffset?: null,
|
|
43
|
+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
|
|
44
|
+
errorHandler?: (error: Error) => void
|
|
45
|
+
)
|
|
46
|
+
constructor(
|
|
47
|
+
cronTime: CronJobParams<OC, C>['cronTime'],
|
|
48
|
+
onTick: CronJobParams<OC, C>['onTick'],
|
|
49
|
+
onComplete?: CronJobParams<OC, C>['onComplete'],
|
|
50
|
+
start?: CronJobParams<OC, C>['start'],
|
|
51
|
+
timeZone?: null,
|
|
52
|
+
context?: CronJobParams<OC, C>['context'],
|
|
53
|
+
runOnInit?: CronJobParams<OC, C>['runOnInit'],
|
|
54
|
+
utcOffset?: CronJobParams<OC, C>['utcOffset'],
|
|
55
|
+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
|
|
56
|
+
errorHandler?: (error: Error) => void
|
|
57
|
+
)
|
|
58
|
+
constructor(
|
|
59
|
+
cronTime: CronJobParams<OC, C>['cronTime'],
|
|
60
|
+
onTick: CronJobParams<OC, C>['onTick'],
|
|
61
|
+
onComplete?: CronJobParams<OC, C>['onComplete'],
|
|
62
|
+
start?: CronJobParams<OC, C>['start'],
|
|
63
|
+
timeZone?: CronJobParams<OC, C>['timeZone'],
|
|
64
|
+
context?: CronJobParams<OC, C>['context'],
|
|
65
|
+
runOnInit?: CronJobParams<OC, C>['runOnInit'],
|
|
66
|
+
utcOffset?: CronJobParams<OC, C>['utcOffset'],
|
|
67
|
+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
|
|
68
|
+
errorHandler?: (error: Error) => void,
|
|
69
|
+
) {
|
|
70
|
+
this._errorHandler = errorHandler
|
|
71
|
+
this.context = (context ?? this) as CronContext<C>
|
|
72
|
+
|
|
73
|
+
const { timeZone: tz, utcOffset: uo } = getTimeZoneAndOffset(
|
|
74
|
+
timeZone,
|
|
75
|
+
utcOffset,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
this.cronTime = new CronTime(cronTime, tz, uo as null | undefined)
|
|
79
|
+
|
|
80
|
+
if (unrefTimeout != null)
|
|
81
|
+
this.unrefTimeout = unrefTimeout
|
|
82
|
+
|
|
83
|
+
if (onComplete != null) {
|
|
84
|
+
// casting to the correct type since we just made sure that WithOnComplete<OC> = true
|
|
85
|
+
this.onComplete = this._fnWrap(
|
|
86
|
+
onComplete,
|
|
87
|
+
) as WithOnComplete<OC> extends true ? CronOnCompleteCallback : undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.cronTime.realDate)
|
|
91
|
+
this.runOnce = true
|
|
92
|
+
|
|
93
|
+
this.addCallback(this._fnWrap(onTick))
|
|
94
|
+
|
|
95
|
+
if (runOnInit) {
|
|
96
|
+
this.lastExecution = new Date()
|
|
97
|
+
this.fireOnTick()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (start)
|
|
101
|
+
this.start()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static from<OC extends CronOnCompleteCommand | null = null, C = null>(
|
|
105
|
+
params: CronJobParams<OC, C>,
|
|
106
|
+
) {
|
|
107
|
+
// runtime check for JS users
|
|
108
|
+
if (params.timeZone != null && params.utcOffset != null)
|
|
109
|
+
throw new ExclusiveParametersError('timeZone', 'utcOffset')
|
|
110
|
+
|
|
111
|
+
if (params.timeZone != null) {
|
|
112
|
+
return new CronJob<OC, C>(
|
|
113
|
+
params.cronTime,
|
|
114
|
+
params.onTick,
|
|
115
|
+
params.onComplete,
|
|
116
|
+
params.start,
|
|
117
|
+
params.timeZone,
|
|
118
|
+
params.context,
|
|
119
|
+
params.runOnInit,
|
|
120
|
+
params.utcOffset,
|
|
121
|
+
params.unrefTimeout,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
else if (params.utcOffset != null) {
|
|
125
|
+
return new CronJob<OC, C>(
|
|
126
|
+
params.cronTime,
|
|
127
|
+
params.onTick,
|
|
128
|
+
params.onComplete,
|
|
129
|
+
params.start,
|
|
130
|
+
null,
|
|
131
|
+
params.context,
|
|
132
|
+
params.runOnInit,
|
|
133
|
+
params.utcOffset,
|
|
134
|
+
params.unrefTimeout,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return new CronJob<OC, C>(
|
|
139
|
+
params.cronTime,
|
|
140
|
+
params.onTick,
|
|
141
|
+
params.onComplete,
|
|
142
|
+
params.start,
|
|
143
|
+
params.timeZone,
|
|
144
|
+
params.context,
|
|
145
|
+
params.runOnInit,
|
|
146
|
+
params.utcOffset,
|
|
147
|
+
params.unrefTimeout,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private _fnWrap(cmd: CronCommand<C, boolean>): CronCallback<C, boolean> {
|
|
153
|
+
switch (typeof cmd) {
|
|
154
|
+
case 'function': {
|
|
155
|
+
return cmd
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'string': {
|
|
159
|
+
const [command, ...args] = cmd.split(' ')
|
|
160
|
+
|
|
161
|
+
return spawn.bind(undefined, command ?? cmd, args, {}) as () => void
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'object': {
|
|
165
|
+
return spawn.bind(
|
|
166
|
+
undefined,
|
|
167
|
+
cmd.command,
|
|
168
|
+
cmd.args ?? [],
|
|
169
|
+
cmd.options ?? {},
|
|
170
|
+
) as () => void
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
addCallback(callback: CronCallback<C, WithOnComplete<OC>>) {
|
|
176
|
+
if (typeof callback === 'function')
|
|
177
|
+
this._callbacks.push(callback)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setTime(time: CronTime) {
|
|
181
|
+
if (!(time instanceof CronTime))
|
|
182
|
+
throw new CronError('time must be an instance of CronTime.')
|
|
183
|
+
|
|
184
|
+
const wasRunning = this.running
|
|
185
|
+
this.stop()
|
|
186
|
+
|
|
187
|
+
this.cronTime = time
|
|
188
|
+
if (time.realDate)
|
|
189
|
+
this.runOnce = true
|
|
190
|
+
|
|
191
|
+
if (wasRunning)
|
|
192
|
+
this.start()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
nextDate() {
|
|
196
|
+
return this.cronTime.sendAt()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fireOnTick() {
|
|
200
|
+
try {
|
|
201
|
+
void callback.call(
|
|
202
|
+
this.context,
|
|
203
|
+
this.onComplete as WithOnComplete<OC> extends true
|
|
204
|
+
? CronOnCompleteCallback
|
|
205
|
+
: never,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (this._errorHandler && error instanceof Error) {
|
|
210
|
+
this._errorHandler(error)
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Handle the case where no error handler is provided or the caught object is not an Error
|
|
214
|
+
console.error('An error occurred in the cron job callback:', error)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
nextDates(i?: number) {
|
|
220
|
+
return this.cronTime.sendAt(i ?? 0)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
start() {
|
|
224
|
+
if (this.running)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
const MAXDELAY = 2147483647 // The maximum number of milliseconds setTimeout will wait.
|
|
228
|
+
let timeout = this.cronTime.getTimeout()
|
|
229
|
+
let remaining = 0
|
|
230
|
+
let startTime: number
|
|
231
|
+
|
|
232
|
+
const setCronTimeout = (t: number) => {
|
|
233
|
+
startTime = Date.now()
|
|
234
|
+
// eslint-disable-next-line ts/no-use-before-define
|
|
235
|
+
this._timeout = setTimeout(callbackWrapper, t)
|
|
236
|
+
if (this.unrefTimeout && typeof this._timeout.unref === 'function')
|
|
237
|
+
this._timeout.unref()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The callback wrapper checks if it needs to sleep another period or not
|
|
241
|
+
// and does the real callback logic when it's time.
|
|
242
|
+
const callbackWrapper = () => {
|
|
243
|
+
const diff = startTime + timeout - Date.now()
|
|
244
|
+
|
|
245
|
+
if (diff > 0) {
|
|
246
|
+
let newTimeout = this.cronTime.getTimeout()
|
|
247
|
+
|
|
248
|
+
if (newTimeout > diff)
|
|
249
|
+
newTimeout = diff
|
|
250
|
+
|
|
251
|
+
remaining += newTimeout
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// If there is sleep time remaining, calculate how long and go to sleep
|
|
255
|
+
// again. This processing might make us miss the deadline by a few ms
|
|
256
|
+
// times the number of sleep sessions. Given a MAXDELAY of almost a
|
|
257
|
+
// month, this should be no issue.
|
|
258
|
+
if (remaining) {
|
|
259
|
+
if (remaining > MAXDELAY) {
|
|
260
|
+
remaining -= MAXDELAY
|
|
261
|
+
timeout = MAXDELAY
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
timeout = remaining
|
|
265
|
+
remaining = 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
setCronTimeout(timeout)
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// We have arrived at the correct point in time.
|
|
272
|
+
this.lastExecution = new Date()
|
|
273
|
+
|
|
274
|
+
this.running = false
|
|
275
|
+
|
|
276
|
+
// start before calling back so the callbacks have the ability to stop the cron job
|
|
277
|
+
if (!this.runOnce)
|
|
278
|
+
this.start()
|
|
279
|
+
|
|
280
|
+
this.fireOnTick()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (timeout >= 0) {
|
|
285
|
+
this.running = true
|
|
286
|
+
|
|
287
|
+
// Don't try to sleep more than MAXDELAY ms at a time.
|
|
288
|
+
|
|
289
|
+
if (timeout > MAXDELAY) {
|
|
290
|
+
remaining = timeout - MAXDELAY
|
|
291
|
+
timeout = MAXDELAY
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setCronTimeout(timeout)
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.stop()
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
lastDate() {
|
|
302
|
+
return this.lastExecution
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Stop the cronjob.
|
|
307
|
+
*/
|
|
308
|
+
stop() {
|
|
309
|
+
if (this._timeout)
|
|
310
|
+
clearTimeout(this._timeout)
|
|
311
|
+
this.running = false
|
|
312
|
+
if (typeof this.onComplete === 'function')
|
|
313
|
+
void this.onComplete.call(this.context)
|
|
314
|
+
}
|
|
315
|
+
}
|
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* eslint-disable no-new */
|
|
2
|
+
import { log } from '@stacksjs/cli'
|
|
3
|
+
import type { DateTime } from 'luxon'
|
|
4
|
+
import { CronTime } from './time'
|
|
5
|
+
|
|
6
|
+
export class Schedule {
|
|
7
|
+
private cronPattern: string = ''
|
|
8
|
+
private timezone: string = 'America/Los_Angeles'
|
|
9
|
+
private task: () => void
|
|
10
|
+
private cmd?: string
|
|
11
|
+
|
|
12
|
+
constructor(task: () => void) {
|
|
13
|
+
this.task = task
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
everySecond() {
|
|
17
|
+
this.cronPattern = '* * * * * *'
|
|
18
|
+
return this
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
everyMinute() {
|
|
22
|
+
this.cronPattern = '0 * * * * *'
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
everyFiveMinutes() {
|
|
27
|
+
this.cronPattern = '*/5 * * * *'
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
everyTenMinutes() {
|
|
32
|
+
this.cronPattern = '*/10 * * * *'
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
everyThirtyMinutes() {
|
|
37
|
+
this.cronPattern = '*/30 * * * *'
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
hourly() {
|
|
42
|
+
this.cronPattern = '0 0 * * * *'
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
daily() {
|
|
47
|
+
this.cronPattern = '0 0 0 * * *'
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
weekly() {
|
|
52
|
+
this.cronPattern = '0 0 0 * * 0'
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
monthly() {
|
|
57
|
+
this.cronPattern = '0 0 0 1 * *'
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
yearly() {
|
|
62
|
+
this.cronPattern = '0 0 0 1 1 *'
|
|
63
|
+
return this
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onDays(days: number[]) {
|
|
67
|
+
const dayPattern = days.join(',')
|
|
68
|
+
this.cronPattern = `0 0 0 * * ${dayPattern}`
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// between(startTime: string, endTime: string) {
|
|
73
|
+
// // This method is a placeholder. Actual implementation will vary based on requirements.
|
|
74
|
+
// // Cron does not directly support "between" times without additional logic.
|
|
75
|
+
// console.warn('The "between" method is not directly supported by cron patterns and requires additional logic.')
|
|
76
|
+
// return this
|
|
77
|
+
// }
|
|
78
|
+
|
|
79
|
+
at(time: string) {
|
|
80
|
+
// Assuming time is in "HH:MM" format
|
|
81
|
+
const [hour, minute] = time.split(':').map(Number)
|
|
82
|
+
this.cronPattern = `${minute} ${hour} * * *`
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setTimeZone(timezone: string) {
|
|
87
|
+
this.timezone = timezone
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start() {
|
|
92
|
+
new CronJob(this.cronPattern, this.task, null, true, this.timezone)
|
|
93
|
+
log.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static command(cmd: string) {
|
|
97
|
+
// log.info(`Executing command: ${cmd}`)
|
|
98
|
+
this.cmd = cmd
|
|
99
|
+
return this
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function sendAt(cronTime: string | Date | DateTime): DateTime {
|
|
104
|
+
return new CronTime(cronTime).sendAt()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function timeout(cronTime: string | Date | DateTime): number {
|
|
108
|
+
return new CronTime(cronTime).getTimeout()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default Schedule
|