• v2.0.0-beta.2 ab08506361

    v2.0.0-beta.2 Stable

    Administrator released this 2026-03-08 19:10:03 +01:00 | 6 commits to main since this release

    v2.0.0-beta.2 — Code Review Bugfixes + Separate Beta Identity

    Critical Bugfixes (6)

    • Post-processor: double attempts increment — Both onProgress and onArchiveFailure callbacks incremented archiveState.attempts++, so each extraction failure consumed 2 attempts against maxAttempts=3. Effectively reduced retry budget from 3 to 1. Fixed by only incrementing in onArchiveFailure (the authoritative failure path).

    • Post-processor: slot leak on abort — When the abort signal fired after acquireSlot() but before the try/finally block, the early return skipped releaseSlot(). Each leaked slot permanently reduced extraction parallelism. Fixed by releasing the slot before the early return.

    • Scheduler: global watchdog high-water mark never decreases — After a stall event triggered download retries, bytesAtHeartbeat reset to 0 but lastGlobalProgressBytes stayed at the old peak. Since totalBytes < lastGlobalProgressBytes was permanently true, the watchdog fired every cycle until downloads exceeded the old peak. Fixed by resetting the high-water mark alongside the timestamp after emitting global-stall.

    • Pipeline + Download-manager: path traversal in isPathInsideDir — path.resolve("C:\downloads\pack-evil").startsWith(path.resolve("C:\downloads\pack")) returned true because no trailing separator was enforced. An attacker-controlled filename could escape the intended directory. Fixed by appending path.sep to the directory path before the startsWith check.

    • Retry-manager: per-kind exhaustion bypassed by shelving — The shelve threshold check (totalFailures ≥ 15) ran before the per-kind exhaustion check (kindCount > maxRetries). Items that exhausted a specific error kind (e.g., 4× NetworkReset with maxRetries=3) would get shelved instead of permanently failed, halving counters and allowing infinite retries. Fixed by checking per-kind exhaustion first.

    • Retry-manager: infinite shelve cycling — No cap on shelveCount, so items with mixed error kinds could shelve indefinitely (every 15 failures: halve counters → accumulate 15 more → shelve again → forever). Added MAX_SHELVE_COUNT = 5 — after 5 shelve cycles, the item fails permanently.

    Important Bugfixes (10)

    • Scheduler: stale retryDelays and providerCooldowns on start() — Previous run's retry delays and provider cooldowns persisted into the next run, causing items to appear delayed or providers to appear cooled down when they shouldn't be. Fixed by clearing both maps in start().

    • Scheduler: repeated stall-detected events — checkStalls() re-emitted stall-detected every 2 seconds for slots already being aborted (abortReason ≠ "none"). Fixed by skipping slots with abortReason !== "none".

    • Download-manager: cleanupAfterExtraction wrong directory — removeDownloadLinkArtifacts() was called with pkg.extractDir (extraction output) instead of pkg.outputDir (download directory where .lnk files actually are). Link artifacts were never cleaned up.

    • Download-manager: normalizeSessionStatuses missing "extracting" — On app restart, packages stuck in "extracting" status weren't reset to "queued". They appeared stuck in the UI with no way to restart them.

    • Download-manager: stop() leaves stale activeTasks entries — this.activeTasks map was never cleared on stop, leaving ghost references to aborted slots. Fixed by adding this.activeTasks.clear() after aborting all slots.

    • Download-manager: cachedDirectUrls memory leak — After a successful download, the direct URL was deleted then immediately re-inserted with the same item ID key. Since completed items never reuse cached URLs, the map grew unbounded. Removed the re-insertion.

    • Stream-writer: duplicate truncation code — The same fs.promises.truncate(effectiveTargetPath, written) block appeared twice in the error path (lines 542-544 and 548-549). Removed the first duplicate.

    • Stream-writer: 5-minute drain wait in finally after error — alignedFlush(true) in the finally block called waitDrain() even when bodyError was already set, potentially blocking for up to 5 minutes on a stuck stream. Fixed by skipping alignedFlush when an error already exists.

    • Stream-writer: speed limiter stale elapsed after sleep — After the speed limiter's await sleep(), the elapsed variable still held the pre-sleep value. The window reset check (elapsed >= 1) used stale data, preventing the speed window from resetting and causing progressively longer sleeps. Fixed by re-reading nowMs() after sleep.

    • Error-classifier: missing HTTP 401 and 410 classification — HTTP 401 (Unauthorized) fell through to Unknown instead of Forbidden. HTTP 410 (Gone) fell through to Unknown instead of NotFound. Added explicit cases for both status codes.

    Separate Beta Identity

    • appId changed to com.sucukdeluxe.realdebrid-beta
    • productName changed to Real-Debrid-Downloader Beta
    • package name changed to real-debrid-downloader-beta
    • Installs to separate directory (%APPDATA%/Real-Debrid-Downloader Beta/)
    • Stable and Beta can run side by side without interfering

    Testing

    • 216 unit tests passing (1 new test added for per-kind exhaustion priority)
    • Existing tests updated to match new shelve/kind-exhaustion priority order and HTTP 401 classification
    • Build compiles cleanly (tsup + vite)
    Downloads