import DataEntity, { EntityEvents } from '../lib/DataEntity'; import ErrorData from '../types/ErrorData'; import { RawJob, RawProject } from './types/RawProject'; import ProjectsApi from './index'; import { Logger } from '../lib/DefaultLogger'; import getUUID from '../lib/getUUID'; export type JobStatus = | 'pending' | 'initiating' | 'processing' | 'completed' | 'failed' | 'canceled'; const JOB_STATUS_MAP: Record = { created: 'pending', queued: 'pending', assigned: 'initiating', initiatingModel: 'initiating', jobStarted: 'processing', jobProgress: 'processing', jobCompleted: 'completed', jobError: 'failed' }; /** * @inline */ export interface JobData { id: string; projectId: string; status: JobStatus; step: number; stepCount: number; workerName?: string; seed?: number; isNSFW?: boolean; userCanceled?: boolean; previewUrl?: string; resultUrl?: string | null; error?: ErrorData; } export interface JobEventMap extends EntityEvents { progress: number; completed: string; failed: ErrorData; } export interface JobOptions { api: ProjectsApi; logger: Logger; } class Job extends DataEntity { static fromRaw(rawProject: RawProject, rawJob: RawJob, options: JobOptions) { return new Job( { id: rawJob.imgID || getUUID(), projectId: rawProject.id, status: JOB_STATUS_MAP[rawJob.status], step: rawJob.performedSteps, stepCount: rawProject.stepCount, workerName: rawJob.worker.name, seed: rawJob.seedUsed, isNSFW: rawJob.triggeredNSFWFilter }, options ); } private readonly _api: ProjectsApi; private readonly _logger: Logger; constructor(data: JobData, options: JobOptions) { super(data); this._api = options.api; this._logger = options.logger; this.on('updated', this.handleUpdated.bind(this)); } get id() { return this.data.id; } get projectId() { return this.data.projectId; } /** * Current status of the job. */ get status() { return this.data.status; } get finished() { return ['completed', 'failed', 'canceled'].includes(this.status); } /** * Progress of the job in percentage (0-100). */ get progress() { return Math.round((this.data.step / this.data.stepCount) * 100); } /** * Current step of the job. */ get step() { return this.data.step; } /** * Total number of steps that worker will perform. */ get stepCount() { return this.data.stepCount; } /** * Seed used to generate the image. This property is only available when the job is completed. */ get seed() { return this.data.seed; } /** * Last preview image URL generated by the worker. */ get previewUrl() { return this.data.previewUrl; } /** * URL to the result image, could be null if the job was canceled or triggered NSFW filter while * it was not disabled explicitly. */ get resultUrl() { return this.data.resultUrl; } get imageUrl() { return this.data.resultUrl || this.data.previewUrl; } get error() { return this.data.error; } /** * Whether the image is NSFW or not. Only makes sense if job is completed. * If NSFW filter is disabled, this property will always be false. * If NSFW filter is enabled and the image is NSFW, image will not be available for download. */ get isNSFW() { return !!this.data.isNSFW; } /** * Name of the worker that is processing this job. */ get workerName() { return this.data.workerName; } /** * Syncs the job data with the data received from the REST API. * @internal * @param data */ async _syncWithRestData(data: RawJob) { const delta: Partial = { step: data.performedSteps, workerName: data.worker.name, seed: data.seedUsed, isNSFW: data.triggeredNSFWFilter }; if (JOB_STATUS_MAP[data.status]) { delta.status = JOB_STATUS_MAP[data.status]; } if (!this.data.resultUrl && delta.status === 'completed' && !data.triggeredNSFWFilter) { try { delta.resultUrl = await this._api.downloadUrl({ jobId: this.projectId, imageId: this.id, type: 'complete' }); } catch (error) { this._logger.error(error); } } this._update(delta); } private handleUpdated(keys: string[]) { if (keys.includes('step') || keys.includes('stepCount')) { this.emit('progress', this.progress); } if (keys.includes('status') && this.status === 'completed') { this.emit('completed', this.resultUrl!); } if (keys.includes('status') && this.status === 'failed') { this.emit('failed', this.data.error!); } } } export default Job;