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