Files
generalFunctions/src/domain/ChildRouter.js
znetsixe f11754635b B3.1 + B3.2 + B3.3: ChildRouter fan-out + commandRegistry 'none' + UnitPolicy dual-shape
B3.1 ChildRouter per-listener fan-out (drop emit monkey-patch):
  Partial-filter subscriptions enumerate every concrete
  <type>.measured.<position> event name (cartesian product over
  the canonical POSITIONS list + 19 KNOWN_TYPES) and register a
  plain emitter.on() per combo. Multi-parent semantics are trivial:
  each ChildRouter's listeners are independent. Drop the wrap/unwrap
  bookkeeping in tearDown. ChildRouter.js 184→164 lines.

B3.2 commandRegistry 'none' + description:
  Add 'none' to payloadSchema.type — handler still fires; logs warn
  if msg.payload is non-empty (catches accidental passes). Add
  optional `description` field per descriptor; surfaced via .list()
  so wikiGen can render per-topic effect text.
  commandRegistry.js 157→164 lines. 23/23 tests pass.

B3.3 UnitPolicy dual-shape:
  policy.canonical/output/curve are now BOTH callable methods AND
  frozen property bags. policy.canonical('flow') === 'm3/s' and
  policy.canonical.flow === 'm3/s' both work. Property bags are
  frozen (assign/delete/redefine throw in strict). Drops the
  _unitView workaround in MGC + rotatingMachine specificClass.
  UnitPolicy.js 149→163 lines, 15/15 tests pass.

CONTRACTS.md §4 + §6 updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:13:15 +02:00

165 lines
5.7 KiB
JavaScript

/**
* 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
* `<type>.<variant>.<position>` 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<fn>
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;