@semiont/jobs 0.2.45 → 0.2.46
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 +92 -553
- package/dist/index.d.ts +314 -2
- package/dist/index.js +12422 -330
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,18 +6,13 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@semiont/jobs)
|
|
7
7
|
[](https://github.com/The-AI-Alliance/semiont/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
Filesystem-based job queue
|
|
9
|
+
Filesystem-based job queue, worker infrastructure, and annotation workers for [Semiont](https://github.com/The-AI-Alliance/semiont).
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Architecture Context
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
In production, the job queue and workers are created by `@semiont/make-meaning`'s `startMakeMeaning()` function. Workers emit commands on the **EventBus** — the **Stower** actor (in @semiont/make-meaning) handles all persistence to the Knowledge Base.
|
|
14
14
|
|
|
15
|
-
**
|
|
16
|
-
- **Decoupled processing** - HTTP responses return immediately while work continues
|
|
17
|
-
- **Reliability** - Jobs are persisted to disk and survive process restarts
|
|
18
|
-
- **Progress tracking** - Long-running tasks can report status updates
|
|
19
|
-
- **Retry logic** - Failed jobs can be retried with exponential backoff
|
|
20
|
-
- **Scalability** - Multiple workers can process jobs concurrently
|
|
15
|
+
Workers are **not** actors. They use a polling loop, not RxJS subscriptions. But they emit the same EventBus commands as any other caller in the system.
|
|
21
16
|
|
|
22
17
|
## Installation
|
|
23
18
|
|
|
@@ -25,43 +20,33 @@ A job queue is a pattern for processing work asynchronously outside of the HTTP
|
|
|
25
20
|
npm install @semiont/jobs
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
**
|
|
29
|
-
-
|
|
30
|
-
- `@semiont/
|
|
31
|
-
|
|
32
|
-
## Architecture Context
|
|
33
|
-
|
|
34
|
-
**Infrastructure Ownership**: In production applications, the job queue is **created and managed by [@semiont/make-meaning](../make-meaning/)'s `startMakeMeaning()` function**, which serves as the single orchestration point for all infrastructure components (EventStore, GraphDB, RepStore, InferenceClient, JobQueue, Workers).
|
|
35
|
-
|
|
36
|
-
The quick start example below shows direct initialization for **testing, CLI tools, or standalone workers**. For backend integration, access the job queue through the `makeMeaning` context object.
|
|
23
|
+
**Dependencies:**
|
|
24
|
+
- `@semiont/core` — Core types, EventBus
|
|
25
|
+
- `@semiont/api-client` — OpenAPI types
|
|
26
|
+
- `@semiont/inference` — InferenceClient for AI operations
|
|
37
27
|
|
|
38
28
|
## Quick Start
|
|
39
29
|
|
|
40
30
|
```typescript
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
initializeJobQueue,
|
|
44
|
-
getJobQueue,
|
|
45
|
-
JobWorker,
|
|
46
|
-
type PendingJob,
|
|
47
|
-
type RunningJob,
|
|
48
|
-
type GenerationParams,
|
|
49
|
-
type AnyJob,
|
|
50
|
-
} from '@semiont/jobs';
|
|
31
|
+
import { JobQueue, type PendingJob, type GenerationParams } from '@semiont/jobs';
|
|
32
|
+
import { EventBus, userId, resourceId, annotationId } from '@semiont/core';
|
|
51
33
|
import { jobId } from '@semiont/api-client';
|
|
52
|
-
import { userId, resourceId, annotationId } from '@semiont/core';
|
|
53
34
|
|
|
54
|
-
//
|
|
55
|
-
|
|
35
|
+
// Initialize
|
|
36
|
+
const eventBus = new EventBus();
|
|
37
|
+
const jobQueue = new JobQueue({ dataDir: './data' }, logger, eventBus);
|
|
38
|
+
await jobQueue.initialize();
|
|
56
39
|
|
|
57
|
-
//
|
|
58
|
-
const jobQueue = getJobQueue();
|
|
40
|
+
// Create a job
|
|
59
41
|
const job: PendingJob<GenerationParams> = {
|
|
60
42
|
status: 'pending',
|
|
61
43
|
metadata: {
|
|
62
44
|
id: jobId('job-abc123'),
|
|
63
45
|
type: 'generation',
|
|
64
46
|
userId: userId('user@example.com'),
|
|
47
|
+
userName: 'Jane Doe',
|
|
48
|
+
userEmail: 'jane@example.com',
|
|
49
|
+
userDomain: 'example.com',
|
|
65
50
|
created: new Date().toISOString(),
|
|
66
51
|
retryCount: 0,
|
|
67
52
|
maxRetries: 3,
|
|
@@ -69,6 +54,8 @@ const job: PendingJob<GenerationParams> = {
|
|
|
69
54
|
params: {
|
|
70
55
|
referenceId: annotationId('ref-123'),
|
|
71
56
|
sourceResourceId: resourceId('doc-456'),
|
|
57
|
+
sourceResourceName: 'Source Document',
|
|
58
|
+
annotation: { /* full W3C Annotation */ },
|
|
72
59
|
title: 'Generated Article',
|
|
73
60
|
prompt: 'Write about AI',
|
|
74
61
|
language: 'en-US',
|
|
@@ -76,562 +63,120 @@ const job: PendingJob<GenerationParams> = {
|
|
|
76
63
|
};
|
|
77
64
|
|
|
78
65
|
await jobQueue.createJob(job);
|
|
79
|
-
|
|
80
|
-
// 3. Create a worker to process jobs
|
|
81
|
-
class MyGenerationWorker extends JobWorker {
|
|
82
|
-
protected getWorkerName(): string {
|
|
83
|
-
return 'MyGenerationWorker';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
protected canProcessJob(job: AnyJob): boolean {
|
|
87
|
-
return job.metadata.type === 'generation';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
protected async executeJob(job: AnyJob): Promise<void> {
|
|
91
|
-
// Type guard ensures job is running
|
|
92
|
-
if (job.status !== 'running') {
|
|
93
|
-
throw new Error('Job must be running');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const genJob = job as RunningJob<GenerationParams>;
|
|
97
|
-
console.log(`Generating resource: ${genJob.params.title}`);
|
|
98
|
-
// Your processing logic here
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 4. Start worker
|
|
103
|
-
const worker = new MyGenerationWorker();
|
|
104
|
-
await worker.start();
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Architecture
|
|
108
|
-
|
|
109
|
-
The jobs package follows a simple status-directory pattern:
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
data/
|
|
113
|
-
jobs/
|
|
114
|
-
pending/ ← Jobs waiting to be processed
|
|
115
|
-
job-123.json
|
|
116
|
-
job-456.json
|
|
117
|
-
running/ ← Jobs currently being processed
|
|
118
|
-
job-789.json
|
|
119
|
-
complete/ ← Successfully completed jobs
|
|
120
|
-
job-111.json
|
|
121
|
-
failed/ ← Failed jobs (with error info)
|
|
122
|
-
job-222.json
|
|
123
|
-
cancelled/ ← Cancelled jobs
|
|
124
|
-
job-333.json
|
|
125
66
|
```
|
|
126
67
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- **JobQueue** - Manages job lifecycle and persistence
|
|
130
|
-
- **JobWorker** - Abstract base class for workers that process jobs
|
|
131
|
-
- **Job Types** - Strongly-typed job definitions for different task types
|
|
132
|
-
|
|
133
|
-
## Core Concepts
|
|
134
|
-
|
|
135
|
-
### Jobs
|
|
136
|
-
|
|
137
|
-
Jobs use discriminated unions based on their status, ensuring type safety and preventing invalid state access:
|
|
68
|
+
## Job Types
|
|
138
69
|
|
|
139
70
|
```typescript
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
type: 'generation',
|
|
148
|
-
userId: userId('user@example.com'),
|
|
149
|
-
created: '2024-01-01T00:00:00Z',
|
|
150
|
-
retryCount: 0,
|
|
151
|
-
maxRetries: 3,
|
|
152
|
-
},
|
|
153
|
-
params: {
|
|
154
|
-
referenceId: annotationId('ref-456'),
|
|
155
|
-
sourceResourceId: resourceId('doc-789'),
|
|
156
|
-
title: 'AI Generated Article',
|
|
157
|
-
prompt: 'Write about quantum computing',
|
|
158
|
-
language: 'en-US',
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// Running job - currently being processed
|
|
163
|
-
const runningJob: RunningJob<GenerationParams, GenerationProgress> = {
|
|
164
|
-
status: 'running',
|
|
165
|
-
metadata: { /* same as above */ },
|
|
166
|
-
params: { /* same as above */ },
|
|
167
|
-
startedAt: '2024-01-01T00:01:00Z',
|
|
168
|
-
progress: {
|
|
169
|
-
stage: 'generating',
|
|
170
|
-
percentage: 45,
|
|
171
|
-
message: 'Generating content...',
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// Complete job - successfully finished
|
|
176
|
-
const completeJob: CompleteJob<GenerationParams, GenerationResult> = {
|
|
177
|
-
status: 'complete',
|
|
178
|
-
metadata: { /* same as above */ },
|
|
179
|
-
params: { /* same as above */ },
|
|
180
|
-
startedAt: '2024-01-01T00:01:00Z',
|
|
181
|
-
completedAt: '2024-01-01T00:05:00Z',
|
|
182
|
-
result: {
|
|
183
|
-
resourceId: resourceId('doc-new'),
|
|
184
|
-
resourceName: 'Generated Article',
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// TypeScript prevents accessing progress on pending jobs!
|
|
189
|
-
// pendingJob.progress // ❌ Compile error
|
|
190
|
-
// runningJob.progress // ✅ Available
|
|
191
|
-
// completeJob.result // ✅ Available
|
|
71
|
+
type JobType =
|
|
72
|
+
| 'reference-annotation' // Entity reference detection
|
|
73
|
+
| 'generation' // AI content generation
|
|
74
|
+
| 'highlight-annotation' // Key passage highlighting
|
|
75
|
+
| 'assessment-annotation' // Evaluative assessments
|
|
76
|
+
| 'comment-annotation' // Explanatory comments
|
|
77
|
+
| 'tag-annotation' // Structural role tagging
|
|
192
78
|
```
|
|
193
79
|
|
|
194
|
-
|
|
80
|
+
## Job Metadata
|
|
195
81
|
|
|
196
|
-
|
|
82
|
+
All jobs share common metadata:
|
|
197
83
|
|
|
198
84
|
```typescript
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
### Job Status
|
|
210
|
-
|
|
211
|
-
Jobs progress through status states stored as directories:
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
type JobStatus =
|
|
215
|
-
| 'pending' // Waiting to be processed
|
|
216
|
-
| 'running' // Currently being processed
|
|
217
|
-
| 'complete' // Successfully finished
|
|
218
|
-
| 'failed' // Failed with error
|
|
219
|
-
| 'cancelled' // Cancelled by user
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Workers
|
|
223
|
-
|
|
224
|
-
Workers poll the queue and process jobs:
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
import { JobWorker, type AnyJob, type RunningJob, type CustomParams } from '@semiont/jobs';
|
|
228
|
-
|
|
229
|
-
class CustomWorker extends JobWorker {
|
|
230
|
-
// Worker identification
|
|
231
|
-
protected getWorkerName(): string {
|
|
232
|
-
return 'CustomWorker';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Filter which jobs this worker processes
|
|
236
|
-
protected canProcessJob(job: AnyJob): boolean {
|
|
237
|
-
return job.metadata.type === 'custom-type';
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Implement job processing logic
|
|
241
|
-
protected async executeJob(job: AnyJob): Promise<void> {
|
|
242
|
-
// 1. Type guard - job must be running
|
|
243
|
-
if (job.status !== 'running') {
|
|
244
|
-
throw new Error('Job must be running');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 2. Access typed job data
|
|
248
|
-
const customJob = job as RunningJob<CustomParams>;
|
|
249
|
-
const params = customJob.params;
|
|
250
|
-
|
|
251
|
-
// 3. Perform async work
|
|
252
|
-
const result = await doWork(params);
|
|
253
|
-
|
|
254
|
-
// 4. Create updated job with result (immutable pattern)
|
|
255
|
-
const updatedJob: RunningJob<CustomParams> = {
|
|
256
|
-
...customJob,
|
|
257
|
-
progress: { stage: 'complete', percentage: 100 },
|
|
258
|
-
};
|
|
259
|
-
await this.updateJobProgress(updatedJob);
|
|
260
|
-
}
|
|
85
|
+
interface JobMetadata {
|
|
86
|
+
id: JobId;
|
|
87
|
+
type: JobType;
|
|
88
|
+
userId: UserId;
|
|
89
|
+
userName: string; // For building W3C Agent creator
|
|
90
|
+
userEmail: string; // For building W3C Agent creator
|
|
91
|
+
userDomain: string; // For building W3C Agent creator
|
|
92
|
+
created: string;
|
|
93
|
+
retryCount: number;
|
|
94
|
+
maxRetries: number;
|
|
261
95
|
}
|
|
262
96
|
```
|
|
263
97
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
📚 **[Job Queue Guide](./docs/JobQueue.md)** - JobQueue API and job management
|
|
267
|
-
|
|
268
|
-
👷 **[Workers Guide](./docs/Workers.md)** - Building custom workers
|
|
269
|
-
|
|
270
|
-
📝 **[Job Types Guide](./docs/JobTypes.md)** - All job type definitions and usage
|
|
271
|
-
|
|
272
|
-
🔷 **[Type System Guide](./docs/TYPES.md)** - Discriminated unions and type safety
|
|
273
|
-
|
|
274
|
-
⚙️ **[Configuration Guide](./docs/Configuration.md)** - Setup and options
|
|
275
|
-
|
|
276
|
-
## Key Features
|
|
277
|
-
|
|
278
|
-
- **Type-safe** - Full TypeScript support with discriminated union types
|
|
279
|
-
- **Filesystem-based** - No external database required (JSON files for jobs)
|
|
280
|
-
- **Status directories** - Jobs organized by status for easy polling
|
|
281
|
-
- **Atomic operations** - Safe concurrent access to job files
|
|
282
|
-
- **Progress tracking** - Jobs can report progress updates during processing
|
|
283
|
-
- **Retry logic** - Built-in retry handling with configurable max attempts
|
|
284
|
-
- **Framework-agnostic** - Pure TypeScript, no web framework dependencies
|
|
285
|
-
|
|
286
|
-
## Use Cases
|
|
287
|
-
|
|
288
|
-
✅ **AI generation** - Long-running LLM inference tasks
|
|
289
|
-
|
|
290
|
-
✅ **Background processing** - Resource analysis, entity detection
|
|
291
|
-
|
|
292
|
-
✅ **Worker microservices** - Separate processes for compute-intensive work
|
|
98
|
+
The `userName`, `userEmail`, and `userDomain` fields are used by workers to build the W3C `Agent` for annotation `creator` attribution via `userToAgent()`.
|
|
293
99
|
|
|
294
|
-
|
|
100
|
+
## Annotation Workers
|
|
295
101
|
|
|
296
|
-
|
|
102
|
+
Six workers process different annotation types:
|
|
297
103
|
|
|
298
|
-
|
|
104
|
+
| Worker | Job Type | Constructor |
|
|
105
|
+
|--------|----------|------------|
|
|
106
|
+
| `ReferenceAnnotationWorker` | `reference-annotation` | `(jobQueue, config, inferenceClient, eventBus, contentFetcher, logger)` |
|
|
107
|
+
| `GenerationWorker` | `generation` | `(jobQueue, config, inferenceClient, eventBus, logger)` |
|
|
108
|
+
| `HighlightAnnotationWorker` | `highlight-annotation` | `(jobQueue, config, inferenceClient, eventBus, contentFetcher, logger)` |
|
|
109
|
+
| `AssessmentAnnotationWorker` | `assessment-annotation` | `(jobQueue, config, inferenceClient, eventBus, contentFetcher, logger)` |
|
|
110
|
+
| `CommentAnnotationWorker` | `comment-annotation` | `(jobQueue, config, inferenceClient, eventBus, contentFetcher, logger)` |
|
|
111
|
+
| `TagAnnotationWorker` | `tag-annotation` | `(jobQueue, config, inferenceClient, eventBus, contentFetcher, logger)` |
|
|
299
112
|
|
|
300
|
-
|
|
113
|
+
Workers emit EventBus commands (`mark:create`, `job:start`, `job:complete`, etc.) — the Stower actor in @semiont/make-meaning handles persistence.
|
|
301
114
|
|
|
302
|
-
|
|
115
|
+
## Custom Workers
|
|
303
116
|
|
|
304
117
|
```typescript
|
|
305
|
-
|
|
118
|
+
import { JobWorker, type AnyJob } from '@semiont/jobs';
|
|
119
|
+
import type { Logger } from '@semiont/core';
|
|
306
120
|
|
|
307
|
-
// Create job
|
|
308
|
-
await queue.createJob(job);
|
|
309
|
-
|
|
310
|
-
// Get job by ID
|
|
311
|
-
const job = await queue.getJob(jobId);
|
|
312
|
-
|
|
313
|
-
// Poll for next pending job
|
|
314
|
-
const next = await queue.pollNextPendingJob();
|
|
315
|
-
|
|
316
|
-
// Update job status
|
|
317
|
-
job.status = 'complete';
|
|
318
|
-
await queue.updateJob(job, 'running');
|
|
319
|
-
|
|
320
|
-
// Query jobs by status
|
|
321
|
-
const pending = await queue.queryJobs({ status: 'pending' });
|
|
322
|
-
const failed = await queue.queryJobs({ status: 'failed' });
|
|
323
|
-
|
|
324
|
-
// Cleanup old jobs
|
|
325
|
-
await queue.cleanupCompletedJobs(Date.now() - 86400000); // 1 day ago
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### JobWorker
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
// Create worker
|
|
332
121
|
class MyWorker extends JobWorker {
|
|
333
|
-
constructor() {
|
|
334
|
-
super(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
122
|
+
constructor(jobQueue: JobQueue, logger: Logger) {
|
|
123
|
+
super(jobQueue, 1000, 5000, logger);
|
|
124
|
+
// ^^^^ ^^^^
|
|
125
|
+
// poll error backoff
|
|
338
126
|
}
|
|
339
127
|
|
|
340
128
|
protected getWorkerName(): string {
|
|
341
129
|
return 'MyWorker';
|
|
342
130
|
}
|
|
343
131
|
|
|
344
|
-
protected canProcessJob(job:
|
|
345
|
-
return job.type === '
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
protected async executeJob(job: Job): Promise<void> {
|
|
349
|
-
// Process job
|
|
132
|
+
protected canProcessJob(job: AnyJob): boolean {
|
|
133
|
+
return job.metadata.type === 'generation';
|
|
350
134
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Start worker
|
|
354
|
-
const worker = new MyWorker();
|
|
355
|
-
await worker.start();
|
|
356
|
-
|
|
357
|
-
// Stop worker (graceful shutdown)
|
|
358
|
-
await worker.stop();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Singleton Pattern
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
import { initializeJobQueue, getJobQueue } from '@semiont/jobs';
|
|
365
|
-
|
|
366
|
-
// Initialize once at startup
|
|
367
|
-
await initializeJobQueue({ dataDir: './data' });
|
|
368
|
-
|
|
369
|
-
// Get queue instance anywhere
|
|
370
|
-
const queue = getJobQueue();
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
## Storage Format
|
|
374
|
-
|
|
375
|
-
Jobs are stored as individual JSON files:
|
|
376
|
-
|
|
377
|
-
```
|
|
378
|
-
data/
|
|
379
|
-
jobs/
|
|
380
|
-
pending/
|
|
381
|
-
job-abc123.json
|
|
382
|
-
running/
|
|
383
|
-
job-def456.json
|
|
384
|
-
complete/
|
|
385
|
-
job-ghi789.json
|
|
386
|
-
```
|
|
387
135
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
```json
|
|
391
|
-
{
|
|
392
|
-
"status": "complete",
|
|
393
|
-
"metadata": {
|
|
394
|
-
"id": "job-abc123",
|
|
395
|
-
"type": "generation",
|
|
396
|
-
"userId": "user@example.com",
|
|
397
|
-
"created": "2024-01-01T00:00:00Z",
|
|
398
|
-
"retryCount": 0,
|
|
399
|
-
"maxRetries": 3
|
|
400
|
-
},
|
|
401
|
-
"params": {
|
|
402
|
-
"referenceId": "ref-456",
|
|
403
|
-
"sourceResourceId": "doc-789",
|
|
404
|
-
"title": "Generated Article",
|
|
405
|
-
"prompt": "Write about AI",
|
|
406
|
-
"language": "en-US"
|
|
407
|
-
},
|
|
408
|
-
"startedAt": "2024-01-01T00:01:00Z",
|
|
409
|
-
"completedAt": "2024-01-01T00:05:00Z",
|
|
410
|
-
"result": {
|
|
411
|
-
"resourceId": "doc-new",
|
|
412
|
-
"resourceName": "Generated Article"
|
|
136
|
+
protected async executeJob(job: AnyJob): Promise<any> {
|
|
137
|
+
// Your processing logic — return result object
|
|
413
138
|
}
|
|
414
139
|
}
|
|
415
140
|
```
|
|
416
141
|
|
|
417
|
-
##
|
|
418
|
-
|
|
419
|
-
- **Polling-based** - Workers poll pending directory at configurable intervals
|
|
420
|
-
- **Filesystem limits** - Performance degrades with >1000 pending jobs per directory
|
|
421
|
-
- **Atomic moves** - Jobs move between status directories atomically (delete + write)
|
|
422
|
-
- **No locks needed** - Status-based organization prevents race conditions
|
|
423
|
-
|
|
424
|
-
**Scaling considerations:**
|
|
425
|
-
- Multiple workers can run concurrently (same or different machines)
|
|
426
|
-
- Workers use `pollNextPendingJob()` for FIFO processing
|
|
427
|
-
- Completed jobs should be cleaned up periodically
|
|
428
|
-
- For high throughput (>1000 jobs/min), consider Redis/database-backed queue
|
|
429
|
-
|
|
430
|
-
## Error Handling
|
|
142
|
+
## Discriminated Unions
|
|
431
143
|
|
|
432
|
-
|
|
144
|
+
Jobs use TypeScript discriminated unions for type safety:
|
|
433
145
|
|
|
434
146
|
```typescript
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
await doWork(job);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
// JobWorker base class handles:
|
|
445
|
-
// 1. Moving job to 'failed' status
|
|
446
|
-
// 2. Recording error message
|
|
447
|
-
// 3. Retry logic (if retryCount < maxRetries)
|
|
448
|
-
throw error; // Let base class handle it
|
|
449
|
-
}
|
|
147
|
+
function handleJob(job: AnyJob) {
|
|
148
|
+
if (job.status === 'running') {
|
|
149
|
+
console.log(job.progress); // Available
|
|
150
|
+
// console.log(job.result); // Compile error
|
|
450
151
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
### Manual Retry
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
const queue = getJobQueue();
|
|
458
|
-
const failedJobs = await queue.queryJobs({ status: 'failed' });
|
|
459
|
-
|
|
460
|
-
for (const job of failedJobs) {
|
|
461
|
-
if (job.status === 'failed' && job.metadata.retryCount < job.metadata.maxRetries) {
|
|
462
|
-
// Create new pending job from failed job
|
|
463
|
-
const retryJob: PendingJob<any> = {
|
|
464
|
-
status: 'pending',
|
|
465
|
-
metadata: {
|
|
466
|
-
...job.metadata,
|
|
467
|
-
retryCount: job.metadata.retryCount + 1,
|
|
468
|
-
},
|
|
469
|
-
params: job.params,
|
|
470
|
-
};
|
|
471
|
-
await queue.updateJob(retryJob, 'failed');
|
|
152
|
+
if (job.status === 'complete') {
|
|
153
|
+
console.log(job.result); // Available
|
|
154
|
+
// console.log(job.progress); // Compile error
|
|
472
155
|
}
|
|
473
156
|
}
|
|
474
157
|
```
|
|
475
158
|
|
|
476
|
-
##
|
|
477
|
-
|
|
478
|
-
```typescript
|
|
479
|
-
import { initializeJobQueue, getJobQueue } from '@semiont/jobs';
|
|
480
|
-
import type { PendingJob, GenerationParams } from '@semiont/jobs';
|
|
481
|
-
import { describe, it, beforeEach } from 'vitest';
|
|
482
|
-
|
|
483
|
-
describe('Job queue', () => {
|
|
484
|
-
beforeEach(async () => {
|
|
485
|
-
await initializeJobQueue({ dataDir: './test-data' });
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('should create and retrieve jobs', async () => {
|
|
489
|
-
const queue = getJobQueue();
|
|
490
|
-
|
|
491
|
-
const job: PendingJob<GenerationParams> = {
|
|
492
|
-
status: 'pending',
|
|
493
|
-
metadata: {
|
|
494
|
-
id: jobId('test-1'),
|
|
495
|
-
type: 'generation',
|
|
496
|
-
userId: userId('user@test.com'),
|
|
497
|
-
created: new Date().toISOString(),
|
|
498
|
-
retryCount: 0,
|
|
499
|
-
maxRetries: 3,
|
|
500
|
-
},
|
|
501
|
-
params: {
|
|
502
|
-
referenceId: annotationId('ref-1'),
|
|
503
|
-
sourceResourceId: resourceId('doc-1'),
|
|
504
|
-
title: 'Test',
|
|
505
|
-
prompt: 'Test prompt',
|
|
506
|
-
language: 'en-US',
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
await queue.createJob(job);
|
|
511
|
-
const retrieved = await queue.getJob(jobId('test-1'));
|
|
512
|
-
|
|
513
|
-
expect(retrieved).toEqual(job);
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
## Examples
|
|
519
|
-
|
|
520
|
-
### Building a Background Worker
|
|
521
|
-
|
|
522
|
-
```typescript
|
|
523
|
-
import { JobWorker, type AnyJob, type RunningJob, type GenerationParams, type GenerationProgress } from '@semiont/jobs';
|
|
524
|
-
import { InferenceService } from './inference';
|
|
525
|
-
|
|
526
|
-
class GenerationWorker extends JobWorker {
|
|
527
|
-
private inference: InferenceService;
|
|
528
|
-
|
|
529
|
-
constructor(inference: InferenceService) {
|
|
530
|
-
super(1000, 5000);
|
|
531
|
-
this.inference = inference;
|
|
532
|
-
}
|
|
159
|
+
## Storage Format
|
|
533
160
|
|
|
534
|
-
|
|
535
|
-
return 'GenerationWorker';
|
|
536
|
-
}
|
|
161
|
+
Jobs are stored as individual JSON files organized by status:
|
|
537
162
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
throw new Error('Job must be running');
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const genJob = job as RunningJob<GenerationParams, GenerationProgress>;
|
|
549
|
-
|
|
550
|
-
// Report progress (create new object - immutable pattern)
|
|
551
|
-
const updatedJob1: RunningJob<GenerationParams, GenerationProgress> = {
|
|
552
|
-
...genJob,
|
|
553
|
-
progress: {
|
|
554
|
-
stage: 'generating',
|
|
555
|
-
percentage: 0,
|
|
556
|
-
message: 'Starting generation...',
|
|
557
|
-
},
|
|
558
|
-
};
|
|
559
|
-
await getJobQueue().updateJob(updatedJob1);
|
|
560
|
-
|
|
561
|
-
// Generate content
|
|
562
|
-
const content = await this.inference.generate({
|
|
563
|
-
prompt: genJob.params.prompt,
|
|
564
|
-
context: genJob.params.context,
|
|
565
|
-
temperature: genJob.params.temperature,
|
|
566
|
-
maxTokens: genJob.params.maxTokens,
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Update progress
|
|
570
|
-
const updatedJob2: RunningJob<GenerationParams, GenerationProgress> = {
|
|
571
|
-
...updatedJob1,
|
|
572
|
-
progress: {
|
|
573
|
-
stage: 'creating',
|
|
574
|
-
percentage: 75,
|
|
575
|
-
message: 'Creating resource...',
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
await getJobQueue().updateJob(updatedJob2);
|
|
579
|
-
|
|
580
|
-
// Create resource (simplified)
|
|
581
|
-
const resourceId = await createResource(content, genJob.params.title);
|
|
582
|
-
|
|
583
|
-
// Set result (will be handled by base class transition to complete)
|
|
584
|
-
return {
|
|
585
|
-
resourceId,
|
|
586
|
-
resourceName: genJob.params.title,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
}
|
|
163
|
+
```
|
|
164
|
+
data/jobs/
|
|
165
|
+
pending/job-abc123.json
|
|
166
|
+
running/job-def456.json
|
|
167
|
+
complete/job-ghi789.json
|
|
168
|
+
failed/job-jkl012.json
|
|
169
|
+
cancelled/job-mno345.json
|
|
590
170
|
```
|
|
591
171
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
import { getJobQueue } from '@semiont/jobs';
|
|
596
|
-
|
|
597
|
-
async function monitorJob(jobId: JobId): Promise<void> {
|
|
598
|
-
const queue = getJobQueue();
|
|
599
|
-
|
|
600
|
-
while (true) {
|
|
601
|
-
const job = await queue.getJob(jobId);
|
|
602
|
-
|
|
603
|
-
if (!job) {
|
|
604
|
-
console.log('Job not found');
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
console.log(`Status: ${job.status}`);
|
|
609
|
-
|
|
610
|
-
// Type-safe progress access - only available on running jobs
|
|
611
|
-
if (job.status === 'running') {
|
|
612
|
-
console.log(`Progress: ${job.progress.percentage}%`);
|
|
613
|
-
console.log(`Stage: ${job.progress.stage}`);
|
|
614
|
-
console.log(`Message: ${job.progress.message || 'Processing...'}`);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Type-safe result access - only available on complete jobs
|
|
618
|
-
if (job.status === 'complete') {
|
|
619
|
-
console.log(`Result: ${JSON.stringify(job.result)}`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Type-safe error access - only available on failed jobs
|
|
623
|
-
if (job.status === 'failed') {
|
|
624
|
-
console.log(`Error: ${job.error}`);
|
|
625
|
-
}
|
|
172
|
+
## Documentation
|
|
626
173
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
```
|
|
174
|
+
- **[Job Queue Guide](./docs/JobQueue.md)** — JobQueue API and job management
|
|
175
|
+
- **[Workers Guide](./docs/Workers.md)** — Building custom workers
|
|
176
|
+
- **[Job Types Guide](./docs/JobTypes.md)** — All job type definitions
|
|
177
|
+
- **[Type System Guide](./docs/TYPES.md)** — Discriminated unions and type safety
|
|
178
|
+
- **[Configuration Guide](./docs/Configuration.md)** — Setup and options
|
|
179
|
+
- **[API Reference](./docs/API.md)** — Complete API reference
|
|
635
180
|
|
|
636
181
|
## License
|
|
637
182
|
|
|
@@ -639,13 +184,7 @@ Apache-2.0
|
|
|
639
184
|
|
|
640
185
|
## Related Packages
|
|
641
186
|
|
|
642
|
-
- [`@semiont/
|
|
643
|
-
- [`@semiont/
|
|
644
|
-
- [`@semiont/
|
|
645
|
-
- [
|
|
646
|
-
|
|
647
|
-
## Learn More
|
|
648
|
-
|
|
649
|
-
- [Background Jobs Pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageQueueing.html) - Queue-based processing
|
|
650
|
-
- [Job Types Guide](./docs/JobTypes.md) - Detailed job type documentation
|
|
651
|
-
- [Workers Guide](./docs/Workers.md) - Building custom workers
|
|
187
|
+
- [`@semiont/core`](../core/) — Domain types, EventBus
|
|
188
|
+
- [`@semiont/api-client`](../api-client/) — OpenAPI types
|
|
189
|
+
- [`@semiont/inference`](../inference/) — AI inference client
|
|
190
|
+
- [`@semiont/make-meaning`](../make-meaning/) — Actor model, Knowledge Base, service orchestration
|