@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
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: dataqueue-core
3
+ description: Core patterns for using @nicnocquee/dataqueue — typed PayloadMap, init, handlers, adding and processing jobs.
4
+ ---
5
+
6
+ # DataQueue Core Patterns
7
+
8
+ ## Imports
9
+
10
+ Always import from `@nicnocquee/dataqueue`. There is no v2/v3 subpath.
11
+
12
+ ```typescript
13
+ import { initJobQueue, JobHandlers } from '@nicnocquee/dataqueue';
14
+ ```
15
+
16
+ ## Step 1: Define a PayloadMap
17
+
18
+ Define an object type mapping job type strings to their payload shapes. This is the foundation of type safety — every API method is generic over this map.
19
+
20
+ ```typescript
21
+ export type JobPayloadMap = {
22
+ send_email: { to: string; subject: string; body: string };
23
+ generate_report: { reportId: string; userId: string };
24
+ };
25
+ ```
26
+
27
+ ## Step 2: Define Handlers
28
+
29
+ Create a `JobHandlers<PayloadMap>` object. TypeScript enforces that every key in the PayloadMap has a handler. Each handler receives `(payload, signal, ctx)`.
30
+
31
+ ```typescript
32
+ import { JobHandlers } from '@nicnocquee/dataqueue';
33
+ import type { JobPayloadMap } from './types';
34
+
35
+ export const jobHandlers: JobHandlers<JobPayloadMap> = {
36
+ send_email: async (payload) => {
37
+ await sendEmail(payload.to, payload.subject, payload.body);
38
+ },
39
+ generate_report: async (payload, signal) => {
40
+ if (signal.aborted) return;
41
+ await generateReport(payload.reportId, payload.userId);
42
+ },
43
+ };
44
+ ```
45
+
46
+ ## Step 3: Initialize the Queue (Singleton)
47
+
48
+ Use a module-level singleton. Each `initJobQueue` call creates a new database pool — never call it per-request.
49
+
50
+ ### PostgreSQL
51
+
52
+ ```typescript
53
+ import { initJobQueue } from '@nicnocquee/dataqueue';
54
+ import type { JobPayloadMap } from './types';
55
+
56
+ let jobQueue: ReturnType<typeof initJobQueue<JobPayloadMap>> | null = null;
57
+
58
+ export const getJobQueue = () => {
59
+ if (!jobQueue) {
60
+ jobQueue = initJobQueue<JobPayloadMap>({
61
+ databaseConfig: {
62
+ connectionString: process.env.PG_DATAQUEUE_DATABASE,
63
+ },
64
+ });
65
+ }
66
+ return jobQueue;
67
+ };
68
+ ```
69
+
70
+ ### Redis
71
+
72
+ ```typescript
73
+ jobQueue = initJobQueue<JobPayloadMap>({
74
+ backend: 'redis',
75
+ redisConfig: {
76
+ url: process.env.REDIS_URL,
77
+ keyPrefix: 'myapp:',
78
+ },
79
+ });
80
+ ```
81
+
82
+ ### Bring Your Own Pool / Client
83
+
84
+ You can pass an existing `pg.Pool` or `ioredis` client instead of connection config:
85
+
86
+ ```typescript
87
+ import { Pool } from 'pg';
88
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
89
+
90
+ jobQueue = initJobQueue<JobPayloadMap>({ pool });
91
+ ```
92
+
93
+ ```typescript
94
+ import IORedis from 'ioredis';
95
+ const redis = new IORedis(process.env.REDIS_URL);
96
+
97
+ jobQueue = initJobQueue<JobPayloadMap>({
98
+ backend: 'redis',
99
+ client: redis,
100
+ keyPrefix: 'myapp:',
101
+ });
102
+ ```
103
+
104
+ When you provide your own pool/client, the library will **not** close it on shutdown — you manage its lifecycle.
105
+
106
+ ## Step 4: Add Jobs
107
+
108
+ ```typescript
109
+ const jobId = await queue.addJob({
110
+ jobType: 'send_email',
111
+ payload: { to: 'user@example.com', subject: 'Hi', body: 'Hello' },
112
+ priority: 10,
113
+ runAt: new Date(Date.now() + 5000),
114
+ tags: ['welcome'],
115
+ idempotencyKey: 'welcome-user-123',
116
+ });
117
+ ```
118
+
119
+ ### Batch Insert
120
+
121
+ Use `addJobs` to insert many jobs in a single database round-trip. Returns IDs in the same order as the input array.
122
+
123
+ ```typescript
124
+ const jobIds = await queue.addJobs([
125
+ {
126
+ jobType: 'send_email',
127
+ payload: { to: 'a@example.com', subject: 'Hi', body: '...' },
128
+ },
129
+ {
130
+ jobType: 'send_email',
131
+ payload: { to: 'b@example.com', subject: 'Hi', body: '...' },
132
+ priority: 10,
133
+ },
134
+ {
135
+ jobType: 'generate_report',
136
+ payload: { reportId: '1', userId: '2' },
137
+ tags: ['monthly'],
138
+ },
139
+ ]);
140
+ ```
141
+
142
+ Each job can independently have its own `idempotencyKey`, `priority`, `runAt`, `tags`, etc. The `{ db }` transactional option is also supported (PostgreSQL only).
143
+
144
+ ### Transactional Job Creation (PostgreSQL only)
145
+
146
+ Pass an external `pg.PoolClient` inside a transaction via `{ db: client }`:
147
+
148
+ ```typescript
149
+ const client = await pool.connect();
150
+ await client.query('BEGIN');
151
+ await client.query('INSERT INTO users (email) VALUES ($1)', [email]);
152
+ await queue.addJob(
153
+ {
154
+ jobType: 'send_email',
155
+ payload: { to: email, subject: 'Welcome!', body: '...' },
156
+ },
157
+ { db: client },
158
+ );
159
+ await client.query('COMMIT');
160
+ client.release();
161
+ ```
162
+
163
+ If the transaction rolls back, the job is never enqueued.
164
+
165
+ ### Retry configuration
166
+
167
+ Control retry behavior per-job with `retryDelay`, `retryBackoff`, and `retryDelayMax`:
168
+
169
+ ```typescript
170
+ await queue.addJob({
171
+ jobType: 'send_email',
172
+ payload: { to: 'user@example.com', subject: 'Hi', body: 'Hello' },
173
+ maxAttempts: 5,
174
+ retryDelay: 10, // base delay: 10 seconds
175
+ retryBackoff: true, // exponential backoff (default)
176
+ retryDelayMax: 300, // cap at 5 minutes
177
+ });
178
+ ```
179
+
180
+ - **Fixed delay**: set `retryBackoff: false` for constant delay between retries.
181
+ - **Exponential backoff** (default): delay doubles each attempt with jitter.
182
+ - **Default**: when no retry options are set, legacy `2^attempts * 60s` is used.
183
+
184
+ ## Step 5: Process Jobs
185
+
186
+ ### Serverless (one-shot)
187
+
188
+ ```typescript
189
+ const processor = queue.createProcessor(handlers, {
190
+ batchSize: 10,
191
+ concurrency: 3,
192
+ });
193
+ const processed = await processor.start();
194
+ ```
195
+
196
+ ### Long-running server
197
+
198
+ ```typescript
199
+ const processor = queue.createProcessor(handlers, {
200
+ batchSize: 10,
201
+ concurrency: 3,
202
+ pollInterval: 5000,
203
+ });
204
+ processor.startInBackground();
205
+
206
+ // Automate maintenance (reclaim stuck jobs, cleanup old data, expire tokens)
207
+ const supervisor = queue.createSupervisor({
208
+ intervalMs: 60_000,
209
+ stuckJobsTimeoutMinutes: 10,
210
+ cleanupJobsDaysToKeep: 30,
211
+ cleanupEventsDaysToKeep: 30,
212
+ });
213
+ supervisor.startInBackground();
214
+
215
+ process.on('SIGTERM', async () => {
216
+ await Promise.all([
217
+ processor.stopAndDrain(30000),
218
+ supervisor.stopAndDrain(30000),
219
+ ]);
220
+ queue.getPool().end();
221
+ process.exit(0);
222
+ });
223
+ ```
224
+
225
+ ## Common Mistakes
226
+
227
+ 1. **Creating a new queue per request** — always use a singleton. Each `initJobQueue` creates a DB pool.
228
+ 2. **Missing handler for a job type** — the job fails with `FailureReason.NoHandler`. Let TypeScript enforce completeness by typing handlers as `JobHandlers<PayloadMap>`.
229
+ 3. **Not checking `signal.aborted`** — timed-out jobs keep running in the background. Always check the signal in long-running handlers.
230
+ 4. **Skipping maintenance** — use `createSupervisor()` to automate reclaiming stuck jobs, cleaning up old data, and expiring tokens. Without it, crashed workers leave jobs stuck in `processing` and tables grow unbounded.
231
+ 5. **Forgetting to run migrations** — PostgreSQL requires `dataqueue-cli migrate` before use. Redis needs no migrations.
232
+ 6. **Not calling `stopAndDrain` on shutdown** — use `stopAndDrain()` (not `stop()`) for graceful shutdown to avoid stuck jobs.
233
+ 7. **Forgetting to commit/rollback when using `db` option** — the `addJob` INSERT sits in an open transaction. If you never `COMMIT` or `ROLLBACK`, the connection leaks and the job is invisible to other sessions.
234
+ 8. **Using `db` option with Redis** — transactional job creation is PostgreSQL only. The Redis backend throws if `db` is provided.
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: dataqueue-react
3
+ description: React SDK and Dashboard patterns for DataQueue — useJob hook, DataqueueProvider, admin dashboard.
4
+ ---
5
+
6
+ # DataQueue React & Dashboard Patterns
7
+
8
+ ## React SDK — @nicnocquee/dataqueue-react
9
+
10
+ ### Installation
11
+
12
+ ```bash
13
+ npm install @nicnocquee/dataqueue-react
14
+ ```
15
+
16
+ Requires React 18+.
17
+
18
+ ### useJob Hook
19
+
20
+ Poll a job's status and progress from the browser.
21
+
22
+ ```tsx
23
+ 'use client';
24
+ import { useJob } from '@nicnocquee/dataqueue-react';
25
+
26
+ function JobTracker({ jobId }: { jobId: number }) {
27
+ const { status, progress, data, isLoading, error } = useJob(jobId, {
28
+ fetcher: (id) =>
29
+ fetch(`/api/jobs/${id}`)
30
+ .then((r) => r.json())
31
+ .then((d) => d.job),
32
+ pollingInterval: 1000,
33
+ onComplete: (job) => toast.success('Done!'),
34
+ onFailed: (job) => toast.error('Failed'),
35
+ onStatusChange: (newStatus, oldStatus) => {
36
+ console.log(`${oldStatus} → ${newStatus}`);
37
+ },
38
+ });
39
+
40
+ if (isLoading) return <p>Loading...</p>;
41
+ if (error) return <p>Error: {error.message}</p>;
42
+
43
+ return (
44
+ <div>
45
+ <p>Status: {status}</p>
46
+ <progress value={progress ?? 0} max={100} />
47
+ </div>
48
+ );
49
+ }
50
+ ```
51
+
52
+ Polling stops automatically on terminal statuses (`completed`, `failed`, `cancelled`).
53
+
54
+ ### DataqueueProvider
55
+
56
+ Avoid repeating `fetcher` and `pollingInterval` by wrapping your app in a provider.
57
+
58
+ ```tsx
59
+ 'use client';
60
+ import { DataqueueProvider } from '@nicnocquee/dataqueue-react';
61
+
62
+ const fetcher = (id: number) =>
63
+ fetch(`/api/jobs/${id}`)
64
+ .then((r) => r.json())
65
+ .then((d) => d.job);
66
+
67
+ export function Providers({ children }: { children: React.ReactNode }) {
68
+ return (
69
+ <DataqueueProvider fetcher={fetcher} pollingInterval={2000}>
70
+ {children}
71
+ </DataqueueProvider>
72
+ );
73
+ }
74
+ ```
75
+
76
+ Then use `useJob` without config:
77
+
78
+ ```tsx
79
+ const { status, progress } = useJob(jobId);
80
+ ```
81
+
82
+ ### API Route for Job Fetching (Next.js)
83
+
84
+ ```typescript
85
+ // app/api/jobs/[id]/route.ts
86
+ import { getJobQueue } from '@/lib/queue';
87
+ import { NextResponse } from 'next/server';
88
+
89
+ export async function GET(
90
+ _request: Request,
91
+ { params }: { params: Promise<{ id: string }> },
92
+ ) {
93
+ const { id } = await params;
94
+ const jobQueue = getJobQueue();
95
+ const job = await jobQueue.getJob(Number(id));
96
+ if (!job) {
97
+ return NextResponse.json({ error: 'Job not found' }, { status: 404 });
98
+ }
99
+ return NextResponse.json({ job });
100
+ }
101
+ ```
102
+
103
+ ### useJob Return Value
104
+
105
+ | Field | Type | Description |
106
+ | ----------- | ------------------- | ------------------------------- |
107
+ | `data` | `JobData \| null` | Latest job data from fetcher |
108
+ | `status` | `JobStatus \| null` | Current job status |
109
+ | `progress` | `number \| null` | Progress percentage (0–100) |
110
+ | `isLoading` | `boolean` | True until first fetch resolves |
111
+ | `error` | `Error \| null` | Last fetch error |
112
+
113
+ ## Dashboard — @nicnocquee/dataqueue-dashboard
114
+
115
+ ### Installation
116
+
117
+ ```bash
118
+ npm install @nicnocquee/dataqueue-dashboard
119
+ ```
120
+
121
+ ### Setup (Next.js App Router)
122
+
123
+ Create a single catch-all route:
124
+
125
+ ```typescript
126
+ // app/admin/dataqueue/[[...path]]/route.ts
127
+ import { createDataqueueDashboard } from '@nicnocquee/dataqueue-dashboard/next';
128
+ import { getJobQueue, jobHandlers } from '@/lib/queue';
129
+
130
+ const { GET, POST } = createDataqueueDashboard({
131
+ jobQueue: getJobQueue(),
132
+ jobHandlers,
133
+ basePath: '/admin/dataqueue',
134
+ });
135
+
136
+ export { GET, POST };
137
+ ```
138
+
139
+ Visit `/admin/dataqueue` to open the dashboard.
140
+
141
+ The `basePath` must match the route file directory. For example, `app/jobs/dashboard/[[...path]]/route.ts` requires `basePath: '/jobs/dashboard'`.
142
+
143
+ ### Dashboard Features
144
+
145
+ - Jobs list with status filter tabs, pagination, auto-refresh
146
+ - Job detail view with payload, error history, step data, events timeline
147
+ - Inline actions: cancel pending/waiting jobs, retry failed/cancelled jobs
148
+ - Process Jobs button for one-shot processing (useful in dev)
149
+
150
+ ### Protecting the Dashboard
151
+
152
+ Wrap the handlers with your auth logic:
153
+
154
+ ```typescript
155
+ const dashboard = createDataqueueDashboard({
156
+ jobQueue: getJobQueue(),
157
+ jobHandlers,
158
+ basePath: '/admin/dataqueue',
159
+ });
160
+
161
+ export async function GET(req: Request, ctx: any) {
162
+ const session = await auth();
163
+ if (!session?.user?.isAdmin) {
164
+ return new Response('Unauthorized', { status: 401 });
165
+ }
166
+ return dashboard.GET(req, ctx);
167
+ }
168
+
169
+ export async function POST(req: Request, ctx: any) {
170
+ const session = await auth();
171
+ if (!session?.user?.isAdmin) {
172
+ return new Response('Unauthorized', { status: 401 });
173
+ }
174
+ return dashboard.POST(req, ctx);
175
+ }
176
+ ```
177
+
178
+ ### Progress Tracking from Handlers
179
+
180
+ Report progress via `ctx.setProgress(percent)` (0–100). The value persists to the database and is exposed via `getJob()` and the `useJob` hook's `progress` field.
181
+
182
+ ```typescript
183
+ const handler = async (payload, signal, ctx) => {
184
+ for (let i = 0; i < chunks.length; i++) {
185
+ await processChunk(chunks[i]);
186
+ await ctx.setProgress(Math.round(((i + 1) / chunks.length) * 100));
187
+ }
188
+ };
189
+ ```