fix(call): rAF batch flush never emptied into state

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) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-04-22 22:26:45 +02:00
parent 6c6a23e672
commit e2e8217b86
@@ -79,16 +79,18 @@ export function ScreenSourcePicker({ open, onClose, onStart }: Props) {
// one setState — cuts re-renders from O(N) to O(frames) during the // 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 // initial fan-in and prevents consecutive 10-30 ms long tasks from
// stacking in one frame. // stacking in one frame.
const pendingUrls: Record<string, string> = {}; //
// 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<string, string> = {};
let rafScheduled = false; let rafScheduled = false;
const flush = () => { const flush = () => {
rafScheduled = false; rafScheduled = false;
if (Object.keys(pendingUrls).length === 0) return;
const batch = pendingUrls; const batch = pendingUrls;
// Capture then reset so new arrivals during the commit land in a if (Object.keys(batch).length === 0) return;
// fresh batch instead of double-applying. pendingUrls = {};
const keys = Object.keys(batch);
for (const k of keys) delete (pendingUrls as Record<string, string>)[k];
startTransition(() => { startTransition(() => {
setThumbnailUrls((prev) => ({ ...prev, ...batch })); setThumbnailUrls((prev) => ({ ...prev, ...batch }));
}); });