@semiont/jobs 0.2.30 → 0.2.31
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/README.md +197 -106
- package/dist/index.d.ts +197 -101
- package/dist/index.js +72 -42
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @semiont/jobs
|
|
2
2
|
|
|
3
3
|
[](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml?query=branch%3Amain+is%3Asuccess+job%3A%22Test+jobs%22)
|
|
4
|
+
[](https://codecov.io/gh/The-AI-Alliance/semiont?flag=jobs)
|
|
4
5
|
[](https://www.npmjs.com/package/@semiont/jobs)
|
|
6
|
+
[](https://www.npmjs.com/package/@semiont/jobs)
|
|
5
7
|
[](https://github.com/The-AI-Alliance/semiont/blob/main/LICENSE)
|
|
6
8
|
|
|
7
9
|
Filesystem-based job queue and worker infrastructure for [Semiont](https://github.com/The-AI-Alliance/semiont) - provides async job processing, background workers, and long-running task management.
|
|
@@ -35,27 +37,36 @@ import {
|
|
|
35
37
|
initializeJobQueue,
|
|
36
38
|
getJobQueue,
|
|
37
39
|
JobWorker,
|
|
38
|
-
type
|
|
40
|
+
type PendingJob,
|
|
41
|
+
type RunningJob,
|
|
42
|
+
type GenerationParams,
|
|
43
|
+
type AnyJob,
|
|
39
44
|
} from '@semiont/jobs';
|
|
40
45
|
import { jobId } from '@semiont/api-client';
|
|
41
|
-
import { userId, resourceId } from '@semiont/core';
|
|
46
|
+
import { userId, resourceId, annotationId } from '@semiont/core';
|
|
42
47
|
|
|
43
48
|
// 1. Initialize job queue
|
|
44
49
|
await initializeJobQueue({ dataDir: './data' });
|
|
45
50
|
|
|
46
51
|
// 2. Create a job
|
|
47
52
|
const jobQueue = getJobQueue();
|
|
48
|
-
const job:
|
|
49
|
-
id: jobId('job-abc123'),
|
|
50
|
-
type: 'generation',
|
|
53
|
+
const job: PendingJob<GenerationParams> = {
|
|
51
54
|
status: 'pending',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
metadata: {
|
|
56
|
+
id: jobId('job-abc123'),
|
|
57
|
+
type: 'generation',
|
|
58
|
+
userId: userId('user@example.com'),
|
|
59
|
+
created: new Date().toISOString(),
|
|
60
|
+
retryCount: 0,
|
|
61
|
+
maxRetries: 3,
|
|
62
|
+
},
|
|
63
|
+
params: {
|
|
64
|
+
referenceId: annotationId('ref-123'),
|
|
65
|
+
sourceResourceId: resourceId('doc-456'),
|
|
66
|
+
title: 'Generated Article',
|
|
67
|
+
prompt: 'Write about AI',
|
|
68
|
+
language: 'en-US',
|
|
69
|
+
},
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
await jobQueue.createJob(job);
|
|
@@ -66,13 +77,18 @@ class MyGenerationWorker extends JobWorker {
|
|
|
66
77
|
return 'MyGenerationWorker';
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
protected canProcessJob(job:
|
|
70
|
-
return job.type === 'generation';
|
|
80
|
+
protected canProcessJob(job: AnyJob): boolean {
|
|
81
|
+
return job.metadata.type === 'generation';
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
protected async executeJob(job:
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
protected async executeJob(job: AnyJob): Promise<void> {
|
|
85
|
+
// Type guard ensures job is running
|
|
86
|
+
if (job.status !== 'running') {
|
|
87
|
+
throw new Error('Job must be running');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const genJob = job as RunningJob<GenerationParams>;
|
|
91
|
+
console.log(`Generating resource: ${genJob.params.title}`);
|
|
76
92
|
// Your processing logic here
|
|
77
93
|
}
|
|
78
94
|
}
|
|
@@ -112,61 +128,75 @@ data/
|
|
|
112
128
|
|
|
113
129
|
### Jobs
|
|
114
130
|
|
|
115
|
-
Jobs
|
|
131
|
+
Jobs use discriminated unions based on their status, ensuring type safety and preventing invalid state access:
|
|
116
132
|
|
|
117
133
|
```typescript
|
|
118
|
-
import type {
|
|
134
|
+
import type { PendingJob, RunningJob, CompleteJob, GenerationParams, GenerationProgress, GenerationResult } from '@semiont/jobs';
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
type: 'generation',
|
|
136
|
+
// Pending job - waiting to be processed
|
|
137
|
+
const pendingJob: PendingJob<GenerationParams> = {
|
|
123
138
|
status: 'pending',
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
metadata: {
|
|
140
|
+
id: jobId('job-123'),
|
|
141
|
+
type: 'generation',
|
|
142
|
+
userId: userId('user@example.com'),
|
|
143
|
+
created: '2024-01-01T00:00:00Z',
|
|
144
|
+
retryCount: 0,
|
|
145
|
+
maxRetries: 3,
|
|
146
|
+
},
|
|
147
|
+
params: {
|
|
148
|
+
referenceId: annotationId('ref-456'),
|
|
149
|
+
sourceResourceId: resourceId('doc-789'),
|
|
150
|
+
title: 'AI Generated Article',
|
|
151
|
+
prompt: 'Write about quantum computing',
|
|
152
|
+
language: 'en-US',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Running job - currently being processed
|
|
157
|
+
const runningJob: RunningJob<GenerationParams, GenerationProgress> = {
|
|
158
|
+
status: 'running',
|
|
159
|
+
metadata: { /* same as above */ },
|
|
160
|
+
params: { /* same as above */ },
|
|
161
|
+
startedAt: '2024-01-01T00:01:00Z',
|
|
144
162
|
progress: {
|
|
145
163
|
stage: 'generating',
|
|
146
164
|
percentage: 45,
|
|
147
165
|
message: 'Generating content...',
|
|
148
166
|
},
|
|
167
|
+
};
|
|
149
168
|
|
|
150
|
-
|
|
169
|
+
// Complete job - successfully finished
|
|
170
|
+
const completeJob: CompleteJob<GenerationParams, GenerationResult> = {
|
|
171
|
+
status: 'complete',
|
|
172
|
+
metadata: { /* same as above */ },
|
|
173
|
+
params: { /* same as above */ },
|
|
174
|
+
startedAt: '2024-01-01T00:01:00Z',
|
|
175
|
+
completedAt: '2024-01-01T00:05:00Z',
|
|
151
176
|
result: {
|
|
152
177
|
resourceId: resourceId('doc-new'),
|
|
153
178
|
resourceName: 'Generated Article',
|
|
154
179
|
},
|
|
155
180
|
};
|
|
181
|
+
|
|
182
|
+
// TypeScript prevents accessing progress on pending jobs!
|
|
183
|
+
// pendingJob.progress // ❌ Compile error
|
|
184
|
+
// runningJob.progress // ✅ Available
|
|
185
|
+
// completeJob.result // ✅ Available
|
|
156
186
|
```
|
|
157
187
|
|
|
158
188
|
### Job Types
|
|
159
189
|
|
|
160
|
-
The package supports multiple job types for different tasks:
|
|
190
|
+
The package supports multiple job types for different tasks, each with their own parameter types:
|
|
161
191
|
|
|
162
192
|
```typescript
|
|
163
193
|
import type {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
DetectionParams, // Entity detection in resources
|
|
195
|
+
GenerationParams, // AI content generation
|
|
196
|
+
HighlightDetectionParams, // Identify key passages
|
|
197
|
+
AssessmentDetectionParams, // Generate evaluative comments
|
|
198
|
+
CommentDetectionParams, // Generate explanatory comments
|
|
199
|
+
TagDetectionParams, // Structural role detection
|
|
170
200
|
} from '@semiont/jobs';
|
|
171
201
|
```
|
|
172
202
|
|
|
@@ -188,7 +218,7 @@ type JobStatus =
|
|
|
188
218
|
Workers poll the queue and process jobs:
|
|
189
219
|
|
|
190
220
|
```typescript
|
|
191
|
-
import { JobWorker, type
|
|
221
|
+
import { JobWorker, type AnyJob, type RunningJob, type CustomParams } from '@semiont/jobs';
|
|
192
222
|
|
|
193
223
|
class CustomWorker extends JobWorker {
|
|
194
224
|
// Worker identification
|
|
@@ -197,20 +227,30 @@ class CustomWorker extends JobWorker {
|
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
// Filter which jobs this worker processes
|
|
200
|
-
protected canProcessJob(job:
|
|
201
|
-
return job.type === 'custom-type';
|
|
230
|
+
protected canProcessJob(job: AnyJob): boolean {
|
|
231
|
+
return job.metadata.type === 'custom-type';
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
// Implement job processing logic
|
|
205
|
-
protected async executeJob(job:
|
|
206
|
-
// 1.
|
|
207
|
-
|
|
235
|
+
protected async executeJob(job: AnyJob): Promise<void> {
|
|
236
|
+
// 1. Type guard - job must be running
|
|
237
|
+
if (job.status !== 'running') {
|
|
238
|
+
throw new Error('Job must be running');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Access typed job data
|
|
242
|
+
const customJob = job as RunningJob<CustomParams>;
|
|
243
|
+
const params = customJob.params;
|
|
208
244
|
|
|
209
|
-
//
|
|
210
|
-
const result = await doWork(
|
|
245
|
+
// 3. Perform async work
|
|
246
|
+
const result = await doWork(params);
|
|
211
247
|
|
|
212
|
-
//
|
|
213
|
-
|
|
248
|
+
// 4. Create updated job with result (immutable pattern)
|
|
249
|
+
const updatedJob: RunningJob<CustomParams> = {
|
|
250
|
+
...customJob,
|
|
251
|
+
progress: { stage: 'complete', percentage: 100 },
|
|
252
|
+
};
|
|
253
|
+
await this.updateJobProgress(updatedJob);
|
|
214
254
|
}
|
|
215
255
|
}
|
|
216
256
|
```
|
|
@@ -337,22 +377,28 @@ data/
|
|
|
337
377
|
job-ghi789.json
|
|
338
378
|
```
|
|
339
379
|
|
|
340
|
-
Each job file contains the complete job object:
|
|
380
|
+
Each job file contains the complete job object using the discriminated union structure:
|
|
341
381
|
|
|
342
382
|
```json
|
|
343
383
|
{
|
|
344
|
-
"id": "job-abc123",
|
|
345
|
-
"type": "generation",
|
|
346
384
|
"status": "complete",
|
|
347
|
-
"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
385
|
+
"metadata": {
|
|
386
|
+
"id": "job-abc123",
|
|
387
|
+
"type": "generation",
|
|
388
|
+
"userId": "user@example.com",
|
|
389
|
+
"created": "2024-01-01T00:00:00Z",
|
|
390
|
+
"retryCount": 0,
|
|
391
|
+
"maxRetries": 3
|
|
392
|
+
},
|
|
393
|
+
"params": {
|
|
394
|
+
"referenceId": "ref-456",
|
|
395
|
+
"sourceResourceId": "doc-789",
|
|
396
|
+
"title": "Generated Article",
|
|
397
|
+
"prompt": "Write about AI",
|
|
398
|
+
"language": "en-US"
|
|
399
|
+
},
|
|
352
400
|
"startedAt": "2024-01-01T00:01:00Z",
|
|
353
401
|
"completedAt": "2024-01-01T00:05:00Z",
|
|
354
|
-
"retryCount": 0,
|
|
355
|
-
"maxRetries": 3,
|
|
356
402
|
"result": {
|
|
357
403
|
"resourceId": "doc-new",
|
|
358
404
|
"resourceName": "Generated Article"
|
|
@@ -379,7 +425,11 @@ Each job file contains the complete job object:
|
|
|
379
425
|
|
|
380
426
|
```typescript
|
|
381
427
|
class ResilientWorker extends JobWorker {
|
|
382
|
-
protected async executeJob(job:
|
|
428
|
+
protected async executeJob(job: AnyJob): Promise<void> {
|
|
429
|
+
if (job.status !== 'running') {
|
|
430
|
+
throw new Error('Job must be running');
|
|
431
|
+
}
|
|
432
|
+
|
|
383
433
|
try {
|
|
384
434
|
await doWork(job);
|
|
385
435
|
} catch (error) {
|
|
@@ -400,11 +450,17 @@ const queue = getJobQueue();
|
|
|
400
450
|
const failedJobs = await queue.queryJobs({ status: 'failed' });
|
|
401
451
|
|
|
402
452
|
for (const job of failedJobs) {
|
|
403
|
-
if (job.retryCount < job.maxRetries) {
|
|
404
|
-
job
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
453
|
+
if (job.status === 'failed' && job.metadata.retryCount < job.metadata.maxRetries) {
|
|
454
|
+
// Create new pending job from failed job
|
|
455
|
+
const retryJob: PendingJob<any> = {
|
|
456
|
+
status: 'pending',
|
|
457
|
+
metadata: {
|
|
458
|
+
...job.metadata,
|
|
459
|
+
retryCount: job.metadata.retryCount + 1,
|
|
460
|
+
},
|
|
461
|
+
params: job.params,
|
|
462
|
+
};
|
|
463
|
+
await queue.updateJob(retryJob, 'failed');
|
|
408
464
|
}
|
|
409
465
|
}
|
|
410
466
|
```
|
|
@@ -413,6 +469,7 @@ for (const job of failedJobs) {
|
|
|
413
469
|
|
|
414
470
|
```typescript
|
|
415
471
|
import { initializeJobQueue, getJobQueue } from '@semiont/jobs';
|
|
472
|
+
import type { PendingJob, GenerationParams } from '@semiont/jobs';
|
|
416
473
|
import { describe, it, beforeEach } from 'vitest';
|
|
417
474
|
|
|
418
475
|
describe('Job queue', () => {
|
|
@@ -423,11 +480,23 @@ describe('Job queue', () => {
|
|
|
423
480
|
it('should create and retrieve jobs', async () => {
|
|
424
481
|
const queue = getJobQueue();
|
|
425
482
|
|
|
426
|
-
const job:
|
|
427
|
-
id: jobId('test-1'),
|
|
428
|
-
type: 'generation',
|
|
483
|
+
const job: PendingJob<GenerationParams> = {
|
|
429
484
|
status: 'pending',
|
|
430
|
-
|
|
485
|
+
metadata: {
|
|
486
|
+
id: jobId('test-1'),
|
|
487
|
+
type: 'generation',
|
|
488
|
+
userId: userId('user@test.com'),
|
|
489
|
+
created: new Date().toISOString(),
|
|
490
|
+
retryCount: 0,
|
|
491
|
+
maxRetries: 3,
|
|
492
|
+
},
|
|
493
|
+
params: {
|
|
494
|
+
referenceId: annotationId('ref-1'),
|
|
495
|
+
sourceResourceId: resourceId('doc-1'),
|
|
496
|
+
title: 'Test',
|
|
497
|
+
prompt: 'Test prompt',
|
|
498
|
+
language: 'en-US',
|
|
499
|
+
},
|
|
431
500
|
};
|
|
432
501
|
|
|
433
502
|
await queue.createJob(job);
|
|
@@ -443,7 +512,7 @@ describe('Job queue', () => {
|
|
|
443
512
|
### Building a Background Worker
|
|
444
513
|
|
|
445
514
|
```typescript
|
|
446
|
-
import { JobWorker, type
|
|
515
|
+
import { JobWorker, type AnyJob, type RunningJob, type GenerationParams, type GenerationProgress } from '@semiont/jobs';
|
|
447
516
|
import { InferenceService } from './inference';
|
|
448
517
|
|
|
449
518
|
class GenerationWorker extends JobWorker {
|
|
@@ -458,44 +527,55 @@ class GenerationWorker extends JobWorker {
|
|
|
458
527
|
return 'GenerationWorker';
|
|
459
528
|
}
|
|
460
529
|
|
|
461
|
-
protected canProcessJob(job:
|
|
462
|
-
return job.type === 'generation';
|
|
530
|
+
protected canProcessJob(job: AnyJob): boolean {
|
|
531
|
+
return job.metadata.type === 'generation';
|
|
463
532
|
}
|
|
464
533
|
|
|
465
|
-
protected async executeJob(job:
|
|
466
|
-
|
|
534
|
+
protected async executeJob(job: AnyJob): Promise<void> {
|
|
535
|
+
// Type guard
|
|
536
|
+
if (job.status !== 'running') {
|
|
537
|
+
throw new Error('Job must be running');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const genJob = job as RunningJob<GenerationParams, GenerationProgress>;
|
|
467
541
|
|
|
468
|
-
// Report progress
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
542
|
+
// Report progress (create new object - immutable pattern)
|
|
543
|
+
const updatedJob1: RunningJob<GenerationParams, GenerationProgress> = {
|
|
544
|
+
...genJob,
|
|
545
|
+
progress: {
|
|
546
|
+
stage: 'generating',
|
|
547
|
+
percentage: 0,
|
|
548
|
+
message: 'Starting generation...',
|
|
549
|
+
},
|
|
473
550
|
};
|
|
474
|
-
await getJobQueue().updateJob(
|
|
551
|
+
await getJobQueue().updateJob(updatedJob1);
|
|
475
552
|
|
|
476
553
|
// Generate content
|
|
477
554
|
const content = await this.inference.generate({
|
|
478
|
-
prompt: genJob.prompt,
|
|
479
|
-
context: genJob.context,
|
|
480
|
-
temperature: genJob.temperature,
|
|
481
|
-
maxTokens: genJob.maxTokens,
|
|
555
|
+
prompt: genJob.params.prompt,
|
|
556
|
+
context: genJob.params.context,
|
|
557
|
+
temperature: genJob.params.temperature,
|
|
558
|
+
maxTokens: genJob.params.maxTokens,
|
|
482
559
|
});
|
|
483
560
|
|
|
484
561
|
// Update progress
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
562
|
+
const updatedJob2: RunningJob<GenerationParams, GenerationProgress> = {
|
|
563
|
+
...updatedJob1,
|
|
564
|
+
progress: {
|
|
565
|
+
stage: 'creating',
|
|
566
|
+
percentage: 75,
|
|
567
|
+
message: 'Creating resource...',
|
|
568
|
+
},
|
|
489
569
|
};
|
|
490
|
-
await getJobQueue().updateJob(
|
|
570
|
+
await getJobQueue().updateJob(updatedJob2);
|
|
491
571
|
|
|
492
572
|
// Create resource (simplified)
|
|
493
|
-
const resourceId = await createResource(content, genJob.title);
|
|
573
|
+
const resourceId = await createResource(content, genJob.params.title);
|
|
494
574
|
|
|
495
|
-
// Set result
|
|
496
|
-
|
|
575
|
+
// Set result (will be handled by base class transition to complete)
|
|
576
|
+
return {
|
|
497
577
|
resourceId,
|
|
498
|
-
resourceName: genJob.title,
|
|
578
|
+
resourceName: genJob.params.title,
|
|
499
579
|
};
|
|
500
580
|
}
|
|
501
581
|
}
|
|
@@ -519,10 +599,21 @@ async function monitorJob(jobId: JobId): Promise<void> {
|
|
|
519
599
|
|
|
520
600
|
console.log(`Status: ${job.status}`);
|
|
521
601
|
|
|
522
|
-
|
|
602
|
+
// Type-safe progress access - only available on running jobs
|
|
603
|
+
if (job.status === 'running') {
|
|
523
604
|
console.log(`Progress: ${job.progress.percentage}%`);
|
|
524
605
|
console.log(`Stage: ${job.progress.stage}`);
|
|
525
|
-
console.log(`Message: ${job.progress.message}`);
|
|
606
|
+
console.log(`Message: ${job.progress.message || 'Processing...'}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Type-safe result access - only available on complete jobs
|
|
610
|
+
if (job.status === 'complete') {
|
|
611
|
+
console.log(`Result: ${JSON.stringify(job.result)}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Type-safe error access - only available on failed jobs
|
|
615
|
+
if (job.status === 'failed') {
|
|
616
|
+
console.log(`Error: ${job.error}`);
|
|
526
617
|
}
|
|
527
618
|
|
|
528
619
|
if (job.status === 'complete' || job.status === 'failed') {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,55 +2,43 @@ import { JobId, EntityType, GenerationContext } from '@semiont/api-client';
|
|
|
2
2
|
import { UserId, ResourceId, AnnotationId } from '@semiont/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Job Queue Type Definitions
|
|
5
|
+
* Job Queue Type Definitions - Discriminated Union Design
|
|
6
6
|
*
|
|
7
7
|
* Jobs represent async work that can be queued, processed, and monitored.
|
|
8
|
-
*
|
|
8
|
+
* Uses TypeScript discriminated unions to enforce valid state transitions.
|
|
9
|
+
*
|
|
10
|
+
* Design principles:
|
|
11
|
+
* - Each job status has specific valid fields
|
|
12
|
+
* - Type narrowing works automatically via status discriminant
|
|
13
|
+
* - No optional fields that may or may not exist
|
|
14
|
+
* - State machine is explicit and type-safe
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
type JobType = 'detection' | 'generation' | 'highlight-detection' | 'assessment-detection' | 'comment-detection' | 'tag-detection';
|
|
12
18
|
type JobStatus = 'pending' | 'running' | 'complete' | 'failed' | 'cancelled';
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
20
|
+
* Job metadata - common to all states
|
|
15
21
|
*/
|
|
16
|
-
interface
|
|
22
|
+
interface JobMetadata {
|
|
17
23
|
id: JobId;
|
|
18
24
|
type: JobType;
|
|
19
|
-
status: JobStatus;
|
|
20
25
|
userId: UserId;
|
|
21
26
|
created: string;
|
|
22
|
-
startedAt?: string;
|
|
23
|
-
completedAt?: string;
|
|
24
|
-
error?: string;
|
|
25
27
|
retryCount: number;
|
|
26
28
|
maxRetries: number;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* Detection job
|
|
31
|
+
* Detection job parameters
|
|
30
32
|
*/
|
|
31
|
-
interface
|
|
32
|
-
type: 'detection';
|
|
33
|
+
interface DetectionParams {
|
|
33
34
|
resourceId: ResourceId;
|
|
34
35
|
entityTypes: EntityType[];
|
|
35
36
|
includeDescriptiveReferences?: boolean;
|
|
36
|
-
progress?: {
|
|
37
|
-
totalEntityTypes: number;
|
|
38
|
-
processedEntityTypes: number;
|
|
39
|
-
currentEntityType?: string;
|
|
40
|
-
entitiesFound: number;
|
|
41
|
-
entitiesEmitted: number;
|
|
42
|
-
};
|
|
43
|
-
result?: {
|
|
44
|
-
totalFound: number;
|
|
45
|
-
totalEmitted: number;
|
|
46
|
-
errors: number;
|
|
47
|
-
};
|
|
48
37
|
}
|
|
49
38
|
/**
|
|
50
|
-
* Generation job
|
|
39
|
+
* Generation job parameters
|
|
51
40
|
*/
|
|
52
|
-
interface
|
|
53
|
-
type: 'generation';
|
|
41
|
+
interface GenerationParams {
|
|
54
42
|
referenceId: AnnotationId;
|
|
55
43
|
sourceResourceId: ResourceId;
|
|
56
44
|
prompt?: string;
|
|
@@ -60,101 +48,208 @@ interface GenerationJob extends BaseJob {
|
|
|
60
48
|
context?: GenerationContext;
|
|
61
49
|
temperature?: number;
|
|
62
50
|
maxTokens?: number;
|
|
63
|
-
progress?: {
|
|
64
|
-
stage: 'fetching' | 'generating' | 'creating' | 'linking';
|
|
65
|
-
percentage: number;
|
|
66
|
-
message?: string;
|
|
67
|
-
};
|
|
68
|
-
result?: {
|
|
69
|
-
resourceId: ResourceId;
|
|
70
|
-
resourceName: string;
|
|
71
|
-
};
|
|
72
51
|
}
|
|
73
52
|
/**
|
|
74
|
-
* Highlight
|
|
53
|
+
* Highlight detection job parameters
|
|
75
54
|
*/
|
|
76
|
-
interface
|
|
77
|
-
type: 'highlight-detection';
|
|
55
|
+
interface HighlightDetectionParams {
|
|
78
56
|
resourceId: ResourceId;
|
|
79
57
|
instructions?: string;
|
|
80
58
|
density?: number;
|
|
81
|
-
progress?: {
|
|
82
|
-
stage: 'analyzing' | 'creating';
|
|
83
|
-
percentage: number;
|
|
84
|
-
message?: string;
|
|
85
|
-
};
|
|
86
|
-
result?: {
|
|
87
|
-
highlightsFound: number;
|
|
88
|
-
highlightsCreated: number;
|
|
89
|
-
};
|
|
90
59
|
}
|
|
91
60
|
/**
|
|
92
|
-
* Assessment
|
|
61
|
+
* Assessment detection job parameters
|
|
93
62
|
*/
|
|
94
|
-
interface
|
|
95
|
-
type: 'assessment-detection';
|
|
63
|
+
interface AssessmentDetectionParams {
|
|
96
64
|
resourceId: ResourceId;
|
|
97
65
|
instructions?: string;
|
|
98
66
|
tone?: 'analytical' | 'critical' | 'balanced' | 'constructive';
|
|
99
67
|
density?: number;
|
|
100
|
-
progress?: {
|
|
101
|
-
stage: 'analyzing' | 'creating';
|
|
102
|
-
percentage: number;
|
|
103
|
-
message?: string;
|
|
104
|
-
};
|
|
105
|
-
result?: {
|
|
106
|
-
assessmentsFound: number;
|
|
107
|
-
assessmentsCreated: number;
|
|
108
|
-
};
|
|
109
68
|
}
|
|
110
69
|
/**
|
|
111
|
-
* Comment
|
|
70
|
+
* Comment detection job parameters
|
|
112
71
|
*/
|
|
113
|
-
interface
|
|
114
|
-
type: 'comment-detection';
|
|
72
|
+
interface CommentDetectionParams {
|
|
115
73
|
resourceId: ResourceId;
|
|
116
74
|
instructions?: string;
|
|
117
75
|
tone?: 'scholarly' | 'explanatory' | 'conversational' | 'technical';
|
|
118
76
|
density?: number;
|
|
119
|
-
progress?: {
|
|
120
|
-
stage: 'analyzing' | 'creating';
|
|
121
|
-
percentage: number;
|
|
122
|
-
message?: string;
|
|
123
|
-
};
|
|
124
|
-
result?: {
|
|
125
|
-
commentsFound: number;
|
|
126
|
-
commentsCreated: number;
|
|
127
|
-
};
|
|
128
77
|
}
|
|
129
78
|
/**
|
|
130
|
-
* Tag
|
|
79
|
+
* Tag detection job parameters
|
|
131
80
|
*/
|
|
132
|
-
interface
|
|
133
|
-
type: 'tag-detection';
|
|
81
|
+
interface TagDetectionParams {
|
|
134
82
|
resourceId: ResourceId;
|
|
135
83
|
schemaId: string;
|
|
136
84
|
categories: string[];
|
|
137
|
-
progress?: {
|
|
138
|
-
stage: 'analyzing' | 'creating';
|
|
139
|
-
percentage: number;
|
|
140
|
-
currentCategory?: string;
|
|
141
|
-
processedCategories: number;
|
|
142
|
-
totalCategories: number;
|
|
143
|
-
message?: string;
|
|
144
|
-
};
|
|
145
|
-
result?: {
|
|
146
|
-
tagsFound: number;
|
|
147
|
-
tagsCreated: number;
|
|
148
|
-
byCategory: Record<string, number>;
|
|
149
|
-
};
|
|
150
85
|
}
|
|
151
86
|
/**
|
|
152
|
-
*
|
|
87
|
+
* Detection job progress
|
|
88
|
+
*/
|
|
89
|
+
interface DetectionProgress {
|
|
90
|
+
totalEntityTypes: number;
|
|
91
|
+
processedEntityTypes: number;
|
|
92
|
+
currentEntityType?: string;
|
|
93
|
+
entitiesFound: number;
|
|
94
|
+
entitiesEmitted: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Detection job result
|
|
98
|
+
*/
|
|
99
|
+
interface DetectionResult {
|
|
100
|
+
totalFound: number;
|
|
101
|
+
totalEmitted: number;
|
|
102
|
+
errors: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Generation job progress
|
|
106
|
+
*/
|
|
107
|
+
interface GenerationProgress {
|
|
108
|
+
stage: 'fetching' | 'generating' | 'creating' | 'linking';
|
|
109
|
+
percentage: number;
|
|
110
|
+
message?: string;
|
|
111
|
+
resultResourceId?: ResourceId;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generation job result
|
|
115
|
+
*/
|
|
116
|
+
interface GenerationResult {
|
|
117
|
+
resourceId: ResourceId;
|
|
118
|
+
resourceName: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Highlight detection job progress
|
|
122
|
+
*/
|
|
123
|
+
interface HighlightDetectionProgress {
|
|
124
|
+
stage: 'analyzing' | 'creating';
|
|
125
|
+
percentage: number;
|
|
126
|
+
message?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Highlight detection job result
|
|
130
|
+
*/
|
|
131
|
+
interface HighlightDetectionResult {
|
|
132
|
+
highlightsFound: number;
|
|
133
|
+
highlightsCreated: number;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Assessment detection job progress
|
|
137
|
+
*/
|
|
138
|
+
interface AssessmentDetectionProgress {
|
|
139
|
+
stage: 'analyzing' | 'creating';
|
|
140
|
+
percentage: number;
|
|
141
|
+
message?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Assessment detection job result
|
|
145
|
+
*/
|
|
146
|
+
interface AssessmentDetectionResult {
|
|
147
|
+
assessmentsFound: number;
|
|
148
|
+
assessmentsCreated: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Comment detection job progress
|
|
152
|
+
*/
|
|
153
|
+
interface CommentDetectionProgress {
|
|
154
|
+
stage: 'analyzing' | 'creating';
|
|
155
|
+
percentage: number;
|
|
156
|
+
message?: string;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Comment detection job result
|
|
160
|
+
*/
|
|
161
|
+
interface CommentDetectionResult {
|
|
162
|
+
commentsFound: number;
|
|
163
|
+
commentsCreated: number;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Tag detection job progress
|
|
167
|
+
*/
|
|
168
|
+
interface TagDetectionProgress {
|
|
169
|
+
stage: 'analyzing' | 'creating';
|
|
170
|
+
percentage: number;
|
|
171
|
+
currentCategory?: string;
|
|
172
|
+
processedCategories: number;
|
|
173
|
+
totalCategories: number;
|
|
174
|
+
message?: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Tag detection job result
|
|
153
178
|
*/
|
|
154
|
-
|
|
179
|
+
interface TagDetectionResult {
|
|
180
|
+
tagsFound: number;
|
|
181
|
+
tagsCreated: number;
|
|
182
|
+
byCategory: Record<string, number>;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Pending job - just created, waiting to be picked up
|
|
186
|
+
*/
|
|
187
|
+
interface PendingJob<P> {
|
|
188
|
+
status: 'pending';
|
|
189
|
+
metadata: JobMetadata;
|
|
190
|
+
params: P;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Running job - actively being processed
|
|
194
|
+
*/
|
|
195
|
+
interface RunningJob<P, PG> {
|
|
196
|
+
status: 'running';
|
|
197
|
+
metadata: JobMetadata;
|
|
198
|
+
params: P;
|
|
199
|
+
startedAt: string;
|
|
200
|
+
progress: PG;
|
|
201
|
+
}
|
|
155
202
|
/**
|
|
156
|
-
*
|
|
203
|
+
* Complete job - successfully finished
|
|
204
|
+
*/
|
|
205
|
+
interface CompleteJob<P, R> {
|
|
206
|
+
status: 'complete';
|
|
207
|
+
metadata: JobMetadata;
|
|
208
|
+
params: P;
|
|
209
|
+
startedAt: string;
|
|
210
|
+
completedAt: string;
|
|
211
|
+
result: R;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Failed job - encountered an error
|
|
215
|
+
*/
|
|
216
|
+
interface FailedJob<P> {
|
|
217
|
+
status: 'failed';
|
|
218
|
+
metadata: JobMetadata;
|
|
219
|
+
params: P;
|
|
220
|
+
startedAt?: string;
|
|
221
|
+
completedAt: string;
|
|
222
|
+
error: string;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Cancelled job - stopped by user
|
|
226
|
+
*/
|
|
227
|
+
interface CancelledJob<P> {
|
|
228
|
+
status: 'cancelled';
|
|
229
|
+
metadata: JobMetadata;
|
|
230
|
+
params: P;
|
|
231
|
+
startedAt?: string;
|
|
232
|
+
completedAt: string;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Generic job - discriminated union of all states
|
|
236
|
+
*/
|
|
237
|
+
type Job<P, PG, R> = PendingJob<P> | RunningJob<P, PG> | CompleteJob<P, R> | FailedJob<P> | CancelledJob<P>;
|
|
238
|
+
type DetectionJob = Job<DetectionParams, DetectionProgress, DetectionResult>;
|
|
239
|
+
type GenerationJob = Job<GenerationParams, GenerationProgress, GenerationResult>;
|
|
240
|
+
type HighlightDetectionJob = Job<HighlightDetectionParams, HighlightDetectionProgress, HighlightDetectionResult>;
|
|
241
|
+
type AssessmentDetectionJob = Job<AssessmentDetectionParams, AssessmentDetectionProgress, AssessmentDetectionResult>;
|
|
242
|
+
type CommentDetectionJob = Job<CommentDetectionParams, CommentDetectionProgress, CommentDetectionResult>;
|
|
243
|
+
type TagDetectionJob = Job<TagDetectionParams, TagDetectionProgress, TagDetectionResult>;
|
|
244
|
+
/**
|
|
245
|
+
* Discriminated union of all job types
|
|
157
246
|
*/
|
|
247
|
+
type AnyJob = DetectionJob | GenerationJob | HighlightDetectionJob | AssessmentDetectionJob | CommentDetectionJob | TagDetectionJob;
|
|
248
|
+
declare function isPendingJob(job: AnyJob): job is PendingJob<any>;
|
|
249
|
+
declare function isRunningJob(job: AnyJob): job is RunningJob<any, any>;
|
|
250
|
+
declare function isCompleteJob(job: AnyJob): job is CompleteJob<any, any>;
|
|
251
|
+
declare function isFailedJob(job: AnyJob): job is FailedJob<any>;
|
|
252
|
+
declare function isCancelledJob(job: AnyJob): job is CancelledJob<any>;
|
|
158
253
|
interface JobQueryFilters {
|
|
159
254
|
status?: JobStatus;
|
|
160
255
|
type?: JobType;
|
|
@@ -183,23 +278,23 @@ declare class JobQueue {
|
|
|
183
278
|
/**
|
|
184
279
|
* Create a new job
|
|
185
280
|
*/
|
|
186
|
-
createJob(job:
|
|
281
|
+
createJob(job: AnyJob): Promise<void>;
|
|
187
282
|
/**
|
|
188
283
|
* Get a job by ID (searches all status directories)
|
|
189
284
|
*/
|
|
190
|
-
getJob(jobId: JobId): Promise<
|
|
285
|
+
getJob(jobId: JobId): Promise<AnyJob | null>;
|
|
191
286
|
/**
|
|
192
287
|
* Update a job (atomic: delete old, write new)
|
|
193
288
|
*/
|
|
194
|
-
updateJob(job:
|
|
289
|
+
updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void>;
|
|
195
290
|
/**
|
|
196
291
|
* Poll for next pending job (FIFO)
|
|
197
292
|
*/
|
|
198
|
-
pollNextPendingJob(): Promise<
|
|
293
|
+
pollNextPendingJob(): Promise<AnyJob | null>;
|
|
199
294
|
/**
|
|
200
295
|
* List jobs with filters
|
|
201
296
|
*/
|
|
202
|
-
listJobs(filters?: JobQueryFilters): Promise<
|
|
297
|
+
listJobs(filters?: JobQueryFilters): Promise<AnyJob[]>;
|
|
203
298
|
/**
|
|
204
299
|
* Cancel a job
|
|
205
300
|
*/
|
|
@@ -238,7 +333,8 @@ declare abstract class JobWorker {
|
|
|
238
333
|
private currentJob;
|
|
239
334
|
private pollIntervalMs;
|
|
240
335
|
private errorBackoffMs;
|
|
241
|
-
|
|
336
|
+
protected jobQueue: JobQueue;
|
|
337
|
+
constructor(jobQueue: JobQueue, pollIntervalMs?: number, errorBackoffMs?: number);
|
|
242
338
|
/**
|
|
243
339
|
* Start the worker (polls queue in loop)
|
|
244
340
|
*/
|
|
@@ -258,11 +354,11 @@ declare abstract class JobWorker {
|
|
|
258
354
|
/**
|
|
259
355
|
* Handle job failure (retry or move to failed)
|
|
260
356
|
*/
|
|
261
|
-
protected handleJobFailure(job:
|
|
357
|
+
protected handleJobFailure(job: AnyJob, error: any): Promise<void>;
|
|
262
358
|
/**
|
|
263
359
|
* Update job progress (best-effort, doesn't throw)
|
|
264
360
|
*/
|
|
265
|
-
protected updateJobProgress(job:
|
|
361
|
+
protected updateJobProgress(job: AnyJob): Promise<void>;
|
|
266
362
|
/**
|
|
267
363
|
* Sleep utility
|
|
268
364
|
*/
|
|
@@ -274,13 +370,13 @@ declare abstract class JobWorker {
|
|
|
274
370
|
/**
|
|
275
371
|
* Check if this worker can process the given job
|
|
276
372
|
*/
|
|
277
|
-
protected abstract canProcessJob(job:
|
|
373
|
+
protected abstract canProcessJob(job: AnyJob): boolean;
|
|
278
374
|
/**
|
|
279
375
|
* Execute the job (job-specific logic)
|
|
280
376
|
* This is where the actual work happens
|
|
281
377
|
* Throw an error to trigger retry logic
|
|
282
378
|
*/
|
|
283
|
-
protected abstract executeJob(job:
|
|
379
|
+
protected abstract executeJob(job: AnyJob): Promise<void>;
|
|
284
380
|
}
|
|
285
381
|
|
|
286
|
-
export { type AssessmentDetectionJob, type
|
|
382
|
+
export { type AnyJob, type AssessmentDetectionJob, type AssessmentDetectionParams, type AssessmentDetectionProgress, type AssessmentDetectionResult, type CancelledJob, type CommentDetectionJob, type CommentDetectionParams, type CommentDetectionProgress, type CommentDetectionResult, type CompleteJob, type DetectionJob, type DetectionParams, type DetectionProgress, type DetectionResult, type FailedJob, type GenerationJob, type GenerationParams, type GenerationProgress, type GenerationResult, type HighlightDetectionJob, type HighlightDetectionParams, type HighlightDetectionProgress, type HighlightDetectionResult, type JobMetadata, type JobQueryFilters, JobQueue, type JobQueueConfig, type JobStatus, type JobType, JobWorker, type PendingJob, type RunningJob, type TagDetectionJob, type TagDetectionParams, type TagDetectionProgress, type TagDetectionResult, getJobQueue, initializeJobQueue, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob };
|
package/dist/index.js
CHANGED
|
@@ -22,9 +22,9 @@ var JobQueue = class {
|
|
|
22
22
|
* Create a new job
|
|
23
23
|
*/
|
|
24
24
|
async createJob(job) {
|
|
25
|
-
const jobPath = this.getJobPath(job.id, job.status);
|
|
25
|
+
const jobPath = this.getJobPath(job.metadata.id, job.status);
|
|
26
26
|
await promises.writeFile(jobPath, JSON.stringify(job, null, 2), "utf-8");
|
|
27
|
-
console.log(`[JobQueue] Created job ${job.id} with status ${job.status}`);
|
|
27
|
+
console.log(`[JobQueue] Created job ${job.metadata.id} with status ${job.status}`);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Get a job by ID (searches all status directories)
|
|
@@ -47,18 +47,18 @@ var JobQueue = class {
|
|
|
47
47
|
*/
|
|
48
48
|
async updateJob(job, oldStatus) {
|
|
49
49
|
if (oldStatus && oldStatus !== job.status) {
|
|
50
|
-
const oldPath = this.getJobPath(job.id, oldStatus);
|
|
50
|
+
const oldPath = this.getJobPath(job.metadata.id, oldStatus);
|
|
51
51
|
try {
|
|
52
52
|
await promises.unlink(oldPath);
|
|
53
53
|
} catch (error) {
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
const newPath = this.getJobPath(job.id, job.status);
|
|
56
|
+
const newPath = this.getJobPath(job.metadata.id, job.status);
|
|
57
57
|
await promises.writeFile(newPath, JSON.stringify(job, null, 2), "utf-8");
|
|
58
58
|
if (oldStatus && oldStatus !== job.status) {
|
|
59
|
-
console.log(`[JobQueue] Moved job ${job.id} from ${oldStatus} to ${job.status}`);
|
|
59
|
+
console.log(`[JobQueue] Moved job ${job.metadata.id} from ${oldStatus} to ${job.status}`);
|
|
60
60
|
} else {
|
|
61
|
-
console.log(`[JobQueue] Updated job ${job.id} (status: ${job.status})`);
|
|
61
|
+
console.log(`[JobQueue] Updated job ${job.metadata.id} (status: ${job.status})`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
@@ -95,15 +95,15 @@ var JobQueue = class {
|
|
|
95
95
|
const jobPath = path.join(statusDir, file);
|
|
96
96
|
const content = await promises.readFile(jobPath, "utf-8");
|
|
97
97
|
const job = JSON.parse(content);
|
|
98
|
-
if (filters.type && job.type !== filters.type) continue;
|
|
99
|
-
if (filters.userId && job.userId !== filters.userId) continue;
|
|
98
|
+
if (filters.type && job.metadata.type !== filters.type) continue;
|
|
99
|
+
if (filters.userId && job.metadata.userId !== filters.userId) continue;
|
|
100
100
|
jobs.push(job);
|
|
101
101
|
}
|
|
102
102
|
} catch (error) {
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
jobs.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
106
|
+
jobs.sort((a, b) => new Date(b.metadata.created).getTime() - new Date(a.metadata.created).getTime());
|
|
107
107
|
const offset = filters.offset || 0;
|
|
108
108
|
const limit = filters.limit || 100;
|
|
109
109
|
return jobs.slice(offset, offset + limit);
|
|
@@ -120,9 +120,14 @@ var JobQueue = class {
|
|
|
120
120
|
return false;
|
|
121
121
|
}
|
|
122
122
|
const oldStatus = job.status;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
const cancelledJob = {
|
|
124
|
+
status: "cancelled",
|
|
125
|
+
metadata: job.metadata,
|
|
126
|
+
params: job.status === "pending" ? job.params : job.params,
|
|
127
|
+
startedAt: job.status === "running" ? job.startedAt : void 0,
|
|
128
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
129
|
+
};
|
|
130
|
+
await this.updateJob(cancelledJob, oldStatus);
|
|
126
131
|
return true;
|
|
127
132
|
}
|
|
128
133
|
/**
|
|
@@ -140,7 +145,7 @@ var JobQueue = class {
|
|
|
140
145
|
const jobPath = path.join(statusDir, file);
|
|
141
146
|
const content = await promises.readFile(jobPath, "utf-8");
|
|
142
147
|
const job = JSON.parse(content);
|
|
143
|
-
if (job.
|
|
148
|
+
if (job.status === "complete" || job.status === "failed" || job.status === "cancelled") {
|
|
144
149
|
const completedTime = new Date(job.completedAt).getTime();
|
|
145
150
|
if (completedTime < cutoffTime) {
|
|
146
151
|
await promises.unlink(jobPath);
|
|
@@ -206,7 +211,9 @@ var JobWorker = class {
|
|
|
206
211
|
currentJob = null;
|
|
207
212
|
pollIntervalMs;
|
|
208
213
|
errorBackoffMs;
|
|
209
|
-
|
|
214
|
+
jobQueue;
|
|
215
|
+
constructor(jobQueue2, pollIntervalMs = 1e3, errorBackoffMs = 5e3) {
|
|
216
|
+
this.jobQueue = jobQueue2;
|
|
210
217
|
this.pollIntervalMs = pollIntervalMs;
|
|
211
218
|
this.errorBackoffMs = errorBackoffMs;
|
|
212
219
|
}
|
|
@@ -243,15 +250,14 @@ var JobWorker = class {
|
|
|
243
250
|
await this.sleep(100);
|
|
244
251
|
}
|
|
245
252
|
if (this.currentJob) {
|
|
246
|
-
console.warn(`[${this.getWorkerName()}] Forced shutdown while processing job ${this.currentJob.id}`);
|
|
253
|
+
console.warn(`[${this.getWorkerName()}] Forced shutdown while processing job ${this.currentJob.metadata.id}`);
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
/**
|
|
250
257
|
* Poll for next job to process
|
|
251
258
|
*/
|
|
252
259
|
async pollNextJob() {
|
|
253
|
-
const
|
|
254
|
-
const job = await jobQueue2.pollNextPendingJob();
|
|
260
|
+
const job = await this.jobQueue.pollNextPendingJob();
|
|
255
261
|
if (job && this.canProcessJob(job)) {
|
|
256
262
|
return job;
|
|
257
263
|
}
|
|
@@ -262,18 +268,33 @@ var JobWorker = class {
|
|
|
262
268
|
*/
|
|
263
269
|
async processJob(job) {
|
|
264
270
|
this.currentJob = job;
|
|
265
|
-
const jobQueue2 = getJobQueue();
|
|
266
271
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
if (job.status !== "pending") {
|
|
273
|
+
console.warn(`[${this.getWorkerName()}] Skipping non-pending job ${job.metadata.id}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const runningJob = {
|
|
277
|
+
status: "running",
|
|
278
|
+
metadata: job.metadata,
|
|
279
|
+
params: job.params,
|
|
280
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
281
|
+
progress: {}
|
|
282
|
+
// Initialize with empty progress
|
|
283
|
+
};
|
|
284
|
+
await this.jobQueue.updateJob(runningJob, "pending");
|
|
285
|
+
console.log(`[${this.getWorkerName()}] \u{1F504} Processing job ${job.metadata.id} (type: ${job.metadata.type})`);
|
|
286
|
+
await this.executeJob(runningJob);
|
|
287
|
+
const completeJob = {
|
|
288
|
+
status: "complete",
|
|
289
|
+
metadata: runningJob.metadata,
|
|
290
|
+
params: runningJob.params,
|
|
291
|
+
startedAt: runningJob.startedAt,
|
|
292
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
293
|
+
result: {}
|
|
294
|
+
// Subclass should set this via updateJobProgress
|
|
295
|
+
};
|
|
296
|
+
await this.jobQueue.updateJob(completeJob, "running");
|
|
297
|
+
console.log(`[${this.getWorkerName()}] \u2705 Job ${job.metadata.id} completed successfully`);
|
|
277
298
|
} catch (error) {
|
|
278
299
|
await this.handleJobFailure(job, error);
|
|
279
300
|
} finally {
|
|
@@ -284,21 +305,31 @@ var JobWorker = class {
|
|
|
284
305
|
* Handle job failure (retry or move to failed)
|
|
285
306
|
*/
|
|
286
307
|
async handleJobFailure(job, error) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
308
|
+
const updatedMetadata = {
|
|
309
|
+
...job.metadata,
|
|
310
|
+
retryCount: job.metadata.retryCount + 1
|
|
311
|
+
};
|
|
312
|
+
if (updatedMetadata.retryCount < updatedMetadata.maxRetries) {
|
|
313
|
+
console.log(`[${this.getWorkerName()}] Job ${job.metadata.id} failed, will retry (${updatedMetadata.retryCount}/${updatedMetadata.maxRetries})`);
|
|
291
314
|
console.log(`[${this.getWorkerName()}] Error:`, error);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
315
|
+
const retryJob = {
|
|
316
|
+
status: "pending",
|
|
317
|
+
metadata: updatedMetadata,
|
|
318
|
+
params: job.status === "pending" ? job.params : job.params
|
|
319
|
+
};
|
|
320
|
+
await this.jobQueue.updateJob(retryJob, job.status);
|
|
295
321
|
} else {
|
|
296
|
-
console.error(`[${this.getWorkerName()}] \u274C Job ${job.id} failed permanently after ${
|
|
322
|
+
console.error(`[${this.getWorkerName()}] \u274C Job ${job.metadata.id} failed permanently after ${updatedMetadata.retryCount} retries`);
|
|
297
323
|
console.error(`[${this.getWorkerName()}] Error:`, error);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
const failedJob = {
|
|
325
|
+
status: "failed",
|
|
326
|
+
metadata: updatedMetadata,
|
|
327
|
+
params: job.status === "pending" ? job.params : job.params,
|
|
328
|
+
startedAt: job.status === "running" ? job.startedAt : void 0,
|
|
329
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
330
|
+
error: error instanceof Error ? error.message : String(error)
|
|
331
|
+
};
|
|
332
|
+
await this.jobQueue.updateJob(failedJob, job.status);
|
|
302
333
|
}
|
|
303
334
|
}
|
|
304
335
|
/**
|
|
@@ -306,8 +337,7 @@ var JobWorker = class {
|
|
|
306
337
|
*/
|
|
307
338
|
async updateJobProgress(job) {
|
|
308
339
|
try {
|
|
309
|
-
|
|
310
|
-
await jobQueue2.updateJob(job);
|
|
340
|
+
await this.jobQueue.updateJob(job);
|
|
311
341
|
} catch (error) {
|
|
312
342
|
console.warn(`[${this.getWorkerName()}] Failed to update job progress:`, error);
|
|
313
343
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/job-queue.ts","../src/job-worker.ts"],"names":["fs","jobQueue"],"mappings":";;;;AAgBO,IAAM,WAAN,MAAe;AAAA,EACZ,OAAA;AAAA,EAER,YAAY,MAAA,EAAwB;AAClC,IAAA,IAAA,CAAK,OAAA,GAAe,IAAA,CAAA,IAAA,CAAK,MAAA,CAAO,OAAA,EAAS,MAAM,CAAA;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAA,GAA4B;AAChC,IAAA,MAAM,WAAwB,CAAC,SAAA,EAAW,SAAA,EAAW,UAAA,EAAY,UAAU,WAAW,CAAA;AAEtF,IAAA,KAAA,MAAW,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAM,GAAA,GAAW,IAAA,CAAA,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,CAAA;AAC1C,MAAA,MAAMA,SAAG,KAAA,CAAM,GAAA,EAAK,EAAE,SAAA,EAAW,MAAM,CAAA;AAAA,IACzC;AAEA,IAAA,OAAA,CAAQ,IAAI,wCAAwC,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,GAAA,EAAyB;AACvC,IAAA,MAAM,UAAU,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,EAAA,EAAI,IAAI,MAAM,CAAA;AAClD,IAAA,MAAMA,QAAA,CAAG,UAAU,OAAA,EAAS,IAAA,CAAK,UAAU,GAAA,EAAK,IAAA,EAAM,CAAC,CAAA,EAAG,OAAO,CAAA;AACjE,IAAA,OAAA,CAAQ,IAAI,CAAA,uBAAA,EAA0B,GAAA,CAAI,EAAE,CAAA,aAAA,EAAgB,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,KAAA,EAAmC;AAC9C,IAAA,MAAM,WAAwB,CAAC,SAAA,EAAW,SAAA,EAAW,UAAA,EAAY,UAAU,WAAW,CAAA;AAEtF,IAAA,KAAA,MAAW,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,KAAA,EAAO,MAAM,CAAA;AAC7C,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,MAAMA,QAAA,CAAG,QAAA,CAAS,SAAS,OAAO,CAAA;AAClD,QAAA,OAAO,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,MAC3B,SAAS,KAAA,EAAO;AAEd,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAA,CAAU,GAAA,EAAU,SAAA,EAAsC;AAE9D,IAAA,IAAI,SAAA,IAAa,SAAA,KAAc,GAAA,CAAI,MAAA,EAAQ;AACzC,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,IAAI,SAAS,CAAA;AACjD,MAAA,IAAI;AACF,QAAA,MAAMA,QAAA,CAAG,OAAO,OAAO,CAAA;AAAA,MACzB,SAAS,KAAA,EAAO;AAAA,MAEhB;AAAA,IACF;AAGA,IAAA,MAAM,UAAU,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,EAAA,EAAI,IAAI,MAAM,CAAA;AAClD,IAAA,MAAMA,QAAA,CAAG,UAAU,OAAA,EAAS,IAAA,CAAK,UAAU,GAAA,EAAK,IAAA,EAAM,CAAC,CAAA,EAAG,OAAO,CAAA;AAEjE,IAAA,IAAI,SAAA,IAAa,SAAA,KAAc,GAAA,CAAI,MAAA,EAAQ;AACzC,MAAA,OAAA,CAAQ,GAAA,CAAI,wBAAwB,GAAA,CAAI,EAAE,SAAS,SAAS,CAAA,IAAA,EAAO,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,IACjF,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,IAAI,CAAA,uBAAA,EAA0B,GAAA,CAAI,EAAE,CAAA,UAAA,EAAa,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IACxE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAAA,GAA0C;AAC9C,IAAA,MAAM,UAAA,GAAkB,IAAA,CAAA,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,SAAS,CAAA;AAEpD,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAMA,QAAA,CAAG,OAAA,CAAQ,UAAU,CAAA;AAEzC,MAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,QAAA,OAAO,IAAA;AAAA,MACT;AAGA,MAAA,KAAA,CAAM,IAAA,EAAK;AAEX,MAAA,MAAM,OAAA,GAAU,MAAM,CAAC,CAAA;AACvB,MAAA,MAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAK,UAAA,EAAY,OAAO,CAAA;AAE7C,MAAA,MAAM,OAAA,GAAU,MAAMA,QAAA,CAAG,QAAA,CAAS,SAAS,OAAO,CAAA;AAClD,MAAA,OAAO,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,IAC3B,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,0CAA0C,KAAK,CAAA;AAC7D,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAA,CAAS,OAAA,GAA2B,EAAC,EAAmB;AAC5D,IAAA,MAAM,OAAc,EAAC;AAGrB,IAAA,MAAM,QAAA,GAAwB,OAAA,CAAQ,MAAA,GAClC,CAAC,OAAA,CAAQ,MAAM,CAAA,GACf,CAAC,SAAA,EAAW,SAAA,EAAW,UAAA,EAAY,QAAA,EAAU,WAAW,CAAA;AAE5D,IAAA,KAAA,MAAW,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAM,SAAA,GAAiB,IAAA,CAAA,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,CAAA;AAEhD,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAMA,QAAA,CAAG,OAAA,CAAQ,SAAS,CAAA;AAExC,QAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,UAAA,MAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAK,SAAA,EAAW,IAAI,CAAA;AACzC,UAAA,MAAM,OAAA,GAAU,MAAMA,QAAA,CAAG,QAAA,CAAS,SAAS,OAAO,CAAA;AAClD,UAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAG9B,UAAA,IAAI,OAAA,CAAQ,IAAA,IAAQ,GAAA,CAAI,IAAA,KAAS,QAAQ,IAAA,EAAM;AAC/C,UAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,GAAA,CAAI,MAAA,KAAW,QAAQ,MAAA,EAAQ;AAErD,UAAA,IAAA,CAAK,KAAK,GAAG,CAAA;AAAA,QACf;AAAA,MACF,SAAS,KAAA,EAAO;AAEd,QAAA;AAAA,MACF;AAAA,IACF;AAGA,IAAA,IAAA,CAAK,KAAK,CAAC,CAAA,EAAG,CAAA,KAAM,IAAI,KAAK,CAAA,CAAE,OAAO,CAAA,CAAE,OAAA,KAAY,IAAI,IAAA,CAAK,EAAE,OAAO,CAAA,CAAE,SAAS,CAAA;AAGjF,IAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,CAAA;AACjC,IAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,GAAA;AAE/B,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,MAAA,GAAS,KAAK,CAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,KAAA,EAAgC;AAC9C,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA;AAEnC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,SAAA,IAAa,GAAA,CAAI,WAAW,SAAA,EAAW;AACxD,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,MAAM,YAAY,GAAA,CAAI,MAAA;AACtB,IAAA,GAAA,CAAI,MAAA,GAAS,WAAA;AACb,IAAA,GAAA,CAAI,WAAA,GAAA,iBAAc,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAEzC,IAAA,MAAM,IAAA,CAAK,SAAA,CAAU,GAAA,EAAK,SAAS,CAAA;AACnC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAA,CAAe,cAAA,GAAyB,EAAA,EAAqB;AACjE,IAAA,MAAM,aAAa,IAAA,CAAK,GAAA,EAAI,GAAK,cAAA,GAAiB,KAAK,EAAA,GAAK,GAAA;AAC5D,IAAA,IAAI,YAAA,GAAe,CAAA;AAEnB,IAAA,MAAM,eAAA,GAA+B,CAAC,UAAA,EAAY,QAAA,EAAU,WAAW,CAAA;AAEvE,IAAA,KAAA,MAAW,UAAU,eAAA,EAAiB;AACpC,MAAA,MAAM,SAAA,GAAiB,IAAA,CAAA,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,CAAA;AAEhD,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAMA,QAAA,CAAG,OAAA,CAAQ,SAAS,CAAA;AAExC,QAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,UAAA,MAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAK,SAAA,EAAW,IAAI,CAAA;AACzC,UAAA,MAAM,OAAA,GAAU,MAAMA,QAAA,CAAG,QAAA,CAAS,SAAS,OAAO,CAAA;AAClD,UAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAE9B,UAAA,IAAI,IAAI,WAAA,EAAa;AACnB,YAAA,MAAM,gBAAgB,IAAI,IAAA,CAAK,GAAA,CAAI,WAAW,EAAE,OAAA,EAAQ;AAExD,YAAA,IAAI,gBAAgB,UAAA,EAAY;AAC9B,cAAA,MAAMA,QAAA,CAAG,OAAO,OAAO,CAAA;AACvB,cAAA,YAAA,EAAA;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,MAAA,CAAA,EAAU,KAAK,CAAA;AAAA,MACrE;AAAA,IACF;AAEA,IAAA,IAAI,eAAe,CAAA,EAAG;AACpB,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,sBAAA,EAAyB,YAAY,CAAA,SAAA,CAAW,CAAA;AAAA,IAC9D;AAEA,IAAA,OAAO,YAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAA,CAAW,OAAc,MAAA,EAA2B;AAC1D,IAAA,OAAY,UAAK,IAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,KAAA,CAAO,CAAA;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAA,GAMH;AACD,IAAA,MAAM,KAAA,GAAQ;AAAA,MACZ,OAAA,EAAS,CAAA;AAAA,MACT,OAAA,EAAS,CAAA;AAAA,MACT,QAAA,EAAU,CAAA;AAAA,MACV,MAAA,EAAQ,CAAA;AAAA,MACR,SAAA,EAAW;AAAA,KACb;AAEA,IAAA,MAAM,WAAwB,CAAC,SAAA,EAAW,SAAA,EAAW,UAAA,EAAY,UAAU,WAAW,CAAA;AAEtF,IAAA,KAAA,MAAW,UAAU,QAAA,EAAU;AAC7B,MAAA,MAAM,SAAA,GAAiB,IAAA,CAAA,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,MAAM,CAAA;AAEhD,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAMA,QAAA,CAAG,OAAA,CAAQ,SAAS,CAAA;AACxC,QAAA,KAAA,CAAM,MAAM,IAAI,KAAA,CAAM,MAAA;AAAA,MACxB,SAAS,KAAA,EAAO;AAEd,QAAA,KAAA,CAAM,MAAM,CAAA,GAAI,CAAA;AAAA,MAClB;AAAA,IACF;AAEA,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGA,IAAI,QAAA,GAA4B,IAAA;AAEzB,SAAS,WAAA,GAAwB;AACtC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,MAAM,4DAA4D,CAAA;AAAA,EAC9E;AACA,EAAA,OAAO,QAAA;AACT;AAEA,eAAsB,mBAAmB,MAAA,EAA2C;AAClF,EAAA,QAAA,GAAW,IAAI,SAAS,MAAM,CAAA;AAC9B,EAAA,MAAM,SAAS,UAAA,EAAW;AAC1B,EAAA,OAAO,QAAA;AACT;;;ACjRO,IAAe,YAAf,MAAyB;AAAA,EACtB,OAAA,GAAU,KAAA;AAAA,EACV,UAAA,GAAyB,IAAA;AAAA,EACzB,cAAA;AAAA,EACA,cAAA;AAAA,EAER,WAAA,CACE,cAAA,GAAyB,GAAA,EACzB,cAAA,GAAyB,GAAA,EACzB;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,cAAA;AACtB,IAAA,IAAA,CAAK,cAAA,GAAiB,cAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,SAAA,CAAW,CAAA;AAE/C,IAAA,OAAO,KAAK,OAAA,EAAS;AACnB,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,WAAA,EAAY;AAEnC,QAAA,IAAI,GAAA,EAAK;AACP,UAAA,MAAM,IAAA,CAAK,WAAW,GAAG,CAAA;AAAA,QAC3B,CAAA,MAAO;AAEL,UAAA,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,cAAc,CAAA;AAAA,QACtC;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,OAAA,CAAQ,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,yBAAyB,KAAK,CAAA;AAEpE,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,cAAc,CAAA;AAAA,MACtC;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,SAAA,CAAW,CAAA;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,IAAA,GAAsB;AAC1B,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,aAAA,CAAe,CAAA;AACnD,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AAGf,IAAA,MAAM,OAAA,GAAU,GAAA;AAChB,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,IAAA,OAAO,KAAK,UAAA,IAAe,IAAA,CAAK,GAAA,EAAI,GAAI,YAAa,OAAA,EAAS;AAC5D,MAAA,MAAM,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACtB;AAEA,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,OAAA,CAAQ,IAAA,CAAK,IAAI,IAAA,CAAK,aAAA,EAAe,CAAA,uCAAA,EAA0C,IAAA,CAAK,UAAA,CAAW,EAAE,CAAA,CAAE,CAAA;AAAA,IACrG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WAAA,GAAmC;AAC/C,IAAA,MAAMC,YAAW,WAAA,EAAY;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAMA,SAAAA,CAAS,kBAAA,EAAmB;AAE9C,IAAA,IAAI,GAAA,IAAO,IAAA,CAAK,aAAA,CAAc,GAAG,CAAA,EAAG;AAClC,MAAA,OAAO,GAAA;AAAA,IACT;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WAAW,GAAA,EAAyB;AAChD,IAAA,IAAA,CAAK,UAAA,GAAa,GAAA;AAClB,IAAA,MAAMA,YAAW,WAAA,EAAY;AAE7B,IAAA,IAAI;AAEF,MAAA,MAAM,YAAY,GAAA,CAAI,MAAA;AACtB,MAAA,GAAA,CAAI,MAAA,GAAS,SAAA;AACb,MAAA,GAAA,CAAI,SAAA,GAAA,iBAAY,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACvC,MAAA,MAAMA,SAAAA,CAAS,SAAA,CAAU,GAAA,EAAK,SAAS,CAAA;AAEvC,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,2BAAA,EAAuB,GAAA,CAAI,EAAE,CAAA,QAAA,EAAW,GAAA,CAAI,IAAI,CAAA,CAAA,CAAG,CAAA;AAGvF,MAAA,MAAM,IAAA,CAAK,WAAW,GAAG,CAAA;AAGzB,MAAA,GAAA,CAAI,MAAA,GAAS,UAAA;AACb,MAAA,GAAA,CAAI,WAAA,GAAA,iBAAc,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACzC,MAAA,MAAMA,SAAAA,CAAS,SAAA,CAAU,GAAA,EAAK,SAAS,CAAA;AAEvC,MAAA,OAAA,CAAQ,GAAA,CAAI,IAAI,IAAA,CAAK,aAAA,EAAe,CAAA,aAAA,EAAW,GAAA,CAAI,EAAE,CAAA,uBAAA,CAAyB,CAAA;AAAA,IAEhF,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,IAAA,CAAK,gBAAA,CAAiB,GAAA,EAAK,KAAK,CAAA;AAAA,IACxC,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,gBAAA,CAAiB,GAAA,EAAU,KAAA,EAA2B;AACpE,IAAA,MAAMA,YAAW,WAAA,EAAY;AAC7B,IAAA,GAAA,CAAI,UAAA,EAAA;AAEJ,IAAA,IAAI,GAAA,CAAI,UAAA,GAAa,GAAA,CAAI,UAAA,EAAY;AACnC,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,MAAA,EAAS,GAAA,CAAI,EAAE,CAAA,qBAAA,EAAwB,GAAA,CAAI,UAAU,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAA,CAAG,CAAA;AAC9G,MAAA,OAAA,CAAQ,IAAI,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,YAAY,KAAK,CAAA;AAGrD,MAAA,GAAA,CAAI,MAAA,GAAS,SAAA;AACb,MAAA,GAAA,CAAI,SAAA,GAAY,MAAA;AAChB,MAAA,MAAMA,SAAAA,CAAS,SAAA,CAAU,GAAA,EAAK,SAAS,CAAA;AAAA,IAEzC,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,CAAA,aAAA,EAAW,GAAA,CAAI,EAAE,CAAA,0BAAA,EAA6B,GAAA,CAAI,UAAU,CAAA,QAAA,CAAU,CAAA;AAC5G,MAAA,OAAA,CAAQ,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,YAAY,KAAK,CAAA;AAGvD,MAAA,GAAA,CAAI,MAAA,GAAS,QAAA;AACb,MAAA,GAAA,CAAI,QAAQ,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACjE,MAAA,GAAA,CAAI,WAAA,GAAA,iBAAc,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACzC,MAAA,MAAMA,SAAAA,CAAS,SAAA,CAAU,GAAA,EAAK,SAAS,CAAA;AAAA,IACzC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,kBAAkB,GAAA,EAAyB;AACzD,IAAA,IAAI;AACF,MAAA,MAAMA,YAAW,WAAA,EAAY;AAC7B,MAAA,MAAMA,SAAAA,CAAS,UAAU,GAAG,CAAA;AAAA,IAC9B,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAK,CAAA,CAAA,EAAI,IAAA,CAAK,aAAA,EAAe,oCAAoC,KAAK,CAAA;AAAA,IAEhF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKU,MAAM,EAAA,EAA2B;AACzC,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,EACvD;AAoBF","file":"index.js","sourcesContent":["/**\n * Job Queue Manager\n *\n * Filesystem-based job queue with atomic operations.\n * Jobs are stored in directories by status for easy polling.\n */\n\nimport { promises as fs } from 'fs';\nimport * as path from 'path';\nimport type { Job, JobStatus, JobQueryFilters } from './types';\nimport type { JobId } from '@semiont/api-client';\n\nexport interface JobQueueConfig {\n dataDir: string;\n}\n\nexport class JobQueue {\n private jobsDir: string;\n\n constructor(config: JobQueueConfig) {\n this.jobsDir = path.join(config.dataDir, 'jobs');\n }\n\n /**\n * Initialize job queue directories\n */\n async initialize(): Promise<void> {\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const dir = path.join(this.jobsDir, status);\n await fs.mkdir(dir, { recursive: true });\n }\n\n console.log('[JobQueue] Initialized job directories');\n }\n\n /**\n * Create a new job\n */\n async createJob(job: Job): Promise<void> {\n const jobPath = this.getJobPath(job.id, job.status);\n await fs.writeFile(jobPath, JSON.stringify(job, null, 2), 'utf-8');\n console.log(`[JobQueue] Created job ${job.id} with status ${job.status}`);\n }\n\n /**\n * Get a job by ID (searches all status directories)\n */\n async getJob(jobId: JobId): Promise<Job | null> {\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const jobPath = this.getJobPath(jobId, status);\n try {\n const content = await fs.readFile(jobPath, 'utf-8');\n return JSON.parse(content) as Job;\n } catch (error) {\n // File doesn't exist in this status directory, try next\n continue;\n }\n }\n\n return null;\n }\n\n /**\n * Update a job (atomic: delete old, write new)\n */\n async updateJob(job: Job, oldStatus?: JobStatus): Promise<void> {\n // If oldStatus provided, delete from old location\n if (oldStatus && oldStatus !== job.status) {\n const oldPath = this.getJobPath(job.id, oldStatus);\n try {\n await fs.unlink(oldPath);\n } catch (error) {\n // Ignore if file doesn't exist\n }\n }\n\n // Write to new location\n const newPath = this.getJobPath(job.id, job.status);\n await fs.writeFile(newPath, JSON.stringify(job, null, 2), 'utf-8');\n\n if (oldStatus && oldStatus !== job.status) {\n console.log(`[JobQueue] Moved job ${job.id} from ${oldStatus} to ${job.status}`);\n } else {\n console.log(`[JobQueue] Updated job ${job.id} (status: ${job.status})`);\n }\n }\n\n /**\n * Poll for next pending job (FIFO)\n */\n async pollNextPendingJob(): Promise<Job | null> {\n const pendingDir = path.join(this.jobsDir, 'pending');\n\n try {\n const files = await fs.readdir(pendingDir);\n\n if (files.length === 0) {\n return null;\n }\n\n // Sort by filename (job IDs have timestamps via nanoid)\n files.sort();\n\n const jobFile = files[0]!;\n const jobPath = path.join(pendingDir, jobFile);\n\n const content = await fs.readFile(jobPath, 'utf-8');\n return JSON.parse(content) as Job;\n } catch (error) {\n console.error('[JobQueue] Error polling pending jobs:', error);\n return null;\n }\n }\n\n /**\n * List jobs with filters\n */\n async listJobs(filters: JobQueryFilters = {}): Promise<Job[]> {\n const jobs: Job[] = [];\n\n // Determine which status directories to scan\n const statuses: JobStatus[] = filters.status\n ? [filters.status]\n : ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n\n for (const file of files) {\n const jobPath = path.join(statusDir, file);\n const content = await fs.readFile(jobPath, 'utf-8');\n const job = JSON.parse(content) as Job;\n\n // Apply filters\n if (filters.type && job.type !== filters.type) continue;\n if (filters.userId && job.userId !== filters.userId) continue;\n\n jobs.push(job);\n }\n } catch (error) {\n // Directory might not exist yet\n continue;\n }\n }\n\n // Sort by created descending (newest first)\n jobs.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());\n\n // Apply pagination\n const offset = filters.offset || 0;\n const limit = filters.limit || 100;\n\n return jobs.slice(offset, offset + limit);\n }\n\n /**\n * Cancel a job\n */\n async cancelJob(jobId: JobId): Promise<boolean> {\n const job = await this.getJob(jobId);\n\n if (!job) {\n return false;\n }\n\n // Can only cancel pending or running jobs\n if (job.status !== 'pending' && job.status !== 'running') {\n return false;\n }\n\n const oldStatus = job.status;\n job.status = 'cancelled';\n job.completedAt = new Date().toISOString();\n\n await this.updateJob(job, oldStatus);\n return true;\n }\n\n /**\n * Clean up old completed/failed jobs (older than retention period)\n */\n async cleanupOldJobs(retentionHours: number = 24): Promise<number> {\n const cutoffTime = Date.now() - (retentionHours * 60 * 60 * 1000);\n let deletedCount = 0;\n\n const cleanupStatuses: JobStatus[] = ['complete', 'failed', 'cancelled'];\n\n for (const status of cleanupStatuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n\n for (const file of files) {\n const jobPath = path.join(statusDir, file);\n const content = await fs.readFile(jobPath, 'utf-8');\n const job = JSON.parse(content) as Job;\n\n if (job.completedAt) {\n const completedTime = new Date(job.completedAt).getTime();\n\n if (completedTime < cutoffTime) {\n await fs.unlink(jobPath);\n deletedCount++;\n }\n }\n }\n } catch (error) {\n console.error(`[JobQueue] Error cleaning up ${status} jobs:`, error);\n }\n }\n\n if (deletedCount > 0) {\n console.log(`[JobQueue] Cleaned up ${deletedCount} old jobs`);\n }\n\n return deletedCount;\n }\n\n /**\n * Get job file path\n */\n private getJobPath(jobId: JobId, status: JobStatus): string {\n return path.join(this.jobsDir, status, `${jobId}.json`);\n }\n\n /**\n * Get statistics about the queue\n */\n async getStats(): Promise<{\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n }> {\n const stats = {\n pending: 0,\n running: 0,\n complete: 0,\n failed: 0,\n cancelled: 0\n };\n\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n stats[status] = files.length;\n } catch (error) {\n // Directory might not exist yet\n stats[status] = 0;\n }\n }\n\n return stats;\n }\n}\n\n// Singleton instance\nlet jobQueue: JobQueue | null = null;\n\nexport function getJobQueue(): JobQueue {\n if (!jobQueue) {\n throw new Error('JobQueue not initialized. Call initializeJobQueue() first.');\n }\n return jobQueue;\n}\n\nexport async function initializeJobQueue(config: JobQueueConfig): Promise<JobQueue> {\n jobQueue = new JobQueue(config);\n await jobQueue.initialize();\n return jobQueue;\n}\n","/**\n * Job Worker Base Class\n *\n * Abstract worker that polls the job queue and processes jobs.\n * Subclasses implement specific job processing logic.\n */\n\nimport type { Job } from './types';\nimport { getJobQueue } from './job-queue';\n\nexport abstract class JobWorker {\n private running = false;\n private currentJob: Job | null = null;\n private pollIntervalMs: number;\n private errorBackoffMs: number;\n\n constructor(\n pollIntervalMs: number = 1000,\n errorBackoffMs: number = 5000\n ) {\n this.pollIntervalMs = pollIntervalMs;\n this.errorBackoffMs = errorBackoffMs;\n }\n\n /**\n * Start the worker (polls queue in loop)\n */\n async start(): Promise<void> {\n this.running = true;\n console.log(`[${this.getWorkerName()}] Started`);\n\n while (this.running) {\n try {\n const job = await this.pollNextJob();\n\n if (job) {\n await this.processJob(job);\n } else {\n // No jobs available, wait before polling again\n await this.sleep(this.pollIntervalMs);\n }\n } catch (error) {\n console.error(`[${this.getWorkerName()}] Error in main loop:`, error);\n // Back off on error to avoid tight error loops\n await this.sleep(this.errorBackoffMs);\n }\n }\n\n console.log(`[${this.getWorkerName()}] Stopped`);\n }\n\n /**\n * Stop the worker (graceful shutdown)\n */\n async stop(): Promise<void> {\n console.log(`[${this.getWorkerName()}] Stopping...`);\n this.running = false;\n\n // Wait for current job to finish (with timeout)\n const timeout = 60000; // 60 seconds\n const startTime = Date.now();\n\n while (this.currentJob && (Date.now() - startTime) < timeout) {\n await this.sleep(100);\n }\n\n if (this.currentJob) {\n console.warn(`[${this.getWorkerName()}] Forced shutdown while processing job ${this.currentJob.id}`);\n }\n }\n\n /**\n * Poll for next job to process\n */\n private async pollNextJob(): Promise<Job | null> {\n const jobQueue = getJobQueue();\n const job = await jobQueue.pollNextPendingJob();\n\n if (job && this.canProcessJob(job)) {\n return job;\n }\n\n return null;\n }\n\n /**\n * Process a job (handles state transitions and error handling)\n */\n private async processJob(job: Job): Promise<void> {\n this.currentJob = job;\n const jobQueue = getJobQueue();\n\n try {\n // Move to running state\n const oldStatus = job.status;\n job.status = 'running';\n job.startedAt = new Date().toISOString();\n await jobQueue.updateJob(job, oldStatus);\n\n console.log(`[${this.getWorkerName()}] 🔄 Processing job ${job.id} (type: ${job.type})`);\n\n // Execute job-specific logic\n await this.executeJob(job);\n\n // Move to complete state\n job.status = 'complete';\n job.completedAt = new Date().toISOString();\n await jobQueue.updateJob(job, 'running');\n\n console.log(`[${this.getWorkerName()}] ✅ Job ${job.id} completed successfully`);\n\n } catch (error) {\n await this.handleJobFailure(job, error);\n } finally {\n this.currentJob = null;\n }\n }\n\n /**\n * Handle job failure (retry or move to failed)\n */\n protected async handleJobFailure(job: Job, error: any): Promise<void> {\n const jobQueue = getJobQueue();\n job.retryCount++;\n\n if (job.retryCount < job.maxRetries) {\n console.log(`[${this.getWorkerName()}] Job ${job.id} failed, will retry (${job.retryCount}/${job.maxRetries})`);\n console.log(`[${this.getWorkerName()}] Error:`, error);\n\n // Move back to pending for retry\n job.status = 'pending';\n job.startedAt = undefined; // Clear start time for retry\n await jobQueue.updateJob(job, 'running');\n\n } else {\n console.error(`[${this.getWorkerName()}] ❌ Job ${job.id} failed permanently after ${job.retryCount} retries`);\n console.error(`[${this.getWorkerName()}] Error:`, error);\n\n // Move to failed state\n job.status = 'failed';\n job.error = error instanceof Error ? error.message : String(error);\n job.completedAt = new Date().toISOString();\n await jobQueue.updateJob(job, 'running');\n }\n }\n\n /**\n * Update job progress (best-effort, doesn't throw)\n */\n protected async updateJobProgress(job: Job): Promise<void> {\n try {\n const jobQueue = getJobQueue();\n await jobQueue.updateJob(job);\n } catch (error) {\n console.warn(`[${this.getWorkerName()}] Failed to update job progress:`, error);\n // Don't throw - progress updates are best-effort\n }\n }\n\n /**\n * Sleep utility\n */\n protected sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n // Abstract methods to be implemented by subclasses\n\n /**\n * Get worker name (for logging)\n */\n protected abstract getWorkerName(): string;\n\n /**\n * Check if this worker can process the given job\n */\n protected abstract canProcessJob(job: Job): boolean;\n\n /**\n * Execute the job (job-specific logic)\n * This is where the actual work happens\n * Throw an error to trigger retry logic\n */\n protected abstract executeJob(job: Job): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/job-queue.ts","../src/job-worker.ts"],"names":["fs","jobQueue"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Job Queue Manager\n *\n * Filesystem-based job queue with atomic operations.\n * Jobs are stored in directories by status for easy polling.\n */\n\nimport { promises as fs } from 'fs';\nimport * as path from 'path';\nimport type { AnyJob, JobStatus, JobQueryFilters, CancelledJob } from './types';\nimport type { JobId } from '@semiont/api-client';\n\nexport interface JobQueueConfig {\n dataDir: string;\n}\n\nexport class JobQueue {\n private jobsDir: string;\n\n constructor(config: JobQueueConfig) {\n this.jobsDir = path.join(config.dataDir, 'jobs');\n }\n\n /**\n * Initialize job queue directories\n */\n async initialize(): Promise<void> {\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const dir = path.join(this.jobsDir, status);\n await fs.mkdir(dir, { recursive: true });\n }\n\n console.log('[JobQueue] Initialized job directories');\n }\n\n /**\n * Create a new job\n */\n async createJob(job: AnyJob): Promise<void> {\n const jobPath = this.getJobPath(job.metadata.id, job.status);\n await fs.writeFile(jobPath, JSON.stringify(job, null, 2), 'utf-8');\n console.log(`[JobQueue] Created job ${job.metadata.id} with status ${job.status}`);\n }\n\n /**\n * Get a job by ID (searches all status directories)\n */\n async getJob(jobId: JobId): Promise<AnyJob | null> {\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const jobPath = this.getJobPath(jobId, status);\n try {\n const content = await fs.readFile(jobPath, 'utf-8');\n return JSON.parse(content) as AnyJob;\n } catch (error) {\n // File doesn't exist in this status directory, try next\n continue;\n }\n }\n\n return null;\n }\n\n /**\n * Update a job (atomic: delete old, write new)\n */\n async updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void> {\n // If oldStatus provided, delete from old location\n if (oldStatus && oldStatus !== job.status) {\n const oldPath = this.getJobPath(job.metadata.id, oldStatus);\n try {\n await fs.unlink(oldPath);\n } catch (error) {\n // Ignore if file doesn't exist\n }\n }\n\n // Write to new location\n const newPath = this.getJobPath(job.metadata.id, job.status);\n await fs.writeFile(newPath, JSON.stringify(job, null, 2), 'utf-8');\n\n if (oldStatus && oldStatus !== job.status) {\n console.log(`[JobQueue] Moved job ${job.metadata.id} from ${oldStatus} to ${job.status}`);\n } else {\n console.log(`[JobQueue] Updated job ${job.metadata.id} (status: ${job.status})`);\n }\n }\n\n /**\n * Poll for next pending job (FIFO)\n */\n async pollNextPendingJob(): Promise<AnyJob | null> {\n const pendingDir = path.join(this.jobsDir, 'pending');\n\n try {\n const files = await fs.readdir(pendingDir);\n\n if (files.length === 0) {\n return null;\n }\n\n // Sort by filename (job IDs have timestamps via nanoid)\n files.sort();\n\n const jobFile = files[0]!;\n const jobPath = path.join(pendingDir, jobFile);\n\n const content = await fs.readFile(jobPath, 'utf-8');\n return JSON.parse(content) as AnyJob;\n } catch (error) {\n console.error('[JobQueue] Error polling pending jobs:', error);\n return null;\n }\n }\n\n /**\n * List jobs with filters\n */\n async listJobs(filters: JobQueryFilters = {}): Promise<AnyJob[]> {\n const jobs: AnyJob[] = [];\n\n // Determine which status directories to scan\n const statuses: JobStatus[] = filters.status\n ? [filters.status]\n : ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n\n for (const file of files) {\n const jobPath = path.join(statusDir, file);\n const content = await fs.readFile(jobPath, 'utf-8');\n const job = JSON.parse(content) as AnyJob;\n\n // Apply filters\n if (filters.type && job.metadata.type !== filters.type) continue;\n if (filters.userId && job.metadata.userId !== filters.userId) continue;\n\n jobs.push(job);\n }\n } catch (error) {\n // Directory might not exist yet\n continue;\n }\n }\n\n // Sort by created descending (newest first)\n jobs.sort((a, b) => new Date(b.metadata.created).getTime() - new Date(a.metadata.created).getTime());\n\n // Apply pagination\n const offset = filters.offset || 0;\n const limit = filters.limit || 100;\n\n return jobs.slice(offset, offset + limit);\n }\n\n /**\n * Cancel a job\n */\n async cancelJob(jobId: JobId): Promise<boolean> {\n const job = await this.getJob(jobId);\n\n if (!job) {\n return false;\n }\n\n // Can only cancel pending or running jobs\n if (job.status !== 'pending' && job.status !== 'running') {\n return false;\n }\n\n const oldStatus = job.status;\n\n // Create cancelled job with proper structure\n const cancelledJob: CancelledJob<any> = {\n status: 'cancelled',\n metadata: job.metadata,\n params: job.status === 'pending' ? job.params : job.params,\n startedAt: job.status === 'running' ? job.startedAt : undefined,\n completedAt: new Date().toISOString(),\n };\n\n await this.updateJob(cancelledJob, oldStatus);\n return true;\n }\n\n /**\n * Clean up old completed/failed jobs (older than retention period)\n */\n async cleanupOldJobs(retentionHours: number = 24): Promise<number> {\n const cutoffTime = Date.now() - (retentionHours * 60 * 60 * 1000);\n let deletedCount = 0;\n\n const cleanupStatuses: JobStatus[] = ['complete', 'failed', 'cancelled'];\n\n for (const status of cleanupStatuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n\n for (const file of files) {\n const jobPath = path.join(statusDir, file);\n const content = await fs.readFile(jobPath, 'utf-8');\n const job = JSON.parse(content) as AnyJob;\n\n if (job.status === 'complete' || job.status === 'failed' || job.status === 'cancelled') {\n const completedTime = new Date(job.completedAt).getTime();\n\n if (completedTime < cutoffTime) {\n await fs.unlink(jobPath);\n deletedCount++;\n }\n }\n }\n } catch (error) {\n console.error(`[JobQueue] Error cleaning up ${status} jobs:`, error);\n }\n }\n\n if (deletedCount > 0) {\n console.log(`[JobQueue] Cleaned up ${deletedCount} old jobs`);\n }\n\n return deletedCount;\n }\n\n /**\n * Get job file path\n */\n private getJobPath(jobId: JobId, status: JobStatus): string {\n return path.join(this.jobsDir, status, `${jobId}.json`);\n }\n\n /**\n * Get statistics about the queue\n */\n async getStats(): Promise<{\n pending: number;\n running: number;\n complete: number;\n failed: number;\n cancelled: number;\n }> {\n const stats = {\n pending: 0,\n running: 0,\n complete: 0,\n failed: 0,\n cancelled: 0\n };\n\n const statuses: JobStatus[] = ['pending', 'running', 'complete', 'failed', 'cancelled'];\n\n for (const status of statuses) {\n const statusDir = path.join(this.jobsDir, status);\n\n try {\n const files = await fs.readdir(statusDir);\n stats[status] = files.length;\n } catch (error) {\n // Directory might not exist yet\n stats[status] = 0;\n }\n }\n\n return stats;\n }\n}\n\n// Singleton instance\nlet jobQueue: JobQueue | null = null;\n\nexport function getJobQueue(): JobQueue {\n if (!jobQueue) {\n throw new Error('JobQueue not initialized. Call initializeJobQueue() first.');\n }\n return jobQueue;\n}\n\nexport async function initializeJobQueue(config: JobQueueConfig): Promise<JobQueue> {\n jobQueue = new JobQueue(config);\n await jobQueue.initialize();\n return jobQueue;\n}\n","/**\n * Job Worker Base Class\n *\n * Abstract worker that polls the job queue and processes jobs.\n * Subclasses implement specific job processing logic.\n */\n\nimport type { AnyJob, RunningJob, CompleteJob, FailedJob, PendingJob } from './types';\nimport type { JobQueue } from './job-queue';\n\nexport abstract class JobWorker {\n private running = false;\n private currentJob: AnyJob | null = null;\n private pollIntervalMs: number;\n private errorBackoffMs: number;\n protected jobQueue: JobQueue;\n\n constructor(\n jobQueue: JobQueue,\n pollIntervalMs: number = 1000,\n errorBackoffMs: number = 5000\n ) {\n this.jobQueue = jobQueue;\n this.pollIntervalMs = pollIntervalMs;\n this.errorBackoffMs = errorBackoffMs;\n }\n\n /**\n * Start the worker (polls queue in loop)\n */\n async start(): Promise<void> {\n this.running = true;\n console.log(`[${this.getWorkerName()}] Started`);\n\n while (this.running) {\n try {\n const job = await this.pollNextJob();\n\n if (job) {\n await this.processJob(job);\n } else {\n // No jobs available, wait before polling again\n await this.sleep(this.pollIntervalMs);\n }\n } catch (error) {\n console.error(`[${this.getWorkerName()}] Error in main loop:`, error);\n // Back off on error to avoid tight error loops\n await this.sleep(this.errorBackoffMs);\n }\n }\n\n console.log(`[${this.getWorkerName()}] Stopped`);\n }\n\n /**\n * Stop the worker (graceful shutdown)\n */\n async stop(): Promise<void> {\n console.log(`[${this.getWorkerName()}] Stopping...`);\n this.running = false;\n\n // Wait for current job to finish (with timeout)\n const timeout = 60000; // 60 seconds\n const startTime = Date.now();\n\n while (this.currentJob && (Date.now() - startTime) < timeout) {\n await this.sleep(100);\n }\n\n if (this.currentJob) {\n console.warn(`[${this.getWorkerName()}] Forced shutdown while processing job ${this.currentJob.metadata.id}`);\n }\n }\n\n /**\n * Poll for next job to process\n */\n private async pollNextJob(): Promise<AnyJob | null> {\n const job = await this.jobQueue.pollNextPendingJob();\n\n if (job && this.canProcessJob(job)) {\n return job;\n }\n\n return null;\n }\n\n /**\n * Process a job (handles state transitions and error handling)\n */\n private async processJob(job: AnyJob): Promise<void> {\n this.currentJob = job;\n\n try {\n // Only process pending jobs\n if (job.status !== 'pending') {\n console.warn(`[${this.getWorkerName()}] Skipping non-pending job ${job.metadata.id}`);\n return;\n }\n\n // Create running job\n const runningJob: RunningJob<any, any> = {\n status: 'running',\n metadata: job.metadata,\n params: job.params,\n startedAt: new Date().toISOString(),\n progress: {}, // Initialize with empty progress\n };\n\n await this.jobQueue.updateJob(runningJob, 'pending');\n\n console.log(`[${this.getWorkerName()}] 🔄 Processing job ${job.metadata.id} (type: ${job.metadata.type})`);\n\n // Execute job-specific logic (passing running job)\n await this.executeJob(runningJob);\n\n // Move to complete state\n const completeJob: CompleteJob<any, any> = {\n status: 'complete',\n metadata: runningJob.metadata,\n params: runningJob.params,\n startedAt: runningJob.startedAt,\n completedAt: new Date().toISOString(),\n result: {}, // Subclass should set this via updateJobProgress\n };\n\n await this.jobQueue.updateJob(completeJob, 'running');\n\n console.log(`[${this.getWorkerName()}] ✅ Job ${job.metadata.id} completed successfully`);\n\n } catch (error) {\n await this.handleJobFailure(job, error);\n } finally {\n this.currentJob = null;\n }\n }\n\n /**\n * Handle job failure (retry or move to failed)\n */\n protected async handleJobFailure(job: AnyJob, error: any): Promise<void> {\n const updatedMetadata = {\n ...job.metadata,\n retryCount: job.metadata.retryCount + 1,\n };\n\n if (updatedMetadata.retryCount < updatedMetadata.maxRetries) {\n console.log(`[${this.getWorkerName()}] Job ${job.metadata.id} failed, will retry (${updatedMetadata.retryCount}/${updatedMetadata.maxRetries})`);\n console.log(`[${this.getWorkerName()}] Error:`, error);\n\n // Move back to pending for retry\n const retryJob: PendingJob<any> = {\n status: 'pending',\n metadata: updatedMetadata,\n params: job.status === 'pending' ? job.params : job.params,\n };\n\n await this.jobQueue.updateJob(retryJob, job.status);\n\n } else {\n console.error(`[${this.getWorkerName()}] ❌ Job ${job.metadata.id} failed permanently after ${updatedMetadata.retryCount} retries`);\n console.error(`[${this.getWorkerName()}] Error:`, error);\n\n // Move to failed state\n const failedJob: FailedJob<any> = {\n status: 'failed',\n metadata: updatedMetadata,\n params: job.status === 'pending' ? job.params : job.params,\n startedAt: job.status === 'running' ? job.startedAt : undefined,\n completedAt: new Date().toISOString(),\n error: error instanceof Error ? error.message : String(error),\n };\n\n await this.jobQueue.updateJob(failedJob, job.status);\n }\n }\n\n /**\n * Update job progress (best-effort, doesn't throw)\n */\n protected async updateJobProgress(job: AnyJob): Promise<void> {\n try {\n await this.jobQueue.updateJob(job);\n } catch (error) {\n console.warn(`[${this.getWorkerName()}] Failed to update job progress:`, error);\n // Don't throw - progress updates are best-effort\n }\n }\n\n /**\n * Sleep utility\n */\n protected sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n // Abstract methods to be implemented by subclasses\n\n /**\n * Get worker name (for logging)\n */\n protected abstract getWorkerName(): string;\n\n /**\n * Check if this worker can process the given job\n */\n protected abstract canProcessJob(job: AnyJob): boolean;\n\n /**\n * Execute the job (job-specific logic)\n * This is where the actual work happens\n * Throw an error to trigger retry logic\n */\n protected abstract executeJob(job: AnyJob): Promise<void>;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semiont/jobs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Filesystem-based job queue and worker infrastructure",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run typecheck && tsup",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
-
"test": "vitest"
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"test:coverage": "vitest run --coverage"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@semiont/api-client": "*",
|
|
25
26
|
"@semiont/core": "*"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
29
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
28
30
|
"tsup": "^8.0.1",
|
|
29
31
|
"typescript": "^5.6.3",
|
|
30
32
|
"vitest": "^2.1.8"
|