5 partial delegation and wrappers
GOAL
A subclass override that PARTIALLY hands off to the base method comes in two flavors — live lookup (`super.updatePosition()`) and a reference pinned at definition time. Predict which flavor a monkey-patch on the BASE prototype can observe, then close the blind spot in a perf probe.
CONCEPT
`super.updatePosition()` walks the prototype chain AT CALL TIME — wrap Player.prototype.updatePosition afterwards and every super-call passes through your wrapper. But `const base = Player.prototype.updatePosition` grabbed at module load freezes the ORIGINAL function; a wrapper installed on the prototype later is invisible to it. Whoever instruments a base method has to know which hand-off style each subclass uses — or wrap at the subclass level instead.
HINT
The probe never sees WarriorSnapshot because its override calls the pinned original. There is no way to un-pin it from outside — so wrap the prototype that actually owns the snapshot override. And do NOT wrap WarriorLive as well: its super-call already funnels through the wrapped base (double count!).
MIRRORS
Game engines and UI toolkits let every entity type override a shared lifecycle method (update, render, layout), and most overrides open with `super.update()` — live, so the engine's own profiler wrapper on the base prototype counts subclass calls too. Plugins that snapshot (`const baseX = proto.x` at import time) quietly slip past every wrapper installed after them — the classic "why does the profiler report 0ms for my warriors?" bug.
Run
node 05-partial-delegation-and-wrappers.cjsSource
// ---------------------------------------------------------------------
// 03-classes-desugared / 05-partial-delegation-and-wrappers.cjs
//
// GOAL: A subclass override that PARTIALLY hands off to the base method
// comes in two flavors — live lookup (`super.updatePosition()`)
// and a reference pinned at definition time. Predict which flavor
// a monkey-patch on the BASE prototype can observe, then close
// the blind spot in a perf probe.
//
// CONCEPT: `super.updatePosition()` walks the prototype chain AT CALL
// TIME — wrap Player.prototype.updatePosition afterwards and
// every super-call passes through your wrapper. But `const
// base = Player.prototype.updatePosition` grabbed at module
// load freezes the ORIGINAL function; a wrapper installed on
// the prototype later is invisible to it. Whoever instruments
// a base method has to know which hand-off style each subclass
// uses — or wrap at the subclass level instead.
//
// HINT: The probe never sees WarriorSnapshot because its override calls
// the pinned original. There is no way to un-pin it from outside
// — so wrap the prototype that actually owns the snapshot
// override. And do NOT wrap WarriorLive as well: its super-call
// already funnels through the wrapped base (double count!).
//
// MIRRORS: Game engines and UI toolkits let every entity type override
// a shared lifecycle method (update, render, layout), and most
// overrides open with `super.update()` — live, so the engine's
// own profiler wrapper on the base prototype counts subclass
// calls too. Plugins that snapshot (`const baseX = proto.x` at
// import time) quietly slip past every wrapper installed after
// them — the classic "why does the profiler report 0ms for my
// warriors?" bug.
// Run: node 05-partial-delegation-and-wrappers.cjs
// ---------------------------------------------------------------------
'use strict';
const assert = require('node:assert');
// -- Setup (do not edit) ----------------------------------------------
class Player {
constructor(name, hits) {
this.name = name;
this.hits = hits; // [{ at (ms), damage }, ...]
this.positions = null;
}
updatePosition() { // base layout: hits → knockback positions on screen
this.positions = this.hits.map((h, i) => ({ x: i, y: 100 - h.damage }));
}
}
// Flavor 1 — LIVE hand-off: super resolves through the chain at call
// time.
class WarriorLive extends Player {
updatePosition() {
super.updatePosition();
this.positions.forEach((p) => { p.hitbox = { x: p.x * 10, height: 100 - p.y }; });
}
}
// Flavor 2 — SNAPSHOT hand-off: base method pinned when this module
// loaded.
const capturedBaseUpdatePosition = Player.prototype.updatePosition;
class WarriorSnapshot extends Player {
updatePosition() {
capturedBaseUpdatePosition.call(this);
this.positions.forEach((p) => { p.hitbox = { x: p.x * 10, height: 100 - p.y }; });
}
}
// A minimal perf probe (do not edit) — counts and logs every call it
// can see.
// ("probe hits" below = wrapper invocations, not damage taken.)
let probeHits = 0;
const probeLog = [];
function installProbe(proto, methodName) {
const original = proto[methodName];
proto[methodName] = function (...args) {
probeHits += 1;
probeLog.push(`${this.name}:${methodName}`);
return original.apply(this, args);
};
}
// The engine's profiler wraps the BASE — "one wrap covers every player
// type", right?
installProbe(Player.prototype, 'updatePosition');
// Three players update their position once each:
const plain = new Player('knight', [{ at: 0, damage: 40 }, { at: 16, damage: 60 }]);
const before1 = probeHits; plain.updatePosition();
const hitsForBase = probeHits - before1;
const live = new WarriorLive('rogue', [{ at: 0, damage: 30 }, { at: 16, damage: 70 }]);
const before2 = probeHits; live.updatePosition();
const hitsForLiveWarrior = probeHits - before2;
const snap = new WarriorSnapshot('mage', [{ at: 0, damage: 20 }, { at: 16, damage: 80 }]);
const before3 = probeHits; snap.updatePosition();
const hitsForSnapshotWarrior = probeHits - before3;
// -- TODO 1: predictions ----------------------------------------------
// How many times did the probe fire for EACH single updatePosition() call
// above? Replace each null with a number (0, 1, 2, ...).
const predictions = {
hitsForBase: null,
hitsForLiveWarrior: null,
hitsForSnapshotWarrior: null,
};
// -- TODO 2: close the blind spot -------------------------------------
// Make the probe see WarriorSnapshot position updates as well — WITHOUT
// touching the snapshot class and WITHOUT making WarriorLive double-count.
// One line.
function fixProbeCoverage() {
// your one line here
}
// -- Checks (do not edit) ---------------------------------------------
assert.strictEqual(predictions.hitsForBase, hitsForBase,
'TODO 1 hitsForBase: plain Player.updatePosition() looks the method up on ' +
'Player.prototype — which now holds the wrapper');
assert.strictEqual(predictions.hitsForLiveWarrior, hitsForLiveWarrior,
'TODO 1 hitsForLiveWarrior: super.updatePosition() resolves through the ' +
'prototype chain AT CALL TIME — does it find the original or the wrapper?');
assert.strictEqual(predictions.hitsForSnapshotWarrior, hitsForSnapshotWarrior,
'TODO 1 hitsForSnapshotWarrior: capturedBaseUpdatePosition was pinned BEFORE ' +
'the probe was installed — can a later prototype swap reach a const?');
assert.deepStrictEqual(snap.positions.map((p) => p.hitbox.height), [20, 80],
'sanity: the snapshot warrior still positioned itself CORRECTLY — that is ' +
'the trap: behavior is fine, observability is not');
fixProbeCoverage();
const before4 = probeHits; snap.updatePosition();
assert.strictEqual(probeHits - before4, 1,
'TODO 2: after your fix, one WarriorSnapshot.updatePosition() must register ' +
'exactly 1 probe hit — wrap the prototype that OWNS the snapshot override');
assert.ok(probeLog.includes('mage:updatePosition'),
'TODO 2: the probe log should now show the snapshot warrior by name');
const before5 = probeHits; live.updatePosition();
assert.strictEqual(probeHits - before5, 1,
'TODO 2: WarriorLive must still count exactly ONCE — if you see 2, you ' +
'wrapped WarriorLive.prototype too, and its super-call already flows ' +
'through the wrapped base');
console.log('OK — 05-partial-delegation-and-wrappers: you now know where wraps can and cannot see.');