Create a channel for one payload type. Subscribe accepts a callback and returns cleanup; publish calls the current subscribers with a payload. Use a Set of subscription records so the same function can be registered twice independently.
Before calling listeners, copy the Set into an array. This is a snapshot policy: additions during dispatch wait until the next publish, while removals already captured can still run now. A callback calling back into its owner is reentrancy; it needs a deliberate rule even in a single thread.
This checkpoint uses void callbacks as a familiar starting point. Day 5 shows the specific async-return problem with that contract and replaces it for our synchronous library. Do not assume this small draft already rejects async listeners.
Work for twenty minutes. First prove duplicate subscriptions and idempotent cleanup. Then mutate the set from a callback and trace the documented snapshot behavior.
Predict, check, and run
Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.
const records = new Set<{ label: string }>();
records.add({ label: "same" });
records.add({ label: "same" });
console.log(records.size);Enable JavaScript to edit and type-check this example.
Check your prediction
2 Observe the snapshot policy
Spreading a Set into an array captures its current records. A callback can add a new listener while the loop is running, but the copied array does not gain that new record. Predict the first and second publish separately.
const listeners = new Set<() => void>();
const events: string[] = [];
let added = false;
listeners.add(() => {
events.push("first");
if (!added) {
added = true;
listeners.add(() => {
events.push("late");
});
}
});
function publish(): void {
for (const listener of [...listeners]) {
listener();
}
}
publish();
console.log(events.join(","));
events.length = 0;
publish();
console.log(events.join(","));Enable JavaScript to edit and type-check this example.
Check your prediction
first
first,late This demonstration stores distinct callbacks directly. Your exercise additionally needs independent records so registering the same callback twice still creates two subscriptions.
Acceptance cases before you code
Write a small test plan before implementing channel identity and snapshot. These cases extend the brief; they are not a second worked solution. Predict the expected behavior independently, then test through the public function. A good ordinary example and a good boundary example should catch different possible mistakes.
| Case to try | What your result should demonstrate |
|---|---|
| Same listener registered twice | Cleanup removes one registration independently |
| Cleanup called twice | Remains harmless |
| Listener adds a subscription while publishing | New subscription waits for the next snapshot |
After the core task passes, deliberately break one rule and identify the case that notices it. Restore the implementation and explain the failure in terms of the contract, not the number of assertions. If a case requires a concept you have not recalled, revisit the relevant lesson before opening the solution.
Build it yourself
Checkpoint: Typed Channels: practise
Implement createChannel<T> with subscribe and publish. Register the same numeric callback twice, remove one registration twice, then publish 25. Print 25 exactly once.
// Start from the stated contract. Add a valid call and an edge case.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
Each subscribe call needs a new record identity, even if its listener function is shared.
Hint 2: a first step
Close over that record in the returned cleanup function; snapshot records before calling listeners.
Show one solution
function createChannel<T>() {
const records = new Set<{ listener: (value: T) => void }>();
return {
subscribe(listener: (value: T) => void): () => void {
const record = { listener };
records.add(record);
return () => {
records.delete(record);
};
},
publish(value: T): void {
for (const record of [...records]) {
record.listener(value);
}
}
};
}
const channel = createChannel<number>();
const listener = (value: number) => {
console.log(value);
};
const remove = channel.subscribe(listener);
channel.subscribe(listener);
remove();
remove();
channel.publish(25);Enable JavaScript to edit and type-check this example.
25Trace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.
Change it and test it
Add a listener during publish and prove it waits until the next publish. Remove a later listener during publish and prove the already captured record still runs. Compare identity tokens with storing functions directly.
Completion is saved in this browser only. It does not measure mastery.