@nicnocquee/dataqueue 1.25.0 → 1.26.0-beta.20260223202259

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 (59) hide show
  1. package/ai/build-docs-content.ts +96 -0
  2. package/ai/build-llms-full.ts +42 -0
  3. package/ai/docs-content.json +284 -0
  4. package/ai/rules/advanced.md +150 -0
  5. package/ai/rules/basic.md +159 -0
  6. package/ai/rules/react-dashboard.md +83 -0
  7. package/ai/skills/dataqueue-advanced/SKILL.md +370 -0
  8. package/ai/skills/dataqueue-core/SKILL.md +234 -0
  9. package/ai/skills/dataqueue-react/SKILL.md +189 -0
  10. package/dist/cli.cjs +1149 -14
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.d.cts +66 -1
  13. package/dist/cli.d.ts +66 -1
  14. package/dist/cli.js +1146 -13
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index.cjs +3236 -1237
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +697 -23
  19. package/dist/index.d.ts +697 -23
  20. package/dist/index.js +3235 -1238
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp-server.cjs +186 -0
  23. package/dist/mcp-server.cjs.map +1 -0
  24. package/dist/mcp-server.d.cts +32 -0
  25. package/dist/mcp-server.d.ts +32 -0
  26. package/dist/mcp-server.js +175 -0
  27. package/dist/mcp-server.js.map +1 -0
  28. package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
  29. package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
  30. package/package.json +24 -21
  31. package/src/backend.ts +170 -5
  32. package/src/backends/postgres.ts +992 -63
  33. package/src/backends/redis-scripts.ts +358 -26
  34. package/src/backends/redis.test.ts +1532 -0
  35. package/src/backends/redis.ts +993 -35
  36. package/src/cli.test.ts +82 -6
  37. package/src/cli.ts +73 -10
  38. package/src/cron.test.ts +126 -0
  39. package/src/cron.ts +40 -0
  40. package/src/db-util.ts +1 -1
  41. package/src/index.test.ts +1034 -11
  42. package/src/index.ts +267 -39
  43. package/src/init-command.test.ts +449 -0
  44. package/src/init-command.ts +709 -0
  45. package/src/install-mcp-command.test.ts +216 -0
  46. package/src/install-mcp-command.ts +185 -0
  47. package/src/install-rules-command.test.ts +218 -0
  48. package/src/install-rules-command.ts +233 -0
  49. package/src/install-skills-command.test.ts +176 -0
  50. package/src/install-skills-command.ts +124 -0
  51. package/src/mcp-server.test.ts +162 -0
  52. package/src/mcp-server.ts +231 -0
  53. package/src/processor.ts +104 -113
  54. package/src/queue.test.ts +465 -0
  55. package/src/queue.ts +34 -252
  56. package/src/supervisor.test.ts +340 -0
  57. package/src/supervisor.ts +177 -0
  58. package/src/types.ts +476 -12
  59. package/LICENSE +0 -21
package/src/index.ts CHANGED
@@ -1,30 +1,37 @@
1
- import {
2
- createWaitpoint,
3
- completeWaitpoint,
4
- getWaitpoint,
5
- expireTimedOutWaitpoints,
6
- } from './queue.js';
1
+ import { EventEmitter } from 'node:events';
7
2
  import { createProcessor } from './processor.js';
3
+ import { createSupervisor } from './supervisor.js';
8
4
  import {
9
5
  JobQueueConfig,
10
6
  JobQueue,
11
7
  JobOptions,
8
+ AddJobOptions,
12
9
  ProcessorOptions,
10
+ SupervisorOptions,
13
11
  JobHandlers,
14
12
  JobType,
15
13
  PostgresJobQueueConfig,
16
14
  RedisJobQueueConfig,
15
+ CronScheduleOptions,
16
+ CronScheduleStatus,
17
+ EditCronScheduleOptions,
18
+ QueueEventMap,
19
+ QueueEventName,
20
+ QueueEmitFn,
17
21
  } from './types.js';
18
- import { QueueBackend } from './backend.js';
22
+ import { QueueBackend, CronScheduleInput } from './backend.js';
19
23
  import { setLogContext } from './log-context.js';
20
24
  import { createPool } from './db-util.js';
21
25
  import { PostgresBackend } from './backends/postgres.js';
22
26
  import { RedisBackend } from './backends/redis.js';
27
+ import { getNextCronOccurrence, validateCronExpression } from './cron.js';
23
28
 
24
29
  /**
25
30
  * Initialize the job queue system.
26
31
  *
27
32
  * Defaults to PostgreSQL when `backend` is omitted.
33
+ * For PostgreSQL, provide either `databaseConfig` or `pool` (bring your own).
34
+ * For Redis, provide either `redisConfig` or `client` (bring your own).
28
35
  */
29
36
  export const initJobQueue = <PayloadMap = any>(
30
37
  config: JobQueueConfig,
@@ -33,35 +40,124 @@ export const initJobQueue = <PayloadMap = any>(
33
40
  setLogContext(config.verbose ?? false);
34
41
 
35
42
  let backend: QueueBackend;
36
- let pool: import('pg').Pool | undefined;
37
43
 
38
44
  if (backendType === 'postgres') {
39
45
  const pgConfig = config as PostgresJobQueueConfig;
40
- pool = createPool(pgConfig.databaseConfig);
41
- backend = new PostgresBackend(pool);
46
+ if (pgConfig.pool) {
47
+ backend = new PostgresBackend(pgConfig.pool);
48
+ } else if (pgConfig.databaseConfig) {
49
+ const pool = createPool(pgConfig.databaseConfig);
50
+ backend = new PostgresBackend(pool);
51
+ } else {
52
+ throw new Error(
53
+ 'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.',
54
+ );
55
+ }
42
56
  } else if (backendType === 'redis') {
43
- const redisConfig = (config as RedisJobQueueConfig).redisConfig;
44
- // RedisBackend constructor will throw if ioredis is not installed
45
- backend = new RedisBackend(redisConfig);
57
+ const redisConfig = config as RedisJobQueueConfig;
58
+ if (redisConfig.client) {
59
+ backend = new RedisBackend(
60
+ redisConfig.client as any,
61
+ redisConfig.keyPrefix,
62
+ );
63
+ } else if (redisConfig.redisConfig) {
64
+ backend = new RedisBackend(redisConfig.redisConfig);
65
+ } else {
66
+ throw new Error(
67
+ 'Redis backend requires either "redisConfig" or "client" to be provided.',
68
+ );
69
+ }
46
70
  } else {
47
71
  throw new Error(`Unknown backend: ${backendType}`);
48
72
  }
49
73
 
50
- const requirePool = () => {
51
- if (!pool) {
52
- throw new Error(
53
- 'Wait/Token features require the PostgreSQL backend. Configure with backend: "postgres" to use these features.',
74
+ const emitter = new EventEmitter();
75
+ const emit: QueueEmitFn = (event, data) => {
76
+ emitter.emit(event, data);
77
+ };
78
+
79
+ /**
80
+ * Enqueue due cron jobs. Shared by the public API and the processor hook.
81
+ */
82
+ const enqueueDueCronJobsImpl = async (): Promise<number> => {
83
+ const dueSchedules = await backend.getDueCronSchedules();
84
+ let count = 0;
85
+
86
+ for (const schedule of dueSchedules) {
87
+ // Overlap check: skip if allowOverlap is false and last job is still active
88
+ if (!schedule.allowOverlap && schedule.lastJobId !== null) {
89
+ const lastJob = await backend.getJob(schedule.lastJobId);
90
+ if (
91
+ lastJob &&
92
+ (lastJob.status === 'pending' ||
93
+ lastJob.status === 'processing' ||
94
+ lastJob.status === 'waiting')
95
+ ) {
96
+ // Still active — advance nextRunAt but don't enqueue
97
+ const nextRunAt = getNextCronOccurrence(
98
+ schedule.cronExpression,
99
+ schedule.timezone,
100
+ );
101
+ await backend.updateCronScheduleAfterEnqueue(
102
+ schedule.id,
103
+ new Date(),
104
+ schedule.lastJobId,
105
+ nextRunAt,
106
+ );
107
+ continue;
108
+ }
109
+ }
110
+
111
+ // Enqueue a new job instance
112
+ const jobId = await backend.addJob<any, any>({
113
+ jobType: schedule.jobType,
114
+ payload: schedule.payload,
115
+ maxAttempts: schedule.maxAttempts,
116
+ priority: schedule.priority,
117
+ timeoutMs: schedule.timeoutMs ?? undefined,
118
+ forceKillOnTimeout: schedule.forceKillOnTimeout,
119
+ tags: schedule.tags,
120
+ retryDelay: schedule.retryDelay ?? undefined,
121
+ retryBackoff: schedule.retryBackoff ?? undefined,
122
+ retryDelayMax: schedule.retryDelayMax ?? undefined,
123
+ });
124
+
125
+ // Advance to next occurrence
126
+ const nextRunAt = getNextCronOccurrence(
127
+ schedule.cronExpression,
128
+ schedule.timezone,
54
129
  );
130
+ await backend.updateCronScheduleAfterEnqueue(
131
+ schedule.id,
132
+ new Date(),
133
+ jobId,
134
+ nextRunAt,
135
+ );
136
+ count++;
55
137
  }
56
- return pool;
138
+
139
+ return count;
57
140
  };
58
141
 
59
142
  // Return the job queue API
60
143
  return {
61
144
  // Job queue operations
62
145
  addJob: withLogContext(
63
- (job: JobOptions<PayloadMap, any>) =>
64
- backend.addJob<PayloadMap, any>(job),
146
+ async (job: JobOptions<PayloadMap, any>, options?: AddJobOptions) => {
147
+ const jobId = await backend.addJob<PayloadMap, any>(job, options);
148
+ emit('job:added', { jobId, jobType: job.jobType });
149
+ return jobId;
150
+ },
151
+ config.verbose ?? false,
152
+ ),
153
+ addJobs: withLogContext(
154
+ async (jobs: JobOptions<PayloadMap, any>[], options?: AddJobOptions) => {
155
+ const jobIds = await backend.addJobs<PayloadMap, any>(jobs, options);
156
+ for (let i = 0; i < jobIds.length; i++) {
157
+ emit('job:added', { jobId: jobIds[i], jobType: jobs[i].jobType });
158
+ }
159
+ return jobIds;
160
+ },
65
161
  config.verbose ?? false,
66
162
  ),
67
163
  getJob: withLogContext(
@@ -93,14 +189,18 @@ export const initJobQueue = <PayloadMap = any>(
93
189
  ) => backend.getJobs<PayloadMap, any>(filters, limit, offset),
94
190
  config.verbose ?? false,
95
191
  ),
96
- retryJob: (jobId: number) => backend.retryJob(jobId),
97
- cleanupOldJobs: (daysToKeep?: number) => backend.cleanupOldJobs(daysToKeep),
98
- cleanupOldJobEvents: (daysToKeep?: number) =>
99
- backend.cleanupOldJobEvents(daysToKeep),
100
- cancelJob: withLogContext(
101
- (jobId: number) => backend.cancelJob(jobId),
102
- config.verbose ?? false,
103
- ),
192
+ retryJob: async (jobId: number) => {
193
+ await backend.retryJob(jobId);
194
+ emit('job:retried', { jobId });
195
+ },
196
+ cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
197
+ backend.cleanupOldJobs(daysToKeep, batchSize),
198
+ cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
199
+ backend.cleanupOldJobEvents(daysToKeep, batchSize),
200
+ cancelJob: withLogContext(async (jobId: number) => {
201
+ await backend.cancelJob(jobId);
202
+ emit('job:cancelled', { jobId });
203
+ }, config.verbose ?? false),
104
204
  editJob: withLogContext(
105
205
  <T extends JobType<PayloadMap>>(
106
206
  jobId: number,
@@ -153,11 +253,24 @@ export const initJobQueue = <PayloadMap = any>(
153
253
  config.verbose ?? false,
154
254
  ),
155
255
 
156
- // Job processing
256
+ // Job processing — automatically enqueues due cron jobs before each batch
157
257
  createProcessor: (
158
258
  handlers: JobHandlers<PayloadMap>,
159
259
  options?: ProcessorOptions,
160
- ) => createProcessor<PayloadMap>(backend, handlers, options),
260
+ ) =>
261
+ createProcessor<PayloadMap>(
262
+ backend,
263
+ handlers,
264
+ options,
265
+ async () => {
266
+ await enqueueDueCronJobsImpl();
267
+ },
268
+ emit,
269
+ ),
270
+
271
+ // Background supervisor — automated maintenance
272
+ createSupervisor: (options?: SupervisorOptions) =>
273
+ createSupervisor(backend, options, emit),
161
274
 
162
275
  // Job events
163
276
  getJobEvents: withLogContext(
@@ -165,34 +278,148 @@ export const initJobQueue = <PayloadMap = any>(
165
278
  config.verbose ?? false,
166
279
  ),
167
280
 
168
- // Wait / Token support (PostgreSQL-only for now)
281
+ // Wait / Token support (works with all backends)
169
282
  createToken: withLogContext(
170
283
  (options?: import('./types.js').CreateTokenOptions) =>
171
- createWaitpoint(requirePool(), null, options),
284
+ backend.createWaitpoint(null, options),
172
285
  config.verbose ?? false,
173
286
  ),
174
287
  completeToken: withLogContext(
175
- (tokenId: string, data?: any) =>
176
- completeWaitpoint(requirePool(), tokenId, data),
288
+ (tokenId: string, data?: any) => backend.completeWaitpoint(tokenId, data),
177
289
  config.verbose ?? false,
178
290
  ),
179
291
  getToken: withLogContext(
180
- (tokenId: string) => getWaitpoint(requirePool(), tokenId),
292
+ (tokenId: string) => backend.getWaitpoint(tokenId),
181
293
  config.verbose ?? false,
182
294
  ),
183
295
  expireTimedOutTokens: withLogContext(
184
- () => expireTimedOutWaitpoints(requirePool()),
296
+ () => backend.expireTimedOutWaitpoints(),
185
297
  config.verbose ?? false,
186
298
  ),
187
299
 
300
+ // Cron schedule operations
301
+ addCronJob: withLogContext(
302
+ <T extends JobType<PayloadMap>>(
303
+ options: CronScheduleOptions<PayloadMap, T>,
304
+ ) => {
305
+ if (!validateCronExpression(options.cronExpression)) {
306
+ return Promise.reject(
307
+ new Error(`Invalid cron expression: "${options.cronExpression}"`),
308
+ );
309
+ }
310
+ const nextRunAt = getNextCronOccurrence(
311
+ options.cronExpression,
312
+ options.timezone ?? 'UTC',
313
+ );
314
+ const input: CronScheduleInput = {
315
+ scheduleName: options.scheduleName,
316
+ cronExpression: options.cronExpression,
317
+ jobType: options.jobType as string,
318
+ payload: options.payload,
319
+ maxAttempts: options.maxAttempts ?? 3,
320
+ priority: options.priority ?? 0,
321
+ timeoutMs: options.timeoutMs ?? null,
322
+ forceKillOnTimeout: options.forceKillOnTimeout ?? false,
323
+ tags: options.tags,
324
+ timezone: options.timezone ?? 'UTC',
325
+ allowOverlap: options.allowOverlap ?? false,
326
+ nextRunAt,
327
+ retryDelay: options.retryDelay ?? null,
328
+ retryBackoff: options.retryBackoff ?? null,
329
+ retryDelayMax: options.retryDelayMax ?? null,
330
+ };
331
+ return backend.addCronSchedule(input);
332
+ },
333
+ config.verbose ?? false,
334
+ ),
335
+ getCronJob: withLogContext(
336
+ (id: number) => backend.getCronSchedule(id),
337
+ config.verbose ?? false,
338
+ ),
339
+ getCronJobByName: withLogContext(
340
+ (name: string) => backend.getCronScheduleByName(name),
341
+ config.verbose ?? false,
342
+ ),
343
+ listCronJobs: withLogContext(
344
+ (status?: CronScheduleStatus) => backend.listCronSchedules(status),
345
+ config.verbose ?? false,
346
+ ),
347
+ removeCronJob: withLogContext(
348
+ (id: number) => backend.removeCronSchedule(id),
349
+ config.verbose ?? false,
350
+ ),
351
+ pauseCronJob: withLogContext(
352
+ (id: number) => backend.pauseCronSchedule(id),
353
+ config.verbose ?? false,
354
+ ),
355
+ resumeCronJob: withLogContext(
356
+ (id: number) => backend.resumeCronSchedule(id),
357
+ config.verbose ?? false,
358
+ ),
359
+ editCronJob: withLogContext(
360
+ async (id: number, updates: EditCronScheduleOptions) => {
361
+ if (
362
+ updates.cronExpression !== undefined &&
363
+ !validateCronExpression(updates.cronExpression)
364
+ ) {
365
+ throw new Error(
366
+ `Invalid cron expression: "${updates.cronExpression}"`,
367
+ );
368
+ }
369
+ let nextRunAt: Date | null | undefined;
370
+ if (
371
+ updates.cronExpression !== undefined ||
372
+ updates.timezone !== undefined
373
+ ) {
374
+ const existing = await backend.getCronSchedule(id);
375
+ const expr = updates.cronExpression ?? existing?.cronExpression ?? '';
376
+ const tz = updates.timezone ?? existing?.timezone ?? 'UTC';
377
+ nextRunAt = getNextCronOccurrence(expr, tz);
378
+ }
379
+ await backend.editCronSchedule(id, updates, nextRunAt);
380
+ },
381
+ config.verbose ?? false,
382
+ ),
383
+ enqueueDueCronJobs: withLogContext(
384
+ () => enqueueDueCronJobsImpl(),
385
+ config.verbose ?? false,
386
+ ),
387
+
388
+ // Event hooks
389
+ on: <K extends QueueEventName>(
390
+ event: K,
391
+ listener: (data: QueueEventMap[K]) => void,
392
+ ) => {
393
+ emitter.on(event, listener as (...args: any[]) => void);
394
+ },
395
+ once: <K extends QueueEventName>(
396
+ event: K,
397
+ listener: (data: QueueEventMap[K]) => void,
398
+ ) => {
399
+ emitter.once(event, listener as (...args: any[]) => void);
400
+ },
401
+ off: <K extends QueueEventName>(
402
+ event: K,
403
+ listener: (data: QueueEventMap[K]) => void,
404
+ ) => {
405
+ emitter.off(event, listener as (...args: any[]) => void);
406
+ },
407
+ removeAllListeners: (event?: QueueEventName) => {
408
+ if (event) {
409
+ emitter.removeAllListeners(event);
410
+ } else {
411
+ emitter.removeAllListeners();
412
+ }
413
+ },
414
+
188
415
  // Advanced access
189
416
  getPool: () => {
190
- if (backendType !== 'postgres') {
417
+ if (!(backend instanceof PostgresBackend)) {
191
418
  throw new Error(
192
419
  'getPool() is only available with the PostgreSQL backend.',
193
420
  );
194
421
  }
195
- return (backend as PostgresBackend).getPool();
422
+ return backend.getPool();
196
423
  },
197
424
  getRedisClient: () => {
198
425
  if (backendType !== 'redis') {
@@ -213,9 +440,10 @@ const withLogContext =
213
440
  };
214
441
 
215
442
  export * from './types.js';
216
- export { QueueBackend } from './backend.js';
443
+ export { QueueBackend, CronScheduleInput } from './backend.js';
217
444
  export { PostgresBackend } from './backends/postgres.js';
218
445
  export {
219
446
  validateHandlerSerializable,
220
447
  testHandlerSerialization,
221
448
  } from './handler-validation.js';
449
+ export { getNextCronOccurrence, validateCronExpression } from './cron.js';