@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 CHANGED
@@ -1,7 +1,9 @@
1
1
  # @semiont/jobs
2
2
 
3
3
  [![Tests](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml/badge.svg)](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml?query=branch%3Amain+is%3Asuccess+job%3A%22Test+jobs%22)
4
+ [![codecov](https://codecov.io/gh/The-AI-Alliance/semiont/graph/badge.svg?flag=jobs)](https://codecov.io/gh/The-AI-Alliance/semiont?flag=jobs)
4
5
  [![npm version](https://img.shields.io/npm/v/@semiont/jobs.svg)](https://www.npmjs.com/package/@semiont/jobs)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@semiont/jobs.svg)](https://www.npmjs.com/package/@semiont/jobs)
5
7
  [![License](https://img.shields.io/npm/l/@semiont/jobs.svg)](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 GenerationJob,
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: GenerationJob = {
49
- id: jobId('job-abc123'),
50
- type: 'generation',
53
+ const job: PendingJob<GenerationParams> = {
51
54
  status: 'pending',
52
- userId: userId('user@example.com'),
53
- referenceId: annotationId('ref-123'),
54
- sourceResourceId: resourceId('doc-456'),
55
- title: 'Generated Article',
56
- created: new Date().toISOString(),
57
- retryCount: 0,
58
- maxRetries: 3,
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: Job): boolean {
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: Job): Promise<void> {
74
- const genJob = job as GenerationJob;
75
- console.log(`Generating resource: ${genJob.title}`);
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 are JSON documents that represent async work:
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 { GenerationJob } from '@semiont/jobs';
134
+ import type { PendingJob, RunningJob, CompleteJob, GenerationParams, GenerationProgress, GenerationResult } from '@semiont/jobs';
119
135
 
120
- const job: GenerationJob = {
121
- id: jobId('job-123'),
122
- type: 'generation',
136
+ // Pending job - waiting to be processed
137
+ const pendingJob: PendingJob<GenerationParams> = {
123
138
  status: 'pending',
124
- userId: userId('user@example.com'),
125
-
126
- // Job-specific fields
127
- referenceId: annotationId('ref-456'),
128
- sourceResourceId: resourceId('doc-789'),
129
- title: 'AI Generated Article',
130
- prompt: 'Write about quantum computing',
131
- language: 'en-US',
132
-
133
- // Timestamps
134
- created: '2024-01-01T00:00:00Z',
135
- startedAt: undefined, // Set when worker picks up job
136
- completedAt: undefined, // Set when job finishes
137
-
138
- // Retry handling
139
- retryCount: 0,
140
- maxRetries: 3,
141
- error: undefined, // Set if job fails
142
-
143
- // Progress tracking (optional)
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
- // Result (optional)
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
- DetectionJob, // Entity detection in resources
165
- GenerationJob, // AI content generation
166
- HighlightDetectionJob, // Identify key passages
167
- AssessmentDetectionJob, // Generate evaluative comments
168
- CommentDetectionJob, // Generate explanatory comments
169
- TagDetectionJob, // Structural role detection
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 Job } from '@semiont/jobs';
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: Job): boolean {
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: Job): Promise<void> {
206
- // 1. Access job data
207
- const customJob = job as CustomJob;
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
- // 2. Perform async work
210
- const result = await doWork(customJob);
245
+ // 3. Perform async work
246
+ const result = await doWork(params);
211
247
 
212
- // 3. Update job with results
213
- customJob.result = result;
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
- "userId": "user@example.com",
348
- "referenceId": "ref-456",
349
- "sourceResourceId": "doc-789",
350
- "title": "Generated Article",
351
- "created": "2024-01-01T00:00:00Z",
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: Job): Promise<void> {
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.status = 'pending';
405
- job.retryCount++;
406
- delete job.error;
407
- await queue.updateJob(job, 'failed');
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: GenerationJob = {
427
- id: jobId('test-1'),
428
- type: 'generation',
483
+ const job: PendingJob<GenerationParams> = {
429
484
  status: 'pending',
430
- // ... other fields
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 Job, type GenerationJob } from '@semiont/jobs';
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: Job): boolean {
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: Job): Promise<void> {
466
- const genJob = job as GenerationJob;
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
- genJob.progress = {
470
- stage: 'generating',
471
- percentage: 0,
472
- message: 'Starting generation...',
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(genJob);
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
- genJob.progress = {
486
- stage: 'creating',
487
- percentage: 75,
488
- message: 'Creating resource...',
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(genJob);
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
- genJob.result = {
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
- if (job.progress) {
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
- * They are completely independent of HTTP request/response cycles.
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
- * Base job interface - all jobs extend this
20
+ * Job metadata - common to all states
15
21
  */
16
- interface BaseJob {
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 - finds entities in a resource using AI inference
31
+ * Detection job parameters
30
32
  */
31
- interface DetectionJob extends BaseJob {
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 - generates a new resource using AI inference
39
+ * Generation job parameters
51
40
  */
52
- interface GenerationJob extends BaseJob {
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 Detection job - finds passages to highlight using AI
53
+ * Highlight detection job parameters
75
54
  */
76
- interface HighlightDetectionJob extends BaseJob {
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 Detection job - evaluates passages using AI
61
+ * Assessment detection job parameters
93
62
  */
94
- interface AssessmentDetectionJob extends BaseJob {
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 Detection job - generates explanatory comments on passages using AI
70
+ * Comment detection job parameters
112
71
  */
113
- interface CommentDetectionJob extends BaseJob {
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 Detection job - identifies passages serving structural roles using AI
79
+ * Tag detection job parameters
131
80
  */
132
- interface TagDetectionJob extends BaseJob {
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
- * Discriminated union of all job types
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
- type Job = DetectionJob | GenerationJob | HighlightDetectionJob | AssessmentDetectionJob | CommentDetectionJob | TagDetectionJob;
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
- * Job query filters
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: Job): Promise<void>;
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<Job | null>;
285
+ getJob(jobId: JobId): Promise<AnyJob | null>;
191
286
  /**
192
287
  * Update a job (atomic: delete old, write new)
193
288
  */
194
- updateJob(job: Job, oldStatus?: JobStatus): Promise<void>;
289
+ updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void>;
195
290
  /**
196
291
  * Poll for next pending job (FIFO)
197
292
  */
198
- pollNextPendingJob(): Promise<Job | null>;
293
+ pollNextPendingJob(): Promise<AnyJob | null>;
199
294
  /**
200
295
  * List jobs with filters
201
296
  */
202
- listJobs(filters?: JobQueryFilters): Promise<Job[]>;
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
- constructor(pollIntervalMs?: number, errorBackoffMs?: number);
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: Job, error: any): Promise<void>;
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: Job): Promise<void>;
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: Job): boolean;
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: Job): Promise<void>;
379
+ protected abstract executeJob(job: AnyJob): Promise<void>;
284
380
  }
285
381
 
286
- export { type AssessmentDetectionJob, type BaseJob, type CommentDetectionJob, type DetectionJob, type GenerationJob, type HighlightDetectionJob, type Job, type JobQueryFilters, JobQueue, type JobQueueConfig, type JobStatus, type JobType, JobWorker, type TagDetectionJob, getJobQueue, initializeJobQueue };
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
- job.status = "cancelled";
124
- job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
125
- await this.updateJob(job, oldStatus);
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.completedAt) {
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
- constructor(pollIntervalMs = 1e3, errorBackoffMs = 5e3) {
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 jobQueue2 = getJobQueue();
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
- const oldStatus = job.status;
268
- job.status = "running";
269
- job.startedAt = (/* @__PURE__ */ new Date()).toISOString();
270
- await jobQueue2.updateJob(job, oldStatus);
271
- console.log(`[${this.getWorkerName()}] \u{1F504} Processing job ${job.id} (type: ${job.type})`);
272
- await this.executeJob(job);
273
- job.status = "complete";
274
- job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
275
- await jobQueue2.updateJob(job, "running");
276
- console.log(`[${this.getWorkerName()}] \u2705 Job ${job.id} completed successfully`);
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 jobQueue2 = getJobQueue();
288
- job.retryCount++;
289
- if (job.retryCount < job.maxRetries) {
290
- console.log(`[${this.getWorkerName()}] Job ${job.id} failed, will retry (${job.retryCount}/${job.maxRetries})`);
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
- job.status = "pending";
293
- job.startedAt = void 0;
294
- await jobQueue2.updateJob(job, "running");
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 ${job.retryCount} retries`);
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
- job.status = "failed";
299
- job.error = error instanceof Error ? error.message : String(error);
300
- job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
301
- await jobQueue2.updateJob(job, "running");
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
- const jobQueue2 = getJobQueue();
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":"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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 { 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.30",
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"