- 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>
140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
/**
|
|
* BaseDomain — shared specificClass scaffolding.
|
|
*
|
|
* Consolidates the constructor boilerplate that every domain (pumpingStation,
|
|
* measurement, MGC, rotatingMachine, …) repeats today: configManager →
|
|
* configUtils → logger → MeasurementContainer → childRegistrationUtils →
|
|
* ChildRouter. Subclasses declare `static name` (matches the JSON config in
|
|
* generalFunctions/src/configs/<name>.json) and optionally `static unitPolicy`
|
|
* (a UnitPolicy.declare(...) instance), then implement `configure()` to wire
|
|
* concern-modules.
|
|
*
|
|
* See CONTRACTS.md §3.
|
|
*/
|
|
|
|
const EventEmitter = require('events');
|
|
|
|
const configManager = require('../configs/index.js');
|
|
const configUtils = require('../helper/configUtils.js');
|
|
const Logger = require('../helper/logger.js');
|
|
const childRegistrationUtils = require('../helper/childRegistrationUtils.js');
|
|
const { MeasurementContainer } = require('../measurements/index.js');
|
|
const ChildRouter = require('./ChildRouter.js');
|
|
|
|
class BaseDomain {
|
|
constructor(userConfig = {}) {
|
|
const ctor = this.constructor;
|
|
if (ctor === BaseDomain) {
|
|
throw new Error('BaseDomain is abstract; subclass it and declare static name');
|
|
}
|
|
|
|
this.emitter = new EventEmitter();
|
|
|
|
this.configManager = new configManager();
|
|
this.defaultConfig = this.configManager.getConfig(ctor.name);
|
|
this.configUtils = new configUtils(this.defaultConfig);
|
|
this.config = this.configUtils.initConfig(userConfig);
|
|
|
|
const loggingCfg = this.config?.general?.logging || {};
|
|
this.logger = new Logger(
|
|
loggingCfg.enabled,
|
|
loggingCfg.logLevel,
|
|
this.config?.general?.name
|
|
);
|
|
|
|
// Read static unitPolicy via the constructor — `this.constructor`
|
|
// resolves to the leaf subclass even when this base ctor is the caller.
|
|
this.unitPolicy = ctor.unitPolicy ?? null;
|
|
if (this.unitPolicy && typeof this.unitPolicy.setLogger === 'function') {
|
|
this.unitPolicy.setLogger(this.logger);
|
|
}
|
|
|
|
const containerOptions = this.unitPolicy?.containerOptions
|
|
? this.unitPolicy.containerOptions()
|
|
: { autoConvert: true };
|
|
this.measurements = new MeasurementContainer(containerOptions, this.logger);
|
|
if (this.config?.general?.id) this.measurements.setChildId(this.config.general.id);
|
|
if (this.config?.general?.name) this.measurements.setChildName(this.config.general.name);
|
|
|
|
this.childRegistrationUtils = new childRegistrationUtils(this);
|
|
this.router = new ChildRouter(this);
|
|
|
|
// childRegistrationUtils calls back into mainClass.registerChild after
|
|
// storing the child. Routing through `this.router` keeps subclasses free
|
|
// of register-switch boilerplate while preserving the existing handshake.
|
|
this.registerChild = (child, softwareType) => {
|
|
this.router.dispatchRegister(child, softwareType);
|
|
return true;
|
|
};
|
|
|
|
if (typeof this.configure === 'function') this.configure();
|
|
if (typeof this._init === 'function') this._init();
|
|
}
|
|
|
|
/**
|
|
* Install a read-only getter that flattens `this.child[softwareType]`
|
|
* (across all categories, or filtered by `category`) into a single
|
|
* id-keyed object. Lets subclasses expose readable accessors like
|
|
* `this.machines` while the registry remains the source of truth.
|
|
*/
|
|
declareChildGetter(name, softwareType, category) {
|
|
const key = String(softwareType || '').toLowerCase();
|
|
Object.defineProperty(this, name, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: () => {
|
|
const slice = this.child?.[key];
|
|
if (!slice) return {};
|
|
const cats = category ? [slice[category] || []] : Object.values(slice);
|
|
const out = {};
|
|
for (const list of cats) {
|
|
if (!Array.isArray(list)) continue;
|
|
for (const c of list) {
|
|
const id = c?.config?.general?.id || c?.config?.general?.name;
|
|
if (id != null) out[id] = c;
|
|
}
|
|
}
|
|
return out;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Frozen view passed to concern-modules so they don't reach into `this`.
|
|
* Subclasses may override to add domain-specific keys.
|
|
*/
|
|
context() {
|
|
return Object.freeze({
|
|
config: this.config,
|
|
logger: this.logger,
|
|
measurements: this.measurements,
|
|
emitter: this.emitter,
|
|
child: this.child,
|
|
unitPolicy: this.unitPolicy,
|
|
router: this.router,
|
|
});
|
|
}
|
|
|
|
/** Default output shape — subclasses extend with concern-module snapshots. */
|
|
getOutput() {
|
|
return this.measurements.getFlattenedOutput?.() || {};
|
|
}
|
|
|
|
/** Subclasses MUST override. Grey placeholder so adapters never crash. */
|
|
getStatusBadge() {
|
|
return { fill: 'grey', shape: 'ring', text: 'no status' };
|
|
}
|
|
|
|
/** Convenience for event-driven nodes — see CONTRACTS.md §3. */
|
|
notifyOutputChanged() {
|
|
this.emitter.emit('output-changed');
|
|
}
|
|
|
|
close() {
|
|
this.router?.tearDown();
|
|
this.emitter.removeAllListeners();
|
|
}
|
|
}
|
|
|
|
module.exports = BaseDomain;
|