Files
generalFunctions/src/domain/BaseDomain.js
znetsixe 57b77f905a Phase 1 wave 2: BaseDomain + commandRegistry + statusUpdater
- 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>
2026-05-10 18:31:50 +02:00

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;