State Management — Build the Library Yourself
A Lane That Stops the Race
Goal
You will stop the asynchronous races that corrupt state in a search box, a like button, and a list refresh, by hand. You build four functions in one file.
Why it matters
In the synchronous world, "whatever you called last is applied last" comes for free. In async it doesn't. A request sent earlier can arrive later, and if the place that modifies state then has no notion of order, a new value is silently overwritten by an old one. Because responses are too fast on the developer's machine to reproduce it, this kind of bug is seen only by users.
The same goes for rollbacks. If you roll back an optimistic update with an inverse operation, other updates that came in during that time are lost along with it. A rollback is not a subtraction but removing the update from the pending list and recomputing. This lab makes you build that difference by hand.
What to build
In /root/work/race/lane.mjs, export the following.
| export | Contract |
|---|---|
createLane() |
run(task) — applies only the latest request. A new run cuts the signal of the previous run |
createDedupe() |
run(key, task) — an in-flight run with the same key is handed the same promise |
createOptimistic(base) |
begin · rollback · settle · getState |
createCache() |
set(key, value, now) · get(key, now, staleMs) |
Steps
- Latest-wins lane
- Cancellation signal
- Per-key deduplication
- Optimistic update and rollback
- Replace the base value with the server response
- Staleness check
- Measure it yourself and write it down —
/root/work/race/07-measure.txt - Wrap-up —
/root/work/race/08-notes.md
Notes
- npm install doesn't work. There is no network and you don't need it.
AbortControllerandqueueMicrotaskare built into node 22. - The grader imports your
lane.mjsand checks only the contract. It uses no screen and no framework. - In step 7, the grader re-measures on the spot and compares against the numbers you wrote. Made-up numbers will not pass.
Latest-wins lane
In /root/work/race/lane.mjs, export createLane(). The run(task) of the returned object waits for the result of task(), and then returns {applied: false} if a newer run started in the meantime, or {applied: true, value} otherwise.
Run mkdir -p /root/work/race. Keep one increasing counter in the lane, have each run remember its own number when it starts, and compare it with the current number after the await. Because the extension is .mjs, you don't need a package.json.
Cancellation signal
Make run(task) pass an AbortSignal to task(signal). When a new run starts, the signal handed to the previous run must become aborted === true, and the signal of the most recent run must not be cut.
Create a new AbortController() for each run and have the lane hold the last one. Discarding the result (step 1) and stopping the work are different things — with numbers alone, the network and the server keep working.
Per-key deduplication
Also export createDedupe(). If a task with the same key is in flight, run(key, task) must not call task again and must return the same Promise. If you call it again after it finishes, it starts fresh.
Keep the in-flight promises in a Map by key, and delete them when they finish (whether they succeed or fail). finally is the place for that. If you leave a failed promise in, every later call gets the same failure forever.
A rollback is not a subtraction
Export createOptimistic(base). begin(patch) adds one pending update and returns a ticket, and rollback(표) (the placeholder is the ticket) must remove only that update and compute the state by replaying the remaining updates on top of the base value. getState() returns that result.
Don't roll back with an inverse operation. If you keep the base value and the pending list separately and compute the state every time it is asked for, with pending.reduce((s, p) => p(s), base), it is correct even for ordered updates. When an update that changes the label to A and an update that changes it to B are both pending, rolling back the earlier one must leave the label as B.
The server response replaces the base value
Add settle(표, 서버상태) (the placeholders are the ticket and the server state) to createOptimistic. It must remove that update from the pending list, replace the base value with the state the server gave, and then replay the pending updates that still remain on top of it, in order.
settle differs from rollback in only one way — it also changes the base value. Other optimistic updates that have not yet been answered must not disappear at this point. The grader checks the case where a server response arrives for only one of two pending updates.
Staleness and discarding are different scales
Export createCache(). Insert with set(key, value, now), and get(key, now, staleMs) returns {value, stale}. If at least staleMs has passed since insertion, stale must be true, and for a missing key it returns undefined.
Don't store only the value — you must store the time of insertion with it to be able to ask about staleness. The boundary is "at least" — the moment exactly staleMs has passed also counts as stale. Don't throw the value away because it is stale. The point is to keep showing what was being viewed while fetching a fresh one in the background.
Measure it yourself and write it down
Run the scenarios below yourself and write the resulting numbers in /root/work/race/07-measure.txt as three lines of 이름=값 (the placeholders are the name and the value). dedupe_calls — how many times the task actually ran when you called run 5 times concurrently with the same key. latest_applied · latest_discarded — how many times applied was true and how many times it was false when you called run 5 times in a row on one lane.
Write a short .mjs, run it, and copy down the values it prints. The grader re-runs the same scenario against your lane.mjs and compares it with the numbers you wrote, so values estimated by eye will not pass. First check that the three lines add up.
What kept the order
In /root/work/race/08-notes.md, write at least three lines: what remains on screen without latest-wins, why cancellation is separate from number comparison, and what gets lost along with it if you write the rollback as a subtraction.
The text must contain 최신, 취소, and 되돌리기 (the Korean words for "latest", "cancel", and "rollback"). These three are problems that TanStack Query, SWR, and RTK Query solve in the same way even though they look different from one another.