@stacksjs/scheduler 0.58.51 → 0.58.53

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/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
+ }
@@ -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