UNPKG

kinto

Version:

An Offline-First JavaScript client for Kinto.

5,872 lines 204 kB
(function (global, factory) {
  typeof exports === "object" && typeof module !== "undefined"
    ? factory(exports)
    : typeof define === "function" && define.amd
      ? define(["exports"], factory)
      : ((global =
          typeof globalThis !== "undefined" ? globalThis : global || self),
        factory((global.Kinto = {})));
})(this, function (exports) {
  "use strict";

  /******************************************************************************
    Copyright (c) Microsoft Corporation.

    Permission to use, copy, modify, and/or distribute this software for any
    purpose with or without fee is hereby granted.

    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
    REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
    INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
    LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
    OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
    PERFORMANCE OF THIS SOFTWARE.
    ***************************************************************************** */
  /* global Reflect, Promise, SuppressedError, Symbol, Iterator */

  function __decorate(decorators, target, key, desc) {
    var c = arguments.length,
      r =
        c < 3
          ? target
          : desc === null
            ? (desc = Object.getOwnPropertyDescriptor(target, key))
            : desc,
      d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return (c > 3 && r && Object.defineProperty(target, key, r), r);
  }

  typeof SuppressedError === "function"
    ? SuppressedError
    : function (error, suppressed, message) {
        var e = new Error(message);
        return (
          (e.name = "SuppressedError"),
          (e.error = error),
          (e.suppressed = suppressed),
          e
        );
      };

  const RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
  /**
   * Checks if a value is undefined.
   * @param  {Any}  value
   * @return {Boolean}
   */
  function _isUndefined(value) {
    return typeof value === "undefined";
  }
  /**
   * Sorts records in a list according to a given ordering.
   *
   * @param  {String} order The ordering, eg. `-last_modified`.
   * @param  {Array}  list  The collection to order.
   * @return {Array}
   */
  function sortObjects(order, list) {
    const hasDash = order[0] === "-";
    const field = hasDash ? order.slice(1) : order;
    const direction = hasDash ? -1 : 1;
    return list.slice().sort((a, b) => {
      if (a[field] && _isUndefined(b[field])) {
        return direction;
      }
      if (b[field] && _isUndefined(a[field])) {
        return -direction;
      }
      if (_isUndefined(a[field]) && _isUndefined(b[field])) {
        return 0;
      }
      return a[field] > b[field] ? direction : -direction;
    });
  }
  /**
   * Test if a single object matches all given filters.
   *
   * @param  {Object} filters  The filters object.
   * @param  {Object} entry    The object to filter.
   * @return {Boolean}
   */
  function filterObject(filters, entry) {
    return Object.keys(filters).every((filter) => {
      const value = filters[filter];
      if (Array.isArray(value)) {
        return value.some((candidate) => candidate === entry[filter]);
      } else if (typeof value === "object") {
        return filterObject(value, entry[filter]);
      } else if (!Object.prototype.hasOwnProperty.call(entry, filter)) {
        console.error(`The property ${filter} does not exist`);
        return false;
      }
      return entry[filter] === value;
    });
  }
  /**
   * Resolves a list of functions sequentially, which can be sync or async; in
   * case of async, functions must return a promise.
   *
   * @param  {Array} fns  The list of functions.
   * @param  {Any}   init The initial value.
   * @return {Promise}
   */
  function waterfall(fns, init) {
    if (!fns.length) {
      return Promise.resolve(init);
    }
    return fns.reduce((promise, nextFn) => {
      return promise.then(nextFn);
    }, Promise.resolve(init));
  }
  /**
   * Simple deep object comparison function. This only supports comparison of
   * serializable JavaScript objects.
   *
   * @param  {Object} a The source object.
   * @param  {Object} b The compared object.
   * @return {Boolean}
   */
  function deepEqual(a, b) {
    if (a === b) {
      return true;
    }
    if (typeof a !== typeof b) {
      return false;
    }
    if (!(a && typeof a === "object") || !(b && typeof b === "object")) {
      return false;
    }
    if (Object.keys(a).length !== Object.keys(b).length) {
      return false;
    }
    for (const k in a) {
      if (!deepEqual(a[k], b[k])) {
        return false;
      }
    }
    return true;
  }
  /**
   * Return an object without the specified keys.
   *
   * @param  {Object} obj        The original object.
   * @param  {Array}  keys       The list of keys to exclude.
   * @return {Object}            A copy without the specified keys.
   */
  function omitKeys(obj, keys = []) {
    const result = Object.assign({}, obj);
    for (const key of keys) {
      delete result[key];
    }
    return result;
  }
  function arrayEqual(a, b) {
    if (a.length !== b.length) {
      return false;
    }
    for (let i = a.length; i--; ) {
      if (a[i] !== b[i]) {
        return false;
      }
    }
    return true;
  }
  function makeNestedObjectFromArr(arr, val, nestedFiltersObj) {
    const last = arr.length - 1;
    return arr.reduce((acc, cv, i) => {
      if (i === last) {
        return (acc[cv] = val);
      } else if (Object.prototype.hasOwnProperty.call(acc, cv)) {
        return acc[cv];
      }
      return (acc[cv] = {});
    }, nestedFiltersObj);
  }
  function transformSubObjectFilters(filtersObj) {
    const transformedFilters = {};
    for (const key in filtersObj) {
      const keysArr = key.split(".");
      const val = filtersObj[key];
      makeNestedObjectFromArr(keysArr, val, transformedFilters);
    }
    return transformedFilters;
  }
  /**
   * Deeply access an object's properties
   * @param obj - The object whose property you want to compare
   * @param key - A dot notation path to the property you want to compare
   */
  function getDeepKey(obj, key) {
    const segments = key.split(".");
    let result = obj;
    for (let p = 0; p < segments.length; p++) {
      result = result ? result[segments[p]] : undefined;
    }
    return result !== null && result !== void 0 ? result : undefined;
  }
  /**
   * Chunks an array into n pieces.
   *
   * @private
   * @param  {Array}  array
   * @param  {Number} n
   * @return {Array}
   */
  function partition(array, n) {
    if (n <= 0) {
      return [array];
    }
    return array.reduce((acc, x, i) => {
      if (i === 0 || i % n === 0) {
        acc.push([x]);
      } else {
        acc[acc.length - 1].push(x);
      }
      return acc;
    }, []);
  }
  /**
   * Returns a Promise always resolving after the specified amount in milliseconds.
   *
   * @return Promise<void>
   */
  function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  /**
   * Always returns a resource data object from the provided argument.
   *
   * @private
   * @param  {Object|String} resource
   * @return {Object}
   */
  function toDataBody(resource) {
    if (isObject(resource)) {
      return resource;
    }
    if (typeof resource === "string") {
      return { id: resource };
    }
    throw new Error("Invalid argument.");
  }
  /**
   * Transforms an object into an URL query string, stripping out any undefined
   * values.
   *
   * @param  {Object} obj
   * @return {String}
   */
  function qsify(obj) {
    const encode = (v) =>
      encodeURIComponent(typeof v === "boolean" ? String(v) : v);
    const stripped = cleanUndefinedProperties(obj);
    return Object.keys(stripped)
      .map((k) => {
        const ks = encode(k) + "=";
        if (Array.isArray(stripped[k])) {
          return ks + stripped[k].map((v) => encode(v)).join(",");
        }
        return ks + encode(stripped[k]);
      })
      .join("&");
  }
  /**
   * Checks if a version is within the provided range.
   *
   * @param  {String} version    The version to check.
   * @param  {String} minVersion The minimum supported version (inclusive).
   * @param  {String} maxVersion The minimum supported version (exclusive).
   * @throws {Error} If the version is outside of the provided range.
   */
  function checkVersion(version, minVersion, maxVersion) {
    const extract = (str) => str.split(".").map((x) => parseInt(x, 10));
    const [verMajor, verMinor] = extract(version);
    const [minMajor, minMinor] = extract(minVersion);
    const [maxMajor, maxMinor] = extract(maxVersion);
    const checks = [
      verMajor < minMajor,
      verMajor === minMajor && verMinor < minMinor,
      verMajor > maxMajor,
      verMajor === maxMajor && verMinor >= maxMinor,
    ];
    if (checks.some((x) => x)) {
      throw new Error(
        `Version ${version} doesn't satisfy ${minVersion} <= x < ${maxVersion}`
      );
    }
  }
  /**
   * Generates a decorator function ensuring a version check is performed against
   * the provided requirements before executing it.
   *
   * @param  {String} min The required min version (inclusive).
   * @param  {String} max The required max version (inclusive).
   * @return {Function}
   */
  function support(min, max) {
    return function (
      // @ts-ignore
      target,
      key,
      descriptor
    ) {
      const fn = descriptor.value;
      return {
        configurable: true,
        get() {
          const wrappedMethod = (...args) => {
            // "this" is the current instance which its method is decorated.
            const client = this.client ? this.client : this;
            return client
              .fetchHTTPApiVersion()
              .then((version) => checkVersion(version, min, max))
              .then(() => fn.apply(this, args));
          };
          Object.defineProperty(this, key, {
            value: wrappedMethod,
            configurable: true,
            writable: true,
          });
          return wrappedMethod;
        },
      };
    };
  }
  /**
   * Generates a decorator function ensuring that the specified capabilities are
   * available on the server before executing it.
   *
   * @param  {Array<String>} capabilities The required capabilities.
   * @return {Function}
   */
  function capable(capabilities) {
    return function (
      // @ts-ignore
      target,
      key,
      descriptor
    ) {
      const fn = descriptor.value;
      return {
        configurable: true,
        get() {
          const wrappedMethod = (...args) => {
            // "this" is the current instance which its method is decorated.
            const client = this.client ? this.client : this;
            return client
              .fetchServerCapabilities()
              .then((available) => {
                const missing = capabilities.filter((c) => !(c in available));
                if (missing.length) {
                  const missingStr = missing.join(", ");
                  throw new Error(
                    `Required capabilities ${missingStr} not present on server`
                  );
                }
              })
              .then(() => fn.apply(this, args));
          };
          Object.defineProperty(this, key, {
            value: wrappedMethod,
            configurable: true,
            writable: true,
          });
          return wrappedMethod;
        },
      };
    };
  }
  /**
   * Generates a decorator function ensuring an operation is not performed from
   * within a batch request.
   *
   * @param  {String} message The error message to throw.
   * @return {Function}
   */
  function nobatch(message) {
    return function (
      // @ts-ignore
      target,
      key,
      descriptor
    ) {
      const fn = descriptor.value;
      return {
        configurable: true,
        get() {
          const wrappedMethod = (...args) => {
            // "this" is the current instance which its method is decorated.
            if (this._isBatch) {
              throw new Error(message);
            }
            return fn.apply(this, args);
          };
          Object.defineProperty(this, key, {
            value: wrappedMethod,
            configurable: true,
            writable: true,
          });
          return wrappedMethod;
        },
      };
    };
  }
  /**
   * Returns true if the specified value is an object (i.e. not an array nor null).
   * @param  {Object} thing The value to inspect.
   * @return {bool}
   */
  function isObject(thing) {
    return typeof thing === "object" && thing !== null && !Array.isArray(thing);
  }
  /**
   * Parses a data url.
   * @param  {String} dataURL The data url.
   * @return {Object}
   */
  function parseDataURL(dataURL) {
    const regex = /^data:(.*);base64,(.*)/;
    const match = dataURL.match(regex);
    if (!match) {
      throw new Error(
        `Invalid data-url: ${String(dataURL).substring(0, 32)}...`
      );
    }
    const props = match[1];
    const base64 = match[2];
    const [type, ...rawParams] = props.split(";");
    const params = rawParams.reduce((acc, param) => {
      const [key, value] = param.split("=");
      return Object.assign(Object.assign({}, acc), { [key]: value });
    }, {});
    return Object.assign(Object.assign({}, params), { type, base64 });
  }
  /**
   * Extracts file information from a data url.
   * @param  {String} dataURL The data url.
   * @return {Object}
   */
  function extractFileInfo(dataURL) {
    const { name, type, base64 } = parseDataURL(dataURL);
    const binary = atob(base64);
    const array = [];
    for (let i = 0; i < binary.length; i++) {
      array.push(binary.charCodeAt(i));
    }
    const blob = new Blob([new Uint8Array(array)], { type });
    return { blob, name };
  }
  /**
   * Creates a FormData instance from a data url and an existing JSON response
   * body.
   * @param  {String} dataURL            The data url.
   * @param  {Object} body               The response body.
   * @param  {Object} [options={}]       The options object.
   * @param  {Object} [options.filename] Force attachment file name.
   * @return {FormData}
   */
  function createFormData(dataURL, body, options = {}) {
    const { filename = "untitled" } = options;
    const { blob, name } = extractFileInfo(dataURL);
    const formData = new FormData();
    formData.append("attachment", blob, name || filename);
    for (const property in body) {
      if (typeof body[property] !== "undefined") {
        formData.append(property, JSON.stringify(body[property]));
      }
    }
    return formData;
  }
  /**
   * Clones an object with all its undefined keys removed.
   * @private
   */
  function cleanUndefinedProperties(obj) {
    const result = {};
    for (const key in obj) {
      if (typeof obj[key] !== "undefined") {
        result[key] = obj[key];
      }
    }
    return result;
  }
  /**
   * Handle common query parameters for Kinto requests.
   *
   * @param  {String}  [path]  The endpoint base path.
   * @param  {Array}   [options.fields]    Fields to limit the
   *   request to.
   * @param  {Object}  [options.query={}]  Additional query arguments.
   */
  function addEndpointOptions(path, options = {}) {
    const query = Object.assign({}, options.query);
    if (options.fields) {
      query._fields = options.fields;
    }
    const queryString = qsify(query);
    if (queryString) {
      return path + "?" + queryString;
    }
    return path;
  }
  /**
   * Replace authorization header with an obscured version
   */
  function obscureAuthorizationHeader(headers) {
    const h = new Headers(headers);
    if (h.has("authorization")) {
      h.set("authorization", "**** (suppressed)");
    }
    const obscuredHeaders = {};
    for (const [header, value] of h.entries()) {
      obscuredHeaders[header] = value;
    }
    return obscuredHeaders;
  }

  /**
   * Kinto server error code descriptors.
   */
  const ERROR_CODES = {
    104: "Missing Authorization Token",
    105: "Invalid Authorization Token",
    106: "Request body was not valid JSON",
    107: "Invalid request parameter",
    108: "Missing request parameter",
    109: "Invalid posted data",
    110: "Invalid Token / id",
    111: "Missing Token / id",
    112: "Content-Length header was not provided",
    113: "Request body too large",
    114: "Resource was created, updated or deleted meanwhile",
    115: "Method not allowed on this end point (hint: server may be readonly)",
    116: "Requested version not available on this server",
    117: "Client has sent too many requests",
    121: "Resource access is forbidden for this user",
    122: "Another resource violates constraint",
    201: "Service Temporary unavailable due to high load",
    202: "Service deprecated",
    999: "Internal Server Error",
  };
  class NetworkTimeoutError extends Error {
    constructor(url, options) {
      super(
        `Timeout while trying to access ${url} with ${JSON.stringify(options)}`
      );
      if (Error.captureStackTrace) {
        Error.captureStackTrace(this, NetworkTimeoutError);
      }
      this.url = url;
      this.options = options;
    }
  }
  class UnparseableResponseError extends Error {
    constructor(response, body, error) {
      const { status } = response;
      super(
        `Response from server unparseable (HTTP ${status || 0}; ${error}): ${body}`
      );
      if (Error.captureStackTrace) {
        Error.captureStackTrace(this, UnparseableResponseError);
      }
      this.status = status;
      this.response = response;
      this.stack = error.stack;
      this.error = error;
    }
  }
  /**
   * "Error" subclass representing a >=400 response from the server.
   *
   * Whether or not this is an error depends on your application.
   *
   * The `json` field can be undefined if the server responded with an
   * empty response body. This shouldn't generally happen. Most "bad"
   * responses come with a JSON error description, or (if they're
   * fronted by a CDN or nginx or something) occasionally non-JSON
   * responses (which become UnparseableResponseErrors, above).
   */
  class ServerResponse extends Error {
    constructor(response, json) {
      const { status } = response;
      let { statusText } = response;
      let errnoMsg;
      if (json) {
        // Try to fill in information from the JSON error.
        statusText = json.error || statusText;
        // Take errnoMsg from either ERROR_CODES or json.message.
        if (json.errno && json.errno in ERROR_CODES) {
          errnoMsg = ERROR_CODES[json.errno];
        } else if (json.message) {
          errnoMsg = json.message;
        }
        // If we had both ERROR_CODES and json.message, and they differ,
        // combine them.
        if (errnoMsg && json.message && json.message !== errnoMsg) {
          errnoMsg += ` (${json.message})`;
        }
      }
      let message = `HTTP ${status} ${statusText}`;
      if (errnoMsg) {
        message += `: ${errnoMsg}`;
      }
      super(message.trim());
      if (Error.captureStackTrace) {
        Error.captureStackTrace(this, ServerResponse);
      }
      this.response = response;
      this.data = json;
    }
  }

  /**
   * Enhanced HTTP client for the Kinto protocol.
   * @private
   */
  class HTTP {
    /**
     * Default HTTP request headers applied to each outgoing request.
     *
     * @type {Object}
     */
    static get DEFAULT_REQUEST_HEADERS() {
      return {
        Accept: "application/json",
        "Content-Type": "application/json",
      };
    }
    /**
     * Default options.
     *
     * @type {Object}
     */
    static get defaultOptions() {
      return { timeout: null, requestMode: "cors" };
    }
    /**
     * Constructor.
     *
     * @param {EventEmitter} events                       The event handler.
     * @param {Object}       [options={}}                 The options object.
     * @param {Number}       [options.timeout=null]       The request timeout in ms, if any (default: `null`).
     * @param {String}       [options.requestMode="cors"] The HTTP request mode (default: `"cors"`).
     */
    constructor(events, options = {}) {
      // public properties
      /**
       * The event emitter instance.
       * @type {EventEmitter}
       */
      this.events = events;
      /**
       * The request mode.
       * @see  https://fetch.spec.whatwg.org/#requestmode
       * @type {String}
       */
      this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode;
      /**
       * The request timeout.
       * @type {Number}
       */
      this.timeout = options.timeout || HTTP.defaultOptions.timeout;
      /**
       * The fetch() function.
       * @type {Function}
       */
      this.fetchFunc = options.fetchFunc || globalThis.fetch.bind(globalThis);
    }
    /**
     * @private
     */
    timedFetch(url, options) {
      let hasTimedout = false;
      return new Promise((resolve, reject) => {
        // Detect if a request has timed out.
        let _timeoutId;
        if (this.timeout) {
          _timeoutId = setTimeout(() => {
            hasTimedout = true;
            if (options && options.headers) {
              options = Object.assign(Object.assign({}, options), {
                headers: obscureAuthorizationHeader(options.headers),
              });
            }
            reject(new NetworkTimeoutError(url, options));
          }, this.timeout);
        }
        function proceedWithHandler(fn) {
          return (arg) => {
            if (!hasTimedout) {
              if (_timeoutId) {
                clearTimeout(_timeoutId);
              }
              fn(arg);
            }
          };
        }
        this.fetchFunc(url, options)
          .then(proceedWithHandler(resolve))
          .catch(proceedWithHandler(reject));
      });
    }
    /**
     * @private
     */
    async processResponse(response) {
      const { status, headers } = response;
      const text = await response.text();
      // Check if we have a body; if so parse it as JSON.
      let json;
      if (text.length !== 0) {
        try {
          json = JSON.parse(text);
        } catch (err) {
          throw new UnparseableResponseError(response, text, err);
        }
      }
      if (status >= 400) {
        throw new ServerResponse(response, json);
      }
      return { status, json: json, headers };
    }
    /**
     * @private
     */
    async retry(url, retryAfter, request, options) {
      await delay(retryAfter);
      return this.request(
        url,
        request,
        Object.assign(Object.assign({}, options), { retry: options.retry - 1 })
      );
    }
    /**
     * Performs an HTTP request to the Kinto server.
     *
     * Resolves with an objet containing the following HTTP response properties:
     * - `{Number}  status`  The HTTP status code.
     * - `{Object}  json`    The JSON response body.
     * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
     *
     * @param  {String} url               The URL.
     * @param  {Object} [request={}]      The request object, passed to
     *     fetch() as its options object.
     * @param  {Object} [request.headers] The request headers object (default: {})
     * @param  {Object} [options={}]      Options for making the
     *     request
     * @param  {Number} [options.retry]   Number of retries (default: 0)
     * @return {Promise}
     */
    async request(url, request = { headers: {} }, options = { retry: 0 }) {
      // Ensure default request headers are always set
      request.headers = Object.assign(
        Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS),
        request.headers
      );
      // If a multipart body is provided, remove any custom Content-Type header as
      // the fetch() implementation will add the correct one for us.
      if (request.body && request.body instanceof FormData) {
        if (request.headers instanceof Headers) {
          request.headers.delete("Content-Type");
        } else if (!Array.isArray(request.headers)) {
          delete request.headers["Content-Type"];
        }
      }
      request.mode = this.requestMode;
      const response = await this.timedFetch(url, request);
      const { headers } = response;
      this._checkForDeprecationHeader(headers);
      this._checkForBackoffHeader(headers);
      // Check if the server summons the client to retry after a while.
      const retryAfter = this._checkForRetryAfterHeader(headers);
      // If number of allowed of retries is not exhausted, retry the same request.
      if (retryAfter && options.retry > 0) {
        return this.retry(url, retryAfter, request, options);
      }
      return this.processResponse(response);
    }
    _checkForDeprecationHeader(headers) {
      const alertHeader = headers.get("Alert");
      if (!alertHeader) {
        return;
      }
      let alert;
      try {
        alert = JSON.parse(alertHeader);
      } catch (err) {
        console.warn("Unable to parse Alert header message", alertHeader);
        return;
      }
      console.warn(alert.message, alert.url);
      if (this.events) {
        this.events.emit("deprecated", alert);
      }
    }
    _checkForBackoffHeader(headers) {
      let backoffMs;
      const backoffHeader = headers.get("Backoff");
      const backoffSeconds = backoffHeader ? parseInt(backoffHeader, 10) : 0;
      if (backoffSeconds > 0) {
        backoffMs = new Date().getTime() + backoffSeconds * 1000;
      } else {
        backoffMs = 0;
      }
      if (this.events) {
        this.events.emit("backoff", backoffMs);
      }
    }
    _checkForRetryAfterHeader(headers) {
      const retryAfter = headers.get("Retry-After");
      if (!retryAfter) {
        return null;
      }
      const delay = parseInt(retryAfter, 10) * 1000;
      const tryAgainAfter = new Date().getTime() + delay;
      if (this.events) {
        this.events.emit("retry-after", tryAgainAfter);
      }
      return delay;
    }
  }

  /**
   * Endpoints templates.
   * @type {Object}
   */
  const ENDPOINTS = {
    root: () => "/",
    batch: () => "/batch",
    permissions: () => "/permissions",
    bucket: (bucket) => "/buckets" + (bucket ? `/${bucket}` : ""),
    history: (bucket) => `${ENDPOINTS.bucket(bucket)}/history`,
    snapshot: (bucket, coll, ts) =>
      `${ENDPOINTS.bucket(bucket)}/snapshot/collections/${coll}@${ts}`,
    collection: (bucket, coll) =>
      `${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""),
    group: (bucket, group) =>
      `${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""),
    record: (bucket, coll, id) =>
      `${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""),
    attachment: (bucket, coll, id) =>
      `${ENDPOINTS.record(bucket, coll, id)}/attachment`,
  };

  const requestDefaults = {
    safe: false,
    // check if we should set default content type here
    headers: {},
    patch: false,
  };
  /**
   * @private
   */
  function safeHeader(safe, last_modified) {
    if (!safe) {
      return {};
    }
    if (last_modified) {
      return { "If-Match": `"${last_modified}"` };
    }
    return { "If-None-Match": "*" };
  }
  /**
   * @private
   */
  function createRequest(path, { data, permissions }, options = {}) {
    const { headers, safe } = Object.assign(
      Object.assign({}, requestDefaults),
      options
    );
    const method = options.method || (data && data.id) ? "PUT" : "POST";
    return {
      method,
      path,
      headers: Object.assign(Object.assign({}, headers), safeHeader(safe)),
      body: { data, permissions },
    };
  }
  /**
   * @private
   */
  function updateRequest(path, { data, permissions }, options = {}) {
    const { headers, safe, patch } = Object.assign(
      Object.assign({}, requestDefaults),
      options
    );
    const { last_modified } = Object.assign(Object.assign({}, data), options);
    const hasNoData =
      data &&
      Object.keys(data).filter((k) => k !== "id" && k !== "last_modified")
        .length === 0;
    if (hasNoData) {
      data = undefined;
    }
    return {
      method: patch ? "PATCH" : "PUT",
      path,
      headers: Object.assign(
        Object.assign({}, headers),
        safeHeader(safe, last_modified)
      ),
      body: { data, permissions },
    };
  }
  /**
   * @private
   */
  function jsonPatchPermissionsRequest(
    path,
    permissions,
    opType,
    options = {}
  ) {
    const { headers, safe, last_modified } = Object.assign(
      Object.assign({}, requestDefaults),
      options
    );
    const ops = [];
    for (const [type, principals] of Object.entries(permissions)) {
      if (principals) {
        for (const principal of principals) {
          ops.push({
            op: opType,
            path: `/permissions/${type}/${principal}`,
          });
        }
      }
    }
    return {
      method: "PATCH",
      path,
      headers: Object.assign(
        Object.assign(
          Object.assign({}, headers),
          safeHeader(safe, last_modified)
        ),
        { "Content-Type": "application/json-patch+json" }
      ),
      body: ops,
    };
  }
  /**
   * @private
   */
  function deleteRequest(path, options = {}) {
    const { headers, safe, last_modified } = Object.assign(
      Object.assign({}, requestDefaults),
      options
    );
    if (safe && !last_modified) {
      throw new Error("Safe concurrency check requires a last_modified value.");
    }
    return {
      method: "DELETE",
      path,
      headers: Object.assign(
        Object.assign({}, headers),
        safeHeader(safe, last_modified)
      ),
    };
  }
  /**
   * @private
   */
  function addAttachmentRequest(
    path,
    dataURI,
    { data, permissions } = {},
    options = {}
  ) {
    const { headers, safe } = Object.assign(
      Object.assign({}, requestDefaults),
      options
    );
    const { last_modified } = Object.assign(Object.assign({}, data), options);
    const body = { data, permissions };
    const formData = createFormData(dataURI, body, options);
    return {
      method: "POST",
      path,
      headers: Object.assign(
        Object.assign({}, headers),
        safeHeader(safe, last_modified)
      ),
      body: formData,
    };
  }

  /**
   * Exports batch responses as a result object.
   *
   * @private
   * @param  {Array} responses The batch subrequest responses.
   * @param  {Array} requests  The initial issued requests.
   * @return {Object}
   */
  function aggregate(responses = [], requests = []) {
    if (responses.length !== requests.length) {
      throw new Error("Responses length should match requests one.");
    }
    const results = {
      errors: [],
      published: [],
      conflicts: [],
      skipped: [],
    };
    return responses.reduce((acc, response, index) => {
      const { status } = response;
      const request = requests[index];
      if (status >= 200 && status < 400) {
        acc.published.push(response.body);
      } else if (status === 404) {
        // Extract the id manually from request path while waiting for Kinto/kinto#818
        const regex = /(buckets|groups|collections|records)\/([^/]+)$/;
        const extracts = request.path.match(regex);
        const id = extracts && extracts.length === 3 ? extracts[2] : undefined;
        acc.skipped.push({
          id,
          path: request.path,
          error: response.body,
        });
      } else if (status === 412) {
        acc.conflicts.push({
          // XXX: specifying the type is probably superfluous
          type: "outgoing",
          local: request.body,
          remote:
            (response.body.details && response.body.details.existing) || null,
        });
      } else {
        acc.errors.push({
          path: request.path,
          sent: request,
          error: response.body,
        });
      }
      return acc;
    }, results);
  }

  const byteToHex = [];
  for (let i = 0; i < 256; ++i) {
    byteToHex.push((i + 0x100).toString(16).slice(1));
  }
  function unsafeStringify(arr, offset = 0) {
    return (
      byteToHex[arr[offset + 0]] +
      byteToHex[arr[offset + 1]] +
      byteToHex[arr[offset + 2]] +
      byteToHex[arr[offset + 3]] +
      "-" +
      byteToHex[arr[offset + 4]] +
      byteToHex[arr[offset + 5]] +
      "-" +
      byteToHex[arr[offset + 6]] +
      byteToHex[arr[offset + 7]] +
      "-" +
      byteToHex[arr[offset + 8]] +
      byteToHex[arr[offset + 9]] +
      "-" +
      byteToHex[arr[offset + 10]] +
      byteToHex[arr[offset + 11]] +
      byteToHex[arr[offset + 12]] +
      byteToHex[arr[offset + 13]] +
      byteToHex[arr[offset + 14]] +
      byteToHex[arr[offset + 15]]
    ).toLowerCase();
  }

  let getRandomValues;
  const rnds8 = new Uint8Array(16);
  function rng() {
    if (!getRandomValues) {
      if (typeof crypto === "undefined" || !crypto.getRandomValues) {
        throw new Error(
          "crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"
        );
      }
      getRandomValues = crypto.getRandomValues.bind(crypto);
    }
    return getRandomValues(rnds8);
  }

  const randomUUID =
    typeof crypto !== "undefined" &&
    crypto.randomUUID &&
    crypto.randomUUID.bind(crypto);
  var native = { randomUUID };

  function _v4(options, buf, offset) {
    options = options || {};
    const rnds = options.random ?? options.rng?.() ?? rng();
    if (rnds.length < 16) {
      throw new Error("Random bytes length must be >= 16");
    }
    rnds[6] = (rnds[6] & 0x0f) | 0x40;
    rnds[8] = (rnds[8] & 0x3f) | 0x80;
    return unsafeStringify(rnds);
  }
  function v4(options, buf, offset) {
    if (native.randomUUID && true && !options) {
      return native.randomUUID();
    }
    return _v4(options);
  }

  /**
   * Abstract representation of a selected collection.
   *
   */
  let Collection$1 = class Collection {
    /**
     * Constructor.
     *
     * @param  {KintoClient}  client            The client instance.
     * @param  {Bucket}       bucket            The bucket instance.
     * @param  {String}       name              The collection name.
     * @param  {Object}       [options={}]      The options object.
     * @param  {Object}       [options.headers] The headers object option.
     * @param  {Boolean}      [options.safe]    The safe option.
     * @param  {Number}       [options.retry]   The retry option.
     * @param  {Boolean}      [options.batch]   (Private) Whether this
     *     Collection is operating as part of a batch.
     */
    constructor(client, bucket, name, options = {}) {
      /**
       * @ignore
       */
      this.client = client;
      /**
       * @ignore
       */
      this.bucket = bucket;
      /**
       * The collection name.
       * @type {String}
       */
      this.name = name;
      this._endpoints = client.endpoints;
      /**
       * @ignore
       */
      this._retry = options.retry || 0;
      this._safe = !!options.safe;
      // FIXME: This is kind of ugly; shouldn't the bucket be responsible
      // for doing the merge?
      this._headers = Object.assign(
        Object.assign({}, this.bucket.headers),
        options.headers
      );
    }
    get execute() {
      return this.client.execute.bind(this.client);
    }
    /**
     * Get the value of "headers" for a given request, merging the
     * per-request headers with our own "default" headers.
     *
     * @private
     */
    _getHeaders(options) {
      return Object.assign(Object.assign({}, this._headers), options.headers);
    }
    /**
     * Get the value of "safe" for a given request, using the
     * per-request option if present or falling back to our default
     * otherwise.
     *
     * @private
     * @param {Object} options The options for a request.
     * @returns {Boolean}
     */
    _getSafe(options) {
      return Object.assign({ safe: this._safe }, options).safe;
    }
    /**
     * As _getSafe, but for "retry".
     *
     * @private
     */
    _getRetry(options) {
      return Object.assign({ retry: this._retry }, options).retry;
    }
    /**
     * Retrieves the total number of records in this collection.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Number, Error>}
     */
    async getTotalRecords(options = {}) {
      const path = this._endpoints.record(this.bucket.name, this.name);
      const request = {
        headers: this._getHeaders(options),
        path,
        method: "HEAD",
      };
      const { headers } = await this.client.execute(request, {
        raw: true,
        retry: this._getRetry(options),
      });
      return parseInt(headers.get("Total-Records"), 10);
    }
    /**
     * Retrieves the ETag of the records list, for use with the `since` filtering option.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<String, Error>}
     */
    async getRecordsTimestamp(options = {}) {
      const path = this._endpoints.record(this.bucket.name, this.name);
      const request = {
        headers: this._getHeaders(options),
        path,
        method: "HEAD",
      };
      const { headers } = await this.client.execute(request, {
        raw: true,
        retry: this._getRetry(options),
      });
      return headers.get("ETag");
    }
    /**
     * Retrieves collection data.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Object} [options.query]   Query parameters to pass in
     *     the request. This might be useful for features that aren't
     *     yet supported by this library.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async getData(options = {}) {
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const request = { headers: this._getHeaders(options), path };
      const { data } = await this.client.execute(request, {
        retry: this._getRetry(options),
        query: options.query,
        fields: options.fields,
      });
      return data;
    }
    /**
     * Set collection data.
     * @param  {Object}   data                    The collection data object.
     * @param  {Object}   [options={}]            The options object.
     * @param  {Object}   [options.headers]       The headers object option.
     * @param  {Number}   [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean}  [options.safe]          The safe option.
     * @param  {Boolean}  [options.patch]         The patch option.
     * @param  {Number}   [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async setData(data, options = {}) {
      if (!isObject(data)) {
        throw new Error("A collection object is required.");
      }
      const { patch, permissions } = options;
      const { last_modified } = Object.assign(Object.assign({}, data), options);
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const request = updateRequest(
        path,
        { data, permissions },
        {
          last_modified,
          patch,
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of permissions for this collection.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async getPermissions(options = {}) {
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const request = { headers: this._getHeaders(options), path };
      const { permissions } = await this.client.execute(request, {
        retry: this._getRetry(options),
      });
      return permissions;
    }
    /**
     * Replaces all existing collection permissions with the ones provided.
     *
     * @param  {Object}   permissions             The permissions object.
     * @param  {Object}   [options={}]            The options object
     * @param  {Object}   [options.headers]       The headers object option.
     * @param  {Number}   [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean}  [options.safe]          The safe option.
     * @param  {Number}   [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async setPermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const data = { last_modified: options.last_modified };
      const request = updateRequest(
        path,
        { data, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Append principals to the collection permissions.
     *
     * @param  {Object}  permissions             The permissions object.
     * @param  {Object}  [options={}]            The options object
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async addPermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const { last_modified } = options;
      const request = jsonPatchPermissionsRequest(path, permissions, "add", {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Remove principals from the collection permissions.
     *
     * @param  {Object}  permissions             The permissions object.
     * @param  {Object}  [options={}]            The options object
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async removePermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.collection(this.bucket.name, this.name);
      const { last_modified } = options;
      const request = jsonPatchPermissionsRequest(path, permissions, "remove", {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Creates a record in current collection.
     *
     * @param  {Object}  record                The record to create.
     * @param  {Object}  [options={}]          The options object.
     * @param  {Object}  [options.headers]     The headers object option.
     * @param  {Number}  [options.retry=0]     Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean} [options.safe]        The safe option.
     * @param  {Object}  [options.permissions] The permissions option.
     * @return {Promise<Object, Error>}
     */
    async createRecord(record, options = {}) {
      const { permissions } = options;
      const path = this._endpoints.record(
        this.bucket.name,
        this.name,
        record.id
      );
      const request = createRequest(
        path,
        { data: record, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Adds an attachment to a record, creating the record when it doesn't exist.
     *
     * @param  {String}  dataURL                 The data url.
     * @param  {Object}  [record={}]             The record data.
     * @param  {Object}  [options={}]            The options object.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Number}  [options.last_modified] The last_modified option.
     * @param  {Object}  [options.permissions]   The permissions option.
     * @param  {String}  [options.filename]      Force the attachment filename.
     * @return {Promise<Object, Error>}
     */
    async addAttachment(dataURI, record = {}, options = {}) {
      const { permissions } = options;
      const id = record.id || v4();
      const path = this._endpoints.attachment(this.bucket.name, this.name, id);
      const { last_modified } = Object.assign(
        Object.assign({}, record),
        options
      );
      const addAttachmentRequest$1 = addAttachmentRequest(
        path,
        dataURI,
        { data: record, permissions },
        {
          last_modified,
          filename: options.filename,
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      await this.client.execute(addAttachmentRequest$1, {
        stringify: false,
        retry: this._getRetry(options),
      });
      return this.getRecord(id);
    }
    /**
     * Removes an attachment from a given record.
     *
     * @param  {Object}  recordId                The record id.
     * @param  {Object}  [options={}]            The options object.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Number}  [options.last_modified] The last_modified option.
     */
    async removeAttachment(recordId, options = {}) {
      const { last_modified } = options;
      const path = this._endpoints.attachment(
        this.bucket.name,
        this.name,
        recordId
      );
      const request = deleteRequest(path, {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Updates a record in current collection.
     *
     * @param  {Object}  record                  The record to update.
     * @param  {Object}  [options={}]            The options object.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Number}  [options.last_modified] The last_modified option.
     * @param  {Object}  [options.permissions]   The permissions option.
     * @return {Promise<Object, Error>}
     */
    async updateRecord(record, options = {}) {
      if (!isObject(record)) {
        throw new Error("A record object is required.");
      }
      if (!record.id) {
        throw new Error("A record id is required.");
      }
      const { permissions } = options;
      const { last_modified } = Object.assign(
        Object.assign({}, record),
        options
      );
      const path = this._endpoints.record(
        this.bucket.name,
        this.name,
        record.id
      );
      const request = updateRequest(
        path,
        { data: record, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
          last_modified,
          patch: !!options.patch,
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes a record from the current collection.
     *
     * @param  {Object|String} record                  The record to delete.
     * @param  {Object}        [options={}]            The options object.
     * @param  {Object}        [options.headers]       The headers object option.
     * @param  {Number}        [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean}       [options.safe]          The safe option.
     * @param  {Number}        [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async deleteRecord(record, options = {}) {
      const recordObj = toDataBody(record);
      if (!recordObj.id) {
        throw new Error("A record id is required.");
      }
      const { id } = recordObj;
      const { last_modified } = Object.assign(
        Object.assign({}, recordObj),
        options
      );
      const path = this._endpoints.record(this.bucket.name, this.name, id);
      const request = deleteRequest(path, {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes records from the current collection.
     *
     * Sorting is done by passing a `sort` string option:
     *
     * - The field to order the results by, prefixed with `-` for descending.
     * Default: `-last_modified`.
     *
     * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html
     *
     * Filtering is done by passing a `filters` option object:
     *
     * - `{fieldname: "value"}`
     * - `{min_fieldname: 4000}`
     * - `{in_fieldname: "1,2,3"}`
     * - `{not_fieldname: 0}`
     * - `{exclude_fieldname: "0,1"}`
     *
     * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html
     *
     * @param  {Object}   [options={}]                    The options object.
     * @param  {Object}   [options.headers]               The headers object option.
     * @param  {Number}   [options.retry=0]               Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}   [options.filters={}]            The filters object.
     * @param  {String}   [options.sort="-last_modified"] The sort field.
     * @param  {String}   [options.at]                    The timestamp to get a snapshot at.
     * @param  {String}   [options.limit=null]            The limit field.
     * @param  {String}   [options.pages=1]               The number of result pages to aggregate.
     * @param  {Number}   [options.since=null]            Only retrieve records modified since the provided timestamp.
     * @param  {Array}    [options.fields]                Limit response to just some fields.
     * @return {Promise<Object, Error>}
     */
    async deleteRecords(options = {}) {
      const path = this._endpoints.record(this.bucket.name, this.name);
      return this.client.paginatedDelete(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves a record from the current collection.
     *
     * @param  {String} id                The record id to retrieve.
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Object} [options.query]   Query parameters to pass in
     *     the request. This might be useful for features that aren't
     *     yet supported by this library.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async getRecord(id, options = {}) {
      const path = this._endpoints.record(this.bucket.name, this.name, id);
      const request = { headers: this._getHeaders(options), path };
      return this.client.execute(request, {
        retry: this._getRetry(options),
        query: options.query,
        fields: options.fields,
      });
    }
    /**
     * Lists records from the current collection.
     *
     * Sorting is done by passing a `sort` string option:
     *
     * - The field to order the results by, prefixed with `-` for descending.
     * Default: `-last_modified`.
     *
     * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html
     *
     * Filtering is done by passing a `filters` option object:
     *
     * - `{fieldname: "value"}`
     * - `{min_fieldname: 4000}`
     * - `{in_fieldname: "1,2,3"}`
     * - `{not_fieldname: 0}`
     * - `{exclude_fieldname: "0,1"}`
     *
     * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html
     *
     * Paginating is done by passing a `limit` option, then calling the `next()`
     * method from the resolved result object to fetch the next page, if any.
     *
     * @param  {Object}   [options={}]                    The options object.
     * @param  {Object}   [options.headers]               The headers object option.
     * @param  {Number}   [options.retry=0]               Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}   [options.filters={}]            The filters object.
     * @param  {String}   [options.sort="-last_modified"] The sort field.
     * @param  {String}   [options.at]                    The timestamp to get a snapshot at.
     * @param  {String}   [options.limit=null]            The limit field.
     * @param  {String}   [options.pages=1]               The number of result pages to aggregate.
     * @param  {Number}   [options.since=null]            Only retrieve records modified since the provided timestamp.
     * @param  {Array}    [options.fields]                Limit response to just some fields.
     * @return {Promise<Object, Error>}
     */
    async listRecords(options = {}) {
      const path = this._endpoints.record(this.bucket.name, this.name);
      if (options.at) {
        return this.getSnapshot(options.at);
      }
      return this.client.paginatedList(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * @private
     */
    async isHistoryComplete() {
      // We consider that if we have the collection creation event part of the
      // history, then all records change events have been tracked.
      const {
        data: [oldestHistoryEntry],
      } = await this.bucket.listHistory({
        limit: 1,
        filters: {
          action: "create",
          resource_name: "collection",
          collection_id: this.name,
        },
      });
      return !!oldestHistoryEntry;
    }
    /**
     * @private
     */
    async getSnapshot(at, options = {}) {
      if (!at || !Number.isInteger(at) || at <= 0) {
        throw new Error("Invalid argument, expected a positive integer.");
      }
      const path = this._endpoints.snapshot(this.bucket.name, this.name, at);
      const request = {
        headers: this._getHeaders(options),
        path,
        method: "GET",
      };
      let snapshot = null;
      try {
        const { json } = await this.client.execute(request, {
          raw: true,
          retry: this._getRetry(options),
        });
        snapshot = json;
      } catch (error) {
        if (!/404/.test(String(error))) {
          throw error;
        }
      }
      if (snapshot != null) {
        return {
          last_modified: String(at),
          data: snapshot.data,
          next: () => {
            throw new Error("Snapshots don't support pagination");
          },
          hasNextPage: false,
          totalRecords: snapshot.data.length,
        };
      }
      // Retrieve history and check it covers the required time range.
      // Ensure we have enough history data to retrieve the complete list of
      // changes.
      if (!(await this.isHistoryComplete())) {
        throw new Error(
          "Computing a snapshot is only possible when the full history for a " +
            "collection is available. Here, the history plugin seems to have " +
            "been enabled after the creation of the collection."
        );
      }
      // Because of https://github.com/Kinto/kinto-http.js/issues/963
      // we cannot simply rely on the history endpoint.
      // Our strategy here is to clean-up the history entries from the
      // records that were deleted via the plural endpoint.
      // We will detect them by comparing the current state of the collection
      // and the full history of the collection since its genesis.
      // List full history of collection.
      const { data: fullHistory } = await this.bucket.listHistory({
        pages: Infinity, // all pages up to target timestamp are required
        sort: "last_modified", // chronological order
        filters: {
          resource_name: "record",
          collection_id: this.name,
        },
      });
      // Keep latest entry ever, and latest within snapshot window.
      // (history is sorted chronologically)
      const latestEver = new Map();
      const latestInSnapshot = new Map();
      for (const entry of fullHistory) {
        if (entry.target.data.last_modified <= at) {
          // Snapshot includes changes right on timestamp.
          latestInSnapshot.set(entry.record_id, entry);
        }
        latestEver.set(entry.record_id, entry);
      }
      // Current records ids in the collection.
      const { data: current } = await this.listRecords({
        pages: Infinity,
        fields: ["id"], // we don't need attributes.
      });
      const currentIds = new Set(current.map((record) => record.id));
      // If a record is not in the current collection, and its
      // latest history entry isn't a delete then this means that
      // it was deleted via the plural endpoint (and that we lost track
      // of this deletion because of bug #963)
      const deletedViaPlural = new Set();
      for (const entry of latestEver.values()) {
        if (entry.action != "delete" && !currentIds.has(entry.record_id)) {
          deletedViaPlural.add(entry.record_id);
        }
      }
      // Now reconstruct the collection based on latest version in snapshot
      // filtering all deleted records.
      const reconstructed = [];
      for (const entry of latestInSnapshot.values()) {
        if (
          entry.action != "delete" &&
          !deletedViaPlural.has(entry.record_id)
        ) {
          reconstructed.push(entry.target.data);
        }
      }
      return {
        last_modified: String(at),
        data: Array.from(reconstructed).sort(
          (a, b) => b.last_modified - a.last_modified
        ),
        next: () => {
          throw new Error("Snapshots don't support pagination");
        },
        hasNextPage: false,
        totalRecords: reconstructed.length,
      };
    }
    /**
     * Performs batch operations at the current collection level.
     *
     * @param  {Function} fn                   The batch operation function.
     * @param  {Object}   [options={}]         The options object.
     * @param  {Object}   [options.headers]    The headers object option.
     * @param  {Boolean}  [options.safe]       The safe option.
     * @param  {Number}   [options.retry]      The retry option.
     * @param  {Boolean}  [options.aggregate]  Produces a grouped result object.
     * @return {Promise<Object, Error>}
     */
    async batch(fn, options = {}) {
      return this.client.batch(fn, {
        bucket: this.bucket.name,
        collection: this.name,
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
        safe: this._getSafe(options),
        aggregate: !!options.aggregate,
      });
    }
  };
  __decorate(
    [capable(["attachments"])],
    Collection$1.prototype,
    "addAttachment",
    null
  );
  __decorate(
    [capable(["attachments"])],
    Collection$1.prototype,
    "removeAttachment",
    null
  );
  __decorate(
    [capable(["history"])],
    Collection$1.prototype,
    "getSnapshot",
    null
  );

  /**
   * Abstract representation of a selected bucket.
   *
   */
  class Bucket {
    /**
     * Constructor.
     *
     * @param  {KintoClient} client            The client instance.
     * @param  {String}      name              The bucket name.
     * @param  {Object}      [options={}]      The headers object option.
     * @param  {Object}      [options.headers] The headers object option.
     * @param  {Boolean}     [options.safe]    The safe option.
     * @param  {Number}      [options.retry]   The retry option.
     */
    constructor(client, name, options = {}) {
      /**
       * @ignore
       */
      this.client = client;
      /**
       * The bucket name.
       * @type {String}
       */
      this.name = name;
      this._endpoints = client.endpoints;
      /**
       * @ignore
       */
      this._headers = options.headers || {};
      this._retry = options.retry || 0;
      this._safe = !!options.safe;
    }
    get execute() {
      return this.client.execute.bind(this.client);
    }
    get headers() {
      return this._headers;
    }
    /**
     * Get the value of "headers" for a given request, merging the
     * per-request headers with our own "default" headers.
     *
     * @private
     */
    _getHeaders(options) {
      return Object.assign(Object.assign({}, this._headers), options.headers);
    }
    /**
     * Get the value of "safe" for a given request, using the
     * per-request option if present or falling back to our default
     * otherwise.
     *
     * @private
     * @param {Object} options The options for a request.
     * @returns {Boolean}
     */
    _getSafe(options) {
      return Object.assign({ safe: this._safe }, options).safe;
    }
    /**
     * As _getSafe, but for "retry".
     *
     * @private
     */
    _getRetry(options) {
      return Object.assign({ retry: this._retry }, options).retry;
    }
    /**
     * Selects a collection.
     *
     * @param  {String}  name              The collection name.
     * @param  {Object}  [options={}]      The options object.
     * @param  {Object}  [options.headers] The headers object option.
     * @param  {Boolean} [options.safe]    The safe option.
     * @return {Collection}
     */
    collection(name, options = {}) {
      return new Collection$1(this.client, this, name, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
        safe: this._getSafe(options),
      });
    }
    /**
     * Retrieves the ETag of the collection list, for use with the `since` filtering option.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<String, Error>}
     */
    async getCollectionsTimestamp(options = {}) {
      const path = this._endpoints.collection(this.name);
      const request = {
        headers: this._getHeaders(options),
        path,
        method: "HEAD",
      };
      const { headers } = await this.client.execute(request, {
        raw: true,
        retry: this._getRetry(options),
      });
      return headers.get("ETag");
    }
    /**
     * Retrieves the ETag of the group list, for use with the `since` filtering option.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<String, Error>}
     */
    async getGroupsTimestamp(options = {}) {
      const path = this._endpoints.group(this.name);
      const request = {
        headers: this._getHeaders(options),
        path,
        method: "HEAD",
      };
      const { headers } = await this.client.execute(request, {
        raw: true,
        retry: this._getRetry(options),
      });
      return headers.get("ETag");
    }
    /**
     * Retrieves bucket data.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Object} [options.query]   Query parameters to pass in
     *     the request. This might be useful for features that aren't
     *     yet supported by this library.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async getData(options = {}) {
      const path = this._endpoints.bucket(this.name);
      const request = {
        headers: this._getHeaders(options),
        path,
      };
      const { data } = await this.client.execute(request, {
        retry: this._getRetry(options),
        query: options.query,
        fields: options.fields,
      });
      return data;
    }
    /**
     * Set bucket data.
     * @param  {Object}  data                    The bucket data object.
     * @param  {Object}  [options={}]            The options object.
     * @param  {Object}  [options.headers={}]    The headers object option.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean} [options.patch]         The patch option.
     * @param  {Number}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async setData(data, options = {}) {
      if (!isObject(data)) {
        throw new Error("A bucket object is required.");
      }
      const bucket = Object.assign(Object.assign({}, data), { id: this.name });
      // For default bucket, we need to drop the id from the data object.
      // Bug in Kinto < 3.1.1
      const bucketId = bucket.id;
      if (bucket.id === "default") {
        delete bucket.id;
      }
      const path = this._endpoints.bucket(bucketId);
      const { patch, permissions } = options;
      const { last_modified } = Object.assign(Object.assign({}, data), options);
      const request = updateRequest(
        path,
        { data: bucket, permissions },
        {
          last_modified,
          patch,
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of history entries in the current bucket.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Array<Object>, Error>}
     */
    async listHistory(options = {}) {
      const path = this._endpoints.history(this.name);
      return this.client.paginatedList(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of collections in the current bucket.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.filters={}] The filters object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @return {Promise<Array<Object>, Error>}
     */
    async listCollections(options = {}) {
      const path = this._endpoints.collection(this.name);
      return this.client.paginatedList(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Creates a new collection in current bucket.
     *
     * @param  {String|undefined}  id          The collection id.
     * @param  {Object}  [options={}]          The options object.
     * @param  {Boolean} [options.safe]        The safe option.
     * @param  {Object}  [options.headers]     The headers object option.
     * @param  {Number}  [options.retry=0]     Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.permissions] The permissions object.
     * @param  {Object}  [options.data]        The data object.
     * @return {Promise<Object, Error>}
     */
    async createCollection(id, options = {}) {
      const { permissions, data = {} } = options;
      data.id = id;
      const path = this._endpoints.collection(this.name, id);
      const request = createRequest(
        path,
        { data, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes a collection from the current bucket.
     *
     * @param  {Object|String} collection              The collection to delete.
     * @param  {Object}        [options={}]            The options object.
     * @param  {Object}        [options.headers]       The headers object option.
     * @param  {Number}        [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean}       [options.safe]          The safe option.
     * @param  {Number}        [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async deleteCollection(collection, options = {}) {
      const collectionObj = toDataBody(collection);
      if (!collectionObj.id) {
        throw new Error("A collection id is required.");
      }
      const { id } = collectionObj;
      const { last_modified } = Object.assign(
        Object.assign({}, collectionObj),
        options
      );
      const path = this._endpoints.collection(this.name, id);
      const request = deleteRequest(path, {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes collections from the current bucket.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.filters={}] The filters object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @return {Promise<Array<Object>, Error>}
     */
    async deleteCollections(options = {}) {
      const path = this._endpoints.collection(this.name);
      return this.client.paginatedDelete(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of groups in the current bucket.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.filters={}] The filters object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @return {Promise<Array<Object>, Error>}
     */
    async listGroups(options = {}) {
      const path = this._endpoints.group(this.name);
      return this.client.paginatedList(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Fetches a group in current bucket.
     *
     * @param  {String} id                The group id.
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @param  {Object} [options.query]   Query parameters to pass in
     *     the request. This might be useful for features that aren't
     *     yet supported by this library.
     * @param  {Array}  [options.fields]  Limit response to
     *     just some fields.
     * @return {Promise<Object, Error>}
     */
    async getGroup(id, options = {}) {
      const path = this._endpoints.group(this.name, id);
      const request = {
        headers: this._getHeaders(options),
        path,
      };
      return this.client.execute(request, {
        retry: this._getRetry(options),
        query: options.query,
        fields: options.fields,
      });
    }
    /**
     * Creates a new group in current bucket.
     *
     * @param  {String|undefined}  id                    The group id.
     * @param  {Array<String>}     [members=[]]          The list of principals.
     * @param  {Object}            [options={}]          The options object.
     * @param  {Object}            [options.data]        The data object.
     * @param  {Object}            [options.permissions] The permissions object.
     * @param  {Boolean}           [options.safe]        The safe option.
     * @param  {Object}            [options.headers]     The headers object option.
     * @param  {Number}            [options.retry=0]     Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async createGroup(id, members = [], options = {}) {
      const data = Object.assign(Object.assign({}, options.data), {
        id,
        members,
      });
      const path = this._endpoints.group(this.name, id);
      const { permissions } = options;
      const request = createRequest(
        path,
        { data, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Updates an existing group in current bucket.
     *
     * @param  {Object}  group                   The group object.
     * @param  {Object}  [options={}]            The options object.
     * @param  {Object}  [options.data]          The data object.
     * @param  {Object}  [options.permissions]   The permissions object.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Number}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async updateGroup(group, options = {}) {
      if (!isObject(group)) {
        throw new Error("A group object is required.");
      }
      if (!group.id) {
        throw new Error("A group id is required.");
      }
      const data = Object.assign(Object.assign({}, options.data), group);
      const path = this._endpoints.group(this.name, group.id);
      const { patch, permissions } = options;
      const { last_modified } = Object.assign(Object.assign({}, data), options);
      const request = updateRequest(
        path,
        { data, permissions },
        {
          last_modified,
          patch,
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes a group from the current bucket.
     *
     * @param  {Object|String} group                   The group to delete.
     * @param  {Object}        [options={}]            The options object.
     * @param  {Object}        [options.headers]       The headers object option.
     * @param  {Number}        [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Boolean}       [options.safe]          The safe option.
     * @param  {Number}        [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async deleteGroup(group, options = {}) {
      const groupObj = toDataBody(group);
      const { id } = groupObj;
      const { last_modified } = Object.assign(
        Object.assign({}, groupObj),
        options
      );
      const path = this._endpoints.group(this.name, id);
      const request = deleteRequest(path, {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Deletes groups from the current bucket.
     *
     * @param  {Object} [options={}]          The options object.
     * @param  {Object} [options.filters={}]  The filters object.
     * @param  {Object} [options.headers]     The headers object option.
     * @param  {Number} [options.retry=0]     Number of retries to make
     *     when faced with transient errors.
     * @param  {Array}  [options.fields]      Limit response to
     *     just some fields.
     * @return {Promise<Array<Object>, Error>}
     */
    async deleteGroups(options = {}) {
      const path = this._endpoints.group(this.name);
      return this.client.paginatedDelete(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of permissions for this bucket.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers] The headers object option.
     * @param  {Number} [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async getPermissions(options = {}) {
      const request = {
        headers: this._getHeaders(options),
        path: this._endpoints.bucket(this.name),
      };
      const { permissions } = await this.client.execute(request, {
        retry: this._getRetry(options),
      });
      return permissions;
    }
    /**
     * Replaces all existing bucket permissions with the ones provided.
     *
     * @param  {Object}  permissions             The permissions object.
     * @param  {Object}  [options={}]            The options object
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers={}]    The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async setPermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.bucket(this.name);
      const { last_modified } = options;
      const data = { last_modified };
      const request = updateRequest(
        path,
        { data, permissions },
        {
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }
      );
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Append principals to the bucket permissions.
     *
     * @param  {Object}  permissions             The permissions object.
     * @param  {Object}  [options={}]            The options object
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async addPermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.bucket(this.name);
      const { last_modified } = options;
      const request = jsonPatchPermissionsRequest(path, permissions, "add", {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Remove principals from the bucket permissions.
     *
     * @param  {Object}  permissions             The permissions object.
     * @param  {Object}  [options={}]            The options object
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object}  [options.headers]       The headers object option.
     * @param  {Number}  [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Object}  [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async removePermissions(permissions, options = {}) {
      if (!isObject(permissions)) {
        throw new Error("A permissions object is required.");
      }
      const path = this._endpoints.bucket(this.name);
      const { last_modified } = options;
      const request = jsonPatchPermissionsRequest(path, permissions, "remove", {
        last_modified,
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
      });
      return this.client.execute(request, {
        retry: this._getRetry(options),
      });
    }
    /**
     * Performs batch operations at the current bucket level.
     *
     * @param  {Function} fn                   The batch operation function.
     * @param  {Object}   [options={}]         The options object.
     * @param  {Object}   [options.headers]    The headers object option.
     * @param  {Boolean}  [options.safe]       The safe option.
     * @param  {Number}   [options.retry=0]    The retry option.
     * @param  {Boolean}  [options.aggregate]  Produces a grouped result object.
     * @return {Promise<Object, Error>}
     */
    async batch(fn, options = {}) {
      return this.client.batch(fn, {
        bucket: this.name,
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
        safe: this._getSafe(options),
        aggregate: !!options.aggregate,
      });
    }
  }
  __decorate([capable(["history"])], Bucket.prototype, "listHistory", null);

  /**
   * High level HTTP client for the Kinto API.
   *
   * @example
   * const client = new KintoClient("https://demo.kinto-storage.org/v1");
   * client.bucket("default")
   *    .collection("my-blog")
   *    .createRecord({title: "First article"})
   *   .then(console.log.bind(console))
   *   .catch(console.error.bind(console));
   */
  class KintoClientBase {
    /**
     * Constructor.
     *
     * @param  {String}       remote  The remote URL.
     * @param  {Object}       [options={}]                  The options object.
     * @param  {Boolean}      [options.safe=true]           Adds concurrency headers to every requests.
     * @param  {EventEmitter} [options.events=EventEmitter] The events handler instance.
     * @param  {Object}       [options.headers={}]          The key-value headers to pass to each request.
     * @param  {Object}       [options.retry=0]             Number of retries when request fails (default: 0)
     * @param  {String}       [options.bucket="default"]    The default bucket to use.
     * @param  {String}       [options.requestMode="cors"]  The HTTP request mode (from ES6 fetch spec).
     * @param  {Number}       [options.timeout=null]        The request timeout in ms, if any.
     * @param  {Function}     [options.fetchFunc=fetch]     The function to be used to execute HTTP requests.
     */
    constructor(remote, options) {
      if (typeof remote !== "string" || !remote.length) {
        throw new Error("Invalid remote URL: " + remote);
      }
      if (remote[remote.length - 1] === "/") {
        remote = remote.slice(0, -1);
      }
      this._backoffReleaseTime = null;
      this._requests = [];
      this._isBatch = !!options.batch;
      this._retry = options.retry || 0;
      this._safe = !!options.safe;
      this._headers = options.headers || {};
      // public properties
      /**
       * The remote server base URL.
       * @type {String}
       */
      this.remote = remote;
      /**
       * Current server information.
       * @ignore
       * @type {Object|null}
       */
      this.serverInfo = null;
      /**
       * The event emitter instance. Should comply with the `EventEmitter`
       * interface.
       * @ignore
       * @type {Class}
       */
      this.events = options.events;
      this.endpoints = ENDPOINTS;
      const { fetchFunc, requestMode, timeout } = options;
      /**
       * The HTTP instance.
       * @ignore
       * @type {HTTP}
       */
      this.http = new HTTP(this.events, { fetchFunc, requestMode, timeout });
      this._registerHTTPEvents();
    }
    /**
     * The remote endpoint base URL. Setting the value will also extract and
     * validate the version.
     * @type {String}
     */
    get remote() {
      return this._remote;
    }
    /**
     * @ignore
     */
    set remote(url) {
      let version;
      try {
        version = url.match(/\/(v\d+)\/?$/)[1];
      } catch (err) {
        throw new Error("The remote URL must contain the version: " + url);
      }
      this._remote = url;
      this._version = version;
    }
    /**
     * The current server protocol version, eg. `v1`.
     * @type {String}
     */
    get version() {
      return this._version;
    }
    /**
     * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
     * ongoing.
     *
     * @type {Number}
     */
    get backoff() {
      const currentTime = new Date().getTime();
      if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
        return this._backoffReleaseTime - currentTime;
      }
      return 0;
    }
    /**
     * Registers HTTP events.
     * @private
     */
    _registerHTTPEvents() {
      // Prevent registering event from a batch client instance
      if (!this._isBatch && this.events) {
        this.events.on("backoff", (backoffMs) => {
          this._backoffReleaseTime = backoffMs;
        });
      }
    }
    /**
     * Retrieve a bucket object to perform operations on it.
     *
     * @param  {String}  name              The bucket name.
     * @param  {Object}  [options={}]      The request options.
     * @param  {Boolean} [options.safe]    The resulting safe option.
     * @param  {Number}  [options.retry]   The resulting retry option.
     * @param  {Object}  [options.headers] The extended headers object option.
     * @return {Bucket}
     */
    bucket(name, options = {}) {
      return new Bucket(this, name, {
        headers: this._getHeaders(options),
        safe: this._getSafe(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Set client "headers" for every request, updating previous headers (if any).
     *
     * @param {Object} headers The headers to merge with existing ones.
     */
    setHeaders(headers) {
      this._headers = Object.assign(Object.assign({}, this._headers), headers);
      this.serverInfo = null;
    }
    /**
     * Get the value of "headers" for a given request, merging the
     * per-request headers with our own "default" headers.
     *
     * Note that unlike other options, headers aren't overridden, but
     * merged instead.
     *
     * @private
     * @param {Object} options The options for a request.
     * @returns {Object}
     */
    _getHeaders(options) {
      return Object.assign(Object.assign({}, this._headers), options.headers);
    }
    /**
     * Get the value of "safe" for a given request, using the
     * per-request option if present or falling back to our default
     * otherwise.
     *
     * @private
     * @param {Object} options The options for a request.
     * @returns {Boolean}
     */
    _getSafe(options) {
      return Object.assign({ safe: this._safe }, options).safe;
    }
    /**
     * As _getSafe, but for "retry".
     *
     * @private
     */
    _getRetry(options) {
      return Object.assign({ retry: this._retry }, options).retry;
    }
    /**
     * Retrieves the server's "hello" endpoint. This endpoint reveals
     * server capabilities and settings as well as telling the client
     * "who they are" according to their given authorization headers.
     *
     * @private
     * @param  {Object}  [options={}] The request options.
     * @param  {Object}  [options.headers={}] Headers to use when making
     *     this request.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async _getHello(options = {}) {
      const path = this.remote + ENDPOINTS.root();
      const { json } = await this.http.request(
        path,
        { headers: this._getHeaders(options) },
        { retry: this._getRetry(options) }
      );
      return json;
    }
    /**
     * Retrieves server information and persist them locally. This operation is
     * usually performed a single time during the instance lifecycle.
     *
     * @param  {Object}  [options={}] The request options.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async fetchServerInfo(options = {}) {
      if (this.serverInfo) {
        return this.serverInfo;
      }
      this.serverInfo = await this._getHello({
        retry: this._getRetry(options),
      });
      return this.serverInfo;
    }
    /**
     * Retrieves Kinto server settings.
     *
     * @param  {Object}  [options={}] The request options.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async fetchServerSettings(options = {}) {
      const { settings } = await this.fetchServerInfo(options);
      return settings;
    }
    /**
     * Retrieve server capabilities information.
     *
     * @param  {Object}  [options={}] The request options.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async fetchServerCapabilities(options = {}) {
      const { capabilities } = await this.fetchServerInfo(options);
      return capabilities;
    }
    /**
     * Retrieve authenticated user information.
     *
     * @param  {Object}  [options={}] The request options.
     * @param  {Object}  [options.headers={}] Headers to use when making
     *     this request.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async fetchUser(options = {}) {
      const { user } = await this._getHello(options);
      return user;
    }
    /**
     * Retrieve authenticated user information.
     *
     * @param  {Object}  [options={}] The request options.
     * @param  {Number}  [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async fetchHTTPApiVersion(options = {}) {
      const { http_api_version } = await this.fetchServerInfo(options);
      return http_api_version;
    }
    /**
     * Process batch requests, chunking them according to the batch_max_requests
     * server setting when needed.
     *
     * @param  {Array}  requests     The list of batch subrequests to perform.
     * @param  {Object} [options={}] The options object.
     * @return {Promise<Object, Error>}
     */
    async _batchRequests(requests, options = {}) {
      const headers = this._getHeaders(options);
      if (!requests.length) {
        return [];
      }
      const serverSettings = await this.fetchServerSettings({
        retry: this._getRetry(options),
      });
      const maxRequests = serverSettings.batch_max_requests;
      if (maxRequests && requests.length > maxRequests) {
        const chunks = partition(requests, maxRequests);
        const results = [];
        for (const chunk of chunks) {
          const result = await this._batchRequests(chunk, options);
          results.push(...result);
        }
        return results;
      }
      const { responses } = await this.execute(
        {
          // FIXME: is this really necessary, since it's also present in
          // the "defaults"?
          headers,
          path: ENDPOINTS.batch(),
          method: "POST",
          body: {
            defaults: { headers },
            requests,
          },
        },
        { retry: this._getRetry(options) }
      );
      return responses;
    }
    /**
     * Sends batch requests to the remote server.
     *
     * Note: Reserved for internal use only.
     *
     * @ignore
     * @param  {Function} fn                        The function to use for describing batch ops.
     * @param  {Object}   [options={}]              The options object.
     * @param  {Boolean}  [options.safe]            The safe option.
     * @param  {Number}   [options.retry]           The retry option.
     * @param  {String}   [options.bucket]          The bucket name option.
     * @param  {String}   [options.collection]      The collection name option.
     * @param  {Object}   [options.headers]         The headers object option.
     * @param  {Boolean}  [options.aggregate=false] Produces an aggregated result object.
     * @return {Promise<Object, Error>}
     */
    async batch(fn, options = {}) {
      const rootBatch = new KintoClientBase(this.remote, {
        events: this.events,
        batch: true,
        safe: this._getSafe(options),
        retry: this._getRetry(options),
      });
      if (options.bucket && options.collection) {
        fn(rootBatch.bucket(options.bucket).collection(options.collection));
      } else if (options.bucket) {
        fn(rootBatch.bucket(options.bucket));
      } else {
        fn(rootBatch);
      }
      const responses = await this._batchRequests(rootBatch._requests, options);
      if (options.aggregate) {
        return aggregate(responses, rootBatch._requests);
      }
      return responses;
    }
    async execute(request, options = {}) {
      const { raw = false, stringify = true } = options;
      // If we're within a batch, add the request to the stack to send at once.
      if (this._isBatch) {
        this._requests.push(request);
        // Resolve with a message in case people attempt at consuming the result
        // from within a batch operation.
        const msg =
          "This result is generated from within a batch " +
          "operation and should not be consumed.";
        return raw ? { status: 0, json: msg, headers: new Headers() } : msg;
      }
      const uri = this.remote + addEndpointOptions(request.path, options);
      const result = await this.http.request(
        uri,
        cleanUndefinedProperties({
          // Limit requests to only those parts that would be allowed in
          // a batch request -- don't pass through other fancy fetch()
          // options like integrity, redirect, mode because they will
          // break on a batch request.  A batch request only allows
          // headers, method, path (above), and body.
          method: request.method,
          headers: request.headers,
          body: stringify ? JSON.stringify(request.body) : request.body,
        }),
        { retry: this._getRetry(options) }
      );
      return raw ? result : result.json;
    }
    /**
     * Perform an operation with a given HTTP method on some pages from
     * a paginated list, following the `next-page` header automatically
     * until we have processed the requested number of pages. Return a
     * response with a `.next()` method that can be called to perform
     * the requested HTTP method on more results.
     *
     * @private
     * @param  {String}  path
     *     The path to make the request to.
     * @param  {Object}  params
     *     The parameters to use when making the request.
     * @param  {String}  [params.sort="-last_modified"]
     *     The sorting order to use when doing operation on pages.
     * @param  {Object}  [params.filters={}]
     *     The filters to send in the request.
     * @param  {Number}  [params.limit=undefined]
     *     The limit to send in the request. Undefined means no limit.
     * @param  {Number}  [params.pages=undefined]
     *     The number of pages to operate on. Undefined means one page. Pass
     *     Infinity to operate on everything.
     * @param  {String}  [params.since=undefined]
     *     The ETag from which to start doing operation on pages.
     * @param  {Array}   [params.fields]
     *     Limit response to just some fields.
     * @param  {Object}  [options={}]
     *     Additional request-level parameters to use in all requests.
     * @param  {Object}  [options.headers={}]
     *     Headers to use during all requests.
     * @param  {Number}  [options.retry=0]
     *     Number of times to retry each request if the server responds
     *     with Retry-After.
     * @param  {String}  [options.method="GET"]
     *     The method to use in the request.
     */
    async paginatedOperation(path, params = {}, options = {}) {
      // FIXME: this is called even in batch requests, which doesn't
      // make any sense (since all batch requests get a "dummy"
      // response; see execute() above).
      const { sort, filters, limit, pages, since, fields } = Object.assign(
        { sort: "-last_modified" },
        params
      );
      // Safety/Consistency check on ETag value.
      if (since && typeof since !== "string") {
        throw new Error(
          `Invalid value for since (${since}), should be ETag value.`
        );
      }
      const query = Object.assign(Object.assign({}, filters), {
        _sort: sort,
        _limit: limit,
        _since: since,
      });
      if (fields) {
        query._fields = fields;
      }
      const querystring = qsify(query);
      let results = [],
        current = 0;
      const next = async function (nextPage) {
        if (!nextPage) {
          throw new Error("Pagination exhausted.");
        }
        return processNextPage(nextPage);
      };
      const processNextPage = async (nextPage) => {
        const { headers } = options;
        return handleResponse(await this.http.request(nextPage, { headers }));
      };
      const pageResults = (results, nextPage, etag) => {
        // ETag string is supposed to be opaque and stored «as-is».
        // ETag header values are quoted (because of * and W/"foo").
        return {
          last_modified: etag ? etag.replace(/"/g, "") : etag,
          data: results,
          next: next.bind(null, nextPage),
          hasNextPage: !!nextPage,
          totalRecords: -1,
        };
      };
      const handleResponse = async function ({
        headers = new Headers(),
        json = {},
      }) {
        const nextPage = headers.get("Next-Page");
        const etag = headers.get("ETag");
        if (!pages) {
          return pageResults(json.data, nextPage, etag);
        }
        // Aggregate new results with previous ones
        results = results.concat(json.data);
        current += 1;
        if (current >= pages || !nextPage) {
          // Pagination exhausted
          return pageResults(results, nextPage, etag);
        }
        // Follow next page
        return processNextPage(nextPage);
      };
      return handleResponse(
        await this.execute(
          // N.B.: This doesn't use _getHeaders, because all calls to
          // `paginatedList` are assumed to come from calls that already
          // have headers merged at e.g. the bucket or collection level.
          {
            headers: options.headers ? options.headers : {},
            path: path + "?" + querystring,
            method: options.method,
          },
          // N.B. This doesn't use _getRetry, because all calls to
          // `paginatedList` are assumed to come from calls that already
          // used `_getRetry` at e.g. the bucket or collection level.
          { raw: true, retry: options.retry || 0 }
        )
      );
    }
    /**
     * Fetch some pages from a paginated list, following the `next-page`
     * header automatically until we have fetched the requested number
     * of pages. Return a response with a `.next()` method that can be
     * called to fetch more results.
     *
     * @private
     * @param  {String}  path
     *     The path to make the request to.
     * @param  {Object}  params
     *     The parameters to use when making the request.
     * @param  {String}  [params.sort="-last_modified"]
     *     The sorting order to use when fetching.
     * @param  {Object}  [params.filters={}]
     *     The filters to send in the request.
     * @param  {Number}  [params.limit=undefined]
     *     The limit to send in the request. Undefined means no limit.
     * @param  {Number}  [params.pages=undefined]
     *     The number of pages to fetch. Undefined means one page. Pass
     *     Infinity to fetch everything.
     * @param  {String}  [params.since=undefined]
     *     The ETag from which to start fetching.
     * @param  {Array}   [params.fields]
     *     Limit response to just some fields.
     * @param  {Object}  [options={}]
     *     Additional request-level parameters to use in all requests.
     * @param  {Object}  [options.headers={}]
     *     Headers to use during all requests.
     * @param  {Number}  [options.retry=0]
     *     Number of times to retry each request if the server responds
     *     with Retry-After.
     */
    async paginatedList(path, params = {}, options = {}) {
      return this.paginatedOperation(path, params, options);
    }
    /**
     * Delete multiple objects, following the pagination if the number of
     * objects exceeds the page limit until we have deleted the requested
     * number of pages. Return a response with a `.next()` method that can
     * be called to delete more results.
     *
     * @private
     * @param  {String}  path
     *     The path to make the request to.
     * @param  {Object}  params
     *     The parameters to use when making the request.
     * @param  {String}  [params.sort="-last_modified"]
     *     The sorting order to use when deleting.
     * @param  {Object}  [params.filters={}]
     *     The filters to send in the request.
     * @param  {Number}  [params.limit=undefined]
     *     The limit to send in the request. Undefined means no limit.
     * @param  {Number}  [params.pages=undefined]
     *     The number of pages to delete. Undefined means one page. Pass
     *     Infinity to delete everything.
     * @param  {String}  [params.since=undefined]
     *     The ETag from which to start deleting.
     * @param  {Array}   [params.fields]
     *     Limit response to just some fields.
     * @param  {Object}  [options={}]
     *     Additional request-level parameters to use in all requests.
     * @param  {Object}  [options.headers={}]
     *     Headers to use during all requests.
     * @param  {Number}  [options.retry=0]
     *     Number of times to retry each request if the server responds
     *     with Retry-After.
     */
    paginatedDelete(path, params = {}, options = {}) {
      const { headers, safe, last_modified } = options;
      const deleteRequest$1 = deleteRequest(path, {
        headers,
        safe: safe ? safe : false,
        last_modified,
      });
      return this.paginatedOperation(
        path,
        params,
        Object.assign(Object.assign({}, options), {
          headers: deleteRequest$1.headers,
          method: "DELETE",
        })
      );
    }
    /**
     * Lists all permissions.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers={}] Headers to use when making
     *     this request.
     * @param  {Number} [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object[], Error>}
     */
    async listPermissions(options = {}) {
      const path = ENDPOINTS.permissions();
      // Ensure the default sort parameter is something that exists in permissions
      // entries, as `last_modified` doesn't; here, we pick "id".
      const paginationOptions = Object.assign({ sort: "id" }, options);
      return this.paginatedList(path, paginationOptions, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Retrieves the list of buckets.
     *
     * @param  {Object} [options={}]      The options object.
     * @param  {Object} [options.headers={}] Headers to use when making
     *     this request.
     * @param  {Number} [options.retry=0]    Number of retries to make
     *     when faced with transient errors.
     * @param  {Object} [options.filters={}] The filters object.
     * @param  {Array}  [options.fields]     Limit response to
     *     just some fields.
     * @return {Promise<Object[], Error>}
     */
    async listBuckets(options = {}) {
      const path = ENDPOINTS.bucket();
      return this.paginatedList(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
      });
    }
    /**
     * Creates a new bucket on the server.
     *
     * @param  {String|null}  id                The bucket name (optional).
     * @param  {Object}       [options={}]      The options object.
     * @param  {Boolean}      [options.data]    The bucket data option.
     * @param  {Boolean}      [options.safe]    The safe option.
     * @param  {Object}       [options.headers] The headers object option.
     * @param  {Number}       [options.retry=0] Number of retries to make
     *     when faced with transient errors.
     * @return {Promise<Object, Error>}
     */
    async createBucket(id, options = {}) {
      const { data, permissions } = options;
      const _data = Object.assign(Object.assign({}, data), {
        id: id ? id : undefined,
      });
      const path = _data.id ? ENDPOINTS.bucket(_data.id) : ENDPOINTS.bucket();
      return this.execute(
        createRequest(
          path,
          { data: _data, permissions },
          {
            headers: this._getHeaders(options),
            safe: this._getSafe(options),
          }
        ),
        { retry: this._getRetry(options) }
      );
    }
    /**
     * Deletes a bucket from the server.
     *
     * @ignore
     * @param  {Object|String} bucket                  The bucket to delete.
     * @param  {Object}        [options={}]            The options object.
     * @param  {Boolean}       [options.safe]          The safe option.
     * @param  {Object}        [options.headers]       The headers object option.
     * @param  {Number}        [options.retry=0]       Number of retries to make
     *     when faced with transient errors.
     * @param  {Number}        [options.last_modified] The last_modified option.
     * @return {Promise<Object, Error>}
     */
    async deleteBucket(bucket, options = {}) {
      const bucketObj = toDataBody(bucket);
      if (!bucketObj.id) {
        throw new Error("A bucket id is required.");
      }
      const path = ENDPOINTS.bucket(bucketObj.id);
      const { last_modified } = Object.assign(
        Object.assign({}, bucketObj),
        options
      );
      return this.execute(
        deleteRequest(path, {
          last_modified,
          headers: this._getHeaders(options),
          safe: this._getSafe(options),
        }),
        { retry: this._getRetry(options) }
      );
    }
    /**
     * Deletes buckets.
     *
     * @param  {Object} [options={}]             The options object.
     * @param  {Boolean} [options.safe]          The safe option.
     * @param  {Object} [options.headers={}]     Headers to use when making
     *     this request.
     * @param  {Number} [options.retry=0]        Number of retries to make
     *     when faced with transient errors.
     * @param  {Object} [options.filters={}]     The filters object.
     * @param  {Array}  [options.fields]         Limit response to
     *     just some fields.
     * @param  {Number}  [options.last_modified] The last_modified option.
     * @return {Promise<Object[], Error>}
     */
    async deleteBuckets(options = {}) {
      const path = ENDPOINTS.bucket();
      return this.paginatedDelete(path, options, {
        headers: this._getHeaders(options),
        retry: this._getRetry(options),
        safe: options.safe,
        last_modified: options.last_modified,
      });
    }
    async createAccount(username, password) {
      return this.execute(
        createRequest(
          `/accounts/${username}`,
          { data: { password } },
          { method: "PUT" }
        )
      );
    }
  }
  __decorate(
    [nobatch("This operation is not supported within a batch operation.")],
    KintoClientBase.prototype,
    "fetchServerSettings",
    null
  );
  __decorate(
    [nobatch("This operation is not supported within a batch operation.")],
    KintoClientBase.prototype,
    "fetchServerCapabilities",
    null
  );
  __decorate(
    [nobatch("This operation is not supported within a batch operation.")],
    KintoClientBase.prototype,
    "fetchUser",
    null
  );
  __decorate(
    [nobatch("This operation is not supported within a batch operation.")],
    KintoClientBase.prototype,
    "fetchHTTPApiVersion",
    null
  );
  __decorate(
    [nobatch("Can't use batch within a batch!")],
    KintoClientBase.prototype,
    "batch",
    null
  );
  __decorate(
    [capable(["permissions_endpoint"])],
    KintoClientBase.prototype,
    "listPermissions",
    null
  );
  __decorate(
    [support("1.4", "2.0")],
    KintoClientBase.prototype,
    "deleteBuckets",
    null
  );
  __decorate(
    [capable(["accounts"])],
    KintoClientBase.prototype,
    "createAccount",
    null
  );

  class KintoClient extends KintoClientBase {
    constructor(remote, options = {}) {
      const events = options.events;
      super(remote, Object.assign({ events }, options));
    }
  }

  class AbstractBaseAdapter {}
  /**
   * Base db adapter.
   *
   * @abstract
   */
  class BaseAdapter {
    /**
     * Deletes every records present in the database.
     *
     * @abstract
     * @return {Promise}
     */
    clear() {
      throw new Error("Not Implemented.");
    }
    /**
     * Executes a batch of operations within a single transaction.
     *
     * @abstract
     * @param  {Function} callback The operation callback.
     * @param  {Object}   options  The options object.
     * @return {Promise}
     */
    execute(callback, options = { preload: [] }) {
      throw new Error("Not Implemented.");
    }
    /**
     * Retrieve a record by its primary key from the database.
     *
     * @abstract
     * @param  {String} id The record id.
     * @return {Promise}
     */
    get(id) {
      throw new Error("Not Implemented.");
    }
    /**
     * Lists all records from the database.
     *
     * @abstract
     * @param  {Object} params  The filters and order to apply to the results.
     * @return {Promise}
     */
    list(
      params = {
        filters: {},
        order: "",
      }
    ) {
      throw new Error("Not Implemented.");
    }
    /**
     * Store the lastModified value.
     *
     * @abstract
     * @param  {Number}  lastModified
     * @return {Promise}
     */
    saveLastModified(lastModified) {
      throw new Error("Not Implemented.");
    }
    /**
     * Retrieve saved lastModified value.
     *
     * @abstract
     * @return {Promise}
     */
    getLastModified() {
      throw new Error("Not Implemented.");
    }
    /**
     * Load records in bulk that were exported from a server.
     *
     * @abstract
     * @param  {Array} records The records to load.
     * @return {Promise}
     */
    importBulk(records) {
      throw new Error("Not Implemented.");
    }
    /**
     * Load a dump of records exported from a server.
     *
     * @deprecated Use {@link importBulk} instead.
     * @abstract
     * @param  {Array} records The records to load.
     * @return {Promise}
     */
    loadDump(records) {
      throw new Error("Not Implemented.");
    }
    saveMetadata(metadata) {
      throw new Error("Not Implemented.");
    }
    getMetadata() {
      throw new Error("Not Implemented.");
    }
  }

  const INDEXED_FIELDS = ["id", "_status", "last_modified"];
  /**
   * Small helper that wraps the opening of an IndexedDB into a Promise.
   *
   * @param dbname          {String}   The database name.
   * @param version         {Integer}  Schema version
   * @param onupgradeneeded {Function} The callback to execute if schema is
   *                                   missing or different.
   * @return {Promise<IDBDatabase>}
   */
  async function open(dbname, { version, onupgradeneeded }) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(dbname, version);
      request.onupgradeneeded = (event) => {
        const db = request.result;
        db.onerror = (event) => reject(request.error);
        // When an upgrade is needed, a transaction is started.
        const transaction = request.transaction;
        transaction.onabort = (event) => {
          const error =
            request.error ||
            transaction.error ||
            new DOMException("The operation has been aborted", "AbortError");
          reject(error);
        };
        // Callback for store creation etc.
        return onupgradeneeded(event);
      };
      request.onerror = (event) => {
        reject(event.target.error);
      };
      request.onsuccess = (event) => {
        const db = request.result;
        resolve(db);
      };
    });
  }
  /**
   * Helper to run the specified callback in a single transaction on the
   * specified store.
   * The helper focuses on transaction wrapping into a promise.
   *
   * @param db           {IDBDatabase} The database instance.
   * @param name         {String}      The store name.
   * @param callback     {Function}    The piece of code to execute in the transaction.
   * @param options      {Object}      Options.
   * @param options.mode {String}      Transaction mode (default: read).
   * @return {Promise} any value returned by the callback.
   */
  async function execute(db, name, callback, options = {}) {
    const { mode } = options;
    return new Promise((resolve, reject) => {
      // On Safari, calling IDBDatabase.transaction with mode == undefined raises
      // a TypeError.
      const transaction = mode
        ? db.transaction([name], mode)
        : db.transaction([name]);
      const store = transaction.objectStore(name);
      // Let the callback abort this transaction.
      const abort = (e) => {
        transaction.abort();
        console.error(e);
        reject(e);
      };
      // Execute the specified callback **synchronously**.
      let result;
      try {
        result = callback(store, abort);
      } catch (e) {
        abort(e);
      }
      transaction.onerror = (event) => reject(event.target.error);
      transaction.oncomplete = (event) => resolve(result);
      transaction.onabort = (event) => {
        const error =
          event.target.error ||
          transaction.error ||
          new DOMException("The operation has been aborted", "AbortError");
        reject(error);
      };
    });
  }
  /**
   * Helper to wrap the deletion of an IndexedDB database into a promise.
   *
   * @param dbName {String} the database to delete
   * @return {Promise}
   */
  async function deleteDatabase(dbName) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.deleteDatabase(dbName);
      request.onsuccess = (event) => resolve(event.target);
      request.onerror = (event) => reject(event.target.error);
    });
  }
  /**
   * IDB cursor handlers.
   * @type {Object}
   */
  const cursorHandlers = {
    all(filters, done) {
      const results = [];
      return (event) => {
        const cursor = event.target.result;
        if (cursor) {
          const { value } = cursor;
          if (filterObject(filters, value)) {
            results.push(value);
          }
          cursor.continue();
        } else {
          done(results);
        }
      };
    },
    in(values, filters, done) {
      const results = [];
      let i = 0;
      return function (event) {
        const cursor = event.target.result;
        if (!cursor) {
          done(results);
          return;
        }
        const { key, value } = cursor;
        // `key` can be an array of two values (see `keyPath` in indices definitions).
        // `values` can be an array of arrays if we filter using an index whose key path
        // is an array (eg. `cursorHandlers.in([["bid/cid", 42], ["bid/cid", 43]], ...)`)
        while (key > values[i]) {
          // The cursor has passed beyond this key. Check next.
          ++i;
          if (i === values.length) {
            done(results); // There is no next. Stop searching.
            return;
          }
        }
        const isEqual = Array.isArray(key)
          ? arrayEqual(key, values[i])
          : key === values[i];
        if (isEqual) {
          if (filterObject(filters, value)) {
            results.push(value);
          }
          cursor.continue();
        } else {
          cursor.continue(values[i]);
        }
      };
    },
  };
  /**
   * Creates an IDB request and attach it the appropriate cursor event handler to
   * perform a list query.
   *
   * Multiple matching values are handled by passing an array.
   *
   * @param  {String}           cid        The collection id (ie. `{bid}/{cid}`)
   * @param  {IDBStore}         store      The IDB store.
   * @param  {Object}           filters    Filter the records by field.
   * @param  {Function}         done       The operation completion handler.
   * @return {IDBRequest}
   */
  function createListRequest(cid, store, filters, done) {
    const filterFields = Object.keys(filters);
    // If no filters, get all results in one bulk.
    if (filterFields.length === 0) {
      const request = store.index("cid").getAll(IDBKeyRange.only(cid));
      request.onsuccess = (event) => done(event.target.result);
      return request;
    }
    // Introspect filters and check if they leverage an indexed field.
    const indexField = filterFields.find((field) => {
      return INDEXED_FIELDS.includes(field);
    });
    if (!indexField) {
      // Iterate on all records for this collection (ie. cid)
      const isSubQuery = Object.keys(filters).some((key) => key.includes(".")); // (ie. filters: {"article.title": "hello"})
      if (isSubQuery) {
        const newFilter = transformSubObjectFilters(filters);
        const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
        request.onsuccess = cursorHandlers.all(newFilter, done);
        return request;
      }
      const request = store.index("cid").openCursor(IDBKeyRange.only(cid));
      request.onsuccess = cursorHandlers.all(filters, done);
      return request;
    }
    // If `indexField` was used already, don't filter again.
    const remainingFilters = omitKeys(filters, [indexField]);
    // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)
    const value = filters[indexField];
    // For the "id" field, use the primary key.
    const indexStore = indexField === "id" ? store : store.index(indexField);
    // WHERE IN equivalent clause
    if (Array.isArray(value)) {
      if (value.length === 0) {
        return done([]);
      }
      const values = value.map((i) => [cid, i]).sort();
      const range = IDBKeyRange.bound(values[0], values[values.length - 1]);
      const request = indexStore.openCursor(range);
      request.onsuccess = cursorHandlers.in(values, remainingFilters, done);
      return request;
    }
    // If no filters on custom attribute, get all results in one bulk.
    if (Object.keys(remainingFilters).length === 0) {
      const request = indexStore.getAll(IDBKeyRange.only([cid, value]));
      request.onsuccess = (event) => done(event.target.result);
      return request;
    }
    // WHERE field = value clause
    const request = indexStore.openCursor(IDBKeyRange.only([cid, value]));
    request.onsuccess = cursorHandlers.all(remainingFilters, done);
    return request;
  }
  class IDBError extends Error {
    constructor(method, err) {
      super(`IndexedDB ${method}() ${err.message}`);
      this.name = err.name;
      this.stack = err.stack;
    }
  }
  /**
   * IndexedDB adapter.
   *
   * This adapter doesn't support any options.
   */
  class IDB extends BaseAdapter {
    /* Expose the IDBError class publicly */
    static get IDBError() {
      return IDBError;
    }
    /**
     * Constructor.
     *
     * @param  {String} cid  The key base for this collection (eg. `bid/cid`)
     * @param  {Object} options
     * @param  {String} options.dbName         The IndexedDB name (default: `"KintoDB"`)
     * @param  {String} options.migrateOldData Whether old database data should be migrated (default: `false`)
     */
    constructor(cid, options = {}) {
      super();
      this.cid = cid;
      this.dbName = options.dbName || "KintoDB";
      this._options = options;
      this._db = null;
    }
    _handleError(method, err) {
      throw new IDBError(method, err);
    }
    /**
     * Ensures a connection to the IndexedDB database has been opened.
     *
     * @override
     * @return {Promise}
     */
    async open() {
      if (this._db) {
        return this;
      }
      // In previous versions, we used to have a database with name `${bid}/${cid}`.
      // Check if it exists, and migrate data once new schema is in place.
      // Note: the built-in migrations from IndexedDB can only be used if the
      // database name does not change.
      const dataToMigrate = this._options.migrateOldData
        ? await migrationRequired(this.cid)
        : null;
      this._db = await open(this.dbName, {
        version: 2,
        onupgradeneeded: (event) => {
          const db = event.target.result;
          if (event.oldVersion < 1) {
            // Records store
            const recordsStore = db.createObjectStore("records", {
              keyPath: ["_cid", "id"],
            });
            // An index to obtain all the records in a collection.
            recordsStore.createIndex("cid", "_cid");
            // Here we create indices for every known field in records by collection.
            // Local record status ("synced", "created", "updated", "deleted")
            recordsStore.createIndex("_status", ["_cid", "_status"]);
            // Last modified field
            recordsStore.createIndex("last_modified", [
              "_cid",
              "last_modified",
            ]);
            // Timestamps store
            db.createObjectStore("timestamps", {
              keyPath: "cid",
            });
          }
          if (event.oldVersion < 2) {
            // Collections store
            db.createObjectStore("collections", {
              keyPath: "cid",
            });
          }
        },
      });
      if (dataToMigrate) {
        const { records, timestamp } = dataToMigrate;
        await this.importBulk(records);
        await this.saveLastModified(
          timestamp !== null && timestamp !== void 0 ? timestamp : 0
        );
        console.log(`${this.cid}: data was migrated successfully.`);
        // Delete the old database.
        await deleteDatabase(this.cid);
        console.warn(`${this.cid}: old database was deleted.`);
      }
      return this;
    }
    /**
     * Closes current connection to the database.
     *
     * @override
     * @return {Promise}
     */
    close() {
      if (this._db) {
        this._db.close(); // indexedDB.close is synchronous
        this._db = null;
      }
      return Promise.resolve();
    }
    /**
     * Returns a transaction and an object store for a store name.
     *
     * To determine if a transaction has completed successfully, we should rather
     * listen to the transaction’s complete event rather than the IDBObjectStore
     * request’s success event, because the transaction may still fail after the
     * success event fires.
     *
     * @param  {String}      name  Store name
     * @param  {Function}    callback to execute
     * @param  {Object}      options Options
     * @param  {String}      options.mode  Transaction mode ("readwrite" or undefined)
     * @return {Object}
     */
    async prepare(name, callback, options) {
      await this.open();
      await execute(this._db, name, callback, options);
    }
    /**
     * Deletes every records in the current collection.
     *
     * @override
     * @return {Promise}
     */
    async clear() {
      try {
        await this.prepare(
          "records",
          (store) => {
            const range = IDBKeyRange.only(this.cid);
            const request = store.index("cid").openKeyCursor(range);
            request.onsuccess = (event) => {
              const cursor = event.target.result;
              if (cursor) {
                store.delete(cursor.primaryKey);
                cursor.continue();
              }
            };
            return request;
          },
          { mode: "readwrite" }
        );
      } catch (e) {
        this._handleError("clear", e);
      }
    }
    /**
     * Executes the set of synchronous CRUD operations described in the provided
     * callback within an IndexedDB transaction, for current db store.
     *
     * The callback will be provided an object exposing the following synchronous
     * CRUD operation methods: get, create, update, delete.
     *
     * Important note: because limitations in IndexedDB implementations, no
     * asynchronous code should be performed within the provided callback; the
     * promise will therefore be rejected if the callback returns a Promise.
     *
     * Options:
     * - {Array} preload: The list of record IDs to fetch and make available to
     *   the transaction object get() method (default: [])
     *
     * @example
     * const db = new IDB("example");
     * const result = await db.execute(transaction => {
     *   transaction.create({id: 1, title: "foo"});
     *   transaction.update({id: 2, title: "bar"});
     *   transaction.delete(3);
     *   return "foo";
     * });
     *
     * @override
     * @param  {Function} callback The operation description callback.
     * @param  {Object}   options  The options object.
     * @return {Promise}
     */
    async execute(callback, options = { preload: [] }) {
      // Transactions in IndexedDB are autocommited when a callback does not
      // perform any additional operation.
      // The way Promises are implemented in Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=1193394)
      // prevents using within an opened transaction.
      // To avoid managing asynchronocity in the specified `callback`, we preload
      // a list of record in order to execute the `callback` synchronously.
      // See also:
      // - http://stackoverflow.com/a/28388805/330911
      // - http://stackoverflow.com/a/10405196
      // - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
      let result;
      await this.prepare(
        "records",
        (store, abort) => {
          const runCallback = (preloaded = {}) => {
            // Expose a consistent API for every adapter instead of raw store methods.
            const proxy = transactionProxy(this, store, preloaded);
            // The callback is executed synchronously within the same transaction.
            try {
              const returned = callback(proxy);
              if (returned instanceof Promise) {
                // XXX: investigate how to provide documentation details in error.
                throw new Error(
                  "execute() callback should not return a Promise."
                );
              }
              // Bring to scope that will be returned (once promise awaited).
              result = returned;
            } catch (e) {
              // The callback has thrown an error explicitly. Abort transaction cleanly.
              abort && abort(e);
            }
          };
          // No option to preload records, go straight to `callback`.
          if (!options.preload) {
            runCallback();
            return;
          }
          // Preload specified records using a list request.
          const filters = { id: options.preload };
          createListRequest(this.cid, store, filters, (records) => {
            // Store obtained records by id.
            const preloaded = {};
            for (const record of records) {
              delete record._cid;
              preloaded[record.id] = record;
            }
            runCallback(preloaded);
          });
        },
        { mode: "readwrite" }
      );
      return result;
    }
    /**
     * Retrieve a record by its primary key from the IndexedDB database.
     *
     * @override
     * @param  {String} id The record id.
     * @return {Promise}
     */
    async get(id) {
      try {
        let record;
        await this.prepare("records", (store) => {
          store.get([this.cid, id]).onsuccess = (e) =>
            (record = e.target.result);
        });
        return record;
      } catch (e) {
        this._handleError("get", e);
      }
      return null;
    }
    /**
     * Lists all records from the IndexedDB database.
     *
     * @override
     * @param  {Object} params  The filters and order to apply to the results.
     * @return {Promise}
     */
    async list(
      params = {
        filters: {},
      }
    ) {
      const { filters } = params;
      try {
        let results = [];
        await this.prepare("records", (store) => {
          createListRequest(this.cid, store, filters, (_results) => {
            // we have received all requested records that match the filters,
            // we now park them within current scope and hide the `_cid` attribute.
            for (const result of _results) {
              delete result._cid;
            }
            results = _results;
          });
        });
        // The resulting list of records is sorted.
        // XXX: with some efforts, this could be fully implemented using IDB API.
        return params.order ? sortObjects(params.order, results) : results;
      } catch (e) {
        this._handleError("list", e);
      }
      return [];
    }
    /**
     * Store the lastModified value into metadata store.
     *
     * @override
     * @param  {Number}  lastModified
     * @return {Promise}
     */
    async saveLastModified(lastModified) {
      const value = lastModified || null;
      try {
        await this.prepare(
          "timestamps",
          (store) => {
            if (value === null) {
              store.delete(this.cid);
            } else {
              store.put({ cid: this.cid, value });
            }
          },
          { mode: "readwrite" }
        );
        return value;
      } catch (e) {
        this._handleError("saveLastModified", e);
      }
      return null;
    }
    /**
     * Retrieve saved lastModified value.
     *
     * @override
     * @return {Promise}
     */
    async getLastModified() {
      try {
        let entry = null;
        await this.prepare("timestamps", (store) => {
          store.get(this.cid).onsuccess = (e) => {
            entry = e.target.result;
          };
        });
        return entry ? entry.value : null;
      } catch (e) {
        this._handleError("getLastModified", e);
      }
      return null;
    }
    /**
     * Load a dump of records exported from a server.
     *
     * @deprecated Use {@link importBulk} instead.
     * @abstract
     * @param  {Array} records The records to load.
     * @return {Promise}
     */
    async loadDump(records) {
      return this.importBulk(records);
    }
    /**
     * Load records in bulk that were exported from a server.
     *
     * @abstract
     * @param  {Array} records The records to load.
     * @return {Promise}
     */
    async importBulk(records) {
      try {
        await this.execute((transaction) => {
          // Since the put operations are asynchronous, we chain
          // them together. The last one will be waited for the
          // `transaction.oncomplete` callback. (see #execute())
          let i = 0;
          putNext();
          function putNext() {
            if (i === records.length) {
              return;
            }
            // On error, `transaction.onerror` is called.
            transaction.update(records[i]).onsuccess = putNext;
            ++i;
          }
        });
        const previousLastModified = await this.getLastModified();
        const lastModified = Math.max(
          ...records.map((record) => record.last_modified)
        );
        if (previousLastModified && lastModified > previousLastModified) {
          await this.saveLastModified(lastModified);
        }
        return records;
      } catch (e) {
        this._handleError("importBulk", e);
      }
      return [];
    }
    async saveMetadata(metadata) {
      try {
        await this.prepare(
          "collections",
          (store) => store.put({ cid: this.cid, metadata }),
          { mode: "readwrite" }
        );
        return metadata;
      } catch (e) {
        this._handleError("saveMetadata", e);
        return null;
      }
    }
    async getMetadata() {
      try {
        let entry = null;
        await this.prepare("collections", (store) => {
          store.get(this.cid).onsuccess = (e) => (entry = e.target.result);
        });
        return entry ? entry.metadata : null;
      } catch (e) {
        this._handleError("getMetadata", e);
        return null;
      }
    }
  }
  /**
   * IDB transaction proxy.
   *
   * @param  {IDB} adapter        The call IDB adapter
   * @param  {IDBStore} store     The IndexedDB database store.
   * @param  {Array}    preloaded The list of records to make available to
   *                              get() (default: []).
   * @return {Object}
   */
  function transactionProxy(adapter, store, preloaded = {}) {
    const _cid = adapter.cid;
    return {
      create(record) {
        store.add(Object.assign(Object.assign({}, record), { _cid }));
      },
      update(record) {
        return store.put(Object.assign(Object.assign({}, record), { _cid }));
      },
      delete(id) {
        store.delete([_cid, id]);
      },
      get(id) {
        return preloaded[id];
      },
    };
  }
  /**
   * Up to version 10.X of kinto.js, each collection had its own collection.
   * The database name was `${bid}/${cid}` (eg. `"blocklists/certificates"`)
   * and contained only one store with the same name.
   */
  async function migrationRequired(dbName) {
    let exists = true;
    const db = await open(dbName, {
      version: 1,
      onupgradeneeded: (event) => {
        exists = false;
      },
    });
    // Check that the DB we're looking at is really a legacy one,
    // and not some remainder of the open() operation above.
    exists =
      db.objectStoreNames.contains("__meta__") &&
      db.objectStoreNames.contains(dbName);
    if (!exists) {
      db.close();
      // Testing the existence creates it, so delete it :)
      await deleteDatabase(dbName);
      return null;
    }
    console.warn(`${dbName}: old IndexedDB database found.`);
    try {
      // Scan all records.
      let records;
      await execute(db, dbName, (store) => {
        store.openCursor().onsuccess = cursorHandlers.all(
          {},
          (res) => (records = res)
        );
      });
      console.log(`${dbName}: found ${records.length} records.`);
      // Check if there's a entry for this.
      let timestamp = null;
      await execute(db, "__meta__", (store) => {
        store.get(`${dbName}-lastModified`).onsuccess = (e) => {
          timestamp = e.target.result ? e.target.result.value : null;
        };
      });
      // Some previous versions, also used to store the timestamps without prefix.
      if (!timestamp) {
        await execute(db, "__meta__", (store) => {
          store.get("lastModified").onsuccess = (e) => {
            timestamp = e.target.result ? e.target.result.value : null;
          };
        });
      }
      console.log(`${dbName}: ${timestamp ? "found" : "no"} timestamp.`);
      // Those will be inserted in the new database/schema.
      return { records: records, timestamp };
    } catch (e) {
      console.error("Error occured during migration", e);
      return null;
    } finally {
      db.close();
    }
  }

  const RECORD_FIELDS_TO_CLEAN = ["_status"];
  const AVAILABLE_HOOKS = ["incoming-changes"];
  const IMPORT_CHUNK_SIZE = 200;
  /**
   * Compare two records omitting local fields and synchronization
   * attributes (like _status and last_modified)
   * @param {Object} a    A record to compare.
   * @param {Object} b    A record to compare.
   * @param {Array} localFields Additional fields to ignore during the comparison
   * @return {boolean}
   */
  function recordsEqual(a, b, localFields = []) {
    const fieldsToClean = [
      ...RECORD_FIELDS_TO_CLEAN,
      "last_modified",
      ...localFields,
    ];
    const cleanLocal = (r) => omitKeys(r, fieldsToClean);
    return deepEqual(cleanLocal(a), cleanLocal(b));
  }
  /**
   * Synchronization result object.
   */
  class SyncResultObject {
    constructor() {
      /**
       * Current synchronization result status; becomes `false` when conflicts or
       * errors are registered.
       * @type {Boolean}
       */
      this.lastModified = null;
      this._lists = {
        errors: [],
        created: [],
        updated: [],
        deleted: [],
        published: [],
        conflicts: [],
        skipped: [],
        resolved: [],
        void: [],
      };
      this._cached = {};
    }
    /**
     * Adds entries for a given result type.
     *
     * @param {String} type    The result type.
     * @param {Array}  entries The result entries.
     * @return {SyncResultObject}
     */
    add(type, entries) {
      if (!Array.isArray(this._lists[type])) {
        console.warn(`Unknown type "${type}"`);
        return this;
      }
      if (!Array.isArray(entries)) {
        entries = [entries];
      }
      this._lists[type] = [...this._lists[type], ...entries];
      delete this._cached[type];
      return this;
    }
    get ok() {
      return this.errors.length + this.conflicts.length === 0;
    }
    get errors() {
      return this._lists.errors;
    }
    get conflicts() {
      return this._lists.conflicts;
    }
    get skipped() {
      return this._deduplicate("skipped");
    }
    get resolved() {
      return this._deduplicate("resolved");
    }
    get created() {
      return this._deduplicate("created");
    }
    get updated() {
      return this._deduplicate("updated");
    }
    get deleted() {
      return this._deduplicate("deleted");
    }
    get published() {
      return this._deduplicate("published");
    }
    _deduplicate(list) {
      if (!(list in this._cached)) {
        // Deduplicate entries by id. If the values don't have `id` attribute, just
        // keep all.
        const recordsWithoutId = new Set();
        const recordsById = new Map();
        this._lists[list].forEach((record) => {
          if (!record.id) {
            recordsWithoutId.add(record);
          } else {
            recordsById.set(record.id, record);
          }
        });
        this._cached[list] = Array.from(recordsById.values()).concat(
          Array.from(recordsWithoutId)
        );
      }
      return this._cached[list];
    }
    /**
     * Reinitializes result entries for a given result type.
     *
     * @param  {String} type The result type.
     * @return {SyncResultObject}
     */
    reset(type) {
      this._lists[type] = [];
      delete this._cached[type];
      return this;
    }
    toObject() {
      // Only used in tests.
      return {
        ok: this.ok,
        lastModified: this.lastModified,
        errors: this.errors,
        created: this.created,
        updated: this.updated,
        deleted: this.deleted,
        skipped: this.skipped,
        published: this.published,
        conflicts: this.conflicts,
        resolved: this.resolved,
      };
    }
  }
  class ServerWasFlushedError extends Error {
    constructor(clientTimestamp, serverTimestamp, message) {
      super(message);
      if (Error.captureStackTrace) {
        Error.captureStackTrace(this, ServerWasFlushedError);
      }
      this.clientTimestamp = clientTimestamp;
      this.serverTimestamp = serverTimestamp;
    }
  }
  function createUUIDSchema() {
    return {
      generate() {
        return v4();
      },
      validate(id) {
        return typeof id === "string" && RE_RECORD_ID.test(id);
      },
    };
  }
  function markStatus(record, status) {
    return Object.assign(Object.assign({}, record), { _status: status });
  }
  function markDeleted(record) {
    return markStatus(record, "deleted");
  }
  function markSynced(record) {
    return markStatus(record, "synced");
  }
  /**
   * Import a remote change into the local database.
   *
   * @param  {IDBTransactionProxy} transaction The transaction handler.
   * @param  {Object}              remote      The remote change object to import.
   * @param  {Array<String>}       localFields The list of fields that remain local.
   * @param  {String}              strategy    The {@link Collection.strategy}.
   * @return {Object}
   */
  function importChange(transaction, remote, localFields, strategy) {
    const local = transaction.get(remote.id);
    if (!local) {
      // Not found locally but remote change is marked as deleted; skip to
      // avoid recreation.
      if (remote.deleted) {
        return { type: "skipped", data: remote };
      }
      const synced = markSynced(remote);
      transaction.create(synced);
      return { type: "created", data: synced };
    }
    // Apply remote changes on local record.
    const synced = Object.assign(Object.assign({}, local), markSynced(remote));
    // With pull only, we don't need to compare records since we override them.
    if (strategy === Collection.strategy.PULL_ONLY) {
      if (remote.deleted) {
        transaction.delete(remote.id);
        return { type: "deleted", data: local };
      }
      transaction.update(synced);
      return { type: "updated", data: { old: local, new: synced } };
    }
    // With other sync strategies, we detect conflicts,
    // by comparing local and remote, ignoring local fields.
    const isIdentical = recordsEqual(local, remote, localFields);
    // Detect or ignore conflicts if record has also been modified locally.
    if (local._status !== "synced") {
      // Locally deleted, unsynced: scheduled for remote deletion.
      if (local._status === "deleted") {
        return { type: "skipped", data: local };
      }
      if (isIdentical) {
        // If records are identical, import anyway, so we bump the
        // local last_modified value from the server and set record
        // status to "synced".
        transaction.update(synced);
        return { type: "updated", data: { old: local, new: synced } };
      }
      if (
        local.last_modified !== undefined &&
        local.last_modified === remote.last_modified
      ) {
        // If our local version has the same last_modified as the remote
        // one, this represents an object that corresponds to a resolved
        // conflict. Our local version represents the final output, so
        // we keep that one. (No transaction operation to do.)
        // But if our last_modified is undefined,
        // that means we've created the same object locally as one on
        // the server, which *must* be a conflict.
        return { type: "void" };
      }
      return {
        type: "conflicts",
        data: { type: "incoming", local, remote },
      };
    }
    // Local record was synced.
    if (remote.deleted) {
      transaction.delete(remote.id);
      return { type: "deleted", data: local };
    }
    // Import locally.
    transaction.update(synced);
    // if identical, simply exclude it from all SyncResultObject lists
    if (isIdentical) {
      return { type: "void" };
    }
    return { type: "updated", data: { old: local, new: synced } };
  }
  /**
   * Abstracts a collection of records stored in the local database, providing
   * CRUD operations and synchronization helpers.
   */
  class Collection {
    constructor(bucket, name, kinto, options = {}) {
      this._bucket = bucket;
      this._name = name;
      this._lastModified = null;
      const db = options.adapter
        ? options.adapter(`${bucket}/${name}`, options.adapterOptions)
        : new IDB(`${bucket}/${name}`, options.adapterOptions);
      if (!(db instanceof BaseAdapter)) {
        throw new Error("Unsupported adapter.");
      }
      // public properties
      this.db = db;
      /**
       * The KintoBase instance.
       * @type {KintoBase}
       */
      this.kinto = kinto;
      /**
       * The event emitter instance.
       * @type {EventEmitter}
       */
      this.events = options.events;
      /**
       * The IdSchema instance.
       * @type {Object}
       */
      this.idSchema = this._validateIdSchema(options.idSchema);
      /**
       * The list of remote transformers.
       * @type {Array}
       */
      this.remoteTransformers = this._validateRemoteTransformers(
        options.remoteTransformers
      );
      /**
       * The list of hooks.
       * @type {Object}
       */
      this.hooks = this._validateHooks(options.hooks);
      /**
       * The list of fields names that will remain local.
       * @type {Array}
       */
      this.localFields = options.localFields || [];
    }
    /**
     * The HTTP client.
     * @type {KintoClient}
     */
    get api() {
      return this.kinto.api;
    }
    /**
     * The collection name.
     * @type {String}
     */
    get name() {
      return this._name;
    }
    /**
     * The bucket name.
     * @type {String}
     */
    get bucket() {
      return this._bucket;
    }
    /**
     * The last modified timestamp.
     * @type {Number}
     */
    get lastModified() {
      return this._lastModified;
    }
    /**
     * Synchronization strategies. Available strategies are:
     *
     * - `MANUAL`: Conflicts will be reported in a dedicated array.
     * - `SERVER_WINS`: Conflicts are resolved using remote data.
     * - `CLIENT_WINS`: Conflicts are resolved using local data.
     *
     * @type {Object}
     */
    static get strategy() {
      return {
        CLIENT_WINS: "client_wins",
        SERVER_WINS: "server_wins",
        PULL_ONLY: "pull_only",
        MANUAL: "manual",
      };
    }
    /**
     * Validates an idSchema.
     *
     * @param  {Object|undefined} idSchema
     * @return {Object}
     */
    _validateIdSchema(idSchema) {
      if (typeof idSchema === "undefined") {
        return createUUIDSchema();
      }
      if (typeof idSchema !== "object") {
        throw new Error("idSchema must be an object.");
      } else if (typeof idSchema.generate !== "function") {
        throw new Error("idSchema must provide a generate function.");
      } else if (typeof idSchema.validate !== "function") {
        throw new Error("idSchema must provide a validate function.");
      }
      return idSchema;
    }
    /**
     * Validates a list of remote transformers.
     *
     * @param  {Array|undefined} remoteTransformers
     * @return {Array}
     */
    _validateRemoteTransformers(remoteTransformers) {
      if (typeof remoteTransformers === "undefined") {
        return [];
      }
      if (!Array.isArray(remoteTransformers)) {
        throw new Error("remoteTransformers should be an array.");
      }
      return remoteTransformers.map((transformer) => {
        if (typeof transformer !== "object") {
          throw new Error("A transformer must be an object.");
        } else if (typeof transformer.encode !== "function") {
          throw new Error("A transformer must provide an encode function.");
        } else if (typeof transformer.decode !== "function") {
          throw new Error("A transformer must provide a decode function.");
        }
        return transformer;
      });
    }
    /**
     * Validate the passed hook is correct.
     *
     * @param {Array|undefined} hook.
     * @return {Array}
     **/
    _validateHook(hook) {
      if (!Array.isArray(hook)) {
        throw new Error("A hook definition should be an array of functions.");
      }
      return hook.map((fn) => {
        if (typeof fn !== "function") {
          throw new Error("A hook definition should be an array of functions.");
        }
        return fn;
      });
    }
    /**
     * Validates a list of hooks.
     *
     * @param  {Object|undefined} hooks
     * @return {Object}
     */
    _validateHooks(hooks) {
      if (typeof hooks === "undefined") {
        return {};
      }
      if (Array.isArray(hooks)) {
        throw new Error("hooks should be an object, not an array.");
      }
      if (typeof hooks !== "object") {
        throw new Error("hooks should be an object.");
      }
      const validatedHooks = {};
      for (const hook in hooks) {
        if (!AVAILABLE_HOOKS.includes(hook)) {
          throw new Error(
            "The hook should be one of " + AVAILABLE_HOOKS.join(", ")
          );
        }
        validatedHooks[hook] = this._validateHook(hooks[hook]);
      }
      return validatedHooks;
    }
    /**
     * Deletes every records in the current collection and marks the collection as
     * never synced.
     *
     * @return {Promise}
     */
    async clear() {
      await this.db.clear();
      await this.db.saveMetadata(null);
      await this.db.saveLastModified(null);
      return { data: [], permissions: {} };
    }
    /**
     * Encodes a record.
     *
     * @param  {String} type   Either "remote" or "local".
     * @param  {Object} record The record object to encode.
     * @return {Promise}
     */
    _encodeRecord(type, record) {
      const transformers = type === "remote" ? this.remoteTransformers : [];
      if (!transformers.length) {
        return Promise.resolve(record);
      }
      return waterfall(
        transformers.map((transformer) => {
          return (record) => transformer.encode(record);
        }),
        record
      );
    }
    /**
     * Decodes a record.
     *
     * @param  {String} type   Either "remote" or "local".
     * @param  {Object} record The record object to decode.
     * @return {Promise}
     */
    _decodeRecord(type, record) {
      const transformers = type === "remote" ? this.remoteTransformers : [];
      if (!transformers.length) {
        return Promise.resolve(record);
      }
      return waterfall(
        transformers.reverse().map((transformer) => {
          return (record) => transformer.decode(record);
        }),
        record
      );
    }
    /**
     * Adds a record to the local database, asserting that none
     * already exist with this ID.
     *
     * Note: If either the `useRecordId` or `synced` options are true, then the
     * record object must contain the id field to be validated. If none of these
     * options are true, an id is generated using the current IdSchema; in this
     * case, the record passed must not have an id.
     *
     * Options:
     * - {Boolean} synced       Sets record status to "synced" (default: `false`).
     * - {Boolean} useRecordId  Forces the `id` field from the record to be used,
     *                          instead of one that is generated automatically
     *                          (default: `false`).
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Promise}
     */
    create(
      record,
      options = {
        useRecordId: false,
        synced: false,
      }
    ) {
      // Validate the record and its ID (if any), even though this
      // validation is also done in the CollectionTransaction method,
      // because we need to pass the ID to preloadIds.
      const reject = (msg) => Promise.reject(new Error(msg));
      if (typeof record !== "object") {
        return reject("Record is not an object.");
      }
      if (
        (options.synced || options.useRecordId) &&
        !Object.prototype.hasOwnProperty.call(record, "id")
      ) {
        return reject(
          "Missing required Id; synced and useRecordId options require one"
        );
      }
      if (
        !options.synced &&
        !options.useRecordId &&
        Object.prototype.hasOwnProperty.call(record, "id")
      ) {
        return reject("Extraneous Id; can't create a record having one set.");
      }
      const newRecord = Object.assign(Object.assign({}, record), {
        id:
          options.synced || options.useRecordId
            ? record.id
            : this.idSchema.generate(record),
        _status: options.synced ? "synced" : "created",
      });
      if (!this.idSchema.validate(newRecord.id)) {
        return reject(`Invalid Id: ${newRecord.id}`);
      }
      return this.execute((txn) => txn.create(newRecord), {
        preloadIds: [newRecord.id],
      }).catch((err) => {
        if (options.useRecordId) {
          throw new Error(
            "Couldn't create record. It may have been virtually deleted."
          );
        }
        throw err;
      });
    }
    /**
     * Like {@link CollectionTransaction#update}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} synced: Sets record status to "synced" (default: false)
     * - {Boolean} patch:  Extends the existing record instead of overwriting it
     *   (default: false)
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Promise}
     */
    update(
      record,
      options = {
        synced: false,
        patch: false,
      }
    ) {
      // Validate the record and its ID, even though this validation is
      // also done in the CollectionTransaction method, because we need
      // to pass the ID to preloadIds.
      if (typeof record !== "object") {
        return Promise.reject(new Error("Record is not an object."));
      }
      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        return Promise.reject(new Error("Cannot update a record missing id."));
      }
      if (!this.idSchema.validate(record.id)) {
        return Promise.reject(new Error(`Invalid Id: ${record.id}`));
      }
      return this.execute(
        (txn) => {
          var _a, _b;
          return txn.update(record, {
            synced:
              (_a = options.synced) !== null && _a !== void 0 ? _a : false,
            patch: (_b = options.patch) !== null && _b !== void 0 ? _b : false,
          });
        },
        {
          preloadIds: [record.id],
        }
      );
    }
    /**
     * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
     *
     * @param  {Object} record
     * @return {Promise}
     */
    upsert(record) {
      // Validate the record and its ID, even though this validation is
      // also done in the CollectionTransaction method, because we need
      // to pass the ID to preloadIds.
      if (typeof record !== "object") {
        return Promise.reject(new Error("Record is not an object."));
      }
      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        return Promise.reject(new Error("Cannot update a record missing id."));
      }
      if (!this.idSchema.validate(record.id)) {
        return Promise.reject(new Error(`Invalid Id: ${record.id}`));
      }
      return this.execute((txn) => txn.upsert(record), {
        preloadIds: [record.id],
      });
    }
    /**
     * Like {@link CollectionTransaction#get}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {String} id
     * @param  {Object} options
     * @return {Promise}
     */
    get(id, options = { includeDeleted: false }) {
      return this.execute((txn) => txn.get(id, options), { preloadIds: [id] });
    }
    /**
     * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
     *
     * @param  {String} id
     * @return {Promise}
     */
    getAny(id) {
      return this.execute((txn) => txn.getAny(id), { preloadIds: [id] });
    }
    /**
     * Same as {@link Collection#delete}, but wrapped in its own transaction.
     *
     * Options:
     * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
     *   update its `_status` attribute to `deleted` instead (default: true)
     *
     * @param  {String} id       The record's Id.
     * @param  {Object} options  The options object.
     * @return {Promise}
     */
    delete(id, options = { virtual: true }) {
      return this.execute(
        (transaction) => {
          return transaction.delete(id, options);
        },
        { preloadIds: [id] }
      );
    }
    /**
     * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter.
     *
     * @return {Promise}
     */
    async deleteAll() {
      const { data } = await this.list({}, { includeDeleted: false });
      const recordIds = data.map((record) => record.id);
      return this.execute(
        (transaction) => {
          return transaction.deleteAll(recordIds);
        },
        { preloadIds: recordIds }
      );
    }
    /**
     * The same as {@link CollectionTransaction#deleteAny}, but wrapped
     * in its own transaction.
     *
     * @param  {String} id       The record's Id.
     * @return {Promise}
     */
    deleteAny(id) {
      return this.execute((txn) => txn.deleteAny(id), { preloadIds: [id] });
    }
    /**
     * Lists records from the local database.
     *
     * Params:
     * - {Object} filters Filter the results (default: `{}`).
     * - {String} order   The order to apply   (default: `-last_modified`).
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {Object} params  The filters and order to apply to the results.
     * @param  {Object} options The options object.
     * @return {Promise}
     */
    async list(params = {}, options = { includeDeleted: false }) {
      params = Object.assign({ order: "-last_modified", filters: {} }, params);
      const results = await this.db.list(params);
      let data = results;
      if (!options.includeDeleted) {
        data = results.filter((record) => record._status !== "deleted");
      }
      return { data, permissions: {} };
    }
    /**
     * Imports remote changes into the local database.
     * This method is in charge of detecting the conflicts, and resolve them
     * according to the specified strategy.
     * @param  {SyncResultObject} syncResultObject The sync result object.
     * @param  {Array}            decodedChanges   The list of changes to import in the local database.
     * @param  {String}           strategy         The {@link Collection.strategy} (default: MANUAL)
     * @return {Promise}
     */
    async importChanges(
      syncResultObject,
      decodedChanges,
      strategy = Collection.strategy.MANUAL
    ) {
      // Retrieve records matching change ids.
      try {
        for (let i = 0; i < decodedChanges.length; i += IMPORT_CHUNK_SIZE) {
          const slice = decodedChanges.slice(i, i + IMPORT_CHUNK_SIZE);
          const { imports, resolved } = await this.db.execute(
            (transaction) => {
              const imports = slice.map((remote) => {
                // Store remote change into local database.
                return importChange(
                  transaction,
                  remote,
                  this.localFields,
                  strategy
                );
              });
              const conflicts = imports
                .filter((i) => i.type === "conflicts")
                .map((i) => i.data);
              const resolved = this._handleConflicts(
                transaction,
                conflicts,
                strategy
              );
              return { imports, resolved };
            },
            { preload: slice.map((record) => record.id) }
          );
          // Lists of created/updated/deleted records
          imports.forEach(({ type, data }) => syncResultObject.add(type, data));
          // Automatically resolved conflicts (if not manual)
          if (resolved.length > 0) {
            syncResultObject.reset("conflicts").add("resolved", resolved);
          }
        }
      } catch (err) {
        const data = {
          type: "incoming",
          message: err.message,
          stack: err.stack,
        };
        // XXX one error of the whole transaction instead of per atomic op
        syncResultObject.add("errors", data);
      }
      return syncResultObject;
    }
    /**
     * Imports the responses of pushed changes into the local database.
     * Basically it stores the timestamp assigned by the server into the local
     * database.
     * @param  {SyncResultObject} syncResultObject The sync result object.
     * @param  {Array}            toApplyLocally   The list of changes to import in the local database.
     * @param  {Array}            conflicts        The list of conflicts that have to be resolved.
     * @param  {String}           strategy         The {@link Collection.strategy}.
     * @return {Promise}
     */
    async _applyPushedResults(
      syncResultObject,
      toApplyLocally,
      conflicts,
      strategy = Collection.strategy.MANUAL
    ) {
      const toDeleteLocally = toApplyLocally.filter((r) => r.deleted);
      const toUpdateLocally = toApplyLocally.filter((r) => !r.deleted);
      const { published, resolved } = await this.db.execute((transaction) => {
        const updated = toUpdateLocally.map((record) => {
          const synced = markSynced(record);
          transaction.update(synced);
          return synced;
        });
        const deleted = toDeleteLocally.map((record) => {
          transaction.delete(record.id);
          // Amend result data with the deleted attribute set
          return { id: record.id, deleted: true };
        });
        const published = updated.concat(deleted);
        // Handle conflicts, if any
        const resolved = this._handleConflicts(
          transaction,
          conflicts,
          strategy
        );
        return { published, resolved };
      });
      syncResultObject.add("published", published);
      if (resolved.length > 0) {
        syncResultObject
          .reset("conflicts")
          .reset("resolved")
          .add("resolved", resolved);
      }
      return syncResultObject;
    }
    /**
     * Handles synchronization conflicts according to specified strategy.
     *
     * @param  {SyncResultObject} result    The sync result object.
     * @param  {String}           strategy  The {@link Collection.strategy}.
     * @return {Promise<Array<Object>>} The resolved conflicts, as an
     *    array of {accepted, rejected} objects
     */
    _handleConflicts(transaction, conflicts, strategy) {
      if (strategy === Collection.strategy.MANUAL) {
        return [];
      }
      return conflicts.map((conflict) => {
        const resolution =
          strategy === Collection.strategy.CLIENT_WINS
            ? conflict.local
            : conflict.remote;
        const rejected =
          strategy === Collection.strategy.CLIENT_WINS
            ? conflict.remote
            : conflict.local;
        let accepted, status, id;
        if (resolution === null) {
          // We "resolved" with the server-side deletion. Delete locally.
          // This only happens during SERVER_WINS because the local
          // version of a record can never be null.
          // We can get "null" from the remote side if we got a conflict
          // and there is no remote version available; see src/http
          // batch.js:aggregate.
          transaction.delete(conflict.local.id);
          accepted = null;
          // The record was deleted, but that status is "synced" with
          // the server, so we don't need to push the change.
          status = "synced";
          id = conflict.local.id;
        } else {
          const updated = this._resolveRaw(conflict, resolution);
          transaction.update(updated);
          accepted = updated;
          status = updated._status;
          id = updated.id;
        }
        return { rejected, accepted, id, _status: status };
      });
    }
    /**
     * Execute a bunch of operations in a transaction.
     *
     * This transaction should be atomic -- either all of its operations
     * will succeed, or none will.
     *
     * The argument to this function is itself a function which will be
     * called with a {@link CollectionTransaction}. Collection methods
     * are available on this transaction, but instead of returning
     * promises, they are synchronous. execute() returns a Promise whose
     * value will be the return value of the provided function.
     *
     * Most operations will require access to the record itself, which
     * must be preloaded by passing its ID in the preloadIds option.
     *
     * Options:
     * - {Array} preloadIds: list of IDs to fetch at the beginning of
     *   the transaction
     *
     * @return {Promise} Resolves with the result of the given function
     *    when the transaction commits.
     */
    execute(doOperations, { preloadIds = [] } = {}) {
      for (const id of preloadIds) {
        if (!this.idSchema.validate(id)) {
          return Promise.reject(Error(`Invalid Id: ${id}`));
        }
      }
      return this.db.execute(
        (transaction) => {
          const txn = new CollectionTransaction(this, transaction);
          const result = doOperations(txn);
          txn.emitEvents();
          return result;
        },
        { preload: preloadIds }
      );
    }
    /**
     * Resets the local records as if they were never synced; existing records are
     * marked as newly created, deleted records are dropped.
     *
     * A next call to {@link Collection.sync} will thus republish the whole
     * content of the local collection to the server.
     *
     * @return {Promise} Resolves with the number of processed records.
     */
    async resetSyncStatus() {
      const unsynced = await this.list(
        { filters: { _status: ["deleted", "synced"] }, order: "" },
        { includeDeleted: true }
      );
      await this.db.execute((transaction) => {
        unsynced.data.forEach((record) => {
          if (record._status === "deleted") {
            // Garbage collect deleted records.
            transaction.delete(record.id);
          } else {
            // Records that were synced become «created».
            transaction.update(
              Object.assign(Object.assign({}, record), {
                last_modified: undefined,
                _status: "created",
              })
            );
          }
        });
      });
      this._lastModified = null;
      await this.db.saveLastModified(null);
      return unsynced.data.length;
    }
    /**
     * Returns an object containing two lists:
     *
     * - `toDelete`: unsynced deleted records we can safely delete;
     * - `toSync`: local updates to send to the server.
     *
     * @return {Promise}
     */
    async gatherLocalChanges() {
      const unsynced = await this.list({
        filters: { _status: ["created", "updated"] },
        order: "",
      });
      const deleted = await this.list(
        { filters: { _status: "deleted" }, order: "" },
        { includeDeleted: true }
      );
      return await Promise.all(
        unsynced.data
          .concat(deleted.data)
          .map(this._encodeRecord.bind(this, "remote"))
      );
    }
    /**
     * Fetch remote changes, import them to the local database, and handle
     * conflicts according to `options.strategy`. Then, updates the passed
     * {@link SyncResultObject} with import results.
     *
     * Options:
     * - {String} strategy: The selected sync strategy.
     * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
     * - {Array<String>} exclude: A list of record ids to exclude from pull.
     * - {Object} headers: The HTTP headers to use in the request.
     * - {int} retry: The number of retries to do if the HTTP request fails.
     * - {int} lastModified: The timestamp to use in `?_since` query.
     *
     * @param  {KintoClient.Collection} client           Kinto client Collection instance.
     * @param  {SyncResultObject}       syncResultObject The sync result object.
     * @param  {Object}                 options          The options object.
     * @return {Promise}
     */
    async pullChanges(client, syncResultObject, options = {}) {
      if (!syncResultObject.ok) {
        return syncResultObject;
      }
      const since = this.lastModified
        ? this.lastModified
        : await this.db.getLastModified();
      options = Object.assign(
        {
          strategy: Collection.strategy.MANUAL,
          lastModified: since,
          headers: {},
        },
        options
      );
      // Optionally ignore some records when pulling for changes.
      // (avoid redownloading our own changes on last step of #sync())
      let filters;
      if (options.exclude) {
        // Limit the list of excluded records to the first 50 records in order
        // to remain under de-facto URL size limit (~2000 chars).
        // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184
        const exclude_id = options.exclude
          .slice(0, 50)
          .map((r) => r.id)
          .join(",");
        filters = { exclude_id };
      }
      if (options.expectedTimestamp) {
        filters = Object.assign(Object.assign({}, filters), {
          _expected: options.expectedTimestamp,
        });
      }
      // First fetch remote changes from the server
      const { data, last_modified } = await client.listRecords({
        // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
        since: options.lastModified ? `${options.lastModified}` : undefined,
        headers: options.headers,
        retry: options.retry,
        // Fetch every page by default (FIXME: option to limit pages, see #277)
        pages: Infinity,
        filters,
      });
      // last_modified is the ETag header value (string).
      // For retro-compatibility with first kinto.js versions
      // parse it to integer.
      const unquoted = last_modified ? parseInt(last_modified, 10) : undefined;
      // Check if server was flushed.
      // This is relevant for the Kinto demo server
      // (and thus for many new comers).
      const localSynced = options.lastModified;
      const serverChanged = unquoted && unquoted > options.lastModified;
      const emptyCollection = data.length === 0;
      if (!options.exclude && localSynced && serverChanged && emptyCollection) {
        const e = new ServerWasFlushedError(
          localSynced,
          unquoted,
          "Server has been flushed. Client Side Timestamp: " +
            localSynced +
            " Server Side Timestamp: " +
            unquoted
        );
        throw e;
      }
      // Atomic updates are not sensible here because unquoted is not
      // computed as a function of syncResultObject.lastModified.
      // eslint-disable-next-line require-atomic-updates
      syncResultObject.lastModified = unquoted;
      // Decode incoming changes.
      const decodedChanges = await Promise.all(
        data.map((change) => {
          return this._decodeRecord("remote", change);
        })
      );
      // Hook receives decoded records.
      const payload = { lastModified: unquoted, changes: decodedChanges };
      const afterHooks = await this.applyHook("incoming-changes", payload);
      // No change, nothing to import.
      if (afterHooks.changes.length > 0) {
        // Reflect these changes locally
        await this.importChanges(
          syncResultObject,
          afterHooks.changes,
          options.strategy
        );
      }
      return syncResultObject;
    }
    applyHook(hookName, payload) {
      if (typeof this.hooks[hookName] === "undefined") {
        return Promise.resolve(payload);
      }
      return waterfall(
        this.hooks[hookName].map((hook) => {
          return (record) => {
            const result = hook(payload, this);
            const resultThenable = result && typeof result.then === "function";
            const resultChanges =
              result && Object.prototype.hasOwnProperty.call(result, "changes");
            if (!(resultThenable || resultChanges)) {
              throw new Error(
                `Invalid return value for hook: ${JSON.stringify(result)} has no 'then()' or 'changes' properties`
              );
            }
            return result;
          };
        }),
        payload
      );
    }
    /**
     * Publish local changes to the remote server and updates the passed
     * {@link SyncResultObject} with publication results.
     *
     * Options:
     * - {String} strategy: The selected sync strategy.
     * - {Object} headers: The HTTP headers to use in the request.
     * - {int} retry: The number of retries to do if the HTTP request fails.
     *
     * @param  {KintoClient.Collection} client           Kinto client Collection instance.
     * @param  {SyncResultObject}       syncResultObject The sync result object.
     * @param  {Object}                 changes          The change object.
     * @param  {Array}                  changes.toDelete The list of records to delete.
     * @param  {Array}                  changes.toSync   The list of records to create/update.
     * @param  {Object}                 options          The options object.
     * @return {Promise}
     */
    async pushChanges(client, changes, syncResultObject, options = {}) {
      if (!syncResultObject.ok) {
        return syncResultObject;
      }
      // FIXME: replacing `undefined` with Collection.strategy.CLIENT_WINS breaks tests
      const safe = !options.strategy || options.strategy !== undefined;
      const toDelete = changes.filter((r) => r._status === "deleted");
      const toSync = changes.filter((r) => r._status != "deleted");
      // Perform a batch request with every changes.
      const synced = await client.batch(
        (batch) => {
          toDelete.forEach((r) => {
            // never published locally deleted records should not be pusblished
            if (r.last_modified) {
              batch.deleteRecord(r);
            }
          });
          toSync.forEach((r) => {
            // Clean local fields (like _status) before sending to server.
            const published = this.cleanLocalFields(r);
            if (r._status === "created") {
              batch.createRecord(published);
            } else {
              batch.updateRecord(published);
            }
          });
        },
        {
          headers: options.headers,
          retry: options.retry,
          safe,
          aggregate: true,
        }
      );
      // Store outgoing errors into sync result object
      syncResultObject.add(
        "errors",
        synced.errors.map((e) =>
          Object.assign(Object.assign({}, e), { type: "outgoing" })
        )
      );
      // Store outgoing conflicts into sync result object
      const conflicts = [];
      for (const { type, local, remote } of synced.conflicts) {
        // Note: we ensure that local data are actually available, as they may
        // be missing in the case of a published deletion.
        const safeLocal = (local && local.data) || { id: remote.id };
        const realLocal = await this._decodeRecord("remote", safeLocal);
        // We can get "null" from the remote side if we got a conflict
        // and there is no remote version available; see src/http
        // batch.js:aggregate.
        const realRemote =
          remote && (await this._decodeRecord("remote", remote));
        const conflict = { type, local: realLocal, remote: realRemote };
        conflicts.push(conflict);
      }
      syncResultObject.add("conflicts", conflicts);
      // Records that must be deleted are either deletions that were pushed
      // to server (published) or deleted records that were never pushed (skipped).
      const missingRemotely = synced.skipped.map((r) =>
        Object.assign(Object.assign({}, r), { deleted: true })
      );
      // For created and updated records, the last_modified coming from server
      // will be stored locally.
      // Reflect publication results locally using the response from
      // the batch request.
      const published = synced.published.map((c) => c.data);
      const toApplyLocally = published.concat(missingRemotely);
      // Apply the decode transformers, if any
      const decoded = await Promise.all(
        toApplyLocally.map((record) => {
          return this._decodeRecord("remote", record);
        })
      );
      // We have to update the local records with the responses of the server
      // (eg. last_modified values etc.).
      if (decoded.length > 0 || conflicts.length > 0) {
        await this._applyPushedResults(
          syncResultObject,
          decoded,
          conflicts,
          options.strategy
        );
      }
      return syncResultObject;
    }
    /**
     * Return a copy of the specified record without the local fields.
     *
     * @param  {Object} record  A record with potential local fields.
     * @return {Object}
     */
    cleanLocalFields(record) {
      const localKeys = [...RECORD_FIELDS_TO_CLEAN, ...this.localFields];
      return omitKeys(record, localKeys);
    }
    /**
     * Resolves a conflict, updating local record according to proposed
     * resolution — keeping remote record `last_modified` value as a reference for
     * further batch sending.
     *
     * @param  {Object} conflict   The conflict object.
     * @param  {Object} resolution The proposed record.
     * @return {Promise}
     */
    resolve(conflict, resolution) {
      return this.db.execute((transaction) => {
        const updated = this._resolveRaw(conflict, resolution);
        transaction.update(updated);
        return { data: updated, permissions: {} };
      });
    }
    /**
     * @private
     */
    _resolveRaw(conflict, resolution) {
      const resolved = Object.assign(Object.assign({}, resolution), {
        // Ensure local record has the latest authoritative timestamp
        last_modified: conflict.remote && conflict.remote.last_modified,
      });
      // If the resolution object is strictly equal to the
      // remote record, then we can mark it as synced locally.
      // Otherwise, mark it as updated (so that the resolution is pushed).
      const synced = deepEqual(resolved, conflict.remote);
      return markStatus(resolved, synced ? "synced" : "updated");
    }
    /**
     * Synchronize remote and local data. The promise will resolve with a
     * {@link SyncResultObject}, though will reject:
     *
     * - if the server is currently backed off;
     * - if the server has been detected flushed.
     *
     * Options:
     * - {Object} headers: HTTP headers to attach to outgoing requests.
     * - {String} expectedTimestamp: A timestamp to use as a "cache busting" query parameter.
     * - {Number} retry: Number of retries when server fails to process the request (default: 1).
     * - {Collection.strategy} strategy: See {@link Collection.strategy}.
     * - {Boolean} ignoreBackoff: Force synchronization even if server is currently
     *   backed off.
     * - {String} bucket: The remove bucket id to use (default: null)
     * - {String} collection: The remove collection id to use (default: null)
     * - {String} remote The remote Kinto server endpoint to use (default: null).
     *
     * @param  {Object} options Options.
     * @return {Promise}
     * @throws {Error} If an invalid remote option is passed.
     */
    async sync(
      options = {
        strategy: Collection.strategy.MANUAL,
        headers: {},
        retry: 1,
        ignoreBackoff: false,
        bucket: null,
        collection: null,
        remote: null,
        expectedTimestamp: null,
      }
    ) {
      var _a, _b;
      options = Object.assign(Object.assign({}, options), {
        bucket: options.bucket || this.bucket,
        collection: options.collection || this.name,
      });
      const previousRemote = this.api.remote;
      if (options.remote) {
        // Note: setting the remote ensures it's valid, throws when invalid.
        this.api.remote = options.remote;
      }
      if (!options.ignoreBackoff && this.api.backoff > 0) {
        const seconds = Math.ceil(this.api.backoff / 1000);
        return Promise.reject(
          new Error(
            `Server is asking clients to back off; retry in ${seconds}s or use the ignoreBackoff option.`
          )
        );
      }
      const client = this.api
        .bucket(options.bucket)
        .collection(options.collection);
      const result = new SyncResultObject();
      try {
        // Fetch collection metadata.
        await this.pullMetadata(client, options);
        // Fetch last changes from the server.
        await this.pullChanges(client, result, options);
        const { lastModified } = result;
        if (options.strategy !== Collection.strategy.PULL_ONLY) {
          // Fetch local changes
          const toSync = await this.gatherLocalChanges();
          // Publish local changes and pull local resolutions
          await this.pushChanges(client, toSync, result, options);
          // Publish local resolution of push conflicts to server (on CLIENT_WINS)
          const resolvedUnsynced = result.resolved.filter(
            (r) => r._status !== "synced"
          );
          if (resolvedUnsynced.length > 0) {
            const resolvedEncoded = await Promise.all(
              resolvedUnsynced.map((resolution) => {
                let record = resolution.accepted;
                if (record === null) {
                  record = { id: resolution.id, _status: resolution._status };
                }
                return this._encodeRecord("remote", record);
              })
            );
            await this.pushChanges(client, resolvedEncoded, result, options);
          }
          // Perform a last pull to catch changes that occured after the last pull,
          // while local changes were pushed. Do not do it nothing was pushed.
          if (result.published.length > 0) {
            // Avoid redownloading our own changes during the last pull.
            const pullOpts = Object.assign(Object.assign({}, options), {
              lastModified,
              exclude: result.published,
            });
            await this.pullChanges(client, result, pullOpts);
          }
        }
        // Don't persist lastModified value if any conflict or error occured
        if (result.ok) {
          // No conflict occured, persist collection's lastModified value
          this._lastModified = await this.db.saveLastModified(
            result.lastModified
          );
        }
      } catch (e) {
        (_a = this.events) === null || _a === void 0
          ? void 0
          : _a.emit(
              "sync:error",
              Object.assign(Object.assign({}, options), { error: e })
            );
        throw e;
      } finally {
        // Ensure API default remote is reverted if a custom one's been used
        this.api.remote = previousRemote;
      }
      (_b = this.events) === null || _b === void 0
        ? void 0
        : _b.emit(
            "sync:success",
            Object.assign(Object.assign({}, options), { result })
          );
      return result;
    }
    /**
     * Load a list of records already synced with the remote server.
     *
     * The local records which are unsynced or whose timestamp is either missing
     * or superior to those being loaded will be ignored.
     *
     * @deprecated Use {@link importBulk} instead.
     * @param  {Array} records The previously exported list of records to load.
     * @return {Promise} with the effectively imported records.
     */
    async loadDump(records) {
      return this.importBulk(records);
    }
    /**
     * Load a list of records already synced with the remote server.
     *
     * The local records which are unsynced or whose timestamp is either missing
     * or superior to those being loaded will be ignored.
     *
     * @param  {Array} records The previously exported list of records to load.
     * @return {Promise} with the effectively imported records.
     */
    async importBulk(records) {
      if (!Array.isArray(records)) {
        throw new Error("Records is not an array.");
      }
      for (const record of records) {
        if (
          !Object.prototype.hasOwnProperty.call(record, "id") ||
          !this.idSchema.validate(record.id)
        ) {
          throw new Error("Record has invalid ID: " + JSON.stringify(record));
        }
        if (!record.last_modified) {
          throw new Error(
            "Record has no last_modified value: " + JSON.stringify(record)
          );
        }
      }
      // Fetch all existing records from local database,
      // and skip those who are newer or not marked as synced.
      // XXX filter by status / ids in records
      const { data } = await this.list({}, { includeDeleted: true });
      const existingById = data.reduce((acc, record) => {
        acc[record.id] = record;
        return acc;
      }, {});
      const newRecords = records.filter((record) => {
        const localRecord = existingById[record.id];
        const shouldKeep =
          // No local record with this id.
          localRecord === undefined ||
          // Or local record is synced
          (localRecord._status === "synced" &&
            // And was synced from server
            localRecord.last_modified !== undefined &&
            // And is older than imported one.
            record.last_modified > localRecord.last_modified);
        return shouldKeep;
      });
      return await this.db.importBulk(newRecords.map(markSynced));
    }
    async pullMetadata(client, options = {}) {
      const { expectedTimestamp, headers } = options;
      const query = expectedTimestamp
        ? { query: { _expected: expectedTimestamp.toString() } }
        : undefined;
      const metadata = await client.getData(
        Object.assign(Object.assign({}, query), { headers })
      );
      return this.db.saveMetadata(metadata);
    }
    async metadata() {
      return this.db.getMetadata();
    }
  }
  /**
   * A Collection-oriented wrapper for an adapter's transaction.
   *
   * This defines the high-level functions available on a collection.
   * The collection itself offers functions of the same name. These will
   * perform just one operation in its own transaction.
   */
  class CollectionTransaction {
    constructor(collection, adapterTransaction) {
      this.collection = collection;
      this.adapterTransaction = adapterTransaction;
      this._events = [];
    }
    _queueEvent(action, payload) {
      this._events.push({ action, payload });
    }
    /**
     * Emit queued events, to be called once every transaction operations have
     * been executed successfully.
     */
    emitEvents() {
      var _a, _b;
      for (const { action, payload } of this._events) {
        (_a = this.collection.events) === null || _a === void 0
          ? void 0
          : _a.emit(action, payload);
      }
      if (this._events.length > 0) {
        const targets = this._events.map(({ action, payload }) =>
          Object.assign({ action }, payload)
        );
        (_b = this.collection.events) === null || _b === void 0
          ? void 0
          : _b.emit("change", { targets });
      }
      this._events = [];
    }
    /**
     * Retrieve a record by its id from the local database, or
     * undefined if none exists.
     *
     * This will also return virtually deleted records.
     *
     * @param  {String} id
     * @return {Object}
     */
    getAny(id) {
      const record = this.adapterTransaction.get(id);
      return { data: record, permissions: {} };
    }
    /**
     * Retrieve a record by its id from the local database.
     *
     * Options:
     * - {Boolean} includeDeleted: Include virtually deleted records.
     *
     * @param  {String} id
     * @param  {Object} options
     * @return {Object}
     */
    get(id, options = { includeDeleted: false }) {
      const res = this.getAny(id);
      if (
        !res.data ||
        (!options.includeDeleted && res.data._status === "deleted")
      ) {
        throw new Error(`Record with id=${id} not found.`);
      }
      return res;
    }
    /**
     * Deletes a record from the local database.
     *
     * Options:
     * - {Boolean} virtual: When set to `true`, doesn't actually delete the record,
     *   update its `_status` attribute to `deleted` instead (default: true)
     *
     * @param  {String} id       The record's Id.
     * @param  {Object} options  The options object.
     * @return {Object}
     */
    delete(id, options = { virtual: true }) {
      // Ensure the record actually exists.
      const existing = this.adapterTransaction.get(id);
      const alreadyDeleted = existing && existing._status === "deleted";
      if (!existing || (alreadyDeleted && options.virtual)) {
        throw new Error(`Record with id=${id} not found.`);
      }
      // Virtual updates status.
      if (options.virtual) {
        this.adapterTransaction.update(markDeleted(existing));
      } else {
        // Delete for real.
        this.adapterTransaction.delete(id);
      }
      this._queueEvent("delete", { data: existing });
      return { data: existing, permissions: {} };
    }
    /**
     * Soft delete all records from the local database.
     *
     * @param  {Array} ids        Array of non-deleted Record Ids.
     * @return {Object}
     */
    deleteAll(ids) {
      const existingRecords = [];
      ids.forEach((id) => {
        existingRecords.push(this.adapterTransaction.get(id));
        this.delete(id);
      });
      this._queueEvent("deleteAll", { data: existingRecords });
      return { data: existingRecords, permissions: {} };
    }
    /**
     * Deletes a record from the local database, if any exists.
     * Otherwise, do nothing.
     *
     * @param  {String} id       The record's Id.
     * @return {Object}
     */
    deleteAny(id) {
      const existing = this.adapterTransaction.get(id);
      if (existing) {
        this.adapterTransaction.update(markDeleted(existing));
        this._queueEvent("delete", { data: existing });
      }
      return {
        data: Object.assign({ id }, existing),
        deleted: !!existing,
        permissions: {},
      };
    }
    /**
     * Adds a record to the local database, asserting that none
     * already exist with this ID.
     *
     * @param  {Object} record, which must contain an ID
     * @return {Object}
     */
    create(record) {
      if (typeof record !== "object") {
        throw new Error("Record is not an object.");
      }
      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot create a record missing id");
      }
      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error(`Invalid Id: ${record.id}`);
      }
      this.adapterTransaction.create(record);
      this._queueEvent("create", { data: record });
      return { data: record, permissions: {} };
    }
    /**
     * Updates a record from the local database.
     *
     * Options:
     * - {Boolean} synced: Sets record status to "synced" (default: false)
     * - {Boolean} patch:  Extends the existing record instead of overwriting it
     *   (default: false)
     *
     * @param  {Object} record
     * @param  {Object} options
     * @return {Object}
     */
    update(
      record,
      options = {
        synced: false,
        patch: false,
      }
    ) {
      if (typeof record !== "object") {
        throw new Error("Record is not an object.");
      }
      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot update a record missing id.");
      }
      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error(`Invalid Id: ${record.id}`);
      }
      const oldRecord = this.adapterTransaction.get(record.id);
      if (!oldRecord) {
        throw new Error(`Record with id=${record.id} not found.`);
      }
      const newRecord = options.patch
        ? Object.assign(Object.assign({}, oldRecord), record)
        : record;
      const updated = this._updateRaw(oldRecord, newRecord, options);
      this.adapterTransaction.update(updated);
      this._queueEvent("update", { data: updated, oldRecord });
      return { data: updated, oldRecord, permissions: {} };
    }
    /**
     * Lower-level primitive for updating a record while respecting
     * _status and last_modified.
     *
     * @param  {Object} oldRecord: the record retrieved from the DB
     * @param  {Object} newRecord: the record to replace it with
     * @return {Object}
     */
    _updateRaw(oldRecord, newRecord, { synced = false } = {}) {
      const updated = Object.assign({}, newRecord);
      // Make sure to never loose the existing timestamp.
      if (oldRecord && oldRecord.last_modified && !updated.last_modified) {
        updated.last_modified = oldRecord.last_modified;
      }
      // If only local fields have changed, then keep record as synced.
      // If status is created, keep record as created.
      // If status is deleted, mark as updated.
      const isIdentical =
        oldRecord &&
        recordsEqual(oldRecord, updated, this.collection.localFields);
      const keepSynced = isIdentical && oldRecord._status === "synced";
      const neverSynced =
        !oldRecord || (oldRecord && oldRecord._status === "created");
      const newStatus =
        keepSynced || synced ? "synced" : neverSynced ? "created" : "updated";
      return markStatus(updated, newStatus);
    }
    /**
     * Upsert a record into the local database.
     *
     * This record must have an ID.
     *
     * If a record with this ID already exists, it will be replaced.
     * Otherwise, this record will be inserted.
     *
     * @param  {Object} record
     * @return {Object}
     */
    upsert(record) {
      if (typeof record !== "object") {
        throw new Error("Record is not an object.");
      }
      if (!Object.prototype.hasOwnProperty.call(record, "id")) {
        throw new Error("Cannot update a record missing id.");
      }
      if (!this.collection.idSchema.validate(record.id)) {
        throw new Error(`Invalid Id: ${record.id}`);
      }
      let oldRecord = this.adapterTransaction.get(record.id);
      const updated = this._updateRaw(oldRecord, record);
      this.adapterTransaction.update(updated);
      // Don't return deleted records -- pretend they are gone
      if (oldRecord && oldRecord._status === "deleted") {
        oldRecord = undefined;
      }
      if (oldRecord) {
        this._queueEvent("update", { data: updated, oldRecord });
      } else {
        this._queueEvent("create", { data: updated });
      }
      return { data: updated, oldRecord, permissions: {} };
    }
  }

  const DEFAULT_BUCKET_NAME = "default";
  const DEFAULT_REMOTE = "http://localhost:8888/v1";
  const DEFAULT_RETRY = 1;
  /**
   * KintoBase class.
   */
  /* eslint-disable @typescript-eslint/no-unused-vars */
  class KintoBase {
    /**
     * Provides a public access to the base adapter class. Users can create a
     * custom DB adapter by extending {@link BaseAdapter}.
     *
     * @type {Object}
     */
    static get adapters() {
      return {
        BaseAdapter,
      };
    }
    /**
     * Synchronization strategies. Available strategies are:
     *
     * - `MANUAL`: Conflicts will be reported in a dedicated array.
     * - `SERVER_WINS`: Conflicts are resolved using remote data.
     * - `CLIENT_WINS`: Conflicts are resolved using local data.
     *
     * @type {Object}
     */
    static get syncStrategy() {
      return Collection.strategy;
    }
    /**
     * Constructor.
     *
     * Options:
     * - `{String}`       `remote`         The server URL to use.
     * - `{String}`       `bucket`         The collection bucket name.
     * - `{EventEmitter}` `events`         Events handler.
     * - `{BaseAdapter}`  `adapter`        The base DB adapter class.
     * - `{Object}`       `adapterOptions` Options given to the adapter.
     * - `{Object}`       `headers`        The HTTP headers to use.
     * - `{Object}`       `retry`          Number of retries when the server fails to process the request (default: `1`)
     * - `{String}`       `requestMode`    The HTTP CORS mode to use.
     * - `{Number}`       `timeout`        The requests timeout in ms (default: `5000`).
     *
     * @param  {Object} options The options object.
     */
    constructor(options = {}) {
      const defaults = {
        bucket: DEFAULT_BUCKET_NAME,
        remote: DEFAULT_REMOTE,
        retry: DEFAULT_RETRY,
      };
      this._options = Object.assign(Object.assign({}, defaults), options);
      if (!this._options.adapter) {
        throw new Error("No adapter provided");
      }
      this._api = null;
      /**
       * The event emitter instance.
       * @type {EventEmitter}
       */
      this.events = this._options.events;
    }
    get ApiClass() {
      throw new Error("ApiClass() must be implemented by subclasses.");
    }
    /**
     * The kinto HTTP client instance.
     * @type {KintoClient}
     */
    get api() {
      const { events, headers, remote, requestMode, retry, timeout } =
        this._options;
      if (!this._api) {
        this._api = new this.ApiClass(remote, {
          events,
          headers,
          requestMode,
          retry,
          timeout,
        });
      }
      return this._api;
    }
    /**
     * Creates a {@link Collection} instance. The second (optional) parameter
     * will set collection-level options like e.g. `remoteTransformers`.
     *
     * @param  {String} collName The collection name.
     * @param  {Object} [options={}]                 Extra options or override client's options.
     * @param  {Object} [options.idSchema]           IdSchema instance (default: UUID)
     * @param  {Object} [options.remoteTransformers] Array<RemoteTransformer> (default: `[]`])
     * @param  {Object} [options.hooks]              Array<Hook> (default: `[]`])
     * @param  {Object} [options.localFields]        Array<Field> (default: `[]`])
     * @return {Collection}
     */
    collection(collName, options = {}) {
      if (!collName) {
        throw new Error("missing collection name");
      }
      const { bucket, events, adapter, adapterOptions } = Object.assign(
        Object.assign({}, this._options),
        options
      );
      const { idSchema, remoteTransformers, hooks, localFields } = options;
      return new Collection(bucket, collName, this, {
        events,
        adapter,
        adapterOptions,
        idSchema,
        remoteTransformers,
        hooks,
        localFields,
      });
    }
  }

  class Kinto extends KintoBase {
    /**
     * Provides a public access to the base adapter classes. Users can create
     * a custom DB adapter by extending BaseAdapter.
     *
     * @type {Object}
     */
    static get adapters() {
      return {
        BaseAdapter,
        IDB,
      };
    }
    get ApiClass() {
      return KintoClient;
    }
    constructor(options = {}) {
      const defaults = {
        adapter: (dbName, options) => {
          return new Kinto.adapters.IDB(dbName, options);
        },
      };
      super(Object.assign(Object.assign({}, defaults), options));
    }
  }

  exports.AbstractBaseAdapter = AbstractBaseAdapter;
  exports.BaseAdapter = BaseAdapter;
  exports.KintoBase = KintoBase;
  exports.KintoClient = KintoClient;
  exports.default = Kinto;
  exports.getDeepKey = getDeepKey;

  Object.defineProperty(exports, "__esModule", { value: true });
});
//# sourceMappingURL=kinto.js.map