@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
JavaScript
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