Enhance document printing progress tracking and rendering functionality
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- Updated the `DocumentPrinterClient` to include detailed progress tracking during the deployment process, improving user feedback. - Modified the `deploy` methods in `CupsInterface` and `ReceiptInterface` to accept an `onProgress` callback for real-time progress updates. - Enhanced the `SocketClient` to support rendering templates with progress notifications, allowing for better handling of asynchronous rendering tasks.
This commit is contained in:
parent
e03a56bb78
commit
0dc7ed3f92
@ -331,6 +331,11 @@ export class DocumentPrinterClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.printerInterface.deploy) {
|
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" });
|
await this.updateJobState(documentJob._id, { type: "deploying" });
|
||||||
const documentTemplate = await this.socketClient.getObject({
|
const documentTemplate = await this.socketClient.getObject({
|
||||||
objectType: "documentTemplate",
|
objectType: "documentTemplate",
|
||||||
@ -338,7 +343,7 @@ export class DocumentPrinterClient {
|
|||||||
});
|
});
|
||||||
await this.updateJobState(documentJob._id, {
|
await this.updateJobState(documentJob._id, {
|
||||||
type: "deploying",
|
type: "deploying",
|
||||||
progress: 0.25,
|
progress: fetchTemplateProgress,
|
||||||
});
|
});
|
||||||
const object = await this.socketClient.getObject({
|
const object = await this.socketClient.getObject({
|
||||||
objectType: documentJob.objectType,
|
objectType: documentJob.objectType,
|
||||||
@ -346,7 +351,7 @@ export class DocumentPrinterClient {
|
|||||||
});
|
});
|
||||||
await this.updateJobState(documentJob._id, {
|
await this.updateJobState(documentJob._id, {
|
||||||
type: "deploying",
|
type: "deploying",
|
||||||
progress: 0.5,
|
progress: fetchObjectProgress,
|
||||||
});
|
});
|
||||||
if (!documentTemplate) {
|
if (!documentTemplate) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -356,12 +361,22 @@ export class DocumentPrinterClient {
|
|||||||
}
|
}
|
||||||
await this.updateJobState(documentJob._id, {
|
await this.updateJobState(documentJob._id, {
|
||||||
type: "deploying",
|
type: "deploying",
|
||||||
progress: 0.75,
|
progress: renderProgressStart,
|
||||||
});
|
});
|
||||||
const result = await this.printerInterface.deploy(
|
const result = await this.printerInterface.deploy(
|
||||||
documentJob,
|
documentJob,
|
||||||
documentTemplate,
|
documentTemplate,
|
||||||
object,
|
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, {
|
await this.updateJobState(documentJob._id, {
|
||||||
type: "deploying",
|
type: "deploying",
|
||||||
|
|||||||
@ -229,16 +229,19 @@ export default class CupsInterface {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(documentJob, documentTemplate, object) {
|
async deploy(documentJob, documentTemplate, object, onProgress) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Deploying job ${documentJob._id} to CUPS printer ${this.name}`
|
`Deploying job ${documentJob._id} to CUPS printer ${this.name}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const pdfObj = await this.documentPrinterClient.socketClient.renderTemplatePDF({
|
const pdfObj = await this.documentPrinterClient.socketClient.renderTemplatePDF(
|
||||||
_id: documentTemplate._id,
|
{
|
||||||
content: documentTemplate.content,
|
_id: documentTemplate._id,
|
||||||
object: object,
|
content: documentTemplate.content,
|
||||||
});
|
object: object,
|
||||||
|
},
|
||||||
|
onProgress,
|
||||||
|
);
|
||||||
|
|
||||||
if (!pdfObj || !pdfObj.pdf) {
|
if (!pdfObj || !pdfObj.pdf) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -163,7 +163,7 @@ export default class ReceiptInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(documentJob, documentTemplate, object) {
|
async deploy(documentJob, documentTemplate, object, onProgress) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Deploying job ${documentJob._id} to receipt printer ${this.name}`
|
`Deploying job ${documentJob._id} to receipt printer ${this.name}`
|
||||||
);
|
);
|
||||||
@ -177,6 +177,7 @@ export default class ReceiptInterface {
|
|||||||
width: 512,
|
width: 512,
|
||||||
},
|
},
|
||||||
"png",
|
"png",
|
||||||
|
onProgress,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!imageObj || !imageObj.images) {
|
if (!imageObj || !imageObj.images) {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export class SocketClient {
|
|||||||
this.documentPrinterManager = new DocumentPrinterManager(this);
|
this.documentPrinterManager = new DocumentPrinterManager(this);
|
||||||
this.scanner = new WebSocketScanner({ maxThreads: 50 });
|
this.scanner = new WebSocketScanner({ maxThreads: 50 });
|
||||||
this.readLine = null;
|
this.readLine = null;
|
||||||
|
this.objectEventCallbacks = new Map();
|
||||||
sendIPC("setOnline", false);
|
sendIPC("setOnline", false);
|
||||||
sendIPC("setAuthenticated", false);
|
sendIPC("setAuthenticated", false);
|
||||||
sendIPC("setLoading", false);
|
sendIPC("setLoading", false);
|
||||||
@ -543,60 +544,171 @@ export class SocketClient {
|
|||||||
return data?.error || error.message;
|
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);
|
logger.debug("Rendering template PDF...", templateData);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const result = await this.renderTemplateWithProgress(
|
||||||
`${this.getApiUrl()}/documenttemplates/${templateData._id}/download`,
|
templateData,
|
||||||
{
|
"pdf",
|
||||||
content: templateData.content,
|
onProgress,
|
||||||
object: templateData.object,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: { type: "pdf", padding: false },
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
headers: this.getApiHeaders(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
logger.trace("Template PDF rendered successfully.");
|
logger.trace("Template PDF rendered successfully.");
|
||||||
return { pdf: Buffer.from(response.data) };
|
if (result?.pdf) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { pdf: result.buffer };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = this.parseTemplateDownloadError(error);
|
const message = this.parseTemplateDownloadError(error) || error.message;
|
||||||
logger.error("Failed to render template PDF:", message);
|
logger.error("Failed to render template PDF:", message);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderTemplateJPG(templateData, type = "jpeg") {
|
async renderTemplateJPG(templateData, type = "jpeg", onProgress) {
|
||||||
logger.debug(`Rendering template ${type.toUpperCase()}...`, templateData);
|
logger.debug(`Rendering template ${type.toUpperCase()}...`, templateData);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const result = await this.renderTemplateWithProgress(
|
||||||
`${this.getApiUrl()}/documenttemplates/${templateData._id}/download`,
|
templateData,
|
||||||
{
|
type,
|
||||||
content: templateData.content,
|
onProgress,
|
||||||
object: templateData.object,
|
|
||||||
width: templateData.width,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: { type, padding: false },
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
headers: this.getApiHeaders(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const contentType = response.headers["content-type"] || "";
|
logger.trace("Template JPG rendered successfully.");
|
||||||
if (contentType.includes("application/json")) {
|
if (Array.isArray(result?.images)) {
|
||||||
const payload = JSON.parse(Buffer.from(response.data).toString("utf8"));
|
|
||||||
logger.trace("Template JPG rendered successfully.");
|
|
||||||
return {
|
return {
|
||||||
images: (payload.images || []).map((image) =>
|
images: result.images.map((image) =>
|
||||||
Buffer.from(image, "base64"),
|
Buffer.isBuffer(image) ? image : Buffer.from(image, "base64"),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
logger.trace("Template JPG rendered successfully.");
|
return { images: [result.buffer] };
|
||||||
return { images: [Buffer.from(response.data)] };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = this.parseTemplateDownloadError(error);
|
const message = this.parseTemplateDownloadError(error) || error.message;
|
||||||
logger.error("Failed to render template JPG:", message);
|
logger.error("Failed to render template JPG:", message);
|
||||||
throw new Error(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...", {
|
logger.debug("Suscribing to object event...", {
|
||||||
objectType,
|
objectType,
|
||||||
_id,
|
_id,
|
||||||
eventType,
|
eventType,
|
||||||
});
|
});
|
||||||
this.socket.emit("subscribeToObjectEvent", {
|
|
||||||
objectType,
|
const emitSubscribe = () => {
|
||||||
_id,
|
this.socket.emit("subscribeToObjectEvent", {
|
||||||
eventType,
|
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...", {
|
logger.debug("Unsuscribing from object event...", {
|
||||||
objectType,
|
objectType,
|
||||||
_id,
|
_id,
|
||||||
eventType,
|
eventType,
|
||||||
});
|
});
|
||||||
this.socket.emit("unsubscribeFromObjectEvent", {
|
|
||||||
objectType,
|
const emitUnsubscribe = () => {
|
||||||
_id,
|
this.socket.emit("unsubscribeObjectEvent", {
|
||||||
eventType,
|
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) {
|
async handleHostChildUpdate(data) {
|
||||||
@ -675,6 +832,19 @@ export class SocketClient {
|
|||||||
_id,
|
_id,
|
||||||
event,
|
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;
|
const data = event.data;
|
||||||
if (
|
if (
|
||||||
(event.type == "childUpdate" ||
|
(event.type == "childUpdate" ||
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user