UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

242 lines (241 loc) 9.59 kB
import { Printer } from "../entities/printer.entity.js"; import { PrintJob } from "../entities/print-job.entity.js"; import { SOCKET_STATE } from "../shared/dtos/socket-state.type.js"; import { API_STATE } from "../shared/dtos/api-state.type.js"; import { IsNull, Not } from "typeorm"; import { captureException } from "@sentry/node"; //#region src/services/print-queue.service.ts /** * Simplified service for managing print job queues per printer */ var PrintQueueService = class PrintQueueService { printJobRepository; printerRepository; eventEmitter2; logger; constructor(loggerFactory, typeormService, eventEmitter2, printerApiFactory, fileStorageService, printerSocketStore) { this.printerApiFactory = printerApiFactory; this.fileStorageService = fileStorageService; this.printerSocketStore = printerSocketStore; this.printJobRepository = typeormService.getDataSource().getRepository(PrintJob); this.printerRepository = typeormService.getDataSource().getRepository(Printer); this.eventEmitter2 = eventEmitter2; this.logger = loggerFactory(PrintQueueService.name); this.eventEmitter2.on("printQueue.jobSubmitted", (event) => { this.handleJobSubmission(event.printerId, event.jobId, event.fileName, event.fileStorageId, event.queuePosition).catch((error) => { this.logger.error(`Failed to handle job submission for job ${event.jobId}`, error); captureException(error); }); }); } isPrinterConnected(printerId) { const socket = this.printerSocketStore.getPrinterSocket(printerId); if (!socket) return { connected: false, reason: "No socket connection found" }; const socketState = socket.socketState; const apiState = socket.apiState; if (socketState !== SOCKET_STATE.opened && socketState !== SOCKET_STATE.authenticated) return { connected: false, reason: `Socket not connected (state: ${socketState})` }; if (apiState !== API_STATE.responding) return { connected: false, reason: `Printer not responding (API state: ${apiState})` }; return { connected: true }; } async addToQueue(printerId, jobId, position) { const job = await this.printJobRepository.findOne({ where: { id: jobId } }); if (!job) throw new Error(`Print job ${jobId} not found`); this.ensurePrinterAssignment(job, printerId); if (position === void 0 || position === null) job.queuePosition = (await this.getMaxQueuePosition(printerId) ?? -1) + 1; else { await this.shiftQueuePositions(printerId, position); job.queuePosition = position; } job.status = "QUEUED"; await this.printJobRepository.save(job); this.logger.log(`Added job ${jobId} to printer ${printerId} queue at position ${job.queuePosition}`); this.eventEmitter2.emit("printQueue.jobAdded", { printerId, jobId, position: job.queuePosition }); } async removeFromQueue(jobId) { const job = await this.printJobRepository.findOne({ where: { id: jobId } }); if (!job) throw new Error(`Print job ${jobId} not found`); const printerId = job.printerId; const oldPosition = job.queuePosition; job.queuePosition = null; if (job.status === "QUEUED") job.status = "PENDING"; await this.printJobRepository.save(job); if (oldPosition !== null && printerId) await this.compactQueuePositions(printerId, oldPosition); this.logger.log(`Removed job ${jobId} from queue`); this.eventEmitter2.emit("printQueue.jobRemoved", { printerId, jobId }); } async getQueue(printerId) { return (await this.printJobRepository.find({ where: { printerId, status: "QUEUED", queuePosition: Not(IsNull()) }, order: { queuePosition: "ASC" } })).map((j) => ({ id: j.id, fileName: j.fileName, queuePosition: j.queuePosition, status: j.status, estimatedTimeSeconds: j.metadata?.gcodePrintTimeSeconds, filamentGrams: j.metadata?.filamentUsedGrams, createdAt: j.createdAt })); } async getGlobalQueuePaged(page, pageSize) { const skip = (page - 1) * pageSize; return await this.printJobRepository.findAndCount({ where: { status: "QUEUED" }, order: { printerId: "ASC", queuePosition: "ASC" }, relations: ["printer"], take: pageSize, skip }); } async getNextInQueue(printerId) { return this.printJobRepository.findOne({ where: { printerId, status: "QUEUED", queuePosition: Not(IsNull()) }, order: { queuePosition: "ASC" } }); } async reorderQueue(printerId, jobIds) { for (let i = 0; i < jobIds.length; i++) { const job = await this.printJobRepository.findOne({ where: { id: jobIds[i] } }); if (job?.printerId === printerId) { job.queuePosition = i; await this.printJobRepository.save(job); } } this.logger.log(`Reordered queue for printer ${printerId}`); this.eventEmitter2.emit("printQueue.reordered", { printerId }); } async clearQueue(printerId) { const jobs = await this.printJobRepository.find({ where: { printerId, status: "QUEUED" } }); for (const job of jobs) { job.status = "PENDING"; job.queuePosition = null; await this.printJobRepository.save(job); } this.logger.log(`Cleared queue for printer ${printerId} (${jobs.length} jobs)`); this.eventEmitter2.emit("printQueue.cleared", { printerId }); } async processQueue(printerId) { const nextJob = await this.getNextInQueue(printerId); if (!nextJob) { this.logger.log(`No jobs in queue for printer ${printerId}`); return null; } this.logger.log(`Processing queue: next job is ${nextJob.id} (${nextJob.fileName})`); this.eventEmitter2.emit("printQueue.processNext", { printerId, jobId: nextJob.id, fileName: nextJob.fileName, fileStorageId: nextJob.fileStorageId }); return nextJob; } ensurePrinterAssignment(job, printerId) { if (!job.printerId) job.printerId = printerId; else if (job.printerId !== printerId) throw new Error(`Job ${job.id} belongs to printer ${job.printerId}, cannot submit to printer ${printerId}`); } async getMaxQueuePosition(printerId) { return (await this.printJobRepository.createQueryBuilder("job").select("MAX(job.queuePosition)", "max").where("job.printerId = :printerId", { printerId }).getRawOne())?.max ?? null; } async shiftQueuePositions(printerId, fromPosition) { await this.printJobRepository.createQueryBuilder().update(PrintJob).set({ queuePosition: () => "queuePosition + 1" }).where("printerId = :printerId", { printerId }).andWhere("queuePosition >= :fromPosition", { fromPosition }).execute(); } async compactQueuePositions(printerId, removedPosition) { await this.printJobRepository.createQueryBuilder().update(PrintJob).set({ queuePosition: () => "queuePosition - 1" }).where("printerId = :printerId", { printerId }).andWhere("queuePosition > :removedPosition", { removedPosition }).execute(); } async submitToPrinter(printerId, jobId) { const job = await this.printJobRepository.findOne({ where: { id: jobId } }); if (!job) throw new Error(`Print job ${jobId} not found`); this.ensurePrinterAssignment(job, printerId); const queuePosition = job.queuePosition; if (job.queuePosition !== null) { const oldPosition = job.queuePosition; job.queuePosition = null; await this.compactQueuePositions(printerId, oldPosition); } job.status = "PRINTING"; job.startedAt = /* @__PURE__ */ new Date(); await this.printJobRepository.save(job); this.logger.log(`Submitting job ${jobId} (${job.fileName}) to printer ${printerId}`); this.eventEmitter2.emit("printQueue.jobSubmitted", { printerId, jobId: job.id, fileName: job.fileName, fileStorageId: job.fileStorageId, queuePosition }); } async handleJobSubmission(printerId, jobId, fileName, fileStorageId, queuePosition) { this.logger.log(`Handling job submission for job ${jobId} on printer ${printerId}`); try { if (!fileStorageId) throw new Error(`Job ${jobId} has no fileStorageId - cannot submit to printer`); const printerApi = this.printerApiFactory.getById(printerId); const fileSize = this.fileStorageService.getFileSize(fileStorageId); const fileStream = this.fileStorageService.readFileStream(fileStorageId); this.logger.log(`Uploading file ${fileName} to printer ${printerId} and starting print`); await printerApi.uploadFile({ stream: fileStream, fileName, contentLength: fileSize, startPrint: true }); this.logger.log(`Successfully submitted job ${jobId} to printer ${printerId}`); if (queuePosition !== null && queuePosition !== void 0) { const job = await this.printJobRepository.findOne({ where: { id: jobId } }); if (job?.queuePosition === queuePosition) { job.queuePosition = null; await this.printJobRepository.save(job); await this.compactQueuePositions(printerId, queuePosition); this.logger.log(`Removed job ${jobId} from queue after successful submission`); } } } catch (error) { this.logger.error(`Failed to submit job ${jobId} to printer ${printerId}`, error); try { const job = await this.printJobRepository.findOne({ where: { id: jobId } }); if (job) { job.status = "FAILED"; job.statusReason = `Print submission failed: ${error instanceof Error ? error.message : "Unknown error"}`; job.endedAt = /* @__PURE__ */ new Date(); await this.printJobRepository.save(job); this.logger.log(`Updated job ${jobId} status to FAILED (still in queue for retry)`); } } catch (updateError) { this.logger.error(`Failed to update job ${jobId} status after submission error`, updateError); } throw error; } } }; //#endregion export { PrintQueueService }; //# sourceMappingURL=print-queue.service.js.map