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

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/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.20260224120854",
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
  });