import { createHash } from "node:crypto"; import { statSync } from "node:fs"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { getScheduler } from "yt-dlp-transcript-common/jobs/scheduler"; import { getWorkerPool } from "yt-dlp-transcript-common/jobs/workerPool"; import type { LiveInputs, ObserveInputs, } from "yt-dlp-transcript-common/views/inputs"; // THE ONE CONSTRUCTOR. // // Every view-model lives in `common/views/` and takes its live state as an // argument (`views/inputs.ts` is the shape). Something has to actually fetch // that state, and this is the only file in the editor allowed to fetch all of // it at once. Concentrating it here is the point: the getters below are LAZY // SINGLETONS — calling one CREATES it — so "who may construct?" is a question // worth being able to answer by reading one file. // // Per-view extras (a disk gate, a log tail, a channel-brief list) are assembled // in each noun's own shell next to the page that needs them, never here. export function liveInputs(): LiveInputs { return { paths: getPaths(), settings: getSettings(), registry: getRegistry(), scheduler: getScheduler(), pool: getWorkerPool(), // A function, not a timestamp: a view that folds "is this cooldown over?" // should read the clock when it looks, and a test should be able to freeze // it. `Date.now` is banned inside views/ so this is the only way in. now: Date.now, }; } // THE OBSERVER'S HALF, AND WHY IT IS DIFFERENT. // // `/api/pulse` runs every few seconds on every open tab and must never CREATE // anything: `/api/test/invalidate-cache` clears exactly these globals between // e2e specs, and a poll landing a moment later used to rebuild them — re-seeding // a worker pool from settings mid-reset, which took out ~16 unrelated specs in // non-deterministic combinations. So read the globals directly and treat "not // created yet" as "nothing to report": a pulse must be able to say the system // is idle without making a system to ask. // // (The globals are declared by the modules that own them — // `common/jobs/{registry,scheduler,workerPool,snapshotScheduler}.ts`.) // // Everything gathered here is either in-memory or a stat(). No readdir, no // corpus contact, no large JSON parse — asserted by e2e/pulse.spec.ts. export function observeInputs(): ObserveInputs { const paths = getPaths(); return { registry: globalThis.__yttJobRegistry__ ?? null, scheduler: globalThis.__yttScheduler__ ?? null, pool: globalThis.__yttWorkerPool__ ?? null, // One integer meaning "a report was rewritten", so the pages whose counts // come from snapshots notice without this endpoint stat-ing 65 files. The // scheduler's state type is not exported, so read the number off it here // rather than passing the object down. snapshotGeneration: globalThis.__yttSnapshotScheduler__?.generation ?? 0, // Files the layout renders from. mtime only — neither is read. settingsMtime: mtime(paths.settingsFile), changelogMtime: mtime(paths.editorChangelogFile), // Injected so the view never imports node:crypto. digest: (s: string) => createHash("sha1").update(s).digest("base64url"), }; } function mtime(file: string): number { try { return statSync(file).mtimeMs; } catch { return 0; } }