@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.
- package/ai/build-docs-content.ts +96 -0
- package/ai/build-llms-full.ts +42 -0
- package/ai/docs-content.json +284 -0
- package/ai/rules/advanced.md +150 -0
- package/ai/rules/basic.md +159 -0
- package/ai/rules/react-dashboard.md +83 -0
- package/ai/skills/dataqueue-advanced/SKILL.md +370 -0
- package/ai/skills/dataqueue-core/SKILL.md +234 -0
- package/ai/skills/dataqueue-react/SKILL.md +189 -0
- package/dist/cli.cjs +1149 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +66 -1
- package/dist/cli.d.ts +66 -1
- package/dist/cli.js +1146 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +3236 -1237
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +697 -23
- package/dist/index.d.ts +697 -23
- package/dist/index.js +3235 -1238
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +186 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +32 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +175 -0
- package/dist/mcp-server.js.map +1 -0
- package/migrations/1781200000004_create_cron_schedules_table.sql +33 -0
- package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
- package/package.json +24 -21
- package/src/backend.ts +170 -5
- package/src/backends/postgres.ts +992 -63
- package/src/backends/redis-scripts.ts +358 -26
- package/src/backends/redis.test.ts +1532 -0
- package/src/backends/redis.ts +993 -35
- package/src/cli.test.ts +82 -6
- package/src/cli.ts +73 -10
- package/src/cron.test.ts +126 -0
- package/src/cron.ts +40 -0
- package/src/db-util.ts +1 -1
- package/src/index.test.ts +1034 -11
- package/src/index.ts +267 -39
- package/src/init-command.test.ts +449 -0
- package/src/init-command.ts +709 -0
- package/src/install-mcp-command.test.ts +216 -0
- package/src/install-mcp-command.ts +185 -0
- package/src/install-rules-command.test.ts +218 -0
- package/src/install-rules-command.ts +233 -0
- package/src/install-skills-command.test.ts +176 -0
- package/src/install-skills-command.ts +124 -0
- package/src/mcp-server.test.ts +162 -0
- package/src/mcp-server.ts +231 -0
- package/src/processor.ts +104 -113
- package/src/queue.test.ts +465 -0
- package/src/queue.ts +34 -252
- package/src/supervisor.test.ts +340 -0
- package/src/supervisor.ts +177 -0
- package/src/types.ts +476 -12
- 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
|
-
|
|
41
|
-
|
|
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 =
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
) =>
|
|
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 (
|
|
281
|
+
// Wait / Token support (works with all backends)
|
|
169
282
|
createToken: withLogContext(
|
|
170
283
|
(options?: import('./types.js').CreateTokenOptions) =>
|
|
171
|
-
createWaitpoint(
|
|
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(
|
|
292
|
+
(tokenId: string) => backend.getWaitpoint(tokenId),
|
|
181
293
|
config.verbose ?? false,
|
|
182
294
|
),
|
|
183
295
|
expireTimedOutTokens: withLogContext(
|
|
184
|
-
() => expireTimedOutWaitpoints(
|
|
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 (
|
|
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
|
|
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';
|