UNPKG

kinto

Version:

An Offline-First JavaScript client for Kinto.

4,408 lines 156 kB
/*
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
"use strict";
/*
 * This file is generated from kinto.js - do not modify directly.
 */
// This is required because with Babel compiles ES2015 modules into a
// require() form that tries to keep its modules on "this", but
// doesn't specify "this", leaving it to default to the global
// object. However, in strict mode, "this" no longer defaults to the
// global object, so expose the global object explicitly. Babel's
// compiled output will use a variable called "global" if one is
// present.
//
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1394556#c3 for
// more details.

var _interopRequireDefault2 = require("@babel/runtime/helpers/interopRequireDefault");

var _toConsumableArray2 = _interopRequireDefault2(require("@babel/runtime/helpers/toConsumableArray"));

var _regenerator = _interopRequireDefault2(require("@babel/runtime/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault2(require("@babel/runtime/helpers/asyncToGenerator"));

var _objectSpread2 = _interopRequireDefault2(require("@babel/runtime/helpers/objectSpread"));

var _classCallCheck2 = _interopRequireDefault2(require("@babel/runtime/helpers/classCallCheck"));

var _possibleConstructorReturn2 = _interopRequireDefault2(require("@babel/runtime/helpers/possibleConstructorReturn"));

var _getPrototypeOf2 = _interopRequireDefault2(require("@babel/runtime/helpers/getPrototypeOf"));

var _get3 = _interopRequireDefault2(require("@babel/runtime/helpers/get"));

var _createClass2 = _interopRequireDefault2(require("@babel/runtime/helpers/createClass"));

var _inherits2 = _interopRequireDefault2(require("@babel/runtime/helpers/inherits"));

var _typeof2 = _interopRequireDefault2(require("@babel/runtime/helpers/typeof"));

var global = void 0;
var EXPORTED_SYMBOLS = ["Kinto"];
/*
 * Version 12.0.1 - 75cb4fe
 */

(function (f) {
  if ((typeof exports === "undefined" ? "undefined" : (0, _typeof2.default)(exports)) === "object" && typeof module !== "undefined") {
    module.exports = f();
  } else if (typeof define === "function" && define.amd) {
    define([], f);
  } else {
    var g;

    if (typeof window !== "undefined") {
      g = window;
    } else if (typeof global !== "undefined") {
      g = global;
    } else if (typeof self !== "undefined") {
      g = self;
    } else {
      g = this;
    }

    g.Kinto = f();
  }
})(function () {
  var define, module, exports;
  return function () {
    function r(e, n, t) {
      function o(i, f) {
        if (!n[i]) {
          if (!e[i]) {
            var c = "function" == typeof require && require;
            if (!f && c) return c(i, !0);
            if (u) return u(i, !0);
            var a = new Error("Cannot find module '" + i + "'");
            throw a.code = "MODULE_NOT_FOUND", a;
          }

          var p = n[i] = {
            exports: {}
          };
          e[i][0].call(p.exports, function (r) {
            var n = e[i][1][r];
            return o(n || r);
          }, p, p.exports, r, e, n, t);
        }

        return n[i].exports;
      }

      for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) {
        o(t[i]);
      }

      return o;
    }

    return r;
  }()({
    1: [function (require, module, exports) {
      /*
       *
       * Licensed under the Apache License, Version 2.0 (the "License");
       * you may not use this file except in compliance with the License.
       * You may obtain a copy of the License at
       *
       *     http://www.apache.org/licenses/LICENSE-2.0
       *
       * Unless required by applicable law or agreed to in writing, software
       * distributed under the License is distributed on an "AS IS" BASIS,
       * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       * See the License for the specific language governing permissions and
       * limitations under the License.
       */
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.default = void 0;

      var _KintoBase = _interopRequireDefault(require("../src/KintoBase"));

      var _base = _interopRequireDefault(require("../src/adapters/base"));

      var _IDB = _interopRequireDefault(require("../src/adapters/IDB"));

      var _utils = require("../src/utils");

      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
          default: obj
        };
      }

      ChromeUtils.import("resource://gre/modules/Timer.jsm");
      ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
      XPCOMUtils.defineLazyGlobalGetters(global, ["fetch", "indexedDB"]);

      var _ChromeUtils$import = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {}),
          EventEmitter = _ChromeUtils$import.EventEmitter;

      var _Cc$MozillaOrgUuid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator),
          generateUUID = _Cc$MozillaOrgUuid.generateUUID; // Use standalone kinto-http module landed in FFx.


      var _ChromeUtils$import2 = ChromeUtils.import("resource://services-common/kinto-http-client.js"),
          KintoHttpClient = _ChromeUtils$import2.KintoHttpClient;

      var Kinto =
      /*#__PURE__*/
      function (_KintoBase$default) {
        (0, _inherits2.default)(Kinto, _KintoBase$default);
        (0, _createClass2.default)(Kinto, null, [{
          key: "adapters",
          get: function get() {
            return {
              BaseAdapter: _base.default,
              IDB: _IDB.default
            };
          }
        }]);

        function Kinto() {
          var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
          (0, _classCallCheck2.default)(this, Kinto);
          var events = {};
          EventEmitter.decorate(events);
          var defaults = {
            adapter: _IDB.default,
            events: events,
            ApiClass: KintoHttpClient
          };
          return (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(Kinto).call(this, (0, _objectSpread2.default)({}, defaults, options)));
        }

        (0, _createClass2.default)(Kinto, [{
          key: "collection",
          value: function collection(collName) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
            var idSchema = {
              validate: function validate(id) {
                return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
              },
              generate: function generate() {
                return generateUUID().toString().replace(/[{}]/g, "");
              }
            };
            return (0, _get3.default)((0, _getPrototypeOf2.default)(Kinto.prototype), "collection", this).call(this, collName, (0, _objectSpread2.default)({
              idSchema: idSchema
            }, options));
          }
        }]);
        return Kinto;
      }(_KintoBase.default); // This fixes compatibility with CommonJS required by browserify.
      // See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495


      exports.default = Kinto;

      if ((0, _typeof2.default)(module) === "object") {
        module.exports = Kinto;
      }
    }, {
      "../src/KintoBase": 3,
      "../src/adapters/IDB": 4,
      "../src/adapters/base": 5,
      "../src/utils": 7
    }],
    2: [function (require, module, exports) {}, {}],
    3: [function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.default = void 0;

      var _collection = _interopRequireDefault(require("./collection"));

      var _base = _interopRequireDefault(require("./adapters/base"));

      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
          default: obj
        };
      }

      var DEFAULT_BUCKET_NAME = "default";
      var DEFAULT_REMOTE = "http://localhost:8888/v1";
      var DEFAULT_RETRY = 1;
      /**
       * KintoBase class.
       */

      var KintoBase =
      /*#__PURE__*/
      function () {
        (0, _createClass2.default)(KintoBase, null, [{
          key: "adapters",

          /**
           * Provides a public access to the base adapter class. Users can create a
           * custom DB adapter by extending {@link BaseAdapter}.
           *
           * @type {Object}
           */
          get: function get() {
            return {
              BaseAdapter: _base.default
            };
          }
          /**
           * 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}
           */

        }, {
          key: "syncStrategy",
          get: function get() {
            return _collection.default.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.
           */

        }]);

        function KintoBase() {
          var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
          (0, _classCallCheck2.default)(this, KintoBase);
          var defaults = {
            bucket: DEFAULT_BUCKET_NAME,
            remote: DEFAULT_REMOTE,
            retry: DEFAULT_RETRY
          };
          this._options = (0, _objectSpread2.default)({}, defaults, options);

          if (!this._options.adapter) {
            throw new Error("No adapter provided");
          }

          var _this$_options = this._options,
              ApiClass = _this$_options.ApiClass,
              events = _this$_options.events,
              headers = _this$_options.headers,
              remote = _this$_options.remote,
              requestMode = _this$_options.requestMode,
              retry = _this$_options.retry,
              timeout = _this$_options.timeout; // public properties

          /**
           * The kinto HTTP client instance.
           * @type {KintoClient}
           */

          this.api = new ApiClass(remote, {
            events: events,
            headers: headers,
            requestMode: requestMode,
            retry: retry,
            timeout: timeout
          });
          /**
           * The event emitter instance.
           * @type {EventEmitter}
           */

          this.events = this._options.events;
        }
        /**
         * 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}
         */


        (0, _createClass2.default)(KintoBase, [{
          key: "collection",
          value: function collection(collName) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

            if (!collName) {
              throw new Error("missing collection name");
            }

            var _this$_options$option = (0, _objectSpread2.default)({}, this._options, options),
                bucket = _this$_options$option.bucket,
                events = _this$_options$option.events,
                adapter = _this$_options$option.adapter,
                adapterOptions = _this$_options$option.adapterOptions;

            var idSchema = options.idSchema,
                remoteTransformers = options.remoteTransformers,
                hooks = options.hooks,
                localFields = options.localFields;
            return new _collection.default(bucket, collName, this.api, {
              events: events,
              adapter: adapter,
              adapterOptions: adapterOptions,
              idSchema: idSchema,
              remoteTransformers: remoteTransformers,
              hooks: hooks,
              localFields: localFields
            });
          }
        }]);
        return KintoBase;
      }();

      exports.default = KintoBase;
    }, {
      "./adapters/base": 5,
      "./collection": 6
    }],
    4: [function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.open = _open2;
      exports.execute = execute;
      exports.default = void 0;

      var _base = _interopRequireDefault(require("./base.js"));

      var _utils = require("../utils");

      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
          default: obj
        };
      }

      var 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>}
       */

      function _open2(_x, _x2) {
        return _open.apply(this, arguments);
      }
      /**
       * 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.
       */


      function _open() {
        _open = (0, _asyncToGenerator2.default)(
        /*#__PURE__*/
        _regenerator.default.mark(function _callee10(dbname, _ref) {
          var version, onupgradeneeded;
          return _regenerator.default.wrap(function _callee10$(_context10) {
            while (1) {
              switch (_context10.prev = _context10.next) {
                case 0:
                  version = _ref.version, onupgradeneeded = _ref.onupgradeneeded;
                  return _context10.abrupt("return", new Promise(function (resolve, reject) {
                    var request = indexedDB.open(dbname, version);

                    request.onupgradeneeded = function (event) {
                      var db = event.target.result;

                      db.onerror = function (event) {
                        return reject(event.target.error);
                      };

                      return onupgradeneeded(event);
                    };

                    request.onerror = function (event) {
                      reject(event.target.error);
                    };

                    request.onsuccess = function (event) {
                      var db = event.target.result;
                      resolve(db);
                    };
                  }));

                case 2:
                case "end":
                  return _context10.stop();
              }
            }
          }, _callee10, this);
        }));
        return _open.apply(this, arguments);
      }

      function execute(_x3, _x4, _x5) {
        return _execute.apply(this, arguments);
      }
      /**
       * Helper to wrap the deletion of an IndexedDB database into a promise.
       *
       * @param dbName {String} the database to delete
       * @return {Promise}
       */


      function _execute() {
        _execute = (0, _asyncToGenerator2.default)(
        /*#__PURE__*/
        _regenerator.default.mark(function _callee11(db, name, callback) {
          var options,
              mode,
              _args11 = arguments;
          return _regenerator.default.wrap(function _callee11$(_context11) {
            while (1) {
              switch (_context11.prev = _context11.next) {
                case 0:
                  options = _args11.length > 3 && _args11[3] !== undefined ? _args11[3] : {};
                  mode = options.mode;
                  return _context11.abrupt("return", new Promise(function (resolve, reject) {
                    // On Safari, calling IDBDatabase.transaction with mode == undefined raises
                    // a TypeError.
                    var transaction = mode ? db.transaction([name], mode) : db.transaction([name]);
                    var store = transaction.objectStore(name); // Let the callback abort this transaction.

                    var abort = function abort(e) {
                      transaction.abort();
                      reject(e);
                    }; // Execute the specified callback **synchronously**.


                    var result;

                    try {
                      result = callback(store, abort);
                    } catch (e) {
                      abort(e);
                    }

                    transaction.onerror = function (event) {
                      return reject(event.target.error);
                    };

                    transaction.oncomplete = function (event) {
                      return resolve(result);
                    };
                  }));

                case 3:
                case "end":
                  return _context11.stop();
              }
            }
          }, _callee11, this);
        }));
        return _execute.apply(this, arguments);
      }

      function deleteDatabase(_x6) {
        return _deleteDatabase.apply(this, arguments);
      }
      /**
       * IDB cursor handlers.
       * @type {Object}
       */


      function _deleteDatabase() {
        _deleteDatabase = (0, _asyncToGenerator2.default)(
        /*#__PURE__*/
        _regenerator.default.mark(function _callee12(dbName) {
          return _regenerator.default.wrap(function _callee12$(_context12) {
            while (1) {
              switch (_context12.prev = _context12.next) {
                case 0:
                  return _context12.abrupt("return", new Promise(function (resolve, reject) {
                    var request = indexedDB.deleteDatabase(dbName);

                    request.onsuccess = function (event) {
                      return resolve(event.target);
                    };

                    request.onerror = function (event) {
                      return reject(event.target.error);
                    };
                  }));

                case 1:
                case "end":
                  return _context12.stop();
              }
            }
          }, _callee12, this);
        }));
        return _deleteDatabase.apply(this, arguments);
      }

      var cursorHandlers = {
        all: function all(filters, done) {
          var results = [];
          return function (event) {
            var cursor = event.target.result;

            if (cursor) {
              var value = cursor.value;

              if ((0, _utils.filterObject)(filters, value)) {
                results.push(value);
              }

              cursor.continue();
            } else {
              done(results);
            }
          };
        },
        in: function _in(values, filters, done) {
          var results = [];
          return function (event) {
            var cursor = event.target.result;

            if (!cursor) {
              done(results);
              return;
            }

            var key = cursor.key,
                value = cursor.value; // `key` can be an array of two values (see `keyPath` in indices definitions).

            var i = 0; // `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;
              }
            }

            var isEqual = Array.isArray(key) ? (0, _utils.arrayEqual)(key, values[i]) : key === values[i];

            if (isEqual) {
              if ((0, _utils.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) {
        // Introspect filters and check if they leverage an indexed field.
        var indexField = Object.keys(filters).find(function (field) {
          return INDEXED_FIELDS.includes(field);
        });

        if (!indexField) {
          // Get all records for this collection (ie. cid)
          var _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.


        var remainingFilters = (0, _utils.omitKeys)(filters, indexField); // value specified in the filter (eg. `filters: { _status: ["created", "updated"] }`)

        var value = filters[indexField]; // WHERE IN equivalent clause

        if (Array.isArray(value)) {
          if (value.length === 0) {
            return done([]);
          }

          var values = value.map(function (i) {
            return [cid, i];
          }).sort();
          var range = IDBKeyRange.bound(values[0], values[values.length - 1]);

          var _request2 = store.index(indexField).openCursor(range);

          _request2.onsuccess = cursorHandlers.in(values, remainingFilters, done);
          return _request2;
        } // WHERE field = value clause


        var request = store.index(indexField).openCursor(IDBKeyRange.only([cid, value]));
        request.onsuccess = cursorHandlers.all(remainingFilters, done);
        return request;
      }
      /**
       * IndexedDB adapter.
       *
       * This adapter doesn't support any options.
       */


      var IDB =
      /*#__PURE__*/
      function (_base$default) {
        (0, _inherits2.default)(IDB, _base$default);

        /**
         * 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`)
         */
        function IDB(cid) {
          var _this;

          var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
          (0, _classCallCheck2.default)(this, IDB);
          _this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(IDB).call(this));
          _this.cid = cid;
          _this.dbName = options.dbName || "KintoDB";
          _this._options = options;
          _this._db = null;
          return _this;
        }

        (0, _createClass2.default)(IDB, [{
          key: "_handleError",
          value: function _handleError(method, err) {
            var error = new Error(method + "() " + err.message);
            error.stack = err.stack;
            throw error;
          }
          /**
           * Ensures a connection to the IndexedDB database has been opened.
           *
           * @override
           * @return {Promise}
           */

        }, {
          key: "open",
          value: function () {
            var _open3 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee() {
              var dataToMigrate, records, timestamp;
              return _regenerator.default.wrap(function _callee$(_context) {
                while (1) {
                  switch (_context.prev = _context.next) {
                    case 0:
                      if (!this._db) {
                        _context.next = 2;
                        break;
                      }

                      return _context.abrupt("return", this);

                    case 2:
                      if (!this._options.migrateOldData) {
                        _context.next = 8;
                        break;
                      }

                      _context.next = 5;
                      return migrationRequired(this.cid);

                    case 5:
                      _context.t0 = _context.sent;
                      _context.next = 9;
                      break;

                    case 8:
                      _context.t0 = null;

                    case 9:
                      dataToMigrate = _context.t0;
                      _context.next = 12;
                      return _open2(this.dbName, {
                        version: 1,
                        onupgradeneeded: function onupgradeneeded(event) {
                          var db = event.target.result; // Records store

                          var 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.
                          // Record id (generated by IdSchema, UUID by default)

                          recordsStore.createIndex("id", ["_cid", "id"]); // 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"
                          });
                        }
                      });

                    case 12:
                      this._db = _context.sent;

                      if (!dataToMigrate) {
                        _context.next = 23;
                        break;
                      }

                      records = dataToMigrate.records, timestamp = dataToMigrate.timestamp;
                      _context.next = 17;
                      return this.loadDump(records);

                    case 17:
                      _context.next = 19;
                      return this.saveLastModified(timestamp);

                    case 19:
                      console.log("".concat(this.cid, ": data was migrated successfully.")); // Delete the old database.

                      _context.next = 22;
                      return deleteDatabase(this.cid);

                    case 22:
                      console.warn("".concat(this.cid, ": old database was deleted."));

                    case 23:
                      return _context.abrupt("return", this);

                    case 24:
                    case "end":
                      return _context.stop();
                  }
                }
              }, _callee, this);
            }));

            return function open() {
              return _open3.apply(this, arguments);
            };
          }()
          /**
           * Closes current connection to the database.
           *
           * @override
           * @return {Promise}
           */

        }, {
          key: "close",
          value: function 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}
           */

        }, {
          key: "prepare",
          value: function () {
            var _prepare = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee2(name, callback, options) {
              return _regenerator.default.wrap(function _callee2$(_context2) {
                while (1) {
                  switch (_context2.prev = _context2.next) {
                    case 0:
                      _context2.next = 2;
                      return this.open();

                    case 2:
                      _context2.next = 4;
                      return execute(this._db, name, callback, options);

                    case 4:
                    case "end":
                      return _context2.stop();
                  }
                }
              }, _callee2, this);
            }));

            return function prepare(_x7, _x8, _x9) {
              return _prepare.apply(this, arguments);
            };
          }()
          /**
           * Deletes every records in the current collection.
           *
           * @override
           * @return {Promise}
           */

        }, {
          key: "clear",
          value: function () {
            var _clear = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee3() {
              var _this2 = this;

              return _regenerator.default.wrap(function _callee3$(_context3) {
                while (1) {
                  switch (_context3.prev = _context3.next) {
                    case 0:
                      _context3.prev = 0;
                      _context3.next = 3;
                      return this.prepare("records", function (store) {
                        var range = IDBKeyRange.only(_this2.cid);
                        var request = store.index("cid").openKeyCursor(range);

                        request.onsuccess = function (event) {
                          var cursor = event.target.result;

                          if (cursor) {
                            store.delete(cursor.primaryKey);
                            cursor.continue();
                          }
                        };

                        return request;
                      }, {
                        mode: "readwrite"
                      });

                    case 3:
                      _context3.next = 8;
                      break;

                    case 5:
                      _context3.prev = 5;
                      _context3.t0 = _context3["catch"](0);

                      this._handleError("clear", _context3.t0);

                    case 8:
                    case "end":
                      return _context3.stop();
                  }
                }
              }, _callee3, this, [[0, 5]]);
            }));

            return function clear() {
              return _clear.apply(this, arguments);
            };
          }()
          /**
           * 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}
           */

        }, {
          key: "execute",
          value: function () {
            var _execute2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee4(callback) {
              var _this3 = this;

              var options,
                  result,
                  _args4 = arguments;
              return _regenerator.default.wrap(function _callee4$(_context4) {
                while (1) {
                  switch (_context4.prev = _context4.next) {
                    case 0:
                      options = _args4.length > 1 && _args4[1] !== undefined ? _args4[1] : {
                        preload: []
                      };
                      _context4.next = 3;
                      return this.prepare("records", function (store, abort) {
                        var runCallback = function runCallback() {
                          var preloaded = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
                          // Expose a consistent API for every adapter instead of raw store methods.
                          var proxy = transactionProxy(_this3, store, preloaded); // The callback is executed synchronously within the same transaction.

                          try {
                            var 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(e);
                          }
                        }; // No option to preload records, go straight to `callback`.


                        if (!options.preload.length) {
                          return runCallback();
                        } // Preload specified records using a list request.


                        var filters = {
                          id: options.preload
                        };
                        createListRequest(_this3.cid, store, filters, function (records) {
                          // Store obtained records by id.
                          var preloaded = records.reduce(function (acc, record) {
                            acc[record.id] = (0, _utils.omitKeys)(record, ["_cid"]);
                            return acc;
                          }, {});
                          runCallback(preloaded);
                        });
                      }, {
                        mode: "readwrite"
                      });

                    case 3:
                      return _context4.abrupt("return", result);

                    case 4:
                    case "end":
                      return _context4.stop();
                  }
                }
              }, _callee4, this);
            }));

            return function execute(_x10) {
              return _execute2.apply(this, arguments);
            };
          }()
          /**
           * Retrieve a record by its primary key from the IndexedDB database.
           *
           * @override
           * @param  {String} id The record id.
           * @return {Promise}
           */

        }, {
          key: "get",
          value: function () {
            var _get2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee5(id) {
              var _this4 = this;

              var record;
              return _regenerator.default.wrap(function _callee5$(_context5) {
                while (1) {
                  switch (_context5.prev = _context5.next) {
                    case 0:
                      _context5.prev = 0;
                      _context5.next = 3;
                      return this.prepare("records", function (store) {
                        store.get([_this4.cid, id]).onsuccess = function (e) {
                          return record = e.target.result;
                        };
                      });

                    case 3:
                      return _context5.abrupt("return", record);

                    case 6:
                      _context5.prev = 6;
                      _context5.t0 = _context5["catch"](0);

                      this._handleError("get", _context5.t0);

                    case 9:
                    case "end":
                      return _context5.stop();
                  }
                }
              }, _callee5, this, [[0, 6]]);
            }));

            return function get(_x11) {
              return _get2.apply(this, arguments);
            };
          }()
          /**
           * Lists all records from the IndexedDB database.
           *
           * @override
           * @param  {Object} params  The filters and order to apply to the results.
           * @return {Promise}
           */

        }, {
          key: "list",
          value: function () {
            var _list = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee6() {
              var _this5 = this;

              var params,
                  filters,
                  results,
                  _args6 = arguments;
              return _regenerator.default.wrap(function _callee6$(_context6) {
                while (1) {
                  switch (_context6.prev = _context6.next) {
                    case 0:
                      params = _args6.length > 0 && _args6[0] !== undefined ? _args6[0] : {
                        filters: {}
                      };
                      filters = params.filters;
                      _context6.prev = 2;
                      results = [];
                      _context6.next = 6;
                      return this.prepare("records", function (store) {
                        createListRequest(_this5.cid, store, filters, function (_results) {
                          // we have received all requested records that match the filters,
                          // we now park them within current scope and hide the `_cid` attribute.
                          results = _results.map(function (r) {
                            return (0, _utils.omitKeys)(r, ["_cid"]);
                          });
                        });
                      });

                    case 6:
                      return _context6.abrupt("return", params.order ? (0, _utils.sortObjects)(params.order, results) : results);

                    case 9:
                      _context6.prev = 9;
                      _context6.t0 = _context6["catch"](2);

                      this._handleError("list", _context6.t0);

                    case 12:
                    case "end":
                      return _context6.stop();
                  }
                }
              }, _callee6, this, [[2, 9]]);
            }));

            return function list() {
              return _list.apply(this, arguments);
            };
          }()
          /**
           * Store the lastModified value into metadata store.
           *
           * @override
           * @param  {Number}  lastModified
           * @return {Promise}
           */

        }, {
          key: "saveLastModified",
          value: function () {
            var _saveLastModified = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee7(lastModified) {
              var _this6 = this;

              var value;
              return _regenerator.default.wrap(function _callee7$(_context7) {
                while (1) {
                  switch (_context7.prev = _context7.next) {
                    case 0:
                      value = parseInt(lastModified, 10) || null;
                      _context7.prev = 1;
                      _context7.next = 4;
                      return this.prepare("timestamps", function (store) {
                        return store.put({
                          cid: _this6.cid,
                          value: value
                        });
                      }, {
                        mode: "readwrite"
                      });

                    case 4:
                      return _context7.abrupt("return", value);

                    case 7:
                      _context7.prev = 7;
                      _context7.t0 = _context7["catch"](1);

                      this._handleError("saveLastModified", _context7.t0);

                    case 10:
                    case "end":
                      return _context7.stop();
                  }
                }
              }, _callee7, this, [[1, 7]]);
            }));

            return function saveLastModified(_x12) {
              return _saveLastModified.apply(this, arguments);
            };
          }()
          /**
           * Retrieve saved lastModified value.
           *
           * @override
           * @return {Promise}
           */

        }, {
          key: "getLastModified",
          value: function () {
            var _getLastModified = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee8() {
              var _this7 = this;

              var entry;
              return _regenerator.default.wrap(function _callee8$(_context8) {
                while (1) {
                  switch (_context8.prev = _context8.next) {
                    case 0:
                      _context8.prev = 0;
                      entry = null;
                      _context8.next = 4;
                      return this.prepare("timestamps", function (store) {
                        store.get(_this7.cid).onsuccess = function (e) {
                          return entry = e.target.result;
                        };
                      });

                    case 4:
                      return _context8.abrupt("return", entry ? entry.value : null);

                    case 7:
                      _context8.prev = 7;
                      _context8.t0 = _context8["catch"](0);

                      this._handleError("getLastModified", _context8.t0);

                    case 10:
                    case "end":
                      return _context8.stop();
                  }
                }
              }, _callee8, this, [[0, 7]]);
            }));

            return function getLastModified() {
              return _getLastModified.apply(this, arguments);
            };
          }()
          /**
           * Load a dump of records exported from a server.
           *
           * @abstract
           * @param  {Array} records The records to load.
           * @return {Promise}
           */

        }, {
          key: "loadDump",
          value: function () {
            var _loadDump = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee9(records) {
              var previousLastModified, lastModified;
              return _regenerator.default.wrap(function _callee9$(_context9) {
                while (1) {
                  switch (_context9.prev = _context9.next) {
                    case 0:
                      _context9.prev = 0;
                      _context9.next = 3;
                      return this.execute(function (transaction) {
                        records.forEach(function (record) {
                          return transaction.update(record);
                        });
                      });

                    case 3:
                      _context9.next = 5;
                      return this.getLastModified();

                    case 5:
                      previousLastModified = _context9.sent;
                      lastModified = Math.max.apply(Math, (0, _toConsumableArray2.default)(records.map(function (record) {
                        return record.last_modified;
                      })));

                      if (!(lastModified > previousLastModified)) {
                        _context9.next = 10;
                        break;
                      }

                      _context9.next = 10;
                      return this.saveLastModified(lastModified);

                    case 10:
                      return _context9.abrupt("return", records);

                    case 13:
                      _context9.prev = 13;
                      _context9.t0 = _context9["catch"](0);

                      this._handleError("loadDump", _context9.t0);

                    case 16:
                    case "end":
                      return _context9.stop();
                  }
                }
              }, _callee9, this, [[0, 13]]);
            }));

            return function loadDump(_x13) {
              return _loadDump.apply(this, arguments);
            };
          }()
        }]);
        return IDB;
      }(_base.default);
      /**
       * 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}
       */


      exports.default = IDB;

      function transactionProxy(adapter, store) {
        var preloaded = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
        var _cid = adapter.cid;
        return {
          create: function create(record) {
            store.add((0, _objectSpread2.default)({}, record, {
              _cid: _cid
            }));
          },
          update: function update(record) {
            store.put((0, _objectSpread2.default)({}, record, {
              _cid: _cid
            }));
          },
          delete: function _delete(id) {
            store.delete([_cid, id]);
          },
          get: function 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.
       */


      function migrationRequired(_x14) {
        return _migrationRequired.apply(this, arguments);
      }

      function _migrationRequired() {
        _migrationRequired = (0, _asyncToGenerator2.default)(
        /*#__PURE__*/
        _regenerator.default.mark(function _callee13(dbName) {
          var exists, db, records, timestamp;
          return _regenerator.default.wrap(function _callee13$(_context13) {
            while (1) {
              switch (_context13.prev = _context13.next) {
                case 0:
                  exists = true;
                  _context13.next = 3;
                  return _open2(dbName, {
                    version: 1,
                    onupgradeneeded: function onupgradeneeded(event) {
                      exists = false;
                    }
                  });

                case 3:
                  db = _context13.sent;
                  // 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__");

                  if (exists) {
                    _context13.next = 10;
                    break;
                  }

                  db.close(); // Testing the existence creates it, so delete it :)

                  _context13.next = 9;
                  return deleteDatabase(dbName);

                case 9:
                  return _context13.abrupt("return", null);

                case 10:
                  console.warn("".concat(dbName, ": old IndexedDB database found."));
                  _context13.prev = 11;
                  _context13.next = 14;
                  return execute(db, dbName, function (store) {
                    store.openCursor().onsuccess = cursorHandlers.all({}, function (res) {
                      return records = res;
                    });
                  });

                case 14:
                  console.log("".concat(dbName, ": found ").concat(records.length, " records.")); // Check if there's a entry for this.

                  timestamp = null;
                  _context13.next = 18;
                  return execute(db, "__meta__", function (store) {
                    store.get("".concat(dbName, "-lastModified")).onsuccess = function (e) {
                      timestamp = e.target.result ? e.target.result.value : null;
                    };
                  });

                case 18:
                  if (timestamp) {
                    _context13.next = 21;
                    break;
                  }

                  _context13.next = 21;
                  return execute(db, "__meta__", function (store) {
                    store.get("lastModified").onsuccess = function (e) {
                      timestamp = e.target.result ? e.target.result.value : null;
                    };
                  });

                case 21:
                  console.log("".concat(dbName, ": ").concat(timestamp ? "found" : "no", " timestamp.")); // Those will be inserted in the new database/schema.

                  return _context13.abrupt("return", {
                    records: records,
                    timestamp: timestamp
                  });

                case 25:
                  _context13.prev = 25;
                  _context13.t0 = _context13["catch"](11);
                  console.error("Error occured during migration", _context13.t0);
                  return _context13.abrupt("return", null);

                case 29:
                  _context13.prev = 29;
                  db.close();
                  return _context13.finish(29);

                case 32:
                case "end":
                  return _context13.stop();
              }
            }
          }, _callee13, this, [[11, 25, 29, 32]]);
        }));
        return _migrationRequired.apply(this, arguments);
      }
    }, {
      "../utils": 7,
      "./base.js": 5
    }],
    5: [function (require, module, exports) {
      "use strict";
      /**
       * Base db adapter.
       *
       * @abstract
       */

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.default = void 0;

      var BaseAdapter =
      /*#__PURE__*/
      function () {
        function BaseAdapter() {
          (0, _classCallCheck2.default)(this, BaseAdapter);
        }

        (0, _createClass2.default)(BaseAdapter, [{
          key: "clear",

          /**
           * Deletes every records present in the database.
           *
           * @abstract
           * @return {Promise}
           */
          value: function 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}
           */

        }, {
          key: "execute",
          value: function execute(callback) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              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}
           */

        }, {
          key: "get",
          value: function 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}
           */

        }, {
          key: "list",
          value: function list() {
            var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
              filters: {},
              order: ""
            };
            throw new Error("Not Implemented.");
          }
          /**
           * Store the lastModified value.
           *
           * @abstract
           * @param  {Number}  lastModified
           * @return {Promise}
           */

        }, {
          key: "saveLastModified",
          value: function saveLastModified(lastModified) {
            throw new Error("Not Implemented.");
          }
          /**
           * Retrieve saved lastModified value.
           *
           * @abstract
           * @return {Promise}
           */

        }, {
          key: "getLastModified",
          value: function getLastModified() {
            throw new Error("Not Implemented.");
          }
          /**
           * Load a dump of records exported from a server.
           *
           * @abstract
           * @param  {Array} records The records to load.
           * @return {Promise}
           */

        }, {
          key: "loadDump",
          value: function loadDump(records) {
            throw new Error("Not Implemented.");
          }
        }]);
        return BaseAdapter;
      }();

      exports.default = BaseAdapter;
    }, {}],
    6: [function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.recordsEqual = recordsEqual;
      exports.CollectionTransaction = exports.default = exports.SyncResultObject = void 0;

      var _base = _interopRequireDefault(require("./adapters/base"));

      var _IDB = _interopRequireDefault(require("./adapters/IDB"));

      var _utils = require("./utils");

      var _uuid = require("uuid");

      function _interopRequireDefault(obj) {
        return obj && obj.__esModule ? obj : {
          default: obj
        };
      }

      var RECORD_FIELDS_TO_CLEAN = ["_status"];
      var AVAILABLE_HOOKS = ["incoming-changes"];
      /**
       * 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) {
        var localFields = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
        var fieldsToClean = RECORD_FIELDS_TO_CLEAN.concat(["last_modified"]).concat(localFields);

        var cleanLocal = function cleanLocal(r) {
          return (0, _utils.omitKeys)(r, fieldsToClean);
        };

        return (0, _utils.deepEqual)(cleanLocal(a), cleanLocal(b));
      }
      /**
       * Synchronization result object.
       */


      var SyncResultObject =
      /*#__PURE__*/
      function () {
        (0, _createClass2.default)(SyncResultObject, null, [{
          key: "defaults",

          /**
           * Object default values.
           * @type {Object}
           */
          get: function get() {
            return {
              ok: true,
              lastModified: null,
              errors: [],
              created: [],
              updated: [],
              deleted: [],
              published: [],
              conflicts: [],
              skipped: [],
              resolved: []
            };
          }
          /**
           * Public constructor.
           */

        }]);

        function SyncResultObject() {
          (0, _classCallCheck2.default)(this, SyncResultObject);

          /**
           * Current synchronization result status; becomes `false` when conflicts or
           * errors are registered.
           * @type {Boolean}
           */
          this.ok = true;
          Object.assign(this, SyncResultObject.defaults);
        }
        /**
         * Adds entries for a given result type.
         *
         * @param {String} type    The result type.
         * @param {Array}  entries The result entries.
         * @return {SyncResultObject}
         */


        (0, _createClass2.default)(SyncResultObject, [{
          key: "add",
          value: function add(type, entries) {
            if (!Array.isArray(this[type])) {
              return;
            }

            if (!Array.isArray(entries)) {
              entries = [entries];
            } // Deduplicate entries by id. If the values don't have `id` attribute, just
            // keep all.


            var recordsWithoutId = new Set();
            var recordsById = new Map();

            function addOneRecord(record) {
              if (!record.id) {
                recordsWithoutId.add(record);
              } else {
                recordsById.set(record.id, record);
              }
            }

            this[type].forEach(addOneRecord);
            entries.forEach(addOneRecord);
            this[type] = Array.from(recordsById.values()).concat(Array.from(recordsWithoutId));
            this.ok = this.errors.length + this.conflicts.length === 0;
            return this;
          }
          /**
           * Reinitializes result entries for a given result type.
           *
           * @param  {String} type The result type.
           * @return {SyncResultObject}
           */

        }, {
          key: "reset",
          value: function reset(type) {
            this[type] = SyncResultObject.defaults[type];
            this.ok = this.errors.length + this.conflicts.length === 0;
            return this;
          }
        }]);
        return SyncResultObject;
      }();

      exports.SyncResultObject = SyncResultObject;

      function createUUIDSchema() {
        return {
          generate: function generate() {
            return (0, _uuid.v4)();
          },
          validate: function validate(id) {
            return typeof id == "string" && _utils.RE_RECORD_ID.test(id);
          }
        };
      }

      function markStatus(record, status) {
        return (0, _objectSpread2.default)({}, 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.
       * @return {Object}
       */


      function importChange(transaction, remote, localFields) {
        var 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
            };
          }

          var _synced = markSynced(remote);

          transaction.create(_synced);
          return {
            type: "created",
            data: _synced
          };
        } // Compare local and remote, ignoring local fields.


        var isIdentical = recordsEqual(local, remote, localFields); // Apply remote changes on local record.

        var synced = (0, _objectSpread2.default)({}, local, markSynced(remote)); // 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: local,
              remote: 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

        var type = isIdentical ? "void" : "updated";
        return {
          type: type,
          data: {
            old: local,
            new: synced
          }
        };
      }
      /**
       * Abstracts a collection of records stored in the local database, providing
       * CRUD operations and synchronization helpers.
       */


      var Collection =
      /*#__PURE__*/
      function () {
        /**
         * Constructor.
         *
         * Options:
         * - `{BaseAdapter} adapter` The DB adapter (default: `IDB`)
         *
         * @param  {String} bucket  The bucket identifier.
         * @param  {String} name    The collection name.
         * @param  {Api}    api     The Api instance.
         * @param  {Object} options The options object.
         */
        function Collection(bucket, name, api) {
          var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
          (0, _classCallCheck2.default)(this, Collection);
          this._bucket = bucket;
          this._name = name;
          this._lastModified = null;
          var DBAdapter = options.adapter || _IDB.default;

          if (!DBAdapter) {
            throw new Error("No adapter provided");
          }

          var db = new DBAdapter("".concat(bucket, "/").concat(name), options.adapterOptions);

          if (!(db instanceof _base.default)) {
            throw new Error("Unsupported adapter.");
          } // public properties

          /**
           * The db adapter instance
           * @type {BaseAdapter}
           */


          this.db = db;
          /**
           * The Api instance.
           * @type {KintoClient}
           */

          this.api = api;
          /**
           * 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 collection name.
         * @type {String}
         */


        (0, _createClass2.default)(Collection, [{
          key: "_validateIdSchema",

          /**
           * Validates an idSchema.
           *
           * @param  {Object|undefined} idSchema
           * @return {Object}
           */
          value: function _validateIdSchema(idSchema) {
            if (typeof idSchema === "undefined") {
              return createUUIDSchema();
            }

            if ((0, _typeof2.default)(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}
           */

        }, {
          key: "_validateRemoteTransformers",
          value: function _validateRemoteTransformers(remoteTransformers) {
            if (typeof remoteTransformers === "undefined") {
              return [];
            }

            if (!Array.isArray(remoteTransformers)) {
              throw new Error("remoteTransformers should be an array.");
            }

            return remoteTransformers.map(function (transformer) {
              if ((0, _typeof2.default)(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}
           **/

        }, {
          key: "_validateHook",
          value: function _validateHook(hook) {
            if (!Array.isArray(hook)) {
              throw new Error("A hook definition should be an array of functions.");
            }

            return hook.map(function (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}
           */

        }, {
          key: "_validateHooks",
          value: function _validateHooks(hooks) {
            if (typeof hooks === "undefined") {
              return {};
            }

            if (Array.isArray(hooks)) {
              throw new Error("hooks should be an object, not an array.");
            }

            if ((0, _typeof2.default)(hooks) !== "object") {
              throw new Error("hooks should be an object.");
            }

            var validatedHooks = {};

            for (var 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}
           */

        }, {
          key: "clear",
          value: function () {
            var _clear2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee14() {
              return _regenerator.default.wrap(function _callee14$(_context14) {
                while (1) {
                  switch (_context14.prev = _context14.next) {
                    case 0:
                      _context14.next = 2;
                      return this.db.clear();

                    case 2:
                      _context14.next = 4;
                      return this.db.saveLastModified(null);

                    case 4:
                      return _context14.abrupt("return", {
                        data: [],
                        permissions: {}
                      });

                    case 5:
                    case "end":
                      return _context14.stop();
                  }
                }
              }, _callee14, this);
            }));

            return function clear() {
              return _clear2.apply(this, arguments);
            };
          }()
          /**
           * Encodes a record.
           *
           * @param  {String} type   Either "remote" or "local".
           * @param  {Object} record The record object to encode.
           * @return {Promise}
           */

        }, {
          key: "_encodeRecord",
          value: function _encodeRecord(type, record) {
            if (!this["".concat(type, "Transformers")].length) {
              return Promise.resolve(record);
            }

            return (0, _utils.waterfall)(this["".concat(type, "Transformers")].map(function (transformer) {
              return function (record) {
                return transformer.encode(record);
              };
            }), record);
          }
          /**
           * Decodes a record.
           *
           * @param  {String} type   Either "remote" or "local".
           * @param  {Object} record The record object to decode.
           * @return {Promise}
           */

        }, {
          key: "_decodeRecord",
          value: function _decodeRecord(type, record) {
            if (!this["".concat(type, "Transformers")].length) {
              return Promise.resolve(record);
            }

            return (0, _utils.waterfall)(this["".concat(type, "Transformers")].reverse().map(function (transformer) {
              return function (record) {
                return 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}
           */

        }, {
          key: "create",
          value: function create(record) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              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.
            var reject = function reject(msg) {
              return Promise.reject(new Error(msg));
            };

            if ((0, _typeof2.default)(record) !== "object") {
              return reject("Record is not an object.");
            }

            if ((options.synced || options.useRecordId) && !record.hasOwnProperty("id")) {
              return reject("Missing required Id; synced and useRecordId options require one");
            }

            if (!options.synced && !options.useRecordId && record.hasOwnProperty("id")) {
              return reject("Extraneous Id; can't create a record having one set.");
            }

            var newRecord = (0, _objectSpread2.default)({}, 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: ".concat(newRecord.id));
            }

            return this.execute(function (txn) {
              return txn.create(newRecord);
            }, {
              preloadIds: [newRecord.id]
            }).catch(function (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}
           */

        }, {
          key: "update",
          value: function update(record) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              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 ((0, _typeof2.default)(record) !== "object") {
              return Promise.reject(new Error("Record is not an object."));
            }

            if (!record.hasOwnProperty("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: ".concat(record.id)));
            }

            return this.execute(function (txn) {
              return txn.update(record, options);
            }, {
              preloadIds: [record.id]
            });
          }
          /**
           * Like {@link CollectionTransaction#upsert}, but wrapped in its own transaction.
           *
           * @param  {Object} record
           * @return {Promise}
           */

        }, {
          key: "upsert",
          value: function 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 ((0, _typeof2.default)(record) !== "object") {
              return Promise.reject(new Error("Record is not an object."));
            }

            if (!record.hasOwnProperty("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: ".concat(record.id)));
            }

            return this.execute(function (txn) {
              return 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}
           */

        }, {
          key: "get",
          value: function get(id) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              includeDeleted: false
            };
            return this.execute(function (txn) {
              return txn.get(id, options);
            }, {
              preloadIds: [id]
            });
          }
          /**
           * Like {@link CollectionTransaction#getAny}, but wrapped in its own transaction.
           *
           * @param  {String} id
           * @return {Promise}
           */

        }, {
          key: "getAny",
          value: function getAny(id) {
            return this.execute(function (txn) {
              return 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}
           */

        }, {
          key: "delete",
          value: function _delete(id) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              virtual: true
            };
            return this.execute(function (transaction) {
              return transaction.delete(id, options);
            }, {
              preloadIds: [id]
            });
          }
          /**
           * Same as {@link Collection#deleteAll}, but wrapped in its own transaction, execulding the parameter.
           *
           * @return {Promise}
           */

        }, {
          key: "deleteAll",
          value: function () {
            var _deleteAll = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee15() {
              var _ref2, data, recordIds;

              return _regenerator.default.wrap(function _callee15$(_context15) {
                while (1) {
                  switch (_context15.prev = _context15.next) {
                    case 0:
                      _context15.next = 2;
                      return this.list({}, {
                        includeDeleted: false
                      });

                    case 2:
                      _ref2 = _context15.sent;
                      data = _ref2.data;
                      recordIds = data.map(function (record) {
                        return record.id;
                      });
                      return _context15.abrupt("return", this.execute(function (transaction) {
                        return transaction.deleteAll(recordIds);
                      }, {
                        preloadIds: recordIds
                      }));

                    case 6:
                    case "end":
                      return _context15.stop();
                  }
                }
              }, _callee15, this);
            }));

            return function deleteAll() {
              return _deleteAll.apply(this, arguments);
            };
          }()
          /**
           * The same as {@link CollectionTransaction#deleteAny}, but wrapped
           * in its own transaction.
           *
           * @param  {String} id       The record's Id.
           * @return {Promise}
           */

        }, {
          key: "deleteAny",
          value: function deleteAny(id) {
            return this.execute(function (txn) {
              return 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}
           */

        }, {
          key: "list",
          value: function () {
            var _list2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee16() {
              var params,
                  options,
                  results,
                  data,
                  _args16 = arguments;
              return _regenerator.default.wrap(function _callee16$(_context16) {
                while (1) {
                  switch (_context16.prev = _context16.next) {
                    case 0:
                      params = _args16.length > 0 && _args16[0] !== undefined ? _args16[0] : {};
                      options = _args16.length > 1 && _args16[1] !== undefined ? _args16[1] : {
                        includeDeleted: false
                      };
                      params = (0, _objectSpread2.default)({
                        order: "-last_modified",
                        filters: {}
                      }, params);
                      _context16.next = 5;
                      return this.db.list(params);

                    case 5:
                      results = _context16.sent;
                      data = results;

                      if (!options.includeDeleted) {
                        data = results.filter(function (record) {
                          return record._status !== "deleted";
                        });
                      }

                      return _context16.abrupt("return", {
                        data: data,
                        permissions: {}
                      });

                    case 9:
                    case "end":
                      return _context16.stop();
                  }
                }
              }, _callee16, this);
            }));

            return function list() {
              return _list2.apply(this, arguments);
            };
          }()
          /**
           * 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}
           */

        }, {
          key: "importChanges",
          value: function () {
            var _importChanges = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee17(syncResultObject, decodedChanges) {
              var _this8 = this;

              var strategy,
                  _ref3,
                  imports,
                  resolved,
                  data,
                  _args17 = arguments;

              return _regenerator.default.wrap(function _callee17$(_context17) {
                while (1) {
                  switch (_context17.prev = _context17.next) {
                    case 0:
                      strategy = _args17.length > 2 && _args17[2] !== undefined ? _args17[2] : Collection.strategy.MANUAL;
                      _context17.prev = 1;
                      _context17.next = 4;
                      return this.db.execute(function (transaction) {
                        var imports = decodedChanges.map(function (remote) {
                          // Store remote change into local database.
                          return importChange(transaction, remote, _this8.localFields);
                        });
                        var conflicts = imports.filter(function (i) {
                          return i.type === "conflicts";
                        }).map(function (i) {
                          return i.data;
                        });

                        var resolved = _this8._handleConflicts(transaction, conflicts, strategy);

                        return {
                          imports: imports,
                          resolved: resolved
                        };
                      }, {
                        preload: decodedChanges.map(function (record) {
                          return record.id;
                        })
                      });

                    case 4:
                      _ref3 = _context17.sent;
                      imports = _ref3.imports;
                      resolved = _ref3.resolved;
                      // Lists of created/updated/deleted records
                      imports.forEach(function (_ref4) {
                        var type = _ref4.type,
                            data = _ref4.data;
                        return syncResultObject.add(type, data);
                      }); // Automatically resolved conflicts (if not manual)

                      if (resolved.length > 0) {
                        syncResultObject.reset("conflicts").add("resolved", resolved);
                      }

                      _context17.next = 15;
                      break;

                    case 11:
                      _context17.prev = 11;
                      _context17.t0 = _context17["catch"](1);
                      data = {
                        type: "incoming",
                        message: _context17.t0.message,
                        stack: _context17.t0.stack
                      }; // XXX one error of the whole transaction instead of per atomic op

                      syncResultObject.add("errors", data);

                    case 15:
                      return _context17.abrupt("return", syncResultObject);

                    case 16:
                    case "end":
                      return _context17.stop();
                  }
                }
              }, _callee17, this, [[1, 11]]);
            }));

            return function importChanges(_x15, _x16) {
              return _importChanges.apply(this, arguments);
            };
          }()
          /**
           * 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}
           */

        }, {
          key: "_applyPushedResults",
          value: function () {
            var _applyPushedResults2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee18(syncResultObject, toApplyLocally, conflicts) {
              var _this9 = this;

              var strategy,
                  toDeleteLocally,
                  toUpdateLocally,
                  _ref5,
                  published,
                  resolved,
                  _args18 = arguments;

              return _regenerator.default.wrap(function _callee18$(_context18) {
                while (1) {
                  switch (_context18.prev = _context18.next) {
                    case 0:
                      strategy = _args18.length > 3 && _args18[3] !== undefined ? _args18[3] : Collection.strategy.MANUAL;
                      toDeleteLocally = toApplyLocally.filter(function (r) {
                        return r.deleted;
                      });
                      toUpdateLocally = toApplyLocally.filter(function (r) {
                        return !r.deleted;
                      });
                      _context18.next = 5;
                      return this.db.execute(function (transaction) {
                        var updated = toUpdateLocally.map(function (record) {
                          var synced = markSynced(record);
                          transaction.update(synced);
                          return synced;
                        });
                        var deleted = toDeleteLocally.map(function (record) {
                          transaction.delete(record.id); // Amend result data with the deleted attribute set

                          return {
                            id: record.id,
                            deleted: true
                          };
                        });
                        var published = updated.concat(deleted); // Handle conflicts, if any

                        var resolved = _this9._handleConflicts(transaction, conflicts, strategy);

                        return {
                          published: published,
                          resolved: resolved
                        };
                      });

                    case 5:
                      _ref5 = _context18.sent;
                      published = _ref5.published;
                      resolved = _ref5.resolved;
                      syncResultObject.add("published", published);

                      if (resolved.length > 0) {
                        syncResultObject.reset("conflicts").reset("resolved").add("resolved", resolved);
                      }

                      return _context18.abrupt("return", syncResultObject);

                    case 11:
                    case "end":
                      return _context18.stop();
                  }
                }
              }, _callee18, this);
            }));

            return function _applyPushedResults(_x17, _x18, _x19) {
              return _applyPushedResults2.apply(this, arguments);
            };
          }()
          /**
           * 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
           */

        }, {
          key: "_handleConflicts",
          value: function _handleConflicts(transaction, conflicts, strategy) {
            var _this10 = this;

            if (strategy === Collection.strategy.MANUAL) {
              return [];
            }

            return conflicts.map(function (conflict) {
              var resolution = strategy === Collection.strategy.CLIENT_WINS ? conflict.local : conflict.remote;
              var rejected = strategy === Collection.strategy.CLIENT_WINS ? conflict.remote : conflict.local;
              var 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 kinto-http.js
                // 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 {
                var updated = _this10._resolveRaw(conflict, resolution);

                transaction.update(updated);
                accepted = updated;
                status = updated._status;
                id = updated.id;
              }

              return {
                rejected: rejected,
                accepted: accepted,
                id: 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.
           */

        }, {
          key: "execute",
          value: function execute(doOperations) {
            var _this11 = this;

            var _ref6 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
                _ref6$preloadIds = _ref6.preloadIds,
                preloadIds = _ref6$preloadIds === void 0 ? [] : _ref6$preloadIds;

            var _iteratorNormalCompletion = true;
            var _didIteratorError = false;
            var _iteratorError = undefined;

            try {
              for (var _iterator = preloadIds[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
                var id = _step.value;

                if (!this.idSchema.validate(id)) {
                  return Promise.reject(Error("Invalid Id: ".concat(id)));
                }
              }
            } catch (err) {
              _didIteratorError = true;
              _iteratorError = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion && _iterator.return != null) {
                  _iterator.return();
                }
              } finally {
                if (_didIteratorError) {
                  throw _iteratorError;
                }
              }
            }

            return this.db.execute(function (transaction) {
              var txn = new CollectionTransaction(_this11, transaction);
              var 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.
           */

        }, {
          key: "resetSyncStatus",
          value: function () {
            var _resetSyncStatus = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee19() {
              var unsynced;
              return _regenerator.default.wrap(function _callee19$(_context19) {
                while (1) {
                  switch (_context19.prev = _context19.next) {
                    case 0:
                      _context19.next = 2;
                      return this.list({
                        filters: {
                          _status: ["deleted", "synced"]
                        },
                        order: ""
                      }, {
                        includeDeleted: true
                      });

                    case 2:
                      unsynced = _context19.sent;
                      _context19.next = 5;
                      return this.db.execute(function (transaction) {
                        unsynced.data.forEach(function (record) {
                          if (record._status === "deleted") {
                            // Garbage collect deleted records.
                            transaction.delete(record.id);
                          } else {
                            // Records that were synced become «created».
                            transaction.update((0, _objectSpread2.default)({}, record, {
                              last_modified: undefined,
                              _status: "created"
                            }));
                          }
                        });
                      });

                    case 5:
                      this._lastModified = null;
                      _context19.next = 8;
                      return this.db.saveLastModified(null);

                    case 8:
                      return _context19.abrupt("return", unsynced.data.length);

                    case 9:
                    case "end":
                      return _context19.stop();
                  }
                }
              }, _callee19, this);
            }));

            return function resetSyncStatus() {
              return _resetSyncStatus.apply(this, arguments);
            };
          }()
          /**
           * Returns an object containing two lists:
           *
           * - `toDelete`: unsynced deleted records we can safely delete;
           * - `toSync`: local updates to send to the server.
           *
           * @return {Promise}
           */

        }, {
          key: "gatherLocalChanges",
          value: function () {
            var _gatherLocalChanges = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee20() {
              var unsynced, deleted;
              return _regenerator.default.wrap(function _callee20$(_context20) {
                while (1) {
                  switch (_context20.prev = _context20.next) {
                    case 0:
                      _context20.next = 2;
                      return this.list({
                        filters: {
                          _status: ["created", "updated"]
                        },
                        order: ""
                      });

                    case 2:
                      unsynced = _context20.sent;
                      _context20.next = 5;
                      return this.list({
                        filters: {
                          _status: "deleted"
                        },
                        order: ""
                      }, {
                        includeDeleted: true
                      });

                    case 5:
                      deleted = _context20.sent;
                      _context20.next = 8;
                      return Promise.all(unsynced.data.concat(deleted.data).map(this._encodeRecord.bind(this, "remote")));

                    case 8:
                      return _context20.abrupt("return", _context20.sent);

                    case 9:
                    case "end":
                      return _context20.stop();
                  }
                }
              }, _callee20, this);
            }));

            return function gatherLocalChanges() {
              return _gatherLocalChanges.apply(this, arguments);
            };
          }()
          /**
           * 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.
           *
           * @param  {KintoClient.Collection} client           Kinto client Collection instance.
           * @param  {SyncResultObject}       syncResultObject The sync result object.
           * @param  {Object}                 options
           * @return {Promise}
           */

        }, {
          key: "pullChanges",
          value: function () {
            var _pullChanges = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee21(client, syncResultObject) {
              var _this12 = this;

              var options,
                  since,
                  filters,
                  exclude_id,
                  _ref7,
                  data,
                  last_modified,
                  unquoted,
                  localSynced,
                  serverChanged,
                  emptyCollection,
                  decodedChanges,
                  payload,
                  afterHooks,
                  _args21 = arguments;

              return _regenerator.default.wrap(function _callee21$(_context21) {
                while (1) {
                  switch (_context21.prev = _context21.next) {
                    case 0:
                      options = _args21.length > 2 && _args21[2] !== undefined ? _args21[2] : {};

                      if (syncResultObject.ok) {
                        _context21.next = 3;
                        break;
                      }

                      return _context21.abrupt("return", syncResultObject);

                    case 3:
                      if (!this.lastModified) {
                        _context21.next = 7;
                        break;
                      }

                      _context21.t0 = this.lastModified;
                      _context21.next = 10;
                      break;

                    case 7:
                      _context21.next = 9;
                      return this.db.getLastModified();

                    case 9:
                      _context21.t0 = _context21.sent;

                    case 10:
                      since = _context21.t0;
                      options = (0, _objectSpread2.default)({
                        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())

                      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
                        exclude_id = options.exclude.slice(0, 50).map(function (r) {
                          return r.id;
                        }).join(",");
                        filters = {
                          exclude_id: exclude_id
                        };
                      } // First fetch remote changes from the server


                      _context21.next = 15;
                      return client.listRecords({
                        // Since should be ETag (see https://github.com/Kinto/kinto.js/issues/356)
                        since: options.lastModified ? "".concat(options.lastModified) : undefined,
                        headers: options.headers,
                        retry: options.retry,
                        // Fetch every page by default (FIXME: option to limit pages, see #277)
                        pages: Infinity,
                        filters: filters
                      });

                    case 15:
                      _ref7 = _context21.sent;
                      data = _ref7.data;
                      last_modified = _ref7.last_modified;
                      // last_modified is the ETag header value (string).
                      // For retro-compatibility with first kinto.js versions
                      // parse it to integer.
                      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).

                      localSynced = options.lastModified;
                      serverChanged = unquoted > options.lastModified;
                      emptyCollection = data.length === 0;

                      if (!(!options.exclude && localSynced && serverChanged && emptyCollection)) {
                        _context21.next = 24;
                        break;
                      }

                      throw Error("Server has been flushed.");

                    case 24:
                      syncResultObject.lastModified = unquoted; // Decode incoming changes.

                      _context21.next = 27;
                      return Promise.all(data.map(function (change) {
                        return _this12._decodeRecord("remote", change);
                      }));

                    case 27:
                      decodedChanges = _context21.sent;
                      // Hook receives decoded records.
                      payload = {
                        lastModified: unquoted,
                        changes: decodedChanges
                      };
                      _context21.next = 31;
                      return this.applyHook("incoming-changes", payload);

                    case 31:
                      afterHooks = _context21.sent;

                      if (!(afterHooks.changes.length > 0)) {
                        _context21.next = 35;
                        break;
                      }

                      _context21.next = 35;
                      return this.importChanges(syncResultObject, afterHooks.changes, options.strategy);

                    case 35:
                      return _context21.abrupt("return", syncResultObject);

                    case 36:
                    case "end":
                      return _context21.stop();
                  }
                }
              }, _callee21, this);
            }));

            return function pullChanges(_x20, _x21) {
              return _pullChanges.apply(this, arguments);
            };
          }()
        }, {
          key: "applyHook",
          value: function applyHook(hookName, payload) {
            var _this13 = this;

            if (typeof this.hooks[hookName] == "undefined") {
              return Promise.resolve(payload);
            }

            return (0, _utils.waterfall)(this.hooks[hookName].map(function (hook) {
              return function (record) {
                var result = hook(payload, _this13);
                var resultThenable = result && typeof result.then === "function";
                var resultChanges = result && result.hasOwnProperty("changes");

                if (!(resultThenable || resultChanges)) {
                  throw new Error("Invalid return value for hook: ".concat(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.
           *
           * @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}
           */

        }, {
          key: "pushChanges",
          value: function () {
            var _pushChanges = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee22(client, changes, syncResultObject) {
              var _this14 = this;

              var options,
                  safe,
                  toDelete,
                  toSync,
                  synced,
                  conflicts,
                  _iteratorNormalCompletion2,
                  _didIteratorError2,
                  _iteratorError2,
                  _iterator2,
                  _step2,
                  _step2$value,
                  type,
                  local,
                  remote,
                  safeLocal,
                  realLocal,
                  realRemote,
                  conflict,
                  missingRemotely,
                  published,
                  toApplyLocally,
                  decoded,
                  _args22 = arguments;

              return _regenerator.default.wrap(function _callee22$(_context22) {
                while (1) {
                  switch (_context22.prev = _context22.next) {
                    case 0:
                      options = _args22.length > 3 && _args22[3] !== undefined ? _args22[3] : {};

                      if (syncResultObject.ok) {
                        _context22.next = 3;
                        break;
                      }

                      return _context22.abrupt("return", syncResultObject);

                    case 3:
                      safe = !options.strategy || options.strategy !== Collection.CLIENT_WINS;
                      toDelete = changes.filter(function (r) {
                        return r._status == "deleted";
                      });
                      toSync = changes.filter(function (r) {
                        return r._status != "deleted";
                      }); // Perform a batch request with every changes.

                      _context22.next = 8;
                      return client.batch(function (batch) {
                        toDelete.forEach(function (r) {
                          // never published locally deleted records should not be pusblished
                          if (r.last_modified) {
                            batch.deleteRecord(r);
                          }
                        });
                        toSync.forEach(function (r) {
                          // Clean local fields (like _status) before sending to server.
                          var published = _this14.cleanLocalFields(r);

                          if (r._status === "created") {
                            batch.createRecord(published);
                          } else {
                            batch.updateRecord(published);
                          }
                        });
                      }, {
                        headers: options.headers,
                        retry: options.retry,
                        safe: safe,
                        aggregate: true
                      });

                    case 8:
                      synced = _context22.sent;
                      // Store outgoing errors into sync result object
                      syncResultObject.add("errors", synced.errors.map(function (e) {
                        return (0, _objectSpread2.default)({}, e, {
                          type: "outgoing"
                        });
                      })); // Store outgoing conflicts into sync result object

                      conflicts = [];
                      _iteratorNormalCompletion2 = true;
                      _didIteratorError2 = false;
                      _iteratorError2 = undefined;
                      _context22.prev = 14;
                      _iterator2 = synced.conflicts[Symbol.iterator]();

                    case 16:
                      if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {
                        _context22.next = 33;
                        break;
                      }

                      _step2$value = _step2.value, type = _step2$value.type, local = _step2$value.local, remote = _step2$value.remote;
                      // Note: we ensure that local data are actually available, as they may
                      // be missing in the case of a published deletion.
                      safeLocal = local && local.data || {
                        id: remote.id
                      };
                      _context22.next = 21;
                      return this._decodeRecord("remote", safeLocal);

                    case 21:
                      realLocal = _context22.sent;
                      _context22.t0 = remote;

                      if (!_context22.t0) {
                        _context22.next = 27;
                        break;
                      }

                      _context22.next = 26;
                      return this._decodeRecord("remote", remote);

                    case 26:
                      _context22.t0 = _context22.sent;

                    case 27:
                      realRemote = _context22.t0;
                      conflict = {
                        type: type,
                        local: realLocal,
                        remote: realRemote
                      };
                      conflicts.push(conflict);

                    case 30:
                      _iteratorNormalCompletion2 = true;
                      _context22.next = 16;
                      break;

                    case 33:
                      _context22.next = 39;
                      break;

                    case 35:
                      _context22.prev = 35;
                      _context22.t1 = _context22["catch"](14);
                      _didIteratorError2 = true;
                      _iteratorError2 = _context22.t1;

                    case 39:
                      _context22.prev = 39;
                      _context22.prev = 40;

                      if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
                        _iterator2.return();
                      }

                    case 42:
                      _context22.prev = 42;

                      if (!_didIteratorError2) {
                        _context22.next = 45;
                        break;
                      }

                      throw _iteratorError2;

                    case 45:
                      return _context22.finish(42);

                    case 46:
                      return _context22.finish(39);

                    case 47:
                      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).

                      missingRemotely = synced.skipped.map(function (r) {
                        return (0, _objectSpread2.default)({}, 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.

                      published = synced.published.map(function (c) {
                        return c.data;
                      });
                      toApplyLocally = published.concat(missingRemotely); // Apply the decode transformers, if any

                      _context22.next = 53;
                      return Promise.all(toApplyLocally.map(function (record) {
                        return _this14._decodeRecord("remote", record);
                      }));

                    case 53:
                      decoded = _context22.sent;

                      if (!(decoded.length > 0 || conflicts.length > 0)) {
                        _context22.next = 57;
                        break;
                      }

                      _context22.next = 57;
                      return this._applyPushedResults(syncResultObject, decoded, conflicts, options.strategy);

                    case 57:
                      return _context22.abrupt("return", syncResultObject);

                    case 58:
                    case "end":
                      return _context22.stop();
                  }
                }
              }, _callee22, this, [[14, 35, 39, 47], [40,, 42, 46]]);
            }));

            return function pushChanges(_x22, _x23, _x24) {
              return _pushChanges.apply(this, arguments);
            };
          }()
          /**
           * Return a copy of the specified record without the local fields.
           *
           * @param  {Object} record  A record with potential local fields.
           * @return {Object}
           */

        }, {
          key: "cleanLocalFields",
          value: function cleanLocalFields(record) {
            var localKeys = RECORD_FIELDS_TO_CLEAN.concat(this.localFields);
            return (0, _utils.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}
           */

        }, {
          key: "resolve",
          value: function resolve(conflict, resolution) {
            var _this15 = this;

            return this.db.execute(function (transaction) {
              var updated = _this15._resolveRaw(conflict, resolution);

              transaction.update(updated);
              return {
                data: updated,
                permissions: {}
              };
            });
          }
          /**
           * @private
           */

        }, {
          key: "_resolveRaw",
          value: function _resolveRaw(conflict, resolution) {
            var resolved = (0, _objectSpread2.default)({}, 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).

            var synced = (0, _utils.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.
           * - {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.
           */

        }, {
          key: "sync",
          value: function () {
            var _sync = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee23() {
              var _this16 = this;

              var options,
                  previousRemote,
                  seconds,
                  client,
                  result,
                  lastModified,
                  toSync,
                  resolvedUnsynced,
                  resolvedEncoded,
                  pullOpts,
                  _args23 = arguments;
              return _regenerator.default.wrap(function _callee23$(_context23) {
                while (1) {
                  switch (_context23.prev = _context23.next) {
                    case 0:
                      options = _args23.length > 0 && _args23[0] !== undefined ? _args23[0] : {
                        strategy: Collection.strategy.MANUAL,
                        headers: {},
                        retry: 1,
                        ignoreBackoff: false,
                        bucket: null,
                        collection: null,
                        remote: null
                      };
                      options = (0, _objectSpread2.default)({}, options, {
                        bucket: options.bucket || this.bucket,
                        collection: options.collection || this.name
                      });
                      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)) {
                        _context23.next = 7;
                        break;
                      }

                      seconds = Math.ceil(this.api.backoff / 1000);
                      return _context23.abrupt("return", Promise.reject(new Error("Server is asking clients to back off; retry in ".concat(seconds, "s or use the ignoreBackoff option."))));

                    case 7:
                      client = this.api.bucket(options.bucket).collection(options.collection);
                      result = new SyncResultObject();
                      _context23.prev = 9;
                      _context23.next = 12;
                      return this.pullChanges(client, result, options);

                    case 12:
                      lastModified = result.lastModified; // Fetch local changes

                      _context23.next = 15;
                      return this.gatherLocalChanges();

                    case 15:
                      toSync = _context23.sent;
                      _context23.next = 18;
                      return this.pushChanges(client, toSync, result, options);

                    case 18:
                      // Publish local resolution of push conflicts to server (on CLIENT_WINS)
                      resolvedUnsynced = result.resolved.filter(function (r) {
                        return r._status !== "synced";
                      });

                      if (!(resolvedUnsynced.length > 0)) {
                        _context23.next = 25;
                        break;
                      }

                      _context23.next = 22;
                      return Promise.all(resolvedUnsynced.map(function (resolution) {
                        var record = resolution.accepted;

                        if (record === null) {
                          record = {
                            id: resolution.id,
                            _status: resolution._status
                          };
                        }

                        return _this16._encodeRecord("remote", record);
                      }));

                    case 22:
                      resolvedEncoded = _context23.sent;
                      _context23.next = 25;
                      return this.pushChanges(client, resolvedEncoded, result, options);

                    case 25:
                      if (!(result.published.length > 0)) {
                        _context23.next = 29;
                        break;
                      }

                      // Avoid redownloading our own changes during the last pull.
                      pullOpts = (0, _objectSpread2.default)({}, options, {
                        lastModified: lastModified,
                        exclude: result.published
                      });
                      _context23.next = 29;
                      return this.pullChanges(client, result, pullOpts);

                    case 29:
                      if (!result.ok) {
                        _context23.next = 33;
                        break;
                      }

                      _context23.next = 32;
                      return this.db.saveLastModified(result.lastModified);

                    case 32:
                      this._lastModified = _context23.sent;

                    case 33:
                      _context23.next = 39;
                      break;

                    case 35:
                      _context23.prev = 35;
                      _context23.t0 = _context23["catch"](9);
                      this.events.emit("sync:error", (0, _objectSpread2.default)({}, options, {
                        error: _context23.t0
                      }));
                      throw _context23.t0;

                    case 39:
                      _context23.prev = 39;
                      // Ensure API default remote is reverted if a custom one's been used
                      this.api.remote = previousRemote;
                      return _context23.finish(39);

                    case 42:
                      this.events.emit("sync:success", (0, _objectSpread2.default)({}, options, {
                        result: result
                      }));
                      return _context23.abrupt("return", result);

                    case 44:
                    case "end":
                      return _context23.stop();
                  }
                }
              }, _callee23, this, [[9, 35, 39, 42]]);
            }));

            return function sync() {
              return _sync.apply(this, arguments);
            };
          }()
          /**
           * 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.
           */

        }, {
          key: "loadDump",
          value: function () {
            var _loadDump2 = (0, _asyncToGenerator2.default)(
            /*#__PURE__*/
            _regenerator.default.mark(function _callee24(records) {
              var _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, record, _ref8, data, existingById, newRecords;

              return _regenerator.default.wrap(function _callee24$(_context24) {
                while (1) {
                  switch (_context24.prev = _context24.next) {
                    case 0:
                      if (Array.isArray(records)) {
                        _context24.next = 2;
                        break;
                      }

                      throw new Error("Records is not an array.");

                    case 2:
                      _iteratorNormalCompletion3 = true;
                      _didIteratorError3 = false;
                      _iteratorError3 = undefined;
                      _context24.prev = 5;
                      _iterator3 = records[Symbol.iterator]();

                    case 7:
                      if (_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done) {
                        _context24.next = 16;
                        break;
                      }

                      record = _step3.value;

                      if (!(!record.hasOwnProperty("id") || !this.idSchema.validate(record.id))) {
                        _context24.next = 11;
                        break;
                      }

                      throw new Error("Record has invalid ID: " + JSON.stringify(record));

                    case 11:
                      if (record.last_modified) {
                        _context24.next = 13;
                        break;
                      }

                      throw new Error("Record has no last_modified value: " + JSON.stringify(record));

                    case 13:
                      _iteratorNormalCompletion3 = true;
                      _context24.next = 7;
                      break;

                    case 16:
                      _context24.next = 22;
                      break;

                    case 18:
                      _context24.prev = 18;
                      _context24.t0 = _context24["catch"](5);
                      _didIteratorError3 = true;
                      _iteratorError3 = _context24.t0;

                    case 22:
                      _context24.prev = 22;
                      _context24.prev = 23;

                      if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
                        _iterator3.return();
                      }

                    case 25:
                      _context24.prev = 25;

                      if (!_didIteratorError3) {
                        _context24.next = 28;
                        break;
                      }

                      throw _iteratorError3;

                    case 28:
                      return _context24.finish(25);

                    case 29:
                      return _context24.finish(22);

                    case 30:
                      _context24.next = 32;
                      return this.list({}, {
                        includeDeleted: true
                      });

                    case 32:
                      _ref8 = _context24.sent;
                      data = _ref8.data;
                      existingById = data.reduce(function (acc, record) {
                        acc[record.id] = record;
                        return acc;
                      }, {});
                      newRecords = records.filter(function (record) {
                        var localRecord = existingById[record.id];
                        var 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;
                      });
                      _context24.next = 38;
                      return this.db.loadDump(newRecords.map(markSynced));

                    case 38:
                      return _context24.abrupt("return", _context24.sent);

                    case 39:
                    case "end":
                      return _context24.stop();
                  }
                }
              }, _callee24, this, [[5, 18, 22, 30], [23,, 25, 29]]);
            }));

            return function loadDump(_x25) {
              return _loadDump2.apply(this, arguments);
            };
          }()
        }, {
          key: "name",
          get: function get() {
            return this._name;
          }
          /**
           * The bucket name.
           * @type {String}
           */

        }, {
          key: "bucket",
          get: function get() {
            return this._bucket;
          }
          /**
           * The last modified timestamp.
           * @type {Number}
           */

        }, {
          key: "lastModified",
          get: function get() {
            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}
           */

        }], [{
          key: "strategy",
          get: function get() {
            return {
              CLIENT_WINS: "client_wins",
              SERVER_WINS: "server_wins",
              MANUAL: "manual"
            };
          }
        }]);
        return Collection;
      }();
      /**
       * 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.
       */


      exports.default = Collection;

      var CollectionTransaction =
      /*#__PURE__*/
      function () {
        function CollectionTransaction(collection, adapterTransaction) {
          (0, _classCallCheck2.default)(this, CollectionTransaction);
          this.collection = collection;
          this.adapterTransaction = adapterTransaction;
          this._events = [];
        }

        (0, _createClass2.default)(CollectionTransaction, [{
          key: "_queueEvent",
          value: function _queueEvent(action, payload) {
            this._events.push({
              action: action,
              payload: payload
            });
          }
          /**
           * Emit queued events, to be called once every transaction operations have
           * been executed successfully.
           */

        }, {
          key: "emitEvents",
          value: function emitEvents() {
            var _iteratorNormalCompletion4 = true;
            var _didIteratorError4 = false;
            var _iteratorError4 = undefined;

            try {
              for (var _iterator4 = this._events[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
                var _step4$value = _step4.value,
                    action = _step4$value.action,
                    payload = _step4$value.payload;
                this.collection.events.emit(action, payload);
              }
            } catch (err) {
              _didIteratorError4 = true;
              _iteratorError4 = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
                  _iterator4.return();
                }
              } finally {
                if (_didIteratorError4) {
                  throw _iteratorError4;
                }
              }
            }

            if (this._events.length > 0) {
              var targets = this._events.map(function (_ref9) {
                var action = _ref9.action,
                    payload = _ref9.payload;
                return (0, _objectSpread2.default)({
                  action: action
                }, payload);
              });

              this.collection.events.emit("change", {
                targets: 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}
           */

        }, {
          key: "getAny",
          value: function getAny(id) {
            var 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}
           */

        }, {
          key: "get",
          value: function get(id) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              includeDeleted: false
            };
            var res = this.getAny(id);

            if (!res.data || !options.includeDeleted && res.data._status === "deleted") {
              throw new Error("Record with id=".concat(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}
           */

        }, {
          key: "delete",
          value: function _delete(id) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              virtual: true
            };
            // Ensure the record actually exists.
            var existing = this.adapterTransaction.get(id);
            var alreadyDeleted = existing && existing._status == "deleted";

            if (!existing || alreadyDeleted && options.virtual) {
              throw new Error("Record with id=".concat(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}
           */

        }, {
          key: "deleteAll",
          value: function deleteAll(ids) {
            var _this17 = this;

            var existingRecords = [];
            ids.forEach(function (id) {
              existingRecords.push(_this17.adapterTransaction.get(id));

              _this17.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}
           */

        }, {
          key: "deleteAny",
          value: function deleteAny(id) {
            var existing = this.adapterTransaction.get(id);

            if (existing) {
              this.adapterTransaction.update(markDeleted(existing));

              this._queueEvent("delete", {
                data: existing
              });
            }

            return {
              data: (0, _objectSpread2.default)({
                id: 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}
           */

        }, {
          key: "create",
          value: function create(record) {
            if ((0, _typeof2.default)(record) !== "object") {
              throw new Error("Record is not an object.");
            }

            if (!record.hasOwnProperty("id")) {
              throw new Error("Cannot create a record missing id");
            }

            if (!this.collection.idSchema.validate(record.id)) {
              throw new Error("Invalid Id: ".concat(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}
           */

        }, {
          key: "update",
          value: function update(record) {
            var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
              synced: false,
              patch: false
            };

            if ((0, _typeof2.default)(record) !== "object") {
              throw new Error("Record is not an object.");
            }

            if (!record.hasOwnProperty("id")) {
              throw new Error("Cannot update a record missing id.");
            }

            if (!this.collection.idSchema.validate(record.id)) {
              throw new Error("Invalid Id: ".concat(record.id));
            }

            var oldRecord = this.adapterTransaction.get(record.id);

            if (!oldRecord) {
              throw new Error("Record with id=".concat(record.id, " not found."));
            }

            var newRecord = options.patch ? (0, _objectSpread2.default)({}, oldRecord, record) : record;

            var updated = this._updateRaw(oldRecord, newRecord, options);

            this.adapterTransaction.update(updated);

            this._queueEvent("update", {
              data: updated,
              oldRecord: oldRecord
            });

            return {
              data: updated,
              oldRecord: 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}
           */

        }, {
          key: "_updateRaw",
          value: function _updateRaw(oldRecord, newRecord) {
            var _ref10 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
                _ref10$synced = _ref10.synced,
                synced = _ref10$synced === void 0 ? false : _ref10$synced;

            var updated = (0, _objectSpread2.default)({}, 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.


            var isIdentical = oldRecord && recordsEqual(oldRecord, updated, this.localFields);
            var keepSynced = isIdentical && oldRecord._status == "synced";
            var neverSynced = !oldRecord || oldRecord && oldRecord._status == "created";
            var 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}
           */

        }, {
          key: "upsert",
          value: function upsert(record) {
            if ((0, _typeof2.default)(record) !== "object") {
              throw new Error("Record is not an object.");
            }

            if (!record.hasOwnProperty("id")) {
              throw new Error("Cannot update a record missing id.");
            }

            if (!this.collection.idSchema.validate(record.id)) {
              throw new Error("Invalid Id: ".concat(record.id));
            }

            var oldRecord = this.adapterTransaction.get(record.id);

            var 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: oldRecord
              });
            } else {
              this._queueEvent("create", {
                data: updated
              });
            }

            return {
              data: updated,
              oldRecord: oldRecord,
              permissions: {}
            };
          }
        }]);
        return CollectionTransaction;
      }();

      exports.CollectionTransaction = CollectionTransaction;
    }, {
      "./adapters/IDB": 4,
      "./adapters/base": 5,
      "./utils": 7,
      "uuid": 2
    }],
    7: [function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      exports.sortObjects = sortObjects;
      exports.filterObject = filterObject;
      exports.filterObjects = filterObjects;
      exports.waterfall = waterfall;
      exports.deepEqual = deepEqual;
      exports.omitKeys = omitKeys;
      exports.arrayEqual = arrayEqual;
      exports.RE_RECORD_ID = void 0;
      var RE_RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
      /**
       * Checks if a value is undefined.
       * @param  {Any}  value
       * @return {Boolean}
       */

      exports.RE_RECORD_ID = RE_RECORD_ID;

      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) {
        var hasDash = order[0] === "-";
        var field = hasDash ? order.slice(1) : order;
        var direction = hasDash ? -1 : 1;
        return list.slice().sort(function (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(function (filter) {
          var value = filters[filter];

          if (Array.isArray(value)) {
            return value.some(function (candidate) {
              return candidate === entry[filter];
            });
          }

          return entry[filter] === value;
        });
      }
      /**
       * Filters records in a list matching all given filters.
       *
       * @param  {Object} filters  The filters object.
       * @param  {Array}  list     The collection to filter.
       * @return {Array}
       */


      function filterObjects(filters, list) {
        return list.filter(function (entry) {
          return filterObject(filters, entry);
        });
      }
      /**
       * 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(function (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 ((0, _typeof2.default)(a) !== (0, _typeof2.default)(b)) {
          return false;
        }

        if (!(a && (0, _typeof2.default)(a) == "object") || !(b && (0, _typeof2.default)(b) == "object")) {
          return false;
        }

        if (Object.keys(a).length !== Object.keys(b).length) {
          return false;
        }

        for (var 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) {
        var keys = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
        return Object.keys(obj).reduce(function (acc, key) {
          if (!keys.includes(key)) {
            acc[key] = obj[key];
          }

          return acc;
        }, {});
      }

      function arrayEqual(a, b) {
        if (a.length !== b.length) {
          return false;
        }

        for (var i = a.length; i--;) {
          if (a[i] !== b[i]) {
            return false;
          }
        }

        return true;
      }
    }, {}]
  }, {}, [1])(1);
});