tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

kinto-http-client.sys.mjs (101834B)


      1 /*
      2 *
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *     http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 *
     15 * This file is generated from kinto.js - do not modify directly.
     16 */
     17 
     18 /* eslint @typescript-eslint/no-unused-vars: off */
     19 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
     20 
     21 /*
     22 * Version 17.0.0 - f643998
     23 */
     24 
     25 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
     26 
     27 /******************************************************************************
     28 Copyright (c) Microsoft Corporation.
     29 
     30 Permission to use, copy, modify, and/or distribute this software for any
     31 purpose with or without fee is hereby granted.
     32 
     33 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
     34 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
     35 AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
     36 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
     37 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
     38 OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
     39 PERFORMANCE OF THIS SOFTWARE.
     40 ***************************************************************************** */
     41 /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
     42 
     43 function __decorate(decorators, target, key, desc) {
     44  var c = arguments.length,
     45    r =
     46      c < 3
     47        ? target
     48        : desc === null
     49          ? (desc = Object.getOwnPropertyDescriptor(target, key))
     50          : desc,
     51    d;
     52  if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
     53    r = Reflect.decorate(decorators, target, key, desc);
     54  else
     55    for (var i = decorators.length - 1; i >= 0; i--)
     56      if ((d = decorators[i]))
     57        r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
     58  return (c > 3 && r && Object.defineProperty(target, key, r), r);
     59 }
     60 
     61 typeof SuppressedError === "function"
     62  ? SuppressedError
     63  : function (error, suppressed, message) {
     64      var e = new Error(message);
     65      return (
     66        (e.name = "SuppressedError"),
     67        (e.error = error),
     68        (e.suppressed = suppressed),
     69        e
     70      );
     71    };
     72 
     73 /**
     74 * Chunks an array into n pieces.
     75 *
     76 * @private
     77 * @param  {Array}  array
     78 * @param  {Number} n
     79 * @return {Array}
     80 */
     81 function partition(array, n) {
     82  if (n <= 0) {
     83    return [array];
     84  }
     85  return array.reduce((acc, x, i) => {
     86    if (i === 0 || i % n === 0) {
     87      acc.push([x]);
     88    } else {
     89      acc[acc.length - 1].push(x);
     90    }
     91    return acc;
     92  }, []);
     93 }
     94 /**
     95 * Returns a Promise always resolving after the specified amount in milliseconds.
     96 *
     97 * @return Promise<void>
     98 */
     99 function delay(ms) {
    100  return new Promise((resolve) => setTimeout(resolve, ms));
    101 }
    102 /**
    103 * Always returns a resource data object from the provided argument.
    104 *
    105 * @private
    106 * @param  {Object|String} resource
    107 * @return {Object}
    108 */
    109 function toDataBody(resource) {
    110  if (isObject(resource)) {
    111    return resource;
    112  }
    113  if (typeof resource === "string") {
    114    return { id: resource };
    115  }
    116  throw new Error("Invalid argument.");
    117 }
    118 /**
    119 * Transforms an object into an URL query string, stripping out any undefined
    120 * values.
    121 *
    122 * @param  {Object} obj
    123 * @return {String}
    124 */
    125 function qsify(obj) {
    126  const encode = (v) =>
    127    encodeURIComponent(typeof v === "boolean" ? String(v) : v);
    128  const stripped = cleanUndefinedProperties(obj);
    129  return Object.keys(stripped)
    130    .map((k) => {
    131      const ks = encode(k) + "=";
    132      if (Array.isArray(stripped[k])) {
    133        return ks + stripped[k].map((v) => encode(v)).join(",");
    134      }
    135      return ks + encode(stripped[k]);
    136    })
    137    .join("&");
    138 }
    139 /**
    140 * Checks if a version is within the provided range.
    141 *
    142 * @param  {String} version    The version to check.
    143 * @param  {String} minVersion The minimum supported version (inclusive).
    144 * @param  {String} maxVersion The minimum supported version (exclusive).
    145 * @throws {Error} If the version is outside of the provided range.
    146 */
    147 function checkVersion(version, minVersion, maxVersion) {
    148  const extract = (str) => str.split(".").map((x) => parseInt(x, 10));
    149  const [verMajor, verMinor] = extract(version);
    150  const [minMajor, minMinor] = extract(minVersion);
    151  const [maxMajor, maxMinor] = extract(maxVersion);
    152  const checks = [
    153    verMajor < minMajor,
    154    verMajor === minMajor && verMinor < minMinor,
    155    verMajor > maxMajor,
    156    verMajor === maxMajor && verMinor >= maxMinor,
    157  ];
    158  if (checks.some((x) => x)) {
    159    throw new Error(
    160      `Version ${version} doesn't satisfy ${minVersion} <= x < ${maxVersion}`
    161    );
    162  }
    163 }
    164 /**
    165 * Generates a decorator function ensuring a version check is performed against
    166 * the provided requirements before executing it.
    167 *
    168 * @param  {String} min The required min version (inclusive).
    169 * @param  {String} max The required max version (inclusive).
    170 * @return {Function}
    171 */
    172 function support(min, max) {
    173  return function (
    174    // @ts-ignore
    175    target,
    176    key,
    177    descriptor
    178  ) {
    179    const fn = descriptor.value;
    180    return {
    181      configurable: true,
    182      get() {
    183        const wrappedMethod = (...args) => {
    184          // "this" is the current instance which its method is decorated.
    185          const client = this.client ? this.client : this;
    186          return client
    187            .fetchHTTPApiVersion()
    188            .then((version) => checkVersion(version, min, max))
    189            .then(() => fn.apply(this, args));
    190        };
    191        Object.defineProperty(this, key, {
    192          value: wrappedMethod,
    193          configurable: true,
    194          writable: true,
    195        });
    196        return wrappedMethod;
    197      },
    198    };
    199  };
    200 }
    201 /**
    202 * Generates a decorator function ensuring that the specified capabilities are
    203 * available on the server before executing it.
    204 *
    205 * @param  {Array<String>} capabilities The required capabilities.
    206 * @return {Function}
    207 */
    208 function capable(capabilities) {
    209  return function (
    210    // @ts-ignore
    211    target,
    212    key,
    213    descriptor
    214  ) {
    215    const fn = descriptor.value;
    216    return {
    217      configurable: true,
    218      get() {
    219        const wrappedMethod = (...args) => {
    220          // "this" is the current instance which its method is decorated.
    221          const client = this.client ? this.client : this;
    222          return client
    223            .fetchServerCapabilities()
    224            .then((available) => {
    225              const missing = capabilities.filter((c) => !(c in available));
    226              if (missing.length) {
    227                const missingStr = missing.join(", ");
    228                throw new Error(
    229                  `Required capabilities ${missingStr} not present on server`
    230                );
    231              }
    232            })
    233            .then(() => fn.apply(this, args));
    234        };
    235        Object.defineProperty(this, key, {
    236          value: wrappedMethod,
    237          configurable: true,
    238          writable: true,
    239        });
    240        return wrappedMethod;
    241      },
    242    };
    243  };
    244 }
    245 /**
    246 * Generates a decorator function ensuring an operation is not performed from
    247 * within a batch request.
    248 *
    249 * @param  {String} message The error message to throw.
    250 * @return {Function}
    251 */
    252 function nobatch(message) {
    253  return function (
    254    // @ts-ignore
    255    target,
    256    key,
    257    descriptor
    258  ) {
    259    const fn = descriptor.value;
    260    return {
    261      configurable: true,
    262      get() {
    263        const wrappedMethod = (...args) => {
    264          // "this" is the current instance which its method is decorated.
    265          if (this._isBatch) {
    266            throw new Error(message);
    267          }
    268          return fn.apply(this, args);
    269        };
    270        Object.defineProperty(this, key, {
    271          value: wrappedMethod,
    272          configurable: true,
    273          writable: true,
    274        });
    275        return wrappedMethod;
    276      },
    277    };
    278  };
    279 }
    280 /**
    281 * Returns true if the specified value is an object (i.e. not an array nor null).
    282 * @param  {Object} thing The value to inspect.
    283 * @return {bool}
    284 */
    285 function isObject(thing) {
    286  return typeof thing === "object" && thing !== null && !Array.isArray(thing);
    287 }
    288 /**
    289 * Parses a data url.
    290 * @param  {String} dataURL The data url.
    291 * @return {Object}
    292 */
    293 function parseDataURL(dataURL) {
    294  const regex = /^data:(.*);base64,(.*)/;
    295  const match = dataURL.match(regex);
    296  if (!match) {
    297    throw new Error(`Invalid data-url: ${String(dataURL).substring(0, 32)}...`);
    298  }
    299  const props = match[1];
    300  const base64 = match[2];
    301  const [type, ...rawParams] = props.split(";");
    302  const params = rawParams.reduce((acc, param) => {
    303    const [key, value] = param.split("=");
    304    return { ...acc, [key]: value };
    305  }, {});
    306  return { ...params, type, base64 };
    307 }
    308 /**
    309 * Extracts file information from a data url.
    310 * @param  {String} dataURL The data url.
    311 * @return {Object}
    312 */
    313 function extractFileInfo(dataURL) {
    314  const { name, type, base64 } = parseDataURL(dataURL);
    315  const binary = atob(base64);
    316  const array = [];
    317  for (let i = 0; i < binary.length; i++) {
    318    array.push(binary.charCodeAt(i));
    319  }
    320  const blob = new Blob([new Uint8Array(array)], { type });
    321  return { blob, name };
    322 }
    323 /**
    324 * Creates a FormData instance from a data url and an existing JSON response
    325 * body.
    326 * @param  {String} dataURL            The data url.
    327 * @param  {Object} body               The response body.
    328 * @param  {Object} [options={}]       The options object.
    329 * @param  {Object} [options.filename] Force attachment file name.
    330 * @return {FormData}
    331 */
    332 function createFormData(dataURL, body, options = {}) {
    333  const { filename = "untitled" } = options;
    334  const { blob, name } = extractFileInfo(dataURL);
    335  const formData = new FormData();
    336  formData.append("attachment", blob, name || filename);
    337  for (const property in body) {
    338    if (typeof body[property] !== "undefined") {
    339      formData.append(property, JSON.stringify(body[property]));
    340    }
    341  }
    342  return formData;
    343 }
    344 /**
    345 * Clones an object with all its undefined keys removed.
    346 * @private
    347 */
    348 function cleanUndefinedProperties(obj) {
    349  const result = {};
    350  for (const key in obj) {
    351    if (typeof obj[key] !== "undefined") {
    352      result[key] = obj[key];
    353    }
    354  }
    355  return result;
    356 }
    357 /**
    358 * Handle common query parameters for Kinto requests.
    359 *
    360 * @param  {String}  [path]  The endpoint base path.
    361 * @param  {Array}   [options.fields]    Fields to limit the
    362 *   request to.
    363 * @param  {Object}  [options.query={}]  Additional query arguments.
    364 */
    365 function addEndpointOptions(path, options = {}) {
    366  const query = { ...options.query };
    367  if (options.fields) {
    368    query._fields = options.fields;
    369  }
    370  const queryString = qsify(query);
    371  if (queryString) {
    372    return path + "?" + queryString;
    373  }
    374  return path;
    375 }
    376 /**
    377 * Replace authorization header with an obscured version
    378 */
    379 function obscureAuthorizationHeader(headers) {
    380  const h = new Headers(headers);
    381  if (h.has("authorization")) {
    382    h.set("authorization", "**** (suppressed)");
    383  }
    384  const obscuredHeaders = {};
    385  for (const [header, value] of h.entries()) {
    386    obscuredHeaders[header] = value;
    387  }
    388  return obscuredHeaders;
    389 }
    390 
    391 /**
    392 * Kinto server error code descriptors.
    393 */
    394 const ERROR_CODES = {
    395  104: "Missing Authorization Token",
    396  105: "Invalid Authorization Token",
    397  106: "Request body was not valid JSON",
    398  107: "Invalid request parameter",
    399  108: "Missing request parameter",
    400  109: "Invalid posted data",
    401  110: "Invalid Token / id",
    402  111: "Missing Token / id",
    403  112: "Content-Length header was not provided",
    404  113: "Request body too large",
    405  114: "Resource was created, updated or deleted meanwhile",
    406  115: "Method not allowed on this end point (hint: server may be readonly)",
    407  116: "Requested version not available on this server",
    408  117: "Client has sent too many requests",
    409  121: "Resource access is forbidden for this user",
    410  122: "Another resource violates constraint",
    411  201: "Service Temporary unavailable due to high load",
    412  202: "Service deprecated",
    413  999: "Internal Server Error",
    414 };
    415 class NetworkTimeoutError extends Error {
    416  constructor(url, options) {
    417    super(
    418      `Timeout while trying to access ${url} with ${JSON.stringify(options)}`
    419    );
    420    if (Error.captureStackTrace) {
    421      Error.captureStackTrace(this, NetworkTimeoutError);
    422    }
    423    this.url = url;
    424    this.options = options;
    425  }
    426 }
    427 class UnparseableResponseError extends Error {
    428  constructor(response, body, error) {
    429    const { status } = response;
    430    super(
    431      `Response from server unparseable (HTTP ${status || 0}; ${error}): ${body}`
    432    );
    433    if (Error.captureStackTrace) {
    434      Error.captureStackTrace(this, UnparseableResponseError);
    435    }
    436    this.status = status;
    437    this.response = response;
    438    this.stack = error.stack;
    439    this.error = error;
    440  }
    441 }
    442 /**
    443 * "Error" subclass representing a >=400 response from the server.
    444 *
    445 * Whether or not this is an error depends on your application.
    446 *
    447 * The `json` field can be undefined if the server responded with an
    448 * empty response body. This shouldn't generally happen. Most "bad"
    449 * responses come with a JSON error description, or (if they're
    450 * fronted by a CDN or nginx or something) occasionally non-JSON
    451 * responses (which become UnparseableResponseErrors, above).
    452 */
    453 class ServerResponse extends Error {
    454  constructor(response, json) {
    455    const { status } = response;
    456    let { statusText } = response;
    457    let errnoMsg;
    458    if (json) {
    459      // Try to fill in information from the JSON error.
    460      statusText = json.error || statusText;
    461      // Take errnoMsg from either ERROR_CODES or json.message.
    462      if (json.errno && json.errno in ERROR_CODES) {
    463        errnoMsg = ERROR_CODES[json.errno];
    464      } else if (json.message) {
    465        errnoMsg = json.message;
    466      }
    467      // If we had both ERROR_CODES and json.message, and they differ,
    468      // combine them.
    469      if (errnoMsg && json.message && json.message !== errnoMsg) {
    470        errnoMsg += ` (${json.message})`;
    471      }
    472    }
    473    let message = `HTTP ${status} ${statusText}`;
    474    if (errnoMsg) {
    475      message += `: ${errnoMsg}`;
    476    }
    477    super(message.trim());
    478    if (Error.captureStackTrace) {
    479      Error.captureStackTrace(this, ServerResponse);
    480    }
    481    this.response = response;
    482    this.data = json;
    483  }
    484 }
    485 
    486 var errors = /*#__PURE__*/ Object.freeze({
    487  __proto__: null,
    488  NetworkTimeoutError,
    489  ServerResponse,
    490  UnparseableResponseError,
    491  default: ERROR_CODES,
    492 });
    493 
    494 /**
    495 * Enhanced HTTP client for the Kinto protocol.
    496 * @private
    497 */
    498 class HTTP {
    499  /**
    500   * Default HTTP request headers applied to each outgoing request.
    501   *
    502   * @type {Object}
    503   */
    504  static get DEFAULT_REQUEST_HEADERS() {
    505    return {
    506      Accept: "application/json",
    507      "Content-Type": "application/json",
    508    };
    509  }
    510  /**
    511   * Default options.
    512   *
    513   * @type {Object}
    514   */
    515  static get defaultOptions() {
    516    return { timeout: null, requestMode: "cors" };
    517  }
    518  /**
    519   * Constructor.
    520   *
    521   * @param {EventEmitter} events                       The event handler.
    522   * @param {Object}       [options={}}                 The options object.
    523   * @param {Number}       [options.timeout=null]       The request timeout in ms, if any (default: `null`).
    524   * @param {String}       [options.requestMode="cors"] The HTTP request mode (default: `"cors"`).
    525   */
    526  constructor(events, options = {}) {
    527    // public properties
    528    /**
    529     * The event emitter instance.
    530     * @type {EventEmitter}
    531     */
    532    this.events = events;
    533    /**
    534     * The request mode.
    535     * @see  https://fetch.spec.whatwg.org/#requestmode
    536     * @type {String}
    537     */
    538    this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode;
    539    /**
    540     * The request timeout.
    541     * @type {Number}
    542     */
    543    this.timeout = options.timeout || HTTP.defaultOptions.timeout;
    544    /**
    545     * The fetch() function.
    546     * @type {Function}
    547     */
    548    this.fetchFunc = options.fetchFunc || globalThis.fetch.bind(globalThis);
    549  }
    550  /**
    551   * @private
    552   */
    553  timedFetch(url, options) {
    554    let hasTimedout = false;
    555    return new Promise((resolve, reject) => {
    556      // Detect if a request has timed out.
    557      let _timeoutId;
    558      if (this.timeout) {
    559        _timeoutId = setTimeout(() => {
    560          hasTimedout = true;
    561          if (options && options.headers) {
    562            options = {
    563              ...options,
    564              headers: obscureAuthorizationHeader(options.headers),
    565            };
    566          }
    567          reject(new NetworkTimeoutError(url, options));
    568        }, this.timeout);
    569      }
    570      function proceedWithHandler(fn) {
    571        return (arg) => {
    572          if (!hasTimedout) {
    573            if (_timeoutId) {
    574              clearTimeout(_timeoutId);
    575            }
    576            fn(arg);
    577          }
    578        };
    579      }
    580      this.fetchFunc(url, options)
    581        .then(proceedWithHandler(resolve))
    582        .catch(proceedWithHandler(reject));
    583    });
    584  }
    585  /**
    586   * @private
    587   */
    588  async processResponse(response) {
    589    const { status, headers } = response;
    590    const text = await response.text();
    591    // Check if we have a body; if so parse it as JSON.
    592    let json;
    593    if (text.length !== 0) {
    594      try {
    595        json = JSON.parse(text);
    596      } catch (err) {
    597        throw new UnparseableResponseError(response, text, err);
    598      }
    599    }
    600    if (status >= 400) {
    601      throw new ServerResponse(response, json);
    602    }
    603    return { status, json: json, headers };
    604  }
    605  /**
    606   * @private
    607   */
    608  async retry(url, retryAfter, request, options) {
    609    await delay(retryAfter);
    610    return this.request(url, request, {
    611      ...options,
    612      retry: options.retry - 1,
    613    });
    614  }
    615  /**
    616   * Performs an HTTP request to the Kinto server.
    617   *
    618   * Resolves with an objet containing the following HTTP response properties:
    619   * - `{Number}  status`  The HTTP status code.
    620   * - `{Object}  json`    The JSON response body.
    621   * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
    622   *
    623   * @param  {String} url               The URL.
    624   * @param  {Object} [request={}]      The request object, passed to
    625   *     fetch() as its options object.
    626   * @param  {Object} [request.headers] The request headers object (default: {})
    627   * @param  {Object} [options={}]      Options for making the
    628   *     request
    629   * @param  {Number} [options.retry]   Number of retries (default: 0)
    630   * @return {Promise}
    631   */
    632  async request(url, request = { headers: {} }, options = { retry: 0 }) {
    633    // Ensure default request headers are always set
    634    request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers };
    635    // If a multipart body is provided, remove any custom Content-Type header as
    636    // the fetch() implementation will add the correct one for us.
    637    if (request.body && request.body instanceof FormData) {
    638      if (request.headers instanceof Headers) {
    639        request.headers.delete("Content-Type");
    640      } else if (!Array.isArray(request.headers)) {
    641        delete request.headers["Content-Type"];
    642      }
    643    }
    644    request.mode = this.requestMode;
    645    const response = await this.timedFetch(url, request);
    646    const { headers } = response;
    647    this._checkForDeprecationHeader(headers);
    648    this._checkForBackoffHeader(headers);
    649    // Check if the server summons the client to retry after a while.
    650    const retryAfter = this._checkForRetryAfterHeader(headers);
    651    // If number of allowed of retries is not exhausted, retry the same request.
    652    if (retryAfter && options.retry > 0) {
    653      return this.retry(url, retryAfter, request, options);
    654    }
    655    return this.processResponse(response);
    656  }
    657  _checkForDeprecationHeader(headers) {
    658    const alertHeader = headers.get("Alert");
    659    if (!alertHeader) {
    660      return;
    661    }
    662    let alert;
    663    try {
    664      alert = JSON.parse(alertHeader);
    665    } catch (err) {
    666      console.warn("Unable to parse Alert header message", alertHeader);
    667      return;
    668    }
    669    console.warn(alert.message, alert.url);
    670    if (this.events) {
    671      this.events.emit("deprecated", alert);
    672    }
    673  }
    674  _checkForBackoffHeader(headers) {
    675    let backoffMs;
    676    const backoffHeader = headers.get("Backoff");
    677    const backoffSeconds = backoffHeader ? parseInt(backoffHeader, 10) : 0;
    678    if (backoffSeconds > 0) {
    679      backoffMs = new Date().getTime() + backoffSeconds * 1000;
    680    } else {
    681      backoffMs = 0;
    682    }
    683    if (this.events) {
    684      this.events.emit("backoff", backoffMs);
    685    }
    686  }
    687  _checkForRetryAfterHeader(headers) {
    688    const retryAfter = headers.get("Retry-After");
    689    if (!retryAfter) {
    690      return null;
    691    }
    692    const delay = parseInt(retryAfter, 10) * 1000;
    693    const tryAgainAfter = new Date().getTime() + delay;
    694    if (this.events) {
    695      this.events.emit("retry-after", tryAgainAfter);
    696    }
    697    return delay;
    698  }
    699 }
    700 
    701 /**
    702 * Endpoints templates.
    703 * @type {Object}
    704 */
    705 const ENDPOINTS = {
    706  root: () => "/",
    707  batch: () => "/batch",
    708  permissions: () => "/permissions",
    709  bucket: (bucket) => "/buckets" + (bucket ? `/${bucket}` : ""),
    710  history: (bucket) => `${ENDPOINTS.bucket(bucket)}/history`,
    711  collection: (bucket, coll) =>
    712    `${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""),
    713  group: (bucket, group) =>
    714    `${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""),
    715  record: (bucket, coll, id) =>
    716    `${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""),
    717  attachment: (bucket, coll, id) =>
    718    `${ENDPOINTS.record(bucket, coll, id)}/attachment`,
    719 };
    720 
    721 const requestDefaults = {
    722  safe: false,
    723  // check if we should set default content type here
    724  headers: {},
    725  patch: false,
    726 };
    727 /**
    728 * @private
    729 */
    730 function safeHeader(safe, last_modified) {
    731  if (!safe) {
    732    return {};
    733  }
    734  if (last_modified) {
    735    return { "If-Match": `"${last_modified}"` };
    736  }
    737  return { "If-None-Match": "*" };
    738 }
    739 /**
    740 * @private
    741 */
    742 function createRequest(path, { data, permissions }, options = {}) {
    743  const { headers, safe } = {
    744    ...requestDefaults,
    745    ...options,
    746  };
    747  const method = options.method || (data && data.id) ? "PUT" : "POST";
    748  return {
    749    method,
    750    path,
    751    headers: { ...headers, ...safeHeader(safe) },
    752    body: { data, permissions },
    753  };
    754 }
    755 /**
    756 * @private
    757 */
    758 function updateRequest(path, { data, permissions }, options = {}) {
    759  const { headers, safe, patch } = { ...requestDefaults, ...options };
    760  const { last_modified } = { ...data, ...options };
    761  const hasNoData =
    762    data &&
    763    Object.keys(data).filter((k) => k !== "id" && k !== "last_modified")
    764      .length === 0;
    765  if (hasNoData) {
    766    data = undefined;
    767  }
    768  return {
    769    method: patch ? "PATCH" : "PUT",
    770    path,
    771    headers: { ...headers, ...safeHeader(safe, last_modified) },
    772    body: { data, permissions },
    773  };
    774 }
    775 /**
    776 * @private
    777 */
    778 function jsonPatchPermissionsRequest(path, permissions, opType, options = {}) {
    779  const { headers, safe, last_modified } = { ...requestDefaults, ...options };
    780  const ops = [];
    781  for (const [type, principals] of Object.entries(permissions)) {
    782    if (principals) {
    783      for (const principal of principals) {
    784        ops.push({
    785          op: opType,
    786          path: `/permissions/${type}/${principal}`,
    787        });
    788      }
    789    }
    790  }
    791  return {
    792    method: "PATCH",
    793    path,
    794    headers: {
    795      ...headers,
    796      ...safeHeader(safe, last_modified),
    797      "Content-Type": "application/json-patch+json",
    798    },
    799    body: ops,
    800  };
    801 }
    802 /**
    803 * @private
    804 */
    805 function deleteRequest(path, options = {}) {
    806  const { headers, safe, last_modified } = {
    807    ...requestDefaults,
    808    ...options,
    809  };
    810  if (safe && !last_modified) {
    811    throw new Error("Safe concurrency check requires a last_modified value.");
    812  }
    813  return {
    814    method: "DELETE",
    815    path,
    816    headers: { ...headers, ...safeHeader(safe, last_modified) },
    817  };
    818 }
    819 /**
    820 * @private
    821 */
    822 function addAttachmentRequest(
    823  path,
    824  dataURI,
    825  { data, permissions } = {},
    826  options = {}
    827 ) {
    828  const { headers, safe } = { ...requestDefaults, ...options };
    829  const { last_modified } = { ...data, ...options };
    830  const body = { data, permissions };
    831  const formData = createFormData(dataURI, body, options);
    832  return {
    833    method: "POST",
    834    path,
    835    headers: { ...headers, ...safeHeader(safe, last_modified) },
    836    body: formData,
    837  };
    838 }
    839 
    840 /**
    841 * Exports batch responses as a result object.
    842 *
    843 * @private
    844 * @param  {Array} responses The batch subrequest responses.
    845 * @param  {Array} requests  The initial issued requests.
    846 * @return {Object}
    847 */
    848 function aggregate(responses = [], requests = []) {
    849  if (responses.length !== requests.length) {
    850    throw new Error("Responses length should match requests one.");
    851  }
    852  const results = {
    853    errors: [],
    854    published: [],
    855    conflicts: [],
    856    skipped: [],
    857  };
    858  return responses.reduce((acc, response, index) => {
    859    const { status } = response;
    860    const request = requests[index];
    861    if (status >= 200 && status < 400) {
    862      acc.published.push(response.body);
    863    } else if (status === 404) {
    864      // Extract the id manually from request path while waiting for Kinto/kinto#818
    865      const regex = /(buckets|groups|collections|records)\/([^/]+)$/;
    866      const extracts = request.path.match(regex);
    867      const id = extracts && extracts.length === 3 ? extracts[2] : undefined;
    868      acc.skipped.push({
    869        id,
    870        path: request.path,
    871        error: response.body,
    872      });
    873    } else if (status === 412) {
    874      acc.conflicts.push({
    875        // XXX: specifying the type is probably superfluous
    876        type: "outgoing",
    877        local: request.body,
    878        remote:
    879          (response.body.details && response.body.details.existing) || null,
    880      });
    881    } else {
    882      acc.errors.push({
    883        path: request.path,
    884        sent: request,
    885        error: response.body,
    886      });
    887    }
    888    return acc;
    889  }, results);
    890 }
    891 
    892 const byteToHex = [];
    893 for (let i = 0; i < 256; ++i) {
    894  byteToHex.push((i + 0x100).toString(16).slice(1));
    895 }
    896 function unsafeStringify(arr, offset = 0) {
    897  return (
    898    byteToHex[arr[offset + 0]] +
    899    byteToHex[arr[offset + 1]] +
    900    byteToHex[arr[offset + 2]] +
    901    byteToHex[arr[offset + 3]] +
    902    "-" +
    903    byteToHex[arr[offset + 4]] +
    904    byteToHex[arr[offset + 5]] +
    905    "-" +
    906    byteToHex[arr[offset + 6]] +
    907    byteToHex[arr[offset + 7]] +
    908    "-" +
    909    byteToHex[arr[offset + 8]] +
    910    byteToHex[arr[offset + 9]] +
    911    "-" +
    912    byteToHex[arr[offset + 10]] +
    913    byteToHex[arr[offset + 11]] +
    914    byteToHex[arr[offset + 12]] +
    915    byteToHex[arr[offset + 13]] +
    916    byteToHex[arr[offset + 14]] +
    917    byteToHex[arr[offset + 15]]
    918  ).toLowerCase();
    919 }
    920 
    921 let getRandomValues;
    922 const rnds8 = new Uint8Array(16);
    923 function rng() {
    924  if (!getRandomValues) {
    925    if (typeof crypto === "undefined" || !crypto.getRandomValues) {
    926      throw new Error(
    927        "crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"
    928      );
    929    }
    930    getRandomValues = crypto.getRandomValues.bind(crypto);
    931  }
    932  return getRandomValues(rnds8);
    933 }
    934 
    935 const randomUUID =
    936  typeof crypto !== "undefined" &&
    937  crypto.randomUUID &&
    938  crypto.randomUUID.bind(crypto);
    939 var native = { randomUUID };
    940 
    941 function _v4(options, buf, offset) {
    942  options = options || {};
    943  const rnds = options.random ?? options.rng?.() ?? rng();
    944  if (rnds.length < 16) {
    945    throw new Error("Random bytes length must be >= 16");
    946  }
    947  rnds[6] = (rnds[6] & 0x0f) | 0x40;
    948  rnds[8] = (rnds[8] & 0x3f) | 0x80;
    949  return unsafeStringify(rnds);
    950 }
    951 function v4(options, buf, offset) {
    952  if (native.randomUUID && true && !options) {
    953    return native.randomUUID();
    954  }
    955  return _v4(options);
    956 }
    957 
    958 /**
    959 * Abstract representation of a selected collection.
    960 *
    961 */
    962 class Collection {
    963  /**
    964   * Constructor.
    965   *
    966   * @param  {KintoClient}  client            The client instance.
    967   * @param  {Bucket}       bucket            The bucket instance.
    968   * @param  {String}       name              The collection name.
    969   * @param  {Object}       [options={}]      The options object.
    970   * @param  {Object}       [options.headers] The headers object option.
    971   * @param  {Boolean}      [options.safe]    The safe option.
    972   * @param  {Number}       [options.retry]   The retry option.
    973   * @param  {Boolean}      [options.batch]   (Private) Whether this
    974   *     Collection is operating as part of a batch.
    975   */
    976  constructor(client, bucket, name, options = {}) {
    977    /**
    978     * @ignore
    979     */
    980    this.client = client;
    981    /**
    982     * @ignore
    983     */
    984    this.bucket = bucket;
    985    /**
    986     * The collection name.
    987     * @type {String}
    988     */
    989    this.name = name;
    990    this._endpoints = client.endpoints;
    991    /**
    992     * @ignore
    993     */
    994    this._retry = options.retry || 0;
    995    this._safe = !!options.safe;
    996    // FIXME: This is kind of ugly; shouldn't the bucket be responsible
    997    // for doing the merge?
    998    this._headers = {
    999      ...this.bucket.headers,
   1000      ...options.headers,
   1001    };
   1002  }
   1003  get execute() {
   1004    return this.client.execute.bind(this.client);
   1005  }
   1006  /**
   1007   * Get the value of "headers" for a given request, merging the
   1008   * per-request headers with our own "default" headers.
   1009   *
   1010   * @private
   1011   */
   1012  _getHeaders(options) {
   1013    return {
   1014      ...this._headers,
   1015      ...options.headers,
   1016    };
   1017  }
   1018  /**
   1019   * Get the value of "safe" for a given request, using the
   1020   * per-request option if present or falling back to our default
   1021   * otherwise.
   1022   *
   1023   * @private
   1024   * @param {Object} options The options for a request.
   1025   * @returns {Boolean}
   1026   */
   1027  _getSafe(options) {
   1028    return { safe: this._safe, ...options }.safe;
   1029  }
   1030  /**
   1031   * As _getSafe, but for "retry".
   1032   *
   1033   * @private
   1034   */
   1035  _getRetry(options) {
   1036    return { retry: this._retry, ...options }.retry;
   1037  }
   1038  /**
   1039   * Retrieves the total number of records in this collection.
   1040   *
   1041   * @param  {Object} [options={}]      The options object.
   1042   * @param  {Object} [options.headers] The headers object option.
   1043   * @param  {Number} [options.retry=0] Number of retries to make
   1044   *     when faced with transient errors.
   1045   * @return {Promise<Number, Error>}
   1046   */
   1047  async getTotalRecords(options = {}) {
   1048    const path = this._endpoints.record(this.bucket.name, this.name);
   1049    const request = {
   1050      headers: this._getHeaders(options),
   1051      path,
   1052      method: "HEAD",
   1053    };
   1054    const { headers } = await this.client.execute(request, {
   1055      raw: true,
   1056      retry: this._getRetry(options),
   1057    });
   1058    return parseInt(headers.get("Total-Records"), 10);
   1059  }
   1060  /**
   1061   * Retrieves the ETag of the records list, for use with the `since` filtering option.
   1062   *
   1063   * @param  {Object} [options={}]      The options object.
   1064   * @param  {Object} [options.headers] The headers object option.
   1065   * @param  {Number} [options.retry=0] Number of retries to make
   1066   *     when faced with transient errors.
   1067   * @return {Promise<String, Error>}
   1068   */
   1069  async getRecordsTimestamp(options = {}) {
   1070    const path = this._endpoints.record(this.bucket.name, this.name);
   1071    const request = {
   1072      headers: this._getHeaders(options),
   1073      path,
   1074      method: "HEAD",
   1075    };
   1076    const { headers } = await this.client.execute(request, {
   1077      raw: true,
   1078      retry: this._getRetry(options),
   1079    });
   1080    return headers.get("ETag");
   1081  }
   1082  /**
   1083   * Retrieves collection data.
   1084   *
   1085   * @param  {Object} [options={}]      The options object.
   1086   * @param  {Object} [options.headers] The headers object option.
   1087   * @param  {Object} [options.query]   Query parameters to pass in
   1088   *     the request. This might be useful for features that aren't
   1089   *     yet supported by this library.
   1090   * @param  {Array}  [options.fields]  Limit response to
   1091   *     just some fields.
   1092   * @param  {Number} [options.retry=0] Number of retries to make
   1093   *     when faced with transient errors.
   1094   * @return {Promise<Object, Error>}
   1095   */
   1096  async getData(options = {}) {
   1097    const path = this._endpoints.collection(this.bucket.name, this.name);
   1098    const request = { headers: this._getHeaders(options), path };
   1099    const { data } = await this.client.execute(request, {
   1100      retry: this._getRetry(options),
   1101      query: options.query,
   1102      fields: options.fields,
   1103    });
   1104    return data;
   1105  }
   1106  /**
   1107   * Set collection data.
   1108   * @param  {Object}   data                    The collection data object.
   1109   * @param  {Object}   [options={}]            The options object.
   1110   * @param  {Object}   [options.headers]       The headers object option.
   1111   * @param  {Number}   [options.retry=0]       Number of retries to make
   1112   *     when faced with transient errors.
   1113   * @param  {Boolean}  [options.safe]          The safe option.
   1114   * @param  {Boolean}  [options.patch]         The patch option.
   1115   * @param  {Number}   [options.last_modified] The last_modified option.
   1116   * @return {Promise<Object, Error>}
   1117   */
   1118  async setData(data, options = {}) {
   1119    if (!isObject(data)) {
   1120      throw new Error("A collection object is required.");
   1121    }
   1122    const { patch, permissions } = options;
   1123    const { last_modified } = { ...data, ...options };
   1124    const path = this._endpoints.collection(this.bucket.name, this.name);
   1125    const request = updateRequest(
   1126      path,
   1127      { data, permissions },
   1128      {
   1129        last_modified,
   1130        patch,
   1131        headers: this._getHeaders(options),
   1132        safe: this._getSafe(options),
   1133      }
   1134    );
   1135    return this.client.execute(request, {
   1136      retry: this._getRetry(options),
   1137    });
   1138  }
   1139  /**
   1140   * Retrieves the list of permissions for this collection.
   1141   *
   1142   * @param  {Object} [options={}]      The options object.
   1143   * @param  {Object} [options.headers] The headers object option.
   1144   * @param  {Number} [options.retry=0] Number of retries to make
   1145   *     when faced with transient errors.
   1146   * @return {Promise<Object, Error>}
   1147   */
   1148  async getPermissions(options = {}) {
   1149    const path = this._endpoints.collection(this.bucket.name, this.name);
   1150    const request = { headers: this._getHeaders(options), path };
   1151    const { permissions } = await this.client.execute(request, {
   1152      retry: this._getRetry(options),
   1153    });
   1154    return permissions;
   1155  }
   1156  /**
   1157   * Replaces all existing collection permissions with the ones provided.
   1158   *
   1159   * @param  {Object}   permissions             The permissions object.
   1160   * @param  {Object}   [options={}]            The options object
   1161   * @param  {Object}   [options.headers]       The headers object option.
   1162   * @param  {Number}   [options.retry=0]       Number of retries to make
   1163   *     when faced with transient errors.
   1164   * @param  {Boolean}  [options.safe]          The safe option.
   1165   * @param  {Number}   [options.last_modified] The last_modified option.
   1166   * @return {Promise<Object, Error>}
   1167   */
   1168  async setPermissions(permissions, options = {}) {
   1169    if (!isObject(permissions)) {
   1170      throw new Error("A permissions object is required.");
   1171    }
   1172    const path = this._endpoints.collection(this.bucket.name, this.name);
   1173    const data = { last_modified: options.last_modified };
   1174    const request = updateRequest(
   1175      path,
   1176      { data, permissions },
   1177      {
   1178        headers: this._getHeaders(options),
   1179        safe: this._getSafe(options),
   1180      }
   1181    );
   1182    return this.client.execute(request, {
   1183      retry: this._getRetry(options),
   1184    });
   1185  }
   1186  /**
   1187   * Append principals to the collection permissions.
   1188   *
   1189   * @param  {Object}  permissions             The permissions object.
   1190   * @param  {Object}  [options={}]            The options object
   1191   * @param  {Boolean} [options.safe]          The safe option.
   1192   * @param  {Object}  [options.headers]       The headers object option.
   1193   * @param  {Number}  [options.retry=0]       Number of retries to make
   1194   *     when faced with transient errors.
   1195   * @param  {Object}  [options.last_modified] The last_modified option.
   1196   * @return {Promise<Object, Error>}
   1197   */
   1198  async addPermissions(permissions, options = {}) {
   1199    if (!isObject(permissions)) {
   1200      throw new Error("A permissions object is required.");
   1201    }
   1202    const path = this._endpoints.collection(this.bucket.name, this.name);
   1203    const { last_modified } = options;
   1204    const request = jsonPatchPermissionsRequest(path, permissions, "add", {
   1205      last_modified,
   1206      headers: this._getHeaders(options),
   1207      safe: this._getSafe(options),
   1208    });
   1209    return this.client.execute(request, {
   1210      retry: this._getRetry(options),
   1211    });
   1212  }
   1213  /**
   1214   * Remove principals from the collection permissions.
   1215   *
   1216   * @param  {Object}  permissions             The permissions object.
   1217   * @param  {Object}  [options={}]            The options object
   1218   * @param  {Boolean} [options.safe]          The safe option.
   1219   * @param  {Object}  [options.headers]       The headers object option.
   1220   * @param  {Number}  [options.retry=0]       Number of retries to make
   1221   *     when faced with transient errors.
   1222   * @param  {Object}  [options.last_modified] The last_modified option.
   1223   * @return {Promise<Object, Error>}
   1224   */
   1225  async removePermissions(permissions, options = {}) {
   1226    if (!isObject(permissions)) {
   1227      throw new Error("A permissions object is required.");
   1228    }
   1229    const path = this._endpoints.collection(this.bucket.name, this.name);
   1230    const { last_modified } = options;
   1231    const request = jsonPatchPermissionsRequest(path, permissions, "remove", {
   1232      last_modified,
   1233      headers: this._getHeaders(options),
   1234      safe: this._getSafe(options),
   1235    });
   1236    return this.client.execute(request, {
   1237      retry: this._getRetry(options),
   1238    });
   1239  }
   1240  /**
   1241   * Creates a record in current collection.
   1242   *
   1243   * @param  {Object}  record                The record to create.
   1244   * @param  {Object}  [options={}]          The options object.
   1245   * @param  {Object}  [options.headers]     The headers object option.
   1246   * @param  {Number}  [options.retry=0]     Number of retries to make
   1247   *     when faced with transient errors.
   1248   * @param  {Boolean} [options.safe]        The safe option.
   1249   * @param  {Object}  [options.permissions] The permissions option.
   1250   * @return {Promise<Object, Error>}
   1251   */
   1252  async createRecord(record, options = {}) {
   1253    const { permissions } = options;
   1254    const path = this._endpoints.record(this.bucket.name, this.name, record.id);
   1255    const request = createRequest(
   1256      path,
   1257      { data: record, permissions },
   1258      {
   1259        headers: this._getHeaders(options),
   1260        safe: this._getSafe(options),
   1261      }
   1262    );
   1263    return this.client.execute(request, {
   1264      retry: this._getRetry(options),
   1265    });
   1266  }
   1267  /**
   1268   * Adds an attachment to a record, creating the record when it doesn't exist.
   1269   *
   1270   * @param  {String}  dataURL                 The data url.
   1271   * @param  {Object}  [record={}]             The record data.
   1272   * @param  {Object}  [options={}]            The options object.
   1273   * @param  {Object}  [options.headers]       The headers object option.
   1274   * @param  {Number}  [options.retry=0]       Number of retries to make
   1275   *     when faced with transient errors.
   1276   * @param  {Boolean} [options.safe]          The safe option.
   1277   * @param  {Number}  [options.last_modified] The last_modified option.
   1278   * @param  {Object}  [options.permissions]   The permissions option.
   1279   * @param  {String}  [options.filename]      Force the attachment filename.
   1280   * @return {Promise<Object, Error>}
   1281   */
   1282  async addAttachment(dataURI, record = {}, options = {}) {
   1283    const { permissions } = options;
   1284    const id = record.id || v4();
   1285    const path = this._endpoints.attachment(this.bucket.name, this.name, id);
   1286    const { last_modified } = { ...record, ...options };
   1287    const addAttachmentRequest$1 = addAttachmentRequest(
   1288      path,
   1289      dataURI,
   1290      { data: record, permissions },
   1291      {
   1292        last_modified,
   1293        filename: options.filename,
   1294        headers: this._getHeaders(options),
   1295        safe: this._getSafe(options),
   1296      }
   1297    );
   1298    await this.client.execute(addAttachmentRequest$1, {
   1299      stringify: false,
   1300      retry: this._getRetry(options),
   1301    });
   1302    return this.getRecord(id);
   1303  }
   1304  /**
   1305   * Removes an attachment from a given record.
   1306   *
   1307   * @param  {Object}  recordId                The record id.
   1308   * @param  {Object}  [options={}]            The options object.
   1309   * @param  {Object}  [options.headers]       The headers object option.
   1310   * @param  {Number}  [options.retry=0]       Number of retries to make
   1311   *     when faced with transient errors.
   1312   * @param  {Boolean} [options.safe]          The safe option.
   1313   * @param  {Number}  [options.last_modified] The last_modified option.
   1314   */
   1315  async removeAttachment(recordId, options = {}) {
   1316    const { last_modified } = options;
   1317    const path = this._endpoints.attachment(
   1318      this.bucket.name,
   1319      this.name,
   1320      recordId
   1321    );
   1322    const request = deleteRequest(path, {
   1323      last_modified,
   1324      headers: this._getHeaders(options),
   1325      safe: this._getSafe(options),
   1326    });
   1327    return this.client.execute(request, {
   1328      retry: this._getRetry(options),
   1329    });
   1330  }
   1331  /**
   1332   * Updates a record in current collection.
   1333   *
   1334   * @param  {Object}  record                  The record to update.
   1335   * @param  {Object}  [options={}]            The options object.
   1336   * @param  {Object}  [options.headers]       The headers object option.
   1337   * @param  {Number}  [options.retry=0]       Number of retries to make
   1338   *     when faced with transient errors.
   1339   * @param  {Boolean} [options.safe]          The safe option.
   1340   * @param  {Number}  [options.last_modified] The last_modified option.
   1341   * @param  {Object}  [options.permissions]   The permissions option.
   1342   * @return {Promise<Object, Error>}
   1343   */
   1344  async updateRecord(record, options = {}) {
   1345    if (!isObject(record)) {
   1346      throw new Error("A record object is required.");
   1347    }
   1348    if (!record.id) {
   1349      throw new Error("A record id is required.");
   1350    }
   1351    const { permissions } = options;
   1352    const { last_modified } = { ...record, ...options };
   1353    const path = this._endpoints.record(this.bucket.name, this.name, record.id);
   1354    const request = updateRequest(
   1355      path,
   1356      { data: record, permissions },
   1357      {
   1358        headers: this._getHeaders(options),
   1359        safe: this._getSafe(options),
   1360        last_modified,
   1361        patch: !!options.patch,
   1362      }
   1363    );
   1364    return this.client.execute(request, {
   1365      retry: this._getRetry(options),
   1366    });
   1367  }
   1368  /**
   1369   * Deletes a record from the current collection.
   1370   *
   1371   * @param  {Object|String} record                  The record to delete.
   1372   * @param  {Object}        [options={}]            The options object.
   1373   * @param  {Object}        [options.headers]       The headers object option.
   1374   * @param  {Number}        [options.retry=0]       Number of retries to make
   1375   *     when faced with transient errors.
   1376   * @param  {Boolean}       [options.safe]          The safe option.
   1377   * @param  {Number}        [options.last_modified] The last_modified option.
   1378   * @return {Promise<Object, Error>}
   1379   */
   1380  async deleteRecord(record, options = {}) {
   1381    const recordObj = toDataBody(record);
   1382    if (!recordObj.id) {
   1383      throw new Error("A record id is required.");
   1384    }
   1385    const { id } = recordObj;
   1386    const { last_modified } = { ...recordObj, ...options };
   1387    const path = this._endpoints.record(this.bucket.name, this.name, id);
   1388    const request = deleteRequest(path, {
   1389      last_modified,
   1390      headers: this._getHeaders(options),
   1391      safe: this._getSafe(options),
   1392    });
   1393    return this.client.execute(request, {
   1394      retry: this._getRetry(options),
   1395    });
   1396  }
   1397  /**
   1398   * Deletes records from the current collection.
   1399   *
   1400   * Sorting is done by passing a `sort` string option:
   1401   *
   1402   * - The field to order the results by, prefixed with `-` for descending.
   1403   * Default: `-last_modified`.
   1404   *
   1405   * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html
   1406   *
   1407   * Filtering is done by passing a `filters` option object:
   1408   *
   1409   * - `{fieldname: "value"}`
   1410   * - `{min_fieldname: 4000}`
   1411   * - `{in_fieldname: "1,2,3"}`
   1412   * - `{not_fieldname: 0}`
   1413   * - `{exclude_fieldname: "0,1"}`
   1414   *
   1415   * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html
   1416   *
   1417   * @param  {Object}   [options={}]                    The options object.
   1418   * @param  {Object}   [options.headers]               The headers object option.
   1419   * @param  {Number}   [options.retry=0]               Number of retries to make
   1420   *     when faced with transient errors.
   1421   * @param  {Object}   [options.filters={}]            The filters object.
   1422   * @param  {String}   [options.sort="-last_modified"] The sort field.
   1423   * @param  {String}   [options.at]                    The timestamp to get a snapshot at.
   1424   * @param  {String}   [options.limit=null]            The limit field.
   1425   * @param  {String}   [options.pages=1]               The number of result pages to aggregate.
   1426   * @param  {Number}   [options.since=null]            Only retrieve records modified since the provided timestamp.
   1427   * @param  {Array}    [options.fields]                Limit response to just some fields.
   1428   * @return {Promise<Object, Error>}
   1429   */
   1430  async deleteRecords(options = {}) {
   1431    const path = this._endpoints.record(this.bucket.name, this.name);
   1432    return this.client.paginatedDelete(path, options, {
   1433      headers: this._getHeaders(options),
   1434      retry: this._getRetry(options),
   1435    });
   1436  }
   1437  /**
   1438   * Retrieves a record from the current collection.
   1439   *
   1440   * @param  {String} id                The record id to retrieve.
   1441   * @param  {Object} [options={}]      The options object.
   1442   * @param  {Object} [options.headers] The headers object option.
   1443   * @param  {Object} [options.query]   Query parameters to pass in
   1444   *     the request. This might be useful for features that aren't
   1445   *     yet supported by this library.
   1446   * @param  {Array}  [options.fields]  Limit response to
   1447   *     just some fields.
   1448   * @param  {Number} [options.retry=0] Number of retries to make
   1449   *     when faced with transient errors.
   1450   * @return {Promise<Object, Error>}
   1451   */
   1452  async getRecord(id, options = {}) {
   1453    const path = this._endpoints.record(this.bucket.name, this.name, id);
   1454    const request = { headers: this._getHeaders(options), path };
   1455    return this.client.execute(request, {
   1456      retry: this._getRetry(options),
   1457      query: options.query,
   1458      fields: options.fields,
   1459    });
   1460  }
   1461  /**
   1462   * Lists records from the current collection.
   1463   *
   1464   * Sorting is done by passing a `sort` string option:
   1465   *
   1466   * - The field to order the results by, prefixed with `-` for descending.
   1467   * Default: `-last_modified`.
   1468   *
   1469   * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html
   1470   *
   1471   * Filtering is done by passing a `filters` option object:
   1472   *
   1473   * - `{fieldname: "value"}`
   1474   * - `{min_fieldname: 4000}`
   1475   * - `{in_fieldname: "1,2,3"}`
   1476   * - `{not_fieldname: 0}`
   1477   * - `{exclude_fieldname: "0,1"}`
   1478   *
   1479   * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html
   1480   *
   1481   * Paginating is done by passing a `limit` option, then calling the `next()`
   1482   * method from the resolved result object to fetch the next page, if any.
   1483   *
   1484   * @param  {Object}   [options={}]                    The options object.
   1485   * @param  {Object}   [options.headers]               The headers object option.
   1486   * @param  {Number}   [options.retry=0]               Number of retries to make
   1487   *     when faced with transient errors.
   1488   * @param  {Object}   [options.filters={}]            The filters object.
   1489   * @param  {String}   [options.sort="-last_modified"] The sort field.
   1490   * @param  {String}   [options.at]                    The timestamp to get a snapshot at.
   1491   * @param  {String}   [options.limit=null]            The limit field.
   1492   * @param  {String}   [options.pages=1]               The number of result pages to aggregate.
   1493   * @param  {Number}   [options.since=null]            Only retrieve records modified since the provided timestamp.
   1494   * @param  {Array}    [options.fields]                Limit response to just some fields.
   1495   * @return {Promise<Object, Error>}
   1496   */
   1497  async listRecords(options = {}) {
   1498    const path = this._endpoints.record(this.bucket.name, this.name);
   1499    if (options.at) {
   1500      return this.getSnapshot(options.at);
   1501    }
   1502    return this.client.paginatedList(path, options, {
   1503      headers: this._getHeaders(options),
   1504      retry: this._getRetry(options),
   1505    });
   1506  }
   1507  /**
   1508   * @private
   1509   */
   1510  async isHistoryComplete() {
   1511    // We consider that if we have the collection creation event part of the
   1512    // history, then all records change events have been tracked.
   1513    const {
   1514      data: [oldestHistoryEntry],
   1515    } = await this.bucket.listHistory({
   1516      limit: 1,
   1517      filters: {
   1518        action: "create",
   1519        resource_name: "collection",
   1520        collection_id: this.name,
   1521      },
   1522    });
   1523    return !!oldestHistoryEntry;
   1524  }
   1525  /**
   1526   * @private
   1527   */
   1528  async getSnapshot(at) {
   1529    if (!at || !Number.isInteger(at) || at <= 0) {
   1530      throw new Error("Invalid argument, expected a positive integer.");
   1531    }
   1532    // Retrieve history and check it covers the required time range.
   1533    // Ensure we have enough history data to retrieve the complete list of
   1534    // changes.
   1535    if (!(await this.isHistoryComplete())) {
   1536      throw new Error(
   1537        "Computing a snapshot is only possible when the full history for a " +
   1538          "collection is available. Here, the history plugin seems to have " +
   1539          "been enabled after the creation of the collection."
   1540      );
   1541    }
   1542    // Because of https://github.com/Kinto/kinto-http.js/issues/963
   1543    // we cannot simply rely on the history endpoint.
   1544    // Our strategy here is to clean-up the history entries from the
   1545    // records that were deleted via the plural endpoint.
   1546    // We will detect them by comparing the current state of the collection
   1547    // and the full history of the collection since its genesis.
   1548    // List full history of collection.
   1549    const { data: fullHistory } = await this.bucket.listHistory({
   1550      pages: Infinity, // all pages up to target timestamp are required
   1551      sort: "last_modified", // chronological order
   1552      filters: {
   1553        resource_name: "record",
   1554        collection_id: this.name,
   1555      },
   1556    });
   1557    // Keep latest entry ever, and latest within snapshot window.
   1558    // (history is sorted chronologically)
   1559    const latestEver = new Map();
   1560    const latestInSnapshot = new Map();
   1561    for (const entry of fullHistory) {
   1562      if (entry.target.data.last_modified <= at) {
   1563        // Snapshot includes changes right on timestamp.
   1564        latestInSnapshot.set(entry.record_id, entry);
   1565      }
   1566      latestEver.set(entry.record_id, entry);
   1567    }
   1568    // Current records ids in the collection.
   1569    const { data: current } = await this.listRecords({
   1570      pages: Infinity,
   1571      fields: ["id"], // we don't need attributes.
   1572    });
   1573    const currentIds = new Set(current.map((record) => record.id));
   1574    // If a record is not in the current collection, and its
   1575    // latest history entry isn't a delete then this means that
   1576    // it was deleted via the plural endpoint (and that we lost track
   1577    // of this deletion because of bug #963)
   1578    const deletedViaPlural = new Set();
   1579    for (const entry of latestEver.values()) {
   1580      if (entry.action != "delete" && !currentIds.has(entry.record_id)) {
   1581        deletedViaPlural.add(entry.record_id);
   1582      }
   1583    }
   1584    // Now reconstruct the collection based on latest version in snapshot
   1585    // filtering all deleted records.
   1586    const reconstructed = [];
   1587    for (const entry of latestInSnapshot.values()) {
   1588      if (entry.action != "delete" && !deletedViaPlural.has(entry.record_id)) {
   1589        reconstructed.push(entry.target.data);
   1590      }
   1591    }
   1592    return {
   1593      last_modified: String(at),
   1594      data: Array.from(reconstructed).sort(
   1595        (a, b) => b.last_modified - a.last_modified
   1596      ),
   1597      next: () => {
   1598        throw new Error("Snapshots don't support pagination");
   1599      },
   1600      hasNextPage: false,
   1601      totalRecords: reconstructed.length,
   1602    };
   1603  }
   1604  /**
   1605   * Performs batch operations at the current collection level.
   1606   *
   1607   * @param  {Function} fn                   The batch operation function.
   1608   * @param  {Object}   [options={}]         The options object.
   1609   * @param  {Object}   [options.headers]    The headers object option.
   1610   * @param  {Boolean}  [options.safe]       The safe option.
   1611   * @param  {Number}   [options.retry]      The retry option.
   1612   * @param  {Boolean}  [options.aggregate]  Produces a grouped result object.
   1613   * @return {Promise<Object, Error>}
   1614   */
   1615  async batch(fn, options = {}) {
   1616    return this.client.batch(fn, {
   1617      bucket: this.bucket.name,
   1618      collection: this.name,
   1619      headers: this._getHeaders(options),
   1620      retry: this._getRetry(options),
   1621      safe: this._getSafe(options),
   1622      aggregate: !!options.aggregate,
   1623    });
   1624  }
   1625 }
   1626 __decorate(
   1627  [capable(["attachments"])],
   1628  Collection.prototype,
   1629  "addAttachment",
   1630  null
   1631 );
   1632 __decorate(
   1633  [capable(["attachments"])],
   1634  Collection.prototype,
   1635  "removeAttachment",
   1636  null
   1637 );
   1638 __decorate([capable(["history"])], Collection.prototype, "getSnapshot", null);
   1639 
   1640 /**
   1641 * Abstract representation of a selected bucket.
   1642 *
   1643 */
   1644 class Bucket {
   1645  /**
   1646   * Constructor.
   1647   *
   1648   * @param  {KintoClient} client            The client instance.
   1649   * @param  {String}      name              The bucket name.
   1650   * @param  {Object}      [options={}]      The headers object option.
   1651   * @param  {Object}      [options.headers] The headers object option.
   1652   * @param  {Boolean}     [options.safe]    The safe option.
   1653   * @param  {Number}      [options.retry]   The retry option.
   1654   */
   1655  constructor(client, name, options = {}) {
   1656    /**
   1657     * @ignore
   1658     */
   1659    this.client = client;
   1660    /**
   1661     * The bucket name.
   1662     * @type {String}
   1663     */
   1664    this.name = name;
   1665    this._endpoints = client.endpoints;
   1666    /**
   1667     * @ignore
   1668     */
   1669    this._headers = options.headers || {};
   1670    this._retry = options.retry || 0;
   1671    this._safe = !!options.safe;
   1672  }
   1673  get execute() {
   1674    return this.client.execute.bind(this.client);
   1675  }
   1676  get headers() {
   1677    return this._headers;
   1678  }
   1679  /**
   1680   * Get the value of "headers" for a given request, merging the
   1681   * per-request headers with our own "default" headers.
   1682   *
   1683   * @private
   1684   */
   1685  _getHeaders(options) {
   1686    return {
   1687      ...this._headers,
   1688      ...options.headers,
   1689    };
   1690  }
   1691  /**
   1692   * Get the value of "safe" for a given request, using the
   1693   * per-request option if present or falling back to our default
   1694   * otherwise.
   1695   *
   1696   * @private
   1697   * @param {Object} options The options for a request.
   1698   * @returns {Boolean}
   1699   */
   1700  _getSafe(options) {
   1701    return { safe: this._safe, ...options }.safe;
   1702  }
   1703  /**
   1704   * As _getSafe, but for "retry".
   1705   *
   1706   * @private
   1707   */
   1708  _getRetry(options) {
   1709    return { retry: this._retry, ...options }.retry;
   1710  }
   1711  /**
   1712   * Selects a collection.
   1713   *
   1714   * @param  {String}  name              The collection name.
   1715   * @param  {Object}  [options={}]      The options object.
   1716   * @param  {Object}  [options.headers] The headers object option.
   1717   * @param  {Boolean} [options.safe]    The safe option.
   1718   * @return {Collection}
   1719   */
   1720  collection(name, options = {}) {
   1721    return new Collection(this.client, this, name, {
   1722      headers: this._getHeaders(options),
   1723      retry: this._getRetry(options),
   1724      safe: this._getSafe(options),
   1725    });
   1726  }
   1727  /**
   1728   * Retrieves the ETag of the collection list, for use with the `since` filtering option.
   1729   *
   1730   * @param  {Object} [options={}]      The options object.
   1731   * @param  {Object} [options.headers] The headers object option.
   1732   * @param  {Number} [options.retry=0] Number of retries to make
   1733   *     when faced with transient errors.
   1734   * @return {Promise<String, Error>}
   1735   */
   1736  async getCollectionsTimestamp(options = {}) {
   1737    const path = this._endpoints.collection(this.name);
   1738    const request = {
   1739      headers: this._getHeaders(options),
   1740      path,
   1741      method: "HEAD",
   1742    };
   1743    const { headers } = await this.client.execute(request, {
   1744      raw: true,
   1745      retry: this._getRetry(options),
   1746    });
   1747    return headers.get("ETag");
   1748  }
   1749  /**
   1750   * Retrieves the ETag of the group list, for use with the `since` filtering option.
   1751   *
   1752   * @param  {Object} [options={}]      The options object.
   1753   * @param  {Object} [options.headers] The headers object option.
   1754   * @param  {Number} [options.retry=0] Number of retries to make
   1755   *     when faced with transient errors.
   1756   * @return {Promise<String, Error>}
   1757   */
   1758  async getGroupsTimestamp(options = {}) {
   1759    const path = this._endpoints.group(this.name);
   1760    const request = {
   1761      headers: this._getHeaders(options),
   1762      path,
   1763      method: "HEAD",
   1764    };
   1765    const { headers } = await this.client.execute(request, {
   1766      raw: true,
   1767      retry: this._getRetry(options),
   1768    });
   1769    return headers.get("ETag");
   1770  }
   1771  /**
   1772   * Retrieves bucket data.
   1773   *
   1774   * @param  {Object} [options={}]      The options object.
   1775   * @param  {Object} [options.headers] The headers object option.
   1776   * @param  {Object} [options.query]   Query parameters to pass in
   1777   *     the request. This might be useful for features that aren't
   1778   *     yet supported by this library.
   1779   * @param  {Array}  [options.fields]  Limit response to
   1780   *     just some fields.
   1781   * @param  {Number} [options.retry=0] Number of retries to make
   1782   *     when faced with transient errors.
   1783   * @return {Promise<Object, Error>}
   1784   */
   1785  async getData(options = {}) {
   1786    const path = this._endpoints.bucket(this.name);
   1787    const request = {
   1788      headers: this._getHeaders(options),
   1789      path,
   1790    };
   1791    const { data } = await this.client.execute(request, {
   1792      retry: this._getRetry(options),
   1793      query: options.query,
   1794      fields: options.fields,
   1795    });
   1796    return data;
   1797  }
   1798  /**
   1799   * Set bucket data.
   1800   * @param  {Object}  data                    The bucket data object.
   1801   * @param  {Object}  [options={}]            The options object.
   1802   * @param  {Object}  [options.headers={}]    The headers object option.
   1803   * @param  {Boolean} [options.safe]          The safe option.
   1804   * @param  {Number}  [options.retry=0]       Number of retries to make
   1805   *     when faced with transient errors.
   1806   * @param  {Boolean} [options.patch]         The patch option.
   1807   * @param  {Number}  [options.last_modified] The last_modified option.
   1808   * @return {Promise<Object, Error>}
   1809   */
   1810  async setData(data, options = {}) {
   1811    if (!isObject(data)) {
   1812      throw new Error("A bucket object is required.");
   1813    }
   1814    const bucket = {
   1815      ...data,
   1816      id: this.name,
   1817    };
   1818    // For default bucket, we need to drop the id from the data object.
   1819    // Bug in Kinto < 3.1.1
   1820    const bucketId = bucket.id;
   1821    if (bucket.id === "default") {
   1822      delete bucket.id;
   1823    }
   1824    const path = this._endpoints.bucket(bucketId);
   1825    const { patch, permissions } = options;
   1826    const { last_modified } = { ...data, ...options };
   1827    const request = updateRequest(
   1828      path,
   1829      { data: bucket, permissions },
   1830      {
   1831        last_modified,
   1832        patch,
   1833        headers: this._getHeaders(options),
   1834        safe: this._getSafe(options),
   1835      }
   1836    );
   1837    return this.client.execute(request, {
   1838      retry: this._getRetry(options),
   1839    });
   1840  }
   1841  /**
   1842   * Retrieves the list of history entries in the current bucket.
   1843   *
   1844   * @param  {Object} [options={}]      The options object.
   1845   * @param  {Object} [options.headers] The headers object option.
   1846   * @param  {Number} [options.retry=0] Number of retries to make
   1847   *     when faced with transient errors.
   1848   * @return {Promise<Array<Object>, Error>}
   1849   */
   1850  async listHistory(options = {}) {
   1851    const path = this._endpoints.history(this.name);
   1852    return this.client.paginatedList(path, options, {
   1853      headers: this._getHeaders(options),
   1854      retry: this._getRetry(options),
   1855    });
   1856  }
   1857  /**
   1858   * Retrieves the list of collections in the current bucket.
   1859   *
   1860   * @param  {Object} [options={}]      The options object.
   1861   * @param  {Object} [options.filters={}] The filters object.
   1862   * @param  {Object} [options.headers] The headers object option.
   1863   * @param  {Number} [options.retry=0] Number of retries to make
   1864   *     when faced with transient errors.
   1865   * @param  {Array}  [options.fields]  Limit response to
   1866   *     just some fields.
   1867   * @return {Promise<Array<Object>, Error>}
   1868   */
   1869  async listCollections(options = {}) {
   1870    const path = this._endpoints.collection(this.name);
   1871    return this.client.paginatedList(path, options, {
   1872      headers: this._getHeaders(options),
   1873      retry: this._getRetry(options),
   1874    });
   1875  }
   1876  /**
   1877   * Creates a new collection in current bucket.
   1878   *
   1879   * @param  {String|undefined}  id          The collection id.
   1880   * @param  {Object}  [options={}]          The options object.
   1881   * @param  {Boolean} [options.safe]        The safe option.
   1882   * @param  {Object}  [options.headers]     The headers object option.
   1883   * @param  {Number}  [options.retry=0]     Number of retries to make
   1884   *     when faced with transient errors.
   1885   * @param  {Object}  [options.permissions] The permissions object.
   1886   * @param  {Object}  [options.data]        The data object.
   1887   * @return {Promise<Object, Error>}
   1888   */
   1889  async createCollection(id, options = {}) {
   1890    const { permissions, data = {} } = options;
   1891    data.id = id;
   1892    const path = this._endpoints.collection(this.name, id);
   1893    const request = createRequest(
   1894      path,
   1895      { data, permissions },
   1896      {
   1897        headers: this._getHeaders(options),
   1898        safe: this._getSafe(options),
   1899      }
   1900    );
   1901    return this.client.execute(request, {
   1902      retry: this._getRetry(options),
   1903    });
   1904  }
   1905  /**
   1906   * Deletes a collection from the current bucket.
   1907   *
   1908   * @param  {Object|String} collection              The collection to delete.
   1909   * @param  {Object}        [options={}]            The options object.
   1910   * @param  {Object}        [options.headers]       The headers object option.
   1911   * @param  {Number}        [options.retry=0]       Number of retries to make
   1912   *     when faced with transient errors.
   1913   * @param  {Boolean}       [options.safe]          The safe option.
   1914   * @param  {Number}        [options.last_modified] The last_modified option.
   1915   * @return {Promise<Object, Error>}
   1916   */
   1917  async deleteCollection(collection, options = {}) {
   1918    const collectionObj = toDataBody(collection);
   1919    if (!collectionObj.id) {
   1920      throw new Error("A collection id is required.");
   1921    }
   1922    const { id } = collectionObj;
   1923    const { last_modified } = { ...collectionObj, ...options };
   1924    const path = this._endpoints.collection(this.name, id);
   1925    const request = deleteRequest(path, {
   1926      last_modified,
   1927      headers: this._getHeaders(options),
   1928      safe: this._getSafe(options),
   1929    });
   1930    return this.client.execute(request, {
   1931      retry: this._getRetry(options),
   1932    });
   1933  }
   1934  /**
   1935   * Deletes collections from the current bucket.
   1936   *
   1937   * @param  {Object} [options={}]      The options object.
   1938   * @param  {Object} [options.filters={}] The filters object.
   1939   * @param  {Object} [options.headers] The headers object option.
   1940   * @param  {Number} [options.retry=0] Number of retries to make
   1941   *     when faced with transient errors.
   1942   * @param  {Array}  [options.fields]  Limit response to
   1943   *     just some fields.
   1944   * @return {Promise<Array<Object>, Error>}
   1945   */
   1946  async deleteCollections(options = {}) {
   1947    const path = this._endpoints.collection(this.name);
   1948    return this.client.paginatedDelete(path, options, {
   1949      headers: this._getHeaders(options),
   1950      retry: this._getRetry(options),
   1951    });
   1952  }
   1953  /**
   1954   * Retrieves the list of groups in the current bucket.
   1955   *
   1956   * @param  {Object} [options={}]      The options object.
   1957   * @param  {Object} [options.filters={}] The filters object.
   1958   * @param  {Object} [options.headers] The headers object option.
   1959   * @param  {Number} [options.retry=0] Number of retries to make
   1960   *     when faced with transient errors.
   1961   * @param  {Array}  [options.fields]  Limit response to
   1962   *     just some fields.
   1963   * @return {Promise<Array<Object>, Error>}
   1964   */
   1965  async listGroups(options = {}) {
   1966    const path = this._endpoints.group(this.name);
   1967    return this.client.paginatedList(path, options, {
   1968      headers: this._getHeaders(options),
   1969      retry: this._getRetry(options),
   1970    });
   1971  }
   1972  /**
   1973   * Fetches a group in current bucket.
   1974   *
   1975   * @param  {String} id                The group id.
   1976   * @param  {Object} [options={}]      The options object.
   1977   * @param  {Object} [options.headers] The headers object option.
   1978   * @param  {Number} [options.retry=0] Number of retries to make
   1979   *     when faced with transient errors.
   1980   * @param  {Object} [options.query]   Query parameters to pass in
   1981   *     the request. This might be useful for features that aren't
   1982   *     yet supported by this library.
   1983   * @param  {Array}  [options.fields]  Limit response to
   1984   *     just some fields.
   1985   * @return {Promise<Object, Error>}
   1986   */
   1987  async getGroup(id, options = {}) {
   1988    const path = this._endpoints.group(this.name, id);
   1989    const request = {
   1990      headers: this._getHeaders(options),
   1991      path,
   1992    };
   1993    return this.client.execute(request, {
   1994      retry: this._getRetry(options),
   1995      query: options.query,
   1996      fields: options.fields,
   1997    });
   1998  }
   1999  /**
   2000   * Creates a new group in current bucket.
   2001   *
   2002   * @param  {String|undefined}  id                    The group id.
   2003   * @param  {Array<String>}     [members=[]]          The list of principals.
   2004   * @param  {Object}            [options={}]          The options object.
   2005   * @param  {Object}            [options.data]        The data object.
   2006   * @param  {Object}            [options.permissions] The permissions object.
   2007   * @param  {Boolean}           [options.safe]        The safe option.
   2008   * @param  {Object}            [options.headers]     The headers object option.
   2009   * @param  {Number}            [options.retry=0]     Number of retries to make
   2010   *     when faced with transient errors.
   2011   * @return {Promise<Object, Error>}
   2012   */
   2013  async createGroup(id, members = [], options = {}) {
   2014    const data = {
   2015      ...options.data,
   2016      id,
   2017      members,
   2018    };
   2019    const path = this._endpoints.group(this.name, id);
   2020    const { permissions } = options;
   2021    const request = createRequest(
   2022      path,
   2023      { data, permissions },
   2024      {
   2025        headers: this._getHeaders(options),
   2026        safe: this._getSafe(options),
   2027      }
   2028    );
   2029    return this.client.execute(request, {
   2030      retry: this._getRetry(options),
   2031    });
   2032  }
   2033  /**
   2034   * Updates an existing group in current bucket.
   2035   *
   2036   * @param  {Object}  group                   The group object.
   2037   * @param  {Object}  [options={}]            The options object.
   2038   * @param  {Object}  [options.data]          The data object.
   2039   * @param  {Object}  [options.permissions]   The permissions object.
   2040   * @param  {Boolean} [options.safe]          The safe option.
   2041   * @param  {Object}  [options.headers]       The headers object option.
   2042   * @param  {Number}  [options.retry=0]       Number of retries to make
   2043   *     when faced with transient errors.
   2044   * @param  {Number}  [options.last_modified] The last_modified option.
   2045   * @return {Promise<Object, Error>}
   2046   */
   2047  async updateGroup(group, options = {}) {
   2048    if (!isObject(group)) {
   2049      throw new Error("A group object is required.");
   2050    }
   2051    if (!group.id) {
   2052      throw new Error("A group id is required.");
   2053    }
   2054    const data = {
   2055      ...options.data,
   2056      ...group,
   2057    };
   2058    const path = this._endpoints.group(this.name, group.id);
   2059    const { patch, permissions } = options;
   2060    const { last_modified } = { ...data, ...options };
   2061    const request = updateRequest(
   2062      path,
   2063      { data, permissions },
   2064      {
   2065        last_modified,
   2066        patch,
   2067        headers: this._getHeaders(options),
   2068        safe: this._getSafe(options),
   2069      }
   2070    );
   2071    return this.client.execute(request, {
   2072      retry: this._getRetry(options),
   2073    });
   2074  }
   2075  /**
   2076   * Deletes a group from the current bucket.
   2077   *
   2078   * @param  {Object|String} group                   The group to delete.
   2079   * @param  {Object}        [options={}]            The options object.
   2080   * @param  {Object}        [options.headers]       The headers object option.
   2081   * @param  {Number}        [options.retry=0]       Number of retries to make
   2082   *     when faced with transient errors.
   2083   * @param  {Boolean}       [options.safe]          The safe option.
   2084   * @param  {Number}        [options.last_modified] The last_modified option.
   2085   * @return {Promise<Object, Error>}
   2086   */
   2087  async deleteGroup(group, options = {}) {
   2088    const groupObj = toDataBody(group);
   2089    const { id } = groupObj;
   2090    const { last_modified } = { ...groupObj, ...options };
   2091    const path = this._endpoints.group(this.name, id);
   2092    const request = deleteRequest(path, {
   2093      last_modified,
   2094      headers: this._getHeaders(options),
   2095      safe: this._getSafe(options),
   2096    });
   2097    return this.client.execute(request, {
   2098      retry: this._getRetry(options),
   2099    });
   2100  }
   2101  /**
   2102   * Deletes groups from the current bucket.
   2103   *
   2104   * @param  {Object} [options={}]          The options object.
   2105   * @param  {Object} [options.filters={}]  The filters object.
   2106   * @param  {Object} [options.headers]     The headers object option.
   2107   * @param  {Number} [options.retry=0]     Number of retries to make
   2108   *     when faced with transient errors.
   2109   * @param  {Array}  [options.fields]      Limit response to
   2110   *     just some fields.
   2111   * @return {Promise<Array<Object>, Error>}
   2112   */
   2113  async deleteGroups(options = {}) {
   2114    const path = this._endpoints.group(this.name);
   2115    return this.client.paginatedDelete(path, options, {
   2116      headers: this._getHeaders(options),
   2117      retry: this._getRetry(options),
   2118    });
   2119  }
   2120  /**
   2121   * Retrieves the list of permissions for this bucket.
   2122   *
   2123   * @param  {Object} [options={}]      The options object.
   2124   * @param  {Object} [options.headers] The headers object option.
   2125   * @param  {Number} [options.retry=0] Number of retries to make
   2126   *     when faced with transient errors.
   2127   * @return {Promise<Object, Error>}
   2128   */
   2129  async getPermissions(options = {}) {
   2130    const request = {
   2131      headers: this._getHeaders(options),
   2132      path: this._endpoints.bucket(this.name),
   2133    };
   2134    const { permissions } = await this.client.execute(request, {
   2135      retry: this._getRetry(options),
   2136    });
   2137    return permissions;
   2138  }
   2139  /**
   2140   * Replaces all existing bucket permissions with the ones provided.
   2141   *
   2142   * @param  {Object}  permissions             The permissions object.
   2143   * @param  {Object}  [options={}]            The options object
   2144   * @param  {Boolean} [options.safe]          The safe option.
   2145   * @param  {Object}  [options.headers={}]    The headers object option.
   2146   * @param  {Number}  [options.retry=0]       Number of retries to make
   2147   *     when faced with transient errors.
   2148   * @param  {Object}  [options.last_modified] The last_modified option.
   2149   * @return {Promise<Object, Error>}
   2150   */
   2151  async setPermissions(permissions, options = {}) {
   2152    if (!isObject(permissions)) {
   2153      throw new Error("A permissions object is required.");
   2154    }
   2155    const path = this._endpoints.bucket(this.name);
   2156    const { last_modified } = options;
   2157    const data = { last_modified };
   2158    const request = updateRequest(
   2159      path,
   2160      { data, permissions },
   2161      {
   2162        headers: this._getHeaders(options),
   2163        safe: this._getSafe(options),
   2164      }
   2165    );
   2166    return this.client.execute(request, {
   2167      retry: this._getRetry(options),
   2168    });
   2169  }
   2170  /**
   2171   * Append principals to the bucket permissions.
   2172   *
   2173   * @param  {Object}  permissions             The permissions object.
   2174   * @param  {Object}  [options={}]            The options object
   2175   * @param  {Boolean} [options.safe]          The safe option.
   2176   * @param  {Object}  [options.headers]       The headers object option.
   2177   * @param  {Number}  [options.retry=0]       Number of retries to make
   2178   *     when faced with transient errors.
   2179   * @param  {Object}  [options.last_modified] The last_modified option.
   2180   * @return {Promise<Object, Error>}
   2181   */
   2182  async addPermissions(permissions, options = {}) {
   2183    if (!isObject(permissions)) {
   2184      throw new Error("A permissions object is required.");
   2185    }
   2186    const path = this._endpoints.bucket(this.name);
   2187    const { last_modified } = options;
   2188    const request = jsonPatchPermissionsRequest(path, permissions, "add", {
   2189      last_modified,
   2190      headers: this._getHeaders(options),
   2191      safe: this._getSafe(options),
   2192    });
   2193    return this.client.execute(request, {
   2194      retry: this._getRetry(options),
   2195    });
   2196  }
   2197  /**
   2198   * Remove principals from the bucket permissions.
   2199   *
   2200   * @param  {Object}  permissions             The permissions object.
   2201   * @param  {Object}  [options={}]            The options object
   2202   * @param  {Boolean} [options.safe]          The safe option.
   2203   * @param  {Object}  [options.headers]       The headers object option.
   2204   * @param  {Number}  [options.retry=0]       Number of retries to make
   2205   *     when faced with transient errors.
   2206   * @param  {Object}  [options.last_modified] The last_modified option.
   2207   * @return {Promise<Object, Error>}
   2208   */
   2209  async removePermissions(permissions, options = {}) {
   2210    if (!isObject(permissions)) {
   2211      throw new Error("A permissions object is required.");
   2212    }
   2213    const path = this._endpoints.bucket(this.name);
   2214    const { last_modified } = options;
   2215    const request = jsonPatchPermissionsRequest(path, permissions, "remove", {
   2216      last_modified,
   2217      headers: this._getHeaders(options),
   2218      safe: this._getSafe(options),
   2219    });
   2220    return this.client.execute(request, {
   2221      retry: this._getRetry(options),
   2222    });
   2223  }
   2224  /**
   2225   * Performs batch operations at the current bucket level.
   2226   *
   2227   * @param  {Function} fn                   The batch operation function.
   2228   * @param  {Object}   [options={}]         The options object.
   2229   * @param  {Object}   [options.headers]    The headers object option.
   2230   * @param  {Boolean}  [options.safe]       The safe option.
   2231   * @param  {Number}   [options.retry=0]    The retry option.
   2232   * @param  {Boolean}  [options.aggregate]  Produces a grouped result object.
   2233   * @return {Promise<Object, Error>}
   2234   */
   2235  async batch(fn, options = {}) {
   2236    return this.client.batch(fn, {
   2237      bucket: this.name,
   2238      headers: this._getHeaders(options),
   2239      retry: this._getRetry(options),
   2240      safe: this._getSafe(options),
   2241      aggregate: !!options.aggregate,
   2242    });
   2243  }
   2244 }
   2245 __decorate([capable(["history"])], Bucket.prototype, "listHistory", null);
   2246 
   2247 /**
   2248 * High level HTTP client for the Kinto API.
   2249 *
   2250 * @example
   2251 * const client = new KintoClient("https://demo.kinto-storage.org/v1");
   2252 * client.bucket("default")
   2253 *    .collection("my-blog")
   2254 *    .createRecord({title: "First article"})
   2255 *   .then(console.log.bind(console))
   2256 *   .catch(console.error.bind(console));
   2257 */
   2258 class KintoClientBase {
   2259  /**
   2260   * Constructor.
   2261   *
   2262   * @param  {String}       remote  The remote URL.
   2263   * @param  {Object}       [options={}]                  The options object.
   2264   * @param  {Boolean}      [options.safe=true]           Adds concurrency headers to every requests.
   2265   * @param  {EventEmitter} [options.events=EventEmitter] The events handler instance.
   2266   * @param  {Object}       [options.headers={}]          The key-value headers to pass to each request.
   2267   * @param  {Object}       [options.retry=0]             Number of retries when request fails (default: 0)
   2268   * @param  {String}       [options.bucket="default"]    The default bucket to use.
   2269   * @param  {String}       [options.requestMode="cors"]  The HTTP request mode (from ES6 fetch spec).
   2270   * @param  {Number}       [options.timeout=null]        The request timeout in ms, if any.
   2271   * @param  {Function}     [options.fetchFunc=fetch]     The function to be used to execute HTTP requests.
   2272   */
   2273  constructor(remote, options) {
   2274    if (typeof remote !== "string" || !remote.length) {
   2275      throw new Error("Invalid remote URL: " + remote);
   2276    }
   2277    if (remote[remote.length - 1] === "/") {
   2278      remote = remote.slice(0, -1);
   2279    }
   2280    this._backoffReleaseTime = null;
   2281    this._requests = [];
   2282    this._isBatch = !!options.batch;
   2283    this._retry = options.retry || 0;
   2284    this._safe = !!options.safe;
   2285    this._headers = options.headers || {};
   2286    // public properties
   2287    /**
   2288     * The remote server base URL.
   2289     * @type {String}
   2290     */
   2291    this.remote = remote;
   2292    /**
   2293     * Current server information.
   2294     * @ignore
   2295     * @type {Object|null}
   2296     */
   2297    this.serverInfo = null;
   2298    /**
   2299     * The event emitter instance. Should comply with the `EventEmitter`
   2300     * interface.
   2301     * @ignore
   2302     * @type {Class}
   2303     */
   2304    this.events = options.events;
   2305    this.endpoints = ENDPOINTS;
   2306    const { fetchFunc, requestMode, timeout } = options;
   2307    /**
   2308     * The HTTP instance.
   2309     * @ignore
   2310     * @type {HTTP}
   2311     */
   2312    this.http = new HTTP(this.events, { fetchFunc, requestMode, timeout });
   2313    this._registerHTTPEvents();
   2314  }
   2315  /**
   2316   * The remote endpoint base URL. Setting the value will also extract and
   2317   * validate the version.
   2318   * @type {String}
   2319   */
   2320  get remote() {
   2321    return this._remote;
   2322  }
   2323  /**
   2324   * @ignore
   2325   */
   2326  set remote(url) {
   2327    let version;
   2328    try {
   2329      version = url.match(/\/(v\d+)\/?$/)[1];
   2330    } catch (err) {
   2331      throw new Error("The remote URL must contain the version: " + url);
   2332    }
   2333    this._remote = url;
   2334    this._version = version;
   2335  }
   2336  /**
   2337   * The current server protocol version, eg. `v1`.
   2338   * @type {String}
   2339   */
   2340  get version() {
   2341    return this._version;
   2342  }
   2343  /**
   2344   * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
   2345   * ongoing.
   2346   *
   2347   * @type {Number}
   2348   */
   2349  get backoff() {
   2350    const currentTime = new Date().getTime();
   2351    if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
   2352      return this._backoffReleaseTime - currentTime;
   2353    }
   2354    return 0;
   2355  }
   2356  /**
   2357   * Registers HTTP events.
   2358   * @private
   2359   */
   2360  _registerHTTPEvents() {
   2361    // Prevent registering event from a batch client instance
   2362    if (!this._isBatch && this.events) {
   2363      this.events.on("backoff", (backoffMs) => {
   2364        this._backoffReleaseTime = backoffMs;
   2365      });
   2366    }
   2367  }
   2368  /**
   2369   * Retrieve a bucket object to perform operations on it.
   2370   *
   2371   * @param  {String}  name              The bucket name.
   2372   * @param  {Object}  [options={}]      The request options.
   2373   * @param  {Boolean} [options.safe]    The resulting safe option.
   2374   * @param  {Number}  [options.retry]   The resulting retry option.
   2375   * @param  {Object}  [options.headers] The extended headers object option.
   2376   * @return {Bucket}
   2377   */
   2378  bucket(name, options = {}) {
   2379    return new Bucket(this, name, {
   2380      headers: this._getHeaders(options),
   2381      safe: this._getSafe(options),
   2382      retry: this._getRetry(options),
   2383    });
   2384  }
   2385  /**
   2386   * Set client "headers" for every request, updating previous headers (if any).
   2387   *
   2388   * @param {Object} headers The headers to merge with existing ones.
   2389   */
   2390  setHeaders(headers) {
   2391    this._headers = {
   2392      ...this._headers,
   2393      ...headers,
   2394    };
   2395    this.serverInfo = null;
   2396  }
   2397  /**
   2398   * Get the value of "headers" for a given request, merging the
   2399   * per-request headers with our own "default" headers.
   2400   *
   2401   * Note that unlike other options, headers aren't overridden, but
   2402   * merged instead.
   2403   *
   2404   * @private
   2405   * @param {Object} options The options for a request.
   2406   * @returns {Object}
   2407   */
   2408  _getHeaders(options) {
   2409    return {
   2410      ...this._headers,
   2411      ...options.headers,
   2412    };
   2413  }
   2414  /**
   2415   * Get the value of "safe" for a given request, using the
   2416   * per-request option if present or falling back to our default
   2417   * otherwise.
   2418   *
   2419   * @private
   2420   * @param {Object} options The options for a request.
   2421   * @returns {Boolean}
   2422   */
   2423  _getSafe(options) {
   2424    return { safe: this._safe, ...options }.safe;
   2425  }
   2426  /**
   2427   * As _getSafe, but for "retry".
   2428   *
   2429   * @private
   2430   */
   2431  _getRetry(options) {
   2432    return { retry: this._retry, ...options }.retry;
   2433  }
   2434  /**
   2435   * Retrieves the server's "hello" endpoint. This endpoint reveals
   2436   * server capabilities and settings as well as telling the client
   2437   * "who they are" according to their given authorization headers.
   2438   *
   2439   * @private
   2440   * @param  {Object}  [options={}] The request options.
   2441   * @param  {Object}  [options.headers={}] Headers to use when making
   2442   *     this request.
   2443   * @param  {Number}  [options.retry=0]    Number of retries to make
   2444   *     when faced with transient errors.
   2445   * @return {Promise<Object, Error>}
   2446   */
   2447  async _getHello(options = {}) {
   2448    const path = this.remote + ENDPOINTS.root();
   2449    const { json } = await this.http.request(
   2450      path,
   2451      { headers: this._getHeaders(options) },
   2452      { retry: this._getRetry(options) }
   2453    );
   2454    return json;
   2455  }
   2456  /**
   2457   * Retrieves server information and persist them locally. This operation is
   2458   * usually performed a single time during the instance lifecycle.
   2459   *
   2460   * @param  {Object}  [options={}] The request options.
   2461   * @param  {Number}  [options.retry=0]    Number of retries to make
   2462   *     when faced with transient errors.
   2463   * @return {Promise<Object, Error>}
   2464   */
   2465  async fetchServerInfo(options = {}) {
   2466    if (this.serverInfo) {
   2467      return this.serverInfo;
   2468    }
   2469    this.serverInfo = await this._getHello({ retry: this._getRetry(options) });
   2470    return this.serverInfo;
   2471  }
   2472  /**
   2473   * Retrieves Kinto server settings.
   2474   *
   2475   * @param  {Object}  [options={}] The request options.
   2476   * @param  {Number}  [options.retry=0]    Number of retries to make
   2477   *     when faced with transient errors.
   2478   * @return {Promise<Object, Error>}
   2479   */
   2480  async fetchServerSettings(options = {}) {
   2481    const { settings } = await this.fetchServerInfo(options);
   2482    return settings;
   2483  }
   2484  /**
   2485   * Retrieve server capabilities information.
   2486   *
   2487   * @param  {Object}  [options={}] The request options.
   2488   * @param  {Number}  [options.retry=0]    Number of retries to make
   2489   *     when faced with transient errors.
   2490   * @return {Promise<Object, Error>}
   2491   */
   2492  async fetchServerCapabilities(options = {}) {
   2493    const { capabilities } = await this.fetchServerInfo(options);
   2494    return capabilities;
   2495  }
   2496  /**
   2497   * Retrieve authenticated user information.
   2498   *
   2499   * @param  {Object}  [options={}] The request options.
   2500   * @param  {Object}  [options.headers={}] Headers to use when making
   2501   *     this request.
   2502   * @param  {Number}  [options.retry=0]    Number of retries to make
   2503   *     when faced with transient errors.
   2504   * @return {Promise<Object, Error>}
   2505   */
   2506  async fetchUser(options = {}) {
   2507    const { user } = await this._getHello(options);
   2508    return user;
   2509  }
   2510  /**
   2511   * Retrieve authenticated user information.
   2512   *
   2513   * @param  {Object}  [options={}] The request options.
   2514   * @param  {Number}  [options.retry=0]    Number of retries to make
   2515   *     when faced with transient errors.
   2516   * @return {Promise<Object, Error>}
   2517   */
   2518  async fetchHTTPApiVersion(options = {}) {
   2519    const { http_api_version } = await this.fetchServerInfo(options);
   2520    return http_api_version;
   2521  }
   2522  /**
   2523   * Process batch requests, chunking them according to the batch_max_requests
   2524   * server setting when needed.
   2525   *
   2526   * @param  {Array}  requests     The list of batch subrequests to perform.
   2527   * @param  {Object} [options={}] The options object.
   2528   * @return {Promise<Object, Error>}
   2529   */
   2530  async _batchRequests(requests, options = {}) {
   2531    const headers = this._getHeaders(options);
   2532    if (!requests.length) {
   2533      return [];
   2534    }
   2535    const serverSettings = await this.fetchServerSettings({
   2536      retry: this._getRetry(options),
   2537    });
   2538    const maxRequests = serverSettings.batch_max_requests;
   2539    if (maxRequests && requests.length > maxRequests) {
   2540      const chunks = partition(requests, maxRequests);
   2541      const results = [];
   2542      for (const chunk of chunks) {
   2543        const result = await this._batchRequests(chunk, options);
   2544        results.push(...result);
   2545      }
   2546      return results;
   2547    }
   2548    const { responses } = await this.execute(
   2549      {
   2550        // FIXME: is this really necessary, since it's also present in
   2551        // the "defaults"?
   2552        headers,
   2553        path: ENDPOINTS.batch(),
   2554        method: "POST",
   2555        body: {
   2556          defaults: { headers },
   2557          requests,
   2558        },
   2559      },
   2560      { retry: this._getRetry(options) }
   2561    );
   2562    return responses;
   2563  }
   2564  /**
   2565   * Sends batch requests to the remote server.
   2566   *
   2567   * Note: Reserved for internal use only.
   2568   *
   2569   * @ignore
   2570   * @param  {Function} fn                        The function to use for describing batch ops.
   2571   * @param  {Object}   [options={}]              The options object.
   2572   * @param  {Boolean}  [options.safe]            The safe option.
   2573   * @param  {Number}   [options.retry]           The retry option.
   2574   * @param  {String}   [options.bucket]          The bucket name option.
   2575   * @param  {String}   [options.collection]      The collection name option.
   2576   * @param  {Object}   [options.headers]         The headers object option.
   2577   * @param  {Boolean}  [options.aggregate=false] Produces an aggregated result object.
   2578   * @return {Promise<Object, Error>}
   2579   */
   2580  async batch(fn, options = {}) {
   2581    const rootBatch = new KintoClientBase(this.remote, {
   2582      events: this.events,
   2583      batch: true,
   2584      safe: this._getSafe(options),
   2585      retry: this._getRetry(options),
   2586    });
   2587    if (options.bucket && options.collection) {
   2588      fn(rootBatch.bucket(options.bucket).collection(options.collection));
   2589    } else if (options.bucket) {
   2590      fn(rootBatch.bucket(options.bucket));
   2591    } else {
   2592      fn(rootBatch);
   2593    }
   2594    const responses = await this._batchRequests(rootBatch._requests, options);
   2595    if (options.aggregate) {
   2596      return aggregate(responses, rootBatch._requests);
   2597    }
   2598    return responses;
   2599  }
   2600  async execute(request, options = {}) {
   2601    const { raw = false, stringify = true } = options;
   2602    // If we're within a batch, add the request to the stack to send at once.
   2603    if (this._isBatch) {
   2604      this._requests.push(request);
   2605      // Resolve with a message in case people attempt at consuming the result
   2606      // from within a batch operation.
   2607      const msg =
   2608        "This result is generated from within a batch " +
   2609        "operation and should not be consumed.";
   2610      return raw ? { status: 0, json: msg, headers: new Headers() } : msg;
   2611    }
   2612    const uri = this.remote + addEndpointOptions(request.path, options);
   2613    const result = await this.http.request(
   2614      uri,
   2615      cleanUndefinedProperties({
   2616        // Limit requests to only those parts that would be allowed in
   2617        // a batch request -- don't pass through other fancy fetch()
   2618        // options like integrity, redirect, mode because they will
   2619        // break on a batch request.  A batch request only allows
   2620        // headers, method, path (above), and body.
   2621        method: request.method,
   2622        headers: request.headers,
   2623        body: stringify ? JSON.stringify(request.body) : request.body,
   2624      }),
   2625      { retry: this._getRetry(options) }
   2626    );
   2627    return raw ? result : result.json;
   2628  }
   2629  /**
   2630   * Perform an operation with a given HTTP method on some pages from
   2631   * a paginated list, following the `next-page` header automatically
   2632   * until we have processed the requested number of pages. Return a
   2633   * response with a `.next()` method that can be called to perform
   2634   * the requested HTTP method on more results.
   2635   *
   2636   * @private
   2637   * @param  {String}  path
   2638   *     The path to make the request to.
   2639   * @param  {Object}  params
   2640   *     The parameters to use when making the request.
   2641   * @param  {String}  [params.sort="-last_modified"]
   2642   *     The sorting order to use when doing operation on pages.
   2643   * @param  {Object}  [params.filters={}]
   2644   *     The filters to send in the request.
   2645   * @param  {Number}  [params.limit=undefined]
   2646   *     The limit to send in the request. Undefined means no limit.
   2647   * @param  {Number}  [params.pages=undefined]
   2648   *     The number of pages to operate on. Undefined means one page. Pass
   2649   *     Infinity to operate on everything.
   2650   * @param  {String}  [params.since=undefined]
   2651   *     The ETag from which to start doing operation on pages.
   2652   * @param  {Array}   [params.fields]
   2653   *     Limit response to just some fields.
   2654   * @param  {Object}  [options={}]
   2655   *     Additional request-level parameters to use in all requests.
   2656   * @param  {Object}  [options.headers={}]
   2657   *     Headers to use during all requests.
   2658   * @param  {Number}  [options.retry=0]
   2659   *     Number of times to retry each request if the server responds
   2660   *     with Retry-After.
   2661   * @param  {String}  [options.method="GET"]
   2662   *     The method to use in the request.
   2663   */
   2664  async paginatedOperation(path, params = {}, options = {}) {
   2665    // FIXME: this is called even in batch requests, which doesn't
   2666    // make any sense (since all batch requests get a "dummy"
   2667    // response; see execute() above).
   2668    const { sort, filters, limit, pages, since, fields } = {
   2669      sort: "-last_modified",
   2670      ...params,
   2671    };
   2672    // Safety/Consistency check on ETag value.
   2673    if (since && typeof since !== "string") {
   2674      throw new Error(
   2675        `Invalid value for since (${since}), should be ETag value.`
   2676      );
   2677    }
   2678    const query = {
   2679      ...filters,
   2680      _sort: sort,
   2681      _limit: limit,
   2682      _since: since,
   2683    };
   2684    if (fields) {
   2685      query._fields = fields;
   2686    }
   2687    const querystring = qsify(query);
   2688    let results = [],
   2689      current = 0;
   2690    const next = async function (nextPage) {
   2691      if (!nextPage) {
   2692        throw new Error("Pagination exhausted.");
   2693      }
   2694      return processNextPage(nextPage);
   2695    };
   2696    const processNextPage = async (nextPage) => {
   2697      const { headers } = options;
   2698      return handleResponse(await this.http.request(nextPage, { headers }));
   2699    };
   2700    const pageResults = (results, nextPage, etag) => {
   2701      // ETag string is supposed to be opaque and stored «as-is».
   2702      // ETag header values are quoted (because of * and W/"foo").
   2703      return {
   2704        last_modified: etag ? etag.replace(/"/g, "") : etag,
   2705        data: results,
   2706        next: next.bind(null, nextPage),
   2707        hasNextPage: !!nextPage,
   2708        totalRecords: -1,
   2709      };
   2710    };
   2711    const handleResponse = async function ({
   2712      headers = new Headers(),
   2713      json = {},
   2714    }) {
   2715      const nextPage = headers.get("Next-Page");
   2716      const etag = headers.get("ETag");
   2717      if (!pages) {
   2718        return pageResults(json.data, nextPage, etag);
   2719      }
   2720      // Aggregate new results with previous ones
   2721      results = results.concat(json.data);
   2722      current += 1;
   2723      if (current >= pages || !nextPage) {
   2724        // Pagination exhausted
   2725        return pageResults(results, nextPage, etag);
   2726      }
   2727      // Follow next page
   2728      return processNextPage(nextPage);
   2729    };
   2730    return handleResponse(
   2731      await this.execute(
   2732        // N.B.: This doesn't use _getHeaders, because all calls to
   2733        // `paginatedList` are assumed to come from calls that already
   2734        // have headers merged at e.g. the bucket or collection level.
   2735        {
   2736          headers: options.headers ? options.headers : {},
   2737          path: path + "?" + querystring,
   2738          method: options.method,
   2739        },
   2740        // N.B. This doesn't use _getRetry, because all calls to
   2741        // `paginatedList` are assumed to come from calls that already
   2742        // used `_getRetry` at e.g. the bucket or collection level.
   2743        { raw: true, retry: options.retry || 0 }
   2744      )
   2745    );
   2746  }
   2747  /**
   2748   * Fetch some pages from a paginated list, following the `next-page`
   2749   * header automatically until we have fetched the requested number
   2750   * of pages. Return a response with a `.next()` method that can be
   2751   * called to fetch more results.
   2752   *
   2753   * @private
   2754   * @param  {String}  path
   2755   *     The path to make the request to.
   2756   * @param  {Object}  params
   2757   *     The parameters to use when making the request.
   2758   * @param  {String}  [params.sort="-last_modified"]
   2759   *     The sorting order to use when fetching.
   2760   * @param  {Object}  [params.filters={}]
   2761   *     The filters to send in the request.
   2762   * @param  {Number}  [params.limit=undefined]
   2763   *     The limit to send in the request. Undefined means no limit.
   2764   * @param  {Number}  [params.pages=undefined]
   2765   *     The number of pages to fetch. Undefined means one page. Pass
   2766   *     Infinity to fetch everything.
   2767   * @param  {String}  [params.since=undefined]
   2768   *     The ETag from which to start fetching.
   2769   * @param  {Array}   [params.fields]
   2770   *     Limit response to just some fields.
   2771   * @param  {Object}  [options={}]
   2772   *     Additional request-level parameters to use in all requests.
   2773   * @param  {Object}  [options.headers={}]
   2774   *     Headers to use during all requests.
   2775   * @param  {Number}  [options.retry=0]
   2776   *     Number of times to retry each request if the server responds
   2777   *     with Retry-After.
   2778   */
   2779  async paginatedList(path, params = {}, options = {}) {
   2780    return this.paginatedOperation(path, params, options);
   2781  }
   2782  /**
   2783   * Delete multiple objects, following the pagination if the number of
   2784   * objects exceeds the page limit until we have deleted the requested
   2785   * number of pages. Return a response with a `.next()` method that can
   2786   * be called to delete more results.
   2787   *
   2788   * @private
   2789   * @param  {String}  path
   2790   *     The path to make the request to.
   2791   * @param  {Object}  params
   2792   *     The parameters to use when making the request.
   2793   * @param  {String}  [params.sort="-last_modified"]
   2794   *     The sorting order to use when deleting.
   2795   * @param  {Object}  [params.filters={}]
   2796   *     The filters to send in the request.
   2797   * @param  {Number}  [params.limit=undefined]
   2798   *     The limit to send in the request. Undefined means no limit.
   2799   * @param  {Number}  [params.pages=undefined]
   2800   *     The number of pages to delete. Undefined means one page. Pass
   2801   *     Infinity to delete everything.
   2802   * @param  {String}  [params.since=undefined]
   2803   *     The ETag from which to start deleting.
   2804   * @param  {Array}   [params.fields]
   2805   *     Limit response to just some fields.
   2806   * @param  {Object}  [options={}]
   2807   *     Additional request-level parameters to use in all requests.
   2808   * @param  {Object}  [options.headers={}]
   2809   *     Headers to use during all requests.
   2810   * @param  {Number}  [options.retry=0]
   2811   *     Number of times to retry each request if the server responds
   2812   *     with Retry-After.
   2813   */
   2814  paginatedDelete(path, params = {}, options = {}) {
   2815    const { headers, safe, last_modified } = options;
   2816    const deleteRequest$1 = deleteRequest(path, {
   2817      headers,
   2818      safe: safe ? safe : false,
   2819      last_modified,
   2820    });
   2821    return this.paginatedOperation(path, params, {
   2822      ...options,
   2823      headers: deleteRequest$1.headers,
   2824      method: "DELETE",
   2825    });
   2826  }
   2827  /**
   2828   * Lists all permissions.
   2829   *
   2830   * @param  {Object} [options={}]      The options object.
   2831   * @param  {Object} [options.headers={}] Headers to use when making
   2832   *     this request.
   2833   * @param  {Number} [options.retry=0]    Number of retries to make
   2834   *     when faced with transient errors.
   2835   * @return {Promise<Object[], Error>}
   2836   */
   2837  async listPermissions(options = {}) {
   2838    const path = ENDPOINTS.permissions();
   2839    // Ensure the default sort parameter is something that exists in permissions
   2840    // entries, as `last_modified` doesn't; here, we pick "id".
   2841    const paginationOptions = { sort: "id", ...options };
   2842    return this.paginatedList(path, paginationOptions, {
   2843      headers: this._getHeaders(options),
   2844      retry: this._getRetry(options),
   2845    });
   2846  }
   2847  /**
   2848   * Retrieves the list of buckets.
   2849   *
   2850   * @param  {Object} [options={}]      The options object.
   2851   * @param  {Object} [options.headers={}] Headers to use when making
   2852   *     this request.
   2853   * @param  {Number} [options.retry=0]    Number of retries to make
   2854   *     when faced with transient errors.
   2855   * @param  {Object} [options.filters={}] The filters object.
   2856   * @param  {Array}  [options.fields]     Limit response to
   2857   *     just some fields.
   2858   * @return {Promise<Object[], Error>}
   2859   */
   2860  async listBuckets(options = {}) {
   2861    const path = ENDPOINTS.bucket();
   2862    return this.paginatedList(path, options, {
   2863      headers: this._getHeaders(options),
   2864      retry: this._getRetry(options),
   2865    });
   2866  }
   2867  /**
   2868   * Creates a new bucket on the server.
   2869   *
   2870   * @param  {String|null}  id                The bucket name (optional).
   2871   * @param  {Object}       [options={}]      The options object.
   2872   * @param  {Boolean}      [options.data]    The bucket data option.
   2873   * @param  {Boolean}      [options.safe]    The safe option.
   2874   * @param  {Object}       [options.headers] The headers object option.
   2875   * @param  {Number}       [options.retry=0] Number of retries to make
   2876   *     when faced with transient errors.
   2877   * @return {Promise<Object, Error>}
   2878   */
   2879  async createBucket(id, options = {}) {
   2880    const { data, permissions } = options;
   2881    const _data = { ...data, id: id ? id : undefined };
   2882    const path = _data.id ? ENDPOINTS.bucket(_data.id) : ENDPOINTS.bucket();
   2883    return this.execute(
   2884      createRequest(
   2885        path,
   2886        { data: _data, permissions },
   2887        {
   2888          headers: this._getHeaders(options),
   2889          safe: this._getSafe(options),
   2890        }
   2891      ),
   2892      { retry: this._getRetry(options) }
   2893    );
   2894  }
   2895  /**
   2896   * Deletes a bucket from the server.
   2897   *
   2898   * @ignore
   2899   * @param  {Object|String} bucket                  The bucket to delete.
   2900   * @param  {Object}        [options={}]            The options object.
   2901   * @param  {Boolean}       [options.safe]          The safe option.
   2902   * @param  {Object}        [options.headers]       The headers object option.
   2903   * @param  {Number}        [options.retry=0]       Number of retries to make
   2904   *     when faced with transient errors.
   2905   * @param  {Number}        [options.last_modified] The last_modified option.
   2906   * @return {Promise<Object, Error>}
   2907   */
   2908  async deleteBucket(bucket, options = {}) {
   2909    const bucketObj = toDataBody(bucket);
   2910    if (!bucketObj.id) {
   2911      throw new Error("A bucket id is required.");
   2912    }
   2913    const path = ENDPOINTS.bucket(bucketObj.id);
   2914    const { last_modified } = { ...bucketObj, ...options };
   2915    return this.execute(
   2916      deleteRequest(path, {
   2917        last_modified,
   2918        headers: this._getHeaders(options),
   2919        safe: this._getSafe(options),
   2920      }),
   2921      { retry: this._getRetry(options) }
   2922    );
   2923  }
   2924  /**
   2925   * Deletes buckets.
   2926   *
   2927   * @param  {Object} [options={}]             The options object.
   2928   * @param  {Boolean} [options.safe]          The safe option.
   2929   * @param  {Object} [options.headers={}]     Headers to use when making
   2930   *     this request.
   2931   * @param  {Number} [options.retry=0]        Number of retries to make
   2932   *     when faced with transient errors.
   2933   * @param  {Object} [options.filters={}]     The filters object.
   2934   * @param  {Array}  [options.fields]         Limit response to
   2935   *     just some fields.
   2936   * @param  {Number}  [options.last_modified] The last_modified option.
   2937   * @return {Promise<Object[], Error>}
   2938   */
   2939  async deleteBuckets(options = {}) {
   2940    const path = ENDPOINTS.bucket();
   2941    return this.paginatedDelete(path, options, {
   2942      headers: this._getHeaders(options),
   2943      retry: this._getRetry(options),
   2944      safe: options.safe,
   2945      last_modified: options.last_modified,
   2946    });
   2947  }
   2948  async createAccount(username, password) {
   2949    return this.execute(
   2950      createRequest(
   2951        `/accounts/${username}`,
   2952        { data: { password } },
   2953        { method: "PUT" }
   2954      )
   2955    );
   2956  }
   2957 }
   2958 __decorate(
   2959  [nobatch("This operation is not supported within a batch operation.")],
   2960  KintoClientBase.prototype,
   2961  "fetchServerSettings",
   2962  null
   2963 );
   2964 __decorate(
   2965  [nobatch("This operation is not supported within a batch operation.")],
   2966  KintoClientBase.prototype,
   2967  "fetchServerCapabilities",
   2968  null
   2969 );
   2970 __decorate(
   2971  [nobatch("This operation is not supported within a batch operation.")],
   2972  KintoClientBase.prototype,
   2973  "fetchUser",
   2974  null
   2975 );
   2976 __decorate(
   2977  [nobatch("This operation is not supported within a batch operation.")],
   2978  KintoClientBase.prototype,
   2979  "fetchHTTPApiVersion",
   2980  null
   2981 );
   2982 __decorate(
   2983  [nobatch("Can't use batch within a batch!")],
   2984  KintoClientBase.prototype,
   2985  "batch",
   2986  null
   2987 );
   2988 __decorate(
   2989  [capable(["permissions_endpoint"])],
   2990  KintoClientBase.prototype,
   2991  "listPermissions",
   2992  null
   2993 );
   2994 __decorate(
   2995  [support("1.4", "2.0")],
   2996  KintoClientBase.prototype,
   2997  "deleteBuckets",
   2998  null
   2999 );
   3000 __decorate(
   3001  [capable(["accounts"])],
   3002  KintoClientBase.prototype,
   3003  "createAccount",
   3004  null
   3005 );
   3006 
   3007 /*
   3008 *
   3009 * Licensed under the Apache License, Version 2.0 (the "License");
   3010 * you may not use this file except in compliance with the License.
   3011 * You may obtain a copy of the License at
   3012 *
   3013 *     http://www.apache.org/licenses/LICENSE-2.0
   3014 *
   3015 * Unless required by applicable law or agreed to in writing, software
   3016 * distributed under the License is distributed on an "AS IS" BASIS,
   3017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   3018 * See the License for the specific language governing permissions and
   3019 * limitations under the License.
   3020 */
   3021 /* @ts-ignore */
   3022 class KintoHttpClient extends KintoClientBase {
   3023  constructor(remote, options = {}) {
   3024    const events = {};
   3025    EventEmitter.decorate(events);
   3026    super(remote, { events: events, ...options });
   3027  }
   3028 }
   3029 KintoHttpClient.errors = errors;
   3030 
   3031 export { KintoHttpClient };