@nicnocquee/dataqueue 1.24.0 → 1.26.0-beta.20260223195940

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 (72) hide show
  1. package/README.md +44 -0
  2. package/ai/build-docs-content.ts +96 -0
  3. package/ai/build-llms-full.ts +42 -0
  4. package/ai/docs-content.json +278 -0
  5. package/ai/rules/advanced.md +132 -0
  6. package/ai/rules/basic.md +159 -0
  7. package/ai/rules/react-dashboard.md +83 -0
  8. package/ai/skills/dataqueue-advanced/SKILL.md +320 -0
  9. package/ai/skills/dataqueue-core/SKILL.md +234 -0
  10. package/ai/skills/dataqueue-react/SKILL.md +189 -0
  11. package/dist/cli.cjs +1149 -14
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.d.cts +66 -1
  14. package/dist/cli.d.ts +66 -1
  15. package/dist/cli.js +1146 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/index.cjs +4630 -928
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1033 -15
  20. package/dist/index.d.ts +1033 -15
  21. package/dist/index.js +4626 -929
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp-server.cjs +186 -0
  24. package/dist/mcp-server.cjs.map +1 -0
  25. package/dist/mcp-server.d.cts +32 -0
  26. package/dist/mcp-server.d.ts +32 -0
  27. package/dist/mcp-server.js +175 -0
  28. package/dist/mcp-server.js.map +1 -0
  29. package/migrations/1751131910825_add_timeout_seconds_to_job_queue.sql +2 -2
  30. package/migrations/1751186053000_add_job_events_table.sql +12 -8
  31. package/migrations/1751984773000_add_tags_to_job_queue.sql +1 -1
  32. package/migrations/1765809419000_add_force_kill_on_timeout_to_job_queue.sql +1 -1
  33. package/migrations/1771100000000_add_idempotency_key_to_job_queue.sql +7 -0
  34. package/migrations/1781200000000_add_wait_support.sql +12 -0
  35. package/migrations/1781200000001_create_waitpoints_table.sql +18 -0
  36. package/migrations/1781200000002_add_performance_indexes.sql +34 -0
  37. package/migrations/1781200000003_add_progress_to_job_queue.sql +7 -0
  38. package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
  39. package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
  40. package/package.json +40 -23
  41. package/src/backend.ts +328 -0
  42. package/src/backends/postgres.ts +2040 -0
  43. package/src/backends/redis-scripts.ts +865 -0
  44. package/src/backends/redis.test.ts +1906 -0
  45. package/src/backends/redis.ts +1792 -0
  46. package/src/cli.test.ts +82 -6
  47. package/src/cli.ts +73 -10
  48. package/src/cron.test.ts +126 -0
  49. package/src/cron.ts +40 -0
  50. package/src/db-util.ts +4 -2
  51. package/src/index.test.ts +688 -1
  52. package/src/index.ts +277 -39
  53. package/src/init-command.test.ts +449 -0
  54. package/src/init-command.ts +709 -0
  55. package/src/install-mcp-command.test.ts +216 -0
  56. package/src/install-mcp-command.ts +185 -0
  57. package/src/install-rules-command.test.ts +218 -0
  58. package/src/install-rules-command.ts +233 -0
  59. package/src/install-skills-command.test.ts +176 -0
  60. package/src/install-skills-command.ts +124 -0
  61. package/src/mcp-server.test.ts +162 -0
  62. package/src/mcp-server.ts +231 -0
  63. package/src/processor.test.ts +559 -18
  64. package/src/processor.ts +456 -49
  65. package/src/queue.test.ts +682 -6
  66. package/src/queue.ts +135 -944
  67. package/src/supervisor.test.ts +340 -0
  68. package/src/supervisor.ts +162 -0
  69. package/src/test-util.ts +32 -0
  70. package/src/types.ts +726 -17
  71. package/src/wait.test.ts +698 -0
  72. package/LICENSE +0 -21
package/src/index.ts CHANGED
@@ -1,63 +1,160 @@
1
- import {
2
- addJob,
3
- getJob,
4
- getJobsByStatus,
5
- retryJob,
6
- cleanupOldJobs,
7
- cancelJob,
8
- cancelAllUpcomingJobs,
9
- getAllJobs,
10
- reclaimStuckJobs,
11
- getJobEvents,
12
- getJobsByTags,
13
- getJobs,
14
- editJob,
15
- editAllPendingJobs,
16
- } from './queue.js';
17
1
  import { createProcessor } from './processor.js';
2
+ import { createSupervisor } from './supervisor.js';
18
3
  import {
19
4
  JobQueueConfig,
20
5
  JobQueue,
21
6
  JobOptions,
7
+ AddJobOptions,
22
8
  ProcessorOptions,
9
+ SupervisorOptions,
23
10
  JobHandlers,
24
11
  JobType,
12
+ PostgresJobQueueConfig,
13
+ RedisJobQueueConfig,
14
+ CronScheduleOptions,
15
+ CronScheduleStatus,
16
+ EditCronScheduleOptions,
25
17
  } from './types.js';
18
+ import { QueueBackend, CronScheduleInput } from './backend.js';
26
19
  import { setLogContext } from './log-context.js';
27
20
  import { createPool } from './db-util.js';
21
+ import { PostgresBackend } from './backends/postgres.js';
22
+ import { RedisBackend } from './backends/redis.js';
23
+ import { getNextCronOccurrence, validateCronExpression } from './cron.js';
28
24
 
29
25
  /**
30
- * Initialize the job queue system
26
+ * Initialize the job queue system.
27
+ *
28
+ * Defaults to PostgreSQL when `backend` is omitted.
29
+ * For PostgreSQL, provide either `databaseConfig` or `pool` (bring your own).
30
+ * For Redis, provide either `redisConfig` or `client` (bring your own).
31
31
  */
32
32
  export const initJobQueue = <PayloadMap = any>(
33
33
  config: JobQueueConfig,
34
34
  ): JobQueue<PayloadMap> => {
35
- const { databaseConfig } = config;
35
+ const backendType = config.backend ?? 'postgres';
36
+ setLogContext(config.verbose ?? false);
36
37
 
37
- // Create database pool
38
- const pool = createPool(databaseConfig);
38
+ let backend: QueueBackend;
39
39
 
40
- setLogContext(config.verbose ?? false);
40
+ if (backendType === 'postgres') {
41
+ const pgConfig = config as PostgresJobQueueConfig;
42
+ if (pgConfig.pool) {
43
+ backend = new PostgresBackend(pgConfig.pool);
44
+ } else if (pgConfig.databaseConfig) {
45
+ const pool = createPool(pgConfig.databaseConfig);
46
+ backend = new PostgresBackend(pool);
47
+ } else {
48
+ throw new Error(
49
+ 'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.',
50
+ );
51
+ }
52
+ } else if (backendType === 'redis') {
53
+ const redisConfig = config as RedisJobQueueConfig;
54
+ if (redisConfig.client) {
55
+ backend = new RedisBackend(
56
+ redisConfig.client as any,
57
+ redisConfig.keyPrefix,
58
+ );
59
+ } else if (redisConfig.redisConfig) {
60
+ backend = new RedisBackend(redisConfig.redisConfig);
61
+ } else {
62
+ throw new Error(
63
+ 'Redis backend requires either "redisConfig" or "client" to be provided.',
64
+ );
65
+ }
66
+ } else {
67
+ throw new Error(`Unknown backend: ${backendType}`);
68
+ }
69
+
70
+ /**
71
+ * Enqueue due cron jobs. Shared by the public API and the processor hook.
72
+ */
73
+ const enqueueDueCronJobsImpl = async (): Promise<number> => {
74
+ const dueSchedules = await backend.getDueCronSchedules();
75
+ let count = 0;
76
+
77
+ for (const schedule of dueSchedules) {
78
+ // Overlap check: skip if allowOverlap is false and last job is still active
79
+ if (!schedule.allowOverlap && schedule.lastJobId !== null) {
80
+ const lastJob = await backend.getJob(schedule.lastJobId);
81
+ if (
82
+ lastJob &&
83
+ (lastJob.status === 'pending' ||
84
+ lastJob.status === 'processing' ||
85
+ lastJob.status === 'waiting')
86
+ ) {
87
+ // Still active — advance nextRunAt but don't enqueue
88
+ const nextRunAt = getNextCronOccurrence(
89
+ schedule.cronExpression,
90
+ schedule.timezone,
91
+ );
92
+ await backend.updateCronScheduleAfterEnqueue(
93
+ schedule.id,
94
+ new Date(),
95
+ schedule.lastJobId,
96
+ nextRunAt,
97
+ );
98
+ continue;
99
+ }
100
+ }
101
+
102
+ // Enqueue a new job instance
103
+ const jobId = await backend.addJob<any, any>({
104
+ jobType: schedule.jobType,
105
+ payload: schedule.payload,
106
+ maxAttempts: schedule.maxAttempts,
107
+ priority: schedule.priority,
108
+ timeoutMs: schedule.timeoutMs ?? undefined,
109
+ forceKillOnTimeout: schedule.forceKillOnTimeout,
110
+ tags: schedule.tags,
111
+ retryDelay: schedule.retryDelay ?? undefined,
112
+ retryBackoff: schedule.retryBackoff ?? undefined,
113
+ retryDelayMax: schedule.retryDelayMax ?? undefined,
114
+ });
115
+
116
+ // Advance to next occurrence
117
+ const nextRunAt = getNextCronOccurrence(
118
+ schedule.cronExpression,
119
+ schedule.timezone,
120
+ );
121
+ await backend.updateCronScheduleAfterEnqueue(
122
+ schedule.id,
123
+ new Date(),
124
+ jobId,
125
+ nextRunAt,
126
+ );
127
+ count++;
128
+ }
129
+
130
+ return count;
131
+ };
41
132
 
42
133
  // Return the job queue API
43
134
  return {
44
135
  // Job queue operations
45
136
  addJob: withLogContext(
46
- (job: JobOptions<PayloadMap, any>) => addJob<PayloadMap, any>(pool, job),
137
+ (job: JobOptions<PayloadMap, any>, options?: AddJobOptions) =>
138
+ backend.addJob<PayloadMap, any>(job, options),
139
+ config.verbose ?? false,
140
+ ),
141
+ addJobs: withLogContext(
142
+ (jobs: JobOptions<PayloadMap, any>[], options?: AddJobOptions) =>
143
+ backend.addJobs<PayloadMap, any>(jobs, options),
47
144
  config.verbose ?? false,
48
145
  ),
49
146
  getJob: withLogContext(
50
- (id: number) => getJob<PayloadMap, any>(pool, id),
147
+ (id: number) => backend.getJob<PayloadMap, any>(id),
51
148
  config.verbose ?? false,
52
149
  ),
53
150
  getJobsByStatus: withLogContext(
54
151
  (status: string, limit?: number, offset?: number) =>
55
- getJobsByStatus<PayloadMap, any>(pool, status, limit, offset),
152
+ backend.getJobsByStatus<PayloadMap, any>(status, limit, offset),
56
153
  config.verbose ?? false,
57
154
  ),
58
155
  getAllJobs: withLogContext(
59
156
  (limit?: number, offset?: number) =>
60
- getAllJobs<PayloadMap, any>(pool, limit, offset),
157
+ backend.getAllJobs<PayloadMap, any>(limit, offset),
61
158
  config.verbose ?? false,
62
159
  ),
63
160
  getJobs: withLogContext(
@@ -72,20 +169,23 @@ export const initJobQueue = <PayloadMap = any>(
72
169
  },
73
170
  limit?: number,
74
171
  offset?: number,
75
- ) => getJobs<PayloadMap, any>(pool, filters, limit, offset),
172
+ ) => backend.getJobs<PayloadMap, any>(filters, limit, offset),
76
173
  config.verbose ?? false,
77
174
  ),
78
- retryJob: (jobId: number) => retryJob(pool, jobId),
79
- cleanupOldJobs: (daysToKeep?: number) => cleanupOldJobs(pool, daysToKeep),
175
+ retryJob: (jobId: number) => backend.retryJob(jobId),
176
+ cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
177
+ backend.cleanupOldJobs(daysToKeep, batchSize),
178
+ cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
179
+ backend.cleanupOldJobEvents(daysToKeep, batchSize),
80
180
  cancelJob: withLogContext(
81
- (jobId: number) => cancelJob(pool, jobId),
181
+ (jobId: number) => backend.cancelJob(jobId),
82
182
  config.verbose ?? false,
83
183
  ),
84
184
  editJob: withLogContext(
85
185
  <T extends JobType<PayloadMap>>(
86
186
  jobId: number,
87
187
  updates: import('./types.js').EditJobOptions<PayloadMap, T>,
88
- ) => editJob<PayloadMap, T>(pool, jobId, updates as any),
188
+ ) => backend.editJob(jobId, updates as import('./backend.js').JobUpdates),
89
189
  config.verbose ?? false,
90
190
  ),
91
191
  editAllPendingJobs: withLogContext(
@@ -104,7 +204,11 @@ export const initJobQueue = <PayloadMap = any>(
104
204
  }
105
205
  | undefined,
106
206
  updates: import('./types.js').EditJobOptions<PayloadMap, T>,
107
- ) => editAllPendingJobs<PayloadMap, T>(pool, filters, updates as any),
207
+ ) =>
208
+ backend.editAllPendingJobs(
209
+ filters,
210
+ updates as import('./backend.js').JobUpdates,
211
+ ),
108
212
  config.verbose ?? false,
109
213
  ),
110
214
  cancelAllUpcomingJobs: withLogContext(
@@ -115,32 +219,163 @@ export const initJobQueue = <PayloadMap = any>(
115
219
  | Date
116
220
  | { gt?: Date; gte?: Date; lt?: Date; lte?: Date; eq?: Date };
117
221
  tags?: { values: string[]; mode?: import('./types.js').TagQueryMode };
118
- }) => cancelAllUpcomingJobs(pool, filters),
222
+ }) => backend.cancelAllUpcomingJobs(filters),
119
223
  config.verbose ?? false,
120
224
  ),
121
225
  reclaimStuckJobs: withLogContext(
122
226
  (maxProcessingTimeMinutes?: number) =>
123
- reclaimStuckJobs(pool, maxProcessingTimeMinutes),
227
+ backend.reclaimStuckJobs(maxProcessingTimeMinutes),
124
228
  config.verbose ?? false,
125
229
  ),
126
230
  getJobsByTags: withLogContext(
127
231
  (tags: string[], mode = 'all', limit?: number, offset?: number) =>
128
- getJobsByTags<PayloadMap, any>(pool, tags, mode, limit, offset),
232
+ backend.getJobsByTags<PayloadMap, any>(tags, mode, limit, offset),
129
233
  config.verbose ?? false,
130
234
  ),
131
235
 
132
- // Job processing
236
+ // Job processing — automatically enqueues due cron jobs before each batch
133
237
  createProcessor: (
134
238
  handlers: JobHandlers<PayloadMap>,
135
239
  options?: ProcessorOptions,
136
- ) => createProcessor<PayloadMap>(pool, handlers, options),
137
- // Advanced access (for custom operations)
138
- getPool: () => pool,
240
+ ) =>
241
+ createProcessor<PayloadMap>(backend, handlers, options, async () => {
242
+ await enqueueDueCronJobsImpl();
243
+ }),
244
+
245
+ // Background supervisor — automated maintenance
246
+ createSupervisor: (options?: SupervisorOptions) =>
247
+ createSupervisor(backend, options),
248
+
139
249
  // Job events
140
250
  getJobEvents: withLogContext(
141
- (jobId: number) => getJobEvents(pool, jobId),
251
+ (jobId: number) => backend.getJobEvents(jobId),
252
+ config.verbose ?? false,
253
+ ),
254
+
255
+ // Wait / Token support (works with all backends)
256
+ createToken: withLogContext(
257
+ (options?: import('./types.js').CreateTokenOptions) =>
258
+ backend.createWaitpoint(null, options),
259
+ config.verbose ?? false,
260
+ ),
261
+ completeToken: withLogContext(
262
+ (tokenId: string, data?: any) => backend.completeWaitpoint(tokenId, data),
263
+ config.verbose ?? false,
264
+ ),
265
+ getToken: withLogContext(
266
+ (tokenId: string) => backend.getWaitpoint(tokenId),
267
+ config.verbose ?? false,
268
+ ),
269
+ expireTimedOutTokens: withLogContext(
270
+ () => backend.expireTimedOutWaitpoints(),
142
271
  config.verbose ?? false,
143
272
  ),
273
+
274
+ // Cron schedule operations
275
+ addCronJob: withLogContext(
276
+ <T extends JobType<PayloadMap>>(
277
+ options: CronScheduleOptions<PayloadMap, T>,
278
+ ) => {
279
+ if (!validateCronExpression(options.cronExpression)) {
280
+ return Promise.reject(
281
+ new Error(`Invalid cron expression: "${options.cronExpression}"`),
282
+ );
283
+ }
284
+ const nextRunAt = getNextCronOccurrence(
285
+ options.cronExpression,
286
+ options.timezone ?? 'UTC',
287
+ );
288
+ const input: CronScheduleInput = {
289
+ scheduleName: options.scheduleName,
290
+ cronExpression: options.cronExpression,
291
+ jobType: options.jobType as string,
292
+ payload: options.payload,
293
+ maxAttempts: options.maxAttempts ?? 3,
294
+ priority: options.priority ?? 0,
295
+ timeoutMs: options.timeoutMs ?? null,
296
+ forceKillOnTimeout: options.forceKillOnTimeout ?? false,
297
+ tags: options.tags,
298
+ timezone: options.timezone ?? 'UTC',
299
+ allowOverlap: options.allowOverlap ?? false,
300
+ nextRunAt,
301
+ retryDelay: options.retryDelay ?? null,
302
+ retryBackoff: options.retryBackoff ?? null,
303
+ retryDelayMax: options.retryDelayMax ?? null,
304
+ };
305
+ return backend.addCronSchedule(input);
306
+ },
307
+ config.verbose ?? false,
308
+ ),
309
+ getCronJob: withLogContext(
310
+ (id: number) => backend.getCronSchedule(id),
311
+ config.verbose ?? false,
312
+ ),
313
+ getCronJobByName: withLogContext(
314
+ (name: string) => backend.getCronScheduleByName(name),
315
+ config.verbose ?? false,
316
+ ),
317
+ listCronJobs: withLogContext(
318
+ (status?: CronScheduleStatus) => backend.listCronSchedules(status),
319
+ config.verbose ?? false,
320
+ ),
321
+ removeCronJob: withLogContext(
322
+ (id: number) => backend.removeCronSchedule(id),
323
+ config.verbose ?? false,
324
+ ),
325
+ pauseCronJob: withLogContext(
326
+ (id: number) => backend.pauseCronSchedule(id),
327
+ config.verbose ?? false,
328
+ ),
329
+ resumeCronJob: withLogContext(
330
+ (id: number) => backend.resumeCronSchedule(id),
331
+ config.verbose ?? false,
332
+ ),
333
+ editCronJob: withLogContext(
334
+ async (id: number, updates: EditCronScheduleOptions) => {
335
+ if (
336
+ updates.cronExpression !== undefined &&
337
+ !validateCronExpression(updates.cronExpression)
338
+ ) {
339
+ throw new Error(
340
+ `Invalid cron expression: "${updates.cronExpression}"`,
341
+ );
342
+ }
343
+ let nextRunAt: Date | null | undefined;
344
+ if (
345
+ updates.cronExpression !== undefined ||
346
+ updates.timezone !== undefined
347
+ ) {
348
+ const existing = await backend.getCronSchedule(id);
349
+ const expr = updates.cronExpression ?? existing?.cronExpression ?? '';
350
+ const tz = updates.timezone ?? existing?.timezone ?? 'UTC';
351
+ nextRunAt = getNextCronOccurrence(expr, tz);
352
+ }
353
+ await backend.editCronSchedule(id, updates, nextRunAt);
354
+ },
355
+ config.verbose ?? false,
356
+ ),
357
+ enqueueDueCronJobs: withLogContext(
358
+ () => enqueueDueCronJobsImpl(),
359
+ config.verbose ?? false,
360
+ ),
361
+
362
+ // Advanced access
363
+ getPool: () => {
364
+ if (!(backend instanceof PostgresBackend)) {
365
+ throw new Error(
366
+ 'getPool() is only available with the PostgreSQL backend.',
367
+ );
368
+ }
369
+ return backend.getPool();
370
+ },
371
+ getRedisClient: () => {
372
+ if (backendType !== 'redis') {
373
+ throw new Error(
374
+ 'getRedisClient() is only available with the Redis backend.',
375
+ );
376
+ }
377
+ return (backend as RedisBackend).getClient();
378
+ },
144
379
  };
145
380
  };
146
381
 
@@ -152,7 +387,10 @@ const withLogContext =
152
387
  };
153
388
 
154
389
  export * from './types.js';
390
+ export { QueueBackend, CronScheduleInput } from './backend.js';
391
+ export { PostgresBackend } from './backends/postgres.js';
155
392
  export {
156
393
  validateHandlerSerializable,
157
394
  testHandlerSerialization,
158
395
  } from './handler-validation.js';
396
+ export { getNextCronOccurrence, validateCronExpression } from './cron.js';