Complete provider waits and lifecycle visibility

Release aborted Real-Debrid, AllDebrid, and BestDebrid web callers even when underlying requests ignore cancellation while retaining terminal rejection observers. Publish provider cooldown deadlines and emit a fresh idle snapshot at the earliest expiry. Abort and visibly drain post-processing before dispatching one pending restart. Surface lifecycle phase, reason, retry countdown, and remaining work in the download controls. Add focused regressions for provider abort races, cooldown expiry, post-processing drain, pending start visibility, and the updated rapid stop contract.
This commit is contained in:
Sucukdeluxe
2026-08-22 10:39:07 +02:00
parent 1f97ce8b42
commit ad29239661
13 changed files with 595 additions and 82 deletions
+36 -4
View File
@@ -1,4 +1,5 @@
import type { ReactElement } from "react";
import type { DownloadLifecycleSnapshot } from "../../../shared/types";
import { RollingMetricValue } from "../../ui/RollingMetricValue";
import { SlidingSelection } from "../../ui/SlidingSelection";
import type { DownloadsViewModelCore, DownloadDisplayMode, DownloadSidebarFilter } from "./downloads-model";
@@ -54,8 +55,10 @@ export interface DownloadsViewModel extends DownloadsViewModelCore {
sortDirection?: "asc" | "desc";
disclosureRevision: number;
animationsEnabled: boolean;
lifecycle: DownloadLifecycleSnapshot;
lifecycleNow: number;
status: DownloadsStatusModel;
}
}
export interface DownloadsViewActions extends DownloadsTableActions {
onResetColumnLayout: () => void;
@@ -134,7 +137,7 @@ export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewAct
const scheduleSlotClass = `downloads-schedule-slot ${scheduleSlotOpen ? "is-open" : "is-closed"}${model.animationsEnabled ? "" : " is-motion-disabled"}`;
return (
<div className="downloads-toolbar" data-visual-region="downloads-toolbar">
<button disabled={model.actionBusy || !model.canStart} onClick={actions.onStartDownloads} type="button">Start</button>
<button disabled={model.actionBusy || !model.canStart} onClick={actions.onStartDownloads} type="button">{model.lifecycle.pendingStart ? "Start vorgemerkt" : "Start"}</button>
<button disabled={!model.canPause || model.paused} onClick={actions.onPauseDownloads} type="button">Pause</button>
<button disabled={!model.canStop || model.actionBusy} onClick={actions.onStopDownloads} type="button">Stop</button>
{!model.scheduleActive ? <button aria-expanded={model.scheduleOpen} onClick={actions.onToggleSchedule} type="button">Zeitplan</button> : null}
@@ -171,13 +174,42 @@ export function DownloadsContent({ actions, model }: { actions: DownloadsViewAct
</main>
);
}
function formatLifecycleStatus(model: DownloadsViewModel): string {
const phaseLabels: Record<DownloadLifecycleSnapshot["phase"], string> = {
idle: "Bereit",
starting: "Startet",
running: "Läuft",
stopping: "Stoppt",
waiting_provider: "Wartet auf Provider",
postprocessing: "Nachbearbeitung"
};
const parts = [phaseLabels[model.lifecycle.phase]];
if (model.lifecycle.reason && model.lifecycle.reason !== parts[0]) {
parts.push(model.lifecycle.reason);
}
if (model.lifecycle.retryAt && model.lifecycle.retryAt > model.lifecycleNow) {
parts.push(`Noch ${Math.max(1, Math.ceil((model.lifecycle.retryAt - model.lifecycleNow) / 1000))} s`);
}
const remaining: string[] = [];
if (model.lifecycle.activeDownloads > 0) {
remaining.push(`${model.lifecycle.activeDownloads} ${model.lifecycle.activeDownloads === 1 ? "Download" : "Downloads"}`);
}
if (model.lifecycle.activePostProcessing > 0) {
remaining.push(`${model.lifecycle.activePostProcessing} ${model.lifecycle.activePostProcessing === 1 ? "Nachbearbeitung" : "Nachbearbeitungen"}`);
}
if (remaining.length > 0) {
parts.push(`Restarbeit: ${remaining.join(", ")}`);
}
return parts.join(" · ");
}
export function DownloadsFooter({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
export function DownloadsFooter({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
return (
<footer className="downloads-footer" data-visual-region="downloads-pagination">
<span>{model.paginationLabel}</span>
{model.limited ? <button onClick={actions.onShowAllPackages} type="button">Alle anzeigen</button> : null}
<span>{model.running ? model.paused ? "Pausiert" : "Download läuft" : "Bereit"}</span>
<span role="status">{formatLifecycleStatus(model)}</span>
</footer>
);
}