Update configuration and introduce object ID handling utilities
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit

- 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.
This commit is contained in:
Tom Butcher 2026-11-07 18:53:01 +00:00
parent 158eb39f06
commit 2522e2f9ce
8 changed files with 195 additions and 60 deletions

View File

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

View File

@ -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: "",

98
src/objectid.js Normal file
View File

@ -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;
}

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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,
};