From 2522e2f9ce7a33a73df0ab1c0fdc6d24741f5308 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 7 Nov 2026 18:53:01 +0000 Subject: [PATCH] Update configuration and introduce object ID handling utilities - Changed the API URL in both `config.json` and `src/config.js` for the development environment. - Added a new `objectid.js` file containing utility functions for handling object IDs, including `stringifyIds` and `refId`. - Updated the `PrinterDatabase`, `PrinterClient`, and `PrinterManager` classes to utilize the new object ID handling functions, ensuring consistent ID management across the application. - Enhanced the `SocketClient` to incorporate a custom parser for handling string IDs in socket communications. --- config.json | 2 +- src/config.js | 2 +- src/objectid.js | 98 +++++++++++++++++++++++++++++++++++ src/printer/database.js | 62 +++++++++++++--------- src/printer/printerclient.js | 65 +++++++++++------------ src/printer/printermanager.js | 5 +- src/socket/socketclient.js | 2 + src/socket/stringidparser.js | 19 +++++++ 8 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 src/objectid.js create mode 100644 src/socket/stringidparser.js diff --git a/config.json b/config.json index fef8889..891ddc8 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "development": { "logLevel": "debug", "url": "https://dev-wss.tombutcher.work", - "apiUrl": "https://dev.tombutcher.work/api", + "apiUrl": "https://dev.tombutcher.work/fcapi", "host": { "id": "691a1db49ce913faf0e51284", "authCode": "6wdCQPKMr_zuFaJe3-AY99uRuAEZRgU2lLNTuAFPW5hPvzzTrSra3rh3woZLj-eE" diff --git a/src/config.js b/src/config.js index 8977485..a894e60 100644 --- a/src/config.js +++ b/src/config.js @@ -136,7 +136,7 @@ const DEFAULT_CONFIG = { development: { logLevel: "debug", url: "https://dev-wss.tombutcher.work", - apiUrl: "https://dev.tombutcher.work/api", + apiUrl: "https://dev.tombutcher.work/fcapi", host: { id: "", authCode: "", diff --git a/src/objectid.js b/src/objectid.js new file mode 100644 index 0000000..7fbfa0d --- /dev/null +++ b/src/objectid.js @@ -0,0 +1,98 @@ +function bytesOf(value) { + if (value == null) return null; + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { + return value.length === 12 ? value : null; + } + if (value instanceof Uint8Array) { + return value.byteLength === 12 ? Buffer.from(value) : null; + } + if ( + value && + value.type === 'Buffer' && + Array.isArray(value.data) && + value.data.length === 12 + ) { + return Buffer.from(value.data); + } + if (typeof value !== 'object' || Array.isArray(value) || value instanceof Date) { + return null; + } + + const keys = Object.keys(value); + if (keys.length !== 12) return null; + + const bytes = Buffer.alloc(12); + for (let index = 0; index < 12; index += 1) { + if (!Object.prototype.hasOwnProperty.call(value, index)) return null; + const byte = Number(value[index]); + if (!Number.isInteger(byte) || byte < 0 || byte > 255) return null; + bytes[index] = byte; + } + return bytes; +} + +const PURE_REF_KEYS = new Set(['_id', 'id', 'buffer', '$oid']); + +function isPureRef(value) { + if (value == null || typeof value !== 'object') return false; + if (typeof value.toHexString === 'function') return true; + if (Array.isArray(value) || value instanceof Date) return false; + const keys = Object.keys(value); + return keys.length > 0 && keys.every(key => PURE_REF_KEYS.has(key)) && refId(value) != null; +} + +function isBinaryPayload(value) { + if (value == null || typeof value !== 'object') return false; + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return true; + if (value instanceof Uint8Array || value instanceof ArrayBuffer) return true; + const name = value.constructor?.name; + return name === 'Blob' || name === 'File'; +} + +// Object ids are always hex strings on the wire. File buffers are left alone. +export function stringifyIds(value) { + if (value == null) return value; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + if (typeof value.toHexString === 'function') return value.toHexString(); + if (value instanceof Date || typeof value === 'function') return value; + if (isBinaryPayload(value)) return value; + if (Array.isArray(value)) return value.map(item => stringifyIds(item)); + if (typeof value === 'object') { + if (isPureRef(value)) { + const id = refId(value); + if (typeof id === 'string') return id; + } + const copy = {}; + for (const [key, child] of Object.entries(value)) { + copy[key] = stringifyIds(child); + } + return copy; + } + return value; +} + +export function refId(value) { + if (value == null || value === '') return null; + if (typeof value === 'string') return value; + if (typeof value === 'object' && typeof value.toHexString === 'function') { + return value.toHexString(); + } + if (typeof value !== 'object' || Array.isArray(value) || value instanceof Date) { + return null; + } + if (typeof value.$oid === 'string') return refId(value.$oid); + + const keys = Object.keys(value); + if ( + keys.length > 0 && + keys.every(key => key === 'buffer' || key === 'id' || key === 'type' || key === 'data') + ) { + const bytes = bytesOf(value.buffer) || bytesOf(value.id) || bytesOf(value); + if (bytes) return bytes.toString('hex'); + } + + if (value._id != null) return refId(value._id); + return null; +} diff --git a/src/printer/database.js b/src/printer/database.js index d06f528..0f3a055 100644 --- a/src/printer/database.js +++ b/src/printer/database.js @@ -5,6 +5,7 @@ import { loadConfig } from "../config.js"; import { sendIPC } from "../desktop/notify.js"; import chalk from "chalk"; import { formatState } from "../commandlineutils.js"; +import { refId } from "../objectid.js"; const config = loadConfig(); const logger = log4js.getLogger("Printer Database"); logger.level = config.logLevel; @@ -13,7 +14,7 @@ export class PrinterDatabase { constructor(socketClient, printer) { this.socketClient = socketClient; this.printer = printer; - this.id = this.printer._id; + this.id = refId(this.printer._id); // Initialize cache with 30 second TTL this.printerCache = new NodeCache({ stdTTL: 30 }); } @@ -26,7 +27,7 @@ export class PrinterDatabase { "queue", ]; const populateKey = populate.sort().join(","); - const cacheKey = `printer_${this.printer._id}_${populateKey}`; + const cacheKey = `printer_${this.id}_${populateKey}`; // Check if printer is in cache const cachedPrinter = this.printerCache.get(cacheKey); @@ -38,7 +39,7 @@ export class PrinterDatabase { // Fetch from socketClient if not in cache const object = await this.socketClient.getObject({ - _id: this.printer._id, + _id: this.id, objectType: "printer", populate: populate, }); @@ -60,21 +61,25 @@ export class PrinterDatabase { "queue", ]; const object = await this.socketClient.editObject({ - _id: this.printer._id, + _id: this.id, objectType: "printer", updateData: updateData, populate: populate, }); - // Update cache with the edited printer (overwrite existing cache) + if (!object || object.error) { + throw new Error(object?.error || "Failed to edit printer"); + } + + // Drop the cache so the next read comes back with populated jobs. + // The patch itself may only contain ids. const populateKey = populate.sort().join(","); - const cacheKey = `printer_${this.printer._id}_${populateKey}`; - this.printerCache.del(cacheKey); // Delete existing cache entry - this.printerCache.set(cacheKey, { ...object, ...updateData }); // Set new cache entry - this.printer = { ...object, ...updateData }; + const cacheKey = `printer_${this.id}_${populateKey}`; + this.printerCache.del(cacheKey); + this.printer = { ...this.printer, ...object }; logger.debug(`Updated printer cache for ${this.id}`); - return object; + return this.printer; } async setPrinterConnectedAt() { @@ -162,6 +167,7 @@ export class PrinterDatabase { async updateSubJobState(subJobId, state) { try { + subJobId = refId(subJobId); const updatedSubJob = await this.socketClient.editObject({ _id: subJobId, objectType: "subJob", @@ -181,14 +187,20 @@ export class PrinterDatabase { async updateJobState(jobId) { try { + jobId = refId(jobId); logger.debug(`Updating job state for ${jobId}`); const subJobs = await this.socketClient.listObjects({ objectType: "subJob", - filter: { job: { _id: jobId } }, + filter: { job: jobId }, cached: false, }); + if (!Array.isArray(subJobs) || subJobs.length === 0) { + logger.warn(`No subjobs found for job ${jobId}`); + return null; + } + const subJobStates = subJobs.map((subJob) => subJob.state); const stateCounts = { printing: 0, @@ -201,6 +213,7 @@ export class PrinterDatabase { }; subJobStates.forEach((state) => { + if (!state?.type || stateCounts[state.type] == null) return; stateCounts[state.type]++; }); @@ -260,7 +273,7 @@ export class PrinterDatabase { moonrakerJobId, }); const updatedSubJob = await this.socketClient.editObject({ - _id: subJobId, + _id: refId(subJobId), objectType: "subJob", updateData: { moonrakerJobId: moonrakerJobId }, }); @@ -321,8 +334,8 @@ export class PrinterDatabase { jobId: job?._id || null, }); const updatedPrinter = await this.editPrinter({ - currentSubJob: subJob, - currentJob: job, + currentSubJob: subJob ? refId(subJob) : null, + currentJob: job ? refId(job) : null, }); return updatedPrinter; } catch (error) { @@ -530,13 +543,13 @@ export class PrinterDatabase { subJob = null, job = null, ) { - const subJobId = subJob._id || subJob.id; - const jobId = job._id || job.id; + const subJobId = refId(subJob); + const jobId = refId(job); try { const filamentStockStockEvents = await this.socketClient.listObjects({ objectType: "stockEvent", filter: { - "parent._id": filamentStock._id, + "parent._id": refId(filamentStock), parentType: "filamentStock", }, populate: ["owner"], @@ -544,7 +557,7 @@ export class PrinterDatabase { const stockEvents = filamentStockStockEvents.filter( (event) => - event.owner._id.toString() === subJobId.toString() && + refId(event.owner) === subJobId && event.ownerType === "subJob", ); @@ -607,7 +620,7 @@ export class PrinterDatabase { // 1. Update SubJob with Moonraker ID and State await this.socketClient.editObject({ - _id: subJob._id, + _id: refId(subJob), objectType: "subJob", updateData: { moonrakerJobId: moonrakerJobId, @@ -616,10 +629,10 @@ export class PrinterDatabase { }); // 2. Add to Printer's Queue - const newQueue = [...this.printer.queue, subJob]; + const newQueue = [...(this.printer.queue || []), subJob]; const updatedPrinter = await this.editPrinter({ - queue: newQueue, + queue: newQueue.map((queued) => refId(queued)), }); this.printer.queue = newQueue; @@ -638,8 +651,9 @@ export class PrinterDatabase { subJobs.map((subJob) => subJob._id).join(", "), ); const updatedPrinter = await this.editPrinter({ - queue: subJobs, + queue: subJobs.map((subJob) => refId(subJob)), }); + this.printer.queue = subJobs; return updatedPrinter; } catch (error) { logger.error( @@ -669,7 +683,7 @@ export class PrinterDatabase { logger.debug(`Getting job by ID ${jobId} for printer ${this.id}`); const job = await this.socketClient.getObject({ objectType: "job", - _id: jobId, + _id: refId(jobId), }); return job; } catch (error) { @@ -697,7 +711,7 @@ export class PrinterDatabase { ); const gcodeFile = await this.socketClient.getObject({ objectType: "gcodeFile", - _id: subJob.gcodeFile._id, + _id: refId(subJob.gcodeFile), populate: ["parts.part", "parts.partSku"], }); const partItems = gcodeFile.parts; diff --git a/src/printer/printerclient.js b/src/printer/printerclient.js index 54fe94d..0d3b73f 100644 --- a/src/printer/printerclient.js +++ b/src/printer/printerclient.js @@ -9,6 +9,7 @@ import { startWaiting, stopWaiting } from "../spinner.js"; import axios from "axios"; import FormData from "form-data"; import _ from "lodash"; +import { refId } from "../objectid.js"; // Load configuration const config = loadConfig(); @@ -17,7 +18,7 @@ logger.level = config.logLevel; export class PrinterClient { constructor(printer, printerManager) { - this.id = printer._id; + this.id = refId(printer._id); this.printer = printer; this.name = printer.name; this.printerManager = printerManager; @@ -360,7 +361,7 @@ export class PrinterClient { async getCurrentFilament() { logger.info(`Getting current filament for printer: ${this.printer.name}`); - if (!this.currentFilamentStock || !this.currentFilamentStock?._id) { + if (!refId(this.currentFilamentStock)) { logger.warn( `No current filament stock found for printer: ${this.printer.name}`, ); @@ -368,7 +369,7 @@ export class PrinterClient { } const result = await this.socketClient.getObject({ objectType: "filamentStock", - _id: this.currentFilamentStock._id, + _id: refId(this.currentFilamentStock), populate: ["filamentSku"], }); this.currentFilamentSku = result.filamentSku; @@ -376,13 +377,13 @@ export class PrinterClient { } async loadCurrentFilament() { - if (!this.currentFilamentStock?.filament?._id) { + if (!refId(this.currentFilamentStock?.filament)) { this.currentFilament = null; return null; } this.currentFilament = await this.socketClient.getObject({ objectType: "filament", - _id: this.currentFilamentStock.filament._id, + _id: refId(this.currentFilamentStock.filament), }); return this.currentFilament; } @@ -830,13 +831,13 @@ export class PrinterClient { if (stateChanged == true) { if (printerFinished == true && isCurrentJobSubJob == true) { logger.info( - `Subjob ${this.currentSubJob._id} completed, posting part stock items for printer ${this.id}`, + `Subjob ${refId(this.currentSubJob)} completed, posting part stock items for printer ${this.id}`, ); await this.database.setSubJobFinishedAt( - this.currentSubJob._id, + refId(this.currentSubJob), new Date(), ); - await this.database.postSubJobPartStockItems(this.currentSubJob._id); + await this.database.postSubJobPartStockItems(refId(this.currentSubJob)); } // Update printer state first await this.updatePrinterState(); @@ -848,13 +849,13 @@ export class PrinterClient { if (this.currentSubJob.startedAt == null) { this.currentSubJob.startedAt = new Date(); await this.database.setSubJobStartedAt( - this.currentSubJob._id, + refId(this.currentSubJob), new Date(), ); } if (this.currentJob.startedAt == null) { this.currentJob.startedAt = new Date(); - await this.database.setJobStartedAt(this.currentJob._id, new Date()); + await this.database.setJobStartedAt(refId(this.currentJob), new Date()); } this.currentFilamentUsed = 0; } @@ -866,10 +867,10 @@ export class PrinterClient { progress: this.state.progress, }; await this.database.updateSubJobState( - this.currentSubJob._id, + refId(this.currentSubJob), subJobState, ); - await this.database.updateJobState(this.currentJob._id); + await this.database.updateJobState(refId(this.currentJob)); } if (progressChanged) { @@ -997,28 +998,28 @@ export class PrinterClient { if (syncedTypes.includes(stateType)) { if (isCurrentJobSubJob == true) { - await this.database.updateSubJobState(this.currentSubJob._id, { + await this.database.updateSubJobState(refId(this.currentSubJob), { type: stateType, }); this.currentSubJob.state.type = stateType; this.currentJob.state = await this.database.updateJobState( - this.currentJob._id, + refId(this.currentJob), ); } } if (failedTypes.includes(stateType)) { for (const subJob of unqueuedSubJobs) { - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "failed", }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); } if (isCurrentJobSubJob == true) { - await this.database.updateSubJobState(this.currentSubJob._id, { + await this.database.updateSubJobState(refId(this.currentSubJob), { type: "failed", }); - await this.database.updateJobState(this.currentJob._id); + await this.database.updateJobState(refId(this.currentJob)); this.currentSubJob = null; this.currentJob = null; await this.database.setCurrentJobAndSubJob(null, null); @@ -1027,14 +1028,14 @@ export class PrinterClient { if (completedTypes.includes(stateType)) { const otherSubJobs = unqueuedSubJobs - .filter((subJob) => subJob._id != this.currentSubJob?._id) + .filter((subJob) => refId(subJob) != refId(this.currentSubJob)) .filter((subJob) => subJob.state.type == "queued"); if (otherSubJobs.length > 0) { for (const subJob of otherSubJobs) { - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "failed", }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); } } @@ -1180,11 +1181,11 @@ export class PrinterClient { Math.max(remainingTime, 0), ); - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "deploying", progress: 0, }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); } async processDeploySubJobQueue() { @@ -1211,10 +1212,10 @@ export class PrinterClient { `Error deploying sub job ${subJob._id} to printer ${this.id}:`, error, ); - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "failed", }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); } } } @@ -1226,29 +1227,29 @@ export class PrinterClient { "to printer:", this.id, "with gcode file:", - `${subJob.gcodeFile._id}`, + `${refId(subJob.gcodeFile)}`, ); const gcodeFile = await this.socketClient.getObject({ objectType: "gcodeFile", - _id: subJob.gcodeFile._id, + _id: refId(subJob.gcodeFile), }); if (!gcodeFile) { throw new Error("G-code file not found"); } - const fileId = (gcodeFile.file?._id || gcodeFile.file).toString(); + const fileId = refId(gcodeFile.file); let deploymentActive = true; const updateDeployProgress = async (progress) => { if (!deploymentActive) { return; } - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "deploying", progress, }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); }; const file = await this.socketClient.fileManager.getFile( @@ -1300,10 +1301,10 @@ export class PrinterClient { subJob._id, mostRecentQueuedJob.job_id, ); - await this.database.updateSubJobState(subJob._id, { + await this.database.updateSubJobState(refId(subJob), { type: "queued", }); - await this.database.updateJobState(subJob.job._id); + await this.database.updateJobState(refId(subJob.job)); this.queue.push({ ...updatedSubJob, diff --git a/src/printer/printermanager.js b/src/printer/printermanager.js index 125fe46..672be32 100644 --- a/src/printer/printermanager.js +++ b/src/printer/printermanager.js @@ -4,6 +4,7 @@ import { loadConfig } from "../config.js"; import log4js from "log4js"; import { sendIPC } from "../desktop/notify.js"; import { startWaiting, stopWaiting } from "../spinner.js"; +import { refId } from "../objectid.js"; // Load configuration const config = loadConfig(); @@ -36,7 +37,7 @@ export class PrinterManager { var removedPrintersCount = 0; // Remove printer clients that are no longer in the printers list - const printerIds = this.printers.map((printer) => printer._id); + const printerIds = this.printers.map((printer) => refId(printer._id)); for (const [printerId, printerClient] of this.printerClients.entries()) { if (!printerIds.includes(printerId)) { // Close the connection before removing @@ -53,7 +54,7 @@ export class PrinterManager { var addedPrintersCount = 0; // Add new printer clients for printers not in the printerClients map for (const printer of this.printers) { - const printerId = printer._id; + const printerId = refId(printer._id); if (!this.printerClients.has(printerId)) { const printerClient = new PrinterClient(printer, this); await printerClient.connect(); diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js index 4f8c603..7852885 100644 --- a/src/socket/socketclient.js +++ b/src/socket/socketclient.js @@ -10,6 +10,7 @@ import { HostManager } from "../host/hostmanager.js"; import { FileManager } from "../files/filemanager.js"; import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js"; import { startWaiting, stopWaiting } from "../spinner.js"; +import { stringIdParser } from "./stringidparser.js"; const config = loadConfig(); @@ -72,6 +73,7 @@ export class SocketClient { await stopWaiting(); startWaiting(`Connecting to Socket.IO server: ${config.url}`, logger); this.socket = io(config.url, { + parser: stringIdParser, auth: { type: "host" }, reconnection: false, timeout: 3000, // 3 second timeout diff --git a/src/socket/stringidparser.js b/src/socket/stringidparser.js new file mode 100644 index 0000000..aa267a1 --- /dev/null +++ b/src/socket/stringidparser.js @@ -0,0 +1,19 @@ +import { createRequire } from "node:module"; +import { stringifyIds } from "../objectid.js"; + +const require = createRequire(import.meta.url); +const { Encoder, Decoder } = createRequire(require.resolve("socket.io-client"))( + "socket.io-parser", +); + +class StringIdEncoder extends Encoder { + encode(packet) { + if (packet?.data == null) return super.encode(packet); + return super.encode({ ...packet, data: stringifyIds(packet.data) }); + } +} + +export const stringIdParser = { + Encoder: StringIdEncoder, + Decoder, +};