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:
@@ -42,7 +42,7 @@ function throwIfAborted(signal?: AbortSignal): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
if (!signal) {
|
||||
await sleep(ms);
|
||||
return;
|
||||
@@ -68,8 +68,44 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
if (!signal) {
|
||||
return promise;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const onAbort = (): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(abortError());
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
promise.then((value) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
}, (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
@@ -280,10 +316,10 @@ export class AllDebridWebFallback {
|
||||
}
|
||||
return job();
|
||||
};
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return run;
|
||||
}
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
|
||||
@@ -23,11 +23,47 @@ function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number):
|
||||
return AbortSignal.any([signal, timeoutSignal]);
|
||||
}
|
||||
|
||||
function throwIfAborted(signal?: AbortSignal): void {
|
||||
function throwIfAborted(signal?: AbortSignal): void {
|
||||
if (signal?.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
if (!signal) {
|
||||
return promise;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const onAbort = (): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(abortError());
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
promise.then((value) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
}, (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(text: string): Record<string, unknown> | null {
|
||||
try {
|
||||
@@ -238,10 +274,10 @@ export class BestDebridWebFallback {
|
||||
}
|
||||
return job();
|
||||
};
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return run;
|
||||
}
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> {
|
||||
throwIfAborted(signal);
|
||||
|
||||
+13
-3
@@ -586,9 +586,19 @@ export function getProviderRuntimeSnapshot(now = Date.now()): ProviderRuntimeSna
|
||||
},
|
||||
debridLink: { keys: dlKeys, hostCooldowns: dlHostCooldowns }
|
||||
};
|
||||
}
|
||||
|
||||
const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
|
||||
}
|
||||
|
||||
export function getNextProviderRuntimeRetryAt(now = Date.now()): number | null {
|
||||
const deadlines = [
|
||||
...[...realDebridAccountCooldowns.values()].map((entry) => entry.until),
|
||||
...[...megaDebridAccountCooldowns.values()].map((entry) => entry.until),
|
||||
...debridLinkKeyCooldowns.values(),
|
||||
...debridLinkKeyHostCooldowns.values()
|
||||
].filter((deadline) => Number.isFinite(deadline) && deadline > now);
|
||||
return deadlines.length > 0 ? Math.min(...deadlines) : null;
|
||||
}
|
||||
|
||||
const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
|
||||
|
||||
const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
||||
realdebrid: "Real-Debrid",
|
||||
|
||||
+108
-44
@@ -62,7 +62,7 @@ function releaseTlsSkip(): void {
|
||||
}
|
||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkDdownloadOnline, checkOneFichierLinks, checkRapidgatorOnline, fetchAllDebridHostInfo, filenameFromDdownloadUrlPath, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, isDdownloadLink, isOneFichierLink, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState, releaseRealDebridAccountCooldown, type DdownloadCheckResult, type OneFichierCheckResult } from "./debrid";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkDdownloadOnline, checkOneFichierLinks, checkRapidgatorOnline, fetchAllDebridHostInfo, filenameFromDdownloadUrlPath, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getNextProviderRuntimeRetryAt, getRealDebridAccountAttemptTimeoutMs, isDdownloadLink, isOneFichierLink, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState, releaseRealDebridAccountCooldown, type DdownloadCheckResult, type OneFichierCheckResult } from "./debrid";
|
||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo, type ExtractProgressUpdate } from "./extractor";
|
||||
import { validateFileAgainstManifest } from "./integrity";
|
||||
import { classifyDiskError } from "./fs-error";
|
||||
@@ -1856,8 +1856,10 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private nonResumableActive = 0;
|
||||
|
||||
private stateEmitTimer: NodeJS.Timeout | null = null;
|
||||
private lastStateEmitAt = 0;
|
||||
private stateEmitTimer: NodeJS.Timeout | null = null;
|
||||
private lastStateEmitAt = 0;
|
||||
private providerRetryTimer: NodeJS.Timeout | null = null;
|
||||
private providerRetryAt = 0;
|
||||
|
||||
private speedBytesLastWindow = 0;
|
||||
|
||||
@@ -2728,11 +2730,15 @@ export class DownloadManager extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
public getSnapshot(): UiSnapshot {
|
||||
const now = nowMs();
|
||||
this.ensureProviderDailyUsageFresh(now, true);
|
||||
this.pruneSpeedEvents(now);
|
||||
const paused = this.session.running && this.session.paused;
|
||||
public getSnapshot(): UiSnapshot {
|
||||
const now = nowMs();
|
||||
this.ensureProviderDailyUsageFresh(now, true);
|
||||
this.pruneSpeedEvents(now);
|
||||
const hasUsableAccount = this.hasUsableDownloadAccount();
|
||||
const providerRetryAt = this.getEarliestProviderRetryAt(now);
|
||||
this.syncProviderRetryTimer(providerRetryAt, now);
|
||||
const lifecycle = this.getLifecycleSnapshot(providerRetryAt, hasUsableAccount);
|
||||
const paused = this.session.running && this.session.paused;
|
||||
const speedBps = !this.session.running || paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
|
||||
|
||||
let totalItems = 0;
|
||||
@@ -2781,7 +2787,7 @@ export class DownloadManager extends EventEmitter {
|
||||
return {
|
||||
rotationEvents: getRecentRotationEvents(40),
|
||||
accountRuntime: createAccountRuntimeEntries(rendererState.accounts, Object.values(snapshotSession.items), now),
|
||||
lifecycle: this.getLifecycleSnapshot(),
|
||||
lifecycle,
|
||||
settings: rendererState.settings,
|
||||
accounts: rendererState.accounts,
|
||||
session: snapshotSession,
|
||||
@@ -2789,7 +2795,9 @@ export class DownloadManager extends EventEmitter {
|
||||
stats: this.getStats(now),
|
||||
speedText: `Geschwindigkeit: ${humanSize(Math.max(0, Math.floor(speedBps)))}/s`,
|
||||
etaText: paused || !this.session.running ? "ETA: --" : `ETA: ${formatEta(eta)}`,
|
||||
canStart: (!this.session.running || paused) && this.hasUsableDownloadAccount(),
|
||||
canStart: hasUsableAccount && (paused || (!this.session.running
|
||||
&& lifecycle.phase !== "waiting_provider"
|
||||
&& (lifecycle.phase !== "stopping" || !lifecycle.pendingStart))),
|
||||
canStop: this.session.running,
|
||||
canPause: this.session.running,
|
||||
clipboardActive: this.settings.clipboardWatch,
|
||||
@@ -3257,10 +3265,15 @@ export class DownloadManager extends EventEmitter {
|
||||
this.clearPersistTimer();
|
||||
this.stop();
|
||||
this.abortPostProcessing("clear_all");
|
||||
if (this.stateEmitTimer) {
|
||||
clearTimeout(this.stateEmitTimer);
|
||||
this.stateEmitTimer = null;
|
||||
}
|
||||
if (this.stateEmitTimer) {
|
||||
clearTimeout(this.stateEmitTimer);
|
||||
this.stateEmitTimer = null;
|
||||
}
|
||||
if (this.providerRetryTimer) {
|
||||
clearTimeout(this.providerRetryTimer);
|
||||
this.providerRetryTimer = null;
|
||||
this.providerRetryAt = 0;
|
||||
}
|
||||
this.session.packageOrder = [];
|
||||
this.session.packages = {};
|
||||
this.session.items = {};
|
||||
@@ -3926,7 +3939,42 @@ export class DownloadManager extends EventEmitter {
|
||||
return tasks.size;
|
||||
}
|
||||
|
||||
private getLifecycleSnapshot(): DownloadLifecycleSnapshot {
|
||||
private getEarliestProviderRetryAt(now: number): number | null {
|
||||
const deadlines = [...this.providerFailures.values()]
|
||||
.map((entry) => entry.cooldownUntil)
|
||||
.filter((deadline) => Number.isFinite(deadline) && deadline > now);
|
||||
const runtimeRetryAt = getNextProviderRuntimeRetryAt(now);
|
||||
if (runtimeRetryAt) {
|
||||
deadlines.push(runtimeRetryAt);
|
||||
}
|
||||
return deadlines.length > 0 ? Math.min(...deadlines) : null;
|
||||
}
|
||||
|
||||
private syncProviderRetryTimer(retryAt: number | null, now: number): void {
|
||||
if (!retryAt || retryAt <= now) {
|
||||
if (this.providerRetryTimer) {
|
||||
clearTimeout(this.providerRetryTimer);
|
||||
this.providerRetryTimer = null;
|
||||
}
|
||||
this.providerRetryAt = 0;
|
||||
return;
|
||||
}
|
||||
if (this.providerRetryTimer && this.providerRetryAt === retryAt) {
|
||||
return;
|
||||
}
|
||||
if (this.providerRetryTimer) {
|
||||
clearTimeout(this.providerRetryTimer);
|
||||
}
|
||||
this.providerRetryAt = retryAt;
|
||||
this.providerRetryTimer = setTimeout(() => {
|
||||
this.providerRetryTimer = null;
|
||||
this.providerRetryAt = 0;
|
||||
this.emitState(true);
|
||||
}, Math.min(2_147_483_647, Math.max(0, retryAt - now)));
|
||||
this.providerRetryTimer.unref?.();
|
||||
}
|
||||
|
||||
private getLifecycleSnapshot(retryAt: number | null, hasUsableAccount: boolean): DownloadLifecycleSnapshot {
|
||||
const activeDownloads = this.activeTasks.size;
|
||||
const activePostProcessing = this.getActivePostProcessingCount();
|
||||
const pendingStart = this.pendingStartOptions !== null;
|
||||
@@ -3934,7 +3982,7 @@ export class DownloadManager extends EventEmitter {
|
||||
return {
|
||||
phase: "stopping",
|
||||
reason: pendingStart ? "Start vorgemerkt, laufende Arbeit wird beendet" : "Laufende Arbeit wird beendet",
|
||||
retryAt: null,
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
@@ -3944,18 +3992,28 @@ export class DownloadManager extends EventEmitter {
|
||||
return {
|
||||
phase: "starting",
|
||||
reason: this.lifecycleReason,
|
||||
retryAt: null,
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
};
|
||||
}
|
||||
if (this.session.running) {
|
||||
if (activeDownloads === 0 && retryAt) {
|
||||
return {
|
||||
phase: "waiting_provider",
|
||||
reason: "Provider vorübergehend nicht verfügbar",
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
};
|
||||
}
|
||||
const postprocessing = activeDownloads === 0 && activePostProcessing > 0;
|
||||
return {
|
||||
phase: postprocessing ? "postprocessing" : "running",
|
||||
reason: postprocessing ? "Nachbearbeitung läuft" : this.session.paused ? "Downloads pausiert" : "Downloads laufen",
|
||||
retryAt: null,
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
@@ -3965,16 +4023,19 @@ export class DownloadManager extends EventEmitter {
|
||||
return {
|
||||
phase: "postprocessing",
|
||||
reason: "Nachbearbeitung läuft",
|
||||
retryAt: null,
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
};
|
||||
}
|
||||
const waitingForProvider = !hasUsableAccount && retryAt !== null;
|
||||
return {
|
||||
phase: "idle",
|
||||
reason: this.lifecycleReason,
|
||||
retryAt: null,
|
||||
phase: waitingForProvider ? "waiting_provider" : "idle",
|
||||
reason: waitingForProvider
|
||||
? "Provider vorübergehend nicht verfügbar"
|
||||
: hasUsableAccount ? this.lifecycleReason : "Kein aktiver Download-Account verfügbar",
|
||||
retryAt,
|
||||
activeDownloads,
|
||||
activePostProcessing,
|
||||
pendingStart
|
||||
@@ -6451,6 +6512,8 @@ export class DownloadManager extends EventEmitter {
|
||||
try {
|
||||
this.beginHealthRun();
|
||||
this.ensureUsableDownloadAccount();
|
||||
this.session.running = true;
|
||||
this.session.paused = false;
|
||||
const recoveryRunPackageIds = new Set(this.session.packageOrder.filter((packageId) => {
|
||||
const pkg = this.session.packages[packageId];
|
||||
return Boolean(pkg && !pkg.cancelled && pkg.enabled && !options?.excludePackageIds?.has(packageId));
|
||||
@@ -6460,12 +6523,12 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
const recoveredItems = await this.recoverRetryableItems("start", recoveryRunPackageIds);
|
||||
if (this.lifecycleGeneration !== generation || this.lifecyclePhase === "stopping") {
|
||||
if (this.lifecycleGeneration !== generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(0);
|
||||
if (this.lifecycleGeneration !== generation || this.lifecyclePhase === "stopping") {
|
||||
if (this.lifecycleGeneration !== generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6631,7 +6694,6 @@ export class DownloadManager extends EventEmitter {
|
||||
this.healthManualStop = !parkForRestart;
|
||||
this.healthShuttingDown = parkForRestart;
|
||||
const abortReason: "stop" | "shutdown" = parkForRestart ? "shutdown" : "stop";
|
||||
const keepExtraction = this.settings.autoExtractWhenStopped;
|
||||
const wasRunning = this.session.running;
|
||||
const stoppedRunContext = wasRunning
|
||||
? this.stopActiveRunContext(this.runPackageIds, this.session.runStartedAt)
|
||||
@@ -6652,12 +6714,10 @@ export class DownloadManager extends EventEmitter {
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.speedBytesPerPackage.clear();
|
||||
this.speedEventsHead = 0;
|
||||
if (!keepExtraction) {
|
||||
this.abortPostProcessing("stop");
|
||||
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
|
||||
this.packagePostProcessWaiters = [];
|
||||
this.packagePostProcessActive = 0;
|
||||
}
|
||||
this.abortPostProcessing("stop");
|
||||
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
|
||||
this.packagePostProcessWaiters = [];
|
||||
this.packagePostProcessActive = 0;
|
||||
for (const active of this.activeTasks.values()) {
|
||||
active.abortReason = abortReason;
|
||||
active.abortController.abort(abortReason);
|
||||
@@ -6672,10 +6732,7 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
for (const pkg of Object.values(this.session.packages)) {
|
||||
if (keepExtraction && (pkg.status === "extracting" || pkg.status === "integrity_check")) {
|
||||
continue;
|
||||
}
|
||||
if (pkg.status === "downloading" || pkg.status === "validating"
|
||||
if (pkg.status === "downloading" || pkg.status === "validating"
|
||||
|| pkg.status === "extracting" || pkg.status === "integrity_check"
|
||||
|| pkg.status === "paused" || pkg.status === "reconnect_wait") {
|
||||
pkg.status = "queued";
|
||||
@@ -6711,12 +6768,17 @@ export class DownloadManager extends EventEmitter {
|
||||
this.healthShuttingDown = true;
|
||||
logger.info(`Shutdown-Vorbereitung gestartet: active=${this.activeTasks.size}, running=${this.session.running}, paused=${this.session.paused}`);
|
||||
this.updateStatisticsActivity(nowMs());
|
||||
this.rotationListenerActive = false;
|
||||
this.clearPersistTimer();
|
||||
if (this.stateEmitTimer) {
|
||||
clearTimeout(this.stateEmitTimer);
|
||||
this.stateEmitTimer = null;
|
||||
}
|
||||
this.rotationListenerActive = false;
|
||||
this.clearPersistTimer();
|
||||
if (this.stateEmitTimer) {
|
||||
clearTimeout(this.stateEmitTimer);
|
||||
this.stateEmitTimer = null;
|
||||
}
|
||||
if (this.providerRetryTimer) {
|
||||
clearTimeout(this.providerRetryTimer);
|
||||
this.providerRetryTimer = null;
|
||||
this.providerRetryAt = 0;
|
||||
}
|
||||
this.session.running = false;
|
||||
this.session.paused = false;
|
||||
this.session.reconnectUntil = 0;
|
||||
@@ -14248,9 +14310,11 @@ export class DownloadManager extends EventEmitter {
|
||||
private finishRun(): void {
|
||||
const runStartedAt = this.session.runStartedAt;
|
||||
const completedAt = nowMs();
|
||||
this.session.running = false;
|
||||
this.session.paused = false;
|
||||
this.session.runStartedAt = 0;
|
||||
this.session.running = false;
|
||||
this.session.paused = false;
|
||||
this.lifecyclePhase = "idle";
|
||||
this.lifecycleReason = "Bereit";
|
||||
this.session.runStartedAt = 0;
|
||||
const total = this.runItemIds.size;
|
||||
const outcomes = Array.from(this.runOutcomes.values());
|
||||
const success = outcomes.filter((status) => status === "completed").length;
|
||||
|
||||
@@ -47,7 +47,7 @@ function throwIfAborted(signal?: AbortSignal): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
if (!signal) {
|
||||
await sleep(ms);
|
||||
return;
|
||||
@@ -73,8 +73,44 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
if (!signal) {
|
||||
return promise;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const onAbort = (): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(abortError());
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
promise.then((value) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
}, (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(text: string): Record<string, unknown> | null {
|
||||
try {
|
||||
@@ -299,10 +335,10 @@ export class RealDebridWebFallback {
|
||||
}
|
||||
return job();
|
||||
};
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return run;
|
||||
}
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
const partition = this.getPartition();
|
||||
|
||||
Reference in New Issue
Block a user