@maestro-js/recurring-jobs 1.0.0-alpha.19 → 1.0.0-alpha.2

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.
Files changed (4) hide show
  1. package/README.md +253 -138
  2. package/dist/index.d.ts +160 -127
  3. package/dist/index.js +115 -185
  4. package/package.json +6 -12
package/README.md CHANGED
@@ -1,233 +1,348 @@
1
- # @maestro-js/recurring-jobs
1
+ # RecurringJobs Skill
2
2
 
3
- Cron-based recurring job scheduling with distributed lock-based deduplication.
3
+ ## Overview
4
+
5
+ RecurringJobs provides cron-based job scheduling backed by the Queue package. Each recurring job gets its own dedicated queue where every execution is a persisted message with full lifecycle tracking. The next run is enqueued before the work function executes, guaranteeing the schedule continues even if the current run fails.
4
6
 
5
7
  ## Quick Setup
6
8
 
7
9
  ```ts
8
10
  import { RecurringJobs } from '@maestro-js/recurring-jobs'
11
+ import { Queue } from '@maestro-js/queue'
12
+ import { Log } from '@maestro-js/log'
13
+
14
+ // Create a logger for recurring job events
15
+ const logger = Log.create<[RecurringJobs.LogMessage]>({
16
+ transports: [{ write: ({ data: [msg] }) => console.log(msg) }]
17
+ })
9
18
 
10
- // Create and register with the local (in-memory) driver
11
- const service = RecurringJobs.Provider.create({
12
- driver: RecurringJobs.drivers.local()
19
+ // Create and register the recurring jobs service with a queue service
20
+ const recurringJobsService = RecurringJobs.Provider.create({
21
+ logger,
22
+ queueService: Queue.provider('default')
13
23
  })
14
- RecurringJobs.Provider.register('default', service)
24
+ RecurringJobs.Provider.register('default', recurringJobsService)
15
25
 
16
26
  // Define a recurring job
17
- RecurringJobs.create({
18
- name: 'send-weekly-digest',
19
- cron: '0 9 * * MON',
20
- handle: async () => sendDigest()
27
+ const dailyReport = RecurringJobs.createJob({
28
+ name: 'daily-sales-report',
29
+ cron: '0 6 * * *',
30
+ workFn: async () => {
31
+ const total = await calculateDailySales()
32
+ return { total }
33
+ }
21
34
  })
22
35
 
23
- // Start polling -- fires immediately then every 60 seconds
24
- const worker = RecurringJobs.work()
36
+ // Start processing
37
+ dailyReport.startProcessing()
38
+ ```
39
+
40
+ ## Cron Expression Format
41
+
42
+ Uses the `cron-parser` library (v4.x) for standard 5-field cron expressions:
25
43
 
26
- // Graceful shutdown
27
- await worker.abort()
44
+ ```
45
+ ┌───────────── minute (0-59)
46
+ │ ┌───────────── hour (0-23)
47
+ │ │ ┌───────────── day of month (1-31)
48
+ │ │ │ ┌───────────── month (1-12)
49
+ │ │ │ │ ┌───────────── day of week (0-7, 0 and 7 are Sunday)
50
+ │ │ │ │ │
51
+ * * * * *
28
52
  ```
29
53
 
30
- ## Drivers
54
+ Common examples:
55
+ - `0 6 * * *` -- every day at 6:00 AM
56
+ - `*/15 * * * *` -- every 15 minutes
57
+ - `0 0 * * 0` -- every Sunday at midnight
58
+ - `0 9 1 * *` -- first day of every month at 9:00 AM
59
+ - `30 */2 * * *` -- every 2 hours at the 30-minute mark
31
60
 
32
- ### Db
61
+ Invalid cron expressions throw immediately when passed to `createJob()`, since `cron-parser.parseExpression()` is called at creation time.
62
+
63
+ ## API Reference
33
64
 
34
- MySQL-backed, for multi-process production deployments. Uses `UPDATE ... WHERE lastRunDate < ?`
35
- and `INSERT IGNORE` for atomic distributed locking.
65
+ ### Provider Setup
66
+
67
+ #### `RecurringJobs.Provider.create(config)`
68
+
69
+ Create a recurring jobs service instance.
36
70
 
37
71
  ```ts
38
- RecurringJobs.drivers.db({
39
- db: Db, // Db service instance
40
- databaseTable: 'recurringJobs'
72
+ RecurringJobs.Provider.create(config: {
73
+ logger: Log.Logger<[RecurringJobs.LogMessage]>
74
+ queueService: Queue.QueueService
41
75
  })
42
76
  ```
43
77
 
44
- Required table schema:
78
+ - `logger` -- a Log instance typed to `RecurringJobs.LogMessage` for structured job lifecycle events
79
+ - `queueService` -- the Queue service used to back each recurring job's dedicated queue
80
+
81
+ #### `RecurringJobs.Provider.register(name, service)`
45
82
 
46
- ```sql
47
- CREATE TABLE recurringJobs (
48
- name VARCHAR(255) NOT NULL PRIMARY KEY,
49
- lastRunDate DATETIME DEFAULT NULL
50
- );
83
+ Register the service instance in the named registry.
84
+
85
+ ```ts
86
+ RecurringJobs.Provider.register('default', recurringJobsService)
51
87
  ```
52
88
 
53
- Source: `packages/recurring-jobs/src/db-recurring-jobs-driver.ts`
89
+ #### `RecurringJobs.provider(key)`
90
+
91
+ Resolve a named instance from the registry. Supports deferred resolution via Proxy.
92
+
93
+ ```ts
94
+ const jobs = RecurringJobs.provider('default')
95
+ ```
54
96
 
55
- ### Cache
97
+ ### Service Methods (on the facade or provider instance)
56
98
 
57
- Redis-backed via `@maestro-js/cache` with distributed locking (`cache.lock()`). Use when you
58
- already have Redis and don't need MySQL.
99
+ #### `RecurringJobs.createJob(config)`
100
+
101
+ Create a recurring job with a dedicated queue. Returns a `RecurringJob` handle.
59
102
 
60
103
  ```ts
61
- RecurringJobs.drivers.cache({
62
- cache: cacheService, // Cache.CacheService instance
63
- keyPrefix: 'recurring-jobs:' // optional, default 'recurring-jobs:'
64
- })
104
+ RecurringJobs.createJob<T>(config: {
105
+ name: string
106
+ cron: string
107
+ workFn: () => Promise<T>
108
+ logger?: Log.LogFunctions<[RecurringJobs.LogMessage]> // optional per-job logger
109
+ }): RecurringJobs.RecurringJob<T>
65
110
  ```
66
111
 
67
- Source: `packages/recurring-jobs/src/cache-recurring-jobs-driver.ts`
112
+ Each job creates a queue named `RecurringJobs: <name>`. The cron expression is validated immediately and throws if invalid.
68
113
 
69
- ### Local
114
+ #### `RecurringJobs.startProcessing()`
70
115
 
71
- In-memory, single-process only. No distributed locking -- every process will run every due job.
72
- Good for development and testing.
116
+ Start processing on all registered recurring jobs.
73
117
 
74
118
  ```ts
75
- RecurringJobs.drivers.local()
119
+ RecurringJobs.startProcessing()
76
120
  ```
77
121
 
78
- Source: `packages/recurring-jobs/src/local-recurring-jobs-driver.ts`
122
+ #### `RecurringJobs.stopProcessing()`
79
123
 
80
- ### Choosing a driver
124
+ Stop processing on all registered recurring jobs.
81
125
 
82
- | Driver | Distributed locking | Persistence | Best for |
83
- |---------|---------------------|-------------|---------------------------------------|
84
- | **Db** | Yes (MySQL row) | Yes | Multi-process production |
85
- | **Cache** | Yes (Redis lock) | Yes* | Production when you have Redis, no MySQL |
86
- | **Local** | No | No | Development and testing |
126
+ ```ts
127
+ RecurringJobs.stopProcessing()
128
+ ```
87
129
 
88
- \* Cache persistence depends on Redis persistence settings.
130
+ #### `RecurringJobs.list()`
89
131
 
90
- ## API Reference
132
+ Return the names of all registered recurring jobs.
91
133
 
92
- ### `RecurringJobs.create(config)`
134
+ ```ts
135
+ RecurringJobs.list() // ['daily-sales-report', 'session-cleanup']
136
+ ```
93
137
 
94
- Register a recurring job. Throws immediately on invalid cron expression or duplicate job name.
138
+ #### `RecurringJobs.get(name)`
139
+
140
+ Return the recurring job with the given name, or `undefined` if not found.
95
141
 
96
142
  ```ts
97
- RecurringJobs.create({
98
- name: string, // unique job identifier
99
- cron: string, // cron expression (e.g. '0 9 * * MON')
100
- handle: () => Promise<Result>, // async handler
101
- middleware?: Middleware<Result>[] // optional middleware chain
102
- })
143
+ const job = RecurringJobs.get<{ total: number }>('daily-sales-report')
103
144
  ```
104
145
 
105
- ### `RecurringJobs.work()`
146
+ ### RecurringJob Instance Methods
106
147
 
107
- Start a 60-second polling loop that checks for due jobs, acquires distributed locks via the
108
- driver, and executes handlers. Returns an object with `abort()` to stop the loop and wait for
109
- in-flight ticks.
148
+ #### `job.startProcessing()`
149
+
150
+ Enqueue the next scheduled run and begin polling. If a waiting job already exists, it is not duplicated (deduplication).
110
151
 
111
152
  ```ts
112
- const worker = RecurringJobs.work()
153
+ dailyReport.startProcessing()
154
+ ```
155
+
156
+ #### `job.stopProcessing()`
113
157
 
114
- // Later, to stop:
115
- await worker.abort()
158
+ Stop polling for this job.
159
+
160
+ ```ts
161
+ dailyReport.stopProcessing()
116
162
  ```
117
163
 
118
- ### `RecurringJobs.stopAllWork()`
164
+ #### `job.getNextUp()`
119
165
 
120
- Abort all active work loops and wait for in-flight ticks to finish.
166
+ Return the first message in `waiting` status, or `null` if none exists.
121
167
 
122
168
  ```ts
123
- await RecurringJobs.stopAllWork()
169
+ const next = await dailyReport.getNextUp()
170
+ // { id: 42, status: 'waiting', scheduledDate: '2026-02-22T06:00:00.000Z', cron: '0 6 * * *', ... }
124
171
  ```
125
172
 
126
- ## Common Patterns
173
+ #### `job.list({ pageSize, pageNumber })`
127
174
 
128
- ### Basic cron job
175
+ Return a paginated list of all messages (past runs and pending) for this job.
129
176
 
130
177
  ```ts
131
- RecurringJobs.create({
132
- name: 'cleanup-expired-tokens',
133
- cron: '0 3 * * *', // daily at 3am
134
- handle: async () => {
135
- await db.update('DELETE FROM tokens WHERE expiresAt < NOW()')
136
- }
137
- })
178
+ const history = await dailyReport.list({ pageSize: 20, pageNumber: 0 })
179
+ // [{ id: 41, status: 'success', result: { total: 15420 }, ... }, ...]
138
180
  ```
139
181
 
140
- ### Middleware wrapping
182
+ #### `job.rescheduleJob({ jobId, scheduledDate })`
183
+
184
+ Change the scheduled time of a waiting job. Only the next waiting job can be rescheduled; throws if `jobId` does not match the current waiting job.
141
185
 
142
186
  ```ts
143
- RecurringJobs.create({
144
- name: 'generate-report',
145
- cron: '0 6 * * MON',
146
- middleware: [
147
- async (next) => {
148
- try {
149
- return await next()
150
- } catch (e) {
151
- await errorReporter.capture(e)
152
- throw e
153
- }
154
- }
155
- ],
156
- handle: async () => generateWeeklyReport()
187
+ await dailyReport.rescheduleJob({
188
+ jobId: 42,
189
+ scheduledDate: '2026-02-22T08:00:00.000Z' as Iso.Instant
157
190
  })
158
191
  ```
159
192
 
160
- ### Graceful shutdown
193
+ #### `job.name` / `job.cron`
194
+
195
+ Read-only properties exposing the job's name and cron expression.
161
196
 
162
197
  ```ts
163
- const worker = RecurringJobs.work()
198
+ dailyReport.name // 'daily-sales-report'
199
+ dailyReport.cron // '0 6 * * *'
200
+ ```
164
201
 
165
- process.on('SIGTERM', async () => {
166
- await worker.abort() // waits for in-flight tick to finish
167
- process.exit(0)
168
- })
202
+ ### JobMessage Interface
169
203
 
170
- // Or abort all workers at once:
171
- process.on('SIGTERM', async () => {
172
- await RecurringJobs.stopAllWork()
173
- process.exit(0)
174
- })
204
+ Each queue message returned by `getNextUp()` and `list()` has these fields:
205
+
206
+ ```ts
207
+ interface JobMessage<Result = any> {
208
+ id: number
209
+ batchId: number | null
210
+ queue: string
211
+ scheduledDate: Iso.Instant
212
+ enqueuedDate: Iso.Instant
213
+ startedDate: Iso.Instant | null
214
+ status: 'waiting' | 'processing' | 'success' | 'error' | 'stuck'
215
+ result: Result
216
+ recovered: number
217
+ runningTime: number | null
218
+ uniqueKey: number | null
219
+ priority: number | null
220
+ cron: string
221
+ }
175
222
  ```
176
223
 
177
- ### Named provider instances
224
+ ## Common Patterns
225
+
226
+ ### Multiple Recurring Jobs
178
227
 
179
228
  ```ts
180
- const primary = RecurringJobs.Provider.create({
181
- driver: RecurringJobs.drivers.db({ db: Db, databaseTable: 'recurringJobs' })
229
+ const report = RecurringJobs.createJob({
230
+ name: 'daily-report',
231
+ cron: '0 6 * * *',
232
+ workFn: async () => generateReport()
182
233
  })
183
- RecurringJobs.Provider.register('default', primary)
184
234
 
185
- const secondary = RecurringJobs.Provider.create({
186
- driver: RecurringJobs.drivers.local()
235
+ const cleanup = RecurringJobs.createJob({
236
+ name: 'session-cleanup',
237
+ cron: '0 0 * * *',
238
+ workFn: async () => cleanStaleSessions()
187
239
  })
188
- RecurringJobs.Provider.register('dev', secondary)
189
240
 
190
- RecurringJobs.provider('dev').create({ ... })
241
+ // Start all jobs at once
242
+ RecurringJobs.startProcessing()
243
+
244
+ // Or start individually
245
+ report.startProcessing()
246
+ cleanup.startProcessing()
191
247
  ```
192
248
 
193
- ## Warnings and Gotchas
249
+ ### Inspecting and Rescheduling
194
250
 
195
- - **60-second polling**: Jobs won't fire more precisely than ~1 minute.
196
- - **Unique job names**: Duplicate names within a service instance throw at `create()` time.
197
- - **Eager cron validation**: Invalid cron expressions throw at `create()` time, not at poll time.
198
- - **First-time jobs**: A never-before-run job doesn't fire immediately on `work()` -- it waits
199
- until its cron expression's next occurrence from now.
200
- - **Handler errors don't crash the loop**: Failed handlers are logged as `jobFailed` and other
201
- jobs continue normally.
202
- - **Db driver requires a table**: The `recurringJobs` table must exist with the schema shown above.
203
- - **Local driver has no distributed locking**: All processes will independently run the same job.
251
+ ```ts
252
+ const job = RecurringJobs.get('daily-report')
253
+ if (job) {
254
+ const next = await job.getNextUp()
255
+ if (next) {
256
+ // Delay the next run by 2 hours
257
+ const delayed = new Date(new Date(next.scheduledDate).getTime() + 2 * 60 * 60 * 1000)
258
+ await job.rescheduleJob({
259
+ jobId: next.id,
260
+ scheduledDate: delayed.toISOString() as Iso.Instant
261
+ })
262
+ }
263
+ }
264
+ ```
204
265
 
205
- ## Cross-Package Integration
266
+ ### Per-Job Logger
206
267
 
207
- - **Depends on**: `@maestro-js/service-registry` (Provider pattern), `@maestro-js/log` (structured
208
- logging), `cron-parser` (cron expression parsing)
209
- - **Optional drivers depend on**: `@maestro-js/db` (Db driver), `@maestro-js/cache` (Cache driver)
268
+ Add a dedicated logger to a specific job for fine-grained observability:
210
269
 
211
- ## Driver Interface
270
+ ```ts
271
+ const jobLogger: Log.LogFunctions<[RecurringJobs.LogMessage]> = {
272
+ write({ data: [msg] }) {
273
+ if (msg.event === 'jobFailed') {
274
+ alertOpsTeam(msg.job, msg.error)
275
+ }
276
+ }
277
+ }
212
278
 
213
- Implement `RecurringJobs.Driver` to create a custom backend:
279
+ const criticalJob = RecurringJobs.createJob({
280
+ name: 'payment-reconciliation',
281
+ cron: '0 */4 * * *',
282
+ workFn: async () => reconcilePayments(),
283
+ logger: jobLogger
284
+ })
285
+ ```
286
+
287
+ ### Execution History
214
288
 
215
289
  ```ts
216
- interface Driver {
217
- listLastRunDates(jobNames: string[]): Promise<{ jobName: string; date: Iso.Instant }[]>
218
- pullJob(options: { jobName: string; date: Iso.Instant }): Promise<boolean>
290
+ const job = RecurringJobs.get('daily-report')
291
+ if (job) {
292
+ const runs = await job.list({ pageSize: 50, pageNumber: 0 })
293
+ const failures = runs.filter(r => r.status === 'error')
294
+ const avgRuntime = runs
295
+ .filter(r => r.runningTime !== null)
296
+ .reduce((sum, r) => sum + r.runningTime!, 0) / runs.length
219
297
  }
220
298
  ```
221
299
 
222
- `listLastRunDates` returns the last run date for each known job name. `pullJob` attempts to
223
- acquire a lock for the given job and date -- returns `true` if the lock was acquired (this process
224
- should run the job), `false` otherwise.
300
+ ## Log Events
301
+
302
+ The service emits structured log events of type `RecurringJobs.LogMessage`:
303
+
304
+ | Event | Level | Description |
305
+ |---|---|---|
306
+ | `jobProcessing` | info | A job message was dequeued and is about to execute |
307
+ | `jobSucceeded` | info | The work function completed successfully |
308
+ | `jobFailed` | error | The work function threw an error |
309
+ | `jobScheduled` | info | The next run was enqueued with its scheduled date |
310
+ | `jobAlreadyScheduled` | info | A waiting job already exists; skip enqueueing |
311
+ | `jobSchedulingFailed` | error | Failed to enqueue the next run |
312
+
313
+ ## Cross-Package Integration
314
+
315
+ ### Dependencies
316
+
317
+ - **@maestro-js/service-registry** -- foundation for the Provider pattern (registry, deferred Proxy resolution)
318
+ - **@maestro-js/queue** -- each recurring job creates a dedicated queue via `queueService.create()`. Queue handles message persistence, polling, dequeuing, and lifecycle transitions
319
+ - **@maestro-js/log** -- structured logging with pluggable transports for job lifecycle events
320
+
321
+ ### How It Connects
322
+
323
+ RecurringJobs wraps Queue to add cron scheduling semantics. When `startProcessing()` is called, the next cron-derived date is computed and a queue message is enqueued for that date. When the Queue dequeues and runs the message, the work function fires and the next run is enqueued before the work executes. This ensures schedule continuity even on failure.
324
+
325
+ Queue itself depends on Db (MySQL) for message persistence, so the full dependency chain is:
326
+
327
+ ```
328
+ RecurringJobs -> Queue -> Db (MySQL)
329
+ -> Log
330
+ -> ServiceRegistry
331
+ ```
332
+
333
+ ### Provider Pattern
334
+
335
+ This package follows the standard maestro Provider pattern:
225
336
 
226
- Source: `packages/recurring-jobs/src/index.ts`
337
+ 1. `RecurringJobs.Provider.create(config)` -- factory that instantiates the service
338
+ 2. `RecurringJobs.Provider.register(name, service)` -- registers the instance by name
339
+ 3. `RecurringJobs.provider(key)` -- resolves a named instance (deferred via Proxy)
340
+ 4. `RecurringJobs.*` -- facade that spreads `provider('default')` for direct method access
227
341
 
228
- ## Key Source Files
342
+ ## Notes
229
343
 
230
- - `packages/recurring-jobs/src/index.ts` -- main export, Provider pattern, polling loop, middleware
231
- - `packages/recurring-jobs/src/db-recurring-jobs-driver.ts` -- MySQL driver with row-level locking
232
- - `packages/recurring-jobs/src/cache-recurring-jobs-driver.ts` -- Redis driver via @maestro-js/cache
233
- - `packages/recurring-jobs/src/local-recurring-jobs-driver.ts` -- in-memory driver for dev/testing
344
+ - No tests exist for this package yet (`pnpm --filter @maestro-js/recurring-jobs test` prints "No tests yet").
345
+ - The `cron-parser` library validates cron expressions at job creation time; invalid expressions throw immediately.
346
+ - Deduplication: `startProcessing()` checks for an existing waiting message before enqueueing, preventing double-fires across process restarts.
347
+ - The next run is enqueued inside the queue's `workFn` (before the user's `workFn` runs), so the schedule continues even if the current execution fails.
348
+ - Only the next waiting job can be rescheduled via `rescheduleJob()`. Attempting to reschedule a non-waiting or non-next job throws an error.
package/dist/index.d.ts CHANGED
@@ -1,167 +1,200 @@
1
1
  import { Iso } from 'iso-fns2';
2
+ import { Queue } from '@maestro-js/queue';
2
3
  import { Log } from '@maestro-js/log';
3
- import { Cache } from '@maestro-js/cache';
4
4
 
5
- interface DbRecurringJobsDriverConfig {
6
- db: {
7
- update(query: string, params?: any[]): Promise<{
8
- affectedRows: number;
9
- changedRows: number;
10
- }>;
11
- select(query: string, params?: any[]): Promise<Array<Record<string, any>>>;
12
- insert(query: string, params?: any[]): Promise<{
13
- insertId: number;
14
- }>;
15
- };
16
- databaseTable: string;
17
- }
18
- declare function DbRecurringJobsDriver({ db, databaseTable }: DbRecurringJobsDriverConfig): RecurringJobs.Driver;
19
-
20
- declare function localRecurringJobsDriver(): RecurringJobs.Driver;
21
-
22
- interface CacheRecurringJobsDriverConfig {
23
- cache: Cache.CacheService;
24
- keyPrefix?: string;
25
- }
26
- declare function cacheRecurringJobsDriver({ cache, keyPrefix }: CacheRecurringJobsDriverConfig): RecurringJobs.Driver;
27
-
28
- declare function createService({ driver, logger: serviceLogger }: RecurringJobs.Provider.ServiceConfig): RecurringJobs.Service;
29
- type RecurringJobsLogMessage = {
30
- event: 'jobDue';
31
- jobName: string;
32
- nextRun: string;
33
- } | {
34
- event: 'jobLockAcquired';
35
- jobName: string;
36
- nextRun: string;
37
- } | {
38
- event: 'jobLockFailed';
39
- jobName: string;
40
- nextRun: string;
41
- error: unknown;
42
- } | {
43
- event: 'jobProcessed';
44
- jobName: string;
45
- } | {
46
- event: 'jobFailed';
47
- jobName: string;
48
- error: unknown;
5
+ declare function create(serviceConfig: RecurringJobs.Provider.RecurringJobsServiceConfig): {
6
+ startProcessing: () => void;
7
+ stopProcessing: () => void;
8
+ list: () => string[];
9
+ get: <Result>(name: string) => RecurringJobs.RecurringJob<Result> | undefined;
10
+ createJob: <T>(jobConfig: RecurringJobs.JobConfig<T>) => RecurringJobs.RecurringJob<T>;
49
11
  };
50
- type KeysWithFallback = keyof RecurringJobs.Provider.Keys extends never ? {
51
- default: unknown;
52
- } : RecurringJobs.Provider.Keys;
53
12
  declare function provider(key: RecurringJobs.Provider.Key): {
54
- work: () => {
55
- abort(): Promise<void>;
56
- };
57
- stopAllWork: () => Promise<void>;
58
- create: <Result = any>(config: RecurringJobs.JobConfig<Result>) => void;
13
+ startProcessing: () => void;
14
+ stopProcessing: () => void;
15
+ list: () => string[];
16
+ get: <Result>(name: string) => RecurringJobs.RecurringJob<Result> | undefined;
17
+ createJob: <T>(jobConfig: RecurringJobs.JobConfig<T>) => RecurringJobs.RecurringJob<T>;
59
18
  };
60
19
  /**
61
- * Cron-based recurring job scheduling with distributed lock-based deduplication.
62
- *
63
- * Register a service with `RecurringJobs.Provider.register('default', RecurringJobs.Provider.create({ driver }))`,
64
- * then define jobs with `RecurringJobs.create({ name, cron, handle })`. Call `RecurringJobs.work()` to start
65
- * a 60-second polling loop that checks for due jobs, acquires distributed locks via the driver, and executes
66
- * handlers with optional middleware.
20
+ * Cron-based job scheduling backed by the Queue package.
67
21
  *
68
- * Three built-in drivers: `db` (MySQL row-level locking), `cache` (Redis via @maestro-js/cache),
69
- * `local` (in-memory, no distributed locking).
22
+ * Each recurring job gets its own dedicated queue where every execution is a
23
+ * persisted message with full lifecycle tracking. The next run is enqueued
24
+ * *before* the work function executes, guaranteeing the schedule continues
25
+ * even if the current run fails. Deduplication prevents double-fires across
26
+ * process restarts.
70
27
  *
71
28
  * @example
72
29
  * ```ts
73
- * RecurringJobs.Provider.register('default', RecurringJobs.Provider.create({
74
- * driver: RecurringJobs.drivers.db({ db: Db, databaseTable: 'recurringJobs' })
75
- * }))
76
- *
77
- * RecurringJobs.create({
78
- * name: 'send-weekly-digest',
79
- * cron: '0 9 * * MON',
80
- * handle: async () => sendDigest()
30
+ * // Create a daily sales report job
31
+ * const salesReport = RecurringJobs.createJob({
32
+ * name: 'daily-sales-report',
33
+ * cron: '0 6 * * *',
34
+ * workFn: async () => {
35
+ * const total = await calculateDailySales()
36
+ * return { total }
37
+ * }
81
38
  * })
82
39
  *
83
- * const worker = RecurringJobs.work()
84
- * // Later: await worker.abort()
40
+ * // Start processing and inspect upcoming runs
41
+ * salesReport.startProcessing()
42
+ * const next = await salesReport.getNextUp() // { status: 'waiting', scheduledDate: '...' }
43
+ *
44
+ * // List all registered jobs
45
+ * RecurringJobs.list() // ['daily-sales-report']
85
46
  * ```
86
47
  */
87
48
  declare namespace RecurringJobs {
88
- /** Union of all recurring jobs lifecycle log event shapes */
89
- type LogMessage = RecurringJobsLogMessage;
90
- /** Middleware function that wraps recurring job execution */
91
- type Middleware<Result = any> = (next: () => Promise<Result>) => Promise<Result>;
92
- /** Pluggable storage backend for tracking job run dates and acquiring distributed locks */
93
- interface Driver {
94
- /** Returns the last run date for each of the given job names */
95
- listLastRunDates(jobNames: string[]): Promise<{
96
- jobName: string;
97
- date: Iso.Instant;
98
- }[]>;
99
- /** Attempts to acquire a lock for the given job and date — returns true if the lock was acquired */
100
- pullJob(options: {
101
- jobName: string;
102
- date: Iso.Instant;
103
- }): Promise<boolean>;
49
+ /** A single queue message representing one scheduled or completed job execution */
50
+ interface JobMessage<Result = any> {
51
+ id: number;
52
+ batchId: number | null;
53
+ queue: string;
54
+ scheduledDate: Iso.Instant;
55
+ enqueuedDate: Iso.Instant;
56
+ startedDate: Iso.Instant | null;
57
+ status: 'waiting' | 'processing' | 'success' | 'error' | 'stuck';
58
+ result: Result;
59
+ recovered: number;
60
+ runningTimeSeconds: number | null;
61
+ uniqueKey: number | null;
62
+ priority: number | null;
63
+ cron: string;
104
64
  }
105
- /** Per-job configuration passed to {@link Service.create} */
106
- interface JobConfig<Result = any> {
107
- /** Cron expression defining the schedule (e.g. '0 9 * * MON') */
65
+ /**
66
+ * A single recurring job instance with its own dedicated queue.
67
+ *
68
+ * Each job has independent start/stop lifecycle, execution history, and
69
+ * rescheduling capability. The queue stores one message per scheduled run —
70
+ * call {@link list} to page through past and pending executions, or
71
+ * {@link getNextUp} to peek at the next waiting run.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const job = RecurringJobs.createJob({
76
+ * name: 'stale-session-cleanup',
77
+ * cron: '0 0 * * *',
78
+ * workFn: async () => { await cleanStaleSessions() }
79
+ * })
80
+ * job.startProcessing()
81
+ * const next = await job.getNextUp() // { status: 'waiting', ... }
82
+ * ```
83
+ */
84
+ interface RecurringJob<T = unknown> {
85
+ /** Enqueues the next scheduled run and begins polling */
86
+ startProcessing(): void;
87
+ /** Stops polling for this job */
88
+ stopProcessing(): void;
89
+ /** The name of the recurring job as specified in the config */
90
+ name: string;
91
+ /** The cron expression defining this job's schedule */
108
92
  cron: string;
109
- /** Async handler invoked when the job is due */
110
- handle(): Promise<Result>;
111
- /** Unique name identifying this job across processes */
93
+ /** Changes the scheduled time of a waiting job */
94
+ rescheduleJob(job: {
95
+ jobId: number;
96
+ scheduledDate: Iso.Instant;
97
+ }): Promise<void>;
98
+ /** Returns the first message in `waiting` status, or `null` if none exists */
99
+ getNextUp(): Promise<JobMessage<T> | null>;
100
+ /** Returns a paginated list of all messages (past runs and pending) for this job */
101
+ list(options: {
102
+ pageSize: number;
103
+ pageNumber: number;
104
+ }): Promise<JobMessage<unknown>[]>;
105
+ }
106
+ /** Union of all recurring job lifecycle log event shapes */
107
+ type LogMessage = {
108
+ event: 'jobProcessing';
109
+ job: string;
110
+ driver: Queue.Driver;
111
+ messageId: number;
112
+ } | {
113
+ event: 'jobSucceeded';
114
+ job: string;
115
+ driver: Queue.Driver;
116
+ result: unknown;
117
+ messageId: number;
118
+ } | {
119
+ event: 'jobFailed';
120
+ job: string;
121
+ driver: Queue.Driver;
122
+ error: unknown;
123
+ messageId: number;
124
+ } | {
125
+ event: 'jobScheduled';
126
+ job: string;
127
+ nextRun: Iso.Instant;
128
+ } | {
129
+ event: 'jobAlreadyScheduled';
130
+ job: string;
131
+ existingJobId: number;
132
+ } | {
133
+ event: 'jobSchedulingFailed';
134
+ job: string;
135
+ error: unknown;
136
+ };
137
+ /** Configuration for a single recurring job passed to {@link RecurringJobsService.createJob} */
138
+ interface JobConfig<T = unknown> {
139
+ cron: string;
140
+ workFn(): Promise<T>;
112
141
  name: string;
113
- /** Optional middleware chain wrapping the handler */
114
- middleware?: Middleware<Result>[];
142
+ logger?: Log.LogFunctions<[LogMessage]>;
115
143
  }
116
144
  /**
117
- * Core recurring jobs service returned by {@link RecurringJobs.Provider.create}.
145
+ * Container that manages multiple recurring jobs sharing the same queue service.
146
+ *
147
+ * Create individual jobs with {@link createJob}, then start or stop processing
148
+ * across all jobs at once or individually. Each job gets its own dedicated queue
149
+ * named `RecurringJobs: <jobName>`.
118
150
  *
119
- * Use {@link Service.create create} to register jobs, then call {@link Service.work work}
120
- * to start polling. The service checks every 60 seconds for due jobs, acquires distributed
121
- * locks via the driver, and executes handlers with middleware.
151
+ * @example
152
+ * ```ts
153
+ * const report = RecurringJobs.createJob({ name: 'daily-report', cron: '0 6 * * *', workFn: generateReport })
154
+ * const cleanup = RecurringJobs.createJob({ name: 'session-cleanup', cron: '0 0 * * *', workFn: cleanSessions })
155
+ * RecurringJobs.startProcessing() // starts both jobs
156
+ * ```
122
157
  */
123
- interface Service {
124
- /** Starts a polling loop that checks for and executes due jobs every 60 seconds — returns an object with an `abort()` method to stop */
125
- work(): {
126
- abort(): Promise<void>;
127
- };
128
- /** Aborts all active work loops and waits for in-flight ticks to finish */
129
- stopAllWork(): Promise<void>;
130
- /** Registers a new recurring job with the given cron schedule and handler */
131
- create<Result = any>(config: JobConfig<Result>): void;
158
+ interface RecurringJobsService {
159
+ /** Creates a recurring job with its own dedicated queue */
160
+ createJob<T>(jobConfig: RecurringJobs.JobConfig<T>): RecurringJobs.RecurringJob<T>;
161
+ /** Starts processing on all registered recurring jobs */
162
+ startProcessing: () => void;
163
+ /** Stops processing on all registered recurring jobs */
164
+ stopProcessing: () => void;
165
+ /** Returns the names of all registered recurring jobs */
166
+ list: () => string[];
167
+ /** Returns the recurring job with the given name, or `undefined` if not found */
168
+ get: <Result>(name: string) => RecurringJobs.RecurringJob<Result> | undefined;
132
169
  }
133
170
  namespace Provider {
134
- interface ServiceConfig {
135
- driver: Driver;
136
- logger?: Log.Logger<[LogMessage]>;
171
+ /** Service-level configuration passed to `RecurringJobs.Provider.create()` */
172
+ interface RecurringJobsServiceConfig {
173
+ logger: Log.Logger<[RecurringJobs.LogMessage]>;
174
+ queueService: Queue.QueueService;
137
175
  }
138
- type Key = keyof KeysWithFallback;
176
+ type Key = keyof Keys;
139
177
  interface Keys {
178
+ default: unknown;
140
179
  }
141
180
  }
142
181
  }
143
182
  /**
144
- * Cron-based recurring job scheduling with distributed lock-based deduplication.
145
- * Call `RecurringJobs.Provider.create()` with a driver, register it, then use `RecurringJobs.create()` to define jobs
146
- * and `RecurringJobs.work()` to start the 60-second polling loop. Drivers: `db`, `cache`, `local`.
183
+ * Cron-based job scheduling backed by the Queue package.
184
+ * Call `RecurringJobs.Provider.create()` with a queue service, register it, then use `RecurringJobs.createJob()` to define jobs.
147
185
  */
148
186
  declare const RecurringJobs: {
149
187
  Provider: {
150
- create: typeof createService;
151
- register: (name: RecurringJobs.Provider.Key, item: RecurringJobs.Service) => void;
188
+ create: typeof create;
189
+ register: (name: RecurringJobs.Provider.Key, item: RecurringJobs.RecurringJobsService) => void;
152
190
  list: () => "default"[];
153
191
  };
154
192
  provider: typeof provider;
155
- work: () => {
156
- abort(): Promise<void>;
157
- };
158
- stopAllWork: () => Promise<void>;
159
- create: <Result = any>(config: RecurringJobs.JobConfig<Result>) => void;
160
- drivers: {
161
- db: typeof DbRecurringJobsDriver;
162
- local: typeof localRecurringJobsDriver;
163
- cache: typeof cacheRecurringJobsDriver;
164
- };
193
+ startProcessing: () => void;
194
+ stopProcessing: () => void;
195
+ list: () => string[];
196
+ get: <Result>(name: string) => RecurringJobs.RecurringJob<Result> | undefined;
197
+ createJob: <T>(jobConfig: RecurringJobs.JobConfig<T>) => RecurringJobs.RecurringJob<T>;
165
198
  };
166
199
 
167
200
  export { RecurringJobs };
package/dist/index.js CHANGED
@@ -1,222 +1,152 @@
1
1
  // src/index.ts
2
- import { instantFns as instantFns2 } from "iso-fns2";
2
+ import "iso-fns2";
3
3
  import cronParser from "cron-parser";
4
+ import { Log } from "@maestro-js/log";
4
5
  import { ServiceRegistry } from "@maestro-js/service-registry";
5
-
6
- // src/db-recurring-jobs-driver.ts
7
- import { instantFns } from "iso-fns2";
8
- function DbRecurringJobsDriver({ db, databaseTable }) {
9
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(databaseTable)) {
10
- throw new Error(`Invalid databaseTable name: ${databaseTable}`);
11
- }
12
- async function listLastRunDates(jobNames) {
13
- if (jobNames.length === 0) return [];
14
- const placeholders = jobNames.map(() => "?").join(", ");
15
- const rows = await db.select(`SELECT name, lastRunDate FROM ${databaseTable} WHERE name IN (${placeholders})`, jobNames);
16
- return rows.filter((row) => row["lastRunDate"] != null).map((row) => ({
17
- jobName: row["name"],
18
- date: row["lastRunDate"]
19
- }));
20
- }
21
- async function pullJob({ jobName, date }) {
22
- const formattedDate = instantFns.formatISO9075(date);
23
- const { changedRows } = await db.update(
24
- `UPDATE ${databaseTable} SET lastRunDate = ? WHERE name = ? AND (lastRunDate IS NULL OR lastRunDate < ?)`,
25
- [formattedDate, jobName, formattedDate]
26
- );
27
- if (changedRows > 0) return true;
28
- const insert = await db.update(`INSERT IGNORE INTO ${databaseTable} (name, lastRunDate) VALUES (?, ?)`, [
29
- jobName,
30
- formattedDate
31
- ]);
32
- return insert.affectedRows > 0;
33
- }
34
- return { listLastRunDates, pullJob };
35
- }
36
-
37
- // src/local-recurring-jobs-driver.ts
38
- function localRecurringJobsDriver() {
39
- const lastRunDates = /* @__PURE__ */ new Map();
40
- async function listLastRunDates(jobNames) {
41
- if (jobNames.length === 0) return [];
42
- return jobNames.filter((name) => lastRunDates.has(name)).map((name) => ({
43
- jobName: name,
44
- date: lastRunDates.get(name)
45
- }));
46
- }
47
- async function pullJob({ jobName, date }) {
48
- const current = lastRunDates.get(jobName);
49
- if (current == null || current < date) {
50
- lastRunDates.set(jobName, date);
51
- return true;
52
- }
53
- return false;
54
- }
55
- return { listLastRunDates, pullJob };
56
- }
57
-
58
- // src/cache-recurring-jobs-driver.ts
59
- function cacheRecurringJobsDriver({
60
- cache,
61
- keyPrefix = "recurring-jobs:"
62
- }) {
63
- async function listLastRunDates(jobNames) {
64
- if (jobNames.length === 0) return [];
65
- const results = await Promise.all(
66
- jobNames.map(async (name) => {
67
- const date = await cache.get(keyPrefix + name);
68
- return date != null ? { jobName: name, date } : null;
6
+ function create(serviceConfig) {
7
+ const queueService = serviceConfig.queueService;
8
+ const jobs = [];
9
+ function createJob(jobConfig) {
10
+ cronParser.parseExpression(jobConfig.cron);
11
+ const logger = Log.create({
12
+ transports: [serviceConfig.logger, ...jobConfig.logger ? [jobConfig.logger] : []]
13
+ });
14
+ const queue = queueService.create({
15
+ name: `RecurringJobs: ${jobConfig.name}`,
16
+ workFn: async () => {
17
+ await addJobToQueue(getNextScheduledDate());
18
+ return jobConfig.workFn();
19
+ },
20
+ logger: Log.create({
21
+ transports: [
22
+ {
23
+ write({ data: [message] }) {
24
+ if (message.event === "messageDequeued") {
25
+ logger.info({
26
+ event: "jobProcessing",
27
+ job: jobConfig.name,
28
+ driver: message.driver,
29
+ messageId: message.messageId
30
+ });
31
+ } else if (message.event === "messageProcessed") {
32
+ logger.info({
33
+ event: "jobSucceeded",
34
+ job: jobConfig.name,
35
+ driver: message.driver,
36
+ result: message.result,
37
+ messageId: message.messageId
38
+ });
39
+ } else if (message.event === "messageFailed") {
40
+ logger.error({
41
+ event: "jobFailed",
42
+ job: jobConfig.name,
43
+ driver: message.driver,
44
+ error: message.error,
45
+ messageId: message.messageId
46
+ });
47
+ }
48
+ }
49
+ }
50
+ ]
69
51
  })
70
- );
71
- return results.filter((r) => r != null);
72
- }
73
- async function pullJob({ jobName, date }) {
74
- const lock = cache.lock(keyPrefix + "lock:" + jobName, 10);
75
- const acquired = await lock.get();
76
- if (!acquired) return false;
77
- try {
78
- const current = await cache.get(keyPrefix + jobName);
79
- if (current == null || current < date) {
80
- await cache.set(keyPrefix + jobName, date, null);
81
- return true;
82
- }
83
- return false;
84
- } finally {
85
- await lock.release();
52
+ });
53
+ function getNextScheduledDate() {
54
+ const interval = cronParser.parseExpression(jobConfig.cron);
55
+ return interval.next().toDate().toISOString();
86
56
  }
87
- }
88
- return { listLastRunDates, pullJob };
89
- }
90
-
91
- // src/index.ts
92
- function createService({ driver, logger: serviceLogger }) {
93
- const allJobs = /* @__PURE__ */ new Map();
94
- const logger = serviceLogger;
95
- let lastRunDatesCached = null;
96
- async function refreshLastRunDates() {
97
- const rows = await driver.listLastRunDates([...allJobs.keys()]);
98
- const map = /* @__PURE__ */ new Map();
99
- for (const row of rows) map.set(row.jobName, row.date);
100
- lastRunDatesCached = map;
101
- return map;
102
- }
103
- async function executeJob(job) {
104
- const middleware = job.middleware ?? [];
105
- const executeFn = middleware.reduceRight(
106
- (acc, cur) => {
107
- return () => cur(acc);
108
- },
109
- () => job.handle()
110
- );
111
- return executeFn();
112
- }
113
- async function tick() {
114
- const now = instantFns2.now();
115
- const lastRunDates = lastRunDatesCached ?? await refreshLastRunDates();
116
- const jobsToRun = [...allJobs.values()].map((jobConfig) => {
117
- const lastRun = lastRunDates.get(jobConfig.name);
118
- const cron = cronParser.parseExpression(jobConfig.cron);
119
- if (lastRun) {
120
- cron.reset(lastRun);
121
- }
122
- const nextRun = cron.next().toDate().toISOString();
123
- return { ...jobConfig, nextRun };
124
- }).filter((c) => c.nextRun <= now);
125
- for (const job of jobsToRun) {
126
- logger?.info({ event: "jobDue", jobName: job.name, nextRun: job.nextRun });
57
+ async function getNextUp() {
58
+ const messages = await queue.listMessages({ pageNumber: 0, pageSize: 100 });
59
+ const result = messages.find((m) => m.status === "waiting");
60
+ return result ? { ...result, cron: jobConfig.cron } : null;
127
61
  }
128
- const lockedJobs = [];
129
- for (const job of jobsToRun) {
130
- try {
131
- const haveLock = await driver.pullJob({ jobName: job.name, date: job.nextRun });
132
- if (haveLock) {
133
- logger?.info({ event: "jobLockAcquired", jobName: job.name, nextRun: job.nextRun });
134
- lockedJobs.push(job);
135
- }
136
- } catch (e) {
137
- logger?.error({ event: "jobLockFailed", jobName: job.name, nextRun: job.nextRun, error: e });
138
- }
62
+ async function list2({
63
+ pageNumber,
64
+ pageSize
65
+ }) {
66
+ const messages = await queue.listMessages({ pageNumber, pageSize });
67
+ return messages.map((m) => ({ ...m, cron: jobConfig.cron }));
139
68
  }
140
- if (jobsToRun.length) {
141
- lastRunDatesCached = null;
69
+ async function rescheduleJob({ jobId, scheduledDate }) {
70
+ const nextUp = await getNextUp();
71
+ if (!nextUp || nextUp.id !== jobId) {
72
+ throw new Error(
73
+ `Cannot reschedule job ${jobId}: only the next waiting job can be rescheduled.` + (nextUp ? ` The current waiting job is ${nextUp.id}.` : " No waiting job found.")
74
+ );
75
+ }
76
+ await queue.updateMessage({ messageId: jobId, scheduledDate, body: {} });
142
77
  }
143
- const results = await Promise.allSettled(lockedJobs.map(executeJob));
144
- for (let i = 0; i < results.length; i++) {
145
- if (results[i].status === "rejected") {
146
- logger?.error({
147
- event: "jobFailed",
148
- jobName: lockedJobs[i].name,
149
- error: results[i].reason
150
- });
151
- } else {
152
- logger?.info({ event: "jobProcessed", jobName: lockedJobs[i].name });
78
+ async function addJobToQueue(nextRun) {
79
+ const data = await getNextUp();
80
+ if (data) {
81
+ logger.info({ event: "jobAlreadyScheduled", job: jobConfig.name, existingJobId: data.id });
82
+ return false;
153
83
  }
84
+ await queue.addMessage({}, { scheduledDate: nextRun, tracingContext: null });
85
+ logger.info({ event: "jobScheduled", job: jobConfig.name, nextRun });
86
+ return true;
154
87
  }
155
- }
156
- let abortWorkFunctions = [];
157
- function work() {
158
- let ticks = [];
159
- function doTick() {
160
- const promise = tick();
161
- ticks.push(promise);
162
- promise.finally(() => {
163
- ticks = ticks.filter((t) => t !== promise);
164
- });
165
- return promise.catch((e) => {
88
+ function startProcessing2() {
89
+ addJobToQueue(getNextScheduledDate()).catch((error) => {
90
+ logger.error({ event: "jobSchedulingFailed", job: jobConfig.name, error });
166
91
  });
92
+ queue.startProcessing();
167
93
  }
168
- doTick();
169
- const interval = setInterval(doTick, 6e4);
170
- async function abort() {
171
- clearInterval(interval);
172
- await Promise.all(ticks);
173
- abortWorkFunctions = abortWorkFunctions.filter((a) => a !== abort);
94
+ function stopProcessing2() {
95
+ queue.stopProcessing();
174
96
  }
175
- abortWorkFunctions.push(abort);
176
- return {
177
- abort
97
+ const jobFunctions = {
98
+ getNextUp,
99
+ list: list2,
100
+ startProcessing: startProcessing2,
101
+ stopProcessing: stopProcessing2,
102
+ rescheduleJob,
103
+ name: jobConfig.name,
104
+ cron: jobConfig.cron
178
105
  };
106
+ jobs.push(jobFunctions);
107
+ return jobFunctions;
179
108
  }
180
- async function stopAllWork() {
181
- await Promise.all(abortWorkFunctions.map((w) => w()));
109
+ function startProcessing() {
110
+ jobs.forEach((job) => job.startProcessing());
182
111
  }
183
- function create(config) {
184
- cronParser.parseExpression(config.cron);
185
- if (allJobs.has(config.name)) {
186
- throw new Error(`Cannot have multiple recurring jobs with name "${config.name}"`);
187
- }
188
- allJobs.set(config.name, config);
112
+ function stopProcessing() {
113
+ jobs.forEach((job) => job.stopProcessing());
189
114
  }
190
- return { work, create, stopAllWork };
115
+ function list() {
116
+ return jobs.map((job) => job.name);
117
+ }
118
+ function get(name) {
119
+ return jobs.find((job) => job.name === name);
120
+ }
121
+ return { startProcessing, stopProcessing, list, get, createJob };
191
122
  }
192
123
  var registry = ServiceRegistry.createRegistry(
193
124
  ServiceRegistry.proxyFunctionsForObject
194
125
  );
195
126
  var Provider = {
196
- create: createService,
127
+ create,
197
128
  register: registry.register,
198
129
  list: registry.list
199
130
  };
200
131
  function provider(key) {
201
132
  const service = registry.resolve(key);
202
133
  return {
203
- work: service.work,
204
- stopAllWork: service.stopAllWork,
205
- create: service.create
134
+ startProcessing: service.startProcessing,
135
+ stopProcessing: service.stopProcessing,
136
+ list: service.list,
137
+ get: service.get,
138
+ createJob: service.createJob
206
139
  };
207
140
  }
208
141
  var facade = provider("default");
209
142
  var RecurringJobs = {
210
143
  Provider,
211
144
  provider,
212
- work: facade.work,
213
- stopAllWork: facade.stopAllWork,
214
- create: facade.create,
215
- drivers: {
216
- db: DbRecurringJobsDriver,
217
- local: localRecurringJobsDriver,
218
- cache: cacheRecurringJobsDriver
219
- }
145
+ startProcessing: facade.startProcessing,
146
+ stopProcessing: facade.stopProcessing,
147
+ list: facade.list,
148
+ get: facade.get,
149
+ createJob: facade.createJob
220
150
  };
221
151
  export {
222
152
  RecurringJobs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maestro-js/recurring-jobs",
3
- "description": "Use when working with @maestro-js/recurring-jobs. Cron-based recurring job scheduling with distributed lock-based deduplication. Use for cron scheduling, periodic background tasks, and recurring jobs that must run exactly once across processes. Drivers: Db (MySQL), Cache (Redis via @maestro-js/cache), Local (in-memory). Follows the Provider pattern with ServiceRegistry. Key capabilities include cron expression parsing, 60-second polling loop, per-job distributed locking, optional middleware wrapping, eager cron validation, structured lifecycle log events (jobDue, jobLockAcquired, jobFailed, jobProcessed), and graceful shutdown via abort/stopAllWork.",
3
+ "description": "Use when working with @maestro-js/recurring-jobs. Cron-based job scheduling backed by the Queue package. Covers creating recurring jobs, managing schedules, start/stop processing, rescheduling, listing jobs, and inspecting upcoming runs. Follows the Provider pattern with ServiceRegistry. Key capabilities include cron expression parsing, deduplication across process restarts, per-job dedicated queues, structured log events, and paginated execution history.",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -11,20 +11,14 @@
11
11
  "dependencies": {
12
12
  "cron-parser": "^4.9.0",
13
13
  "iso-fns2": "npm:iso-fns@2.0.0-alpha.26",
14
- "@maestro-js/service-registry": "1.0.0-alpha.19"
15
- },
16
- "peerDependencies": {
17
- "@maestro-js/log": "1.0.0-alpha.19",
18
- "@maestro-js/cache": "1.0.0-alpha.19"
14
+ "@maestro-js/log": "1.0.0-alpha.2",
15
+ "@maestro-js/service-registry": "1.0.0-alpha.2"
19
16
  },
20
17
  "devDependencies": {
21
18
  "@types/node": "^22.19.11",
22
- "@maestro-js/cache": "1.0.0-alpha.19",
23
- "@maestro-js/db": "1.0.0-alpha.19",
24
- "@maestro-js/log": "1.0.0-alpha.19",
25
- "@maestro-js/queue": "1.0.0-alpha.19"
19
+ "@maestro-js/queue": "1.0.0-alpha.2"
26
20
  },
27
- "version": "1.0.0-alpha.19",
21
+ "version": "1.0.0-alpha.2",
28
22
  "publishConfig": {
29
23
  "access": "restricted"
30
24
  },
@@ -39,7 +33,7 @@
39
33
  "scripts": {
40
34
  "build": "tsup --config ../../tsup.config.ts",
41
35
  "typecheck": "tsc --noEmit",
42
- "test": "beartest ./tests/**/*",
36
+ "test": "echo 'No tests yet'",
43
37
  "format": "prettier --write src/",
44
38
  "lint": "prettier --check src/"
45
39
  }