Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 90ea0a8ca0a60dbfc2af889269df14de36b3d857
parent 7116122aec85ffbe5b13c2150c279eba8e0ecddf
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 16:51:47 -0400

common, editor: the auto-queue status poll shares one fold of the snapshots

singleFlightMemo (views/autoQueueStatus.ts): concurrent callers share the
computation in flight and a landed value is reused for 3 s
(AUTO_QUEUE_STATUS_MEMO_MS), expiring by time only. The shell memoizes the
snapshot-derived half (channel briefs + the four lanes' pending work) and reads
holds, runners, picks and the priority view fresh on every call. The e2e reset
route drops the memo with the other singletons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mcommon/views/autoQueueStatus.test.ts | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/views/autoQueueStatus.ts | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/api/test/invalidate-cache/route.ts | 8++++++++
Meditor/app/operations/status.ts | 37++++++++++++++++++++++++++++++++-----
4 files changed, 205 insertions(+), 5 deletions(-)

diff --git a/common/views/autoQueueStatus.test.ts b/common/views/autoQueueStatus.test.ts @@ -8,7 +8,9 @@ import type { AutoRunnerStatus, LeafPending } from "../controller/autoRunner"; import { buildAutoQueueLanes } from "./autoQueueLanes"; import type { PriorityView } from "./channelPriority"; import { + AUTO_QUEUE_STATUS_MEMO_MS, buildAutoQueueStatusPayload, + singleFlightMemo, type AutoQueueStatusInputs, } from "./autoQueueStatus"; @@ -203,3 +205,82 @@ test("the pending fold is per lane and reaches the focus banner", () => { // A lane with nothing pending is still an entry, with the same shape. assert.deepEqual(payload.download.pendingByLeaf, {}); }); + +// THE POLL'S MEMO (slice D0, release 17): N pollers of the 3 s poll cost one +// fold of the snapshots, concurrent callers share the one in flight, and time +// is the only thing that expires it. + +function deferred<T>() { + let resolve!: (v: T) => void; + let reject!: (e: unknown) => void; + const promise = new Promise<T>((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +test("memo: concurrent callers share the computation in flight", async () => { + const memo = singleFlightMemo<number>({ now: () => 0 }); + const d = deferred<number>(); + let calls = 0; + const compute = () => { + calls++; + return d.promise; + }; + const all = Promise.all([memo.get(compute), memo.get(compute), memo.get(compute)]); + d.resolve(42); + assert.deepEqual(await all, [42, 42, 42]); + assert.equal(calls, 1); +}); + +test("memo: a landed value is reused for the window, then recomputed", async () => { + let t = 1_000; + const memo = singleFlightMemo<number>({ now: () => t }); + let calls = 0; + const compute = async () => ++calls; + assert.equal(await memo.get(compute), 1); + t += AUTO_QUEUE_STATUS_MEMO_MS - 1; + assert.equal(await memo.get(compute), 1, "inside the window: the memo"); + t += 1; + assert.equal(await memo.get(compute), 2, "at the window's end: computed again"); + assert.equal(calls, 2); +}); + +test("memo: the window runs from when the value LANDED, not when it started", async () => { + let t = 0; + const memo = singleFlightMemo<number>({ ttlMs: 3_000, now: () => t }); + const d = deferred<number>(); + const first = memo.get(() => d.promise); + t = 10_000; // a slow fold: ten seconds + d.resolve(7); + assert.equal(await first, 7); + t = 12_000; + assert.equal(await memo.get(async () => 8), 7); +}); + +test("memo: a rejection is shared by its waiters and never memoized", async () => { + const memo = singleFlightMemo<number>({ now: () => 0 }); + const d = deferred<number>(); + const a = memo.get(() => d.promise); + const b = memo.get(async () => 99); + d.reject(new Error("drive not answering")); + await assert.rejects(a, /drive not answering/); + await assert.rejects(b, /drive not answering/); + assert.equal(await memo.get(async () => 5), 5); +}); + +test("memo: clear() drops the value and detaches the computation in flight", async () => { + const memo = singleFlightMemo<string>({ now: () => 0 }); + assert.equal(await memo.get(async () => "old fixture"), "old fixture"); + memo.clear(); + const slow = deferred<string>(); + const before = memo.get(() => slow.promise); + memo.clear(); + // A caller after the clear does not join the detached computation… + assert.equal(await memo.get(async () => "new fixture"), "new fixture"); + // …and when it lands, it neither answers the memo nor evicts the new value. + slow.resolve("stale"); + assert.equal(await before, "stale"); + assert.equal(await memo.get(async () => "unused"), "new fixture"); +}); diff --git a/common/views/autoQueueStatus.ts b/common/views/autoQueueStatus.ts @@ -208,3 +208,87 @@ export function buildAutoQueueStatusPayload( lanes: inputs.lanes, }; } + +// THE POLL'S EXPENSIVE HALF, SHARED: single-flight plus a short memo. +// +// The payload above is cheap; what it is built FROM is not. Each lane's +// pending work is a fold over every channel's snapshot (the shell's +// `computeLeafPending`, four lanes), and every surface that shows the lanes +// polls it every 3 s (`useOperationsStatus`). Each poll used to compute its +// own: on 2026-10-01, with the main thread busy regenerating a report, one +// computation took 96 s, and every tab's poll started another behind it — a +// queue that only grew. Now concurrent callers share the computation in flight, +// and a result is reused for AUTO_QUEUE_STATUS_MEMO_MS after it lands, so N +// pollers cost one fold per window. +// +// TIME IS THE ONLY INVALIDATION. Nothing tells the memo that a snapshot moved; +// the window is short enough that the next poll is the correction. What the +// operator toggles — a lane's hold, the runner, its picks — is NOT behind it: +// the shell reads those fresh on every call and memoizes only the +// snapshot-derived half (editor/app/operations/status.ts). +export const AUTO_QUEUE_STATUS_MEMO_MS = 3_000; + +export type SingleFlightMemo<T> = { + // The memoized value while it is fresh; else the computation in flight; else + // `compute()`, started now and shared with every caller until it settles. A + // rejection is not memoized: the next caller computes again. + get(compute: () => Promise<T>): Promise<T>; + // Forget the value AND detach the computation in flight, whose result is + // then dropped rather than stored (a test reset must not be answered with + // the previous fixture's numbers). + clear(): void; +}; + +export function singleFlightMemo<T>( + opts: { ttlMs?: number; now?: () => number } = {}, +): SingleFlightMemo<T> { + const ttlMs = opts.ttlMs ?? AUTO_QUEUE_STATUS_MEMO_MS; + const now = opts.now ?? Date.now; + let value: { v: T; at: number } | null = null; + let inFlight: Promise<T> | null = null; + // Bumped by clear(): a computation started under an older epoch neither + // stores its value nor clears a newer computation's slot. + let epoch = 0; + return { + get(compute) { + if (value && now() - value.at < ttlMs) return Promise.resolve(value.v); + if (inFlight) return inFlight; + const mine = epoch; + const p = compute().then( + (v) => { + if (epoch === mine) { + value = { v, at: now() }; + inFlight = null; + } + return v; + }, + (err: unknown) => { + if (epoch === mine) inFlight = null; + throw err; + }, + ); + inFlight = p; + return p; + }, + clear() { + epoch++; + value = null; + inFlight = null; + }, + }; +} + +// The editor's one instance, on globalThis like the registry: the polled route +// and the operations pages are separate bundles, and the e2e reset route +// (editor/app/api/test/invalidate-cache) drops it through the same global. +declare global { + // eslint-disable-next-line no-var + var __yttAutoQueueStatusMemo__: SingleFlightMemo<unknown> | undefined; +} + +export function autoQueueStatusMemo<T>(): SingleFlightMemo<T> { + if (!globalThis.__yttAutoQueueStatusMemo__) { + globalThis.__yttAutoQueueStatusMemo__ = singleFlightMemo<unknown>(); + } + return globalThis.__yttAutoQueueStatusMemo__ as SingleFlightMemo<T>; +} diff --git a/editor/app/api/test/invalidate-cache/route.ts b/editor/app/api/test/invalidate-cache/route.ts @@ -125,6 +125,14 @@ function invalidate() { // a spec that rewrites a title in place inside one mtime tick would otherwise // see the previous spec's title. resetVideoTitleMemo(); + // And the auto-queue status poll's three-second memo of the snapshot-derived + // pending counts (common/views/autoQueueStatus.ts): the next spec's first + // poll must count ITS fixture, not the previous spec's. Dropped through the + // global like the singletons above, so this route imports nothing that + // reaches the runner; a computation still in flight lands in the dropped + // object. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).__yttAutoQueueStatusMemo__ = undefined; revalidatePath("/", "layout"); return NextResponse.json({ ok: true }); } diff --git a/editor/app/operations/status.ts b/editor/app/operations/status.ts @@ -14,9 +14,11 @@ 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"; @@ -29,12 +31,22 @@ import { readPriorityView } from "./channelPriorityView"; // 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. -export async function buildAutoQueueStatusPayload(): Promise<AutoQueueStatusPayload> { + +// 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. A +// window of AUTO_QUEUE_STATUS_MEMO_MS (3 s) is the one exception to +// requestCache.ts's "no cache longer than a request" rule, and it is bounded by +// time alone: a count up to 3 s old, on a payload polled every 3 s. +type SnapshotHalf = { + briefs: ChannelBrief[]; + pendingByKind: LeafPending[]; +}; + +async function computeSnapshotHalf(): Promise<SnapshotHalf> { const paths = getPaths(); - const settings = getSettings(); - const pool = getWorkerPool(); - const [priority, state, briefs] = await Promise.all([ - readPriorityView(), + const [state, briefs] = await Promise.all([ readAutoQueueState(paths), getChannelBriefs(paths), ]); @@ -47,6 +59,21 @@ export async function buildAutoQueueStatusPayload(): Promise<AutoQueueStatusPayl computeLeafPending(lane, paths, { configs: briefs, state }), ), ); + return { briefs, pendingByKind }; +} + +export async function buildAutoQueueStatusPayload(): Promise<AutoQueueStatusPayload> { + 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 — everything an operator's click changes. + const [priority, state, { briefs, pendingByKind }] = await Promise.all([ + readPriorityView(), + readAutoQueueState(paths), + autoQueueStatusMemo<SnapshotHalf>().get(computeSnapshotHalf), + ]); const byLane = <T>(values: readonly T[]): Record<AutoQueueKind, T> => Object.fromEntries(LANES.map((lane, i) => [lane, values[i]])) as Record< AutoQueueKind,