Technical Deep Dives5 min read

One Bug, Seven Queues: How a Copy-Pasted State Machine Made Us Refresh the Page

"Cancel a job, and you can't add another without refreshing." The same bug lived in seven separate queue stores because each had its own copy of one flawed line. The fix, and why deriving state beats tracking it.

A user reported: process one image, then try to add another — nothing happens until you refresh the page. Cancel a job, same thing. We'd seen versions of this before and patched it in one place. It came back, because the bug existed in seven places.

The duplicated flaw

We have seven queue stores — image, batch, video, video-upscale, gif, pdf, upscale. Each was written independently but converged on the same shape, including the same cancel logic:

isProcessing: isActive ? false : state.isProcessing

Read that carefully. When you cancel an item, it only clears the isProcessing flag if the cancelled item was currently active. But there's a race: if an item finishes its work a microtask before the cancel lands, it's no longer "active" — so isActive is false, and isProcessing is left at its previous value: stuck on true. The UI gates "can I add another?" on isProcessing, so the queue refuses new items until a full page reload resets everything.

Because the line was copy-pasted into seven stores, fixing it in one never fixed the others. That's the real lesson: duplicated logic means duplicated bugs, and a fix that doesn't reach all copies isn't a fix.

Don't track state you can derive

The deeper problem is that isProcessing was being tracked — toggled by hand at every transition — when it should be derived. A queue is processing if and only if some item is in an active state. That's not a fact to maintain; it's a fact to compute:

function deriveProcessing(items) {
  return items.some(i => isActiveStatus(i.status))
}

There is no code path that can leave a derived value stuck, because it's recomputed from the source of truth every time. We extracted one canonical queue machine — the queued → active → (done | error | cancelled) lifecycle plus this deriveProcessing helper — and had all seven stores consume it. The stuck-flag class of bug is now structurally impossible across every queue tool, not just the one we happened to test.

The general principle

State you toggle by hand drifts. State you derive can't. Any time you find yourself writing "set this flag to false here, and also here, and also in the cancel handler," that's a signal the flag should be a computed function of something more fundamental — and that the something should live in one place every consumer shares.

The symptom was "I have to refresh to add another file." The cause was one clever-looking ternary, copied six times. The fix was to stop tracking and start deriving — once.