3 hand-rolled emitter
GOAL
implement subscribe/notify with the frozen-copy contract: every panel subscribed when a round starts redraws exactly once, no matter who joins or leaves mid-round.
CONCEPT
notifying over the LIVE listener Set makes the outcome depend on iteration order the moment a listener mutates that Set mid-loop. Freezing the guest list first makes every round deterministic.
HINT
one spread does it. Copy the Set into an array at the start of the round, then walk the copy. Mutations land on the Set, not on the copy you are walking.
MIRRORS
the HUD's redraw bus. A panel subscribes when it opens and keeps the unsubscribe closure it was handed; when a skill unlocks, the bus walks a frozen copy of its panel list, so a panel closed mid-round still redraws once this round, and a panel opened mid-round waits for the next unlock.
Run
node 03-hand-rolled-emitter.cjsSource
// DOJO · Module 9 / Exercise 3 — hand-rolled emitter
// GOAL: implement subscribe/notify with the frozen-copy contract:
// every panel subscribed when a round starts redraws exactly
// once, no matter who joins or leaves mid-round.
// CONCEPT: notifying over the LIVE listener Set makes the outcome
// depend on iteration order the moment a listener mutates that
// Set mid-loop. Freezing the guest list first makes every round
// deterministic.
// HINT: one spread does it. Copy the Set into an array at the start
// of the round, then walk the copy. Mutations land on the Set,
// not on the copy you are walking.
// MIRRORS: the HUD's redraw bus. A panel subscribes when it opens and
// keeps the unsubscribe closure it was handed; when a skill
// unlocks, the bus walks a frozen copy of its panel list, so a
// panel closed mid-round still redraws once this round, and a
// panel opened mid-round waits for the next unlock.
// Run: node 03-hand-rolled-emitter.cjs
'use strict';
const assert = require('node:assert');
function createSkillTreeStore() {
const listeners = new Set();
return {
// --- TODO 1 ------------------------------------------------------
// subscribe must add the panel's redraw to the Set and return an
// unsubscribe function: a function that removes THAT redraw when
// it is called. Right now it adds the redraw but hands back null,
// so a panel that closes has no way to leave the guest list.
subscribe(redraw) {
listeners.add(redraw);
return null; // replace me: return a function removing `redraw`
},
// --- TODO 2 ------------------------------------------------------
// notify must call every redraw that was subscribed when the round
// STARTED, exactly once each. This loop walks the live Set, so a
// panel removed mid-round before its turn is skipped, and a panel
// added mid-round redraws in the same round. Freeze a copy of the
// guest list first, then loop over the copy.
notify() {
for (const redraw of listeners) {
redraw();
}
},
};
}
// --- TODO 1's contract: subscribe returns a working unsubscribe ----
const store = createSkillTreeStore();
let questLogRedraws = 0;
const closeQuestLog = store.subscribe(() => {
questLogRedraws += 1;
});
assert.strictEqual(
typeof closeQuestLog,
'function',
'TODO 1 - subscribe must return an unsubscribe function',
);
store.notify();
assert.strictEqual(
questLogRedraws,
1,
'TODO 1 - a subscribed panel redraws on notify',
);
closeQuestLog();
store.notify();
assert.strictEqual(
questLogRedraws,
1,
'TODO 1 - after unsubscribe the panel must not redraw again',
);
// --- TODO 2's contract, part 1: unsubscribing mid-round ------------
// The quest log closes the minimap while a round is already in flight.
// The minimap was on the guest list when the round started, so it must
// still redraw this round, and never again after that.
{
const hud = createSkillTreeStore();
const redraws = { questLog: 0, minimap: 0, healthBar: 0 };
let closeMinimap;
hud.subscribe(() => {
redraws.questLog += 1;
closeMinimap();
});
closeMinimap = hud.subscribe(() => {
redraws.minimap += 1;
});
hud.subscribe(() => {
redraws.healthBar += 1;
});
hud.notify();
assert.deepStrictEqual(
redraws,
{ questLog: 1, minimap: 1, healthBar: 1 },
'TODO 2 - every panel on the guest list redraws exactly once',
);
hud.notify();
assert.deepStrictEqual(
redraws,
{ questLog: 2, minimap: 1, healthBar: 2 },
'TODO 2 - the mid-round unsubscribe counts for the NEXT round',
);
}
// --- TODO 2's contract, part 2: subscribing mid-round --------------
// The first panel opens a newcomer panel while the round is in flight.
// The newcomer was not on the frozen guest list, so it must sit out
// this round and redraw for the first time on the next one.
{
const hud = createSkillTreeStore();
let newcomerRedraws = 0;
let opened = false;
hud.subscribe(() => {
if (!opened) {
opened = true;
hud.subscribe(() => {
newcomerRedraws += 1;
});
}
});
hud.notify();
assert.strictEqual(
newcomerRedraws,
0,
'TODO 2 - a panel subscribed mid-round does not redraw this round',
);
hud.notify();
assert.strictEqual(
newcomerRedraws,
1,
'TODO 2 - it redraws on the next round',
);
}
console.log('PASS');