@stacksjs/scheduler 0.64.5 → 0.65.0

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.
@@ -0,0 +1,32 @@
1
+ import type { DateTime } from "luxon";
2
+ export declare class Schedule {
3
+ private cronPattern;
4
+ private timezone;
5
+ private readonly task;
6
+ constructor(task: () => void);
7
+ everySecond(): Schedule;
8
+ everyMinute(): Schedule;
9
+ everyTwoMinutes(): Schedule;
10
+ everyFiveMinutes(): Schedule;
11
+ everyTenMinutes(): Schedule;
12
+ everyThirtyMinutes(): Schedule;
13
+ everyHour(): Schedule;
14
+ everyDay(): Schedule;
15
+ hourly(): Schedule;
16
+ daily(): Schedule;
17
+ weekly(): Schedule;
18
+ monthly(): Schedule;
19
+ yearly(): Schedule;
20
+ onDays(days: number[]): Schedule;
21
+ at(time: string): Schedule;
22
+ setTimeZone(timezone: string): Schedule;
23
+ start(): void;
24
+ job(path: string): Schedule;
25
+ action(path: string): Schedule;
26
+ static command(cmd: string): Schedule;
27
+ }
28
+ export declare class Job extends Schedule {}
29
+ export declare function sendAt(cronTime: string | Date | DateTime): DateTime;
30
+ export declare function timeout(cronTime: string | Date | DateTime): number;
31
+ export type Scheduler = typeof Schedule;
32
+ export default Schedule;
@@ -0,0 +1,4 @@
1
+ export type IntRange<
2
+ Min extends number,
3
+ Max extends number
4
+ > = number extends Min | Max ? never : number | [Min | number, Max | number];
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@stacksjs/scheduler",
3
3
  "type": "module",
4
- "version": "0.64.5",
4
+ "version": "0.65.0",
5
5
  "description": "The Stacks scheduler.",
6
6
  "author": "Chris Breuer",
7
+ "contributors": ["Chris Breuer <chris@stacksjs.org>"],
7
8
  "license": "MIT",
8
9
  "funding": "https://github.com/sponsors/chrisbbreuer",
9
10
  "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/scheduler#readme",
@@ -15,13 +16,7 @@
15
16
  "bugs": {
16
17
  "url": "https://github.com/stacksjs/stacks/issues"
17
18
  },
18
- "keywords": [
19
- "scheduler",
20
- "node-cron",
21
- "jobs",
22
- "bun",
23
- "stacks"
24
- ],
19
+ "keywords": ["scheduler", "node-cron", "jobs", "bun", "stacks"],
25
20
  "exports": {
26
21
  ".": {
27
22
  "bun": "./src/index.ts",
@@ -34,27 +29,16 @@
34
29
  },
35
30
  "module": "dist/index.js",
36
31
  "types": "dist/index.d.ts",
37
- "contributors": [
38
- "Chris Breuer <chris@stacksjs.org>"
39
- ],
40
- "files": [
41
- "README.md",
42
- "dist",
43
- "src"
44
- ],
32
+ "files": ["README.md", "dist", "src"],
45
33
  "scripts": {
46
- "build": "bun --bun build.ts",
47
- "typecheck": "bun --bun tsc --noEmit",
34
+ "build": "bun build.ts",
35
+ "typecheck": "bun tsc --noEmit",
48
36
  "prepublishOnly": "bun run build"
49
37
  },
50
38
  "dependencies": {
51
- "luxon": "^3.5.0"
39
+ "cron": "^3.1.7"
52
40
  },
53
41
  "devDependencies": {
54
- "@fast-check/jest": "^2.0.2",
55
- "@stacksjs/development": "latest",
56
- "@types/luxon": "^3.4.2",
57
- "@types/sinon": "^17.0.3",
58
- "sinon": "^18.0.0"
42
+ "@stacksjs/development": "0.64.6"
59
43
  }
60
44
  }
package/src/cron.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type {
2
+ CronCallback,
3
+ CronCommand,
4
+ CronContext,
5
+ CronJobParams,
6
+ CronOnCompleteCallback,
7
+ CronOnCompleteCommand,
8
+ Ranges,
9
+ TimeUnit,
10
+ } from 'cron'
11
+ export { CronJob, CronTime } from 'cron'
package/src/index.ts CHANGED
@@ -1,20 +1,7 @@
1
- export { Schedule } from './schedule'
1
+ // export { Schedule } from './schedule'
2
2
 
3
- export { CronJob as BunCronJob } from './job'
4
- export { CronTime } from './time'
5
-
6
- export type {
7
- CronCallback,
8
- CronCommand,
9
- CronContext,
10
- CronJobParams,
11
- CronOnCompleteCallback,
12
- CronOnCompleteCommand,
13
- Ranges,
14
- TimeUnit,
15
- } from './types/cron'
16
-
17
- export * from './types/utils'
3
+ export * from './cron'
18
4
  export * from './schedule'
5
+ export * from './types'
19
6
 
20
7
  // export default Schedule
package/src/schedule.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { log } from '@stacksjs/cli'
2
1
  import type { DateTime } from 'luxon'
3
- import { CronJob } from './job'
4
- import { CronTime } from './time'
2
+ import { log, runCommand } from '@stacksjs/cli'
3
+ import { CronJob, CronTime } from './'
5
4
 
6
5
  export class Schedule {
7
6
  private cronPattern = ''
@@ -13,62 +12,72 @@ export class Schedule {
13
12
  this.task = task
14
13
  }
15
14
 
16
- everySecond() {
15
+ everySecond(): Schedule {
17
16
  this.cronPattern = '* * * * * *'
18
17
  return this
19
18
  }
20
19
 
21
- everyMinute() {
20
+ everyMinute(): Schedule {
22
21
  this.cronPattern = '0 * * * * *'
23
22
  return this
24
23
  }
25
24
 
26
- everyTwoMinutes() {
25
+ everyTwoMinutes(): Schedule {
27
26
  this.cronPattern = '*/2 * * * * *'
28
27
  return this
29
28
  }
30
29
 
31
- everyFiveMinutes() {
30
+ everyFiveMinutes(): Schedule {
32
31
  this.cronPattern = '*/5 * * * *'
33
32
  return this
34
33
  }
35
34
 
36
- everyTenMinutes() {
35
+ everyTenMinutes(): Schedule {
37
36
  this.cronPattern = '*/10 * * * *'
38
37
  return this
39
38
  }
40
39
 
41
- everyThirtyMinutes() {
40
+ everyThirtyMinutes(): Schedule {
42
41
  this.cronPattern = '*/30 * * * *'
43
42
  return this
44
43
  }
45
44
 
46
- hourly() {
45
+ everyHour(): Schedule {
47
46
  this.cronPattern = '0 0 * * * *'
48
47
  return this
49
48
  }
50
49
 
51
- daily() {
50
+ everyDay(): Schedule {
52
51
  this.cronPattern = '0 0 0 * * *'
53
52
  return this
54
53
  }
55
54
 
56
- weekly() {
55
+ hourly(): Schedule {
56
+ this.cronPattern = '0 0 * * * *'
57
+ return this
58
+ }
59
+
60
+ daily(): Schedule {
61
+ this.cronPattern = '0 0 0 * * *'
62
+ return this
63
+ }
64
+
65
+ weekly(): Schedule {
57
66
  this.cronPattern = '0 0 0 * * 0'
58
67
  return this
59
68
  }
60
69
 
61
- monthly() {
70
+ monthly(): Schedule {
62
71
  this.cronPattern = '0 0 0 1 * *'
63
72
  return this
64
73
  }
65
74
 
66
- yearly() {
75
+ yearly(): Schedule {
67
76
  this.cronPattern = '0 0 0 1 1 *'
68
77
  return this
69
78
  }
70
79
 
71
- onDays(days: number[]) {
80
+ onDays(days: number[]): Schedule {
72
81
  const dayPattern = days.join(',')
73
82
  this.cronPattern = `0 0 0 * * ${dayPattern}`
74
83
  return this
@@ -81,41 +90,54 @@ export class Schedule {
81
90
  // return this
82
91
  // }
83
92
 
84
- at(time: string) {
93
+ at(time: string): Schedule {
85
94
  // Assuming time is in "HH:MM" format
86
95
  const [hour, minute] = time.split(':').map(Number)
87
96
  this.cronPattern = `${minute} ${hour} * * *`
88
97
  return this
89
98
  }
90
99
 
91
- setTimeZone(timezone: string) {
100
+ setTimeZone(timezone: string): Schedule {
92
101
  this.timezone = timezone
93
102
  return this
94
103
  }
95
104
 
96
- start() {
105
+ start(): void {
106
+ // eslint-disable-next-line no-new
97
107
  new CronJob(this.cronPattern, this.task, null, true, this.timezone)
98
108
  log.info(`Scheduled task with pattern: ${this.cronPattern} in timezone: ${this.timezone}`)
99
109
  }
100
110
 
101
111
  // job and action methods need to be added and they accept a path string param
102
- job(path: string) {
112
+ job(path: string): Schedule {
103
113
  log.info(`Scheduling job: ${path}`)
104
114
  return this
105
115
  }
106
116
 
107
- action(path: string) {
117
+ action(path: string): Schedule {
108
118
  log.info(`Scheduling action: ${path}`)
109
119
  return this
110
120
  }
111
121
 
112
- static command(cmd: string) {
122
+ static command(cmd: string): Schedule {
113
123
  log.info(`Executing command: ${cmd}`)
114
- // this.cmd = cmd
115
- return this
124
+ return new Schedule(async () => {
125
+ log.info(`Executing command: ${cmd}`)
126
+
127
+ const result = await runCommand(cmd)
128
+
129
+ if (result.isErr()) {
130
+ log.error(result.error)
131
+ return
132
+ }
133
+
134
+ log.info(result.value)
135
+ })
116
136
  }
117
137
  }
118
138
 
139
+ export class Job extends Schedule {}
140
+
119
141
  export function sendAt(cronTime: string | Date | DateTime): DateTime {
120
142
  return new CronTime(cronTime).sendAt()
121
143
  }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type IntRange<Min extends number, Max extends number> = number extends Min | Max
2
+ ? never
3
+ : number | [Min | number, Max | number]
package/src/constants.ts DELETED
@@ -1,81 +0,0 @@
1
- export const CONSTRAINTS = Object.freeze({
2
- second: [0, 59],
3
- minute: [0, 59],
4
- hour: [0, 23],
5
- dayOfMonth: [1, 31],
6
- month: [1, 12],
7
- dayOfWeek: [0, 7],
8
- } as const)
9
- export const MONTH_CONSTRAINTS = Object.freeze({
10
- 1: 31,
11
- 2: 29, // support leap year...not perfect
12
- 3: 31,
13
- 4: 30,
14
- 5: 31,
15
- 6: 30,
16
- 7: 31,
17
- 8: 31,
18
- 9: 30,
19
- 10: 31,
20
- 11: 30,
21
- 12: 31,
22
- } as const)
23
- export const PARSE_DEFAULTS = Object.freeze({
24
- second: '0',
25
- minute: '*',
26
- hour: '*',
27
- dayOfMonth: '*',
28
- month: '*',
29
- dayOfWeek: '*',
30
- } as const)
31
- export const ALIASES = Object.freeze({
32
- jan: 1,
33
- feb: 2,
34
- mar: 3,
35
- apr: 4,
36
- may: 5,
37
- jun: 6,
38
- jul: 7,
39
- aug: 8,
40
- sep: 9,
41
- oct: 10,
42
- nov: 11,
43
- dec: 12,
44
- sun: 0,
45
- mon: 1,
46
- tue: 2,
47
- wed: 3,
48
- thu: 4,
49
- fri: 5,
50
- sat: 6,
51
- } as const)
52
- export const TIME_UNITS_MAP = Object.freeze({
53
- SECOND: 'second',
54
- MINUTE: 'minute',
55
- HOUR: 'hour',
56
- DAY_OF_MONTH: 'dayOfMonth',
57
- MONTH: 'month',
58
- DAY_OF_WEEK: 'dayOfWeek',
59
- } as const)
60
- export const TIME_UNITS = Object.freeze(Object.values(TIME_UNITS_MAP)) as [
61
- 'second',
62
- 'minute',
63
- 'hour',
64
- 'dayOfMonth',
65
- 'month',
66
- 'dayOfWeek',
67
- ]
68
- export const TIME_UNITS_LEN: number = TIME_UNITS.length
69
- export const PRESETS = Object.freeze({
70
- '@yearly': '0 0 0 1 1 *',
71
- '@monthly': '0 0 0 1 * *',
72
- '@weekly': '0 0 0 * * 0',
73
- '@daily': '0 0 0 * * *',
74
- '@hourly': '0 0 * * * *',
75
- '@minutely': '0 * * * * *',
76
- '@secondly': '* * * * * *',
77
- '@weekdays': '0 0 0 * * 1-5',
78
- '@weekends': '0 0 0 * * 0,6',
79
- } as const)
80
- export const RE_WILDCARDS = /\*/g
81
- export const RE_RANGE = /^(\d+)(?:-(\d+))?(?:\/(\d+))?$/g
package/src/errors.ts DELETED
@@ -1,7 +0,0 @@
1
- export class CronError extends Error {}
2
-
3
- export class ExclusiveParametersError extends CronError {
4
- constructor(param1: string, param2: string) {
5
- super(`You can't specify both ${param1} and ${param2}`)
6
- }
7
- }
package/src/job.ts DELETED
@@ -1,280 +0,0 @@
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 ? CronOnCompleteCallback : undefined
27
-
28
- private _timeout?: NodeJS.Timeout
29
- private _callbacks: CronCallback<C, WithOnComplete<OC>>[] = []
30
- private _errorHandler?: (error: Error) => void
31
-
32
- constructor(
33
- cronTime: CronJobParams<OC, C>['cronTime'],
34
- onTick: CronJobParams<OC, C>['onTick'],
35
- onComplete?: CronJobParams<OC, C>['onComplete'],
36
- start?: CronJobParams<OC, C>['start'],
37
- timeZone?: CronJobParams<OC, C>['timeZone'],
38
- context?: CronJobParams<OC, C>['context'],
39
- runOnInit?: CronJobParams<OC, C>['runOnInit'],
40
- utcOffset?: null,
41
- unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
42
- errorHandler?: (error: Error) => void,
43
- )
44
- constructor(
45
- cronTime: CronJobParams<OC, C>['cronTime'],
46
- onTick: CronJobParams<OC, C>['onTick'],
47
- onComplete?: CronJobParams<OC, C>['onComplete'],
48
- start?: CronJobParams<OC, C>['start'],
49
- timeZone?: null,
50
- context?: CronJobParams<OC, C>['context'],
51
- runOnInit?: CronJobParams<OC, C>['runOnInit'],
52
- utcOffset?: CronJobParams<OC, C>['utcOffset'],
53
- unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
54
- errorHandler?: (error: Error) => void,
55
- )
56
- constructor(
57
- cronTime: CronJobParams<OC, C>['cronTime'],
58
- onTick: CronJobParams<OC, C>['onTick'],
59
- onComplete?: CronJobParams<OC, C>['onComplete'],
60
- start?: CronJobParams<OC, C>['start'],
61
- timeZone?: CronJobParams<OC, C>['timeZone'],
62
- context?: CronJobParams<OC, C>['context'],
63
- runOnInit?: CronJobParams<OC, C>['runOnInit'],
64
- utcOffset?: CronJobParams<OC, C>['utcOffset'],
65
- unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
66
- errorHandler?: (error: Error) => void,
67
- ) {
68
- this._errorHandler = errorHandler
69
- this.context = (context ?? this) as CronContext<C>
70
-
71
- const { timeZone: tz, utcOffset: uo } = getTimeZoneAndOffset(timeZone, utcOffset)
72
-
73
- this.cronTime = new CronTime(cronTime, tz, uo as null | undefined)
74
-
75
- if (unrefTimeout != null) this.unrefTimeout = unrefTimeout
76
-
77
- if (onComplete != null) {
78
- // casting to the correct type since we just made sure that WithOnComplete<OC> = true
79
- this.onComplete = this._fnWrap(onComplete) as WithOnComplete<OC> extends true ? CronOnCompleteCallback : undefined
80
- }
81
-
82
- if (this.cronTime.realDate) this.runOnce = true
83
-
84
- this.addCallback(this._fnWrap(onTick))
85
-
86
- if (runOnInit) {
87
- this.lastExecution = new Date()
88
- this.fireOnTick()
89
- }
90
-
91
- if (start) this.start()
92
- }
93
-
94
- static from<OC extends CronOnCompleteCommand | null = null, C = null>(params: CronJobParams<OC, C>) {
95
- // runtime check for JS users
96
- if (params.timeZone != null && params.utcOffset != null) throw new ExclusiveParametersError('timeZone', 'utcOffset')
97
-
98
- if (params.timeZone != null) {
99
- return new CronJob<OC, C>(
100
- params.cronTime,
101
- params.onTick,
102
- params.onComplete,
103
- params.start,
104
- params.timeZone,
105
- params.context,
106
- params.runOnInit,
107
- params.utcOffset,
108
- params.unrefTimeout,
109
- )
110
- }
111
-
112
- if (params.utcOffset != null) {
113
- return new CronJob<OC, C>(
114
- params.cronTime,
115
- params.onTick,
116
- params.onComplete,
117
- params.start,
118
- null,
119
- params.context,
120
- params.runOnInit,
121
- params.utcOffset,
122
- params.unrefTimeout,
123
- )
124
- }
125
-
126
- return new CronJob<OC, C>(
127
- params.cronTime,
128
- params.onTick,
129
- params.onComplete,
130
- params.start,
131
- params.timeZone,
132
- params.context,
133
- params.runOnInit,
134
- params.utcOffset,
135
- params.unrefTimeout,
136
- )
137
- }
138
-
139
- private _fnWrap(cmd: CronCommand<C, boolean>): CronCallback<C, boolean> {
140
- switch (typeof cmd) {
141
- case 'function': {
142
- return cmd
143
- }
144
-
145
- case 'string': {
146
- const [command, ...args] = cmd.split(' ')
147
-
148
- return spawn.bind(undefined, command ?? cmd, args, {}) as () => void
149
- }
150
-
151
- case 'object': {
152
- return spawn.bind(undefined, cmd.command, cmd.args ?? [], cmd.options ?? {}) as () => void
153
- }
154
- }
155
- }
156
-
157
- addCallback(callback: CronCallback<C, WithOnComplete<OC>>) {
158
- if (typeof callback === 'function') this._callbacks.push(callback)
159
- }
160
-
161
- setTime(time: CronTime) {
162
- if (!(time instanceof CronTime)) throw new CronError('time must be an instance of CronTime.')
163
-
164
- const wasRunning = this.running
165
- this.stop()
166
-
167
- this.cronTime = time
168
- if (time.realDate) this.runOnce = true
169
-
170
- if (wasRunning) this.start()
171
- }
172
-
173
- nextDate() {
174
- return this.cronTime.sendAt()
175
- }
176
-
177
- fireOnTick() {
178
- try {
179
- for (const callback of this._callbacks) {
180
- void callback.call(
181
- this.context,
182
- this.onComplete as WithOnComplete<OC> extends true ? CronOnCompleteCallback : never,
183
- )
184
- }
185
- } catch (error) {
186
- if (this._errorHandler && error instanceof Error) {
187
- this._errorHandler(error)
188
- } else {
189
- // Handle the case where no error handler is provided or the caught object is not an Error
190
- console.error('An error occurred in the cron job callback:', error)
191
- }
192
- }
193
- }
194
-
195
- nextDates(i?: number) {
196
- return this.cronTime.sendAt(i ?? 0)
197
- }
198
-
199
- start() {
200
- if (this.running) return
201
-
202
- const MAXDELAY = 2147483647 // The maximum number of milliseconds setTimeout will wait.
203
- let timeout = this.cronTime.getTimeout()
204
- let remaining = 0
205
- let startTime: number
206
-
207
- const setCronTimeout = (t: number) => {
208
- this._timeout = setTimeout(callbackWrapper, t) as NodeJS.Timeout
209
- if (this.unrefTimeout && typeof this._timeout.unref === 'function') this._timeout.unref()
210
- }
211
-
212
- // The callback wrapper checks if it needs to sleep another period or not
213
- // and does the real callback logic when it’s time.
214
- const callbackWrapper = () => {
215
- const diff = startTime + timeout - Date.now()
216
-
217
- if (diff > 0) {
218
- let newTimeout = this.cronTime.getTimeout()
219
-
220
- if (newTimeout > diff) newTimeout = diff
221
-
222
- remaining += newTimeout
223
- }
224
-
225
- // If there is sleep time remaining, calculate how long and go to sleep
226
- // again. This processing might make us miss the deadline by a few ms
227
- // times the number of sleep sessions. Given a MAXDELAY of almost a
228
- // month, this should be no issue.
229
- if (remaining) {
230
- if (remaining > MAXDELAY) {
231
- remaining -= MAXDELAY
232
- timeout = MAXDELAY
233
- } else {
234
- timeout = remaining
235
- remaining = 0
236
- }
237
-
238
- setCronTimeout(timeout)
239
- } else {
240
- // We have arrived at the correct point in time.
241
- this.lastExecution = new Date()
242
-
243
- this.running = false
244
-
245
- // start before calling back so the callbacks have the ability to stop the cron job
246
- if (!this.runOnce) this.start()
247
-
248
- this.fireOnTick()
249
- }
250
- }
251
-
252
- if (timeout >= 0) {
253
- this.running = true
254
-
255
- // Don't try to sleep more than MAXDELAY ms at a time.
256
-
257
- if (timeout > MAXDELAY) {
258
- remaining = timeout - MAXDELAY
259
- timeout = MAXDELAY
260
- }
261
-
262
- setCronTimeout(timeout)
263
- } else {
264
- this.stop()
265
- }
266
- }
267
-
268
- lastDate() {
269
- return this.lastExecution
270
- }
271
-
272
- /**
273
- * Stop the cronjob.
274
- */
275
- stop() {
276
- if (this._timeout) clearTimeout(this._timeout)
277
- this.running = false
278
- if (typeof this.onComplete === 'function') void this.onComplete.call(this.context)
279
- }
280
- }