Bug 916445 - DataStore API - sync method, r=ehsan

This commit is contained in:
Andrea Marchesini 2013-10-02 16:39:54 -04:00
parent f32fa0a678
commit 32a9f57d4c
10 changed files with 1034 additions and 0 deletions

View File

@ -23,6 +23,7 @@ const REVISION_VOID = "void";
// and yet we don't know if it's too low or too high.
const MAX_REQUESTS = 25;
Cu.import("resource://gre/modules/DataStoreCursor.jsm");
Cu.import("resource://gre/modules/DataStoreDB.jsm");
Cu.import("resource://gre/modules/ObjectWrapper.jsm");
Cu.import('resource://gre/modules/Services.jsm');
@ -86,6 +87,8 @@ this.DataStore.prototype = {
_owner: null,
_readOnly: null,
_revisionId: null,
_exposedObject: null,
_cursor: null,
init: function(aWindow, aName, aOwner, aReadOnly) {
debug("DataStore init");
@ -317,12 +320,34 @@ this.DataStore.prototype = {
this.retrieveRevisionId(
function() {
// If we have an active cursor we don't emit events.
if (self._cursor) {
return;
}
let event = new self._window.DataStoreChangeEvent('change', aMessage.data);
self.__DOM_IMPL__.dispatchEvent(event);
}
);
},
get exposedObject() {
debug("get exposedObject");
return this._exposedObject;
},
set exposedObject(aObject) {
debug("set exposedObject");
this._exposedObject = aObject;
},
syncTerminated: function(aCursor) {
// This checks is to avoid that an invalid cursor stops a sync.
if (this._cursor == aCursor) {
this._cursor = null;
}
},
// Public interface :
get name() {
@ -548,5 +573,11 @@ this.DataStore.prototype = {
get onchange() {
debug("Get OnChange");
return this.__DOM_IMPL__.getEventHandler("onchange");
},
sync: function(aRevisionId) {
debug("Sync");
this._cursor = new DataStoreCursor(this._window, this, aRevisionId);
return this._window.DataStoreCursor._create(this._window, this._cursor);
}
};

View File

@ -0,0 +1,416 @@
/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* vim: set ft=javascript ts=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/. */
'use strict'
this.EXPORTED_SYMBOLS = ['DataStoreCursor'];
function debug(s) {
// dump('DEBUG DataStoreCursor: ' + s + '\n');
}
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const STATE_INIT = 0;
const STATE_REVISION_INIT = 1;
const STATE_REVISION_CHECK = 2;
const STATE_SEND_ALL = 3;
const STATE_REVISION_SEND = 4;
const STATE_DONE = 5;
const REVISION_ADDED = 'added';
const REVISION_UPDATED = 'updated';
const REVISION_REMOVED = 'removed';
const REVISION_VOID = 'void';
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
* - 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'
*
* 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';
*
* 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: 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
* YES and object.R > R: continue
* YES and object.R <= R: send 'update' with ID and object
* YES L[0] == 'void': R=L[0]; state->init; loop
* NO: state->revisionCheck; loop
*
* State: done: send a 'done' with R
*/
/* Helper functions */
function createDOMError(aWindow, aEvent) {
return new aWindow.DOMError(aEvent.target.error.name);
}
/* DataStoreCursor object */
this.DataStoreCursor = function(aWindow, aDataStore, aRevisionId) {
this.init(aWindow, aDataStore, aRevisionId);
}
this.DataStoreCursor.prototype = {
classDescription: 'DataStoreCursor XPCOM Component',
classID: Components.ID('{b6d14349-1eab-46b8-8513-584a7328a26b}'),
contractID: '@mozilla.org/dom/datastore-cursor;1',
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;
this._revisionId = aRevisionId;
},
// This is the implementation of the state machine.
// Read the comments at the top of this file in order to follow what it does.
stateMachine: function(aStore, aRevisionStore, aResolve, aReject) {
debug('StateMachine: ' + this._state);
switch (this._state) {
case STATE_INIT:
this.stateMachineInit(aStore, aRevisionStore, aResolve, aReject);
break;
case STATE_REVISION_INIT:
this.stateMachineRevisionInit(aStore, aRevisionStore, aResolve, aReject);
break;
case STATE_REVISION_CHECK:
this.stateMachineRevisionCheck(aStore, aRevisionStore, aResolve, aReject);
break;
case STATE_SEND_ALL:
this.stateMachineSendAll(aStore, aRevisionStore, aResolve, aReject);
break;
case STATE_REVISION_SEND:
this.stateMachineRevisionSend(aStore, aRevisionStore, aResolve, aReject);
break;
case STATE_DONE:
this.stateMachineDone(aStore, aRevisionStore, aResolve, aReject);
break;
}
},
stateMachineInit: function(aStore, aRevisionStore, aResolve, aReject) {
debug('StateMachineInit');
if (this._revisionId) {
this._state = STATE_REVISION_INIT;
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));
}
);
}
},
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._state = STATE_INIT;
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
self._revision = { revisionId: self._revisionId,
internalRevisionId: aInternalRevisionId };
self._state = STATE_REVISION_CHECK;
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
}
);
},
stateMachineRevisionCheck: function(aStore, aRevisionStore, aResolve, aReject) {
debug('StateMachineRevisionCheck');
let changes = {
addedIds: {},
updatedIds: {},
removedIds: {}
};
let self = this;
let request = aRevisionStore.mozGetAll(
self._window.IDBKeyRange.lowerBound(this._revision.internalRevisionId, true));
request.onsuccess = function(aEvent) {
// Optimize the operations.
for (let i = 0; i < aEvent.target.result.length; ++i) {
let data = aEvent.target.result[i];
switch (data.operation) {
case REVISION_ADDED:
changes.addedIds[data.objectId] = data.internalRevisionId;
break;
case REVISION_UPDATED:
// We don't consider an update if this object has been added
// or if it has been already modified by a previous
// operation.
if (!(data.objectId in changes.addedIds) &&
!(data.objectId in changes.updatedIds)) {
changes.updatedIds[data.objectId] = data.internalRevisionId;
}
break;
case REVISION_REMOVED:
let id = data.objectId;
// If the object has been added in this range of revisions
// we can ignore it and remove it from the list.
if (id in changes.addedIds) {
delete changes.addedIds[id];
} else {
changes.removedIds[id] = data.internalRevisionId;
}
if (id in changes.updatedIds) {
delete changes.updatedIds[id];
}
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);
}
);
return;
}
}
// From changes to a map of internalRevisionId.
let revisions = {};
function addRevisions(obj) {
for (let key in obj) {
revisions[obj[key]] = true;
}
}
addRevisions(changes.addedIds);
addRevisions(changes.updatedIds);
addRevisions(changes.removedIds);
// Create the list of revisions.
let list = [];
for (let i = 0; i < aEvent.target.result.length; ++i) {
let data = aEvent.target.result[i];
// If this revision doesn't contain useful data, we still need to keep
// it in the list because we need to update the internal revision ID.
if (!(data.internalRevisionId in revisions)) {
data.operation = REVISION_SKIP;
}
list.push(data);
}
if (list.length == 0) {
self._state = STATE_DONE;
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
// Some revision has to be sent.
self._revisionsList = list;
self._state = STATE_REVISION_SEND;
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));
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);
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;
this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
this._revision = this._revisionsList.shift();
switch (this._revision.operation) {
case REVISION_REMOVED:
aResolve(ObjectWrapper.wrap({ operation: 'remove', id: this._revision.objectId },
this._window));
break;
case REVISION_ADDED: {
let request = aStore.get(this._revision.objectId);
let self = this;
request.onsuccess = function(aEvent) {
if (aEvent.target.result == undefined) {
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
aResolve(ObjectWrapper.wrap({ operation: 'add', id: self._revision.objectId,
data: aEvent.target.result }, self._window));
}
break;
}
case REVISION_UPDATED: {
let request = aStore.get(this._revision.objectId);
let self = this;
request.onsuccess = function(aEvent) {
if (aEvent.target.result == undefined) {
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
if (aEvent.target.result.revisionId > self._revision.internalRevisionId) {
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
return;
}
aResolve(ObjectWrapper.wrap({ operation: 'update', id: self._revision.objectId,
data: aEvent.target.result }, self._window));
}
break;
}
case REVISION_VOID:
// Internal error!
dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
break;
case REVISION_SKIP:
// This revision contains data that has already been sent by another one.
this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
break;
}
},
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');
let self = this;
return new this._window.Promise(function(aResolve, aReject) {
self._dataStore._db.cursorTxn(
function(aTxn, aStore, aRevisionStore) {
self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
},
function(aEvent) {
aReject(createDOMError(self._window, aEvent));
}
);
});
},
close: function() {
this._dataStore.syncTerminated(this);
}
};

View File

@ -59,6 +59,19 @@ DataStoreDB.prototype = {
);
},
cursorTxn: function(aCallback, aErrorCb) {
debug('Cursor transaction request');
this.newTxn(
'readonly',
[ DATASTOREDB_OBJECTSTORE_NAME, DATASTOREDB_REVISION ],
function(aTxn, aStores) {
aCallback(aTxn, aStores[0], aStores[1]);
},
function() {},
aErrorCb
);
},
revisionTxn: function(aType, aCallback, aErrorCb) {
debug("Transaction request");
this.newTxn(

View File

@ -325,6 +325,8 @@ DataStoreService.prototype = {
let obj = new DataStore(aWindow, aStores[i].name,
aStores[i].owner, aStores[i].readOnly);
let exposedObj = aWindow.DataStore._create(aWindow, obj);
obj.exposedObject = exposedObj;
results.push(exposedObj);
obj.retrieveRevisionId(

View File

@ -22,6 +22,7 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES += [
'DataStore.jsm',
'DataStoreChangeNotifier.jsm',
'DataStoreCursor.jsm',
'DataStoreDB.jsm',
'DataStoreServiceInternal.jsm',
]

View File

@ -29,6 +29,8 @@ MOCHITEST_FILES = \
test_arrays.html \
file_arrays.html \
test_oop.html \
test_sync.html \
file_sync.html \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -38,6 +38,10 @@
ok("add" in store, "store.add exists");
ok("remove" in store, "store.remove exists");
ok("clear" in store, "store.clear exists");
ok("revisionId" in store, "store.revisionId exists");
ok("getChanges" in store, "store.getChanges exists");
ok("getLength" in store, "store.getLength exists");
ok("sync" in store, "store.sync exists");
gStore = stores[0];

View File

@ -0,0 +1,406 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - sync</title>
</head>
<body>
<div id="container"></div>
<script type="application/javascript;version=1.7">
var gStore;
var gRevisions = [];
var gCursor;
var gExpectedEvents = true;
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) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
gStore = stores[0];
gRevisions.push(gStore.revisionId);
gStore.onchange = function(aEvent) {
ok(gExpectedEvents, "Events received!");
runTest();
}
runTest();
}, cbError);
}
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");
runTest();
}
function testCursor(cursor, steps) {
if (!steps.length) {
runTest();
return;
}
var step = steps.shift();
cursor.next().then(function(data) {
ok(!!data, "Cursor.next returns data");
is(data.operation, step.operation, "Waiting for operation: '" + step.operation + "' received '" + data.operation + "'");
switch (data.operation) {
case 'done':
is(/[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}/.test(data.revisionId), true, "done has a valid revisionId");
is (data.revisionId, gRevisions[gRevisions.length-1], "Last revision matches");
break;
case 'add':
case 'update':
if ('id' in step) {
is(data.id, step.id, "next() add: id matches: " + data.id + " " + step.id);
}
if ('data' in step) {
is(data.data, step.data, "next() add: data matches: " + data.data + " " + step.data);
}
break;
case 'remove':
if ('id' in step) {
is(data.id, step.id, "next() add: id matches: " + data.id + " " + step.id);
}
break;
}
testCursor(cursor, steps);
});
}
var tests = [
// Test for GetDataStore
testGetDataStores,
// interface test
testBasicInterface,
// empty DataStore
function() {
var cursor = gStore.sync();
var steps = [ { operation: 'clear' },
{ operation: 'done' },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
gExpectedEvents = false;
var cursor = gStore.sync('wrong revision ID');
var steps = [ { operation: 'clear' },
{ operation: 'done' },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[0]);
var steps = [ { operation: 'done' },
{ operation: 'done' }];
testCursor(cursor, steps);
},
// Test add from scratch
function() {
gExpectedEvents = true;
gStore.add(1).then(function(id) {
gRevisions.push(gStore.revisionId);
ok(true, "Iteme: " + id + " added");
});
},
function() {
gStore.add(2).then(function(id) {
gRevisions.push(gStore.revisionId);
ok(true, "Iteme: " + id + " added");
});
},
function() {
gExpectedEvents = false;
var cursor = gStore.sync();
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 1 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync('wrong revision ID');
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 1 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[0]);
var steps = [ { operation: 'add', id: 1, data: 1 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[1]);
var steps = [ { operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[2]);
var steps = [ { operation: 'done' }];
testCursor(cursor, steps);
},
// Test after an update
function() {
gExpectedEvents = true;
gStore.update(1, 3).then(function() {
gRevisions.push(gStore.revisionId);
});
},
function() {
gExpectedEvents = false;
var cursor = gStore.sync();
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 3 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync('wrong revision ID');
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 3 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[0]);
var steps = [ { operation: 'add', id: 1, data: 3 },
{ operation: 'add', id: 2, data: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[1]);
var steps = [ { operation: 'add', id: 2, data: 2 },
{ operation: 'update', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[2]);
var steps = [ { operation: 'update', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[3]);
var steps = [ { operation: 'done' }];
testCursor(cursor, steps);
},
// Test after a remove
function() {
gExpectedEvents = true;
gStore.remove(2).then(function() {
gRevisions.push(gStore.revisionId);
});
},
function() {
gExpectedEvents = false;
var cursor = gStore.sync();
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync('wrong revision ID');
var steps = [ { operation: 'clear', },
{ operation: 'add', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[0]);
var steps = [ { operation: 'add', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[1]);
var steps = [ { operation: 'update', id: 1, data: 3 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[2]);
var steps = [ { operation: 'update', id: 1, data: 3 },
{ operation: 'remove', id: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[3]);
var steps = [ { operation: 'remove', id: 2 },
{ operation: 'done' }];
testCursor(cursor, steps);
},
function() {
var cursor = gStore.sync(gRevisions[4]);
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: 1, data: 3 } ];
testCursor(gCursor, steps);
},
function() {
gStore.add(42).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: 3, data: 42 } ];
testCursor(gCursor, steps);
},
function() {
gStore.update(1, 42).then(function(id) {
gRevisions.push(gStore.revisionId);
runTest();
});
},
function() {
var steps = [ { operation: 'update', id: 1, data: 42 } ];
testCursor(gCursor, steps);
},
function() {
gStore.remove(1).then(function(id) {
gRevisions.push(gStore.revisionId);
runTest();
});
},
function() {
var steps = [ { operation: 'remove', id: 1 } ];
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: 4, data: 42 } ];
testCursor(gCursor, steps);
},
function() {
gStore.clear().then(function() {
gRevisions.push(gStore.revisionId);
runTest();
});
},
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 },
{ operation: 'done' } ];
testCursor(gCursor, steps);
},
function() {
gExpectedEvents = true;
gStore.add(42).then(function(id) {
});
}
];
function runTest() {
if (!tests.length) {
finish();
return;
}
var test = tests.shift();
test();
}
runTest();
</script>
</body>
</html>

View File

@ -0,0 +1,128 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - sync</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_sync.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]]}, runTest);
},
function() {
SpecialPowers.pushPrefEnv({"set": [["dom.datastore.enabled", true]]}, runTest);
},
function() {
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>

View File

@ -44,6 +44,8 @@ interface DataStore : EventTarget {
// Promise<unsigned long>
Promise getLength();
DataStoreCursor sync(optional DOMString revisionId = "");
};
dictionary DataStoreChanges {
@ -52,3 +54,32 @@ dictionary DataStoreChanges {
sequence<unsigned long> updatedIds;
sequence<unsigned long> removedIds;
};
[Pref="dom.datastore.enabled",
JSImplementation="@mozilla.org/dom/datastore-cursor;1"]
interface DataStoreCursor {
// the DataStore
readonly attribute DataStore store;
// Promise<DataStoreTask>
Promise next();
void close();
};
enum DataStoreOperation {
"add",
"update",
"remove",
"clear",
"done"
};
dictionary DataStoreTask {
DOMString revisionId;
DataStoreOperation operation;
unsigned long id;
any data;
};