@strav/queue 0.4.31 → 1.0.0-alpha.4
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/README.md +243 -0
- package/package.json +21 -29
- package/src/console/index.ts +10 -0
- package/src/console/queue_console_provider.ts +32 -0
- package/src/console/queue_failed.ts +57 -0
- package/src/console/queue_flush.ts +41 -0
- package/src/console/queue_retry.ts +68 -0
- package/src/console/queue_work.ts +85 -0
- package/src/console/scheduler_list.ts +27 -0
- package/src/console/scheduler_run.ts +31 -0
- package/src/console/scheduler_work.ts +32 -0
- package/src/cron.ts +194 -0
- package/src/database_queue.ts +177 -0
- package/src/failed_jobs_schema.ts +37 -0
- package/src/index.ts +62 -3
- package/src/job.ts +153 -0
- package/src/job_registry.ts +135 -0
- package/src/job_schema.ts +49 -0
- package/src/queue.ts +69 -0
- package/src/scheduler.ts +271 -0
- package/src/scheduler_runs_schema.ts +33 -0
- package/src/sync_queue.ts +126 -0
- package/src/worker.ts +351 -0
- package/src/providers/index.ts +0 -3
- package/src/providers/queue_provider.ts +0 -29
- package/src/queue/circuit_breaker.ts +0 -135
- package/src/queue/index.ts +0 -22
- package/src/queue/queue.ts +0 -493
- package/src/queue/worker.ts +0 -273
- package/src/scheduler/cron.ts +0 -146
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/runner.ts +0 -116
- package/src/scheduler/schedule.ts +0 -292
- package/src/scheduler/scheduler.ts +0 -71
- package/tsconfig.json +0 -5
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { parseCron, cronMatches } from './cron.ts'
|
|
2
|
-
import type { CronExpression } from './cron.ts'
|
|
3
|
-
|
|
4
|
-
export type TaskHandler = () => void | Promise<void>
|
|
5
|
-
|
|
6
|
-
export enum TimeUnit {
|
|
7
|
-
Minutes = 'minutes',
|
|
8
|
-
Hours = 'hours',
|
|
9
|
-
Days = 'days',
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const DAY_NAMES: Record<string, number> = {
|
|
13
|
-
sunday: 0,
|
|
14
|
-
sun: 0,
|
|
15
|
-
monday: 1,
|
|
16
|
-
mon: 1,
|
|
17
|
-
tuesday: 2,
|
|
18
|
-
tue: 2,
|
|
19
|
-
wednesday: 3,
|
|
20
|
-
wed: 3,
|
|
21
|
-
thursday: 4,
|
|
22
|
-
thu: 4,
|
|
23
|
-
friday: 5,
|
|
24
|
-
fri: 5,
|
|
25
|
-
saturday: 6,
|
|
26
|
-
sat: 6,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* A scheduled task definition with a fluent configuration API.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* new Schedule('cleanup', handler).dailyAt('02:30').withoutOverlapping()
|
|
34
|
-
*/
|
|
35
|
-
export class Schedule {
|
|
36
|
-
readonly name: string
|
|
37
|
-
readonly handler: TaskHandler
|
|
38
|
-
|
|
39
|
-
private _cron: CronExpression | null = null
|
|
40
|
-
private _noOverlap = false
|
|
41
|
-
private _runImmediately = false
|
|
42
|
-
|
|
43
|
-
private _sporadicMin: number | null = null
|
|
44
|
-
private _sporadicMax: number | null = null
|
|
45
|
-
private _sporadicUnit: TimeUnit | null = null
|
|
46
|
-
private _nextRunAt: Date | null = null
|
|
47
|
-
|
|
48
|
-
constructor(name: string, handler: TaskHandler) {
|
|
49
|
-
this.name = name
|
|
50
|
-
this.handler = handler
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Raw cron ──────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
/** Set a raw 5-field cron expression. */
|
|
56
|
-
cron(expression: string): this {
|
|
57
|
-
this._cron = parseCron(expression)
|
|
58
|
-
return this
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Minute-based ──────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/** Run every minute. */
|
|
64
|
-
everyMinute(): this {
|
|
65
|
-
return this.cron('* * * * *')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Run every 2 minutes. */
|
|
69
|
-
everyTwoMinutes(): this {
|
|
70
|
-
return this.cron('*/2 * * * *')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Run every 5 minutes. */
|
|
74
|
-
everyFiveMinutes(): this {
|
|
75
|
-
return this.cron('*/5 * * * *')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Run every 10 minutes. */
|
|
79
|
-
everyTenMinutes(): this {
|
|
80
|
-
return this.cron('*/10 * * * *')
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Run every 15 minutes. */
|
|
84
|
-
everyFifteenMinutes(): this {
|
|
85
|
-
return this.cron('*/15 * * * *')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Run every 30 minutes. */
|
|
89
|
-
everyThirtyMinutes(): this {
|
|
90
|
-
return this.cron('*/30 * * * *')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Hourly ────────────────────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
/** Run once per hour at minute 0. */
|
|
96
|
-
hourly(): this {
|
|
97
|
-
return this.cron('0 * * * *')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Run once per hour at the given minute. */
|
|
101
|
-
hourlyAt(minute: number): this {
|
|
102
|
-
return this.cron(`${minute} * * * *`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── Daily ─────────────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/** Run once per day at midnight. */
|
|
108
|
-
daily(): this {
|
|
109
|
-
return this.cron('0 0 * * *')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Run once per day at the given time (HH:MM). */
|
|
113
|
-
dailyAt(time: string): this {
|
|
114
|
-
const [hour, minute] = parseTime(time)
|
|
115
|
-
return this.cron(`${minute} ${hour} * * *`)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Run twice per day at the given hours (minute 0). */
|
|
119
|
-
twiceDaily(hour1: number, hour2: number): this {
|
|
120
|
-
return this.cron(`0 ${hour1},${hour2} * * *`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── Weekly ────────────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
/** Run once per week on Sunday at midnight. */
|
|
126
|
-
weekly(): this {
|
|
127
|
-
return this.cron('0 0 * * 0')
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Run once per week on the given day and optional time. */
|
|
131
|
-
weeklyOn(day: string | number, time?: string): this {
|
|
132
|
-
const dow = typeof day === 'string' ? dayToNumber(day) : day
|
|
133
|
-
const [hour, minute] = time ? parseTime(time) : [0, 0]
|
|
134
|
-
return this.cron(`${minute} ${hour} * * ${dow}`)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── Monthly ───────────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
/** Run once per month on the 1st at midnight. */
|
|
140
|
-
monthly(): this {
|
|
141
|
-
return this.cron('0 0 1 * *')
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Run once per month on the given day and optional time. */
|
|
145
|
-
monthlyOn(day: number, time?: string): this {
|
|
146
|
-
const [hour, minute] = time ? parseTime(time) : [0, 0]
|
|
147
|
-
return this.cron(`${minute} ${hour} ${day} * *`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── Sporadic ──────────────────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Run at random intervals between `min` and `max` in the given unit.
|
|
154
|
-
* Simulates human-like, non-periodic scheduling.
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* Scheduler.task('scrape', handler).sporadically(5, 30, TimeUnit.Minutes)
|
|
158
|
-
*/
|
|
159
|
-
sporadically(min: number, max: number, unit: TimeUnit): this {
|
|
160
|
-
if (min < 0 || max < 0) {
|
|
161
|
-
throw new Error('sporadically: min and max must be non-negative')
|
|
162
|
-
}
|
|
163
|
-
if (min >= max) {
|
|
164
|
-
throw new Error('sporadically: min must be less than max')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this._sporadicMin = min
|
|
168
|
-
this._sporadicMax = max
|
|
169
|
-
this._sporadicUnit = unit
|
|
170
|
-
this._cron = null
|
|
171
|
-
this._nextRunAt = this.computeNextRun(new Date())
|
|
172
|
-
return this
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ── Options ───────────────────────────────────────────────────────────────
|
|
176
|
-
|
|
177
|
-
/** Prevent overlapping runs within this process. */
|
|
178
|
-
withoutOverlapping(): this {
|
|
179
|
-
this._noOverlap = true
|
|
180
|
-
return this
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Execute this task immediately upon registration, then follow the configured schedule.
|
|
185
|
-
* Useful for bootstrap tasks, cache warming, or ensuring tasks run on deployment.
|
|
186
|
-
*/
|
|
187
|
-
runImmediately(): this {
|
|
188
|
-
this._runImmediately = true
|
|
189
|
-
|
|
190
|
-
// Execute the handler immediately
|
|
191
|
-
this.executeHandler().catch(error => {
|
|
192
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
193
|
-
console.error(`[scheduler] Immediate execution of "${this.name}" failed: ${message}`)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
return this
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ── Internal ──────────────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
/** Check if this task is due at the given Date (evaluated in UTC). */
|
|
202
|
-
isDue(now: Date): boolean {
|
|
203
|
-
if (this._sporadicMin !== null) {
|
|
204
|
-
if (!this._nextRunAt) return false
|
|
205
|
-
if (now >= this._nextRunAt) {
|
|
206
|
-
this._nextRunAt = this.computeNextRun(now)
|
|
207
|
-
return true
|
|
208
|
-
}
|
|
209
|
-
return false
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!this._cron) return false
|
|
213
|
-
return cronMatches(this._cron, now)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Whether overlap prevention is enabled. */
|
|
217
|
-
get preventsOverlap(): boolean {
|
|
218
|
-
return this._noOverlap
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Whether this task should run immediately upon registration. */
|
|
222
|
-
get shouldRunImmediately(): boolean {
|
|
223
|
-
return this._runImmediately
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** The parsed cron expression (for testing/debugging). */
|
|
227
|
-
get expression(): CronExpression | null {
|
|
228
|
-
return this._cron
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** The next scheduled run time (for sporadic schedules). */
|
|
232
|
-
get nextRunAt(): Date | null {
|
|
233
|
-
return this._nextRunAt
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Execute the task handler, handling both sync and async cases. */
|
|
237
|
-
private async executeHandler(): Promise<void> {
|
|
238
|
-
const result = this.handler()
|
|
239
|
-
if (result instanceof Promise) {
|
|
240
|
-
await result
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Compute the next random run time from a reference point. */
|
|
245
|
-
private computeNextRun(from: Date): Date {
|
|
246
|
-
const ms = unitToMs(this._sporadicUnit!)
|
|
247
|
-
const minMs = this._sporadicMin! * ms
|
|
248
|
-
const maxMs = this._sporadicMax! * ms
|
|
249
|
-
const delay = minMs + Math.random() * (maxMs - minMs)
|
|
250
|
-
return new Date(from.getTime() + delay)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// Helpers
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
|
|
258
|
-
function parseTime(time: string): [number, number] {
|
|
259
|
-
const parts = time.split(':')
|
|
260
|
-
if (parts.length !== 2) {
|
|
261
|
-
throw new Error(`Invalid time format "${time}": expected HH:MM`)
|
|
262
|
-
}
|
|
263
|
-
const hour = parseInt(parts[0]!, 10)
|
|
264
|
-
const minute = parseInt(parts[1]!, 10)
|
|
265
|
-
if (isNaN(hour) || isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
266
|
-
throw new Error(`Invalid time "${time}": hour must be 0–23, minute must be 0–59`)
|
|
267
|
-
}
|
|
268
|
-
return [hour, minute]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function unitToMs(unit: TimeUnit): number {
|
|
272
|
-
switch (unit) {
|
|
273
|
-
case TimeUnit.Minutes:
|
|
274
|
-
return 60_000
|
|
275
|
-
case TimeUnit.Hours:
|
|
276
|
-
return 3_600_000
|
|
277
|
-
case TimeUnit.Days:
|
|
278
|
-
return 86_400_000
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function dayToNumber(day: string): number {
|
|
283
|
-
const n = DAY_NAMES[day.toLowerCase()]
|
|
284
|
-
if (n === undefined) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
`Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES)
|
|
287
|
-
.filter((_, i) => i % 2 === 0)
|
|
288
|
-
.join(', ')}`
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
return n
|
|
292
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Schedule } from './schedule.ts'
|
|
2
|
-
import type { TaskHandler } from './schedule.ts'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Static task registry for periodic jobs.
|
|
6
|
-
*
|
|
7
|
-
* No DI, no database — tasks are registered in code and evaluated
|
|
8
|
-
* in-memory by the {@link SchedulerRunner}.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* Scheduler.task('cleanup:sessions', async () => {
|
|
12
|
-
* await db.sql`DELETE FROM "_strav_sessions" WHERE "expires_at" < NOW()`
|
|
13
|
-
* }).hourly()
|
|
14
|
-
*
|
|
15
|
-
* Scheduler.task('reports:daily', () => generateDailyReport()).dailyAt('02:00')
|
|
16
|
-
*/
|
|
17
|
-
export default class Scheduler {
|
|
18
|
-
private static _tasks: Schedule[] = []
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Register a periodic task. Returns the {@link Schedule} for fluent configuration.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* Scheduler.task('prune-cache', () => cache.flush()).everyFifteenMinutes()
|
|
25
|
-
*/
|
|
26
|
-
static task(name: string, handler: TaskHandler): Schedule {
|
|
27
|
-
const schedule = new Schedule(name, handler)
|
|
28
|
-
Scheduler._tasks.push(schedule)
|
|
29
|
-
return schedule
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** All registered tasks. */
|
|
33
|
-
static get tasks(): readonly Schedule[] {
|
|
34
|
-
return Scheduler._tasks
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Return tasks that are due at the given time (defaults to now, UTC). */
|
|
38
|
-
static due(now?: Date): Schedule[] {
|
|
39
|
-
const date = now ?? new Date()
|
|
40
|
-
return Scheduler._tasks.filter(t => t.isDue(date))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Manually execute a task by name immediately.
|
|
45
|
-
*
|
|
46
|
-
* @param name The name of the task to execute
|
|
47
|
-
* @returns Promise that resolves when the task completes
|
|
48
|
-
* @throws Error if task is not found
|
|
49
|
-
*/
|
|
50
|
-
static async runNow(name: string): Promise<void> {
|
|
51
|
-
const task = Scheduler._tasks.find(t => t.name === name)
|
|
52
|
-
if (!task) {
|
|
53
|
-
throw new Error(`Task "${name}" not found. Available tasks: ${Scheduler._tasks.map(t => t.name).join(', ')}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const result = task.handler()
|
|
58
|
-
if (result instanceof Promise) {
|
|
59
|
-
await result
|
|
60
|
-
}
|
|
61
|
-
} catch (error) {
|
|
62
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
63
|
-
throw new Error(`Manual execution of task "${name}" failed: ${message}`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Clear all registered tasks. For testing. */
|
|
68
|
-
static reset(): void {
|
|
69
|
-
Scheduler._tasks = []
|
|
70
|
-
}
|
|
71
|
-
}
|