@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.
@@ -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
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "include": ["src/**/*.ts"],
4
- "exclude": ["node_modules", "tests"]
5
- }