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