/** * 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/.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;