js-dojo

7 weakmap key identity

GOAL

See WeakMap-keyed module state SPLIT IN TWO the moment somebody passes your API a Proxy of the game — then repair stateFor so the raw game and every proxy of it land on ONE shared state object.

CONCEPT

WeakMap.get(key) looks up by IDENTITY. proxy !== target, no exceptions — so state saved through the proxy is unreachable from the raw game, and the other way round (split-brain). A proxy you did not create cannot be unwrapped: nothing in the language exposes [[ProxyTarget]], and that is on purpose (security membranes rely on proxies being undetectable). PROPERTY operations, however, pass straight through a proxy — that is the entire point of one — so a Symbol-keyed stash ON the game tunnels: proxy[STATE] and game[STATE] hit the same slot via the get/set traps. 04-03's trade-offs flip here: a WeakMap is the most private stash but brittle under identity games; a Symbol property survives proxies, and its lifetime (gone when the game is gone) is precisely what per-game state needs. (Own the proxy handler? Then a third route opens: answer a known escape-hatch key with the raw target — the __v_raw trick reactivity systems use.)

HINT

const STATE = Symbol('...'); stateFor(g) { if (!g[STATE]) g[STATE] = { tags: [] }; return g[STATE]; } A symbol-keyed prop already stays out of JSON and Object.keys — 04-03 had you prove exactly that.

MIRRORS

any plugin for a UI toolkit or game engine that keeps per-instance state in a WeakMap and dispatches by `this` has this hole. Wrap the instance in a dev-tools spy (05-06's transparentSpy) and game.addNameTag(...) files the tag under the PROXY while the render event fires with the RAW game — name tags silently vanish, not one error logged. Same family as [[MapData]] and #privates: identity-bound storage meeting a stand-in object.

Run

node 07-weakmap-key-identity.cjs   (fails until you fix the TODOs)

Source

/*
 * proto-dojo 05-proxy-reflect / 07-weakmap-key-identity
 * -----------------------------------------------
 * GOAL: See WeakMap-keyed module state SPLIT IN TWO the moment somebody
 *       passes your API a Proxy of the game — then repair stateFor so
 *       the raw game and every proxy of it land on ONE shared state
 *       object.
 *
 * CONCEPT: WeakMap.get(key) looks up by IDENTITY. proxy !== target, no
 *          exceptions — so state saved through the proxy is unreachable
 *          from the raw game, and the other way round (split-brain). A
 *          proxy you did not create cannot be unwrapped: nothing in the
 *          language exposes [[ProxyTarget]], and that is on purpose
 *          (security membranes rely on proxies being undetectable).
 *          PROPERTY operations, however, pass straight through a proxy
 *          — that is the entire point of one — so a Symbol-keyed stash
 *          ON the game tunnels: proxy[STATE] and game[STATE] hit the
 *          same slot via the get/set traps. 04-03's trade-offs flip
 *          here: a WeakMap is the most private stash but brittle under
 *          identity games; a Symbol property survives proxies, and its
 *          lifetime (gone when the game is gone) is precisely what
 *          per-game state needs. (Own the proxy handler? Then a third
 *          route opens: answer a known escape-hatch key with the raw
 *          target — the __v_raw trick reactivity systems use.)
 *
 * HINT: const STATE = Symbol('...'); stateFor(g) { if (!g[STATE])
 *       g[STATE] = { tags: [] }; return g[STATE]; } A symbol-keyed prop
 *       already stays out of JSON and Object.keys — 04-03 had you prove
 *       exactly that.
 *
 * MIRRORS: any plugin for a UI toolkit or game engine that keeps
 *          per-instance state in a WeakMap and dispatches by `this` has
 *          this hole. Wrap the instance in a dev-tools spy (05-06's
 *          transparentSpy) and game.addNameTag(...) files the tag under
 *          the PROXY while the render event fires with the RAW game —
 *          name tags silently vanish, not one error logged. Same family
 *          as [[MapData]] and #privates: identity-bound storage meeting
 *          a stand-in object.
 *
 * Run: node 07-weakmap-key-identity.cjs   (fails until you fix the TODOs)
 */
'use strict';
const assert = require('node:assert');

/* ---------- the breakage, demonstrated (this part already passes) --- */
const gameState = new WeakMap(); // the name-tags-module pattern
function weakStateFor(g) {
    let s = gameState.get(g);
    if (!s) { s = { tags: [] }; gameState.set(g, s); }
    return s;
}

const game = { id: 'match-1' };                // pretend engine.Game
const wrapped = new Proxy(game, {});           // "transparent", right? (06 said no)

weakStateFor(wrapped).tags.push('knight: MVP');      // API called via the proxy
assert.strictEqual(weakStateFor(game).tags.length, 0,
    'split-brain demonstrated: the tag stored via the proxy is INVISIBLE via ' +
    'the raw game — two states now exist for one game');
assert.notStrictEqual(weakStateFor(game), weakStateFor(wrapped),
    'proxy !== target, so identity lookup minted two separate state objects');

/* ------------------------------------------------------------------ */
/* TODO 1: a proxy-proof stateFor.                                      */
/*   1a: STATE must be an actual Symbol.                                */
/*   1b: stash { tags: [] } ON the game under STATE (create on first     */
/*       access), so raw game and any proxy tunnel to the same object.  */
/* ------------------------------------------------------------------ */
const STATE = 'state'; // TODO 1a: a string key — turn it into a Symbol
function stateFor(g) {
    return weakStateFor(g); // TODO 1b: stash ON the game under STATE instead
}

/* ------------------------------------------------------------------ */
/* TODO 2: prediction — replace null with true or false.                */
/* ------------------------------------------------------------------ */
const predictions = {
    canUnwrapAForeignProxy: null, // is there ANY API to get the target out
                                  // of a proxy someone else created?
};

/* ------------------------- checks --------------------------------- */
assert.strictEqual(typeof STATE, 'symbol',
    'TODO 1a: STATE must be a real Symbol — collision-proof and invisible ' +
    'to JSON/Object.keys (04-03), and property ops forward through proxies');

const s1 = stateFor(game);
assert.ok(s1 && Array.isArray(s1.tags),
    'TODO 1b: stateFor must lazily create and return { tags: [] }');

assert.strictEqual(stateFor(wrapped), s1,
    'TODO 1b — THE POINT: stateFor(proxy) and stateFor(game) must return ' +
    'the SAME object. Identity does not survive a proxy, but property reads ' +
    'do: proxy[STATE] forwards to the target\'s slot');

stateFor(wrapped).tags.push('knight: MVP');
assert.deepStrictEqual(stateFor(game).tags, ['knight: MVP'],
    'a tag added through the proxy must be visible through the raw game — ' +
    'no more split-brain');

assert.deepStrictEqual(Object.keys(game), ['id'],
    'the stash must not pollute enumeration — if you see STATE in here you ' +
    'used a string key, not a Symbol');

assert.strictEqual(predictions.canUnwrapAForeignProxy, false,
    'TODO 2: no API exists to extract [[ProxyTarget]] from a foreign proxy — ' +
    'deliberate spec design. If you OWN the handler, you add an escape-hatch ' +
    'key that answers with the raw target (the __v_raw trick); if you don\'t, ' +
    'you make your storage property-based so it tunnels — which you just did');

console.log('OK 07-weakmap-key-identity — identity stashes split under proxies; property stashes tunnel through.');

Solution