Repository: hydralauncher/hydra
File: src/main/services/download/js-http-downloader.ts (TypeScript)

Code before:

    const { url, savePath, filename, headers = {} } = this.currentOptions;
    const { filePath, startByte, usedFallback } = this.prepareDownloadPath(
      savePath,
      filename,
      url
    );
    const requestHeaders = this.buildRequestHeaders(headers, startByte);

    this.startStallDetection();

    try {
      await this.executeDownload(
        url,
        requestHeaders,
        filePath,
        startByte,
        savePath,
        usedFallback
      );
    } catch (err) {
      await this.handleDownloadErrorWithRetry(err as Error);
    } finally {
      this.isDownloading = false;
      this.stopStallDetection();
      this.cleanup();
    }
  }

  private startStallDetection(): void {
    this.stopStallDetection();
    this.stallCheckInterval = setInterval(() => {
      if (this.status !== "active" || this.isPaused || this.isStallRetry)
        return;

      const timeSinceLastData = Date.now() - this.lastDataReceivedAt;
      if (timeSinceLastData > STALL_TIMEOUT_MS) {
        logger.log(
          `[JsHttpDownloader] Download stalled (no data for ${Math.round(timeSinceLastData / 1000)}s), triggering retry`
        );
        this.triggerRetry();
      }
    }, 5000);
  }

  private stopStallDetection(): void {
    if (this.stallCheckInterval) {
      clearInterval(this.stallCheckInterval);
      this.stallCheckInterval = null;
    }
  }

Review comment (suggestion):
blocking: calling `cleanup()` in this `finally` looks like it resets state (e.g., `status`/`isPaused`) after success or user pause. Consider splitting cleanup into (1) resource cleanup (streams/abort/interval) vs (2) state reset, and avoid resetting `status`/pause flags from the `finally` path.

Code after:
      this.abortController = new AbortController();
      this.status = "active";
      this.isDownloading = true;
      this.isStallRetry = false;
      this.lastDataReceivedAt = Date.now();

      const { url, savePath, filename, headers = {} } = this.currentOptions;
      const { filePath, startByte, usedFallback } = this.prepareDownloadPath(
        savePath,
        filename,
        url
      );
      const requestHeaders = this.buildRequestHeaders(headers, startByte);

      this.startStallDetection();

      try {
        await this.executeDownload(
          url,
          requestHeaders,
          filePath,
          startByte,
          savePath,
          usedFallback
        );
        break;
      } catch (err) {
        const shouldRetry = await this.handleDownloadErrorWithRetry(
          err as Error
        );
        if (!shouldRetry) {
          break;
        }
      } finally {
        this.stopStallDetection();
        this.cleanupResources();
      }
    }

    this.isDownloading = false;
  }

  private startStallDetection(): void {
    this.stopStallDetection();
    this.stallCheckInterval = setInterval(() => {
      if (this.status !== "active" || this.isPaused || this.isStallRetry) {
        return;
      }

      const timeSinceLastData = Date.now() - this.lastDataReceivedAt;
      if (timeSinceLastData > STALL_TIMEOUT_MS) {