sponses from our host.
  this._localtimeOffsetMsec = 0;
};

HawkClient.prototype = {
  /**
   * Construct an error message for a response.  Private.
   *
   * @param restResponse
   *        A RESTResponse object from a RESTRequest
   *
   * @param error
   *        A string or object describing the error
   */
  _constructError(restResponse, error) {
    let errorObj = {
      error,
      // This object is likely to be JSON.stringify'd, but neither Error()
      // objects nor Components.Exception objects do the right thing there,
      // so we add a new element which is simply the .toString() version of
      // the error object, so it does appear in JSON'd values.
      errorString: error.toString(),
      message: restResponse.statusText,
      code: restResponse.status,
      errno: restResponse.status,
      toString() {
        return this.code + ": " + this.message;
      },
    };
    let retryAfter =
      restResponse.headers && restResponse.headers["retry-after"];
    retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
    if (retryAfter) {
      errorObj.retryAfter = retryAfter;
      // and notify observers of the retry interval
      if (this.observerPrefix) {
        Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter);
      }
    }
    return errorObj;
  },

  /**
   *
   * Update clock offset by determining difference from date gives in the (RFC
   * 1123) Date header of a server response.  Because HAWK tolerates a window
   * of one minute of clock skew (so two minutes total since the skew can be
   * positive or negative), the simple method of calculating offset here is
   * probably good enough.  We keep the value in milliseconds to make life
   * easier, even though the value will not have millisecond accuracy.
   *
   * @param dateString
   *        An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
   *
   * For HAWK clock skew and replay protection, see
   * https://github.com/hueniverse/hawk#replay-protection
   */
  _updateClockOffset(dateString) {
    try {
      let serverDateMsec = Date.parse(dateString);
      this._localtimeOffsetMsec = serverDateMsec - this.now();
      lazy.log.debug(
        "Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec
      );
    } catch (err) {
      lazy.log.warn("Bad date header in server response: " + dateString);
    }
  },

  /*
   * Get the current clock offset in milliseconds.
   *
   * The offset is the number of milliseconds that must be added to the client
   * clock to make it equal to the server clock.  For example, if the client is
   * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
   */
  get localtimeOffsetMsec() {
    return this._localtimeOffsetMsec;
  },

  /*
   * return current time in milliseconds
   */
  now() {
    return Date.now();
  },

  /**
   * A general method for sending raw RESTRequest calls authorized using HAWK.
   *
   * @param path
   *        API endpoint path
   * @param method
   *        The HTTP request method
   * @param credentials
   *        Hawk credentials
   * @param payloadObj
   *        An object that can be encodable as JSON as the payload of the
   *        request
   * @param extraHeaders
   *        An object with header/value pairs to send with the request.
   * @return Promise
   *        Returns a promise that resolves to the response of the API call,
   *        or is rejected with an error.  If the server response can be parsed
   *        as JSON and contains an 'error' property, the promise will be
   *        rejected with this JSON-parsed response.
   */
  async request(
    path,
    method,
    credentials = null,
    payloadObj = {},
    extraHeaders = {},
    retryOK = true
  ) {
    method = method.toLowerCase();

    let uri = this.host + path;

    let extra = {
      now: this.now(),
      localtimeOffsetMsec: this.localtimeOffsetMsec,
      headers: extraHeaders,
    };

    let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
    let error;
    let restResponse = await request[method](payloadObj).catch(e => {
      // Keep a reference to the error, log a message about it, and return the
      // response anyway.
      error = e;
      lazy.log.warn("hawk request error", error);
      return request.response;
    });

    // This shouldn't happen anymore, but it's not exactly difficult to handle.
    if (!restResponse) {
      throw error;
    }

    let status = restResponse.status;

    lazy.log.debug(
      "(Response) " +
        path +
        ": code: " +
        status +
        " - Status text: " +
        restResponse.statusText
    );
    if (lazy.logPII) {
      lazy.log.debug("Response text", restResponse.body);
    }

    // All responses may have backoff headers, which are a server-side safety
    // valve to allow slowing down clients without hurting performance.
    this._maybeNotifyBackoff(restResponse, "x-weave-backoff");
    this._maybeNotifyBackoff(restResponse, "x-backoff");

    if (error) {
      // When things really blow up, reconstruct an error object that follows
      // the general format of the server on error responses.
      throw this._constructError(restResponse, error);
    }

    this._updateClockOffset(restResponse.headers.date);

    if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
      // Retry once if we were rejected due to a bad timestamp.
      // Clock offset is adjusted already in the top of this function.
      lazy.log.debug("Received 401 for " + path + ": retrying");
      return this.request(
        path,
        method,
        credentials,
        payloadObj,
        extraHeaders,
        false
      );
    }

    // If the server returned a json error message, use it in the rejection
    // of the promise.
    //
    // In the case of a 401, in which we are probably being rejected for a
    // bad timestamp, retry exactly once, during which time clock offset will
    // be adjusted.

    let jsonResponse = {};
    try {
      jsonResponse = JSON.parse(restResponse.body);
    } catch (notJSON) {}

    let okResponse = 200 <= status && status < 300;
    if (!okResponse || jsonResponse.error) {
      if (jsonResponse.error) {
        throw jsonResponse;
      }
      throw this._constructError(restResponse, "Request failed");
    }

    // It's up to the caller to know how to decode the response.
    // We just return the whole response.
    return restResponse;
  },

  /*
   * The prefix used for all notifications sent by this module.  This
   * allows the handler of notifications to be sure they are handling
   * notifications for the service they expect.
   *
   * If not set, no notifications will be sent.
   */
  observerPrefix: null,

  // Given an optional header value, notify that a backoff has been requested.
  _maybeNotifyBackoff(response, headerName) {
    if (!this.observerPrefix || !response.headers) {
      return;
    }
    let headerVal = response.headers[headerName];
    if (!headerVal) {
      return;
    }
    let backoffInterval;
    try {
      backoffInterval = parseInt(headerVal, 10);
    } catch (ex) {
      lazy.log.error(
        "hawkclient response had invalid backoff value in '" +
          headerName +
          "' header: " +
          headerVal
      );
      return;
    }
    Observers.notify(
      this.observerPrefix + ":backoff:interval",
      backoffInterval
    );
  },

  // override points for testing.
  newHAWKAuthenticatedRESTRequest(uri, credentials, extra) {
    return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
  },
};
PK