1 get set reflect
GOAL
Wrap a game-settings object in an "audited" shell that writes down each property read and each property write, yet stays indistinguishable from the bare object — even for getters that consult `this`.
CONCEPT
The `get` and `set` traps of a Proxy. When a trap wants to fall back to normal behaviour, reach for Reflect.get(target, key, receiver) and Reflect.set(target, key, value, receiver) rather than plain `target[key]`. Handing `receiver` through is the whole difference: a getter defined on the target then runs with `this` === the proxy, so every `this.x` it performs flows back through your traps instead of sneaking around them.
HINT
Trap parameters line up one-to-one with the Reflect method they mirror — get(target, key, receiver), set(target, key, value, receiver). Record a {type, key} entry, then delegate to Reflect.* and hand back its result. Watch out: under 'use strict' (see below) a `set` trap that yields a falsy value turns the assignment into a thrown TypeError; Reflect.set already produces the right boolean, so pass it straight back.
MIRRORS
"Which plugin keeps flipping settings.audio.volume after the level loads?" Any engine, UI toolkit or state store that hands a settings object to dozens of modules can wrap it in dev mode and get a full read/write trail without touching a single call site.
Run
node 01-get-set-reflect.cjs (fails until you fix the TODO)Source
/*
* proto-dojo 05-proxy-reflect / 01-get-set-reflect
* ------------------------------------------
* GOAL: Wrap a game-settings object in an "audited" shell that writes
* down each property read and each property write, yet stays
* indistinguishable from the bare object — even for getters that
* consult `this`.
*
* CONCEPT: The `get` and `set` traps of a Proxy. When a trap wants to
* fall back to normal behaviour, reach for Reflect.get(target,
* key, receiver) and Reflect.set(target, key, value, receiver)
* rather than plain `target[key]`. Handing `receiver` through
* is the whole difference: a getter defined on the target then
* runs with `this` === the proxy, so every `this.x` it
* performs flows back through your traps instead of sneaking
* around them.
*
* HINT: Trap parameters line up one-to-one with the Reflect method they
* mirror — get(target, key, receiver), set(target, key, value,
* receiver). Record a {type, key} entry, then delegate to
* Reflect.* and hand back its result. Watch out: under 'use
* strict' (see below) a `set` trap that yields a falsy value
* turns the assignment into a thrown TypeError; Reflect.set
* already produces the right boolean, so pass it straight back.
*
* MIRRORS: "Which plugin keeps flipping settings.audio.volume after the
* level loads?" Any engine, UI toolkit or state store that
* hands a settings object to dozens of modules can wrap it in
* dev mode and get a full read/write trail without touching a
* single call site.
*
* Run: node 01-get-set-reflect.cjs (fails until you fix the TODO)
*/
'use strict';
const assert = require('node:assert');
const gameSettings = {
hero: 'knight',
volume: 5,
// A getter that reaches for a DIFFERENT property via `this`.
// Whether that inner read shows up in the audit hinges on
// `receiver`.
get loudness() {
return this.volume;
},
};
/* ------------------------------------------------------------------ */
/* TODO 1: return a Proxy around `target` whose get/set traps */
/* a) push { type: 'get'|'set', key } onto `log` (string keys */
/* only — ignore symbols to keep node's inspect happy), and */
/* b) forward to the target via Reflect.get / Reflect.set, */
/* passing `receiver` through. */
/* ------------------------------------------------------------------ */
function audited(target, log) {
// TODO 1: replace this passthrough with `new Proxy(target, { ... })`
return target;
}
/* ------------------------- checks --------------------------------- */
const log = [];
const settings = audited(gameSettings, log);
assert.strictEqual(settings.hero, 'knight',
'reads must still return the real values');
assert.ok(log.some((e) => e.type === 'get' && e.key === 'hero'),
"TODO 1: nothing was logged for reading settings.hero — audited() must return " +
'a real Proxy with a `get` trap, not the raw target');
settings.volume = 3; // strict mode: if your set trap returns falsy, this line throws
assert.strictEqual(gameSettings.volume, 3,
'writes through the proxy must land on the underlying target');
assert.ok(log.some((e) => e.type === 'set' && e.key === 'volume'),
'TODO 1: the write to settings.volume was not logged — add a `set` trap');
// The receiver koan: reading `loudness` fires a getter on the TARGET, and
// that getter reads `this.volume`. Forward with Reflect.get(target, key,
// receiver) and `this` inside the getter is the PROXY — the inner read
// is
// audited as well. Forward with `target[key]` and it quietly sneaks past.
log.length = 0;
assert.strictEqual(settings.loudness, 3, 'the getter must still work');
assert.ok(log.some((e) => e.type === 'get' && e.key === 'volume'),
"TODO 1 (receiver): reading settings.loudness did NOT log the getter's inner " +
"read of `this.volume` — forward with Reflect.get(target, key, receiver) " +
'so the getter re-enters the proxy');
console.log('OK 01-get-set-reflect — audit log:', JSON.stringify(log));