plan: start 004-coalesce-stream-resolves-a92d40
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
---
|
||||
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<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
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user