commit feef1dd454f68e27d7702271bc2d19fd8779394a
parent abcfb4233f72ce9f921e78ebd16618afef19f738
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 26 Jun 2026 14:53:16 -0400
Phase 1: consolidate per-kind job metadata into one table
Before this, per-kind knowledge was scattered across four sites: the
DRAINABLE_KINDS set (registry.ts), the JOB_KIND_LABELS map (editor
jobKindLabels.ts), the runJobSpec switch, and the spec-presence bookmark
check. This adds common/jobs/jobKinds.ts as the single source of truth for
a kind's label, drainability, bookmarkability, queue-key strategy, and
(future) scheduler tier.
- registry.ts: drop DRAINABLE_KINDS; re-export isDrainableKind from jobKinds
(import path for existing callers unchanged).
- editor jobKindLabels.ts: re-export jobKindLabel from the shared table.
- jobKinds.test.ts: pins the pre-refactor drainable set + label map verbatim,
proving the move is byte-for-byte behavior-preserving (label-less replayable
kinds still fall back to the raw kind).
Data-only, zero behavior change. Typecheck (common+editor) clean; jobKinds +
registry unit tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 319 insertions(+), 48 deletions(-)
diff --git a/common/jobs/jobKinds.test.ts b/common/jobs/jobKinds.test.ts
@@ -0,0 +1,77 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { isDrainableKind, jobKindLabel, getJobKind } from "./jobKinds";
+
+// Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test jobs/jobKinds.test.ts
+//
+// Phase 1 of the queue refactor moved the drainable-kinds set and the label map
+// into one metadata table (jobKinds.ts). These snapshots pin the PRE-refactor
+// values verbatim, so the consolidation is provably behavior-preserving: every
+// kind that drained before still drains, every label is byte-identical, and
+// kinds that previously had no label still fall back to their raw kind.
+
+// Verbatim copy of the old DRAINABLE_KINDS set from registry.ts.
+const OLD_DRAINABLE = [
+ "whisper-all",
+ "whisper-bucket-downloaded-no-transcript",
+ "download-from-playlist",
+ "download-missing",
+ "download-missing-subs",
+ "sync",
+ "retry-bucket",
+ "auto-transcribe",
+ "auto-download",
+];
+
+// Verbatim copy of the old JOB_KIND_LABELS map from editor jobKindLabels.ts.
+const OLD_LABELS: Record<string, string> = {
+ "auto-transcribe": "Auto-transcribe",
+ "auto-download": "Auto-download runner",
+ "auto-download-unit": "Auto-download",
+ "whisper-all": "Transcribe all",
+ "whisper-bucket-downloaded-no-transcript": "Transcribe downloaded audio",
+ "download-from-playlist": "Download from playlist",
+ "download-missing": "Download missing",
+ "download-missing-subs": "Download missing subs",
+ "import-one": "Import video",
+ "redownload-archive": "Archive source video",
+ "retry-bucket": "Retry",
+ "clean-audio-transcribed": "Clean audio",
+ "check-kept-deleted": "Check kept videos",
+ "persist-kept": "Persist kept videos",
+ "backup-saved-videos": "Back up saved videos",
+ "verify-saved-video-backup": "Verify saved-video backup",
+ sync: "Sync",
+};
+
+test("drainable kinds match the old DRAINABLE_KINDS set exactly", () => {
+ const drainSet = new Set(OLD_DRAINABLE);
+ for (const kind of OLD_DRAINABLE) {
+ assert.equal(isDrainableKind(kind), true, `${kind} should drain`);
+ }
+ // No kind in the table drains unless it was in the old set.
+ for (const kind of Object.keys(OLD_LABELS)) {
+ assert.equal(
+ isDrainableKind(kind),
+ drainSet.has(kind),
+ `${kind} drainability changed`,
+ );
+ }
+ // Unknown kinds are not drainable.
+ assert.equal(isDrainableKind("no-such-kind"), false);
+});
+
+test("labels match the old JOB_KIND_LABELS map exactly", () => {
+ for (const [kind, label] of Object.entries(OLD_LABELS)) {
+ assert.equal(jobKindLabel(kind), label, `label for ${kind}`);
+ }
+});
+
+test("label-less and unknown kinds fall back to the raw kind", () => {
+ // Replayable kinds that never had a label entry must still fall back.
+ assert.equal(jobKindLabel("store-playlist"), "store-playlist");
+ assert.equal(jobKindLabel("transcode-failures"), "transcode-failures");
+ // Genuinely unknown kind.
+ assert.equal(jobKindLabel("totally-unknown"), "totally-unknown");
+ assert.equal(getJobKind("totally-unknown"), undefined);
+});
diff --git a/common/jobs/jobKinds.ts b/common/jobs/jobKinds.ts
@@ -0,0 +1,232 @@
+// The single source of truth for what a job `kind` IS. Before this table, the
+// same per-kind knowledge was scattered across four places: the DRAINABLE_KINDS
+// set (registry.ts), the JOB_KIND_LABELS map (editor jobKindLabels.ts), the
+// switch in editor runJobSpec.ts, and the spec-presence bookmark check. Phase 1
+// of the queue refactor consolidates the drainable + label data here; later
+// phases consume `defaultTier` (scheduler) and lean on `bookmarkable`.
+//
+// Adding a new job kind should mean adding ONE entry here (plus its replay
+// handler in editor/app/jobs/jobReplayRegistry.ts if it is bookmarkable).
+
+// How a kind picks its registry queueKey. Descriptive only — the real key is
+// still computed by the action that creates the job (channelQueueKey /
+// downloadQueueKey / ""). "parallel" means queueKey === "" (runs unserialized).
+export type QueueKeyStrategy = "parallel" | "platform" | "custom";
+
+// Scheduler priority tiers. A higher-priority tier's queued jobs run before a
+// lower one's; FIFO within a tier. Consumed starting Phase 4 (the scheduler);
+// today every non-background job is effectively "foreground".
+export type SchedulerTier = "urgent" | "foreground" | "background";
+
+export type JobKindMeta = {
+ kind: string;
+ // Human-readable label for the UI. Optional: a kind without one falls back to
+ // its raw machine kind (preserving the prior JOB_KIND_LABELS fallback), so a
+ // new kind is never invisible.
+ label?: string;
+ // The Drain (soft-cancel) button is only offered for running jobs of a
+ // drainable kind — i.e. one whose controller honors the drain signal (stops
+ // starting new sub-operations, lets in-flight ones finish).
+ drainable: boolean;
+ // Whether this kind's action attaches a replayable JobSpec (and thus has a
+ // replay handler). Actual bookmark-ability still keys off spec PRESENCE on the
+ // record at runtime — this flag just says the kind CAN be bookmarked/retried.
+ bookmarkable: boolean;
+ // Descriptive: how the action derives its queueKey. Not consumed as logic.
+ queueKeyStrategy: QueueKeyStrategy;
+ // Fallback scheduler tier when a record is not explicitly background. Left
+ // undefined today so Phase 4 maps it to "foreground" (unchanged behavior).
+ defaultTier?: SchedulerTier;
+};
+
+// One entry per kind known to the system. `label` is included only where the
+// old JOB_KIND_LABELS had one, so jobKindLabel() behavior is byte-identical.
+const JOB_KINDS: Record<string, JobKindMeta> = {
+ "auto-transcribe": {
+ kind: "auto-transcribe",
+ label: "Auto-transcribe",
+ drainable: true,
+ bookmarkable: false,
+ queueKeyStrategy: "parallel",
+ },
+ "auto-download": {
+ kind: "auto-download",
+ label: "Auto-download runner",
+ drainable: true,
+ bookmarkable: false,
+ queueKeyStrategy: "parallel",
+ },
+ "auto-download-unit": {
+ kind: "auto-download-unit",
+ label: "Auto-download",
+ drainable: false,
+ bookmarkable: false,
+ queueKeyStrategy: "platform",
+ },
+ "whisper-all": {
+ kind: "whisper-all",
+ label: "Transcribe all",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "whisper-bucket-downloaded-no-transcript": {
+ kind: "whisper-bucket-downloaded-no-transcript",
+ label: "Transcribe downloaded audio",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "download-from-playlist": {
+ kind: "download-from-playlist",
+ label: "Download from playlist",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "platform",
+ },
+ "download-missing": {
+ kind: "download-missing",
+ label: "Download missing",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "platform",
+ },
+ "download-missing-subs": {
+ kind: "download-missing-subs",
+ label: "Download missing subs",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "platform",
+ },
+ "import-one": {
+ kind: "import-one",
+ label: "Import video",
+ drainable: false,
+ bookmarkable: false,
+ queueKeyStrategy: "custom",
+ },
+ "redownload-archive": {
+ kind: "redownload-archive",
+ label: "Archive source video",
+ drainable: false,
+ bookmarkable: false,
+ queueKeyStrategy: "custom",
+ },
+ "retry-bucket": {
+ kind: "retry-bucket",
+ label: "Retry",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "platform",
+ },
+ "clean-audio-transcribed": {
+ kind: "clean-audio-transcribed",
+ label: "Clean audio",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "check-kept-deleted": {
+ kind: "check-kept-deleted",
+ label: "Check kept videos",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "persist-kept": {
+ kind: "persist-kept",
+ label: "Persist kept videos",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "backup-saved-videos": {
+ kind: "backup-saved-videos",
+ label: "Back up saved videos",
+ drainable: false,
+ bookmarkable: false,
+ queueKeyStrategy: "custom",
+ },
+ "verify-saved-video-backup": {
+ kind: "verify-saved-video-backup",
+ label: "Verify saved-video backup",
+ drainable: false,
+ bookmarkable: false,
+ queueKeyStrategy: "custom",
+ },
+ sync: {
+ kind: "sync",
+ label: "Sync",
+ drainable: true,
+ bookmarkable: true,
+ queueKeyStrategy: "platform",
+ },
+ // Replayable kinds that never had a JOB_KIND_LABELS entry: label omitted so
+ // jobKindLabel() keeps falling back to the raw kind (unchanged behavior).
+ "store-playlist": {
+ kind: "store-playlist",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "clear-failed-transcriptions": {
+ kind: "clear-failed-transcriptions",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "transcode-failures": {
+ kind: "transcode-failures",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "transcode-untranscoded": {
+ kind: "transcode-untranscoded",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "remove-failed-transcodings": {
+ kind: "remove-failed-transcodings",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "clear-failed-transcodings": {
+ kind: "clear-failed-transcodings",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "clean-extra-audio-formats": {
+ kind: "clean-extra-audio-formats",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+ "remove-wrong-format-audio": {
+ kind: "remove-wrong-format-audio",
+ drainable: false,
+ bookmarkable: true,
+ queueKeyStrategy: "custom",
+ },
+};
+
+export function getJobKind(kind: string): JobKindMeta | undefined {
+ return JOB_KINDS[kind];
+}
+
+// Human-readable label, falling back to the raw kind for unknown/label-less
+// kinds (a new kind is never invisible).
+export function jobKindLabel(kind: string): string {
+ return JOB_KINDS[kind]?.label ?? kind;
+}
+
+export function isDrainableKind(kind: string): boolean {
+ return JOB_KINDS[kind]?.drainable ?? false;
+}
+
+export function isBookmarkableKind(kind: string): boolean {
+ return JOB_KINDS[kind]?.bookmarkable ?? false;
+}
diff --git a/common/jobs/registry.ts b/common/jobs/registry.ts
@@ -351,23 +351,7 @@ export function newJobId(): string {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
-// Batch kinds whose controllers honor the drain signal (stop starting new
-// sub-operations, let in-flight ones finish). The Drain button is only offered
-// for running jobs of these kinds.
-const DRAINABLE_KINDS = new Set<string>([
- "whisper-all",
- "whisper-bucket-downloaded-no-transcript",
- "download-from-playlist",
- "download-missing",
- "download-missing-subs",
- "sync",
- "retry-bucket",
- // The auto-queue runners: drain stops launching new units, lets in-flight
- // finish, then the loop returns and the job completes. See autoRunner.ts.
- "auto-transcribe",
- "auto-download",
-]);
-
-export function isDrainableKind(kind: string): boolean {
- return DRAINABLE_KINDS.has(kind);
-}
+// Which kinds honor the drain signal now lives in the job-kind metadata table
+// (jobKinds.ts), the single source of truth. Re-exported here so existing
+// importers of `isDrainableKind` from the registry keep resolving unchanged.
+export { isDrainableKind } from "./jobKinds";
diff --git a/editor/app/jobs/jobKindLabels.ts b/editor/app/jobs/jobKindLabels.ts
@@ -1,28 +1,6 @@
-// Human-readable labels for job `kind` values. The registry stores terse
-// machine kinds (e.g. "whisper-all", "auto-transcribe"); the UI shows these.
-// Unknown kinds fall back to the raw value so a new kind is never invisible.
-const JOB_KIND_LABELS: Record<string, string> = {
- "auto-transcribe": "Auto-transcribe",
- // The long-lived orchestrator loop (one per kind) vs. the per-video download
- // it now launches as a real job on the channel's platform queue.
- "auto-download": "Auto-download runner",
- "auto-download-unit": "Auto-download",
- "whisper-all": "Transcribe all",
- "whisper-bucket-downloaded-no-transcript": "Transcribe downloaded audio",
- "download-from-playlist": "Download from playlist",
- "download-missing": "Download missing",
- "download-missing-subs": "Download missing subs",
- "import-one": "Import video",
- "redownload-archive": "Archive source video",
- "retry-bucket": "Retry",
- "clean-audio-transcribed": "Clean audio",
- "check-kept-deleted": "Check kept videos",
- "persist-kept": "Persist kept videos",
- "backup-saved-videos": "Back up saved videos",
- "verify-saved-video-backup": "Verify saved-video backup",
- sync: "Sync",
-};
-
-export function jobKindLabel(kind: string): string {
- return JOB_KIND_LABELS[kind] ?? kind;
-}
+// Human-readable labels for job `kind` values now live in the shared job-kind
+// metadata table (common/jobs/jobKinds.ts), the single source of truth. This
+// module re-exports the lookup so existing editor imports stay unchanged. The
+// registry stores terse machine kinds (e.g. "whisper-all"); the UI shows these,
+// falling back to the raw value so a new kind is never invisible.
+export { jobKindLabel } from "yt-dlp-transcript-common/jobs/jobKinds";