A callback can call back into its owner before the outer operation finishes. This is reentrancy; it does not require threads. Listener dispatch is a useful small example: a listener can subscribe another listener or unsubscribe itself while emit is iterating.
Choose an iteration policy. Iterating a snapshot means newly added listeners wait until the next emission. With the simple snapshot policy below, a listener removed during an emission can still run if it was already in that snapshot. A stricter active-membership check is a different contract; neither should be accidental.
For this exercise subscriptions have token identity. Two subscriptions using the same callback are independent. Cleanup is idempotent: calling its returned function twice is harmless. Callback exceptions deliberately stop this synchronous dispatch; a real API should document whether it propagates, isolates or aggregates failures.
This standalone lab prepares you to inspect callback side effects in the pool. It is not an event-emitter feature secretly included in the capstone.
Predict, run, and explain
Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.
const listeners = new Set();
listeners.add(() => {
console.log("A");
listeners.add(() => console.log("late"));
});
listeners.add(() => console.log("B"));
for (const listener of [...listeners]) {
listener();
}
console.log(listeners.size);Check your prediction
A
B
3 Define what happens during an ongoing dispatch
A callback can change the collection being dispatched or invoke the owner again before its current call returns. This is reentrancy, and it can happen without threads. A snapshot gives one dispatch a stable set of registrations, but does not prohibit nested dispatch.
Subscription identity must be chosen independently of callback identity. If registering the same function twice creates two subscriptions, each registration needs a separate token. Unsubscribing one must not remove the other accidentally.
const events = [];
const listeners = new Set();
let added = false;
listeners.add(() => {
events.push("first");
if (!added) {
added = true;
listeners.add(() => events.push("late"));
}
});
for (const listener of [...listeners]) {
listener();
}
console.log(events.join(","));
for (const listener of [...listeners]) {
listener();
}
console.log(events.join(","));Check your prediction
first
first,first,late Late is absent from the first captured array and present in the second. Iterating the live Set would follow a different policy and can even keep growing during dispatch.
const callback = () => {};
const functions = new Set([callback, callback]);
const tokens = new Set([{ callback }, { callback }]);
console.log(functions.size);
console.log(tokens.size);Check your prediction
1
2 The same function identity collapses in a Set; separate token objects do not. Neither collection is wrong in isolation, but only one fits independent duplicate subscriptions.
Try the rule on new inputs
Test addition, removal and a guarded nested emit from inside a callback. Write the expected call order before running. Never introduce unbounded recursive emission as a casual timing experiment.
Your turn
Callbacks & Reentrancy: practise
Write subscribe and emit using a Set of subscription records, not a Set of functions. subscribe returns idempotent cleanup. Subscribe the same function twice, remove one subscription and emit: it should print tick once.
// Each subscribe call should allocate its own token record.Hint 1: the idea
Callback identity and subscription identity are different.
Hint 2: a first step
Store { callback } in the Set and close over that record in the cleanup function.
Show one solution
const subscriptions = new Set();
function subscribe(callback) {
const token = { callback };
subscriptions.add(token);
return () => {
subscriptions.delete(token);
};
}
function emit() {
for (const token of [...subscriptions]) {
token.callback();
}
}
const callback = () => console.log("tick");
const remove = subscribe(callback);
subscribe(callback);
remove();
remove();
emit();tickExplain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?
Change it and test it
Test a callback subscribing another callback during emit and explain why the new subscription waits. Then remove a later subscription during dispatch and verify the documented snapshot policy. Write a different contract only if you also update its tests.
Completion is saved in this browser only. It does not measure mastery.