@semiont/jobs 0.2.34 → 0.2.35-build.101

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/dist/index.d.ts CHANGED
@@ -270,11 +270,26 @@ declare class JobQueue {
270
270
  private eventBus?;
271
271
  private jobsDir;
272
272
  private logger;
273
+ private pendingQueue;
274
+ private watcher;
275
+ private loadDebounceTimer;
273
276
  constructor(config: JobQueueConfig, logger: Logger, eventBus?: EventBus | undefined);
274
277
  /**
275
- * Initialize job queue directories
278
+ * Initialize job queue directories, load pending jobs, and start fs.watch
276
279
  */
277
280
  initialize(): Promise<void>;
281
+ /**
282
+ * Clean up watcher
283
+ */
284
+ destroy(): void;
285
+ /**
286
+ * Load pending jobs from disk into in-memory queue
287
+ */
288
+ private loadPendingJobs;
289
+ /**
290
+ * Debounced version of loadPendingJobs — fs.watch can fire rapidly
291
+ */
292
+ private debouncedLoadPendingJobs;
278
293
  /**
279
294
  * Create a new job
280
295
  */
@@ -288,9 +303,10 @@ declare class JobQueue {
288
303
  */
289
304
  updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void>;
290
305
  /**
291
- * Poll for next pending job (FIFO)
306
+ * Poll for next pending job (FIFO) from in-memory queue.
307
+ * If a predicate is provided, returns the first matching job (skipping non-matching ones).
292
308
  */
293
- pollNextPendingJob(): Promise<AnyJob | null>;
309
+ pollNextPendingJob(predicate?: (job: AnyJob) => boolean): Promise<AnyJob | null>;
294
310
  /**
295
311
  * List jobs with filters
296
312
  */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { promises } from 'fs';
1
+ import { promises, watch } from 'fs';
2
2
  import * as path from 'path';
3
3
 
4
4
  // src/job-queue.ts
@@ -10,8 +10,12 @@ var JobQueue = class {
10
10
  }
11
11
  jobsDir;
12
12
  logger;
13
+ // In-memory pending queue: avoids fs.readdir() on every poll (6×/sec with 6 workers)
14
+ pendingQueue = [];
15
+ watcher = null;
16
+ loadDebounceTimer = null;
13
17
  /**
14
- * Initialize job queue directories
18
+ * Initialize job queue directories, load pending jobs, and start fs.watch
15
19
  */
16
20
  async initialize() {
17
21
  const statuses = ["pending", "running", "complete", "failed", "cancelled"];
@@ -19,8 +23,63 @@ var JobQueue = class {
19
23
  const dir = path.join(this.jobsDir, status);
20
24
  await promises.mkdir(dir, { recursive: true });
21
25
  }
26
+ await this.loadPendingJobs();
27
+ const pendingDir = path.join(this.jobsDir, "pending");
28
+ try {
29
+ this.watcher = watch(pendingDir, () => {
30
+ this.debouncedLoadPendingJobs();
31
+ });
32
+ } catch (error) {
33
+ this.logger.warn("Failed to watch pending directory", {
34
+ error: error instanceof Error ? error.message : String(error)
35
+ });
36
+ }
22
37
  this.logger.info("Job queue initialized");
23
38
  }
39
+ /**
40
+ * Clean up watcher
41
+ */
42
+ destroy() {
43
+ if (this.watcher) {
44
+ this.watcher.close();
45
+ this.watcher = null;
46
+ }
47
+ if (this.loadDebounceTimer) {
48
+ clearTimeout(this.loadDebounceTimer);
49
+ this.loadDebounceTimer = null;
50
+ }
51
+ }
52
+ /**
53
+ * Load pending jobs from disk into in-memory queue
54
+ */
55
+ async loadPendingJobs() {
56
+ const pendingDir = path.join(this.jobsDir, "pending");
57
+ try {
58
+ const files = await promises.readdir(pendingDir);
59
+ files.sort();
60
+ const jobs = [];
61
+ for (const file of files) {
62
+ try {
63
+ const content = await promises.readFile(path.join(pendingDir, file), "utf-8");
64
+ jobs.push(JSON.parse(content));
65
+ } catch {
66
+ }
67
+ }
68
+ this.pendingQueue = jobs;
69
+ } catch {
70
+ this.pendingQueue = [];
71
+ }
72
+ }
73
+ /**
74
+ * Debounced version of loadPendingJobs — fs.watch can fire rapidly
75
+ */
76
+ debouncedLoadPendingJobs() {
77
+ if (this.loadDebounceTimer) return;
78
+ this.loadDebounceTimer = setTimeout(async () => {
79
+ this.loadDebounceTimer = null;
80
+ await this.loadPendingJobs();
81
+ }, 100);
82
+ }
24
83
  /**
25
84
  * Create a new job
26
85
  */
@@ -28,6 +87,10 @@ var JobQueue = class {
28
87
  const jobPath = this.getJobPath(job.metadata.id, job.status);
29
88
  await promises.writeFile(jobPath, JSON.stringify(job, null, 2), "utf-8");
30
89
  this.logger.info("Job created", { jobId: job.metadata.id, status: job.status });
90
+ if (job.status === "pending") {
91
+ this.pendingQueue.push(job);
92
+ this.pendingQueue.sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
93
+ }
31
94
  if (this.eventBus && "params" in job && "resourceId" in job.params) {
32
95
  const resourceBus = this.eventBus.scope(job.params.resourceId);
33
96
  resourceBus.get("job:queued").next({
@@ -63,6 +126,14 @@ var JobQueue = class {
63
126
  await promises.unlink(oldPath);
64
127
  } catch (error) {
65
128
  }
129
+ if (oldStatus === "pending") {
130
+ const idx = this.pendingQueue.findIndex((j) => j.metadata.id === job.metadata.id);
131
+ if (idx !== -1) this.pendingQueue.splice(idx, 1);
132
+ }
133
+ if (job.status === "pending") {
134
+ this.pendingQueue.push(job);
135
+ this.pendingQueue.sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));
136
+ }
66
137
  }
67
138
  const newPath = this.getJobPath(job.metadata.id, job.status);
68
139
  await promises.writeFile(newPath, JSON.stringify(job, null, 2), "utf-8");
@@ -73,24 +144,16 @@ var JobQueue = class {
73
144
  }
74
145
  }
75
146
  /**
76
- * Poll for next pending job (FIFO)
147
+ * Poll for next pending job (FIFO) from in-memory queue.
148
+ * If a predicate is provided, returns the first matching job (skipping non-matching ones).
77
149
  */
78
- async pollNextPendingJob() {
79
- const pendingDir = path.join(this.jobsDir, "pending");
80
- try {
81
- const files = await promises.readdir(pendingDir);
82
- if (files.length === 0) {
83
- return null;
84
- }
85
- files.sort();
86
- const jobFile = files[0];
87
- const jobPath = path.join(pendingDir, jobFile);
88
- const content = await promises.readFile(jobPath, "utf-8");
89
- return JSON.parse(content);
90
- } catch (error) {
91
- this.logger.error("Error polling pending jobs", { error: error instanceof Error ? error.message : String(error) });
92
- return null;
150
+ async pollNextPendingJob(predicate) {
151
+ if (!predicate) {
152
+ return this.pendingQueue.shift() ?? null;
93
153
  }
154
+ const index = this.pendingQueue.findIndex(predicate);
155
+ if (index === -1) return null;
156
+ return this.pendingQueue.splice(index, 1)[0] ?? null;
94
157
  }
95
158
  /**
96
159
  * List jobs with filters
@@ -270,11 +333,7 @@ var JobWorker = class {
270
333
  * Poll for next job to process
271
334
  */
272
335
  async pollNextJob() {
273
- const job = await this.jobQueue.pollNextPendingJob();
274
- if (job && this.canProcessJob(job)) {
275
- return job;
276
- }
277
- return null;
336
+ return this.jobQueue.pollNextPendingJob((job) => this.canProcessJob(job));
278
337
  }
279
338
  /**
280
339
  * Process a job (handles state transitions and error handling)
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 { AnyJob, JobStatus, JobQueryFilters, CancelledJob } from './types';\nimport type { JobId, Logger } from '@semiont/core';\nimport type { EventBus } from '@semiont/core';\n\nexport interface JobQueueConfig {\n dataDir: string;\n}\n\nexport class JobQueue {\n private jobsDir: string;\n private logger: Logger;\n\n constructor(\n config: JobQueueConfig,\n logger: Logger,\n private eventBus?: EventBus\n ) {\n this.jobsDir = path.join(config.dataDir, 'jobs');\n this.logger = logger;\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 this.logger.info('Job queue initialized');\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 this.logger.info('Job created', { jobId: job.metadata.id, status: job.status });\n\n // Emit job:queued event if EventBus is available\n if (this.eventBus && 'params' in job && 'resourceId' in job.params) {\n const resourceBus = this.eventBus.scope(job.params.resourceId);\n resourceBus.get('job:queued').next({\n jobId: job.metadata.id,\n jobType: job.metadata.type,\n resourceId: job.params.resourceId\n });\n }\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 this.logger.info('Job moved', { jobId: job.metadata.id, oldStatus, newStatus: job.status });\n } else {\n this.logger.info('Job updated', { jobId: 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 this.logger.error('Error polling pending jobs', { error: error instanceof Error ? error.message : String(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 this.logger.error('Error cleaning up jobs', { status, error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n if (deletedCount > 0) {\n this.logger.info('Jobs cleaned up', { deletedCount });\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, logger: Logger, eventBus?: EventBus): Promise<JobQueue> {\n jobQueue = new JobQueue(config, logger, eventBus);\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';\nimport type { Logger } from '@semiont/core';\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 protected logger: Logger;\n\n constructor(\n jobQueue: JobQueue,\n pollIntervalMs: number = 1000,\n errorBackoffMs: number = 5000,\n logger: Logger\n ) {\n this.jobQueue = jobQueue;\n this.pollIntervalMs = pollIntervalMs;\n this.errorBackoffMs = errorBackoffMs;\n this.logger = logger;\n }\n\n /**\n * Start the worker (polls queue in loop)\n */\n async start(): Promise<void> {\n this.running = true;\n this.logger.info('Worker started', { worker: this.getWorkerName() });\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 this.logger.error('Error in worker main loop', { worker: this.getWorkerName(), error: error instanceof Error ? error.message : String(error) });\n // Back off on error to avoid tight error loops\n await this.sleep(this.errorBackoffMs);\n }\n }\n\n this.logger.info('Worker stopped', { worker: this.getWorkerName() });\n }\n\n /**\n * Stop the worker (graceful shutdown)\n */\n async stop(): Promise<void> {\n this.logger.info('Stopping worker', { worker: this.getWorkerName() });\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 this.logger.warn('Forced worker shutdown', { worker: this.getWorkerName(), jobId: 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 this.logger.warn('Skipping non-pending job', { worker: this.getWorkerName(), jobId: job.metadata.id, status: job.status });\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 this.logger.info('Processing job', { worker: this.getWorkerName(), jobId: job.metadata.id, jobType: job.metadata.type });\n\n // Execute job-specific logic (passing running job) and get result\n const result = await this.executeJob(runningJob);\n\n // Allow subclasses to emit completion events with result data\n await this.emitCompletionEvent(runningJob, result);\n\n // Move to complete state with result\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: result ?? {}, // Use returned result or empty object\n };\n\n await this.jobQueue.updateJob(completeJob, 'running');\n\n this.logger.info('Job completed successfully', { worker: this.getWorkerName(), jobId: job.metadata.id });\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 this.logger.info('Job failed, will retry', { worker: this.getWorkerName(), jobId: job.metadata.id, retryCount: updatedMetadata.retryCount, maxRetries: updatedMetadata.maxRetries });\n this.logger.debug('Job error details', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined });\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 this.logger.error('Job failed permanently', { worker: this.getWorkerName(), jobId: job.metadata.id, retryCount: updatedMetadata.retryCount });\n this.logger.error('Job error details', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined });\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 this.logger.warn('Failed to update job progress', { worker: this.getWorkerName(), error: error instanceof Error ? error.message : String(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 /**\n * Emit completion event (optional hook for subclasses)\n * Override this to emit job-specific completion events (e.g., job.completed)\n */\n protected async emitCompletionEvent(_job: RunningJob<any, any>, _result: any): Promise<void> {\n // Default: do nothing\n // Subclasses can override to emit events\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 * Return the result object (or void for jobs without results)\n * Throw an error to trigger retry logic\n */\n protected abstract executeJob(job: AnyJob): Promise<any>;\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, watch, type FSWatcher } from 'fs';\nimport * as path from 'path';\nimport type { AnyJob, JobStatus, JobQueryFilters, CancelledJob } from './types';\nimport type { JobId, Logger } from '@semiont/core';\nimport type { EventBus } from '@semiont/core';\n\nexport interface JobQueueConfig {\n dataDir: string;\n}\n\nexport class JobQueue {\n private jobsDir: string;\n private logger: Logger;\n // In-memory pending queue: avoids fs.readdir() on every poll (6×/sec with 6 workers)\n private pendingQueue: AnyJob[] = [];\n private watcher: FSWatcher | null = null;\n private loadDebounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n config: JobQueueConfig,\n logger: Logger,\n private eventBus?: EventBus\n ) {\n this.jobsDir = path.join(config.dataDir, 'jobs');\n this.logger = logger;\n }\n\n /**\n * Initialize job queue directories, load pending jobs, and start fs.watch\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 // Load existing pending jobs into memory\n await this.loadPendingJobs();\n\n // Watch for external changes (other processes, crash recovery)\n const pendingDir = path.join(this.jobsDir, 'pending');\n try {\n this.watcher = watch(pendingDir, () => {\n this.debouncedLoadPendingJobs();\n });\n } catch (error) {\n this.logger.warn('Failed to watch pending directory', {\n error: error instanceof Error ? error.message : String(error)\n });\n }\n\n this.logger.info('Job queue initialized');\n }\n\n /**\n * Clean up watcher\n */\n destroy(): void {\n if (this.watcher) {\n this.watcher.close();\n this.watcher = null;\n }\n if (this.loadDebounceTimer) {\n clearTimeout(this.loadDebounceTimer);\n this.loadDebounceTimer = null;\n }\n }\n\n /**\n * Load pending jobs from disk into in-memory queue\n */\n private async loadPendingJobs(): Promise<void> {\n const pendingDir = path.join(this.jobsDir, 'pending');\n try {\n const files = await fs.readdir(pendingDir);\n files.sort();\n\n const jobs: AnyJob[] = [];\n for (const file of files) {\n try {\n const content = await fs.readFile(path.join(pendingDir, file), 'utf-8');\n jobs.push(JSON.parse(content) as AnyJob);\n } catch {\n // Skip unreadable files\n }\n }\n this.pendingQueue = jobs;\n } catch {\n // Directory might not exist yet\n this.pendingQueue = [];\n }\n }\n\n /**\n * Debounced version of loadPendingJobs — fs.watch can fire rapidly\n */\n private debouncedLoadPendingJobs(): void {\n if (this.loadDebounceTimer) return;\n this.loadDebounceTimer = setTimeout(async () => {\n this.loadDebounceTimer = null;\n await this.loadPendingJobs();\n }, 100);\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 this.logger.info('Job created', { jobId: job.metadata.id, status: job.status });\n\n // Push to in-memory queue for immediate pickup\n if (job.status === 'pending') {\n this.pendingQueue.push(job);\n this.pendingQueue.sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));\n }\n\n // Emit job:queued event if EventBus is available\n if (this.eventBus && 'params' in job && 'resourceId' in job.params) {\n const resourceBus = this.eventBus.scope(job.params.resourceId);\n resourceBus.get('job:queued').next({\n jobId: job.metadata.id,\n jobType: job.metadata.type,\n resourceId: job.params.resourceId\n });\n }\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 // Keep in-memory queue in sync\n if (oldStatus === 'pending') {\n // Leaving pending: remove from queue\n const idx = this.pendingQueue.findIndex(j => j.metadata.id === job.metadata.id);\n if (idx !== -1) this.pendingQueue.splice(idx, 1);\n }\n if (job.status === 'pending') {\n // Entering pending (e.g., retry): add to queue\n this.pendingQueue.push(job);\n this.pendingQueue.sort((a, b) => a.metadata.id.localeCompare(b.metadata.id));\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 this.logger.info('Job moved', { jobId: job.metadata.id, oldStatus, newStatus: job.status });\n } else {\n this.logger.info('Job updated', { jobId: job.metadata.id, status: job.status });\n }\n }\n\n /**\n * Poll for next pending job (FIFO) from in-memory queue.\n * If a predicate is provided, returns the first matching job (skipping non-matching ones).\n */\n async pollNextPendingJob(predicate?: (job: AnyJob) => boolean): Promise<AnyJob | null> {\n if (!predicate) {\n return this.pendingQueue.shift() ?? null;\n }\n\n const index = this.pendingQueue.findIndex(predicate);\n if (index === -1) return null;\n return this.pendingQueue.splice(index, 1)[0] ?? null;\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 this.logger.error('Error cleaning up jobs', { status, error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n if (deletedCount > 0) {\n this.logger.info('Jobs cleaned up', { deletedCount });\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, logger: Logger, eventBus?: EventBus): Promise<JobQueue> {\n jobQueue = new JobQueue(config, logger, eventBus);\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';\nimport type { Logger } from '@semiont/core';\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 protected logger: Logger;\n\n constructor(\n jobQueue: JobQueue,\n pollIntervalMs: number = 1000,\n errorBackoffMs: number = 5000,\n logger: Logger\n ) {\n this.jobQueue = jobQueue;\n this.pollIntervalMs = pollIntervalMs;\n this.errorBackoffMs = errorBackoffMs;\n this.logger = logger;\n }\n\n /**\n * Start the worker (polls queue in loop)\n */\n async start(): Promise<void> {\n this.running = true;\n this.logger.info('Worker started', { worker: this.getWorkerName() });\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 this.logger.error('Error in worker main loop', { worker: this.getWorkerName(), error: error instanceof Error ? error.message : String(error) });\n // Back off on error to avoid tight error loops\n await this.sleep(this.errorBackoffMs);\n }\n }\n\n this.logger.info('Worker stopped', { worker: this.getWorkerName() });\n }\n\n /**\n * Stop the worker (graceful shutdown)\n */\n async stop(): Promise<void> {\n this.logger.info('Stopping worker', { worker: this.getWorkerName() });\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 this.logger.warn('Forced worker shutdown', { worker: this.getWorkerName(), jobId: this.currentJob.metadata.id });\n }\n }\n\n /**\n * Poll for next job to process\n */\n private async pollNextJob(): Promise<AnyJob | null> {\n return this.jobQueue.pollNextPendingJob(job => this.canProcessJob(job));\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 this.logger.warn('Skipping non-pending job', { worker: this.getWorkerName(), jobId: job.metadata.id, status: job.status });\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 this.logger.info('Processing job', { worker: this.getWorkerName(), jobId: job.metadata.id, jobType: job.metadata.type });\n\n // Execute job-specific logic (passing running job) and get result\n const result = await this.executeJob(runningJob);\n\n // Allow subclasses to emit completion events with result data\n await this.emitCompletionEvent(runningJob, result);\n\n // Move to complete state with result\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: result ?? {}, // Use returned result or empty object\n };\n\n await this.jobQueue.updateJob(completeJob, 'running');\n\n this.logger.info('Job completed successfully', { worker: this.getWorkerName(), jobId: job.metadata.id });\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 this.logger.info('Job failed, will retry', { worker: this.getWorkerName(), jobId: job.metadata.id, retryCount: updatedMetadata.retryCount, maxRetries: updatedMetadata.maxRetries });\n this.logger.debug('Job error details', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined });\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 this.logger.error('Job failed permanently', { worker: this.getWorkerName(), jobId: job.metadata.id, retryCount: updatedMetadata.retryCount });\n this.logger.error('Job error details', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined });\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 this.logger.warn('Failed to update job progress', { worker: this.getWorkerName(), error: error instanceof Error ? error.message : String(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 /**\n * Emit completion event (optional hook for subclasses)\n * Override this to emit job-specific completion events (e.g., job.completed)\n */\n protected async emitCompletionEvent(_job: RunningJob<any, any>, _result: any): Promise<void> {\n // Default: do nothing\n // Subclasses can override to emit events\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 * Return the result object (or void for jobs without results)\n * Throw an error to trigger retry logic\n */\n protected abstract executeJob(job: AnyJob): Promise<any>;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/jobs",
3
- "version": "0.2.34",
3
+ "version": "0.2.35-build.101",
4
4
  "type": "module",
5
5
  "description": "Filesystem-based job queue and worker infrastructure",
6
6
  "main": "./dist/index.js",