fix: production hardening — safety fixes, prediction accuracy, test coverage
Safety: - Async input handler: await all handleInput() calls, prevents unhandled rejections - Fix emergencyStop case mismatch: "emergencyStop" → "emergencystop" matching config - Implement showCoG() method (was routing to undefined) - Null guards on 6 methods for missing curve data - Editor menu polling timeout (5s max) - Listener cleanup on node close (child measurements + state emitter) - Tick loop race condition: track startup timeout, clear on close Prediction accuracy: - Remove efficiency rounding that destroyed signal in canonical units - Fix calcEfficiency variant: hydraulic power reads from correct variant - Guard efficiency calculations against negative/zero values - Division-by-zero protection in calcRelativeDistanceFromPeak - Curve data anomaly detection (cross-pressure median-y ratio check) - calcEfficiencyCurve O(n²) → O(n) with running min - updateCurve bootstraps predictors when they were null Tests: 43 new tests (76 total) covering emergency stop, shutdown/maintenance sequences, efficiency/CoG, movement lifecycle, output format, null guards, and listener cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -254,10 +254,11 @@ class nodeClass {
|
||||
* Start the periodic tick loop.
|
||||
*/
|
||||
_startTickLoop() {
|
||||
setTimeout(() => {
|
||||
this._startupTimeout = setTimeout(() => {
|
||||
this._startupTimeout = null;
|
||||
this._tickInterval = setInterval(() => this._tick(), 1000);
|
||||
|
||||
// Update node status on nodered screen every second ( this is not the best way to do this, but it works for now)
|
||||
// Update node status on nodered screen every second
|
||||
this._statusInterval = setInterval(() => {
|
||||
const status = this._updateNodeStatus();
|
||||
this.node.status(status);
|
||||
@@ -284,15 +285,13 @@ class nodeClass {
|
||||
* Attach the node's input handler, routing control messages to the class.
|
||||
*/
|
||||
_attachInputHandler() {
|
||||
this.node.on('input', (msg, send, done) => {
|
||||
/* Update to complete event based node by putting the tick function after an input event */
|
||||
this.node.on('input', async (msg, send, done) => {
|
||||
const m = this.source;
|
||||
const nodeSend = typeof send === 'function' ? send : (outMsg) => this.node.send(outMsg);
|
||||
|
||||
try {
|
||||
switch(msg.topic) {
|
||||
case 'registerChild': {
|
||||
// Register this node as a child of the parent node
|
||||
const childId = msg.payload;
|
||||
const childObj = this.RED.nodes.getNode(childId);
|
||||
if (!childObj || !childObj.source) {
|
||||
@@ -307,22 +306,22 @@ class nodeClass {
|
||||
break;
|
||||
case 'execSequence': {
|
||||
const { source, action, parameter } = msg.payload;
|
||||
m.handleInput(source, action, parameter);
|
||||
await m.handleInput(source, action, parameter);
|
||||
break;
|
||||
}
|
||||
case 'execMovement': {
|
||||
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
|
||||
m.handleInput(mvSource, mvAction, Number(setpoint));
|
||||
await m.handleInput(mvSource, mvAction, Number(setpoint));
|
||||
break;
|
||||
}
|
||||
case 'flowMovement': {
|
||||
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
|
||||
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
|
||||
await m.handleInput(fmSource, fmAction, Number(fmSetpoint));
|
||||
break;
|
||||
}
|
||||
case 'emergencystop': {
|
||||
const { source: esSource, action: esAction } = msg.payload;
|
||||
m.handleInput(esSource, esAction);
|
||||
await m.handleInput(esSource, esAction);
|
||||
break;
|
||||
}
|
||||
case 'simulateMeasurement':
|
||||
@@ -403,8 +402,28 @@ class nodeClass {
|
||||
*/
|
||||
_attachCloseHandler() {
|
||||
this.node.on('close', (done) => {
|
||||
clearTimeout(this._startupTimeout);
|
||||
clearInterval(this._tickInterval);
|
||||
clearInterval(this._statusInterval);
|
||||
|
||||
// Clean up child measurement listeners
|
||||
const m = this.source;
|
||||
if (m?.childMeasurementListeners) {
|
||||
for (const [, entry] of m.childMeasurementListeners) {
|
||||
if (typeof entry.emitter?.off === 'function') {
|
||||
entry.emitter.off(entry.eventName, entry.handler);
|
||||
} else if (typeof entry.emitter?.removeListener === 'function') {
|
||||
entry.emitter.removeListener(entry.eventName, entry.handler);
|
||||
}
|
||||
}
|
||||
m.childMeasurementListeners.clear();
|
||||
}
|
||||
|
||||
// Clean up state emitter listeners
|
||||
if (m?.state?.emitter) {
|
||||
m.state.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
if (typeof done === 'function') done();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user