diff --git a/src/documentprinter/documentprinterclient.js b/src/documentprinter/documentprinterclient.js index 991b754..b121711 100644 --- a/src/documentprinter/documentprinterclient.js +++ b/src/documentprinter/documentprinterclient.js @@ -331,6 +331,11 @@ export class DocumentPrinterClient { try { if (this.printerInterface.deploy) { + const fetchTemplateProgress = 0.05; + const fetchObjectProgress = 0.1; + const renderProgressStart = 0.1; + const renderProgressSpan = 0.9; + await this.updateJobState(documentJob._id, { type: "deploying" }); const documentTemplate = await this.socketClient.getObject({ objectType: "documentTemplate", @@ -338,7 +343,7 @@ export class DocumentPrinterClient { }); await this.updateJobState(documentJob._id, { type: "deploying", - progress: 0.25, + progress: fetchTemplateProgress, }); const object = await this.socketClient.getObject({ objectType: documentJob.objectType, @@ -346,7 +351,7 @@ export class DocumentPrinterClient { }); await this.updateJobState(documentJob._id, { type: "deploying", - progress: 0.5, + progress: fetchObjectProgress, }); if (!documentTemplate) { logger.error( @@ -356,12 +361,22 @@ export class DocumentPrinterClient { } await this.updateJobState(documentJob._id, { type: "deploying", - progress: 0.75, + progress: renderProgressStart, }); const result = await this.printerInterface.deploy( documentJob, documentTemplate, object, + async ({ progress, message } = {}) => { + const mapped = + renderProgressStart + + (Number(progress) || 0) * renderProgressSpan; + await this.updateJobState(documentJob._id, { + type: "deploying", + progress: Math.min(1, Math.round(mapped * 1000) / 1000), + ...(message ? { message } : {}), + }); + }, ); await this.updateJobState(documentJob._id, { type: "deploying", diff --git a/src/documentprinter/interfaces/cupsinterface.js b/src/documentprinter/interfaces/cupsinterface.js index 826af19..846bca3 100644 --- a/src/documentprinter/interfaces/cupsinterface.js +++ b/src/documentprinter/interfaces/cupsinterface.js @@ -229,16 +229,19 @@ export default class CupsInterface { return data; } - async deploy(documentJob, documentTemplate, object) { + async deploy(documentJob, documentTemplate, object, onProgress) { logger.info( `Deploying job ${documentJob._id} to CUPS printer ${this.name}` ); - const pdfObj = await this.documentPrinterClient.socketClient.renderTemplatePDF({ - _id: documentTemplate._id, - content: documentTemplate.content, - object: object, - }); + const pdfObj = await this.documentPrinterClient.socketClient.renderTemplatePDF( + { + _id: documentTemplate._id, + content: documentTemplate.content, + object: object, + }, + onProgress, + ); if (!pdfObj || !pdfObj.pdf) { throw new Error( diff --git a/src/documentprinter/interfaces/receiptinterface.js b/src/documentprinter/interfaces/receiptinterface.js index bb46d64..b8bee68 100644 --- a/src/documentprinter/interfaces/receiptinterface.js +++ b/src/documentprinter/interfaces/receiptinterface.js @@ -163,7 +163,7 @@ export default class ReceiptInterface { } } - async deploy(documentJob, documentTemplate, object) { + async deploy(documentJob, documentTemplate, object, onProgress) { logger.info( `Deploying job ${documentJob._id} to receipt printer ${this.name}` ); @@ -177,6 +177,7 @@ export default class ReceiptInterface { width: 512, }, "png", + onProgress, ); if (!imageObj || !imageObj.images) { diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js index f8228e6..86cd52b 100644 --- a/src/socket/socketclient.js +++ b/src/socket/socketclient.js @@ -32,6 +32,7 @@ export class SocketClient { this.documentPrinterManager = new DocumentPrinterManager(this); this.scanner = new WebSocketScanner({ maxThreads: 50 }); this.readLine = null; + this.objectEventCallbacks = new Map(); sendIPC("setOnline", false); sendIPC("setAuthenticated", false); sendIPC("setLoading", false); @@ -543,60 +544,171 @@ export class SocketClient { return data?.error || error.message; } - async renderTemplatePDF(templateData) { + getObjectEventKey(objectType, _id, eventType) { + return `${objectType}:${_id}:events:${eventType}`; + } + + async downloadTemplate(templateData, type, extra = {}) { + const { async: asyncMode, renderRequestId } = extra; + const padding = extra.padding === true; + const response = await axios.post( + `${this.getApiUrl()}/documenttemplates/${templateData._id}/download`, + { + content: templateData.content, + object: templateData.object, + width: templateData.width, + padding, + ...(asyncMode === true ? { async: true } : {}), + ...(renderRequestId ? { renderRequestId } : {}), + }, + { + params: { + type, + padding: padding ? "true" : "false", + ...(asyncMode === true ? { async: true } : {}), + ...(renderRequestId ? { renderRequestId } : {}), + }, + responseType: "arraybuffer", + headers: this.getApiHeaders(), + }, + ); + const contentType = response.headers["content-type"] || ""; + if (contentType.includes("application/json")) { + return JSON.parse(Buffer.from(response.data).toString("utf8")); + } + return { + type, + mime: contentType, + buffer: Buffer.from(response.data), + }; + } + + async renderTemplateWithProgress(templateData, type, onProgress) { + const startResult = await this.downloadTemplate(templateData, type, { + async: true, + }); + if (startResult?.error) { + throw new Error(startResult.error); + } + if (!startResult?.renderRequestId) { + return startResult; + } + + const renderRequestId = startResult.renderRequestId; + const eventType = `render:${renderRequestId}`; + if (typeof onProgress === "function") { + await onProgress({ progress: 0, message: "Starting render..." }); + } + + return new Promise((resolve, reject) => { + let settled = false; + let unsubscribe; + + const finish = (result, error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeoutId); + clearInterval(pollId); + if (typeof unsubscribe === "function") { + unsubscribe(); + } + if (error) { + reject(error); + return; + } + resolve(result); + }; + + const tryFetchRenderedFile = async () => { + try { + const result = await this.downloadTemplate(templateData, type, { + renderRequestId, + }); + if (result?.error) { + finish(null, new Error(result.error)); + return true; + } + finish(result); + return true; + } catch (error) { + if (error?.response?.status === 404) { + return false; + } + finish(null, new Error(this.parseTemplateDownloadError(error))); + return true; + } + }; + + unsubscribe = this.subscribeToObjectEvent({ + objectType: "render", + _id: renderRequestId, + eventType, + callback: (event) => { + if (typeof onProgress === "function") { + onProgress({ + progress: Number(event?.progress) || 0, + message: event?.message || "", + error: event?.error, + }); + } + if (event?.error) { + finish(null, new Error(event.error)); + return; + } + if (Number(event?.progress) >= 1) { + tryFetchRenderedFile(); + } + }, + }); + + const pollId = setInterval(tryFetchRenderedFile, 1000); + const timeoutId = setTimeout(() => { + finish(null, new Error("Timed out waiting for template render.")); + }, 120000); + }); + } + + async renderTemplatePDF(templateData, onProgress) { logger.debug("Rendering template PDF...", templateData); try { - const response = await axios.post( - `${this.getApiUrl()}/documenttemplates/${templateData._id}/download`, - { - content: templateData.content, - object: templateData.object, - }, - { - params: { type: "pdf", padding: false }, - responseType: "arraybuffer", - headers: this.getApiHeaders(), - }, + const result = await this.renderTemplateWithProgress( + templateData, + "pdf", + onProgress, ); logger.trace("Template PDF rendered successfully."); - return { pdf: Buffer.from(response.data) }; + if (result?.pdf) { + return result; + } + return { pdf: result.buffer }; } catch (error) { - const message = this.parseTemplateDownloadError(error); + const message = this.parseTemplateDownloadError(error) || error.message; logger.error("Failed to render template PDF:", message); throw new Error(message); } } - async renderTemplateJPG(templateData, type = "jpeg") { + async renderTemplateJPG(templateData, type = "jpeg", onProgress) { logger.debug(`Rendering template ${type.toUpperCase()}...`, templateData); try { - const response = await axios.post( - `${this.getApiUrl()}/documenttemplates/${templateData._id}/download`, - { - content: templateData.content, - object: templateData.object, - width: templateData.width, - }, - { - params: { type, padding: false }, - responseType: "arraybuffer", - headers: this.getApiHeaders(), - }, + const result = await this.renderTemplateWithProgress( + templateData, + type, + onProgress, ); - const contentType = response.headers["content-type"] || ""; - if (contentType.includes("application/json")) { - const payload = JSON.parse(Buffer.from(response.data).toString("utf8")); - logger.trace("Template JPG rendered successfully."); + logger.trace("Template JPG rendered successfully."); + if (Array.isArray(result?.images)) { return { - images: (payload.images || []).map((image) => - Buffer.from(image, "base64"), + images: result.images.map((image) => + Buffer.isBuffer(image) ? image : Buffer.from(image, "base64"), ), }; } - logger.trace("Template JPG rendered successfully."); - return { images: [Buffer.from(response.data)] }; + return { images: [result.buffer] }; } catch (error) { - const message = this.parseTemplateDownloadError(error); + const message = this.parseTemplateDownloadError(error) || error.message; logger.error("Failed to render template JPG:", message); throw new Error(message); } @@ -635,30 +747,75 @@ export class SocketClient { }); } - async subscribeToObjectEvent({ objectType, _id, eventType }) { + subscribeToObjectEvent({ objectType, _id, eventType, callback }) { logger.debug("Suscribing to object event...", { objectType, _id, eventType, }); - this.socket.emit("subscribeToObjectEvent", { - objectType, - _id, - eventType, - }); + + const emitSubscribe = () => { + this.socket.emit("subscribeToObjectEvent", { + objectType, + _id, + eventType, + }); + }; + + if (typeof callback === "function") { + const key = this.getObjectEventKey(objectType, _id, eventType); + if (!this.objectEventCallbacks.has(key)) { + this.objectEventCallbacks.set(key, []); + } + const callbacks = this.objectEventCallbacks.get(key); + const needsSocketSubscribe = callbacks.length === 0; + callbacks.push(callback); + if (needsSocketSubscribe) { + emitSubscribe(); + } + return () => + this.unsubscribeFromObjectEvent({ + objectType, + _id, + eventType, + callback, + }); + } + + emitSubscribe(); } - async unsubscribeFromObjectEvent({ objectType, _id, eventType }) { + unsubscribeFromObjectEvent({ objectType, _id, eventType, callback }) { logger.debug("Unsuscribing from object event...", { objectType, _id, eventType, }); - this.socket.emit("unsubscribeFromObjectEvent", { - objectType, - _id, - eventType, - }); + + const emitUnsubscribe = () => { + this.socket.emit("unsubscribeObjectEvent", { + objectType, + _id, + eventType, + }); + }; + + const key = this.getObjectEventKey(objectType, _id, eventType); + if (typeof callback === "function" && this.objectEventCallbacks.has(key)) { + const remaining = this.objectEventCallbacks + .get(key) + .filter((registered) => registered !== callback); + if (remaining.length === 0) { + this.objectEventCallbacks.delete(key); + emitUnsubscribe(); + } else { + this.objectEventCallbacks.set(key, remaining); + } + return; + } + + this.objectEventCallbacks.delete(key); + emitUnsubscribe(); } async handleHostChildUpdate(data) { @@ -675,6 +832,19 @@ export class SocketClient { _id, event, }); + + const key = this.getObjectEventKey(objectType, _id, event?.type); + const callbacks = this.objectEventCallbacks.get(key); + if (Array.isArray(callbacks) && callbacks.length > 0) { + callbacks.forEach((callback) => { + try { + callback(event); + } catch (error) { + logger.error("Error in object event callback:", error); + } + }); + } + const data = event.data; if ( (event.type == "childUpdate" ||