✨ feat(megadebrid): add API mode with toggle and provider labels
- Add Mega-Debrid API support (connectUser + getLink endpoints) - API mode preferred by default, with automatic web fallback on failure - User toggle "Mega-Debrid bevorzugt über API" in settings UI - Provider labels now show source: "Mega-Debrid (API)" or "Mega-Debrid (Web)" - sourceLabel propagated through all provider result paths - API session token cached for 20 minutes with auto-invalidation - Remove megaWebUnrestrict requirement for Mega-Debrid provider config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6341650916
commit
faece1cf26
@ -44,6 +44,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
realDebridUseWebLogin: false,
|
realDebridUseWebLogin: false,
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
|
megaDebridPreferApi: true,
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
allDebridUseWebLogin: false,
|
allDebridUseWebLogin: false,
|
||||||
|
|||||||
@ -12,6 +12,8 @@ const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
|
|||||||
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
|
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
|
||||||
const ALL_DEBRID_API_BASE_V41 = "https://api.alldebrid.com/v4.1";
|
const ALL_DEBRID_API_BASE_V41 = "https://api.alldebrid.com/v4.1";
|
||||||
|
|
||||||
|
const MEGA_DEBRID_API_BASE = "https://www.mega-debrid.eu/api.php";
|
||||||
|
|
||||||
const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1";
|
const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1";
|
||||||
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
|
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
|
||||||
|
|
||||||
@ -159,6 +161,14 @@ function parseJson(text: string): unknown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJsonSafe(text: string): Record<string, unknown> | null {
|
||||||
|
const parsed = parseJson(text);
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
function pickString(payload: Record<string, unknown> | null, keys: string[]): string {
|
function pickString(payload: Record<string, unknown> | null, keys: string[]): string {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return "";
|
return "";
|
||||||
@ -659,11 +669,105 @@ function buildBestDebridRequests(link: string, token: string): BestDebridRequest
|
|||||||
class MegaDebridClient {
|
class MegaDebridClient {
|
||||||
private megaWebUnrestrict?: MegaWebUnrestrictor;
|
private megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
|
|
||||||
public constructor(megaWebUnrestrict?: MegaWebUnrestrictor) {
|
private login: string;
|
||||||
|
|
||||||
|
private password: string;
|
||||||
|
|
||||||
|
private preferApi: boolean;
|
||||||
|
|
||||||
|
private static cachedApiToken = "";
|
||||||
|
|
||||||
|
private static cachedApiTokenAt = 0;
|
||||||
|
|
||||||
|
public constructor(login: string, password: string, preferApi: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) {
|
||||||
|
this.login = login;
|
||||||
|
this.password = password;
|
||||||
|
this.preferApi = preferApi;
|
||||||
this.megaWebUnrestrict = megaWebUnrestrict;
|
this.megaWebUnrestrict = megaWebUnrestrict;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
private async connectApi(signal?: AbortSignal): Promise<string | null> {
|
||||||
|
// Return cached token if fresh (max 20 min)
|
||||||
|
if (MegaDebridClient.cachedApiToken && Date.now() - MegaDebridClient.cachedApiTokenAt < 20 * 60 * 1000) {
|
||||||
|
return MegaDebridClient.cachedApiToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${MEGA_DEBRID_API_BASE}?action=connectUser&login=${encodeURIComponent(this.login)}&password=${encodeURIComponent(this.password)}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { "User-Agent": DEBRID_USER_AGENT },
|
||||||
|
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = parseJsonSafe(text);
|
||||||
|
if (!payload || payload.response_code !== "ok") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const token = String(payload.token || "").trim();
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MegaDebridClient.cachedApiToken = token;
|
||||||
|
MegaDebridClient.cachedApiTokenAt = Date.now();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unrestrictViaApi(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
||||||
|
const token = await this.connectApi(signal);
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${MEGA_DEBRID_API_BASE}?action=getLink&token=${encodeURIComponent(token)}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"User-Agent": DEBRID_USER_AGENT
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ link }),
|
||||||
|
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
// Token might be invalid, clear cache
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
MegaDebridClient.cachedApiToken = "";
|
||||||
|
MegaDebridClient.cachedApiTokenAt = 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = parseJsonSafe(text);
|
||||||
|
if (!payload || payload.response_code !== "ok") {
|
||||||
|
// Token expired — clear cache for next attempt
|
||||||
|
if (payload && String(payload.response_code || "").includes("token")) {
|
||||||
|
MegaDebridClient.cachedApiToken = "";
|
||||||
|
MegaDebridClient.cachedApiTokenAt = 0;
|
||||||
|
}
|
||||||
|
const errorText = String(payload?.response_text || "").trim();
|
||||||
|
if (errorText) {
|
||||||
|
throw new Error(`Mega-Debrid API: ${errorText}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directUrl = String(payload.debridLink || "").trim();
|
||||||
|
if (!directUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fileName = String(payload.filename || "").trim() || filenameFromUrl(directUrl) || filenameFromUrl(link);
|
||||||
|
return {
|
||||||
|
directUrl,
|
||||||
|
fileName,
|
||||||
|
fileSize: null,
|
||||||
|
retriesUsed: 0,
|
||||||
|
sourceLabel: "API"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unrestrictViaWeb(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
||||||
if (!this.megaWebUnrestrict) {
|
if (!this.megaWebUnrestrict) {
|
||||||
throw new Error("Mega-Web-Fallback nicht verfügbar");
|
throw new Error("Mega-Web-Fallback nicht verfügbar");
|
||||||
}
|
}
|
||||||
@ -681,6 +785,7 @@ class MegaDebridClient {
|
|||||||
}
|
}
|
||||||
if (web?.directUrl) {
|
if (web?.directUrl) {
|
||||||
web.retriesUsed = attempt - 1;
|
web.retriesUsed = attempt - 1;
|
||||||
|
web.sourceLabel = "Web";
|
||||||
return web;
|
return web;
|
||||||
}
|
}
|
||||||
if (web && !web.directUrl) {
|
if (web && !web.directUrl) {
|
||||||
@ -699,6 +804,29 @@ class MegaDebridClient {
|
|||||||
}
|
}
|
||||||
throw new Error(String(lastError || "Mega-Web Unrestrict fehlgeschlagen").replace(/^Error:\s*/i, ""));
|
throw new Error(String(lastError || "Mega-Web Unrestrict fehlgeschlagen").replace(/^Error:\s*/i, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
||||||
|
if (this.preferApi && this.login.trim() && this.password.trim()) {
|
||||||
|
// API mode: try API first, fall back to web on failure
|
||||||
|
try {
|
||||||
|
const apiResult = await this.unrestrictViaApi(link, signal);
|
||||||
|
if (apiResult) {
|
||||||
|
logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`);
|
||||||
|
return apiResult;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = compactErrorText(error);
|
||||||
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logger.warn(`Mega-Debrid API fehlgeschlagen, versuche Web-Fallback: ${errorText}`);
|
||||||
|
}
|
||||||
|
return this.unrestrictViaWeb(link, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web mode only
|
||||||
|
return this.unrestrictViaWeb(link, signal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BestDebridClient {
|
class BestDebridClient {
|
||||||
@ -1454,7 +1582,7 @@ export class DebridService {
|
|||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
provider: "onefichier",
|
provider: "onefichier",
|
||||||
providerLabel: PROVIDER_LABELS["onefichier"]
|
providerLabel: PROVIDER_LABELS["onefichier"] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = compactErrorText(error);
|
const errorText = compactErrorText(error);
|
||||||
@ -1474,7 +1602,7 @@ export class DebridService {
|
|||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
provider: "ddownload",
|
provider: "ddownload",
|
||||||
providerLabel: PROVIDER_LABELS["ddownload"]
|
providerLabel: PROVIDER_LABELS["ddownload"] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = compactErrorText(error);
|
const errorText = compactErrorText(error);
|
||||||
@ -1509,7 +1637,7 @@ export class DebridService {
|
|||||||
...result,
|
...result,
|
||||||
fileName,
|
fileName,
|
||||||
provider: primary,
|
provider: primary,
|
||||||
providerLabel: PROVIDER_LABELS[primary]
|
providerLabel: PROVIDER_LABELS[primary] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = compactErrorText(error);
|
const errorText = compactErrorText(error);
|
||||||
@ -1542,7 +1670,7 @@ export class DebridService {
|
|||||||
...result,
|
...result,
|
||||||
fileName,
|
fileName,
|
||||||
provider,
|
provider,
|
||||||
providerLabel: PROVIDER_LABELS[provider]
|
providerLabel: PROVIDER_LABELS[provider] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = compactErrorText(error);
|
const errorText = compactErrorText(error);
|
||||||
@ -1565,7 +1693,7 @@ export class DebridService {
|
|||||||
return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim());
|
return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim());
|
||||||
}
|
}
|
||||||
if (provider === "megadebrid") {
|
if (provider === "megadebrid") {
|
||||||
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim() && this.options.megaWebUnrestrict);
|
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
||||||
}
|
}
|
||||||
if (provider === "alldebrid") {
|
if (provider === "alldebrid") {
|
||||||
return Boolean(this.shouldUseAllDebridWeb(settings) || settings.allDebridToken.trim());
|
return Boolean(this.shouldUseAllDebridWeb(settings) || settings.allDebridToken.trim());
|
||||||
@ -1591,7 +1719,7 @@ export class DebridService {
|
|||||||
return new RealDebridClient(settings.token).unrestrictLink(link, signal);
|
return new RealDebridClient(settings.token).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
if (provider === "megadebrid") {
|
if (provider === "megadebrid") {
|
||||||
return new MegaDebridClient(this.options.megaWebUnrestrict).unrestrictLink(link, signal);
|
return new MegaDebridClient(settings.megaLogin, settings.megaPassword, settings.megaDebridPreferApi, this.options.megaWebUnrestrict).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
if (provider === "alldebrid") {
|
if (provider === "alldebrid") {
|
||||||
if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) {
|
if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface UnrestrictedLink {
|
|||||||
fileSize: number | null;
|
fileSize: number | null;
|
||||||
retriesUsed: number;
|
retriesUsed: number;
|
||||||
skipTlsVerify?: boolean;
|
skipTlsVerify?: boolean;
|
||||||
|
sourceLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldRetryStatus(status: number): boolean {
|
function shouldRetryStatus(status: number): boolean {
|
||||||
|
|||||||
@ -110,6 +110,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
||||||
megaLogin: asText(settings.megaLogin),
|
megaLogin: asText(settings.megaLogin),
|
||||||
megaPassword: asText(settings.megaPassword),
|
megaPassword: asText(settings.megaPassword),
|
||||||
|
megaDebridPreferApi: settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true,
|
||||||
bestToken: asText(settings.bestToken),
|
bestToken: asText(settings.bestToken),
|
||||||
allDebridToken: asText(settings.allDebridToken),
|
allDebridToken: asText(settings.allDebridToken),
|
||||||
allDebridUseWebLogin: Boolean(settings.allDebridUseWebLogin),
|
allDebridUseWebLogin: Boolean(settings.allDebridUseWebLogin),
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridPreferApi: true, bestToken: "", allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||||
@ -2841,6 +2841,7 @@ export function App(): ReactElement {
|
|||||||
<input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} />
|
<input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} />
|
||||||
<label>Mega-Debrid Passwort</label>
|
<label>Mega-Debrid Passwort</label>
|
||||||
<input type="password" value={settingsDraft.megaPassword} onChange={(e) => setText("megaPassword", e.target.value)} />
|
<input type="password" value={settingsDraft.megaPassword} onChange={(e) => setText("megaPassword", e.target.value)} />
|
||||||
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.megaDebridPreferApi} onChange={(e) => setBool("megaDebridPreferApi", e.target.checked)} /> Mega-Debrid bevorzugt über API (schneller, Fallback auf Web)</label>
|
||||||
<label>BestDebrid API Token</label>
|
<label>BestDebrid API Token</label>
|
||||||
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} />
|
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} />
|
||||||
<label>AllDebrid API Key</label>
|
<label>AllDebrid API Key</label>
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface AppSettings {
|
|||||||
realDebridUseWebLogin: boolean;
|
realDebridUseWebLogin: boolean;
|
||||||
megaLogin: string;
|
megaLogin: string;
|
||||||
megaPassword: string;
|
megaPassword: string;
|
||||||
|
megaDebridPreferApi: boolean;
|
||||||
bestToken: string;
|
bestToken: string;
|
||||||
allDebridToken: string;
|
allDebridToken: string;
|
||||||
allDebridUseWebLogin: boolean;
|
allDebridUseWebLogin: boolean;
|
||||||
|
|||||||
@ -397,11 +397,11 @@ describe("debrid service", () => {
|
|||||||
expect(realDebridWeb).not.toHaveBeenCalled();
|
expect(realDebridWeb).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats MegaDebrid as not configured when web fallback callback is unavailable", async () => {
|
it("treats MegaDebrid as not configured when no credentials are set", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
megaLogin: "user",
|
megaLogin: "",
|
||||||
megaPassword: "pass",
|
megaPassword: "",
|
||||||
providerPrimary: "megadebrid" as const,
|
providerPrimary: "megadebrid" as const,
|
||||||
providerSecondary: "none" as const,
|
providerSecondary: "none" as const,
|
||||||
providerTertiary: "none" as const,
|
providerTertiary: "none" as const,
|
||||||
@ -412,7 +412,7 @@ describe("debrid service", () => {
|
|||||||
await expect(service.unrestrictLink("https://rapidgator.net/file/missing-mega-web")).rejects.toThrow(/nicht konfiguriert/i);
|
await expect(service.unrestrictLink("https://rapidgator.net/file/missing-mega-web")).rejects.toThrow(/nicht konfiguriert/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Mega web path exclusively", async () => {
|
it("uses Mega web fallback when API fails", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
@ -426,6 +426,7 @@ describe("debrid service", () => {
|
|||||||
autoProviderFallback: true
|
autoProviderFallback: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// API returns 404 for connectUser → API fails, falls back to web
|
||||||
const fetchSpy = vi.fn(async () => new Response("not-found", { status: 404 }));
|
const fetchSpy = vi.fn(async () => new Response("not-found", { status: 404 }));
|
||||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
@ -441,7 +442,6 @@ describe("debrid service", () => {
|
|||||||
expect(result.provider).toBe("megadebrid");
|
expect(result.provider).toBe("megadebrid");
|
||||||
expect(result.directUrl).toContain("unrestrict.link/download/file/");
|
expect(result.directUrl).toContain("unrestrict.link/download/file/");
|
||||||
expect(megaWeb).toHaveBeenCalledTimes(1);
|
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aborts Mega web unrestrict when caller signal is cancelled", async () => {
|
it("aborts Mega web unrestrict when caller signal is cancelled", async () => {
|
||||||
@ -458,6 +458,9 @@ describe("debrid service", () => {
|
|||||||
autoProviderFallback: false
|
autoProviderFallback: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// API connect fails fast → falls through to web fallback
|
||||||
|
globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch;
|
||||||
|
|
||||||
const megaWeb = vi.fn((_link: string, signal?: AbortSignal): Promise<never> => new Promise((_, reject) => {
|
const megaWeb = vi.fn((_link: string, signal?: AbortSignal): Promise<never> => new Promise((_, reject) => {
|
||||||
const onAbort = (): void => reject(new Error("aborted:mega-web-test"));
|
const onAbort = (): void => reject(new Error("aborted:mega-web-test"));
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user