Types
```typescript
// types.ts
export enum JobStatus {
QUEUED = 'queued',
PROCESSING = 'processing',
COMPLETED = 'completed',
FAILED = 'failed',
PARTIAL = 'partial',
}
export const VALID_TRANSITIONS: Record = {
[JobStatus.QUEUED]: [JobStatus.PROCESSING],
[JobStatus.PROCESSING]: [JobStatus.COMPLETED, JobStatus.PARTIAL, JobStatus.FAILED],
[JobStatus.COMPLETED]: [],
[JobStatus.FAILED]: [],
[JobStatus.PARTIAL]: [],
};
export function isTerminalState(status: JobStatus): boolean {
return VALID_TRANSITIONS[status].length === 0;
}
export interface Job {
id: string;
userId: string;
jobType: string;
status: JobStatus;
progress: number;
errorMessage?: string;
parameters?: Record;
result?: Record;
createdAt: Date;
updatedAt: Date;
completedAt?: Date;
}
export interface Asset {
id: string;
jobId: string;
userId: string;
assetType: string;
url: string;
storagePath: string;
fileSize: number;
createdAt: Date;
}
```
Job Service
```typescript
// job-service.ts
import { Job, JobStatus, Asset, VALID_TRANSITIONS, isTerminalState } from './types';
export class InvalidStateTransitionError extends Error {
constructor(public currentStatus: JobStatus, public targetStatus: JobStatus) {
super(Cannot transition from '${currentStatus}' to '${targetStatus}');
this.name = 'InvalidStateTransitionError';
}
}
export class JobService {
constructor(private db: Database) {}
async createJob(
userId: string,
jobType: string,
parameters?: Record
): Promise {
const job: Job = {
id: crypto.randomUUID(),
userId,
jobType,
status: JobStatus.QUEUED,
progress: 0,
parameters,
createdAt: new Date(),
updatedAt: new Date(),
};
await this.db.jobs.insert(job);
return job;
}
async getJob(jobId: string, userId: string): Promise {
const job = await this.db.jobs.findOne({ id: jobId });
if (!job) throw new Error('Job not found');
if (job.userId !== userId) throw new Error('Unauthorized');
return job;
}
async transitionStatus(
jobId: string,
targetStatus: JobStatus,
options: {
progress?: number;
errorMessage?: string;
result?: Record;
} = {}
): Promise {
const job = await this.db.jobs.findOne({ id: jobId });
if (!job) throw new Error('Job not found');
// Allow same-state for progress updates
if (job.status !== targetStatus) {
if (!VALID_TRANSITIONS[job.status].includes(targetStatus)) {
throw new InvalidStateTransitionError(job.status, targetStatus);
}
}
const updates: Partial = {
status: targetStatus,
progress: options.progress ?? job.progress,
updatedAt: new Date(),
};
if (options.errorMessage !== undefined) {
updates.errorMessage = options.errorMessage;
}
if (options.result !== undefined) {
updates.result = options.result;
}
if (isTerminalState(targetStatus)) {
updates.completedAt = new Date();
}
await this.db.jobs.update({ id: jobId }, updates);
return { ...job, ...updates };
}
async updateProgress(jobId: string, progress: number): Promise {
const job = await this.db.jobs.findOne({ id: jobId });
if (!job) throw new Error('Job not found');
if (job.status !== JobStatus.PROCESSING) {
console.warn(Ignoring progress update for job ${jobId} in ${job.status} state);
return job;
}
return this.transitionStatus(jobId, JobStatus.PROCESSING, { progress });
}
async markCompleted(jobId: string, result?: Record): Promise {
return this.transitionStatus(jobId, JobStatus.COMPLETED, { progress: 100, result });
}
async markFailed(jobId: string, errorMessage: string): Promise {
return this.transitionStatus(jobId, JobStatus.FAILED, { errorMessage });
}
async markPartial(
jobId: string,
result?: Record,
errorMessage?: string
): Promise {
return this.transitionStatus(jobId, JobStatus.PARTIAL, {
progress: 100,
result,
errorMessage,
});
}
async createAsset(
jobId: string,
userId: string,
assetType: string,
url: string,
storagePath: string,
fileSize: number
): Promise {
const asset: Asset = {
id: crypto.randomUUID(),
jobId,
userId,
assetType,
url,
storagePath,
fileSize,
createdAt: new Date(),
};
await this.db.assets.insert(asset);
return asset;
}
async getJobAssets(jobId: string, userId: string): Promise {
await this.getJob(jobId, userId); // Verify ownership
return this.db.assets.find({ jobId });
}
}
```