js-dojo

3 guarded settings

GOAL

Build a guard around a running game's settings that 1) throws on UNKNOWN keys — both reading a typo ('contols') and writing one ('difficulty') — instead of quietly handing back undefined, 2) keeps '_'-prefixed internals out of sight of the `in` operator, and 3) refuses to delete load-bearing keys ('players', 'controls').

CONCEPT

Four traps cooperating: get, set, has, deleteProperty. has(target, key) is what the `in` operator consults. deleteProperty(target, key) is what `delete` consults — and under strict mode, a trap that returns false makes the `delete` statement itself THROW a TypeError. That throw is your protection.

HINT

- Validation rule for get/set: throw a TypeError when the key is a string, does not start with '_', and is not in KNOWN_SETTINGS. (Let symbols and '_' keys through — node's inspect probes symbols.) - has: answer false for string keys that begin with '_'; otherwise defer to Reflect.has. - deleteProperty: for a PROTECTED key just return false and let strict mode raise the TypeError; otherwise defer to Reflect.deleteProperty.

MIRRORS

Game engines and UI libraries tend to IGNORE settings keys they do not recognise — a misspelled 'invincible' flag costs you an afternoon of "why is the hero still dying?". A dev-mode guard like this one, wrapped around your applySettings payload, turns that silent typo into a loud TypeError at the exact line that made it.

Run

node 03-guarded-settings.cjs   (fails until you fix the TODO)

Source

/*
 * proto-dojo 05-proxy-reflect / 03-guarded-settings
 * -------------------------------------------
 * GOAL: Build a guard around a running game's settings that 1) throws
 *       on UNKNOWN keys — both reading a typo ('contols') and writing
 *       one ('difficulty') — instead of quietly handing back undefined,
 *       2) keeps '_'-prefixed internals out of sight of the `in`
 *       operator, and 3) refuses to delete load-bearing keys
 *       ('players', 'controls').
 *
 * CONCEPT: Four traps cooperating: get, set, has, deleteProperty.
 *          has(target, key) is what the `in` operator consults.
 *          deleteProperty(target, key) is what `delete` consults — and
 *          under strict mode, a trap that returns false makes the
 *          `delete` statement itself THROW a TypeError. That throw is
 *          your protection.
 *
 * HINT:   - Validation rule for get/set: throw a TypeError when the
 *           key is a string, does not start with '_', and is not in
 *           KNOWN_SETTINGS. (Let symbols and '_' keys through — node's
 *           inspect probes symbols.)
 *         - has: answer false for string keys that begin with '_';
 *           otherwise defer to Reflect.has.
 *         - deleteProperty: for a PROTECTED key just return false and
 *           let strict mode raise the TypeError; otherwise defer to
 *           Reflect.deleteProperty.
 *
 * MIRRORS: Game engines and UI libraries tend to IGNORE settings keys
 *          they do not recognise — a misspelled 'invincible' flag costs
 *          you an afternoon of "why is the hero still dying?". A
 *          dev-mode guard like this one, wrapped around your
 *          applySettings payload, turns that silent typo into a loud
 *          TypeError at the exact line that made it.
 *
 * Run: node 03-guarded-settings.cjs   (fails until you fix the TODO)
 */
'use strict';
const assert = require('node:assert');

const KNOWN_SETTINGS = new Set([
    'game', 'players', 'audio', 'controls', 'hud', 'defaults', 'title',
]);
const PROTECTED = new Set(['players', 'controls']);

const liveSettings = {
    players: [{ id: 'knight', hits: [] }],
    controls: { keys: { jump: 'Space' } },
    hud: { scale: 1, showScore: true },
    _dirty: false, // internal bookkeeping — consumers should not see it
};

/* ------------------------------------------------------------------ */
/* TODO 1: implement guard(target) -> Proxy with get/set/has/           */
/*         deleteProperty traps as described in the HINT above.         */
/* ------------------------------------------------------------------ */
function guard(target) {
    // TODO 1: replace this passthrough with `new Proxy(target, { ... })`
    return target;
}

/* ------------------------- checks --------------------------------- */
const settings = guard(liveSettings);

assert.strictEqual(settings.hud.showScore, true,
    'known keys must read through normally');

assert.throws(() => settings.contols, TypeError,
    "TODO 1 (get): reading the typo key 'contols' returned undefined instead " +
    'of throwing — add a validating get trap');

assert.throws(() => { settings.difficulty = 'hard'; }, TypeError,
    "TODO 1 (set): writing the unknown key 'difficulty' succeeded silently — " +
    'add a validating set trap');

settings.title = { text: 'knight vs rogue' }; // known key: must pass validation
assert.deepStrictEqual(liveSettings.title, { text: 'knight vs rogue' },
    'valid writes must land on the underlying target');

assert.strictEqual('_dirty' in settings, false,
    "TODO 1 (has): '_dirty' must be invisible to the `in` operator — add a " +
    'has trap that hides _-prefixed keys');
assert.strictEqual('_dirty' in liveSettings, true,
    'the raw target still owns _dirty — only the proxy hides it');
assert.strictEqual('players' in settings, true,
    'has trap must still report normal keys via Reflect.has');

assert.throws(() => { delete settings.players; }, TypeError,
    "TODO 1 (deleteProperty): deleting the protected key 'players' must throw " +
    '— return false from the trap and let strict mode do the throwing');
assert.ok(Array.isArray(liveSettings.players),
    'players must survive the blocked delete');

delete settings.hud; // unprotected: must be allowed
assert.strictEqual('hud' in liveSettings, false,
    'deleting an unprotected key must fall through via Reflect.deleteProperty');

console.log('OK 03-guarded-settings — typos throw, internals hidden, players protected');

Solution