// THE FEED METADATA BACKFILL AS A JOB. The work is // controller/feedMetadataBackfill.ts, which the CLI runs offline; this is what // the editor's action and `/api/ops/feed-metadata` enqueue. Its own module so // the CLI never loads the job registry. import { getPaths, type Paths } from "../lib/paths"; import { downloadQueueKey } from "../lib/queueKeys"; import { runManagedFunction, type StreamActionResult, } from "../jobs/streamCommand"; import { readChannelConfig } from "./channels"; import { backfillFeedMetadata, feedBackfillSummary, } from "./feedMetadataBackfill"; export const FEED_METADATA_JOB_KIND = "feed-metadata"; // One job per run, on the channel's DOWNLOAD queue: the fetch contends with // that queue's own requests to the same host, and the writes must not land // beside a download rewriting the same metadata.info.json. `needsText` (the // kind's entry in jobs/jobKinds.ts) refuses it for a channel whose text cannot // be read. Not replayable: a re-run is the same click. export async function runFeedMetadataJob(opts: { paths?: Paths; slug: string; feedUrl?: string; dryRun?: boolean; requestedBy?: string; afterRun?: () => void | Promise; }): Promise { const paths = opts.paths ?? getPaths(); const config = await readChannelConfig(paths, opts.slug); if (!config) return { ok: false, error: `Channel "${opts.slug}" not found` }; return runManagedFunction({ kind: FEED_METADATA_JOB_KIND, queueKey: downloadQueueKey(config), paths, channelSlug: opts.slug, fn: async (onLog, signal) => { onLog( `${opts.dryRun ? "Previewing" : "Backfilling"} feed metadata for ${opts.slug}.`, ); const result = await backfillFeedMetadata({ paths, slug: opts.slug, ...(opts.feedUrl ? { feedUrl: opts.feedUrl } : {}), ...(opts.dryRun ? { dryRun: true } : {}), ...(opts.requestedBy ? { requestedBy: opts.requestedBy } : {}), onLog, signal, }); onLog(feedBackfillSummary(result)); if (result.failed.length > 0) { throw new Error(`${result.failed.length} record(s) could not be written`); } await opts.afterRun?.(); }, }); }