Archilyzer · Source

archilyzer

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

commit 6b4b523e7d43d35e8b2376483e225780689a7ad5
parent 26689d10b832efe9345baa20d62a27facfeb089e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 11 Sep 2026 11:37:19 -0400

editor: one writer for the channel priority document, and it recompiles

`saveChannelPriorityAction` in editor/app/channels/actions.ts is the only
function that writes `settings.channelPriority`. It has to be, because the
document is not the only thing a priority change produces: the four
`autoQueue[lane].root` trees are COMPILED from it, so a second writer would
leave the model and the trees disagreeing until whoever wrote next happened to
recompile. The recompile therefore happens in the same `writeSettings` call,
with each policy SPREAD rather than rebuilt — `enabled`, `held`, `order`,
`snoozeUntil`, `maxWorkers` and `replaceAutoSubs` all survive, which is the rule
`saveAutoQueueAction` already states: a focus must never start a stopped lane or
unhold a held one.

The edit is a serializable union, not a callback — a server action's arguments
cross the boundary, so "apply this function to the current document" is not
expressible. Each variant is one operator gesture: a base tier on a set of
slugs, one operation pinned or unpinned, a preset, or the corpus-wide focus.
`applyPriorityEdit` only has to state the gesture; `sanitizeChannelPriority`
normalises the result (an override equal to the base is dropped, and so is an
entry saying nothing the default does not).

The site membership behind `focus.kind === "site"` is read at WRITE time, never
stored: a site focus tracks the site's membership rather than freezing a list,
which is the whole reason that variant exists.

The named gestures — setChannelTierAction, setChannelOperationTierAction,
applyChannelPriorityPresetAction, focusChannelsAction, focusSiteAction,
endFocusAction — are one call each to the writer, so a control names what it
does instead of assembling an edit inline.

Nothing reads these yet; the UI lands next.

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

Diffstat:
Meditor/app/channels/actions.ts | 198++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 197 insertions(+), 1 deletion(-)

diff --git a/editor/app/channels/actions.ts b/editor/app/channels/actions.ts @@ -29,7 +29,28 @@ import { requestChannelSnapshot } from "yt-dlp-transcript-common/jobs/snapshotSc import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { runManagedFunction } from "yt-dlp-transcript-common/jobs/streamCommand"; import { drainStream } from "yt-dlp-transcript-common/jobs/drainStream"; -import type { Site } from "yt-dlp-transcript-common/lib/site"; +import { + listSites, + siteChannelSlugs, + type Site, +} from "yt-dlp-transcript-common/lib/site"; +import { + getSettings, + writeSettings, +} from "yt-dlp-transcript-common/lib/settings"; +import { LANES } from "yt-dlp-transcript-common/lib/autoQueueTypes"; +import { + compileLanes, + DEFAULT_CHANNEL_TIER, + resolveFocusSlugs, + sanitizeChannelPriority, + type ChannelFocus, + type ChannelPriority, + type ChannelPriorityEntry, + type PriorityOperation, + type SiteChannelIndex, + type StoredChannelTier, +} from "yt-dlp-transcript-common/lib/channelPriority"; import { activeSyncSlugs } from "yt-dlp-transcript-common/jobs/syncJobs"; import { readSchedulerState, @@ -569,3 +590,178 @@ export async function renameChannelAction( revalidatePath("/"); redirect(`/channels/${newSlug}`); } + +// --------------------------------------------------------------------------- +// CHANNEL PRIORITY — the one writer +// --------------------------------------------------------------------------- + +// EVERY WRITE OF `settings.channelPriority` GOES THROUGH `saveChannelPriorityAction`. +// +// One writer, for the same reason `withGateHeld` is the one writer of a lane's +// `held`: the document is not the only thing a priority change produces. The +// four `autoQueue[lane].root` trees are COMPILED from it (common/lib/ +// channelPriority.ts), so a second writer would leave the model and the trees +// disagreeing until whoever wrote next happened to recompile. The recompile +// therefore happens HERE, in the same `writeSettings` call that persists the +// document, and every control on /channels funnels through the edit vocabulary +// below rather than assembling a `ChannelPriority` of its own. +// +// The edit is a SERIALIZABLE union, not a callback: a server action's arguments +// cross the network boundary, so "apply this function to the current document" +// is not expressible. Each variant is one operator gesture. +export type ChannelPriorityEdit = + // Set the BASE tier of one or more channels. Overrides survive; the sanitizer + // drops any that now equal the base. + | { kind: "tier"; slugs: string[]; tier: StoredChannelTier } + // Pin ONE operation to a tier, or clear the pin (`tier: null` = inherit). + | { kind: "operation"; slugs: string[]; operation: PriorityOperation; tier: StoredChannelTier | null } + // The two presets. "sync-only" is `{tier:"paused", overrides:{sync:"normal"}}` + // — keep the playlist current, dispatch nothing. "clear" returns the channel + // to the default (normal, unranked, unpinned) by dropping its entry. + | { kind: "preset"; slugs: string[]; preset: "sync-only" | "clear" } + // The corpus-wide focus selector, including `{kind:"none"}` (End focus). + | { kind: "focus"; focus: ChannelFocus }; + +function entryFor( + model: ChannelPriority, + slug: string, +): ChannelPriorityEntry { + const existing = model.channels[slug]; + return existing + ? { ...existing, overrides: { ...(existing.overrides ?? {}) } } + : { tier: DEFAULT_CHANNEL_TIER }; +} + +// Pure. The sanitizer is what normalises the result — an override equal to the +// base is dropped there, and so is an entry that says nothing the default does +// not — so this only has to state the gesture. +function applyPriorityEdit( + model: ChannelPriority, + edit: ChannelPriorityEdit, +): ChannelPriority { + if (edit.kind === "focus") return { ...model, focus: edit.focus }; + const channels: Record<string, ChannelPriorityEntry> = { ...model.channels }; + for (const raw of edit.slugs) { + const slug = raw.trim(); + if (!slug) continue; + if (edit.kind === "tier") { + channels[slug] = { ...entryFor(model, slug), tier: edit.tier }; + continue; + } + if (edit.kind === "operation") { + const entry = entryFor(model, slug); + const overrides = { ...(entry.overrides ?? {}) }; + if (edit.tier === null) delete overrides[edit.operation]; + else overrides[edit.operation] = edit.tier; + channels[slug] = { ...entry, overrides }; + continue; + } + if (edit.preset === "clear") { + delete channels[slug]; + continue; + } + // "sync-only": paused everywhere, normal for sync. Its rank survives — + // the sync scheduler still orders it. + const entry = entryFor(model, slug); + channels[slug] = { + ...entry, + tier: "paused", + overrides: { sync: "normal" }, + }; + } + return { ...model, channels }; +} + +// siteId -> its channel slugs, for `resolveFocusSlugs`. Read at WRITE time, not +// stored: a site focus tracks the site's membership rather than freezing a list, +// which is the whole reason `focus.kind === "site"` exists. +function siteChannelIndex(paths: ReturnType<typeof getPaths>): SiteChannelIndex { + const index: Record<string, string[]> = {}; + for (const site of listSites(paths)) { + index[site.siteId] = [...siteChannelSlugs(site)]; + } + return index; +} + +// THE ONE WRITER. Reads the current settings, applies one edit through +// `sanitizeChannelPriority`, recompiles the four lane roots from the result and +// persists both in a single `writeSettings`. +// +// Each policy is SPREAD rather than rebuilt, so `enabled`, `held`, `order`, +// `snoozeUntil`, `maxWorkers` and `replaceAutoSubs` survive a priority change — +// the rule `saveAutoQueueAction` states: a focus must never start a stopped lane +// or unhold a held one. +export async function saveChannelPriorityAction( + edit: ChannelPriorityEdit, +): Promise<ActionResult> { + const paths = getPaths(); + const settings = getSettings(); + const next = sanitizeChannelPriority( + applyPriorityEdit(settings.channelPriority, edit), + ); + const slugs = (await listChannelConfigs(paths)).map((c) => c.slug); + const focusSlugs = resolveFocusSlugs(next, siteChannelIndex(paths), slugs); + const roots = compileLanes(next, slugs, focusSlugs); + const autoQueue = { ...settings.autoQueue }; + for (const lane of LANES) { + autoQueue[lane] = { ...settings.autoQueue[lane], root: roots[lane] }; + } + try { + await writeSettings({ ...settings, channelPriority: next, autoQueue }); + } catch (e) { + return { error: (e as Error).message }; + } + revalidatePath("/channels"); + revalidatePath("/operations"); + revalidatePath("/operations/[id]", "page"); + return undefined; +} + +// The named gestures. Each is one call to the writer above — they exist so a +// control names what it does rather than assembling an edit union inline. +export async function setChannelTierAction( + slugs: string[], + tier: StoredChannelTier, +): Promise<ActionResult> { + return saveChannelPriorityAction({ kind: "tier", slugs, tier }); +} + +export async function setChannelOperationTierAction( + slugs: string[], + operation: PriorityOperation, + tier: StoredChannelTier | null, +): Promise<ActionResult> { + return saveChannelPriorityAction({ + kind: "operation", + slugs, + operation, + tier, + }); +} + +export async function applyChannelPriorityPresetAction( + slugs: string[], + preset: "sync-only" | "clear", +): Promise<ActionResult> { + return saveChannelPriorityAction({ kind: "preset", slugs, preset }); +} + +export async function focusChannelsAction( + slugs: string[], +): Promise<ActionResult> { + return saveChannelPriorityAction({ + kind: "focus", + focus: { kind: "channels", slugs }, + }); +} + +export async function focusSiteAction(siteId: string): Promise<ActionResult> { + return saveChannelPriorityAction({ + kind: "focus", + focus: { kind: "site", siteId }, + }); +} + +export async function endFocusAction(): Promise<ActionResult> { + return saveChannelPriorityAction({ kind: "focus", focus: { kind: "none" } }); +}