@nicnocquee/dataqueue 1.25.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 (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 +278 -0
  4. package/ai/rules/advanced.md +132 -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 +320 -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 +3157 -1237
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +613 -23
  19. package/dist/index.d.ts +613 -23
  20. package/dist/index.js +3156 -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 +1363 -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 +682 -0
  42. package/src/index.ts +209 -34
  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 +36 -97
  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 +162 -0
  58. package/src/types.ts +388 -12
  59. package/LICENSE +0 -21
package/src/index.ts CHANGED
@@ -1,30 +1,33 @@
1
- import {
2
- createWaitpoint,
3
- completeWaitpoint,
4
- getWaitpoint,
5
- expireTimedOutWaitpoints,
6
- } from './queue.js';
7
1
  import { createProcessor } from './processor.js';
2
+ import { createSupervisor } from './supervisor.js';
8
3
  import {
9
4
  JobQueueConfig,
10
5
  JobQueue,
11
6
  JobOptions,
7
+ AddJobOptions,
12
8
  ProcessorOptions,
9
+ SupervisorOptions,
13
10
  JobHandlers,
14
11
  JobType,
15
12
  PostgresJobQueueConfig,
16
13
  RedisJobQueueConfig,
14
+ CronScheduleOptions,
15
+ CronScheduleStatus,
16
+ EditCronScheduleOptions,
17
17
  } from './types.js';
18
- import { QueueBackend } from './backend.js';
18
+ import { QueueBackend, CronScheduleInput } from './backend.js';
19
19
  import { setLogContext } from './log-context.js';
20
20
  import { createPool } from './db-util.js';
21
21
  import { PostgresBackend } from './backends/postgres.js';
22
22
  import { RedisBackend } from './backends/redis.js';
23
+ import { getNextCronOccurrence, validateCronExpression } from './cron.js';
23
24
 
24
25
  /**
25
26
  * Initialize the job queue system.
26
27
  *
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).
28
31
  */
29
32
  export const initJobQueue = <PayloadMap = any>(
30
33
  config: JobQueueConfig,
@@ -33,35 +36,111 @@ export const initJobQueue = <PayloadMap = any>(
33
36
  setLogContext(config.verbose ?? false);
34
37
 
35
38
  let backend: QueueBackend;
36
- let pool: import('pg').Pool | undefined;
37
39
 
38
40
  if (backendType === 'postgres') {
39
41
  const pgConfig = config as PostgresJobQueueConfig;
40
- pool = createPool(pgConfig.databaseConfig);
41
- backend = new PostgresBackend(pool);
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
+ }
42
52
  } 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);
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
+ }
46
66
  } else {
47
67
  throw new Error(`Unknown backend: ${backendType}`);
48
68
  }
49
69
 
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.',
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,
54
120
  );
121
+ await backend.updateCronScheduleAfterEnqueue(
122
+ schedule.id,
123
+ new Date(),
124
+ jobId,
125
+ nextRunAt,
126
+ );
127
+ count++;
55
128
  }
56
- return pool;
129
+
130
+ return count;
57
131
  };
58
132
 
59
133
  // Return the job queue API
60
134
  return {
61
135
  // Job queue operations
62
136
  addJob: withLogContext(
63
- (job: JobOptions<PayloadMap, any>) =>
64
- backend.addJob<PayloadMap, any>(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),
65
144
  config.verbose ?? false,
66
145
  ),
67
146
  getJob: withLogContext(
@@ -94,9 +173,10 @@ export const initJobQueue = <PayloadMap = any>(
94
173
  config.verbose ?? false,
95
174
  ),
96
175
  retryJob: (jobId: number) => backend.retryJob(jobId),
97
- cleanupOldJobs: (daysToKeep?: number) => backend.cleanupOldJobs(daysToKeep),
98
- cleanupOldJobEvents: (daysToKeep?: number) =>
99
- backend.cleanupOldJobEvents(daysToKeep),
176
+ cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
177
+ backend.cleanupOldJobs(daysToKeep, batchSize),
178
+ cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
179
+ backend.cleanupOldJobEvents(daysToKeep, batchSize),
100
180
  cancelJob: withLogContext(
101
181
  (jobId: number) => backend.cancelJob(jobId),
102
182
  config.verbose ?? false,
@@ -153,11 +233,18 @@ export const initJobQueue = <PayloadMap = any>(
153
233
  config.verbose ?? false,
154
234
  ),
155
235
 
156
- // Job processing
236
+ // Job processing — automatically enqueues due cron jobs before each batch
157
237
  createProcessor: (
158
238
  handlers: JobHandlers<PayloadMap>,
159
239
  options?: ProcessorOptions,
160
- ) => createProcessor<PayloadMap>(backend, handlers, options),
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),
161
248
 
162
249
  // Job events
163
250
  getJobEvents: withLogContext(
@@ -165,34 +252,121 @@ export const initJobQueue = <PayloadMap = any>(
165
252
  config.verbose ?? false,
166
253
  ),
167
254
 
168
- // Wait / Token support (PostgreSQL-only for now)
255
+ // Wait / Token support (works with all backends)
169
256
  createToken: withLogContext(
170
257
  (options?: import('./types.js').CreateTokenOptions) =>
171
- createWaitpoint(requirePool(), null, options),
258
+ backend.createWaitpoint(null, options),
172
259
  config.verbose ?? false,
173
260
  ),
174
261
  completeToken: withLogContext(
175
- (tokenId: string, data?: any) =>
176
- completeWaitpoint(requirePool(), tokenId, data),
262
+ (tokenId: string, data?: any) => backend.completeWaitpoint(tokenId, data),
177
263
  config.verbose ?? false,
178
264
  ),
179
265
  getToken: withLogContext(
180
- (tokenId: string) => getWaitpoint(requirePool(), tokenId),
266
+ (tokenId: string) => backend.getWaitpoint(tokenId),
181
267
  config.verbose ?? false,
182
268
  ),
183
269
  expireTimedOutTokens: withLogContext(
184
- () => expireTimedOutWaitpoints(requirePool()),
270
+ () => backend.expireTimedOutWaitpoints(),
271
+ config.verbose ?? false,
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(),
185
359
  config.verbose ?? false,
186
360
  ),
187
361
 
188
362
  // Advanced access
189
363
  getPool: () => {
190
- if (backendType !== 'postgres') {
364
+ if (!(backend instanceof PostgresBackend)) {
191
365
  throw new Error(
192
366
  'getPool() is only available with the PostgreSQL backend.',
193
367
  );
194
368
  }
195
- return (backend as PostgresBackend).getPool();
369
+ return backend.getPool();
196
370
  },
197
371
  getRedisClient: () => {
198
372
  if (backendType !== 'redis') {
@@ -213,9 +387,10 @@ const withLogContext =
213
387
  };
214
388
 
215
389
  export * from './types.js';
216
- export { QueueBackend } from './backend.js';
390
+ export { QueueBackend, CronScheduleInput } from './backend.js';
217
391
  export { PostgresBackend } from './backends/postgres.js';
218
392
  export {
219
393
  validateHandlerSerializable,
220
394
  testHandlerSerialization,
221
395
  } from './handler-validation.js';
396
+ export { getNextCronOccurrence, validateCronExpression } from './cron.js';