Bug 1134265 - Add async-storage for a simple key value store in devtools. r=jryans
authorBrian Grinstead <bgrinstead@mozilla.com>
Fri, 20 Feb 2015 15:57:00 -0500
changeset 230139 aae9d2200d629ced681f5c734abe6919db8acf67
parent 230138 ab63972551cb18dec790f714dd284daa637f327c
child 230140 de5f7b01ed51f085d18ba8947406a343946541b7
push id28309
push userryanvm@gmail.com
push dateSat, 21 Feb 2015 21:38:24 +0000
treeherdermozilla-central@ef051b501adf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1134265
milestone38.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 1134265 - Add async-storage for a simple key value store in devtools. r=jryans
toolkit/devtools/moz.build
toolkit/devtools/shared/async-storage.js
toolkit/devtools/shared/moz.build
toolkit/devtools/shared/tests/browser/browser.ini
toolkit/devtools/shared/tests/browser/browser_async_storage.js
--- a/toolkit/devtools/moz.build
+++ b/toolkit/devtools/moz.build
@@ -11,16 +11,17 @@ DIRS += [
     'discovery',
     'gcli',
     'jsbeautify',
     'pretty-fast',
     'qrcode',
     'security',
     'server',
     'sourcemap',
+    'shared',
     'styleinspector',
     'tern',
     'transport',
     'webconsole',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/shared/async-storage.js
@@ -0,0 +1,193 @@
+/**
+ *
+ * Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/shared/js/async_storage.js
+ * (converted to use Promises instead of callbacks).
+ *
+ * This file defines an asynchronous version of the localStorage API, backed by
+ * an IndexedDB database.  It creates a global asyncStorage object that has
+ * methods like the localStorage object.
+ *
+ * To store a value use setItem:
+ *
+ *   asyncStorage.setItem("key", "value");
+ *
+ * This returns a promise in case you want confirmation that the value has been stored.
+ *
+ *  asyncStorage.setItem("key", "newvalue").then(function() {
+ *    console.log("new value stored");
+ *  });
+ *
+ * To read a value, call getItem(), but note that you must wait for a promise
+ * resolution for the value to be retrieved.
+ *
+ *  asyncStorage.getItem("key").then(function(value) {
+ *    console.log("The value of key is:", value);
+ *  });
+ *
+ * Note that unlike localStorage, asyncStorage does not allow you to store and
+ * retrieve values by setting and querying properties directly. You cannot just
+ * write asyncStorage.key; you have to explicitly call setItem() or getItem().
+ *
+ * removeItem(), clear(), length(), and key() are like the same-named methods of
+ * localStorage, and all return a promise.
+ *
+ * The asynchronous nature of getItem() makes it tricky to retrieve multiple
+ * values. But unlike localStorage, asyncStorage does not require the values you
+ * store to be strings.  So if you need to save multiple values and want to
+ * retrieve them together, in a single asynchronous operation, just group the
+ * values into a single object. The properties of this object may not include
+ * DOM elements, but they may include things like Blobs and typed arrays.
+ *
+ */
+const {Cc, Ci, Cu, Cr} = require("chrome");
+const {indexedDB} = require("sdk/indexed-db");
+const {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+
+module.exports = (function() {
+  "use strict";
+
+  var DBNAME = "devtools-async-storage";
+  var DBVERSION = 1;
+  var STORENAME = "keyvaluepairs";
+  var db = null;
+
+  function withStore(type, onsuccess, onerror) {
+    if (db) {
+      var transaction = db.transaction(STORENAME, type);
+      var store = transaction.objectStore(STORENAME);
+      onsuccess(store);
+    } else {
+      var openreq = indexedDB.open(DBNAME, DBVERSION);
+      openreq.onerror = function withStoreOnError() {
+        onerror();
+      };
+      openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
+        // First time setup: create an empty object store
+        openreq.result.createObjectStore(STORENAME);
+      };
+      openreq.onsuccess = function withStoreOnSuccess() {
+        db = openreq.result;
+        var transaction = db.transaction(STORENAME, type);
+        var store = transaction.objectStore(STORENAME);
+        onsuccess(store);
+      };
+    }
+  }
+
+  function getItem(key) {
+    return new Promise((resolve, reject) => {
+      var req;
+      withStore("readonly", (store) => {
+        store.transaction.oncomplete = function onComplete() {
+          var value = req.result;
+          if (value === undefined) {
+            value = null;
+          }
+          resolve(value);
+        };
+        req = store.get(key);
+        req.onerror = function getItemOnError() {
+          reject("Error in asyncStorage.getItem(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  function setItem(key, value) {
+    return new Promise((resolve, reject) => {
+      withStore("readwrite", (store) => {
+        store.transaction.oncomplete = resolve;
+        var req = store.put(value, key);
+        req.onerror = function setItemOnError() {
+          reject("Error in asyncStorage.setItem(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  function removeItem(key) {
+    return new Promise((resolve, reject) => {
+      withStore("readwrite", (store) => {
+        store.transaction.oncomplete = resolve;
+        var req = store.delete(key);
+        req.onerror = function removeItemOnError() {
+          reject("Error in asyncStorage.removeItem(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  function clear() {
+    return new Promise((resolve, reject) => {
+      withStore("readwrite", (store) => {
+        store.transaction.oncomplete = resolve;
+        var req = store.clear();
+        req.onerror = function clearOnError() {
+          reject("Error in asyncStorage.clear(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  function length() {
+    return new Promise((resolve, reject) => {
+      var req;
+      withStore("readonly", (store) => {
+        store.transaction.oncomplete = function onComplete() {
+          resolve(req.result);
+        }
+        req = store.count();
+        req.onerror = function lengthOnError() {
+          reject("Error in asyncStorage.length(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  function key(n) {
+    return new Promise((resolve, reject) => {
+      if (n < 0) {
+        resolve(null);
+        return;
+      }
+
+      var req;
+      withStore("readonly", (store) => {
+        store.transaction.oncomplete = function onComplete() {
+          var cursor = req.result;
+          resolve(cursor ? cursor.key : null);
+        };
+        var advanced = false;
+        req = store.openCursor();
+        req.onsuccess = function keyOnSuccess() {
+          var cursor = req.result;
+          if (!cursor) {
+            // this means there weren"t enough keys
+            return;
+          }
+          if (n === 0 || advanced) {
+            // Either 1) we have the first key, return it if that's what they
+            // wanted, or 2) we"ve got the nth key.
+            return;
+          }
+
+          // Otherwise, ask the cursor to skip ahead n records
+          advanced = true;
+          cursor.advance(n);
+        };
+        req.onerror = function keyOnError() {
+          reject("Error in asyncStorage.key(): ", req.error.name);
+        };
+      }, reject);
+    });
+  }
+
+  return {
+    getItem: getItem,
+    setItem: setItem,
+    removeItem: removeItem,
+    clear: clear,
+    length: length,
+    key: key
+  };
+}());
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/shared/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
+EXTRA_JS_MODULES.devtools.shared += [
+  'async-storage.js'
+]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/shared/tests/browser/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+  ../../../server/tests/browser/head.js
+
+[browser_async_storage.js]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/shared/tests/browser/browser_async_storage.js
@@ -0,0 +1,77 @@
+/* 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 the basic functionality of async-storage.
+// Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
+
+const asyncStorage = require("devtools/toolkit/shared/async-storage");
+add_task(function*() {
+  is(typeof asyncStorage.length, 'function', "API exists.");
+  is(typeof asyncStorage.key, 'function', "API exists.");
+  is(typeof asyncStorage.getItem, 'function', "API exists.");
+  is(typeof asyncStorage.setItem, 'function', "API exists.");
+  is(typeof asyncStorage.removeItem, 'function', "API exists.");
+  is(typeof asyncStorage.clear, 'function', "API exists.");
+});
+
+add_task(function*() {
+  yield asyncStorage.setItem('foo', 'bar');
+  let value = yield asyncStorage.getItem('foo');
+  is(value, 'bar', 'value is correct');
+  yield asyncStorage.setItem('foo', 'overwritten');
+  value = yield asyncStorage.getItem('foo');
+  is(value, 'overwritten', 'value is correct');
+  yield asyncStorage.removeItem('foo');
+  value = yield asyncStorage.getItem('foo');
+  is(value, null, 'value is correct');
+});
+
+add_task(function*() {
+  var object = {
+    x: 1,
+    y: 'foo',
+    z: true
+  };
+
+  yield asyncStorage.setItem('myobj', object);
+  let value = yield asyncStorage.getItem('myobj');
+  is(object.x, value.x, 'value is correct');
+  is(object.y, value.y, 'value is correct');
+  is(object.z, value.z, 'value is correct');
+  yield asyncStorage.removeItem('myobj');
+  value = yield asyncStorage.getItem('myobj');
+  is(value, null, 'value is correct');
+});
+
+add_task(function*() {
+  yield asyncStorage.clear();
+  let len = yield asyncStorage.length();
+  is(len, 0, 'length is correct');
+  yield asyncStorage.setItem('key1', 'value1');
+  len = yield asyncStorage.length();
+  is(len, 1, 'length is correct');
+  yield asyncStorage.setItem('key2', 'value2');
+  len = yield asyncStorage.length();
+  is(len, 2, 'length is correct');
+  yield asyncStorage.setItem('key3', 'value3');
+  len = yield asyncStorage.length();
+  is(len, 3, 'length is correct');
+
+  let key = yield asyncStorage.key(0);
+  is(key, 'key1', 'key is correct');
+  key = yield asyncStorage.key(1);
+  is(key, 'key2', 'key is correct');
+  key = yield asyncStorage.key(2);
+  is(key, 'key3', 'key is correct');
+  key = yield asyncStorage.key(3);
+  is(key, null, 'key is correct');
+  yield asyncStorage.clear();
+  key = yield asyncStorage.key(0);
+  is(key, null, 'key is correct');
+
+  len = yield asyncStorage.length();
+  is(len, 0, 'length is correct');
+});