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>
This commit is contained in:
@@ -195,3 +195,74 @@ test('chainable API returns the router instance', () => {
|
||||
.onPrediction('machine', { type: 'flow', position: 'downstream' }, () => {});
|
||||
assert.equal(r, router);
|
||||
});
|
||||
|
||||
test('multi-parent: two routers on the same child both receive every event and tear down independently', () => {
|
||||
// Regression for the pre-2026-05-11 emit-patching stack: two parents
|
||||
// subscribing partial-filter wildcards on the same child must compose
|
||||
// without stacking wrappers, and either teardown order must work.
|
||||
const routerA = new ChildRouter(makeDomain());
|
||||
const routerB = new ChildRouter(makeDomain());
|
||||
const a = []; const b = [];
|
||||
|
||||
routerA.onMeasurement('measurement', { type: 'pressure' },
|
||||
(data) => a.push(data.value));
|
||||
routerB.onMeasurement('measurement', { type: 'pressure' },
|
||||
(data) => b.push(data.value));
|
||||
|
||||
const ch = makeChild();
|
||||
routerA.dispatchRegister(ch, 'measurement');
|
||||
routerB.dispatchRegister(ch, 'measurement');
|
||||
|
||||
emitMeasured(ch, 'pressure', 'upstream', 11);
|
||||
emitMeasured(ch, 'pressure', 'downstream', 22);
|
||||
assert.deepEqual(a.sort(), [11, 22]);
|
||||
assert.deepEqual(b.sort(), [11, 22]);
|
||||
|
||||
// Tear down B first — A must continue to fire on subsequent events.
|
||||
routerB.tearDown();
|
||||
emitMeasured(ch, 'pressure', 'upstream', 33);
|
||||
assert.deepEqual(a.sort(), [11, 22, 33]);
|
||||
assert.deepEqual(b.sort(), [11, 22], 'B receives nothing after its teardown');
|
||||
|
||||
// Now tear down A in the reverse order; neither should fire.
|
||||
routerA.tearDown();
|
||||
emitMeasured(ch, 'pressure', 'upstream', 44);
|
||||
assert.deepEqual(a.sort(), [11, 22, 33], 'A receives nothing after its teardown');
|
||||
assert.deepEqual(b.sort(), [11, 22]);
|
||||
});
|
||||
|
||||
test('position-only filter fans out across every known type for that position', () => {
|
||||
const router = new ChildRouter(makeDomain());
|
||||
const hits = [];
|
||||
|
||||
router.onMeasurement('measurement', { position: 'upstream' },
|
||||
(data) => hits.push(data.value));
|
||||
|
||||
const ch = makeChild();
|
||||
router.dispatchRegister(ch, 'measurement');
|
||||
|
||||
emitMeasured(ch, 'pressure', 'upstream', 1);
|
||||
emitMeasured(ch, 'flow', 'upstream', 2);
|
||||
emitMeasured(ch, 'temperature', 'upstream', 3);
|
||||
emitMeasured(ch, 'pressure', 'downstream', 99); // wrong position
|
||||
emitPredicted(ch, 'pressure', 'upstream', 99); // wrong variant
|
||||
|
||||
assert.deepEqual(hits.sort(), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('empty filter ({}) fires for every type/position combination', () => {
|
||||
const router = new ChildRouter(makeDomain());
|
||||
const hits = [];
|
||||
|
||||
router.onMeasurement('measurement', {}, (data) => hits.push(data.value));
|
||||
|
||||
const ch = makeChild();
|
||||
router.dispatchRegister(ch, 'measurement');
|
||||
|
||||
emitMeasured(ch, 'pressure', 'upstream', 1);
|
||||
emitMeasured(ch, 'flow', 'downstream', 2);
|
||||
emitMeasured(ch, 'level', 'atequipment', 3);
|
||||
emitPredicted(ch, 'flow', 'upstream', 99); // wrong variant
|
||||
|
||||
assert.deepEqual(hits.sort(), [1, 2, 3]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user