/** * ChildRouter — declarative parent-side child registration & event routing. * * Replaces the per-node `registerChild` switch + manual * `child.measurements.emitter.on(...)` wiring repeated in pumpingStation, * rotatingMachine and machineGroupControl. * * See CONTRACTS.md §5. Built on top of `childRegistrationUtils`, which * already canonicalises softwareType (e.g. rotatingmachine → machine). * * Wildcard / partial-filter subscriptions enumerate every concrete * `..` event name the filter matches and attach a * plain `emitter.on(...)` per combination. No emit patching — multi-parent * stacks compose cleanly because each parent owns its own listeners. */ const { POSITION_VALUES } = require('../constants/positions'); const SOFTWARE_TYPE_ALIASES = { rotatingmachine: 'machine', machinegroupcontrol: 'machinegroup', }; // Canonical measurement-type set used to enumerate position-only and // match-everything filters. Sourced from MeasurementContainer.measureMap // plus the EVOLV-specific synthetic types the nodes routinely emit // (level / volumePercent / efficiency / Ncog / netFlowRate). Keep in sync // with MeasurementContainer if new types land there. const KNOWN_TYPES = Object.freeze([ 'flow', 'pressure', 'atmPressure', 'power', 'hydraulicPower', 'reactivePower', 'apparentPower', 'temperature', 'level', 'volume', 'volumePercent', 'length', 'mass', 'energy', 'reactiveEnergy', 'efficiency', 'Ncog', 'netFlowRate', ]); function canonicalType(rawType) { const t = String(rawType || '').toLowerCase(); return SOFTWARE_TYPE_ALIASES[t] || t; } function lowerPosition(p) { return String(p).toLowerCase(); } class ChildRouter { constructor(domain) { this.domain = domain; this.logger = domain?.logger || null; this._registerSubs = new Map(); // softwareType -> Array this._measurementSubs = new Map(); // softwareType -> Array<{filter, fn}> this._predictionSubs = new Map(); // softwareType -> Array<{filter, fn}> // Every plain emitter listener we attach, so tearDown can remove them. this._listeners = []; } // ── declaration API ──────────────────────────────────────────────── onRegister(softwareType, fn) { if (typeof fn !== 'function') { throw new TypeError('ChildRouter.onRegister: fn must be a function'); } const key = canonicalType(softwareType); if (!this._registerSubs.has(key)) this._registerSubs.set(key, []); this._registerSubs.get(key).push(fn); return this; } onMeasurement(softwareType, filter, fn) { return this._addEventSub(this._measurementSubs, softwareType, filter, fn, 'onMeasurement'); } onPrediction(softwareType, filter, fn) { return this._addEventSub(this._predictionSubs, softwareType, filter, fn, 'onPrediction'); } _addEventSub(table, softwareType, filter, fn, label) { if (typeof filter === 'function' && fn === undefined) { fn = filter; filter = {}; } if (typeof fn !== 'function') { throw new TypeError(`ChildRouter.${label}: fn must be a function`); } const key = canonicalType(softwareType); if (!table.has(key)) table.set(key, []); table.get(key).push({ filter: filter || {}, fn }); return this; } // ── dispatch ────────────────────────────────────────────────────── dispatchRegister(child, softwareType) { const key = canonicalType(softwareType); const regHandlers = this._registerSubs.get(key) || []; for (const fn of regHandlers) { try { fn.call(this.domain, child, key); } catch (err) { this._logHandlerError('onRegister', key, err); } } const emitter = child?.measurements?.emitter; if (!emitter || typeof emitter.on !== 'function') return; this._attachVariantListeners(child, key, emitter, 'measured', this._measurementSubs); this._attachVariantListeners(child, key, emitter, 'predicted', this._predictionSubs); } _attachVariantListeners(child, key, emitter, variant, table) { const subs = table.get(key) || []; for (const { filter, fn } of subs) { const types = filter.type ? [filter.type] : KNOWN_TYPES; const positions = filter.position ? [lowerPosition(filter.position)] : POSITION_VALUES.map(lowerPosition); const handlerLabel = variant === 'measured' ? 'onMeasurement' : 'onPrediction'; for (const type of types) { for (const pos of positions) { const eventName = `${type}.${variant}.${pos}`; const listener = (data) => this._invoke(fn, data, child, handlerLabel); emitter.on(eventName, listener); this._listeners.push({ emitter, eventName, listener }); } } } } _invoke(fn, eventData, child, handlerLabel) { try { fn.call(this.domain, eventData, child); } catch (err) { this._logHandlerError(handlerLabel, '', err); } } _logHandlerError(kind, key, err) { if (this.logger?.warn) { this.logger.warn(`ChildRouter ${kind}${key ? `[${key}]` : ''} handler threw: ${err?.message || err}`); } } // ── teardown ────────────────────────────────────────────────────── tearDown() { for (const { emitter, eventName, listener } of this._listeners) { if (typeof emitter.off === 'function') emitter.off(eventName, listener); else if (typeof emitter.removeListener === 'function') emitter.removeListener(eventName, listener); } this._listeners = []; } } module.exports = ChildRouter; module.exports.KNOWN_TYPES = KNOWN_TYPES;