@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
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DataQueue MCP Server — exposes documentation search over stdio.
5
+ * Run via: dataqueue-cli mcp
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { z } from 'zod';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ interface DocPage {
19
+ slug: string;
20
+ title: string;
21
+ description: string;
22
+ content: string;
23
+ }
24
+
25
+ /** @internal Loads docs-content.json from the ai/ directory bundled with the package. */
26
+ export function loadDocsContent(
27
+ docsPath: string = path.join(__dirname, '../ai/docs-content.json'),
28
+ ): DocPage[] {
29
+ const raw = fs.readFileSync(docsPath, 'utf-8');
30
+ return JSON.parse(raw) as DocPage[];
31
+ }
32
+
33
+ /** @internal Scores a doc page against a search query using simple term matching. */
34
+ export function scorePageForQuery(page: DocPage, queryTerms: string[]): number {
35
+ const titleLower = page.title.toLowerCase();
36
+ const descLower = page.description.toLowerCase();
37
+ const contentLower = page.content.toLowerCase();
38
+
39
+ let score = 0;
40
+ for (const term of queryTerms) {
41
+ if (titleLower.includes(term)) score += 10;
42
+ if (descLower.includes(term)) score += 5;
43
+
44
+ const contentMatches = contentLower.split(term).length - 1;
45
+ score += Math.min(contentMatches, 10);
46
+ }
47
+ return score;
48
+ }
49
+
50
+ /** @internal Extracts a relevant excerpt around the first match of any query term. */
51
+ export function extractExcerpt(
52
+ content: string,
53
+ queryTerms: string[],
54
+ maxLength = 500,
55
+ ): string {
56
+ const lower = content.toLowerCase();
57
+ let earliestIndex = -1;
58
+
59
+ for (const term of queryTerms) {
60
+ const idx = lower.indexOf(term);
61
+ if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
62
+ earliestIndex = idx;
63
+ }
64
+ }
65
+
66
+ if (earliestIndex === -1) {
67
+ return content.slice(0, maxLength);
68
+ }
69
+
70
+ const start = Math.max(0, earliestIndex - 100);
71
+ const end = Math.min(content.length, start + maxLength);
72
+ let excerpt = content.slice(start, end);
73
+
74
+ if (start > 0) excerpt = '...' + excerpt;
75
+ if (end < content.length) excerpt = excerpt + '...';
76
+
77
+ return excerpt;
78
+ }
79
+
80
+ /**
81
+ * Creates and starts the DataQueue MCP server over stdio.
82
+ *
83
+ * @param deps - Injectable dependencies for testing.
84
+ */
85
+ export async function startMcpServer(
86
+ deps: {
87
+ docsPath?: string;
88
+ transport?: InstanceType<typeof StdioServerTransport>;
89
+ } = {},
90
+ ): Promise<McpServer> {
91
+ const pages = loadDocsContent(deps.docsPath);
92
+
93
+ const server = new McpServer({
94
+ name: 'dataqueue-docs',
95
+ version: '1.0.0',
96
+ });
97
+
98
+ server.resource('llms-txt', 'dataqueue://llms.txt', async () => {
99
+ const llmsPath = path.join(
100
+ __dirname,
101
+ '../ai/skills/dataqueue-core/SKILL.md',
102
+ );
103
+ let content: string;
104
+ try {
105
+ content = fs.readFileSync(llmsPath, 'utf-8');
106
+ } catch {
107
+ content = pages
108
+ .map((p) => `## ${p.title}\n\nSlug: ${p.slug}\n\n${p.description}`)
109
+ .join('\n\n');
110
+ }
111
+ return { contents: [{ uri: 'dataqueue://llms.txt', text: content }] };
112
+ });
113
+
114
+ server.tool(
115
+ 'list-doc-pages',
116
+ 'List all available DataQueue documentation pages with titles and descriptions.',
117
+ {},
118
+ async () => {
119
+ const listing = pages.map((p) => ({
120
+ slug: p.slug,
121
+ title: p.title,
122
+ description: p.description,
123
+ }));
124
+ return {
125
+ content: [
126
+ { type: 'text' as const, text: JSON.stringify(listing, null, 2) },
127
+ ],
128
+ };
129
+ },
130
+ );
131
+
132
+ server.tool(
133
+ 'get-doc-page',
134
+ 'Fetch a specific DataQueue doc page by slug. Returns full page content as markdown.',
135
+ {
136
+ slug: z
137
+ .string()
138
+ .describe('The doc page slug, e.g. "usage/add-job" or "api/job-queue"'),
139
+ },
140
+ async ({ slug }) => {
141
+ const page = pages.find((p) => p.slug === slug);
142
+ if (!page) {
143
+ return {
144
+ content: [
145
+ {
146
+ type: 'text' as const,
147
+ text: `Page not found: "${slug}". Use list-doc-pages to see available slugs.`,
148
+ },
149
+ ],
150
+ isError: true,
151
+ };
152
+ }
153
+ const header = page.description
154
+ ? `# ${page.title}\n\n> ${page.description}\n\n`
155
+ : `# ${page.title}\n\n`;
156
+ return {
157
+ content: [{ type: 'text' as const, text: header + page.content }],
158
+ };
159
+ },
160
+ );
161
+
162
+ server.tool(
163
+ 'search-docs',
164
+ 'Full-text search across all DataQueue documentation pages. Returns matching sections with page titles and content excerpts.',
165
+ {
166
+ query: z
167
+ .string()
168
+ .describe('Search query, e.g. "cron scheduling" or "waitForToken"'),
169
+ },
170
+ async ({ query }) => {
171
+ const queryTerms = query
172
+ .toLowerCase()
173
+ .split(/\s+/)
174
+ .filter((t) => t.length > 1);
175
+
176
+ if (queryTerms.length === 0) {
177
+ return {
178
+ content: [
179
+ { type: 'text' as const, text: 'Please provide a search query.' },
180
+ ],
181
+ isError: true,
182
+ };
183
+ }
184
+
185
+ const scored = pages
186
+ .map((page) => ({
187
+ page,
188
+ score: scorePageForQuery(page, queryTerms),
189
+ }))
190
+ .filter((r) => r.score > 0)
191
+ .sort((a, b) => b.score - a.score)
192
+ .slice(0, 5);
193
+
194
+ if (scored.length === 0) {
195
+ return {
196
+ content: [
197
+ {
198
+ type: 'text' as const,
199
+ text: `No results for "${query}". Try different keywords or use list-doc-pages to browse.`,
200
+ },
201
+ ],
202
+ };
203
+ }
204
+
205
+ const results = scored.map((r) => {
206
+ const excerpt = extractExcerpt(r.page.content, queryTerms);
207
+ return `## ${r.page.title} (${r.page.slug})\n\n${r.page.description}\n\n${excerpt}`;
208
+ });
209
+
210
+ return {
211
+ content: [{ type: 'text' as const, text: results.join('\n\n---\n\n') }],
212
+ };
213
+ },
214
+ );
215
+
216
+ const transport = deps.transport ?? new StdioServerTransport();
217
+ await server.connect(transport);
218
+ return server;
219
+ }
220
+
221
+ const isDirectRun =
222
+ process.argv[1] &&
223
+ (process.argv[1].endsWith('/mcp-server.js') ||
224
+ process.argv[1].endsWith('/mcp-server.cjs'));
225
+
226
+ if (isDirectRun) {
227
+ startMcpServer().catch((err) => {
228
+ console.error('Failed to start MCP server:', err);
229
+ process.exit(1);
230
+ });
231
+ }
package/src/processor.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Worker } from 'worker_threads';
2
- import { Pool } from 'pg';
3
2
  import {
4
3
  JobRecord,
5
4
  ProcessorOptions,
@@ -15,69 +14,8 @@ import {
15
14
  WaitTokenResult,
16
15
  } from './types.js';
17
16
  import { QueueBackend } from './backend.js';
18
- import { PostgresBackend } from './backends/postgres.js';
19
- import {
20
- waitJob,
21
- updateStepData,
22
- createWaitpoint,
23
- getWaitpoint,
24
- } from './queue.js';
25
17
  import { log, setLogContext } from './log-context.js';
26
18
 
27
- /**
28
- * Try to extract the underlying pg Pool from a QueueBackend.
29
- * Returns null for non-PostgreSQL backends.
30
- */
31
- function tryExtractPool(backend: QueueBackend): Pool | null {
32
- if (backend instanceof PostgresBackend) {
33
- return backend.getPool();
34
- }
35
- return null;
36
- }
37
-
38
- /**
39
- * Build a JobContext without wait support (for non-PostgreSQL backends).
40
- * prolong/onTimeout work normally; wait-related methods throw helpful errors.
41
- */
42
- function buildBasicContext(
43
- backend: QueueBackend,
44
- jobId: number,
45
- baseCtx: {
46
- prolong: JobContext['prolong'];
47
- onTimeout: JobContext['onTimeout'];
48
- },
49
- ): JobContext {
50
- const waitError = () =>
51
- new Error(
52
- 'Wait features (waitFor, waitUntil, createToken, waitForToken, ctx.run) are currently only supported with the PostgreSQL backend.',
53
- );
54
- return {
55
- prolong: baseCtx.prolong,
56
- onTimeout: baseCtx.onTimeout,
57
- run: async <T>(_stepName: string, fn: () => Promise<T>): Promise<T> => {
58
- // Without PostgreSQL, just execute the function directly (no persistence)
59
- return fn();
60
- },
61
- waitFor: async () => {
62
- throw waitError();
63
- },
64
- waitUntil: async () => {
65
- throw waitError();
66
- },
67
- createToken: async () => {
68
- throw waitError();
69
- },
70
- waitForToken: async () => {
71
- throw waitError();
72
- },
73
- setProgress: async (percent: number) => {
74
- if (percent < 0 || percent > 100)
75
- throw new Error('Progress must be between 0 and 100');
76
- await backend.updateProgress(jobId, Math.round(percent));
77
- },
78
- };
79
- }
80
-
81
19
  /**
82
20
  * Validates that a handler can be serialized for worker thread execution.
83
21
  * Throws an error with helpful message if serialization fails.
@@ -388,7 +326,7 @@ function createNoOpContext(
388
326
  * Marks pending waits as completed and fetches token outputs.
389
327
  */
390
328
  async function resolveCompletedWaits(
391
- pool: Pool,
329
+ backend: QueueBackend,
392
330
  stepData: Record<string, any>,
393
331
  ): Promise<void> {
394
332
  for (const key of Object.keys(stepData)) {
@@ -401,7 +339,7 @@ async function resolveCompletedWaits(
401
339
  stepData[key] = { ...entry, completed: true };
402
340
  } else if (entry.type === 'token' && entry.tokenId) {
403
341
  // Token-based wait -- fetch the waitpoint result
404
- const wp = await getWaitpoint(pool, entry.tokenId);
342
+ const wp = await backend.getWaitpoint(entry.tokenId);
405
343
  if (wp && wp.status === 'completed') {
406
344
  stepData[key] = {
407
345
  ...entry,
@@ -422,10 +360,10 @@ async function resolveCompletedWaits(
422
360
 
423
361
  /**
424
362
  * Build the extended JobContext with step tracking and wait support.
363
+ * Works with any QueueBackend (Postgres or Redis).
425
364
  */
426
365
  function buildWaitContext(
427
366
  backend: QueueBackend,
428
- pool: Pool,
429
367
  jobId: number,
430
368
  stepData: Record<string, any>,
431
369
  baseCtx: {
@@ -455,7 +393,7 @@ function buildWaitContext(
455
393
 
456
394
  // Persist step result
457
395
  stepData[stepName] = { __completed: true, result };
458
- await updateStepData(pool, jobId, stepData);
396
+ await backend.updateStepData(jobId, stepData);
459
397
 
460
398
  return result;
461
399
  },
@@ -498,7 +436,7 @@ function buildWaitContext(
498
436
  },
499
437
 
500
438
  createToken: async (options?) => {
501
- const token = await createWaitpoint(pool, jobId, options);
439
+ const token = await backend.createWaitpoint(jobId, options);
502
440
  return token;
503
441
  },
504
442
 
@@ -517,7 +455,7 @@ function buildWaitContext(
517
455
  }
518
456
 
519
457
  // Check if the token is already completed (e.g., completed while job was still processing)
520
- const wp = await getWaitpoint(pool, tokenId);
458
+ const wp = await backend.getWaitpoint(tokenId);
521
459
  if (wp && wp.status === 'completed') {
522
460
  const result: WaitTokenResult<T> = {
523
461
  ok: true,
@@ -529,7 +467,7 @@ function buildWaitContext(
529
467
  completed: true,
530
468
  result,
531
469
  };
532
- await updateStepData(pool, jobId, stepData);
470
+ await backend.updateStepData(jobId, stepData);
533
471
  return result;
534
472
  }
535
473
  if (wp && wp.status === 'timed_out') {
@@ -543,7 +481,7 @@ function buildWaitContext(
543
481
  completed: true,
544
482
  result,
545
483
  };
546
- await updateStepData(pool, jobId, stepData);
484
+ await backend.updateStepData(jobId, stepData);
547
485
  return result;
548
486
  }
549
487
 
@@ -591,17 +529,14 @@ export async function processJobWithHandlers<
591
529
  // Load step data (may contain completed steps from previous invocations)
592
530
  const stepData: Record<string, any> = { ...(job.stepData || {}) };
593
531
 
594
- // Try to get pool for wait features (PostgreSQL-only)
595
- const pool = tryExtractPool(backend);
596
-
597
532
  // If resuming from a wait, resolve any pending wait entries
598
533
  const hasStepHistory = Object.keys(stepData).some((k) =>
599
534
  k.startsWith('__wait_'),
600
535
  );
601
- if (hasStepHistory && pool) {
602
- await resolveCompletedWaits(pool, stepData);
536
+ if (hasStepHistory) {
537
+ await resolveCompletedWaits(backend, stepData);
603
538
  // Persist the resolved step data
604
- await updateStepData(pool, job.id, stepData);
539
+ await backend.updateStepData(job.id, stepData);
605
540
  }
606
541
 
607
542
  // Per-job timeout logic
@@ -685,10 +620,8 @@ export async function processJobWithHandlers<
685
620
  },
686
621
  };
687
622
 
688
- // Build context: full wait support for PostgreSQL, basic for others
689
- const ctx = pool
690
- ? buildWaitContext(backend, pool, job.id, stepData, baseCtx)
691
- : buildBasicContext(backend, job.id, baseCtx);
623
+ // Build context: full wait support for all backends
624
+ const ctx = buildWaitContext(backend, job.id, stepData, baseCtx);
692
625
 
693
626
  // If forceKillOnTimeout was set but timeoutMs was missing, warn
694
627
  if (forceKillOnTimeout && !hasTimeout) {
@@ -720,22 +653,10 @@ export async function processJobWithHandlers<
720
653
 
721
654
  // Check if this is a WaitSignal (not a real error)
722
655
  if (error instanceof WaitSignal) {
723
- if (!pool) {
724
- // Wait signals should never happen with non-PostgreSQL backends
725
- // since the context methods throw, but guard just in case
726
- await backend.failJob(
727
- job.id,
728
- new Error(
729
- 'WaitSignal received but wait features require the PostgreSQL backend.',
730
- ),
731
- FailureReason.HandlerError,
732
- );
733
- return;
734
- }
735
656
  log(
736
657
  `Job ${job.id} entering wait: type=${error.type}, waitUntil=${error.waitUntil?.toISOString() ?? 'none'}, tokenId=${error.tokenId ?? 'none'}`,
737
658
  );
738
- await waitJob(pool, job.id, {
659
+ await backend.waitJob(job.id, {
739
660
  waitUntil: error.waitUntil,
740
661
  waitTokenId: error.tokenId,
741
662
  stepData: error.stepData,
@@ -818,16 +739,18 @@ export async function processBatchWithHandlers<PayloadMap>(
818
739
  }
819
740
 
820
741
  /**
821
- * Start a job processor that continuously processes jobs
822
- * @param backend - The queue backend
823
- * @param handlers - The job handlers for this processor instance
742
+ * Start a job processor that continuously processes jobs.
743
+ * @param backend - The queue backend.
744
+ * @param handlers - The job handlers for this processor instance.
824
745
  * @param options - The processor options. Leave pollInterval empty to run only once. Use jobType to filter jobs by type.
825
- * @returns {Processor} The processor instance
746
+ * @param onBeforeBatch - Optional callback invoked before each batch. Used internally to enqueue due cron jobs.
747
+ * @returns {Processor} The processor instance.
826
748
  */
827
749
  export const createProcessor = <PayloadMap = any>(
828
750
  backend: QueueBackend,
829
751
  handlers: JobHandlers<PayloadMap>,
830
752
  options: ProcessorOptions = {},
753
+ onBeforeBatch?: () => Promise<void>,
831
754
  ): Processor => {
832
755
  const {
833
756
  workerId = `worker-${Math.random().toString(36).substring(2, 9)}`,
@@ -847,6 +770,22 @@ export const createProcessor = <PayloadMap = any>(
847
770
  const processJobs = async (): Promise<number> => {
848
771
  if (!running) return 0;
849
772
 
773
+ // Run pre-batch hook (e.g. enqueue due cron jobs) before processing
774
+ if (onBeforeBatch) {
775
+ try {
776
+ await onBeforeBatch();
777
+ } catch (hookError) {
778
+ log(`onBeforeBatch hook error: ${hookError}`);
779
+ if (onError) {
780
+ onError(
781
+ hookError instanceof Error
782
+ ? hookError
783
+ : new Error(String(hookError)),
784
+ );
785
+ }
786
+ }
787
+ }
788
+
850
789
  log(
851
790
  `Processing jobs with workerId: ${workerId}${jobType ? ` and jobType: ${Array.isArray(jobType) ? jobType.join(',') : jobType}` : ''}`,
852
791
  );