Bug 946316 - Allow the use of strings as DataStore IDs. r=ehsan, a=1.3+
☠☠ backed out by edd34e3ec168 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 19 Dec 2013 09:07:21 +0000
changeset 175942 3fe07c50c854223ecb154810f5046310b3e15827
parent 175941 1a55393434e0b7080407d5f0aa07997e08d420c1
child 175943 d17316ce716eef5aeadaf4e6e3203cb62f2bd843
push id445
push userffxbld
push dateMon, 10 Mar 2014 22:05:19 +0000
treeherdermozilla-release@dc38b741b04e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, 1.3
bugs946316
milestone28.0a2
Bug 946316 - Allow the use of strings as DataStore IDs. r=ehsan, a=1.3+
dom/datastore/DataStore.jsm
dom/datastore/DataStoreCursor.jsm
dom/datastore/tests/file_arrays.html
dom/datastore/tests/file_basic.html
dom/datastore/tests/file_keys.html
dom/datastore/tests/file_sync.html
dom/datastore/tests/mochitest.ini
dom/datastore/tests/test_keys.html
dom/webidl/DataStore.webidl
dom/webidl/DataStoreChangeEvent.webidl
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -44,34 +44,24 @@ function throwInvalidArg(aWindow) {
     new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
 }
 
 function throwReadOnly(aWindow) {
   return aWindow.Promise.reject(
     new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
 }
 
-function parseIds(aId) {
-  function parseId(aId) {
-    aId = parseInt(aId);
-    return (isNaN(aId) || aId <= 0) ? null : aId;
+function validateId(aId) {
+  // If string, it cannot be empty.
+  if (typeof(aId) == 'string') {
+    return aId.length;
   }
 
-  if (!Array.isArray(aId)) {
-    return parseId(aId);
-  }
-
-  for (let i = 0; i < aId.length; ++i) {
-    aId[i] = parseId(aId[i]);
-    if (aId[i] === null) {
-      return null;
-    }
-  }
-
-  return aId;
+  aId = parseInt(aId);
+  return (!isNaN(aId) && aId > 0);
 }
 
 /* DataStore object */
 this.DataStore = function(aWindow, aName, aOwner, aReadOnly) {
   debug("DataStore created");
   this.init(aWindow, aName, aOwner, aReadOnly);
 }
 
@@ -360,39 +350,39 @@ this.DataStore.prototype = {
   get owner() {
     return this._owner;
   },
 
   get readOnly() {
     return this._readOnly;
   },
 
-  get: function(aId) {
-    aId = parseIds(aId);
-    if (aId === null) {
-      return throwInvalidArg(this._window);
+  get: function() {
+    let ids = Array.prototype.slice.call(arguments);
+    for (let i = 0; i < ids.length; ++i) {
+      if (!validateId(ids[i])) {
+        return throwInvalidArg(this._window);
+      }
     }
 
     let self = this;
 
     // Promise<Object>
     return this.newDBPromise("readonly",
       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
-               self.getInternal(aStore,
-                                Array.isArray(aId) ?  aId : [ aId ],
+               self.getInternal(aStore, ids,
                                 function(aResults) {
-          aResolve(Array.isArray(aId) ? aResults : aResults[0]);
+          aResolve(ids.length > 1 ? aResults : aResults[0]);
         });
       }
     );
   },
 
   put: function(aObj, aId) {
-    aId = parseInt(aId);
-    if (isNaN(aId) || aId <= 0) {
+    if (!validateId(aId)) {
       return throwInvalidArg(this._window);
     }
 
     if (this._readOnly) {
       return throwReadOnly(this._window);
     }
 
     let self = this;
@@ -402,18 +392,17 @@ this.DataStore.prototype = {
       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
         self.putInternal(aResolve, aStore, aRevisionStore, aObj, aId);
       }
     );
   },
 
   add: function(aObj, aId) {
     if (aId) {
-      aId = parseInt(aId);
-      if (isNaN(aId) || aId <= 0) {
+      if (!validateId(aId)) {
         return throwInvalidArg(this._window);
       }
     }
 
     if (this._readOnly) {
       return throwReadOnly(this._window);
     }
 
@@ -423,18 +412,17 @@ this.DataStore.prototype = {
     return this.newDBPromise("readwrite",
       function(aResolve, aReject, aTxn, aStore, aRevisionStore) {
         self.addInternal(aResolve, aStore, aRevisionStore, aObj, aId);
       }
     );
   },
 
   remove: function(aId) {
-    aId = parseInt(aId);
-    if (isNaN(aId) || aId <= 0) {
+    if (!validateId(aId)) {
       return throwInvalidArg(this._window);
     }
 
     if (this._readOnly) {
       return throwReadOnly(this._window);
     }
 
     let self = this;
--- a/dom/datastore/DataStoreCursor.jsm
+++ b/dom/datastore/DataStoreCursor.jsm
@@ -29,37 +29,37 @@ const REVISION_SKIP = 'skip'
 
 Cu.import('resource://gre/modules/ObjectWrapper.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 /**
  * legend:
  * - RID = revision ID
  * - R = revision object (with the internalRevisionId that is a number)
- * - X = current object ID. Default value is 0
- * - MX = max known object ID
+ * - X = current object ID.
  * - L = the list of revisions that we have to send
  *
  * State: init: do you have RID ?
  *   YES: state->initRevision; loop
- *   NO: get R; get MX; state->sendAll; send a 'clear'
+ *   NO: get R; X=0; state->sendAll; send a 'clear'
  *
  * State: initRevision. Get R from RID. Done?
  *   YES: state->revisionCheck; loop
  *   NO: RID = null; state->init; loop
  *
  * State: revisionCheck: get all the revisions between R and NOW. Done?
  *   YES and R == NOW: state->done; loop
  *   YES and R != NOW: Store this revisions in L; state->revisionSend; loop
- *   NO: R = NOW; get MX; state->sendAll; send a 'clear';
+ *   NO: R = NOW; X=0; state->sendAll; send a 'clear'
  *
- * State: sendAll: get the first object with id > X. Done?
- *   YES and object.id > MX: state->revisionCheck; loop
- *   YES and object.id <= MX: X = object.id; send 'add'
- *   NO: state->revisionCheck; loop
+ * State: sendAll: is R still the last revision?
+ *   YES get the first object with id > X. Done?
+ *     YES: X = object.id; send 'add'
+ *     NO: state->revisionCheck; loop
+ *   NO: R = NOW; X=0; send a 'clear'
  *
  * State: revisionSend: do you have something from L to send?
  *   YES and L[0] == 'removed': R=L[0]; send 'remove' with ID
  *   YES and L[0] == 'added': R=L[0]; get the object; found?
  *     NO: loop
  *     YES: send 'add' with ID and object
  *   YES and L[0] == 'updated': R=L[0]; get the object; found?
  *     NO: loop
@@ -88,17 +88,16 @@ this.DataStoreCursor.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]),
 
   _window: null,
   _dataStore: null,
   _revisionId: null,
   _revision: null,
   _revisionsList: null,
   _objectId: 0,
-  _maxObjectId: 0,
 
   _state: STATE_INIT,
 
   init: function(aWindow, aDataStore, aRevisionId) {
     debug('DataStoreCursor init');
 
     this._window = aWindow;
     this._dataStore = aDataStore;
@@ -145,36 +144,34 @@ this.DataStoreCursor.prototype = {
       this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
       return;
     }
 
     let self = this;
     let request = aRevisionStore.openCursor(null, 'prev');
     request.onsuccess = function(aEvent) {
       self._revision = aEvent.target.result.value;
-      self.getMaxObjectId(aStore,
-        function() {
-          self._state = STATE_SEND_ALL;
-          aResolve(ObjectWrapper.wrap({ operation: 'clear' }, self._window));
-        }
-      );
+      self._objectId = 0;
+      self._state = STATE_SEND_ALL;
+      aResolve(ObjectWrapper.wrap({ operation: 'clear' }, self._window));
     }
   },
 
   stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) {
     debug('StateMachineRevisionInit');
 
     let self = this;
     let request = this._dataStore._db.getInternalRevisionId(
       self._revisionId,
       aRevisionStore,
       function(aInternalRevisionId) {
         // This revision doesn't exist.
         if (aInternalRevisionId == undefined) {
           self._revisionId = null;
+          self._objectId = 0;
           self._state = STATE_INIT;
           self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
           return;
         }
 
         self._revision = { revisionId: self._revisionId,
                            internalRevisionId: aInternalRevisionId };
         self._state = STATE_REVISION_CHECK;
@@ -233,23 +230,20 @@ this.DataStoreCursor.prototype = {
             break;
 
           case REVISION_VOID:
             if (i != 0) {
               dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
               return;
             }
 
-            self.getMaxObjectId(aStore,
-              function() {
-                self._revisionId = null;
-                self._state = STATE_INIT;
-                self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
-              }
-            );
+            self._revisionId = null;
+            self._objectId = 0;
+            self._state = STATE_INIT;
+            self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
             return;
         }
       }
 
       // From changes to a map of internalRevisionId.
       let revisions = {};
       function addRevisions(obj) {
         for (let key in obj) {
@@ -287,28 +281,38 @@ this.DataStoreCursor.prototype = {
       self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
     };
   },
 
   stateMachineSendAll: function(aStore, aRevisionStore, aResolve, aReject) {
     debug('StateMachineSendAll');
 
     let self = this;
-    let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(this._objectId, true));
+    let request = aRevisionStore.openCursor(null, 'prev');
     request.onsuccess = function(aEvent) {
-      let cursor = aEvent.target.result;
-      if (!cursor || cursor.key > self._maxObjectId) {
-        self._state = STATE_REVISION_CHECK;
-        self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
+      if (self._revision.revisionId != aEvent.target.result.value.revisionId) {
+        self._revision = aEvent.target.result.value;
+        self._objectId = 0;
+        aResolve(ObjectWrapper.wrap({ operation: 'clear' }, self._window));
         return;
       }
 
-      self._objectId = cursor.key;
-      aResolve(ObjectWrapper.wrap({ operation: 'add', id: self._objectId,
-                                    data: cursor.value }, self._window));
+      let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true));
+      request.onsuccess = function(aEvent) {
+        let cursor = aEvent.target.result;
+        if (!cursor) {
+          self._state = STATE_REVISION_CHECK;
+          self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
+          return;
+        }
+
+        self._objectId = cursor.key;
+        aResolve(ObjectWrapper.wrap({ operation: 'add', id: self._objectId,
+                                      data: cursor.value }, self._window));
+      };
     };
   },
 
   stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) {
     debug('StateMachineRevisionSend');
 
     if (!this._revisionsList.length) {
       this._state = STATE_REVISION_CHECK;
@@ -372,27 +376,16 @@ this.DataStoreCursor.prototype = {
   },
 
   stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) {
     this.close();
     aResolve(ObjectWrapper.wrap({ revisionId: this._revision.revisionId,
                                   operation: 'done' }, this._window));
   },
 
-  getMaxObjectId: function(aStore, aCallback) {
-    let self = this;
-    let request = aStore.openCursor(null, 'prev');
-    request.onsuccess = function(aEvent) {
-      if (aEvent.target.result) {
-        self._maxObjectId = aEvent.target.result.key;
-      }
-      aCallback();
-    }
-  },
-
   // public interface
 
   get store() {
     return this._dataStore.exposedObject;
   },
 
   next: function() {
     debug('Next');
--- a/dom/datastore/tests/file_arrays.html
+++ b/dom/datastore/tests/file_arrays.html
@@ -71,17 +71,17 @@
   }
 
   function testStoreGet() {
     var objects = [];
     for (var i = 1; i <= itemNumber; ++i) {
       objects.push(i);
     }
 
-    gStore.get(objects).then(function(data) {
+    gStore.get.apply(gStore, objects).then(function(data) {
        is(data.length, objects.length, "Get - Data matches");
        for (var i = 0; i < data.length; ++i) {
          is(data[i], objects[i] - 1, "Get - Data matches: " + i + " " + data[i] + " == " + objects[i]);
        }
        runTest();
     }, cbError);
   }
 
--- a/dom/datastore/tests/file_basic.html
+++ b/dom/datastore/tests/file_basic.html
@@ -96,17 +96,16 @@
     // Unknown ID
     function() { testStoreGet(42, undefined); },
     function() { testStoreGet(42, undefined); }, // twice
 
     // Add + Get - number
     function() { testStoreAdd(42).then(function(id) {
                    gId = id; runTest(); }, cbError); },
     function() { testStoreGet(gId, 42); },
-    function() { testStoreGet(gId + "", 42); },
 
     // Add + Get - boolean
     function() { testStoreAdd(true).then(function(id) {
                    gId = id; runTest(); }, cbError); },
     function() { testStoreGet(gId, true); },
 
     // Add + Get - string
     function() { testStoreAdd("hello world").then(function(id) {
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/file_keys.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - string or unsigned long keys</title>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var gStore;
+  var gEvent;
+  var gChangeId;
+
+  function is(a, b, msg) {
+    alert((a === b ? 'OK' : 'KO') + ' ' + msg)
+  }
+
+  function ok(a, msg) {
+    alert((a ? 'OK' : 'KO')+ ' ' + msg)
+  }
+
+  function cbError() {
+    alert('KO error');
+  }
+
+  function finish() {
+    alert('DONE');
+  }
+
+  function testGetDataStores() {
+    navigator.getDataStores('foo').then(function(stores) {
+      gStore = stores[0];
+      runTest();
+    }, cbError);
+  }
+
+  function testAdd_noKey(key) {
+    gEvent = 'added';
+    gChangeId = key;
+
+    gStore.add({ a: 42 }).then(function(id) {
+      is(id, key, "Id must be " + key + " received: " + id);
+    });
+  }
+
+  function testAdd_withKey(key) {
+    gEvent = 'added';
+    gChangeId = key;
+
+    gStore.add({ a: 42 }, key).then(function(id) {
+      is(id, key, "Id must be " + key + " received: " + id);
+    });
+  }
+
+  function testPut(key) {
+    gEvent = 'updated';
+    gChangeId = key;
+
+    gStore.put({ a: 42 }, key).then(function(id) {
+      is(id, key, "Id must be " + key + " received: " + id);
+    });
+  }
+
+  function testGet(key) {
+    gStore.get(key).then(function(value) {
+      ok(value, "Object received!");
+      is(value.a, 42, "Object received with right value!");
+      runTest();
+    });
+  }
+
+  function testArrayGet(key) {
+    gStore.get.apply(gStore, key).then(function(values) {
+      is(values.length, key.length, "Object received!");
+      for (var i = 0; i < values.length; ++i) {
+        is(values[i].a, 42, "Object received with right value!");
+      }
+
+      runTest();
+    });
+  }
+
+  function testRemove(key, success) {
+    gEvent = 'removed';
+    gChangeId = key;
+
+    gStore.remove(key).then(function(value) {
+      is(value, success, "Status must be " + success + " received: " + value);
+      if (value == false) {
+        runTest();
+        return;
+      }
+    });
+  }
+
+  function eventListener() {
+    gStore.onchange = function(e) {
+      is(e.operation, gEvent, "Operation matches: " + e.operation + " " + gEvent);
+      ok(e.id === gChangeId, "Operation id matches");
+      runTest();
+    };
+
+    runTest();
+  }
+
+  var tests = [
+    // Test for GetDataStore
+    testGetDataStores,
+
+    // Event listener
+    eventListener,
+
+    // add
+    function() { testAdd_noKey(1); },
+    function() { testAdd_withKey(123); },
+    function() { testAdd_noKey(124); },
+    function() { testAdd_withKey('foobar'); },
+    function() { testAdd_noKey(125); },
+    function() { testAdd_withKey('125'); },
+    function() { testAdd_withKey('126'); },
+    function() { testAdd_noKey(126); },
+
+    // put
+    function() { testPut(42); },
+    function() { testPut('42'); },
+
+    // get
+    function() { testGet('42'); },
+    function() { testGet(42); },
+    function() { testGet(1); },
+    function() { testGet(123); },
+    function() { testGet(124); },
+    function() { testGet('foobar'); },
+    function() { testGet(125); },
+    function() { testGet('125'); },
+    function() { testGet('126'); },
+    function() { testGet(126); },
+    function() { testArrayGet(['42', 42, 1, 123, 124, 'foobar', 125, '125', '126', 126]); },
+
+    // remove
+    function() { testRemove(42, true); },
+    function() { testRemove('42', true); },
+    function() { testRemove('43', false); },
+    function() { testRemove(43, false); },
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  runTest();
+  </script>
+</body>
+</html>
--- a/dom/datastore/tests/file_sync.html
+++ b/dom/datastore/tests/file_sync.html
@@ -46,16 +46,19 @@
   }
 
   function testBasicInterface() {
     var cursor = gStore.sync();
     ok(cursor, "Cursor is created");
     is(cursor.store, gStore, "Cursor.store is the store");
 
     ok("next" in cursor, "Cursor.next exists");
+    ok("close" in cursor, "Cursor.close exists");
+
+    cursor.close();
 
     runTest();
   }
 
   function testCursor(cursor, steps) {
     if (!steps.length) {
       runTest();
       return;
@@ -128,241 +131,319 @@
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     // Test add from scratch
     function() {
       gExpectedEvents = true;
 
-      gStore.add(1,2).then(function(id) {
+      gStore.add(1).then(function(id) {
         gRevisions.push(gStore.revisionId);
-        ok(true, "Iteme: " + id + " added");
+        ok(true, "Item: " + id + " added");
       });
     },
 
     function() {
-      gStore.add(2,3).then(function(id) {
+      gStore.add(2,"foobar").then(function(id) {
         gRevisions.push(gStore.revisionId);
-        ok(true, "Iteme: " + id + " added");
+        ok(true, "Item: " + id + " added");
+      });
+    },
+
+    function() {
+      gStore.add(3,3).then(function(id) {
+        gRevisions.push(gStore.revisionId);
+        ok(true, "Item: " + id + " added");
       });
     },
 
     function() {
       gExpectedEvents = false;
       var cursor = gStore.sync();
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 1 },
-                    { operation: 'add', id: 3, data: 2 },
+                    { operation: 'add', id: 1, data: 1 },
+                    { operation: 'add', id: 3, data: 3 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync('wrong revision ID');
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 1 },
-                    { operation: 'add', id: 3, data: 2 },
+                    { operation: 'add', id: 1, data: 1 },
+                    { operation: 'add', id: 3, data: 3 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[0]);
-      var steps = [ { operation: 'add', id: 2, data: 1 },
-                    { operation: 'add', id: 3, data: 2 },
+      var steps = [ { operation: 'add', id: 1, data: 1 },
+                    { operation: 'add', id: 'foobar', data: 2 },
+                    { operation: 'add', id: 3, data: 3 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[1]);
-      var steps = [ { operation: 'add', id: 3, data: 2 },
+      var steps = [ { operation: 'add', id: 'foobar', data: 2 },
+                    { operation: 'add', id: 3, data: 3 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[2]);
+      var steps = [ { operation: 'add', id: 3, data: 3 },
+                    { operation: 'done' }];
+      testCursor(cursor, steps);
+    },
+
+    function() {
+      var cursor = gStore.sync(gRevisions[3]);
       var steps = [ { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     // Test after an update
     function() {
       gExpectedEvents = true;
-      gStore.put(3, 2).then(function() {
+      gStore.put(123, 1).then(function() {
         gRevisions.push(gStore.revisionId);
       });
     },
 
     function() {
       gExpectedEvents = false;
       var cursor = gStore.sync();
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 3 },
-                    { operation: 'add', id: 3, data: 2 },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 3, data: 3 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync('wrong revision ID');
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 3 },
-                    { operation: 'add', id: 3, data: 2 },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 3, data: 3 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[0]);
-      var steps = [ { operation: 'add', id: 2, data: 3 },
-                    { operation: 'add', id: 3, data: 2 },
+      var steps = [ { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 'foobar', data: 2 },
+                    { operation: 'add', id: 3, data: 3 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[1]);
-      var steps = [ { operation: 'add', id: 3, data: 2 },
-                    { operation: 'update', id: 2, data: 3 },
+      var steps = [ { operation: 'add', id: 'foobar', data: 2 },
+                    { operation: 'add', id: 3, data: 3 },
+                    { operation: 'update', id: 1, data: 123 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[2]);
-      var steps = [ { operation: 'update', id: 2, data: 3 },
+      var steps = [ { operation: 'add', id: 3, data: 3 },
+                    { operation: 'update', id: 1, data: 123 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[3]);
+      var steps = [ { operation: 'update', id: 1, data: 123 },
+                    { operation: 'done' }];
+      testCursor(cursor, steps);
+    },
+
+    function() {
+      var cursor = gStore.sync(gRevisions[4]);
       var steps = [ { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     // Test after a remove
     function() {
       gExpectedEvents = true;
       gStore.remove(3).then(function() {
         gRevisions.push(gStore.revisionId);
       });
     },
 
     function() {
       gExpectedEvents = false;
       var cursor = gStore.sync();
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 3 },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync('wrong revision ID');
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 3 },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[0]);
-      var steps = [ { operation: 'add', id: 2, data: 3 },
+      var steps = [ { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 'foobar', data: 2 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[1]);
-      var steps = [ { operation: 'update', id: 2, data: 3 },
+      var steps = [ { operation: 'add', id: 'foobar', data: 2 },
+                    { operation: 'update', id: 1, data: 123 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[2]);
-      var steps = [ { operation: 'update', id: 2, data: 3 },
-                    { operation: 'remove', id: 3 },
+      var steps = [ { operation: 'update', id: 1, data: 123 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
       var cursor = gStore.sync(gRevisions[3]);
+      var steps = [ { operation: 'update', id: 1, data: 123 },
+                    { operation: 'remove', id: 3 },
+                    { operation: 'done' }];
+      testCursor(cursor, steps);
+    },
+
+    function() {
+      var cursor = gStore.sync(gRevisions[4]);
       var steps = [ { operation: 'remove', id: 3 },
                     { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     function() {
-      var cursor = gStore.sync(gRevisions[4]);
+      var cursor = gStore.sync(gRevisions[5]);
       var steps = [ { operation: 'done' }];
       testCursor(cursor, steps);
     },
 
     // New events when the cursor is active
     function() {
       gCursor = gStore.sync();
       var steps = [ { operation: 'clear', },
-                    { operation: 'add', id: 2, data: 3 } ];
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 'foobar', data: 2 } ];
       testCursor(gCursor, steps);
     },
 
     function() {
-      gStore.add(42).then(function(id) {
+      gStore.add(42, 2).then(function(id) {
         ok(true, "Item: " + id + " added");
         gRevisions.push(gStore.revisionId);
         runTest();
       });
     },
 
-    // New events when the cursor is active
     function() {
-      var steps = [ { operation: 'add', id: 4, data: 42 } ];
+      var steps = [ { operation: 'clear', },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 2, data: 42 },
+		    { operation: 'add', id: 'foobar', data: 2 } ]
       testCursor(gCursor, steps);
     },
 
     function() {
-      gStore.put(42, 2).then(function(id) {
+      gStore.put(43, 2).then(function(id) {
         gRevisions.push(gStore.revisionId);
         runTest();
       });
     },
 
     function() {
-      var steps = [ { operation: 'update', id: 2, data: 42 } ];
+      var steps = [ { operation: 'clear', },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 2, data: 43 },
+		    { operation: 'add', id: 'foobar', data: 2 } ]
       testCursor(gCursor, steps);
     },
 
     function() {
       gStore.remove(2).then(function(id) {
         gRevisions.push(gStore.revisionId);
         runTest();
       });
     },
 
     function() {
-      var steps = [ { operation: 'remove', id: 2 } ];
+      var steps = [ { operation: 'clear', },
+                    { operation: 'add', id: 1, data: 123 },
+		    { operation: 'add', id: 'foobar', data: 2 } ]
       testCursor(gCursor, steps);
     },
 
     function() {
       gStore.add(42).then(function(id) {
         ok(true, "Item: " + id + " added");
         gRevisions.push(gStore.revisionId);
         runTest();
       });
     },
 
     function() {
-      var steps = [ { operation: 'add', id: 5, data: 42 } ];
+      var steps = [ { operation: 'clear', },
+                    { operation: 'add', id: 1, data: 123 },
+                    { operation: 'add', id: 4, data: 42 },
+		    { operation: 'add', id: 'foobar', data: 2 } ]
+      testCursor(gCursor, steps);
+    },
+
+    function() {
+      gStore.clear().then(function() {
+        gRevisions.push(gStore.revisionId);
+        runTest();
+      });
+    },
+
+    function() {
+      var steps = [ { operation: 'clear' } ];
+      testCursor(gCursor, steps);
+    },
+
+    function() {
+      gStore.add(42).then(function(id) {
+        ok(true, "Item: " + id + " added");
+        gRevisions.push(gStore.revisionId);
+        runTest();
+      });
+    },
+
+    function() {
+      var steps = [ { operation: 'clear', },
+                    { operation: 'add', id: 5, data: 42 } ];
       testCursor(gCursor, steps);
     },
 
     function() {
       gStore.clear().then(function() {
         gRevisions.push(gStore.revisionId);
         runTest();
       });
@@ -374,17 +455,17 @@
         gRevisions.push(gStore.revisionId);
         runTest();
       });
     },
 
     function() {
       var steps = [ { operation: 'clear' },
                     { operation: 'add', id: 6, data: 42 },
-                    { operation: 'done' } ];
+                    { operation: 'done'} ];
       testCursor(gCursor, steps);
     },
 
     function() {
       gExpectedEvents = true;
       gStore.add(42).then(function(id) {
       });
     }
--- a/dom/datastore/tests/mochitest.ini
+++ b/dom/datastore/tests/mochitest.ini
@@ -7,18 +7,20 @@ support-files =
   file_changes2.html
   file_app.sjs
   file_app.template.webapp
   file_app2.template.webapp
   file_arrays.html
   file_sync.html
   file_bug924104.html
   file_certifiedApp.html
+  file_keys.html
 
 [test_app_install.html]
 [test_readonly.html]
 [test_basic.html]
 [test_changes.html]
 [test_arrays.html]
 [test_oop.html]
 [test_sync.html]
 [test_bug924104.html]
 [test_certifiedApp.html]
+[test_keys.html]
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_keys.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - string or unsigned long keys</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var gHostedManifestURL = 'http://test/tests/dom/datastore/tests/file_app.sjs?testToken=file_keys.html';
+  var gApp;
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp() {
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApp = request.result;
+      runTest();
+    }
+  }
+
+  function uninstallApp() {
+    // Uninstall the app.
+    var request = navigator.mozApps.mgmt.uninstall(gApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      // All done.
+      info("All done");
+      runTest();
+    }
+  }
+
+  function testApp() {
+    var ifr = document.createElement('iframe');
+    ifr.setAttribute('mozbrowser', 'true');
+    ifr.setAttribute('mozapp', gApp.manifestURL);
+    ifr.setAttribute('src', gApp.manifest.launch_path);
+    var domParent = document.getElementById('container');
+
+    // Set us up to listen for messages from the app.
+    var listener = function(e) {
+      var message = e.detail.message;
+      if (/^OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(ifr);
+        runTest();
+      }
+    }
+
+    // This event is triggered when the app calls "alert".
+    ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
+    domParent.appendChild(ifr);
+  }
+
+  var tests = [
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "browser", "allow": 1, "context": document },
+         { "type": "embed-apps", "allow": 1, "context": document },
+         { "type": "webapps-manage", "allow": 1, "context": document }], runTest);
+    },
+
+    // Preferences
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true],
+                                         ["dom.datastore.enabled", true],
+                                         ["dom.testing.ignore_ipc_principal", true],
+                                         ["dom.testing.datastore_enabled_for_hosted_apps", true]]}, runTest);
+    },
+
+    function() {
+      SpecialPowers.setAllAppsLaunchable(true);
+      SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
+      runTest();
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(runTest);
+    },
+
+    // Installing the app
+    installApp,
+
+    // Run tests in app
+    testApp,
+
+    // Uninstall the app
+    uninstallApp
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  if (SpecialPowers.isMainProcess()) {
+    SpecialPowers.Cu.import("resource://gre/modules/DataStoreChangeNotifier.jsm");
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTest();
+  </script>
+</body>
+</html>
--- a/dom/webidl/DataStore.webidl
+++ b/dom/webidl/DataStore.webidl
@@ -1,41 +1,40 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/.
  */
 
+typedef (DOMString or unsigned long) DataStoreKey;
+
 [Pref="dom.datastore.enabled",
  JSImplementation="@mozilla.org/dom/datastore;1"]
 interface DataStore : EventTarget {
   // Returns the label of the DataSource.
   readonly attribute DOMString name;
 
   // Returns the origin of the DataSource (e.g., 'facebook.com').
   // This value is the manifest URL of the owner app.
   readonly attribute DOMString owner;
 
   // is readOnly a F(current_app, datastore) function? yes
   readonly attribute boolean readOnly;
 
   // Promise<any>
-  Promise get(unsigned long id);
-
-  // Promise<any>
-  Promise get(sequence<unsigned long> id);
+  Promise get(DataStoreKey... id);
 
   // Promise<void>
-  Promise put(any obj, unsigned long id);
+  Promise put(any obj, DataStoreKey id);
 
-  // Promise<unsigned long>
-  Promise add(any obj, optional unsigned long id);
+  // Promise<DataStoreKey>
+  Promise add(any obj, optional DataStoreKey id);
 
   // Promise<boolean>
-  Promise remove(unsigned long id);
+  Promise remove(DataStoreKey id);
 
   // Promise<void>
   Promise clear();
 
   readonly attribute DOMString revisionId;
 
   attribute EventHandler onchange;
 
@@ -65,11 +64,11 @@ enum DataStoreOperation {
   "clear",
   "done"
 };
 
 dictionary DataStoreTask {
   DOMString revisionId;
 
   DataStoreOperation operation;
-  unsigned long id;
+  DataStoreKey id;
   any data;
 };
--- a/dom/webidl/DataStoreChangeEvent.webidl
+++ b/dom/webidl/DataStoreChangeEvent.webidl
@@ -1,19 +1,19 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/.
  */
 
 dictionary DataStoreChangeEventInit : EventInit {
   DOMString revisionId = "";
-  unsigned long id = 0;
+  DataStoreKey id = 0;
   DOMString operation = "";
 };
 
 [Pref="dom.datastore.enabled",
  Constructor(DOMString type, optional DataStoreChangeEventInit eventInitDict)]
 interface DataStoreChangeEvent : Event {
   readonly attribute DOMString revisionId;
-  readonly attribute unsigned long id;
+  readonly attribute DataStoreKey id;
   readonly attribute DOMString operation;
 };