An effect subscribes to a live feed for the current room id; after two room switches, messages from old rooms still arrive. Why?
answer
- several live connections, not one
- cleanup pairs with the run that opened
- runs before the next setup, not only at teardown
- close the handle captured in that run
- drop arrivals whose id does not match
basics
~20 sThe effect opens a connection per run but never closes the one the previous run opened, so connections accumulate. Cleanup must run before each re-run, closing exactly the connection that run created - not only at teardown.
solid answer
~50 sOld rooms still deliver because their connections are still open: the effect re-ran when the room id changed and opened a new one without closing the old. The contract is per run - each run registers a cleanup that closes what *that* run opened, and the runtime calls it before the next run as well as at teardown, so exactly one connection is live at any moment. Two near-misses produce the same complaint: a cleanup that closes a shared or reassigned handle rather than the one captured in that run, and a cleanup that detaches the handler while leaving the transport open. A third variant looks identical to users but has a single stuck connection instead of several - that is the room id missing from the dependency set, so the effect never re-ran at all.
go deeper
Recognise the pattern: something opened once per run and closed never. Remember that an effect that subscribes must also unsubscribe, and that the unsubscribe happens between runs as well as at the end.
Walk the sequence out loud - open A, cleanup A, open B - and show where the missing step lets two feeds live at once. Distinguish it from a missing dependency, which leaves one stuck feed.
Instrument before guessing: count live connections, check whether the cleanup closes the handle its own run created, and add an identity check on arriving values so late data cannot corrupt state.
Notice when the per-run rule is being asked to fix a structural problem. If several views each open the same feed, give the connection one owner rather than adding a cleanup in every consumer.
## What the symptom is telling you Messages from a room the user has left are still arriving. Two facts follow immediately: the connection for the old room is **still open**, and the effect for the new room opened **another** one. Values for two or three rooms now reach the same component, and whichever arrives last wins whatever state the handler writes. This is the canonical missing-cleanup signature: the amount of wrong work grows with the number of times the user changed the input, not with the size of the data. ## The rule the code broke An effect that opens something is expected to register a cleanup that closes **exactly what that run opened**, and the runtime calls it in two places: before the effect's next run, and at teardown. The intended sequence across two room switches is: 1. Run for room A opens connection A. 2. The room id changes. Cleanup for run A closes connection A. 3. Run for room B opens connection B. 4. The room id changes again. Cleanup for run B closes connection B, then run C opens connection C. At every moment exactly one connection is live. Remove step 2's cleanup and connections accumulate; the effect is still correct about *opening*, and wrong about *owning*. ## The variants worth ruling out | What you find | Why old messages still arrive | Fix | |---|---|---| | No cleanup at all | Nothing ever closes a connection; one per run stays live | Register a cleanup that closes the connection that run created | | Cleanup closes a shared or latest handle | It closes the newest connection, or a variable that has been reassigned, not the one this run opened | Close the handle captured by *that* run, held in the run's own scope | | Cleanup only detaches the handler, leaves the transport open | The connection keeps receiving and buffering; reattaching reveals a backlog | Close the transport as well as the listener | | The room id is not part of the dependency set | The effect never re-ran, so one stale connection serves every room | Make the id a real dependency, declared or read inside the tracked run | | Two effects, or two components, each open one | Cleanup is fine per effect; the duplication is structural | One owner for the connection, not one per view that wants it | The last two rows matter because "add a cleanup" is not always the fix. A stale-dependency bug produces the same user-visible complaint with a *single* connection stuck on the first room. ## Making teardown provable rather than hopeful - **Capture the handle in the run.** The cleanup should refer to a value created in the same run, so it cannot possibly close somebody else's connection. - **Make close idempotent and tolerant.** It may be called when the connection never finished opening. Closing a half-open connection must be a no-op, not an error. - **Ignore late arrivals by identity.** Even after a correct close, a value already in flight can land. Compare the value's room id against the one the run was created for and drop the mismatch; the check costs nothing and it also protects against the transport delivering after close. - **Count the live connections in development.** A counter incremented on open and decremented on close should never exceed one for a single-room view. This turns an invisible defect into an assertion. - **Lean on the extra pass.** Some runtimes deliberately run setup and cleanup twice during development for exactly this class of bug; an effect that survives that pass with one live connection is almost certainly balanced. ## The shape a correct version has One effect, whose input is the room id, whose body opens one connection for that id, and whose cleanup closes that connection. Nothing about rooms lives outside it: no module-level variable holding "the current connection", no close call in an unrelated handler. The reason to keep it that tight is that the runtime's guarantee is per-run - it will call the cleanup it was handed - and any state kept outside the run breaks the pairing the guarantee relies on. If several components genuinely need the same feed, the answer is not a cleanup in each of them; it is one owner of the connection that the components read from, with a single place responsible for closing it. That keeps the per-run rule intact where it applies and stops it being asked to solve a structural duplication it cannot see.
- Users report the same thing, but instrumentation shows exactly one connection, stuck on the first room. What is the cause now?The room id is not in the effect's dependency set, so the effect never re-ran. One connection was opened for the first room and cleanup was never due. The cure is the opposite of adding a cleanup: make the id a genuine dependency - declared, or read inside the tracked part of the run - so switching rooms re-runs the effect and its cleanup.
- Why guard against a late-arriving message even when the cleanup closes the connection correctly?Because a value can already be in flight when close is called, and some transports deliver buffered data during shutdown. Comparing the arriving value's room id with the id the run was created for drops such strays cheaply, and the same check protects you when a future refactor makes teardown slightly less immediate.
- How would you make this class of bug fail during development rather than in production?Instrument the pairing: increment a counter on open, decrement on close, and assert it never exceeds the number of feeds the view should hold. Some runtimes also run setup and cleanup an extra time in development specifically to expose an unbalanced pair on the first render, which turns a silent accumulation into an immediate signal.
saying these in an interview costs you the question
- Adds cleanup only for teardown and expects re-runs to be handled too
- Closes a module-level handle that later runs have already reassigned
- Detaches the message handler and leaves the connection open
- Blames the server for delivering messages to a room the user left
- Opens the feed in a handler and never re-opens it after a re-run