2 settings path spy
GOAL
Wrap a large game-settings tree in a "spy" so that after a consumer function has run you know EXACTLY which dotted settings paths it read (e.g. 'hud.scoreDecimals') — and which whole subtrees it never looked at.
CONCEPT
A recursive `get` trap. When the value behind a property is itself an object, record nothing yet — hand back ANOTHER spy around that object, with the path extended by one segment. Only a read of a leaf (non-object) value records the full dotted path.
HINT
- Grow the path one segment per hop: path ? path + '.' + key : String(key) - Remember that typeof null is also 'object', so rule out null first. - Symbol keys are not settings — forward them and record nothing. - Because `reads` is a Set, hitting the same leaf twice costs no extra entry.
MIRRORS
A game engine's settings tree is ~40 keys deep and the frame loop diffs ALL of it on every scheduled render. Spying on what the HUD renderer actually reads tells you which slice deserves a deep diff — everything else can be reference-compared.
Run
node 02-settings-path-spy.cjs (fails until you fix the TODO)Source
/*
* proto-dojo 05-proxy-reflect / 02-settings-path-spy
* --------------------------------------------------
* GOAL: Wrap a large game-settings tree in a "spy" so that after a
* consumer function has run you know EXACTLY which dotted
* settings paths it read (e.g. 'hud.scoreDecimals') — and which
* whole subtrees it never looked at.
*
* CONCEPT: A recursive `get` trap. When the value behind a property is
* itself an object, record nothing yet — hand back ANOTHER spy
* around that object, with the path extended by one segment.
* Only a read of a leaf (non-object) value records the full
* dotted path.
*
* HINT: - Grow the path one segment per hop: path ? path + '.' + key
* : String(key)
* - Remember that typeof null is also 'object', so rule out
* null first.
* - Symbol keys are not settings — forward them and record
* nothing.
* - Because `reads` is a Set, hitting the same leaf twice costs
* no extra entry.
*
* MIRRORS: A game engine's settings tree is ~40 keys deep and the frame
* loop diffs ALL of it on every scheduled render. Spying on
* what the HUD renderer actually reads tells you which slice
* deserves a deep diff — everything else can be
* reference-compared.
*
* Run: node 02-settings-path-spy.cjs (fails until you fix the TODO)
*/
'use strict';
const assert = require('node:assert');
const settings = {
game: { animation: false, mode: 'arena' },
hud: { scoreDecimals: 5, showTimer: true },
defaults: { player: { speed: 0, nameTags: { enabled: false } } },
audio: { volume: 0.8 },
};
// The consumer being profiled. Leave it untouched — the exercise is to
// find
// out what it reads WITHOUT opening its source.
function renderHud(cfg) {
const score = (1234.56789).toFixed(cfg.hud.scoreDecimals);
const animate = cfg.game.animation !== false;
return { score, animate };
}
/* ------------------------------------------------------------------ */
/* TODO 1: implement spy(obj, reads, path) -> Proxy */
/* get trap: */
/* - symbol key -> just forward, record nothing */
/* - value is an object -> return spy(value, reads, fullPath) */
/* - value is a leaf -> reads.add(fullPath), return value */
/* ------------------------------------------------------------------ */
function spy(obj, reads, path = '') {
return obj; // TODO 1: replace with a Proxy whose get trap records leaf paths
}
/* ------------------------- checks --------------------------------- */
const reads = new Set();
const result = renderHud(spy(settings, reads));
assert.deepStrictEqual(result, { score: '1234.56789', animate: false },
'the spy must be invisible — renderHud must produce the same output');
assert.ok(reads.size > 0,
'TODO 1: the spy recorded nothing, but renderHud read 2 leaf paths — ' +
'return a Proxy whose get trap records dotted paths');
assert.deepStrictEqual([...reads].sort(), ['game.animation', 'hud.scoreDecimals'],
'TODO 1: expected exactly [game.animation, hud.scoreDecimals]. ' +
'Record ONLY leaf reads (non-objects); for object values return a nested ' +
"spy with the extended path instead. Got: " + JSON.stringify([...reads].sort()));
assert.ok(![...reads].some((p) => p.startsWith('defaults') || p.startsWith('audio')),
'untouched subtrees (defaults, audio) must not appear — that absence ' +
'is the actionable insight');
console.log('OK 02-settings-path-spy — HUD renderer touched:', [...reads].sort());