--- id: 004-coalesce-stream-resolves-a92d40 title: Coalesce concurrent resolveStreams calls for the same video created: 2026-09-29 depends_on: [001-perf-timing-marks-105acc] est_files: 1 --- # 004 — Coalesce concurrent resolveStreams calls ## Objective `resolveStreams(videoId)` checks `streamCache` but has no in-flight map, so two requests for the same id that arrive before the first finishes (a warm-up + the real play, two devices, the media cache's `getInfo` + `/api/streams`) each spawn a ~6 s `yt-dlp -J`. After this plan, concurrent callers share ONE promise; a failure is not cached (the next call retries). ## Context the executor must NOT rediscover `server/server.js:457-494`: ```js async function resolveStreams(videoId) { const now = Date.now(); const cached = streamCache.get(videoId); if (cached && now < cached.expiresAt) return cached; const out = await runYtdlpResilient(['-J', '--no-warnings', `https://www.youtube.com/watch?v=${videoId}`]); const info = JSON.parse(out); ... streamCache.set(videoId, entry); return entry; } ``` `streamCache` and `STREAM_CACHE_MAX` are declared just above it. ## Steps 1. `server/server.js` — rename the existing function `resolveStreams` to `resolveStreamsUncached` (definition only; body unchanged). 2. Directly after that function add: ```js // One yt-dlp -J per video at a time: concurrent callers (warm-up + play, // two devices, the media cache's getInfo) share the in-flight promise. const inflightStreams = new Map(); // videoId -> Promise function resolveStreams(videoId) { const cached = streamCache.get(videoId); if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached); let p = inflightStreams.get(videoId); if (!p) { p = resolveStreamsUncached(videoId).finally(() => inflightStreams.delete(videoId)); inflightStreams.set(videoId, p); } return p; } ``` All existing callers keep calling `resolveStreams` (it still returns a Promise). 3. Create `server/streams-inflight.test.js`? — NO. Keep it in-file; instead verify with the script below. ## Out of scope / do NOT touch - Cache TTL logic, `runYtdlpResilient`, format filtering. ## Verification ```bash cd /home/user/ytplayer/server && grep -n "function resolveStreams\|function resolveStreamsUncached\|inflightStreams" server.js bun build server.js --target=bun --outdir=/tmp/ytp-check >/dev/null && echo SERVER_OK bun run test 2>&1 | grep -E "^ *[0-9]+ (pass|fail)" ``` Expected: both functions + map present, `SERVER_OK`, all server test files `0 fail`. ## Report format (executor: follow exactly) Output ONLY the following, no other prose: 1. `git diff` (unified) of all changes. 2. Raw output of the Verification commands. 3. `Findings:` — max 10 lines. Do not commit. Do not push. Do not touch files outside the Steps. ## Execution log - Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0. - Orchestrator re-ran Verification: both functions and `inflightStreams` present, `bun build` ok, `bun run test` all 5 server files 0 fail (media-cache 23 pass is the original baseline). - Executor Findings (verbatim): Plan executed successfully. Both renamed function and wrapper present in correct location. In-flight map correctly coalesces concurrent calls. All 58 server tests pass. Build completes with no errors. No deviations from plan requirements.