Bug 1388830 - upgrade kinto-http.js to 4.5.3. r=MattN
authorEthan Glasser-Camp <ethan@betacantrips.com>
Wed, 14 Feb 2018 12:48:09 -0500
changeset 458780 684f8415581ee3763d5c896e8212ce24ff6c4a47
parent 458779 aba40941f0274fb7b47a896fc6519875174e1f1f
child 458781 af0f71f70a18b758041ae69bead2811c44e30ffc
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1388830
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1388830 - upgrade kinto-http.js to 4.5.3. r=MattN This introduces an `UnparseableResponseError`, which exposes the text of the actual non-JSON response. It's also catcheable by client code (i.e. ExtensionStorageSync.jsm) if we believe this error is common enough to be silenced. MozReview-Commit-ID: H3ADFBFJRKA
services/common/kinto-http-client.js
--- a/services/common/kinto-http-client.js
+++ b/services/common/kinto-http-client.js
@@ -18,20 +18,20 @@
  * This file is generated from kinto-http.js - do not modify directly.
  */
 
 const global = this;
 
 this.EXPORTED_SYMBOLS = ["KintoHttpClient"];
 
 /*
- * Version 4.3.4 - 1294207
+ * Version 4.5.3 - 5179c56
  */
 
-(function(f){if(typeof 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.KintoHttpClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+(function(f){if(typeof 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.KintoHttpClient = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({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
  *
@@ -44,149 +44,161 @@ this.EXPORTED_SYMBOLS = ["KintoHttpClien
 
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _base = require("../src/base");
 
 var _base2 = _interopRequireDefault(_base);
 
+var _errors = require("../src/errors");
+
+var errors = _interopRequireWildcard(_errors);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
-Cu.importGlobalProperties(['fetch']);
+Cu.importGlobalProperties(["fetch"]);
 const { EventEmitter } = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
 
 let KintoHttpClient = class KintoHttpClient extends _base2.default {
   constructor(remote, options = {}) {
     const events = {};
     EventEmitter.decorate(events);
-    super(remote, _extends({ events }, options));
+    super(remote, { events, ...options });
   }
 };
+exports.default = KintoHttpClient;
+
+
+KintoHttpClient.errors = errors;
 
 // 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 = KintoHttpClient;
 if (typeof module === "object") {
   module.exports = KintoHttpClient;
 }
 
-},{"../src/base":7}],2:[function(require,module,exports){
+},{"../src/base":7,"../src/errors":12}],2:[function(require,module,exports){
 var v1 = require('./v1');
 var v4 = require('./v4');
 
 var uuid = v4;
 uuid.v1 = v1;
 uuid.v4 = v4;
 
 module.exports = uuid;
 
 },{"./v1":5,"./v4":6}],3:[function(require,module,exports){
 /**
  * Convert array of 16 byte values to UUID string format of the form:
- * XXXXXXXX-XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+ * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  */
 var byteToHex = [];
 for (var i = 0; i < 256; ++i) {
   byteToHex[i] = (i + 0x100).toString(16).substr(1);
 }
 
 function bytesToUuid(buf, offset) {
   var i = offset || 0;
   var bth = byteToHex;
-  return  bth[buf[i++]] + bth[buf[i++]] +
+  return bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]];
 }
 
 module.exports = bytesToUuid;
 
 },{}],4:[function(require,module,exports){
 // Unique ID creation requires a high quality random # generator.  In the
 // browser this is a little complicated due to unknown quality of Math.random()
 // and inconsistent support for the `crypto` API.  We do the best we can via
 // feature-detection
-var rng;
 
-var crypto = global.crypto || global.msCrypto; // for IE 11
-if (crypto && crypto.getRandomValues) {
+// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation.
+var getRandomValues = (typeof(crypto) != 'undefined' && crypto.getRandomValues.bind(crypto)) ||
+                      (typeof(msCrypto) != 'undefined' && msCrypto.getRandomValues.bind(msCrypto));
+if (getRandomValues) {
   // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
-  var rnds8 = new Uint8Array(16);
-  rng = function whatwgRNG() {
-    crypto.getRandomValues(rnds8);
+  var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef
+
+  module.exports = function whatwgRNG() {
+    getRandomValues(rnds8);
     return rnds8;
   };
-}
-
-if (!rng) {
+} else {
   // Math.random()-based (RNG)
   //
   // If all else fails, use Math.random().  It's fast, but is of unspecified
   // quality.
-  var  rnds = new Array(16);
-  rng = function() {
+  var rnds = new Array(16);
+
+  module.exports = function mathRNG() {
     for (var i = 0, r; i < 16; i++) {
       if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
       rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
     }
 
     return rnds;
   };
 }
 
-module.exports = rng;
-
 },{}],5:[function(require,module,exports){
-// Unique ID creation requires a high quality random # generator.  We feature
-// detect to determine the best RNG source, normalizing to a function that
-// returns 128-bits of randomness, since that's what's usually required
 var rng = require('./lib/rng');
 var bytesToUuid = require('./lib/bytesToUuid');
 
 // **`v1()` - Generate time-based UUID**
 //
 // Inspired by https://github.com/LiosK/UUID.js
 // and http://docs.python.org/library/uuid.html
 
-// random #'s we need to init node and clockseq
-var _seedBytes = rng();
-
-// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
-var _nodeId = [
-  _seedBytes[0] | 0x01,
-  _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
-];
-
-// Per 4.2.2, randomize (14 bit) clockseq
-var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+var _nodeId;
+var _clockseq;
 
 // Previous uuid creation time
-var _lastMSecs = 0, _lastNSecs = 0;
+var _lastMSecs = 0;
+var _lastNSecs = 0;
 
 // See https://github.com/broofa/node-uuid for API details
 function v1(options, buf, offset) {
   var i = buf && offset || 0;
   var b = buf || [];
 
   options = options || {};
+  var node = options.node || _nodeId;
+  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
 
-  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
+  // node and clockseq need to be initialized to random values if they're not
+  // specified.  We do this lazily to minimize issues related to insufficient
+  // system entropy.  See #189
+  if (node == null || clockseq == null) {
+    var seedBytes = rng();
+    if (node == null) {
+      // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+      node = _nodeId = [
+        seedBytes[0] | 0x01,
+        seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]
+      ];
+    }
+    if (clockseq == null) {
+      // Per 4.2.2, randomize (14 bit) clockseq
+      clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
+    }
+  }
 
   // UUID timestamps are 100 nano-second units since the Gregorian epoch,
   // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
   // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
   // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
   var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
 
   // Per 4.2.1.2, use count of uuid's generated during the current clock
@@ -237,17 +249,16 @@ function v1(options, buf, offset) {
 
   // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
   b[i++] = clockseq >>> 8 | 0x80;
 
   // `clock_seq_low`
   b[i++] = clockseq & 0xff;
 
   // `node`
-  var node = options.node || _nodeId;
   for (var n = 0; n < 6; ++n) {
     b[i + n] = node[n];
   }
 
   return buf ? buf : bytesToUuid(b);
 }
 
 module.exports = v1;
@@ -255,17 +266,17 @@ module.exports = v1;
 },{"./lib/bytesToUuid":3,"./lib/rng":4}],6:[function(require,module,exports){
 var rng = require('./lib/rng');
 var bytesToUuid = require('./lib/bytesToUuid');
 
 function v4(options, buf, offset) {
   var i = buf && offset || 0;
 
   if (typeof(options) == 'string') {
-    buf = options == 'binary' ? new Array(16) : null;
+    buf = options === 'binary' ? new Array(16) : null;
     options = null;
   }
   options = options || {};
 
   var rnds = options.random || (options.rng || rng)();
 
   // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
   rnds[6] = (rnds[6] & 0x0f) | 0x40;
@@ -286,18 +297,16 @@ module.exports = v4;
 },{"./lib/bytesToUuid":3,"./lib/rng":4}],7:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = exports.SUPPORTED_PROTOCOL_VERSION = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _desc, _value, _class;
 
 var _utils = require("./utils");
 
 var _http = require("./http");
 
 var _http2 = _interopRequireDefault(_http);
 
@@ -355,18 +364,18 @@ function _applyDecoratedDescriptor(targe
 const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
 
 /**
  * High level HTTP client for the Kinto API.
  *
  * @example
  * const client = new KintoClient("https://kinto.dev.mozaws.net/v1");
  * client.bucket("default")
-*    .collection("my-blog")
-*    .createRecord({title: "First article"})
+ *    .collection("my-blog")
+ *    .createRecord({title: "First article"})
  *   .then(console.log.bind(console))
  *   .catch(console.error.bind(console));
  */
 let KintoClientBase = (_dec = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec2 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec3 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec4 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec5 = (0, _utils.nobatch)("Can't use batch within a batch!"), _dec6 = (0, _utils.capable)(["permissions_endpoint"]), _dec7 = (0, _utils.support)("1.4", "2.0"), (_class = class KintoClientBase {
   /**
    * Constructor.
    *
    * @param  {String}       remote  The remote URL.
@@ -500,50 +509,66 @@ let KintoClientBase = (_dec = (0, _utils
       batch: this._isBatch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options),
       retry: this._getRetry(options)
     });
   }
 
   /**
+   * Set client "headers" for every request, updating previous headers (if any).
+   *
+   * @param {Object} headers The headers to merge with existing ones.
+   */
+  setHeaders(headers) {
+    this._headers = {
+      ...this._headers,
+      ...headers
+    };
+    this.serverInfo = null;
+  }
+
+  /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * Note that unlike other options, headers aren't overridden, but
    * merged instead.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Object}
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Retrieves the server's "hello" endpoint. This endpoint reveals
    * server capabilities and settings as well as telling the client
    * "who they are" according to their given authorization headers.
    *
    * @private
@@ -782,29 +807,31 @@ let KintoClientBase = (_dec = (0, _utils
    * @param  {Number}  [options.retry=0]
    *     Number of times to retry each request if the server responds
    *     with Retry-After.
    */
   async paginatedList(path, params, options = {}) {
     // FIXME: this is called even in batch requests, which doesn't
     // make any sense (since all batch requests get a "dummy"
     // response; see execute() above).
-    const { sort, filters, limit, pages, since } = _extends({
-      sort: "-last_modified"
-    }, params);
+    const { sort, filters, limit, pages, since } = {
+      sort: "-last_modified",
+      ...params
+    };
     // Safety/Consistency check on ETag value.
     if (since && typeof since !== "string") {
       throw new Error(`Invalid value for since (${since}), should be ETag value.`);
     }
 
-    const querystring = (0, _utils.qsify)(_extends({}, filters, {
+    const querystring = (0, _utils.qsify)({
+      ...filters,
       _sort: sort,
       _limit: limit,
       _since: since
-    }));
+    });
     let results = [],
         current = 0;
 
     const next = async function (nextPage) {
       if (!nextPage) {
         throw new Error("Pagination exhausted.");
       }
       return processNextPage(nextPage);
@@ -867,17 +894,17 @@ let KintoClientBase = (_dec = (0, _utils
    *     when faced with transient errors.
    * @return {Promise<Object[], Error>}
    */
 
   async listPermissions(options = {}) {
     const path = (0, _endpoint2.default)("permissions");
     // Ensure the default sort parameter is something that exists in permissions
     // entries, as `last_modified` doesn't; here, we pick "id".
-    const paginationOptions = _extends({ sort: "id" }, options);
+    const paginationOptions = { sort: "id", ...options };
     return this.paginatedList(path, paginationOptions, {
       headers: this._getHeaders(options),
       retry: this._getRetry(options)
     });
   }
 
   /**
    * Retrieves the list of buckets.
@@ -935,17 +962,17 @@ let KintoClientBase = (_dec = (0, _utils
    * @return {Promise<Object, Error>}
    */
   async deleteBucket(bucket, options = {}) {
     const bucketObj = (0, _utils.toDataBody)(bucket);
     if (!bucketObj.id) {
       throw new Error("A bucket id is required.");
     }
     const path = (0, _endpoint2.default)("bucket", bucketObj.id);
-    const { last_modified } = _extends({}, bucketObj, options);
+    const { last_modified } = { ...bucketObj, ...options };
     return this.execute(requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     }), { retry: this._getRetry(options) });
   }
 
   /**
@@ -997,17 +1024,17 @@ function aggregate(responses = [], reque
   };
   return responses.reduce((acc, response, index) => {
     const { status } = response;
     const request = requests[index];
     if (status >= 200 && status < 400) {
       acc.published.push(response.body);
     } else if (status === 404) {
       // Extract the id manually from request path while waiting for Kinto/kinto#818
-      const regex = /(buckets|groups|collections|records)\/([^\/]+)$/;
+      const regex = /(buckets|groups|collections|records)\/([^/]+)$/;
       const extracts = request.path.match(regex);
       const id = extracts.length === 3 ? extracts[2] : undefined;
       acc.skipped.push({
         id,
         path: request.path,
         error: response.body
       });
     } else if (status === 412) {
@@ -1031,18 +1058,16 @@ function aggregate(responses = [], reque
 },{}],9:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _desc, _value, _class;
 
 var _utils = require("./utils");
 
 var _collection = require("./collection");
 
 var _collection2 = _interopRequireDefault(_collection);
 
@@ -1126,39 +1151,42 @@ let Bucket = (_dec = (0, _utils.capable)
 
   /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * @private
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Selects a collection.
    *
    * @param  {String}  name              The collection name.
    * @param  {Object}  [options={}]      The options object.
    * @param  {Object}  [options.headers] The headers object option.
@@ -1206,28 +1234,28 @@ let Bucket = (_dec = (0, _utils.capable)
    * @param  {Number}  [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async setData(data, options = {}) {
     if (!(0, _utils.isObject)(data)) {
       throw new Error("A bucket object is required.");
     }
 
-    const bucket = _extends({}, data, { id: this.name });
+    const bucket = { ...data, id: this.name };
 
     // For default bucket, we need to drop the id from the data object.
     // Bug in Kinto < 3.1.1
     const bucketId = bucket.id;
     if (bucket.id === "default") {
       delete bucket.id;
     }
 
     const path = (0, _endpoint2.default)("bucket", bucketId);
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
     const request = requests.updateRequest(path, { data: bucket, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1304,17 +1332,17 @@ let Bucket = (_dec = (0, _utils.capable)
    * @return {Promise<Object, Error>}
    */
   async deleteCollection(collection, options = {}) {
     const collectionObj = (0, _utils.toDataBody)(collection);
     if (!collectionObj.id) {
       throw new Error("A collection id is required.");
     }
     const { id } = collectionObj;
-    const { last_modified } = _extends({}, collectionObj, options);
+    const { last_modified } = { ...collectionObj, ...options };
     const path = (0, _endpoint2.default)("collection", this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1364,20 +1392,21 @@ let Bucket = (_dec = (0, _utils.capable)
    * @param  {Object}            [options.permissions] The permissions object.
    * @param  {Boolean}           [options.safe]        The safe option.
    * @param  {Object}            [options.headers]     The headers object option.
    * @param  {Number}            [options.retry=0]     Number of retries to make
    *     when faced with transient errors.
    * @return {Promise<Object, Error>}
    */
   async createGroup(id, members = [], options = {}) {
-    const data = _extends({}, options.data, {
+    const data = {
+      ...options.data,
       id,
       members
-    });
+    };
     const path = (0, _endpoint2.default)("group", this.name, id);
     const { permissions } = options;
     const request = requests.createRequest(path, { data, permissions }, {
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1398,20 +1427,23 @@ let Bucket = (_dec = (0, _utils.capable)
    */
   async updateGroup(group, options = {}) {
     if (!(0, _utils.isObject)(group)) {
       throw new Error("A group object is required.");
     }
     if (!group.id) {
       throw new Error("A group id is required.");
     }
-    const data = _extends({}, options.data, group);
+    const data = {
+      ...options.data,
+      ...group
+    };
     const path = (0, _endpoint2.default)("group", this.name, group.id);
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
     const request = requests.updateRequest(path, { data, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1426,17 +1458,17 @@ let Bucket = (_dec = (0, _utils.capable)
    *     when faced with transient errors.
    * @param  {Boolean}       [options.safe]          The safe option.
    * @param  {Number}        [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async deleteGroup(group, options = {}) {
     const groupObj = (0, _utils.toDataBody)(group);
     const { id } = groupObj;
-    const { last_modified } = _extends({}, groupObj, options);
+    const { last_modified } = { ...groupObj, ...options };
     const path = (0, _endpoint2.default)("group", this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1565,18 +1597,16 @@ exports.default = Bucket;
 },{"./collection":10,"./endpoint":11,"./requests":14,"./utils":15}],10:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _dec2, _dec3, _desc, _value, _class;
 
 var _uuid = require("uuid");
 
 var _utils = require("./utils");
 
 var _requests = require("./requests");
 
@@ -1659,49 +1689,55 @@ let Collection = (_dec = (0, _utils.capa
 
     /**
      * @ignore
      */
     this._retry = options.retry || 0;
     this._safe = !!options.safe;
     // FIXME: This is kind of ugly; shouldn't the bucket be responsible
     // for doing the merge?
-    this._headers = _extends({}, this.bucket._headers, options.headers);
+    this._headers = {
+      ...this.bucket._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * @private
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Retrieves the total number of records in this collection.
    *
    * @param  {Object} [options={}]      The options object.
    * @param  {Object} [options.headers] The headers object option.
    * @param  {Number} [options.retry=0] Number of retries to make
@@ -1752,17 +1788,17 @@ let Collection = (_dec = (0, _utils.capa
    * @param  {Number}   [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async setData(data, options = {}) {
     if (!(0, _utils.isObject)(data)) {
       throw new Error("A collection object is required.");
     }
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
 
     const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name);
     const request = requests.updateRequest(path, { data, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
@@ -1902,17 +1938,17 @@ let Collection = (_dec = (0, _utils.capa
    * @param  {String}  [options.gzipped]       Force the attachment to be gzipped or not.
    * @return {Promise<Object, Error>}
    */
 
   async addAttachment(dataURI, record = {}, options = {}) {
     const { permissions } = options;
     const id = record.id || _uuid.v4.v4();
     const path = (0, _endpoint2.default)("attachment", this.bucket.name, this.name, id);
-    const { last_modified } = _extends({}, record, options);
+    const { last_modified } = { ...record, ...options };
     const addAttachmentRequest = requests.addAttachmentRequest(path, dataURI, { data: record, permissions }, {
       last_modified,
       filename: options.filename,
       gzipped: options.gzipped,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     await this.client.execute(addAttachmentRequest, {
@@ -1961,17 +1997,17 @@ let Collection = (_dec = (0, _utils.capa
   async updateRecord(record, options = {}) {
     if (!(0, _utils.isObject)(record)) {
       throw new Error("A record object is required.");
     }
     if (!record.id) {
       throw new Error("A record id is required.");
     }
     const { permissions } = options;
-    const { last_modified } = _extends({}, record, options);
+    const { last_modified } = { ...record, ...options };
     const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id);
     const request = requests.updateRequest(path, { data: record, permissions }, {
       headers: this._getHeaders(options),
       safe: this._getSafe(options),
       last_modified,
       patch: !!options.patch
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
@@ -1990,17 +2026,17 @@ let Collection = (_dec = (0, _utils.capa
    * @return {Promise<Object, Error>}
    */
   async deleteRecord(record, options = {}) {
     const recordObj = (0, _utils.toDataBody)(record);
     if (!recordObj.id) {
       throw new Error("A record id is required.");
     }
     const { id } = recordObj;
-    const { last_modified } = _extends({}, recordObj, options);
+    const { last_modified } = { ...recordObj, ...options };
     const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -2095,17 +2131,18 @@ let Collection = (_dec = (0, _utils.capa
       throw new Error("Computing a snapshot is only possible when the full history for a " + "collection is available. Here, the history plugin seems to have " + "been enabled after the creation of the collection.");
     }
     const { data: changes } = await this.bucket.listHistory({
       pages: Infinity, // all pages up to target timestamp are required
       sort: "-target.data.last_modified",
       filters: {
         resource_name: "record",
         collection_id: this.name,
-        "max_target.data.last_modified": String(at) }
+        "max_target.data.last_modified": String(at) // eq. to <=
+      }
     });
     return changes;
   }
 
   /**
    * @private
    */
 
@@ -2113,19 +2150,17 @@ let Collection = (_dec = (0, _utils.capa
     if (!Number.isInteger(at) || at <= 0) {
       throw new Error("Invalid argument, expected a positive integer.");
     }
     // Retrieve history and check it covers the required time range.
     const changes = await this.listChangesBackTo(at);
     // Replay changes to compute the requested snapshot.
     const seenIds = new Set();
     let snapshot = [];
-    for (const _ref of changes) {
-      const { action, target: { data: record } } = _ref;
-
+    for (const { action, target: { data: record } } of changes) {
       if (action == "delete") {
         seenIds.add(record.id); // ensure not reprocessing deleted entries
         snapshot = snapshot.filter(r => r.id !== record.id);
       } else if (!seenIds.has(record.id)) {
         seenIds.add(record.id);
         snapshot.push(record);
       }
     }
@@ -2204,17 +2239,17 @@ function endpoint(name, ...args) {
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 /**
  * Kinto server error code descriptors.
  * @type {Object}
  */
-exports.default = {
+const ERROR_CODES = {
   104: "Missing Authorization Token",
   105: "Invalid Authorization Token",
   106: "Request body was not valid JSON",
   107: "Invalid request parameter",
   108: "Missing request parameter",
   109: "Invalid posted data",
   110: "Invalid Token / id",
   111: "Missing Token / id",
@@ -2226,34 +2261,112 @@ exports.default = {
   117: "Client has sent too many requests",
   121: "Resource access is forbidden for this user",
   122: "Another resource violates constraint",
   201: "Service Temporary unavailable due to high load",
   202: "Service deprecated",
   999: "Internal Server Error"
 };
 
+exports.default = ERROR_CODES;
+let NetworkTimeoutError = class NetworkTimeoutError extends Error {
+  constructor(url, options) {
+    super(`Timeout while trying to access ${url} with ${JSON.stringify(options)}`);
+
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, NetworkTimeoutError);
+    }
+
+    this.url = url;
+    this.options = options;
+  }
+};
+let UnparseableResponseError = class UnparseableResponseError extends Error {
+  constructor(response, body, error) {
+    const { status } = response;
+
+    super(`Response from server unparseable (HTTP ${status || 0}; ${error}): ${body}`);
+
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, UnparseableResponseError);
+    }
+
+    this.status = status;
+    this.response = response;
+    this.stack = error.stack;
+    this.error = error;
+  }
+};
+
+/**
+ * "Error" subclass representing a >=400 response from the server.
+ *
+ * Whether or not this is an error depends on your application.
+ *
+ * The `json` field can be undefined if the server responded with an
+ * empty response body. This shouldn't generally happen. Most "bad"
+ * responses come with a JSON error description, or (if they're
+ * fronted by a CDN or nginx or something) occasionally non-JSON
+ * responses (which become UnparseableResponseErrors, above).
+ */
+
+let ServerResponse = class ServerResponse extends Error {
+  constructor(response, json) {
+    const { status } = response;
+    let { statusText } = response;
+    let errnoMsg;
+
+    if (json) {
+      // Try to fill in information from the JSON error.
+      statusText = json.error || statusText;
+
+      // Take errnoMsg from either ERROR_CODES or json.message.
+      if (json.errno && json.errno in ERROR_CODES) {
+        errnoMsg = ERROR_CODES[json.errno];
+      } else if (json.message) {
+        errnoMsg = json.message;
+      }
+
+      // If we had both ERROR_CODES and json.message, and they differ,
+      // combine them.
+      if (errnoMsg && json.message && json.message !== errnoMsg) {
+        errnoMsg += ` (${json.message})`;
+      }
+    }
+
+    let message = `HTTP ${status} ${statusText}`;
+    if (errnoMsg) {
+      message += `: ${errnoMsg}`;
+    }
+
+    super(message.trim());
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, ServerResponse);
+    }
+
+    this.response = response;
+    this.data = json;
+  }
+};
+exports.NetworkTimeoutError = NetworkTimeoutError;
+exports.ServerResponse = ServerResponse;
+exports.UnparseableResponseError = UnparseableResponseError;
+
 },{}],13:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _utils = require("./utils");
 
 var _errors = require("./errors");
 
-var _errors2 = _interopRequireDefault(_errors);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
 /**
  * Enhanced HTTP client for the Kinto protocol.
  * @private
  */
 let HTTP = class HTTP {
   /**
    * Default HTTP request headers applied to each outgoing request.
    *
@@ -2314,17 +2427,17 @@ let HTTP = class HTTP {
   timedFetch(url, options) {
     let hasTimedout = false;
     return new Promise((resolve, reject) => {
       // Detect if a request has timed out.
       let _timeoutId;
       if (this.timeout) {
         _timeoutId = setTimeout(() => {
           hasTimedout = true;
-          reject(new Error("Request timeout."));
+          reject(new _errors.NetworkTimeoutError(url, options));
         }, this.timeout);
       }
       function proceedWithHandler(fn) {
         return arg => {
           if (!hasTimedout) {
             if (_timeoutId) {
               clearTimeout(_timeoutId);
             }
@@ -2335,62 +2448,39 @@ let HTTP = class HTTP {
       fetch(url, options).then(proceedWithHandler(resolve)).catch(proceedWithHandler(reject));
     });
   }
 
   /**
    * @private
    */
   async processResponse(response) {
-    const { status } = response;
+    const { status, headers } = response;
     const text = await response.text();
     // Check if we have a body; if so parse it as JSON.
-    if (text.length === 0) {
-      return this.formatResponse(response, null);
-    }
-    try {
-      return this.formatResponse(response, JSON.parse(text));
-    } catch (err) {
-      const error = new Error(`HTTP ${status || 0}; ${err}`);
-      error.response = response;
-      error.stack = err.stack;
-      throw error;
+    let json;
+    if (text.length !== 0) {
+      try {
+        json = JSON.parse(text);
+      } catch (err) {
+        throw new _errors.UnparseableResponseError(response, text, err);
+      }
     }
-  }
-
-  /**
-   * @private
-   */
-  formatResponse(response, json) {
-    const { status, statusText, headers } = response;
-    if (json && status >= 400) {
-      let message = `HTTP ${status} ${json.error || ""}: `;
-      if (json.errno && json.errno in _errors2.default) {
-        const errnoMsg = _errors2.default[json.errno];
-        message += errnoMsg;
-        if (json.message && json.message !== errnoMsg) {
-          message += ` (${json.message})`;
-        }
-      } else {
-        message += statusText || "";
-      }
-      const error = new Error(message.trim());
-      error.response = response;
-      error.data = json;
-      throw error;
+    if (status >= 400) {
+      throw new _errors.ServerResponse(response, json);
     }
     return { status, json, headers };
   }
 
   /**
    * @private
    */
   async retry(url, retryAfter, request, options) {
     await (0, _utils.delay)(retryAfter);
-    return this.request(url, request, _extends({}, options, { retry: options.retry - 1 }));
+    return this.request(url, request, { ...options, retry: options.retry - 1 });
   }
 
   /**
    * Performs an HTTP request to the Kinto server.
    *
    * Resolves with an objet containing the following HTTP response properties:
    * - `{Number}  status`  The HTTP status code.
    * - `{Object}  json`    The JSON response body.
@@ -2402,17 +2492,17 @@ let HTTP = class HTTP {
    * @param  {Object} [request.headers] The request headers object (default: {})
    * @param  {Object} [options={}]      Options for making the
    *     request
    * @param  {Number} [options.retry]   Number of retries (default: 0)
    * @return {Promise}
    */
   async request(url, request = { headers: {} }, options = { retry: 0 }) {
     // Ensure default request headers are always set
-    request.headers = _extends({}, HTTP.DEFAULT_REQUEST_HEADERS, request.headers);
+    request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers };
     // If a multipart body is provided, remove any custom Content-Type header as
     // the fetch() implementation will add the correct one for us.
     if (request.body && typeof request.body.append === "function") {
       delete request.headers["Content-Type"];
     }
     request.mode = this.requestMode;
 
     const response = await this.timedFetch(url, request);
@@ -2472,19 +2562,16 @@ let HTTP = class HTTP {
 exports.default = HTTP;
 
 },{"./errors":12,"./utils":15}],14:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 exports.createRequest = createRequest;
 exports.updateRequest = updateRequest;
 exports.jsonPatchPermissionsRequest = jsonPatchPermissionsRequest;
 exports.deleteRequest = deleteRequest;
 exports.addAttachmentRequest = addAttachmentRequest;
 
 var _utils = require("./utils");
 
@@ -2509,115 +2596,120 @@ function safeHeader(safe, last_modified)
   }
   return { "If-None-Match": "*" };
 }
 
 /**
  * @private
  */
 function createRequest(path, { data, permissions }, options = {}) {
-  const { headers, safe } = _extends({}, requestDefaults, options);
+  const { headers, safe } = {
+    ...requestDefaults,
+    ...options
+  };
   return {
     method: data && data.id ? "PUT" : "POST",
     path,
-    headers: _extends({}, headers, safeHeader(safe)),
+    headers: { ...headers, ...safeHeader(safe) },
     body: { data, permissions }
   };
 }
 
 /**
  * @private
  */
 function updateRequest(path, { data, permissions }, options = {}) {
-  const { headers, safe, patch } = _extends({}, requestDefaults, options);
-  const { last_modified } = _extends({}, data, options);
+  const { headers, safe, patch } = { ...requestDefaults, ...options };
+  const { last_modified } = { ...data, ...options };
 
   if (Object.keys((0, _utils.omit)(data, "id", "last_modified")).length === 0) {
     data = undefined;
   }
 
   return {
     method: patch ? "PATCH" : "PUT",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified)),
+    headers: { ...headers, ...safeHeader(safe, last_modified) },
     body: { data, permissions }
   };
 }
 
 /**
  * @private
  */
 function jsonPatchPermissionsRequest(path, permissions, opType, options = {}) {
-  const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+  const { headers, safe, last_modified } = { ...requestDefaults, ...options };
 
   const ops = [];
 
   for (const [type, principals] of Object.entries(permissions)) {
     for (const principal of principals) {
       ops.push({
         op: opType,
         path: `/permissions/${type}/${principal}`
       });
     }
   }
 
   return {
     method: "PATCH",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified), {
+    headers: {
+      ...headers,
+      ...safeHeader(safe, last_modified),
       "Content-Type": "application/json-patch+json"
-    }),
+    },
     body: ops
   };
 }
 
 /**
  * @private
  */
 function deleteRequest(path, options = {}) {
-  const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+  const { headers, safe, last_modified } = {
+    ...requestDefaults,
+    ...options
+  };
   if (safe && !last_modified) {
     throw new Error("Safe concurrency check requires a last_modified value.");
   }
   return {
     method: "DELETE",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified))
+    headers: { ...headers, ...safeHeader(safe, last_modified) }
   };
 }
 
 /**
  * @private
  */
 function addAttachmentRequest(path, dataURI, { data, permissions } = {}, options = {}) {
-  const { headers, safe, gzipped } = _extends({}, requestDefaults, options);
-  const { last_modified } = _extends({}, data, options);
+  const { headers, safe, gzipped } = { ...requestDefaults, ...options };
+  const { last_modified } = { ...data, ...options };
 
   const body = { data, permissions };
   const formData = (0, _utils.createFormData)(dataURI, body, options);
 
   let customPath = gzipped != null ? customPath = path + "?gzipped=" + (gzipped ? "true" : "false") : path;
 
   return {
     method: "POST",
     path: customPath,
-    headers: _extends({}, headers, safeHeader(safe, last_modified)),
+    headers: { ...headers, ...safeHeader(safe, last_modified) },
     body: formData
   };
 }
 
 },{"./utils":15}],15:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 exports.partition = partition;
 exports.delay = delay;
 exports.pMap = pMap;
 exports.omit = omit;
 exports.toDataBody = toDataBody;
 exports.qsify = qsify;
 exports.checkVersion = checkVersion;
 exports.support = support;
@@ -2868,19 +2960,19 @@ function parseDataURL(dataURL) {
   if (!match) {
     throw new Error(`Invalid data-url: ${String(dataURL).substr(0, 32)}...`);
   }
   const props = match[1];
   const base64 = match[2];
   const [type, ...rawParams] = props.split(";");
   const params = rawParams.reduce((acc, param) => {
     const [key, value] = param.split("=");
-    return _extends({}, acc, { [key]: value });
+    return { ...acc, [key]: value };
   }, {});
-  return _extends({}, params, { type, base64 });
+  return { ...params, type, base64 };
 }
 
 /**
  * Extracts file information from a data url.
  * @param  {String} dataURL The data url.
  * @return {Object}
  */
 function extractFileInfo(dataURL) {