Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 15 Apr 2016 15:39:25 -0700
changeset 331374 d9e341cca8e53923f8f3a45845753f7d3fd0ef50
parent 331373 89e7caeaad61edb3ff63eaafc7fd2a1cfa1fffcb (current diff)
parent 331323 f5a97eb5c89a2b5133ff8bba915acba6344fa7cc (diff)
child 331375 223a5febb34f8dfdb326d15e4736c82d51a43811
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.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
Merge m-c to inbound, a=merge MozReview-Commit-ID: 2zwhun4JqPs
js/src/jit-test/tests/auto-regress/bug1263558.js
js/src/tests/js1_6/Array/generics.js
js/src/tests/js1_6/String/generics.js
mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java
mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestTabsRecord.java
mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java
services/common/moz-kinto-client.js
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -19,16 +19,18 @@ Cu.import("resource://gre/modules/Task.j
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
   "resource:///modules/ContentLinkHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+  "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
   "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
@@ -56,20 +58,23 @@ addMessageListener("ContextMenu:DoCustom
     () => PageMenuChild.executeMenu(message.data.generatedItemId));
 });
 
 addMessageListener("RemoteLogins:fillForm", function(message) {
   LoginManagerContent.receiveMessage(message, content);
 });
 addEventListener("DOMFormHasPassword", function(event) {
   LoginManagerContent.onDOMFormHasPassword(event, content);
-  InsecurePasswordUtils.checkForInsecurePasswords(event.target);
+  let formLike = FormLikeFactory.createFromForm(event.target);
+  InsecurePasswordUtils.checkForInsecurePasswords(formLike);
 });
 addEventListener("DOMInputPasswordAdded", function(event) {
   LoginManagerContent.onDOMInputPasswordAdded(event, content);
+  let formLike = FormLikeFactory.createFromField(event.target);
+  InsecurePasswordUtils.checkForInsecurePasswords(formLike);
 });
 addEventListener("pageshow", function(event) {
   LoginManagerContent.onPageShow(event, content);
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -10,18 +10,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
                                   "resource:///modules/DownloadsCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/Console.jsm");
@@ -53,42 +51,45 @@ Sanitizer.prototype = {
   },
 
   /**
    * Deletes privacy sensitive data in a batch, according to user preferences.
    * Returns a promise which is resolved if no errors occurred.  If an error
    * occurs, a message is reported to the console and all other items are still
    * cleared before the promise is finally rejected.
    *
-   * If the consumer specifies the (optional) array parameter, only those
-   * items get cleared (irrespective of the preference settings)
+   * @param [optional] itemsToClear
+   *        Array of items to be cleared. if specified only those
+   *        items get cleared, irrespectively of the preference settings.
+   * @param [optional] options
+   *        Object whose properties are options for this sanitization.
+   *        TODO (bug 1167238) document options here.
    */
-  sanitize: Task.async(function*(aItemsToClear = null) {
-    let progress = {};
-    let promise = this._sanitize(aItemsToClear, progress);
+  sanitize: Task.async(function*(itemsToClear = null, options = {}) {
+    let progress = options.progress || {};
+    let promise = this._sanitize(itemsToClear, progress);
 
-    //
     // Depending on preferences, the sanitizer may perform asynchronous
     // work before it starts cleaning up the Places database (e.g. closing
     // windows). We need to make sure that the connection to that database
     // hasn't been closed by the time we use it.
-    //
-    let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
-       .getService(Ci.nsPIPlacesDatabase)
-       .shutdownClient
-       .jsclient;
+    // Though, if this is a sanitize on shutdown, we already have a blocker.
+    if (!progress.isShutdown) {
+      let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+                             .getService(Ci.nsPIPlacesDatabase)
+                             .shutdownClient
+                             .jsclient;
+      shutdownClient.addBlocker("sanitize.js: Sanitize",
+        promise,
+        {
+          fetchState: () => ({ progress })
+        }
+      );
+    }
 
-    shutdownClient.addBlocker("sanitize.js: Sanitize",
-      promise,
-      {
-        fetchState: () => {
-          return { progress };
-        }
-      }
-    );
     try {
       yield promise;
     } finally {
       Services.obs.notifyObservers(null, "sanitizer-sanitization-complete", "");
     }
   }),
 
   _sanitize: Task.async(function*(aItemsToClear, progress = {}) {
@@ -828,56 +829,54 @@ Sanitizer.onStartup = Task.async(functio
   if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
     // Reset the pref, so that if we crash before having a chance to
     // sanitize on shutdown, we will do at the next startup.
     // Flushing prefs has a cost, so do this only if necessary.
     Preferences.reset(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
     Services.prefs.savePrefFile(null);
   }
 
-  // Make sure that we are triggered during shutdown, at the right time,
-  // and only once.
-  let placesClient = Cc["@mozilla.org/browser/nav-history-service;1"]
-                       .getService(Ci.nsPIPlacesDatabase)
-                       .shutdownClient
-                       .jsclient;
-
-  let deferredSanitization = PromiseUtils.defer();
-  let sanitizationInProgress = false;
-  let doSanitize = function() {
-    if (!sanitizationInProgress) {
-      sanitizationInProgress = true;
-      Sanitizer.onShutdown().catch(er => {Promise.reject(er) /* Do not return rejected promise */;}).then(() =>
-        deferredSanitization.resolve()
-      );
+  // Make sure that we are triggered during shutdown.
+  let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+                         .getService(Ci.nsPIPlacesDatabase)
+                         .shutdownClient
+                         .jsclient;
+  // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
+  // that tracks the status of the shutdown blocker. This `progress` object
+  // will be updated during sanitization and reported with the crash in case of
+  // a shutdown timeout.
+  // We use the `options` argument to pass the `progress` object to sanitize().
+  let progress = { isShutdown: true };
+  shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+    () => sanitizeOnShutdown({ progress }),
+    {
+      fetchState: () => ({ progress })
     }
-    return deferredSanitization.promise;
-  }
-  placesClient.addBlocker("sanitize.js: Sanitize on shutdown", doSanitize);
+  );
 
   // Check if Firefox crashed during a sanitization.
   let lastInterruptedSanitization = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
   if (lastInterruptedSanitization) {
     let s = new Sanitizer();
     // If the json is invalid this will just throw and reject the Task.
     let itemsToClear = JSON.parse(lastInterruptedSanitization);
     yield s.sanitize(itemsToClear);
   } else if (shutownSanitizationWasInterrupted) {
     // Otherwise, could be we were supposed to sanitize on shutdown but we
     // didn't have a chance, due to an earlier crash.
     // In such a case, just redo a shutdown sanitize now, during startup.
-    yield Sanitizer.onShutdown();
+    yield sanitizeOnShutdown();
   }
 });
 
-Sanitizer.onShutdown = Task.async(function*() {
+var sanitizeOnShutdown = Task.async(function*(options = {}) {
   if (!Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
     return;
   }
   // Need to sanitize upon shutdown
   let s = new Sanitizer();
   s.prefDomain = "privacy.clearOnShutdown.";
-  yield s.sanitize();
+  yield s.sanitize(null, options);
   // We didn't crash during shutdown sanitization, so annotate it to avoid
   // sanitizing again on startup.
   Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
   Services.prefs.savePrefFile(null);
 });
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1355,27 +1355,51 @@ file, You can obtain one at http://mozil
           var rect = window.document.documentElement.getBoundingClientRect();
           var width = rect.right - rect.left;
           this.setAttribute("width", width);
 
           // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
           var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
           this.style.direction = popupDirection;
 
-          // Make the popup's starting margin negaxtive so that the start of the
-          // popup aligns with the window border.
+          // Make the popup's starting margin negative so that the leading edge
+          // of the popup aligns with the window border.
           let elementRect = aElement.getBoundingClientRect();
           if (popupDirection == "rtl") {
             let offset = elementRect.right - rect.right
             this.style.marginRight = offset + "px";
           } else {
             let offset = rect.left - elementRect.left;
             this.style.marginLeft = offset + "px";
           }
 
+          // Keep the popup items' site icons aligned with the urlbar's identity
+          // icon if it's not too far from the edge of the window.  If there are
+          // at most two toolbar buttons between the window edge and the urlbar,
+          // then consider that as "not too far."  The forward button's
+          // visibility may have changed since the last time the popup was
+          // opened, so this needs to happen now.  Do it *before* the popup
+          // opens because otherwise the items will visibly shift.
+          let nodes = [...document.getElementById("nav-bar-customization-target").childNodes];
+          let urlbarPosition = nodes.findIndex(n => n.id == "urlbar-container");
+          let alignSiteIcons = urlbarPosition <= 2 &&
+                               nodes.slice(0, urlbarPosition)
+                                    .every(n => n.localName == "toolbarbutton");
+          if (alignSiteIcons) {
+            let identityRect =
+              document.getElementById("identity-icon").getBoundingClientRect();
+            this.siteIconStart = popupDirection == "rtl" ? identityRect.right
+                                                         : identityRect.left;
+          }
+          else {
+            // Reset the alignment so that the site icons are positioned
+            // according to whatever's in the CSS.
+            this.siteIconStart = undefined;
+          }
+
           // Position the popup below the navbar.  To get the y-coordinate,
           // which is an offset from the bottom of the input, subtract the
           // bottom of the navbar from the buttom of the input.
           let yOffset =
             document.getElementById("nav-bar").getBoundingClientRect().bottom -
             aInput.getBoundingClientRect().bottom;
           this.openPopup(aElement, "after_start", 0, yOffset, false, false);
         ]]></body>
@@ -2523,18 +2547,16 @@ file, You can obtain one at http://mozil
         ]]></setter>
       </property>
       <property name="_notificationType">
         <getter><![CDATA[
           // Use the popupid attribute to identify the notification type,
           // otherwise just rely on the panel id for common arrowpanels.
           let type = this._panel.firstChild.getAttribute("popupid") ||
                      this._panel.id;
-          if (type == "password")
-            return "passwords";
           if (type == "editBookmarkPanel")
             return "bookmarks";
           if (type == "addon-install-complete" || type == "addon-install-restart") {
             if (!Services.prefs.prefHasUserValue("services.sync.username"))
               return "addons";
             if (!Services.prefs.getBoolPref("services.sync.engine.addons"))
               return "addons-sync-disabled";
           }
@@ -2581,27 +2603,16 @@ file, You can obtain one at http://mozil
 
           // Only handle supported notification panels.
           if (!this._notificationType) {
             return;
           }
 
           let viewsLeft = this._viewsLeft;
           if (viewsLeft) {
-            let notification = this._panel.firstElementChild.notification;
-            if (this._notificationType == "passwords" && notification && notification.options &&
-                notification.options.displayURI instanceof Ci.nsIStandardURL) {
-              let fxAOrigin = new URL(Services.prefs.getCharPref("identity.fxaccounts.remote.signup.uri")).origin
-              if (notification.options.displayURI.prePath == fxAOrigin) {
-                // Somewhat gross hack - we don't want to show the sync promo while
-                // the user may be logging into Sync.
-                return;
-              }
-            }
-
             if (Services.prefs.prefHasUserValue("services.sync.username") &&
                this._notificationType != "addons-sync-disabled") {
               // If the user has already setup Sync, don't show the notification.
               this._viewsLeft = 0;
               // Be sure to hide the panel, in case it was visible and the user
               // decided to setup Sync after noticing it.
               viewsLeft = 0;
               // The panel is still hidden, just bail out.
--- a/browser/components/syncedtabs/SyncedTabsListStore.js
+++ b/browser/components/syncedtabs/SyncedTabsListStore.js
@@ -146,17 +146,17 @@ Object.assign(SyncedTabsListStore.protot
 
   // Selects a row and makes sure the selection is within bounds
   selectRow(parent, child) {
     let maxParentRow = this.filter ? this._tabCount() : this.data.length;
     let parentRow = parent;
     if (parent <= -1) {
       parentRow = 0;
     } else if (parent >= maxParentRow) {
-      parentRow = maxParentRow - 1;
+      return;
     }
 
     let childRow = child;
     if (parentRow === -1 || this.filter || typeof child === "undefined" || child < -1) {
       childRow = -1;
     } else if (child >= this.data[parentRow].tabs.length) {
       childRow = this.data[parentRow].tabs.length - 1;
     }
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -90,16 +90,19 @@ TabListView.prototype = {
     this._clearChilden(this.list);
     for (let client of state.clients) {
       if (state.filter) {
         this._renderFilteredClient(client);
       } else {
         this._renderClient(client);
       }
     }
+    if (this.list.firstChild) {
+      this.list.firstChild.querySelector(".item.tab:first-child .item-title").setAttribute("tabindex", 2);
+    }
   },
 
   destroy() {
     this._teardownContextMenu();
     this.container.remove();
   },
 
   _update(state) {
--- a/browser/components/syncedtabs/sidebar.xhtml
+++ b/browser/components/syncedtabs/sidebar.xhtml
@@ -61,17 +61,17 @@
           <div class="item-icon-container"></div>
           <p class="item-title"></p>
         </div>
       </div>
     </template>
 
     <template id="tabs-container-template">
       <div class="tabs-container">
-        <div class="list" role="listbox" tabindex="1"></div>
+        <div class="list" role="listbox"></div>
       </div>
     </template>
 
     <template id="deck-template">
       <div class="deck">
         <div class="tabs-fetching sync-state">
           <!-- Show intentionally blank panel, see bug 1239845 -->
         </div>
@@ -92,23 +92,23 @@
     </template>
 
     <div class="content-container">
       <!-- the non-scrollable header -->
       <div class="content-header">
         <div class="sidebar-search-container tabs-container sync-state">
           <div class="search-box compact">
             <div class="textbox-input-box">
-              <input type="text" class="tabsFilter textbox-input"/>
+              <input type="text" class="tabsFilter textbox-input" tabindex="1"/>
               <div class="textbox-search-icons">
                 <a class="textbox-search-clear"></a>
                 <a class="textbox-search-icon"></a>
               </div>
             </div>
           </div>
         </div>
       </div>
       <!-- the scrollable content area where our templates are inserted -->
-      <div id="template-container" class="content-scrollable">
+      <div id="template-container" class="content-scrollable" tabindex="-1">
       </div>
     </div>
   </body>
 </html>
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -440,20 +440,16 @@ safeModeRestartButton=Restart
 # up in the Firefox button. If users frequently use the "Text Encoding"
 # menu, set this to "true". Otherwise, you can leave it as "false".
 browser.menu.showCharacterEncoding=false
 
 # LOCALIZATION NOTE (syncPromoNotification.bookmarks.label): This appears in
 # the add bookmark star panel.  %S will be replaced by syncBrandShortName.
 # The final space separates this text from the Learn More link.
 syncPromoNotification.bookmarks.description=You can access your bookmarks on all your devices with %S.\u0020
-# LOCALIZATION NOTE (syncPromoNotification.passwords.label): This appears in
-# the remember password panel.  %S will be replaced by syncBrandShortName.
-# The final space separates this text from the Learn More link.
-syncPromoNotification.passwords.description=You can access your passwords on all your devices with %S.\u0020
 syncPromoNotification.learnMoreLinkText=Learn More
 # LOCALIZATION NOTE (syncPromoNotification.addons.label): This appears in
 # the add-on install complete panel when Sync isn't set.
 # %S will be replaced by syncBrandShortName.
 # The final space separates this text from the Learn More link.
 syncPromoNotification.addons.description=You can access your add-ons on all your devices with %S.\u0020
 # LOCALIZATION NOTE (syncPromoNotification.addons-sync-disabled.label):
 # This appears in the add-on install complete panel when Sync is set
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1161,21 +1161,16 @@ toolbar[brighttext] .toolbarbutton-1 > .
       transition-property: border-color, box-shadow;
       transition-duration: .1s;
     }
 
     #urlbar:not(:-moz-lwtheme)[focused],
     .searchbar-textbox:not(:-moz-lwtheme)[focused] {
       box-shadow: 0 0 0 1px Highlight inset;
     }
-
-    /* overlap the urlbar's border and inset box-shadow */
-    #PopupAutoCompleteRichResult:not(:-moz-lwtheme) {
-      margin-top: -2px;
-    }
   }
 
   @media not all and (-moz-os-version: windows-xp) {
     #urlbar:not(:-moz-lwtheme)[focused],
     .searchbar-textbox:not(:-moz-lwtheme)[focused] {
       border-color: Highlight;
     }
   }
--- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_appear_on_panel_init.js
@@ -5,22 +5,16 @@
 "use strict";
 
 // Test that player widgets are displayed right when the animation panel is
 // initialized, if the selected node (<body> by default) is animated.
 
 const { ANIMATION_TYPES } = require("devtools/server/actors/animation");
 
 add_task(function* () {
-  yield new Promise(resolve => {
-    SpecialPowers.pushPrefEnv({"set": [
-      ["dom.animations-api.core.enabled", true]
-    ]}, resolve);
-  });
-
   yield addTab(URL_ROOT + "doc_multiple_animation_types.html");
 
   let {panel} = yield openAnimationInspector();
   is(panel.animationsTimelineComponent.animations.length, 3,
     "Three animations are handled by the timeline after init");
   assertAnimationsDisplayed(panel, 3,
     "Three animations are displayed after init");
   is(
--- a/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -7,22 +7,16 @@
 // Check that if an animation has had its playbackRate changed via the DOM, then
 // the timeline UI shows the right delay and duration.
 // Indeed, the header in the timeline UI always shows the unaltered time,
 // because there might be multiple animations displayed at the same time, some
 // of which may have a different rate than others. Those that have had their
 // rate changed have a delay = delay/rate and a duration = duration/rate.
 
 add_task(function* () {
-  yield new Promise(resolve => {
-    SpecialPowers.pushPrefEnv({"set": [
-      ["dom.animations-api.core.enabled", true]
-    ]}, resolve);
-  });
-
   yield addTab(URL_ROOT + "doc_modify_playbackRate.html");
 
   let {panel} = yield openAnimationInspector();
 
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -26,24 +26,34 @@ registerCleanupFunction(function* () {
 });
 
 // Clean-up all prefs that might have been changed during a test run
 // (safer here because if the test fails, then the pref is never reverted)
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.debugger.log");
 });
 
+// WebAnimations API is not enabled by default in all release channels yet, see
+// Bug 1264101.
+function enableWebAnimationsAPI() {
+  return new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": [
+      ["dom.animations-api.core.enabled", true]
+    ]}, resolve);
+  });
+}
+
 /**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
  * @return a promise that resolves to the tab object when the url is loaded
  */
 var _addTab = addTab;
 addTab = function(url) {
-  return _addTab(url).then(tab => {
+  return enableWebAnimationsAPI().then(() => _addTab(url)).then(tab => {
     let browser = tab.linkedBrowser;
     info("Loading the helper frame script " + FRAME_SCRIPT_URL);
     browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
     info("Loading the helper frame script " + COMMON_FRAME_SCRIPT_URL);
     browser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
     return tab;
   });
 };
--- a/devtools/client/framework/toolbox-options.xhtml
+++ b/devtools/client/framework/toolbox-options.xhtml
@@ -40,17 +40,17 @@
                 data-pref="devtools.theme">
         <legend>&options.selectDevToolsTheme.label2;</legend>
       </fieldset>
 
       <fieldset id="commonprefs-options" class="options-groupbox">
         <legend>&options.commonPrefs.label;</legend>
         <label title="&options.enablePersistentLogs.tooltip;">
           <input type="checkbox" data-pref="devtools.webconsole.persistlog" />
-          &options.enablePersistentLogs.label;
+          <span>&options.enablePersistentLogs.label;</span>
         </label>
       </fieldset>
 
       <fieldset id="inspector-options" class="options-groupbox">
         <legend>&options.context.inspector;</legend>
         <label title="&options.showUserAgentStyles.tooltip;">
           <input type="checkbox"
                  data-pref="devtools.inspector.showUserAgentStyles"/>
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -1047,27 +1047,27 @@ InspectorPanel.prototype = {
       button.setAttribute("tooltiptext", strings.GetStringFromName("inspector.collapsePane"));
     }
   },
 
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
-  addNode: Task.async(function*() {
-    let root = this.selection.nodeFront;
-    if (!this.canAddHTMLChild(root)) {
+  addNode: Task.async(function* () {
+    if (!this.canAddHTMLChild()) {
       return;
     }
 
     let html = "<div></div>";
 
     // Insert the html and expect a childList markup mutation.
     let onMutations = this.once("markupmutation");
-    let {nodes} = yield this.walker.insertAdjacentHTML(root, "beforeEnd", html);
+    let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront,
+                                                       "beforeEnd", html);
     yield onMutations;
 
     // Select the new node (this will auto-expand its parent).
     this.selection.setNodeFront(nodes[0]);
   }),
 
   /**
    * Toggle a pseudo class.
--- a/devtools/client/performance/views/details-waterfall.js
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -77,16 +77,19 @@ var WaterfallView = Heritage.extend(Deta
   /**
    * Method for handling all the set up for rendering a new waterfall.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
   render: function(interval={}) {
     let recording = PerformanceController.getCurrentRecording();
+    if (recording.isRecording()) {
+      return;
+    }
     let startTime = interval.startTime || 0;
     let endTime = interval.endTime || recording.getDuration();
     let markers = recording.getMarkers();
     let rootMarkerNode = this._prepareWaterfallTree(markers);
 
     this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
     this.emit(EVENTS.UI_WATERFALL_RENDERED);
   },
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -21,17 +21,16 @@ this.EXPORTED_SYMBOLS = ["DevToolsLoader
 
 /**
  * Providers are different strategies for loading the devtools.
  */
 
 var loaderModules = {
   "Services": Object.create(Services),
   "toolkit/loader": Loader,
-  promise,
   PromiseDebugging,
   ChromeUtils,
   ThreadSafeChromeUtils,
   HeapSnapshot,
 };
 XPCOMUtils.defineLazyGetter(loaderModules, "Debugger", () => {
   // addDebuggerToGlobal only allows adding the Debugger object to a global. The
   // this object is not guaranteed to be a global (in particular on B2G, due to
@@ -77,46 +76,68 @@ XPCOMUtils.defineLazyGetter(loaderModule
 
 XPCOMUtils.defineLazyGetter(loaderModules, "URL", () => {
   let sandbox
     = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(),
                  {wantGlobalProperties: ["URL"]});
   return sandbox.URL;
 });
 
+const loaderPaths = {
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "": "resource://gre/modules/commonjs/",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "devtools": "resource://devtools",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "gcli": "resource://devtools/shared/gcli/source/lib/gcli",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "acorn": "resource://devtools/acorn",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "acorn/util/walk": "resource://devtools/acorn/walk.js",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  "source-map": "resource://devtools/shared/sourcemap/source-map.js",
+  // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+  // Allow access to xpcshell test items from the loader.
+  "xpcshell-test": "resource://test",
+};
+
 var sharedGlobalBlocklist = ["sdk/indexed-db"];
 
 /**
  * Used when the tools should be loaded from the Firefox package itself.
  * This is the default case.
  */
 function BuiltinProvider() {}
 BuiltinProvider.prototype = {
   load: function() {
+    // Copy generic paths and modules for this loader instance
+    let paths = {};
+    for (let path in loaderPaths) {
+      paths[path] = loaderPaths[path];
+    }
+    let modules = {};
+    for (let name in loaderModules) {
+      XPCOMUtils.defineLazyGetter(modules, name, (function (name) {
+        return loaderModules[name];
+      }).bind(null, name));
+    }
+    // When creating a Loader invisible to the Debugger, we have to ensure
+    // using only modules and not depend on any JSM. As everything that is
+    // not loaded with Loader isn't going to respect `invisibleToDebugger`.
+    // But we have to keep using Promise.jsm for other loader to prevent
+    // breaking unhandled promise rejection in tests.
+    if (this.invisibleToDebugger) {
+      paths["promise"] = "resource://gre/modules/Promise-backend.js";
+    } else {
+      modules["promise"] = promise;
+    }
     this.loader = new Loader.Loader({
       id: "fx-devtools",
-      modules: loaderModules,
-      paths: {
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "": "resource://gre/modules/commonjs/",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "devtools": "resource://devtools",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "gcli": "resource://devtools/shared/gcli/source/lib/gcli",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "acorn": "resource://devtools/acorn",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "acorn/util/walk": "resource://devtools/acorn/walk.js",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        "source-map": "resource://devtools/shared/sourcemap/source-map.js",
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-        // Allow access to xpcshell test items from the loader.
-        "xpcshell-test": "resource://test"
-        // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
-      },
+      modules,
+      paths,
       globals: this.globals,
       invisibleToDebugger: this.invisibleToDebugger,
       sharedGlobal: true,
       sharedGlobalBlocklist,
     });
 
     return promise.resolve(undefined);
   },
--- a/devtools/shared/tests/unit/test_invisible_loader.js
+++ b/devtools/shared/tests/unit/test_invisible_loader.js
@@ -22,25 +22,37 @@ function visible_loader() {
   let sandbox = loader._provider.loader.sharedGlobalSandbox;
 
   try {
     dbg.addDebuggee(sandbox);
     do_check_true(true);
   } catch(e) {
     do_throw("debugger could not add visible value");
   }
+
+  // Check that for common loader used for tabs, promise modules is Promise.jsm
+  // Which is required to support unhandled promises rejection in mochitests
+  const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+  do_check_eq(loader.require("promise"), promise);
 }
 
 function invisible_loader() {
   let loader = new DevToolsLoader();
   loader.invisibleToDebugger = true;
   loader.require("devtools/shared/css-color");
 
   let dbg = new Debugger();
   let sandbox = loader._provider.loader.sharedGlobalSandbox;
 
   try {
     dbg.addDebuggee(sandbox);
     do_throw("debugger added invisible value");
   } catch(e) {
     do_check_true(true);
   }
+
+  // But for browser toolbox loader, promise is loaded as a regular modules out
+  // of Promise-backend.js, that to be invisible to the debugger and not step
+  // into it.
+  const promise = loader.require("promise");
+  const promiseModule = loader._provider.loader.modules["resource://gre/modules/Promise-backend.js"];
+  do_check_eq(promise, promiseModule.exports);
 }
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -1022,65 +1022,8 @@ function ArrayConcat(arg1) {
 }
 
 function ArrayStaticConcat(arr, arg1) {
     if (arguments.length < 1)
         ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.concat');
     var args = callFunction(std_Array_slice, arguments, 1);
     return callFunction(std_Function_apply, ArrayConcat, arr, args);
 }
-
-function ArrayStaticJoin(arr, separator) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.join');
-    return callFunction(std_Array_join, arr, separator);
-}
-
-function ArrayStaticReverse(arr) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.reverse');
-    return callFunction(std_Array_reverse, arr);
-}
-
-function ArrayStaticSort(arr, comparefn) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.sort');
-    return callFunction(std_Array_sort, arr, comparefn);
-}
-
-function ArrayStaticPush(arr, arg1) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.push');
-    var args = callFunction(std_Array_slice, arguments, 1);
-    return callFunction(std_Function_apply, std_Array_push, arr, args);
-}
-
-function ArrayStaticPop(arr) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.pop');
-    return callFunction(std_Array_pop, arr);
-}
-
-function ArrayStaticShift(arr) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.shift');
-    return callFunction(std_Array_shift, arr);
-}
-
-function ArrayStaticUnshift(arr, arg1) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.unshift');
-    var args = callFunction(std_Array_slice, arguments, 1);
-    return callFunction(std_Function_apply, std_Array_unshift, arr, args);
-}
-
-function ArrayStaticSplice(arr, start, deleteCount) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.splice');
-    var args = callFunction(std_Array_slice, arguments, 1);
-    return callFunction(std_Function_apply, std_Array_splice, arr, args);
-}
-
-function ArrayStaticSlice(arr, start, end) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'Array.slice');
-    return callFunction(std_Array_slice, arr, start, end);
-}
--- a/js/src/builtin/String.js
+++ b/js/src/builtin/String.js
@@ -717,21 +717,17 @@ function String_static_raw(callSite, ...
  * Mozilla proprietary.
  * Spec: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String#String_generic_methods
  */
 function String_static_localeCompare(str1, str2) {
     if (arguments.length < 1)
         ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "String.localeCompare");
     var locales = arguments.length > 2 ? arguments[2] : undefined;
     var options = arguments.length > 3 ? arguments[3] : undefined;
-#if EXPOSE_INTL_API
     return callFunction(String_localeCompare, str1, str2, locales, options);
-#else
-    return callFunction(std_String_localeCompare, str1, str2, locales, options);
-#endif
 }
 
 // ES6 draft 2014-04-27 B.2.3.3
 function String_big() {
     RequireObjectCoercible(this);
     return "<big>" + ToString(this) + "</big>";
 }
 
@@ -823,113 +819,8 @@ function String_fontsize(size) {
 }
 
 // ES6 draft 2014-04-27 B.2.3.10
 function String_link(url) {
     RequireObjectCoercible(this);
     var S = ToString(this);
     return '<a href="' + EscapeAttributeValue(url) + '">' + S + "</a>";
 }
-
-function String_static_toLowerCase(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toLowerCase');
-    return callFunction(std_String_toLowerCase, string);
-}
-
-function String_static_toUpperCase(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toUpperCase');
-    return callFunction(std_String_toUpperCase, string);
-}
-
-function String_static_charAt(string, pos) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.charAt');
-    return callFunction(std_String_charAt, string, pos);
-}
-
-function String_static_charCodeAt(string, pos) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.charCodeAt');
-    return callFunction(std_String_charCodeAt, string, pos);
-}
-
-function String_static_includes(string, searchString) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.includes');
-    var position = arguments.length > 2 ? arguments[2] : undefined;
-    return callFunction(std_String_includes, string, searchString, position);
-}
-
-function String_static_indexOf(string, searchString) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.indexOf');
-    var position = arguments.length > 2 ? arguments[2] : undefined;
-    return callFunction(std_String_indexOf, string, searchString, position);
-}
-
-function String_static_lastIndexOf(string, searchString) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.lastIndexOf');
-    var position = arguments.length > 2 ? arguments[2] : undefined;
-    return callFunction(std_String_lastIndexOf, string, searchString, position);
-}
-
-function String_static_startsWith(string, searchString) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.startsWith');
-    var position = arguments.length > 2 ? arguments[2] : undefined;
-    return callFunction(std_String_startsWith, string, searchString, position);
-}
-
-function String_static_endsWith(string, searchString) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.endsWith');
-    var endPosition = arguments.length > 2 ? arguments[2] : undefined;
-    return callFunction(std_String_endsWith, string, searchString, endPosition);
-}
-
-function String_static_trim(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.trim');
-    return callFunction(std_String_trim, string);
-}
-
-function String_static_trimLeft(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.trimLeft');
-    return callFunction(std_String_trimLeft, string);
-}
-
-function String_static_trimRight(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.trimRight');
-    return callFunction(std_String_trimRight, string);
-}
-
-function String_static_toLocaleLowerCase(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toLocaleLowerCase');
-    return callFunction(std_String_toLocaleLowerCase, string);
-}
-
-function String_static_toLocaleUpperCase(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toLocaleUpperCase');
-    return callFunction(std_String_toLocaleUpperCase, string);
-}
-
-#if EXPOSE_INTL_API
-function String_static_normalize(string) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.normalize');
-    var form = arguments.length > 1 ? arguments[1] : undefined;
-    return callFunction(std_String_normalize, string, form);
-}
-#endif
-
-function String_static_concat(string, arg1) {
-    if (arguments.length < 1)
-        ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.concat');
-    var args = callFunction(std_Array_slice, arguments, 1);
-    return callFunction(std_Function_apply, std_String_concat, string, args);
-}
deleted file mode 100644
--- a/js/src/jit-test/tests/auto-regress/bug1263558.js
+++ /dev/null
@@ -1,16 +0,0 @@
-if (!('oomTest' in this))
-    quit();
-
-evalcx(`
-    eval('\
-        var appendToActual = function(s) {};\
-        gczeal = function() {};\
-        gcslice = function() {};\
-        selectforgc = function() {};\
-        if (!("verifyprebarriers" in this)) {\
-            verifyprebarriers = function() {};\
-        }\
-    ');
-    oomTest(() => eval('Array(..."")'));
-    Intl.NumberFormat.prototype.format(0);
-`, newGlobal());
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -832,17 +832,27 @@ class MOZ_STACK_CLASS SourceBufferHolder
 #define JSPROP_INTERNAL_USE_BIT 0x80    /* internal JS engine use only */
 //                             0x100    /* Unused */
 #define JSFUN_STUB_GSOPS       0x200    /* use JS_PropertyStub getter/setter
                                            instead of defaulting to class gsops
                                            for property holding function */
 
 #define JSFUN_CONSTRUCTOR      0x400    /* native that can be called as a ctor */
 
-//                             0x800    /* Unused */
+/*
+ * Specify a generic native prototype methods, i.e., methods of a class
+ * prototype that are exposed as static methods taking an extra leading
+ * argument: the generic |this| parameter.
+ *
+ * If you set this flag in a JSFunctionSpec struct's flags initializer, then
+ * that struct must live at least as long as the native static method object
+ * created due to this flag by JS_DefineFunctions or JS_InitClass.  Typically
+ * JSFunctionSpec structs are allocated in static arrays.
+ */
+#define JSFUN_GENERIC_NATIVE   0x800
 
 #define JSFUN_HAS_REST        0x1000    /* function has ...rest parameter. */
 
 #define JSFUN_FLAGS_MASK      0x1e00    /* | of all the JSFUN_* flags */
 
 /*
  * If set, will allow redefining a non-configurable property, but only on a
  * non-DOM global.  This is a temporary hack that will need to go away in bug
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -1385,18 +1385,18 @@ ArrayReverseDenseKernel(JSContext* cx, H
     }
 
     return DenseElementResult::Success;
 }
 
 DefineBoxedOrUnboxedFunctor3(ArrayReverseDenseKernel,
                              JSContext*, HandleObject, uint32_t);
 
-bool
-js::array_reverse(JSContext* cx, unsigned argc, Value* vp)
+static bool
+array_reverse(JSContext* cx, unsigned argc, Value* vp)
 {
     AutoSPSEntry pseudoFrame(cx->runtime(), "Array.prototype.reverse");
     CallArgs args = CallArgsFromVp(argc, vp);
     RootedObject obj(cx, ToObject(cx, args.thisv()));
     if (!obj)
         return false;
 
     uint32_t len;
@@ -2376,18 +2376,18 @@ CanOptimizeForDenseStorage(HandleObject 
      * other indexed properties on the object.  (Note that non-writable length
      * is subsumed by the initializedLength comparison.)
      */
     return !ObjectMayHaveExtraIndexedProperties(arr) &&
            startingIndex + count <= GetAnyBoxedOrUnboxedInitializedLength(arr);
 }
 
 /* ES 2016 draft Mar 25, 2016 22.1.3.26. */
-bool
-js::array_splice(JSContext* cx, unsigned argc, Value* vp)
+static bool
+array_splice(JSContext* cx, unsigned argc, Value* vp)
 {
     return array_splice_impl(cx, argc, vp, true);
 }
 
 static inline bool
 ArraySpliceCopy(JSContext* cx, HandleObject arr, HandleObject obj,
                 uint32_t actualStart, uint32_t actualDeleteCount)
 {
@@ -3077,36 +3077,38 @@ array_of(JSContext* cx, unsigned argc, V
     if (!SetLengthProperty(cx, obj, args.length()))
         return false;
 
     // Step 11.
     args.rval().setObject(*obj);
     return true;
 }
 
+#define GENERIC JSFUN_GENERIC_NATIVE
+
 static const JSFunctionSpec array_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,      array_toSource,     0,0),
 #endif
     JS_SELF_HOSTED_FN(js_toString_str, "ArrayToString",      0,0),
     JS_FN(js_toLocaleString_str,       array_toLocaleString, 0,0),
 
     /* Perl-ish methods. */
-    JS_INLINABLE_FN("join",     array_join,         1,0, ArrayJoin),
-    JS_FN("reverse",            array_reverse,      0,0),
-    JS_FN("sort",               array_sort,         1,0),
-    JS_INLINABLE_FN("push",     array_push,         1,0, ArrayPush),
-    JS_INLINABLE_FN("pop",      array_pop,          0,0, ArrayPop),
-    JS_INLINABLE_FN("shift",    array_shift,        0,0, ArrayShift),
-    JS_FN("unshift",            array_unshift,      1,0),
-    JS_INLINABLE_FN("splice",   array_splice,       2,0, ArraySplice),
+    JS_INLINABLE_FN("join",     array_join,         1,JSFUN_GENERIC_NATIVE, ArrayJoin),
+    JS_FN("reverse",            array_reverse,      0,JSFUN_GENERIC_NATIVE),
+    JS_FN("sort",               array_sort,         1,JSFUN_GENERIC_NATIVE),
+    JS_INLINABLE_FN("push",     array_push,         1,JSFUN_GENERIC_NATIVE, ArrayPush),
+    JS_INLINABLE_FN("pop",      array_pop,          0,JSFUN_GENERIC_NATIVE, ArrayPop),
+    JS_INLINABLE_FN("shift",    array_shift,        0,JSFUN_GENERIC_NATIVE, ArrayShift),
+    JS_FN("unshift",            array_unshift,      1,JSFUN_GENERIC_NATIVE),
+    JS_INLINABLE_FN("splice",   array_splice,       2,JSFUN_GENERIC_NATIVE, ArraySplice),
 
     /* Pythonic sequence methods. */
     JS_SELF_HOSTED_FN("concat",      "ArrayConcat",      1,0),
-    JS_INLINABLE_FN("slice",    array_slice,        2,0, ArraySlice),
+    JS_INLINABLE_FN("slice",    array_slice,        2,JSFUN_GENERIC_NATIVE, ArraySlice),
 
     JS_SELF_HOSTED_FN("lastIndexOf", "ArrayLastIndexOf", 1,0),
     JS_SELF_HOSTED_FN("indexOf",     "ArrayIndexOf",     1,0),
     JS_SELF_HOSTED_FN("forEach",     "ArrayForEach",     1,0),
     JS_SELF_HOSTED_FN("map",         "ArrayMap",         1,0),
     JS_SELF_HOSTED_FN("filter",      "ArrayFilter",      1,0),
     JS_SELF_HOSTED_FN("reduce",      "ArrayReduce",      1,0),
     JS_SELF_HOSTED_FN("reduceRight", "ArrayReduceRight", 1,0),
@@ -3137,25 +3139,16 @@ static const JSFunctionSpec array_static
     JS_SELF_HOSTED_FN("indexOf",     "ArrayStaticIndexOf", 2,0),
     JS_SELF_HOSTED_FN("forEach",     "ArrayStaticForEach", 2,0),
     JS_SELF_HOSTED_FN("map",         "ArrayStaticMap",   2,0),
     JS_SELF_HOSTED_FN("filter",      "ArrayStaticFilter", 2,0),
     JS_SELF_HOSTED_FN("every",       "ArrayStaticEvery", 2,0),
     JS_SELF_HOSTED_FN("some",        "ArrayStaticSome",  2,0),
     JS_SELF_HOSTED_FN("reduce",      "ArrayStaticReduce", 2,0),
     JS_SELF_HOSTED_FN("reduceRight", "ArrayStaticReduceRight", 2,0),
-    JS_SELF_HOSTED_FN("join",        "ArrayStaticJoin", 2,0),
-    JS_SELF_HOSTED_FN("reverse",     "ArrayStaticReverse", 1,0),
-    JS_SELF_HOSTED_FN("sort",        "ArrayStaticSort", 2,0),
-    JS_SELF_HOSTED_FN("push",        "ArrayStaticPush", 2,0),
-    JS_SELF_HOSTED_FN("pop",         "ArrayStaticPop", 1,0),
-    JS_SELF_HOSTED_FN("shift",       "ArrayStaticShift", 1,0),
-    JS_SELF_HOSTED_FN("unshift",     "ArrayStaticUnshift", 2,0),
-    JS_SELF_HOSTED_FN("splice",      "ArrayStaticSplice", 3,0),
-    JS_SELF_HOSTED_FN("slice",       "ArrayStaticSlice", 3,0),
     JS_SELF_HOSTED_FN("from",        "ArrayFrom", 3,0),
     JS_FN("of",                 array_of,           0,0),
 
     JS_FS_END
 };
 
 const JSPropertySpec array_static_props[] = {
     JS_SELF_HOSTED_SYM_GET(species, "ArraySpecies", 0),
--- a/js/src/jsarray.h
+++ b/js/src/jsarray.h
@@ -179,22 +179,16 @@ extern bool
 array_unshift(JSContext* cx, unsigned argc, js::Value* vp);
 
 extern bool
 array_slice(JSContext* cx, unsigned argc, js::Value* vp);
 
 extern JSObject*
 array_slice_dense(JSContext* cx, HandleObject obj, int32_t begin, int32_t end, HandleObject result);
 
-extern bool
-array_reverse(JSContext* cx, unsigned argc, js::Value* vp);
-
-extern bool
-array_splice(JSContext* cx, unsigned argc, js::Value* vp);
-
 /*
  * Append the given (non-hole) value to the end of an array.  The array must be
  * a newborn array -- that is, one which has not been exposed to script for
  * arbitrary manipulation.  (This method optimizes on the assumption that
  * extending the array to accommodate the element will never make the array
  * sparse, which requires that the array be completely filled.)
  */
 extern bool
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -2894,16 +2894,44 @@ js::HasDataProperty(JSContext* cx, Nativ
             *vp = obj->getSlot(shape->slot());
             return true;
         }
     }
 
     return false;
 }
 
+static bool
+GenericNativeMethodDispatcher(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    const JSFunctionSpec* fs = (JSFunctionSpec*)
+        args.callee().as<JSFunction>().getExtendedSlot(0).toPrivate();
+    MOZ_ASSERT((fs->flags & JSFUN_GENERIC_NATIVE) != 0);
+
+    if (argc < 1) {
+        ReportMissingArg(cx, args.calleev(), 0);
+        return false;
+    }
+
+    /*
+     * Copy all actual (argc) arguments down over our |this| parameter, vp[1],
+     * which is almost always the class constructor object, e.g. Array.  Then
+     * call the corresponding prototype native method with our first argument
+     * passed as |this|.
+     */
+    memmove(vp + 1, vp + 2, argc * sizeof(Value));
+
+    /* Clear the last parameter in case too few arguments were passed. */
+    vp[2 + --argc].setUndefined();
+
+    return fs->call.op(cx, argc, vp);
+}
+
 extern bool
 PropertySpecNameToId(JSContext* cx, const char* name, MutableHandleId id,
                      js::PinningBehavior pin = js::DoNotPinAtom);
 
 static bool
 DefineFunctionFromSpec(JSContext* cx, HandleObject obj, const JSFunctionSpec* fs, unsigned flags,
                        DefineAsIntrinsic intrinsic)
 {
@@ -2921,16 +2949,37 @@ DefineFunctionFromSpec(JSContext* cx, Ha
         MOZ_ASSERT(gop != JS_PropertyStub);
         MOZ_ASSERT(sop != JS_StrictPropertyStub);
     }
 
     RootedId id(cx);
     if (!PropertySpecNameToId(cx, fs->name, &id))
         return false;
 
+    // Define a generic arity N+1 static method for the arity N prototype
+    // method if flags contains JSFUN_GENERIC_NATIVE.
+    if (flags & JSFUN_GENERIC_NATIVE) {
+        // We require that any consumers using JSFUN_GENERIC_NATIVE stash
+        // the prototype and constructor in the global slots before invoking
+        // JS_DefineFunctions on the proto.
+        JSProtoKey key = JSCLASS_CACHED_PROTO_KEY(obj->getClass());
+        MOZ_ASSERT(obj == &obj->global().getPrototype(key).toObject());
+        RootedObject ctor(cx, &obj->global().getConstructor(key).toObject());
+
+        flags &= ~JSFUN_GENERIC_NATIVE;
+        JSFunction* fun = DefineFunction(cx, ctor, id,
+                                         GenericNativeMethodDispatcher,
+                                         fs->nargs + 1, flags,
+                                         gc::AllocKind::FUNCTION_EXTENDED);
+        if (!fun)
+            return false;
+
+        fun->setExtendedSlot(0, PrivateValue(const_cast<JSFunctionSpec*>(fs)));
+    }
+
     JSFunction* fun = NewFunctionFromSpec(cx, fs, id);
     if (!fun)
         return false;
 
     if (intrinsic == AsIntrinsic)
         fun->setIsIntrinsic();
 
     RootedValue funVal(cx, ObjectValue(*fun));
--- a/js/src/jsstr.cpp
+++ b/js/src/jsstr.cpp
@@ -677,18 +677,18 @@ ToLowerCaseHelper(JSContext* cx, CallRec
 }
 
 bool
 js::str_toLowerCase(JSContext* cx, unsigned argc, Value* vp)
 {
     return ToLowerCaseHelper(cx, CallArgsFromVp(argc, vp));
 }
 
-bool
-js::str_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     /*
      * Forcefully ignore the first (or any) argument and return toLowerCase(),
      * ECMA has reserved that argument, presumably for defining the locale.
      */
     if (cx->runtime()->localeCallbacks && cx->runtime()->localeCallbacks->localeToLowerCase) {
@@ -828,18 +828,18 @@ ToUpperCaseHelper(JSContext* cx, CallRec
 }
 
 bool
 js::str_toUpperCase(JSContext* cx, unsigned argc, Value* vp)
 {
     return ToUpperCaseHelper(cx, CallArgsFromVp(argc, vp));
 }
 
-bool
-js::str_toLocaleUpperCase(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_toLocaleUpperCase(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     /*
      * Forcefully ignore the first (or any) argument and return toUpperCase(),
      * ECMA has reserved that argument, presumably for defining the locale.
      */
     if (cx->runtime()->localeCallbacks && cx->runtime()->localeCallbacks->localeToUpperCase) {
@@ -854,18 +854,18 @@ js::str_toLocaleUpperCase(JSContext* cx,
         args.rval().set(result);
         return true;
     }
 
     return ToUpperCaseHelper(cx, args);
 }
 
 #if !EXPOSE_INTL_API
-bool
-js::str_localeCompare(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_localeCompare(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     RootedString str(cx, ThisToStringForStringProto(cx, args));
     if (!str)
         return false;
 
     RootedString thatStr(cx, ToString<CanGC>(cx, args.get(0)));
     if (!thatStr)
@@ -886,18 +886,18 @@ js::str_localeCompare(JSContext* cx, uns
 
     args.rval().setInt32(result);
     return true;
 }
 #endif
 
 #if EXPOSE_INTL_API
 /* ES6 20140210 draft 21.1.3.12. */
-bool
-js::str_normalize(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_normalize(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Steps 1-3.
     RootedString str(cx, ThisToStringForStringProto(cx, args));
     if (!str)
         return false;
 
@@ -1815,18 +1815,18 @@ js::str_startsWith(JSContext* cx, unsign
     if (!text)
         return false;
 
     args.rval().setBoolean(HasSubstringAt(text, searchStr, start));
     return true;
 }
 
 /* ES6 draft rc3 21.1.3.6. */
-bool
-js::str_endsWith(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_endsWith(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Steps 1, 2, and 3
     RootedString str(cx, ThisToStringForStringProto(cx, args));
     if (!str)
         return false;
 
@@ -1934,30 +1934,30 @@ TrimString(JSContext* cx, Value* vp, boo
     str = NewDependentString(cx, str, begin, end - begin);
     if (!str)
         return false;
 
     call.rval().setString(str);
     return true;
 }
 
-bool
-js::str_trim(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_trim(JSContext* cx, unsigned argc, Value* vp)
 {
     return TrimString(cx, vp, true, true);
 }
 
-bool
-js::str_trimLeft(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_trimLeft(JSContext* cx, unsigned argc, Value* vp)
 {
     return TrimString(cx, vp, true, false);
 }
 
-bool
-js::str_trimRight(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_trimRight(JSContext* cx, unsigned argc, Value* vp)
 {
     return TrimString(cx, vp, false, true);
 }
 
 // Utility for building a rope (lazy concatenation) of strings.
 class RopeBuilder {
     JSContext* cx;
     RootedString res;
@@ -2481,18 +2481,18 @@ js::str_split_string(JSContext* cx, Hand
         return CharSplitHelper(cx, linearStr, limit, group);
 
     return SplitHelper(cx, linearStr, limit, linearSep, group);
 }
 
 /*
  * Python-esque sequence operations.
  */
-bool
-js::str_concat(JSContext* cx, unsigned argc, Value* vp)
+static bool
+str_concat(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     JSString* str = ThisToStringForStringProto(cx, args);
     if (!str)
         return false;
 
     for (unsigned i = 0; i < args.length(); i++) {
         JSString* argStr = ToString<NoGC>(cx, args[i]);
@@ -2522,53 +2522,53 @@ js::str_concat(JSContext* cx, unsigned a
 static const JSFunctionSpec string_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,     str_toSource,          0,0),
 #endif
 
     /* Java-like methods. */
     JS_FN(js_toString_str,     str_toString,          0,0),
     JS_FN(js_valueOf_str,      str_toString,          0,0),
-    JS_FN("toLowerCase",       str_toLowerCase,       0,0),
-    JS_FN("toUpperCase",       str_toUpperCase,       0,0),
-    JS_INLINABLE_FN("charAt",  str_charAt,            1,0, StringCharAt),
-    JS_INLINABLE_FN("charCodeAt", str_charCodeAt,     1,0, StringCharCodeAt),
+    JS_FN("toLowerCase",       str_toLowerCase,       0,JSFUN_GENERIC_NATIVE),
+    JS_FN("toUpperCase",       str_toUpperCase,       0,JSFUN_GENERIC_NATIVE),
+    JS_INLINABLE_FN("charAt",  str_charAt,            1,JSFUN_GENERIC_NATIVE, StringCharAt),
+    JS_INLINABLE_FN("charCodeAt", str_charCodeAt,     1,JSFUN_GENERIC_NATIVE, StringCharCodeAt),
     JS_SELF_HOSTED_FN("substring", "String_substring", 2,0),
     JS_SELF_HOSTED_FN("padStart", "String_pad_start", 2,0),
     JS_SELF_HOSTED_FN("padEnd", "String_pad_end", 2,0),
     JS_SELF_HOSTED_FN("codePointAt", "String_codePointAt", 1,0),
-    JS_FN("includes",          str_includes,          1,0),
-    JS_FN("indexOf",           str_indexOf,           1,0),
-    JS_FN("lastIndexOf",       str_lastIndexOf,       1,0),
-    JS_FN("startsWith",        str_startsWith,        1,0),
-    JS_FN("endsWith",          str_endsWith,          1,0),
-    JS_FN("trim",              str_trim,              0,0),
-    JS_FN("trimLeft",          str_trimLeft,          0,0),
-    JS_FN("trimRight",         str_trimRight,         0,0),
-    JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0,0),
-    JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0,0),
+    JS_FN("includes",          str_includes,          1,JSFUN_GENERIC_NATIVE),
+    JS_FN("indexOf",           str_indexOf,           1,JSFUN_GENERIC_NATIVE),
+    JS_FN("lastIndexOf",       str_lastIndexOf,       1,JSFUN_GENERIC_NATIVE),
+    JS_FN("startsWith",        str_startsWith,        1,JSFUN_GENERIC_NATIVE),
+    JS_FN("endsWith",          str_endsWith,          1,JSFUN_GENERIC_NATIVE),
+    JS_FN("trim",              str_trim,              0,JSFUN_GENERIC_NATIVE),
+    JS_FN("trimLeft",          str_trimLeft,          0,JSFUN_GENERIC_NATIVE),
+    JS_FN("trimRight",         str_trimRight,         0,JSFUN_GENERIC_NATIVE),
+    JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0,JSFUN_GENERIC_NATIVE),
+    JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0,JSFUN_GENERIC_NATIVE),
 #if EXPOSE_INTL_API
     JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1,0),
 #else
-    JS_FN("localeCompare",     str_localeCompare,     1,0),
+    JS_FN("localeCompare",     str_localeCompare,     1,JSFUN_GENERIC_NATIVE),
 #endif
     JS_SELF_HOSTED_FN("repeat", "String_repeat",      1,0),
 #if EXPOSE_INTL_API
-    JS_FN("normalize",         str_normalize,         0,0),
+    JS_FN("normalize",         str_normalize,         0,JSFUN_GENERIC_NATIVE),
 #endif
 
     /* Perl-ish methods (search is actually Python-esque). */
     JS_SELF_HOSTED_FN("match", "String_match",        1,0),
     JS_SELF_HOSTED_FN("search", "String_search",      1,0),
     JS_SELF_HOSTED_FN("replace", "String_replace",    2,0),
     JS_SELF_HOSTED_FN("split",  "String_split",       2,0),
     JS_SELF_HOSTED_FN("substr", "String_substr",      2,0),
 
     /* Python-esque sequence methods. */
-    JS_FN("concat",            str_concat,            1,0),
+    JS_FN("concat",            str_concat,            1,JSFUN_GENERIC_NATIVE),
     JS_SELF_HOSTED_FN("slice", "String_slice",        2,0),
 
     /* HTML string methods. */
     JS_SELF_HOSTED_FN("bold",     "String_bold",       0,0),
     JS_SELF_HOSTED_FN("italics",  "String_italics",    0,0),
     JS_SELF_HOSTED_FN("fixed",    "String_fixed",      0,0),
     JS_SELF_HOSTED_FN("strike",   "String_strike",     0,0),
     JS_SELF_HOSTED_FN("small",    "String_small",      0,0),
@@ -2711,36 +2711,21 @@ static const JSFunctionSpec string_stati
     JS_SELF_HOSTED_FN("substr",          "String_static_substr",        3,0),
     JS_SELF_HOSTED_FN("slice",           "String_static_slice",         3,0),
 
     JS_SELF_HOSTED_FN("match",           "String_generic_match",        2,0),
     JS_SELF_HOSTED_FN("replace",         "String_generic_replace",      3,0),
     JS_SELF_HOSTED_FN("search",          "String_generic_search",       2,0),
     JS_SELF_HOSTED_FN("split",           "String_generic_split",        3,0),
 
-    JS_SELF_HOSTED_FN("toLowerCase",     "String_static_toLowerCase",   1,0),
-    JS_SELF_HOSTED_FN("toUpperCase",     "String_static_toUpperCase",   1,0),
-    JS_SELF_HOSTED_FN("charAt",          "String_static_charAt",        2,0),
-    JS_SELF_HOSTED_FN("charCodeAt",      "String_static_charCodeAt",    2,0),
-    JS_SELF_HOSTED_FN("includes",        "String_static_includes",      2,0),
-    JS_SELF_HOSTED_FN("indexOf",         "String_static_indexOf",       2,0),
-    JS_SELF_HOSTED_FN("lastIndexOf",     "String_static_lastIndexOf",   2,0),
-    JS_SELF_HOSTED_FN("startsWith",      "String_static_startsWith",    2,0),
-    JS_SELF_HOSTED_FN("endsWith",        "String_static_endsWith",      2,0),
-    JS_SELF_HOSTED_FN("trim",            "String_static_trim",          1,0),
-    JS_SELF_HOSTED_FN("trimLeft",        "String_static_trimLeft",      1,0),
-    JS_SELF_HOSTED_FN("trimRight",       "String_static_trimRight",     1,0),
-    JS_SELF_HOSTED_FN("toLocaleLowerCase","String_static_toLocaleLowerCase",1,0),
-    JS_SELF_HOSTED_FN("toLocaleUpperCase","String_static_toLocaleUpperCase",1,0),
+    // This must be at the end because of bug 853075: functions listed after
+    // self-hosted methods aren't available in self-hosted code.
 #if EXPOSE_INTL_API
-    JS_SELF_HOSTED_FN("normalize",       "String_static_normalize",     1,0),
+    JS_SELF_HOSTED_FN("localeCompare",   "String_static_localeCompare", 2,0),
 #endif
-    JS_SELF_HOSTED_FN("concat",          "String_static_concat",        2,0),
-
-    JS_SELF_HOSTED_FN("localeCompare",   "String_static_localeCompare", 2,0),
     JS_FS_END
 };
 
 /* static */ Shape*
 StringObject::assignInitialShape(ExclusiveContext* cx, Handle<StringObject*> obj)
 {
     MOZ_ASSERT(obj->empty());
 
@@ -2762,35 +2747,35 @@ js::InitStringClass(JSContext* cx, Handl
 
     /* Now create the String function. */
     RootedFunction ctor(cx);
     ctor = global->createConstructor(cx, StringConstructor, cx->names().String, 1,
                                      AllocKind::FUNCTION, &jit::JitInfo_String);
     if (!ctor)
         return nullptr;
 
+    if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_String, ctor, proto))
+        return nullptr;
+
     if (!LinkConstructorAndPrototype(cx, ctor, proto))
         return nullptr;
 
     if (!DefinePropertiesAndFunctions(cx, proto, nullptr, string_methods) ||
         !DefinePropertiesAndFunctions(cx, ctor, nullptr, string_static_methods))
     {
         return nullptr;
     }
 
     /*
      * Define escape/unescape, the URI encode/decode functions, and maybe
      * uneval on the global object.
      */
     if (!JS_DefineFunctions(cx, global, string_functions))
         return nullptr;
 
-    if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_String, ctor, proto))
-        return nullptr;
-
     return proto;
 }
 
 const char*
 js::ValueToPrintable(JSContext* cx, const Value& vArg, JSAutoByteString* bytes, bool asSource)
 {
     RootedValue v(cx, vArg);
     JSString* str;
--- a/js/src/jsstr.h
+++ b/js/src/jsstr.h
@@ -333,49 +333,16 @@ str_toString(JSContext* cx, unsigned arg
 extern bool
 str_charAt(JSContext* cx, unsigned argc, Value* vp);
 
 extern bool
 str_charCodeAt_impl(JSContext* cx, HandleString string, HandleValue index, MutableHandleValue res);
 
 extern bool
 str_charCodeAt(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_contains(JSContext *cx, unsigned argc, Value *vp);
-
-extern bool
-str_endsWith(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_trim(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_trimLeft(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_trimRight(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp);
-
-extern bool
-str_toLocaleUpperCase(JSContext* cx, unsigned argc, Value* vp);
-
-#if !EXPOSE_INTL_API
-extern bool
-str_localeCompare(JSContext* cx, unsigned argc, Value* vp);
-#else
-extern bool
-str_normalize(JSContext* cx, unsigned argc, Value* vp);
-#endif
-
-extern bool
-str_concat(JSContext* cx, unsigned argc, Value* vp);
-
 /*
  * Convert one UCS-4 char and write it into a UTF-8 buffer, which must be at
  * least 4 bytes long.  Return the number of UTF-8 bytes of data written.
  */
 extern uint32_t
 OneUcs4ToUtf8Char(uint8_t* utf8Buffer, uint32_t ucs4Char);
 
 extern size_t
deleted file mode 100644
--- a/js/src/tests/js1_6/Array/generics.js
+++ /dev/null
@@ -1,331 +0,0 @@
-var BUGNUMBER = 1263558;
-var summary = "Self-host all Array generics.";
-
-print(BUGNUMBER + ": " + summary);
-
-var arr, arrLike, tmp, f;
-
-function reset() {
-  arr = [5, 7, 13];
-  arrLike = {
-    length: 3,
-    0: 5,
-    1: 7,
-    2: 13,
-    toString() {
-      return "arrLike";
-    }
-  };
-  tmp = [];
-}
-function toString() {
-  return "G";
-}
-
-// Array.join (test this first to use it in remaining tests).
-reset();
-assertThrowsInstanceOf(() => Array.join(), TypeError);
-assertEq(Array.join(arr), "5,7,13");
-assertEq(Array.join(arr, "-"), "5-7-13");
-assertEq(Array.join(arrLike), "5,7,13");
-assertEq(Array.join(arrLike, "-"), "5-7-13");
-
-// Array.concat.
-reset();
-assertThrowsInstanceOf(() => Array.concat(), TypeError);
-assertEq(Array.join(Array.concat(arr), ","), "5,7,13");
-assertEq(Array.join(Array.concat(arr, 11), ","), "5,7,13,11");
-assertEq(Array.join(Array.concat(arr, 11, 17), ","), "5,7,13,11,17");
-assertEq(Array.join(Array.concat(arrLike), ","), "arrLike");
-assertEq(Array.join(Array.concat(arrLike, 11), ","), "arrLike,11");
-assertEq(Array.join(Array.concat(arrLike, 11, 17), ","), "arrLike,11,17");
-
-// Array.lastIndexOf.
-reset();
-assertThrowsInstanceOf(() => Array.lastIndexOf(), TypeError);
-assertEq(Array.lastIndexOf(arr), -1);
-assertEq(Array.lastIndexOf(arr, 1), -1);
-assertEq(Array.lastIndexOf(arr, 5), 0);
-assertEq(Array.lastIndexOf(arr, 7), 1);
-assertEq(Array.lastIndexOf(arr, 13, 1), -1);
-assertEq(Array.lastIndexOf(arrLike), -1);
-assertEq(Array.lastIndexOf(arrLike, 1), -1);
-assertEq(Array.lastIndexOf(arrLike, 5), 0);
-assertEq(Array.lastIndexOf(arrLike, 7), 1);
-assertEq(Array.lastIndexOf(arrLike, 13, 1), -1);
-
-// Array.indexOf.
-reset();
-assertThrowsInstanceOf(() => Array.indexOf(), TypeError);
-assertEq(Array.indexOf(arr), -1);
-assertEq(Array.indexOf(arr, 1), -1);
-assertEq(Array.indexOf(arr, 5), 0);
-assertEq(Array.indexOf(arr, 7), 1);
-assertEq(Array.indexOf(arr, 1, 5), -1);
-assertEq(Array.indexOf(arrLike), -1);
-assertEq(Array.indexOf(arrLike, 1), -1);
-assertEq(Array.indexOf(arrLike, 5), 0);
-assertEq(Array.indexOf(arrLike, 7), 1);
-assertEq(Array.indexOf(arrLike, 1, 5), -1);
-
-// Array.forEach.
-reset();
-assertThrowsInstanceOf(() => Array.forEach(), TypeError);
-assertThrowsInstanceOf(() => Array.forEach(arr), TypeError);
-assertThrowsInstanceOf(() => Array.forEach(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(this, ...args);
-};
-tmp = [];
-Array.forEach(arr, f);
-assertEq(tmp.join(","), "G,5,0,5,7,13," + "G,7,1,5,7,13," + "G,13,2,5,7,13");
-tmp = [];
-Array.forEach(arr, f, "T");
-assertEq(tmp.join(","), "T,5,0,5,7,13," + "T,7,1,5,7,13," + "T,13,2,5,7,13");
-tmp = [];
-Array.forEach(arrLike, f);
-assertEq(tmp.join(","), "G,5,0,arrLike," + "G,7,1,arrLike," + "G,13,2,arrLike");
-tmp = [];
-Array.forEach(arrLike, f, "T");
-assertEq(tmp.join(","), "T,5,0,arrLike," + "T,7,1,arrLike," + "T,13,2,arrLike");
-
-// Array.map.
-reset();
-assertThrowsInstanceOf(() => Array.map(), TypeError);
-assertThrowsInstanceOf(() => Array.map(arr), TypeError);
-assertThrowsInstanceOf(() => Array.map(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(this, ...args);
-  return args[0] * 2;
-}
-tmp = [];
-assertEq(Array.join(Array.map(arr, f), ","), "10,14,26");
-assertEq(tmp.join(","), "G,5,0,5,7,13," + "G,7,1,5,7,13," + "G,13,2,5,7,13");
-tmp = [];
-assertEq(Array.join(Array.map(arr, f, "T"), ","), "10,14,26");
-assertEq(tmp.join(","), "T,5,0,5,7,13," + "T,7,1,5,7,13," + "T,13,2,5,7,13");
-tmp = [];
-assertEq(Array.join(Array.map(arrLike, f), ","), "10,14,26");
-assertEq(tmp.join(","), "G,5,0,arrLike," + "G,7,1,arrLike," + "G,13,2,arrLike");
-tmp = [];
-assertEq(Array.join(Array.map(arrLike, f, "T"), ","), "10,14,26");
-assertEq(tmp.join(","), "T,5,0,arrLike," + "T,7,1,arrLike," + "T,13,2,arrLike");
-
-// Array.filter.
-reset();
-assertThrowsInstanceOf(() => Array.filter(), TypeError);
-assertThrowsInstanceOf(() => Array.filter(arr), TypeError);
-assertThrowsInstanceOf(() => Array.filter(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(this, ...args);
-  return args[0] < 10;
-}
-tmp = [];
-assertEq(Array.join(Array.filter(arr, f), ","), "5,7");
-assertEq(tmp.join(","), "G,5,0,5,7,13," + "G,7,1,5,7,13," + "G,13,2,5,7,13");
-tmp = [];
-assertEq(Array.join(Array.filter(arr, f, "T"), ","), "5,7");
-assertEq(tmp.join(","), "T,5,0,5,7,13," + "T,7,1,5,7,13," + "T,13,2,5,7,13");
-tmp = [];
-assertEq(Array.join(Array.filter(arrLike, f), ","), "5,7");
-assertEq(tmp.join(","), "G,5,0,arrLike," + "G,7,1,arrLike," + "G,13,2,arrLike");
-tmp = [];
-assertEq(Array.join(Array.filter(arrLike, f, "T"), ","), "5,7");
-assertEq(tmp.join(","), "T,5,0,arrLike," + "T,7,1,arrLike," + "T,13,2,arrLike");
-
-// Array.every.
-reset();
-assertThrowsInstanceOf(() => Array.every(), TypeError);
-assertThrowsInstanceOf(() => Array.every(arr), TypeError);
-assertThrowsInstanceOf(() => Array.every(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(this, ...args);
-  return args[0] < 6;
-}
-tmp = [];
-assertEq(Array.every(arr, f), false);
-assertEq(tmp.join(","), "G,5,0,5,7,13," + "G,7,1,5,7,13");
-tmp = [];
-assertEq(Array.every(arr, f, "T"), false);
-assertEq(tmp.join(","), "T,5,0,5,7,13," + "T,7,1,5,7,13");
-tmp = [];
-assertEq(Array.every(arrLike, f), false);
-assertEq(tmp.join(","), "G,5,0,arrLike," + "G,7,1,arrLike");
-tmp = [];
-assertEq(Array.every(arrLike, f, "T"), false);
-assertEq(tmp.join(","), "T,5,0,arrLike," + "T,7,1,arrLike");
-
-// Array.some.
-reset();
-assertThrowsInstanceOf(() => Array.some(), TypeError);
-assertThrowsInstanceOf(() => Array.some(arr), TypeError);
-assertThrowsInstanceOf(() => Array.some(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(this, ...args);
-  return args[0] == 7;
-}
-tmp = [];
-assertEq(Array.some(arr, f), true);
-assertEq(tmp.join(","), "G,5,0,5,7,13," + "G,7,1,5,7,13");
-tmp = [];
-assertEq(Array.some(arr, f, "T"), true);
-assertEq(tmp.join(","), "T,5,0,5,7,13," + "T,7,1,5,7,13");
-tmp = [];
-assertEq(Array.some(arrLike, f), true);
-assertEq(tmp.join(","), "G,5,0,arrLike," + "G,7,1,arrLike");
-tmp = [];
-assertEq(Array.some(arrLike, f, "T"), true);
-assertEq(tmp.join(","), "T,5,0,arrLike," + "T,7,1,arrLike");
-
-// Array.reduce.
-reset();
-assertThrowsInstanceOf(() => Array.reduce(), TypeError);
-assertThrowsInstanceOf(() => Array.reduce(arr), TypeError);
-assertThrowsInstanceOf(() => Array.reduce(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(...args);
-  return args[0] + args[1];
-}
-tmp = [];
-assertEq(Array.reduce(arr, f), 25);
-assertEq(tmp.join(","), "5,7,1,5,7,13," + "12,13,2,5,7,13");
-tmp = [];
-assertEq(Array.reduce(arr, f, 17), 42);
-assertEq(tmp.join(","), "17,5,0,5,7,13," + "22,7,1,5,7,13," + "29,13,2,5,7,13");
-tmp = [];
-assertEq(Array.reduce(arrLike, f), 25);
-assertEq(tmp.join(","), "5,7,1,arrLike," + "12,13,2,arrLike");
-tmp = [];
-assertEq(Array.reduce(arrLike, f, 17), 42);
-assertEq(tmp.join(","), "17,5,0,arrLike," + "22,7,1,arrLike," + "29,13,2,arrLike");
-
-// Array.reduceRight.
-reset();
-assertThrowsInstanceOf(() => Array.reduceRight(), TypeError);
-assertThrowsInstanceOf(() => Array.reduceRight(arr), TypeError);
-assertThrowsInstanceOf(() => Array.reduceRight(arrLike), TypeError);
-f = function(...args) {
-  tmp.push(...args);
-  return args[0] + args[1];
-}
-tmp = [];
-assertEq(Array.reduceRight(arr, f), 25);
-assertEq(tmp.join(","), "13,7,1,5,7,13," + "20,5,0,5,7,13");
-tmp = [];
-assertEq(Array.reduceRight(arr, f, 17), 42);
-assertEq(tmp.join(","), "17,13,2,5,7,13," + "30,7,1,5,7,13," + "37,5,0,5,7,13");
-tmp = [];
-assertEq(Array.reduceRight(arrLike, f), 25);
-assertEq(tmp.join(","), "13,7,1,arrLike," + "20,5,0,arrLike");
-tmp = [];
-assertEq(Array.reduceRight(arrLike, f, 17), 42);
-assertEq(tmp.join(","), "17,13,2,arrLike," + "30,7,1,arrLike," + "37,5,0,arrLike");
-
-// Array.reverse.
-reset();
-assertThrowsInstanceOf(() => Array.reverse(), TypeError);
-assertEq(Array.join(Array.reverse(arr), ","), "13,7,5");
-assertEq(Array.join(arr, ","), "13,7,5");
-assertEq(Array.join(Array.reverse(arrLike), ","), "13,7,5");
-assertEq(Array.join(arrLike, ","), "13,7,5");
-
-// Array.sort.
-reset();
-assertThrowsInstanceOf(() => Array.sort(), TypeError);
-f = function(x, y) {
-  return y - x;
-}
-assertEq(Array.join(Array.sort(arr), ","), "13,5,7");
-assertEq(Array.join(Array.sort(arr, f), ","), "13,7,5");
-assertEq(Array.join(Array.sort(arrLike), ","), "13,5,7");
-assertEq(Array.join(Array.sort(arrLike, f), ","), "13,7,5");
-
-// Array.push.
-reset();
-assertThrowsInstanceOf(() => Array.push(), TypeError);
-assertEq(Array.push(arr), 3);
-assertEq(Array.join(arr), "5,7,13");
-assertEq(Array.push(arr, 17), 4);
-assertEq(Array.join(arr), "5,7,13,17");
-assertEq(Array.push(arr, 19, 21), 6);
-assertEq(Array.join(arr), "5,7,13,17,19,21");
-assertEq(Array.push(arrLike), 3);
-assertEq(Array.join(arrLike), "5,7,13");
-assertEq(Array.push(arrLike, 17), 4);
-assertEq(Array.join(arrLike), "5,7,13,17");
-assertEq(Array.push(arrLike, 19, 21), 6);
-assertEq(Array.join(arrLike), "5,7,13,17,19,21");
-
-// Array.pop.
-reset();
-assertThrowsInstanceOf(() => Array.pop(), TypeError);
-assertEq(Array.pop(arr), 13);
-assertEq(Array.join(arr), "5,7");
-assertEq(Array.pop(arr), 7);
-assertEq(Array.join(arr), "5");
-assertEq(Array.pop(arrLike), 13);
-assertEq(Array.join(arrLike), "5,7");
-assertEq(Array.pop(arrLike), 7);
-assertEq(Array.join(arrLike), "5");
-
-// Array.shift.
-reset();
-assertThrowsInstanceOf(() => Array.shift(), TypeError);
-assertEq(Array.shift(arr), 5);
-assertEq(Array.join(arr), "7,13");
-assertEq(Array.shift(arr), 7);
-assertEq(Array.join(arr), "13");
-assertEq(Array.shift(arrLike), 5);
-assertEq(Array.join(arrLike), "7,13");
-assertEq(Array.shift(arrLike), 7);
-assertEq(Array.join(arrLike), "13");
-
-// Array.unshift.
-reset();
-assertThrowsInstanceOf(() => Array.unshift(), TypeError);
-assertEq(Array.unshift(arr), 3);
-assertEq(Array.join(arr), "5,7,13");
-assertEq(Array.unshift(arr, 17), 4);
-assertEq(Array.join(arr), "17,5,7,13");
-assertEq(Array.unshift(arr, 19, 21), 6);
-assertEq(Array.join(arr), "19,21,17,5,7,13");
-assertEq(Array.unshift(arrLike), 3);
-assertEq(Array.join(arrLike), "5,7,13");
-assertEq(Array.unshift(arrLike, 17), 4);
-assertEq(Array.join(arrLike), "17,5,7,13");
-assertEq(Array.unshift(arrLike, 19, 21), 6);
-assertEq(Array.join(arrLike), "19,21,17,5,7,13");
-
-// Array.splice.
-reset();
-assertThrowsInstanceOf(() => Array.splice(), TypeError);
-assertEq(Array.join(Array.splice(arr)), "");
-assertEq(Array.join(arr), "5,7,13");
-assertEq(Array.join(Array.splice(arr, 1)), "7,13");
-assertEq(Array.join(arr), "5");
-reset();
-assertEq(Array.join(Array.splice(arr, 1, 1)), "7");
-assertEq(Array.join(arr), "5,13");
-reset();
-assertEq(Array.join(Array.splice(arrLike)), "");
-assertEq(Array.join(arrLike), "5,7,13");
-assertEq(Array.join(Array.splice(arrLike, 1)), "7,13");
-assertEq(Array.join(arrLike), "5");
-reset();
-assertEq(Array.join(Array.splice(arrLike, 1, 1)), "7");
-assertEq(Array.join(arrLike), "5,13");
-
-// Array.slice.
-reset();
-assertThrowsInstanceOf(() => Array.slice(), TypeError);
-assertEq(Array.join(Array.slice(arr)), "5,7,13");
-assertEq(Array.join(Array.slice(arr, 1)), "7,13");
-assertEq(Array.join(Array.slice(arr, 1, 1)), "");
-assertEq(Array.join(Array.slice(arr, 1, 2)), "7");
-assertEq(Array.join(Array.slice(arrLike)), "5,7,13");
-assertEq(Array.join(Array.slice(arrLike, 1)), "7,13");
-assertEq(Array.join(Array.slice(arrLike, 1, 1)), "");
-assertEq(Array.join(Array.slice(arrLike, 1, 2)), "7");
-
-if (typeof reportCompare === "function")
-  reportCompare(true, true);
deleted file mode 100644
--- a/js/src/tests/js1_6/String/generics.js
+++ /dev/null
@@ -1,228 +0,0 @@
-var BUGNUMBER = 1263558;
-var summary = "Self-host all String generics.";
-
-print(BUGNUMBER + ": " + summary);
-
-var result;
-var str = "ABCde";
-var strObj = {
-  toString() {
-    return "ABCde";
-  }
-};
-
-// String.substring.
-assertThrowsInstanceOf(() => String.substring(), TypeError);
-assertEq(String.substring(str), "ABCde");
-assertEq(String.substring(str, 1), "BCde");
-assertEq(String.substring(str, 1, 3), "BC");
-assertEq(String.substring(strObj), "ABCde");
-assertEq(String.substring(strObj, 1), "BCde");
-assertEq(String.substring(strObj, 1, 3), "BC");
-
-// String.substr.
-assertThrowsInstanceOf(() => String.substr(), TypeError);
-assertEq(String.substr(str), "ABCde");
-assertEq(String.substr(str, 1), "BCde");
-assertEq(String.substr(str, 1, 3), "BCd");
-assertEq(String.substr(strObj), "ABCde");
-assertEq(String.substr(strObj, 1), "BCde");
-assertEq(String.substr(strObj, 1, 3), "BCd");
-
-// String.slice.
-assertThrowsInstanceOf(() => String.slice(), TypeError);
-assertEq(String.slice(str), "ABCde");
-assertEq(String.slice(str, 1), "BCde");
-assertEq(String.slice(str, 1, 3), "BC");
-assertEq(String.slice(strObj), "ABCde");
-assertEq(String.slice(strObj, 1), "BCde");
-assertEq(String.slice(strObj, 1, 3), "BC");
-
-// String.match.
-assertThrowsInstanceOf(() => String.match(), TypeError);
-result = String.match(str);
-assertEq(result.index, 0);
-assertEq(result.length, 1);
-assertEq(result[0], "");
-result = String.match(str, /c/i);
-assertEq(result.index, 2);
-assertEq(result.length, 1);
-assertEq(result[0], "C");
-result = String.match(strObj);
-assertEq(result.index, 0);
-assertEq(result.length, 1);
-assertEq(result[0], "");
-result = String.match(strObj, /c/i);
-assertEq(result.index, 2);
-assertEq(result.length, 1);
-assertEq(result[0], "C");
-
-// String.replace.
-assertThrowsInstanceOf(() => String.replace(), TypeError);
-assertEq(String.replace(str), "ABCde");
-assertEq(String.replace(str, /c/i), "ABundefinedde");
-assertEq(String.replace(str, /c/i, "x"), "ABxde");
-assertEq(String.replace(strObj), "ABCde");
-assertEq(String.replace(strObj, /c/i), "ABundefinedde");
-assertEq(String.replace(strObj, /c/i, "x"), "ABxde");
-
-// String.search.
-assertThrowsInstanceOf(() => String.search(), TypeError);
-assertEq(String.search(str), 0);
-assertEq(String.search(str, /c/i), 2);
-assertEq(String.search(strObj), 0);
-assertEq(String.search(strObj, /c/i), 2);
-
-// String.split.
-assertThrowsInstanceOf(() => String.split(), TypeError);
-assertEq(String.split(str).join(","), "ABCde");
-assertEq(String.split(str, /[bd]/i).join(","), "A,C,e");
-assertEq(String.split(str, /[bd]/i, 2).join(","), "A,C");
-assertEq(String.split(strObj).join(","), "ABCde");
-assertEq(String.split(strObj, /[bd]/i).join(","), "A,C,e");
-assertEq(String.split(strObj, /[bd]/i, 2).join(","), "A,C");
-
-// String.toLowerCase.
-assertThrowsInstanceOf(() => String.toLowerCase(), TypeError);
-assertEq(String.toLowerCase(str), "abcde");
-assertEq(String.toLowerCase(strObj), "abcde");
-
-// String.toUpperCase.
-assertThrowsInstanceOf(() => String.toUpperCase(), TypeError);
-assertEq(String.toUpperCase(str), "ABCDE");
-assertEq(String.toUpperCase(strObj), "ABCDE");
-
-// String.charAt.
-assertThrowsInstanceOf(() => String.charAt(), TypeError);
-assertEq(String.charAt(str), "A");
-assertEq(String.charAt(str, 2), "C");
-assertEq(String.charAt(strObj), "A");
-assertEq(String.charAt(strObj, 2), "C");
-
-// String.charCodeAt.
-assertThrowsInstanceOf(() => String.charCodeAt(), TypeError);
-assertEq(String.charCodeAt(str), 65);
-assertEq(String.charCodeAt(str, 2), 67);
-assertEq(String.charCodeAt(strObj), 65);
-assertEq(String.charCodeAt(strObj, 2), 67);
-
-// String.includes.
-assertThrowsInstanceOf(() => String.includes(), TypeError);
-assertEq(String.includes(str), false);
-assertEq(String.includes(str, "C"), true);
-assertEq(String.includes(str, "C", 2), true);
-assertEq(String.includes(str, "C", 3), false);
-assertEq(String.includes(strObj), false);
-assertEq(String.includes(strObj, "C"), true);
-assertEq(String.includes(strObj, "C", 2), true);
-assertEq(String.includes(strObj, "C", 3), false);
-
-// String.indexOf.
-assertThrowsInstanceOf(() => String.indexOf(), TypeError);
-assertEq(String.indexOf(str), -1);
-assertEq(String.indexOf(str, "C"), 2);
-assertEq(String.indexOf(str, "C", 2), 2);
-assertEq(String.indexOf(str, "C", 3), -1);
-assertEq(String.indexOf(strObj), -1);
-assertEq(String.indexOf(strObj, "C"), 2);
-assertEq(String.indexOf(strObj, "C", 2), 2);
-assertEq(String.indexOf(strObj, "C", 3), -1);
-
-// String.lastIndexOf.
-assertThrowsInstanceOf(() => String.lastIndexOf(), TypeError);
-assertEq(String.lastIndexOf(str), -1);
-assertEq(String.lastIndexOf(str, "C"), 2);
-assertEq(String.lastIndexOf(str, "C", 2), 2);
-assertEq(String.lastIndexOf(str, "C", 1), -1);
-assertEq(String.lastIndexOf(strObj), -1);
-assertEq(String.lastIndexOf(strObj, "C"), 2);
-assertEq(String.lastIndexOf(strObj, "C", 2), 2);
-assertEq(String.lastIndexOf(strObj, "C", 1), -1);
-
-// String.startsWith.
-assertThrowsInstanceOf(() => String.startsWith(), TypeError);
-assertEq(String.startsWith(str), false);
-assertEq(String.startsWith(str, "A"), true);
-assertEq(String.startsWith(str, "B", 0), false);
-assertEq(String.startsWith(str, "B", 1), true);
-assertEq(String.startsWith(strObj), false);
-assertEq(String.startsWith(strObj, "A"), true);
-assertEq(String.startsWith(strObj, "B", 0), false);
-assertEq(String.startsWith(strObj, "B", 1), true);
-
-// String.endsWith.
-assertThrowsInstanceOf(() => String.endsWith(), TypeError);
-assertEq(String.endsWith(str), false);
-assertEq(String.endsWith(str, "e"), true);
-assertEq(String.endsWith(str, "B", 0), false);
-assertEq(String.endsWith(str, "B", 2), true);
-assertEq(String.endsWith(strObj), false);
-assertEq(String.endsWith(strObj, "e"), true);
-assertEq(String.endsWith(strObj, "B", 0), false);
-assertEq(String.endsWith(strObj, "B", 2), true);
-
-// String.trim.
-var str2 = "  ABCde  ";
-var strObj2 = {
-  toString() {
-    return "  ABCde  ";
-  }
-};
-assertThrowsInstanceOf(() => String.trim(), TypeError);
-assertEq(String.trim(str2), "ABCde");
-assertEq(String.trim(strObj2), "ABCde");
-
-// String.trimLeft.
-assertThrowsInstanceOf(() => String.trimLeft(), TypeError);
-assertEq(String.trimLeft(str2), "ABCde  ");
-assertEq(String.trimLeft(strObj2), "ABCde  ");
-
-// String.trimRight.
-assertThrowsInstanceOf(() => String.trimRight(), TypeError);
-assertEq(String.trimRight(str2), "  ABCde");
-assertEq(String.trimRight(strObj2), "  ABCde");
-
-// String.toLocaleLowerCase.
-assertThrowsInstanceOf(() => String.toLocaleLowerCase(), TypeError);
-assertEq(String.toLocaleLowerCase(str), str.toLocaleLowerCase());
-assertEq(String.toLocaleLowerCase(strObj), str.toLocaleLowerCase());
-
-// String.toLocaleUpperCase.
-assertThrowsInstanceOf(() => String.toLocaleUpperCase(), TypeError);
-assertEq(String.toLocaleUpperCase(str), str.toLocaleUpperCase());
-assertEq(String.toLocaleUpperCase(strObj), str.toLocaleUpperCase());
-
-// String.localeCompare.
-assertThrowsInstanceOf(() => String.localeCompare(), TypeError);
-assertEq(String.localeCompare(str), str.localeCompare());
-assertEq(String.localeCompare(str, "abcde"), str.localeCompare("abcde"));
-assertEq(String.localeCompare(strObj), str.localeCompare());
-assertEq(String.localeCompare(strObj, "abcde"), str.localeCompare("abcde"));
-
-// String.normalize.
-if ("normalize" in String.prototype) {
-  var str3 = "\u3082\u3058\u3089 \u3082\u3057\u3099\u3089";
-  var strObj3 = {
-    toString() {
-      return "\u3082\u3058\u3089 \u3082\u3057\u3099\u3089";
-    }
-  };
-  assertThrowsInstanceOf(() => String.normalize(), TypeError);
-
-  assertEq(String.normalize(str3), "\u3082\u3058\u3089 \u3082\u3058\u3089");
-  assertEq(String.normalize(str3, "NFD"), "\u3082\u3057\u3099\u3089 \u3082\u3057\u3099\u3089");
-  assertEq(String.normalize(strObj3), "\u3082\u3058\u3089 \u3082\u3058\u3089");
-  assertEq(String.normalize(strObj3, "NFD"), "\u3082\u3057\u3099\u3089 \u3082\u3057\u3099\u3089");
-}
-
-// String.concat.
-assertThrowsInstanceOf(() => String.concat(), TypeError);
-assertEq(String.concat(str), "ABCde");
-assertEq(String.concat(str, "f"), "ABCdef");
-assertEq(String.concat(str, "f", "g"), "ABCdefg");
-assertEq(String.concat(strObj), "ABCde");
-assertEq(String.concat(strObj, "f"), "ABCdef");
-assertEq(String.concat(strObj, "f", "g"), "ABCdefg");
-
-if (typeof reportCompare === "function")
-  reportCompare(true, true);
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -2243,18 +2243,16 @@ static const JSFunctionSpec intrinsic_fu
     JS_INLINABLE_FN("std_Array",                 ArrayConstructor,             1,0, Array),
     JS_FN("std_Array_join",                      array_join,                   1,0),
     JS_INLINABLE_FN("std_Array_push",            array_push,                   1,0, ArrayPush),
     JS_INLINABLE_FN("std_Array_pop",             array_pop,                    0,0, ArrayPop),
     JS_INLINABLE_FN("std_Array_shift",           array_shift,                  0,0, ArrayShift),
     JS_FN("std_Array_unshift",                   array_unshift,                1,0),
     JS_INLINABLE_FN("std_Array_slice",           array_slice,                  2,0, ArraySlice),
     JS_FN("std_Array_sort",                      array_sort,                   1,0),
-    JS_FN("std_Array_reverse",                   array_reverse,                0,0),
-    JS_INLINABLE_FN("std_Array_splice",          array_splice,                 2,0, ArraySplice),
 
     JS_FN("std_Date_now",                        date_now,                     0,0),
     JS_FN("std_Date_valueOf",                    date_valueOf,                 0,0),
 
     JS_FN("std_Function_apply",                  fun_apply,                    2,0),
 
     JS_INLINABLE_FN("std_Math_floor",            math_floor,                   1,0, MathFloor),
     JS_INLINABLE_FN("std_Math_max",              math_max,                     2,0, MathMax),
@@ -2287,31 +2285,16 @@ static const JSFunctionSpec intrinsic_fu
     JS_INLINABLE_FN("std_String_charCodeAt",     str_charCodeAt,               1,0, StringCharCodeAt),
     JS_FN("std_String_includes",                 str_includes,                 1,0),
     JS_FN("std_String_indexOf",                  str_indexOf,                  1,0),
     JS_FN("std_String_lastIndexOf",              str_lastIndexOf,              1,0),
     JS_FN("std_String_startsWith",               str_startsWith,               1,0),
     JS_FN("std_String_toLowerCase",              str_toLowerCase,              0,0),
     JS_FN("std_String_toUpperCase",              str_toUpperCase,              0,0),
 
-    JS_INLINABLE_FN("std_String_charAt",         str_charAt,                   1,0, StringCharAt),
-    JS_FN("std_String_endsWith",                 str_endsWith,                 1,0),
-    JS_FN("std_String_trim",                     str_trim,                     0,0),
-    JS_FN("std_String_trimLeft",                 str_trimLeft,                 0,0),
-    JS_FN("std_String_trimRight",                str_trimRight,                0,0),
-    JS_FN("std_String_toLocaleLowerCase",        str_toLocaleLowerCase,        0,0),
-    JS_FN("std_String_toLocaleUpperCase",        str_toLocaleUpperCase,        0,0),
-#if !EXPOSE_INTL_API
-    JS_FN("std_String_localeCompare",            str_localeCompare,            1,0),
-#else
-    JS_FN("std_String_normalize",                str_normalize,                0,0),
-#endif
-    JS_FN("std_String_concat",                   str_concat,                   1,0),
-
-
     JS_FN("std_WeakMap_has",                     WeakMap_has,                  1,0),
     JS_FN("std_WeakMap_get",                     WeakMap_get,                  2,0),
     JS_FN("std_WeakMap_set",                     WeakMap_set,                  2,0),
     JS_FN("std_WeakMap_delete",                  WeakMap_delete,               1,0),
 
     JS_FN("std_SIMD_Int8x16_extractLane",        simd_int8x16_extractLane,     2,0),
     JS_FN("std_SIMD_Int16x8_extractLane",        simd_int16x8_extractLane,     2,0),
     JS_INLINABLE_FN("std_SIMD_Int32x4_extractLane",   simd_int32x4_extractLane,  2,0, SimdInt32x4_extractLane),
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul
+++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul
@@ -534,17 +534,23 @@ https://bugzilla.mozilla.org/show_bug.cg
     // |own| data property. So we add it to the ignore list here, and check it
     // separately.
     //
     // |Symbol.unscopables| should in principle be exposed, but it is
     // inconvenient (as it's a data property, unsupported by ClassSpec) and
     // low value.
     let propsToSkip = ['length', Symbol.unscopables];
 
-    testXray('Array', new iwin.Array(20), new iwin.Array(), propsToSkip);
+    // On the constructor, we want to skip all the non-standard "generic"
+    // functions.  We're trying to remove them anyway; no point doing extra work
+    // to expose them over Xrays.
+    let ctorPropsToSkip = ["join", "reverse", "sort", "push", "pop", "shift",
+                           "unshift", "splice", "slice"];
+    testXray('Array', new iwin.Array(20), new iwin.Array(), propsToSkip,
+             ctorPropsToSkip);
 
     let symbolProps = '';
     uniqueSymbol = iwin.eval('var uniqueSymbol = Symbol("uniqueSymbol"); uniqueSymbol');
     symbolProps = `trickyArray[uniqueSymbol] = 43;
                    trickyArray[Symbol.for("registrySymbolProp")] = 44;`;
     var trickyArray =
       iwin.eval(`var trickyArray = [];
                  trickyArray.primitiveProp = 42;
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -48,17 +48,17 @@
              org.mozilla.{fennec,firefox,firefox_beta}.  The internal Java
              package hierarchy inside the Android package used to have an
              org.mozilla.{fennec,firefox,firefox_beta} subtree *and* an
              org.mozilla.gecko subtree; it now only has org.mozilla.gecko. -->
         <activity android:name="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
                   android:label="@string/moz_app_displayname"
                   android:taskAffinity="@ANDROID_PACKAGE_NAME@.BROWSER"
                   android:alwaysRetainTaskState="true"
-                  android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection"
+                  android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleTask"
                   android:exported="true"
                   android:theme="@style/Gecko.App">
 
             <!-- android:priority ranges between -1000 and 1000.  We never want
                  another activity to usurp the MAIN action, so we ratchet our
                  priority up. -->
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -2,38 +2,36 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.Manifest;
 import android.app.DownloadManager;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
 import android.os.Environment;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import org.json.JSONArray;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
-import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.cleanup.FileCleanupController;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.distribution.Distribution.DistributionDescriptor;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
-import org.mozilla.gecko.dlc.catalog.DownloadContent;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
@@ -81,16 +79,17 @@ import org.mozilla.gecko.telemetry.Telem
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.Experiments;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.NativeEventListener;
@@ -125,16 +124,17 @@ import android.nfc.NfcAdapter;
 import android.nfc.NfcEvent;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.StrictMode;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.ContextCompat;
 import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Base64;
 import android.util.Base64OutputStream;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
@@ -358,17 +358,25 @@ public class BrowserApp extends GeckoApp
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
                 }
                 break;
             case PAGE_SHOW:
                 tab.loadFavicon();
                 break;
             case BOOKMARK_ADDED:
-                showBookmarkAddedSnackbar();
+                // We always show the special offline snackbar whenever we bookmark a reader page.
+                // It's possible that the page is already stored offline, however this is highly
+                // unlikely, and even so it is probably nicer to show the same offline notification
+                // every time we bookmark an about:reader page.
+                if (!AboutPages.isAboutReader(tab.getURL())) {
+                    showBookmarkAddedSnackbar();
+                } else {
+                    showReaderModeBookmarkAddedSnackbar();
+                }
                 break;
             case BOOKMARK_REMOVED:
                 showBookmarkRemovedSnackbar();
                 break;
 
             case UNSELECTED:
                 // We receive UNSELECTED immediately after the SELECTED listeners run
                 // so we are ensured that the unselectedTabEditingText has not changed.
@@ -437,16 +445,36 @@ public class BrowserApp extends GeckoApp
 
         SnackbarHelper.showSnackbarWithAction(this,
                 getResources().getString(R.string.bookmark_added),
                 Snackbar.LENGTH_LONG,
                 getResources().getString(R.string.bookmark_options),
                 callback);
     }
 
+    private void showReaderModeBookmarkAddedSnackbar() {
+        final Drawable iconDownloaded = DrawableUtil.tintDrawable(getContext(), R.drawable.status_icon_readercache, Color.WHITE);
+
+        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
+            @Override
+            public void onClick(View v) {
+                openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
+            }
+        };
+
+        SnackbarHelper.showSnackbarWithActionAndColors(this,
+                getResources().getString(R.string.reader_saved_offline),
+                Snackbar.LENGTH_LONG,
+                getResources().getString(R.string.reader_switch_to_bookmarks),
+                callback,
+                iconDownloaded,
+                ContextCompat.getColor(this, R.color.link_blue),
+                Color.WHITE);
+    }
+
     private void showBookmarkRemovedSnackbar() {
         SnackbarHelper.showSnackbar(this, getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
     }
 
     @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
         if (AndroidGamepadManager.handleKeyEvent(event)) {
             return true;
@@ -3186,16 +3214,18 @@ public class BrowserApp extends GeckoApp
         // In ICS+, it's easy to kill an app through the task switcher.
         final boolean visible = Versions.preICS ||
                                 HardwareUtils.isTelevision() ||
                                 !PrefUtils.getStringSet(GeckoSharedPrefs.forProfile(this),
                                                         ClearOnShutdownPref.PREF,
                                                         new HashSet<String>()).isEmpty();
         aMenu.findItem(R.id.quit).setVisible(visible);
 
+        // If tab data is unavailable we disable most of the context menu and related items and
+        // return early.
         if (tab == null || tab.getURL() == null) {
             bookmark.setEnabled(false);
             back.setEnabled(false);
             forward.setEnabled(false);
             share.setEnabled(false);
             quickShare.setEnabled(false);
             saveAsPDF.setEnabled(false);
             print.setEnabled(false);
@@ -3209,18 +3239,24 @@ public class BrowserApp extends GeckoApp
             }
             MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false);
             MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false);
             MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false);
 
             return true;
         }
 
+        // If tab data IS available we need to manually enable items as necessary. They may have
+        // been disabled if returning early above, hence every item must be toggled, even if it's
+        // always expected to be enabled (e.g. the bookmark star is always enabled, except when
+        // we don't have tab data).
+
         final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
 
+        bookmark.setEnabled(true); // Might have been disabled above, ensure it's reenabled
         bookmark.setVisible(!inGuestMode);
         bookmark.setCheckable(true);
         bookmark.setChecked(tab.isBookmark());
         bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));
 
         if (Versions.feature11Plus) {
             // We don't use icons on GB builds so not resolving icons might conserve resources.
             bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark()));
--- a/mobile/android/base/java/org/mozilla/gecko/Locales.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Locales.java
@@ -9,16 +9,17 @@ import java.util.Locale;
 
 import org.mozilla.gecko.LocaleManager;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.StrictMode;
 import android.support.v4.app.FragmentActivity;
+import android.support.v7.app.AppCompatActivity;
 
 /**
  * This is a helper class to do typical locale switching operations without
  * hitting StrictMode errors or adding boilerplate to common activity
  * subclasses.
  *
  * Either call {@link Locales#initializeLocale(Context)} in your
  * <code>onCreate</code> method, or inherit from
@@ -42,16 +43,24 @@ public class Locales {
         StrictMode.allowThreadDiskWrites();
         try {
             localeManager.getAndApplyPersistedLocale(context);
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
+    public static class LocaleAwareAppCompatActivity extends AppCompatActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            Locales.initializeLocale(getApplicationContext());
+            super.onCreate(savedInstanceState);
+        }
+
+    }
     public static class LocaleAwareFragmentActivity extends FragmentActivity {
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             Locales.initializeLocale(getApplicationContext());
             super.onCreate(savedInstanceState);
         }
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/SnackbarHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/SnackbarHelper.java
@@ -4,20 +4,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeJSObject;
 
 import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.support.design.widget.Snackbar;
 import android.support.v4.content.ContextCompat;
 import android.text.TextUtils;
+import android.util.TypedValue;
 import android.view.View;
+import android.widget.TextView;
 
 import java.lang.ref.WeakReference;
 
 /**
  * Helper class for creating and dismissing snackbars. Use this class to guarantee a consistent style and behavior
  * across the app.
  */
 public class SnackbarHelper {
@@ -93,25 +97,56 @@ public class SnackbarHelper {
      *
      * @param activity Activity to show the snackbar in.
      * @param message The text to show. Can be formatted text.
      * @param duration How long to display the message.
      * @param action Action text to display.
      * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
      */
     public static void showSnackbarWithAction(Activity activity, String message, int duration, String action, SnackbarCallback callback) {
+        showSnackbarWithActionAndColors(activity, message, duration, action, callback, null, null, null);
+    }
+
+
+    public static void showSnackbarWithActionAndColors(Activity activity,
+                                                       String message,
+                                                       int duration,
+                                                       String action,
+                                                       SnackbarCallback callback,
+                                                       Drawable icon,
+                                                       Integer backgroundColor,
+                                                       Integer actionColor) {
         final View parentView = findBestParentView(activity);
         final Snackbar snackbar = Snackbar.make(parentView, message, duration);
 
         if (callback != null && !TextUtils.isEmpty(action)) {
             snackbar.setAction(action, callback);
-            snackbar.setActionTextColor(ContextCompat.getColor(activity, R.color.fennec_ui_orange));
+            if (actionColor == null) {
+                ContextCompat.getColor(activity, R.color.fennec_ui_orange);
+            } else {
+                snackbar.setActionTextColor(actionColor);
+            }
             snackbar.setCallback(callback);
         }
 
+        if (icon != null) {
+            int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, activity.getResources().getDisplayMetrics());
+
+            final InsetDrawable paddedIcon = new InsetDrawable(icon, 0, 0, leftPadding, 0);
+
+            paddedIcon.setBounds(0, 0, leftPadding + icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+
+            TextView textView = (TextView) snackbar.getView().findViewById(android.support.design.R.id.snackbar_text);
+            textView.setCompoundDrawables(paddedIcon, null, null, null);
+        }
+
+        if (backgroundColor != null) {
+            snackbar.getView().setBackgroundColor(backgroundColor);
+        }
+
         snackbar.show();
 
         synchronized (currentSnackbarLock) {
             currentSnackbar = new WeakReference<>(snackbar);
         }
     }
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java
@@ -33,17 +33,17 @@ public abstract class SharedBrowserDatab
 
     private static PerProfileDatabases<BrowserDatabaseHelper> databases;
 
     @Override
     protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
         return databases;
     }
 
-    // Can't mark as @Override. Added in API 11.
+    @Override
     public void shutdown() {
         synchronized (SharedBrowserDatabaseProvider.class) {
             databases.shutdown();
             databases = null;
         }
     }
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -30,17 +30,17 @@ import android.content.SharedPreferences
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class HomeConfigPrefsBackend implements HomeConfigBackend {
     private static final String LOGTAG = "GeckoHomeConfigBackend";
 
     // Increment this to trigger a migration.
-    private static final int VERSION = 4;
+    private static final int VERSION = 5;
 
     // This key was originally used to store only an array of panel configs.
     public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
 
     // This key is now used to store a version number with the array of panel configs.
     public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
 
     // Keys used with JSON object stored in prefs.
@@ -181,16 +181,17 @@ public class HomeConfigPrefsBackend impl
 
         if (historyIndex == -1 || syncIndex == -1) {
             throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
         }
 
         PanelConfig newPanel;
         int replaceIndex;
         int removeIndex;
+
         if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
             // Replace the Sync panel if it's visible and the History panel is disabled.
             replaceIndex = syncIndex;
             removeIndex = historyIndex;
             newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags);
         } else {
             // Otherwise, just replace the History panel.
             replaceIndex = historyIndex;
@@ -208,16 +209,35 @@ public class HomeConfigPrefsBackend impl
             } else {
                 newArray.put(jsonPanels.get(i));
             }
         }
 
         return newArray;
     }
 
+    private static void ensureDefaultPanelForV5(Context context, JSONArray jsonPanels) throws JSONException {
+        int historyIndex = -1;
+
+        for (int i = 0; i < jsonPanels.length(); i++) {
+            final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
+            if (panelConfig.isDefault()) {
+                return;
+            }
+
+            if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
+                historyIndex = i;
+            }
+        }
+
+        // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
+        final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
+        jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
+    }
+
     /**
      * Checks to see if the reading list panel already exists.
      *
      * @param jsonPanels JSONArray array representing the curent set of panel configs.
      *
      * @return boolean Whether or not the reading list panel exists.
      */
     private static boolean readingListPanelExists(JSONArray jsonPanels) {
@@ -311,16 +331,21 @@ public class HomeConfigPrefsBackend impl
                     break;
 
                 case 4:
                     // Combine the History and Sync panels. In order to minimize an unexpected reordering
                     // of panels, we try to replace the History panel if it's visible, and fall back to
                     // the Sync panel if that's visible.
                     jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
                     break;
+
+                case 5:
+                    // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
+                    ensureDefaultPanelForV5(context, jsonPanels);
+                    break;
             }
         }
 
         // Save the new panel config and the new version number.
         final JSONObject newJson = new JSONObject();
         newJson.put(JSON_KEY_PANELS, jsonPanels);
         newJson.put(JSON_KEY_VERSION, VERSION);
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
@@ -140,17 +140,17 @@ class SearchEngineRow extends AnimatedHe
         mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout);
         mIconView = (FaviconView) findViewById(R.id.suggestion_icon);
 
         // User-entered search term is first suggestion
         mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered);
         mUserEnteredView.setOnClickListener(mClickListener);
 
         mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text);
-        mSearchHistorySuggestionIcon = DrawableUtil.tintDrawable(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey);
+        mSearchHistorySuggestionIcon = DrawableUtil.tintDrawableWithColorRes(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey);
 
         // Suggestion limits
         mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions);
         mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions);
     }
 
     private void setDescriptionOnSuggestion(View v, String suggestion) {
         v.setContentDescription(getResources().getString(R.string.suggestion_for_engine,
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -151,16 +151,17 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_CLEAR_PRIVATE_DATA = NON_PREF_PREFIX + "privacy.clear";
     private static final String PREFS_CLEAR_PRIVATE_DATA_EXIT = NON_PREF_PREFIX + "history.clear_on_exit";
     private static final String PREFS_SCREEN_ADVANCED = NON_PREF_PREFIX + "advanced_screen";
     public static final String PREFS_HOMEPAGE = NON_PREF_PREFIX + "homepage";
     public static final String PREFS_HISTORY_SAVED_SEARCH = NON_PREF_PREFIX + "search.search_history.enabled";
     private static final String PREFS_FAQ_LINK = NON_PREF_PREFIX + "faq.link";
     private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link";
     public static final String PREFS_NOTIFICATIONS_CONTENT = NON_PREF_PREFIX + "notifications.content";
+    public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more";
     public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
@@ -876,17 +877,18 @@ OnSharedPreferenceChangeListener
                     final String url = getResources().getString(R.string.feedback_link, AppConstants.MOZ_APP_VERSION, AppConstants.MOZ_UPDATE_CHANNEL);
                     ((LinkPreference) pref).setUrl(url);
                 } else if (PREFS_DYNAMIC_TOOLBAR.equals(key)) {
                     if (DynamicToolbar.isForceDisabled()) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
-                } else if (PREFS_NOTIFICATIONS_CONTENT.equals(key)) {
+                } else if (PREFS_NOTIFICATIONS_CONTENT.equals(key) ||
+                        PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE.equals(key)) {
                     if (!FeedService.isInExperiment(this)) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
                 }
 
                 // Some Preference UI elements are not actually preferences,
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java
@@ -145,20 +145,20 @@ public class ToolbarEditLayout extends T
         if (!HardwareUtils.isTablet()) {
             return;
         }
 
         // When on tablet show a magnifying glass in editing mode
         final int searchDrawableId = R.drawable.search_icon_active;
         final Drawable searchDrawable;
         if (!isActive) {
-            searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.placeholder_grey);
+            searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.placeholder_grey);
         } else {
             if (isPrivateMode()) {
-                searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
+                searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
             } else {
                 searchDrawable = getResources().getDrawable(searchDrawableId);
             }
         }
 
         mSearchIcon.setImageDrawable(searchDrawable);
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
@@ -2,39 +2,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.util;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.CheckResult;
+import android.support.annotation.ColorInt;
 import android.support.annotation.ColorRes;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.graphics.drawable.DrawableCompat;
 
 public class DrawableUtil {
 
     /**
      * Tints the given drawable with the given color and returns it.
      */
     @CheckResult
-    public static Drawable tintDrawable(@NonNull final Context context, @DrawableRes final int drawableID,
-                @ColorRes final int colorID) {
+    public static Drawable tintDrawable(@NonNull final Context context,
+                                        @DrawableRes final int drawableID,
+                                        @ColorInt final int color) {
         final Drawable icon = DrawableCompat.wrap(
                 ContextCompat.getDrawable(context, drawableID).mutate());
-        DrawableCompat.setTint(icon, ContextCompat.getColor(context, colorID));
+        DrawableCompat.setTint(icon, color);
         return icon;
     }
 
     /**
+     * Tints the given drawable with the given color and returns it.
+     */
+    @CheckResult
+    public static Drawable tintDrawableWithColorRes(@NonNull final Context context,
+                                                    @DrawableRes final int drawableID,
+                                                    @ColorRes final int colorID) {
+        return tintDrawable(context, drawableID, ContextCompat.getColor(context, colorID));
+    }
+
+    /**
      * Tints the given drawable with the given tint list and returns it. Note that you
      * should no longer use the argument Drawable because the argument is not mutated
      * on pre-Lollipop devices but is mutated on L+ due to differences in the Support
      * Library implementation (bug 1193950).
      */
     @CheckResult
     public static Drawable tintDrawableWithStateList(@NonNull final Drawable drawable,
             @NonNull final ColorStateList colorList) {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -65,16 +65,22 @@
 <!ENTITY bookmark_options "Options">
 <!ENTITY screenshot_added_to_bookmarks "Screenshot added to bookmarks">
 <!-- Localization note (screenshot_folder_label_in_bookmarks): We save links to screenshots
      the user takes. The folder we store these links in is located in the bookmarks list
      and is labeled by this String. -->
 <!ENTITY screenshot_folder_label_in_bookmarks "Screenshots">
 <!ENTITY readinglist_smartfolder_label_in_bookmarks "Reading List">
 
+<!ENTITY reader_saved_offline "Saved offline">
+<!-- Localization note (reader_switch_to_bookmarks) : This
+     string is used as an action in a snackbar - it lets you
+     "switch" to the bookmarks (saved items) panel. -->
+<!ENTITY reader_switch_to_bookmarks "Switch">
+
 <!ENTITY history_today_section "Today">
 <!ENTITY history_yesterday_section "Yesterday">
 <!ENTITY history_week_section3 "Last 7 days">
 <!ENTITY history_older_section3 "Older than 6 months">
 
 <!ENTITY search "Search">
 <!ENTITY reload "Reload">
 <!ENTITY forward "Forward">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -143,16 +143,17 @@ stjar.sources += [ thirdparty_source_dir
 stjar.javac_flags = ['-Xlint:none']
 
 services_jar = add_java_jar('services')
 services_jar.sources += sync_java_files
 services_jar.extra_jars = [
     CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
     CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
     CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+    CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB'],
     'constants.jar',
     'gecko-R.jar',
     'gecko-mozglue.jar',
     'gecko-thirdparty.jar',
     'gecko-util.jar',
     'sync-thirdparty.jar',
 ]
 services_jar.javac_flags += ['-Xlint:all,-deprecation']
--- a/mobile/android/base/resources/xml/preferences_notifications.xml
+++ b/mobile/android/base/resources/xml/preferences_notifications.xml
@@ -1,11 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
     <SwitchPreference android:key="android.not_a_preference.notifications.content"
         android:title="@string/pref_content_notifications"
         android:summary="@string/pref_content_notifications_summary"
         android:defaultValue="true" />
+    <org.mozilla.gecko.preferences.AlignRightLinkPreference
+        android:key="android.not_a_preference.notifications.content.learn_more"
+        android:title="@string/pref_learn_more"
+        android:persistent="false"
+        url="https://support.mozilla.org/kb/notifications-firefox-android?utm_source=inproduct&amp;utm_medium=notifications&amp;utm_campaign=mobileandroid" />
     <SwitchPreference android:key="android.not_a_preference.notifications.whats_new"
         android:title="@string/pref_whats_new_notification"
         android:summary="@string/pref_whats_new_notification_summary"
         android:defaultValue="true" />
 </PreferenceScreen>
\ No newline at end of file
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -87,16 +87,19 @@
   <string name="bookmark_already_added">&bookmark_already_added;</string>
   <string name="bookmark_removed">&bookmark_removed;</string>
   <string name="bookmark_updated">&bookmark_updated;</string>
   <string name="bookmark_options">&bookmark_options;</string>
   <string name="screenshot_added_to_bookmarks">&screenshot_added_to_bookmarks;</string>
   <string name="screenshot_folder_label_in_bookmarks">&screenshot_folder_label_in_bookmarks;</string>
   <string name="readinglist_smartfolder_label_in_bookmarks">&readinglist_smartfolder_label_in_bookmarks;</string>
 
+  <string name="reader_saved_offline">&reader_saved_offline;</string>
+  <string name="reader_switch_to_bookmarks">&reader_switch_to_bookmarks;</string>
+
   <string name="history_today_section">&history_today_section;</string>
   <string name="history_yesterday_section">&history_yesterday_section;</string>
   <string name="history_week_section">&history_week_section3;</string>
   <string name="history_older_section">&history_older_section3;</string>
 
   <string name="share">&share;</string>
   <string name="share_title">&share_title;</string>
   <string name="share_image_failed">&share_image_failed;</string>
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -504,17 +504,17 @@ var ActionBarHandler = {
         );
 
         UITelemetry.addEvent("action.1", "actionbar", null, "search");
       },
     },
 
     SEARCH_ADD: {
       id: "search_add_action",
-      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
+      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
       icon: "drawable://ab_add_search_engine",
       order: 0,
       floatingOrder: 8,
 
       selector: {
         matches: function(element, win) {
           if(!(element instanceof HTMLInputElement)) {
             return false;
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -789,17 +789,17 @@ var SelectionHandler = {
 
           return SelectionHandler.isSelectionActive();
         }
       }
     },
 
     SEARCH_ADD: {
       id: "search_add_action",
-      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
+      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
       icon: "drawable://ab_add_search_engine",
 
       selector: {
         matches: function(element) {
           if(!(element instanceof HTMLInputElement)) {
             return false;
           }
           let form = element.form;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4081,19 +4081,25 @@ Tab.prototype = {
         Messaging.sendRequest({
           type: "DOMTitleChanged",
           tabID: this.id,
           title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
         });
         break;
       }
 
+      case "TabPreZombify": {
+        if (!this.playingAudio) {
+          return;
+        }
+        // Fall through to the DOMAudioPlayback events, so the
+        // audio playback indicator gets reset upon zombification.
+      }
       case "DOMAudioPlaybackStarted":
-      case "DOMAudioPlaybackStopped":
-      case "TabPreZombify": {
+      case "DOMAudioPlaybackStopped": {
         if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
             !aEvent.isTrusted) {
           return;
         }
 
         let browser = aEvent.originalTarget;
         if (browser != this.browser) {
           return;
@@ -6667,17 +6673,17 @@ var SearchEngines = {
               formData.push({ name: escapedName, value: escapedValue });
               break;
             }
           }
       }
     }
 
     // prompt user for name of search engine
-    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine2");
+    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
     let title = { value: (aElement.ownerDocument.title || docURI.host) };
     if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
       return;
 
     // fetch the favicon for this page
     let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
     let mDBConn = Services.storage.openDatabase(dbFile);
     let stmts = [];
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -257,17 +257,23 @@ contextmenu.copyImageLocation=Copy Image
 contextmenu.shareImage=Share Image
 # LOCALIZATION NOTE (contextmenu.search):
 # The label of the contextmenu item which allows you to search with your default search engine for
 # the text you have selected. %S is the name of the search engine. For example, "Google".
 contextmenu.search=%S Search
 contextmenu.saveImage=Save Image
 contextmenu.showImage=Show Image
 contextmenu.setImageAs=Set Image As
-contextmenu.addSearchEngine2=Add as Search Engine
+# LOCALIZATION NOTE (contextmenu.addSearchEngine3): This string should be rather short. If it is
+# significantly longer than the translation for the "Paste" action then this might trigger an
+# Android bug positioning the floating text selection partially off the screen. This issue heavily
+# depends on the screen size and the specific translations. For English "Paste" / "Add search engine"
+# is working while "Paste" / "Add as search engine" triggers the bug. See bug 1262098 for more details.
+# Manual testing the scenario described in bug 1262098 is highly recommended.
+contextmenu.addSearchEngine3=Add Search Engine
 contextmenu.playMedia=Play
 contextmenu.pauseMedia=Pause
 contextmenu.shareMedia=Share Video
 contextmenu.showControls2=Show Controls
 contextmenu.mute=Mute
 contextmenu.unmute=Unmute
 contextmenu.saveVideo=Save Video
 contextmenu.saveAudio=Save Audio
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
@@ -5,43 +5,43 @@
 package org.mozilla.gecko.fxa.activities;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountManagerCallback;
 import android.accounts.AccountManagerFuture;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
-import android.app.ActionBar;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.v7.app.ActionBar;
 import android.util.TypedValue;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.widget.Toast;
 import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Locales.LocaleAwareFragmentActivity;
+import org.mozilla.gecko.Locales.LocaleAwareAppCompatActivity;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.sync.Utils;
 
 /**
  * Activity which displays account status.
  */
-public class FxAccountStatusActivity extends LocaleAwareFragmentActivity {
+public class FxAccountStatusActivity extends LocaleAwareAppCompatActivity {
   private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
 
   protected FxAccountStatusFragment statusFragment;
 
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
 
@@ -62,20 +62,21 @@ public class FxAccountStatusActivity ext
    * more information.
    */
   @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
   protected void maybeSetHomeButtonEnabled() {
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
       Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
       return;
     }
-    final ActionBar actionBar = getActionBar();
+    final ActionBar actionBar = getSupportActionBar();
     if (actionBar != null) {
       Logger.debug(LOG_TAG, "Enabling home button.");
       actionBar.setHomeButtonEnabled(true);
+      actionBar.setDisplayHomeAsUpEnabled(true);
       return;
     }
     Logger.debug(LOG_TAG, "Not enabling home button.");
   }
 
   @Override
   public void onResume() {
     super.onResume();
--- a/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
+++ b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
@@ -72,19 +72,21 @@
         <Button
             android:id="@+id/add"
             style="@style/Widget.BaseButton"
             android:layout_width="wrap_content"
             android:layout_height="50dp"
             android:layout_alignParentRight="true"
             android:layout_below="@id/host"
             android:layout_marginBottom="20dp"
-            android:layout_marginLeft="20dp"
+            android:layout_marginLeft="100dp"
             android:layout_marginRight="30dp"
             android:background="@drawable/button_background_action_orange_round"
             android:paddingLeft="16dp"
             android:paddingRight="16dp"
             android:text="@string/promotion_add_to_homescreen"
+            android:maxLines="2"
+            android:ellipsize="end"
             android:textColor="@android:color/white"
             android:textSize="16sp" />
 
     </RelativeLayout>
 </merge>
--- a/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml
+++ b/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml
@@ -7,17 +7,15 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <!-- FxAccountStatusActivity ActionBar -->
     <style name="ActionBar.FxAccountStatusActivity">
          <item name="android:displayOptions">showHome|homeAsUp|showTitle</item>
     </style>
 
-    <style name="FxAccountTheme" parent="@style/Gecko" />
+    <style name="FxAccountTheme" parent="Gecko.Preferences" />
 
     <style name="FxAccountTheme.FxAccountStatusActivity" parent="Gecko.Preferences">
-         <item name="android:windowActionBar">true</item>
-         <item name="android:windowNoTitle">false</item>
          <item name="android:actionBarStyle">@style/ActionBar.FxAccountStatusActivity</item>
     </style>
 
 </resources>
--- a/mobile/android/services/src/main/res/values/fxaccount_styles.xml
+++ b/mobile/android/services/src/main/res/values/fxaccount_styles.xml
@@ -2,17 +2,17 @@
 <!--
    This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="FxAccountTheme" parent="@style/Gecko" />
+    <style name="FxAccountTheme" parent="Gecko.Preferences" />
 
     <style name="FxAccountTheme.FxAccountStatusActivity" parent="@style/FxAccountTheme">
         <item name="android:windowNoTitle">false</item>
     </style>
 
     <style name="FxAccountTextItem" parent="@android:style/TextAppearance.Medium">
         <item name="android:textColor">@color/fxaccount_textColor</item>
         <item name="android:layout_width">fill_parent</item>
--- a/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
+++ b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
@@ -13,17 +13,16 @@ background_junit3_sources = [
     'src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java',
     'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java',
     'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java',
     'src/org/mozilla/gecko/background/db/TestBookmarks.java',
     'src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java',
     'src/org/mozilla/gecko/background/db/TestClientsDatabase.java',
     'src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java',
     'src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java',
-    'src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java',
     'src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java',
     'src/org/mozilla/gecko/background/db/TestPasswordsRepository.java',
     'src/org/mozilla/gecko/background/db/TestTopSites.java',
     'src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java',
     'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
     'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
     'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
     'src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java',
@@ -61,17 +60,16 @@ background_junit3_sources = [
     'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java',
     'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java',
     'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java',
     'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java',
     'src/org/mozilla/gecko/background/sync/TestClientsStage.java',
     'src/org/mozilla/gecko/background/sync/TestResetting.java',
     'src/org/mozilla/gecko/background/sync/TestStoreTracking.java',
     'src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java',
-    'src/org/mozilla/gecko/background/sync/TestTabsRecord.java',
     'src/org/mozilla/gecko/background/sync/TestWebURLFinder.java',
     'src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java',
     'src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java',
     'src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java',
     'src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java',
     'src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java',
     'src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java',
     'src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java',
--- a/mobile/android/tests/background/junit3/instrumentation.ini
+++ b/mobile/android/tests/background/junit3/instrumentation.ini
@@ -8,20 +8,18 @@ subsuite = background
 [src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java]
 [src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java]
 [src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java]
 [src/org/mozilla/gecko/background/db/TestBookmarks.java]
 [src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java]
 [src/org/mozilla/gecko/background/db/TestClientsDatabase.java]
 [src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java]
 [src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java]
-[src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java]
 [src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java]
 [src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
 [src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
 [src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
 [src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
 [src/org/mozilla/gecko/background/sync/TestClientsStage.java]
 [src/org/mozilla/gecko/background/sync/TestResetting.java]
 [src/org/mozilla/gecko/background/sync/TestStoreTracking.java]
 [src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java]
-[src/org/mozilla/gecko/background/sync/TestTabsRecord.java]
 [src/org/mozilla/gecko/background/sync/TestWebURLFinder.java]
deleted file mode 100644
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-package org.mozilla.gecko.background.db;
-
-import org.json.simple.JSONArray;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
-
-import android.app.Activity;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.test.ActivityInstrumentationTestCase2;
-
-/**
- * Exercise Fennec's tabs provider.
- *
- * @author rnewman
- *
- */
-public class TestFennecTabsStorage extends ActivityInstrumentationTestCase2<Activity> {
-  public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
-  public static final String TEST_CLIENT_NAME = "test client name";
-
-  public static final String CLIENTS_GUID_IS = BrowserContract.Clients.GUID + " = ?";
-  public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
-
-  protected Tab testTab1;
-  protected Tab testTab2;
-  protected Tab testTab3;
-
-  public TestFennecTabsStorage() {
-    super(Activity.class);
-  }
-
-  protected ContentProviderClient getClientsClient() {
-    final ContentResolver cr = getInstrumentation().getTargetContext().getApplicationContext().getContentResolver();
-    return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
-  }
-
-  protected ContentProviderClient getTabsClient() {
-    final ContentResolver cr = getInstrumentation().getTargetContext().getApplicationContext().getContentResolver();
-    return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
-  }
-
-  protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
-    if (clientsClient == null) {
-      return -1;
-    }
-    return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String[] { TEST_CLIENT_GUID });
-  }
-
-  protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
-    if (tabsClient == null) {
-      return -1;
-    }
-    return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[]{TEST_CLIENT_GUID});
-  }
-
-  @Override
-  protected void tearDown() throws Exception {
-    deleteAllTestTabs(getTabsClient());
-  }
-
-  protected void insertTestClient(final ContentProviderClient clientsClient) throws RemoteException {
-    ContentValues cv = new ContentValues();
-    cv.put(BrowserContract.Clients.GUID, TEST_CLIENT_GUID);
-    cv.put(BrowserContract.Clients.NAME, TEST_CLIENT_NAME);
-    clientsClient.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, cv);
-  }
-
-  @SuppressWarnings("unchecked")
-  protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
-    final JSONArray history1 = new JSONArray();
-    history1.add("http://test.com/test1.html");
-    testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
-
-    final JSONArray history2 = new JSONArray();
-    history2.add("http://test.com/test2.html#1");
-    history2.add("http://test.com/test2.html#2");
-    history2.add("http://test.com/test2.html#3");
-    testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
-
-    final JSONArray history3 = new JSONArray();
-    history3.add("http://test.com/test3.html#1");
-    history3.add("http://test.com/test3.html#2");
-    testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
-
-    tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
-    tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
-    tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
-  }
-
-  // Sanity.
-  public void testObtainCP() {
-    final ContentProviderClient clientsClient = getClientsClient();
-    assertNotNull(clientsClient);
-    clientsClient.release();
-
-    final ContentProviderClient tabsClient = getTabsClient();
-    assertNotNull(tabsClient);
-    tabsClient.release();
-  }
-
-  public void testWipeClients() throws RemoteException {
-    final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
-    final ContentProviderClient clientsClient = getClientsClient();
-
-    // Have to ensure that it's empty…
-    clientsClient.delete(uri, null, null);
-
-    int deleted = clientsClient.delete(uri, null, null);
-    assertEquals(0, deleted);
-  }
-
-  public void testWipeTabs() throws RemoteException {
-    final ContentProviderClient tabsClient = getTabsClient();
-
-    // Have to ensure that it's empty…
-    deleteAllTestTabs(tabsClient);
-
-    int deleted = deleteAllTestTabs(tabsClient);
-    assertEquals(0, deleted);
-  }
-
-  public void testStoreAndRetrieveClients() throws RemoteException {
-    final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
-    final ContentProviderClient clientsClient = getClientsClient();
-
-    // Have to ensure that it's empty…
-    clientsClient.delete(uri, null, null);
-
-    final long now = System.currentTimeMillis();
-    final ContentValues first = new ContentValues();
-    final ContentValues second = new ContentValues();
-    first.put(BrowserContract.Clients.GUID, "abcdefghijkl");
-    first.put(BrowserContract.Clients.NAME, "Frist Psot");
-    first.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
-    second.put(BrowserContract.Clients.GUID, "mnopqrstuvwx");
-    second.put(BrowserContract.Clients.NAME, "Second!!1!");
-    second.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
-
-    ContentValues[] values = new ContentValues[] { first, second };
-    final int inserted = clientsClient.bulkInsert(uri, values);
-    assertEquals(2, inserted);
-
-    final String since = BrowserContract.Clients.LAST_MODIFIED + " >= ?";
-    final String[] nowArg = new String[] { String.valueOf(now) };
-    final String guidAscending = BrowserContract.Clients.GUID + " ASC";
-    Cursor cursor = clientsClient.query(uri, null, since, nowArg, guidAscending);
-
-    assertNotNull(cursor);
-    try {
-      assertTrue(cursor.moveToFirst());
-      assertEquals(2, cursor.getCount());
-
-      final String g1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
-      final String n1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
-      final long m1   = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
-      assertEquals(first.get(BrowserContract.Clients.GUID), g1);
-      assertEquals(first.get(BrowserContract.Clients.NAME), n1);
-      assertEquals(now + 1, m1);
-
-      assertTrue(cursor.moveToNext());
-      final String g2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
-      final String n2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
-      final long m2   = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
-      assertEquals(second.get(BrowserContract.Clients.GUID), g2);
-      assertEquals(second.get(BrowserContract.Clients.NAME), n2);
-      assertEquals(now + 2, m2);
-
-      assertFalse(cursor.moveToNext());
-    } finally {
-      cursor.close();
-    }
-
-    int deleted = clientsClient.delete(uri, null, null);
-    assertEquals(2, deleted);
-  }
-
-  public void testTabFromCursor() throws Exception {
-    final ContentProviderClient tabsClient = getTabsClient();
-    final ContentProviderClient clientsClient = getClientsClient();
-
-    deleteAllTestTabs(tabsClient);
-    deleteTestClient(clientsClient);
-    insertTestClient(clientsClient);
-    insertSomeTestTabs(tabsClient);
-
-    final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
-    Cursor cursor = null;
-    try {
-      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
-      assertEquals(3, cursor.getCount());
-
-      cursor.moveToFirst();
-      final Tab parsed1 = Tab.fromCursor(cursor);
-      assertEquals(testTab1, parsed1);
-
-      cursor.moveToNext();
-      final Tab parsed2 = Tab.fromCursor(cursor);
-      assertEquals(testTab2, parsed2);
-
-      cursor.moveToPosition(2);
-      final Tab parsed3 = Tab.fromCursor(cursor);
-      assertEquals(testTab3, parsed3);
-    } finally {
-      cursor.close();
-    }
-  }
-
-  public void testDeletingClientDeletesTabs() throws Exception {
-    final ContentProviderClient tabsClient = getTabsClient();
-    final ContentProviderClient clientsClient = getClientsClient();
-
-    deleteAllTestTabs(tabsClient);
-    deleteTestClient(clientsClient);
-    insertTestClient(clientsClient);
-    insertSomeTestTabs(tabsClient);
-
-    // Delete just the client...
-    clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String [] { TEST_CLIENT_GUID });
-
-    Cursor cursor = null;
-    try {
-      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, null);
-      // ... and all that client's tabs should be removed.
-      assertEquals(0, cursor.getCount());
-    } finally {
-      cursor.close();
-    }
-  }
-}
deleted file mode 100644
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestTabsRecord.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-package org.mozilla.gecko.background.sync;
-
-import org.mozilla.gecko.background.db.CursorDumper;
-import org.mozilla.gecko.background.db.TestFennecTabsStorage;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
-import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
-import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
-
-import android.content.ContentProviderClient;
-import android.database.Cursor;
-
-public class TestTabsRecord extends TestFennecTabsStorage {
-  public void testTabsRecordFromCursor() throws Exception {
-    final ContentProviderClient tabsClient = getTabsClient();
-
-    deleteAllTestTabs(tabsClient);
-    insertTestClient(getClientsClient());
-    insertSomeTestTabs(tabsClient);
-
-    final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
-    Cursor cursor = null;
-    try {
-      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
-      assertEquals(3, cursor.getCount());
-
-      cursor.moveToPosition(1);
-
-      final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
-
-      // Make sure we clean up after ourselves.
-      assertEquals(1, cursor.getPosition());
-
-      assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
-      assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
-
-      assertEquals(3, tabsRecord.tabs.size());
-      assertEquals(testTab1, tabsRecord.tabs.get(0));
-      assertEquals(testTab2, tabsRecord.tabs.get(1));
-      assertEquals(testTab3, tabsRecord.tabs.get(2));
-
-      assertEquals(Math.max(Math.max(testTab1.lastUsed, testTab2.lastUsed), testTab3.lastUsed), tabsRecord.lastModified);
-    } finally {
-      cursor.close();
-    }
-  }
-
-  // Verify that we can fetch a record when there are no local tabs at all.
-  public void testEmptyTabsRecordFromCursor() throws Exception {
-    final ContentProviderClient tabsClient = getTabsClient();
-
-    deleteAllTestTabs(tabsClient);
-
-    final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
-    Cursor cursor = null;
-    try {
-      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
-      assertEquals(0, cursor.getCount());
-
-      final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
-
-      assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
-      assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
-
-      assertNotNull(tabsRecord.tabs);
-      assertEquals(0, tabsRecord.tabs.size());
-
-      assertEquals(0, tabsRecord.lastModified);
-    } finally {
-      cursor.close();
-    }
-  }
-
-  // Not much of a test, but verifies the tabs record at least agrees with the
-  // disk data and doubles as a database inspector.
-  public void testLocalTabs() throws Exception {
-    final ContentProviderClient tabsClient = getTabsClient();
-
-    final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
-    Cursor cursor = null;
-    try {
-      // Keep this in sync with the Fennec schema.
-      cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, BrowserContract.Tabs.CLIENT_GUID + " IS NULL", null, positionAscending);
-      CursorDumper.dumpCursor(cursor);
-
-      final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
-
-      assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
-      assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
-
-      assertNotNull(tabsRecord.tabs);
-      assertEquals(cursor.getCount(), tabsRecord.tabs.size());
-    } finally {
-      cursor.close();
-    }
-  }
-}
--- a/mobile/android/tests/background/junit4/resources/robolectric.properties
+++ b/mobile/android/tests/background/junit4/resources/robolectric.properties
@@ -1,2 +1,3 @@
 sdk=21
 constants=org.mozilla.gecko.BuildConfig
+packageName=org.mozilla.gecko
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.mozilla.gecko.db.BrowserContract;
+
+import java.util.ArrayList;
+
+/**
+ * Wrap a ContentProvider, appending &test=1 to all queries.
+ */
+public class DelegatingTestContentProvider extends ContentProvider {
+    protected final ContentProvider mTargetProvider;
+
+    protected static Uri appendUriParam(Uri uri, String param, String value) {
+        return uri.buildUpon().appendQueryParameter(param, value).build();
+    }
+
+    public DelegatingTestContentProvider(ContentProvider targetProvider) {
+        super();
+        mTargetProvider = targetProvider;
+    }
+
+    private Uri appendTestParam(Uri uri) {
+        return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+    }
+
+    @Override
+    public boolean onCreate() {
+        return mTargetProvider.onCreate();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return mTargetProvider.getType(uri);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return mTargetProvider.insert(appendTestParam(uri), values);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+                      String[] selectionArgs) {
+        return mTargetProvider.update(appendTestParam(uri), values,
+                selection, selectionArgs);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+                        String[] selectionArgs, String sortOrder) {
+        return mTargetProvider.query(appendTestParam(uri), projection, selection,
+                selectionArgs, sortOrder);
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        return mTargetProvider.applyBatch(operations);
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        return mTargetProvider.bulkInsert(appendTestParam(uri), values);
+    }
+
+    public ContentProvider getTargetProvider() {
+        return mTargetProvider;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java
@@ -0,0 +1,338 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.json.simple.JSONArray;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.TabsProvider;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+import org.robolectric.shadows.ShadowContentResolver;
+
+@RunWith(TestRunner.class)
+public class TestTabsProvider {
+    public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
+    public static final String TEST_CLIENT_NAME = "test client name";
+
+    public static final String CLIENTS_GUID_IS = BrowserContract.Clients.GUID + " = ?";
+    public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
+
+    protected Tab testTab1;
+    protected Tab testTab2;
+    protected Tab testTab3;
+
+    protected TabsProvider provider;
+
+    @Before
+    public void setUp() {
+        provider = new TabsProvider();
+        provider.onCreate();
+        ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        provider.shutdown();
+        provider = null;
+    }
+
+    protected ContentProviderClient getClientsClient() {
+        final ShadowContentResolver cr = new ShadowContentResolver();
+        return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+    }
+
+    protected ContentProviderClient getTabsClient() {
+        final ShadowContentResolver cr = new ShadowContentResolver();
+        return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
+    }
+
+    protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+        if (clientsClient == null) {
+            throw new IllegalStateException("Provided ContentProviderClient is null");
+        }
+        return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String[] { TEST_CLIENT_GUID });
+    }
+
+    protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
+        if (tabsClient == null) {
+            throw new IllegalStateException("Provided ContentProviderClient is null");
+        }
+        return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID });
+    }
+
+    protected void insertTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+        ContentValues cv = new ContentValues();
+        cv.put(BrowserContract.Clients.GUID, TEST_CLIENT_GUID);
+        cv.put(BrowserContract.Clients.NAME, TEST_CLIENT_NAME);
+        clientsClient.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, cv);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
+        final JSONArray history1 = new JSONArray();
+        history1.add("http://test.com/test1.html");
+        testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
+
+        final JSONArray history2 = new JSONArray();
+        history2.add("http://test.com/test2.html#1");
+        history2.add("http://test.com/test2.html#2");
+        history2.add("http://test.com/test2.html#3");
+        testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
+
+        final JSONArray history3 = new JSONArray();
+        history3.add("http://test.com/test3.html#1");
+        history3.add("http://test.com/test3.html#2");
+        testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
+
+        tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
+        tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
+        tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
+    }
+
+    // Sanity.
+    @Test
+    public void testObtainCP() {
+        final ContentProviderClient clientsClient = getClientsClient();
+        Assert.assertNotNull(clientsClient);
+        clientsClient.release();
+
+        final ContentProviderClient tabsClient = getTabsClient();
+        Assert.assertNotNull(tabsClient);
+        tabsClient.release();
+    }
+
+    @Test
+    public void testDeleteEmptyClients() throws RemoteException {
+        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+        final ContentProviderClient clientsClient = getClientsClient();
+
+        // Have to ensure that it's empty…
+        clientsClient.delete(uri, null, null);
+
+        int deleted = clientsClient.delete(uri, null, null);
+        Assert.assertEquals(0, deleted);
+    }
+
+    @Test
+    public void testDeleteEmptyTabs() throws RemoteException {
+        final ContentProviderClient tabsClient = getTabsClient();
+
+        // Have to ensure that it's empty…
+        deleteAllTestTabs(tabsClient);
+
+        int deleted = deleteAllTestTabs(tabsClient);
+        Assert.assertEquals(0, deleted);
+    }
+
+    @Test
+    public void testStoreAndRetrieveClients() throws RemoteException {
+        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+        final ContentProviderClient clientsClient = getClientsClient();
+
+        // Have to ensure that it's empty…
+        clientsClient.delete(uri, null, null);
+
+        final long now = System.currentTimeMillis();
+        final ContentValues first = new ContentValues();
+        final ContentValues second = new ContentValues();
+        first.put(BrowserContract.Clients.GUID, "abcdefghijkl");
+        first.put(BrowserContract.Clients.NAME, "Frist Psot");
+        first.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+        second.put(BrowserContract.Clients.GUID, "mnopqrstuvwx");
+        second.put(BrowserContract.Clients.NAME, "Second!!1!");
+        second.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+        ContentValues[] values = new ContentValues[] { first, second };
+        final int inserted = clientsClient.bulkInsert(uri, values);
+        Assert.assertEquals(2, inserted);
+
+        final String since = BrowserContract.Clients.LAST_MODIFIED + " >= ?";
+        final String[] nowArg = new String[] { String.valueOf(now) };
+        final String guidAscending = BrowserContract.Clients.GUID + " ASC";
+        Cursor cursor = clientsClient.query(uri, null, since, nowArg, guidAscending);
+
+        Assert.assertNotNull(cursor);
+        try {
+            Assert.assertTrue(cursor.moveToFirst());
+            Assert.assertEquals(2, cursor.getCount());
+
+            final String g1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
+            final String n1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
+            final long m1   = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
+            Assert.assertEquals(first.get(BrowserContract.Clients.GUID), g1);
+            Assert.assertEquals(first.get(BrowserContract.Clients.NAME), n1);
+            Assert.assertEquals(now + 1, m1);
+
+            Assert.assertTrue(cursor.moveToNext());
+            final String g2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
+            final String n2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
+            final long m2   = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
+            Assert.assertEquals(second.get(BrowserContract.Clients.GUID), g2);
+            Assert.assertEquals(second.get(BrowserContract.Clients.NAME), n2);
+            Assert.assertEquals(now + 2, m2);
+
+            Assert.assertFalse(cursor.moveToNext());
+        } finally {
+            cursor.close();
+        }
+
+        int deleted = clientsClient.delete(uri, null, null);
+        Assert.assertEquals(2, deleted);
+    }
+
+    @Test
+    public void testTabFromCursor() throws Exception {
+        final ContentProviderClient tabsClient = getTabsClient();
+        final ContentProviderClient clientsClient = getClientsClient();
+
+        deleteAllTestTabs(tabsClient);
+        deleteTestClient(clientsClient);
+        insertTestClient(clientsClient);
+        insertSomeTestTabs(tabsClient);
+
+        final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+        Cursor cursor = null;
+        try {
+            cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+            Assert.assertEquals(3, cursor.getCount());
+
+            cursor.moveToFirst();
+            final Tab parsed1 = Tab.fromCursor(cursor);
+            Assert.assertEquals(testTab1, parsed1);
+
+            cursor.moveToNext();
+            final Tab parsed2 = Tab.fromCursor(cursor);
+            Assert.assertEquals(testTab2, parsed2);
+
+            cursor.moveToPosition(2);
+            final Tab parsed3 = Tab.fromCursor(cursor);
+            Assert.assertEquals(testTab3, parsed3);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Test
+    public void testDeletingClientDeletesTabs() throws Exception {
+        final ContentProviderClient tabsClient = getTabsClient();
+        final ContentProviderClient clientsClient = getClientsClient();
+
+        deleteAllTestTabs(tabsClient);
+        deleteTestClient(clientsClient);
+        insertTestClient(clientsClient);
+        insertSomeTestTabs(tabsClient);
+
+        // Delete just the client...
+        clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String [] { TEST_CLIENT_GUID });
+
+        Cursor cursor = null;
+        try {
+            cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, null);
+            // ... and all that client's tabs should be removed.
+            Assert.assertEquals(0, cursor.getCount());
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Test
+    public void testTabsRecordFromCursor() throws Exception {
+        final ContentProviderClient tabsClient = getTabsClient();
+
+        deleteAllTestTabs(tabsClient);
+        insertTestClient(getClientsClient());
+        insertSomeTestTabs(tabsClient);
+
+        final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+        Cursor cursor = null;
+        try {
+            cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+            Assert.assertEquals(3, cursor.getCount());
+
+            cursor.moveToPosition(1);
+
+            final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+            // Make sure we clean up after ourselves.
+            Assert.assertEquals(1, cursor.getPosition());
+
+            Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+            Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+            Assert.assertEquals(3, tabsRecord.tabs.size());
+            Assert.assertEquals(testTab1, tabsRecord.tabs.get(0));
+            Assert.assertEquals(testTab2, tabsRecord.tabs.get(1));
+            Assert.assertEquals(testTab3, tabsRecord.tabs.get(2));
+
+            Assert.assertEquals(Math.max(Math.max(testTab1.lastUsed, testTab2.lastUsed), testTab3.lastUsed), tabsRecord.lastModified);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    // Verify that we can fetch a record when there are no local tabs at all.
+    @Test
+    public void testEmptyTabsRecordFromCursor() throws Exception {
+        final ContentProviderClient tabsClient = getTabsClient();
+
+        deleteAllTestTabs(tabsClient);
+
+        final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+        Cursor cursor = null;
+        try {
+            cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+            Assert.assertEquals(0, cursor.getCount());
+
+            final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+            Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+            Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+            Assert.assertNotNull(tabsRecord.tabs);
+            Assert.assertEquals(0, tabsRecord.tabs.size());
+
+            Assert.assertEquals(0, tabsRecord.lastModified);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    // Not much of a test, but verifies the tabs record at least agrees with the
+    // disk data and doubles as a database inspector.
+    @Test
+    public void testLocalTabs() throws Exception {
+        final ContentProviderClient tabsClient = getTabsClient();
+
+        final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+        Cursor cursor = null;
+        try {
+            // Keep this in sync with the Fennec schema.
+            cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, BrowserContract.Tabs.CLIENT_GUID + " IS NULL", null, positionAscending);
+            CursorDumper.dumpCursor(cursor);
+
+            final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+            Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+            Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+            Assert.assertNotNull(tabsRecord.tabs);
+            Assert.assertEquals(cursor.getCount(), tabsRecord.tabs.size());
+        } finally {
+            cursor.close();
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java
@@ -0,0 +1,244 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.LocalTabsAccessor;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.db.TabsProvider;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.internal.runtime.RuntimeAdapter;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import java.util.List;
+
+@RunWith(TestRunner.class)
+public class TestTabsProviderRemoteTabs {
+    private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
+    private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
+    private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
+
+    protected TabsProvider provider;
+
+    @Before
+    public void setUp() {
+        provider = new TabsProvider();
+        provider.onCreate();
+        ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        provider.shutdown();
+        provider = null;
+    }
+
+    protected ContentProviderClient getClientsClient() {
+        final ShadowContentResolver cr = new ShadowContentResolver();
+        return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+    }
+
+    @Test
+    public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception {
+        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+        final ContentProviderClient cpc = getClientsClient();
+        final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
+
+        try {
+            // Delete all tabs to begin with.
+            cpc.delete(uri, null, null);
+            Cursor allClients = cpc.query(uri, null, null, null, null);
+            try {
+                Assert.assertEquals(0, allClients.getCount());
+            } finally {
+                allClients.close();
+            }
+
+            // Insert a local and remote1 client record, neither with tabs.
+            final long now = System.currentTimeMillis();
+            // Local client has GUID = null.
+            final ContentValues local = new ContentValues();
+            local.put(BrowserContract.Clients.NAME, "local");
+            local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+            // Remote clients have GUID != null.
+            final ContentValues remote1 = new ContentValues();
+            remote1.put(BrowserContract.Clients.GUID, "guid1");
+            remote1.put(BrowserContract.Clients.NAME, "remote1");
+            remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+            final ContentValues remote2 = new ContentValues();
+            remote2.put(BrowserContract.Clients.GUID, "guid2");
+            remote2.put(BrowserContract.Clients.NAME, "remote2");
+            remote2.put(BrowserContract.Clients.LAST_MODIFIED, now + 3);
+
+            ContentValues[] values = new ContentValues[]{local, remote1, remote2};
+            int inserted = cpc.bulkInsert(uri, values);
+            Assert.assertEquals(3, inserted);
+
+            allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, null, null, null);
+            try {
+                CursorDumper.dumpCursor(allClients);
+                // The local client is not ignored.
+                Assert.assertEquals(3, allClients.getCount());
+                final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
+                Assert.assertEquals(3, clients.size());
+                for (RemoteClient client : clients) {
+                    // Each client should not have any tabs.
+                    Assert.assertNotNull(client.tabs);
+                    Assert.assertEquals(0, client.tabs.size());
+                }
+                // Since there are no tabs, the order should be based on last_modified.
+                Assert.assertEquals("guid2", clients.get(0).guid);
+                Assert.assertEquals("guid1", clients.get(1).guid);
+                Assert.assertEquals(null, clients.get(2).guid);
+            } finally {
+                allClients.close();
+            }
+
+            // Now let's add a few tabs to one client.  The times are chosen so that one tab's
+            // last used is not relevant, and the other tab is the most recent used.
+            final ContentValues remoteTab1 = new ContentValues();
+            remoteTab1.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
+            remoteTab1.put(BrowserContract.Tabs.TITLE, "title1");
+            remoteTab1.put(BrowserContract.Tabs.URL, "http://test.com/test1");
+            remoteTab1.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test1\"]");
+            remoteTab1.put(BrowserContract.Tabs.LAST_USED, now);
+            remoteTab1.put(BrowserContract.Tabs.POSITION, 0);
+
+            final ContentValues remoteTab2 = new ContentValues();
+            remoteTab2.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
+            remoteTab2.put(BrowserContract.Tabs.TITLE, "title2");
+            remoteTab2.put(BrowserContract.Tabs.URL, "http://test.com/test2");
+            remoteTab2.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test2\"]");
+            remoteTab2.put(BrowserContract.Tabs.LAST_USED, now + 5);
+            remoteTab2.put(BrowserContract.Tabs.POSITION, 1);
+
+            values = new ContentValues[]{remoteTab1, remoteTab2};
+            inserted = cpc.bulkInsert(BrowserContract.Tabs.CONTENT_URI, values);
+            Assert.assertEquals(2, inserted);
+
+            allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, BrowserContract.Clients.GUID + " IS NOT NULL", null, null);
+            try {
+                CursorDumper.dumpCursor(allClients);
+                // The local client is ignored.
+                Assert.assertEquals(2, allClients.getCount());
+                final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
+                Assert.assertEquals(2, clients.size());
+                for (RemoteClient client : clients) {
+                    // Each client should be remote and should not have any tabs.
+                    Assert.assertNotNull(client.guid);
+                    Assert.assertNotNull(client.tabs);
+                    Assert.assertEquals(0, client.tabs.size());
+                }
+                // Since now there is a tab attached to the remote2 client more recent than the
+                // remote1 client modified time, it should be first.
+                Assert.assertEquals("guid1", clients.get(0).guid);
+                Assert.assertEquals("guid2", clients.get(1).guid);
+            } finally {
+                allClients.close();
+            }
+        } finally {
+            cpc.release();
+        }
+    }
+
+    @Test
+    public void testGetRecentRemoteClientsUpToOneWeekOld() throws Exception {
+        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+        final ContentProviderClient cpc = getClientsClient();
+        final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
+        final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+        try {
+            // Start Clean
+            cpc.delete(uri, null, null);
+            final Cursor allClients = cpc.query(uri, null, null, null, null);
+            try {
+                Assert.assertEquals(0, allClients.getCount());
+            } finally {
+                allClients.close();
+            }
+
+            // Insert a local and remote1 client record, neither with tabs.
+            final long now = System.currentTimeMillis();
+            // Local client has GUID = null.
+            final ContentValues local = new ContentValues();
+            local.put(BrowserContract.Clients.NAME, "local");
+            local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+            // Remote clients have GUID != null.
+            final ContentValues remote1 = new ContentValues();
+            remote1.put(BrowserContract.Clients.GUID, "guid1");
+            remote1.put(BrowserContract.Clients.NAME, "remote1");
+            remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+            // Insert a Remote Client that is 6 days old.
+            final ContentValues remote2 = new ContentValues();
+            remote2.put(BrowserContract.Clients.GUID, "guid2");
+            remote2.put(BrowserContract.Clients.NAME, "remote2");
+            remote2.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+            // Insert a Remote Client with the same name as previous but with more than 3 weeks old
+            final ContentValues remote3 = new ContentValues();
+            remote3.put(BrowserContract.Clients.GUID, "guid21");
+            remote3.put(BrowserContract.Clients.NAME, "remote2");
+            remote3.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS - ONE_DAY_IN_MILLISECONDS);
+
+            // Insert another remote client with the same name as previous but with 3 weeks - 1 day old.
+            final ContentValues remote4 = new ContentValues();
+            remote4.put(BrowserContract.Clients.GUID, "guid22");
+            remote4.put(BrowserContract.Clients.NAME, "remote2");
+            remote4.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+            // Insert a Remote Client that is exactly one week old.
+            final ContentValues remote5 = new ContentValues();
+            remote5.put(BrowserContract.Clients.GUID, "guid3");
+            remote5.put(BrowserContract.Clients.NAME, "remote3");
+            remote5.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS);
+
+            ContentValues[] values = new ContentValues[]{local, remote1, remote2, remote3, remote4, remote5};
+            int inserted = cpc.bulkInsert(uri, values);
+            Assert.assertEquals(values.length, inserted);
+
+            final Cursor remoteClients =
+                    accessor.getRemoteClientsByRecencyCursor(context);
+
+            try {
+                CursorDumper.dumpCursor(remoteClients);
+                // Local client is not included.
+                // (remote1, guid1), (remote2, guid2), (remote3, guid3) are expected.
+                Assert.assertEquals(3, remoteClients.getCount());
+
+                // Check the inner data, according to recency.
+                List<RemoteClient> recentRemoteClientsList =
+                        accessor.getClientsWithoutTabsByRecencyFromCursor(remoteClients);
+                Assert.assertEquals(3, recentRemoteClientsList.size());
+                Assert.assertEquals("remote1", recentRemoteClientsList.get(0).name);
+                Assert.assertEquals("guid1", recentRemoteClientsList.get(0).guid);
+                Assert.assertEquals("remote2", recentRemoteClientsList.get(1).name);
+                Assert.assertEquals("guid2", recentRemoteClientsList.get(1).guid);
+                Assert.assertEquals("remote3", recentRemoteClientsList.get(2).name);
+                Assert.assertEquals("guid3", recentRemoteClientsList.get(2).guid);
+            } finally {
+                remoteClients.close();
+            }
+        } finally {
+            cpc.release();
+        }
+    }
+}
--- a/mobile/android/tests/browser/junit3/moz.build
+++ b/mobile/android/tests/browser/junit3/moz.build
@@ -16,17 +16,16 @@ jar.sources += [
     'src/org/mozilla/tests/browser/junit3/TestDistribution.java',
     'src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java',
     'src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java',
     'src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java',
     'src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java',
     'src/org/mozilla/tests/browser/junit3/TestImageDownloader.java',
     'src/org/mozilla/tests/browser/junit3/TestJarReader.java',
     'src/org/mozilla/tests/browser/junit3/TestRawResource.java',
-    'src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java',
     'src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java',
 ]
 jar.generated_sources = [] # None yet -- try to keep it this way.
 jar.javac_flags += ['-Xlint:all']
 
 jar.extra_jars += [
     CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
     CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB'],
deleted file mode 100644
--- a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-package org.mozilla.tests.browser.junit3;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.test.InstrumentationTestCase;
-
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.background.db.CursorDumper;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.LocalTabsAccessor;
-import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
-
-import java.util.List;
-
-public class TestRemoteTabs extends InstrumentationTestCase {
-    private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
-    private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
-    private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
-
-    public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception {
-        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
-        final ContentResolver cr = getInstrumentation().getTargetContext().getContentResolver();
-        final ContentProviderClient cpc = cr.acquireContentProviderClient(uri);
-        final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
-
-        try {
-            // Delete all tabs to begin with.
-            cpc.delete(uri, null, null);
-            Cursor allClients = cpc.query(uri, null, null, null, null);
-            try {
-                assertEquals(0, allClients.getCount());
-            } finally {
-                allClients.close();
-            }
-
-            // Insert a local and remote1 client record, neither with tabs.
-            final long now = System.currentTimeMillis();
-            // Local client has GUID = null.
-            final ContentValues local = new ContentValues();
-            local.put(BrowserContract.Clients.NAME, "local");
-            local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
-            // Remote clients have GUID != null.
-            final ContentValues remote1 = new ContentValues();
-            remote1.put(BrowserContract.Clients.GUID, "guid1");
-            remote1.put(BrowserContract.Clients.NAME, "remote1");
-            remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
-
-            final ContentValues remote2 = new ContentValues();
-            remote2.put(BrowserContract.Clients.GUID, "guid2");
-            remote2.put(BrowserContract.Clients.NAME, "remote2");
-            remote2.put(BrowserContract.Clients.LAST_MODIFIED, now + 3);
-
-            ContentValues[] values = new ContentValues[]{local, remote1, remote2};
-            int inserted = cpc.bulkInsert(uri, values);
-            assertEquals(3, inserted);
-
-            allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, null, null, null);
-            try {
-                CursorDumper.dumpCursor(allClients);
-                // The local client is not ignored.
-                assertEquals(3, allClients.getCount());
-                final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
-                assertEquals(3, clients.size());
-                for (RemoteClient client : clients) {
-                    // Each client should not have any tabs.
-                    assertNotNull(client.tabs);
-                    assertEquals(0, client.tabs.size());
-                }
-                // Since there are no tabs, the order should be based on last_modified.
-                assertEquals("guid2", clients.get(0).guid);
-                assertEquals("guid1", clients.get(1).guid);
-                assertEquals(null, clients.get(2).guid);
-            } finally {
-                allClients.close();
-            }
-
-            // Now let's add a few tabs to one client.  The times are chosen so that one tab's
-            // last used is not relevant, and the other tab is the most recent used.
-            final ContentValues remoteTab1 = new ContentValues();
-            remoteTab1.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
-            remoteTab1.put(BrowserContract.Tabs.TITLE, "title1");
-            remoteTab1.put(BrowserContract.Tabs.URL, "http://test.com/test1");
-            remoteTab1.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test1\"]");
-            remoteTab1.put(BrowserContract.Tabs.LAST_USED, now);
-            remoteTab1.put(BrowserContract.Tabs.POSITION, 0);
-
-            final ContentValues remoteTab2 = new ContentValues();
-            remoteTab2.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
-            remoteTab2.put(BrowserContract.Tabs.TITLE, "title2");
-            remoteTab2.put(BrowserContract.Tabs.URL, "http://test.com/test2");
-            remoteTab2.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test2\"]");
-            remoteTab2.put(BrowserContract.Tabs.LAST_USED, now + 5);
-            remoteTab2.put(BrowserContract.Tabs.POSITION, 1);
-
-            values = new ContentValues[]{remoteTab1, remoteTab2};
-            inserted = cpc.bulkInsert(BrowserContract.Tabs.CONTENT_URI, values);
-            assertEquals(2, inserted);
-
-            allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, BrowserContract.Clients.GUID + " IS NOT NULL", null, null);
-            try {
-                CursorDumper.dumpCursor(allClients);
-                // The local client is ignored.
-                assertEquals(2, allClients.getCount());
-                final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
-                assertEquals(2, clients.size());
-                for (RemoteClient client : clients) {
-                    // Each client should be remote and should not have any tabs.
-                    assertNotNull(client.guid);
-                    assertNotNull(client.tabs);
-                    assertEquals(0, client.tabs.size());
-                }
-                // Since now there is a tab attached to the remote2 client more recent than the
-                // remote1 client modified time, it should be first.
-                assertEquals("guid1", clients.get(0).guid);
-                assertEquals("guid2", clients.get(1).guid);
-            } finally {
-                allClients.close();
-            }
-        } finally {
-            cpc.release();
-        }
-    }
-
-    public void testGetRecentRemoteClientsUpToOneWeekOld() throws Exception {
-        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
-        final Context context = getInstrumentation().getTargetContext();
-        final String profileName = GeckoProfile.get(context).getName();
-        final ContentResolver cr = context.getContentResolver();
-        final ContentProviderClient cpc = cr.acquireContentProviderClient(uri);
-        final LocalTabsAccessor accessor = new LocalTabsAccessor(profileName);
-
-        try {
-            // Start Clean
-            cpc.delete(uri, null, null);
-            final Cursor allClients = cpc.query(uri, null, null, null, null);
-            try {
-                assertEquals(0, allClients.getCount());
-            } finally {
-                allClients.close();
-            }
-
-            // Insert a local and remote1 client record, neither with tabs.
-            final long now = System.currentTimeMillis();
-            // Local client has GUID = null.
-            final ContentValues local = new ContentValues();
-            local.put(BrowserContract.Clients.NAME, "local");
-            local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
-            // Remote clients have GUID != null.
-            final ContentValues remote1 = new ContentValues();
-            remote1.put(BrowserContract.Clients.GUID, "guid1");
-            remote1.put(BrowserContract.Clients.NAME, "remote1");
-            remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
-
-            // Insert a Remote Client that is 6 days old.
-            final ContentValues remote2 = new ContentValues();
-            remote2.put(BrowserContract.Clients.GUID, "guid2");
-            remote2.put(BrowserContract.Clients.NAME, "remote2");
-            remote2.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
-
-            // Insert a Remote Client with the same name as previous but with more than 3 weeks old
-            final ContentValues remote3 = new ContentValues();
-            remote3.put(BrowserContract.Clients.GUID, "guid21");
-            remote3.put(BrowserContract.Clients.NAME, "remote2");
-            remote3.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS - ONE_DAY_IN_MILLISECONDS);
-
-            // Insert another remote client with the same name as previous but with 3 weeks - 1 day old.
-            final ContentValues remote4 = new ContentValues();
-            remote4.put(BrowserContract.Clients.GUID, "guid22");
-            remote4.put(BrowserContract.Clients.NAME, "remote2");
-            remote4.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
-
-            // Insert a Remote Client that is exactly one week old.
-            final ContentValues remote5 = new ContentValues();
-            remote5.put(BrowserContract.Clients.GUID, "guid3");
-            remote5.put(BrowserContract.Clients.NAME, "remote3");
-            remote5.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS);
-
-            ContentValues[] values = new ContentValues[]{local, remote1, remote2, remote3, remote4, remote5};
-            int inserted = cpc.bulkInsert(uri, values);
-            assertEquals(values.length, inserted);
-
-            final Cursor remoteClients =
-                    accessor.getRemoteClientsByRecencyCursor(context);
-
-            try {
-                CursorDumper.dumpCursor(remoteClients);
-                // Local client is not included.
-                // (remote1, guid1), (remote2, guid2), (remote3, guid3) are expected.
-                assertEquals(3, remoteClients.getCount());
-
-                // Check the inner data, according to recency.
-                List<RemoteClient> recentRemoteClientsList =
-                        accessor.getClientsWithoutTabsByRecencyFromCursor(remoteClients);
-                assertEquals(3, recentRemoteClientsList.size());
-                assertEquals("remote1", recentRemoteClientsList.get(0).name);
-                assertEquals("guid1", recentRemoteClientsList.get(0).guid);
-                assertEquals("remote2", recentRemoteClientsList.get(1).name);
-                assertEquals("guid2", recentRemoteClientsList.get(1).guid);
-                assertEquals("remote3", recentRemoteClientsList.get(2).name);
-                assertEquals("guid3", recentRemoteClientsList.get(2).guid);
-            } finally {
-                remoteClients.close();
-            }
-        } finally {
-            cpc.release();
-        }
-    }
-}
--- a/services/common/KintoCertificateBlocklist.js
+++ b/services/common/KintoCertificateBlocklist.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["OneCRLClient"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
-Cu.import("resource://services-common/moz-kinto-client.js");
+Cu.import("resource://services-common/kinto-offline-client.js");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
new file mode 100644
--- /dev/null
+++ b/services/common/kinto-http-client.js
@@ -0,0 +1,1866 @@
+/*
+ *
+ * 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.
+ */
+
+/*
+ * This file is generated from kinto-client.js - do not modify directly.
+ */
+
+this.EXPORTED_SYMBOLS = ["KintoHttpClient"];
+
+/*
+ * Version 0.6.0 - 6b6c736
+ */
+
+(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){
+/*
+ *
+ * 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 = 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);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.importGlobalProperties(['fetch']);
+const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
+
+let KintoHttpClient = class KintoHttpClient extends _base2.default {
+  constructor(remote, options = {}) {
+    const events = {};
+    EventEmitter.decorate(events);
+    super(remote, _extends({ events }, options));
+  }
+};
+
+// 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":2}],2:[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, _desc, _value, _class;
+
+var _utils = require("./utils");
+
+var _http = require("./http");
+
+var _http2 = _interopRequireDefault(_http);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+var _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _batch = require("./batch");
+
+var _bucket2 = require("./bucket");
+
+var _bucket3 = _interopRequireDefault(_bucket2);
+
+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 }; }
+
+function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
+  var desc = {};
+  Object['ke' + 'ys'](descriptor).forEach(function (key) {
+    desc[key] = descriptor[key];
+  });
+  desc.enumerable = !!desc.enumerable;
+  desc.configurable = !!desc.configurable;
+
+  if ('value' in desc || desc.initializer) {
+    desc.writable = true;
+  }
+
+  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
+    return decorator(target, property, desc) || desc;
+  }, desc);
+
+  if (context && desc.initializer !== void 0) {
+    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
+    desc.initializer = undefined;
+  }
+
+  if (desc.initializer === void 0) {
+    Object['define' + 'Property'](target, property, desc);
+    desc = null;
+  }
+
+  return desc;
+}
+
+/**
+ * Currently supported protocol version.
+ * @type {String}
+ */
+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"})
+ *   .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.support)("1.4", "2.0"), (_class = class KintoClientBase {
+  /**
+   * Constructor.
+   *
+   * @param  {String} remote  The remote URL.
+   * @param  {Object}  options The options object.
+   * @param  {Boolean} options.safe        Adds concurrency headers to every
+   * requests (default: `true`).
+   * @param  {EventEmitter} options.events The events handler. If none provided
+   * an `EventEmitter` instance will be created.
+   * @param  {Object}  options.headers     The key-value headers to pass to each
+   * request (default: `{}`).
+   * @param  {String}  options.bucket      The default bucket to use (default:
+   * `"default"`)
+   * @param  {String}  options.requestMode The HTTP request mode (from ES6 fetch
+   * spec).
+   */
+  constructor(remote, options = {}) {
+    if (typeof remote !== "string" || !remote.length) {
+      throw new Error("Invalid remote URL: " + remote);
+    }
+    if (remote[remote.length - 1] === "/") {
+      remote = remote.slice(0, -1);
+    }
+    this._backoffReleaseTime = null;
+
+    /**
+     * Default request options container.
+     * @private
+     * @type {Object}
+     */
+    this.defaultReqOptions = {
+      bucket: options.bucket || "default",
+      headers: options.headers || {},
+      safe: !!options.safe
+    };
+
+    this._options = options;
+    this._requests = [];
+    this._isBatch = !!options.batch;
+
+    // public properties
+    /**
+     * The remote server base URL.
+     * @type {String}
+     */
+    this.remote = remote;
+    /**
+     * Current server information.
+     * @ignore
+     * @type {Object|null}
+     */
+    this.serverInfo = null;
+    /**
+     * The event emitter instance. Should comply with the `EventEmitter`
+     * interface.
+     * @ignore
+     * @type {Class}
+     */
+    this.events = options.events;
+
+    /**
+     * The HTTP instance.
+     * @ignore
+     * @type {HTTP}
+     */
+    this.http = new _http2.default(this.events, { requestMode: options.requestMode });
+    this._registerHTTPEvents();
+  }
+
+  /**
+   * The remote endpoint base URL. Setting the value will also extract and
+   * validate the version.
+   * @type {String}
+   */
+  get remote() {
+    return this._remote;
+  }
+
+  /**
+   * @ignore
+   */
+  set remote(url) {
+    let version;
+    try {
+      version = url.match(/\/(v\d+)\/?$/)[1];
+    } catch (err) {
+      throw new Error("The remote URL must contain the version: " + url);
+    }
+    if (version !== SUPPORTED_PROTOCOL_VERSION) {
+      throw new Error(`Unsupported protocol version: ${ version }`);
+    }
+    this._remote = url;
+    this._version = version;
+  }
+
+  /**
+   * The current server protocol version, eg. `v1`.
+   * @type {String}
+   */
+  get version() {
+    return this._version;
+  }
+
+  /**
+   * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
+   * ongoing.
+   *
+   * @type {Number}
+   */
+  get backoff() {
+    const currentTime = new Date().getTime();
+    if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
+      return this._backoffReleaseTime - currentTime;
+    }
+    return 0;
+  }
+
+  /**
+   * Registers HTTP events.
+   * @private
+   */
+  _registerHTTPEvents() {
+    this.events.on("backoff", backoffMs => {
+      this._backoffReleaseTime = backoffMs;
+    });
+  }
+
+  /**
+   * Retrieve a bucket object to perform operations on it.
+   *
+   * @param  {String}  name    The bucket name.
+   * @param  {Object}  options The request options.
+   * @param  {Boolean} safe    The resulting safe option.
+   * @param  {String}  bucket  The resulting bucket name option.
+   * @param  {Object}  headers The extended headers object option.
+   * @return {Bucket}
+   */
+  bucket(name, options = {}) {
+    const bucketOptions = (0, _utils.omit)(this._getRequestOptions(options), "bucket");
+    return new _bucket3.default(this, name, bucketOptions);
+  }
+
+  /**
+   * Generates a request options object, deeply merging the client configured
+   * defaults with the ones provided as argument.
+   *
+   * Note: Headers won't be overriden but merged with instance default ones.
+   *
+   * @private
+   * @param    {Object} options The request options.
+   * @return   {Object}
+   * @property {Boolean} safe    The resulting safe option.
+   * @property {String}  bucket  The resulting bucket name option.
+   * @property {Object}  headers The extended headers object option.
+   */
+  _getRequestOptions(options = {}) {
+    return _extends({}, this.defaultReqOptions, options, {
+      batch: this._isBatch,
+      // Note: headers should never be overriden but extended
+      headers: _extends({}, this.defaultReqOptions.headers, options.headers)
+    });
+  }
+
+  /**
+   * Retrieves server information and persist them locally. This operation is
+   * usually performed a single time during the instance lifecycle.
+   *
+   * @return {Promise<Object, Error>}
+   */
+  fetchServerInfo() {
+    if (this.serverInfo) {
+      return Promise.resolve(this.serverInfo);
+    }
+    return this.http.request(this.remote + (0, _endpoint2.default)("root"), {
+      headers: this.defaultReqOptions.headers
+    }).then(({ json }) => {
+      this.serverInfo = json;
+      return this.serverInfo;
+    });
+  }
+
+  /**
+   * Retrieves Kinto server settings.
+   *
+   * @return {Promise<Object, Error>}
+   */
+
+  fetchServerSettings() {
+    return this.fetchServerInfo().then(({ settings }) => settings);
+  }
+
+  /**
+   * Retrieve server capabilities information.
+   *
+   * @return {Promise<Object, Error>}
+   */
+
+  fetchServerCapabilities() {
+    return this.fetchServerInfo().then(({ capabilities }) => capabilities);
+  }
+
+  /**
+   * Retrieve authenticated user information.
+   *
+   * @return {Promise<Object, Error>}
+   */
+
+  fetchUser() {
+    return this.fetchServerInfo().then(({ user }) => user);
+  }
+
+  /**
+   * Retrieve authenticated user information.
+   *
+   * @return {Promise<Object, Error>}
+   */
+
+  fetchHTTPApiVersion() {
+    return this.fetchServerInfo().then(({ http_api_version }) => {
+      return http_api_version;
+    });
+  }
+
+  /**
+   * Process batch requests, chunking them according to the batch_max_requests
+   * server setting when needed.
+   *
+   * @param  {Array}  requests The list of batch subrequests to perform.
+   * @param  {Object} options  The options object.
+   * @return {Promise<Object, Error>}
+   */
+  _batchRequests(requests, options = {}) {
+    const headers = _extends({}, this.defaultReqOptions.headers, options.headers);
+    if (!requests.length) {
+      return Promise.resolve([]);
+    }
+    return this.fetchServerSettings().then(serverSettings => {
+      const maxRequests = serverSettings["batch_max_requests"];
+      if (maxRequests && requests.length > maxRequests) {
+        const chunks = (0, _utils.partition)(requests, maxRequests);
+        return (0, _utils.pMap)(chunks, chunk => this._batchRequests(chunk, options));
+      }
+      return this.execute({
+        path: (0, _endpoint2.default)("batch"),
+        method: "POST",
+        headers: headers,
+        body: {
+          defaults: { headers },
+          requests: requests
+        }
+      })
+      // we only care about the responses
+      .then(({ responses }) => responses);
+    });
+  }
+
+  /**
+   * Sends batch requests to the remote server.
+   *
+   * Note: Reserved for internal use only.
+   *
+   * @ignore
+   * @param  {Function} fn      The function to use for describing batch ops.
+   * @param  {Object}   options The options object.
+   * @param  {Boolean}  options.safe      The safe option.
+   * @param  {String}   options.bucket    The bucket name option.
+   * @param  {Object}   options.headers   The headers object option.
+   * @param  {Boolean}  options.aggregate Produces an aggregated result object
+   * (default: `false`).
+   * @return {Promise<Object, Error>}
+   */
+
+  batch(fn, options = {}) {
+    const rootBatch = new KintoClientBase(this.remote, _extends({}, this._options, this._getRequestOptions(options), {
+      batch: true
+    }));
+    let bucketBatch, collBatch;
+    if (options.bucket) {
+      bucketBatch = rootBatch.bucket(options.bucket);
+      if (options.collection) {
+        collBatch = bucketBatch.collection(options.collection);
+      }
+    }
+    const batchClient = collBatch || bucketBatch || rootBatch;
+    try {
+      fn(batchClient);
+    } catch (err) {
+      return Promise.reject(err);
+    }
+    return this._batchRequests(rootBatch._requests, options).then(responses => {
+      if (options.aggregate) {
+        return (0, _batch.aggregate)(responses, rootBatch._requests);
+      }
+      return responses;
+    });
+  }
+
+  /**
+   * Executes an atomic HTTP request.
+   *
+   * @private
+   * @param  {Object}  request     The request object.
+   * @param  {Object}  options     The options object.
+   * @param  {Boolean} options.raw Resolve with full response object, including
+   * json body and headers (Default: `false`, so only the json body is
+   * retrieved).
+   * @return {Promise<Object, Error>}
+   */
+  execute(request, options = { raw: false }) {
+    // If we're within a batch, add the request to the stack to send at once.
+    if (this._isBatch) {
+      this._requests.push(request);
+      // Resolve with a message in case people attempt at consuming the result
+      // from within a batch operation.
+      const msg = "This result is generated from within a batch " + "operation and should not be consumed.";
+      return Promise.resolve(options.raw ? { json: msg } : msg);
+    }
+    const promise = this.fetchServerSettings().then(_ => {
+      return this.http.request(this.remote + request.path, _extends({}, request, {
+        body: JSON.stringify(request.body)
+      }));
+    });
+    return options.raw ? promise : promise.then(({ json }) => json);
+  }
+
+  /**
+   * Retrieves the list of buckets.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object[], Error>}
+   */
+  listBuckets(options = {}) {
+    return this.execute({
+      path: (0, _endpoint2.default)("buckets"),
+      headers: _extends({}, this.defaultReqOptions.headers, options.headers)
+    });
+  }
+
+  /**
+   * Creates a new bucket on the server.
+   *
+   * @param  {String}   bucketName      The bucket name.
+   * @param  {Object}   options         The options object.
+   * @param  {Boolean}  options.safe    The safe option.
+   * @param  {Object}   options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  createBucket(bucketName, options = {}) {
+    const reqOptions = this._getRequestOptions(options);
+    return this.execute(requests.createBucket(bucketName, reqOptions));
+  }
+
+  /**
+   * Deletes a bucket from the server.
+   *
+   * @ignore
+   * @param  {Object|String} bucket          The bucket to delete.
+   * @param  {Object}        options         The options object.
+   * @param  {Boolean}       options.safe    The safe option.
+   * @param  {Object}        options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  deleteBucket(bucket, options = {}) {
+    const _bucket = typeof bucket === "object" ? bucket : { id: bucket };
+    const reqOptions = this._getRequestOptions(options);
+    return this.execute(requests.deleteBucket(_bucket, reqOptions));
+  }
+
+  /**
+   * Deletes all buckets on the server.
+   *
+   * @ignore
+   * @param  {Object}  options         The options object.
+   * @param  {Boolean} options.safe    The safe option.
+   * @param  {Object}  options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+
+  deleteBuckets(options = {}) {
+    const reqOptions = this._getRequestOptions(options);
+    return this.execute(requests.deleteBuckets(reqOptions));
+  }
+}, (_applyDecoratedDescriptor(_class.prototype, "fetchServerSettings", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerSettings"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchServerCapabilities", [_dec2], Object.getOwnPropertyDescriptor(_class.prototype, "fetchServerCapabilities"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchUser", [_dec3], Object.getOwnPropertyDescriptor(_class.prototype, "fetchUser"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "fetchHTTPApiVersion", [_dec4], Object.getOwnPropertyDescriptor(_class.prototype, "fetchHTTPApiVersion"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "batch", [_dec5], Object.getOwnPropertyDescriptor(_class.prototype, "batch"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "deleteBuckets", [_dec6], Object.getOwnPropertyDescriptor(_class.prototype, "deleteBuckets"), _class.prototype)), _class));
+exports.default = KintoClientBase;
+
+},{"./batch":3,"./bucket":4,"./endpoint":6,"./http":8,"./requests":9,"./utils":10}],3:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.aggregate = aggregate;
+/**
+ * Exports batch responses as a result object.
+ *
+ * @private
+ * @param  {Array} responses The batch subrequest responses.
+ * @param  {Array} requests  The initial issued requests.
+ * @return {Object}
+ */
+function aggregate(responses = [], requests = []) {
+  if (responses.length !== requests.length) {
+    throw new Error("Responses length should match requests one.");
+  }
+  const results = {
+    errors: [],
+    published: [],
+    conflicts: [],
+    skipped: []
+  };
+  return responses.reduce((acc, response, index) => {
+    const { status } = response;
+    if (status >= 200 && status < 400) {
+      acc.published.push(response.body);
+    } else if (status === 404) {
+      acc.skipped.push(response.body);
+    } else if (status === 412) {
+      acc.conflicts.push({
+        // XXX: specifying the type is probably superfluous
+        type: "outgoing",
+        local: requests[index].body,
+        remote: response.body.details && response.body.details.existing || null
+      });
+    } else {
+      acc.errors.push({
+        path: response.path,
+        sent: requests[index],
+        error: response.body
+      });
+    }
+    return acc;
+  }, results);
+}
+
+},{}],4:[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 _collection = require("./collection");
+
+var _collection2 = _interopRequireDefault(_collection);
+
+var _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+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 }; }
+
+/**
+ * Abstract representation of a selected bucket.
+ *
+ */
+let Bucket = class Bucket {
+  /**
+   * Constructor.
+   *
+   * @param  {KintoClient} client          The client instance.
+   * @param  {String}      name            The bucket name.
+   * @param  {Object}      options.headers The headers object option.
+   * @param  {Boolean}     options.safe    The safe option.
+   */
+  constructor(client, name, options = {}) {
+    /**
+     * @ignore
+     */
+    this.client = client;
+    /**
+     * The bucket name.
+     * @type {String}
+     */
+    this.name = name;
+    /**
+     * The default options object.
+     * @ignore
+     * @type {Object}
+     */
+    this.options = options;
+    /**
+     * @ignore
+     */
+    this._isBatch = !!options.batch;
+  }
+
+  /**
+   * Merges passed request options with default bucket ones, if any.
+   *
+   * @private
+   * @param  {Object} options The options to merge.
+   * @return {Object}         The merged options.
+   */
+  _bucketOptions(options = {}) {
+    const headers = _extends({}, this.options && this.options.headers, options.headers);
+    return _extends({}, this.options, options, {
+      headers,
+      bucket: this.name,
+      batch: this._isBatch
+    });
+  }
+
+  /**
+   * Selects a collection.
+   *
+   * @param  {String} name            The collection name.
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @param  {Boolean}  options.safe  The safe option.
+   * @return {Collection}
+   */
+  collection(name, options) {
+    return new _collection2.default(this.client, this, name, this._bucketOptions(options));
+  }
+
+  /**
+   * Retrieves bucket properties.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getAttributes(options = {}) {
+    return this.client.execute({
+      path: (0, _endpoint2.default)("bucket", this.name),
+      headers: _extends({}, this.options.headers, options.headers)
+    });
+  }
+
+  /**
+   * Retrieves the list of collections in the current bucket.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Array<Object>, Error>}
+   */
+  listCollections(options = {}) {
+    return this.client.execute({
+      path: (0, _endpoint2.default)("collections", this.name),
+      headers: _extends({}, this.options.headers, options.headers)
+    });
+  }
+
+  /**
+   * Creates a new collection in current bucket.
+   *
+   * @param  {String|undefined}  id        The collection id.
+   * @param  {Object}  options             The options object.
+   * @param  {Boolean} options.safe        The safe option.
+   * @param  {Object}  options.headers     The headers object option.
+   * @param  {Object}  options.permissions The permissions object.
+   * @param  {Object}  options.data        The metadadata object.
+   * @param  {Object}  options.schema      The JSONSchema object.
+   * @return {Promise<Object, Error>}
+   */
+  createCollection(id, options) {
+    const reqOptions = this._bucketOptions(options);
+    const request = requests.createCollection(id, reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Deletes a collection from the current bucket.
+   *
+   * @param  {Object|String} collection  The collection to delete.
+   * @param  {Object}    options         The options object.
+   * @param  {Object}    options.headers The headers object option.
+   * @param  {Boolean}   options.safe    The safe option.
+   * @return {Promise<Object, Error>}
+   */
+  deleteCollection(collection, options) {
+    const reqOptions = this._bucketOptions(options);
+    const request = requests.deleteCollection((0, _utils.toDataBody)(collection), reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Retrieves the list of permissions for this bucket.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getPermissions(options) {
+    return this.getAttributes(this._bucketOptions(options)).then(res => res.permissions);
+  }
+
+  /**
+   * Recplaces all existing bucket permissions with the ones provided.
+   *
+   * @param  {Object}  permissions           The permissions object.
+   * @param  {Object}  options               The options object
+   * @param  {Object}  options               The options object.
+   * @param  {Boolean} options.safe          The safe option.
+   * @param  {Object}  options.headers       The headers object option.
+   * @param  {Object}  options.last_modified The last_modified option.
+   * @return {Promise<Object, Error>}
+   */
+  setPermissions(permissions, options = {}) {
+    return this.client.execute(requests.updateBucket({
+      id: this.name,
+      last_modified: options.last_modified
+    }, _extends({}, this._bucketOptions(options), { permissions })));
+  }
+
+  /**
+   * Performs batch operations at the current bucket level.
+   *
+   * @param  {Function} fn                 The batch operation function.
+   * @param  {Object}   options            The options object.
+   * @param  {Object}   options.headers    The headers object option.
+   * @param  {Boolean}  options.safe       The safe option.
+   * @param  {Boolean}  options.aggregate  Produces a grouped result object.
+   * @return {Promise<Object, Error>}
+   */
+  batch(fn, options) {
+    return this.client.batch(fn, this._bucketOptions(options));
+  }
+};
+exports.default = Bucket;
+
+},{"./collection":5,"./endpoint":6,"./requests":9,"./utils":10}],5:[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 _requests = require("./requests");
+
+var requests = _interopRequireWildcard(_requests);
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+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; } }
+
+/**
+ * Abstract representation of a selected collection.
+ *
+ */
+let Collection = class Collection {
+  /**
+   * Constructor.
+   *
+   * @param  {KintoClient}  client          The client instance.
+   * @param  {Bucket}       bucket          The bucket instance.
+   * @param  {String}       name            The collection name.
+   * @param  {Object}       options.headers The headers object option.
+   * @param  {Boolean}      options.safe    The safe option.
+   */
+  constructor(client, bucket, name, options = {}) {
+    /**
+     * @ignore
+     */
+    this.client = client;
+    /**
+     * @ignore
+     */
+    this.bucket = bucket;
+    /**
+     * The collection name.
+     * @type {String}
+     */
+    this.name = name;
+
+    /**
+     * The default collection options object, embedding the default bucket ones.
+     * @ignore
+     * @type {Object}
+     */
+    this.options = _extends({}, this.bucket.options, options, {
+      headers: _extends({}, this.bucket.options && this.bucket.options.headers, options.headers)
+    });
+    /**
+     * @ignore
+     */
+    this._isBatch = !!options.batch;
+  }
+
+  /**
+   * Merges passed request options with default bucket and collection ones, if
+   * any.
+   *
+   * @private
+   * @param  {Object} options The options to merge.
+   * @return {Object}         The merged options.
+   */
+  _collOptions(options = {}) {
+    const headers = _extends({}, this.options && this.options.headers, options.headers);
+    return _extends({}, this.options, options, {
+      headers,
+      // XXX soon to be removed once we've migrated everything from KintoClient
+      bucket: this.bucket.name
+    });
+  }
+
+  /**
+   * Updates current collection properties.
+   *
+   * @private
+   * @param  {Object} options  The request options.
+   * @return {Promise<Object, Error>}
+   */
+  _updateAttributes(options = {}) {
+    const collection = (0, _utils.toDataBody)(this.name);
+    const reqOptions = this._collOptions(options);
+    const request = requests.updateCollection(collection, reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Retrieves collection properties.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getAttributes(options) {
+    const { headers } = this._collOptions(options);
+    return this.client.execute({
+      path: (0, _endpoint2.default)("collection", this.bucket.name, this.name),
+      headers
+    });
+  }
+
+  /**
+   * Retrieves the list of permissions for this collection.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getPermissions(options) {
+    return this.getAttributes(options).then(res => res.permissions);
+  }
+
+  /**
+   * Replaces all existing collection permissions with the ones provided.
+   *
+   * @param  {Object}   permissions     The permissions object.
+   * @param  {Object}   options         The options object
+   * @param  {Object}   options.headers The headers object option.
+   * @param  {Boolean}  options.safe    The safe option.
+   * @param  {Number}   options.last_modified The last_modified option.
+   * @return {Promise<Object, Error>}
+   */
+  setPermissions(permissions, options) {
+    return this._updateAttributes(_extends({}, options, { permissions }));
+  }
+
+  /**
+   * Retrieves the JSON schema for this collection, if any.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object|null, Error>}
+   */
+  getSchema(options) {
+    return this.getAttributes(options).then(res => res.data && res.data.schema || null);
+  }
+
+  /**
+   * Sets the JSON schema for this collection.
+   *
+   * @param  {Object}   schema          The JSON schema object.
+   * @param  {Object}   options         The options object.
+   * @param  {Object}   options.headers The headers object option.
+   * @param  {Boolean}  options.safe    The safe option.
+   * @param  {Number}   options.last_modified The last_modified option.
+   * @return {Promise<Object|null, Error>}
+   */
+  setSchema(schema, options) {
+    return this._updateAttributes(_extends({}, options, { schema }));
+  }
+
+  /**
+   * Retrieves metadata attached to current collection.
+   *
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getMetadata(options) {
+    return this.getAttributes(options).then(({ data }) => (0, _utils.omit)(data, "schema"));
+  }
+
+  /**
+   * Sets metadata for current collection.
+   *
+   * @param  {Object}   metadata        The metadata object.
+   * @param  {Object}   options         The options object.
+   * @param  {Object}   options.headers The headers object option.
+   * @param  {Boolean}  options.safe  The safe option.
+   * @param  {Number}   options.last_modified The last_modified option.
+   * @return {Promise<Object, Error>}
+   */
+  setMetadata(metadata, options) {
+    // Note: patching allows preventing overridding the schema, which lives
+    // within the "data" namespace.
+    return this._updateAttributes(_extends({}, options, { metadata, patch: true }));
+  }
+
+  /**
+   * Creates a record in current collection.
+   *
+   * @param  {Object} record          The record to create.
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @param  {Boolean}  options.safe  The safe option.
+   * @return {Promise<Object, Error>}
+   */
+  createRecord(record, options) {
+    const reqOptions = this._collOptions(options);
+    const request = requests.createRecord(this.name, record, reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Updates a record in current collection.
+   *
+   * @param  {Object}  record                The record to update.
+   * @param  {Object}  options               The options object.
+   * @param  {Object}  options.headers       The headers object option.
+   * @param  {Boolean} options.safe          The safe option.
+   * @param  {Number}  options.last_modified The last_modified option.
+   * @return {Promise<Object, Error>}
+   */
+  updateRecord(record, options) {
+    const reqOptions = this._collOptions(options);
+    const request = requests.updateRecord(this.name, record, reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Deletes a record from the current collection.
+   *
+   * @param  {Object|String} record          The record to delete.
+   * @param  {Object}        options         The options object.
+   * @param  {Object}        options.headers The headers object option.
+   * @param  {Boolean}       options.safe    The safe option.
+   * @param  {Number}        options.last_modified The last_modified option.
+   * @return {Promise<Object, Error>}
+   */
+  deleteRecord(record, options) {
+    const reqOptions = this._collOptions(options);
+    const request = requests.deleteRecord(this.name, (0, _utils.toDataBody)(record), reqOptions);
+    return this.client.execute(request);
+  }
+
+  /**
+   * Retrieves a record from the current collection.
+   *
+   * @param  {String} id              The record id to retrieve.
+   * @param  {Object} options         The options object.
+   * @param  {Object} options.headers The headers object option.
+   * @return {Promise<Object, Error>}
+   */
+  getRecord(id, options) {
+    return this.client.execute(_extends({
+      path: (0, _endpoint2.default)("record", this.bucket.name, this.name, id)
+    }, this._collOptions(options)));
+  }
+
+  /**
+   * Lists records from the current collection.
+   *
+   * Sorting is done by passing a `sort` string option:
+   *
+   * - The field to order the results by, prefixed with `-` for descending.
+   * Default: `-last_modified`.
+   *
+   * @see http://kinto.readthedocs.org/en/latest/api/1.x/cliquet/resource.html#sorting
+   *
+   * Filtering is done by passing a `filters` option object:
+   *
+   * - `{fieldname: "value"}`
+   * - `{min_fieldname: 4000}`
+   * - `{in_fieldname: "1,2,3"}`
+   * - `{not_fieldname: 0}`
+   * - `{exclude_fieldname: "0,1"}`
+   *
+   * @see http://kinto.readthedocs.org/en/latest/api/1.x/cliquet/resource.html#filtering
+   *
+   * Paginating is done by passing a `limit` option, then calling the `next()`
+   * method from the resolved result object to fetch the next page, if any.
+   *
+   * @param  {Object}   options         The options object.
+   * @param  {Object}   options.headers The headers object option.
+   * @param  {Object}   options.filters The filters object.
+   * @param  {String}   options.sort    The sort field.
+   * @param  {String}   options.limit   The limit field.
+   * @param  {String}   options.pages   The number of result pages to aggregate.
+   * @param  {Number}   options.since   Only retrieve records modified since the
+   * provided timestamp.
+   * @return {Promise<Object, Error>}
+   */
+  listRecords(options = {}) {
+    const { http } = this.client;
+    const { sort, filters, limit, pages, since } = _extends({
+      sort: "-last_modified"
+    }, options);
+    const collHeaders = this.options.headers;
+    const path = (0, _endpoint2.default)("records", this.bucket.name, this.name);
+    const querystring = (0, _utils.qsify)(_extends({}, filters, {
+      _sort: sort,
+      _limit: limit,
+      _since: since
+    }));
+    let results = [],
+        current = 0;
+
+    const next = function (nextPage) {
+      if (!nextPage) {
+        throw new Error("Pagination exhausted.");
+      }
+      return processNextPage(nextPage);
+    };
+
+    const processNextPage = nextPage => {
+      return http.request(nextPage, { headers: collHeaders }).then(handleResponse);
+    };
+
+    const pageResults = (results, nextPage, etag) => {
+      return {
+        last_modified: etag,
+        data: results,
+        next: next.bind(null, nextPage)
+      };
+    };
+
+    const handleResponse = ({ headers, json }) => {
+      const nextPage = headers.get("Next-Page");
+      // ETag are supposed to be opaque and stored «as-is».
+      const etag = headers.get("ETag");
+      if (!pages) {
+        return pageResults(json.data, nextPage, etag);
+      }
+      // Aggregate new results with previous ones
+      results = results.concat(json.data);
+      current += 1;
+      if (current >= pages || !nextPage) {
+        // Pagination exhausted
+        return pageResults(results, nextPage, etag);
+      }
+      // Follow next page
+      return processNextPage(nextPage);
+    };
+
+    return this.client.execute(_extends({
+      path: path + "?" + querystring
+    }, this._collOptions(options)), { raw: true }).then(handleResponse);
+  }
+
+  /**
+   * Performs batch operations at the current collection level.
+   *
+   * @param  {Function} fn                 The batch operation function.
+   * @param  {Object}   options            The options object.
+   * @param  {Object}   options.headers    The headers object option.
+   * @param  {Boolean}  options.safe       The safe option.
+   * @param  {Boolean}  options.aggregate  Produces a grouped result object.
+   * @return {Promise<Object, Error>}
+   */
+  batch(fn, options) {
+    const reqOptions = this._collOptions(options);
+    return this.client.batch(fn, _extends({}, reqOptions, {
+      collection: this.name
+    }));
+  }
+};
+exports.default = Collection;
+
+},{"./endpoint":6,"./requests":9,"./utils":10}],6:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = endpoint;
+/**
+ * Endpoints templates.
+ * @type {Object}
+ */
+const ENDPOINTS = {
+  root: () => "/",
+  batch: () => "/batch",
+  buckets: () => "/buckets",
+  bucket: bucket => `/buckets/${ bucket }`,
+  collections: bucket => `${ ENDPOINTS.bucket(bucket) }/collections`,
+  collection: (bucket, coll) => `${ ENDPOINTS.bucket(bucket) }/collections/${ coll }`,
+  records: (bucket, coll) => `${ ENDPOINTS.collection(bucket, coll) }/records`,
+  record: (bucket, coll, id) => `${ ENDPOINTS.records(bucket, coll) }/${ id }`
+};
+
+/**
+ * Retrieves a server enpoint by its name.
+ *
+ * @private
+ * @param  {String}    name The endpoint name.
+ * @param  {...string} args The endpoint parameters.
+ * @return {String}
+ */
+function endpoint(name, ...args) {
+  return ENDPOINTS[name](...args);
+}
+
+},{}],7:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+/**
+ * Kinto server error code descriptors.
+ * @type {Object}
+ */
+exports.default = {
+  104: "Missing Authorization Token",
+  105: "Invalid Authorization Token",
+  106: "Request body was not valid JSON",
+  107: "Invalid request parameter",
+  108: "Missing request parameter",
+  109: "Invalid posted data",
+  110: "Invalid Token / id",
+  111: "Missing Token / id",
+  112: "Content-Length header was not provided",
+  113: "Request body too large",
+  114: "Resource was modified meanwhile",
+  115: "Method not allowed on this end point",
+  116: "Requested version not available on this server",
+  117: "Client has sent too many requests",
+  121: "Resource access is forbidden for this user",
+  122: "Another resource violates constraint",
+  201: "Service Temporary unavailable due to high load",
+  202: "Service deprecated",
+  999: "Internal Server Error"
+};
+
+},{}],8:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = undefined;
+
+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.
+   *
+   * @type {Object}
+   */
+  static get DEFAULT_REQUEST_HEADERS() {
+    return {
+      "Accept": "application/json",
+      "Content-Type": "application/json"
+    };
+  }
+
+  /**
+   * Default options.
+   *
+   * @type {Object}
+   */
+  static get defaultOptions() {
+    return { timeout: 5000, requestMode: "cors" };
+  }
+
+  /**
+   * Constructor.
+   *
+   * Options:
+   * - {Number} timeout      The request timeout in ms (default: `5000`).
+   * - {String} requestMode  The HTTP request mode (default: `"cors"`).
+   *
+   * @param {EventEmitter} events  The event handler.
+   * @param {Object}       options The options object.
+   */
+  constructor(events, options = {}) {
+    // public properties
+    /**
+     * The event emitter instance.
+     * @type {EventEmitter}
+     */
+    if (!events) {
+      throw new Error("No events handler provided");
+    }
+    this.events = events;
+
+    options = Object.assign({}, HTTP.defaultOptions, options);
+
+    /**
+     * The request mode.
+     * @see  https://fetch.spec.whatwg.org/#requestmode
+     * @type {String}
+     */
+    this.requestMode = options.requestMode;
+
+    /**
+     * The request timeout.
+     * @type {Number}
+     */
+    this.timeout = options.timeout;
+  }
+
+  /**
+   * Performs an HTTP request to the Kinto server.
+   *
+   * Options:
+   * - `{Object} headers` The request headers object (default: {})
+   *
+   * Resolves with an objet containing the following HTTP response properties:
+   * - `{Number}  status`  The HTTP status code.
+   * - `{Object}  json`    The JSON response body.
+   * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
+   *
+   * @param  {String} url     The URL.
+   * @param  {Object} options The fetch() options object.
+   * @return {Promise}
+   */
+  request(url, options = { headers: {} }) {
+    let response, status, statusText, headers, hasTimedout;
+    // Ensure default request headers are always set
+    options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
+    options.mode = this.requestMode;
+    return new Promise((resolve, reject) => {
+      const _timeoutId = setTimeout(() => {
+        hasTimedout = true;
+        reject(new Error("Request timeout."));
+      }, this.timeout);
+      fetch(url, options).then(res => {
+        if (!hasTimedout) {
+          clearTimeout(_timeoutId);
+          resolve(res);
+        }
+      }).catch(err => {
+        if (!hasTimedout) {
+          clearTimeout(_timeoutId);
+          reject(err);
+        }
+      });
+    }).then(res => {
+      response = res;
+      headers = res.headers;
+      status = res.status;
+      statusText = res.statusText;
+      this._checkForDeprecationHeader(headers);
+      this._checkForBackoffHeader(status, headers);
+      return res.text();
+    })
+    // Check if we have a body; if so parse it as JSON.
+    .then(text => {
+      if (text.length === 0) {
+        return null;
+      }
+      // Note: we can't consume the response body twice.
+      return JSON.parse(text);
+    }).catch(err => {
+      const error = new Error(`HTTP ${ status || 0 }; ${ err }`);
+      error.response = response;
+      error.stack = err.stack;
+      throw error;
+    }).then(json => {
+      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;
+      }
+      return { status, json, headers };
+    });
+  }
+
+  _checkForDeprecationHeader(headers) {
+    const alertHeader = headers.get("Alert");
+    if (!alertHeader) {
+      return;
+    }
+    let alert;
+    try {
+      alert = JSON.parse(alertHeader);
+    } catch (err) {
+      console.warn("Unable to parse Alert header message", alertHeader);
+      return;
+    }
+    console.warn(alert.message, alert.url);
+    this.events.emit("deprecated", alert);
+  }
+
+  _checkForBackoffHeader(status, headers) {
+    let backoffMs;
+    const backoffSeconds = parseInt(headers.get("Backoff"), 10);
+    if (backoffSeconds > 0) {
+      backoffMs = new Date().getTime() + backoffSeconds * 1000;
+    } else {
+      backoffMs = 0;
+    }
+    this.events.emit("backoff", backoffMs);
+  }
+};
+exports.default = HTTP;
+
+},{"./errors":7}],9:[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.createBucket = createBucket;
+exports.updateBucket = updateBucket;
+exports.deleteBucket = deleteBucket;
+exports.deleteBuckets = deleteBuckets;
+exports.createCollection = createCollection;
+exports.updateCollection = updateCollection;
+exports.deleteCollection = deleteCollection;
+exports.createRecord = createRecord;
+exports.updateRecord = updateRecord;
+exports.deleteRecord = deleteRecord;
+
+var _endpoint = require("./endpoint");
+
+var _endpoint2 = _interopRequireDefault(_endpoint);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const requestDefaults = {
+  safe: false,
+  // check if we should set default content type here
+  headers: {},
+  bucket: "default",
+  permissions: {},
+  data: {},
+  patch: false
+};
+
+function safeHeader(safe, last_modified) {
+  if (!safe) {
+    return {};
+  }
+  if (last_modified) {
+    return { "If-Match": `"${ last_modified }"` };
+  }
+  return { "If-None-Match": "*" };
+}
+
+/**
+ * @private
+ */
+function createBucket(bucketName, options = {}) {
+  if (!bucketName) {
+    throw new Error("A bucket name is required.");
+  }
+  // Note that we simply ignore any "bucket" option passed here, as the one
+  // we're interested in is the one provided as a required argument.
+  const { headers, permissions, safe } = _extends({}, requestDefaults, options);
+  return {
+    method: "PUT",
+    path: (0, _endpoint2.default)("bucket", bucketName),
+    headers: _extends({}, headers, safeHeader(safe)),
+    body: {
+      // XXX We can't pass the data option just yet, see Kinto/kinto/issues/239
+      permissions
+    }
+  };
+}
+
+/**
+ * @private
+ */
+function updateBucket(bucket, options = {}) {
+  if (typeof bucket !== "object") {
+    throw new Error("A bucket object is required.");
+  }
+  if (!bucket.id) {
+    throw new Error("A bucket id is required.");
+  }
+  const { headers, permissions, safe, patch, last_modified } = _extends({}, requestDefaults, options);
+  return {
+    method: patch ? "PATCH" : "PUT",
+    path: (0, _endpoint2.default)("bucket", bucket.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified || bucket.last_modified)),
+    body: {
+      data: bucket,
+      permissions
+    }
+  };
+}
+
+/**
+ * @private
+ */
+function deleteBucket(bucket, options = {}) {
+  if (typeof bucket !== "object") {
+    throw new Error("A bucket object is required.");
+  }
+  if (!bucket.id) {
+    throw new Error("A bucket id is required.");
+  }
+  const { headers, safe, last_modified } = _extends({}, requestDefaults, {
+    last_modified: bucket.last_modified
+  }, options);
+  if (safe && !last_modified) {
+    throw new Error("Safe concurrency check requires a last_modified value.");
+  }
+  return {
+    method: "DELETE",
+    path: (0, _endpoint2.default)("bucket", bucket.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified))
+  };
+}
+
+/**
+ * @private
+ */
+function deleteBuckets(options = {}) {
+  const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+  if (safe && !last_modified) {
+    throw new Error("Safe concurrency check requires a last_modified value.");
+  }
+  return {
+    method: "DELETE",
+    path: (0, _endpoint2.default)("buckets"),
+    headers: _extends({}, headers, safeHeader(safe, last_modified))
+  };
+}
+
+/**
+ * @private
+ */
+function createCollection(id, options = {}) {
+  const { bucket, headers, permissions, data, safe } = _extends({}, requestDefaults, options);
+  // XXX checks that provided data can't override schema when provided
+  const path = id ? (0, _endpoint2.default)("collection", bucket, id) : (0, _endpoint2.default)("collections", bucket);
+  return {
+    method: id ? "PUT" : "POST",
+    path,
+    headers: _extends({}, headers, safeHeader(safe)),
+    body: { data, permissions }
+  };
+}
+
+/**
+ * @private
+ */
+function updateCollection(collection, options = {}) {
+  if (typeof collection !== "object") {
+    throw new Error("A collection object is required.");
+  }
+  if (!collection.id) {
+    throw new Error("A collection id is required.");
+  }
+  const {
+    bucket,
+    headers,
+    permissions,
+    schema,
+    metadata,
+    safe,
+    patch,
+    last_modified
+  } = _extends({}, requestDefaults, options);
+  const collectionData = _extends({}, metadata, collection);
+  if (options.schema) {
+    collectionData.schema = schema;
+  }
+  return {
+    method: patch ? "PATCH" : "PUT",
+    path: (0, _endpoint2.default)("collection", bucket, collection.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified || collection.last_modified)),
+    body: {
+      data: collectionData,
+      permissions
+    }
+  };
+}
+
+/**
+ * @private
+ */
+function deleteCollection(collection, options = {}) {
+  if (typeof collection !== "object") {
+    throw new Error("A collection object is required.");
+  }
+  if (!collection.id) {
+    throw new Error("A collection id is required.");
+  }
+  const { bucket, headers, safe, last_modified } = _extends({}, requestDefaults, {
+    last_modified: collection.last_modified
+  }, options);
+  if (safe && !last_modified) {
+    throw new Error("Safe concurrency check requires a last_modified value.");
+  }
+  return {
+    method: "DELETE",
+    path: (0, _endpoint2.default)("collection", bucket, collection.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified))
+  };
+}
+
+/**
+ * @private
+ */
+function createRecord(collName, record, options = {}) {
+  if (!collName) {
+    throw new Error("A collection name is required.");
+  }
+  const { bucket, headers, permissions, safe } = _extends({}, requestDefaults, options);
+  return {
+    // Note: Safe POST using a record id would fail.
+    // see https://github.com/Kinto/kinto/issues/489
+    method: record.id ? "PUT" : "POST",
+    path: record.id ? (0, _endpoint2.default)("record", bucket, collName, record.id) : (0, _endpoint2.default)("records", bucket, collName),
+    headers: _extends({}, headers, safeHeader(safe)),
+    body: {
+      data: record,
+      permissions
+    }
+  };
+}
+
+/**
+ * @private
+ */
+function updateRecord(collName, record, options = {}) {
+  if (!collName) {
+    throw new Error("A collection name is required.");
+  }
+  if (!record.id) {
+    throw new Error("A record id is required.");
+  }
+  const { bucket, headers, permissions, safe, patch, last_modified } = _extends({}, requestDefaults, options);
+  return {
+    method: patch ? "PATCH" : "PUT",
+    path: (0, _endpoint2.default)("record", bucket, collName, record.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified || record.last_modified)),
+    body: {
+      data: record,
+      permissions
+    }
+  };
+}
+
+/**
+ * @private
+ */
+function deleteRecord(collName, record, options = {}) {
+  if (!collName) {
+    throw new Error("A collection name is required.");
+  }
+  if (typeof record !== "object") {
+    throw new Error("A record object is required.");
+  }
+  if (!record.id) {
+    throw new Error("A record id is required.");
+  }
+  const { bucket, headers, safe, last_modified } = _extends({}, requestDefaults, {
+    last_modified: record.last_modified
+  }, options);
+  if (safe && !last_modified) {
+    throw new Error("Safe concurrency check requires a last_modified value.");
+  }
+  return {
+    method: "DELETE",
+    path: (0, _endpoint2.default)("record", bucket, collName, record.id),
+    headers: _extends({}, headers, safeHeader(safe, last_modified))
+  };
+}
+
+},{"./endpoint":6}],10:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.partition = partition;
+exports.pMap = pMap;
+exports.omit = omit;
+exports.toDataBody = toDataBody;
+exports.qsify = qsify;
+exports.checkVersion = checkVersion;
+exports.support = support;
+exports.nobatch = nobatch;
+/**
+ * Chunks an array into n pieces.
+ *
+ * @private
+ * @param  {Array}  array
+ * @param  {Number} n
+ * @return {Array}
+ */
+function partition(array, n) {
+  if (n <= 0) {
+    return array;
+  }
+  return array.reduce((acc, x, i) => {
+    if (i === 0 || i % n === 0) {
+      acc.push([x]);
+    } else {
+      acc[acc.length - 1].push(x);
+    }
+    return acc;
+  }, []);
+}
+
+/**
+ * Maps a list to promises using the provided mapping function, executes them
+ * sequentially then returns a Promise resolving with ordered results obtained.
+ * Think of this as a sequential Promise.all.
+ *
+ * @private
+ * @param  {Array}    list The list to map.
+ * @param  {Function} fn   The mapping function.
+ * @return {Promise}
+ */
+function pMap(list, fn) {
+  let results = [];
+  return list.reduce((promise, entry) => {
+    return promise.then(() => {
+      return Promise.resolve(fn(entry)).then(result => results = results.concat(result));
+    });
+  }, Promise.resolve()).then(() => results);
+}
+
+/**
+ * Takes an object and returns a copy of it with the provided keys omitted.
+ *
+ * @private
+ * @param  {Object}    obj  The source object.
+ * @param  {...String} keys The keys to omit.
+ * @return {Object}
+ */
+function omit(obj, ...keys) {
+  return Object.keys(obj).reduce((acc, key) => {
+    if (keys.indexOf(key) === -1) {
+      acc[key] = obj[key];
+    }
+    return acc;
+  }, {});
+}
+
+/**
+ * Always returns a resource data object from the provided argument.
+ *
+ * @private
+ * @param  {Object|String} value
+ * @return {Object}
+ */
+function toDataBody(value) {
+  if (typeof value === "object") {
+    return value;
+  }
+  if (typeof value === "string") {
+    return { id: value };
+  }
+  throw new Error("Invalid collection argument.");
+}
+
+/**
+ * Transforms an object into an URL query string, stripping out any undefined
+ * values.
+ *
+ * @param  {Object} obj
+ * @return {String}
+ */
+function qsify(obj) {
+  const sep = "&";
+  const encode = v => encodeURIComponent(typeof v === "boolean" ? String(v) : v);
+  const stripUndefined = o => JSON.parse(JSON.stringify(o));
+  const stripped = stripUndefined(obj);
+  return Object.keys(stripped).map(k => {
+    const ks = encode(k) + "=";
+    if (Array.isArray(stripped[k])) {
+      return stripped[k].map(v => ks + encode(v)).join(sep);
+    } else {
+      return ks + encode(stripped[k]);
+    }
+  }).join(sep);
+}
+
+/**
+ * Checks if a version is within the provided range.
+ *
+ * @param  {String} version    The version to check.
+ * @param  {String} minVersion The minimum supported version (inclusive).
+ * @param  {String} maxVersion The minimum supported version (exclusive).
+ * @throws {Error} If the version is outside of the provided range.
+ */
+function checkVersion(version, minVersion, maxVersion) {
+  const extract = str => str.split(".").map(x => parseInt(x, 10));
+  const [verMajor, verMinor] = extract(version);
+  const [minMajor, minMinor] = extract(minVersion);
+  const [maxMajor, maxMinor] = extract(maxVersion);
+  const checks = [verMajor < minMajor, verMajor === minMajor && verMinor < minMinor, verMajor > maxMajor, verMajor === maxMajor && verMinor >= maxMinor];
+  if (checks.some(x => x)) {
+    throw new Error(`Version ${ version } doesn't satisfy ` + `${ minVersion } <= x < ${ maxVersion }`);
+  }
+}
+
+/**
+ * Generates a decorator function ensuring a version check is performed against
+ * the provided requirements before executing it.
+ *
+ * @param  {String} min The required min version (inclusive).
+ * @param  {String} max The required max version (inclusive).
+ * @return {Function}
+ */
+function support(min, max) {
+  return function (target, key, descriptor) {
+    const fn = descriptor.value;
+    return {
+      configurable: true,
+      get() {
+        const wrappedMethod = (...args) => {
+          // "this" is the current instance which its method is decorated.
+          const client = "client" in this ? this.client : this;
+          return client.fetchHTTPApiVersion().then(version => checkVersion(version, min, max)).then(Promise.resolve(fn.apply(this, args)));
+        };
+        Object.defineProperty(this, key, {
+          value: wrappedMethod,
+          configurable: true,
+          writable: true
+        });
+        return wrappedMethod;
+      }
+    };
+  };
+}
+
+/**
+ * Generates a decorator function ensuring an operation is not performed from
+ * within a batch request.
+ *
+ * @param  {String} message The error message to throw.
+ * @return {Function}
+ */
+function nobatch(message) {
+  return function (target, key, descriptor) {
+    const fn = descriptor.value;
+    return {
+      configurable: true,
+      get() {
+        const wrappedMethod = (...args) => {
+          // "this" is the current instance which its method is decorated.
+          if (this._isBatch) {
+            throw new Error(message);
+          }
+          return fn.apply(this, args);
+        };
+        Object.defineProperty(this, key, {
+          value: wrappedMethod,
+          configurable: true,
+          writable: true
+        });
+        return wrappedMethod;
+      }
+    };
+  };
+}
+
+},{}]},{},[1])(1)
+});
\ No newline at end of file
rename from services/common/moz-kinto-client.js
rename to services/common/kinto-offline-client.js
--- a/services/common/moz-kinto-client.js
+++ b/services/common/kinto-offline-client.js
@@ -13,43 +13,51 @@
  * limitations under the License.
  */
 
 /*
  * This file is generated from kinto.js - do not modify directly.
  */
 
 this.EXPORTED_SYMBOLS = ["loadKinto"];
+
+/*
+ * Version 2.0.0 - 8b846f8
+ */
+
 (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.loadKinto = 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){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
 var _base = require("../src/adapters/base");
 
 var _base2 = _interopRequireDefault(_base);
 
+var _utils = require("../src/utils");
+
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-Components.utils.import("resource://gre/modules/Sqlite.jsm"); /*
-                                                               * 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.
-                                                               */
+/*
+ * 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.
+ */
 
+Components.utils.import("resource://gre/modules/Sqlite.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 
 const statements = {
   "createCollectionData": `
     CREATE TABLE collection_data (
       collection_name TEXT,
       record_id TEXT,
       record TEXT
@@ -121,40 +129,19 @@ class FirefoxAdapter extends _base2.defa
   }
 
   _init(connection) {
     return Task.spawn(function* () {
       yield connection.executeTransaction(function* doSetup() {
         const schema = yield connection.getSchemaVersion();
 
         if (schema == 0) {
-          var _iteratorNormalCompletion = true;
-          var _didIteratorError = false;
-          var _iteratorError = undefined;
 
-          try {
-
-            for (var _iterator = createStatements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
-              const statementName = _step.value;
-
-              yield connection.execute(statements[statementName]);
-            }
-          } catch (err) {
-            _didIteratorError = true;
-            _iteratorError = err;
-          } finally {
-            try {
-              if (!_iteratorNormalCompletion && _iterator.return) {
-                _iterator.return();
-              }
-            } finally {
-              if (_didIteratorError) {
-                throw _iteratorError;
-              }
-            }
+          for (let statementName of createStatements) {
+            yield connection.execute(statements[statementName]);
           }
 
           yield connection.setSchemaVersion(currentSchemaVersion);
         } else if (schema != 1) {
           throw new Error("Unknown database schema: " + schema);
         }
       });
       return connection;
@@ -205,39 +192,18 @@ class FirefoxAdapter extends _base2.defa
     let result;
     try {
       result = callback(proxy);
     } catch (e) {
       return Promise.reject(e);
     }
     const conn = this._connection;
     return conn.executeTransaction(function* doExecuteTransaction() {
-      var _iteratorNormalCompletion2 = true;
-      var _didIteratorError2 = false;
-      var _iteratorError2 = undefined;
-
-      try {
-        for (var _iterator2 = proxy.operations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
-          const { statement, params } = _step2.value;
-
-          yield conn.executeCached(statement, params);
-        }
-      } catch (err) {
-        _didIteratorError2 = true;
-        _iteratorError2 = err;
-      } finally {
-        try {
-          if (!_iteratorNormalCompletion2 && _iterator2.return) {
-            _iterator2.return();
-          }
-        } finally {
-          if (_didIteratorError2) {
-            throw _iteratorError2;
-          }
-        }
+      for (let { statement, params } of proxy.operations) {
+        yield conn.executeCached(statement, params);
       }
     }).then(_ => result);
   }
 
   get(id) {
     const params = {
       collection_name: this.collection,
       record_id: id
@@ -245,27 +211,31 @@ class FirefoxAdapter extends _base2.defa
     return this._executeStatement(statements.getRecord, params).then(result => {
       if (result.length == 0) {
         return;
       }
       return JSON.parse(result[0].getResultByName("record"));
     });
   }
 
-  list() {
-    const params = {
+  list(params = { filters: {}, order: "" }) {
+    const parameters = {
       collection_name: this.collection
     };
-    return this._executeStatement(statements.listRecords, params).then(result => {
+    return this._executeStatement(statements.listRecords, parameters).then(result => {
       const records = [];
       for (let k = 0; k < result.length; k++) {
         const row = result[k];
         records.push(JSON.parse(row.getResultByName("record")));
       }
       return records;
+    }).then(results => {
+      // The resulting list of records is filtered and sorted.
+      // XXX: with some efforts, this could be implemented using SQL.
+      return (0, _utils.reduceRecords)(params.filters, params.order, results);
     });
   }
 
   /**
    * Load a list of records into the local database.
    *
    * Note: The adapter is not in charge of filtering the already imported
    * records. This is done in `Collection#loadDump()`, as a common behaviour
@@ -274,52 +244,30 @@ class FirefoxAdapter extends _base2.defa
    * @param  {Array} records.
    * @return {Array} imported records.
    */
   loadDump(records) {
     const connection = this._connection;
     const collection_name = this.collection;
     return Task.spawn(function* () {
       yield connection.executeTransaction(function* doImport() {
-        var _iteratorNormalCompletion3 = true;
-        var _didIteratorError3 = false;
-        var _iteratorError3 = undefined;
-
-        try {
-          for (var _iterator3 = records[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
-            const record = _step3.value;
-
-            const params = {
-              collection_name: collection_name,
-              record_id: record.id,
-              record: JSON.stringify(record)
-            };
-            yield connection.execute(statements.importData, params);
-          }
-        } catch (err) {
-          _didIteratorError3 = true;
-          _iteratorError3 = err;
-        } finally {
-          try {
-            if (!_iteratorNormalCompletion3 && _iterator3.return) {
-              _iterator3.return();
-            }
-          } finally {
-            if (_didIteratorError3) {
-              throw _iteratorError3;
-            }
-          }
+        for (let record of records) {
+          const params = {
+            collection_name: collection_name,
+            record_id: record.id,
+            record: JSON.stringify(record)
+          };
+          yield connection.execute(statements.importData, params);
         }
-
         const lastModified = Math.max(...records.map(record => record.last_modified));
         const params = {
           collection_name: collection_name
         };
         const previousLastModified = yield connection.execute(statements.getLastModified, params).then(result => {
-          return result ? result[0].getResultByName('last_modified') : -1;
+          return result.length > 0 ? result[0].getResultByName('last_modified') : -1;
         });
         if (lastModified > previousLastModified) {
           const params = {
             collection_name: collection_name,
             last_modified: lastModified
           };
           yield connection.execute(statements.saveLastModified, params);
         }
@@ -393,17 +341,17 @@ function transactionProxy(collection, pr
 
     get(id) {
       // Gecko JS engine outputs undesired warnings if id is not in preloaded.
       return id in preloaded ? preloaded[id] : undefined;
     }
   };
 }
 
-},{"../src/adapters/base":11}],2:[function(require,module,exports){
+},{"../src/adapters/base":6,"../src/utils":8}],2:[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
  *
@@ -428,1363 +376,214 @@ var _base2 = _interopRequireDefault(_bas
 var _KintoBase = require("../src/KintoBase");
 
 var _KintoBase2 = _interopRequireDefault(_KintoBase);
 
 var _FirefoxStorage = require("./FirefoxStorage");
 
 var _FirefoxStorage2 = _interopRequireDefault(_FirefoxStorage);
 
+var _utils = require("../src/utils");
+
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-const Cu = Components.utils;
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 function loadKinto() {
   const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
+  const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+  // Use standalone kinto-client module landed in FFx.
+  const { KintoHttpClient } = Cu.import("resource://services-common/kinto-http-client.js");
 
   Cu.import("resource://gre/modules/Timer.jsm");
   Cu.importGlobalProperties(['fetch']);
 
+  // Leverage Gecko service to generate UUIDs.
+  function makeIDSchema() {
+    return {
+      validate: _utils.RE_UUID.test.bind(_utils.RE_UUID),
+      generate: function () {
+        return generateUUID().toString().replace(/[{}]/g, "");
+      }
+    };
+  }
+
   class KintoFX extends _KintoBase2.default {
     static get adapters() {
       return {
         BaseAdapter: _base2.default,
         FirefoxAdapter: _FirefoxStorage2.default
       };
     }
 
     constructor(options = {}) {
       const emitter = {};
       EventEmitter.decorate(emitter);
 
       const defaults = {
-        events: emitter
+        events: emitter,
+        ApiClass: KintoHttpClient
       };
 
       const expandedOptions = Object.assign(defaults, options);
       super(expandedOptions);
     }
+
+    collection(collName, options = {}) {
+      const idSchema = makeIDSchema();
+      const expandedOptions = Object.assign({ idSchema }, options);
+      return super.collection(collName, expandedOptions);
+    }
   }
 
   return KintoFX;
 }
 
 // This fixes compatibility with CommonJS required by browserify.
 // See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
 if (typeof module === "object") {
   module.exports = loadKinto;
 }
 
-},{"../src/KintoBase":10,"../src/adapters/base":11,"./FirefoxStorage":1}],3:[function(require,module,exports){
-// http://wiki.commonjs.org/wiki/Unit_Testing/1.0
-//
-// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
-//
-// Originally from narwhal.js (http://narwhaljs.org)
-// Copyright (c) 2009 Thomas Robinson <280north.com>
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the 'Software'), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+},{"../src/KintoBase":5,"../src/adapters/base":6,"../src/utils":8,"./FirefoxStorage":1}],3:[function(require,module,exports){
 
-// when used in node, this will actually load the util module we depend on
-// versus loading the builtin util module as happens otherwise
-// this is a bug in node module loading as far as I am concerned
-var util = require('util/');
-
-var pSlice = Array.prototype.slice;
-var hasOwn = Object.prototype.hasOwnProperty;
+},{}],4:[function(require,module,exports){
+'use strict'
 
-// 1. The assert module provides functions that throw
-// AssertionError's when particular conditions are not met. The
-// assert module must conform to the following interface.
-
-var assert = module.exports = ok;
-
-// 2. The AssertionError is defined in assert.
-// new assert.AssertionError({ message: message,
-//                             actual: actual,
-//                             expected: expected })
+function isArguments (object) {
+  return Object.prototype.toString.call(object) === '[object Arguments]'
+}
 
-assert.AssertionError = function AssertionError(options) {
-  this.name = 'AssertionError';
-  this.actual = options.actual;
-  this.expected = options.expected;
-  this.operator = options.operator;
-  if (options.message) {
-    this.message = options.message;
-    this.generatedMessage = false;
-  } else {
-    this.message = getMessage(this);
-    this.generatedMessage = true;
-  }
-  var stackStartFunction = options.stackStartFunction || fail;
-
-  if (Error.captureStackTrace) {
-    Error.captureStackTrace(this, stackStartFunction);
-  }
-  else {
-    // non v8 browsers so we can have a stacktrace
-    var err = new Error();
-    if (err.stack) {
-      var out = err.stack;
+function deeper (a, b) {
+  return deeper_(a, b, [], [])
+}
 
-      // try to strip useless frames
-      var fn_name = stackStartFunction.name;
-      var idx = out.indexOf('\n' + fn_name);
-      if (idx >= 0) {
-        // once we have located the function frame
-        // we need to strip out everything before it (and its line)
-        var next_line = out.indexOf('\n', idx + 1);
-        out = out.substring(next_line + 1);
-      }
-
-      this.stack = out;
-    }
-  }
-};
+module.exports = deeper
 
-// assert.AssertionError instanceof Error
-util.inherits(assert.AssertionError, Error);
-
-function replacer(key, value) {
-  if (util.isUndefined(value)) {
-    return '' + value;
-  }
-  if (util.isNumber(value) && !isFinite(value)) {
-    return value.toString();
-  }
-  if (util.isFunction(value) || util.isRegExp(value)) {
-    return value.toString();
-  }
-  return value;
+try {
+  deeper.fastEqual = require('buffertools').equals
+} catch (e) {
+  // whoops, nobody told buffertools it wasn't installed
 }
 
-function truncate(s, n) {
-  if (util.isString(s)) {
-    return s.length < n ? s : s.slice(0, n);
-  } else {
-    return s;
-  }
-}
-
-function getMessage(self) {
-  return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' +
-         self.operator + ' ' +
-         truncate(JSON.stringify(self.expected, replacer), 128);
-}
-
-// At present only the three keys mentioned above are used and
-// understood by the spec. Implementations or sub modules can pass
-// other keys to the AssertionError's constructor - they will be
-// ignored.
-
-// 3. All of the following functions must throw an AssertionError
-// when a corresponding condition is not met, with a message that
-// may be undefined if not provided.  All assertion methods provide
-// both the actual and expected values to the assertion error for
-// display purposes.
-
-function fail(actual, expected, message, operator, stackStartFunction) {
-  throw new assert.AssertionError({
-    message: message,
-    actual: actual,
-    expected: expected,
-    operator: operator,
-    stackStartFunction: stackStartFunction
-  });
-}
-
-// EXTENSION! allows for well behaved errors defined elsewhere.
-assert.fail = fail;
+/**
+ * This is a Node-specific version of a structural equality test, modeled on
+ * bits and pieces of loads of other implementations of this algorithm, most
+ * notably the one in the Node.js source and the Underscore library. It doesn't
+ * throw and handles cycles.
+ *
+ * Everybody who writes one of these functions puts the documentation
+ * inline, which makes it incredibly hard to follow. Here's what this version
+ * of the algorithm does, in order:
+ *
+ * 1. `===` only tests objects and functions by reference. `null` is an object.
+ *    Any pairs of identical entities failing this test are therefore objects
+ *    (including `null`), which need to be recursed into and compared attribute by
+ *    attribute.
+ * 2. Since the only entities to get to this test must be objects, if `a` or `b`
+ *    is not an object, they're clearly not the same. All unfiltered `a` and `b`
+ *    getting past this are objects (including `null`).
+ * 3. `null` is an object, but `null === null.` All unfiltered `a` and `b` are
+ *    non-null `Objects`.
+ * 4. Buffers need to be special-cased because they live partially on the wrong
+ *    side of the C++ / JavaScript barrier. Still, calling this on structures
+ *    that can contain Buffers is a bad idea, because they can contain
+ *    multiple megabytes of data and comparing them byte-by-byte is hella
+ *    expensive.
+ * 5. It's much faster to compare dates by numeric value (`.getTime()`) than by
+ *    lexical value.
+ * 6. Compare `RegExps` by their components, not the objects themselves.
+ * 7. Treat argumens objects like arrays. The parts of an arguments list most
+ *    people care about are the arguments themselves, not `callee`, which you
+ *    shouldn't be looking at anyway.
+ * 8. Objects are more complex:
+ *     1. Ensure that `a` and `b` are on the same constructor chain.
+ *     2. Ensure that `a` and `b` have the same number of own properties (which is
+ *        what `Object.keys()` returns).
+ *     3. Ensure that cyclical references don't blow up the stack.
+ *     4. Ensure that all the key names match (faster).
+ *     5. Ensure that all of the associated values match, recursively (slower).
+ *
+ * (somewhat untested) assumptions:
+ *
+ * - Functions are only considered identical if they unify to the same
+ *   reference. To anything else is to invite the wrath of the halting problem.
+ * - V8 is smart enough to optimize treating an Array like any other kind of
+ *   object.
+ * - Users of this function are cool with mutually recursive data structures
+ *   that are otherwise identical being treated as the same.
+ */
+function deeper_ (a, b, ca, cb) {
+  if (a === b) {
+    return true
+  } else if (typeof a !== 'object' || typeof b !== 'object') {
+    return false
+  } else if (a === null || b === null) {
+    return false
+  } else if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
+    if (a.equals) {
+      return a.equals(b)
+    } else if (deeper.fastEqual) {
+      return deeper.fastEqual.call(a, b)
+    } else {
+      if (a.length !== b.length) return false
 
-// 4. Pure assertion tests whether a value is truthy, as determined
-// by !!guard.
-// assert.ok(guard, message_opt);
-// This statement is equivalent to assert.equal(true, !!guard,
-// message_opt);. To test strictly for the value true, use
-// assert.strictEqual(true, guard, message_opt);.
+      for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
 
-function ok(value, message) {
-  if (!value) fail(value, true, message, '==', assert.ok);
-}
-assert.ok = ok;
-
-// 5. The equality assertion tests shallow, coercive equality with
-// ==.
-// assert.equal(actual, expected, message_opt);
-
-assert.equal = function equal(actual, expected, message) {
-  if (actual != expected) fail(actual, expected, message, '==', assert.equal);
-};
-
-// 6. The non-equality assertion tests for whether two objects are not equal
-// with != assert.notEqual(actual, expected, message_opt);
+      return true
+    }
+  } else if (a instanceof Date && b instanceof Date) {
+    return a.getTime() === b.getTime()
+  } else if (a instanceof RegExp && b instanceof RegExp) {
+    return a.source === b.source &&
+    a.global === b.global &&
+    a.multiline === b.multiline &&
+    a.lastIndex === b.lastIndex &&
+    a.ignoreCase === b.ignoreCase
+  } else if (isArguments(a) || isArguments(b)) {
+    if (!(isArguments(a) && isArguments(b))) return false
 
-assert.notEqual = function notEqual(actual, expected, message) {
-  if (actual == expected) {
-    fail(actual, expected, message, '!=', assert.notEqual);
-  }
-};
+    var slice = Array.prototype.slice
+    return deeper_(slice.call(a), slice.call(b), ca, cb)
+  } else {
+    if (a.constructor !== b.constructor) return false
 
-// 7. The equivalence assertion tests a deep equality relation.
-// assert.deepEqual(actual, expected, message_opt);
+    var ka = Object.keys(a)
+    var kb = Object.keys(b)
+    // don't bother with stack acrobatics if there's nothing there
+    if (ka.length === 0 && kb.length === 0) return true
+    if (ka.length !== kb.length) return false
 
-assert.deepEqual = function deepEqual(actual, expected, message) {
-  if (!_deepEqual(actual, expected)) {
-    fail(actual, expected, message, 'deepEqual', assert.deepEqual);
-  }
-};
+    var cal = ca.length
+    while (cal--) if (ca[cal] === a) return cb[cal] === b
+    ca.push(a); cb.push(b)
 
-function _deepEqual(actual, expected) {
-  // 7.1. All identical values are equivalent, as determined by ===.
-  if (actual === expected) {
-    return true;
+    ka.sort(); kb.sort()
+    for (var j = ka.length - 1; j >= 0; j--) if (ka[j] !== kb[j]) return false
 
-  } else if (util.isBuffer(actual) && util.isBuffer(expected)) {
-    if (actual.length != expected.length) return false;
-
-    for (var i = 0; i < actual.length; i++) {
-      if (actual[i] !== expected[i]) return false;
+    var key
+    for (var k = ka.length - 1; k >= 0; k--) {
+      key = ka[k]
+      if (!deeper_(a[key], b[key], ca, cb)) return false
     }
 
-    return true;
-
-  // 7.2. If the expected value is a Date object, the actual value is
-  // equivalent if it is also a Date object that refers to the same time.
-  } else if (util.isDate(actual) && util.isDate(expected)) {
-    return actual.getTime() === expected.getTime();
-
-  // 7.3 If the expected value is a RegExp object, the actual value is
-  // equivalent if it is also a RegExp object with the same source and
-  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
-  } else if (util.isRegExp(actual) && util.isRegExp(expected)) {
-    return actual.source === expected.source &&
-           actual.global === expected.global &&
-           actual.multiline === expected.multiline &&
-           actual.lastIndex === expected.lastIndex &&
-           actual.ignoreCase === expected.ignoreCase;
-
-  // 7.4. Other pairs that do not both pass typeof value == 'object',
-  // equivalence is determined by ==.
-  } else if (!util.isObject(actual) && !util.isObject(expected)) {
-    return actual == expected;
-
-  // 7.5 For all other Object pairs, including Array objects, equivalence is
-  // determined by having the same number of owned properties (as verified
-  // with Object.prototype.hasOwnProperty.call), the same set of keys
-  // (although not necessarily the same order), equivalent values for every
-  // corresponding key, and an identical 'prototype' property. Note: this
-  // accounts for both named and indexed properties on Arrays.
-  } else {
-    return objEquiv(actual, expected);
-  }
-}
-
-function isArguments(object) {
-  return Object.prototype.toString.call(object) == '[object Arguments]';
-}
-
-function objEquiv(a, b) {
-  if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b))
-    return false;
-  // an identical 'prototype' property.
-  if (a.prototype !== b.prototype) return false;
-  // if one is a primitive, the other must be same
-  if (util.isPrimitive(a) || util.isPrimitive(b)) {
-    return a === b;
-  }
-  var aIsArgs = isArguments(a),
-      bIsArgs = isArguments(b);
-  if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))
-    return false;
-  if (aIsArgs) {
-    a = pSlice.call(a);
-    b = pSlice.call(b);
-    return _deepEqual(a, b);
-  }
-  var ka = objectKeys(a),
-      kb = objectKeys(b),
-      key, i;
-  // having the same number of owned properties (keys incorporates
-  // hasOwnProperty)
-  if (ka.length != kb.length)
-    return false;
-  //the same set of keys (although not necessarily the same order),
-  ka.sort();
-  kb.sort();
-  //~~~cheap key test
-  for (i = ka.length - 1; i >= 0; i--) {
-    if (ka[i] != kb[i])
-      return false;
-  }
-  //equivalent values for every corresponding key, and
-  //~~~possibly expensive deep test
-  for (i = ka.length - 1; i >= 0; i--) {
-    key = ka[i];
-    if (!_deepEqual(a[key], b[key])) return false;
-  }
-  return true;
-}
-
-// 8. The non-equivalence assertion tests for any deep inequality.
-// assert.notDeepEqual(actual, expected, message_opt);
-
-assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
-  if (_deepEqual(actual, expected)) {
-    fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual);
-  }
-};
-
-// 9. The strict equality assertion tests strict equality, as determined by ===.
-// assert.strictEqual(actual, expected, message_opt);
-
-assert.strictEqual = function strictEqual(actual, expected, message) {
-  if (actual !== expected) {
-    fail(actual, expected, message, '===', assert.strictEqual);
-  }
-};
+    ca.pop(); cb.pop()
 
-// 10. The strict non-equality assertion tests for strict inequality, as
-// determined by !==.  assert.notStrictEqual(actual, expected, message_opt);
-
-assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
-  if (actual === expected) {
-    fail(actual, expected, message, '!==', assert.notStrictEqual);
-  }
-};
-
-function expectedException(actual, expected) {
-  if (!actual || !expected) {
-    return false;
-  }
-
-  if (Object.prototype.toString.call(expected) == '[object RegExp]') {
-    return expected.test(actual);
-  } else if (actual instanceof expected) {
-    return true;
-  } else if (expected.call({}, actual) === true) {
-    return true;
-  }
-
-  return false;
-}
-
-function _throws(shouldThrow, block, expected, message) {
-  var actual;
-
-  if (util.isString(expected)) {
-    message = expected;
-    expected = null;
-  }
-
-  try {
-    block();
-  } catch (e) {
-    actual = e;
-  }
-
-  message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
-            (message ? ' ' + message : '.');
-
-  if (shouldThrow && !actual) {
-    fail(actual, expected, 'Missing expected exception' + message);
-  }
-
-  if (!shouldThrow && expectedException(actual, expected)) {
-    fail(actual, expected, 'Got unwanted exception' + message);
-  }
-
-  if ((shouldThrow && actual && expected &&
-      !expectedException(actual, expected)) || (!shouldThrow && actual)) {
-    throw actual;
-  }
-}
-
-// 11. Expected to throw an error:
-// assert.throws(block, Error_opt, message_opt);
-
-assert.throws = function(block, /*optional*/error, /*optional*/message) {
-  _throws.apply(this, [true].concat(pSlice.call(arguments)));
-};
-
-// EXTENSION! This is annoying to write outside this module.
-assert.doesNotThrow = function(block, /*optional*/message) {
-  _throws.apply(this, [false].concat(pSlice.call(arguments)));
-};
-
-assert.ifError = function(err) { if (err) {throw err;}};
-
-var objectKeys = Object.keys || function (obj) {
-  var keys = [];
-  for (var key in obj) {
-    if (hasOwn.call(obj, key)) keys.push(key);
-  }
-  return keys;
-};
-
-},{"util/":7}],4:[function(require,module,exports){
-if (typeof Object.create === 'function') {
-  // implementation from standard node.js 'util' module
-  module.exports = function inherits(ctor, superCtor) {
-    ctor.super_ = superCtor
-    ctor.prototype = Object.create(superCtor.prototype, {
-      constructor: {
-        value: ctor,
-        enumerable: false,
-        writable: true,
-        configurable: true
-      }
-    });
-  };
-} else {
-  // old school shim for old browsers
-  module.exports = function inherits(ctor, superCtor) {
-    ctor.super_ = superCtor
-    var TempCtor = function () {}
-    TempCtor.prototype = superCtor.prototype
-    ctor.prototype = new TempCtor()
-    ctor.prototype.constructor = ctor
+    return true
   }
 }
 
-},{}],5:[function(require,module,exports){
-// shim for using process in browser
-
-var process = module.exports = {};
-var queue = [];
-var draining = false;
-var currentQueue;
-var queueIndex = -1;
-
-function cleanUpNextTick() {
-    draining = false;
-    if (currentQueue.length) {
-        queue = currentQueue.concat(queue);
-    } else {
-        queueIndex = -1;
-    }
-    if (queue.length) {
-        drainQueue();
-    }
-}
-
-function drainQueue() {
-    if (draining) {
-        return;
-    }
-    var timeout = setTimeout(cleanUpNextTick);
-    draining = true;
-
-    var len = queue.length;
-    while(len) {
-        currentQueue = queue;
-        queue = [];
-        while (++queueIndex < len) {
-            if (currentQueue) {
-                currentQueue[queueIndex].run();
-            }
-        }
-        queueIndex = -1;
-        len = queue.length;
-    }
-    currentQueue = null;
-    draining = false;
-    clearTimeout(timeout);
-}
-
-process.nextTick = function (fun) {
-    var args = new Array(arguments.length - 1);
-    if (arguments.length > 1) {
-        for (var i = 1; i < arguments.length; i++) {
-            args[i - 1] = arguments[i];
-        }
-    }
-    queue.push(new Item(fun, args));
-    if (queue.length === 1 && !draining) {
-        setTimeout(drainQueue, 0);
-    }
-};
-
-// v8 likes predictible objects
-function Item(fun, array) {
-    this.fun = fun;
-    this.array = array;
-}
-Item.prototype.run = function () {
-    this.fun.apply(null, this.array);
-};
-process.title = 'browser';
-process.browser = true;
-process.env = {};
-process.argv = [];
-process.version = ''; // empty string to avoid regexp issues
-process.versions = {};
-
-function noop() {}
-
-process.on = noop;
-process.addListener = noop;
-process.once = noop;
-process.off = noop;
-process.removeListener = noop;
-process.removeAllListeners = noop;
-process.emit = noop;
-
-process.binding = function (name) {
-    throw new Error('process.binding is not supported');
-};
-
-process.cwd = function () { return '/' };
-process.chdir = function (dir) {
-    throw new Error('process.chdir is not supported');
-};
-process.umask = function() { return 0; };
-
-},{}],6:[function(require,module,exports){
-module.exports = function isBuffer(arg) {
-  return arg && typeof arg === 'object'
-    && typeof arg.copy === 'function'
-    && typeof arg.fill === 'function'
-    && typeof arg.readUInt8 === 'function';
-}
-},{}],7:[function(require,module,exports){
-(function (process,global){
-// Copyright Joyent, Inc. and other Node contributors.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to permit
-// persons to whom the Software is furnished to do so, subject to the
-// following conditions:
-//
-// The above copyright notice and this permission notice shall be included
-// in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-// USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-var formatRegExp = /%[sdj%]/g;
-exports.format = function(f) {
-  if (!isString(f)) {
-    var objects = [];
-    for (var i = 0; i < arguments.length; i++) {
-      objects.push(inspect(arguments[i]));
-    }
-    return objects.join(' ');
-  }
-
-  var i = 1;
-  var args = arguments;
-  var len = args.length;
-  var str = String(f).replace(formatRegExp, function(x) {
-    if (x === '%%') return '%';
-    if (i >= len) return x;
-    switch (x) {
-      case '%s': return String(args[i++]);
-      case '%d': return Number(args[i++]);
-      case '%j':
-        try {
-          return JSON.stringify(args[i++]);
-        } catch (_) {
-          return '[Circular]';
-        }
-      default:
-        return x;
-    }
-  });
-  for (var x = args[i]; i < len; x = args[++i]) {
-    if (isNull(x) || !isObject(x)) {
-      str += ' ' + x;
-    } else {
-      str += ' ' + inspect(x);
-    }
-  }
-  return str;
-};
-
-
-// Mark that a method should not be used.
-// Returns a modified function which warns once by default.
-// If --no-deprecation is set, then it is a no-op.
-exports.deprecate = function(fn, msg) {
-  // Allow for deprecating things in the process of starting up.
-  if (isUndefined(global.process)) {
-    return function() {
-      return exports.deprecate(fn, msg).apply(this, arguments);
-    };
-  }
-
-  if (process.noDeprecation === true) {
-    return fn;
-  }
-
-  var warned = false;
-  function deprecated() {
-    if (!warned) {
-      if (process.throwDeprecation) {
-        throw new Error(msg);
-      } else if (process.traceDeprecation) {
-        console.trace(msg);
-      } else {
-        console.error(msg);
-      }
-      warned = true;
-    }
-    return fn.apply(this, arguments);
-  }
-
-  return deprecated;
-};
-
-
-var debugs = {};
-var debugEnviron;
-exports.debuglog = function(set) {
-  if (isUndefined(debugEnviron))
-    debugEnviron = process.env.NODE_DEBUG || '';
-  set = set.toUpperCase();
-  if (!debugs[set]) {
-    if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
-      var pid = process.pid;
-      debugs[set] = function() {
-        var msg = exports.format.apply(exports, arguments);
-        console.error('%s %d: %s', set, pid, msg);
-      };
-    } else {
-      debugs[set] = function() {};
-    }
-  }
-  return debugs[set];
-};
-
-
-/**
- * Echos the value of a value. Trys to print the value out
- * in the best way possible given the different types.
- *
- * @param {Object} obj The object to print out.
- * @param {Object} opts Optional options object that alters the output.
- */
-/* legacy: obj, showHidden, depth, colors*/
-function inspect(obj, opts) {
-  // default options
-  var ctx = {
-    seen: [],
-    stylize: stylizeNoColor
-  };
-  // legacy...
-  if (arguments.length >= 3) ctx.depth = arguments[2];
-  if (arguments.length >= 4) ctx.colors = arguments[3];
-  if (isBoolean(opts)) {
-    // legacy...
-    ctx.showHidden = opts;
-  } else if (opts) {
-    // got an "options" object
-    exports._extend(ctx, opts);
-  }
-  // set default options
-  if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
-  if (isUndefined(ctx.depth)) ctx.depth = 2;
-  if (isUndefined(ctx.colors)) ctx.colors = false;
-  if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
-  if (ctx.colors) ctx.stylize = stylizeWithColor;
-  return formatValue(ctx, obj, ctx.depth);
-}
-exports.inspect = inspect;
-
-
-// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
-inspect.colors = {
-  'bold' : [1, 22],
-  'italic' : [3, 23],
-  'underline' : [4, 24],
-  'inverse' : [7, 27],
-  'white' : [37, 39],
-  'grey' : [90, 39],
-  'black' : [30, 39],
-  'blue' : [34, 39],
-  'cyan' : [36, 39],
-  'green' : [32, 39],
-  'magenta' : [35, 39],
-  'red' : [31, 39],
-  'yellow' : [33, 39]
-};
-
-// Don't use 'blue' not visible on cmd.exe
-inspect.styles = {
-  'special': 'cyan',
-  'number': 'yellow',
-  'boolean': 'yellow',
-  'undefined': 'grey',
-  'null': 'bold',
-  'string': 'green',
-  'date': 'magenta',
-  // "name": intentionally not styling
-  'regexp': 'red'
-};
-
-
-function stylizeWithColor(str, styleType) {
-  var style = inspect.styles[styleType];
-
-  if (style) {
-    return '\u001b[' + inspect.colors[style][0] + 'm' + str +
-           '\u001b[' + inspect.colors[style][1] + 'm';
-  } else {
-    return str;
-  }
-}
-
-
-function stylizeNoColor(str, styleType) {
-  return str;
-}
-
-
-function arrayToHash(array) {
-  var hash = {};
-
-  array.forEach(function(val, idx) {
-    hash[val] = true;
-  });
-
-  return hash;
-}
-
-
-function formatValue(ctx, value, recurseTimes) {
-  // Provide a hook for user-specified inspect functions.
-  // Check that value is an object with an inspect function on it
-  if (ctx.customInspect &&
-      value &&
-      isFunction(value.inspect) &&
-      // Filter out the util module, it's inspect function is special
-      value.inspect !== exports.inspect &&
-      // Also filter out any prototype objects using the circular check.
-      !(value.constructor && value.constructor.prototype === value)) {
-    var ret = value.inspect(recurseTimes, ctx);
-    if (!isString(ret)) {
-      ret = formatValue(ctx, ret, recurseTimes);
-    }
-    return ret;
-  }
-
-  // Primitive types cannot have properties
-  var primitive = formatPrimitive(ctx, value);
-  if (primitive) {
-    return primitive;
-  }
-
-  // Look up the keys of the object.
-  var keys = Object.keys(value);
-  var visibleKeys = arrayToHash(keys);
-
-  if (ctx.showHidden) {
-    keys = Object.getOwnPropertyNames(value);
-  }
-
-  // IE doesn't make error fields non-enumerable
-  // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
-  if (isError(value)
-      && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
-    return formatError(value);
-  }
-
-  // Some type of object without properties can be shortcutted.
-  if (keys.length === 0) {
-    if (isFunction(value)) {
-      var name = value.name ? ': ' + value.name : '';
-      return ctx.stylize('[Function' + name + ']', 'special');
-    }
-    if (isRegExp(value)) {
-      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
-    }
-    if (isDate(value)) {
-      return ctx.stylize(Date.prototype.toString.call(value), 'date');
-    }
-    if (isError(value)) {
-      return formatError(value);
-    }
-  }
-
-  var base = '', array = false, braces = ['{', '}'];
-
-  // Make Array say that they are Array
-  if (isArray(value)) {
-    array = true;
-    braces = ['[', ']'];
-  }
-
-  // Make functions say that they are functions
-  if (isFunction(value)) {
-    var n = value.name ? ': ' + value.name : '';
-    base = ' [Function' + n + ']';
-  }
-
-  // Make RegExps say that they are RegExps
-  if (isRegExp(value)) {
-    base = ' ' + RegExp.prototype.toString.call(value);
-  }
-
-  // Make dates with properties first say the date
-  if (isDate(value)) {
-    base = ' ' + Date.prototype.toUTCString.call(value);
-  }
-
-  // Make error with message first say the error
-  if (isError(value)) {
-    base = ' ' + formatError(value);
-  }
-
-  if (keys.length === 0 && (!array || value.length == 0)) {
-    return braces[0] + base + braces[1];
-  }
-
-  if (recurseTimes < 0) {
-    if (isRegExp(value)) {
-      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
-    } else {
-      return ctx.stylize('[Object]', 'special');
-    }
-  }
-
-  ctx.seen.push(value);
-
-  var output;
-  if (array) {
-    output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
-  } else {
-    output = keys.map(function(key) {
-      return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
-    });
-  }
-
-  ctx.seen.pop();
-
-  return reduceToSingleString(output, base, braces);
-}
-
-
-function formatPrimitive(ctx, value) {
-  if (isUndefined(value))
-    return ctx.stylize('undefined', 'undefined');
-  if (isString(value)) {
-    var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
-                                             .replace(/'/g, "\\'")
-                                             .replace(/\\"/g, '"') + '\'';
-    return ctx.stylize(simple, 'string');
-  }
-  if (isNumber(value))
-    return ctx.stylize('' + value, 'number');
-  if (isBoolean(value))
-    return ctx.stylize('' + value, 'boolean');
-  // For some reason typeof null is "object", so special case here.
-  if (isNull(value))
-    return ctx.stylize('null', 'null');
-}
-
-
-function formatError(value) {
-  return '[' + Error.prototype.toString.call(value) + ']';
-}
-
-
-function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
-  var output = [];
-  for (var i = 0, l = value.length; i < l; ++i) {
-    if (hasOwnProperty(value, String(i))) {
-      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
-          String(i), true));
-    } else {
-      output.push('');
-    }
-  }
-  keys.forEach(function(key) {
-    if (!key.match(/^\d+$/)) {
-      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
-          key, true));
-    }
-  });
-  return output;
-}
-
-
-function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
-  var name, str, desc;
-  desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
-  if (desc.get) {
-    if (desc.set) {
-      str = ctx.stylize('[Getter/Setter]', 'special');
-    } else {
-      str = ctx.stylize('[Getter]', 'special');
-    }
-  } else {
-    if (desc.set) {
-      str = ctx.stylize('[Setter]', 'special');
-    }
-  }
-  if (!hasOwnProperty(visibleKeys, key)) {
-    name = '[' + key + ']';
-  }
-  if (!str) {
-    if (ctx.seen.indexOf(desc.value) < 0) {
-      if (isNull(recurseTimes)) {
-        str = formatValue(ctx, desc.value, null);
-      } else {
-        str = formatValue(ctx, desc.value, recurseTimes - 1);
-      }
-      if (str.indexOf('\n') > -1) {
-        if (array) {
-          str = str.split('\n').map(function(line) {
-            return '  ' + line;
-          }).join('\n').substr(2);
-        } else {
-          str = '\n' + str.split('\n').map(function(line) {
-            return '   ' + line;
-          }).join('\n');
-        }
-      }
-    } else {
-      str = ctx.stylize('[Circular]', 'special');
-    }
-  }
-  if (isUndefined(name)) {
-    if (array && key.match(/^\d+$/)) {
-      return str;
-    }
-    name = JSON.stringify('' + key);
-    if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
-      name = name.substr(1, name.length - 2);
-      name = ctx.stylize(name, 'name');
-    } else {
-      name = name.replace(/'/g, "\\'")
-                 .replace(/\\"/g, '"')
-                 .replace(/(^"|"$)/g, "'");
-      name = ctx.stylize(name, 'string');
-    }
-  }
-
-  return name + ': ' + str;
-}
-
-
-function reduceToSingleString(output, base, braces) {
-  var numLinesEst = 0;
-  var length = output.reduce(function(prev, cur) {
-    numLinesEst++;
-    if (cur.indexOf('\n') >= 0) numLinesEst++;
-    return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
-  }, 0);
-
-  if (length > 60) {
-    return braces[0] +
-           (base === '' ? '' : base + '\n ') +
-           ' ' +
-           output.join(',\n  ') +
-           ' ' +
-           braces[1];
-  }
-
-  return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
-}
-
-
-// NOTE: These type checking functions intentionally don't use `instanceof`
-// because it is fragile and can be easily faked with `Object.create()`.
-function isArray(ar) {
-  return Array.isArray(ar);
-}
-exports.isArray = isArray;
-
-function isBoolean(arg) {
-  return typeof arg === 'boolean';
-}
-exports.isBoolean = isBoolean;
-
-function isNull(arg) {
-  return arg === null;
-}
-exports.isNull = isNull;
-
-function isNullOrUndefined(arg) {
-  return arg == null;
-}
-exports.isNullOrUndefined = isNullOrUndefined;
-
-function isNumber(arg) {
-  return typeof arg === 'number';
-}
-exports.isNumber = isNumber;
-
-function isString(arg) {
-  return typeof arg === 'string';
-}
-exports.isString = isString;
-
-function isSymbol(arg) {
-  return typeof arg === 'symbol';
-}
-exports.isSymbol = isSymbol;
-
-function isUndefined(arg) {
-  return arg === void 0;
-}
-exports.isUndefined = isUndefined;
-
-function isRegExp(re) {
-  return isObject(re) && objectToString(re) === '[object RegExp]';
-}
-exports.isRegExp = isRegExp;
-
-function isObject(arg) {
-  return typeof arg === 'object' && arg !== null;
-}
-exports.isObject = isObject;
-
-function isDate(d) {
-  return isObject(d) && objectToString(d) === '[object Date]';
-}
-exports.isDate = isDate;
-
-function isError(e) {
-  return isObject(e) &&
-      (objectToString(e) === '[object Error]' || e instanceof Error);
-}
-exports.isError = isError;
-
-function isFunction(arg) {
-  return typeof arg === 'function';
-}
-exports.isFunction = isFunction;
-
-function isPrimitive(arg) {
-  return arg === null ||
-         typeof arg === 'boolean' ||
-         typeof arg === 'number' ||
-         typeof arg === 'string' ||
-         typeof arg === 'symbol' ||  // ES6 symbol
-         typeof arg === 'undefined';
-}
-exports.isPrimitive = isPrimitive;
-
-exports.isBuffer = require('./support/isBuffer');
-
-function objectToString(o) {
-  return Object.prototype.toString.call(o);
-}
-
-
-function pad(n) {
-  return n < 10 ? '0' + n.toString(10) : n.toString(10);
-}
-
-
-var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
-              'Oct', 'Nov', 'Dec'];
-
-// 26 Feb 16:19:34
-function timestamp() {
-  var d = new Date();
-  var time = [pad(d.getHours()),
-              pad(d.getMinutes()),
-              pad(d.getSeconds())].join(':');
-  return [d.getDate(), months[d.getMonth()], time].join(' ');
-}
-
-
-// log is just a thin wrapper to console.log that prepends a timestamp
-exports.log = function() {
-  console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
-};
-
-
-/**
- * Inherit the prototype methods from one constructor into another.
- *
- * The Function.prototype.inherits from lang.js rewritten as a standalone
- * function (not on Function.prototype). NOTE: If this file is to be loaded
- * during bootstrapping this function needs to be rewritten using some native
- * functions as prototype setup using normal JavaScript does not work as
- * expected during bootstrapping (see mirror.js in r114903).
- *
- * @param {function} ctor Constructor function which needs to inherit the
- *     prototype.
- * @param {function} superCtor Constructor function to inherit prototype from.
- */
-exports.inherits = require('inherits');
-
-exports._extend = function(origin, add) {
-  // Don't do anything if add isn't an object
-  if (!add || !isObject(add)) return origin;
-
-  var keys = Object.keys(add);
-  var i = keys.length;
-  while (i--) {
-    origin[keys[i]] = add[keys[i]];
-  }
-  return origin;
-};
-
-function hasOwnProperty(obj, prop) {
-  return Object.prototype.hasOwnProperty.call(obj, prop);
-}
-
-}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./support/isBuffer":6,"_process":5,"inherits":4}],8:[function(require,module,exports){
-(function (global){
-
-var rng;
-
-if (global.crypto && crypto.getRandomValues) {
-  // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
-  // Moderately fast, high quality
-  var _rnds8 = new Uint8Array(16);
-  rng = function whatwgRNG() {
-    crypto.getRandomValues(_rnds8);
-    return _rnds8;
-  };
-}
-
-if (!rng) {
-  // 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() {
-    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;
-
-
-}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],9:[function(require,module,exports){
-//     uuid.js
-//
-//     Copyright (c) 2010-2012 Robert Kieffer
-//     MIT License - http://opensource.org/licenses/mit-license.php
-
-// 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('./rng');
-
-// Maps for number <-> hex string conversion
-var _byteToHex = [];
-var _hexToByte = {};
-for (var i = 0; i < 256; i++) {
-  _byteToHex[i] = (i + 0x100).toString(16).substr(1);
-  _hexToByte[_byteToHex[i]] = i;
-}
-
-// **`parse()` - Parse a UUID into it's component bytes**
-function parse(s, buf, offset) {
-  var i = (buf && offset) || 0, ii = 0;
-
-  buf = buf || [];
-  s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
-    if (ii < 16) { // Don't overflow!
-      buf[i + ii++] = _hexToByte[oct];
-    }
-  });
-
-  // Zero out remaining bytes if string was short
-  while (ii < 16) {
-    buf[i + ii++] = 0;
-  }
-
-  return buf;
-}
-
-// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
-function unparse(buf, offset) {
-  var i = offset || 0, bth = _byteToHex;
-  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++]];
-}
-
-// **`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;
-
-// Previous uuid creation time
-var _lastMSecs = 0, _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 clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
-
-  // 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
-  // cycle to simulate higher resolution clock
-  var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
-
-  // Time since last uuid creation (in msecs)
-  var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
-
-  // Per 4.2.1.2, Bump clockseq on clock regression
-  if (dt < 0 && options.clockseq === undefined) {
-    clockseq = clockseq + 1 & 0x3fff;
-  }
-
-  // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
-  // time interval
-  if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
-    nsecs = 0;
-  }
-
-  // Per 4.2.1.2 Throw error if too many uuids are requested
-  if (nsecs >= 10000) {
-    throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
-  }
-
-  _lastMSecs = msecs;
-  _lastNSecs = nsecs;
-  _clockseq = clockseq;
-
-  // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
-  msecs += 12219292800000;
-
-  // `time_low`
-  var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
-  b[i++] = tl >>> 24 & 0xff;
-  b[i++] = tl >>> 16 & 0xff;
-  b[i++] = tl >>> 8 & 0xff;
-  b[i++] = tl & 0xff;
-
-  // `time_mid`
-  var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
-  b[i++] = tmh >>> 8 & 0xff;
-  b[i++] = tmh & 0xff;
-
-  // `time_high_and_version`
-  b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
-  b[i++] = tmh >>> 16 & 0xff;
-
-  // `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 : unparse(b);
-}
-
-// **`v4()` - Generate random UUID**
-
-// See https://github.com/broofa/node-uuid for API details
-function v4(options, buf, offset) {
-  // Deprecated - 'format' argument, as supported in v1.2
-  var i = buf && offset || 0;
-
-  if (typeof(options) == 'string') {
-    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;
-  rnds[8] = (rnds[8] & 0x3f) | 0x80;
-
-  // Copy bytes to buffer, if provided
-  if (buf) {
-    for (var ii = 0; ii < 16; ii++) {
-      buf[i + ii] = rnds[ii];
-    }
-  }
-
-  return buf || unparse(rnds);
-}
-
-// Export public API
-var uuid = v4;
-uuid.v1 = v1;
-uuid.v4 = v4;
-uuid.parse = parse;
-uuid.unparse = unparse;
-
-module.exports = uuid;
-
-},{"./rng":8}],10:[function(require,module,exports){
+},{"buffertools":3}],5:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
-var _api = require("./api");
-
-var _api2 = _interopRequireDefault(_api);
-
 var _collection = require("./collection");
 
 var _collection2 = _interopRequireDefault(_collection);
 
 var _base = require("./adapters/base");
 
 var _base2 = _interopRequireDefault(_base);
 
@@ -1841,18 +640,18 @@ class KintoBase {
       bucket: DEFAULT_BUCKET_NAME,
       remote: DEFAULT_REMOTE
     };
     this._options = Object.assign(defaults, options);
     if (!this._options.adapter) {
       throw new Error("No adapter provided");
     }
 
-    const { remote, events, headers, requestMode } = this._options;
-    this._api = new _api2.default(remote, events, { headers, requestMode });
+    const { remote, events, headers, requestMode, ApiClass } = this._options;
+    this._api = new ApiClass(remote, { events, headers, requestMode });
 
     // public properties
     /**
      * The event emitter instance.
      * @type {EventEmitter}
      */
     this.events = this._options.events;
   }
@@ -1872,23 +671,24 @@ class KintoBase {
     }
 
     const bucket = this._options.bucket;
     return new _collection2.default(bucket, collName, this._api, {
       events: this._options.events,
       adapter: this._options.adapter,
       dbPrefix: this._options.dbPrefix,
       idSchema: options.idSchema,
-      remoteTransformers: options.remoteTransformers
+      remoteTransformers: options.remoteTransformers,
+      hooks: options.hooks
     });
   }
 }
 exports.default = KintoBase;
 
-},{"./adapters/base":11,"./api":12,"./collection":13}],11:[function(require,module,exports){
+},{"./adapters/base":6,"./collection":7}],6:[function(require,module,exports){
 "use strict";
 
 /**
  * Base db adapter.
  *
  * @abstract
  */
 
@@ -1948,19 +748,20 @@ class BaseAdapter {
   get(id) {
     throw new Error("Not Implemented.");
   }
 
   /**
    * Lists all records from the database.
    *
    * @abstract
+   * @param  {Object} params  The filters and order to apply to the results.
    * @return {Promise}
    */
-  list() {
+  list(params = { filters: {}, order: "" }) {
     throw new Error("Not Implemented.");
   }
 
   /**
    * Store the lastModified value.
    *
    * @abstract
    * @param  {Number}  lastModified
@@ -1987,39 +788,41 @@ class BaseAdapter {
    * @return {Promise}
    */
   loadDump(records) {
     throw new Error("Not Implemented.");
   }
 }
 exports.default = BaseAdapter;
 
-},{}],12:[function(require,module,exports){
+},{}],7:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.SUPPORTED_PROTOCOL_VERSION = undefined;
+exports.SyncResultObject = undefined;
 exports.cleanRecord = cleanRecord;
 
-var _utils = require("./utils.js");
+var _base = require("./adapters/base");
+
+var _base2 = _interopRequireDefault(_base);
+
+var _utils = require("./utils");
 
-var _http = require("./http.js");
+var _uuid = require("uuid");
 
-var _http2 = _interopRequireDefault(_http);
+var _deeper = require("deeper");
+
+var _deeper2 = _interopRequireDefault(_deeper);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const RECORD_FIELDS_TO_CLEAN = ["_status", "last_modified"];
-/**
- * Currently supported protocol version.
- * @type {String}
- */
-const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
+const AVAILABLE_HOOKS = ["incoming-changes"];
 
 /**
  * Cleans a record object, excluding passed keys.
  *
  * @param  {Object} record        The record object.
  * @param  {Array}  excludeFields The list of keys to exclude.
  * @return {Object}               A clean copy of source record object.
  */
@@ -2028,341 +831,16 @@ function cleanRecord(record, excludeFiel
     if (excludeFields.indexOf(key) === -1) {
       acc[key] = record[key];
     }
     return acc;
   }, {});
 }
 
 /**
- * High level HTTP client for the Kinto API.
- */
-class Api {
-  /**
-   * Constructor.
-   *
-   * Options:
-   * - {Object} headers      The key-value headers to pass to each request.
-   * - {String} requestMode  The HTTP request mode.
-   *
-   * @param  {String}       remote  The remote URL.
-   * @param  {EventEmitter} events  The events handler
-   * @param  {Object}       options The options object.
-   */
-  constructor(remote, events, options = {}) {
-    if (typeof remote !== "string" || !remote.length) {
-      throw new Error("Invalid remote URL: " + remote);
-    }
-    if (remote[remote.length - 1] === "/") {
-      remote = remote.slice(0, -1);
-    }
-    this._backoffReleaseTime = null;
-    this.remote = remote;
-
-    // public properties
-    /**
-     * The optional generic headers.
-     * @type {Object}
-     */
-    this.optionHeaders = options.headers || {};
-    /**
-     * Current server settings, retrieved from the server.
-     * @type {Object}
-     */
-    this.serverSettings = null;
-    /**
-     * The even emitter instance.
-     * @type {EventEmitter}
-     */
-    if (!events) {
-      throw new Error("No events handler provided");
-    }
-    this.events = events;
-
-    /**
-     * The HTTP instance.
-     * @type {HTTP}
-     */
-    this.http = new _http2.default(this.events, { requestMode: options.requestMode });
-    this._registerHTTPEvents();
-  }
-
-  /**
-   * The remote endpoint base URL. Setting the value will also extract and
-   * validate the version.
-   * @type {String}
-   */
-  get remote() {
-    return this._remote;
-  }
-
-  set remote(url) {
-    let version;
-    try {
-      version = url.match(/\/(v\d+)\/?$/)[1];
-    } catch (err) {
-      throw new Error("The remote URL must contain the version: " + url);
-    }
-    if (version !== SUPPORTED_PROTOCOL_VERSION) {
-      throw new Error(`Unsupported protocol version: ${ version }`);
-    }
-    this._remote = url;
-    this._version = version;
-  }
-
-  /**
-   * The current server protocol version, eg. `v1`.
-   * @type {String}
-   */
-  get version() {
-    return this._version;
-  }
-
-  /**
-   * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is
-   * ongoing.
-   *
-   * @return {Number}
-   */
-  get backoff() {
-    const currentTime = new Date().getTime();
-    if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) {
-      return this._backoffReleaseTime - currentTime;
-    }
-    return 0;
-  }
-
-  /**
-   * Registers HTTP events.
-   */
-  _registerHTTPEvents() {
-    this.events.on("backoff", backoffMs => {
-      this._backoffReleaseTime = backoffMs;
-    });
-  }
-
-  /**
-   * Retrieves available server enpoints.
-   *
-   * Options:
-   * - {Boolean} fullUrl: Retrieve a fully qualified URL (default: true).
-   *
-   * @param  {Object} options Options object.
-   * @return {String}
-   */
-  endpoints(options = { fullUrl: true }) {
-    const root = options.fullUrl ? this.remote : `/${ this.version }`;
-    const urls = {
-      root: () => `${ root }/`,
-      batch: () => `${ root }/batch`,
-      bucket: bucket => `${ root }/buckets/${ bucket }`,
-      collection: (bucket, coll) => `${ urls.bucket(bucket) }/collections/${ coll }`,
-      records: (bucket, coll) => `${ urls.collection(bucket, coll) }/records`,
-      record: (bucket, coll, id) => `${ urls.records(bucket, coll) }/${ id }`
-    };
-    return urls;
-  }
-
-  /**
-   * Retrieves Kinto server settings.
-   *
-   * @return {Promise}
-   */
-  fetchServerSettings() {
-    if (this.serverSettings) {
-      return Promise.resolve(this.serverSettings);
-    }
-    return this.http.request(this.endpoints().root()).then(res => {
-      this.serverSettings = res.json.settings;
-      return this.serverSettings;
-    });
-  }
-
-  /**
-   * Fetches latest changes from the remote server.
-   *
-   * @param  {String} bucketName  The bucket name.
-   * @param  {String} collName    The collection name.
-   * @param  {Object} options     The options object.
-   * @return {Promise}
-   */
-  fetchChangesSince(bucketName, collName, options = { lastModified: null, headers: {} }) {
-    const recordsUrl = this.endpoints().records(bucketName, collName);
-    let queryString = "";
-    const headers = Object.assign({}, this.optionHeaders, options.headers);
-
-    if (options.lastModified) {
-      queryString = "?_since=" + options.lastModified;
-      headers["If-None-Match"] = (0, _utils.quote)(options.lastModified);
-    }
-
-    return this.fetchServerSettings().then(_ => this.http.request(recordsUrl + queryString, { headers })).then(res => {
-      // If HTTP 304, nothing has changed
-      if (res.status === 304) {
-        return {
-          lastModified: options.lastModified,
-          changes: []
-        };
-      }
-      // XXX: ETag are supposed to be opaque and stored «as-is».
-      // Extract response data
-      let etag = res.headers.get("ETag"); // e.g. '"42"'
-      etag = etag ? parseInt((0, _utils.unquote)(etag), 10) : options.lastModified;
-      const records = res.json.data;
-
-      // Check if server was flushed
-      const localSynced = options.lastModified;
-      const serverChanged = etag > options.lastModified;
-      const emptyCollection = records ? records.length === 0 : true;
-      if (localSynced && serverChanged && emptyCollection) {
-        throw Error("Server has been flushed.");
-      }
-
-      return { lastModified: etag, changes: records };
-    });
-  }
-
-  /**
-   * Builds an individual record batch request body.
-   *
-   * @param  {Object}  record The record object.
-   * @param  {String}  path   The record endpoint URL.
-   * @param  {Boolean} safe   Safe update?
-   * @return {Object}         The request body object.
-   */
-  _buildRecordBatchRequest(record, path, safe) {
-    const isDeletion = record._status === "deleted";
-    const method = isDeletion ? "DELETE" : "PUT";
-    const body = isDeletion ? undefined : { data: cleanRecord(record) };
-    const headers = {};
-    if (safe) {
-      if (record.last_modified) {
-        // Safe replace.
-        headers["If-Match"] = (0, _utils.quote)(record.last_modified);
-      } else if (!isDeletion) {
-        // Safe creation.
-        headers["If-None-Match"] = "*";
-      }
-    }
-    return { method, headers, path, body };
-  }
-
-  /**
-   * Process a batch request response.
-   *
-   * @param  {Object}  results          The results object.
-   * @param  {Array}   records          The initial records list.
-   * @param  {Object}  response         The response HTTP object.
-   * @return {Promise}
-   */
-  _processBatchResponses(results, records, response) {
-    // Handle individual batch subrequests responses
-    response.json.responses.forEach((response, index) => {
-      // TODO: handle 409 when unicity rule is violated (ex. POST with
-      // existing id, unique field, etc.)
-      if (response.status && response.status >= 200 && response.status < 400) {
-        results.published.push(response.body.data);
-      } else if (response.status === 404) {
-        results.skipped.push(records[index]);
-      } else if (response.status === 412) {
-        results.conflicts.push({
-          type: "outgoing",
-          local: records[index],
-          remote: response.body.details && response.body.details.existing || null
-        });
-      } else {
-        results.errors.push({
-          path: response.path,
-          sent: records[index],
-          error: response.body
-        });
-      }
-    });
-    return results;
-  }
-
-  /**
-   * Sends batch update requests to the remote server.
-   *
-   * Options:
-   * - {Object}  headers  Headers to attach to main and all subrequests.
-   * - {Boolean} safe     Safe update (default: `true`)
-   *
-   * @param  {String} bucketName  The bucket name.
-   * @param  {String} collName    The collection name.
-   * @param  {Array}  records     The list of record updates to send.
-   * @param  {Object} options     The options object.
-   * @return {Promise}
-   */
-  batch(bucketName, collName, records, options = { headers: {} }) {
-    const safe = options.safe || true;
-    const headers = Object.assign({}, this.optionHeaders, options.headers);
-    const results = {
-      errors: [],
-      published: [],
-      conflicts: [],
-      skipped: []
-    };
-    if (!records.length) {
-      return Promise.resolve(results);
-    }
-    return this.fetchServerSettings().then(serverSettings => {
-      // Kinto 1.6.1 possibly exposes multiple setting prefixes
-      const maxRequests = serverSettings["batch_max_requests"] || serverSettings["cliquet.batch_max_requests"];
-      if (maxRequests && records.length > maxRequests) {
-        return Promise.all((0, _utils.partition)(records, maxRequests).map(chunk => {
-          return this.batch(bucketName, collName, chunk, options);
-        })).then(batchResults => {
-          // Assemble responses of chunked batch results into one single
-          // result object
-          return batchResults.reduce((acc, batchResult) => {
-            Object.keys(batchResult).forEach(key => {
-              acc[key] = results[key].concat(batchResult[key]);
-            });
-            return acc;
-          }, results);
-        });
-      }
-      return this.http.request(this.endpoints().batch(), {
-        method: "POST",
-        headers: headers,
-        body: JSON.stringify({
-          defaults: { headers },
-          requests: records.map(record => {
-            const path = this.endpoints({ full: false }).record(bucketName, collName, record.id);
-            return this._buildRecordBatchRequest(record, path, safe);
-          })
-        })
-      }).then(res => this._processBatchResponses(results, records, res));
-    });
-  }
-}
-exports.default = Api;
-
-},{"./http.js":15,"./utils.js":16}],13:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
-  value: true
-});
-exports.SyncResultObject = undefined;
-
-var _base = require("./adapters/base");
-
-var _base2 = _interopRequireDefault(_base);
-
-var _utils = require("./utils");
-
-var _api = require("./api");
-
-var _uuid = require("uuid");
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
  * Synchronization result object.
  */
 class SyncResultObject {
   /**
    * Object default values.
    * @type {Object}
    */
   static get defaults() {
@@ -2461,29 +939,29 @@ function importChange(transaction, remot
     // avoid recreation.
     if (remote.deleted) {
       return { type: "skipped", data: remote };
     }
     const synced = markSynced(remote);
     transaction.create(synced);
     return { type: "created", data: synced };
   }
-  const identical = (0, _utils.deepEquals)((0, _api.cleanRecord)(local), (0, _api.cleanRecord)(remote));
+  const identical = (0, _deeper2.default)(cleanRecord(local), cleanRecord(remote));
   if (local._status !== "synced") {
     // Locally deleted, unsynced: scheduled for remote deletion.
     if (local._status === "deleted") {
       return { type: "skipped", data: local };
     }
     if (identical) {
       // If records are identical, import anyway, so we bump the
       // local last_modified value from the server and set record
       // status to "synced".
       const synced = markSynced(remote);
       transaction.update(synced);
-      return { type: "updated", data: synced };
+      return { type: "updated", data: synced, previous: local };
     }
     return {
       type: "conflicts",
       data: { type: "incoming", local: local, remote: remote }
     };
   }
   if (remote.deleted) {
     transaction.delete(remote.id);
@@ -2530,34 +1008,40 @@ class Collection {
     // public properties
     /**
      * The db adapter instance
      * @type {BaseAdapter}
      */
     this.db = db;
     /**
      * The Api instance.
-     * @type {Api}
+     * @type {KintoClient}
      */
     this.api = api;
+    this._apiCollection = this.api.bucket(this.bucket).collection(this.name);
     /**
      * 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 collection name.
    * @type {String}
    */
   get name() {
     return this._name;
@@ -2637,16 +1121,62 @@ class Collection {
       } else if (typeof transformer.decode !== "function") {
         throw new Error("A transformer must provide a decode function.");
       }
       return transformer;
     });
   }
 
   /**
+   * Validate the passed hook is correct.
+   *
+   * @param {Array|undefined} hook.
+   * @return {Array}
+   **/
+  _validateHook(hook) {
+    if (!Array.isArray(hook)) {
+      throw new Error("A hook definition should be an array of functions.");
+    }
+    return hook.map(fn => {
+      if (typeof fn !== "function") {
+        throw new Error("A hook definition should be an array of functions.");
+      }
+      return fn;
+    });
+  }
+
+  /**
+   * Validates a list of hooks.
+   *
+   * @param  {Object|undefined} hooks
+   * @return {Object}
+   */
+  _validateHooks(hooks) {
+    if (typeof hooks === "undefined") {
+      return {};
+    }
+    if (Array.isArray(hooks)) {
+      throw new Error("hooks should be an object, not an array.");
+    }
+    if (typeof hooks !== "object") {
+      throw new Error("hooks should be an object.");
+    }
+
+    const validatedHooks = {};
+
+    for (let hook in hooks) {
+      if (AVAILABLE_HOOKS.indexOf(hook) === -1) {
+        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}
    */
   clear() {
     return this.db.clear().then(_ => this.db.saveLastModified(null)).then(_ => ({ data: [], permissions: {} }));
   }
@@ -2749,22 +1279,17 @@ class Collection {
     if (!record.id) {
       return Promise.reject(new Error("Cannot update a record missing id."));
     }
     if (!this.idSchema.validate(record.id)) {
       return Promise.reject(new Error(`Invalid Id: ${ record.id }`));
     }
     return this.get(record.id).then(res => {
       const existing = res.data;
-      let newStatus = "updated";
-      if (record._status === "deleted") {
-        newStatus = "deleted";
-      } else if (options.synced) {
-        newStatus = "synced";
-      }
+      const newStatus = options.synced ? "synced" : "updated";
       return this.db.execute(transaction => {
         const source = options.patch ? Object.assign({}, existing, record) : record;
         const updated = markStatus(source, newStatus);
         if (existing.last_modified && !updated.last_modified) {
           updated.last_modified = existing.last_modified;
         }
         transaction.update(updated);
         return { data: updated, permissions: {} };
@@ -2822,34 +1347,34 @@ class Collection {
       });
     });
   }
 
   /**
    * Lists records from the local database.
    *
    * Params:
-   * - {Object} filters The filters to apply (default: `{}`).
+   * - {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}
    */
   list(params = {}, options = { includeDeleted: false }) {
     params = Object.assign({ order: "-last_modified", filters: {} }, params);
-    return this.db.list().then(results => {
-      let reduced = (0, _utils.reduceRecords)(params.filters, params.order, results);
+    return this.db.list(params).then(results => {
+      let data = results;
       if (!options.includeDeleted) {
-        reduced = reduced.filter(record => record._status !== "deleted");
+        data = results.filter(record => record._status !== "deleted");
       }
-      return { data: reduced, permissions: {} };
+      return { data, permissions: {} };
     });
   }
 
   /**
    * Import changes into the local database.
    *
    * @param  {SyncResultObject} syncResultObject The sync result object.
    * @param  {Object}           changeObject     The change object.
@@ -2857,61 +1382,42 @@ class Collection {
    */
   importChanges(syncResultObject, changeObject) {
     return Promise.all(changeObject.changes.map(change => {
       if (change.deleted) {
         return Promise.resolve(change);
       }
       return this._decodeRecord("remote", change);
     })).then(decodedChanges => {
-      // XXX: list() should filter only ids in changes.
-      return this.list({ order: "" }, { includeDeleted: true }).then(res => {
-        return { decodedChanges, existingRecords: res.data };
-      });
-    }).then(({ decodedChanges, existingRecords }) => {
-      return this.db.execute(transaction => {
-        return decodedChanges.map(remote => {
-          // Store remote change into local database.
-          return importChange(transaction, remote);
-        });
-      }, { preload: existingRecords });
-    }).catch(err => {
-      // XXX todo
-      err.type = "incoming";
-      // XXX one error of the whole transaction instead of one per atomic op
-      return [{ type: "errors", data: err }];
-    }).then(imports => {
-      var _iteratorNormalCompletion = true;
-      var _didIteratorError = false;
-      var _iteratorError = undefined;
-
-      try {
-        for (var _iterator = imports[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
-          const imported = _step.value;
-
+      // No change, nothing to import.
+      if (decodedChanges.length === 0) {
+        return Promise.resolve(syncResultObject);
+      }
+      // Retrieve records matching change ids.
+      const remoteIds = decodedChanges.map(change => change.id);
+      return this.list({ filters: { id: remoteIds }, order: "" }, { includeDeleted: true }).then(res => ({ decodedChanges, existingRecords: res.data })).then(({ decodedChanges, existingRecords }) => {
+        return this.db.execute(transaction => {
+          return decodedChanges.map(remote => {
+            // Store remote change into local database.
+            return importChange(transaction, remote);
+          });
+        }, { preload: existingRecords });
+      }).catch(err => {
+        // XXX todo
+        err.type = "incoming";
+        // XXX one error of the whole transaction instead of per atomic op
+        return [{ type: "errors", data: err }];
+      }).then(imports => {
+        for (let imported of imports) {
           if (imported.type !== "void") {
             syncResultObject.add(imported.type, imported.data);
           }
         }
-      } catch (err) {
-        _didIteratorError = true;
-        _iteratorError = err;
-      } finally {
-        try {
-          if (!_iteratorNormalCompletion && _iterator.return) {
-            _iterator.return();
-          }
-        } finally {
-          if (_didIteratorError) {
-            throw _iteratorError;
-          }
-        }
-      }
-
-      return syncResultObject;
+        return syncResultObject;
+      });
     }).then(syncResultObject => {
       syncResultObject.lastModified = changeObject.lastModified;
       // Don't persist lastModified value if any conflict or error occured
       if (!syncResultObject.ok) {
         return syncResultObject;
       }
       // No conflict occured, persist collection's lastModified value
       return this.db.saveLastModified(syncResultObject.lastModified).then(lastModified => {
@@ -2920,34 +1426,33 @@ class Collection {
       });
     });
   }
 
   /**
    * 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.
+   * 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.
    */
   resetSyncStatus() {
     let _count;
-    // XXX filter by status
-    return this.list({}, { includeDeleted: true }).then(result => {
+    return this.list({ filters: { _status: ["deleted", "synced"] }, order: "" }, { includeDeleted: true }).then(unsynced => {
       return this.db.execute(transaction => {
-        _count = result.data.length;
-        result.data.forEach(r => {
-          // Garbage collect deleted records.
-          if (r._status === "deleted") {
-            transaction.delete(r.id);
+        _count = unsynced.data.length;
+        unsynced.data.forEach(record => {
+          if (record._status === "deleted") {
+            // Garbage collect deleted records.
+            transaction.delete(record.id);
           } else {
             // Records that were synced become «created».
-            transaction.update(Object.assign({}, r, {
+            transaction.update(Object.assign({}, record, {
               last_modified: undefined,
               _status: "created"
             }));
           }
         });
       });
     }).then(() => this.db.saveLastModified(null)).then(() => _count);
   }
@@ -2957,30 +1462,20 @@ class Collection {
    *
    * - `toDelete`: unsynced deleted records we can safely delete;
    * - `toSync`: local updates to send to the server.
    *
    * @return {Object}
    */
   gatherLocalChanges() {
     let _toDelete;
-    // XXX filter by status
-    return this.list({}, { includeDeleted: true }).then(res => {
-      return res.data.reduce((acc, record) => {
-        if (record._status === "deleted" && !record.last_modified) {
-          acc.toDelete.push(record);
-        } else if (record._status !== "synced") {
-          acc.toSync.push(record);
-        }
-        return acc;
-        // rename toSync to toPush or toPublish
-      }, { toDelete: [], toSync: [] });
-    }).then(({ toDelete, toSync }) => {
-      _toDelete = toDelete;
-      return Promise.all(toSync.map(this._encodeRecord.bind(this, "remote")));
+    return Promise.all([this.list({ filters: { _status: ["created", "updated"] }, order: "" }), this.list({ filters: { _status: "deleted" }, order: "" }, { includeDeleted: true })]).then(([unsynced, deleted]) => {
+      _toDelete = deleted.data;
+      // Encode unsynced records.
+      return Promise.all(unsynced.data.map(this._encodeRecord.bind(this, "remote")));
     }).then(toSync => ({ toDelete: _toDelete, toSync }));
   }
 
   /**
    * 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.
    *
@@ -2996,26 +1491,53 @@ class Collection {
       return Promise.resolve(syncResultObject);
     }
     options = Object.assign({
       strategy: Collection.strategy.MANUAL,
       lastModified: this.lastModified,
       headers: {}
     }, options);
     // First fetch remote changes from the server
-    return this.api.fetchChangesSince(this.bucket, this.name, {
-      lastModified: options.lastModified,
+    return this._apiCollection.listRecords({
+      since: options.lastModified || undefined,
       headers: options.headers
+    }).then(({ data, last_modified }) => {
+      // last_modified is the ETag header value (string).
+      // For retro-compatibility with first kinto.js versions
+      // parse it to integer.
+      const unquoted = last_modified ? parseInt(last_modified.replace(/"/g, ""), 10) : undefined;
+
+      // Check if server was flushed.
+      // This is relevant for the Kinto demo server
+      // (and thus for many new comers).
+      const localSynced = options.lastModified;
+      const serverChanged = unquoted > options.lastModified;
+      const emptyCollection = data.length === 0;
+      if (localSynced && serverChanged && emptyCollection) {
+        throw Error("Server has been flushed.");
+      }
+
+      const payload = { lastModified: unquoted, changes: data };
+      return this.applyHook("incoming-changes", payload);
     })
     // Reflect these changes locally
     .then(changes => this.importChanges(syncResultObject, changes))
     // Handle conflicts, if any
     .then(result => this._handleConflicts(result, options.strategy));
   }
 
+  applyHook(hookName, payload) {
+    if (typeof this.hooks[hookName] == "undefined") {
+      return Promise.resolve(payload);
+    }
+    return (0, _utils.waterfall)(this.hooks[hookName].map(hook => {
+      return record => hook(payload, this);
+    }), payload);
+  }
+
   /**
    * Publish local changes to the remote server and updates the passed
    * {@link SyncResultObject} with publication results.
    *
    * @param  {SyncResultObject} syncResultObject The sync result object.
    * @param  {Object}           options          The options object.
    * @return {Promise}
    */
@@ -3023,40 +1545,62 @@ class Collection {
     if (!syncResultObject.ok) {
       return Promise.resolve(syncResultObject);
     }
     const safe = options.strategy === Collection.SERVER_WINS;
     options = Object.assign({ safe }, options);
 
     // Fetch local changes
     return this.gatherLocalChanges().then(({ toDelete, toSync }) => {
-      return Promise.all([
-      // Delete never synced records marked for deletion
-      this.db.execute(transaction => {
-        toDelete.forEach(record => {
-          transaction.delete(record.id);
+      // Send batch update requests
+      return this._apiCollection.batch(batch => {
+        toDelete.forEach(r => {
+          // never published locally deleted records should not be pusblished
+          if (r.last_modified) {
+            batch.deleteRecord(r);
+          }
         });
-      }),
-      // Send batch update requests
-      this.api.batch(this.bucket, this.name, toSync, options)]);
+        toSync.forEach(r => {
+          const isCreated = r._status === "created";
+          // Do not store status on server.
+          // XXX: cleanRecord() removes last_modified, required by safe.
+          delete r._status;
+          if (isCreated) {
+            batch.createRecord(r);
+          } else {
+            batch.updateRecord(r);
+          }
+        });
+      }, { headers: options.headers, safe: true, aggregate: true });
     })
     // Update published local records
-    .then(([deleted, synced]) => {
-      const { errors, conflicts, published, skipped } = synced;
+    .then(synced => {
       // Merge outgoing errors into sync result object
-      syncResultObject.add("errors", errors.map(error => {
+      syncResultObject.add("errors", synced.errors.map(error => {
         error.type = "outgoing";
         return error;
       }));
+
+      // The result of a batch returns data and permissions.
+      // XXX: permissions are ignored currently.
+      const conflicts = synced.conflicts.map(c => {
+        return { type: c.type, local: c.local.data, remote: c.remote };
+      });
+      const published = synced.published.map(c => c.data);
+      const skipped = synced.skipped.map(c => c.data);
+
       // Merge outgoing conflicts into sync result object
       syncResultObject.add("conflicts", conflicts);
       // Reflect publication results locally
       const missingRemotely = skipped.map(r => Object.assign({}, r, { deleted: true }));
       const toApplyLocally = published.concat(missingRemotely);
       // Deleted records are distributed accross local and missing records
+      // XXX: When tackling the issue to avoid downloading our own changes
+      // from the server. `toDeleteLocally` should be obtained from local db.
+      // See https://github.com/Kinto/kinto.js/issues/144
       const toDeleteLocally = toApplyLocally.filter(r => r.deleted);
       const toUpdateLocally = toApplyLocally.filter(r => !r.deleted);
       // First, apply the decode transformers, if any
       return Promise.all(toUpdateLocally.map(record => {
         return this._decodeRecord("remote", record);
       }))
       // Process everything within a single transaction
       .then(results => {
@@ -3161,17 +1705,17 @@ class Collection {
   }) {
     const previousRemote = this.api.remote;
     if (options.remote) {
       // Note: setting the remote ensures it's valid, throws when invalid.
       this.api.remote = options.remote;
     }
     if (!options.ignoreBackoff && this.api.backoff > 0) {
       const seconds = Math.ceil(this.api.backoff / 1000);
-      return Promise.reject(new Error(`Server is backed off; retry in ${ seconds }s or use the ignoreBackoff option.`));
+      return Promise.reject(new Error(`Server is asking clients to back off; retry in ${ seconds }s or use the ignoreBackoff option.`));
     }
     const result = new SyncResultObject();
     const syncPromise = this.db.getLastModified().then(lastModified => this._lastModified = lastModified).then(_ => this.pullChanges(result, options)).then(result => this.pushChanges(result, options)).then(result => {
       // Avoid performing a last pull if nothing has been published.
       if (result.published.length === 0) {
         return result;
       }
       return this.pullChanges(result, options);
@@ -3190,52 +1734,31 @@ class Collection {
    * @return {Promise} with the effectively imported records.
    */
   loadDump(records) {
     const reject = msg => Promise.reject(new Error(msg));
     if (!Array.isArray(records)) {
       return reject("Records is not an array.");
     }
 
-    var _iteratorNormalCompletion2 = true;
-    var _didIteratorError2 = false;
-    var _iteratorError2 = undefined;
-
-    try {
-      for (var _iterator2 = records[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
-        const record = _step2.value;
-
-        if (!record.id || !this.idSchema.validate(record.id)) {
-          return reject("Record has invalid ID: " + JSON.stringify(record));
-        }
-
-        if (!record.last_modified) {
-          return reject("Record has no last_modified value: " + JSON.stringify(record));
-        }
+    for (let record of records) {
+      if (!record.id || !this.idSchema.validate(record.id)) {
+        return reject("Record has invalid ID: " + JSON.stringify(record));
       }
 
-      // Fetch all existing records from local database,
-      // and skip those who are newer or not marked as synced.
-
-      // XXX filter by status / ids in records
-    } catch (err) {
-      _didIteratorError2 = true;
-      _iteratorError2 = err;
-    } finally {
-      try {
-        if (!_iteratorNormalCompletion2 && _iterator2.return) {
-          _iterator2.return();
-        }
-      } finally {
-        if (_didIteratorError2) {
-          throw _iteratorError2;
-        }
+      if (!record.last_modified) {
+        return reject("Record has no last_modified value: " + JSON.stringify(record));
       }
     }
 
+    // Fetch all existing records from local database,
+    // and skip those who are newer or not marked as synced.
+
+    // XXX filter by status / ids in records
+
     return this.list({}, { includeDeleted: true }).then(res => {
       return res.data.reduce((acc, record) => {
         acc[record.id] = record;
         return acc;
       }, {});
     }).then(existingById => {
       return records.filter(record => {
         const localRecord = existingById[record.id];
@@ -3250,285 +1773,29 @@ class Collection {
         record.last_modified > localRecord.last_modified;
         return shouldKeep;
       });
     }).then(newRecords => newRecords.map(markSynced)).then(newRecords => this.db.loadDump(newRecords));
   }
 }
 exports.default = Collection;
 
-},{"./adapters/base":11,"./api":12,"./utils":16,"uuid":9}],14:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
-  value: true
-});
-/**
- * Kinto server error code descriptors.
- * @type {Object}
- */
-exports.default = {
-  104: "Missing Authorization Token",
-  105: "Invalid Authorization Token",
-  106: "Request body was not valid JSON",
-  107: "Invalid request parameter",
-  108: "Missing request parameter",
-  109: "Invalid posted data",
-  110: "Invalid Token / id",
-  111: "Missing Token / id",
-  112: "Content-Length header was not provided",
-  113: "Request body too large",
-  114: "Resource was modified meanwhile",
-  115: "Method not allowed on this end point",
-  116: "Requested version not available on this server",
-  117: "Client has sent too many requests",
-  121: "Resource access is forbidden for this user",
-  122: "Another resource violates constraint",
-  201: "Service Temporary unavailable due to high load",
-  202: "Service deprecated",
-  999: "Internal Server Error"
-};
-
-},{}],15:[function(require,module,exports){
+},{"./adapters/base":6,"./utils":8,"deeper":4,"uuid":3}],8:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-
-var _errors = require("./errors.js");
-
-var _errors2 = _interopRequireDefault(_errors);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-/**
- * Enhanced HTTP client for the Kinto protocol.
- */
-class HTTP {
-  /**
-   * Default HTTP request headers applied to each outgoing request.
-   *
-   * @type {Object}
-   */
-  static get DEFAULT_REQUEST_HEADERS() {
-    return {
-      "Accept": "application/json",
-      "Content-Type": "application/json"
-    };
-  }
-
-  /**
-   * Default options.
-   *
-   * @type {Object}
-   */
-  static get defaultOptions() {
-    return { timeout: 5000, requestMode: "cors" };
-  }
-
-  /**
-   * Constructor.
-   *
-   * Options:
-   * - {Number} timeout      The request timeout in ms (default: `5000`).
-   * - {String} requestMode  The HTTP request mode (default: `"cors"`).
-   *
-   * @param {EventEmitter} events  The event handler.
-   * @param {Object}       options The options object.
-   */
-  constructor(events, options = {}) {
-    // public properties
-    /**
-     * The event emitter instance.
-     * @type {EventEmitter}
-     */
-    if (!events) {
-      throw new Error("No events handler provided");
-    }
-    this.events = events;
-
-    options = Object.assign({}, HTTP.defaultOptions, options);
-
-    /**
-     * The request mode.
-     * @see  https://fetch.spec.whatwg.org/#requestmode
-     * @type {String}
-     */
-    this.requestMode = options.requestMode;
-
-    /**
-     * The request timeout.
-     * @type {Number}
-     */
-    this.timeout = options.timeout;
-  }
-
-  /**
-   * Performs an HTTP request to the Kinto server.
-   *
-   * Options:
-   * - `{Object} headers` The request headers object (default: {})
-   *
-   * Resolves with an objet containing the following HTTP response properties:
-   * - `{Number}  status`  The HTTP status code.
-   * - `{Object}  json`    The JSON response body.
-   * - `{Headers} headers` The response headers object; see the ES6 fetch() spec.
-   *
-   * @param  {String} url     The URL.
-   * @param  {Object} options The fetch() options object.
-   * @return {Promise}
-   */
-  request(url, options = { headers: {} }) {
-    let response, status, statusText, headers, _timeoutId, hasTimedout;
-    // Ensure default request headers are always set
-    options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
-    options.mode = this.requestMode;
-    return new Promise((resolve, reject) => {
-      _timeoutId = setTimeout(() => {
-        hasTimedout = true;
-        reject(new Error("Request timeout."));
-      }, this.timeout);
-      fetch(url, options).then(res => {
-        if (!hasTimedout) {
-          clearTimeout(_timeoutId);
-          resolve(res);
-        }
-      }).catch(err => {
-        if (!hasTimedout) {
-          clearTimeout(_timeoutId);
-          reject(err);
-        }
-      });
-    }).then(res => {
-      response = res;
-      headers = res.headers;
-      status = res.status;
-      statusText = res.statusText;
-      this._checkForDeprecationHeader(headers);
-      this._checkForBackoffHeader(status, headers);
-      return res.text();
-    })
-    // Check if we have a body; if so parse it as JSON.
-    .then(text => {
-      if (text.length === 0) {
-        return null;
-      }
-      // Note: we can't consume the response body twice.
-      return JSON.parse(text);
-    }).catch(err => {
-      const error = new Error(`HTTP ${ status || 0 }; ${ err }`);
-      error.response = response;
-      error.stack = err.stack;
-      throw error;
-    }).then(json => {
-      if (json && status >= 400) {
-        let message = `HTTP ${ status }; `;
-        if (json.errno && json.errno in _errors2.default) {
-          message += _errors2.default[json.errno];
-          if (json.message) {
-            message += `: ${ json.message }`;
-          }
-        } else {
-          message += statusText || "";
-        }
-        const error = new Error(message.trim());
-        error.response = response;
-        error.data = json;
-        throw error;
-      }
-      return { status, json, headers };
-    });
-  }
-
-  _checkForDeprecationHeader(headers) {
-    const alertHeader = headers.get("Alert");
-    if (!alertHeader) {
-      return;
-    }
-    let alert;
-    try {
-      alert = JSON.parse(alertHeader);
-    } catch (err) {
-      console.warn("Unable to parse Alert header message", alertHeader);
-      return;
-    }
-    console.warn(alert.message, alert.url);
-    this.events.emit("deprecated", alert);
-  }
-
-  _checkForBackoffHeader(status, headers) {
-    let backoffMs;
-    const backoffSeconds = parseInt(headers.get("Backoff"), 10);
-    if (backoffSeconds > 0) {
-      backoffMs = new Date().getTime() + backoffSeconds * 1000;
-    } else {
-      backoffMs = 0;
-    }
-    this.events.emit("backoff", backoffMs);
-  }
-}
-exports.default = HTTP;
-
-},{"./errors.js":14}],16:[function(require,module,exports){
-"use strict";
-
-Object.defineProperty(exports, "__esModule", {
-  value: true
-});
-exports.deepEquals = deepEquals;
-exports.quote = quote;
-exports.unquote = unquote;
 exports.sortObjects = sortObjects;
 exports.filterObjects = filterObjects;
 exports.reduceRecords = reduceRecords;
-exports.partition = partition;
 exports.isUUID = isUUID;
 exports.waterfall = waterfall;
 exports.pFinally = pFinally;
-
-var _assert = require("assert");
-
-const RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-
-/**
- * Deeply checks if two structures are equals.
- *
- * @param  {Any} a
- * @param  {Any} b
- * @return {Boolean}
- */
-function deepEquals(a, b) {
-  try {
-    (0, _assert.deepEqual)(a, b);
-  } catch (err) {
-    return false;
-  }
-  return true;
-}
-
-/**
- * Returns the specified string with double quotes.
- *
- * @param  {String} str  A string to quote.
- * @return {String}
- */
-function quote(str) {
-  return `"${ str }"`;
-}
-
-/**
- * Trim double quotes from specified string.
- *
- * @param  {String} str  A string to unquote.
- * @return {String}
- */
-function unquote(str) {
-  return str.replace(/^"/, "").replace(/"$/, "");
-}
+const RE_UUID = exports.RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 
 /**
  * Checks if a value is undefined.
  * @param  {Any}  value
  * @return {Boolean}
  */
 function _isUndefined(value) {
   return typeof value === "undefined";
@@ -3558,23 +1825,27 @@ function sortObjects(order, list) {
     return a[field] > b[field] ? direction : -direction;
   });
 }
 
 /**
  * Filters records in a list matching all given filters.
  *
  * @param  {String} filters  The filters object.
- * @param  {Array}  list     The collection to order.
+ * @param  {Array}  list     The collection to filter.
  * @return {Array}
  */
 function filterObjects(filters, list) {
   return list.filter(entry => {
     return Object.keys(filters).every(filter => {
-      return entry[filter] === filters[filter];
+      const value = filters[filter];
+      if (Array.isArray(value)) {
+        return value.some(candidate => candidate === entry[filter]);
+      }
+      return entry[filter] === value;
     });
   });
 }
 
 /**
  * Filter and sort list against provided filters and order.
  *
  * @param  {Object} filters  The filters to apply.
@@ -3583,37 +1854,16 @@ function filterObjects(filters, list) {
  * @return {Array}
  */
 function reduceRecords(filters, order, list) {
   const filtered = filters ? filterObjects(filters, list) : list;
   return order ? sortObjects(order, filtered) : filtered;
 }
 
 /**
- * Chunks an array into n pieces.
- *
- * @param  {Array}  array
- * @param  {Number} n
- * @return {Array}
- */
-function partition(array, n) {
-  if (n <= 0) {
-    return array;
-  }
-  return array.reduce((acc, x, i) => {
-    if (i === 0 || i % n === 0) {
-      acc.push([x]);
-    } else {
-      acc[acc.length - 1].push(x);
-    }
-    return acc;
-  }, []);
-}
-
-/**
  * Checks if a string is an UUID.
  *
  * @param  {String} uuid The uuid to validate.
  * @return {Boolean}
  */
 function isUUID(uuid) {
   return RE_UUID.test(uuid);
 }
@@ -3644,10 +1894,10 @@ function waterfall(fns, init) {
  * @return {Promise}
  */
 function pFinally(promise, fn) {
   return promise.then(value => Promise.resolve(fn()).then(() => value), reason => Promise.resolve(fn()).then(() => {
     throw reason;
   }));
 }
 
-},{"assert":3}]},{},[2])(2)
+},{}]},{},[2])(2)
 });
\ No newline at end of file
--- a/services/common/moz.build
+++ b/services/common/moz.build
@@ -10,20 +10,21 @@ with Files('**'):
 TEST_DIRS += ['tests']
 
 EXTRA_COMPONENTS += [
     'servicesComponents.manifest',
 ]
 
 EXTRA_JS_MODULES['services-common'] += [
     'async.js',
+    'kinto-http-client.js',
+    'kinto-offline-client.js',
     'kinto-updater.js',
     'KintoCertificateBlocklist.js',
     'logmanager.js',
-    'moz-kinto-client.js',
     'observers.js',
     'rest.js',
     'stringbundle.js',
     'utils.js',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
     EXTRA_JS_MODULES['services-common'] += [
--- a/services/common/tests/unit/test_kinto.js
+++ b/services/common/tests/unit/test_kinto.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-common/moz-kinto-client.js")
+Cu.import("resource://services-common/kinto-offline-client.js");
 Cu.import("resource://testing-common/httpd.js");
 
 const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 var server;
 
 // set up what we need to make storage adapters
@@ -354,28 +354,28 @@ function getSampleResponse(req, port) {
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress"
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": JSON.stringify({"settings":{"cliquet.batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
     },
-    "GET:/v1/buckets/default/collections/test_collection/records?": {
+    "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified": {
       "sampleHeaders": [
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress",
         "Etag: \"1445606341071\""
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": JSON.stringify({"data":[{"last_modified":1445606341071, "done":false, "id":"68db8313-686e-4fff-835e-07d78ad6f2af", "title":"New test"}]})
     },
-    "GET:/v1/buckets/default/collections/test_collection/records?_since=1445606341071": {
+    "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071": {
       "sampleHeaders": [
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress",
         "Etag: \"1445607941223\""
       ],
       "status": {status: 200, statusText: "OK"},
--- a/services/common/tests/unit/test_kintoCertBlocklist.js
+++ b/services/common/tests/unit/test_kintoCertBlocklist.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const { Constructor: CC } = Components;
 
 Cu.import("resource://services-common/KintoCertificateBlocklist.js");
-Cu.import("resource://services-common/moz-kinto-client.js")
+Cu.import("resource://services-common/kinto-offline-client.js");
 Cu.import("resource://testing-common/httpd.js");
 
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 var server;
 
 // set up what we need to make storage adapters
@@ -140,33 +140,33 @@ function getSampleResponse(req, port) {
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress"
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": JSON.stringify({"settings":{"cliquet.batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
     },
-    "GET:/v1/buckets/blocklists/collections/certificates/records?": {
+    "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified": {
       "sampleHeaders": [
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress",
         "Etag: \"3000\""
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": JSON.stringify({"data":[{
         "issuerName": "MEQxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xHjAcBgNVBAMTFXRoYXd0ZSBFViBTU0wgQ0EgLSBHMw==",
         "serialNumber":"CrTHPEE6AZSfI3jysin2bA==",
         "id":"78cf8900-fdea-4ce5-f8fb-b78710617718",
         "last_modified":3000
       }]})
     },
-    "GET:/v1/buckets/blocklists/collections/certificates/records?_since=3000": {
+    "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=3000": {
       "sampleHeaders": [
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress",
         "Etag: \"4000\""
       ],
       "status": {status: 200, statusText: "OK"},
--- a/services/common/tests/unit/test_storage_adapter.js
+++ b/services/common/tests/unit/test_storage_adapter.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-common/moz-kinto-client.js");
+Cu.import("resource://services-common/kinto-offline-client.js");
 
 // set up what we need to make storage adapters
 const Kinto = loadKinto();
 const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
 const kintoFilename = "kinto.sqlite";
 
 let gFirefoxAdapter = null;
 
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -1,13 +1,15 @@
 "use strict";
 
 var {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
 
 // WeakMap[Extension -> BackgroundPage]
 var backgroundPagesMap = new WeakMap();
 
 // Responsible for the background_page section of the manifest.
 function BackgroundPage(options, extension) {
   this.extension = extension;
   this.scripts = options.scripts || [];
@@ -60,16 +62,21 @@ BackgroundPage.prototype = {
     this.webNav = webNav;
 
     webNav.loadURI(url, 0, null, null, null);
 
     let window = webNav.document.defaultView;
     this.contentWindow = window;
     this.context.contentWindow = window;
 
+    if (this.extension.addonData.instanceID) {
+      AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                  .then(addon => addon.setDebugGlobal(window));
+    }
+
     // TODO: Right now we run onStartup after the background page
     // finishes. See if this is what Chrome does.
     let loadListener = event => {
       if (event.target != window.document) {
         return;
       }
       event.currentTarget.removeEventListener("load", loadListener, true);
 
@@ -94,16 +101,21 @@ BackgroundPage.prototype = {
     // Navigate away from the background page to invalidate any
     // setTimeouts or other callbacks.
     this.webNav.loadURI("about:blank", 0, null, null, null);
     this.webNav = null;
 
     this.chromeWebNav.loadURI("about:blank", 0, null, null, null);
     this.chromeWebNav.close();
     this.chromeWebNav = null;
+
+    if (this.extension.addonData.instanceID) {
+      AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                  .then(addon => addon.setDebugGlobal(null));
+    }
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_background", (type, directive, extension, manifest) => {
   let bgPage = new BackgroundPage(manifest.background, extension);
   bgPage.build();
   backgroundPagesMap.set(extension, bgPage);
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 support-files =
   file_download.html
   file_download.txt
   interruptible.sjs
   file_sample.html
 
+[test_chrome_ext_background_debug_global.html]
+skip-if = (os == 'android') # android doesn't have devtools
 [test_chrome_ext_downloads_download.html]
 [test_chrome_ext_downloads_misc.html]
 [test_chrome_ext_downloads_search.html]
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>WebExtension test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTe