import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { computeLeafPending, getAutoRunnerStatus, type AutoRunnerStatus, type LeafPending, } from "yt-dlp-transcript-common/controller/autoRunner"; import { readAutoQueueState, type AutoQueueKind, } from "yt-dlp-transcript-common/jobs/autoQueueState"; import { LANES } from "yt-dlp-transcript-common/lib/autoQueueTypes"; import { getWorkerPool } from "yt-dlp-transcript-common/jobs/workerPool"; import { buildAutoQueueLanes } from "yt-dlp-transcript-common/views/autoQueueLanes"; import { autoQueueStatusMemo, buildAutoQueueStatusPayload as build, type AutoQueueStatusPayload, } from "yt-dlp-transcript-common/views/autoQueueStatus"; import type { ChannelBrief } from "yt-dlp-transcript-common/controller/channels"; import { getChannelBriefs } from "../lib/requestCache"; import { readPriorityView } from "./channelPriorityView"; // THE SHELL. The payload is `common/views/autoQueueStatus.ts` (and the rail's // bands are `views/autoQueueLanes.ts`); everything below is the reading. // // ONE RESOLUTION FOR ALL FOUR LANES, in three places rather than one. The focus // set costs a channel listing and, for a site focus, a sites read; the // auto-queue state document is one JSON parse — it was read once PER LANE, four // times per poll, because `buildKind` did its own reading; and the channel // briefs are shared with the lanes builder through the per-request cache, and // with the four computeLeafPending calls through their `shared` argument. // THE SNAPSHOT-DERIVED HALF, behind the shared single-flight memo // (`autoQueueStatusMemo`, common/views/autoQueueStatus.ts): the channel briefs // and the four lanes' pending work. It is the expensive half, a fold over every // channel's snapshot, and the one every poller used to pay for separately. The // memo is KEYED by the settings the fold reads (each lane's policy and tree in // `autoQueue`, and `channelPriority`), so an operator's edit to either misses it // and is never paired with counts from the tree before; otherwise it holds for // AUTO_QUEUE_STATUS_MEMO_MS (3 s) — a snapshot rewritten or a video the runner // just picked shows at most one poll late. That window is the one exception to // requestCache.ts's "no cache longer than a request" rule, bounded by time. type SnapshotHalf = { briefs: ChannelBrief[]; pendingByKind: LeafPending[]; }; async function computeSnapshotHalf(): Promise { const paths = getPaths(); const [state, briefs] = await Promise.all([ readAutoQueueState(paths), getChannelBriefs(paths), ]); // …and the four lanes' pending work re-reads NEITHER. `briefs` is the channel // listing and `state` the auto-queue document, both already in hand one line // up; without them each of the four calls listed every channel's config off // disk again and re-parsed the state, on a ~3 s poll. const pendingByKind = await Promise.all( LANES.map((lane) => computeLeafPending(lane, paths, { configs: briefs, state }), ), ); return { briefs, pendingByKind }; } export async function buildAutoQueueStatusPayload(): Promise { const paths = getPaths(); const settings = getSettings(); const pool = getWorkerPool(); // FRESH on every call: the priority view, the state document (picks, // cooldowns, deferrals), the settings (holds, policies), the pool and the // runners. The pending counts are memoized under a key of the settings they // are folded from, so an edit to a policy, a tree or a priority recomputes // them at once; only what changes without a settings write (a snapshot, the // runner's in-flight set) can be up to 3 s behind. const memoKey = JSON.stringify([settings.autoQueue, settings.channelPriority]); const [priority, state, { briefs, pendingByKind }] = await Promise.all([ readPriorityView(), readAutoQueueState(paths), autoQueueStatusMemo().get(memoKey, computeSnapshotHalf), ]); const byLane = (values: readonly T[]): Record => Object.fromEntries(LANES.map((lane, i) => [lane, values[i]])) as Record< AutoQueueKind, T >; return build({ settings, pool, now: Date.now, priority, // Constructs the runner if it is not already there, which is exactly why // the view cannot ask for it itself. runner: byLane(LANES.map((lane) => getAutoRunnerStatus(lane))), state, pending: byLane(pendingByKind), lanes: buildAutoQueueLanes({ settings, briefs, workerSummary: pool.summary(), }), }); }