Bug 1231445 - Part 3: Support for removing IndexedDB records in storage inspector r=mratcliffe
authorJarda Snajdr <jsnajdr@gmail.com>
Mon, 04 Jul 2016 04:10:00 +0200
changeset 303538 caf2ddae3120ced2ddd5c9c50d186ff862665c34
parent 303537 8f8bfc4fab08c56e355c470a26b704f648782db7
child 303539 d06d81ac4cf252b5ee1521396524f908938034ac
push id30393
push userphilringnalda@gmail.com
push dateMon, 04 Jul 2016 21:48:04 +0000
treeherdermozilla-central@0842107a80e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmratcliffe
bugs1231445
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1231445 - Part 3: Support for removing IndexedDB records in storage inspector r=mratcliffe
devtools/client/storage/ui.js
devtools/server/actors/storage.js
devtools/shared/specs/storage.js
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -840,26 +840,30 @@ StorageUI.prototype = {
     if (item.length > 2) {
       names = [JSON.stringify(item.slice(2))];
     }
     this.fetchStorageObjects(type, host, names, REASON.NEXT_50_ITEMS);
   },
 
   /**
    * Fires before a cell context menu with the "Delete" action is shown.
-   * If the current storage actor doesn't support removing items, prevent
+   * If the currently selected storage object doesn't support removing items, prevent
    * showing the menu.
    */
   onTablePopupShowing: function (event) {
-    if (!this.getCurrentActor().removeItem) {
+    let selectedItem = this.tree.selectedItem;
+    let type = selectedItem[0];
+    let actor = this.getCurrentActor();
+
+    // IndexedDB only supports removing items from object stores (level 4 of the tree)
+    if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) {
       event.preventDefault();
       return;
     }
 
-    let [type] = this.tree.selectedItem;
     let rowId = this.table.contextMenuRowId;
     let data = this.table.items.get(rowId);
     let name = addEllipsis(data[this.table.uniqueId]);
 
     this._tablePopupDelete.setAttribute("label",
       L10N.getFormatStr("storage.popupMenu.deleteLabel", name));
 
     if (type === "cookies") {
@@ -873,23 +877,30 @@ StorageUI.prototype = {
     }
   },
 
   onTreePopupShowing: function (event) {
     let showMenu = false;
     let selectedItem = this.tree.selectedItem;
 
     if (selectedItem) {
-      // this.currentActor() would return wrong value here
-      let actor = this.storageTypes[selectedItem[0]];
+      let type = selectedItem[0];
+      let actor = this.storageTypes[type];
 
-      let showDeleteAll = selectedItem.length == 2 && actor.removeAll;
+      // The delete all (aka clear) action is displayed for IndexedDB object stores
+      // (level 4 of tree) and for the whole host (level 2 of tree) of other storage
+      // types (cookies, localStorage, ...).
+      let showDeleteAll = actor.removeAll &&
+        (selectedItem.length === (type === "indexedDB" ? 4 : 2));
+
       this._treePopupDeleteAll.hidden = !showDeleteAll;
 
-      let showDeleteDb = selectedItem.length == 3 && actor.removeDatabase;
+      // The action to delete database is available for IndexedDB databases, i.e.,
+      // at level 3 of the tree.
+      let showDeleteDb = actor.removeDatabase && selectedItem.length === 3;
       this._treePopupDeleteDatabase.hidden = !showDeleteDb;
       if (showDeleteDb) {
         let dbName = addEllipsis(selectedItem[2]);
         this._treePopupDeleteDatabase.setAttribute("label",
           L10N.getFormatStr("storage.popupMenu.deleteLabel", dbName));
       }
 
       showMenu = showDeleteAll || showDeleteDb;
@@ -899,35 +910,38 @@ StorageUI.prototype = {
       event.preventDefault();
     }
   },
 
   /**
    * Handles removing an item from the storage
    */
   onRemoveItem: function () {
-    let [, host] = this.tree.selectedItem;
+    let [, host, ...path] = this.tree.selectedItem;
     let actor = this.getCurrentActor();
     let rowId = this.table.contextMenuRowId;
     let data = this.table.items.get(rowId);
-
-    actor.removeItem(host, data[this.table.uniqueId]);
+    let name = data[this.table.uniqueId];
+    if (path.length > 0) {
+      name = JSON.stringify([...path, name]);
+    }
+    actor.removeItem(host, name);
   },
 
   /**
    * Handles removing all items from the storage
    */
   onRemoveAll: function () {
     // Cannot use this.currentActor() if the handler is called from the
     // tree context menu: it returns correct value only after the table
     // data from server are successfully fetched (and that's async).
-    let [type, host] = this.tree.selectedItem;
+    let [type, host, ...path] = this.tree.selectedItem;
     let actor = this.storageTypes[type];
-
-    actor.removeAll(host);
+    let name = path.length > 0 ? JSON.stringify(path) : undefined;
+    actor.removeAll(host, name);
   },
 
   /**
    * Handles removing all cookies with exactly the same domain as the
    * cookie in the selected row.
    */
   onRemoveAllFrom: function () {
     let [, host] = this.tree.selectedItem;
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -1388,16 +1388,40 @@ StorageActors.createActor({
     if (!win) {
       return { error: `Window for host ${host} not found` };
     }
 
     let principal = win.document.nodePrincipal;
     return this.removeDB(host, principal, name);
   }),
 
+  removeAll: Task.async(function* (host, name) {
+    let [db, store] = JSON.parse(name);
+
+    let win = this.storageActor.getWindowFromHost(host);
+    if (!win) {
+      return;
+    }
+
+    let principal = win.document.nodePrincipal;
+    this.clearDBStore(host, principal, db, store);
+  }),
+
+  removeItem: Task.async(function* (host, name) {
+    let [db, store, id] = JSON.parse(name);
+
+    let win = this.storageActor.getWindowFromHost(host);
+    if (!win) {
+      return;
+    }
+
+    let principal = win.document.nodePrincipal;
+    this.removeDBRecord(host, principal, db, store, id);
+  }),
+
   getHostName(location) {
     if (!location.host) {
       return location.href;
     }
     return location.protocol + "//" + location.host;
   },
 
   /**
@@ -1548,71 +1572,74 @@ StorageActors.createActor({
     }
 
     return {
       actor: this.actorID,
       hosts: hosts
     };
   },
 
-  onDatabaseRemoved(host, name) {
-    if (this.hostVsStores.has(host)) {
-      this.hostVsStores.get(host).delete(name);
+  onItemUpdated(action, host, path) {
+    // Database was removed, remove it from stores map
+    if (action === "deleted" && path.length === 1) {
+      if (this.hostVsStores.has(host)) {
+        this.hostVsStores.get(host).delete(path[0]);
+      }
     }
 
-    this.storageActor.update("deleted", "indexedDB", {
-      [host]: [ JSON.stringify([name]) ]
+    this.storageActor.update(action, "indexedDB", {
+      [host]: [ JSON.stringify(path) ]
     });
   },
 
   maybeSetupChildProcess() {
     if (!DebuggerServer.isInChildProcess) {
       this.backToChild = (func, rv) => rv;
       this.getDBMetaData = indexedDBHelpers.getDBMetaData;
       this.openWithPrincipal = indexedDBHelpers.openWithPrincipal;
       this.getDBNamesForHost = indexedDBHelpers.getDBNamesForHost;
       this.getSanitizedHost = indexedDBHelpers.getSanitizedHost;
       this.getNameFromDatabaseFile = indexedDBHelpers.getNameFromDatabaseFile;
       this.getValuesForHost = indexedDBHelpers.getValuesForHost;
       this.getObjectStoreData = indexedDBHelpers.getObjectStoreData;
       this.removeDB = indexedDBHelpers.removeDB;
+      this.removeDBRecord = indexedDBHelpers.removeDBRecord;
+      this.clearDBStore = indexedDBHelpers.clearDBStore;
       return;
     }
 
     const { sendAsyncMessage, addMessageListener } =
       this.conn.parentMessageManager;
 
     this.conn.setupInParent({
       module: "devtools/server/actors/storage",
       setupParent: "setupParentProcessForIndexedDB"
     });
 
-    this.getDBMetaData =
-      callParentProcessAsync.bind(null, "getDBMetaData");
-    this.getDBNamesForHost =
-      callParentProcessAsync.bind(null, "getDBNamesForHost");
-    this.getValuesForHost =
-      callParentProcessAsync.bind(null, "getValuesForHost");
-    this.removeDB =
-      callParentProcessAsync.bind(null, "removeDB");
+    this.getDBMetaData = callParentProcessAsync.bind(null, "getDBMetaData");
+    this.getDBNamesForHost = callParentProcessAsync.bind(null, "getDBNamesForHost");
+    this.getValuesForHost = callParentProcessAsync.bind(null, "getValuesForHost");
+    this.removeDB = callParentProcessAsync.bind(null, "removeDB");
+    this.removeDBRecord = callParentProcessAsync.bind(null, "removeDBRecord");
+    this.clearDBStore = callParentProcessAsync.bind(null, "clearDBStore");
 
     addMessageListener("storage:storage-indexedDB-request-child", msg => {
       switch (msg.json.method) {
         case "backToChild": {
           let [func, rv] = msg.json.args;
           let deferred = unresolvedPromises.get(func);
           if (deferred) {
             unresolvedPromises.delete(func);
             deferred.resolve(rv);
           }
           break;
         }
-        case "onDatabaseRemoved": {
-          let [host, name] = msg.json.args;
-          this.onDatabaseRemoved(host, name);
+        case "onItemUpdated": {
+          let [action, host, path] = msg.json.args;
+          this.onItemUpdated(action, host, path);
         }
       }
     });
 
     let unresolvedPromises = new Map();
     function callParentProcessAsync(methodName, ...args) {
       let deferred = promise.defer();
 
@@ -1634,23 +1661,23 @@ var indexedDBHelpers = {
                .getService(Ci.nsIMessageListenerManager);
 
     mm.broadcastAsyncMessage("storage:storage-indexedDB-request-child", {
       method: "backToChild",
       args: args
     });
   },
 
-  onDatabaseRemoved: function (host, name) {
+  onItemUpdated(action, host, path) {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
 
     mm.broadcastAsyncMessage("storage:storage-indexedDB-request-child", {
-      method: "onDatabaseRemoved",
-      args: [ host, name ]
+      method: "onItemUpdated",
+      args: [ action, host, path ]
     });
   },
 
   /**
    * Fetches and stores all the metadata information for the given database
    * `name` for the given `host` with its `principal`. The stored metadata
    * information is of `DatabaseMetadata` type.
    */
@@ -1661,62 +1688,126 @@ var indexedDBHelpers = {
     request.onsuccess = event => {
       let db = event.target.result;
 
       let dbData = new DatabaseMetadata(host, db);
       db.close();
 
       success.resolve(this.backToChild("getDBMetaData", dbData));
     };
-    request.onerror = () => {
+    request.onerror = ({target}) => {
       console.error(
-        `Error opening indexeddb database ${name} for host ${host}`);
+        `Error opening indexeddb database ${name} for host ${host}`, target.error);
       success.resolve(this.backToChild("getDBMetaData", null));
     };
     return success.promise;
   }),
 
   /**
    * Opens an indexed db connection for the given `principal` and
    * database `name`.
    */
   openWithPrincipal(principal, name) {
     return require("indexedDB").openForPrincipal(principal, name);
   },
 
   removeDB: Task.async(function* (host, principal, name) {
-    let request = require("indexedDB").deleteForPrincipal(principal, name);
+    let result = new promise(resolve => {
+      let request = require("indexedDB").deleteForPrincipal(principal, name);
 
-    let result = new promise(resolve => {
       request.onsuccess = () => {
         resolve({});
-        this.onDatabaseRemoved(host, name);
+        this.onItemUpdated("deleted", host, [name]);
       };
 
       request.onblocked = () => {
-        console.error(
-          `Deleting indexedDB database ${name} for host ${host} is blocked`);
+        console.warn(`Deleting indexedDB database ${name} for host ${host} is blocked`);
         resolve({ blocked: true });
       };
 
       request.onerror = () => {
-        console.error(
-          `Error deleting indexedDB database ${name} for host ${host}`);
-        resolve({ error: request.error });
+        let { error } = request;
+        console.warn(
+          `Error deleting indexedDB database ${name} for host ${host}: ${error}`);
+        resolve({ error: error.message });
       };
 
       // If the database is blocked repeatedly, the onblocked event will not
       // be fired again. To avoid waiting forever, report as blocked if nothing
       // else happens after 3 seconds.
       setTimeout(() => resolve({ blocked: true }), 3000);
     });
 
     return this.backToChild("removeDB", yield result);
   }),
 
+  removeDBRecord: Task.async(function* (host, principal, dbName, storeName, id) {
+    let db;
+
+    try {
+      db = yield new promise((resolve, reject) => {
+        let request = this.openWithPrincipal(principal, dbName);
+        request.onsuccess = ev => resolve(ev.target.result);
+        request.onerror = ev => reject(ev.target.error);
+      });
+
+      let transaction = db.transaction(storeName, "readwrite");
+      let store = transaction.objectStore(storeName);
+
+      yield new promise((resolve, reject) => {
+        let request = store.delete(id);
+        request.onsuccess = () => resolve();
+        request.onerror = ev => reject(ev.target.error);
+      });
+
+      this.onItemUpdated("deleted", host, [dbName, storeName, id]);
+    } catch (error) {
+      let recordPath = [dbName, storeName, id].join("/");
+      console.error(`Failed to delete indexedDB record: ${recordPath}: ${error}`);
+    }
+
+    if (db) {
+      db.close();
+    }
+
+    return this.backToChild("removeDBRecord", null);
+  }),
+
+  clearDBStore: Task.async(function* (host, principal, dbName, storeName) {
+    let db;
+
+    try {
+      db = yield new promise((resolve, reject) => {
+        let request = this.openWithPrincipal(principal, dbName);
+        request.onsuccess = ev => resolve(ev.target.result);
+        request.onerror = ev => reject(ev.target.error);
+      });
+
+      let transaction = db.transaction(storeName, "readwrite");
+      let store = transaction.objectStore(storeName);
+
+      yield new promise((resolve, reject) => {
+        let request = store.clear();
+        request.onsuccess = () => resolve();
+        request.onerror = ev => reject(ev.target.error);
+      });
+
+      this.onItemUpdated("cleared", host, [dbName, storeName]);
+    } catch (error) {
+      let storePath = [dbName, storeName].join("/");
+      console.error(`Failed to clear indexedDB store: ${storePath}: ${error}`);
+    }
+
+    if (db) {
+      db.close();
+    }
+
+    return this.backToChild("clearDBStore", null);
+  }),
+
   /**
    * Fetches all the databases and their metadata for the given `host`.
    */
   getDBNamesForHost: Task.async(function* (host) {
     let sanitizedHost = this.getSanitizedHost(host);
     let directory = OS.Path.join(OS.Constants.Path.profileDir, "storage",
                                  "default", sanitizedHost, "idb");
 
@@ -1982,16 +2073,24 @@ var indexedDBHelpers = {
         let [host, name, options, hostVsStores, principal] = args;
         return indexedDBHelpers.getValuesForHost(host, name, options,
                                                  hostVsStores, principal);
       }
       case "removeDB": {
         let [host, principal, name] = args;
         return indexedDBHelpers.removeDB(host, principal, name);
       }
+      case "removeDBRecord": {
+        let [host, principal, db, store, id] = args;
+        return indexedDBHelpers.removeDBRecord(host, principal, db, store, id);
+      }
+      case "clearDBStore": {
+        let [host, principal, db, store] = args;
+        return indexedDBHelpers.clearDBStore(host, principal, db, store);
+      }
       default:
         console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
         throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
     }
   }
 };
 
 /**
--- a/devtools/shared/specs/storage.js
+++ b/devtools/shared/specs/storage.js
@@ -182,17 +182,31 @@ createStorageSpec({
   storeObjectType: "idbstoreobject",
   methods: {
     removeDatabase: {
       request: {
         host: Arg(0, "string"),
         name: Arg(1, "string"),
       },
       response: RetVal("idbdeleteresult")
-    }
+    },
+    removeAll: {
+      request: {
+        host: Arg(0, "string"),
+        name: Arg(1, "string"),
+      },
+      response: {}
+    },
+    removeItem: {
+      request: {
+        host: Arg(0, "string"),
+        name: Arg(1, "string"),
+      },
+      response: {}
+    },
   }
 });
 
 // Update notification object
 types.addDictType("storeUpdateObject", {
   changed: "nullable:json",
   deleted: "nullable:json",
   added: "nullable:json"