Merge mozilla-central to autoland. a=merge on a CLOSED TREE
authorRazvan Maries <rmaries@mozilla.com>
Fri, 30 Nov 2018 05:13:14 +0200
changeset 508124 095c92e4b6b569c2af6e24990a36fac1c4d2e214
parent 508123 d8064028f0e66760a66105089a0bc1258877b6bf (current diff)
parent 508064 e18555e129e755fd2fa06e2760f05d4ebafe4187 (diff)
child 508125 1b5ec7fefbbf8959db1bb438b6491d925074b066
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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 mozilla-central to autoland. a=merge on a CLOSED TREE
browser/base/content/test/webextensions/browser_legacy.xpi
devtools/client/inspector/flexbox/test/browser.ini
dom/base/test/mochitest.ini
editor/libeditor/tests/mochitest.ini
gfx/layers/wr/WebRenderBridgeParent.cpp
mobile/android/base/java/org/mozilla/gecko/FennecKiller.java
modules/libpref/init/all.js
toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.json
toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.json
toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.json
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrapped_chrome_manifest.js
toolkit/mozapps/extensions/test/xpcshell/test_invalid_install_rdf.js
toolkit/mozapps/extensions/test/xpcshell/test_legacy.js
toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
toolkit/mozapps/extensions/test/xpcshell/test_manifest_locales.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -185,16 +185,17 @@ dom/events/**
 dom/fetch/**
 dom/file/**
 dom/filesystem/**
 dom/flex/**
 dom/grid/**
 dom/html/**
 dom/ipc/**
 dom/jsurl/**
+dom/localstorage/**
 dom/manifest/**
 dom/media/test/**
 dom/media/tests/**
 dom/media/webaudio/**
 dom/media/webspeech/**
 dom/messagechannel/**
 dom/midi/**
 dom/network/**
--- a/browser/base/content/test/sanitize/browser_cookiePermission.js
+++ b/browser/base/content/test/sanitize/browser_cookiePermission.js
@@ -272,17 +272,17 @@ add_task(async function deleteStorageInA
 
   ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
 
   // Clean up.
   await Sanitizer.sanitize([ "cookies", "offlineApps" ]);
 
   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("about:newtab");
   await new Promise(aResolve => {
-    let req = Services.qms.clearStoragesForPrincipal(principal, null, false);
+    let req = Services.qms.clearStoragesForPrincipal(principal);
     req.callback = () => { aResolve(); };
   });
 });
 
 add_task(async function deleteStorageOnlyCustomPermissionInAboutURL() {
   info("Test about:newtab + permissions");
 
   // Let's clean up all the data.
@@ -308,14 +308,14 @@ add_task(async function deleteStorageOnl
 
   ok(await checkDataForAboutURL(), "about:newtab data is not deleted.");
 
   // Clean up.
   await Sanitizer.sanitize([ "cookies", "offlineApps" ]);
 
   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("about:newtab");
   await new Promise(aResolve => {
-    let req = Services.qms.clearStoragesForPrincipal(principal, null, false);
+    let req = Services.qms.clearStoragesForPrincipal(principal);
     req.callback = () => { aResolve(); };
   });
 
   Services.perms.remove(uri, "cookie");
 });
--- a/browser/base/content/test/webextensions/browser.ini
+++ b/browser/base/content/test/webextensions/browser.ini
@@ -1,13 +1,12 @@
 [DEFAULT]
 support-files =
   head.js
   file_install_extensions.html
-  browser_legacy.xpi
   browser_legacy_webext.xpi
   browser_webext_permissions.xpi
   browser_webext_nopermissions.xpi
   browser_webext_unsigned.xpi
   browser_webext_update1.xpi
   browser_webext_update2.xpi
   browser_webext_update_icon1.xpi
   browser_webext_update_icon2.xpi
--- a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
+++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js
@@ -1,12 +1,11 @@
 const {AddonManagerPrivate} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm", {});
 
 const ID_PERMS = "update_perms@tests.mozilla.org";
-const ID_LEGACY = "legacy_update@tests.mozilla.org";
 const ID_ORIGINS = "update_origins@tests.mozilla.org";
 
 function getBadgeStatus() {
   let menuButton = document.getElementById("PanelUI-menu-button");
   return menuButton.getAttribute("badge-status");
 }
 
 // Set some prefs that apply to all the tests in this file
@@ -75,17 +74,12 @@ async function testNoPrompt(origUrl, id)
   await SpecialPowers.popPrefEnv();
 }
 
 // Test that an update that adds new non-promptable permissions is just
 // applied without showing a notification dialog.
 add_task(() => testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`,
                             ID_PERMS));
 
-// Test that an update from a legacy extension to a webextension
-// doesn't show a prompt even when the webextension uses
-// promptable required permissions.
-add_task(() => testNoPrompt(`${BASE}/browser_legacy.xpi`, ID_LEGACY));
-
 // Test that an update that narrows origin permissions is just applied without
 // showing a notification promt
 add_task(() => testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`,
                             ID_ORIGINS));
deleted file mode 100644
index da4b3062796c180155819f83f6514ad6738827ab..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
+++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
@@ -46,21 +46,16 @@ async function testUpdateNoPrompt(filena
 
   ok(!sawPopup, "Should not have seen a permission notification");
   PopupNotifications.panel.removeEventListener("popupshown", popupListener);
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
   await addon.uninstall();
 }
 
-// Test that we don't see a prompt when updating from a legacy
-// extension to a webextension.
-add_task(() => testUpdateNoPrompt("browser_legacy.xpi",
-                                  "legacy_update@tests.mozilla.org", "1.1"));
-
 // Test that we don't see a prompt when no new promptable permissions
 // are added.
 add_task(() => testUpdateNoPrompt("browser_webext_update_perms1.xpi",
                                   "update_perms@tests.mozilla.org"));
 
 // Test that an update that narrows origin permissions is just applied without
 // showing a notification promt
 add_task(() => testUpdateNoPrompt("browser_webext_update_origins1.xpi",
--- a/browser/components/aboutconfig/content/aboutconfig.css
+++ b/browser/components/aboutconfig/content/aboutconfig.css
@@ -9,16 +9,17 @@
   box-sizing: border-box;
   width: calc(100% - 20px);
   min-width: 644px;
   background-image: url("chrome://global/skin/icons/search-textbox.svg");
   background-repeat: no-repeat;
   background-position: 9px center;
   background-size: 12px 12px;
   padding-left: 30px;
+  z-index: 1;
 }
 
 #prefs {
   background-color: var(--in-content-box-background);
   color: var(--in-content-text-color);
   margin: 10px;
   min-width: 644px;
   /* To stay consistent with about:preferences (664px - 20px margin). */
@@ -34,21 +35,33 @@
 #prefs > tr:hover {
   background-color: var(--in-content-item-hover);
 }
 
 #prefs > tr.has-user-value {
   font-weight: bold;
 }
 
+#prefs > tr.locked {
+  opacity: 0.4;
+  background-image: url("chrome://browser/skin/preferences/in-content/privacy-security.svg");
+  background-repeat: no-repeat;
+  background-position: 9px center;
+  background-size: 16px 16px;
+}
+
 #prefs > tr > td {
   padding: 4px;
   width: 50%;
 }
 
+#prefs > tr > td.cell-name {
+  padding-inline-start: 30px;
+}
+
 .cell-value {
   word-break: break-all;
 }
 
 td.cell-value > form > input[type="text"] {
   width: 100%;
   box-sizing: border-box;
 }
--- a/browser/components/aboutconfig/content/aboutconfig.js
+++ b/browser/components/aboutconfig/content/aboutconfig.js
@@ -14,24 +14,26 @@ function getPrefName(prefRow) {
   return prefRow.getAttribute("aria-label");
 }
 
 function onLoad() {
   gPrefArray = Services.prefs.getChildList("").map(function(name) {
     let hasUserValue = Services.prefs.prefHasUserValue(name);
     let pref = {
       name,
-      value: Preferences.get(name),
       hasUserValue,
       hasDefaultValue: hasUserValue ? prefHasDefaultValue(name) : true,
+      isLocked: Services.prefs.prefIsLocked(name),
     };
-    // Try in case it's a localized string.
-    // Throws an exception if there is no equivalent value in the localized files for the pref.
+    // Try in case it's a localized string or locked user added pref
     // If an execption is thrown the pref value is set to the empty string.
     try {
+      // Throws an exception in case locked user added pref without default value
+      pref.value = Preferences.get(name);
+      // Throws an exception if there is no equivalent value in the localized files for the pref.
       if (!pref.hasUserValue && /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.value)) {
         pref.value = Services.prefs.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
       }
     } catch (ex) {
       pref.value = "";
     }
     return pref;
   });
@@ -116,30 +118,34 @@ function filterPrefs() {
 
 function createPrefsFragment(prefArray) {
   let fragment = document.createDocumentFragment();
   for (let pref of prefArray) {
     let row = document.createElement("tr");
     if (pref.hasUserValue) {
       row.classList.add("has-user-value");
     }
+    if (pref.isLocked) {
+      row.classList.add("locked");
+    }
     row.setAttribute("aria-label", pref.name);
 
     row.appendChild(getPrefRow(pref));
     fragment.appendChild(row);
   }
   return fragment;
 }
 
 function createNewPrefFragment(name) {
   let fragment = document.createDocumentFragment();
   let row = document.createElement("tr");
   row.classList.add("has-user-value");
   row.setAttribute("aria-label", name);
   let nameCell = document.createElement("td");
+  nameCell.className = "cell-name";
   nameCell.append(name);
   row.appendChild(nameCell);
 
   let valueCell = document.createElement("td");
   valueCell.classList.add("cell-value");
   let guideText = document.createElement("span");
   document.l10n.setAttributes(guideText, "about-config-pref-add");
   valueCell.appendChild(guideText);
@@ -159,16 +165,17 @@ function createNewPrefFragment(name) {
 
   fragment.appendChild(row);
   return fragment;
 }
 
 function getPrefRow(pref) {
   let rowFragment = document.createDocumentFragment();
   let nameCell = document.createElement("td");
+  nameCell.className = "cell-name";
   // Add <wbr> behind dots to prevent line breaking in random mid-word places.
   let parts = pref.name.split(".");
   for (let i = 0; i < parts.length - 1; i++) {
     nameCell.append(parts[i] + ".", document.createElement("wbr"));
   }
   nameCell.append(parts[parts.length - 1]);
   rowFragment.appendChild(nameCell);
 
@@ -182,30 +189,34 @@ function getPrefRow(pref) {
   let button = document.createElement("button");
   if (Services.prefs.getPrefType(pref.name) == Services.prefs.PREF_BOOL) {
     document.l10n.setAttributes(button, "about-config-pref-toggle");
     button.className = "button-toggle";
   } else {
     document.l10n.setAttributes(button, "about-config-pref-edit");
     button.className = "button-edit";
   }
+  if (pref.isLocked) {
+    button.disabled = true;
+  }
   editCell.appendChild(button);
   rowFragment.appendChild(editCell);
 
   let buttonCell = document.createElement("td");
-  if (pref.hasUserValue) {
+  if (!pref.isLocked && pref.hasUserValue) {
     let resetButton = document.createElement("button");
     if (!pref.hasDefaultValue) {
       document.l10n.setAttributes(resetButton, "about-config-pref-delete");
     } else {
       document.l10n.setAttributes(resetButton, "about-config-pref-reset");
       resetButton.className = "button-reset";
     }
     buttonCell.appendChild(resetButton);
   }
+
   rowFragment.appendChild(buttonCell);
   return rowFragment;
 }
 
 function startEditingPref(row, arrayEntry) {
   if (gPrefRowInEdit != undefined) {
     // Abort editing-process first.
     gPrefRowInEdit.textContent = "";
--- a/browser/components/aboutconfig/test/browser/browser.ini
+++ b/browser/components/aboutconfig/test/browser/browser.ini
@@ -1,7 +1,9 @@
 [DEFAULT]
 
 [browser_basic.js]
 [browser_edit.js]
 skip-if = debug # Bug 1507747
 [browser_search.js]
 skip-if = debug # Bug 1507747
+[browser_locked.js]
+
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutconfig/test/browser/browser_locked.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PAGE_URL = "chrome://browser/content/aboutconfig/aboutconfig.html";
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["test.aboutconfig.a", "some value"],
+    ],
+  });
+});
+
+add_task(async function test_locked() {
+  registerCleanupFunction(() => {
+    Services.prefs.unlockPref("browser.search.searchEnginesURL");
+    Services.prefs.unlockPref("test.aboutconfig.a");
+    Services.prefs.unlockPref("accessibility.AOM.enabled");
+  });
+
+  Services.prefs.lockPref("browser.search.searchEnginesURL");
+  Services.prefs.lockPref("test.aboutconfig.a");
+  Services.prefs.lockPref("accessibility.AOM.enabled");
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: PAGE_URL,
+  }, async browser => {
+    await ContentTask.spawn(browser, null, () => {
+      let list = [...content.document.getElementById("prefs")
+        .getElementsByTagName("tr")];
+      function getRow(name) {
+        return list.find(row => row.querySelector("td").textContent == name);
+      }
+      function getValue(name) {
+          return getRow(name).querySelector("td.cell-value").textContent;
+      }
+      function getButton(name) {
+        return getRow(name).querySelector("button");
+      }
+
+      // Test locked default string pref.
+      let lockedPref = getRow("browser.search.searchEnginesURL");
+      Assert.ok(lockedPref.classList.contains("locked"));
+      Assert.equal(getValue("browser.search.searchEnginesURL"), "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/");
+      Assert.ok(getButton("browser.search.searchEnginesURL").classList.contains("button-edit"));
+      Assert.equal(getButton("browser.search.searchEnginesURL").disabled, true);
+
+      // Test locked default boolean pref.
+      lockedPref = getRow("accessibility.AOM.enabled");
+      Assert.ok(lockedPref.classList.contains("locked"));
+      Assert.equal(getValue("accessibility.AOM.enabled"), "false");
+      Assert.ok(getButton("accessibility.AOM.enabled").classList.contains("button-toggle"));
+      Assert.equal(getButton("accessibility.AOM.enabled").disabled, true);
+
+      // Test locked user added pref.
+      lockedPref = getRow("test.aboutconfig.a");
+      Assert.ok(lockedPref.classList.contains("locked"));
+      Assert.equal(getValue("test.aboutconfig.a"), "");
+      Assert.ok(getButton("test.aboutconfig.a").classList.contains("button-edit"));
+      Assert.equal(getButton("test.aboutconfig.a").disabled, true);
+
+      // Test pref not locked
+      let unlockedPref = getRow("accessibility.indicator.enabled");
+      Assert.equal(unlockedPref.classList.contains("locked"), false);
+      Assert.equal(getValue("accessibility.indicator.enabled"), "false");
+      Assert.ok(getButton("accessibility.indicator.enabled").classList.contains("button-toggle"));
+      Assert.equal(getButton("accessibility.indicator.enabled").disabled, false);
+    });
+  });
+});
--- a/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js
+++ b/browser/components/contextualidentity/test/browser/browser_forgetAPI_quota_clearStoragesForPrincipal.js
@@ -87,16 +87,20 @@ async function checkIndexedDB(browser) {
     });
 
     try {
       db.transaction(["obj"], "readonly");
       ok(false, "The indexedDB should not exist");
     } catch (e) {
       is(e.name, "NotFoundError", "The indexedDB does not exist as expected");
     }
+
+    db.close();
+
+    content.indexedDB.deleteDatabase("idb");
   });
 }
 
 //
 // Test functions.
 //
 
 add_task(async function setup() {
@@ -122,17 +126,22 @@ add_task(async function test_quota_clear
 
   // Using quota manager to clear all indexed DB for a given domain.
   let caUtils = {};
   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js",
                                       caUtils);
   let httpURI = caUtils.makeURI("http://" + TEST_HOST);
   let httpPrincipal = Services.scriptSecurityManager
                               .createCodebasePrincipal(httpURI, {});
-  Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true);
+  let clearRequest = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, null, true);
+  await new Promise(resolve => {
+    clearRequest.callback = () => {
+      resolve();
+    };
+  });
 
   for (let userContextId of Object.keys(USER_CONTEXTS)) {
     // Open our tab in the given user context.
     tabs[userContextId] = await openTabInUserContext(TEST_URL + "empty_file.html", userContextId);
 
     // Check whether indexed DB has been cleared.
     await checkIndexedDB(tabs[userContextId].browser);
 
--- a/browser/components/extensions/parent/ext-browsingData.js
+++ b/browser/components/extensions/parent/ext-browsingData.js
@@ -72,48 +72,92 @@ const clearFormData = options => {
 
 const clearHistory = options => {
   return Sanitizer.items.history.clear(makeRange(options));
 };
 
 const clearIndexedDB = async function(options) {
   let promises = [];
 
-  await new Promise(resolve => {
+  await new Promise((resolve, reject) => {
     quotaManagerService.getUsage(request => {
       if (request.resultCode != Cr.NS_OK) {
-        // We are probably shutting down. We don't want to propagate the error,
-        // rejecting the promise.
-        resolve();
+        reject({message: "Clear indexedDB failed"});
         return;
       }
 
       for (let item of request.result) {
         let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
-        let uri = principal.URI;
-        if (uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "file") {
-          promises.push(new Promise(r => {
-            let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false);
-            req.callback = () => { r(); };
+        let scheme = principal.URI.scheme;
+        if (scheme == "http" || scheme == "https" || scheme == "file") {
+          promises.push(new Promise((resolve, reject) => {
+            let clearRequest = quotaManagerService.clearStoragesForPrincipal(principal, null, "idb");
+            clearRequest.callback = () => {
+              if (clearRequest.resultCode == Cr.NS_OK) {
+                resolve();
+              } else {
+                reject({message: "Clear indexedDB failed"});
+              }
+            };
           }));
         }
       }
 
       resolve();
     });
   });
 
   return Promise.all(promises);
 };
 
 const clearLocalStorage = async function(options) {
   if (options.since) {
     return Promise.reject(
       {message: "Firefox does not support clearing localStorage with 'since'."});
   }
+
+  if (Services.lsm.nextGenLocalStorageEnabled) {
+    // Ideally we could reuse the logic in Sanitizer.jsm or nsIClearDataService,
+    // but this API exposes an ability to wipe data at a much finger granularity
+    // than those APIs.  So custom logic is used here to wipe only the QM
+    // localStorage client (when in use).
+
+    let promises = [];
+
+    await new Promise((resolve, reject) => {
+      quotaManagerService.getUsage(request => {
+        if (request.resultCode != Cr.NS_OK) {
+          reject({message: "Clear localStorage failed"});
+          return;
+        }
+
+        for (let item of request.result) {
+          let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+          let host = principal.URI.hostPort;
+          if (!options.hostnames || options.hostnames.includes(host)) {
+            promises.push(new Promise((resolve, reject) => {
+              let clearRequest = quotaManagerService.clearStoragesForPrincipal(principal, "default", "ls");
+              clearRequest.callback = () => {
+                if (clearRequest.resultCode == Cr.NS_OK) {
+                  resolve();
+                } else {
+                  reject({message: "Clear localStorage failed"});
+                }
+              };
+            }));
+          }
+        }
+
+        resolve();
+      });
+    });
+
+    return Promise.all(promises);
+  }
+
   if (options.hostnames) {
     for (let hostname of options.hostnames) {
       Services.obs.notifyObservers(null, "extension:purge-localStorage", hostname);
     }
   } else {
     Services.obs.notifyObservers(null, "extension:purge-localStorage");
   }
 };
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_indexedDB.js
@@ -48,16 +48,19 @@ add_task(async function testIndexedDB() 
   await extension.awaitMessage("indexedDBCreated");
   await extension.awaitMessage("indexedDBCreated");
 
   function getOrigins() {
     return new Promise(resolve => {
       let origins = [];
       Services.qms.getUsage(request => {
         for (let i = 0; i < request.result.length; ++i) {
+          if (request.result[i].usage === 0) {
+            continue;
+          }
           if (request.result[i].origin.startsWith("http://mochi.test") ||
               request.result[i].origin.startsWith("http://example.com")) {
             origins.push(request.result[i].origin);
           }
         }
         resolve(origins);
       });
     });
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -55,16 +55,19 @@ add_task(async function testLocalStorage
     await browser.browsingData.removeLocalStorage({});
     await sendMessageToTabs(tabs, "checkLocalStorageCleared");
 
     await sendMessageToTabs(tabs, "resetLocalStorage");
     await sendMessageToTabs(tabs, "checkLocalStorageSet");
     await browser.browsingData.remove({}, {localStorage: true});
     await sendMessageToTabs(tabs, "checkLocalStorageCleared");
 
+    // Cleanup (checkLocalStorageCleared creates empty LS databases).
+    await browser.browsingData.removeLocalStorage({});
+
     browser.tabs.remove(tabs.map(tab => tab.id));
 
     browser.test.notifyPass("done");
   }
 
   function contentScript() {
     browser.runtime.onMessage.addListener(msg => {
       if (msg === "resetLocalStorage") {
--- a/browser/components/preferences/in-content/tests/siteData/browser.ini
+++ b/browser/components/preferences/in-content/tests/siteData/browser.ini
@@ -4,12 +4,12 @@ support-files =
   site_data_test.html
   service_worker_test.html
   service_worker_test.js
   offline/offline.html
   offline/manifest.appcache
 
 [browser_clearSiteData.js]
 [browser_siteData.js]
-skip-if = (os == 'linux' && debug) # Bug 1439332
+skip-if = (os == 'linux' && debug) || verify # Bug 1439332 and bug 1436395
 [browser_siteData2.js]
 [browser_siteData3.js]
 [browser_siteData_multi_select.js]
--- a/browser/components/preferences/in-content/tests/siteData/browser_siteData.js
+++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData.js
@@ -39,17 +39,17 @@ add_task(async function() {
   let qoutaUsageSite = frameDoc.querySelector(`richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]`);
   ok(qoutaUsageSite, "Should list site using quota usage");
 
   // Always remember to clean up
   OfflineAppCacheHelper.clear();
   await new Promise(resolve => {
     let principal = Services.scriptSecurityManager
                             .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
-    let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+    let request = Services.qms.clearStoragesForPrincipal(principal, null, null, true);
     request.callback = resolve;
   });
 
   await SiteDataManager.removeAll();
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 }).skip(); // Bug 1414751
 
 // Test buttons are disabled and loading message shown while updating sites
--- a/browser/modules/SiteDataManager.jsm
+++ b/browser/modules/SiteDataManager.jsm
@@ -299,17 +299,17 @@ var SiteDataManager = {
         // below we have already removed across OAs so skip the same origin without suffix
         continue;
       }
       removals.add(originNoSuffix);
       promises.push(new Promise(resolve => {
         // We are clearing *All* across OAs so need to ensure a principal without suffix here,
         // or the call of `clearStoragesForPrincipal` would fail.
         principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(originNoSuffix);
-        let request = this._qms.clearStoragesForPrincipal(principal, null, true);
+        let request = this._qms.clearStoragesForPrincipal(principal, null, null, true);
         request.callback = resolve;
       }));
     }
     return Promise.all(promises);
   },
 
   _removeAppCache(site) {
     for (let cache of site.appCacheList) {
--- a/build.gradle
+++ b/build.gradle
@@ -53,132 +53,54 @@ buildscript {
     if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         ext.google_play_services_version = '15.0.1'
     }
 
     dependencies {
         classpath 'org.mozilla.apilint:apilint:0.1.4'
         classpath 'com.android.tools.build:gradle:3.1.4'
         classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
-        classpath 'org.apache.commons:commons-exec:1.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
 
-// A stream that processes bytes line by line, prepending a tag before sending
-// each line to Gradle's logging.
-class TaggedLogOutputStream extends org.apache.commons.exec.LogOutputStream {
-    String tag
-    Logger logger
-
-    TaggedLogOutputStream(tag, logger) {
-        this.tag = tag
-        this.logger = logger
-    }
-
-    void processLine(String line, int level) {
-        logger.lifecycle("${this.tag} ${line}")
-    }
-}
-
-ext.geckoBinariesOnlyIf = { task ->
-    // Never for official builds.
-    if (mozconfig.substs.MOZILLA_OFFICIAL) {
-        rootProject.logger.lifecycle("Skipping task ${task.path} because: MOZILLA_OFFICIAL")
-        return false
-    }
-
+if ('multi' == System.env.AB_CD) {
     // Multi-l10n builds set `AB_CD=multi`, which isn't a valid locale.  This
     // causes the
     //
-    // |mach build| > |mach gradle| > AndroidManifest.xml > strings.xml > multi/brand.dtd
+    // |mach build| > |mach gradle| > |make gradle-targets| > AndroidManifest.xml > strings.xml > multi/brand.dtd
     //
     // dependency chain to fail, since multi isn't a real locale.  To avoid
     // this, if Gradle is invoked with AB_CD=multi, we don't invoke Make at all.
-    if ('multi' == System.env.AB_CD) {
-        rootProject.logger.lifecycle("Skipping task ${task.path} because: AB_CD=multi")
-        return false
-    }
-
+    task generateCodeAndResources()
+} else if (System.env.IS_LANGUAGE_REPACK == '1') {
     // Single-locale l10n repacks set `IS_LANGUAGE_REPACK=1` and handle resource
     // and code generation themselves.
-    if ('1' == System.env.IS_LANGUAGE_REPACK) {
-        rootProject.logger.lifecycle("Skipping task ${task.path} because: IS_LANGUAGE_REPACK")
-        return false
-    }
-
-    rootProject.logger.lifecycle("Executing task ${task.path}")
-    return true
-}
-
-task machBuildFaster(type: Exec) {
-    onlyIf rootProject.ext.geckoBinariesOnlyIf
-
-    workingDir "${topsrcdir}"
-
-    commandLine mozconfig.substs.PYTHON
-    args "${topsrcdir}/mach"
-    args 'build'
-    args 'faster'
-
-    // Add `-v` if we're running under `--info` (or `--debug`).
-    if (project.logger.isEnabled(LogLevel.INFO)) {
-        args '-v'
-    }
-
-    // `path` is like `:machBuildFaster`.
-    standardOutput = new TaggedLogOutputStream("${path}>", logger)
-    errorOutput = standardOutput
-}
-
-def createMachStagePackageTask(name) {
-    return task(name, type: Exec) {
-        onlyIf rootProject.ext.geckoBinariesOnlyIf
-
-        dependsOn rootProject.machBuildFaster
-
-        // We'd prefer to take these from the :omnijar project directly, but
-        // it's awkward to reach across projects at evaluation time, so we
-        // duplicate the list here.
-        inputs.dir "${topsrcdir}/mobile/android/chrome"
-        inputs.dir "${topsrcdir}/mobile/android/components"
-        inputs.dir "${topsrcdir}/mobile/android/locales"
-        inputs.dir "${topsrcdir}/mobile/android/modules"
-        inputs.dir "${topsrcdir}/mobile/android/themes"
-        inputs.dir "${topsrcdir}/toolkit"
-
+    task generateCodeAndResources()
+} else {
+    task generateCodeAndResources(type:Exec) {
         workingDir "${topobjdir}"
 
-        // We'd prefer this to be a `mach` invocation, but `mach build
-        // mobile/android/installer/stage-package` doesn't work as expected.
         commandLine mozconfig.substs.GMAKE
         args '-C'
-        args "${topobjdir}/mobile/android/installer"
-        args 'stage-package'
+        args "${topobjdir}/mobile/android/base"
+        args 'gradle-targets'
 
-        outputs.file "${topobjdir}/dist/fennec/assets/${mozconfig.substs.ANDROID_CPU_ARCH}/libxul.so"
-        outputs.file "${topobjdir}/dist/fennec/lib/${mozconfig.substs.ANDROID_CPU_ARCH}/libmozglue.so"
-
-        // `path` is like `:machStagePackage`.
-        standardOutput = new TaggedLogOutputStream("${path}>", logger)
+        // Only show the output if something went wrong.
+        ignoreExitValue = true
+        standardOutput = new ByteArrayOutputStream()
         errorOutput = standardOutput
+        doLast {
+            if (execResult.exitValue != 0) {
+                throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+            }
+        }
     }
 }
 
-createMachStagePackageTask("machStagePackageForFennec").with {
-    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
-}
-
-createMachStagePackageTask("machStagePackageForGeckoview").with {
-    args 'MOZ_GECKOVIEW_JAR=1'
-    outputs.file "${topobjdir}/dist/geckoview/assets/omni.ja"
-    // Avoid races between stage-package invocations.
-    mustRunAfter tasks["machStagePackageForFennec"]
-}
-
 afterEvaluate {
     subprojects { project ->
         if (project.name != 'thirdparty') {
             tasks.withType(JavaCompile) {
                 // Add compiler args for all code except third-party code.
                 options.compilerArgs += [
                     // Turn on all warnings, except...
                     "-Xlint:all",
@@ -204,20 +126,20 @@ afterEvaluate {
                 }
             }
         }
 
         if (!hasProperty('android')) {
             return
         }
         android.applicationVariants.all {
-            preBuild.dependsOn rootProject.machBuildFaster
+            preBuild.dependsOn rootProject.generateCodeAndResources
         }
         android.libraryVariants.all {
-            preBuild.dependsOn rootProject.machBuildFaster
+            preBuild.dependsOn rootProject.generateCodeAndResources
         }
     }
 }
 
 apply plugin: 'idea'
 
 idea {
     project {
--- a/caps/ContentPrincipal.cpp
+++ b/caps/ContentPrincipal.cpp
@@ -391,18 +391,27 @@ ContentPrincipal::SetDomain(nsIURI* aDom
 
 static nsresult
 GetSpecialBaseDomain(const nsCOMPtr<nsIURI>& aCodebase,
                      bool* aHandled,
                      nsACString& aBaseDomain)
 {
   *aHandled = false;
 
-  // For a file URI, we return the file path.
+  // Special handling for a file URI.
   if (NS_URIIsLocalFile(aCodebase)) {
+    // If strict file origin policy is not in effect, all local files are
+    // considered to be same-origin, so return a known dummy domain here.
+    if (!nsScriptSecurityManager::GetStrictFileOriginPolicy()) {
+      *aHandled = true;
+      aBaseDomain.AssignLiteral("UNIVERSAL_FILE_URI_ORIGIN");
+      return NS_OK;
+    }
+
+    // Otherwise, we return the file path.
     nsCOMPtr<nsIURL> url = do_QueryInterface(aCodebase);
 
     if (url) {
       *aHandled = true;
       return url->GetFilePath(aBaseDomain);
     }
   }
 
--- a/config/check_macroassembler_style.py
+++ b/config/check_macroassembler_style.py
@@ -43,16 +43,18 @@ reMatchArg = re.compile(reBeforeArg + re
 
 def get_normalized_signatures(signature, fileAnnot=None):
     # Remove static
     signature = signature.replace('static', '')
     # Remove semicolon.
     signature = signature.replace(';', ' ')
     # Normalize spaces.
     signature = re.sub(r'\s+', ' ', signature).strip()
+    # Remove new-line induced spaces after opening braces.
+    signature = re.sub(r'\(\s+', '(', signature).strip()
     # Match arguments, and keep only the type.
     signature = reMatchArg.sub('\g<type>', signature)
     # Remove class name
     signature = signature.replace('MacroAssembler::', '')
 
     # Extract list of architectures
     archs = ['generic']
     if fileAnnot:
@@ -139,55 +141,72 @@ def get_file_annotation(filename):
 
 def get_macroassembler_definitions(filename):
     try:
         fileAnnot = get_file_annotation(filename)
     except Exception:
         return []
 
     style_section = False
-    code_section = False
     lines = ''
     signatures = []
     with open(filename) as f:
         for line in f:
             if '//{{{ check_macroassembler_style' in line:
+                if style_section:
+                    raise 'check_macroassembler_style section already opened.'
                 style_section = True
+                braces_depth = 0
             elif '//}}} check_macroassembler_style' in line:
                 style_section = False
             if not style_section:
                 continue
 
+            # Remove comments from the processed line.
             line = re.sub(r'//.*', '', line)
-            if line.startswith('{') or line.strip() == "{}":
+
+            # Locate and count curly braces.
+            open_curly_brace = line.find('{')
+            was_braces_depth = braces_depth
+            braces_depth = braces_depth + line.count('{') - line.count('}')
+
+            # Raise an error if the check_macroassembler_style macro is used
+            # across namespaces / classes scopes.
+            if braces_depth < 0:
+                raise 'check_macroassembler_style annotations are not well scoped.'
+
+            # If the current line contains an opening curly brace, check if
+            # this line combines with the previous one can be identified as a
+            # MacroAssembler function signature.
+            if open_curly_brace != -1 and was_braces_depth == 0:
+                lines = lines + line[:open_curly_brace]
                 if 'MacroAssembler::' in lines:
                     signatures.extend(
                         get_normalized_signatures(lines, fileAnnot))
-                if line.strip() != "{}":  # Empty declaration, no need to declare
-                    # a new code section
-                    code_section = True
-                continue
-            if line.startswith('}'):
-                code_section = False
                 lines = ''
                 continue
-            if code_section:
-                continue
 
-            if len(line.strip()) == 0:
+            # We do not aggregate any lines if we are scanning lines which are
+            # in-between a set of curly braces.
+            if braces_depth > 0:
+                continue
+            if was_braces_depth != 0:
+                line = line[line.rfind('}') + 1:]
+
+            # This logic is used to remove template instantiation, static
+            # variable definitions and function declaration from the next
+            # function definition.
+            last_semi_colon = line.rfind(';')
+            if last_semi_colon != -1:
                 lines = ''
-                continue
+                line = line[last_semi_colon + 1:]
+
+            # Aggregate lines of non-braced text, which corresponds to the space
+            # where we are expecting to find function definitions.
             lines = lines + line
-            # Continue until we have a complete declaration
-            if '{' not in lines:
-                continue
-            # Skip variable declarations
-            if ')' not in lines:
-                lines = ''
-                continue
 
     return signatures
 
 
 def get_macroassembler_declaration(filename):
     style_section = False
     lines = ''
     signatures = []
@@ -196,24 +215,27 @@ def get_macroassembler_declaration(filen
             if '//{{{ check_macroassembler_decl_style' in line:
                 style_section = True
             elif '//}}} check_macroassembler_decl_style' in line:
                 style_section = False
             if not style_section:
                 continue
 
             line = re.sub(r'//.*', '', line)
-            if len(line.strip()) == 0:
+            if len(line.strip()) == 0 or 'public:' in line or 'private:' in line:
                 lines = ''
                 continue
             lines = lines + line
+
             # Continue until we have a complete declaration
             if ';' not in lines:
                 continue
-            # Skip variable declarations
+
+            # Skip member declarations: which are lines ending with a
+            # semi-colon without any list of arguments.
             if ')' not in lines:
                 lines = ''
                 continue
 
             signatures.extend(get_normalized_signatures(lines))
             lines = ''
 
     return signatures
--- a/config/faster/rules.mk
+++ b/config/faster/rules.mk
@@ -61,23 +61,16 @@ endif
 # Extra define to trigger some workarounds. We should strive to limit the
 # use of those. As of writing the only one is in browser/locales/jar.mn.
 ACDEFINES += -DBUILD_FASTER
 
 # Files under the faster/ sub-directory, however, are not meant to use the
 # fallback
 $(TOPOBJDIR)/faster/%: ;
 
-ifeq ($(MOZ_BUILD_APP),mobile/android)
-# The generic rule doesn't handle relative directories, which are used
-# extensively in mobile/android/base.
-$(TOPOBJDIR)/mobile/android/base/% : $(TOPOBJDIR)/buildid.h FORCE
-	$(MAKE) -C $(TOPOBJDIR)/mobile/android/base $*
-endif
-
 # Generic rule to fall back to the recursive make backend.
 # This needs to stay after other $(TOPOBJDIR)/* rules because GNU Make
 # <3.82 apply pattern rules in definition order, not stem length like
 # modern GNU Make.
 $(TOPOBJDIR)/%: FORCE
 	$(MAKE) -C $(dir $@) $(notdir $@)
 
 # Install files using install manifests
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -18,32 +18,39 @@ support-files =
   service-workers/fetch-sw.html
   service-workers/fetch-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_addons_debug_bootstrapped.js]
+# To be removed in bug 1497264
+skip-if = true
 [browser_addons_debug_info.js]
 [browser_addons_debug_webextension.js]
 tags = webextensions
 [browser_addons_debug_webextension_inspector.js]
 tags = webextensions
 [browser_addons_debug_webextension_nobg.js]
 tags = webextensions
 [browser_addons_debug_webextension_popup.js]
 skip-if = (verify && debug) || (debug && os == "linux" && bits == 64) # verify: crashes on shutdown, timeouts linux debug Bug 1299001
 tags = webextensions
 [browser_addons_debugging_initial_state.js]
+# To be removed or updated in bug 1497264
+skip-if = true
 [browser_addons_install.js]
-skip-if = verify && debug
+# To be updated in bug 1497264 (was "verify && debug")
+skip-if = true
 [browser_addons_reload.js]
 [browser_addons_remove.js]
 [browser_addons_toggle_debug.js]
+# To be removed or updated in bug 1497264
+skip-if = true
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_fetch_flag.js]
 skip-if = os == 'mac' # bug 1333759
 [browser_service_workers_multi_content_process.js]
 skip-if = !e10s # This test is only valid in e10s
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
@@ -8,16 +8,18 @@ const SHOW_SYSTEM_ADDONS_PREF = "devtool
 function testFilePath(container, expectedFilePath) {
   // Verify that the path to the install location is shown next to its label.
   const filePath = container.querySelector(".file-path");
   ok(filePath, "file path is in DOM");
   ok(filePath.textContent.endsWith(expectedFilePath), "file path is set correctly");
   is(filePath.previousElementSibling.textContent, "Location", "file path has label");
 }
 
+// Remove in Bug 1497264
+/*
 add_task(async function testLegacyAddon() {
   const addonId = "test-devtools@mozilla.org";
   const addonName = "test-devtools";
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
   await installAddon({
     document,
@@ -27,16 +29,17 @@ add_task(async function testLegacyAddon(
 
   const container = document.querySelector(`[data-addon-id="${addonId}"]`);
   testFilePath(container, "browser/devtools/client/aboutdebugging/test/addons/unpacked/");
 
   await uninstallAddon({document, id: addonId, name: addonName});
 
   await closeAboutDebugging(tab);
 });
+*/
 
 add_task(async function testWebExtension() {
   const addonId = "test-devtools-webextension-nobg@mozilla.org";
   const addonName = "test-devtools-webextension-nobg";
   const { tab, document } = await openAboutDebugging("addons");
 
   await waitForInitialAddonList(document);
 
--- a/devtools/client/aboutdebugging/test/browser_addons_install.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -2,19 +2,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 loader.lazyImporter(this, "AddonTestUtils",
   "resource://testing-common/AddonTestUtils.jsm");
 
 AddonTestUtils.initMochitest(this);
 
-const ADDON_ID = "test-devtools@mozilla.org";
-const ADDON_NAME = "test-devtools";
-
 function mockFilePicker(window, file) {
   // Mock the file picker to select a test addon
   const MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(window);
   MockFilePicker.setFiles([file]);
 }
 
 /**
@@ -30,16 +27,19 @@ function promiseWriteWebManifestForExten
   const files = {
     "manifest.json": JSON.stringify(manifest),
   };
   return AddonTestUtils.promiseWriteFilesToExtension(
     dir.path, manifest.applications.gecko.id, files, true);
 }
 
 add_task(async function testLegacyInstallSuccess() {
+  const ADDON_ID = "test-devtools@mozilla.org";
+  const ADDON_NAME = "test-devtools";
+
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
   await installAddon({
     document,
     path: "addons/unpacked/install.rdf",
     name: ADDON_NAME,
--- a/devtools/client/aboutdebugging/test/browser_addons_reload.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -1,14 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const ADDON_ID = "test-devtools@mozilla.org";
-const ADDON_NAME = "test-devtools";
 
 const PACKAGED_ADDON_ID = "bug1273184@tests";
 const PACKAGED_ADDON_NAME = "bug 1273184";
 
 function getReloadButton(document, addonName) {
   const names = getInstalledAddonNames(document);
   const name = names.filter(element => element.textContent === addonName)[0];
   ok(name, `Found ${addonName} add-on in the list`);
@@ -55,17 +54,20 @@ class TempWebExt {
     fos.close();
   }
 
   remove() {
     return this.tmpDir.remove(true);
   }
 }
 
+// Remove in Bug 1497264
+/*
 add_task(async function reloadButtonReloadsAddon() {
+  const ADDON_NAME = "test-devtools";
   const { tab, document, window } = await openAboutDebugging("addons");
   const { AboutDebugging } = window;
   await waitForInitialAddonList(document);
   await installAddon({
     document,
     path: "addons/unpacked/install.rdf",
     name: ADDON_NAME,
   });
@@ -91,16 +93,17 @@ add_task(async function reloadButtonRelo
   const [reloadedAddon] = await onInstalled;
   is(reloadedAddon.name, ADDON_NAME,
      "Add-on was reloaded: " + reloadedAddon.name);
 
   await onBootstrapInstallCalled;
   await tearDownAddon(AboutDebugging, reloadedAddon);
   await closeAboutDebugging(tab);
 });
+*/
 
 add_task(async function reloadButtonRefreshesMetadata() {
   const { tab, document, window } = await openAboutDebugging("addons");
   const { AboutDebugging } = window;
   await waitForInitialAddonList(document);
 
   const manifestBase = {
     "manifest_version": 2,
--- a/devtools/client/aboutdebugging/test/browser_addons_remove.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_remove.js
@@ -7,16 +7,18 @@ const PACKAGED_ADDON_NAME = "bug 1273184
 function getTargetEl(document, id) {
   return document.querySelector(`[data-addon-id="${id}"]`);
 }
 
 function getRemoveButton(document, id) {
   return document.querySelector(`[data-addon-id="${id}"] .uninstall-button`);
 }
 
+// Remove in Bug 1497264
+/*
 add_task(async function removeLegacyExtension() {
   const addonID = "test-devtools@mozilla.org";
   const addonName = "test-devtools";
 
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
@@ -31,16 +33,17 @@ add_task(async function removeLegacyExte
   info("Click on the remove button and wait until the addon container is removed");
   getRemoveButton(document, addonID).click();
   await waitUntil(() => !getTargetEl(document, addonID), 100);
 
   info("add-on is not shown");
 
   await closeAboutDebugging(tab);
 });
+*/
 
 add_task(async function removeWebextension() {
   const addonID = "test-devtools-webextension@mozilla.org";
   const addonName = "test-devtools-webextension";
 
   const { tab, document } = await openAboutDebugging("addons");
   await waitForInitialAddonList(document);
 
--- a/devtools/client/inspector/flexbox/flexbox.js
+++ b/devtools/client/inspector/flexbox/flexbox.js
@@ -396,17 +396,17 @@ class FlexboxInspector {
    * Handler for a change in the input checkboxes in the FlexboxItem component.
    * Toggles on/off the flexbox highlighter for the provided flex container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the flex container element for which the flexbox
    *         highlighter is toggled on/off for.
    */
   onToggleFlexboxHighlighter(node) {
-    this.highlighters.toggleFlexboxHighlighter(node);
+    this.highlighters.toggleFlexboxHighlighter(node, "layout");
     this.store.dispatch(updateFlexboxHighlighted(node !==
       this.highlighters.flexboxHighlighterShow));
   }
 
   /**
    * Handler for "new-root" event fired by the inspector and "new-node-front" event fired
    * by the inspector selection. Updates the flexbox panel if it is visible.
    */
--- a/devtools/client/inspector/flexbox/test/browser.ini
+++ b/devtools/client/inspector/flexbox/test/browser.ini
@@ -21,16 +21,17 @@ support-files =
 [browser_flexbox_accordion_state.js]
 [browser_flexbox_container_and_item.js]
 [browser_flexbox_container_and_item_updates_on_change.js]
 [browser_flexbox_container_element_rep.js]
 [browser_flexbox_container_properties.js]
 [browser_flexbox_empty_state.js]
 [browser_flexbox_highlighter_color_picker_on_ESC.js]
 [browser_flexbox_highlighter_color_picker_on_RETURN.js]
+[browser_flexbox_highlighter_opened_telemetry.js]
 [browser_flexbox_item_list_01.js]
 [browser_flexbox_item_list_02.js]
 [browser_flexbox_item_list_updates_on_change.js]
 [browser_flexbox_item_outline_exists.js]
 [browser_flexbox_item_outline_has_correct_layout.js]
 [browser_flexbox_item_outline_hidden_when_useless.js]
 [browser_flexbox_item_outline_renders_basisfinal_points_correctly.js]
 [browser_flexbox_item_outline_rotates_for_column.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_highlighter_opened_telemetry.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry is correct when the flexbox highlighter is activated from
+// the layout view.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_specific_cases.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  startTelemetry();
+  const { inspector, flexboxInspector } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+  const { highlighters, store } = inspector;
+
+  const onFlexHighlighterToggleRendered = waitForDOM(doc, "#flexbox-checkbox-toggle");
+  await selectNode("#container", inspector);
+  const [flexHighlighterToggle] = await onFlexHighlighterToggleRendered;
+
+  await toggleHighlighterON(flexHighlighterToggle, highlighters, store);
+  await toggleHighlighterOFF(flexHighlighterToggle, highlighters, store);
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.layout.flexboxhighlighter.opened", "", 1, "scalar");
+  checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null,
+    "hasentries");
+}
--- a/devtools/client/inspector/flexbox/test/head.js
+++ b/devtools/client/inspector/flexbox/test/head.js
@@ -1,12 +1,13 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
 /* import-globals-from ../../test/head.js */
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
--- a/devtools/client/inspector/grids/test/browser.ini
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -34,12 +34,13 @@ skip-if = (verify && (os == 'win'))
 [browser_grids_grid-outline-highlight-cell.js]
 skip-if = (verify && (os == 'win'))
 [browser_grids_grid-outline-multiple-grids.js]
 [browser_grids_grid-outline-selected-grid.js]
 [browser_grids_grid-outline-updates-on-grid-change.js]
 [browser_grids_grid-outline-writing-mode.js]
 skip-if = (verify && (os == 'win'))
 [browser_grids_highlighter-setting-rules-grid-toggle.js]
+[browser_grids_highlighter-toggle-telemetry.js]
 [browser_grids_number-of-css-grids-telemetry.js]
 [browser_grids_persist-color-palette.js]
 [browser_grids_restored-after-reload.js]
 [browser_grids_restored-multiple-grids-after-reload.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_highlighter-toggle-telemetry.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry count is correct when the grid highlighter is activated from
+// the layout view.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  startTelemetry();
+  const { gridInspector, inspector } = await openLayoutView();
+  const { document: doc } = gridInspector;
+  const { highlighters, store } = inspector;
+
+  await selectNode("#grid", inspector);
+  const gridList = doc.getElementById("grid-list");
+  const checkbox = gridList.children[0].querySelector("input");
+
+  info("Toggling ON the CSS grid highlighter from the layout panel.");
+  const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  const onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 1 &&
+    state.grids[0].highlighted);
+  checkbox.click();
+  await onHighlighterShown;
+  await onCheckboxChange;
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.grid.gridinspector.opened", "", 1, "scalar");
+}
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -130,18 +130,20 @@ skip-if = true # Bug 1177550
 [browser_markup_events_react_development_15.4.1_jsx.js]
 [browser_markup_events_react_production_15.3.1.js]
 [browser_markup_events_react_production_15.3.1_jsx.js]
 [browser_markup_events_react_production_16.2.0.js]
 [browser_markup_events_react_production_16.2.0_jsx.js]
 [browser_markup_events_source_map.js]
 [browser_markup_events-windowed-host.js]
 [browser_markup_flex_display_badge.js]
+[browser_markup_flex_display_badge_telemetry.js]
 [browser_markup_grid_display_badge_01.js]
 [browser_markup_grid_display_badge_02.js]
+[browser_markup_grid_display_badge_telemetry.js]
 [browser_markup_links_01.js]
 [browser_markup_links_02.js]
 [browser_markup_links_03.js]
 [browser_markup_links_04.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_markup_links_05.js]
 [browser_markup_links_06.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_flex_display_badge_telemetry.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry is correct when the flexbox highlighter is activated from
+// the markup view.
+
+const TEST_URI = `
+  <style type="text/css">
+    #flex {
+      display: flex;
+    }
+  </style>
+  <div id="flex"></div>
+`;
+
+add_task(async function() {
+  await pushPref("devtools.inspector.flexboxHighlighter.enabled", true);
+  await pushPref("devtools.flexboxinspector.enabled", true);
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  startTelemetry();
+  const { inspector } = await openLayoutView();
+  const { highlighters } = inspector;
+
+  await selectNode("#flex", inspector);
+  const flexContainer = await getContainerForSelector("#flex", inspector);
+  const flexDisplayBadge = flexContainer.elt.querySelector(
+    ".inspector-badge.interactive[data-display]");
+
+  info("Toggling ON the flexbox highlighter from the flex display badge.");
+  const onHighlighterShown = highlighters.once("flexbox-highlighter-shown");
+  flexDisplayBadge.click();
+  await onHighlighterShown;
+
+  info("Toggling OFF the flexbox highlighter from the flex display badge.");
+  const onHighlighterHidden = highlighters.once("flexbox-highlighter-hidden");
+  flexDisplayBadge.click();
+  await onHighlighterHidden;
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.markup.flexboxhighlighter.opened", "", 1, "scalar");
+  checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null,
+    "hasentries");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_grid_display_badge_telemetry.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry count is correct when the grid highlighter is activated from
+// the markup view.
+
+const TEST_URI = `
+  <style type="text/css">
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid"></div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  startTelemetry();
+  const { inspector } = await openLayoutView();
+  const { highlighters, store } = inspector;
+
+  await selectNode("#grid", inspector);
+  const gridContainer = await getContainerForSelector("#grid", inspector);
+  const gridDisplayBadge = gridContainer.elt.querySelector(
+    ".inspector-badge.interactive[data-display]");
+
+  info("Toggling ON the CSS grid highlighter from the grid display badge.");
+  const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  const onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length === 1 &&
+    state.grids[0].highlighted);
+  gridDisplayBadge.click();
+  await onHighlighterShown;
+  await onCheckboxChange;
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.markup.gridinspector.opened", "", 1, "scalar");
+}
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -1,12 +1,13 @@
 /* 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/. */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
 /* import-globals-from ../../test/head.js */
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
--- a/devtools/client/inspector/markup/views/element-editor.js
+++ b/devtools/client/inspector/markup/views/element-editor.js
@@ -744,17 +744,17 @@ ElementEditor.prototype = {
     const target = event.target;
 
     if (Services.prefs.getBoolPref("devtools.inspector.flexboxHighlighter.enabled") &&
         (target.dataset.display === "flex" || target.dataset.display === "inline-flex")) {
       // Stop tracking highlighter events to avoid flickering of the active class.
       this.stopTrackingFlexboxHighlighterEvents();
 
       this._displayBadge.classList.toggle("active");
-      await this.highlighters.toggleFlexboxHighlighter(this.node);
+      await this.highlighters.toggleFlexboxHighlighter(this.node, "markup");
 
       this.startTrackingFlexboxHighlighterEvents();
     }
 
     if (target.dataset.display === "grid" || target.dataset.display === "inline-grid") {
       // Don't toggle the grid highlighter if the max number of new grid highlighters
       // allowed has been reached.
       if (!this.highlighters.canGridHighlighterToggle(this.node)) {
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -161,26 +161,28 @@ skip-if = os == "mac" # Bug 1245996 : cl
 [browser_rules_filtereditor-appears-on-swatch-click.js]
 [browser_rules_filtereditor-commit-on-ENTER.js]
 [browser_rules_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) # bug 963492: win.
 [browser_rules_flexbox-highlighter-on-mutation.js]
 [browser_rules_flexbox-highlighter-on-navigate.js]
 [browser_rules_flexbox-highlighter-on-reload.js]
 [browser_rules_flexbox-highlighter-restored-after-reload.js]
+[browser_rules_flexbox-toggle-telemetry.js]
 [browser_rules_flexbox-toggle_01.js]
 [browser_rules_flexbox-toggle_01b.js]
 [browser_rules_flexbox-toggle_02.js]
 [browser_rules_flexbox-toggle_03.js]
 [browser_rules_flexbox-toggle_04.js]
 [browser_rules_font-family-parsing.js]
 [browser_rules_grid-highlighter-on-mutation.js]
 [browser_rules_grid-highlighter-on-navigate.js]
 [browser_rules_grid-highlighter-on-reload.js]
 [browser_rules_grid-highlighter-restored-after-reload.js]
+[browser_rules_grid-toggle-telemetry.js]
 [browser_rules_grid-toggle_01.js]
 [browser_rules_grid-toggle_01b.js]
 [browser_rules_grid-toggle_02.js]
 [browser_rules_grid-toggle_03.js]
 [browser_rules_grid-toggle_04.js]
 [browser_rules_grid-toggle_05.js]
 [browser_rules_gridline-names-autocomplete.js]
 [browser_rules_guessIndentation.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_flexbox-toggle-telemetry.js
@@ -0,0 +1,46 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry is correct when the flexbox highlighter is activated from
+// the rules view.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #flex {
+      display: flex;
+    }
+  </style>
+  <div id="flex"></div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  startTelemetry();
+  const {inspector, view} = await openRuleView();
+  const {highlighters} = view;
+
+  await selectNode("#flex", inspector);
+  const container = getRuleViewProperty(view, "#flex", "display").valueSpan;
+  const flexboxToggle = container.querySelector(".ruleview-flex");
+
+  info("Toggling ON the flexbox highlighter from the rule-view.");
+  const onHighlighterShown = highlighters.once("flexbox-highlighter-shown");
+  flexboxToggle.click();
+  await onHighlighterShown;
+
+  info("Toggling OFF the flexbox highlighter from the rule-view.");
+  const onHighlighterHidden = highlighters.once("flexbox-highlighter-hidden");
+  flexboxToggle.click();
+  await onHighlighterHidden;
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.rules.flexboxhighlighter.opened", "", 1, "scalar");
+  checkTelemetry("DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS", "", null,
+    "hasentries");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle-telemetry.js
@@ -0,0 +1,42 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the telemetry count is correct when the grid highlighter is activated from
+// the rules view.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  startTelemetry();
+  const {inspector, view} = await openRuleView();
+  const highlighters = view.highlighters;
+
+  await selectNode("#grid", inspector);
+  const container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+  const gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Toggling ON the CSS grid highlighter from the rule-view.");
+  const onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  gridToggle.click();
+  await onHighlighterShown;
+
+  checkResults();
+});
+
+function checkResults() {
+  checkTelemetry("devtools.rules.gridinspector.opened", "", 1, "scalar");
+}
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -1,12 +1,13 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../shared/test/telemetry-test-helpers.js */
 /* import-globals-from ../../test/head.js */
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -253,51 +253,68 @@ class HighlightersOverlay {
     return { color };
   }
 
   /**
    * Toggle the flexbox highlighter for the given flexbox container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the flexbox container element to highlight.
-   * @param  {Object} options
-   *         Object used for passing options to the flexbox highlighter.
+   * @param. {String} trigger
+   *         String name matching "layout", "markup" or "rule" to indicate where the
+   *         flexbox highlighter was toggled on from. "layout" represents the layout view.
+   *         "markup" represents the markup view. "rule" represents the rule view.
    */
-  async toggleFlexboxHighlighter(node, options = {}) {
+  async toggleFlexboxHighlighter(node, trigger) {
     if (node == this.flexboxHighlighterShown) {
       await this.hideFlexboxHighlighter(node);
       return;
     }
 
-    await this.showFlexboxHighlighter(node, options);
+    await this.showFlexboxHighlighter(node, {}, trigger);
   }
 
   /**
    * Show the flexbox highlighter for the given flexbox container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the flexbox container element to highlight.
    * @param  {Object} options
    *         Object used for passing options to the flexbox highlighter.
+   * @param. {String} trigger
+   *         String name matching "layout", "markup" or "rule" to indicate where the
+   *         flexbox highlighter was toggled on from. "layout" represents the layout view.
+   *         "markup" represents the markup view. "rule" represents the rule view.
    */
-  async showFlexboxHighlighter(node, options) {
+  async showFlexboxHighlighter(node, options, trigger) {
     const highlighter = await this._getHighlighter("FlexboxHighlighter");
     if (!highlighter) {
       return;
     }
 
     options = Object.assign({}, options, this.getFlexboxHighlighterSettings(node));
 
     const isShown = await highlighter.show(node, options);
     if (!isShown) {
       return;
     }
 
     this._toggleRuleViewIcon(node, true, ".ruleview-flex");
 
+    this.telemetry.toolOpened("flexbox_highlighter", this.inspector.toolbox.sessionId,
+      this);
+
+    if (trigger === "layout") {
+      this.telemetry.scalarAdd("devtools.layout.flexboxhighlighter.opened", 1);
+    } else if (trigger === "markup") {
+      this.telemetry.scalarAdd("devtools.markup.flexboxhighlighter.opened", 1);
+    } else if (trigger === "rule") {
+      this.telemetry.scalarAdd("devtools.rules.flexboxhighlighter.opened", 1);
+    }
+
     try {
       // Save flexbox highlighter state.
       const { url } = this.inspector.target;
       const selector = await node.getUniqueSelector();
       this.state.flexbox = { selector, options, url };
       this.flexboxHighlighterShown = node;
 
       // Emit the NodeFront of the flexbox container element that the flexbox highlighter
@@ -314,16 +331,19 @@ class HighlightersOverlay {
    * @param  {NodeFront} node
    *         The NodeFront of the flexbox container element to unhighlight.
    */
   async hideFlexboxHighlighter(node) {
     if (!this.flexboxHighlighterShown || !this.highlighters.FlexboxHighlighter) {
       return;
     }
 
+    this.telemetry.toolClosed("flexbox_highlighter", this.inspector.toolbox.sessionId,
+      this);
+
     this._toggleRuleViewIcon(node, false, ".ruleview-flex");
 
     await this.highlighters.FlexboxHighlighter.hide();
 
     // Emit the NodeFront of the flexbox container element that the flexbox highlighter
     // was hidden for.
     const nodeFront = this.flexboxHighlighterShown;
     this.flexboxHighlighterShown = null;
@@ -402,20 +422,20 @@ class HighlightersOverlay {
     return Object.assign({}, highlighterSettings, { color });
   }
 
   /**
    * Toggle the grid highlighter for the given grid container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to highlight.
-   * @param. {String|null} trigger
-   *         String name matching "grid" or "rule" to indicate where the
-   *         grid highlighter was toggled on from. "grid" represents the grid view
-   *         "rule" represents the rule view.
+   * @param. {String} trigger
+   *         String name matching "grid", "markup" or "rule" to indicate where the
+   *         grid highlighter was toggled on from. "grid" represents the grid view.
+   *         "markup" represents the markup view. "rule" represents the rule view.
    */
   async toggleGridHighlighter(node, trigger) {
     if (this.gridHighlighters.has(node)) {
       await this.hideGridHighlighter(node);
       return;
     }
 
     await this.showGridHighlighter(node, {}, trigger);
@@ -423,20 +443,20 @@ class HighlightersOverlay {
 
   /**
    * Show the grid highlighter for the given grid container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to highlight.
    * @param  {Object} options
    *         Object used for passing options to the grid highlighter.
-   * @param. {String|null} trigger
-   *         String name matching "grid" or "rule" to indicate where the
-   *         grid highlighter was toggled on from. "grid" represents the grid view
-   *         "rule" represents the rule view.
+   * @param. {String} trigger
+   *         String name matching "grid", "markup" or "rule" to indicate where the
+   *         grid highlighter was toggled on from. "grid" represents the grid view.
+   *         "markup" represents the markup view. "rule" represents the rule view.
    */
   async showGridHighlighter(node, options, trigger) {
     // When the grid highlighter has the given node, it is probably called with new
     // highlighting options, so skip any extra grid highlighter handling.
     if (!this.gridHighlighters.has(node)) {
       if (this.maxGridHighlighters === 1) {
         // Only one grid highlighter can be shown at a time. Hides any instantiated
         // grid highlighters.
@@ -459,19 +479,21 @@ class HighlightersOverlay {
 
     const isShown = await highlighter.show(node, options);
     if (!isShown) {
       return;
     }
 
     this._toggleRuleViewIcon(node, true, ".ruleview-grid");
 
-    if (trigger == "grid") {
+    if (trigger === "grid") {
       this.telemetry.scalarAdd("devtools.grid.gridinspector.opened", 1);
-    } else if (trigger == "rule") {
+    } else if (trigger === "markup") {
+      this.telemetry.scalarAdd("devtools.markup.gridinspector.opened", 1);
+    } else if (trigger === "rule") {
       this.telemetry.scalarAdd("devtools.rules.gridinspector.opened", 1);
     }
 
     try {
       // Save grid highlighter state.
       const { url } = this.inspector.target;
       const selector = await node.getUniqueSelector();
       this.state.grids.set(node, { selector, options, url });
@@ -963,17 +985,17 @@ class HighlightersOverlay {
   onClick(event) {
     if (this._isRuleViewDisplayGrid(event.target)) {
       event.stopPropagation();
       this.toggleGridHighlighter(this.inspector.selection.nodeFront, "rule");
     }
 
     if (this._isRuleViewDisplayFlex(event.target)) {
       event.stopPropagation();
-      this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront);
+      this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront, "rule");
     }
 
     if (this._isRuleViewShapeSwatch(event.target)) {
       event.stopPropagation();
 
       const view = this.inspector.getPanel("ruleview").view;
       const nodeInfo = view.getNodeInfo(event.target);
 
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -727,26 +727,24 @@ function getChartsFromToolId(id) {
     case "COMPUTEDVIEW":
     case "FONTINSPECTOR":
     case "LAYOUTVIEW":
     case "RULEVIEW":
       useTimedEvent = true;
       timerHist = `DEVTOOLS_${id}_TIME_ACTIVE_SECONDS`;
       countHist = `DEVTOOLS_${id}_OPENED_COUNT`;
       break;
+    case "FLEXBOX_HIGHLIGHTER":
+      timerHist = `DEVTOOLS_${id}_TIME_ACTIVE_SECONDS`;
+      break;
     default:
       timerHist = `DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS`;
       countHist = `DEVTOOLS_CUSTOM_OPENED_COUNT`;
   }
 
-  if (!timerHist || (!countHist && !countScalar)) {
-    throw new Error(`getChartsFromToolId cannot be called without a timer ` +
-                    `histogram and either a count histogram or count scalar.`);
-  }
-
   return {
     useTimedEvent: useTimedEvent,
     timerHist: timerHist,
     countHist: countHist,
     countScalar: countScalar,
   };
 }
 
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -72,17 +72,19 @@ support-files =
 [browser_cubic-bezier-01.js]
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
 [browser_cubic-bezier-04.js]
 [browser_cubic-bezier-05.js]
 [browser_cubic-bezier-06.js]
 [browser_cubic-bezier-07.js]
 [browser_dbg_addon-console.js]
-skip-if = (e10s && debug || os == 'win' || verify)
+# To be removed or updated in bug 1497264
+# previously was: (e10s && debug || os == 'win' || verify)
+skip-if = true 
 tags = addons
 [browser_dbg_debugger-statement.js]
 skip-if = e10s && debug
 [browser_dbg_event-listeners-01.js]
 skip-if = e10s && debug
 [browser_dbg_event-listeners-02.js]
 skip-if = e10s && debug
 [browser_dbg_event-listeners-03.js]
@@ -240,17 +242,19 @@ skip-if = !e10s || os == "win" # RDM onl
 [browser_telemetry_toolboxtabs_webconsole.js]
 [browser_treeWidget_basic.js]
 [browser_treeWidget_keyboard_interaction.js]
 [browser_treeWidget_mouse_interaction.js]
 [browser_devices.js]
 skip-if = verify
 [browser_theme_switching.js]
 [browser_dbg_listaddons.js]
-skip-if = e10s && debug
+# To be removed or updated in bug 1497264
+# previously was: e10s && debug
+skip-if = true
 tags = addons
 [browser_dbg_listtabs-01.js]
 [browser_dbg_listtabs-02.js]
 skip-if = true # Never worked for remote frames, needs a mock DebuggerServerConnection
 [browser_dbg_listtabs-03.js]
 skip-if = e10s && debug
 [browser_dbg_multiple-windows.js]
 [browser_dbg_navigation.js]
--- a/docshell/base/nsAboutRedirector.cpp
+++ b/docshell/base/nsAboutRedirector.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "nsAboutRedirector.h"
 #include "nsNetUtil.h"
 #include "nsAboutProtocolUtils.h"
+#include "nsBaseChannel.h"
 #include "mozilla/ArrayUtils.h"
 #include "nsIProtocolHandler.h"
 
 #if defined(MOZ_WIDGET_ANDROID) && defined(RELEASE_OR_BETA)
 #define ABOUT_CONFIG_BLOCKED_GV
 #endif
 
 #ifdef ABOUT_CONFIG_BLOCKED_GV
@@ -22,16 +23,46 @@ NS_IMPL_ISUPPORTS(nsAboutRedirector, nsI
 
 struct RedirEntry
 {
   const char* id;
   const char* url;
   uint32_t flags;
 };
 
+class CrashChannel final : public nsBaseChannel
+{
+public:
+  explicit CrashChannel(nsIURI* aURI)
+  {
+    SetURI(aURI);
+  }
+
+  nsresult OpenContentStream(bool async, nsIInputStream **stream,
+                             nsIChannel** channel) override
+  {
+    nsAutoCString spec;
+    mURI->GetSpec(spec);
+
+    if (spec.EqualsASCII("about:crashparent") && XRE_IsParentProcess()) {
+      MOZ_CRASH("Crash via about:crashparent");
+    }
+
+    if (spec.EqualsASCII("about:crashcontent") && XRE_IsContentProcess()) {
+      MOZ_CRASH("Crash via about:crashcontent");
+    }
+
+    NS_WARNING("Unhandled about:crash* URI or wrong process");
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+protected:
+  virtual ~CrashChannel() = default;
+};
+
 /*
   Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome
   privileges. This is potentially dangerous. Please use
   URI_SAFE_FOR_UNTRUSTED_CONTENT in the third argument to each map item below
   unless your about: page really needs chrome privileges. Security review is
   required before adding new map entries without
   URI_SAFE_FOR_UNTRUSTED_CONTENT.
 
@@ -140,22 +171,20 @@ static const RedirEntry kRedirMap[] = {
   {
     "printpreview", "about:blank",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::URI_CAN_LOAD_IN_CHILD
   },
   {
     "crashparent", "about:blank",
-    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT
   },
   {
     "crashcontent", "about:blank",
-    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::URI_CAN_LOAD_IN_CHILD |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD
   }
 };
 static const int kRedirTotal = mozilla::ArrayLength(kRedirMap);
 
 NS_IMETHODIMP
@@ -169,22 +198,20 @@ nsAboutRedirector::NewChannel(nsIURI* aU
 
   nsAutoCString path;
   nsresult rv = NS_GetAboutModuleName(aURI, path);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (XRE_IsParentProcess() && path.EqualsASCII("crashparent")) {
-    MOZ_CRASH("Crash via about:crashparent");
-  }
-
-  if (XRE_IsContentProcess() && path.EqualsASCII("crashcontent")) {
-    MOZ_CRASH("Crash via about:crashcontent");
+  if (path.EqualsASCII("crashparent") || path.EqualsASCII("crashcontent")) {
+    nsCOMPtr<nsIChannel> channel = new CrashChannel(aURI);
+    channel.forget(aResult);
+    return NS_OK;
   }
 
 #ifdef ABOUT_CONFIG_BLOCKED_GV
   // We don't want to allow access to about:config from
   // GeckoView on release or beta, but it's fine for Fennec.
   if (path.EqualsASCII("config") && !mozilla::jni::IsFennec()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
--- a/docshell/test/mochitest/mochitest.ini
+++ b/docshell/test/mochitest/mochitest.ini
@@ -110,8 +110,9 @@ support-files = file_bug675587.html
 [test_close_onpagehide_by_window_close.html]
 [test_forceinheritprincipal_overrule_owner.html]
 [test_framedhistoryframes.html]
 skip-if = toolkit == 'android' # bug 784321
 support-files = file_framedhistoryframes.html
 [test_pushState_after_document_open.html]
 [test_windowedhistoryframes.html]
 [test_triggeringprincipal_location_seturi.html]
+[test_bug1507702.html]
new file mode 100644
--- /dev/null
+++ b/docshell/test/mochitest/test_bug1507702.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1507702
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1507702</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <link rel="icon" href="about:crashparent"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1507702">Mozilla Bug 1507702</a>
+<img src="about:crashparent">
+<img src="about:crashcontent">
+<iframe src="about:crashparent"></iframe>
+<iframe src="about:crashcontent"></iframe>
+<script>
+  let urls = ["about:crashparent", "about:crashcontent"];
+  async function testFetch() {
+    const url = urls.shift();
+    if (!url) {
+      return Promise.resolve();
+    }
+
+    let threw;
+    try {
+      await fetch(url);
+      threw = false;
+    } catch (e) {
+      threw = true;
+    };
+
+    ok(threw === true, "fetch should reject");
+    return testFetch();
+  }
+
+  document.body.onload = async () => {
+    for (const url of ["about:crashparent", "about:crashcontent"]) {
+      SimpleTest.doesThrow(() => {
+        top.location.href = url;
+      }, "navigation should throw");
+
+      SimpleTest.doesThrow(() => {
+        location.href = url;
+      }, "navigation should throw");
+    }
+
+    await testFetch();
+    SimpleTest.finish();
+  };
+
+  SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
--- a/dom/asmjscache/AsmJSCache.cpp
+++ b/dom/asmjscache/AsmJSCache.cpp
@@ -787,17 +787,19 @@ ParentRunnable::ReadMetadata()
   AssertIsOnIOThread();
   MOZ_ASSERT(mState == eReadyToReadMetadata);
 
   QuotaManager* qm = QuotaManager::Get();
   MOZ_ASSERT(qm, "We are on the QuotaManager's IO thread");
 
   nsresult rv =
     qm->EnsureOriginIsInitialized(quota::PERSISTENCE_TYPE_TEMPORARY, mSuffix,
-                                  mGroup, mOrigin, getter_AddRefs(mDirectory));
+                                  mGroup, mOrigin,
+                                  /* aCreateIfNotExists */ true,
+                                  getter_AddRefs(mDirectory));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     mResult = JS::AsmJSCache_StorageInitFailure;
     return rv;
   }
 
   rv = mDirectory->Append(NS_LITERAL_STRING(ASMJSCACHE_DIRECTORY_NAME));
   NS_ENSURE_SUCCESS(rv, rv);
 
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -614,20 +614,17 @@ ChromeUtils::IsOriginAttributesEqual(dom
 {
   return IsOriginAttributesEqual(aA, aB);
 }
 
 /* static */ bool
 ChromeUtils::IsOriginAttributesEqual(const dom::OriginAttributesDictionary& aA,
                                      const dom::OriginAttributesDictionary& aB)
 {
-  return aA.mAppId == aB.mAppId &&
-         aA.mInIsolatedMozBrowser == aB.mInIsolatedMozBrowser &&
-         aA.mUserContextId == aB.mUserContextId &&
-         aA.mPrivateBrowsingId == aB.mPrivateBrowsingId;
+  return aA == aB;
 }
 
 #ifdef NIGHTLY_BUILD
 /* static */ void
 ChromeUtils::GetRecentJSDevError(GlobalObject& aGlobal,
                                 JS::MutableHandleValue aRetval,
                                 ErrorResult& aRv)
 {
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -18,16 +18,18 @@
 #include "nsDOMNavigationTiming.h"
 #include "nsIDOMStorageManager.h"
 #include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/ContentFrameMessageManager.h"
 #include "mozilla/dom/DOMJSProxyHandler.h"
 #include "mozilla/dom/DOMPrefs.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/LSObject.h"
 #include "mozilla/dom/PartitionedLocalStorage.h"
 #include "mozilla/dom/Storage.h"
 #include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
 #include "mozilla/dom/StorageEventBinding.h"
 #include "mozilla/dom/StorageNotifierService.h"
 #include "mozilla/dom/StorageUtils.h"
@@ -4903,42 +4905,48 @@ nsGlobalWindowInner::GetLocalStorage(Err
 
   // Note that this behavior is observable: if we grant storage permission to a
   // tracker, we pass from the partitioned LocalStorage to the 'normal'
   // LocalStorage. The previous data is lost and the 2 window.localStorage
   // objects, before and after the permission granted, will be different.
   if (access != nsContentUtils::StorageAccess::ePartitionedOrDeny &&
       (!mLocalStorage ||
         mLocalStorage->Type() == Storage::ePartitionedLocalStorage)) {
-    nsresult rv;
-    nsCOMPtr<nsIDOMStorageManager> storageManager =
-      do_GetService("@mozilla.org/dom/localStorage-manager;1", &rv);
-    if (NS_FAILED(rv)) {
-      aError.Throw(rv);
-      return nullptr;
-    }
-
-    nsString documentURI;
-    if (mDoc) {
-      aError = mDoc->GetDocumentURI(documentURI);
-      if (NS_WARN_IF(aError.Failed())) {
+    RefPtr<Storage> storage;
+
+    if (NextGenLocalStorageEnabled()) {
+      aError = LSObject::CreateForWindow(this, getter_AddRefs(storage));
+    } else {
+      nsresult rv;
+      nsCOMPtr<nsIDOMStorageManager> storageManager =
+        do_GetService("@mozilla.org/dom/localStorage-manager;1", &rv);
+      if (NS_FAILED(rv)) {
+        aError.Throw(rv);
         return nullptr;
       }
-    }
-
-    nsIPrincipal *principal = GetPrincipal();
-    if (!principal) {
-      aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
-      return nullptr;
-    }
-
-    RefPtr<Storage> storage;
-    aError = storageManager->CreateStorage(this, principal, documentURI,
-                                           IsPrivateBrowsing(),
-                                           getter_AddRefs(storage));
+
+      nsString documentURI;
+      if (mDoc) {
+        aError = mDoc->GetDocumentURI(documentURI);
+        if (NS_WARN_IF(aError.Failed())) {
+          return nullptr;
+        }
+      }
+
+      nsIPrincipal *principal = GetPrincipal();
+      if (!principal) {
+        aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+        return nullptr;
+      }
+
+      aError = storageManager->CreateStorage(this, principal, documentURI,
+                                             IsPrivateBrowsing(),
+                                             getter_AddRefs(storage));
+    }
+
     if (aError.Failed()) {
       return nullptr;
     }
 
     mLocalStorage = storage;
     MOZ_ASSERT(mLocalStorage);
   }
 
@@ -5804,29 +5812,30 @@ nsGlobalWindowInner::ObserveStorageNotif
   }
 
   else {
     MOZ_ASSERT(!NS_strcmp(aStorageType, u"localStorage"));
 
     MOZ_DIAGNOSTIC_ASSERT(StorageUtils::PrincipalsEqual(aEvent->GetPrincipal(),
                                                         principal));
 
-    fireMozStorageChanged = mLocalStorage == aEvent->GetStorageArea();
+    fireMozStorageChanged =
+       mLocalStorage && mLocalStorage == aEvent->GetStorageArea();
 
     if (fireMozStorageChanged) {
       eventType.AssignLiteral("MozLocalStorageChanged");
     }
   }
 
   // Clone the storage event included in the observer notification. We want
   // to dispatch clones rather than the original event.
   IgnoredErrorResult error;
   RefPtr<StorageEvent> clonedEvent =
     CloneStorageEvent(eventType, aEvent, error);
-  if (error.Failed()) {
+  if (error.Failed() || !clonedEvent) {
     return;
   }
 
   clonedEvent->SetTrusted(true);
 
   if (fireMozStorageChanged) {
     WidgetEvent* internalEvent = clonedEvent->WidgetEventPtr();
     internalEvent->mFlags.mOnlyChromeDispatch = true;
@@ -5851,26 +5860,28 @@ nsGlobalWindowInner::CloneStorageEvent(c
 
   RefPtr<Storage> storageArea = aEvent->GetStorageArea();
 
   RefPtr<Storage> storage;
 
   // If null, this is a localStorage event received by IPC.
   if (!storageArea) {
     storage = GetLocalStorage(aRv);
-    if (aRv.Failed() || !storage) {
-      return nullptr;
-    }
-
-    if (storage->Type() == Storage::eLocalStorage) {
-      RefPtr<LocalStorage> localStorage =
-        static_cast<LocalStorage*>(storage.get());
-
-      // We must apply the current change to the 'local' localStorage.
-      localStorage->ApplyEvent(aEvent);
+    if (!NextGenLocalStorageEnabled()) {
+      if (aRv.Failed() || !storage) {
+        return nullptr;
+      }
+
+      if (storage->Type() == Storage::eLocalStorage) {
+        RefPtr<LocalStorage> localStorage =
+          static_cast<LocalStorage*>(storage.get());
+
+        // We must apply the current change to the 'local' localStorage.
+        localStorage->ApplyEvent(aEvent);
+      }
     }
   } else if (storageArea->Type() == Storage::eSessionStorage) {
     storage = GetSessionStorage(aRv);
   } else {
     MOZ_ASSERT(storageArea->Type() == Storage::eLocalStorage);
     storage = GetLocalStorage(aRv);
   }
 
@@ -6785,29 +6796,50 @@ nsGlobalWindowInner::EventListenerAdded(
     mTabChild->BeforeUnloadAdded();
   }
 
   // We need to initialize localStorage in order to receive notifications.
   if (aType == nsGkAtoms::onstorage) {
     ErrorResult rv;
     GetLocalStorage(rv);
     rv.SuppressException();
+
+    if (NextGenLocalStorageEnabled() &&
+        mLocalStorage && mLocalStorage->Type() == Storage::eLocalStorage) {
+      auto object = static_cast<LSObject*>(mLocalStorage.get());
+
+      Unused << NS_WARN_IF(NS_FAILED(object->EnsureObserver()));
+    }
   }
 }
 
 void
 nsGlobalWindowInner::EventListenerRemoved(nsAtom* aType)
 {
   if (aType == nsGkAtoms::onbeforeunload &&
       mTabChild &&
       (!mDoc || !(mDoc->GetSandboxFlags() & SANDBOXED_MODALS))) {
     mBeforeUnloadListenerCount--;
     MOZ_ASSERT(mBeforeUnloadListenerCount >= 0);
     mTabChild->BeforeUnloadRemoved();
   }
+
+  if (aType == nsGkAtoms::onstorage) {
+    if (NextGenLocalStorageEnabled() &&
+        mLocalStorage &&
+        mLocalStorage->Type() == Storage::eLocalStorage &&
+        // The remove event is fired even if this isn't the last listener, so
+        // only remove if there are no other listeners left.
+        mListenerManager &&
+        !mListenerManager->HasListenersFor(nsGkAtoms::onstorage)) {
+      auto object = static_cast<LSObject*>(mLocalStorage.get());
+
+      object->DropObserver();
+    }
+  }
 }
 
 void
 nsGlobalWindowInner::NotifyVREventListenerAdded()
 {
   mHasVREvents = true;
   EnableVRUpdates();
 }
@@ -7959,16 +7991,24 @@ nsGlobalWindowInner::StorageAccessGrante
     IgnoredErrorResult error;
     GetLocalStorage(error);
     if (NS_WARN_IF(error.Failed())) {
       return;
     }
 
     MOZ_ASSERT(mLocalStorage &&
                mLocalStorage->Type() == Storage::eLocalStorage);
+
+    if (NextGenLocalStorageEnabled() &&
+        mListenerManager &&
+        mListenerManager->HasListenersFor(nsGkAtoms::onstorage)) {
+      auto object = static_cast<LSObject*>(mLocalStorage.get());
+
+      object->EnsureObserver();
+    }
   }
 }
 
 mozilla::dom::TabGroup*
 nsPIDOMWindowInner::TabGroup()
 {
   return nsGlobalWindowInner::Cast(this)->TabGroupInner();
 }
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -22,16 +22,17 @@
 #include "nsIPermissionManager.h"
 #include "nsIPrefBranch.h"
 #include "nsISecureBrowserUI.h"
 #include "nsIWebProgressListener.h"
 #include "mozilla/AntiTrackingCommon.h"
 #include "mozilla/dom/ContentFrameMessageManager.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
+#include "mozilla/dom/LSObject.h"
 #include "mozilla/dom/Storage.h"
 #include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
 #include "mozilla/dom/StorageEventBinding.h"
 #include "mozilla/dom/StorageNotifierService.h"
 #include "mozilla/dom/StorageUtils.h"
 #include "mozilla/dom/Timeout.h"
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -627,17 +627,17 @@ skip-if = toolkit == 'android' # Timeout
 [test_copyimage.html]
 subsuite = clipboard
 skip-if = toolkit == 'android' #bug 904183
 [test_copypaste.html]
 subsuite = clipboard
 skip-if = toolkit == 'android' #bug 904183
 [test_copypaste.xhtml]
 subsuite = clipboard
-skip-if = toolkit == 'android' #bug 904183
+skip-if = toolkit == 'android' && !e10s #bug 904183
 [test_createHTMLDocument.html]
 [test_data_uri.html]
 skip-if = verify
 [test_document.all_iteration.html]
 [test_document.all_unqualified.html]
 [test_document_constructor.html]
 [test_document_importNode_document.html]
 [test_custom_element.html]
--- a/dom/bindings/BindingDeclarations.h
+++ b/dom/bindings/BindingDeclarations.h
@@ -158,16 +158,21 @@ public:
     mImpl.emplace(aValue);
   }
 
   bool operator==(const Optional_base<T, InternalType>& aOther) const
   {
     return mImpl == aOther.mImpl;
   }
 
+  bool operator!=(const Optional_base<T, InternalType>& aOther) const
+  {
+    return mImpl != aOther.mImpl;
+  }
+
   template<typename T1, typename T2>
   explicit Optional_base(const T1& aValue1, const T2& aValue2)
   {
     mImpl.emplace(aValue1, aValue2);
   }
 
   bool WasPassed() const
   {
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -12991,16 +12991,40 @@ class CGDictionary(CGThing):
             body.append(memberAssign)
         body.append(CGGeneric("return *this;\n"))
         return ClassMethod(
             "operator=", "%s&" % self.makeClassName(self.dictionary),
             [Argument("const %s&" % self.makeClassName(self.dictionary),
                       "aOther")],
             body=body.define())
 
+    def canHaveEqualsOperator(self):
+        return all(m.type.isString() or m.type.isPrimitive() for (m,_) in
+                   self.memberInfo)
+
+    def equalsOperator(self):
+        body = CGList([])
+
+        for m, _ in self.memberInfo:
+            memberName = self.makeMemberName(m.identifier.name)
+            memberTest = CGGeneric(fill(
+                """
+                if (${memberName} != aOther.${memberName}) {
+                    return false;
+                }
+                """,
+                memberName=memberName))
+            body.append(memberTest)
+        body.append(CGGeneric("return true;\n"))
+        return ClassMethod(
+            "operator==", "bool",
+            [Argument("const %s&" % self.makeClassName(self.dictionary),
+                      "aOther")
+            ], const=True, body=body.define())
+
     def getStructs(self):
         d = self.dictionary
         selfName = self.makeClassName(d)
         members = [ClassMember(self.makeMemberName(m[0].identifier.name),
                                self.getMemberType(m),
                                visibility="public",
                                body=self.getMemberInitializer(m),
                                hasIgnoreInitCheckFlag=True)
@@ -13071,16 +13095,19 @@ class CGDictionary(CGThing):
                                           bodyInHeader=True,
                                           visibility="public",
                                           explicit=True,
                                           body="*this = aOther;\n"))
             methods.append(self.assignmentOperator())
         else:
             disallowCopyConstruction = True
 
+        if self.canHaveEqualsOperator():
+            methods.append(self.equalsOperator())
+
         struct = CGClass(selfName,
                          bases=[ClassBase(self.base())],
                          members=members,
                          constructors=ctors,
                          methods=methods,
                          isStruct=True,
                          disallowCopyConstruction=disallowCopyConstruction)
 
--- a/dom/browser-element/mochitest/mochitest.ini
+++ b/dom/browser-element/mochitest/mochitest.ini
@@ -102,17 +102,17 @@ support-files =
 [test_browserElement_inproc_BrowserWindowNamespace.html]
 [test_browserElement_inproc_BrowserWindowResize.html]
 [test_browserElement_inproc_Close.html]
 [test_browserElement_inproc_CloseFromOpener.html]
 [test_browserElement_inproc_ContextmenuEvents.html]
 [test_browserElement_inproc_CookiesNotThirdParty.html]
 [test_browserElement_inproc_CopyPaste.html]
 subsuite = clipboard
-skip-if = (os == "android") # Disabled on Android, see bug 1230421
+skip-if = (os == "android" && !e10s) # Disabled on Android, see bug 1230421
 [test_browserElement_inproc_DataURI.html]
 [test_browserElement_inproc_ExposableURI.html]
 [test_browserElement_inproc_FirstPaint.html]
 [test_browserElement_inproc_ForwardName.html]
 [test_browserElement_inproc_FrameWrongURI.html]
 [test_browserElement_inproc_Iconchange.html]
 [test_browserElement_inproc_LoadEvents.html]
 [test_browserElement_inproc_Metachange.html]
--- a/dom/cache/Context.cpp
+++ b/dom/cache/Context.cpp
@@ -430,16 +430,17 @@ Context::QuotaInitRunnable::Run()
       }
 
       QuotaManager* qm = QuotaManager::Get();
       MOZ_DIAGNOSTIC_ASSERT(qm);
       nsresult rv = qm->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT,
                                                   mQuotaInfo.mSuffix,
                                                   mQuotaInfo.mGroup,
                                                   mQuotaInfo.mOrigin,
+                                                  /* aCreateIfNotExists */ true,
                                                   getter_AddRefs(mQuotaInfo.mDir));
       if (NS_FAILED(rv)) {
         resolver->Resolve(rv);
         break;
       }
 
       mState = STATE_RUN_ON_TARGET;
 
--- a/dom/cache/FileUtils.cpp
+++ b/dom/cache/FileUtils.cpp
@@ -289,17 +289,17 @@ BodyMaybeUpdatePaddingSize(const QuotaIn
   MOZ_DIAGNOSTIC_ASSERT(bodyFile);
 
   QuotaManager* quotaManager = QuotaManager::Get();
   MOZ_DIAGNOSTIC_ASSERT(quotaManager);
 
   int64_t fileSize = 0;
   RefPtr<QuotaObject> quotaObject =
     quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT, aQuotaInfo.mGroup,
-                                 aQuotaInfo.mOrigin, bodyFile, &fileSize);
+                                 aQuotaInfo.mOrigin, bodyFile, -1, &fileSize);
   MOZ_DIAGNOSTIC_ASSERT(quotaObject);
   MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0);
   // XXXtt: bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1422815
   if (!quotaObject) { return NS_ERROR_UNEXPECTED; }
 
   if (*aPaddingSizeOut == InternalResponse::UNKNOWN_PADDING_SIZE) {
     *aPaddingSizeOut = BodyGeneratePadding(fileSize, aPaddingInfo);
   }
--- a/dom/file/ipc/IPCBlobInputStreamThread.cpp
+++ b/dom/file/ipc/IPCBlobInputStreamThread.cpp
@@ -242,10 +242,27 @@ IPCBlobInputStreamThread::DispatchFromSc
 }
 
 NS_IMETHODIMP
 IPCBlobInputStreamThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
+bool
+IsOnDOMFileThread()
+{
+  mozilla::StaticMutexAutoLock lock(gIPCBlobThreadMutex);
+
+  MOZ_ASSERT(!gShutdownHasStarted);
+  MOZ_ASSERT(gIPCBlobThread);
+
+  return gIPCBlobThread->IsOnCurrentThreadInfallible();
+}
+
+void
+AssertIsOnDOMFileThread()
+{
+  MOZ_ASSERT(IsOnDOMFileThread());
+}
+
 } // dom namespace
 } // mozilla namespace
--- a/dom/file/ipc/IPCBlobInputStreamThread.h
+++ b/dom/file/ipc/IPCBlobInputStreamThread.h
@@ -49,12 +49,18 @@ private:
 
   nsCOMPtr<nsIThread> mThread;
 
   // This is populated if MigrateActor() is called before the initialization of
   // the thread.
   nsTArray<RefPtr<IPCBlobInputStreamChild>> mPendingActors;
 };
 
+bool
+IsOnDOMFileThread();
+
+void
+AssertIsOnDOMFileThread();
+
 } // dom namespace
 } // mozilla namespace
 
 #endif // mozilla_dom_IPCBlobInputStreamThread_h
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -18414,21 +18414,23 @@ Maintenance::DirectoryWork()
                                                     origin,
                                                     std::move(databasePaths)));
 
         nsCOMPtr<nsIFile> directory;
 
         // Idle maintenance may occur before origin is initailized.
         // Ensure origin is initialized first. It will initialize all origins
         // for temporary storage including IDB origins.
-        rv = quotaManager->EnsureOriginIsInitialized(persistenceType,
-                                                     suffix,
-                                                     group,
-                                                     origin,
-                                                     getter_AddRefs(directory));
+        rv = quotaManager->EnsureOriginIsInitialized(
+                                                  persistenceType,
+                                                  suffix,
+                                                  group,
+                                                  origin,
+                                                  /* aCreateIfNotExists */ true,
+                                                  getter_AddRefs(directory));
 
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return rv;
         }
       }
     }
   }
 
@@ -21314,16 +21316,17 @@ OpenDatabaseOp::DoDatabaseWork()
 
   nsCOMPtr<nsIFile> dbDirectory;
 
   nsresult rv =
     quotaManager->EnsureOriginIsInitialized(persistenceType,
                                             mSuffix,
                                             mGroup,
                                             mOrigin,
+                                            /* aCreateIfNotExists */ true,
                                             getter_AddRefs(dbDirectory));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = dbDirectory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
--- a/dom/interfaces/storage/nsIDOMStorageManager.idl
+++ b/dom/interfaces/storage/nsIDOMStorageManager.idl
@@ -44,16 +44,21 @@ interface nsIDOMStorageManager : nsISupp
    * @param aPrivate
    *    Whether the demanding document is running in Private Browsing mode or not.
    */
   Storage createStorage(in mozIDOMWindow aWindow,
                         in nsIPrincipal aPrincipal,
                         in AString aDocumentURI,
                         [optional] in bool aPrivate);
   /**
+   * DEPRECATED.  The only good reason to use this was if you were writing a
+   * test and wanted to hackily determine if a preload happened.  That's now
+   * covered by `nsILocalStorageManager.isPreloaded` and you should use that if
+   * that's what you want.  If LSNG is in use, this will throw.
+   *
    * Returns instance of DOM storage object for given principal.
    * If there is no storage managed for the scope, then null is returned and
    * no object is created.  Otherwise, an object (new) for the existing storage
    * scope is returned.
    *
    * @param aWindow
    *    The parent window.
    * @param aPrincipal
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -36,16 +36,17 @@
 #include "mozilla/dom/VideoDecoderManagerChild.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/DataTransfer.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/ExternalHelperAppChild.h"
 #include "mozilla/dom/FileCreatorHelper.h"
 #include "mozilla/dom/GetFilesHelper.h"
 #include "mozilla/dom/IPCBlobUtils.h"
+#include "mozilla/dom/LSObject.h"
 #include "mozilla/dom/MemoryReportRequest.h"
 #include "mozilla/dom/PLoginReputationChild.h"
 #include "mozilla/dom/PushNotifier.h"
 #include "mozilla/dom/RemoteWorkerService.h"
 #include "mozilla/dom/ServiceWorkerManager.h"
 #include "mozilla/dom/TabGroup.h"
 #include "mozilla/dom/nsIContentChild.h"
 #include "mozilla/dom/URLClassifierChild.h"
@@ -3806,25 +3807,31 @@ ContentChild::GetSpecificMessageEventTar
 
       return do_AddRef(SystemGroup::EventTargetFor(TaskCategory::Other));
 
     default:
       return nullptr;
   }
 }
 
-#ifdef NIGHTLY_BUILD
 void
 ContentChild::OnChannelReceivedMessage(const Message& aMsg)
 {
+  if (aMsg.is_sync()) {
+    LSObject::CancelSyncLoop();
+  }
+
+#ifdef NIGHTLY_BUILD
   if (nsContentUtils::IsMessageInputEvent(aMsg)) {
     mPendingInputEvents++;
   }
+#endif
 }
 
+#ifdef NIGHTLY_BUILD
 PContentChild::Result
 ContentChild::OnMessageReceived(const Message& aMsg)
 {
   if (nsContentUtils::IsMessageInputEvent(aMsg)) {
     DebugOnly<uint32_t> prevEvts = mPendingInputEvents--;
     MOZ_ASSERT(prevEvts > 0);
   }
 
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -763,20 +763,20 @@ private:
   virtual void ProcessingError(Result aCode, const char* aReason) override;
 
   virtual already_AddRefed<nsIEventTarget>
   GetConstructedEventTarget(const Message& aMsg) override;
 
   virtual already_AddRefed<nsIEventTarget>
   GetSpecificMessageEventTarget(const Message& aMsg) override;
 
-#ifdef NIGHTLY_BUILD
   virtual void
   OnChannelReceivedMessage(const Message& aMsg) override;
 
+#ifdef NIGHTLY_BUILD
   virtual PContentChild::Result
   OnMessageReceived(const Message& aMsg) override;
 
   virtual PContentChild::Result
   OnMessageReceived(const Message& aMsg, Message*& aReply) override;
 #endif
 
   InfallibleTArray<nsAutoPtr<AlertObserver> > mAlertObservers;
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -44,16 +44,17 @@
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/FileCreatorHelper.h"
 #include "mozilla/dom/FileSystemSecurity.h"
 #include "mozilla/dom/IPCBlobUtils.h"
 #include "mozilla/dom/ExternalHelperAppParent.h"
 #include "mozilla/dom/GetFilesHelper.h"
 #include "mozilla/dom/GeolocationBinding.h"
+#include "mozilla/dom/LocalStorageCommon.h"
 #include "mozilla/dom/MemoryReportRequest.h"
 #include "mozilla/dom/Notification.h"
 #include "mozilla/dom/PContentBridgeParent.h"
 #include "mozilla/dom/PContentPermissionRequestParent.h"
 #include "mozilla/dom/PCycleCollectWithLogsParent.h"
 #include "mozilla/dom/PositionError.h"
 #include "mozilla/dom/ServiceWorkerRegistrar.h"
 #include "mozilla/dom/power/PowerManagerService.h"
@@ -136,16 +137,17 @@
 #include "nsIDragService.h"
 #include "mozilla/dom/WakeLock.h"
 #include "nsIDOMWindow.h"
 #include "nsIExternalProtocolService.h"
 #include "nsIFormProcessor.h"
 #include "nsIGfxInfo.h"
 #include "nsIIdleService.h"
 #include "nsIInterfaceRequestorUtils.h"
+#include "nsILocalStorageManager.h"
 #include "nsIMemoryInfoDumper.h"
 #include "nsIMemoryReporter.h"
 #include "nsIMozBrowserFrame.h"
 #include "nsIMutable.h"
 #include "nsIObserverService.h"
 #include "nsIParentChannel.h"
 #include "nsIPresShell.h"
 #include "nsIRemoteWindowContext.h"
@@ -5702,16 +5704,34 @@ ContentParent::AboutToLoadHttpFtpWyciwyg
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsLoadFlags newLoadFlags;
   aChannel->GetLoadFlags(&newLoadFlags);
   if (newLoadFlags & nsIRequest::LOAD_DOCUMENT_NEEDS_COOKIE) {
     UpdateCookieStatus(aChannel);
   }
 
+  if (!NextGenLocalStorageEnabled()) {
+    return NS_OK;
+  }
+
+  if (principal->GetIsCodebasePrincipal()) {
+    nsCOMPtr<nsILocalStorageManager> lsm =
+      do_GetService("@mozilla.org/dom/localStorage-manager;1");
+    if (NS_WARN_IF(!lsm)) {
+      return NS_ERROR_FAILURE;
+    }
+
+    nsCOMPtr<nsISupports> dummy;
+    rv = lsm->Preload(principal, nullptr, getter_AddRefs(dummy));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
   return NS_OK;
 }
 
 nsresult
 ContentParent::TransmitPermissionsForPrincipal(nsIPrincipal* aPrincipal)
 {
   // Create the key, and send it down to the content process.
   nsTArray<nsCString> keys =
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/ActorsChild.cpp
@@ -0,0 +1,336 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ActorsChild.h"
+
+#include "LocalStorageCommon.h"
+#include "LSDatabase.h"
+#include "LSObject.h"
+#include "LSObserver.h"
+#include "LSSnapshot.h"
+
+namespace mozilla {
+namespace dom {
+
+/*******************************************************************************
+ * LSDatabaseChild
+ ******************************************************************************/
+
+LSDatabaseChild::LSDatabaseChild(LSDatabase* aDatabase)
+  : mDatabase(aDatabase)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aDatabase);
+
+  MOZ_COUNT_CTOR(LSDatabaseChild);
+}
+
+LSDatabaseChild::~LSDatabaseChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSDatabaseChild);
+}
+
+void
+LSDatabaseChild::SendDeleteMeInternal()
+{
+  AssertIsOnOwningThread();
+
+  if (mDatabase) {
+    mDatabase->ClearActor();
+    mDatabase = nullptr;
+
+    MOZ_ALWAYS_TRUE(PBackgroundLSDatabaseChild::SendDeleteMe());
+  }
+}
+
+void
+LSDatabaseChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+
+  if (mDatabase) {
+    mDatabase->ClearActor();
+#ifdef DEBUG
+    mDatabase = nullptr;
+#endif
+  }
+}
+
+mozilla::ipc::IPCResult
+LSDatabaseChild::RecvRequestAllowToClose()
+{
+  AssertIsOnOwningThread();
+
+  if (mDatabase) {
+    mDatabase->RequestAllowToClose();
+
+    // TODO: A new datastore will be prepared at first LocalStorage API
+    //       synchronous call. It would be better to start preparing a new
+    //       datastore right here, but asynchronously.
+    //       However, we probably shouldn't do that if we are shutting down.
+  }
+
+  return IPC_OK();
+}
+
+PBackgroundLSSnapshotChild*
+LSDatabaseChild::AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI,
+                                                 const bool& aIncreasePeakUsage,
+                                                 const int64_t& aRequestedSize,
+                                                 const int64_t& aMinSize,
+                                                 LSSnapshotInitInfo* aInitInfo)
+{
+  MOZ_CRASH("PBackgroundLSSnapshotChild actor should be manually constructed!");
+}
+
+bool
+LSDatabaseChild::DeallocPBackgroundLSSnapshotChild(
+                                             PBackgroundLSSnapshotChild* aActor)
+{
+  MOZ_ASSERT(aActor);
+
+  delete aActor;
+  return true;
+}
+
+/*******************************************************************************
+ * LSObserverChild
+ ******************************************************************************/
+
+LSObserverChild::LSObserverChild(LSObserver* aObserver)
+  : mObserver(aObserver)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aObserver);
+
+  MOZ_COUNT_CTOR(LSObserverChild);
+}
+
+LSObserverChild::~LSObserverChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSObserverChild);
+}
+
+void
+LSObserverChild::SendDeleteMeInternal()
+{
+  AssertIsOnOwningThread();
+
+  if (mObserver) {
+    mObserver->ClearActor();
+    mObserver = nullptr;
+
+    MOZ_ALWAYS_TRUE(PBackgroundLSObserverChild::SendDeleteMe());
+  }
+}
+
+void
+LSObserverChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+
+  if (mObserver) {
+    mObserver->ClearActor();
+#ifdef DEBUG
+    mObserver = nullptr;
+#endif
+  }
+}
+
+mozilla::ipc::IPCResult
+LSObserverChild::RecvObserve(const PrincipalInfo& aPrincipalInfo,
+                             const uint32_t& aPrivateBrowsingId,
+                             const nsString& aDocumentURI,
+                             const nsString& aKey,
+                             const nsString& aOldValue,
+                             const nsString& aNewValue)
+{
+  AssertIsOnOwningThread();
+
+  if (!mObserver) {
+    return IPC_OK();
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsIPrincipal> principal =
+    PrincipalInfoToPrincipal(aPrincipalInfo, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  Storage::NotifyChange(/* aStorage */ nullptr,
+                        principal,
+                        aKey,
+                        aOldValue,
+                        aNewValue,
+                        /* aStorageType */ kLocalStorageType,
+                        aDocumentURI,
+                        /* aIsPrivate */ !!aPrivateBrowsingId,
+                        /* aImmediateDispatch */ true);
+
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * LocalStorageRequestChild
+ ******************************************************************************/
+
+LSRequestChild::LSRequestChild(LSRequestChildCallback* aCallback)
+  : mCallback(aCallback)
+  , mFinishing(false)
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_CTOR(LSRequestChild);
+}
+
+LSRequestChild::~LSRequestChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSRequestChild);
+}
+
+bool
+LSRequestChild::Finishing() const
+{
+  AssertIsOnOwningThread();
+
+  return mFinishing;
+}
+
+void
+LSRequestChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+}
+
+mozilla::ipc::IPCResult
+LSRequestChild::Recv__delete__(const LSRequestResponse& aResponse)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mCallback);
+
+  mCallback->OnResponse(aResponse);
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+LSRequestChild::RecvReady()
+{
+  AssertIsOnOwningThread();
+
+  mFinishing = true;
+
+  SendFinish();
+
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSSimpleRequestChild
+ ******************************************************************************/
+
+LSSimpleRequestChild::LSSimpleRequestChild(
+                                        LSSimpleRequestChildCallback* aCallback)
+  : mCallback(aCallback)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aCallback);
+
+  MOZ_COUNT_CTOR(LSSimpleRequestChild);
+}
+
+LSSimpleRequestChild::~LSSimpleRequestChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSSimpleRequestChild);
+}
+
+void
+LSSimpleRequestChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+}
+
+mozilla::ipc::IPCResult
+LSSimpleRequestChild::Recv__delete__(const LSSimpleRequestResponse& aResponse)
+{
+  AssertIsOnOwningThread();
+
+  mCallback->OnResponse(aResponse);
+
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSSnapshotChild
+ ******************************************************************************/
+
+LSSnapshotChild::LSSnapshotChild(LSSnapshot* aSnapshot)
+  : mSnapshot(aSnapshot)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aSnapshot);
+
+  MOZ_COUNT_CTOR(LSSnapshotChild);
+}
+
+LSSnapshotChild::~LSSnapshotChild()
+{
+  AssertIsOnOwningThread();
+
+  MOZ_COUNT_DTOR(LSSnapshotChild);
+}
+
+void
+LSSnapshotChild::SendDeleteMeInternal()
+{
+  AssertIsOnOwningThread();
+
+  if (mSnapshot) {
+    mSnapshot->ClearActor();
+    mSnapshot = nullptr;
+
+    MOZ_ALWAYS_TRUE(PBackgroundLSSnapshotChild::SendDeleteMe());
+  }
+}
+
+void
+LSSnapshotChild::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+
+  if (mSnapshot) {
+    mSnapshot->ClearActor();
+#ifdef DEBUG
+    mSnapshot = nullptr;
+#endif
+  }
+}
+
+mozilla::ipc::IPCResult
+LSSnapshotChild::RecvMarkDirty()
+{
+  AssertIsOnOwningThread();
+
+  if (!mSnapshot) {
+    return IPC_OK();
+  }
+
+  mSnapshot->MarkDirty();
+
+  return IPC_OK();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/ActorsChild.h
@@ -0,0 +1,313 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_localstorage_ActorsChild_h
+#define mozilla_dom_localstorage_ActorsChild_h
+
+#include "mozilla/dom/PBackgroundLSDatabaseChild.h"
+#include "mozilla/dom/PBackgroundLSObserverChild.h"
+#include "mozilla/dom/PBackgroundLSRequestChild.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequestChild.h"
+#include "mozilla/dom/PBackgroundLSSnapshotChild.h"
+
+namespace mozilla {
+
+namespace ipc {
+
+class BackgroundChildImpl;
+
+} // namespace ipc
+
+namespace dom {
+
+class LocalStorageManager2;
+class LSDatabase;
+class LSObject;
+class LSObserver;
+class LSRequestChildCallback;
+class LSSimpleRequestChildCallback;
+class LSSnapshot;
+
+/**
+ * Minimal glue actor with standard IPC-managed new/delete existence that exists
+ * primarily to track the continued existence of the LSDatabase in the child.
+ * Most of the interesting bits happen via PBackgroundLSSnapshot.
+ *
+ * Mutual raw pointers are maintained between LSDatabase and this class that are
+ * cleared at either (expected) when the child starts the deletion process
+ * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy).
+ *
+ * See `PBackgroundLSDatabase.ipdl` for more information.
+ *
+ *
+ * ## Low-Level Lifecycle ##
+ * - Created by LSObject::EnsureDatabase if it had to create a database.
+ * - Deletion begun by LSDatabase's destructor invoking SendDeleteMeInternal
+ *   which will result in the parent sending __delete__ which destroys the
+ *   actor.
+ */
+class LSDatabaseChild final
+  : public PBackgroundLSDatabaseChild
+{
+  friend class mozilla::ipc::BackgroundChildImpl;
+  friend class LSDatabase;
+  friend class LSObject;
+
+  LSDatabase* mDatabase;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSDatabaseChild);
+  }
+
+private:
+  // Only created by LSObject.
+  explicit LSDatabaseChild(LSDatabase* aDatabase);
+
+  // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+  ~LSDatabaseChild();
+
+  void
+  SendDeleteMeInternal();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvRequestAllowToClose() override;
+
+  PBackgroundLSSnapshotChild*
+  AllocPBackgroundLSSnapshotChild(const nsString& aDocumentURI,
+                                  const bool& aIncreasePeakUsage,
+                                  const int64_t& aRequestedSize,
+                                  const int64_t& aMinSize,
+                                  LSSnapshotInitInfo* aInitInfo) override;
+
+  bool
+  DeallocPBackgroundLSSnapshotChild(PBackgroundLSSnapshotChild* aActor)
+                                    override;
+};
+
+/**
+ * Minimal IPC-managed (new/delete) actor that exists to receive and relay
+ * "storage" events from changes to LocalStorage that take place in other
+ * processes as their Snapshots are checkpointed to the canonical Datastore in
+ * the parent process.
+ *
+ * See `PBackgroundLSObserver.ipdl` for more info.
+ */
+class LSObserverChild final
+  : public PBackgroundLSObserverChild
+{
+  friend class mozilla::ipc::BackgroundChildImpl;
+  friend class LSObserver;
+  friend class LSObject;
+
+  LSObserver* mObserver;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSObserverChild);
+  }
+
+private:
+  // Only created by LSObject.
+  explicit LSObserverChild(LSObserver* aObserver);
+
+  // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+  ~LSObserverChild();
+
+  void
+  SendDeleteMeInternal();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvObserve(const PrincipalInfo& aPrinciplaInfo,
+              const uint32_t& aPrivateBrowsingId,
+              const nsString& aDocumentURI,
+              const nsString& aKey,
+              const nsString& aOldValue,
+              const nsString& aNewValue) override;
+};
+
+/**
+ * Minimal glue IPC-managed (new/delete) actor that is used by LSObject and its
+ * RequestHelper to perform synchronous requests on top of an asynchronous
+ * protocol.
+ *
+ * Takes an `LSReuestChildCallback` to be invoked when a response is received
+ * via __delete__.
+ *
+ * See `PBackgroundLSRequest.ipdl`, `LSObject`, and `RequestHelper` for more
+ * info.
+ */
+class LSRequestChild final
+  : public PBackgroundLSRequestChild
+{
+  friend class LSObject;
+  friend class LocalStorageManager2;
+
+  RefPtr<LSRequestChildCallback> mCallback;
+
+  bool mFinishing;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSReqeustChild);
+  }
+
+  bool
+  Finishing() const;
+
+private:
+  // Only created by LSObject.
+  explicit LSRequestChild(LSRequestChildCallback* aCallback);
+
+  // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+  ~LSRequestChild();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  Recv__delete__(const LSRequestResponse& aResponse) override;
+
+  mozilla::ipc::IPCResult
+  RecvReady() override;
+};
+
+class NS_NO_VTABLE LSRequestChildCallback
+{
+public:
+  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+  virtual void
+  OnResponse(const LSRequestResponse& aResponse) = 0;
+
+protected:
+  virtual ~LSRequestChildCallback()
+  { }
+};
+
+/**
+ * Minimal glue IPC-managed (new/delete) actor used by `LocalStorageManager2` to
+ * issue asynchronous requests in an asynchronous fashion.
+ *
+ * Takes an `LSSimpleRequestChildCallback` to be invoked when a response is
+ * received via __delete__.
+ *
+ * See `PBackgroundLSSimpleRequest.ipdl` for more info.
+ */
+class LSSimpleRequestChild final
+  : public PBackgroundLSSimpleRequestChild
+{
+  friend class LocalStorageManager2;
+
+  RefPtr<LSSimpleRequestChildCallback> mCallback;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSSimpleReqeustChild);
+  }
+
+private:
+  // Only created by LocalStorageManager2.
+  explicit LSSimpleRequestChild(LSSimpleRequestChildCallback* aCallback);
+
+  // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+  ~LSSimpleRequestChild();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  Recv__delete__(const LSSimpleRequestResponse& aResponse) override;
+};
+
+class NS_NO_VTABLE LSSimpleRequestChildCallback
+{
+public:
+  NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+  virtual void
+  OnResponse(const LSSimpleRequestResponse& aResponse) = 0;
+
+protected:
+  virtual ~LSSimpleRequestChildCallback()
+  { }
+};
+
+/**
+ * Minimal IPC-managed (new/delete) actor that lasts as long as its owning
+ * LSSnapshot.
+ *
+ * Mutual raw pointers are maintained between LSSnapshot and this class that are
+ * cleared at either (expected) when the child starts the deletion process
+ * (SendDeleteMeInternal) or unexpected actor death (ActorDestroy).
+ *
+ * See `PBackgroundLSSnapshot.ipdl` and `LSSnapshot` for more info.
+ */
+class LSSnapshotChild final
+  : public PBackgroundLSSnapshotChild
+{
+  friend class LSDatabase;
+  friend class LSSnapshot;
+
+  LSSnapshot* mSnapshot;
+
+  NS_DECL_OWNINGTHREAD
+
+public:
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(LSSnapshotChild);
+  }
+
+private:
+  // Only created by LSDatabase.
+  explicit LSSnapshotChild(LSSnapshot* aSnapshot);
+
+  // Only destroyed by LSDatabaseChild.
+  ~LSSnapshotChild();
+
+  void
+  SendDeleteMeInternal();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvMarkDirty() override;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_localstorage_ActorsChild_h
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/ActorsParent.cpp
@@ -0,0 +1,8539 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ActorsParent.h"
+
+#include "LocalStorageCommon.h"
+#include "LSObject.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageFunction.h"
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/PBackgroundLSDatabaseParent.h"
+#include "mozilla/dom/PBackgroundLSObserverParent.h"
+#include "mozilla/dom/PBackgroundLSRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSnapshotParent.h"
+#include "mozilla/dom/StorageDBUpdater.h"
+#include "mozilla/dom/StorageUtils.h"
+#include "mozilla/dom/quota/OriginScope.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundParent.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsClassHashtable.h"
+#include "nsDataHashtable.h"
+#include "nsInterfaceHashtable.h"
+#include "nsISimpleEnumerator.h"
+#include "nsNetUtil.h"
+#include "nsRefPtrHashtable.h"
+#include "ReportInternalError.h"
+
+#define DISABLE_ASSERTS_FOR_FUZZING 0
+
+#if DISABLE_ASSERTS_FOR_FUZZING
+#define ASSERT_UNLESS_FUZZING(...) do { } while (0)
+#else
+#define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__)
+#endif
+
+#if defined(MOZ_WIDGET_ANDROID)
+#define LS_MOBILE
+#endif
+
+namespace mozilla {
+namespace dom {
+
+using namespace mozilla::dom::quota;
+using namespace mozilla::dom::StorageUtils;
+using namespace mozilla::ipc;
+
+namespace {
+
+struct ArchivedOriginInfo;
+class ArchivedOriginScope;
+class Connection;
+class ConnectionThread;
+class Database;
+class PrepareDatastoreOp;
+class PreparedDatastore;
+class QuotaClient;
+class Snapshot;
+
+typedef nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>
+  ArchivedOriginHashtable;
+
+/*******************************************************************************
+ * Constants
+ ******************************************************************************/
+
+// Major schema version. Bump for almost everything.
+const uint32_t kMajorSchemaVersion = 1;
+
+// Minor schema version. Should almost always be 0 (maybe bump on release
+// branches if we have to).
+const uint32_t kMinorSchemaVersion = 0;
+
+// The schema version we store in the SQLite database is a (signed) 32-bit
+// integer. The major version is left-shifted 4 bits so the max value is
+// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
+static_assert(kMajorSchemaVersion <= 0xFFFFFFF,
+              "Major version needs to fit in 28 bits.");
+static_assert(kMinorSchemaVersion <= 0xF,
+              "Minor version needs to fit in 4 bits.");
+
+const int32_t kSQLiteSchemaVersion =
+  int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion);
+
+// Changing the value here will override the page size of new databases only.
+// A journal mode change and VACUUM are needed to change existing databases, so
+// the best way to do that is to use the schema version upgrade mechanism.
+const uint32_t kSQLitePageSizeOverride =
+#ifdef LS_MOBILE
+  512;
+#else
+  1024;
+#endif
+
+static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 ||
+              (kSQLitePageSizeOverride % 2 == 0 &&
+               kSQLitePageSizeOverride >= 512  &&
+               kSQLitePageSizeOverride <= 65536),
+              "Must be 0 (disabled) or a power of 2 between 512 and 65536!");
+
+// Set to some multiple of the page size to grow the database in larger chunks.
+const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;
+
+static_assert(kSQLiteGrowthIncrement >= 0 &&
+              kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
+              kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
+              "Must be 0 (disabled) or a positive multiple of the page size!");
+
+/**
+ * The database name for LocalStorage data in a per-origin directory.
+ */
+#define DATA_FILE_NAME "data.sqlite"
+/**
+ * The journal corresponding to DATA_FILE_NAME.  (We don't use WAL mode.)
+ */
+#define JOURNAL_FILE_NAME "data.sqlite-journal"
+
+/**
+ * How long between the first moment we know we have data to be written on a
+ * `Connection` and when we should actually perform the write.  This helps
+ * limit disk churn under silly usage patterns and is historically consistent
+ * with the previous, legacy implementation.
+ *
+ * Note that flushing happens downstream of Snapshot checkpointing and its
+ * batch mechanism which helps avoid wasteful IPC in the case of silly content
+ * code.
+ */
+const uint32_t kFlushTimeoutMs = 5000;
+
+const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";
+
+const uint32_t kDefaultOriginLimitKB = 5 * 1024;
+const uint32_t kDefaultShadowWrites = true;
+const uint32_t kDefaultSnapshotPrefill = 4096;
+/**
+ * LocalStorage data limit as determined by summing up the lengths of all string
+ * keys and values.  This is consistent with the legacy implementation and other
+ * browser engines.  This value should really only ever change in unit testing
+ * where being able to lower it makes it easier for us to test certain edge
+ * cases.
+ */
+const char kDefaultQuotaPref[] = "dom.storage.default_quota";
+/**
+ * Should all mutations also be reflected in the "shadow" database, which is
+ * the legacy webappsstore.sqlite database.  When this is enabled, users can
+ * downgrade their version of Firefox and/or otherwise fall back to the legacy
+ * implementation without loss of data.  (Older versions of Firefox will
+ * recognize the presence of ls-archive.sqlite and purge it and the other
+ * LocalStorage directories so privacy is maintained.)
+ */
+const char kShadowWritesPref[] = "dom.storage.shadow_writes";
+/**
+ * Byte budget for sending data down to the LSSnapshot instance when it is first
+ * created.  If there is less data than this (measured by tallying the string
+ * length of the keys and values), all data is sent, otherwise partial data is
+ * sent.  See `Snapshot`.
+ */
+const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill";
+
+/**
+ * The amount of time a PreparedDatastore instance should stick around after a
+ * preload is triggered in order to give time for the page to use LocalStorage
+ * without triggering worst-case synchronous jank.
+ */
+const uint32_t kPreparedDatastoreTimeoutMs = 20000;
+
+/**
+ * Cold storage for LocalStorage data extracted from webappsstore.sqlite at
+ * LSNG first-run that has not yet been migrated to its own per-origin directory
+ * by use.
+ *
+ * In other words, at first run, LSNG copies the contents of webappsstore.sqlite
+ * into this database.  As requests are made for that LocalStorage data, the
+ * contents are removed from this database and placed into per-origin QM
+ * storage.  So the contents of this database are always old, unused
+ * LocalStorage data that we can potentially get rid of at some point in the
+ * future.
+ */
+#define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite"
+/**
+ * The legacy LocalStorage database.  Its contents are maintained as our
+ * "shadow" database so that LSNG can be disabled without loss of user data.
+ */
+#define WEB_APPS_STORE_FILE_NAME "webappsstore.sqlite"
+
+// Shadow database Write Ahead Log's maximum size is 512KB
+const uint32_t kShadowMaxWALSize = 512 * 1024;
+
+const uint32_t kShadowJournalSizeLimit = kShadowMaxWALSize * 3;
+
+bool
+IsOnConnectionThread();
+
+void
+AssertIsOnConnectionThread();
+
+/*******************************************************************************
+ * SQLite functions
+ ******************************************************************************/
+
+#if 0
+int32_t
+MakeSchemaVersion(uint32_t aMajorSchemaVersion,
+                  uint32_t aMinorSchemaVersion)
+{
+  return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion);
+}
+#endif
+
+nsCString
+GetArchivedOriginHashKey(const nsACString& aOriginSuffix,
+                         const nsACString& aOriginNoSuffix)
+{
+  return aOriginSuffix + NS_LITERAL_CSTRING(":") + aOriginNoSuffix;
+}
+
+nsresult
+CreateTables(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  // Table `database`
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "CREATE TABLE database"
+      "( origin TEXT NOT NULL"
+      ", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
+      ", last_analyze_time INTEGER NOT NULL DEFAULT 0"
+      ", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
+      ");"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Table `data`
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "CREATE TABLE data"
+      "( key TEXT PRIMARY KEY"
+      ", value TEXT NOT NULL"
+      ", compressed INTEGER NOT NULL DEFAULT 0"
+      ", lastAccessTime INTEGER NOT NULL DEFAULT 0"
+      ");"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = aConnection->SetSchemaVersion(kSQLiteSchemaVersion);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+#if 0
+nsresult
+UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+#endif
+
+nsresult
+SetDefaultPragmas(mozIStorageConnection* aConnection)
+{
+  MOZ_ASSERT(!NS_IsMainThread());
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "PRAGMA synchronous = FULL;"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+#ifndef LS_MOBILE
+  if (kSQLiteGrowthIncrement) {
+    // This is just an optimization so ignore the failure if the disk is
+    // currently too full.
+    rv = aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement,
+                                         EmptyCString());
+    if (rv != NS_ERROR_FILE_TOO_BIG && NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+#endif // LS_MOBILE
+
+  return NS_OK;
+}
+
+nsresult
+CreateStorageConnection(nsIFile* aDBFile,
+                        const nsACString& aOrigin,
+                        mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aDBFile);
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv;
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenDatabase(aDBFile, getter_AddRefs(connection));
+  if (rv == NS_ERROR_FILE_CORRUPTED) {
+    // Nuke the database file.
+    rv = aDBFile->Remove(false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = ss->OpenDatabase(aDBFile, getter_AddRefs(connection));
+  }
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = SetDefaultPragmas(connection);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Check to make sure that the database schema is correct.
+  int32_t schemaVersion;
+  rv = connection->GetSchemaVersion(&schemaVersion);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (schemaVersion > kSQLiteSchemaVersion) {
+    LS_WARNING("Unable to open LocalStorage database, schema is too high!");
+    return NS_ERROR_FAILURE;
+  }
+
+  if (schemaVersion != kSQLiteSchemaVersion) {
+    const bool newDatabase = !schemaVersion;
+
+    if (newDatabase) {
+      // Set the page size first.
+      if (kSQLitePageSizeOverride) {
+        rv = connection->ExecuteSimpleSQL(
+          nsPrintfCString("PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)
+        );
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+      }
+
+      // We have to set the auto_vacuum mode before opening a transaction.
+      rv = connection->ExecuteSimpleSQL(
+#ifdef LS_MOBILE
+        // Turn on full auto_vacuum mode to reclaim disk space on mobile
+        // devices (at the cost of some COMMIT speed).
+        NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;")
+#else
+        // Turn on incremental auto_vacuum mode on desktop builds.
+        NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;")
+#endif
+      );
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+
+    mozStorageTransaction transaction(connection, false,
+                                  mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+    if (newDatabase) {
+      rv = CreateTables(connection);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion)));
+      MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+
+      nsCOMPtr<mozIStorageStatement> stmt;
+      nsresult rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+        "INSERT INTO database (origin) "
+        "VALUES (:origin)"
+      ), getter_AddRefs(stmt));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      rv = stmt->Execute();
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    } else  {
+      // This logic needs to change next time we change the schema!
+      static_assert(kSQLiteSchemaVersion == int32_t((1 << 4) + 0),
+                    "Upgrade function needed due to schema version increase.");
+
+      while (schemaVersion != kSQLiteSchemaVersion) {
+#if 0
+        if (schemaVersion == MakeSchemaVersion(1, 0)) {
+          rv = UpgradeSchemaFrom1_0To2_0(connection);
+        } else {
+#endif
+          LS_WARNING("Unable to open LocalStorage database, no upgrade path is "
+                     "available!");
+          return NS_ERROR_FAILURE;
+#if 0
+        }
+#endif
+
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+
+        rv = connection->GetSchemaVersion(&schemaVersion);
+        if (NS_WARN_IF(NS_FAILED(rv))) {
+          return rv;
+        }
+      }
+
+      MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+    }
+
+    rv = transaction.Commit();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    if (newDatabase) {
+      // Windows caches the file size, let's force it to stat the file again.
+      bool dummy;
+      rv = aDBFile->Exists(&dummy);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      int64_t fileSize;
+      rv = aDBFile->GetFileSize(&fileSize);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      MOZ_ASSERT(fileSize > 0);
+
+      PRTime vacuumTime = PR_Now();
+      MOZ_ASSERT(vacuumTime);
+
+      nsCOMPtr<mozIStorageStatement> vacuumTimeStmt;
+      rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+        "UPDATE database "
+          "SET last_vacuum_time = :time"
+            ", last_vacuum_size = :size;"
+      ), getter_AddRefs(vacuumTimeStmt));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("time"),
+                                           vacuumTime);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("size"),
+                                           fileSize);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      rv = vacuumTimeStmt->Execute();
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+GetStorageConnection(const nsAString& aDatabaseFilePath,
+                     mozIStorageConnection** aConnection)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
+  MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, NS_LITERAL_STRING(".sqlite")));
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> databaseFile;
+  nsresult rv = NS_NewLocalFile(aDatabaseFilePath, false,
+                                getter_AddRefs(databaseFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool exists;
+  rv = databaseFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!exists)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenDatabase(databaseFile, getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = SetDefaultPragmas(connection);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+GetArchiveFile(const nsAString& aStoragePath,
+               nsIFile** aArchiveFile)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aArchiveFile);
+
+  nsCOMPtr<nsIFile> archiveFile;
+  nsresult rv = NS_NewLocalFile(aStoragePath,
+                                false,
+                                getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = archiveFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  archiveFile.forget(aArchiveFile);
+  return NS_OK;
+}
+
+nsresult
+CreateArchiveStorageConnection(const nsAString& aStoragePath,
+                               mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> archiveFile;
+  nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // QuotaManager ensures this file always exists.
+  DebugOnly<bool> exists;
+  MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists)));
+  MOZ_ASSERT(exists);
+
+  bool isDirectory;
+  rv = archiveFile->IsDirectory(&isDirectory);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (isDirectory) {
+    LS_WARNING("ls-archive is not a file!");
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenUnsharedDatabase(archiveFile, getter_AddRefs(connection));
+  if (rv == NS_ERROR_FILE_CORRUPTED) {
+    // Don't throw an error, leave a corrupted ls-archive database as it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = StorageDBUpdater::Update(connection);
+  if (NS_FAILED(rv)) {
+    // Don't throw an error, leave a non-updateable ls-archive database as
+    // it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+AttachArchiveDatabase(const nsAString& aStoragePath,
+                      mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+  nsCOMPtr<nsIFile> archiveFile;
+
+  nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+#ifdef DEBUG
+  bool exists;
+  rv = archiveFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(exists);
+#endif
+
+  nsString path;
+  rv = archiveFile->GetPath(path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = aConnection->CreateStatement(
+    NS_LITERAL_CSTRING("ATTACH DATABASE :path AS archive;"),
+    getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+DetachArchiveDatabase(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DETACH DATABASE archive"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+GetShadowFile(const nsAString& aBasePath,
+              nsIFile** aArchiveFile)
+{
+  MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+  MOZ_ASSERT(!aBasePath.IsEmpty());
+  MOZ_ASSERT(aArchiveFile);
+
+  nsCOMPtr<nsIFile> archiveFile;
+  nsresult rv = NS_NewLocalFile(aBasePath,
+                                false,
+                                getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = archiveFile->Append(NS_LITERAL_STRING(WEB_APPS_STORE_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  archiveFile.forget(aArchiveFile);
+  return NS_OK;
+}
+
+nsresult
+SetShadowJournalMode(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  // Try enabling WAL mode. This can fail in various circumstances so we have to
+  // check the results here.
+  NS_NAMED_LITERAL_CSTRING(journalModeQueryStart, "PRAGMA journal_mode = ");
+  NS_NAMED_LITERAL_CSTRING(journalModeWAL, "wal");
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv =
+    aConnection->CreateStatement(journalModeQueryStart + journalModeWAL,
+                                 getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool hasResult;
+  rv = stmt->ExecuteStep(&hasResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(hasResult);
+
+  nsCString journalMode;
+  rv = stmt->GetUTF8String(0, journalMode);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (journalMode.Equals(journalModeWAL)) {
+    // WAL mode successfully enabled. Set limits on its size here.
+
+    // Set the threshold for auto-checkpointing the WAL. We don't want giant
+    // logs slowing down us.
+    rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+      "PRAGMA page_size;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    bool hasResult;
+    rv = stmt->ExecuteStep(&hasResult);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    MOZ_ASSERT(hasResult);
+
+    int32_t pageSize;
+    rv = stmt->GetInt32(0, &pageSize);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);
+
+    nsAutoCString pageCount;
+    pageCount.AppendInt(static_cast<int32_t>(kShadowMaxWALSize / pageSize));
+
+    rv = aConnection->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = ") + pageCount);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    // Set the maximum WAL log size to reduce footprint on mobile (large empty
+    // WAL files will be truncated)
+    nsAutoCString sizeLimit;
+    sizeLimit.AppendInt(kShadowJournalSizeLimit);
+
+    rv = aConnection->ExecuteSimpleSQL(
+      NS_LITERAL_CSTRING("PRAGMA journal_size_limit = ") + sizeLimit);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  } else {
+    rv = aConnection->ExecuteSimpleSQL(journalModeQueryStart +
+                                       NS_LITERAL_CSTRING("truncate"));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+CreateShadowStorageConnection(const nsAString& aBasePath,
+                              mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aBasePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> shadowFile;
+  nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection));
+  if (rv == NS_ERROR_FILE_CORRUPTED) {
+    rv = shadowFile->Remove(false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection));
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = SetShadowJournalMode(connection);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = StorageDBUpdater::Update(connection);
+  if (NS_FAILED(rv)) {
+    rv = connection->Close();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = shadowFile->Remove(false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = SetShadowJournalMode(connection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = StorageDBUpdater::Update(connection);
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+GetShadowStorageConnection(const nsAString& aBasePath,
+                           mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aBasePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> shadowFile;
+  nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool exists;
+  rv = shadowFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!exists)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenUnsharedDatabase(shadowFile, getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+AttachShadowDatabase(const nsAString& aBasePath,
+                     mozIStorageConnection* aConnection)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(!aBasePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> shadowFile;
+  nsresult rv = GetShadowFile(aBasePath, getter_AddRefs(shadowFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+#ifdef DEBUG
+  bool exists;
+  rv = shadowFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(exists);
+#endif
+
+  nsString path;
+  rv = shadowFile->GetPath(path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = aConnection->CreateStatement(
+    NS_LITERAL_CSTRING("ATTACH DATABASE :path AS shadow;"),
+    getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+DetachShadowDatabase(mozIStorageConnection* aConnection)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DETACH DATABASE shadow"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+/*******************************************************************************
+ * Non-actor class declarations
+ ******************************************************************************/
+
+/**
+ * Coalescing manipulation queue used by `Connection` and `DataStore`.  Used by
+ * `Connection` to buffer and coalesce manipulations applied to the Datastore
+ * in batches by Snapshot Checkpointing until flushed to disk.  Used by
+ * `Datastore` to update `DataStore::mOrderedItems` efficiently/for code
+ * simplification.  (DataStore does not actually depend on the coalescing, as
+ * mutations are applied atomically when a Snapshot Checkpoints, and with
+ * `Datastore::mValues` being updated at the same time the mutations are applied
+ * to Datastore's mWriteOptimizer.)
+ */
+class WriteOptimizer final
+{
+  class WriteInfo;
+  class AddItemInfo;
+  class UpdateItemInfo;
+  class RemoveItemInfo;
+  class ClearInfo;
+
+  nsAutoPtr<WriteInfo> mClearInfo;
+  nsClassHashtable<nsStringHashKey, WriteInfo> mWriteInfos;
+
+public:
+  WriteOptimizer()
+  { }
+
+  WriteOptimizer(WriteOptimizer&& aWriteOptimizer)
+    : mClearInfo(std::move(aWriteOptimizer.mClearInfo))
+  {
+    AssertIsOnBackgroundThread();
+    MOZ_ASSERT(&aWriteOptimizer != this);
+
+    mWriteInfos.SwapElements(aWriteOptimizer.mWriteInfos);
+  }
+
+  void
+  AddItem(const nsString& aKey,
+          const nsString& aValue);
+
+  void
+  UpdateItem(const nsString& aKey,
+             const nsString& aValue);
+
+  void
+  RemoveItem(const nsString& aKey);
+
+  void
+  Clear();
+
+  bool
+  HasWrites() const
+  {
+    AssertIsOnBackgroundThread();
+
+    return mClearInfo || !mWriteInfos.IsEmpty();
+  }
+
+  void
+  ApplyWrites(nsTArray<LSItemInfo>& aOrderedItems);
+
+  nsresult
+  PerformWrites(Connection* aConnection, bool aShadowWrites);
+};
+
+/**
+ * Base class for specific mutations.  Each subclass knows how to `Perform` the
+ * manipulation against a `Connection` and the "shadow" database (legacy
+ * webappsstore.sqlite database that exists so LSNG can be disabled/safely
+ * downgraded from.)
+ */
+class WriteOptimizer::WriteInfo
+{
+public:
+  enum Type {
+    AddItem = 0,
+    UpdateItem,
+    RemoveItem,
+    Clear
+  };
+
+  virtual Type
+  GetType() = 0;
+
+  virtual nsresult
+  Perform(Connection* aConnection, bool aShadowWrites) = 0;
+
+  virtual ~WriteInfo() = default;
+};
+
+/**
+ * SetItem mutation where the key did not previously exist.
+ */
+class WriteOptimizer::AddItemInfo
+  : public WriteInfo
+{
+  nsString mKey;
+  nsString mValue;
+
+public:
+  AddItemInfo(const nsAString& aKey,
+              const nsAString& aValue)
+    : mKey(aKey)
+    , mValue(aValue)
+  { }
+
+  const nsAString&
+  GetKey() const
+  {
+    return mKey;
+  }
+
+  const nsAString&
+  GetValue() const
+  {
+    return mValue;
+  }
+
+private:
+  Type
+  GetType() override
+  {
+    return AddItem;
+  }
+
+  nsresult
+  Perform(Connection* aConnection, bool aShadowWrites) override;
+};
+
+/**
+ * SetItem mutation where the key already existed.
+ */
+class WriteOptimizer::UpdateItemInfo final
+  : public AddItemInfo
+{
+public:
+  UpdateItemInfo(const nsAString& aKey,
+                 const nsAString& aValue)
+    : AddItemInfo(aKey, aValue)
+  { }
+
+private:
+  Type
+  GetType() override
+  {
+    return UpdateItem;
+  }
+};
+
+class WriteOptimizer::RemoveItemInfo final
+  : public WriteInfo
+{
+  nsString mKey;
+
+public:
+  explicit RemoveItemInfo(const nsAString& aKey)
+    : mKey(aKey)
+  { }
+
+  const nsAString&
+  GetKey() const
+  {
+    return mKey;
+  }
+
+private:
+  Type
+  GetType() override
+  {
+    return RemoveItem;
+  }
+
+  nsresult
+  Perform(Connection* aConnection, bool aShadowWrites) override;
+};
+
+/**
+ * Clear mutation.
+ */
+class WriteOptimizer::ClearInfo final
+  : public WriteInfo
+{
+public:
+  ClearInfo()
+  { }
+
+private:
+  Type
+  GetType() override
+  {
+    return Clear;
+  }
+
+  nsresult
+  Perform(Connection* aConnection, bool aShadowWrites) override;
+};
+
+class DatastoreOperationBase
+  : public Runnable
+{
+  nsCOMPtr<nsIEventTarget> mOwningEventTarget;
+  nsresult mResultCode;
+  Atomic<bool> mMayProceedOnNonOwningThread;
+  bool mMayProceed;
+
+public:
+  nsIEventTarget*
+  OwningEventTarget() const
+  {
+    MOZ_ASSERT(mOwningEventTarget);
+
+    return mOwningEventTarget;
+  }
+
+  bool
+  IsOnOwningThread() const
+  {
+    MOZ_ASSERT(mOwningEventTarget);
+
+    bool current;
+    return
+      NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) && current;
+  }
+
+  void
+  AssertIsOnOwningThread() const
+  {
+    MOZ_ASSERT(IsOnBackgroundThread());
+    MOZ_ASSERT(IsOnOwningThread());
+  }
+
+  nsresult
+  ResultCode() const
+  {
+    return mResultCode;
+  }
+
+  void
+  SetFailureCode(nsresult aErrorCode)
+  {
+    MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
+    MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+    mResultCode = aErrorCode;
+  }
+
+  void
+  MaybeSetFailureCode(nsresult aErrorCode)
+  {
+    MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+    if (NS_SUCCEEDED(mResultCode)) {
+      mResultCode = aErrorCode;
+    }
+  }
+
+  void
+  NoteComplete()
+  {
+    AssertIsOnOwningThread();
+
+    mMayProceed = false;
+    mMayProceedOnNonOwningThread = false;
+  }
+
+  bool
+  MayProceed() const
+  {
+    AssertIsOnOwningThread();
+
+    return mMayProceed;
+  }
+
+  // May be called on any thread, but you should call MayProceed() if you know
+  // you're on the background thread because it is slightly faster.
+  bool
+  MayProceedOnNonOwningThread() const
+  {
+    return mMayProceedOnNonOwningThread;
+  }
+
+protected:
+  DatastoreOperationBase()
+    : Runnable("dom::DatastoreOperationBase")
+    , mOwningEventTarget(GetCurrentThreadEventTarget())
+    , mResultCode(NS_OK)
+    , mMayProceedOnNonOwningThread(true)
+    , mMayProceed(true)
+  { }
+
+  ~DatastoreOperationBase() override
+  {
+    MOZ_ASSERT(!mMayProceed);
+  }
+};
+
+class ConnectionDatastoreOperationBase
+  : public DatastoreOperationBase
+{
+protected:
+  RefPtr<Connection> mConnection;
+
+public:
+  // This callback will be called on the background thread before releasing the
+  // final reference to this request object. Subclasses may perform any
+  // additional cleanup here but must always call the base class implementation.
+  virtual void
+  Cleanup();
+
+protected:
+  ConnectionDatastoreOperationBase(Connection* aConnection);
+
+  ~ConnectionDatastoreOperationBase();
+
+  // Must be overridden in subclasses. Called on the target thread to allow the
+  // subclass to perform necessary datastore operations. A successful return
+  // value will trigger an OnSuccess callback on the background thread while
+  // while a failure value will trigger an OnFailure callback.
+  virtual nsresult
+  DoDatastoreWork() = 0;
+
+  // Methods that subclasses may implement.
+  virtual void
+  OnSuccess();
+
+  virtual void
+  OnFailure(nsresult aResultCode);
+
+private:
+  void
+  RunOnConnectionThread();
+
+  void
+  RunOnOwningThread();
+
+  // Not to be overridden by subclasses.
+  NS_DECL_NSIRUNNABLE
+};
+
+class Connection final
+{
+  friend class ConnectionThread;
+
+public:
+  class CachedStatement;
+
+private:
+  class FlushOp;
+  class CloseOp;
+
+  RefPtr<ConnectionThread> mConnectionThread;
+  nsCOMPtr<nsITimer> mFlushTimer;
+  nsCOMPtr<mozIStorageConnection> mStorageConnection;
+  nsAutoPtr<ArchivedOriginScope> mArchivedOriginScope;
+  nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
+    mCachedStatements;
+  WriteOptimizer mWriteOptimizer;
+  const nsCString mOrigin;
+  const nsString mFilePath;
+  bool mFlushScheduled;
+#ifdef DEBUG
+  bool mInUpdateBatch;
+#endif
+
+public:
+  NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection)
+
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(Connection);
+  }
+
+  ArchivedOriginScope*
+  GetArchivedOriginScope() const
+  {
+    return mArchivedOriginScope;
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Methods which can only be called on the owning thread.
+
+  // This method is used to asynchronously execute a connection datastore
+  // operation on the connection thread.
+  void
+  Dispatch(ConnectionDatastoreOperationBase* aOp);
+
+  // This method is used to asynchronously close the storage connection on the
+  // connection thread.
+  void
+  Close(nsIRunnable* aCallback);
+
+  void
+  AddItem(const nsString& aKey,
+          const nsString& aValue);
+
+  void
+  UpdateItem(const nsString& aKey,
+             const nsString& aValue);
+
+  void
+  RemoveItem(const nsString& aKey);
+
+  void
+  Clear();
+
+  void
+  BeginUpdateBatch();
+
+  void
+  EndUpdateBatch();
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Methods which can only be called on the connection thread.
+
+  nsresult
+  EnsureStorageConnection();
+
+  mozIStorageConnection*
+  StorageConnection() const
+  {
+    AssertIsOnConnectionThread();
+    MOZ_ASSERT(mStorageConnection);
+
+    return mStorageConnection;
+  }
+
+  void
+  CloseStorageConnection();
+
+  nsresult
+  GetCachedStatement(const nsACString& aQuery,
+                     CachedStatement* aCachedStatement);
+
+private:
+  // Only created by ConnectionThread.
+  Connection(ConnectionThread* aConnectionThread,
+             const nsACString& aOrigin,
+             const nsAString& aFilePath,
+             nsAutoPtr<ArchivedOriginScope>&& aArchivedOriginScope);
+
+  ~Connection();
+
+  void
+  ScheduleFlush();
+
+  void
+  Flush();
+
+  static void
+  FlushTimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+class Connection::CachedStatement final
+{
+  friend class Connection;
+
+  nsCOMPtr<mozIStorageStatement> mStatement;
+  Maybe<mozStorageStatementScoper> mScoper;
+
+public:
+  CachedStatement();
+  ~CachedStatement();
+
+  operator mozIStorageStatement*() const;
+
+  mozIStorageStatement*
+  operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN;
+
+private:
+  // Only called by Connection.
+  void
+  Assign(Connection* aConnection,
+         already_AddRefed<mozIStorageStatement> aStatement);
+
+  // No funny business allowed.
+  CachedStatement(const CachedStatement&) = delete;
+  CachedStatement& operator=(const CachedStatement&) = delete;
+};
+
+class Connection::FlushOp final
+  : public ConnectionDatastoreOperationBase
+{
+  RefPtr<QuotaClient> mQuotaClient;
+  WriteOptimizer mWriteOptimizer;
+  bool mShadowWrites;
+
+public:
+  FlushOp(Connection* aConnection,
+          WriteOptimizer&& aWriteOptimizer);
+
+private:
+  nsresult
+  DoDatastoreWork() override;
+};
+
+class Connection::CloseOp final
+  : public ConnectionDatastoreOperationBase
+{
+  nsCOMPtr<nsIRunnable> mCallback;
+
+public:
+  CloseOp(Connection* aConnection,
+          nsIRunnable* aCallback)
+    : ConnectionDatastoreOperationBase(aConnection)
+    , mCallback(aCallback)
+  { }
+
+private:
+  nsresult
+  DoDatastoreWork() override;
+
+  void
+  Cleanup() override;
+};
+
+class ConnectionThread final
+{
+  friend class Connection;
+
+  nsCOMPtr<nsIThread> mThread;
+  nsRefPtrHashtable<nsCStringHashKey, Connection> mConnections;
+
+public:
+  ConnectionThread();
+
+  void
+  AssertIsOnOwningThread() const
+  {
+    NS_ASSERT_OWNINGTHREAD(ConnectionThread);
+  }
+
+  bool
+  IsOnConnectionThread();
+
+  void
+  AssertIsOnConnectionThread();
+
+  already_AddRefed<Connection>
+  CreateConnection(const nsACString& aOrigin,
+                   const nsAString& aFilePath,
+                   nsAutoPtr<ArchivedOriginScope>&& aArchivedOriginScope);
+
+  void
+  Shutdown();
+
+  NS_INLINE_DECL_REFCOUNTING(ConnectionThread)
+
+private:
+  ~ConnectionThread();
+};
+
+/**
+ * Canonical state of Storage for an origin, containing all keys and their
+ * values in the parent process.  Specifically, this is the state that will
+ * be handed out to freshly created Snapshots and that will be persisted to disk
+ * when the Connection's flush completes.  State is mutated in batches as
+ * Snapshot instances Checkpoint their mutations locally accumulated in the
+ * child LSSnapshots.
+ */
+class Datastore final
+{
+  RefPtr<DirectoryLock> mDirectoryLock;
+  RefPtr<Connection> mConnection;
+  RefPtr<QuotaObject> mQuotaObject;
+  nsCOMPtr<nsIRunnable> mCompleteCallback;
+  /**
+   * PrepareDatastoreOps register themselves with the Datastore at
+   * and unregister in PrepareDatastoreOp::Cleanup.
+   */
+  nsTHashtable<nsPtrHashKey<PrepareDatastoreOp>> mPrepareDatastoreOps;
+  /**
+   * PreparedDatastore instances register themselves with their associated
+   * Datastore at construction time and unregister at destruction time.  They
+   * hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore
+   * from closing itself via MaybeClose(), thereby giving the document enough
+   * time to load and access LocalStorage.
+   */
+  nsTHashtable<nsPtrHashKey<PreparedDatastore>> mPreparedDatastores;
+  /**
+   * A database is live (and in this hashtable) if it has a live LSDatabase
+   * actor.  There is at most one Database per origin per content process.  Each
+   * Database corresponds to an LSDatabase in its associated content process.
+   */
+  nsTHashtable<nsPtrHashKey<Database>> mDatabases;
+  /**
+   * A database is active if it has a non-null `mSnapshot`.  As long as there
+   * are any active databases final deltas can't be calculated and
+   * `UpdateUsage()` can't be invoked.
+   */
+  nsTHashtable<nsPtrHashKey<Database>> mActiveDatabases;
+  /**
+   * Non-authoritative hashtable representation of mOrderedItems for efficient
+   * lookup.
+   */
+  nsDataHashtable<nsStringHashKey, nsString> mValues;
+  /**
+   * The authoritative ordered state of the Datastore; mValue also exists as an
+   * unordered hashtable for efficient lookup.
+   */
+  nsTArray<LSItemInfo> mOrderedItems;
+  nsTArray<int64_t> mPendingUsageDeltas;
+  WriteOptimizer mWriteOptimizer;
+  const nsCString mOrigin;
+  const uint32_t mPrivateBrowsingId;
+  int64_t mUsage;
+  int64_t mUpdateBatchUsage;
+  int64_t mSizeOfKeys;
+  int64_t mSizeOfItems;
+  bool mClosed;
+#ifdef DEBUG
+  bool mInUpdateBatch;
+#endif
+
+public:
+  // Created by PrepareDatastoreOp.
+  Datastore(const nsACString& aOrigin,
+            uint32_t aPrivateBrowsingId,
+            int64_t aUsage,
+            int64_t aSizeOfKeys,
+            int64_t aSizeOfItems,
+            already_AddRefed<DirectoryLock>&& aDirectoryLock,
+            already_AddRefed<Connection>&& aConnection,
+            already_AddRefed<QuotaObject>&& aQuotaObject,
+            nsDataHashtable<nsStringHashKey, nsString>& aValues,
+            nsTArray<LSItemInfo>& aOrderedItems);
+
+  const nsCString&
+  Origin() const
+  {
+    return mOrigin;
+  }
+
+  uint32_t
+  PrivateBrowsingId() const
+  {
+    return mPrivateBrowsingId;
+  }
+
+  bool
+  IsPersistent() const
+  {
+    // Private-browsing is forbidden from touching disk, but
+    // StorageAccess::eSessionScoped is allowed to touch disk because
+    // QuotaManager's storage for such origins is wiped at shutdown.
+    return mPrivateBrowsingId == 0;
+  }
+
+  void
+  Close();
+
+  bool
+  IsClosed() const
+  {
+    AssertIsOnBackgroundThread();
+
+    return mClosed;
+  }
+
+  void
+  WaitForConnectionToComplete(nsIRunnable* aCallback);
+
+  void
+  NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+  void
+  NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+  void
+  NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+  void
+  NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+#ifdef DEBUG
+  bool
+  HasLivePreparedDatastores() const;
+#endif
+
+  void
+  NoteLiveDatabase(Database* aDatabase);
+
+  void
+  NoteFinishedDatabase(Database* aDatabase);
+
+#ifdef DEBUG
+  bool
+  HasLiveDatabases() const;
+#endif
+
+  void
+  NoteActiveDatabase(Database* aDatabase);
+
+  void
+  NoteInactiveDatabase(Database* aDatabase);
+
+  void
+  GetSnapshotInitInfo(nsTHashtable<nsStringHashKey>& aLoadedItems,
+                      nsTArray<LSItemInfo>& aItemInfos,
+                      uint32_t& aTotalLength,
+                      int64_t& aInitialUsage,
+                      int64_t& aPeakUsage,
+                      LSSnapshot::LoadState& aLoadState);
+
+  void
+  GetItem(const nsString& aKey, nsString& aValue) const;
+
+  void
+  GetKeys(nsTArray<nsString>& aKeys) const;
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Mutation Methods
+  //
+  // These are only called during Snapshot::RecvCheckpoint
+
+  /**
+   * Used by Snapshot::RecvCheckpoint to set a key/value pair as part of a an
+   * explicit batch.
+   */
+  void
+  SetItem(Database* aDatabase,
+          const nsString& aDocumentURI,
+          const nsString& aKey,
+          const nsString& aOldValue,
+          const nsString& aValue);
+
+  void
+  RemoveItem(Database* aDatabase,
+             const nsString& aDocumentURI,
+             const nsString& aKey,
+             const nsString& aOldValue);
+
+  void
+  Clear(Database* aDatabase,
+        const nsString& aDocumentURI);
+
+  void
+  PrivateBrowsingClear();
+
+  void
+  BeginUpdateBatch(int64_t aSnapshotInitialUsage);
+
+  int64_t
+  EndUpdateBatch(int64_t aSnapshotPeakUsage);
+
+  int64_t
+  RequestUpdateUsage(int64_t aRequestedSize,
+                     int64_t aMinSize);
+
+  NS_INLINE_DECL_REFCOUNTING(Datastore)
+
+private:
+  // Reference counted.
+  ~Datastore();
+
+  bool
+  UpdateUsage(int64_t aDelta);
+
+  void
+  MaybeClose();
+
+  void
+  ConnectionClosedCallback();
+
+  void
+  CleanupMetadata();
+
+  void
+  NotifySnapshots(Database* aDatabase,
+                  const nsAString& aKey,
+                  const nsAString& aOldValue,
+                  bool aAffectsOrder);
+
+  void
+  MarkSnapshotsDirty();
+
+  void
+  NotifyObservers(Database* aDatabase,
+                  const nsString& aDocumentURI,
+                  const nsString& aKey,
+                  const nsString& aOldValue,
+                  const nsString& aNewValue);
+};
+
+class PreparedDatastore
+{
+  RefPtr<Datastore> mDatastore;
+  nsCOMPtr<nsITimer> mTimer;
+  const Maybe<ContentParentId> mContentParentId;
+  // Strings share buffers if possible, so it's not a problem to duplicate the
+  // origin here.
+  const nsCString mOrigin;
+  uint64_t mDatastoreId;
+  bool mForPreload;
+  bool mInvalidated;
+
+public:
+  PreparedDatastore(Datastore* aDatastore,
+                    const Maybe<ContentParentId>& aContentParentId,
+                    const nsACString& aOrigin,
+                    uint64_t aDatastoreId,
+                    bool aForPreload)
+    : mDatastore(aDatastore)
+    , mTimer(NS_NewTimer())
+    , mContentParentId(aContentParentId)
+    , mOrigin(aOrigin)
+    , mDatastoreId(aDatastoreId)
+    , mForPreload(aForPreload)
+    , mInvalidated(false)
+  {
+    AssertIsOnBackgroundThread();
+    MOZ_ASSERT(aDatastore);
+    MOZ_ASSERT(mTimer);
+
+    aDatastore->NoteLivePreparedDatastore(this);
+
+    MOZ_ALWAYS_SUCCEEDS(
+      mTimer->InitWithNamedFuncCallback(TimerCallback,
+                                        this,
+                                        kPreparedDatastoreTimeoutMs,
+                                        nsITimer::TYPE_ONE_SHOT,
+                                        "PreparedDatastore::TimerCallback"));
+  }
+
+  ~PreparedDatastore()
+  {
+    MOZ_ASSERT(mDatastore);
+    MOZ_ASSERT(mTimer);
+
+    mTimer->Cancel();
+
+    mDatastore->NoteFinishedPreparedDatastore(this);
+  }
+
+  Datastore*
+  GetDatastore() const
+  {
+    AssertIsOnBackgroundThread();
+    MOZ_ASSERT(mDatastore);
+
+    return mDatastore;
+  }
+
+  const Maybe<ContentParentId>&
+  GetContentParentId() const
+  {
+    return mContentParentId;
+  }
+
+  const nsCString&
+  Origin() const
+  {
+    return mOrigin;
+  }
+
+  void
+  Invalidate()
+  {
+    AssertIsOnBackgroundThread();
+
+    mInvalidated = true;
+
+    if (mForPreload) {
+      mTimer->Cancel();
+
+      MOZ_ALWAYS_SUCCEEDS(
+        mTimer->InitWithNamedFuncCallback(TimerCallback,
+                                          this,
+                                          0,
+                                          nsITimer::TYPE_ONE_SHOT,
+                                          "PreparedDatastore::TimerCallback"));
+    }
+  }
+
+  bool
+  IsInvalidated() const
+  {
+    AssertIsOnBackgroundThread();
+
+    return mInvalidated;
+  }
+
+private:
+  void
+  Destroy();
+
+  static void
+  TimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+/*******************************************************************************
+ * Actor class declarations
+ ******************************************************************************/
+
+class Database final
+  : public PBackgroundLSDatabaseParent
+{
+  RefPtr<Datastore> mDatastore;
+  Snapshot* mSnapshot;
+  const PrincipalInfo mPrincipalInfo;
+  const Maybe<ContentParentId> mContentParentId;
+  // Strings share buffers if possible, so it's not a problem to duplicate the
+  // origin here.
+  nsCString mOrigin;
+  uint32_t mPrivateBrowsingId;
+  bool mAllowedToClose;
+  bool mActorDestroyed;
+  bool mRequestedAllowToClose;
+#ifdef DEBUG
+  bool mActorWasAlive;
+#endif
+
+public:
+  // Created in AllocPBackgroundLSDatabaseParent.
+  Database(const PrincipalInfo& aPrincipalInfo,
+           const Maybe<ContentParentId>& aContentParentId,
+           const nsACString& aOrigin,
+           uint32_t aPrivateBrowsingId);
+
+  Datastore*
+  GetDatastore() const
+  {
+    AssertIsOnBackgroundThread();
+    return mDatastore;
+  }
+
+  const PrincipalInfo&
+  GetPrincipalInfo() const
+  {
+    return mPrincipalInfo;
+  }
+
+  bool
+  IsOwnedByProcess(ContentParentId aContentParentId) const
+  {
+    return mContentParentId && mContentParentId.value() == aContentParentId;
+  }
+
+  uint32_t
+  PrivateBrowsingId() const
+  {
+    return mPrivateBrowsingId;
+  }
+
+  const nsCString&
+  Origin() const
+  {
+    return mOrigin;
+  }
+
+  void
+  SetActorAlive(Datastore* aDatastore);
+
+  void
+  RegisterSnapshot(Snapshot* aSnapshot);
+
+  void
+  UnregisterSnapshot(Snapshot* aSnapshot);
+
+  Snapshot*
+  GetSnapshot() const
+  {
+    AssertIsOnBackgroundThread();
+    return mSnapshot;
+  }
+
+  void
+  RequestAllowToClose();
+
+  NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database)
+
+private:
+  // Reference counted.
+  ~Database();
+
+  void
+  AllowToClose();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvDeleteMe() override;
+
+  mozilla::ipc::IPCResult
+  RecvAllowToClose() override;
+
+  PBackgroundLSSnapshotParent*
+  AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI,
+                                   const bool& aIncreasePeakUsage,
+                                   const int64_t& aRequestedSize,
+                                   const int64_t& aMinSize,
+                                   LSSnapshotInitInfo* aInitInfo) override;
+
+  mozilla::ipc::IPCResult
+  RecvPBackgroundLSSnapshotConstructor(PBackgroundLSSnapshotParent* aActor,
+                                       const nsString& aDocumentURI,
+                                       const bool& aIncreasePeakUsage,
+                                       const int64_t& aRequestedSize,
+                                       const int64_t& aMinSize,
+                                       LSSnapshotInitInfo* aInitInfo) override;
+
+  bool
+  DeallocPBackgroundLSSnapshotParent(PBackgroundLSSnapshotParent* aActor)
+                                     override;
+};
+
+/**
+ * Attempts to capture the state of the underlying Datastore at the time of its
+ * creation so run-to-completion semantics can be honored.
+ *
+ * Rather than simply duplicate the contents of `DataStore::mValues` and
+ * `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks
+ * mutations to the Datastore as they happen, saving off the state of values as
+ * they existed when the Snapshot was created.  In other words, given an initial
+ * Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those
+ * values until it hears via `SaveItem` that "foo" is being over-written.  At
+ * that time, it will save off foo='bar' in mValues.
+ *
+ * ## Quota Allocation ##
+ *
+ * ## States ##
+ *
+ */
+class Snapshot final
+  : public PBackgroundLSSnapshotParent
+{
+  /**
+   * The Database that owns this snapshot.  There is a 1:1 relationship between
+   * snapshots and databases.
+   */
+  RefPtr<Database> mDatabase;
+  RefPtr<Datastore> mDatastore;
+  /**
+   * The set of keys for which values have been sent to the child LSSnapshot.
+   * Cleared once all values have been sent as indicated by
+   * mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be
+   * true.  No requests should be received for keys already in this set, and
+   * this is enforced by fatal IPC error (unless fuzzing).
+   */
+  nsTHashtable<nsStringHashKey> mLoadedItems;
+  /**
+   * The set of keys for which a RecvLoadItem request was received but there
+   * was no such key, and so null was returned.  The child LSSnapshot will also
+   * cache these values, so redundant requests are also handled with fatal
+   * process termination just like for mLoadedItems.  Also cleared when
+   * mLoadedAllItems becomes true because then the child can infer that all
+   * other values must be null.  (Note: this could also be done when
+   * mLoadKeysReceived is true as a further optimization, but is not.)
+   */
+  nsTHashtable<nsStringHashKey> mUnknownItems;
+  /**
+   * Values that have changed in mDatastore as reported by SaveItem
+   * notifications that are not yet known to the child LSSnapshot.
+   *
+   * The naive way to snapshot the state of mDatastore would be to duplicate its
+   * internal mValues at the time of our creation, but that is wasteful if few
+   * changes are made to the Datastore's state.  So we only track values that
+   * are changed/evicted from the Datastore as they happen, as reported to us by
+   * SaveItem notifications.
+   */
+  nsDataHashtable<nsStringHashKey, nsString> mValues;
+  /**
+   * Latched state of mDatastore's keys during a SaveItem notification with
+   * aAffectsOrder=true.  The ordered keys needed to be saved off so that a
+   * consistent ordering could be presented to the child LSSnapshot when it asks
+   * for them via RecvLoadKeys.
+   */
+  nsTArray<nsString> mKeys;
+  nsString mDocumentURI;
+  /**
+   * The number of key/value pairs that were present in the Datastore at the
+   * time the snapshot was created.  Once we have sent this many values to the
+   * child LSSnapshot, we can infer that it has received all of the keys/values
+   * and set mLoadedAllItems to true and clear mLoadedItems and mUnknownItems.
+   * Note that knowing the keys/values is not the same as knowing their ordering
+   * and so mKeys may be retained.
+   */
+  uint32_t mTotalLength;
+  int64_t mUsage;
+  int64_t mPeakUsage;
+  /**
+   * True if SaveItem has saved mDatastore's keys into mKeys because a SaveItem
+   * notification with aAffectsOrder=true was received.
+   */
+  bool mSavedKeys;
+  bool mActorDestroyed;
+  bool mFinishReceived;
+  bool mLoadedReceived;
+  /**
+   * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+   * LoadState::AllUnorderedItems.  It will be AllOrderedItems if the initial
+   * snapshot contained all the data or if the state was AllOrderedKeys and
+   * successive RecvLoadItem requests have resulted in the LSSnapshot being told
+   * all of the key/value pairs.  It will be AllUnorderedItems if the state was
+   * LoadState::Partial and successive RecvLoadItem requests got all the
+   * keys/values but the key ordering was not retrieved.
+   */
+  bool mLoadedAllItems;
+  /**
+   * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+   * AllOrderedKeys.  This can occur because of the initial snapshot, or because
+   * a RecvLoadKeys request was received.
+   */
+  bool mLoadKeysReceived;
+  bool mSentMarkDirty;
+
+public:
+  // Created in AllocPBackgroundLSSnapshotParent.
+  Snapshot(Database* aDatabase,
+           const nsAString& aDocumentURI);
+
+  void
+  Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
+       uint32_t aTotalLength,
+       int64_t aInitialUsage,
+       int64_t aPeakUsage,
+       LSSnapshot::LoadState aLoadState)
+  {
+    AssertIsOnBackgroundThread();
+    MOZ_ASSERT(aInitialUsage >= 0);
+    MOZ_ASSERT(aPeakUsage >= aInitialUsage);
+    MOZ_ASSERT_IF(aLoadState == LSSnapshot::LoadState::AllOrderedItems,
+                  aLoadedItems.Count() == 0);
+    MOZ_ASSERT(mTotalLength == 0);
+    MOZ_ASSERT(mUsage == -1);
+    MOZ_ASSERT(mPeakUsage == -1);
+
+    mLoadedItems.SwapElements(aLoadedItems);
+    mTotalLength = aTotalLength;
+    mUsage = aInitialUsage;
+    mPeakUsage = aPeakUsage;
+    if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) {
+      mLoadKeysReceived = true;
+    } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) {
+      mLoadedReceived = true;
+      mLoadedAllItems = true;
+      mLoadKeysReceived = true;
+    }
+  }
+
+  /**
+   * Called via NotifySnapshots by Datastore whenever it is updating its
+   * internal state so that snapshots can save off the state of a value at the
+   * time of their creation.
+   */
+  void
+  SaveItem(const nsAString& aKey,
+           const nsAString& aOldValue,
+           bool aAffectsOrder);
+
+  void
+  MarkDirty();
+
+  NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
+
+private:
+  // Reference counted.
+  ~Snapshot();
+
+  void
+  Finish();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvDeleteMe() override;
+
+  mozilla::ipc::IPCResult
+  RecvCheckpoint(nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+  mozilla::ipc::IPCResult
+  RecvFinish() override;
+
+  mozilla::ipc::IPCResult
+  RecvLoaded() override;
+
+  mozilla::ipc::IPCResult
+  RecvLoadItem(const nsString& aKey,
+               nsString* aValue) override;
+
+  mozilla::ipc::IPCResult
+  RecvLoadKeys(nsTArray<nsString>* aKeys) override;
+
+  mozilla::ipc::IPCResult
+  RecvIncreasePeakUsage(const int64_t& aRequestedSize,
+                        const int64_t& aMinSize,
+                        int64_t* aSize) override;
+
+  mozilla::ipc::IPCResult
+  RecvPing() override;
+};
+
+class Observer final
+  : public PBackgroundLSObserverParent
+{
+  nsCString mOrigin;
+  bool mActorDestroyed;
+
+public:
+  // Created in AllocPBackgroundLSObserverParent.
+  explicit Observer(const nsACString& aOrigin);
+
+  const nsCString&
+  Origin() const
+  {
+    return mOrigin;
+  }
+
+  void
+  Observe(Database* aDatabase,
+          const nsString& aDocumentURI,
+          const nsString& aKey,
+          const nsString& aOldValue,
+          const nsString& aNewValue);
+
+  NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Observer)
+
+private:
+  // Reference counted.
+  ~Observer();
+
+  // IPDL methods are only called by IPDL.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  mozilla::ipc::IPCResult
+  RecvDeleteMe() override;
+};
+
+class LSRequestBase
+  : public DatastoreOperationBase
+  , public PBackgroundLSRequestParent
+{
+protected:
+  enum class State
+  {
+    // Just created on the PBackground thread. Next step is Opening.
+    Initial,
+
+    // Waiting to open/opening on the main thread. Next step is either
+    // Nesting if a subclass needs to process more nested states or
+    // SendingReadyMessage if a subclass doesn't need any nested processing.
+    Opening,
+
+    // Doing nested processing.
+    Nesting,
+
+    // Waiting to send/sending the ready message on the PBackground thread. Next
+    // step is WaitingForFinish.
+    SendingReadyMessage,
+
+    // Waiting for the finish message on the PBackground thread. Next step is
+    // SendingResults.
+    WaitingForFinish,
+
+    // Waiting to send/sending results on the PBackground thread. Next step is
+    // Completed.
+    SendingResults,
+
+    // All done.
+    Completed
+  };
+
+  nsCOMPtr<nsIEventTarget> mMainEventTarget;
+  State mState;
+
+public:
+  explicit LSRequestBase(nsIEventTarget* aMainEventTarget);
+
+  void
+  Dispatch();
+
+protected:
+  ~LSRequestBase() override;
+
+  virtual nsresult
+  Open() = 0;
+
+  virtual nsresult
+  NestedRun();
+
+  virtual void
+  GetResponse(LSRequestResponse& aResponse) = 0;
+
+  virtual void
+  Cleanup()
+  { }
+
+private:
+  void
+  SendReadyMessage();
+
+  void
+  SendResults();
+
+protected:
+  // Common nsIRunnable implementation that subclasses may not override.
+  NS_IMETHOD
+  Run() final;
+
+  // IPDL methods.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+private:
+  mozilla::ipc::IPCResult
+  RecvCancel() override;
+
+  mozilla::ipc::IPCResult
+  RecvFinish() override;
+};
+
+class PrepareDatastoreOp
+  : public LSRequestBase
+  , public OpenDirectoryListener
+{
+  class LoadDataOp;
+
+  enum class NestedState
+  {
+    // The nesting has not yet taken place. Next step is
+    // CheckExistingOperations.
+    BeforeNesting,
+
+    // Checking if a prepare datastore operation is already running for given
+    // origin on the PBackground thread. Next step is CheckClosingDatastore.
+    CheckExistingOperations,
+
+    // Checking if a datastore is closing the connection for given origin on
+    // the PBackground thread. Next step is PreparationPending.
+    CheckClosingDatastore,
+
+    // Opening directory or initializing quota manager on the PBackground
+    // thread. Next step is either DirectoryOpenPending if quota manager is
+    // already initialized or QuotaManagerPending if quota manager needs to be
+    // initialized.
+    // If a datastore already exists for given origin then the next state is
+    // SendingReadyMessage.
+    PreparationPending,
+
+    // Waiting for quota manager initialization to complete on the PBackground
+    // thread. Next step is either SendingReadyMessage if initialization failed
+    // or DirectoryOpenPending if initialization succeeded.
+    QuotaManagerPending,
+
+    // Waiting for directory open allowed on the PBackground thread. The next
+    // step is either SendingReadyMessage if directory lock failed to acquire,
+    // or DatabaseWorkOpen if directory lock is acquired.
+    DirectoryOpenPending,
+
+    // Waiting to do/doing work on the QuotaManager IO thread. Its next step is
+    // BeginLoadData.
+    DatabaseWorkOpen,
+
+    // Starting a load data operation on the PBackground thread. Next step is
+    // DatabaseWorkLoadData.
+    BeginLoadData,
+
+    // Waiting to do/doing work on the connection thread. This involves waiting
+    // for the LoadDataOp to do its work. Eventually the state will transition
+    // to SendingReadyMessage.
+    DatabaseWorkLoadData,
+
+    // The nesting has completed.
+    AfterNesting
+  };
+
+  nsCOMPtr<nsIEventTarget> mMainEventTarget;
+  RefPtr<ContentParent> mContentParent;
+  RefPtr<PrepareDatastoreOp> mDelayedOp;
+  RefPtr<DirectoryLock> mDirectoryLock;
+  RefPtr<Connection> mConnection;
+  RefPtr<Datastore> mDatastore;
+  nsAutoPtr<ArchivedOriginScope> mArchivedOriginScope;
+  LoadDataOp* mLoadDataOp;
+  nsDataHashtable<nsStringHashKey, nsString> mValues;
+  nsTArray<LSItemInfo> mOrderedItems;
+  const LSRequestPrepareDatastoreParams mParams;
+  Maybe<ContentParentId> mContentParentId;
+  nsCString mSuffix;
+  nsCString mGroup;
+  nsCString mMainThreadOrigin;
+  nsCString mOrigin;
+  nsString mDatabaseFilePath;
+  uint32_t mPrivateBrowsingId;
+  int64_t mUsage;
+  int64_t mSizeOfKeys;
+  int64_t mSizeOfItems;
+  NestedState mNestedState;
+  bool mDatabaseNotAvailable;
+  bool mRequestedDirectoryLock;
+  bool mInvalidated;
+
+#ifdef DEBUG
+  int64_t mDEBUGUsage;
+#endif
+
+public:
+  PrepareDatastoreOp(nsIEventTarget* aMainEventTarget,
+                     already_AddRefed<ContentParent> aContentParent,
+                     const LSRequestParams& aParams);
+
+  bool
+  OriginIsKnown() const
+  {
+    AssertIsOnOwningThread();
+
+    return !mOrigin.IsEmpty();
+  }
+
+  const nsCString&
+  Origin() const
+  {
+    AssertIsOnOwningThread();
+    MOZ_ASSERT(OriginIsKnown());
+
+    return mOrigin;
+  }
+
+  bool
+  RequestedDirectoryLock() const
+  {
+    AssertIsOnOwningThread();
+
+    return mRequestedDirectoryLock;
+  }
+
+  void
+  Invalidate()
+  {
+    AssertIsOnOwningThread();
+
+    mInvalidated = true;
+  }
+
+private:
+  ~PrepareDatastoreOp() override;
+
+  nsresult
+  Open() override;
+
+  nsresult
+  CheckExistingOperations();
+
+  nsresult
+  CheckClosingDatastoreInternal();
+
+  nsresult
+  CheckClosingDatastore();
+
+  nsresult
+  BeginDatastorePreparationInternal();
+
+  nsresult
+  BeginDatastorePreparation();
+
+  nsresult
+  QuotaManagerOpen();
+
+  nsresult
+  OpenDirectory();
+
+  void
+  SendToIOThread();
+
+  nsresult
+  DatabaseWork();
+
+  nsresult
+  DatabaseNotAvailable();
+
+  nsresult
+  EnsureDirectoryEntry(nsIFile* aEntry,
+                       bool aCreateIfNotExists,
+                       bool aDirectory,
+                       bool* aAlreadyExisted = nullptr);
+
+  nsresult
+  VerifyDatabaseInformation(mozIStorageConnection* aConnection);
+
+  already_AddRefed<QuotaObject>
+  GetQuotaObject();
+
+  nsresult
+  BeginLoadData();
+
+  void
+  FinishNesting();
+
+  nsresult
+  FinishNestingOnNonOwningThread();
+
+  nsresult
+  NestedRun() override;
+
+  void
+  GetResponse(LSRequestResponse& aResponse) override;
+
+  void
+  Cleanup() override;
+
+  void
+  ConnectionClosedCallback();
+
+  void
+  CleanupMetadata();
+
+  NS_DECL_ISUPPORTS_INHERITED
+
+  // IPDL overrides.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+
+  // OpenDirectoryListener overrides.
+  void
+  DirectoryLockAcquired(DirectoryLock* aLock) override;
+
+  void
+  DirectoryLockFailed() override;
+};
+
+class PrepareDatastoreOp::LoadDataOp final
+  : public ConnectionDatastoreOperationBase
+{
+  RefPtr<PrepareDatastoreOp> mPrepareDatastoreOp;
+
+public:
+  explicit LoadDataOp(PrepareDatastoreOp* aPrepareDatastoreOp)
+    : ConnectionDatastoreOperationBase(aPrepareDatastoreOp->mConnection)
+    , mPrepareDatastoreOp(aPrepareDatastoreOp)
+  { }
+
+private:
+  ~LoadDataOp() = default;
+
+  nsresult
+  DoDatastoreWork() override;
+
+  void
+  OnSuccess() override;
+
+  void
+  OnFailure(nsresult aResultCode) override;
+
+  void
+  Cleanup() override;
+};
+
+class PrepareObserverOp
+  : public LSRequestBase
+{
+  const LSRequestPrepareObserverParams mParams;
+  nsCString mOrigin;
+
+public:
+  PrepareObserverOp(nsIEventTarget* aMainEventTarget,
+                    const LSRequestParams& aParams);
+
+private:
+  nsresult
+  Open() override;
+
+  void
+  GetResponse(LSRequestResponse& aResponse) override;
+};
+
+class LSSimpleRequestBase
+  : public DatastoreOperationBase
+  , public PBackgroundLSSimpleRequestParent
+{
+protected:
+  enum class State
+  {
+    // Just created on the PBackground thread. Next step is Opening.
+    Initial,
+
+    // Waiting to open/opening on the main thread. Next step is SendingResults.
+    Opening,
+
+    // Waiting to send/sending results on the PBackground thread. Next step is
+    // Completed.
+    SendingResults,
+
+    // All done.
+    Completed
+  };
+
+  State mState;
+
+public:
+  LSSimpleRequestBase();
+
+  void
+  Dispatch();
+
+protected:
+  ~LSSimpleRequestBase() override;
+
+  virtual nsresult
+  Open() = 0;
+
+  virtual void
+  GetResponse(LSSimpleRequestResponse& aResponse) = 0;
+
+private:
+  void
+  SendResults();
+
+  // Common nsIRunnable implementation that subclasses may not override.
+  NS_IMETHOD
+  Run() final;
+
+  // IPDL methods.
+  void
+  ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
+class PreloadedOp
+  : public LSSimpleRequestBase
+{
+  const LSSimpleRequestPreloadedParams mParams;
+  nsCString mOrigin;
+
+public:
+  explicit PreloadedOp(const LSSimpleRequestParams& aParams);
+
+private:
+  nsresult
+  Open() override;
+
+  void
+  GetResponse(LSSimpleRequestResponse& aResponse) override;
+};
+
+/*******************************************************************************
+ * Other class declarations
+ ******************************************************************************/
+
+struct ArchivedOriginInfo
+{
+  OriginAttributes mOriginAttributes;
+  nsCString mOriginNoSuffix;
+
+  ArchivedOriginInfo(const OriginAttributes& aOriginAttributes,
+                     const nsACString& aOriginNoSuffix)
+    : mOriginAttributes(aOriginAttributes)
+    , mOriginNoSuffix(aOriginNoSuffix)
+  { }
+};
+
+class ArchivedOriginScope
+{
+  struct Origin
+  {
+    nsCString mOriginSuffix;
+    nsCString mOriginNoSuffix;
+
+    Origin(const nsACString& aOriginSuffix,
+           const nsACString& aOriginNoSuffix)
+      : mOriginSuffix(aOriginSuffix)
+      , mOriginNoSuffix(aOriginNoSuffix)
+    { }
+
+    const nsACString&
+    OriginSuffix() const
+    {
+      return mOriginSuffix;
+    }
+
+    const nsACString&
+    OriginNoSuffix() const
+    {
+      return mOriginNoSuffix;
+    }
+  };
+
+  struct Prefix
+  {
+    nsCString mOriginNoSuffix;
+
+    explicit Prefix(const nsACString& aOriginNoSuffix)
+      : mOriginNoSuffix(aOriginNoSuffix)
+    { }
+
+    const nsACString&
+    OriginNoSuffix() const
+    {
+      return mOriginNoSuffix;
+    }
+  };
+
+  struct Pattern
+  {
+    UniquePtr<OriginAttributesPattern> mPattern;
+
+    explicit Pattern(const OriginAttributesPattern& aPattern)
+      : mPattern(MakeUnique<OriginAttributesPattern>(aPattern))
+    { }
+
+    Pattern(const Pattern& aOther)
+      : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern))
+    { }
+
+    Pattern(Pattern&& aOther) = default;
+
+    const OriginAttributesPattern&
+    GetPattern() const
+    {
+      MOZ_ASSERT(mPattern);
+      return *mPattern;
+    }
+  };
+
+  struct Null
+  { };
+
+  using DataType = Variant<Origin, Pattern, Prefix, Null>;
+
+  DataType mData;
+
+public:
+  static ArchivedOriginScope*
+  CreateFromOrigin(nsIPrincipal* aPrincipal);
+
+  static ArchivedOriginScope*
+  CreateFromPrefix(nsIPrincipal* aPrincipal);
+
+  static ArchivedOriginScope*
+  CreateFromPattern(const OriginAttributesPattern& aPattern);
+
+  static ArchivedOriginScope*
+  CreateFromNull();
+
+  bool
+  IsOrigin() const
+  {
+    return mData.is<Origin>();
+  }
+
+  bool
+  IsPrefix() const
+  {
+    return mData.is<Prefix>();
+  }
+
+  bool
+  IsPattern() const
+  {
+    return mData.is<Pattern>();
+  }
+
+  bool
+  IsNull() const
+  {
+    return mData.is<Null>();
+  }
+
+  const nsACString&
+  OriginSuffix() const
+  {
+    MOZ_ASSERT(IsOrigin());
+
+    return mData.as<Origin>().OriginSuffix();
+  }
+
+  const nsACString&
+  OriginNoSuffix() const
+  {
+    MOZ_ASSERT(IsOrigin() || IsPrefix());
+
+    if (IsOrigin()) {
+      return mData.as<Origin>().OriginNoSuffix();
+    }
+    return mData.as<Prefix>().OriginNoSuffix();
+  }
+
+  const OriginAttributesPattern&
+  GetPattern() const
+  {
+    MOZ_ASSERT(IsPattern());
+
+    return mData.as<Pattern>().GetPattern();
+  }
+
+  void
+  GetBindingClause(nsACString& aBindingClause) const;
+
+  nsresult
+  BindToStatement(mozIStorageStatement* aStatement) const;
+
+  bool
+  HasMatches(ArchivedOriginHashtable* aHashtable) const;
+
+  void
+  RemoveMatches(ArchivedOriginHashtable* aHashtable) const;
+
+private:
+  // Move constructors
+  explicit ArchivedOriginScope(const Origin&& aOrigin)
+    : mData(aOrigin)
+  { }
+
+  explicit ArchivedOriginScope(const Pattern&& aPattern)
+    : mData(aPattern)
+  { }
+
+  explicit ArchivedOriginScope(const Prefix&& aPrefix)
+    : mData(aPrefix)
+  { }
+
+  explicit ArchivedOriginScope(const Null&& aNull)
+    : mData(aNull)
+  { }
+};
+
+class ArchivedOriginScopeHelper
+  : public Runnable
+{
+  Monitor mMonitor;
+  const OriginAttributes mAttrs;
+  const nsCString mSpec;
+  nsAutoPtr<ArchivedOriginScope> mArchivedOriginScope;
+  nsresult mMainThreadResultCode;
+  bool mWaiting;
+  bool mPrefix;
+
+public:
+  ArchivedOriginScopeHelper(const nsACString& aSpec,
+                            const OriginAttributes& aAttrs,
+                            bool aPrefix)
+    : Runnable("dom::localstorage::ArchivedOriginScopeHelper")
+    , mMonitor("ArchivedOriginScopeHelper::mMonitor")
+    , mAttrs(aAttrs)
+    , mSpec(aSpec)
+    , mMainThreadResultCode(NS_OK)
+    , mWaiting(true)
+    , mPrefix(aPrefix)
+  {
+    AssertIsOnIOThread();
+  }
+
+  nsresult
+  BlockAndReturnArchivedOriginScope(
+                          nsAutoPtr<ArchivedOriginScope>& aArchivedOriginScope);
+
+private:
+  nsresult
+  RunOnMainThread();
+
+  NS_DECL_NSIRUNNABLE
+};
+
+class QuotaClient final
+  : public mozilla::dom::quota::Client
+{
+  class ClearPrivateBrowsingRunnable;
+  class Observer;
+  class MatchFunction;
+
+  static QuotaClient* sInstance;
+  static bool sObserversRegistered;
+
+  Mutex mShadowDatabaseMutex;
+  bool mShutdownRequested;
+
+public:
+  QuotaClient();
+
+  static QuotaClient*
+  GetInstance()
+  {
+    AssertIsOnBackgroundThread();
+
+    return sInstance;
+  }
+
+  static bool
+  IsShuttingDownOnBackgroundThread()
+  {
+    AssertIsOnBackgroundThread();
+
+    if (sInstance) {
+      return sInstance->IsShuttingDown();
+    }
+
+    return QuotaManager::IsShuttingDown();
+  }
+
+  static bool
+  IsShuttingDownOnNonBackgroundThread()
+  {
+    MOZ_ASSERT(!IsOnBackgroundThread());
+
+    return QuotaManager::IsShuttingDown();
+  }
+
+  static nsresult
+  RegisterObservers(nsIEventTarget* aBackgroundEventTarget);
+
+  mozilla::Mutex&
+  ShadowDatabaseMutex()
+  {
+    MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread());
+
+    return mShadowDatabaseMutex;
+  }
+
+  bool
+  IsShuttingDown() const
+  {
+    AssertIsOnBackgroundThread();
+
+    return mShutdownRequested;
+  }
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override)
+
+  Type
+  GetType() override;
+
+  nsresult
+  InitOrigin(PersistenceType aPersistenceType,
+             const nsACString& aGroup,
+             const nsACString& aOrigin,
+             const AtomicBool& aCanceled,
+             UsageInfo* aUsageInfo) override;
+
+  nsresult
+  GetUsageForOrigin(PersistenceType aPersistenceType,
+                    const nsACString& aGroup,
+                    const nsACString& aOrigin,
+                    const AtomicBool& aCanceled,
+                    UsageInfo* aUsageInfo) override;
+
+  nsresult
+  AboutToClearOrigins(const Nullable<PersistenceType>& aPersistenceType,
+                      const OriginScope& aOriginScope) override;
+
+  void
+  OnOriginClearCompleted(PersistenceType aPersistenceType,
+                         const nsACString& aOrigin)
+                         override;
+
+  void
+  ReleaseIOThreadObjects() override;
+
+  void
+  AbortOperations(const nsACString& aOrigin) override;
+
+  void
+  AbortOperationsForProcess(ContentParentId aContentParentId) override;
+
+  void
+  StartIdleMaintenance() override;
+
+  void
+  StopIdleMaintenance() override;
+
+  void
+  ShutdownWorkThreads() override;
+
+private:
+  ~QuotaClient() override;
+
+  nsresult
+  CreateArchivedOriginScope(
+                          const OriginScope& aOriginScope,
+                          nsAutoPtr<ArchivedOriginScope>& aArchivedOriginScope);
+
+  nsresult
+  PerformDelete(mozIStorageConnection* aConnection,
+                const nsACString& aSchemaName,
+                ArchivedOriginScope* aArchivedOriginScope) const;
+};
+
+class QuotaClient::ClearPrivateBrowsingRunnable final
+  : public Runnable
+{
+public:
+  ClearPrivateBrowsingRunnable()
+    : Runnable("mozilla::dom::ClearPrivateBrowsingRunnable")
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+private:
+  ~ClearPrivateBrowsingRunnable() = default;
+
+  NS_DECL_NSIRUNNABLE
+};
+
+class QuotaClient::Observer final
+  : public nsIObserver
+{
+  nsCOMPtr<nsIEventTarget> mBackgroundEventTarget;
+
+public:
+  explicit Observer(nsIEventTarget* aBackgroundEventTarget)
+    : mBackgroundEventTarget(aBackgroundEventTarget)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  NS_DECL_ISUPPORTS
+
+private:
+  ~Observer()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  NS_DECL_NSIOBSERVER
+};
+
+class QuotaClient::MatchFunction final
+  : public mozIStorageFunction
+{
+  OriginAttributesPattern mPattern;
+
+public:
+  explicit MatchFunction(const OriginAttributesPattern& aPattern)
+    : mPattern(aPattern)
+  { }
+
+private:
+  ~MatchFunction() = default;
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_MOZISTORAGEFUNCTION
+};
+
+/*******************************************************************************
+ * Globals
+ ******************************************************************************/
+
+typedef nsTArray<PrepareDatastoreOp*> PrepareDatastoreOpArray;
+
+StaticAutoPtr<PrepareDatastoreOpArray> gPrepareDatastoreOps;
+
+typedef nsDataHashtable<nsCStringHashKey, Datastore*> DatastoreHashtable;
+
+StaticAutoPtr<DatastoreHashtable> gDatastores;
+
+uint64_t gLastDatastoreId = 0;
+
+typedef nsClassHashtable<nsUint64HashKey, PreparedDatastore>
+  PreparedDatastoreHashtable;
+
+StaticAutoPtr<PreparedDatastoreHashtable> gPreparedDatastores;
+
+typedef nsTArray<Database*> LiveDatabaseArray;
+
+StaticAutoPtr<LiveDatabaseArray> gLiveDatabases;
+
+StaticRefPtr<ConnectionThread> gConnectionThread;
+
+uint64_t gLastObserverId = 0;
+
+typedef nsRefPtrHashtable<nsUint64HashKey, Observer> PreparedObserverHashtable;
+
+StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers;
+
+typedef nsClassHashtable<nsCStringHashKey, nsTArray<Observer*>>
+  ObserverHashtable;
+
+StaticAutoPtr<ObserverHashtable> gObservers;
+
+Atomic<uint32_t, Relaxed> gOriginLimitKB(kDefaultOriginLimitKB);
+Atomic<bool> gShadowWrites(kDefaultShadowWrites);
+Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill);
+
+typedef nsDataHashtable<nsCStringHashKey, int64_t> UsageHashtable;
+
+// Can only be touched on the Quota Manager I/O thread.
+StaticAutoPtr<UsageHashtable> gUsages;
+
+StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins;
+
+// Can only be touched on the Quota Manager I/O thread.
+bool gInitializedShadowStorage = false;
+
+bool
+IsOnConnectionThread()
+{
+  MOZ_ASSERT(gConnectionThread);
+  return gConnectionThread->IsOnConnectionThread();
+}
+
+void
+AssertIsOnConnectionThread()
+{
+  MOZ_ASSERT(gConnectionThread);
+  gConnectionThread->AssertIsOnConnectionThread();
+}
+
+void
+InitUsageForOrigin(const nsACString& aOrigin, int64_t aUsage)
+{
+  AssertIsOnIOThread();
+
+  if (!gUsages) {
+    gUsages = new UsageHashtable();
+  }
+
+  MOZ_ASSERT(!gUsages->Contains(aOrigin));
+  gUsages->Put(aOrigin, aUsage);
+}
+
+nsresult
+LoadArchivedOrigins()
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!gArchivedOrigins);
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  // Ensure that the webappsstore.sqlite is moved to new place.
+  nsresult rv = quotaManager->EnsureStorageIsInitialized();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = CreateArchiveStorageConnection(quotaManager->GetStoragePath(),
+                                      getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!connection) {
+    gArchivedOrigins = new ArchivedOriginHashtable();
+    return NS_OK;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT DISTINCT originAttributes, originKey "
+      "FROM webappsstore2;"
+  ), getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsAutoPtr<ArchivedOriginHashtable> archivedOrigins(
+    new ArchivedOriginHashtable());
+
+  bool hasResult;
+  while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult) {
+    nsCString originSuffix;
+    rv = stmt->GetUTF8String(0, originSuffix);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCString originNoSuffix;
+    rv = stmt->GetUTF8String(1, originNoSuffix);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCString hashKey = GetArchivedOriginHashKey(originSuffix, originNoSuffix);
+
+    OriginAttributes originAttributes;
+    if (NS_WARN_IF(!originAttributes.PopulateFromSuffix(originSuffix))) {
+      return NS_ERROR_FAILURE;
+    }
+
+    nsAutoPtr<ArchivedOriginInfo> archivedOriginInfo(
+      new ArchivedOriginInfo(originAttributes, originNoSuffix));
+
+    archivedOrigins->Put(hashKey, archivedOriginInfo.forget());
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  gArchivedOrigins = archivedOrigins.forget();
+  return NS_OK;
+}
+
+nsresult
+GetUsage(mozIStorageConnection* aConnection,
+         ArchivedOriginScope* aArchivedOriginScope,
+         int64_t* aUsage)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+  MOZ_ASSERT(aUsage);
+
+  nsresult rv;
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (aArchivedOriginScope) {
+    rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT sum(length(key) + length(value)) "
+      "FROM webappsstore2 "
+      "WHERE originKey = :originKey "
+      "AND originAttributes = :originAttributes;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = aArchivedOriginScope->BindToStatement(stmt);
+  } else {
+    rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT sum(length(key) + length(value)) "
+      "FROM data"
+    ), getter_AddRefs(stmt));
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool hasResult;
+  rv = stmt->ExecuteStep(&hasResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!hasResult)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  int64_t usage;
+  rv = stmt->GetInt64(0, &usage);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  *aUsage = usage;
+  return NS_OK;
+}
+
+void
+ShadowWritesPrefChangedCallback(const char* aPrefName, void* aClosure)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!strcmp(aPrefName, kShadowWritesPref));
+  MOZ_ASSERT(!aClosure);
+
+  gShadowWrites = Preferences::GetBool(aPrefName, kDefaultShadowWrites);
+}
+
+void
+SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref));
+  MOZ_ASSERT(!aClosure);
+
+  int32_t snapshotPrefill =
+    Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill);
+
+  // The magic -1 is for use only by tests.
+  if (snapshotPrefill == -1) {
+    snapshotPrefill = INT32_MAX;
+  }
+
+  gSnapshotPrefill = snapshotPrefill;
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Exported functions
+ ******************************************************************************/
+
+PBackgroundLSDatabaseParent*
+AllocPBackgroundLSDatabaseParent(const PrincipalInfo& aPrincipalInfo,
+                                 const uint32_t& aPrivateBrowsingId,
+                                 const uint64_t& aDatastoreId)
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+    return nullptr;
+  }
+
+  if (NS_WARN_IF(!gPreparedDatastores)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId);
+  if (NS_WARN_IF(!preparedDatastore)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  // If we ever decide to return null from this point on, we need to make sure
+  // that the datastore is closed and the prepared datastore is removed from the
+  // gPreparedDatastores hashtable.
+  // We also assume that IPDL must call RecvPBackgroundLSDatabaseConstructor
+  // once we return a valid actor in this method.
+
+  RefPtr<Database> database =
+    new Database(aPrincipalInfo,
+                 preparedDatastore->GetContentParentId(),
+                 preparedDatastore->Origin(),
+                 aPrivateBrowsingId);
+
+  // Transfer ownership to IPDL.
+  return database.forget().take();
+}
+
+bool
+RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor,
+                                     const PrincipalInfo& aPrincipalInfo,
+                                     const uint32_t& aPrivateBrowsingId,
+                                     const uint64_t& aDatastoreId)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+  MOZ_ASSERT(gPreparedDatastores);
+  MOZ_ASSERT(gPreparedDatastores->Get(aDatastoreId));
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+  // The actor is now completely built (it has a manager, channel and it's
+  // registered as a subprotocol).
+  // ActorDestroy will be called if we fail here.
+
+  nsAutoPtr<PreparedDatastore> preparedDatastore;
+  gPreparedDatastores->Remove(aDatastoreId, &preparedDatastore);
+  MOZ_ASSERT(preparedDatastore);
+
+  auto* database = static_cast<Database*>(aActor);
+
+  database->SetActorAlive(preparedDatastore->GetDatastore());
+
+  // It's possible that AbortOperations was called before the database actor
+  // was created and became live. Let the child know that the database in no
+  // longer valid.
+  if (preparedDatastore->IsInvalidated()) {
+    database->RequestAllowToClose();
+  }
+
+  return true;
+}
+
+bool
+DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+
+  // Transfer ownership back from IPDL.
+  RefPtr<Database> actor = dont_AddRef(static_cast<Database*>(aActor));
+
+  return true;
+}
+
+PBackgroundLSObserverParent*
+AllocPBackgroundLSObserverParent(const uint64_t& aObserverId)
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+    return nullptr;
+  }
+
+  if (NS_WARN_IF(!gPreparedObsevers)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId);
+  if (NS_WARN_IF(!observer)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  //observer->SetObject(this);
+
+  // Transfer ownership to IPDL.
+  return observer.forget().take();
+}
+
+bool
+RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor,
+                                     const uint64_t& aObserverId)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+  MOZ_ASSERT(gPreparedObsevers);
+  MOZ_ASSERT(gPreparedObsevers->GetWeak(aObserverId));
+
+  RefPtr<Observer> observer;
+  gPreparedObsevers->Remove(aObserverId, observer.StartAssignment());
+  MOZ_ASSERT(observer);
+
+  if (!gPreparedObsevers->Count()) {
+    gPreparedObsevers = nullptr;
+  }
+
+  if (!gObservers) {
+    gObservers = new ObserverHashtable();
+  }
+
+  nsTArray<Observer*>* array;
+  if (!gObservers->Get(observer->Origin(), &array)) {
+    array = new nsTArray<Observer*>();
+    gObservers->Put(observer->Origin(), array);
+  }
+  array->AppendElement(observer);
+
+  return true;
+}
+
+bool
+DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+
+  // Transfer ownership back from IPDL.
+  RefPtr<Observer> actor = dont_AddRef(static_cast<Observer*>(aActor));
+
+  return true;
+}
+
+PBackgroundLSRequestParent*
+AllocPBackgroundLSRequestParent(PBackgroundParent* aBackgroundActor,
+                                const LSRequestParams& aParams)
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+    return nullptr;
+  }
+
+  // If we're in the same process as the actor, we need to get the target event
+  // queue from the current RequestHelper.
+  nsCOMPtr<nsIEventTarget> mainEventTarget;
+  if (!BackgroundParent::IsOtherProcessActor(aBackgroundActor)) {
+    mainEventTarget = LSObject::GetSyncLoopEventTarget();
+  }
+
+  RefPtr<LSRequestBase> actor;
+
+  switch (aParams.type()) {
+    case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+      RefPtr<ContentParent> contentParent =
+        BackgroundParent::GetContentParent(aBackgroundActor);
+
+      RefPtr<PrepareDatastoreOp> prepareDatastoreOp =
+        new PrepareDatastoreOp(mainEventTarget,
+                               contentParent.forget(),
+                               aParams);
+
+      if (!gPrepareDatastoreOps) {
+        gPrepareDatastoreOps = new PrepareDatastoreOpArray();
+      }
+      gPrepareDatastoreOps->AppendElement(prepareDatastoreOp);
+
+      actor = std::move(prepareDatastoreOp);
+
+      break;
+    }
+
+    case LSRequestParams::TLSRequestPrepareObserverParams: {
+      RefPtr<PrepareObserverOp> prepareObserverOp =
+        new PrepareObserverOp(mainEventTarget, aParams);
+
+      actor = std::move(prepareObserverOp);
+
+      break;
+    }
+
+    default:
+      MOZ_CRASH("Should never get here!");
+  }
+
+  // Transfer ownership to IPDL.
+  return actor.forget().take();
+}
+
+bool
+RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor,
+                                    const LSRequestParams& aParams)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+  MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+  // The actor is now completely built.
+
+  auto* op = static_cast<LSRequestBase*>(aActor);
+
+  op->Dispatch();
+
+  return true;
+}
+
+bool
+DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+
+  // Transfer ownership back from IPDL.
+  RefPtr<LSRequestBase> actor =
+    dont_AddRef(static_cast<LSRequestBase*>(aActor));
+
+  return true;
+}
+
+PBackgroundLSSimpleRequestParent*
+AllocPBackgroundLSSimpleRequestParent(const LSSimpleRequestParams& aParams)
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+    return nullptr;
+  }
+
+  RefPtr<LSSimpleRequestBase> actor;
+
+  switch (aParams.type()) {
+    case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
+      RefPtr<PreloadedOp> preloadedOp =
+        new PreloadedOp(aParams);
+
+      actor = std::move(preloadedOp);
+
+      break;
+    }
+
+    default:
+      MOZ_CRASH("Should never get here!");
+  }
+
+  // Transfer ownership to IPDL.
+  return actor.forget().take();
+}
+
+bool
+RecvPBackgroundLSSimpleRequestConstructor(
+                                       PBackgroundLSSimpleRequestParent* aActor,
+                                       const LSSimpleRequestParams& aParams)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+  MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+  // The actor is now completely built.
+
+  auto* op = static_cast<LSSimpleRequestBase*>(aActor);
+
+  op->Dispatch();
+
+  return true;
+}
+
+bool
+DeallocPBackgroundLSSimpleRequestParent(
+                                       PBackgroundLSSimpleRequestParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+
+  // Transfer ownership back from IPDL.
+  RefPtr<LSSimpleRequestBase> actor =
+    dont_AddRef(static_cast<LSSimpleRequestBase*>(aActor));
+
+  return true;
+}
+
+namespace localstorage {
+
+already_AddRefed<mozilla::dom::quota::Client>
+CreateQuotaClient()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+  RefPtr<QuotaClient> client = new QuotaClient();
+  return client.forget();
+}
+
+} // namespace localstorage
+
+/*******************************************************************************
+ * WriteOptimizer
+ ******************************************************************************/
+
+void
+WriteOptimizer::AddItem(const nsString& aKey,
+                        const nsString& aValue)
+{
+  AssertIsOnBackgroundThread();
+
+  WriteInfo* existingWriteInfo;
+  nsAutoPtr<WriteInfo> newWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::RemoveItem) {
+    newWriteInfo = new UpdateItemInfo(aKey, aValue);
+  } else {
+    newWriteInfo = new AddItemInfo(aKey, aValue);
+  }
+  mWriteInfos.Put(aKey, newWriteInfo.forget());
+}
+
+void
+WriteOptimizer::UpdateItem(const nsString& aKey,
+                           const nsString& aValue)
+{
+  AssertIsOnBackgroundThread();
+
+  WriteInfo* existingWriteInfo;
+  nsAutoPtr<WriteInfo> newWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::AddItem) {
+    newWriteInfo = new AddItemInfo(aKey, aValue);
+  } else {
+    newWriteInfo = new UpdateItemInfo(aKey, aValue);
+  }
+  mWriteInfos.Put(aKey, newWriteInfo.forget());
+}
+
+void
+WriteOptimizer::RemoveItem(const nsString& aKey)
+{
+  AssertIsOnBackgroundThread();
+
+  WriteInfo* existingWriteInfo;
+  if (mWriteInfos.Get(aKey, &existingWriteInfo) &&
+      existingWriteInfo->GetType() == WriteInfo::AddItem) {
+    mWriteInfos.Remove(aKey);
+    return;
+  }
+
+  nsAutoPtr<WriteInfo> newWriteInfo(new RemoveItemInfo(aKey));
+  mWriteInfos.Put(aKey, newWriteInfo.forget());
+}
+
+void
+WriteOptimizer::Clear()
+{
+  AssertIsOnBackgroundThread();
+
+  mWriteInfos.Clear();
+
+  if (!mClearInfo) {
+    mClearInfo = new ClearInfo();
+  }
+}
+
+void
+WriteOptimizer::ApplyWrites(nsTArray<LSItemInfo>& aOrderedItems)
+{
+  AssertIsOnBackgroundThread();
+
+  if (mClearInfo) {
+    aOrderedItems.Clear();
+    mClearInfo = nullptr;
+  }
+
+  for (int32_t index = aOrderedItems.Length() - 1;
+       index >= 0;
+       index--) {
+    LSItemInfo& item = aOrderedItems[index];
+
+    if (auto entry = mWriteInfos.Lookup(item.key())) {
+      WriteInfo* writeInfo = entry.Data();
+
+      switch (writeInfo->GetType()) {
+        case WriteInfo::RemoveItem:
+          aOrderedItems.RemoveElementAt(index);
+          entry.Remove();
+          break;
+
+        case WriteInfo::UpdateItem: {
+          auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo);
+          item.value() = updateItemInfo->GetValue();
+          entry.Remove();
+          break;
+        }
+
+        case WriteInfo::AddItem:
+          break;
+
+        default:
+          MOZ_CRASH("Bad type!");
+      }
+    }
+  }
+
+  for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
+    WriteInfo* writeInfo = iter.Data();
+
+    MOZ_ASSERT(writeInfo->GetType() == WriteInfo::AddItem);
+
+    auto addItemInfo = static_cast<AddItemInfo*>(writeInfo);
+
+    LSItemInfo* itemInfo = aOrderedItems.AppendElement();
+    itemInfo->key() = addItemInfo->GetKey();
+    itemInfo->value() = addItemInfo->GetValue();
+  }
+
+  mWriteInfos.Clear();
+}
+
+nsresult
+WriteOptimizer::PerformWrites(Connection* aConnection, bool aShadowWrites)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv;
+
+  if (mClearInfo) {
+    rv = mClearInfo->Perform(aConnection, aShadowWrites);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) {
+    rv = iter.Data()->Perform(aConnection, aShadowWrites);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+WriteOptimizer::
+AddItemInfo::Perform(Connection* aConnection, bool aShadowWrites)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(aConnection);
+
+  Connection::CachedStatement stmt;
+  nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "INSERT OR REPLACE INTO data (key, value) "
+    "VALUES(:key, :value)"),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("value"), mValue);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!aShadowWrites) {
+    return NS_OK;
+  }
+
+  rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "INSERT OR REPLACE INTO shadow.webappsstore2 "
+      "(originAttributes, originKey, scope, key, value) "
+      "VALUES (:originAttributes, :originKey, :scope, :key, :value) "),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  ArchivedOriginScope* archivedOriginScope =
+    aConnection->GetArchivedOriginScope();
+
+  rv = archivedOriginScope->BindToStatement(stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCString scope = Scheme0Scope(archivedOriginScope->OriginSuffix(),
+                                 archivedOriginScope->OriginNoSuffix());
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"),
+                                  scope);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("value"), mValue);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+WriteOptimizer::
+RemoveItemInfo::Perform(Connection* aConnection, bool aShadowWrites)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(aConnection);
+
+  Connection::CachedStatement stmt;
+  nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM data "
+      "WHERE key = :key;"),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!aShadowWrites) {
+    return NS_OK;
+  }
+
+  rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM shadow.webappsstore2 "
+      "WHERE originAttributes = :originAttributes "
+      "AND originKey = :originKey "
+      "AND key = :key;"),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), mKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+WriteOptimizer::
+ClearInfo::Perform(Connection* aConnection, bool aShadowWrites)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(aConnection);
+
+  Connection::CachedStatement stmt;
+  nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM data;"),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!aShadowWrites) {
+    return NS_OK;
+  }
+
+  rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM shadow.webappsstore2 "
+      "WHERE originAttributes = :originAttributes "
+      "AND originKey = :originKey;"),
+    &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+/*******************************************************************************
+ * DatastoreOperationBase
+ ******************************************************************************/
+
+/*******************************************************************************
+ * ConnectionDatastoreOperationBase
+ ******************************************************************************/
+
+ConnectionDatastoreOperationBase::ConnectionDatastoreOperationBase(
+                                                        Connection* aConnection)
+  : mConnection(aConnection)
+{
+  MOZ_ASSERT(aConnection);
+}
+
+ConnectionDatastoreOperationBase::~ConnectionDatastoreOperationBase()
+{
+  MOZ_ASSERT(!mConnection,
+             "ConnectionDatabaseOperationBase::Cleanup() was not called by a "
+             "subclass!");
+}
+
+void
+ConnectionDatastoreOperationBase::Cleanup()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mConnection);
+
+  mConnection = nullptr;
+
+  NoteComplete();
+}
+
+void
+ConnectionDatastoreOperationBase::OnSuccess()
+{
+  AssertIsOnOwningThread();
+}
+
+void
+ConnectionDatastoreOperationBase::OnFailure(nsresult aResultCode)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(NS_FAILED(aResultCode));
+}
+
+void
+ConnectionDatastoreOperationBase::RunOnConnectionThread()
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(mConnection);
+  MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+
+  if (!MayProceedOnNonOwningThread()) {
+    SetFailureCode(NS_ERROR_FAILURE);
+  } else {
+    nsresult rv = mConnection->EnsureStorageConnection();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      SetFailureCode(rv);
+    } else {
+      MOZ_ASSERT(mConnection->StorageConnection());
+
+      rv = DoDatastoreWork();
+      if (NS_FAILED(rv)) {
+        SetFailureCode(rv);
+      }
+    }
+  }
+
+  MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+void
+ConnectionDatastoreOperationBase::RunOnOwningThread()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mConnection);
+
+  if (!MayProceed()) {
+    MaybeSetFailureCode(NS_ERROR_FAILURE);
+  } else {
+    if (NS_SUCCEEDED(ResultCode())) {
+      OnSuccess();
+    } else {
+      OnFailure(ResultCode());
+    }
+  }
+
+  Cleanup();
+}
+
+NS_IMETHODIMP
+ConnectionDatastoreOperationBase::Run()
+{
+  if (IsOnConnectionThread()) {
+    RunOnConnectionThread();
+  } else {
+    RunOnOwningThread();
+  }
+
+  return NS_OK;
+}
+
+/*******************************************************************************
+ * Connection implementation
+ ******************************************************************************/
+
+Connection::Connection(ConnectionThread* aConnectionThread,
+                       const nsACString& aOrigin,
+                       const nsAString& aFilePath,
+                       nsAutoPtr<ArchivedOriginScope>&& aArchivedOriginScope)
+  : mConnectionThread(aConnectionThread)
+  , mArchivedOriginScope(std::move(aArchivedOriginScope))
+  , mOrigin(aOrigin)
+  , mFilePath(aFilePath)
+  , mFlushScheduled(false)
+#ifdef DEBUG
+  , mInUpdateBatch(false)
+#endif
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!aOrigin.IsEmpty());
+  MOZ_ASSERT(!aFilePath.IsEmpty());
+}
+
+Connection::~Connection()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!mStorageConnection);
+  MOZ_ASSERT(!mCachedStatements.Count());
+  MOZ_ASSERT(!mInUpdateBatch);
+  MOZ_ASSERT(!mFlushScheduled);
+}
+
+void
+Connection::Dispatch(ConnectionDatastoreOperationBase* aOp)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mConnectionThread);
+
+  MOZ_ALWAYS_SUCCEEDS(mConnectionThread->mThread->Dispatch(aOp,
+                                                           NS_DISPATCH_NORMAL));
+}
+
+void
+Connection::Close(nsIRunnable* aCallback)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aCallback);
+
+  if (mFlushScheduled) {
+    MOZ_ASSERT(mFlushTimer);
+    MOZ_ALWAYS_SUCCEEDS(mFlushTimer->Cancel());
+
+    Flush();
+
+    mFlushTimer = nullptr;
+  }
+
+  RefPtr<CloseOp> op = new CloseOp(this, aCallback);
+
+  Dispatch(op);
+}
+
+void
+Connection::AddItem(const nsString& aKey,
+                    const nsString& aValue)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mInUpdateBatch);
+
+  mWriteOptimizer.AddItem(aKey, aValue);
+}
+
+void
+Connection::UpdateItem(const nsString& aKey,
+                       const nsString& aValue)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mInUpdateBatch);
+
+  mWriteOptimizer.UpdateItem(aKey, aValue);
+}
+
+void
+Connection::RemoveItem(const nsString& aKey)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mInUpdateBatch);
+
+  mWriteOptimizer.RemoveItem(aKey);
+}
+
+void
+Connection::Clear()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mInUpdateBatch);
+
+  mWriteOptimizer.Clear();
+}
+
+void
+Connection::BeginUpdateBatch()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+  mInUpdateBatch = true;
+#endif
+}
+
+void
+Connection::EndUpdateBatch()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mInUpdateBatch);
+
+  if (mWriteOptimizer.HasWrites() && !mFlushScheduled)  {
+    ScheduleFlush();
+  }
+
+#ifdef DEBUG
+  mInUpdateBatch = false;
+#endif
+}
+
+nsresult
+Connection::EnsureStorageConnection()
+{
+  AssertIsOnConnectionThread();
+
+  if (!mStorageConnection) {
+    nsCOMPtr<mozIStorageConnection> storageConnection;
+    nsresult rv =
+      GetStorageConnection(mFilePath,
+                           getter_AddRefs(storageConnection));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    mStorageConnection = storageConnection;
+  }
+
+  return NS_OK;
+}
+
+void
+Connection::CloseStorageConnection()
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(mStorageConnection);
+
+  mCachedStatements.Clear();
+
+  MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close());
+  mStorageConnection = nullptr;
+}
+
+nsresult
+Connection::GetCachedStatement(const nsACString& aQuery,
+                                       CachedStatement* aCachedStatement)
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(!aQuery.IsEmpty());
+  MOZ_ASSERT(aCachedStatement);
+  MOZ_ASSERT(mStorageConnection);
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+
+  if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) {
+    nsresult rv =
+      mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt));
+    if (NS_FAILED(rv)) {
+#ifdef DEBUG
+      nsCString msg;
+      MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg));
+
+      nsAutoCString error =
+        NS_LITERAL_CSTRING("The statement '") + aQuery +
+        NS_LITERAL_CSTRING("' failed to compile with the error message '") +
+        msg + NS_LITERAL_CSTRING("'.");
+
+      NS_WARNING(error.get());
+#endif
+      return rv;
+    }
+
+    mCachedStatements.Put(aQuery, stmt);
+  }
+
+  aCachedStatement->Assign(this, stmt.forget());
+  return NS_OK;
+}
+
+void
+Connection::ScheduleFlush()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mWriteOptimizer.HasWrites());
+  MOZ_ASSERT(!mFlushScheduled);
+
+  if (!mFlushTimer) {
+    mFlushTimer = NS_NewTimer();
+    MOZ_ASSERT(mFlushTimer);
+  }
+
+  MOZ_ALWAYS_SUCCEEDS(
+    mFlushTimer->InitWithNamedFuncCallback(FlushTimerCallback,
+                                           this,
+                                           kFlushTimeoutMs,
+                                           nsITimer::TYPE_ONE_SHOT,
+                                           "Connection::FlushTimerCallback"));
+
+  mFlushScheduled = true;
+}
+
+void
+Connection::Flush()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mFlushScheduled);
+
+  if (mWriteOptimizer.HasWrites()) {
+    RefPtr<FlushOp> op = new FlushOp(this, std::move(mWriteOptimizer));
+
+    Dispatch(op);
+  }
+
+  mFlushScheduled = false;
+}
+
+// static
+void
+Connection::FlushTimerCallback(nsITimer* aTimer, void* aClosure)
+{
+  MOZ_ASSERT(aClosure);
+
+  auto* self = static_cast<Connection*>(aClosure);
+  MOZ_ASSERT(self);
+  MOZ_ASSERT(self->mFlushScheduled);
+
+  self->Flush();
+}
+
+Connection::
+CachedStatement::CachedStatement()
+{
+  AssertIsOnConnectionThread();
+
+  MOZ_COUNT_CTOR(Connection::CachedStatement);
+}
+
+Connection::
+CachedStatement::~CachedStatement()
+{
+  AssertIsOnConnectionThread();
+
+  MOZ_COUNT_DTOR(Connection::CachedStatement);
+}
+
+Connection::
+CachedStatement::operator mozIStorageStatement*() const
+{
+  AssertIsOnConnectionThread();
+
+  return mStatement;
+}
+
+mozIStorageStatement*
+Connection::
+CachedStatement::operator->() const
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(mStatement);
+
+  return mStatement;
+}
+
+void
+Connection::
+CachedStatement::Assign(Connection* aConnection,
+                        already_AddRefed<mozIStorageStatement> aStatement)
+{
+  AssertIsOnConnectionThread();
+
+  mScoper.reset();
+
+  mStatement = aStatement;
+
+  if (mStatement) {
+    mScoper.emplace(mStatement);
+  }
+}
+
+Connection::
+FlushOp::FlushOp(Connection* aConnection,
+                 WriteOptimizer&& aWriteOptimizer)
+  : ConnectionDatastoreOperationBase(aConnection)
+  , mQuotaClient(QuotaClient::GetInstance())
+  , mWriteOptimizer(std::move(aWriteOptimizer))
+  , mShadowWrites(gShadowWrites)
+{
+  MOZ_ASSERT(mQuotaClient);
+}
+
+nsresult
+Connection::
+FlushOp::DoDatastoreWork()
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(mConnection);
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  nsCOMPtr<mozIStorageConnection> storageConnection =
+    mConnection->StorageConnection();
+  MOZ_ASSERT(storageConnection);
+
+  nsresult rv;
+
+  Maybe<MutexAutoLock> shadowDatabaseLock;
+
+  if (mShadowWrites) {
+    MOZ_ASSERT(mQuotaClient);
+
+    shadowDatabaseLock.emplace(mQuotaClient->ShadowDatabaseMutex());
+
+    rv = AttachShadowDatabase(quotaManager->GetBasePath(), storageConnection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  CachedStatement stmt;
+  rv = mConnection->GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"),
+                                       &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mWriteOptimizer.PerformWrites(mConnection, mShadowWrites);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mConnection->GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &stmt);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (mShadowWrites) {
+    rv = DetachShadowDatabase(storageConnection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+Connection::
+CloseOp::DoDatastoreWork()
+{
+  AssertIsOnConnectionThread();
+  MOZ_ASSERT(mConnection);
+
+  mConnection->CloseStorageConnection();
+
+  return NS_OK;
+}
+
+void
+Connection::
+CloseOp::Cleanup()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mConnection);
+
+  mConnection->mConnectionThread->mConnections.Remove(mConnection->mOrigin);
+
+  nsCOMPtr<nsIRunnable> callback;
+  mCallback.swap(callback);
+
+  callback->Run();
+
+  ConnectionDatastoreOperationBase::Cleanup();
+}
+
+/*******************************************************************************
+ * ConnectionThread implementation
+ ******************************************************************************/
+
+ConnectionThread::ConnectionThread()
+{
+  AssertIsOnOwningThread();
+  AssertIsOnBackgroundThread();
+
+  MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("LS Thread", getter_AddRefs(mThread)));
+}
+
+ConnectionThread::~ConnectionThread()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!mConnections.Count());
+}
+
+bool
+ConnectionThread::IsOnConnectionThread()
+{
+  MOZ_ASSERT(mThread);
+
+  bool current;
+  return NS_SUCCEEDED(mThread->IsOnCurrentThread(&current)) && current;
+}
+
+void
+ConnectionThread::AssertIsOnConnectionThread()
+{
+  MOZ_ASSERT(IsOnConnectionThread());
+}
+
+already_AddRefed<Connection>
+ConnectionThread::CreateConnection(
+                          const nsACString& aOrigin,
+                          const nsAString& aFilePath,
+                          nsAutoPtr<ArchivedOriginScope>&& aArchivedOriginScope)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(!aOrigin.IsEmpty());
+  MOZ_ASSERT(!mConnections.GetWeak(aOrigin));
+
+  RefPtr<Connection> connection =
+    new Connection(this, aOrigin, aFilePath, std::move(aArchivedOriginScope));
+  mConnections.Put(aOrigin, connection);
+
+  return connection.forget();
+}
+
+void
+ConnectionThread::Shutdown()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mThread);
+
+  mThread->Shutdown();
+}
+
+/*******************************************************************************
+ * Datastore
+ ******************************************************************************/
+
+Datastore::Datastore(const nsACString& aOrigin,
+                     uint32_t aPrivateBrowsingId,
+                     int64_t aUsage,
+                     int64_t aSizeOfKeys,
+                     int64_t aSizeOfItems,
+                     already_AddRefed<DirectoryLock>&& aDirectoryLock,
+                     already_AddRefed<Connection>&& aConnection,
+                     already_AddRefed<QuotaObject>&& aQuotaObject,
+                     nsDataHashtable<nsStringHashKey, nsString>& aValues,
+                     nsTArray<LSItemInfo>& aOrderedItems)
+  : mDirectoryLock(std::move(aDirectoryLock))
+  , mConnection(std::move(aConnection))
+  , mQuotaObject(std::move(aQuotaObject))
+  , mOrigin(aOrigin)
+  , mPrivateBrowsingId(aPrivateBrowsingId)
+  , mUsage(aUsage)
+  , mUpdateBatchUsage(-1)
+  , mSizeOfKeys(aSizeOfKeys)
+  , mSizeOfItems(aSizeOfItems)
+  , mClosed(false)
+#ifdef DEBUG
+  , mInUpdateBatch(false)
+#endif
+{
+  AssertIsOnBackgroundThread();
+
+  mValues.SwapElements(aValues);
+  mOrderedItems.SwapElements(aOrderedItems);
+}
+
+Datastore::~Datastore()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mClosed);
+}
+
+void
+Datastore::Close()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(!mDatabases.Count());
+  MOZ_ASSERT(mDirectoryLock);
+
+  mClosed = true;
+
+  if (IsPersistent()) {
+    MOZ_ASSERT(mConnection);
+    MOZ_ASSERT(mQuotaObject);
+
+    // We can't release the directory lock and unregister itself from the
+    // hashtable until the connection is fully closed.
+    nsCOMPtr<nsIRunnable> callback =
+      NewRunnableMethod("dom::Datastore::ConnectionClosedCallback",
+                        this,
+                        &Datastore::ConnectionClosedCallback);
+    mConnection->Close(callback);
+  } else {
+    MOZ_ASSERT(!mConnection);
+    MOZ_ASSERT(!mQuotaObject);
+
+    // There's no connection, so it's safe to release the directory lock and
+    // unregister itself from the hashtable.
+
+    mDirectoryLock = nullptr;
+
+    CleanupMetadata();
+  }
+}
+
+void
+Datastore::WaitForConnectionToComplete(nsIRunnable* aCallback)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aCallback);
+  MOZ_ASSERT(!mCompleteCallback);
+  MOZ_ASSERT(mClosed);
+
+  mCompleteCallback = aCallback;
+}
+
+void
+Datastore::NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aPrepareDatastoreOp);
+  MOZ_ASSERT(!mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mPrepareDatastoreOps.PutEntry(aPrepareDatastoreOp);
+}
+
+void
+Datastore::NoteFinishedPrepareDatastoreOp(
+                                        PrepareDatastoreOp* aPrepareDatastoreOp)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aPrepareDatastoreOp);
+  MOZ_ASSERT(mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mPrepareDatastoreOps.RemoveEntry(aPrepareDatastoreOp);
+
+  MaybeClose();
+}
+
+void
+Datastore::NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aPreparedDatastore);
+  MOZ_ASSERT(!mPreparedDatastores.GetEntry(aPreparedDatastore));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mPreparedDatastores.PutEntry(aPreparedDatastore);
+}
+
+void
+Datastore::NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aPreparedDatastore);
+  MOZ_ASSERT(mPreparedDatastores.GetEntry(aPreparedDatastore));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mPreparedDatastores.RemoveEntry(aPreparedDatastore);
+
+  MaybeClose();
+}
+
+#ifdef DEBUG
+bool
+Datastore::HasLivePreparedDatastores() const
+{
+  AssertIsOnBackgroundThread();
+
+  return mPreparedDatastores.Count();
+}
+#endif
+
+void
+Datastore::NoteLiveDatabase(Database* aDatabase)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(!mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mDatabases.PutEntry(aDatabase);
+}
+
+void
+Datastore::NoteFinishedDatabase(Database* aDatabase)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(!mClosed);
+
+  mDatabases.RemoveEntry(aDatabase);
+
+  MaybeClose();
+}
+
+#ifdef DEBUG
+bool
+Datastore::HasLiveDatabases() const
+{
+  AssertIsOnBackgroundThread();
+
+  return mDatabases.Count();
+}
+#endif
+
+void
+Datastore::NoteActiveDatabase(Database* aDatabase)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mClosed);
+
+  mActiveDatabases.PutEntry(aDatabase);
+}
+
+void
+Datastore::NoteInactiveDatabase(Database* aDatabase)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(mDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(mActiveDatabases.GetEntry(aDatabase));
+  MOZ_ASSERT(!mClosed);
+
+  mActiveDatabases.RemoveEntry(aDatabase);
+
+  if (!mActiveDatabases.Count() &&
+      mPendingUsageDeltas.Length()) {
+    int64_t finalDelta = 0;
+
+    for (auto delta : mPendingUsageDeltas) {
+      finalDelta += delta;
+    }
+
+    MOZ_ASSERT(finalDelta <= 0);
+
+    if (finalDelta != 0) {
+      DebugOnly<bool> ok = UpdateUsage(finalDelta);
+      MOZ_ASSERT(ok);
+    }
+
+    mPendingUsageDeltas.Clear();
+  }
+}
+
+void
+Datastore::GetSnapshotInitInfo(nsTHashtable<nsStringHashKey>& aLoadedItems,
+                               nsTArray<LSItemInfo>& aItemInfos,
+                               uint32_t& aTotalLength,
+                               int64_t& aInitialUsage,
+                               int64_t& aPeakUsage,
+                               LSSnapshot::LoadState& aLoadState)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+  int64_t sizeOfKeys = 0;
+  int64_t sizeOfItems = 0;
+  for (auto item : mOrderedItems) {
+    int64_t sizeOfKey = static_cast<int64_t>(item.key().Length());
+    sizeOfKeys += sizeOfKey;
+    sizeOfItems += sizeOfKey + static_cast<int64_t>(item.value().Length());
+  }
+  MOZ_ASSERT(mSizeOfKeys == sizeOfKeys);
+  MOZ_ASSERT(mSizeOfItems == sizeOfItems);
+#endif
+
+  int64_t size = 0;
+  if (mSizeOfKeys <= gSnapshotPrefill) {
+    if (mSizeOfItems <= gSnapshotPrefill) {
+      aItemInfos.AppendElements(mOrderedItems);
+      aLoadState = LSSnapshot::LoadState::AllOrderedItems;
+    } else {
+      nsString value;
+      for (auto item : mOrderedItems) {
+        if (!value.IsVoid()) {
+          value = item.value();
+
+          size += static_cast<int64_t>(item.key().Length()) +
+                  static_cast<int64_t>(value.Length());
+
+          if (size <= gSnapshotPrefill) {
+            aLoadedItems.PutEntry(item.key());
+          } else {
+            value.SetIsVoid(true);
+          }
+        }
+
+        LSItemInfo* itemInfo = aItemInfos.AppendElement();
+        itemInfo->key() = item.key();
+        itemInfo->value() = value;
+      }
+
+      aLoadState = LSSnapshot::LoadState::AllOrderedKeys;
+    }
+  } else {
+    for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+      const nsAString& key = iter.Key();
+      const nsString& value = iter.Data();
+
+      size += static_cast<int64_t>(key.Length()) +
+              static_cast<int64_t>(value.Length());
+
+      if (size > gSnapshotPrefill) {
+        break;
+      }
+
+      aLoadedItems.PutEntry(key);
+
+      LSItemInfo* itemInfo = aItemInfos.AppendElement();
+      itemInfo->key() = iter.Key();
+      itemInfo->value() = iter.Data();
+    }
+
+    MOZ_ASSERT(aItemInfos.Length() < mOrderedItems.Length());
+    aLoadState = LSSnapshot::LoadState::Partial;
+  }
+
+  aTotalLength = mValues.Count();
+
+  aInitialUsage = mUsage;
+  aPeakUsage = aInitialUsage;
+}
+
+void
+Datastore::GetItem(const nsString& aKey, nsString& aValue) const
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+
+  if (!mValues.Get(aKey, &aValue)) {
+    aValue.SetIsVoid(true);
+  }
+}
+
+void
+Datastore::GetKeys(nsTArray<nsString>& aKeys) const
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+
+  for (auto item : mOrderedItems) {
+    aKeys.AppendElement(item.key());
+  }
+}
+
+void
+Datastore::SetItem(Database* aDatabase,
+                   const nsString& aDocumentURI,
+                   const nsString& aKey,
+                   const nsString& aOldValue,
+                   const nsString& aValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  nsString oldValue;
+  GetItem(aKey, oldValue);
+
+  if (oldValue != aValue || oldValue.IsVoid() != aValue.IsVoid()) {
+    bool isNewItem = oldValue.IsVoid();
+
+    NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem);
+
+    mValues.Put(aKey, aValue);
+
+    if (isNewItem) {
+      mWriteOptimizer.AddItem(aKey, aValue);
+
+      int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+      int64_t sizeOfItem = sizeOfKey + static_cast<int64_t>(aValue.Length());
+
+      mUpdateBatchUsage += sizeOfItem;
+
+      mSizeOfKeys += sizeOfKey;
+      mSizeOfItems += sizeOfItem;
+    } else {
+      mWriteOptimizer.UpdateItem(aKey, aValue);
+
+      int64_t delta = static_cast<int64_t>(aValue.Length()) -
+                      static_cast<int64_t>(oldValue.Length());
+
+      mUpdateBatchUsage += delta;
+
+      mSizeOfItems += delta;
+    }
+
+    if (IsPersistent()) {
+      if (oldValue.IsVoid()) {
+        mConnection->AddItem(aKey, aValue);
+      } else {
+        mConnection->UpdateItem(aKey, aValue);
+      }
+    }
+  }
+
+  NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, aValue);
+}
+
+void
+Datastore::RemoveItem(Database* aDatabase,
+                      const nsString& aDocumentURI,
+                      const nsString& aKey,
+                      const nsString& aOldValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  nsString oldValue;
+  GetItem(aKey, oldValue);
+
+  if (!oldValue.IsVoid()) {
+    NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true);
+
+    mValues.Remove(aKey);
+
+    mWriteOptimizer.RemoveItem(aKey);
+
+    int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+    int64_t sizeOfItem = sizeOfKey + static_cast<int64_t>(oldValue.Length());
+
+    mUpdateBatchUsage -= sizeOfItem;
+
+    mSizeOfKeys -= sizeOfKey;
+    mSizeOfItems -= sizeOfItem;
+
+    if (IsPersistent()) {
+      mConnection->RemoveItem(aKey);
+    }
+  }
+
+  NotifyObservers(aDatabase, aDocumentURI, aKey, aOldValue, VoidString());
+}
+
+void
+Datastore::Clear(Database* aDatabase,
+                 const nsString& aDocumentURI)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  if (mValues.Count()) {
+    int64_t updateBatchUsage = mUpdateBatchUsage;
+    for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+      const nsAString& key = iter.Key();
+      const nsAString& value = iter.Data();
+
+      updateBatchUsage -= (static_cast<int64_t>(key.Length()) +
+                           static_cast<int64_t>(value.Length()));
+
+      NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true);
+    }
+
+    mValues.Clear();
+
+    mWriteOptimizer.Clear();
+
+    mUpdateBatchUsage = updateBatchUsage;
+
+    mSizeOfKeys = 0;
+    mSizeOfItems = 0;
+
+    if (IsPersistent()) {
+      mConnection->Clear();
+    }
+  }
+
+  NotifyObservers(aDatabase,
+                  aDocumentURI,
+                  VoidString(),
+                  VoidString(),
+                  VoidString());
+}
+
+void
+Datastore::PrivateBrowsingClear()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mPrivateBrowsingId);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(!mInUpdateBatch);
+
+  if (mValues.Count()) {
+    MarkSnapshotsDirty();
+
+    mValues.Clear();
+
+    mOrderedItems.Clear();
+
+    DebugOnly<bool> ok = UpdateUsage(-mSizeOfItems);
+    MOZ_ASSERT(ok);
+
+    mSizeOfKeys = 0;
+    mSizeOfItems = 0;
+  }
+}
+
+void
+Datastore::BeginUpdateBatch(int64_t aSnapshotInitialUsage)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSnapshotInitialUsage >= 0);
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mUpdateBatchUsage == -1);
+  MOZ_ASSERT(!mInUpdateBatch);
+
+  mUpdateBatchUsage = aSnapshotInitialUsage;
+
+  if (IsPersistent()) {
+    mConnection->BeginUpdateBatch();
+  }
+
+#ifdef DEBUG
+  mInUpdateBatch = true;
+#endif
+}
+
+int64_t
+Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mClosed);
+  MOZ_ASSERT(mInUpdateBatch);
+
+  mWriteOptimizer.ApplyWrites(mOrderedItems);
+
+  if (aSnapshotPeakUsage >= 0) {
+    int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage;
+
+    if (mActiveDatabases.Count()) {
+      // We can't apply deltas while other databases are still active.
+      // The final delta must be zero or negative, but individual deltas can
+      // be positive. A positive delta can't be applied asynchronously since
+      // there's no way to fire the quota exceeded error event.
+
+      mPendingUsageDeltas.AppendElement(delta);
+    } else {
+      MOZ_ASSERT(delta <= 0);
+      if (delta != 0) {
+        DebugOnly<bool> ok = UpdateUsage(delta);
+        MOZ_ASSERT(ok);
+      }
+    }
+  }
+
+  int64_t result = mUpdateBatchUsage;
+  mUpdateBatchUsage = -1;
+
+  if (IsPersistent()) {
+    mConnection->EndUpdateBatch();
+  }
+
+#ifdef DEBUG
+  mInUpdateBatch = false;
+#endif
+
+  return result;
+}
+
+int64_t
+Datastore::RequestUpdateUsage(int64_t aRequestedSize,
+                              int64_t aMinSize)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aRequestedSize > 0);
+  MOZ_ASSERT(aMinSize > 0);
+
+  if (UpdateUsage(aRequestedSize)) {
+    return aRequestedSize;
+  }
+
+  if (UpdateUsage(aMinSize)) {
+    return aMinSize;
+  }
+
+  return 0;
+}
+
+bool
+Datastore::UpdateUsage(int64_t aDelta)
+{
+  AssertIsOnBackgroundThread();
+
+  // Check internal LocalStorage origin limit.
+  int64_t newUsage = mUsage + aDelta;
+  if (newUsage > gOriginLimitKB * 1024) {
+    return false;
+  }
+
+  // Check QuotaManager limits (group and global limit).
+  if (IsPersistent()) {
+    MOZ_ASSERT(mQuotaObject);
+
+    if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+      return false;
+    }
+
+  }
+
+  // Quota checks passed, set new usage.
+
+  mUsage = newUsage;
+
+  if (IsPersistent()) {
+    RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+      "Datastore::UpdateUsage",
+      [origin = mOrigin, newUsage] () {
+        MOZ_ASSERT(gUsages);
+        MOZ_ASSERT(gUsages->Contains(origin));
+        gUsages->Put(origin, newUsage);
+      });
+
+    QuotaManager* quotaManager = QuotaManager::Get();
+    MOZ_ASSERT(quotaManager);
+
+    MOZ_ALWAYS_SUCCEEDS(
+      quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL));
+  }
+
+  return true;
+}
+
+void
+Datastore::MaybeClose()
+{
+  AssertIsOnBackgroundThread();
+
+  if (!mPrepareDatastoreOps.Count() &&
+      !mPreparedDatastores.Count() &&
+      !mDatabases.Count()) {
+    Close();
+  }
+}
+
+void
+Datastore::ConnectionClosedCallback()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mDirectoryLock);
+  MOZ_ASSERT(mConnection);
+  MOZ_ASSERT(mQuotaObject);
+  MOZ_ASSERT(mClosed);
+
+  // Release the quota object first.
+  mQuotaObject = nullptr;
+
+  // Now it's safe to release the directory lock and unregister itself from
+  // the hashtable.
+
+  mDirectoryLock = nullptr;
+  mConnection = nullptr;
+
+  CleanupMetadata();
+
+  if (mCompleteCallback) {
+    MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget()));
+  }
+}
+
+void
+Datastore::CleanupMetadata()
+{
+  AssertIsOnBackgroundThread();
+
+  MOZ_ASSERT(gDatastores);
+  MOZ_ASSERT(gDatastores->Get(mOrigin));
+  gDatastores->Remove(mOrigin);
+
+  if (!gDatastores->Count()) {
+    gDatastores = nullptr;
+  }
+}
+
+void
+Datastore::NotifySnapshots(Database* aDatabase,
+                           const nsAString& aKey,
+                           const nsAString& aOldValue,
+                           bool aAffectsOrder)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+
+  for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+    Database* database = iter.Get()->GetKey();
+    if (database == aDatabase) {
+      continue;
+    }
+
+    Snapshot* snapshot = database->GetSnapshot();
+    if (snapshot) {
+      snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
+    }
+  }
+}
+
+void
+Datastore::MarkSnapshotsDirty()
+{
+  AssertIsOnBackgroundThread();
+
+  for (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) {
+    Database* database = iter.Get()->GetKey();
+
+    Snapshot* snapshot = database->GetSnapshot();
+    if (snapshot) {
+      snapshot->MarkDirty();
+    }
+  }
+}
+
+void
+Datastore::NotifyObservers(Database* aDatabase,
+                           const nsString& aDocumentURI,
+                           const nsString& aKey,
+                           const nsString& aOldValue,
+                           const nsString& aNewValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+
+  if (!gObservers) {
+    return;
+  }
+
+  nsTArray<Observer*>* array;
+  if (!gObservers->Get(mOrigin, &array)) {
+    return;
+  }
+
+  MOZ_ASSERT(array);
+
+  // We do not want to send information about events back to the content process
+  // that caused the change.
+  PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+  for (Observer* observer : *array) {
+    if (observer->Manager() != databaseBackgroundActor) {
+      observer->Observe(aDatabase, aDocumentURI, aKey, aOldValue, aNewValue);
+    }
+  }
+}
+
+/*******************************************************************************
+ * PreparedDatastore
+ ******************************************************************************/
+
+void
+PreparedDatastore::Destroy()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(gPreparedDatastores);
+  MOZ_ASSERT(gPreparedDatastores->Get(mDatastoreId));
+
+  nsAutoPtr<PreparedDatastore> preparedDatastore;
+  gPreparedDatastores->Remove(mDatastoreId, &preparedDatastore);
+  MOZ_ASSERT(preparedDatastore);
+}
+
+// static
+void
+PreparedDatastore::TimerCallback(nsITimer* aTimer, void* aClosure)
+{
+  AssertIsOnBackgroundThread();
+
+  auto* self = static_cast<PreparedDatastore*>(aClosure);
+  MOZ_ASSERT(self);
+
+  self->Destroy();
+}
+
+/*******************************************************************************
+ * Database
+ ******************************************************************************/
+
+Database::Database(const PrincipalInfo& aPrincipalInfo,
+                   const Maybe<ContentParentId>& aContentParentId,
+                   const nsACString& aOrigin,
+                   uint32_t aPrivateBrowsingId)
+  : mSnapshot(nullptr)
+  , mPrincipalInfo(aPrincipalInfo)
+  , mContentParentId(aContentParentId)
+  , mOrigin(aOrigin)
+  , mPrivateBrowsingId(aPrivateBrowsingId)
+  , mAllowedToClose(false)
+  , mActorDestroyed(false)
+  , mRequestedAllowToClose(false)
+#ifdef DEBUG
+  , mActorWasAlive(false)
+#endif
+{
+  AssertIsOnBackgroundThread();
+}
+
+Database::~Database()
+{
+  MOZ_ASSERT_IF(mActorWasAlive, mAllowedToClose);
+  MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed);
+}
+
+void
+Database::SetActorAlive(Datastore* aDatastore)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorWasAlive);
+  MOZ_ASSERT(!mActorDestroyed);
+
+#ifdef DEBUG
+  mActorWasAlive = true;
+#endif
+
+  mDatastore = aDatastore;
+
+  mDatastore->NoteLiveDatabase(this);
+
+  if (!gLiveDatabases) {
+    gLiveDatabases = new LiveDatabaseArray();
+  }
+
+  gLiveDatabases->AppendElement(this);
+}
+
+void
+Database::RegisterSnapshot(Snapshot* aSnapshot)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSnapshot);
+  MOZ_ASSERT(!mSnapshot);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  // Only one snapshot at a time is currently supported.
+  mSnapshot = aSnapshot;
+
+  mDatastore->NoteActiveDatabase(this);
+}
+
+void
+Database::UnregisterSnapshot(Snapshot* aSnapshot)
+{
+  MOZ_ASSERT(aSnapshot);
+  MOZ_ASSERT(mSnapshot == aSnapshot);
+
+  mSnapshot = nullptr;
+
+  mDatastore->NoteInactiveDatabase(this);
+}
+
+void
+Database::RequestAllowToClose()
+{
+  AssertIsOnBackgroundThread();
+
+  if (mRequestedAllowToClose) {
+    return;
+  }
+
+  mRequestedAllowToClose = true;
+
+  // Send the RequestAllowToClose message to the child to avoid racing with the
+  // child actor. Except the case when the actor was already destroyed.
+  if (mActorDestroyed) {
+    MOZ_ASSERT(mAllowedToClose);
+  } else {
+    Unused << SendRequestAllowToClose();
+  }
+}
+
+void
+Database::AllowToClose()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mAllowedToClose);
+  MOZ_ASSERT(mDatastore);
+
+  mAllowedToClose = true;
+
+  mDatastore->NoteFinishedDatabase(this);
+
+  mDatastore = nullptr;
+
+  MOZ_ASSERT(gLiveDatabases);
+  gLiveDatabases->RemoveElement(this);
+
+  if (gLiveDatabases->IsEmpty()) {
+    gLiveDatabases = nullptr;
+  }
+}
+
+void
+Database::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  mActorDestroyed = true;
+
+  if (!mAllowedToClose) {
+    AllowToClose();
+  }
+}
+
+mozilla::ipc::IPCResult
+Database::RecvDeleteMe()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  IProtocol* mgr = Manager();
+  if (!PBackgroundLSDatabaseParent::Send__delete__(this)) {
+    return IPC_FAIL_NO_REASON(mgr);
+  }
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Database::RecvAllowToClose()
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mAllowedToClose)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  AllowToClose();
+
+  return IPC_OK();
+}
+
+PBackgroundLSSnapshotParent*
+Database::AllocPBackgroundLSSnapshotParent(const nsString& aDocumentURI,
+                                           const bool& aIncreasePeakUsage,
+                                           const int64_t& aRequestedSize,
+                                           const int64_t& aMinSize,
+                                           LSSnapshotInitInfo* aInitInfo)
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(aIncreasePeakUsage && aRequestedSize <= 0)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  if (NS_WARN_IF(aIncreasePeakUsage && aMinSize <= 0)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  if (NS_WARN_IF(mAllowedToClose)) {
+    ASSERT_UNLESS_FUZZING();
+    return nullptr;
+  }
+
+  RefPtr<Snapshot> snapshot = new Snapshot(this, aDocumentURI);
+
+  // Transfer ownership to IPDL.
+  return snapshot.forget().take();
+}
+
+mozilla::ipc::IPCResult
+Database::RecvPBackgroundLSSnapshotConstructor(
+                                            PBackgroundLSSnapshotParent* aActor,
+                                            const nsString& aDocumentURI,
+                                            const bool& aIncreasePeakUsage,
+                                            const int64_t& aRequestedSize,
+                                            const int64_t& aMinSize,
+                                            LSSnapshotInitInfo* aInitInfo)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT_IF(aIncreasePeakUsage, aRequestedSize > 0);
+  MOZ_ASSERT_IF(aIncreasePeakUsage, aMinSize > 0);
+  MOZ_ASSERT(aInitInfo);
+  MOZ_ASSERT(!mAllowedToClose);
+
+  auto* snapshot = static_cast<Snapshot*>(aActor);
+
+  // TODO: This can be optimized depending on which operation triggers snapshot
+  //       creation. For example clear() doesn't need to receive items at all.
+  nsTHashtable<nsStringHashKey> loadedItems;
+  nsTArray<LSItemInfo> itemInfos;
+  uint32_t totalLength;
+  int64_t initialUsage;
+  int64_t peakUsage;
+  LSSnapshot::LoadState loadState;
+  mDatastore->GetSnapshotInitInfo(loadedItems,
+                                  itemInfos,
+                                  totalLength,
+                                  initialUsage,
+                                  peakUsage,
+                                  loadState);
+
+  if (aIncreasePeakUsage) {
+    int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize);
+    peakUsage += size;
+  }
+
+  snapshot->Init(loadedItems, totalLength, initialUsage, peakUsage, loadState);
+
+  RegisterSnapshot(snapshot);
+
+  aInitInfo->itemInfos() = std::move(itemInfos);
+  aInitInfo->totalLength() = totalLength;
+  aInitInfo->initialUsage() = initialUsage;
+  aInitInfo->peakUsage() = peakUsage;
+  aInitInfo->loadState() = loadState;
+
+  return IPC_OK();
+}
+
+bool
+Database::DeallocPBackgroundLSSnapshotParent(
+                                            PBackgroundLSSnapshotParent* aActor)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aActor);
+
+  // Transfer ownership back from IPDL.
+  RefPtr<Snapshot> actor = dont_AddRef(static_cast<Snapshot*>(aActor));
+
+  return true;
+}
+
+/*******************************************************************************
+ * Snapshot
+ ******************************************************************************/
+
+Snapshot::Snapshot(Database* aDatabase,
+                   const nsAString& aDocumentURI)
+  : mDatabase(aDatabase)
+  , mDatastore(aDatabase->GetDatastore())
+  , mDocumentURI(aDocumentURI)
+  , mTotalLength(0)
+  , mUsage(-1)
+  , mPeakUsage(-1)
+  , mSavedKeys(false)
+  , mActorDestroyed(false)
+  , mFinishReceived(false)
+  , mLoadedReceived(false)
+  , mLoadedAllItems(false)
+  , mLoadKeysReceived(false)
+  , mSentMarkDirty(false)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+}
+
+Snapshot::~Snapshot()
+{
+  MOZ_ASSERT(mActorDestroyed);
+  MOZ_ASSERT(mFinishReceived);
+}
+
+void
+Snapshot::SaveItem(const nsAString& aKey,
+                   const nsAString& aOldValue,
+                   bool aAffectsOrder)
+{
+  AssertIsOnBackgroundThread();
+
+  MarkDirty();
+
+  if (mLoadedAllItems) {
+    return;
+  }
+
+  if (!mLoadedItems.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) {
+    nsString oldValue(aOldValue);
+    mValues.LookupForAdd(aKey).OrInsert([oldValue]() {
+      return oldValue;
+    });
+  }
+
+  if (aAffectsOrder && !mSavedKeys && !mLoadKeysReceived) {
+    mDatastore->GetKeys(mKeys);
+    mSavedKeys = true;
+  }
+}
+
+void
+Snapshot::MarkDirty()
+{
+  AssertIsOnBackgroundThread();
+
+  if (!mSentMarkDirty) {
+    Unused << SendMarkDirty();
+    mSentMarkDirty = true;
+  }
+}
+
+void
+Snapshot::Finish()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mDatabase);
+  MOZ_ASSERT(mDatastore);
+  MOZ_ASSERT(!mFinishReceived);
+
+  mDatastore->BeginUpdateBatch(mUsage);
+
+  mDatastore->EndUpdateBatch(mPeakUsage);
+
+  mDatabase->UnregisterSnapshot(this);
+
+  mFinishReceived = true;
+}
+
+void
+Snapshot::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  mActorDestroyed = true;
+
+  if (!mFinishReceived) {
+    Finish();
+  }
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvDeleteMe()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  IProtocol* mgr = Manager();
+  if (!PBackgroundLSSnapshotParent::Send__delete__(this)) {
+    return IPC_FAIL_NO_REASON(mgr);
+  }
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvCheckpoint(nsTArray<LSWriteInfo>&& aWriteInfos)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mUsage >= 0);
+  MOZ_ASSERT(mPeakUsage >= mUsage);
+
+  if (NS_WARN_IF(aWriteInfos.IsEmpty())) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  mDatastore->BeginUpdateBatch(mUsage);
+
+  for (uint32_t index = 0; index < aWriteInfos.Length(); index++) {
+    const LSWriteInfo& writeInfo = aWriteInfos[index];
+    switch (writeInfo.type()) {
+      case LSWriteInfo::TLSSetItemInfo: {
+        const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo();
+
+        mDatastore->SetItem(mDatabase,
+                            mDocumentURI,
+                            info.key(),
+                            info.oldValue(),
+                            info.value());
+
+        break;
+      }
+
+      case LSWriteInfo::TLSRemoveItemInfo: {
+        const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo();
+
+        mDatastore->RemoveItem(mDatabase,
+                               mDocumentURI,
+                               info.key(),
+                               info.oldValue());
+
+        break;
+      }
+
+      case LSWriteInfo::TLSClearInfo: {
+        mDatastore->Clear(mDatabase, mDocumentURI);
+
+        break;
+      }
+
+      default:
+        MOZ_CRASH("Should never get here!");
+    }
+  }
+
+  mUsage = mDatastore->EndUpdateBatch(-1);
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvFinish()
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  Finish();
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvLoaded()
+{
+  AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedAllItems)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadKeysReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  mLoadedReceived = true;
+
+  mLoadedItems.Clear();
+  mUnknownItems.Clear();
+  mValues.Clear();
+  mKeys.Clear();
+  mLoadedAllItems = true;
+  mLoadKeysReceived = true;
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvLoadItem(const nsString& aKey,
+                       nsString* aValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aValue);
+  MOZ_ASSERT(mDatastore);
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedAllItems)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (auto entry = mValues.Lookup(aKey)) {
+    *aValue = entry.Data();
+    entry.Remove();
+  } else {
+    mDatastore->GetItem(aKey, *aValue);
+  }
+
+  if (aValue->IsVoid()) {
+    mUnknownItems.PutEntry(aKey);
+  } else {
+    mLoadedItems.PutEntry(aKey);
+
+    if (mLoadedItems.Count() == mTotalLength) {
+      mLoadedItems.Clear();
+      mUnknownItems.Clear();
+#ifdef DEBUG
+      for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) {
+        MOZ_ASSERT(iter.Data().IsVoid());
+      }
+#endif
+      mValues.Clear();
+      mLoadedAllItems = true;
+    }
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvLoadKeys(nsTArray<nsString>* aKeys)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aKeys);
+  MOZ_ASSERT(mDatastore);
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadedReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mLoadKeysReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  mLoadKeysReceived = true;
+
+  if (mSavedKeys) {
+    aKeys->AppendElements(std::move(mKeys));
+  } else {
+    mDatastore->GetKeys(*aKeys);
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvIncreasePeakUsage(const int64_t& aRequestedSize,
+                                const int64_t& aMinSize,
+                                int64_t* aSize)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aSize);
+
+  if (NS_WARN_IF(aRequestedSize <= 0)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(aMinSize <= 0)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  if (NS_WARN_IF(mFinishReceived)) {
+    ASSERT_UNLESS_FUZZING();
+    return IPC_FAIL_NO_REASON(this);
+  }
+
+  int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize);
+
+  mPeakUsage += size;
+
+  *aSize = size;
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+Snapshot::RecvPing()
+{
+  AssertIsOnBackgroundThread();
+
+  // Do nothing here. This is purely a sync message allowing the child to
+  // confirm that the actor has received previous async message.
+
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * Observer
+ ******************************************************************************/
+
+Observer::Observer(const nsACString& aOrigin)
+  : mOrigin(aOrigin)
+  , mActorDestroyed(false)
+{
+  AssertIsOnBackgroundThread();
+}
+
+Observer::~Observer()
+{
+  MOZ_ASSERT(mActorDestroyed);
+}
+
+void
+Observer::Observe(Database* aDatabase,
+                  const nsString& aDocumentURI,
+                  const nsString& aKey,
+                  const nsString& aOldValue,
+                  const nsString& aNewValue)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(aDatabase);
+
+  Unused << SendObserve(aDatabase->GetPrincipalInfo(),
+                        aDatabase->PrivateBrowsingId(),
+                        aDocumentURI,
+                        aKey,
+                        aOldValue,
+                        aNewValue);
+}
+
+void
+Observer::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  mActorDestroyed = true;
+
+  MOZ_ASSERT(gObservers);
+
+  nsTArray<Observer*>* array;
+  gObservers->Get(mOrigin, &array);
+  MOZ_ASSERT(array);
+
+  array->RemoveElement(this);
+
+  if (array->IsEmpty()) {
+    gObservers->Remove(mOrigin);
+  }
+
+  if (!gObservers->Count()) {
+    gObservers = nullptr;
+  }
+}
+
+mozilla::ipc::IPCResult
+Observer::RecvDeleteMe()
+{
+  AssertIsOnBackgroundThread();
+  MOZ_ASSERT(!mActorDestroyed);
+
+  IProtocol* mgr = Manager();
+  if (!PBackgroundLSObserverParent::Send__delete__(this)) {
+    return IPC_FAIL_NO_REASON(mgr);
+  }
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSRequestBase
+ ******************************************************************************/
+
+LSRequestBase::LSRequestBase(nsIEventTarget* aMainEventTarget)
+  : mMainEventTarget(aMainEventTarget)
+  , mState(State::Initial)
+{
+}
+
+LSRequestBase::~LSRequestBase()
+{
+  MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+                mState == State::Initial || mState == State::Completed);
+}
+
+void
+LSRequestBase::Dispatch()
+{
+  AssertIsOnOwningThread();
+
+  mState = State::Opening;
+
+  if (mMainEventTarget) {
+    MOZ_ALWAYS_SUCCEEDS(mMainEventTarget->Dispatch(this, NS_DISPATCH_NORMAL));
+  } else {
+    MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this));
+  }
+}
+
+nsresult
+LSRequestBase::NestedRun()
+{
+  return NS_OK;
+}
+
+void
+LSRequestBase::SendReadyMessage()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    MaybeSetFailureCode(NS_ERROR_FAILURE);
+  }
+
+  if (MayProceed()) {
+    Unused << SendReady();
+
+    mState = State::WaitingForFinish;
+  } else {
+    Cleanup();
+
+    mState = State::Completed;
+  }
+}
+
+void
+LSRequestBase::SendResults()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::SendingResults);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    MaybeSetFailureCode(NS_ERROR_FAILURE);
+  }
+
+  if (MayProceed()) {
+    LSRequestResponse response;
+
+    if (NS_SUCCEEDED(ResultCode())) {
+      GetResponse(response);
+    } else {
+      response = ResultCode();
+    }
+
+    Unused <<
+      PBackgroundLSRequestParent::Send__delete__(this, response);
+  }
+
+  Cleanup();
+
+  mState = State::Completed;
+}
+
+NS_IMETHODIMP
+LSRequestBase::Run()
+{
+  nsresult rv;
+
+  switch (mState) {
+    case State::Opening:
+      rv = Open();
+      break;
+
+    case State::Nesting:
+      rv = NestedRun();
+      break;
+
+    case State::SendingReadyMessage:
+      SendReadyMessage();
+      return NS_OK;
+
+    case State::SendingResults:
+      SendResults();
+      return NS_OK;
+
+    default:
+      MOZ_CRASH("Bad state!");
+  }
+
+  if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingReadyMessage) {
+    MaybeSetFailureCode(rv);
+
+    // Must set mState before dispatching otherwise we will race with the owning
+    // thread.
+    mState = State::SendingReadyMessage;
+
+    if (IsOnOwningThread()) {
+      SendReadyMessage();
+    } else {
+      MOZ_ALWAYS_SUCCEEDS(
+        OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+    }
+  }
+
+  return NS_OK;
+}
+
+void
+LSRequestBase::ActorDestroy(ActorDestroyReason aWhy)
+{
+  AssertIsOnOwningThread();
+
+  NoteComplete();
+}
+
+mozilla::ipc::IPCResult
+LSRequestBase::RecvCancel()
+{
+  AssertIsOnOwningThread();
+
+  IProtocol* mgr = Manager();
+  if (!PBackgroundLSRequestParent::Send__delete__(this, NS_ERROR_FAILURE)) {
+    return IPC_FAIL_NO_REASON(mgr);
+  }
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+LSRequestBase::RecvFinish()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::WaitingForFinish);
+
+  mState = State::SendingResults;
+
+  // This LSRequestBase can only be held alive by the IPDL. Run() can end up
+  // with clearing that last reference. So we need to add a self reference here.
+  RefPtr<LSRequestBase> kungFuDeathGrip = this;
+
+  MOZ_ALWAYS_SUCCEEDS(this->Run());
+
+  return IPC_OK();
+}
+
+/*******************************************************************************
+ * PrepareDatastoreOp
+ ******************************************************************************/
+
+PrepareDatastoreOp::PrepareDatastoreOp(
+                                 nsIEventTarget* aMainEventTarget,
+                                 already_AddRefed<ContentParent> aContentParent,
+                                 const LSRequestParams& aParams)
+  : LSRequestBase(aMainEventTarget)
+  , mMainEventTarget(aMainEventTarget)
+  , mContentParent(std::move(aContentParent))
+  , mLoadDataOp(nullptr)
+  , mParams(aParams.get_LSRequestPrepareDatastoreParams())
+  , mPrivateBrowsingId(0)
+  , mUsage(0)
+  , mSizeOfKeys(0)
+  , mSizeOfItems(0)
+  , mNestedState(NestedState::BeforeNesting)
+  , mDatabaseNotAvailable(false)
+  , mRequestedDirectoryLock(false)
+  , mInvalidated(false)
+#ifdef DEBUG
+  , mDEBUGUsage(0)
+#endif
+{
+  MOZ_ASSERT(aParams.type() ==
+               LSRequestParams::TLSRequestPrepareDatastoreParams);
+
+  if (mContentParent) {
+    mContentParentId = Some(mContentParent->ChildID());
+  }
+}
+
+PrepareDatastoreOp::~PrepareDatastoreOp()
+{
+  MOZ_ASSERT(!mDirectoryLock);
+  MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+                mState == State::Initial || mState == State::Completed);
+  MOZ_ASSERT(!mLoadDataOp);
+}
+
+nsresult
+PrepareDatastoreOp::Open()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mState == State::Opening);
+  MOZ_ASSERT(mNestedState == NestedState::BeforeNesting);
+
+  // Swap this to the stack now to ensure that we release it on this thread.
+  RefPtr<ContentParent> contentParent;
+  mContentParent.swap(contentParent);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+      !MayProceedOnNonOwningThread()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  const PrincipalInfo& principalInfo = mParams.principalInfo();
+
+  if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+    QuotaManager::GetInfoForChrome(&mSuffix, &mGroup, &mOrigin);
+  } else {
+    MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+
+    nsresult rv;
+    nsCOMPtr<nsIPrincipal> principal =
+      PrincipalInfoToPrincipal(principalInfo, &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = QuotaManager::GetInfoFromPrincipal(principal,
+                                            &mSuffix,
+                                            &mGroup,
+                                            &mMainThreadOrigin);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = principal->GetPrivateBrowsingId(&mPrivateBrowsingId);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(principal);
+    if (NS_WARN_IF(!mArchivedOriginScope)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  // This service has to be started on the main thread currently.
+  nsCOMPtr<mozIStorageService> ss;
+  if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv = QuotaClient::RegisterObservers(OwningEventTarget());
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  mState = State::Nesting;
+  mNestedState = NestedState::CheckExistingOperations;
+
+  MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::CheckExistingOperations()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations);
+  MOZ_ASSERT(gPrepareDatastoreOps);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Normally it's safe to access member variables without a mutex because even
+  // though we hop between threads, the variables are never accessed by multiple
+  // threads at the same time.
+  // However, the methods OriginIsKnown and Origin can be called at any time.
+  // So we have to make sure the member variable is set on the same thread as
+  // those methods are called.
+  mOrigin = mMainThreadOrigin;
+
+  MOZ_ASSERT(!mOrigin.IsEmpty());
+
+  mNestedState = NestedState::CheckClosingDatastore;
+
+  // See if this PrepareDatastoreOp needs to wait.
+  bool foundThis = false;
+  for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; index--) {
+    PrepareDatastoreOp* existingOp = (*gPrepareDatastoreOps)[index - 1];
+
+    if (existingOp == this) {
+      foundThis = true;
+      continue;
+    }
+
+    if (foundThis && existingOp->Origin() == mOrigin) {
+      // Only one op can be delayed.
+      MOZ_ASSERT(!existingOp->mDelayedOp);
+      existingOp->mDelayedOp = this;
+
+      return NS_OK;
+    }
+  }
+
+  nsresult rv = CheckClosingDatastoreInternal();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::CheckClosingDatastore()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv = CheckClosingDatastoreInternal();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::CheckClosingDatastoreInternal()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+  MOZ_ASSERT(MayProceed());
+
+  mNestedState = NestedState::PreparationPending;
+
+  RefPtr<Datastore> datastore;
+  if (gDatastores &&
+      (datastore = gDatastores->Get(mOrigin)) &&
+      datastore->IsClosed()) {
+    datastore->WaitForConnectionToComplete(this);
+
+    return NS_OK;
+  }
+
+  nsresult rv = BeginDatastorePreparationInternal();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::BeginDatastorePreparation()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv = BeginDatastorePreparationInternal();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::BeginDatastorePreparationInternal()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+  MOZ_ASSERT(MayProceed());
+
+  if (gDatastores && (mDatastore = gDatastores->Get(mOrigin))) {
+    MOZ_ASSERT(!mDatastore->IsClosed());
+
+    mDatastore->NoteLivePrepareDatastoreOp(this);
+
+    FinishNesting();
+
+    return NS_OK;
+  }
+
+  if (QuotaManager::Get()) {
+    nsresult rv = OpenDirectory();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    return NS_OK;
+  }
+
+  mNestedState = NestedState::QuotaManagerPending;
+  QuotaManager::GetOrCreate(this, mMainEventTarget);
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::QuotaManagerOpen()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::QuotaManagerPending);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (NS_WARN_IF(!QuotaManager::Get())) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv = OpenDirectory();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::OpenDirectory()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::PreparationPending ||
+             mNestedState == NestedState::QuotaManagerPending);
+  MOZ_ASSERT(!mOrigin.IsEmpty());
+  MOZ_ASSERT(!mDirectoryLock);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+  MOZ_ASSERT(MayProceed());
+  MOZ_ASSERT(QuotaManager::Get());
+
+  mNestedState = NestedState::DirectoryOpenPending;
+  QuotaManager::Get()->OpenDirectory(PERSISTENCE_TYPE_DEFAULT,
+                                     mGroup,
+                                     mOrigin,
+                                     mozilla::dom::quota::Client::LS,
+                                     /* aExclusive */ false,
+                                     this);
+
+  mRequestedDirectoryLock = true;
+
+  return NS_OK;
+}
+
+void
+PrepareDatastoreOp::SendToIOThread()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+  MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+  MOZ_ASSERT(MayProceed());
+
+  // Skip all disk related stuff and transition to SendingReadyMessage if we
+  // are preparing a datastore for private browsing.
+  // Note that we do use a directory lock for private browsing even though we
+  // don't do any stuff on disk. The thing is that without a directory lock,
+  // quota manager wouldn't call AbortOperations for our private browsing
+  // origin when a clear origin operation is requested. AbortOperations
+  // requests all databases to close and the datastore is destroyed in the end.
+  // Any following LocalStorage API call will trigger preparation of a new
+  // (empty) datastore.
+  if (mPrivateBrowsingId) {
+    FinishNesting();
+
+    return;
+  }
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  // Must set this before dispatching otherwise we will race with the IO thread.
+  mNestedState = NestedState::DatabaseWorkOpen;
+
+  MOZ_ALWAYS_SUCCEEDS(
+    quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+nsresult
+PrepareDatastoreOp::DatabaseWork()
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(mArchivedOriginScope);
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+      !MayProceedOnNonOwningThread()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  nsresult rv;
+
+  if (!gArchivedOrigins) {
+    rv = LoadArchivedOrigins();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+    MOZ_ASSERT(gArchivedOrigins);
+  }
+
+  bool hasDataForMigration = mArchivedOriginScope->HasMatches(gArchivedOrigins);
+
+  bool createIfNotExists = mParams.createIfNotExists() || hasDataForMigration;
+
+  nsCOMPtr<nsIFile> directoryEntry;
+  rv = quotaManager->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT,
+                                               mSuffix,
+                                               mGroup,
+                                               mOrigin,
+                                               createIfNotExists,
+                                               getter_AddRefs(directoryEntry));
+  if (rv == NS_ERROR_NOT_AVAILABLE) {
+    return DatabaseNotAvailable();
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = directoryEntry->Append(NS_LITERAL_STRING(LS_DIRECTORY_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = EnsureDirectoryEntry(directoryEntry,
+                            createIfNotExists,
+                            /* aIsDirectory */ true);
+  if (rv == NS_ERROR_NOT_AVAILABLE) {
+    return DatabaseNotAvailable();
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = directoryEntry->Append(NS_LITERAL_STRING(DATA_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool alreadyExisted;
+  rv = EnsureDirectoryEntry(directoryEntry,
+                            createIfNotExists,
+                            /* aIsDirectory */ false,
+                            &alreadyExisted);
+  if (rv == NS_ERROR_NOT_AVAILABLE) {
+    return DatabaseNotAvailable();
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (alreadyExisted) {
+    MOZ_ASSERT(gUsages);
+    MOZ_ASSERT(gUsages->Get(mOrigin, &mUsage));
+  } else {
+    MOZ_ASSERT(mUsage == 0);
+    InitUsageForOrigin(mOrigin, mUsage);
+  }
+
+  rv = directoryEntry->GetPath(mDatabaseFilePath);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = CreateStorageConnection(directoryEntry,
+                               mOrigin,
+                               getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = VerifyDatabaseInformation(connection);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (hasDataForMigration) {
+    MOZ_ASSERT(mUsage == 0);
+
+    rv = AttachArchiveDatabase(quotaManager->GetStoragePath(), connection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    int64_t newUsage;
+    rv = GetUsage(connection, mArchivedOriginScope, &newUsage);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    RefPtr<QuotaObject> quotaObject = GetQuotaObject();
+    MOZ_ASSERT(quotaObject);
+
+    if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+      return NS_ERROR_FILE_NO_DEVICE_SPACE;
+    }
+
+    mozStorageTransaction transaction(connection, false,
+                                  mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+      "INSERT INTO data (key, value) "
+        "SELECT key, value "
+        "FROM webappsstore2 "
+        "WHERE originKey = :originKey "
+        "AND originAttributes = :originAttributes;"
+
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = mArchivedOriginScope->BindToStatement(stmt);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = stmt->Execute();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+      "DELETE FROM webappsstore2 "
+        "WHERE originKey = :originKey "
+        "AND originAttributes = :originAttributes;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = mArchivedOriginScope->BindToStatement(stmt);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = stmt->Execute();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = transaction.Commit();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = DetachArchiveDatabase(connection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    MOZ_ASSERT(gArchivedOrigins);
+    MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins));
+    mArchivedOriginScope->RemoveMatches(gArchivedOrigins);
+
+    mUsage = newUsage;
+
+    MOZ_ASSERT(gUsages);
+    MOZ_ASSERT(gUsages->Contains(mOrigin));
+    gUsages->Put(mOrigin, newUsage);
+  }
+
+  nsCOMPtr<mozIStorageConnection> shadowConnection;
+  if (!gInitializedShadowStorage) {
+    rv = CreateShadowStorageConnection(quotaManager->GetBasePath(),
+                                       getter_AddRefs(shadowConnection));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    gInitializedShadowStorage = true;
+  }
+
+  // Must close connections before dispatching otherwise we might race with the
+  // connection thread which needs to open the same databases.
+  MOZ_ALWAYS_SUCCEEDS(connection->Close());
+
+  if (shadowConnection) {
+    MOZ_ALWAYS_SUCCEEDS(shadowConnection->Close());
+  }
+
+  // Must set this before dispatching otherwise we will race with the owning
+  // thread.
+  mNestedState = NestedState::BeginLoadData;
+
+  rv = OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::DatabaseNotAvailable()
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+  mDatabaseNotAvailable = true;
+
+  nsresult rv = FinishNestingOnNonOwningThread();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry,
+                                         bool aCreateIfNotExists,
+                                         bool aIsDirectory,
+                                         bool* aAlreadyExisted)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aEntry);
+
+  bool exists;
+  nsresult rv = aEntry->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!exists) {
+    if (!aCreateIfNotExists) {
+      return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    if (aIsDirectory) {
+      rv = aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+  }
+#ifdef DEBUG
+  else {
+    bool isDirectory;
+    MOZ_ASSERT(NS_SUCCEEDED(aEntry->IsDirectory(&isDirectory)));
+    MOZ_ASSERT(isDirectory == aIsDirectory);
+  }
+#endif
+
+  if (aAlreadyExisted) {
+    *aAlreadyExisted = exists;
+  }
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::VerifyDatabaseInformation(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT origin "
+    "FROM database"
+  ), getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool hasResult;
+  rv = stmt->ExecuteStep(&hasResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!hasResult)) {
+    return NS_ERROR_FILE_CORRUPTED;
+  }
+
+  nsCString origin;
+  rv = stmt->GetUTF8String(0, origin);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!QuotaManager::AreOriginsEqualOnDisk(mOrigin, origin))) {
+    return NS_ERROR_FILE_CORRUPTED;
+  }
+
+  return NS_OK;
+}
+
+already_AddRefed<QuotaObject>
+PrepareDatastoreOp::GetQuotaObject()
+{
+  MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+  MOZ_ASSERT(!mGroup.IsEmpty());
+  MOZ_ASSERT(!mOrigin.IsEmpty());
+  MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  RefPtr<QuotaObject> quotaObject =
+    quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT,
+                                 mGroup,
+                                 mOrigin,
+                                 mDatabaseFilePath,
+                                 mUsage);
+  MOZ_ASSERT(quotaObject);
+
+  return quotaObject.forget();
+}
+
+nsresult
+PrepareDatastoreOp::BeginLoadData()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+  MOZ_ASSERT(mNestedState == NestedState::BeginLoadData);
+  MOZ_ASSERT(!mConnection);
+
+  if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+      !MayProceed()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (!gConnectionThread) {
+    gConnectionThread = new ConnectionThread();
+  }
+
+  mConnection =
+    gConnectionThread->CreateConnection(mOrigin,
+                                        mDatabaseFilePath,
+                                        std::move(mArchivedOriginScope));
+  MOZ_ASSERT(mConnection);
+
+  // Must set this before dispatching otherwise we will race with the
+  // connection thread.
+  mNestedState = NestedState::DatabaseWorkLoadData;
+
+  // Can't assign to mLoadDataOp directly since that's a weak reference and
+  // LoadDataOp is reference counted.
+  RefPtr<LoadDataOp> loadDataOp = new LoadDataOp(this);
+
+  // This add refs loadDataOp.
+  mConnection->Dispatch(loadDataOp);
+
+  // This is cleared in LoadDataOp::Cleanup() before the load data op is
+  // destroyed.
+  mLoadDataOp = loadDataOp;
+
+  return NS_OK;
+}
+
+void
+PrepareDatastoreOp::FinishNesting()
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::Nesting);
+
+  // The caller holds a strong reference to us, no need for a self reference
+  // before calling Run().
+
+  mState = State::SendingReadyMessage;
+  mNestedState = NestedState::AfterNesting;
+
+  MOZ_ALWAYS_SUCCEEDS(Run());
+}
+
+nsresult
+PrepareDatastoreOp::FinishNestingOnNonOwningThread()
+{
+  MOZ_ASSERT(!IsOnOwningThread());
+  MOZ_ASSERT(mState == State::Nesting);
+
+  // Must set mState before dispatching otherwise we will race with the owning
+  // thread.
+  mState = State::SendingReadyMessage;
+  mNestedState = NestedState::AfterNesting;
+
+  nsresult rv = OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PrepareDatastoreOp::NestedRun()
+{
+  nsresult rv;
+
+  switch (mNestedState) {
+    case NestedState::CheckExistingOperations:
+      rv = CheckExistingOperations();
+      break;
+
+    case NestedState::CheckClosingDatastore:
+      rv = CheckClosingDatastore();
+      break;
+
+    case NestedState::PreparationPending:
+      rv = BeginDatastorePreparation();
+      break;
+
+    case NestedState::QuotaManagerPending:
+      rv = QuotaManagerOpen();
+      break;
+
+    case NestedState::DatabaseWorkOpen:
+      rv = DatabaseWork();
+      break;
+
+    case NestedState::BeginLoadData:
+      rv = BeginLoadData();
+      break;
+
+    default:
+      MOZ_CRASH("Bad state!");
+  }
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    mNestedState = NestedState::AfterNesting;
+
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+void
+PrepareDatastoreOp::GetResponse(LSRequestResponse& aResponse)
+{
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(mState == State::SendingResults);
+  MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+
+  if (mDatabaseNotAvailable) {
+    MOZ_ASSERT(!mParams.createIfNotExists());
+
+    LSRequestPrepareDatastoreResponse prepareDatastoreResponse;
+    prepareDatastoreResponse.datastoreId() = null_t();
+
+    aResponse = prepareDatastoreResponse;
+
+    return;
+  }
+
+  if (!mDatastore) {
+    MOZ_ASSERT(mUsage == mDEBUGUsage);
+
+    RefPtr<QuotaObject> quotaObject;
+
+    if (mPrivateBrowsingId == 0) {
+      quotaObject = GetQuotaObject();
+      MOZ_ASSERT(quotaObject);
+    }
+
+    mDatastore = new Datastore(mOrigin,
+                               mPrivateBrowsingId,
+                               mUsage,
+                               mSizeOfKeys,
+                               mSizeOfItems,
+                               mDirectoryLock.forget(),
+                               mConnection.forget(),
+                               quotaObject.forget(),
+                               mValues,
+                               mOrderedItems);
+
+    mDatastore->NoteLivePrepareDatastoreOp(this);
+
+    if (!gDatastores) {
+      gDatastores = new DatastoreHashtable();
+    }
+
+    MOZ_ASSERT(!gDatastores->Get(mOrigin));
+    gDatastores->Put(mOrigin, mDatastore);
+  }
+
+  uint64_t datastoreId = ++gLastDatastoreId;
+
+  nsAutoPtr<PreparedDatastore> preparedDatastore(
+    new PreparedDatastore(mDatastore,
+                          mContentParentId,
+                          mOrigin,
+                          datastoreId,
+                          /* aForPreload */ !mParams.createIfNotExists()));
+
+  if (!gPreparedDatastores) {
+    gPreparedDatastores = new PreparedDatastoreHashtable();
+  }
+  gPreparedDatastores->Put(datastoreId, preparedDatastore);
+
+  if (mInvalidated) {
+    preparedDatastore->Invalidate();
+  }
+
+  preparedDatastore.forget();
+
+  LSRequestPrepareDatastoreResponse prepareDatastoreResponse;
+  prepareDatastoreResponse.datastoreId() = datastoreId;
+
+  aResponse = prepareDatastoreResponse;
+}
+
+void
+PrepareDatastoreOp::Cleanup()
+{
+  AssertIsOnOwningThread();
+
+  if (mDatastore) {
+    MOZ_ASSERT(!mDirectoryLock);