- src/domain/BaseDomain.js — base class for every specificClass; wires emitter/config/logger/measurements/childRouter - src/nodered/commandRegistry.js — declarative msg.topic dispatch with alias deprecation - src/nodered/statusUpdater.js — 1Hz status badge poller with error-resilient loop Additive. 43 new tests; all 99 basic tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3.0 KiB
JavaScript
91 lines
3.0 KiB
JavaScript
/**
|
|
* StatusUpdater — periodic Node-RED status badge poller.
|
|
*
|
|
* Replaces the per-node `_statusInterval` boilerplate (e.g. pumpingStation
|
|
* nodeClass lines 160-171) with one class. The adapter constructs it once
|
|
* with a `node` (Node-RED handle) and a `source` (the domain), and the
|
|
* loop drives `node.status(source.getStatusBadge())` at a fixed cadence.
|
|
*
|
|
* Errors thrown from the domain become a red error badge instead of
|
|
* crashing the interval — operators see the failure in the editor.
|
|
*
|
|
* See CONTRACTS.md §7 for the badge shape; statusBadge.js for the helpers.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { statusBadge } = require('./statusBadge');
|
|
|
|
const CLEAR_BADGE = {};
|
|
|
|
class StatusUpdater {
|
|
constructor({ node, source, intervalMs, logger } = {}) {
|
|
if (!node || typeof node.status !== 'function') {
|
|
throw new Error('StatusUpdater: node must expose a .status(badge) method');
|
|
}
|
|
if (!source || typeof source.getStatusBadge !== 'function') {
|
|
throw new Error('StatusUpdater: source must expose a .getStatusBadge() method');
|
|
}
|
|
this._node = node;
|
|
this._source = source;
|
|
this._intervalMs = Number.isFinite(intervalMs) ? intervalMs : 0;
|
|
this._logger = logger || null;
|
|
this._timer = null;
|
|
}
|
|
|
|
get isRunning() {
|
|
return this._timer !== null;
|
|
}
|
|
|
|
start() {
|
|
// intervalMs=0 keeps unit tests / headless harnesses silent.
|
|
if (this._intervalMs <= 0) return;
|
|
if (this._timer !== null) return;
|
|
this._timer = setInterval(() => this._tick(), this._intervalMs);
|
|
}
|
|
|
|
stop() {
|
|
if (this._timer !== null) {
|
|
clearInterval(this._timer);
|
|
this._timer = null;
|
|
}
|
|
// Wipe the badge so a stale label doesn't linger in the editor
|
|
// after the node is closed/redeployed.
|
|
try { this._node.status(CLEAR_BADGE); } catch (_) { /* best effort */ }
|
|
}
|
|
|
|
_tick() {
|
|
let badge;
|
|
try {
|
|
badge = this._source.getStatusBadge();
|
|
} catch (err) {
|
|
const msg = err && err.message ? err.message : String(err);
|
|
if (this._logger && typeof this._logger.error === 'function') {
|
|
this._logger.error(`StatusUpdater: getStatusBadge threw: ${msg}`);
|
|
}
|
|
this._safeApply(statusBadge.error(msg));
|
|
return;
|
|
}
|
|
if (badge == null) {
|
|
this._safeApply(CLEAR_BADGE);
|
|
return;
|
|
}
|
|
this._safeApply(badge);
|
|
}
|
|
|
|
_safeApply(badge) {
|
|
try {
|
|
this._node.status(badge);
|
|
} catch (err) {
|
|
// node.status itself failing is exotic (e.g. node already
|
|
// closed). Log once per tick; the next tick will retry.
|
|
if (this._logger && typeof this._logger.error === 'function') {
|
|
const msg = err && err.message ? err.message : String(err);
|
|
this._logger.error(`StatusUpdater: node.status threw: ${msg}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { StatusUpdater };
|