js-dojo

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));

Solution