commit 72ac50ae460720f6d3851696ef81c6f92453461a
parent 664a68d30365dfc22e8a37ba2bb5ec49f5b82f4f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 20:59:33 -0400
editor: the per-channel Refresh report runs on the refresh-report queue (D0 review L3)
refreshChannelSnapshotAction (Refresh report, ops {slug}, e2e generateReport)
starts a refresh-report job through startRefreshReport and waits for it (or for
the one already queued for the channel), keeping the unmounted-drive sentence as
its error. No walk runs outside the queue.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 45 insertions(+), 14 deletions(-)
diff --git a/editor/app/api/ops/refresh-report/route.ts b/editor/app/api/ops/refresh-report/route.ts
@@ -16,8 +16,9 @@ export const dynamic = "force-dynamic";
// POST { slug: string } | { all: true }
//
-// The single-channel form REGENERATES SYNCHRONOUSLY (it is a filesystem scan,
-// not a job) and returns `{ ok: true }` once snapshot.json is on disk. The
+// The single-channel form queues a refresh-report job (the one serial queue
+// every walk runs on, release 17) and returns `{ ok: true }` once snapshot.json
+// is on disk — it waits for the job, behind any other channel's. The
// `all` form queues one refresh-report job per channel and returns the bulk
// { queued, skipped } the /channels header button shows.
export async function POST(request: Request) {
diff --git a/editor/app/channels/actions.ts b/editor/app/channels/actions.ts
@@ -23,12 +23,13 @@ import {
import { renameChannel } from "yt-dlp-transcript-common/controller/renameChannel";
import { inspectChannelMedia } from "yt-dlp-transcript-common/lib/channelMedia";
import { channelMediaBusyReason } from "./lib/mediaBusy";
-import { generateChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot";
import {
REFRESH_REPORT_ACTIVE,
+ queuedRefreshReportId,
requestChannelSnapshot,
startRefreshReport,
} from "yt-dlp-transcript-common/jobs/snapshotScheduler";
+import { getRegistry } from "yt-dlp-transcript-common/jobs/registry";
import { drainStream } from "yt-dlp-transcript-common/jobs/drainStream";
import {
siteChannelIndex,
@@ -348,6 +349,16 @@ export async function gotoVideoAction(
redirect(`/channels/${slug}/videos/${encodeURIComponent(id)}`);
}
+// Poll a registry job until it is terminal (or gone from the registry). The
+// registry hands out no completion promise for a job someone else started.
+async function waitForJobToEnd(id: string): Promise<void> {
+ for (;;) {
+ const job = getRegistry().get(id);
+ if (!job || (job.status !== "queued" && job.status !== "running")) return;
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ }
+}
+
export async function refreshChannelSnapshotAction(
slug: string,
): Promise<ActionResult> {
@@ -355,17 +366,36 @@ export async function refreshChannelSnapshotAction(
if (!(await channelExists(paths, slug))) {
return { error: `Channel "${slug}" not found` };
}
- // generateChannelSnapshot now THROWS on a channel whose media is not
- // reachable (guard 3) rather than writing a snapshot that says every video is
- // undownloaded. That is the right behaviour and the wrong exception to let
- // out of a server action: an uncaught throw here reaches the client as a
- // digest-only "an error occurred", and the one thing the operator needs is
- // the sentence naming the unmounted drive. Every sibling in this file returns
- // { error }; so does this.
- try {
- await generateChannelSnapshot(paths, slug);
- } catch (e) {
- return { error: (e as Error).message };
+ // THROUGH THE REFRESH-REPORT QUEUE, like every other walk (release 17 slice
+ // D0): a walk run here, in the request, beside a queued one was two walks
+ // side by side — half of the 2026-10-01 outage — and could land an older
+ // read over a newer one. The action still answers when the report is on
+ // disk: it waits for the job it started, or for the one already queued for
+ // this channel (which has not started reading, so it is as fresh).
+ //
+ // generateChannelSnapshot THROWS on a channel whose media is not reachable
+ // (guard 3) rather than writing a snapshot that says every video is
+ // undownloaded. The job fails with that sentence in its log; it comes back
+ // here through `onError`, because an uncaught throw from a server action
+ // reaches the client as a digest-only "an error occurred", and the one thing
+ // the operator needs is the sentence naming the unmounted drive. Every
+ // sibling in this file returns { error }; so does this.
+ let failure: string | null = null;
+ const started = await startRefreshReport(paths, slug, {
+ onError: (message) => {
+ failure = message;
+ },
+ });
+ if (started.ok) {
+ const { status } = await started.done;
+ if (status !== "done") {
+ return { error: failure ?? `Refresh report ${status} (job ${started.jobId})` };
+ }
+ } else if (!started.info) {
+ return { error: started.error };
+ } else {
+ const queued = queuedRefreshReportId(slug);
+ if (queued) await waitForJobToEnd(queued);
}
revalidatePath(`/channels/${slug}`);
// The Report column on /channels is read off this snapshot, and the row