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>
This commit is contained in:
139
src/domain/BaseDomain.js
Normal file
139
src/domain/BaseDomain.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user