From e2e8217b867a26cee89eb8eccd9407323e330960 Mon Sep 17 00:00:00 2001 From: byGalax Date: Wed, 22 Apr 2026 22:26:45 +0200 Subject: [PATCH] fix(call): rAF batch flush never emptied into state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real reason every thumbnail card stayed on the placeholder was a reference-aliasing bug in the flush closure. `const batch = pendingUrls` captured the same object; `delete (pendingUrls)[k]` for each key then emptied `batch` too, because they were the same reference. By the time `setThumbnailUrls(prev => ({ ...prev, ...batch }))` ran, batch was {} and the state never picked up any URL — every card rendered the empty placeholder icon. Fixed by aliasing first, then replacing pendingUrls with a fresh empty object (let instead of const on the outer binding). The cloned `batch` retains its entries for the spread; any new arrivals during the commit land in the new empty pendingUrls and coalesce into the next frame. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/src/components/ScreenSourcePicker.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/ScreenSourcePicker.tsx b/apps/desktop/src/components/ScreenSourcePicker.tsx index 6089d1c..567e155 100644 --- a/apps/desktop/src/components/ScreenSourcePicker.tsx +++ b/apps/desktop/src/components/ScreenSourcePicker.tsx @@ -79,16 +79,18 @@ export function ScreenSourcePicker({ open, onClose, onStart }: Props) { // one setState — cuts re-renders from O(N) to O(frames) during the // initial fan-in and prevents consecutive 10-30 ms long tasks from // stacking in one frame. - const pendingUrls: Record = {}; + // + // Previously `batch` was aliased to `pendingUrls` and then we cleared + // pendingUrls via `delete` — which emptied batch too (same reference) + // and every flush ended up spreading nothing into the state. Clone + // first, then clear, so the batch keeps its entries. + let pendingUrls: Record = {}; let rafScheduled = false; const flush = () => { rafScheduled = false; - if (Object.keys(pendingUrls).length === 0) return; const batch = pendingUrls; - // Capture then reset so new arrivals during the commit land in a - // fresh batch instead of double-applying. - const keys = Object.keys(batch); - for (const k of keys) delete (pendingUrls as Record)[k]; + if (Object.keys(batch).length === 0) return; + pendingUrls = {}; startTransition(() => { setThumbnailUrls((prev) => ({ ...prev, ...batch })); });