Compare commits
No commits in common. "211e7e16cf99be59e1d757933e2aa36e893269bb" and "b414ab1773f6fc85a03a4115287d856721e65c46" have entirely different histories.
211e7e16cf
...
b414ab1773
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.7.167",
|
||||
"version": "1.7.166",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -1,22 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import type { RotationEvent } from "../shared/types";
|
||||
|
||||
/** Item-scoped sink: while a single item's link-unrestrict runs, the
|
||||
* download-manager wraps it in runWithRotationItemSink() so EVERY rotation
|
||||
* event for that item (Account 1 wird versucht, fehlgeschlagen, → Account 2)
|
||||
* lands in that item's own log — exactly where the user looks. AsyncLocalStorage
|
||||
* keeps this correct even with 8 items unrestricting in parallel: each runs in
|
||||
* its own async context, so events never cross-attribute. */
|
||||
export type RotationItemSink = (event: RotationEvent) => void;
|
||||
const rotationItemContext = new AsyncLocalStorage<RotationItemSink>();
|
||||
|
||||
/** Run `fn` with an item-scoped rotation sink active for its whole async chain. */
|
||||
export function runWithRotationItemSink<T>(sink: RotationItemSink, fn: () => Promise<T>): Promise<T> {
|
||||
return rotationItemContext.run(sink, fn);
|
||||
}
|
||||
|
||||
/** Dedicated log file for multi-account/key rotation events:
|
||||
* Mega-Debrid account selection, Debrid-Link key selection, per-attempt
|
||||
* test result, cooldown set, fallback to next account/key, etc.
|
||||
@ -60,6 +45,9 @@ function pushRotationEvent(
|
||||
fields?: Record<string, unknown>,
|
||||
at = Date.now()
|
||||
): void {
|
||||
if (!isUiRelevantRotationEvent(event)) {
|
||||
return;
|
||||
}
|
||||
rotationEventSeq += 1;
|
||||
const entry: RotationEvent = {
|
||||
id: `rot_${at}_${rotationEventSeq}`,
|
||||
@ -73,24 +61,6 @@ function pushRotationEvent(
|
||||
cooldownSec: fields && fields.cooldownSec != null ? Number(fields.cooldownSec) || 0 : undefined,
|
||||
next: fields && fields.next != null ? String(fields.next) : undefined
|
||||
};
|
||||
|
||||
// Always route to the item-scoped sink (if any) — the per-item log wants the
|
||||
// FULL trail including "TEST" (Account X wird versucht), so the user sees the
|
||||
// rotation right where they look.
|
||||
const itemSink = rotationItemContext.getStore();
|
||||
if (itemSink) {
|
||||
try {
|
||||
itemSink(entry);
|
||||
} catch {
|
||||
// never let item logging break the rotation flow
|
||||
}
|
||||
}
|
||||
|
||||
// The global UI panel ring + live push skip noisy per-attempt TEST markers;
|
||||
// it focuses on outcomes (OK / FAILED / FATAL / skips).
|
||||
if (!isUiRelevantRotationEvent(event)) {
|
||||
return;
|
||||
}
|
||||
rotationEventRing.push(entry);
|
||||
if (rotationEventRing.length > ROTATION_EVENT_RING_MAX) {
|
||||
rotationEventRing.splice(0, rotationEventRing.length - ROTATION_EVENT_RING_MAX);
|
||||
|
||||
@ -56,8 +56,7 @@ import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, Meg
|
||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
|
||||
import { validateFileAgainstManifest } from "./integrity";
|
||||
import { logger } from "./logger";
|
||||
import { getRecentRotationEvents, runWithRotationItemSink, setRotationEventListener } from "./account-rotation-log";
|
||||
import type { RotationEvent } from "../shared/types";
|
||||
import { getRecentRotationEvents, setRotationEventListener } from "./account-rotation-log";
|
||||
import { ensureItemLog, getItemLogPath as getPersistedItemLogPath, logItemEvent as writeItemLogEvent } from "./item-log";
|
||||
import { ensurePackageLog, getPackageLogPath as getPersistedPackageLogPath, logPackageEvent as writePackageLogEvent } from "./package-log";
|
||||
import { logRenameEvent as writeRenameLogEvent } from "./rename-log";
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { logAccountRotation, runWithRotationItemSink, getRecentRotationEvents } from "../src/main/account-rotation-log";
|
||||
import type { RotationEvent } from "../src/shared/types";
|
||||
|
||||
describe("rotation item-sink (AsyncLocalStorage)", () => {
|
||||
it("routes the FULL rotation trail (incl. TEST) to the active item sink", async () => {
|
||||
const captured: RotationEvent[] = [];
|
||||
await runWithRotationItemSink((ev) => captured.push(ev), async () => {
|
||||
logAccountRotation("INFO", "Mega-Debrid Web", "Account 1/3 (ab**xy)", "TEST", { link: "x" });
|
||||
logAccountRotation("WARN", "Mega-Debrid Web", "Account 1/3 (ab**xy)", "FAILED", { reason: "Timeout", cooldownSec: 30, next: "Account 2/3 (cd**zw)" });
|
||||
logAccountRotation("INFO", "Mega-Debrid Web", "Account 2/3 (cd**zw)", "TEST", { link: "x" });
|
||||
logAccountRotation("INFO", "Mega-Debrid Web", "Account 2/3 (cd**zw)", "OK", { fileName: "f.mkv" });
|
||||
// simulate an await boundary — ALS must survive it
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const events = captured.map((e) => e.event);
|
||||
expect(events).toEqual(["TEST", "FAILED", "TEST", "OK"]);
|
||||
const failed = captured.find((e) => e.event === "FAILED");
|
||||
expect(failed?.reason).toBe("Timeout");
|
||||
expect(failed?.next).toBe("Account 2/3 (cd**zw)");
|
||||
});
|
||||
|
||||
it("does not leak events to the sink outside the run() scope", () => {
|
||||
const captured: RotationEvent[] = [];
|
||||
// No active sink here
|
||||
logAccountRotation("INFO", "Debrid-Link", "Key 1/2 (k1)", "OK");
|
||||
expect(captured).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("isolates two parallel item sinks (no cross-attribution)", async () => {
|
||||
const a: RotationEvent[] = [];
|
||||
const b: RotationEvent[] = [];
|
||||
await Promise.all([
|
||||
runWithRotationItemSink((ev) => a.push(ev), async () => {
|
||||
logAccountRotation("INFO", "Mega-Debrid Web", "Account 1 (a)", "TEST");
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
logAccountRotation("INFO", "Mega-Debrid Web", "Account 1 (a)", "OK");
|
||||
}),
|
||||
runWithRotationItemSink((ev) => b.push(ev), async () => {
|
||||
logAccountRotation("INFO", "Debrid-Link", "Key 1 (b)", "TEST");
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
logAccountRotation("WARN", "Debrid-Link", "Key 1 (b)", "FAILED", { reason: "badToken" });
|
||||
})
|
||||
]);
|
||||
// Each sink only saw its own provider's events
|
||||
expect(a.every((e) => e.provider === "Mega-Debrid Web")).toBe(true);
|
||||
expect(b.every((e) => e.provider === "Debrid-Link")).toBe(true);
|
||||
expect(a.map((e) => e.event)).toEqual(["TEST", "OK"]);
|
||||
expect(b.map((e) => e.event)).toEqual(["TEST", "FAILED"]);
|
||||
});
|
||||
|
||||
it("still feeds the global UI ring (outcomes only, TEST filtered)", () => {
|
||||
logAccountRotation("INFO", "Mega-Debrid API", "Account 9 (zz)", "TEST");
|
||||
logAccountRotation("INFO", "Mega-Debrid API", "Account 9 (zz)", "OK", { fileName: "ring.mkv" });
|
||||
const ring = getRecentRotationEvents(10);
|
||||
// OK is in the ring; the TEST marker is filtered out of the panel
|
||||
expect(ring.some((e) => e.event === "OK" && e.accountLabel === "Account 9 (zz)")).toBe(true);
|
||||
expect(ring.some((e) => e.event === "TEST" && e.accountLabel === "Account 9 (zz)")).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user