/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This module is used to interact with the Windows BITS component (Background
 * Intelligent Transfer Service). This functionality cannot be used unless on
 * Windows.
 *
 * The reason for this file's existence is that the interfaces in nsIBits.idl
 * are asynchronous, but are unable to use Promises because they are implemented
 * in Rust, which does not yet support Promises. This file functions as a layer
 * between the Rust and the JS that provides access to the functionality
 * provided by nsIBits via Promises rather than callbacks.
 */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

// This conditional prevents errors if this file is imported from operating
// systems other than Windows. This is purely for convenient importing, because
// attempting to use anything in this file on platforms other than Windows will
// result in an error.
if (AppConstants.MOZ_BITS_DOWNLOAD) {
  XPCOMUtils.defineLazyServiceGetter(
    lazy,
    "gBits",
    "@mozilla.org/bits;1",
    Ci.nsIBits
  );
}

// This value exists to mitigate a very unlikely problem: If a BITS method
// catastrophically fails, it may never call its callback. This would result in
// methods in this file returning promises that never resolve. This could, in
// turn, lead to download code hanging altogether rather than being able to
// report errors and utilize fallback mechanisms.
// This problem is mitigated by giving these promises a timeout, the length of
// which will be determined by this value.
const kBitsMethodTimeoutMs = 10 * 60 * 1000; // 10 minutes

/**
 * This class will wrap the errors returned by the nsIBits interface to make
 * them more uniform and more easily consumable.
 *
 * The values of stored by this error type are entirely numeric. This should
 * make them easier to consume with JS and telemetry, but does make them fairly
 * unreadable. nsIBits.idl will need to be referenced to look up what errors
 * the values correspond to.
 *
 * The type of BitsError.code is dependent on the value of BitsError.codeType.
 * It may be null, a number (corresponding to an nsresult or hresult value),
 * a string, or an exception.
 */
export class BitsError extends Error {
  // If codeType == "none", code may be unspecified.
  constructor(type, action, stage, codeType, code) {
    let message =
      `${BitsError.name} {type: ${type}, action: ${action}, ` +
      `stage: ${stage}`;
    switch (codeType) {
      case lazy.gBits.ERROR_CODE_TYPE_NONE:
        code = null;
        message += ", codeType: none}";
        break;
      case lazy.gBits.ERROR_CODE_TYPE_NSRESULT:
        message += `, codeType: nsresult, code: ${code}}`;
        break;
      case lazy.gBits.ERROR_CODE_TYPE_HRESULT:
        message += `, codeType: hresult, code: 0x${BigInt(code >>> 0).toString(
          16
        )}`;
        break;
      case lazy.gBits.ERROR_CODE_TYPE_STRING:
        message += `, codeType: string, code: ${JSON.stringify(code)}}`;
        break;
      case lazy.gBits.ERROR_CODE_TYPE_EXCEPTION:
        message += `, codeType: exception, code: ${code}}`;
        break;
      default:
        message += ", codeType: invalid}";
        break;
    }
    super(message);

    this.type = type;
    this.action = action;
    this.stage = stage;
    this.codeType = codeType;
    this.code = code;
    this.name = this.constructor.name;
    this.succeeded = false;
  }
}

// These specializations exist to make them easier to construct since they may
// need to be constructed outside of this file.
export class BitsVerificationError extends BitsError {
  constructor() {
    super(
      Ci.nsIBits.ERROR_TYPE_VERIFICATION_FAILURE,
      Ci.nsIBits.ERROR_ACTION_NONE,
      Ci.nsIBits.ERROR_STAGE_VERIFICATION,
      Ci.nsIBits.ERROR_CODE_TYPE_NONE
    );
  }
}

export class BitsUnknownError extends BitsError {
  constructor() {
    super(
      Ci.nsIBits.ERROR_TYPE_UNKNOWN,
      Ci.nsIBits.ERROR_ACTION_UNKNOWN,
      Ci.nsIBits.ERROR_STAGE_UNKNOWN,
      Ci.nsIBits.ERROR_CODE_TYPE_NONE
    );
  }
}

/**
 * Returns a timer object. If the timer expires, reject will be called with
 * a BitsError error. The timer's cancel method should be called if the promise
 * resolves or rejects without the timeout expiring.
 */
function makeTimeout(reject, errorAction) {
  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(
    () => {
      let error = new BitsError(
        lazy.gBits.ERROR_TYPE_METHOD_TIMEOUT,
        errorAction,
        lazy.gBits.ERROR_STAGE_UNKNOWN,
        lazy.gBits.ERROR_CODE_TYPE_NONE
      );
      reject(error);
    },
    kBitsMethodTimeoutMs,
    Ci.nsITimer.TYPE_ONE_SHOT
  );
  return timer;
}

/**
 * This function does all of the wrapping and error handling for an async
 * BitsRequest method. This allows the implementations for those methods to
 * simply call this function with a closure that executes appropriate
 * nsIBitsRequest method.
 *
 * Specifically, this function takes an nsBitsErrorAction and a function.
 * The nsBitsErrorAction will be used when constructing a BitsError, if the
 * wrapper encounters an error.
 * The function will be passed the callback function that should be passed to
 * the nsIBitsRequest method.
 */
async function requestPromise(errorAction, actionFn) {
  return new Promise((resolve, reject) => {
    let timer = makeTimeout(reject, errorAction);

    let callback = {
      QueryInterface: ChromeUtils.generateQI(["nsIBitsCallback"]),
      success() {
        timer.cancel();
        resolve();
      },
      failure(type, action, stage) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_NONE
        );
        reject(error);
      },
      failureNsresult(type, action, stage, code) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_NSRESULT,
          code
        );
        reject(error);
      },
      failureHresult(type, action, stage, code) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_HRESULT,
          code
        );
        reject(error);
      },
      failureString(type, action, stage, message) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_STRING,
          message
        );
        reject(error);
      },
    };

    try {
      actionFn(callback);
    } catch (e) {
      let error = new BitsError(
        lazy.gBits.ERROR_TYPE_METHOD_THREW,
        errorAction,
        lazy.gBits.ERROR_STAGE_PRETASK,
        lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
        e
      );
      reject(error);
    }
  });
}

/**
 * This class is a wrapper around nsIBitsRequest that converts functions taking
 * callbacks to asynchronous functions. This class implements nsIRequest.
 *
 * Note that once the request has been shutdown, calling methods on it will
 * cause an exception to be thrown. The request will be shutdown automatically
 * when the BITS job is successfully completed or cancelled. If the request is
 * no longer needed, but the transfer is still in progress, the shutdown method
 * should be called manually to prevent memory leaks.
 * Getter methods (except loadGroup and loadFlags) should continue to be
 * accessible, even after shutdown.
 */
export class BitsRequest {
  constructor(request) {
    this._request = request;
    this._request.QueryInterface(Ci.nsIBitsRequest);
  }

  /**
   * This function releases the Rust request that backs this wrapper. Calling
   * any method on this request after calling release will result in a BitsError
   * being thrown.
   *
   * This step is important, because otherwise a cycle exists that the cycle
   * collector doesn't know about. To break this cycle, either the Rust request
   * needs to let go of the observer, or the JS request wrapper needs to let go
   * of the Rust request (which is what we do here).
   *
   * Once there is support for cycle collection of cycles that extend through
   * Rust, this function may no longer be necessary.
   */
  shutdown() {
    if (this.hasShutdown) {
      return;
    }
    // Cache some values before we shut down so they are still available
    this._name = this._request.name;
    this._status = this._request.status;
    this._bitsId = this._request.bitsId;
    this._transferError = this._request.transferError;
    this._transferErrorCode = this._request.transferErrorCode;

    this._request = null;
  }

  /**
   * Allows consumers to determine if this request has been shutdown.
   */
  get hasShutdown() {
    return !this._request;
  }

  /**
   * This is the nsIRequest implementation. Since this._request is an
   * nsIRequest, these functions just call the corresponding method on it.
   *
   * Note that nsIBitsRequest does not yet properly implement load groups or
   * load flags. This class will still forward those calls, but they will have
   * not succeed.
   */
  get name() {
    if (!this._request) {
      return this._name;
    }
    return this._request.name;
  }
  isPending() {
    if (!this._request) {
      return false;
    }
    return this._request.isPending();
  }
  get status() {
    if (!this._request) {
      return this._status;
    }
    return this._request.status;
  }
  cancel(status) {
    return this.cancelAsync(status);
  }
  suspend() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_SUSPEND,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    return this._request.suspend();
  }
  resume() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_RESUME,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    return this._request.resume();
  }
  get loadGroup() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_NONE,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    return this._request.loadGroup;
  }
  set loadGroup(group) {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_NONE,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    this._request.loadGroup = group;
  }
  get loadFlags() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_NONE,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    return this._request.loadFlags;
  }
  set loadFlags(flags) {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_NONE,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    this._request.loadFlags = flags;
  }

  /**
   * This function wraps nsIBitsRequest::bitsId.
   */
  get bitsId() {
    if (!this._request) {
      return this._bitsId;
    }
    return this._request.bitsId;
  }

  /**
   * This function wraps nsIBitsRequest::transferError.
   *
   * Instead of simply returning the nsBitsErrorType value, however, it returns
   * a BitsError object, or null.
   */
  get transferError() {
    let bitsErrorType;
    let bitsErrorCode;
    if (this._request) {
      bitsErrorType = this._request.transferError;
      bitsErrorCode = this._request.transferErrorCode;
    } else {
      bitsErrorType = this._transferError;
      bitsErrorCode = this._transferErrorCode;
    }

    if (bitsErrorType == Ci.nsIBits.ERROR_TYPE_SUCCESS) {
      return null;
    }

    let bitsErrorCodeType;
    if (bitsErrorCode) {
      bitsErrorCodeType = Ci.nsIBits.ERROR_CODE_TYPE_HRESULT;
    } else {
      bitsErrorCodeType = Ci.nsIBits.ERROR_CODE_TYPE_NONE;
    }

    return new BitsError(
      bitsErrorType,
      Ci.nsIBits.ERROR_ACTION_NONE,
      Ci.nsIBits.ERROR_STAGE_MONITOR,
      bitsErrorCodeType,
      bitsErrorCode
    );
  }

  /**
   * This function wraps nsIBitsRequest::changeMonitorInterval.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async changeMonitorInterval(monitorIntervalMs) {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_CHANGE_MONITOR_INTERVAL;
    return requestPromise(action, callback => {
      this._request.changeMonitorInterval(monitorIntervalMs, callback);
    });
  }

  /**
   * This function wraps nsIBitsRequest::cancelAsync.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   *
   * Adds a default status of NS_ERROR_ABORT if one is not provided.
   */
  async cancelAsync(status) {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_CANCEL,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    if (status === undefined) {
      status = Cr.NS_ERROR_ABORT;
    }
    let action = lazy.gBits.ERROR_ACTION_CANCEL;
    return requestPromise(action, callback => {
      this._request.cancelAsync(status, callback);
    }).then(() => this.shutdown());
  }

  /**
   * This function wraps nsIBitsRequest::setPriorityHigh.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async setPriorityHigh() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_SET_PRIORITY,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY;
    return requestPromise(action, callback => {
      this._request.setPriorityHigh(callback);
    });
  }

  /**
   * This function wraps nsIBitsRequest::setPriorityLow.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async setPriorityLow() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_SET_PRIORITY,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_SET_PRIORITY;
    return requestPromise(action, callback => {
      this._request.setPriorityLow(callback);
    });
  }

  /**
   * This function wraps nsIBitsRequest::setNoProgressTimeout.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async setNoProgressTimeout(timeoutSecs) {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_SET_NO_PROGRESS_TIMEOUT;
    return requestPromise(action, callback => {
      this._request.setNoProgressTimeout(timeoutSecs, callback);
    });
  }

  /**
   * This function wraps nsIBitsRequest::complete.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async complete() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_COMPLETE,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_COMPLETE;
    return requestPromise(action, callback => {
      this._request.complete(callback);
    }).then(() => this.shutdown());
  }

  /**
   * This function wraps nsIBitsRequest::suspendAsync.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async suspendAsync() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_SUSPEND,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_SUSPEND;
    return requestPromise(action, callback => {
      this._request.suspendAsync(callback);
    });
  }

  /**
   * This function wraps nsIBitsRequest::resumeAsync.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with no data, or rejects with a BitsError.
   */
  async resumeAsync() {
    if (!this._request) {
      throw new BitsError(
        Ci.nsIBits.ERROR_TYPE_USE_AFTER_REQUEST_SHUTDOWN,
        Ci.nsIBits.ERROR_ACTION_RESUME,
        Ci.nsIBits.ERROR_STAGE_PRETASK,
        Ci.nsIBits.ERROR_CODE_TYPE_NONE
      );
    }
    let action = lazy.gBits.ERROR_ACTION_RESUME;
    return requestPromise(action, callback => {
      this._request.resumeAsync(callback);
    });
  }
}

BitsRequest.prototype.QueryInterface = ChromeUtils.generateQI(["nsIRequest"]);

/**
 * This function does all of the wrapping and error handling for an async
 * Bits Service method. This allows the implementations for those methods to
 * simply call this function with a closure that executes appropriate
 * nsIBits method.
 *
 * Specifically, this function takes an nsBitsErrorAction, an optional observer,
 * and a function.
 * The nsBitsErrorAction will be used when constructing a BitsError, if the
 * wrapper encounters an error.
 * The observer should be the one that the caller passed to the Bits Interface
 * method. It will be wrapped so that its methods are passed a BitsRequest
 * rather than an nsIBitsRequest.
 * The function will be passed the callback function and the wrapped observer,
 * both of which should be passed to the nsIBitsRequest method.
 */
async function servicePromise(errorAction, observer, actionFn) {
  return new Promise((resolve, reject) => {
    let isProgressEventSink = false;
    if (observer) {
      try {
        observer.QueryInterface(Ci.nsIRequestObserver);
      } catch (e) {
        let error = new BitsError(
          lazy.gBits.ERROR_TYPE_INVALID_ARGUMENT,
          errorAction,
          lazy.gBits.ERROR_STAGE_PRETASK,
          lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
          e
        );
        reject(error);
        return;
      }
      try {
        observer.QueryInterface(Ci.nsIProgressEventSink);
        isProgressEventSink = true;
      } catch (e) {}
    }

    // Check if we are not late in creating new requests.
    if (
      Services &&
      Services.startup &&
      Services.startup.isInOrBeyondShutdownPhase(
        Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
      )
    ) {
      let error = new BitsError(
        lazy.gBits.ERROR_TYPE_BROWSER_SHUTTING_DOWN,
        errorAction,
        lazy.gBits.ERROR_STAGE_PRETASK,
        lazy.gBits.ERROR_CODE_TYPE_NONE
      );
      reject(error);
      return;
    }

    // This will be set to the BitsRequest (wrapping the nsIBitsRequest), once
    // it is available. This prevents a new wrapper from having to be made every
    // time an observer function is called.
    let wrappedRequest;

    let wrappedObserver = {
      onStartRequest: function wrappedObserver_onStartRequest(request) {
        if (observer) {
          if (!wrappedRequest) {
            wrappedRequest = new BitsRequest(request);
          }
          observer.onStartRequest(wrappedRequest);
        }
      },
      onStopRequest: function wrappedObserver_onStopRequest(request, status) {
        if (observer) {
          if (!wrappedRequest) {
            wrappedRequest = new BitsRequest(request);
          }
          observer.onStopRequest(wrappedRequest, status);
        }
      },
      onProgress: function wrappedObserver_onProgress(
        request,
        progress,
        progressMax
      ) {
        if (observer && isProgressEventSink) {
          if (!wrappedRequest) {
            wrappedRequest = new BitsRequest(request);
          }
          observer.onProgress(wrappedRequest, progress, progressMax);
        }
      },
      onStatus: function wrappedObserver_onStatus(request, status, statusArg) {
        if (observer && isProgressEventSink) {
          if (!wrappedRequest) {
            wrappedRequest = new BitsRequest(request);
          }
          observer.onStatus(wrappedRequest, status, statusArg);
        }
      },
      QueryInterface: ChromeUtils.generateQI([
        "nsIRequestObserver",
        "nsIProgressEventSink",
      ]),
    };

    let timer = makeTimeout(reject, errorAction);
    let callback = {
      QueryInterface: ChromeUtils.generateQI(["nsIBitsNewRequestCallback"]),
      success(request) {
        timer.cancel();
        if (!wrappedRequest) {
          wrappedRequest = new BitsRequest(request);
        }
        resolve(wrappedRequest);
      },
      failure(type, action, stage) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_NONE
        );
        reject(error);
      },
      failureNsresult(type, action, stage, code) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_NSRESULT,
          code
        );
        reject(error);
      },
      failureHresult(type, action, stage, code) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_HRESULT,
          code
        );
        reject(error);
      },
      failureString(type, action, stage, message) {
        timer.cancel();
        let error = new BitsError(
          type,
          action,
          stage,
          lazy.gBits.ERROR_CODE_TYPE_STRING,
          message
        );
        reject(error);
      },
    };

    try {
      actionFn(wrappedObserver, callback);
    } catch (e) {
      let error = new BitsError(
        lazy.gBits.ERROR_TYPE_METHOD_THREW,
        errorAction,
        lazy.gBits.ERROR_STAGE_PRETASK,
        lazy.gBits.ERROR_CODE_TYPE_EXCEPTION,
        e
      );
      reject(error);
    }
  });
}

export var Bits = {
  /**
   * This function wraps nsIBits::initialized.
   */
  get initialized() {
    return lazy.gBits.initialized;
  },

  /**
   * This function wraps nsIBits::init.
   */
  init(jobName, savePathPrefix, monitorTimeoutMs) {
    return lazy.gBits.init(jobName, savePathPrefix, monitorTimeoutMs);
  },

  /**
   * This function wraps nsIBits::startDownload.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with a BitsRequest (which is also an
   * nsIRequest), or rejects with a BitsError.
   */
  async startDownload(
    downloadURL,
    saveRelPath,
    proxy,
    noProgressTimeoutSecs,
    monitorIntervalMs,
    customHeaders,
    observer,
    context
  ) {
    let action = lazy.gBits.ERROR_ACTION_START_DOWNLOAD;
    return servicePromise(action, observer, (wrappedObserver, callback) => {
      lazy.gBits.startDownload(
        downloadURL,
        saveRelPath,
        proxy,
        noProgressTimeoutSecs,
        monitorIntervalMs,
        customHeaders,
        wrappedObserver,
        context,
        callback
      );
    });
  },

  /**
   * This function wraps nsIBits::monitorDownload.
   *
   * Instead of taking a callback, the function is asynchronous.
   * This method either resolves with a BitsRequest (which is also an
   * nsIRequest), or rejects with a BitsError.
   */
  async monitorDownload(id, monitorIntervalMs, observer, context) {
    let action = lazy.gBits.ERROR_ACTION_MONITOR_DOWNLOAD;
    return servicePromise(action, observer, (wrappedObserver, callback) => {
      lazy.gBits.monitorDownload(
        id,
        monitorIntervalMs,
        wrappedObserver,
        context,
        callback
      );
    });
  },
};
