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>
165 lines
5.7 KiB
JavaScript
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;
|