Files
ytplayer/plans/queue/004-coalesce-stream-resolves-a92d40.md

2.8 KiB

id, title, created, depends_on, est_files
id title created depends_on est_files
004-coalesce-stream-resolves-a92d40 Coalesce concurrent resolveStreams calls for the same video 2026-09-29
001-perf-timing-marks-105acc
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:

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:
    // 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<entry>
    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

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.