@nicnocquee/dataqueue 1.35.0-beta.20260224112317 → 1.35.0-beta.20260224123025

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.
@@ -116,8 +116,8 @@
116
116
  {
117
117
  "slug": "intro/comparison",
118
118
  "title": "Comparison",
119
- "description": "How DataQueue compares to BullMQ and Trigger.dev",
120
- "content": "Choosing a job queue depends on your stack, infrastructure preferences, and the features you need. Here is a side-by-side comparison of **DataQueue**, **BullMQ**, and **Trigger.dev**.\n\n| Feature | DataQueue | BullMQ | Trigger.dev |\n| ----------------------- | ------------------------------------------------------- | ------------------------------------------- | --------------------------------------- |\n| **Backend** | PostgreSQL or Redis | Redis only | Cloud or self-hosted (Postgres + Redis) |\n| **Type Safety** | Full generic `PayloadMap` | Basic types | Full TypeScript tasks |\n| **Scheduling** | `runAt`, Cron | Cron, delayed, recurring | Cron, delayed |\n| **Retries** | Exponential backoff, configurable `maxAttempts` | Exponential backoff, custom strategies, DLQ | Auto retries, bulk replay, DLQ |\n| **Priority** | Integer priority | Priority levels | Queue-based priority |\n| **Concurrency Control** | `batchSize` + `concurrency` + global `groupConcurrency` | Built-in | Per-task + shared limits |\n| **Rate Limiting** | - | Yes | Via concurrency limits |\n| **Job Flows / DAGs** | - | Parent-child flows | Workflows |\n| **Dashboard** | Built-in Next.js package | Third-party (Bull Board, etc.) | Built-in web dashboard |\n| **Wait / Pause Jobs** | `waitFor`, `waitUntil`, token system | - | Durable execution |\n| **Human-in-the-Loop** | Token system | - | Yes |\n| **Progress Tracking** | Yes (0-100%) | Yes | Yes (realtime) |\n| **Serverless-First** | Yes | No (needs long-running process) | Yes (cloud) |\n| **Self-Hosted** | Yes | Yes (your Redis) | Yes (containers) |\n| **Cloud Option** | - | - | Yes |\n| **License** | MIT | MIT | Apache-2.0 |\n| **Pricing** | Free (OSS) | Free (OSS) | Free tier + paid plans |\n| **Infrastructure** | Your own Postgres or Redis | Your own Redis | Their cloud or your infra |\n\n## Where DataQueue shines\n\n- **Serverless-first** — designed from the ground up for Vercel, AWS Lambda, and other serverless platforms. No long-running process required.\n- **Use your existing database** — back your queue with PostgreSQL or Redis. No additional infrastructure to provision or pay for.\n- **Wait and token system** — pause jobs with `waitFor`, `waitUntil`, or token-based waits for human-in-the-loop workflows, all within a single handler function.\n- **Type-safe PayloadMap** — a generic `PayloadMap` gives you compile-time validation of every job type and its payload, catching bugs before they reach production.\n- **Built-in Next.js dashboard** — add a full admin UI to your Next.js app with a single route file. No separate service to deploy."
119
+ "description": "How DataQueue compares to pg-boss, BullMQ, and Trigger.dev",
120
+ "content": "Choosing a job queue depends on your stack, infrastructure preferences, and the features you need. Here is a side-by-side comparison of **DataQueue**, **pg-boss**, **BullMQ**, and **Trigger.dev**.\n\n| Feature | DataQueue | pg-boss | BullMQ | Trigger.dev |\n| ----------------------- | ------------------------------------------------------- | ------------------------ | ------------------------------------------- | --------------------------------------- |\n| **Backend** | PostgreSQL or Redis | PostgreSQL only | Redis only | Cloud or self-hosted (Postgres + Redis) |\n| **Type Safety** | Full generic `PayloadMap` | TypeScript support | Basic types | Full TypeScript tasks |\n| **Scheduling** | `runAt`, Cron | Cron, delayed jobs | Cron, delayed, recurring | Cron, delayed |\n| **Retries** | Exponential backoff, configurable `maxAttempts` | Retries with backoff | Exponential backoff, custom strategies, DLQ | Auto retries, bulk replay, DLQ |\n| **Priority** | Integer priority | Priority support | Priority levels | Queue-based priority |\n| **Concurrency Control** | `batchSize` + `concurrency` + global `groupConcurrency` | Workers + queue policies | Built-in | Per-task + shared limits |\n| **Rate Limiting** | - | - | Yes | Via concurrency limits |\n| **Job Flows / DAGs** | - | - | Parent-child flows | Workflows |\n| **Dashboard** | Built-in Next.js package | - | Third-party (Bull Board, etc.) | Built-in web dashboard |\n| **Wait / Pause Jobs** | `waitFor`, `waitUntil`, token system | Delayed/scheduled jobs | - | Durable execution |\n| **Human-in-the-Loop** | Token system | - | - | Yes |\n| **Progress Tracking** | Yes (0-100%) | - | Yes | Yes (realtime) |\n| **Serverless-First** | Yes | Yes | No (needs long-running process) | Yes (cloud) |\n| **Self-Hosted** | Yes | Yes | Yes (your Redis) | Yes (containers) |\n| **Cloud Option** | - | - | - | Yes |\n| **License** | MIT | MIT | MIT | Apache-2.0 |\n| **Pricing** | Free (OSS) | Free (OSS) | Free (OSS) | Free tier + paid plans |\n| **Infrastructure** | Your own Postgres or Redis | Your own Postgres | Your own Redis | Their cloud or your infra |\n\n## Where DataQueue shines\n\n- **Serverless-first** — designed from the ground up for Vercel, AWS Lambda, and other serverless platforms. No long-running process required.\n- **Use your existing database** — back your queue with PostgreSQL or Redis. No additional infrastructure to provision or pay for.\n- **Wait and token system** — pause jobs with `waitFor`, `waitUntil`, or token-based waits for human-in-the-loop workflows, all within a single handler function.\n- **Type-safe PayloadMap** — a generic `PayloadMap` gives you compile-time validation of every job type and its payload, catching bugs before they reach production.\n- **Built-in Next.js dashboard** — add a full admin UI to your Next.js app with a single route file. No separate service to deploy."
121
121
  },
122
122
  {
123
123
  "slug": "intro",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nicnocquee/dataqueue",
3
- "version": "1.35.0-beta.20260224112317",
3
+ "version": "1.35.0-beta.20260224123025",
4
4
  "description": "PostgreSQL or Redis-backed job queue for Node.js applications with support for serverless environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,7 @@
23
23
  "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test",
24
24
  "lint": "tsc",
25
25
  "test": "vitest run --reporter=verbose",
26
+ "test:coverage": "vitest run --reporter=verbose --coverage",
26
27
  "format": "prettier --write .",
27
28
  "check-format": "prettier --check .",
28
29
  "check-exports": "attw --pack .",
@@ -56,6 +57,7 @@
56
57
  "@arethetypeswrong/cli": "^0.18.2",
57
58
  "@types/node": "^24.0.4",
58
59
  "@types/pg": "^8.15.4",
60
+ "@vitest/coverage-v8": "^3.2.4",
59
61
  "ioredis": "^5.9.3",
60
62
  "node-pg-migrate": "^8.0.3",
61
63
  "pnpm": "^9.0.0",
@@ -544,6 +544,71 @@ describe('Redis backend integration', () => {
544
544
  expect(job?.status).toBe('pending');
545
545
  });
546
546
 
547
+ it('reclaims an in-flight job via supervisor and allows reprocessing', async () => {
548
+ let firstAttempt = true;
549
+ const handler = vi.fn(async () => {
550
+ if (firstAttempt) {
551
+ firstAttempt = false;
552
+ await new Promise<void>(() => {});
553
+ }
554
+ });
555
+
556
+ const jobId = await jobQueue.addJob({
557
+ jobType: 'test',
558
+ payload: { foo: 'redis-reclaim-live-loop' },
559
+ });
560
+
561
+ const firstProcessor = jobQueue.createProcessor(
562
+ {
563
+ email: vi.fn(async () => {}),
564
+ sms: vi.fn(async () => {}),
565
+ test: handler,
566
+ },
567
+ { pollInterval: 25, batchSize: 1, concurrency: 1 },
568
+ );
569
+ firstProcessor.startInBackground();
570
+
571
+ let processingJob = await jobQueue.getJob(jobId);
572
+ for (let i = 0; i < 50; i++) {
573
+ if (processingJob?.status === 'processing') {
574
+ break;
575
+ }
576
+ await new Promise((resolve) => setTimeout(resolve, 20));
577
+ processingJob = await jobQueue.getJob(jobId);
578
+ }
579
+ expect(processingJob?.status).toBe('processing');
580
+
581
+ await firstProcessor.stopAndDrain(25);
582
+
583
+ const supervisor = jobQueue.createSupervisor({
584
+ stuckJobsTimeoutMinutes: 0,
585
+ cleanupJobsDaysToKeep: 0,
586
+ cleanupEventsDaysToKeep: 0,
587
+ expireTimedOutTokens: false,
588
+ });
589
+ const maintenance = await supervisor.start();
590
+ expect(maintenance.reclaimedJobs).toBe(1);
591
+
592
+ const reclaimedJob = await jobQueue.getJob(jobId);
593
+ expect(reclaimedJob?.status).toBe('pending');
594
+ expect(reclaimedJob?.lockedAt).toBeNull();
595
+ expect(reclaimedJob?.lockedBy).toBeNull();
596
+
597
+ const secondProcessor = jobQueue.createProcessor(
598
+ {
599
+ email: vi.fn(async () => {}),
600
+ sms: vi.fn(async () => {}),
601
+ test: handler,
602
+ },
603
+ { batchSize: 1, concurrency: 1 },
604
+ );
605
+ await secondProcessor.start();
606
+
607
+ const completedJob = await jobQueue.getJob(jobId);
608
+ expect(completedJob?.status).toBe('completed');
609
+ expect(handler).toHaveBeenCalledTimes(2);
610
+ });
611
+
547
612
  it('getPool should throw for Redis backend', () => {
548
613
  expect(() => jobQueue.getPool()).toThrow(
549
614
  'getPool() is only available with the PostgreSQL backend',
package/src/index.test.ts CHANGED
@@ -120,6 +120,71 @@ describe('index integration', () => {
120
120
  expect(job).toBeNull();
121
121
  });
122
122
 
123
+ it('reclaims an in-flight job via supervisor and allows reprocessing', async () => {
124
+ let firstAttempt = true;
125
+ const handler = vi.fn(async () => {
126
+ if (firstAttempt) {
127
+ firstAttempt = false;
128
+ await new Promise<void>(() => {});
129
+ }
130
+ });
131
+
132
+ const jobId = await jobQueue.addJob({
133
+ jobType: 'test',
134
+ payload: { foo: 'reclaim-live-loop' },
135
+ });
136
+
137
+ const firstProcessor = jobQueue.createProcessor(
138
+ {
139
+ email: vi.fn(async () => {}),
140
+ sms: vi.fn(async () => {}),
141
+ test: handler,
142
+ },
143
+ { pollInterval: 25, batchSize: 1, concurrency: 1 },
144
+ );
145
+ firstProcessor.startInBackground();
146
+
147
+ let processingJob = await jobQueue.getJob(jobId);
148
+ for (let i = 0; i < 50; i++) {
149
+ if (processingJob?.status === 'processing') {
150
+ break;
151
+ }
152
+ await new Promise((resolve) => setTimeout(resolve, 20));
153
+ processingJob = await jobQueue.getJob(jobId);
154
+ }
155
+ expect(processingJob?.status).toBe('processing');
156
+
157
+ await firstProcessor.stopAndDrain(25);
158
+
159
+ const supervisor = jobQueue.createSupervisor({
160
+ stuckJobsTimeoutMinutes: 0,
161
+ cleanupJobsDaysToKeep: 0,
162
+ cleanupEventsDaysToKeep: 0,
163
+ expireTimedOutTokens: false,
164
+ });
165
+ const maintenance = await supervisor.start();
166
+ expect(maintenance.reclaimedJobs).toBe(1);
167
+
168
+ const reclaimedJob = await jobQueue.getJob(jobId);
169
+ expect(reclaimedJob?.status).toBe('pending');
170
+ expect(reclaimedJob?.lockedAt).toBeNull();
171
+ expect(reclaimedJob?.lockedBy).toBeNull();
172
+
173
+ const secondProcessor = jobQueue.createProcessor(
174
+ {
175
+ email: vi.fn(async () => {}),
176
+ sms: vi.fn(async () => {}),
177
+ test: handler,
178
+ },
179
+ { batchSize: 1, concurrency: 1 },
180
+ );
181
+ await secondProcessor.start();
182
+
183
+ const completedJob = await jobQueue.getJob(jobId);
184
+ expect(completedJob?.status).toBe('completed');
185
+ expect(handler).toHaveBeenCalledTimes(2);
186
+ });
187
+
123
188
  it('getPool should return the underlying pool', () => {
124
189
  expect(jobQueue.getPool()).toBeInstanceOf(Pool);
125
190
  });