more templates back into cli

This commit is contained in:
Thomas Reynolds 2014-05-26 18:44:11 -07:00
parent be5fad55ca
commit ba01a0a72b
121 changed files with 5 additions and 42 deletions

View file

@ -0,0 +1,27 @@
How to run the tests:
* The tests themselves assume that jsunit is in a sibling directory
to the one containing the distrbution. If this is not so, it is sufficient
to edit the paths in the test files. (On UNIX, symlinks may be your
friend if this is not convenient.)
* jsmock.js (available from http://jsmock.sourceforge.net/) should
be placed in the distribution directory.
* Specify the test files via a URL parameter. (This might be an issue
with jsunit: http://digitalmihailo.blogspot.com/2008/06/make-jsunit-work-in-firefox-30.html)
For example, if the root of your downloaded of the distribution is /mypath:
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapper_gears_test.html
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapper_html5_test.html
file:///mypath/jsunit/testRunner.html?testpage=mypath/webstorageportabilitylayer/dbwrapperapi_test.html
NB: the leading / in a UNIX path is not included in mypath so setting
it via pwd will not deliver the desired effect.

View file

@ -0,0 +1,45 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Namespace.
google.wspl.DatabaseFactory = google.wspl.DatabaseFactory || {};
/**
* Factory function to build databases in a cross-API manner.
* @param {string} dbName of the database
* @param {string} dbworkerUrl the URL for Gears worker.
* @return {google.wspl.Database} The database object.
*/
google.wspl.DatabaseFactory.createDatabase = function(dbName, dbworkerUrl) {
var dbms;
if (window.openDatabase) {
// We have HTML5 functionality.
dbms = new google.wspl.html5.Database(dbName);
} else {
// Try to use Google Gears.
var gearsDb = goog.gears.getFactory().create('beta.database');
var wp = goog.gears.getFactory().create('beta.workerpool');
// Note that Gears will not allow file based URLs when creating a worker.
dbms = new wireless.db.gears.Database();
dbms.openDatabase('', dbName, gearsDb);
wp.onmessage = google.bind(dbms.onMessage_, dbms);
// Comment this line out to use the synchronous database.
dbms.startWorker(wp, dbworkerUrl, 0);
}
return dbms;
};

View file

@ -0,0 +1,324 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview A worker thread that performs synchronous queries against a
* Gears database on behalf of an asynchronous calling client.
*
* The worker replies to the sender with messages to pass results, errors, and
* notifications about completed transactions. The type field of the message
* body specifies the message type. For each successful statement, a RESULT
* message is sent with a result attribute containing the Gears result set. For
* the first unsuccessful statement, an ERROR message will be sent with details
* stored in the error field. After the transaction has been committed, a COMMIT
* message is sent. If the transaction is rolled back, a ROLLBACK message is
* sent.
*
* NB: The worker must be served over http. Further, to operate successfully,
* it requires the inclusion of global_functions.js and gearsutils.js.
*/
/**
* Creates a DbWorker to handle incoming messages, execute queries, and return
* results to the main thread.
*
* @param {GearsWorkerPool} wp The gears worker pool.
* @constructor
*/
google.wspl.gears.DbWorker = function(wp) {
/**
* An array of transaction ids representing the transactions that are open on
* the database.
* @type {Array.<number>}
* @private
*/
this.transactions_ = [];
/**
* The gears worker pool.
* @type {GearsWorkerPool}
* @private
*/
this.wp_ = wp;
this.wp_.onmessage = google.bind(this.onMessage_, this);
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.STARTED
});
};
/**
* The gears database that this worker thread will interact with.
* @type {GearsDatabase}
* @private
*/
google.wspl.gears.DbWorker.prototype.db_;
/**
* A singleton instance of DbWorker.
* @type {google.wspl.gears.DbWorker?}
* @private
*/
google.wspl.gears.DbWorker.instance_;
/**
* The sender ID of the incomming messages. Default to 0 for workerpool ID.
* @type {number}
* @private
*/
google.wspl.gears.DbWorker.prototype.senderId_ = 0;
/**
* Message type constants for worker command messages.
* @enum {number}
*/
google.wspl.gears.DbWorker.CommandTypes = {
OPEN: 1,
BEGIN: 2,
EXECUTE: 3,
COMMIT: 4,
ROLLBACK: 5
};
/**
* Message type constants for worker reply messages.
* @enum {number}
*/
google.wspl.gears.DbWorker.ReplyTypes = {
RESULT: 1,
FAILURE: 2,
COMMIT: 3,
ROLLBACK: 4,
STARTED: 5,
OPEN_SUCCESSFUL: 6,
OPEN_FAILED: 7,
LOG: 8
};
/**
* Starts the DbWorker.
*/
google.wspl.gears.DbWorker.start = function() {
var wp = google.gears.workerPool;
google.wspl.gears.DbWorker.instance_ = new google.wspl.gears.DbWorker(wp);
};
/**
* Handles an OPEN command from the main thread.
*
* @param {string} userId The user to which the database belongs.
* @param {string} name The database's name.
*/
google.wspl.gears.DbWorker.prototype.handleOpen_ = function(userId, name) {
this.log_('Attempting to create Gears database: userId=' + userId + ', name='
+ name);
try {
this.db_ = google.gears.factory.create('beta.database', '1.0');
google.wspl.GearsUtils.openDatabase(userId, name, this.db_, this.log_);
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL
});
} catch (ex) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED,
'error': ex
});
}
};
/**
* Handles a EXECUTE command from the main thread.
*
* @param {!Array.<google.wspl.Statement>} statements The statements to execute.
* @param {number} callbackId The callback to invoke after each execution.
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleExecute_ =
function(statements, callbackId, transactionId) {
var self = this;
try {
this.executeAll_(statements, function(results) {
self.sendMessageToWorker_(/** @type {string} */({
'type': google.wspl.gears.DbWorker.ReplyTypes.RESULT,
'results': results,
'callbackId': callbackId,
'transactionId': transactionId
}));
});
} catch (e) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.FAILURE,
'error': e,
'callbackId': callbackId,
'transactionId': transactionId
});
}
};
/**
* Executes all of the statements on the Gears database. The callback is
* invoked with the query results after each successful query execution.
*
* @param {!Array.<Object>} statements The statements to execute.
* @param {Function} callback The callback to invoke with query results.
* @private
*/
google.wspl.gears.DbWorker.prototype.executeAll_ =
function(statements, callback) {
var results = [];
for (var i = 0; i < statements.length; i++) {
var resultset = this.db_.execute(statements[i]['sql'],
statements[i]['params']);
var result = google.wspl.GearsUtils.resultSetToObjectArray(resultset);
results.push(result);
}
callback(results);
};
/**
* Handles a BEGIN command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleBegin_ = function(transactionId) {
this.transactions_.push(transactionId);
this.db_.execute('BEGIN IMMEDIATE');
};
/**
* Handles a COMMIT command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleCommit_ = function(transactionId) {
this.db_.execute('COMMIT');
this.postCommit_();
};
/**
* Handles a ROLLBACK command from the main thread.
*
* @param {number} transactionId The transaction that the statements belong to.
* @private
*/
google.wspl.gears.DbWorker.prototype.handleRollback_ = function(transactionId) {
this.db_.execute('ROLLBACK');
this.postRollback_();
};
/**
* Sends a COMMIT reply to the main thread for each transaction that was
* committed.
*
* @private
*/
google.wspl.gears.DbWorker.prototype.postCommit_ = function() {
for (var i = this.transactions_.length - 1; i >= 0; i--) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.COMMIT,
'transactionId': this.transactions_[i]
});
}
this.transactions_ = [];
};
/**
* Sends a ROLLBACK reply to the main thread for each transaction that was
* rolled back.
*
* @private
*/
google.wspl.gears.DbWorker.prototype.postRollback_ = function() {
for (var i = this.transactions_.length - 1; i >= 0; i --) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK,
'transactionId': this.transactions_[i]
});
}
this.transactions_ = [];
};
/**
* Handles incomming messages.
* @param {string} a Deprecated.
* @param {number} b Deprecated.
* @param {Object} messageObject The message object.
* @private
*/
google.wspl.gears.DbWorker.prototype.onMessage_ =
function(a, b, messageObject) {
this.senderId_ = messageObject.sender;
var message = messageObject.body;
var type = message['type'];
var name = message['name'];
var statements = message['statements'];
var callbackId = message['callbackId'];
var transactionId = message['transactionId'];
var userId = message['userId'];
try {
switch(type) {
case google.wspl.gears.DbWorker.CommandTypes.OPEN:
this.handleOpen_(userId, name);
break;
case google.wspl.gears.DbWorker.CommandTypes.EXECUTE:
this.handleExecute_(statements, callbackId, transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.BEGIN:
this.handleBegin_(transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.COMMIT:
this.handleCommit_(transactionId);
break;
case google.wspl.gears.DbWorker.CommandTypes.ROLLBACK:
this.handleRollback_(transactionId);
break;
}
} catch (ex) {
this.log_('Database worker failed: ' + ex.message);
}
};
/**
* Sends a log message to the main thread to be logged.
* @param {string} msg The message to log.
* @private
*/
google.wspl.gears.DbWorker.prototype.log_ = function(msg) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.ReplyTypes.LOG,
'msg': msg
});
};
/**
* Sends a message to the main worker thread.
* @param {Object} msg The message object to send.
* @private
*/
google.wspl.gears.DbWorker.prototype.sendMessageToWorker_ = function(msg) {
this.wp_.sendMessage(msg, this.senderId_);
};

View file

@ -0,0 +1,393 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Gears worker tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
<script type="text/javascript" src="dbworker.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var db;
var wp;
var factory;
var callbackId = 10;
var transactionId = 15;
var name = 'name';
var userId = 'userId';
var utils;
function setUp() {
mockControl = new MockControl();
// Mock the Gears factory.
factory = mockControl.createMock();
factory.addMockMethod('create');
// Mock Google Gears.
google.gears = {};
google.gears.factory = {};
google.gears.factory.create = factory.create;
// Mock the Gears workerpool object.
wp = mockControl.createMock({
allowCrossOrigin: function(){},
sendMessage: function(){}
});
// Mock the Gears database object.
db = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
// Mock the Gears utility classes
utils = mockControl.createMock({
openDatabase: function(){},
});
google.wspl = google.wspl || {};
google.wspl.GearsUtils = google.wspl.GearsUtils || {};
google.wspl.GearsUtils.openDatabase = utils.openDatabase;
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
return rs;
};
}
function buildWorker() {
wp.expects().sendMessage(TypeOf.isA(Object), 0).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.',
google.wspl.gears.DbWorker.ReplyTypes.STARTED, msg.type);
});
var worker = new google.wspl.gears.DbWorker(wp);
worker.db_ = db;
worker.log_ = function() {};
return worker;
}
function testConstruction() {
var worker = buildWorker();
mockControl.verify();
}
function testHandleExecute_success() {
var worker = buildWorker();
var stat1 = {sql: 'sql1', params: [1, 2]};
var stat2 = {sql: 'sql2', params: [3, 4]};
var statements = [stat1, stat2];
var type = google.wspl.gears.DbWorker.ReplyTypes.RESULT;
db.expects().execute(stat1.sql, stat1.params).andReturn('result1');
db.expects().execute(stat2.sql, stat2.params).andReturn('result2');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.', type, msg.type);
assertEquals('Wrong results.length', 2, msg.results.length);
assertEquals('Wrong results[0].', 'result1', msg.results[0]);
assertEquals('Wrong results[1].', 'result2', msg.results[1]);
assertEquals('Wrong callbackId.', callbackId, msg.callbackId);
assertEquals('Wrong transactionId.', transactionId, msg.transactionId);
});
worker.handleExecute_(statements, callbackId, transactionId);
mockControl.verify();
}
function testHandleExecute_failure() {
var worker = buildWorker();
var stat1 = {sql: 'sql1', params: [1, 2]};
var stat2 = {sql: 'sql2', params: [3, 4]};
var stat3 = {sql: 'sql3', params: [5, 6]};
var statements = [stat1, stat2, stat3];
var type1 = google.wspl.gears.DbWorker.ReplyTypes.RESULT;
var type2 = google.wspl.gears.DbWorker.ReplyTypes.FAILURE;
var error = 'sql error';
db.expects().execute(stat1.sql, stat1.params).andReturn('result1');
db.expects().execute(stat2.sql, stat2.params).andThrow(error);
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Wrong message type.', type2, msg.type);
assertEquals('Wrong result.', error, msg.error.message);
assertEquals('Wrong callbackId.', callbackId, msg.callbackId);
assertEquals('Wrong transactionId.', transactionId, msg.transactionId);
});
worker.handleExecute_(statements, callbackId, transactionId);
mockControl.verify();
}
function testHandleBegin() {
var worker = buildWorker();
// Expecting two transactions to begin.
db.expects().execute('BEGIN IMMEDIATE');
db.expects().execute('BEGIN IMMEDIATE');
worker.handleBegin_(transactionId);
worker.handleBegin_(22);
assertEquals('Did not save first transaction id', transactionId,
worker.transactions_[0]);
assertEquals('Did not save second transaction id', 22,
worker.transactions_[1]);
mockControl.verify();
}
function testHandleCommit() {
var worker = buildWorker();
db.expects().execute('COMMIT');
worker.handleCommit_(transactionId);
mockControl.verify();
}
function testHandleRollback() {
var worker = buildWorker();
db.expects().execute('ROLLBACK');
worker.handleRollback_(transactionId);
mockControl.verify();
}
function testHandleOpen_success() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andReturn(db);
utils.expects().openDatabase(userId, name, db, worker.log_).andReturn(db);
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL, msg.type);
});
worker.handleOpen_(userId, name);
assertEquals('Database wrongly set', db, worker.db_);
mockControl.verify();
}
function testHandleOpen_failure_gearsfactory() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andThrow('blah!');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED, msg.type);
});
worker.handleOpen_(userId, name);
mockControl.verify();
}
function testHandleOpen_failure_dbopen() {
var worker = buildWorker();
worker.db_ = null;
factory.expects().create('beta.database', '1.0').andReturn(null);
utils.expects().openDatabase(userId, name, null, worker.log_).andThrow('blah!');
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function(msg) {
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED, msg.type);
});
worker.handleOpen_(userId, name);
mockControl.verify();
}
function testPostCommit() {
var worker = buildWorker();
worker.transactions_ = [4, 5];
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.COMMIT, msg.type);
assertEquals('Transaction id not set correctly.',
5, msg.transactionId);
});
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.COMMIT, msg.type);
assertEquals('Transaction id not set correctly.',
4, msg.transactionId);
});
worker.postCommit_();
assertEquals('Did not clear the transactions.', 0,
worker.transactions_.length);
mockControl.verify();
}
function testPostRollback() {
var worker = buildWorker();
worker.transactions_ = [4, 5];
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK, msg.type);
assertEquals('Transaction id not set correctly.',
5, msg.transactionId);
});
wp.expects().sendMessage(TypeOf.isA(Object), worker.senderId_).andStub(
function() {
var msg = arguments[0];
assertEquals('Type not set correctly.',
google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK, msg.type);
assertEquals('Transaction id not set correctly.',
4, msg.transactionId);
});
worker.postRollback_();
assertEquals('Did not clear the transactions.', 0,
worker.transactions_.length);
mockControl.verify();
}
function testOnmessage() {
var messageObject = {sender: 123, body: {}};
var worker = buildWorker();
worker.onMessage_(null, null, messageObject);
assertEquals('Wrong sender ID.', 123, worker.senderId_);
mockControl.verify();
}
function testOnmessage_open() {
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.OPEN,
name: name,
userId: userId
}};
var worker = buildWorker();
var handler = mockControl.createMock();
handler.addMockMethod('open');
worker.handleOpen_ = handler.open;
handler.expects().open(userId, name);
worker.onMessage_(null, null, messageObject);
mockControl.verify();
}
function testOnmessage_execute() {
var worker = buildWorker();
var statements = ['stat1', 'stat2'];
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.EXECUTE,
statements: statements,
callbackId: callbackId,
transactionId: transactionId
}};
var called = false;
worker.handleExecute_ = function(stat, call, trans) {
called = true;
assertEquals('Wrong statements.', statements, stat);
assertEquals('Wrong callback id.', callbackId, call);
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleExecute_ not called.', called);
mockControl.verify();
}
function testOnmessage_begin() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.BEGIN,
transactionId: transactionId
}};
var called = false;
worker.handleBegin_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleBegin_ not called.', called);
mockControl.verify();
}
function testOnmessage_commit() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.COMMIT,
transactionId: transactionId
}};
var called = false;
worker.handleCommit_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleCommit_ not called.', called);
mockControl.verify();
}
function testOnmessage_rollback() {
var worker = buildWorker();
var messageObject = {sender: 123, body: {
type: google.wspl.gears.DbWorker.CommandTypes.ROLLBACK,
transactionId: transactionId
}};
var called = false;
worker.handleRollback_ = function(trans) {
called = true;
assertEquals('Wrong transaction id.', transactionId, trans);
};
worker.onMessage_(null, null, messageObject);
assertTrue('handleRollback_ not called.', called);
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,32 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview Starts the dbworker.
*
* When constructing the worker for execution, this needs to be the last
* file. The worker consists of the following source files combined together.
*
* globalfunctions.js
* gearsutils.js
* dbworker.js
* dbworkerstarter.js
*
* and then loaded into a Gears worker process as implemented in
* databasefactory.js
*/
google.wspl.gears.DbWorker.start();

View file

@ -0,0 +1,595 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi Database.
*
* This implementation locks database access upon invoking the transaction's
* populate callback. Statements are then asynchronously sent to a worker
* thread for execution.
*/
/**
* @see google.wspl.Database#Database
* @param {boolean} opt_sync Perform all callbacks synchronously.
* @constructor
* @extends google.wspl.Database
*/
google.wspl.gears.Database = function(opt_sync) {
google.wspl.Database.call(this);
/**
* Begin transactions synchronously.
* @type {boolean}
* @private
*/
this.synchronous_ = !!opt_sync;
};
google.inherits(google.wspl.gears.Database, google.wspl.Database);
/**
* The time to wait for the dbworker to reply with STARTED.
* @type {number}
*/
google.wspl.gears.Database.TIMEOUT = 60000;
/**
* Whether the gears worker failed to reply with STARTED before TIMEOUT.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.workerTimeout_ = false;
/**
* Flag set when the worker is ready with an open database connection.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.workerReady_ = false;
/**
* Flag set when this database should use the worker to process transactions.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.useWorker_ = false;
/**
* The user for this database.
* @type {string}
* @private
*/
google.wspl.gears.Database.prototype.userId_;
/**
* The name for this database.
* @type {string}
* @private
*/
google.wspl.gears.Database.prototype.name_;
/**
* A map of open transactions and their callbacks.
* @type {Object}
* @private
*/
google.wspl.gears.Database.prototype.transactions_ = {};
/**
* An array of transaction ids that should be executed in order as the lock
* becomes available.
* @type {Array.<number>}
* @private
*/
google.wspl.gears.Database.prototype.queuedTransactions_ = [];
/**
* The transaction lock for this database.
* @type {boolean}
* @private
*/
google.wspl.gears.Database.prototype.locked_ = false;
/**
* The number of transactions to be used as an index.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.transCount_ = 1;
/**
* The id of the transaction being executed.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.currentTransactionId_;
/**
* The Gears worker pool.
* @type {GearsWorkerPool}
* @private
*/
google.wspl.gears.Database.prototype.wp_;
/**
* The worker ID.
* @type {number}
* @private
*/
google.wspl.gears.Database.prototype.workerId_;
/**
* The Gears database object.
* @type {GearsDatabase}
* @private
*/
google.wspl.gears.Database.prototype.db_;
/**
* Opens a new Gears database. This operation can only be performed once.
* @param {string} userId The user for this database.
* @param {string} name The name for this database.
* @param {GearsDatabase} gearsDb The gears database.
*/
google.wspl.gears.Database.prototype.openDatabase = function(userId, name,
gearsDb) {
if (!this.db_) {
this.db_ = gearsDb;
this.userId_ = userId;
this.name_ = name;
google.wspl.GearsUtils.openDatabase(userId, name, this.db_,
google.logger);
} else {
google.logger('openDatabase already invoked.');
}
};
/**
* Starts a worker to handle the database interactions. The worker will be
* asynchronously started after the specified delay and will not be used until
* the completion of any pending transaction.
* @param {GearsWorkerPool} wp The Gears worker pool.
* @param {string} workerUrl The URL to find the gears database worker.
* @return {number} The worker ID.
*/
google.wspl.gears.Database.prototype.startWorker = function(wp, workerUrl) {
this.wp_ = wp;
google.logger('Starting dbworker thread.');
this.workerId_ = wp.createWorkerFromUrl(workerUrl);
this.timeoutId_ = window.setTimeout(google.bind(this.handleTimeout_, this),
google.wspl.gears.Database.TIMEOUT);
return this.workerId_;
};
/**
* @see google.wspl.Transaction#createTransaction
* @inheritDoc
*/
google.wspl.gears.Database.prototype.createTransaction = function(populate,
opt_callback) {
var transactionCallback = opt_callback || {
onSuccess : function() {},
onFailure : function() {}
};
var id = this.transCount_++;
var transaction = new google.wspl.gears.Transaction(id, this);
this.saveTransaction_(transaction, transactionCallback, populate);
this.queuedTransactions_.push(transaction.id_);
this.nextTransaction_();
};
/**
* Saves the transaction and transaction callback to be accessed later when a
* commit or rollback is performed.
*
* @param {google.wspl.gears.Transaction} transaction The transaction that the
* callback belongs to.
* @param {Object} callback A transaction callback with onSuccess and onFailure
* @private
*/
google.wspl.gears.Database.prototype.saveTransaction_ = function(
transaction, callback, populate) {
this.transactions_[transaction.id_] = {
transaction: transaction,
callback: callback,
populate: populate
};
};
/**
* Handles incomming messages.
* @param {string} a Deprecated.
* @param {number} b Deprecated.
* @param {Object} messageObject The message object.
* @private
*/
google.wspl.gears.Database.prototype.onMessage_ =
function(a, b, messageObject) {
var message = messageObject.body;
try {
switch(message['type']) {
case google.wspl.gears.DbWorker.ReplyTypes.RESULT:
this.handleResult_(message['results'], message['callbackId'],
message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.FAILURE:
this.handleFailure_(message['error'], message['callbackId'],
message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.COMMIT:
this.handleCommit_(message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.ROLLBACK:
this.handleRollback_(message['transactionId']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.STARTED:
this.handleStarted_();
break;
case google.wspl.gears.DbWorker.ReplyTypes.OPEN_SUCCESSFUL:
this.handleOpenSuccessful_();
break;
case google.wspl.gears.DbWorker.ReplyTypes.OPEN_FAILED:
this.handleOpenFailed_(message['error']);
break;
case google.wspl.gears.DbWorker.ReplyTypes.LOG:
google.logger(message['msg']);
break;
}
} catch (ex) {
google.logger('Gears database failed: ' + ex.message, ex);
}
};
/**
* Opens a new Gears database.
*
* @param {string} userId The user to which the database belongs.
* @param {string} name The name of the database.
*/
google.wspl.gears.Database.prototype.doOpen = function(userId, name) {
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.OPEN,
'name': name,
'userId': userId
});
};
/**
* Begins a new transaction on the Gears database.
*
* @param {number} transactionId The id of the transaction being committed.
*/
google.wspl.gears.Database.prototype.doBegin = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('BEGIN IMMEDIATE');
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.BEGIN,
'transactionId': transactionId
});
};
/**
* Commits the current transaction on the Gears database. The transactionId
* is used to invoke the callback associated with the transaction.
*
* @param {number} transactionId The id of the transaction being committed.
*/
google.wspl.gears.Database.prototype.doCommit = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('COMMIT');
this.postCommit_();
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.COMMIT,
'transactionId': transactionId
});
};
/**
* Rolls the current transaction back on the Gears database. The transactionId
* is used to invoke the callback associated with the transaction.
*
* @param {number} transactionId The id of the transaction being rolled back.
*/
google.wspl.gears.Database.prototype.doRollback = function(transactionId) {
if (!this.useWorker_) {
this.db_.execute('ROLLBACK');
this.postRollback_();
return;
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.ROLLBACK,
'transactionId': transactionId
});
};
/**
* Executes an array of statements on the Gears database. The transactionId and
* callbackId are used to identify the callback that should be invoked when
* handleResult or handleFailure is called.
*
* @param {Array.<google.wspl.Statement>} statements The group of statements to
* execute
* @param {number} callbackId The callback to invoke for each statement
* @param {number} transactionId The transaction that the statements belong to
*/
google.wspl.gears.Database.prototype.doExecute = function(statements,
callbackId,
transactionId) {
if (!this.useWorker_) {
this.doExecuteSynchronously_(statements, callbackId, transactionId);
return;
}
var newStatements = [];
for (var i = 0; i < statements.length; i++) {
newStatements[i] = {
'sql': statements[i].sql,
'params': statements[i].params
};
}
this.sendMessageToWorker_({
'type': google.wspl.gears.DbWorker.CommandTypes.EXECUTE,
'statements': newStatements,
'callbackId': callbackId,
'transactionId': transactionId
});
};
/**
* Executes an array of statements on the synchronous Gears databse.
* @param {Array.<google.wspl.Statement>} statements
* @param {number} callbackId
* @param {number} transactionId
* @private
*/
google.wspl.gears.Database.prototype.doExecuteSynchronously_ =
function(statements, callbackId, transactionId) {
var db = this;
var results = [];
for (var i = 0; i < statements.length; i++) {
try {
var resultset = this.db_.execute(statements[i].sql, statements[i].params);
var result = google.wspl.GearsUtils.resultSetToObjectArray(resultset);
results.push(result);
} catch (e) {
var error = e;
function failureCallback() {
db.handleFailure_(error, callbackId, transactionId);
};
this.setTimeout_(failureCallback, 0);
return;
}
}
function resultCallback() {
db.handleResult_(results, callbackId, transactionId);
};
this.setTimeout_(resultCallback, 0);
};
/**
* Handles a RESULT message from the worker thread.
*
* @param {!Array.<!Array.<Object>>} results A Gears result set.
* @param {number} callbackId The callback to invoke.
* @param {number} transactionId The transaction that the statement is executing
* in.
* @private
*/
google.wspl.gears.Database.prototype.handleResult_ = function(results,
callbackId, transactionId) {
var transInfo = this.transactions_[transactionId];
if (transInfo) {
for (var i = 0, l = results.length; i < l; i++) {
var resultSet = new google.wspl.gears.ResultSet(results[i]);
transInfo.transaction.success(resultSet, callbackId);
}
}
};
/**
* Handles a FAILURE message from the worker thread.
*
* @param {Error} error An error produced by the Gears database
* @param {number} callbackId The callback to invoke
* @param {number} transactionId The transaction that the statement is executing
* in
* @private
*/
google.wspl.gears.Database.prototype.handleFailure_ = function(error,
callbackId, transactionId) {
var transInfo = this.transactions_[transactionId];
if (transInfo) {
transInfo.error = error;
transInfo.transaction.failure(error, callbackId);
}
};
/**
* Handles a COMMIT message from the worker thread.
*
* @param {number} id The transaction id.
* @private
*/
google.wspl.gears.Database.prototype.handleCommit_ = function(id) {
var transaction = this.removeTransaction_(id);
if (transaction) {
transaction.callback.onSuccess();
}
this.nextTransaction_();
};
/**
* Handles the completion of a commit from the synchronous database.
* @private
*/
google.wspl.gears.Database.prototype.postCommit_ = function() {
this.handleCommit_(this.currentTransactionId_);
};
/**
* Handles a ROLLBACK message from the worker thread.
*
* @param {number} id The transaction id
* @private
*/
google.wspl.gears.Database.prototype.handleRollback_ = function(id) {
var transaction = this.removeTransaction_(id);
if (transaction) {
transaction.callback.onFailure(transaction.error);
}
this.nextTransaction_();
};
/**
* Handles the completion of a rollback from the synchronous database.
* @private
*/
google.wspl.gears.Database.prototype.postRollback_ = function() {
this.handleRollback_(this.currentTransactionId_);
};
/**
* Handles a STARTED message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleStarted_ = function() {
if (!this.workerTimeout_) {
google.logger('Dbworker started.');
window.clearTimeout(this.timeoutId_);
this.timeoutId_ = 0;
this.doOpen(this.userId_, this.name_);
}
};
/**
* Handles a timeout of waiting for a STARTED message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleTimeout_ = function() {
this.workerTimeout_ = true;
google.logger('Timed out while waiting for the dbworker to start.');
};
/**
* Handles a OPEN_SUCCESSFUL message from the worker thread.
*
* @private
*/
google.wspl.gears.Database.prototype.handleOpenSuccessful_ = function() {
this.workerReady_ = true;
};
/**
* Handles a OPEN_FAILED message from the worker thread.
* @param {string} error
* @private
*/
google.wspl.gears.Database.prototype.handleOpenFailed_ = function(error) {
google.logger('Worker failed to open Gears database.');
};
/**
* Executes the next transaction if there is one queued.
*
* @private
*/
google.wspl.gears.Database.prototype.nextTransaction_ = function() {
if (this.queuedTransactions_.length && !this.locked_) {
this.locked_ = true;
if (this.workerReady_ && !this.useWorker_) {
this.useWorker_ = true;
google.logger('Switching to asynchronous database interface.');
}
var id = this.queuedTransactions_.shift();
this.currentTransactionId_ = id;
var transactionData = this.transactions_[id];
var db = this;
function populate() {
transactionData.populate(transactionData.transaction);
// If populate did not execute statements on the database, invoke the
// success callback and process the next transaction.
if (!transactionData.transaction.isExecuting()) {
db.handleCommit_(id);
}
};
this.setTimeout_(populate, 0);
}
};
/**
* Cleans up the transaction and transaction callback for the id specified.
*
* @param {number} id The transaction id.
* @return {google.wspl.Transaction} The transaction and callback in an object.
* @private
*/
google.wspl.gears.Database.prototype.removeTransaction_ = function(id) {
this.locked_ = false;
var transaction = this.transactions_[id];
if (transaction) {
delete this.transactions_[id];
}
return transaction;
};
/**
* Execute a function using window's setTimeout.
* @param {Function} func The function to execute.
* @param {number} time The time delay before invocation.
* @private
*/
google.wspl.gears.Database.prototype.setTimeout_ = function(func, time) {
if (this.synchronous_) {
func();
} else {
window.setTimeout(func, time);
}
};
/**
* Sends a message to the database worker thread.
* @param {Object} msg The message object to send.
* @private
*/
google.wspl.gears.Database.prototype.sendMessageToWorker_ = function(msg) {
this.wp_.sendMessage(msg, this.workerId_);
};

View file

@ -0,0 +1,404 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Gears database wrapper tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
<script type="text/javascript" src="gears_transaction.js"></script>
<script type="text/javascript" src="dbworker.js"></script>
<script type="text/javascript" src="dbwrapper_gears.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var callbackMock;
var dbMock;
var wp;
var workerId = 123;
var transMock;
function setUp() {
mockControl = new MockControl();
callbackMock = mockControl.createMock({
onSuccess: function(){},
onFailure: function(){}
});
transMock = mockControl.createMock({
success: function(){},
failure: function(){}
});
wp = mockControl.createMock({
allowCrossOrigin: function(){},
createWorkerFromUrl: function(){},
sendMessage: function(){}
});
dbMock = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
return rs;
};
}
function buildSyncDatabase() {
var database = new google.wspl.gears.Database(true);
database.userId_ = 'userId';
database.name_ = 'name';
database.db_ = dbMock;
return database;
}
function buildDatabase() {
var database = buildSyncDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.OPEN;
wp.expects().createWorkerFromUrl('workerUrl').andReturn(workerId);
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('Wrong message type.', type, arguments[0].type);
assertEquals('Incorrect name.', 'name', arguments[0].name);
assertEquals('Incorrect user id.', 'userId', arguments[0].userId);
});
database.startWorker(wp, 'workerUrl', 0);
database.handleStarted_();
database.handleOpenSuccessful_();
database.useWorker_ = true;
return database;
}
function testConstructor() {
var db = buildSyncDatabase();
mockControl.verify();
}
function testStartWorker() {
var db = buildDatabase();
assertTrue('Expected worker to be ready.', db.workerReady_);
mockControl.verify();
}
function testCreateTransaction() {
var db = buildDatabase();
var tx;
var populateMock = function(txa) {
tx = txa;
var transactions = db.transactions_;
assertEquals('missing transaction', tx, transactions[tx.id_].transaction);
assertEquals('missing callback', callbackMock,
transactions[tx.id_].callback);
assertEquals('database not saved', db, tx.db_);
assertTrue('database should be locked', db.locked_);
};
callbackMock.expects().onSuccess();
var transactions = db.transactions_;
db.createTransaction(populateMock, callbackMock);
assertEquals('failed to clean up transaction', undefined,
transactions[tx.id_]);
assertFalse('database should not be locked', db.locked_);
mockControl.verify();
}
function testMultipleTransactions() {
var db = buildDatabase();
var handler = mockControl.createMock();
handler.addMockMethod('populate');
handler.addMockMethod('onSuccess');
handler.addMockMethod('onFailure');
var trans;
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.createTransaction(handler.populate, handler);
db.createTransaction(handler.populate, handler);
mockControl.verify();
handler.expects().onSuccess();
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.handleCommit_(trans.id_);
mockControl.verify();
handler.expects().onFailure(undefined).andStub(function() {
db.createTransaction(handler.populate, handler);
});
handler.expects().populate(TypeOf.isA(google.wspl.gears.Transaction)).andStub(
function(tx) {
trans = tx;
tx.numActiveExecutes_ = 1;
});
db.handleRollback_(trans.id_);
mockControl.verify();
handler.expects().onSuccess();
db.handleCommit_(trans.id_);
mockControl.verify();
}
function testDoBegin() {
var db = buildDatabase();
var transactionId = 10;
var type = google.wspl.gears.DbWorker.CommandTypes.BEGIN;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertNotUndefined('Missing transaction id.', arguments[0].transactionId);
});
db.doBegin(transactionId);
mockControl.verify();
}
function testSynchronousBegin() {
var db = buildSyncDatabase();
dbMock.expects().execute('BEGIN IMMEDIATE');
db.doBegin(1);
mockControl.verify();
}
function testDoExecute() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.EXECUTE;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('statements do not match', statements.length,
arguments[0].statements.length);
for (var i = 0; i < statements.length; i++) {
assertEquals('a statement sql does not match', statements[i].sql,
arguments[0].statements[i].sql);
assertEquals('a statement params does not match', statements[i].params,
arguments[0].statements[i].params);
}
assertEquals('missing callback id', callbackId, arguments[0].callbackId);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testSynchronousExecute() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildSyncDatabase();
for (var i = 0; i < 3; i++) {
var stat = statements[i];
dbMock.expects().execute(stat.sql, stat.params).andReturn('result' + i);
}
var handler = mockControl.createMock();
handler.addMockMethod('handleResult_');
db.handleResult_ = handler.handleResult_;
handler.expects().handleResult_(
TypeOf.isA(Array), callbackId, transactionId).andStub(function(results) {
var expected = ['result0', 'result1', 'result2'];
assertArrayEquals('Wrong results.', expected, results);
});
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testSynchronousExecute_failure() {
var statements = [{sql: 's1', params: 'p1'},
{sql: 's2', params: 'p2'},
{sql: 's3', params: 'p3'}];
var callbackId = 5;
var transactionId = 10;
var db = buildSyncDatabase();
dbMock.expects().execute('s1', 'p1').andReturn('result0');
dbMock.expects().execute('s2', 'p2').andThrow(Error('db error'));
var handler = mockControl.createMock();
handler.addMockMethod('handleFailure_');
db.handleFailure_ = handler.handleFailure_;
handler.expects().handleFailure_(
TypeOf.isA(Error), callbackId, transactionId);
db.doExecute(statements, callbackId, transactionId);
mockControl.verify();
}
function testDoCommit() {
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.COMMIT;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doCommit(transactionId);
mockControl.verify();
}
function testSynchronousCommit() {
var db = buildSyncDatabase();
dbMock.expects().execute('COMMIT');
db.doCommit(1);
mockControl.verify();
}
function testDoRollback() {
var transactionId = 10;
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.ROLLBACK;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('missing trans id', transactionId, arguments[0].transactionId);
});
db.doRollback(transactionId);
mockControl.verify();
}
function testSynchronousRollback() {
var db = buildSyncDatabase();
dbMock.expects().execute('ROLLBACK');
db.doRollback(1);
mockControl.verify();
}
function testHandleCommit() {
var db = buildDatabase();
db.transactions_[5] = {transaction: 'tx', callback: callbackMock};
callbackMock.expects().onSuccess();
db.handleCommit_(5);
assertUndefined('failed to remove transaction', db.transactions_[5]);
mockControl.verify();
}
function testHandleRollback() {
var db = buildDatabase();
db.transactions_[5] = {
transaction: 'tx',
callback: callbackMock,
error: Error('error')
};
callbackMock.expects().onFailure(TypeOf.isA(Error)).andStub(function() {
assertEquals('did not pass error', 'error', arguments[0].message);
});
db.handleRollback_(5);
assertUndefined('failed to remove transaction', db.transactions_[5]);
mockControl.verify();
}
function testHandleResult() {
var db = buildDatabase();
transMock.expects().success(TypeOf.isA(google.wspl.gears.ResultSet),
22).andStub(function() {
assertEquals('result not set', 'result1', arguments[0].resultArray_);
});
transMock.expects().success(TypeOf.isA(google.wspl.gears.ResultSet),
22).andStub(function() {
assertEquals('result not set', 'result2', arguments[0].resultArray_);
});
db.transactions_[5] = {transaction: transMock, callback: 'cb'};
db.handleResult_(['result1', 'result2'], 22, 5);
mockControl.verify();
}
function testHandleFailure() {
var db = buildDatabase();
transMock.expects().failure(TypeOf.isA(Error), 22).andStub(function() {
assertEquals('error not set', 'error', arguments[0].message);
});
db.transactions_[5] = {transaction: transMock, callback: 'cb'};
db.handleFailure_(Error('error'), 22, 5);
assertEquals('failed to save error', 'error',
db.transactions_[5].error.message);
mockControl.verify();
}
function testHandleStarted() {
var db = buildDatabase();
var type = google.wspl.gears.DbWorker.CommandTypes.OPEN;
wp.expects().sendMessage(TypeOf.isA(Object), workerId).andStub(function() {
assertEquals('wrong message type', type, arguments[0].type);
assertEquals('wrong db userId', 'userId', arguments[0].userId);
assertEquals('wrong db name', 'name', arguments[0].name);
});
db.handleStarted_();
mockControl.verify();
}
function testHandleStarted_afterTimeout() {
var db = buildDatabase();
db.handleTimeout_();
// This should do nothing.
db.handleStarted_();
mockControl.verify();
}
function testHandleTimeout() {
var db = buildDatabase();
db.handleTimeout_();
mockControl.verify();
}
function testHandleOpenSuccessful() {
var db = buildDatabase();
db.handleOpenSuccessful_();
assertTrue('Worker should be ready.', db.workerReady_);
mockControl.verify();
}
function testHandleOpenFailed() {
var db = buildDatabase();
db.handleOpenFailed_('error');
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,203 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview Generic Database API.
*
* A small set of classes to define how we interact with databases that
* can easily be implemented on top of HTML5.
*/
google.wspl.html5 = google.wspl.html5 || {};
/**
* Specification's default largest database size in HTML5 databases.
* @type{number}
*/
google.wspl.LARGEST_SUPPORTED_DATABASE = 1024 * 1024 * 4;
/**
* Creates an HTML5 Transaction object.
* @see google.wspl.Transaction#Transaction
*
* @constructor
* @extends google.wspl.Transaction
*
* @param {SQLTransaction} html5tx The HTML5 implementation of transactions.
*/
google.wspl.html5.Transaction = function(html5tx) {
this.tx_ = html5tx;
};
google.inherits(google.wspl.html5.Transaction, google.wspl.Transaction);
/**
* Runs an array of statements in a single database transaction.
* Invokes the onSuccess callback once for each succeesfully executed
* statement and
* once for the first failed statement.
*
* @param {Array.<google.wspl.Statement>} statements The statements to
* execute.
* @param {Object?} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.html5.Transaction.prototype.executeAll = function(statements,
opt_callback) {
if (statements.length == 0) {
throw Error('Possibly silly attempt to execute empty statement list.');
}
var self = this;
for (var i = 0; i < statements.length; ++i) {
var statement = statements[i];
google.logger('SQL: ' + statement.sql + ' PARAMS: ' + statement.params);
this.tx_.executeSql(statement.sql, statement.params,
function(tx, result) {
if (opt_callback && opt_callback.onSuccess) {
var resultSet = new google.wspl.html5.ResultSet(result);
opt_callback.onSuccess(self, resultSet);
}
},
function(tx, error) {
if (opt_callback && opt_callback.onFailure) {
opt_callback.onFailure(error);
}
// fail the whole transaction if any step fails
return true;
});
}
};
/**
* @see google.wspl.Database#Database
* @param {string} name The name for this database.
* @param {window} opt_window A window object for dependency injection.
* @constructor
* @extends google.wspl.Database
*/
google.wspl.html5.Database = function(name, opt_window) {
/**
* Sequence number for transactions.
* @type {number}
* @private
*/
this.sequenceNum_ = 1;
/**
* Map of transactionIds -> transaction start time in millis.
* @type {Object}
* @private
*/
this.inflightTransactions_ = {};
var win = opt_window || window;
this.db_ = win.openDatabase(name, '',
name, google.wspl.LARGEST_SUPPORTED_DATABASE);
if (this.db_ == null) {
throw Error('The returned database was null.');
}
};
google.inherits(google.wspl.html5.Database, google.wspl.Database);
/**
* @see google.wspl.Database#createTransaction
*/
google.wspl.html5.Database.prototype.createTransaction = function(populate,
opt_callback) {
var transactionCallback = opt_callback || {
onSuccess: function() {},
onFailure: function() {}
};
var transactionId = this.sequenceNum_++;
var inflightTransactions = this.inflightTransactions_;
inflightTransactions[transactionId] = this.getCurrentTime();
this.db_.transaction(
function(tx) {
// Delete the transaction before the executing it because our
// definition of an 'in-flight' transaction is the time between
// when the request was made and when the database starts to
// execute the transaction.
delete inflightTransactions[transactionId];
populate(new google.wspl.html5.Transaction(tx));
},
function(error) {transactionCallback.onFailure(error);},
function() {transactionCallback.onSuccess();});
};
/**
* Determine if there is an in-flight database transaction that's older than
* the given time period.
* @param {number} olderThanMillis The time period.
* @return {boolean} True if the database has an in-flight transaction older
* than the given time period, false otherwise.
*/
google.wspl.html5.Database.prototype.hasInflightTransactions =
function(olderThanMillis) {
for (var transactionId in this.inflightTransactions_) {
var startTime = this.inflightTransactions_[transactionId];
if (this.getCurrentTime() - startTime > olderThanMillis) {
return true;
}
}
return false;
};
/**
* Returns the current time.
* @return {number} The current time in millis.
*/
google.wspl.html5.Database.prototype.getCurrentTime = function() {
// The iPhone does not support Date.now()
var d = new Date();
return d.getTime();
};
/**
* Creates an HTML5 ResultSet object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.ResultSet
*
* @param {Object} html5_result The HTML5 implementation of result set.
*/
google.wspl.html5.ResultSet = function(html5_result) {
this.result_ = html5_result;
this.index_ = 0;
};
google.inherits(google.wspl.html5.ResultSet, google.wspl.ResultSet);
/**
* @see google.wspl.ResultSet#isValidRow
*/
google.wspl.html5.ResultSet.prototype.isValidRow = function() {
return this.index_ >= 0 && this.index_ < this.result_.rows.length;
};
/**
* @see google.wspl.ResultSet#next
*/
google.wspl.html5.ResultSet.prototype.next = function() {
this.index_ ++;
};
/**
* @see google.wspl.ResultSet#getRow
*/
google.wspl.html5.ResultSet.prototype.getRow = function() {
return this.result_.rows.item(this.index_);
};

View file

@ -0,0 +1,468 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Database wrapper tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="dbwrapper_html5.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var html5Tx;
var callbackMock;
var html5Window;
var html5Db;
var populateMock;
function setUp() {
mockControl = new MockControl();
html5Tx = mockControl.createMock({executeSql: function(){}});
callbackMock = mockControl.createMock({onSuccess: function(){},
onFailure: function(){}});
html5Window = mockControl.createMock({openDatabase: function(){}});
html5Db = mockControl.createMock({transaction: function(){}});
populateMock = mockControl.createMock({populate: function(){}});
dbTx = mockControl.createMock({execute: function(){}, executeAll:
function(){}});
dbRows = mockControl.createMock({item: function(){}});
}
function testConstructTransaction() {
var trans = new google.wspl.html5.Transaction('foo');
assertEquals('transaction instance not set correctly', 'foo', trans.tx_);
}
function testTransactionExecuteAll() {
var trans = new google.wspl.html5.Transaction(html5Tx);
try {
trans.executeAll([], callbackMock);
fail('Should never get here');
} catch(e) {
assertEquals(
'did not exception fault on empty statement list',
'Error: Possibly silly attempt to execute empty statement list.',
e.toString());
}
mockControl.verify();
}
/*
The sequence of digits at the end of the test names indicate as boolean
variables the success or failure of each statement.
*/
function testTransactionExecuteAll_1() {
var stat1 = new google.wspl.Statement('stat1');
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback = arguments[2];
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset', arguments[1].result_)});
trans.executeAll([stat1], callbackMock);
successCallback(html5Tx, 'resultset');
mockControl.verify();
}
function testTransactionExecuteAll_11() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
trans.executeAll([stat1, stat2], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
mockControl.verify();
}
function testTransactionExecuteAll_111() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback3 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset3', arguments[1].result_)});
trans.executeAll([stat1, stat2, stat3], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
successCallback3(html5Tx, 'resultset3');
mockControl.verify();
}
function testTransactionExecuteAll_noCallback_111() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(
function() { successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(
function() { successCallback3 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
trans.executeAll([stat1, stat2, stat3]);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
successCallback3(html5Tx, 'resultset3');
mockControl.verify();
}
function testTransactionExecuteAll_110() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback1 = null;
var successCallback2 = null;
var successCallback3 = null;
var failureCallback3 = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback1 = arguments[2];
});
html5Tx.expects().executeSql('stat2', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback2 = arguments[2];
var params = arguments[1];
assertEquals('incorrect params to sql', 1, params[0]);
});
html5Tx.expects().executeSql('stat3', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback3 = arguments[2];
failureCallback3 = arguments[3];
var params = arguments[1];
assertEquals('incorrect params to sql', 2, params[0]);
assertEquals('incorrect params to sql', 3, params[1]);
});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset1', arguments[1].result_)});
callbackMock.expects().onSuccess(trans,
TypeOf.isA(google.wspl.html5.ResultSet)).andStub(function() {
assertEquals('result not returned', 'resultset2', arguments[1].result_)});
callbackMock.expects().onFailure('error3');
trans.executeAll([stat1, stat2, stat3], callbackMock);
successCallback1(html5Tx, 'resultset1');
successCallback2(html5Tx, 'resultset2');
assertTrue('failure case callback not terminating transaction',
failureCallback3(html5Tx, 'error3'));
mockControl.verify();
}
function testTransactionExecute_nocallback() {
var stat1 = new google.wspl.Statement('stat1');
var trans = new google.wspl.html5.Transaction(html5Tx);
var successCallback = null;
var failureCallback = null;
html5Tx.expects().executeSql('stat1', TypeOf.isA(Array),
TypeOf.isA(Function), TypeOf.isA(Function)).andStub(function() {
successCallback = arguments[2];
});
trans.execute(stat1);
successCallback(html5Tx, 'resultset');
mockControl.verify();
}
function buildDatabase() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(html5Db);
return new google.wspl.html5.Database('name', html5Window);
}
function testConstructDatabase() {
var db = buildDatabase();
mockControl.verify();
assertEquals('did not set db_ correctly', db.db_, html5Db);
}
function testConstructDatabase_null() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(null);
try {
var db = google.wspl.html5.Database('name', html5Window);
fail('Should never get here');
} catch (e) {
if (e.isJsUnitException) {
throw e;
}
}
mockControl.verify();
}
function testConstructDatabase_undefined() {
html5Window.expects().openDatabase('name', '', 'name',
google.wspl.LARGEST_SUPPORTED_DATABASE).
andReturn(undefined);
try {
var db = google.wspl.html5.Database('name', html5Window);
fail('Should never get here');
} catch (e) {
if (e.isJsUnitException) {
throw e;
}
}
mockControl.verify();
}
function createTransactionCore() {
var db = buildDatabase();
var fun1 = null;
var failure = null;
var success = null;
html5Db.expects().transaction(TypeOf.isA(Function), TypeOf.isA(Function),
TypeOf.isA(Function)).andStub(function() {
fun1 = arguments[0];
failure = arguments[1];
success = arguments[2];
});
var tx = null;
var populateMock = function(txa) {
tx = txa;
};
db.createTransaction(populateMock, callbackMock);
fun1('transactionTest');
assertEquals('transaction not saved', 'transactionTest', tx.tx_);
assertEquals('did not set db_ correctly', db.db_, html5Db);
return {failure: failure, success: success};
}
function testCreateTransaction_success() {
cbs = createTransactionCore();
callbackMock.expects().onSuccess();
cbs.success('success');
mockControl.verify();
}
function testCreateTransaction_failure() {
cbs = createTransactionCore();
callbackMock.expects().onFailure('error');
cbs.failure('error');
mockControl.verify();
}
function testCreateTransaction_onlyPopulate() {
cbs = createTransactionCore();
mockControl.verify();
}
function testDatabaseExecute_nocallback() {
var db = buildDatabase();
dbTx.expects().execute('statementText', null);
db.execute('statementText');
}
function testDatabaseExecute_nocallback() {
var db = buildDatabase();
dbTx.expects().execute('statementText', null);
db.execute('statementText');
}
function testDatabaseExecuteAll() {
var db = buildDatabase();
dbTx.expects().executeAll(TypeOf.isA(Array));
}
function testResultSetNext() {
var res = {rows: {length: 4}};
var result = new google.wspl.html5.ResultSet(res);
for (var i = 0; i < 4; i++) {
assertTrue('expected valid row', result.isValidRow());
result.next();
}
assertFalse('expected invalid row', result.isValidRow());
}
function testResultSetIsValidRow() {
var res = {rows: {length: 0}};
var result = new google.wspl.html5.ResultSet(res);
assertFalse('expected invalid row', result.isValidRow());
res.rows.length = 1;
assertTrue('expected valid row', result.isValidRow());
result.next();
assertFalse('expected invalid row', result.isValidRow());
}
function testResultSetGetRow() {
var res = {rows: {
length: 3,
item: function(index) {
assertEquals('expected index of 1', 1, index);
return {h0: 'value0', h1: 'value1', h2: 'value2'};
}
}};
var result = new google.wspl.html5.ResultSet(res);
result.next();
var row = result.getRow();
assertEquals('first field is not valid', 'value0', row.h0);
assertEquals('first field is not valid', 'value1', row.h1);
assertEquals('first field is not valid', 'value2', row.h2);
}
function testHasInflightTransactions() {
var fakeTime = 1000;
var db = buildDatabase();
var dummyPopulate = function() {};
db.getCurrentTime = function() {return fakeTime;};
// Create a transaction, make sure it's kept track of.
var transId = db.sequenceNum_;
db.createTransaction(dummyPopulate);
assertEquals('There should be an inflight request.',
fakeTime, db.inflightTransactions_[transId]);
// Haven't advanced time, so hasInflightTransactions should return false.
assertFalse('Expected hasInflightTransactions=false.',
db.hasInflightTransactions(100));
// Advance time, now hasInflightTransactions should return true.
fakeTime += 10000;
var faqs = db.hasInflightTransactions(100);
assertTrue('Expected hasInflightTransactions=true.',
db.hasInflightTransactions(100));
// Now now have the transaction completed, hasInflightTransactions
// should return false.
delete db.inflightTransactions_[transId];
assertFalse('Expected hasInflightTransactions=false.',
db.hasInflightTransactions(100));
};
</script>
</body>
</html>

View file

@ -0,0 +1,202 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview Generic Database API.
*
* A small set of classes to define how we interact with databases that can
* easily be implemented on top of HTML5 and Gears. The classes in this file
* should be extended to provide the missing method implementations and more
* sophisticated constructors where applicable.
*
*/
/**
* Constructs a Statement object. A Statement is an SQL statement paired
* with the parameters needed to execute it.
*
* @constructor
* @param {!string} sql The SQL statement.
* @param {Array.<Object>?} opt_params The parameters for the SQL statement.
*/
google.wspl.Statement = function(sql, opt_params) {
/**
* The SQL statement with '?' in place of parameters.
* @type {string}
*/
this.sql = sql;
/**
* The parameters to use with the SQL statement.
* @type {!Array}
*/
this.params = opt_params || [];
};
/**
* Returns a new statement object from the given statement with the parameters
* set as specified.
* @param {Array.<Object>} params The array of values for ? placeholders.
* @return {!google.wspl.Statement} The created Statement.
*/
google.wspl.Statement.prototype.createStatement = function(params) {
return new google.wspl.Statement(this.sql, params);
};
/**
* Constructs a Transaction object. Transaction objects
* group together a series of statements into a single atomic
* action on the database.
*
* @constructor
*/
google.wspl.Transaction = function() {
};
/**
* Takes a statement and an optional callback object and
* runs the statement on the database. The callback can be used to
* add more statements to the same transaction, or execute can be
* called repeatedly and the transactions will later execute in the
* order provided.
*
* @param {google.wspl.Statement} statement The statement to execute.
* @param {Object} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.Transaction.prototype.execute = function(statement,
opt_callback) {
this.executeAll([statement], opt_callback);
};
/**
* Runs an array of statements in a single database transaction.
* Invokes the onSuccess callback once for each successfully executed
* statement and once for the first failed statement. The callback can be
* used to add more statements to the same transaction, or executeAll can
* be called repeatedly and each block of statements given will execute
* in the same order as the sequence of calls to executeAll.
*
* @param {Array.<google.wspl.Statement>} statements The statements to
* execute.
* @param {Object?} opt_callback An object containing onSuccess and onFailure
* handlers.
*/
google.wspl.Transaction.prototype.executeAll = function(statements,
opt_callback) {
throw Error('executeAll not implemented');
};
/**
* Constructs a Database object. Database objects are handles that allow
* access to a database (and create the corresponding database if it doesn't
* already exist). To open the database, pass the name of the needed
* database to the constructor, and then execute transactions on it using
* the execute method.
*
* @constructor
*/
google.wspl.Database = function() {
};
/**
* Creates a transaction object that can execute a series of SQL statements
* atomically.
*
* @param {Function} populate A callback to run execute calls on the
* transaction.
* @param {Object?} opt_callback An optional success/failure callback that is
* called when the entire transaction is finished executing.
*/
google.wspl.Database.prototype.createTransaction = function(populate,
opt_callback) {
throw Error('createTransaction not implemented');
};
/**
* Executes an array of statements on the database, invoking the optional
* callback after statement execution and the optional transactionCallback upon
* completion of the transaction.
*
* @param {google.wspl.Statement} statement the statement to execute
* @param {Object?} opt_callback object that defines onSuccess and onFailure
* @param {Object?} opt_transactionCallback object that defines onSuccess and
* onFailure
*/
google.wspl.Database.prototype.execute = function(statement,
opt_callback,
opt_transactionCallback) {
this.createTransaction(function(tx) {
tx.execute(statement, opt_callback);
}, opt_transactionCallback);
};
/**
* Executes an array of statements on the database, invoking the optional
* callback for each statement in the transaction. In the case of a statement
* failure, only the first failed statement will be reported and the transaction
* will be rolled back. This method invokes the optional transactionCallback
* upon completion of the transaction.
*
* @param {Array.<google.wspl.Statement>} statements the statements to execute
* @param {Object?} opt_callback object that defines onSuccess and onFailure
* @param {Object?} opt_transactionCallback object that defines onSuccess and
* onFailure
*/
google.wspl.Database.prototype.executeAll = function(statements,
opt_callback,
opt_transactionCallback) {
this.createTransaction(function(tx) {
tx.executeAll(statements, opt_callback);
}, opt_transactionCallback);
};
/**
* An immutable set of results that is returned from a single successful query
* on the database.
*
* @constructor
*/
google.wspl.ResultSet = function() {
};
/**
* Returns true if next() will advance to a valid row in the result set.
*
* @return {boolean} if next() will advance to a valid row in the result set
*/
google.wspl.ResultSet.prototype.isValidRow = function() {
throw Error('isValidRow not implemented');
};
/**
* Advances to the next row in the results.
*/
google.wspl.ResultSet.prototype.next = function() {
throw Error('next not implemented');
};
/**
* Returns the current row as an object with a property for each field returned
* by the database. The property will have the name of the column and the value
* of the cell.
*
* @return {Object} The current row
*/
google.wspl.ResultSet.prototype.getRow = function() {
throw Error('getRow not implemented');
};

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Database wrapper api</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
</head>
<body>
<script type='text/javascript'>
function subStatementTest(stm) {
assertEquals('statement not set correctly', 'bar', stm.sql);
assertEquals('first param not set correctly', 1, stm.params[0]);
assertEquals('second param not set correctly', 2, stm.params[1]);
}
function testConstructStatement() {
var stm = new google.wspl.Statement('foo');
assertEquals('statement not set correctly', 'foo', stm.sql);
var stm = new google.wspl.Statement('bar', [1,2]);
subStatementTest(stm);
}
function testConstructStatementFromTemplate() {
var temp = new google.wspl.Statement('bar');
var stm = temp.createStatement([1,2]);
subStatementTest(stm);
}
</script>
</body>
</html>

View file

@ -0,0 +1,71 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi ResultSet.
*/
/**
* Creates a Gears ResultSet object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.ResultSet
* @param {!Array.<Object>} resultArray An array of hash objects where the
* column names in the query are used as members of the objects.
*/
google.wspl.gears.ResultSet = function(resultArray) {
google.wspl.ResultSet.call(this);
/**
* The result set as an array of hash objects.
* @type {!Array.<Object>}
* @private
*/
this.resultArray_ = resultArray;
};
google.inherits(google.wspl.gears.ResultSet, google.wspl.ResultSet);
/**
* The current record in the result set.
* @type {number}
* @private
*/
google.wspl.gears.ResultSet.prototype.current_ = 0;
/**
* @see google.wspl.ResultSet#isValidRow
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.isValidRow = function() {
return this.current_ < this.resultArray_.length;
};
/**
* @see google.wspl.ResultSet#next
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.next = function() {
this.current_++;
};
/**
* @see google.wspl.ResultSet#getRow
* @inheritDoc
*/
google.wspl.gears.ResultSet.prototype.getRow = function() {
return this.resultArray_[this.current_];
};

View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Gears resultset tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
</head>
<body>
<script type='text/javascript'>
var resultArray;
function setUp() {
var obj1 = {
field1: 'a',
field2: 2,
field3: 'c'
};
var obj2 = {
field1: 'd',
field2: 4,
field3: 'f'
};
var obj3 = {
field1: 'g',
field2: 6,
field3: 'i'
};
resultArray = [obj1, obj2, obj3];
}
function testResultSetNext() {
var result = new google.wspl.gears.ResultSet(resultArray);
assertEquals('incorrect value for current', 0, result.current_);
result.next();
assertEquals('incorrect value for current', 1, result.current_);
result.next();
assertEquals('incorrect value for current', 2, result.current_);
}
function testResultSetIsValidRow() {
var result = new google.wspl.gears.ResultSet(resultArray);
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertTrue('incorrect return from isValidRow', result.isValidRow());
result.next();
assertFalse('incorrect return from isValidRow', result.isValidRow());
}
function testResultSetGetRow() {
var result = new google.wspl.gears.ResultSet(resultArray);
result.next();
var row = result.getRow();
assertEquals('first field is not valid', 'd', row.field1);
assertEquals('first field is not valid', 4, row.field2);
assertEquals('first field is not valid', 'f', row.field3);
}
</script>
</body>
</html>

View file

@ -0,0 +1,196 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview A Gears implementation of dbwrapperapi Transaction.
*/
/**
* Creates a Gears Transaction object.
* @see google.wspl.ResultSet#ResultSet
*
* @constructor
* @extends google.wspl.Transaction
*
* @param {number} id The unique id for this transaction
* @param {google.wspl.gears.Database} db The Gears implementation of the
* dbwrapperapi database
*/
google.wspl.gears.Transaction = function(id, db) {
google.wspl.Transaction.call(this);
/**
* The unique id for this transaction.
* @type {number}
* @private
*/
this.id_ = id;
/**
* The Gears implementation of the dbwrapperapi database.
* @type {google.wspl.gears.Database}
* @private
*/
this.db_ = db;
/**
* A map of statements, callback, and current statement.
* @type {Object}
* @private
*/
this.activeExecutes_ = {};
};
google.inherits(google.wspl.gears.Transaction, google.wspl.Transaction);
/**
* The number of active executes.
* @type {number}
* @private
*/
google.wspl.gears.Transaction.prototype.numActiveExecutes_ = 0;
/**
* The id for the next call to execute. Incremented after use.
* @type {number}
* @private
*/
google.wspl.gears.Transaction.prototype.nextCallbackId_ = 1;
/**
* Whether the transaction should be rolled back or not. This property is set
* to true when a statement fails.
* @type {boolean}
* @private
*/
google.wspl.gears.Transaction.prototype.needsRollback_ = false;
/**
* Begins a new transaction with the Gears database. Commits the transaction if
* all calls to executeAll for this transaction have finished receiving
* callbacks. Rolls the transaction back if a statement failed.
*
* @see google.wspl.Transaction#executeAll
* @inheritDoc
*/
google.wspl.gears.Transaction.prototype.executeAll = function(statements,
opt_callback) {
if (statements.length == 0) {
throw Error('Possibly silly attempt to execute empty statement list.');
}
if (this.numActiveExecutes_ == 0) {
this.db_.doBegin(this.id_);
}
this.numActiveExecutes_++;
var callbackId = this.nextCallbackId_++;
var callback = opt_callback || {
onSuccess : function() {},
onFailure : function() {}
};
this.activeExecutes_[callbackId] = {
statements: statements,
currentStatement: 0,
callback: callback
};
this.db_.doExecute(statements, callbackId, this.id_);
};
/**
* Invokes onSuccess on the specified callback.
*
* @param {google.wspl.ResultSet} result The result of a successful statement
* @param {number} callbackId The callback to invoke
*/
google.wspl.gears.Transaction.prototype.success = function(result,
callbackId) {
if (!this.needsRollback_) {
var activeExecute = this.activeExecutes_[callbackId];
activeExecute.callback.onSuccess(this, result);
}
this.endStatement_(callbackId);
};
/**
* Invokes onFailure on the specified callback.
*
* @param {Error} error The error of an unsuccessful statement
* @param {number} callbackId The callback to invoke
*/
google.wspl.gears.Transaction.prototype.failure = function(error,
callbackId) {
if (!this.needsRollback_) {
this.needsRollback_ = true;
var activeExecute = this.activeExecutes_[callbackId];
activeExecute.callback.onFailure(error);
}
this.endStatement_(callbackId);
};
/**
* Handles clean up for the end of a single execution.
*
* @param {number} callbackId The callback to clean up.
* @private
*/
google.wspl.gears.Transaction.prototype.endStatement_ = function(callbackId) {
var activeExecute = this.activeExecutes_[callbackId];
var statements = activeExecute.statements;
var currentStatement = ++activeExecute.currentStatement;
if (currentStatement == statements.length) {
this.endExecute_(callbackId);
}
};
/**
* Handles clean up for the end of a call to executeAll. Performs a commit or
* rollback if this is the last active execute to clean up.
*
* @param {number} callbackId The callback to clean up
* @private
*/
google.wspl.gears.Transaction.prototype.endExecute_ = function(callbackId) {
delete this.activeExecutes_[callbackId];
this.numActiveExecutes_--;
if (!this.isExecuting()) {
this.endTransaction_();
}
};
/**
* Instructs the worker to commit the transaction or roll it back if a failure
* occurred and a rollback is required.
*
* @private
*/
google.wspl.gears.Transaction.prototype.endTransaction_ = function() {
if (this.needsRollback_) {
this.db_.doRollback(this.id_);
} else {
this.db_.doCommit(this.id_);
}
};
/**
* @return {boolean} True if the transaction has statements executing, false
* otherwise.
*/
google.wspl.gears.Transaction.prototype.isExecuting = function() {
return this.numActiveExecutes_ > 0;
};

View file

@ -0,0 +1,221 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Gears transaction tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="dbwrapperapi.js"></script>
<script type="text/javascript" src="gears_resultset.js"></script>
<script type="text/javascript" src="gears_transaction.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var callbackMock;
var dbMock;
function setUp() {
mockControl = new MockControl();
callbackMock = mockControl.createMock({
onSuccess: function(){},
onFailure: function(){}
});
dbMock = mockControl.createMock({
doBegin: function(){},
doCommit: function(){},
doRollback: function(){},
doExecute: function(){}
});
}
function testConstructTransaction() {
var trans = new google.wspl.gears.Transaction(10, 'foo');
assertEquals('foo', trans.db_);
assertEquals(10, trans.id_);
}
function testExecuteAll_noStatements() {
var trans = new google.wspl.gears.Transaction(27, dbMock);
try {
trans.executeAll([], callbackMock);
fail('Should never get here');
} catch(e) {
if (e.isJsUnitException) {
throw e;
}
assertEquals('did not exception fault on empty statement list',
'Error: Possibly silly attempt to execute empty statement list.',
e.toString());
}
mockControl.verify();
}
function testExecuteAll_noCallback() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3], callbackId, transactionId);
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2, stat3]);
trans.success('resultset1', callbackId);
trans.success('resultset2', callbackId);
trans.success('resultset3', callbackId);
}
function testExecuteAll_success() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3], callbackId, transactionId);
callbackMock.expect().onSuccess(trans, 'resultset1');
callbackMock.expect().onSuccess(trans, 'resultset2');
callbackMock.expect().onSuccess(trans, 'resultset3');
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2, stat3], callbackMock);
trans.success('resultset1', callbackId);
trans.success('resultset2', callbackId);
trans.success('resultset3', callbackId);
mockControl.verify();
}
function testExecuteAll_failure() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var stat4 = new google.wspl.Statement('stat4', [4, 5]);
var transactionId = 27;
var callbackId = 1;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2, stat3, stat4], callbackId,
transactionId);
callbackMock.expect().onSuccess(trans, 'resultset1');
callbackMock.expect().onFailure('sql error');
dbMock.expects().doRollback(transactionId);
trans.executeAll([stat1, stat2, stat3, stat4], callbackMock);
trans.success('resultset1', callbackId);
trans.failure('sql error', callbackId);
// These should do nothing.
trans.success('resultset3', callbackId);
trans.failure('sql error', callbackId);
mockControl.verify();
}
function testExecuteAll_multipleCalls() {
var stat1 = new google.wspl.Statement('stat1');
var stat2 = new google.wspl.Statement('stat2', [1]);
var stat3 = new google.wspl.Statement('stat3', [2, 3]);
var stat4 = new google.wspl.Statement('stat4', [4, 5]);
var transactionId = 27;
var callbackId_1 = 1;
var callbackId_2 = 2;
var callbackId_3 = 3;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
dbMock.expects().doBegin(transactionId);
dbMock.expects().doExecute([stat1, stat2], callbackId_1, transactionId);
dbMock.expects().doExecute([stat3], callbackId_2, transactionId);
callbackMock.expects().onSuccess(trans, 'resultset1').andStub(function() {
trans.executeAll([stat4], callbackMock);
});
dbMock.expects().doExecute([stat4], callbackId_3, transactionId);
callbackMock.expects().onSuccess(trans, 'resultset2').andStub(function() {});
callbackMock.expects().onSuccess(trans, 'resultset3');
callbackMock.expects().onSuccess(trans, 'resultset4');
dbMock.expects().doCommit(transactionId);
trans.executeAll([stat1, stat2], callbackMock);
trans.executeAll([stat3], callbackMock);
trans.success('resultset1', callbackId_1);
trans.success('resultset2', callbackId_1);
trans.success('resultset3', callbackId_2);
trans.success('resultset4', callbackId_3);
mockControl.verify();
}
function testOnSuccess() {
var transactionId = 27;
var callbackId = 3;
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
callbackMock.expects().onSuccess(trans, 'result2');
callbackMock.expects().onSuccess(trans, 'result3');
dbMock.expects().doCommit(transactionId);
trans.numActiveExecutes_ = 1;
trans.activeExecutes_[callbackId] = {
statements: ['s1', 's2', 's3'],
currentStatement: 1,
callback: callbackMock
};
trans.success('result2', callbackId);
trans.success('result3', callbackId);
assertUndefined('activeExecute not removed',
trans.activeExecutes_[callbackId]);
mockControl.verify();
}
function testOnFailure() {
var transactionId = 27;
var callbackId = 5;
callbackMock.expects().onFailure(TypeOf.isA(Error)).andStub(function() {
assertEquals('error not returned', 'error', arguments[0].message);
});
dbMock.expects().doRollback(transactionId);
var trans = new google.wspl.gears.Transaction(transactionId, dbMock);
trans.numActiveExecutes_ = 1;
trans.activeExecutes_[callbackId] = {
statements: ['s1', 's2', 's3'],
currentStatement: 1,
callback: callbackMock
};
trans.failure(Error('error'), callbackId);
trans.success('result3', callbackId);
assertUndefined('activeExecute not removed',
trans.activeExecutes_[callbackId]);
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,94 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview Some simple utilities for supporting Gears usage.
*/
google.wspl.GearsUtils = google.wspl.GearsUtils || {};
/**
* Returns an array of hash objects, one per row in the result set,
* where the column names in the query are used as the members of
* the object.
*
* @param {GearsResultSet} rs the result set returned by execute.
* @return {Array.<Object>} An array containing hashes. Returns an empty
* array if there are no matching rows.
*/
google.wspl.GearsUtils.resultSetToObjectArray = function(rs) {
var rv = [];
if (rs) {
var cols = rs['fieldCount']();
var colNames = [];
for (var i = 0; i < cols; i++) {
colNames.push(rs['fieldName'](i));
}
while (rs['isValidRow']()) {
var h = {};
for (var i = 0; i < cols; i++) {
h[colNames[i]] = rs['field'](i);
}
rv.push(h);
rs['next']();
}
}
return rv;
};
/**
* Maximum file name length.
* @type {number}
* @private
*/
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_ = 64;
/**
* Ensures that the given dbName is safe to use as a Gears database name.
* @type {!string} dbName
* @return {!string} The sanitized name.
* @private
*/
google.wspl.GearsUtils.makeSafeFileName_ = function(dbName) {
var sanitizedFileName = dbName.replace(/[^a-zA-Z0-9\.\-@_]/g, '');
if (sanitizedFileName.length <=
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_) {
return sanitizedFileName;
} else {
return sanitizedFileName.substring(0,
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_);
}
};
/**
* Opens a Gears Database using the provided userid and name.
* @param {string} userId The user to which the database belongs.
* @param {string} name The database's name.
* @param {GearsDatabase} db The Gears database to open.
* @param {function(string)} opt_logger A logger function for writing
* messages.
* @return {GearsDatabase} The open GearsDatabase object.
*/
google.wspl.GearsUtils.openDatabase = function(userId, name, db,
opt_logger) {
var dbId = userId + '-' + name;
var safeDbId = google.wspl.GearsUtils.makeSafeFileName_(dbId);
if (opt_logger && dbId != safeDbId) {
opt_logger('database name ' + dbId + '->' + safeDbId);
}
db.open(safeDbId);
return db;
};

View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Gears Utilities Tests</title>
<script type="text/javascript" src="../jsunit/app/jsUnitCore.js"></script>
<script type="text/javascript" src="jsmock.js"></script>
<script type="text/javascript" src="global_functions.js"></script>
<script type="text/javascript" src="gearsutils.js"></script>
</head>
<body>
<script type='text/javascript'>
var mockControl;
var mockGearsDb;
var mockLogger;
function setUp() {
mockControl = new MockControl();
mockGearsDb = mockControl.createMock({
execute: function(){},
open: function(){},
close: function(){},
remove: function(){}
});
mockLogger = mockControl.createMock({
info: function(){}
});
}
function testMakeSafeFileName() {
assertEquals('simple name wrongly modified', 'ABCDEFGHIJKLMNOPQRSTUVXYZ',
google.wspl.GearsUtils.makeSafeFileName_('ABCDEFGHIJKLMNOPQRSTUVXYZ'));
assertEquals('simple name wrongly modified', 'abcdefghijklmnopqrstuvxyz',
google.wspl.GearsUtils.makeSafeFileName_('abcdefghijklmnopqrstuvxyz'));
assertEquals('simple name wrongly modified', '.-@_',
google.wspl.GearsUtils.makeSafeFileName_('.-@_'));
var longName = 'abcdefghijklmnopqrstuvxyz' + 'abcdefghijklmnopqrstuvxyz' +
'abcdefghijklmnopqrstuvxyz';
assertEquals('long name not truncated',
google.wspl.GearsUtils.MAX_FILE_NAME_LENGTH_,
google.wspl.GearsUtils.makeSafeFileName_(longName).length);
assertEquals('forbidden character not stripped', 'abc',
google.wspl.GearsUtils.makeSafeFileName_('a#b$()//c'));
}
function testCreateDatabase_success() {
mockGearsDb.expects().open('hello-there');
google.wspl.GearsUtils.openDatabase('hello', 'there', mockGearsDb,
mockLogger);
mockControl.verify();
}
function testCreateDatabase_invalidname() {
mockLogger.expects().info('database name foo/bar-ranch->foobar-ranch');
mockGearsDb.expects().open('foobar-ranch');
assertEquals('bad return value', mockGearsDb,
google.wspl.GearsUtils.openDatabase('foo/bar', 'ranch',
mockGearsDb, mockLogger.info));
mockControl.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,72 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview Global function implementations used for running the
* web storage portability layer code outside of the Google internal
* development environment.
*
* Include this file only once.
*
*/
/**
* Namespace object.
* @type {Object}
*/
var google = google || {};
google.wspl = google.wspl || {};
google.wspl.gears = google.wspl.gears || {};
/**
* Inherit the prototype methods from one constructor into another.
* @param {Function} childConstructor Child class.
* @param {Function} parentConstructor Parent class.
*/
google.inherits = function(childConstructor, parentConstructor) {
function tempConstructor() {};
tempConstructor.prototype = parentConstructor.prototype;
childConstructor.prototype = new tempConstructor();
childConstructor.prototype.constructor = childConstructor;
};
/**
* Binds a context object to the function.
* @param {Function} fn The function to bind to.
* @param {Object} context The "this" object to use when the function is run.
* @return {Function} A partially-applied form of fn.
*/
google.bind = function(fn, context) {
return function() {
return fn.apply(context, arguments);
};
};
/**
* Null function used for callbacks.
* @type {Function}
*/
google.nullFunction = function() {};
/**
* Simple logging facility.
* @param {string} msg A message to write to the console.
*/
google.logger = function(msg) {
// Uncomment the below to get log messages
// May require firebug enabled to work in FireFox.
// window.console.info(msg);
};

View file

@ -0,0 +1,347 @@
<!DOCTYPE html>
<!--
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
A simple application showing the use of the web storage portability layer
code and cache pattern for offline web application.
-->
<html>
<head>
<title>SimpleNotes Demo</title>
<link href="styles.css" rel="stylesheet" type="text/css" />
<script src="../global_functions.js" language="javascript"
type="text/javascript"></script>
<script src="../dbwrapperapi.js" language="javascript"
type="text/javascript"></script>
<script src="../dbwrapper_html5.js" language="javascript"
type="text/javascript"></script>
<script src="../databasefactory.js" language="javascript"
type="text/javascript"></script>
<script src="template.js" language="javascript"
type="text/javascript"></script>
<script src="simplenotes.js" language="javascript"
type="text/javascript"></script>
</head>
<body>
<div id="list-note" class="screen list-note-screen" >
<div class="title-bar">Note List</div>
<div class="top command-bar">
<button id="view-note-new">New</button>
</div>
<div class="status-bar">status bar</div>
<div class="list-note-container-empty" style="display:none;"></div>
<div class="list-note-container" style="display:none;">
<div class="list-note-item">
<a href="javascript:showNote(%noteKey%);">%subject%</a>
</div>
</div>
<div class="top command-bar">
<button id="view-note-new">New</button>
</div>
</div>
<div id="view-note" class="screen view-note-screen" style="display:none;">
<div class="title-bar">View Note</div>
<div class="top command-bar">
<button id="view-note-back">Back</button>
<button id="view-note-edit">Edit Note</button>
</div>
<div class="status-bar">status bar</div>
<div class="view-note-container" style="display:none;">
<div class="subject">%subject%</div>
<div class="body">%body%</div>
</div>
<div class="bottom command-bar">
<button id="view-note-back">Back</button>
<button id="view-note-edit">Edit Note</button>
</div>
</div>
<div id="edit-note" class="screen edit-note-screen" style="display:none;">
<div class="title-bar">Edit Note</div>
<div class="top command-bar">
<button id="edit-back-save">Save Note</button>
<button id="edit-back-revert">Revert Note</button>
</div>
<div class="status-bar">status bar</div>
<div>Subject:</div>
<textarea rows="1" style="width:100%;" id="edit-note-subject"></textarea>
<br>
<div>Note:</div>
<textarea rows="20" style="width:100%;" id="edit-note-body"></textarea>
<div class="bottom command-bar">
<button id="edit-back-save">Save Note</button>
<button id="edit-back-revert">Revert Note</button>
</div>
</div>
</body>
<script>
/**
* Tests the given DOM element for a specific CSS class.
* @param {Object} e A DOM element.
* @param {string} css A CSS class identifier to match.
* @return {boolean} true if the element has the CSS class.
*/
function hasCssClass(e, css) {
return e && e.className && e.className.search(css + '( |$)') > -1;
};
/**
* Finds the parent element of a given element that has
* the specified CSS class.
* @param {Object} e A DOM element.
* @param {string} css A CSS class identifier to match.
* @param {Object} opt_stop A parent DOM element to stop at.
* @return {Object} A DOM element parent of e or null.
*/
function findParentOfClass(e, css, opt_stop) {
var stop = opt_stop || document;
while (e && e != stop && !hasCssClass(e, css)) {
e = e.parentElement;
}
return hasCssClass(e, css) ? e : null;
};
/**
* Navigates to the note creation screen.
*/
function editNote(event) {
google.logger('editNote: <' + currentNote.subject + '>');
hideBlock('#edit-note .status-bar');
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
subject.value = currentNote.subject;
body.value = currentNote.body;
google.logger('end of editNote');
}
/**
* Saves a note.
*/
function saveNote(event) {
google.logger('saveNote: do database or other activity here');
// Copy contents from fields and save.
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
cache.applyUiChange(currentNote.noteKey, subject.value,
body.value, showNote);
google.logger('saveNote');
}
/**
* Abandons the note.
*/
function abandonNote(event) {
google.logger('abandonNote: do database or other activity here');
showNote(currentNote.noteKey);
google.logger('abandonNote');
}
/**
* Prepares to edit a new note.
*/
function newNote(event) {
google.logger('newNote: do database or other activity here');
alert('not implemented');
// grab the contents and inject into the database.
var subject = document.getElementById('edit-note-subject');
var body = document.getElementById('edit-note-body');
subject.value = '';
body.value = '';
google.logger('newNote');
}
/**
* Maps button names to customizer functions.
*/
var buttonHandlerSpecification = {
'view-note-new': ['edit-note', newNote],
'view-note-back': ['list-note', showNoteList],
'view-note-edit': ['edit-note', editNote],
'edit-back-save': ['view-note', saveNote],
'edit-back-revert': ['view-note', abandonNote]
};
/**
* Hides a block specified by a CSS selector and parent node.
* @param {string} selector A CSS selector string.
* @param {Object} opt_el A DOM element.
*/
function hideBlock(selector, opt_el) {
var el = opt_el || document;
var targetEl = el.querySelector(selector);
targetEl.style.display = 'none';
}
/**
* Shows a block specified by a CSS selector and parent node, inserting
* the specified string as the innerHTML property of the selected block.
* @param {string} selector A CSS selector
* @param {string} html String to assign to the innerHTML property.
* @param {Object} opt_el A DOM element.
*/
function showBlock(selector, html, opt_el) {
var el = opt_el || document;
var targetEl = el.querySelector(selector);
if (html) targetEl.innerHTML = html;
targetEl.style.display = 'block';
}
/**
* Handles all button events.
*/
function buttonHandler(event) {
google.logger('clicked on: ' + event.target.id);
var currentScreen = findParentOfClass(event.target, 'screen');
var nextScreen = document.getElementById(
buttonHandlerSpecification[event.target.id][0]);
var helperFunction = buttonHandlerSpecification[event.target.id][1];
// All button handlers have a common implementation.
// 1. hide current screen
currentScreen.style.display = 'none';
// 2. show new screen
nextScreen.style.display = 'block';
// 3. Activate status
showBlock('.status-bar', 'Working...', nextScreen);
// 4. Call customizer function (which will probably query the model
// stored in the cache.
helperFunction(event);
// 5. A (possibly asynchronous) callback from the customizer will
// finish the action by running the appropriate view function.
google.logger('end of button Handler');
}
/**
* Call back from a specific JavaScript url to show a specific
* note.
* @param {number} noteKey
* @return {boolean}
*/
function showNote(noteKey) {
hideBlock('#list-note');
showBlock('#view-note .status-bar', 'Working...');
showBlock('#view-note');
cache.getValues('fullnote', [noteKey], renderFullNote);
return false;
}
/**
* Generates HTML for a note and display it.
* @param {Object} oneNote
* @param {boolean} complete True if the note has been provided or
* if an updated note is arriving. (Necessary for future support for
* sub-note updates.)
*/
function renderFullNote(oneNote, complete) {
google.logger('renderFullNote');
showBlock('.view-note-container', noteViewTemplate.process(oneNote));
if (complete) {
hideBlock('#view-note .status-bar');
}
currentNote = oneNote;
google.logger('done renderFullNote');
}
/**
* Generates HTML for a note list and display it.
* @param {Array.<Object>} noteHeaders
* @param {boolean} complete True if all in-existence notes that lie
* in the query range have been provided even if less than what was
* requested.
*/
function renderNoteList(noteHeaders, complete) {
google.logger('renderNoteList: ' + noteHeaders.length);
if (noteHeaders.length == 0) {
showBlock('.list-note-container-empty', 'No notes');
} else {
var notes = noteHeaders.map(function(n) {
return noteListTemplate.process(n);
});
showBlock('.list-note-container', notes.join());
}
if (complete) {
hideBlock('#list-note .status-bar');
}
google.logger('done renderNoteList');
}
/**
* Query for an updated note list.
*/
function showNoteList() {
google.logger('showNoteList');
cache.getValues('list', [0,20], renderNoteList);
currentNote = undefined;
google.logger('end of showNoteList');
}
/**
* A constructor for the controller. Sets up the UI and
* queries the cache for initial notelist contents.
*/
function createController() {
google.logger('createController:');
var buttons = document.querySelectorAll('.command-bar button');
Array.prototype.forEach.call(buttons, function(b) {
b.addEventListener('click', buttonHandler);
});
google.logger('controller is live');
showNoteList();
}
google.logger('about to fetch the template');
var currentNote = undefined;
var noteListTemplate = new google.wspl.simplenotes.Template(
document.querySelector('.list-note-container').innerHTML);
var noteViewTemplate = new google.wspl.simplenotes.Template(
document.querySelector('.view-note-container').innerHTML);
// var noteEditTemplate = new google.wspl.simplenotes.Template();
google.logger('created the template');
// debugging code... tear out...
// test the template construction...
// showBlock('.list-note-container', noteListTemplate.process(
// {noteKey: 123, subject: 'hi rob'}));
google.logger('loaded database');
var cache = new google.wspl.simplenotes.Cache();
cache.startCache(createController);
google.logger('finished loading page');
</script>
</html>

View file

@ -0,0 +1,503 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview A concrete example of the cache pattern for building an offline
* webapplication: a cache for SimpleNotes.
*/
google.wspl.simplenotes = google.wspl.simplenotes || {};
google.logger('start simplenotes.js');
/**
* Status keys for the write buffer.
* @enum {number}
*/
google.wspl.simplenotes.WriteBufferStates = {
/**
* The update is in flight to the server.
*/
INFLIGHT: 1,
/**
* The update needs to be (re)sent to the server but is not in flight.
*/
SEND: 2,
/**
* The update needs to be applied to the cached notes.
*/
REAPPLY: 8
};
/**
* Creates a SimpleNotes cache wrapping a backing database.
* @constructor
*/
google.wspl.simplenotes.Cache = function() {
this.dbms_ = google.wspl.DatabaseFactory.createDatabase(
'simple-notes', 'http://yourdomain/dbworker.js');
/**
* Cache directory is a two-tuple over a range. (Holes
* must be allowed to support delection.)
* This is the lower (inclusive) bound of the cached range.
* @type {number}
* @private
*/
this.start_ = -1;
/**
*
* This is the upper (inclusive) bound of the cached range.
* @type {number}
* @private
*/
this.end_ = -1;
/**
* Start of range of notes known to exist on server at time of last
* response.
* @param {number}
*/
this.serverStart_ = -1;
/**
* End of range of notes known to exist on server at time of last
* response.
* @param {number}
*/
this.serverEnd_ = -1;
/**
* Time of last refresh.
* @type {number}
* @private
*/
this.lastRefresh_ = -1;
/**
* Last missing query.
* @type {Object}
* @private
*/
this.lastMiss_ = undefined;
};
/**
* Interval between refreshes in milliseconds.
* @type {number}
* @private
*/
google.wspl.simplenotes.Cache.TIME_BETWEEN_REFRESH_ = 2000;
google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_ =
new google.wspl.Statement(
'CREATE TABLE IF NOT EXISTS cached_notes (' +
'noteKey INTEGER UNIQUE PRIMARY KEY,' +
'subject TEXT,' +
'body TEXT' +
');'
);
google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_ =
new google.wspl.Statement(
'CREATE TABLE IF NOT EXISTS write_buffer (' +
'sequence INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT,' +
'noteKey INTEGER,' +
'status INTEGER,' +
'subject TEXT,' +
'body TEXT' +
');'
);
google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_ =
new google.wspl.Statement(
'SELECT MIN(noteKey) as minNoteKey FROM cached_notes;');
google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_ =
new google.wspl.Statement(
'SELECT MAX(noteKey) as maxNoteKey FROM cached_notes;');
/**
* Builds a cache and writebuffer combination for notes and then
* invokes the given callback.
* @param {function) callback.
*/
google.wspl.simplenotes.Cache.prototype.startCache = function(callback) {
google.logger('startCache');
var statc = 0;
var self = this;
var perStatCallback = function(tx, result) {
google.logger('perStatCallback');
if (statc == 4) {
self.start_ = (result.isValidRow()) ? result.getRow().minNoteKey : -1;
self.serverStart_ = self.start_; // Temporary. Remove when server exists.
} else if (statc == 5) {
self.end_ = (result.isValidRow()) ? result.getRow().maxNoteKey : -1;
self.serverEnd_ = self.end_; // Temporary. Remove when server exists.
}
statc++;
};
this.dbms_.executeAll([
google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_,
google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_,
google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_,
google.wspl.simplenotes.Cache.CREATE_REPLAY_TRIGGER_,
google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_,
google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_],
{onSuccess: perStatCallback, onFailure: this.logError_},
{onSuccess: callback, onFailure: this.logError_});
google.logger('finished startCache');
};
/**
* Stub function to be replaced with a server communication.
* @param {Array.<Object>} updates Payload to send to server.
*/
google.wspl.simplenotes.Cache.prototype.sendXhrPost = function(updates) {
google.logger('Should dispatch XHR to server now.');
};
/**
* @type {google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.LIST_CACHED_NOTES_ =
new google.wspl.Statement(
'SELECT noteKey, subject from cached_notes WHERE ' +
'noteKey >= ? AND ' +
'noteKey <= ? ' +
';'
);
/**
* Tests if the given range is stored in the cache.
* Note that this mechanism requires extension to handle the
* creation of new notes.
* @param {number} start Lower bound (inclusive) on range.
* @param {number} end Uppder bound (inclusive) on range.
* @private
*/
google.wspl.simplenotes.Cache.prototype.isCacheHit_ = function(start, end) {
return start >= this.start_ && end <= this.end_
};
/**
* Logs a possibly useful error message.
* @param {Object} error An error descriptor.
* @private
*/
google.wspl.simplenotes.Cache.prototype.logError_ = function(error) {
google.logger('Simple Notes Cache is sad: ' + error);
};
/**
* Queries the cache for a list of note headers.
* @param {number} start The lower id in a range of notes.
* @param {number} end The higher id in a range of notes.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
* @private
*/
google.wspl.simplenotes.Cache.prototype.getNoteList_ = function(start, end,
valuesCallback) {
var notes = [];
var accumulateResults = function(tx, result) {
for(; result.isValidRow(); result.next()) {
notes.push(result.getRow());
google.logger('pushed...');
}
};
var inTransactionGetNotes = function(tx) {
tx.execute(google.wspl.simplenotes.Cache.LIST_CACHED_NOTES_.
createStatement([start, end]), {
onSuccess: accumulateResults,
onFailure: this.logError_});
};
var hit = this.isCacheHit_(start, end);
this.dbms_.createTransaction(inTransactionGetNotes, {onSuccess: function() {
valuesCallback(notes, hit);
}, onFailure: this.logError_});
if (hit) {
this.fetchFromServer(this.start_, this.end_); // Refresh
} else {
this.fetchFromServer(Math.min(this.start_, start), Math.max(this.end_, end));
this.lastMiss_ = {callback: valuesCallback, start: start, end: end};
}
};
/**
* @type {google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.GET_ONE_NOTE_ =
new google.wspl.Statement(
'SELECT noteKey, subject, body from cached_notes WHERE ' +
'noteKey = ? ' +
';'
);
/**
* Queries the cache for a list of note headers.
* @param {number} noteId The note to get from the cache.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
* @private
*/
google.wspl.simplenotes.Cache.prototype.getOneNote_ = function(noteId,
callback) {
var note;
this.dbms_.execute(google.wspl.simplenotes.Cache.GET_ONE_NOTE_.
createStatement([noteId]),
{onSuccess: function(tx, result) { note = result.getRow(); },
onFailure: this.logError_},
{onSuccess: function() { callback(note, true); },
onFailure: this.logError_});
};
/**
* Queries the cache for either a list of notes or a single note including
* its body.
* @param {string} type The kind of values desired: 'list' or 'fullnote'.
* @param {Array.<number>} query The query for the values.
* @param {function(Array.<Object>)} valuesCallback A function to call
* with the result set after the transaction has completed.
*/
google.wspl.simplenotes.Cache.prototype.getValues = function(type,
query, valuesCallback) {
// Reduce any query to what would be available from the server
query[0] = Math.max(this.serverStart_, query[0]);
query[1] = Math.min(this.serverEnd_, query[1]);
if (type == 'list') {
this.getNoteList_(query[0], query[1], valuesCallback);
} else if (type == 'fullnote') {
this.getOneNote_(query[0], valuesCallback);
}
};
/**
* SQL trigger to insert a new change from write buffer to
* cache.
* @private
*/
google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_ =
new google.wspl.Statement(
'CREATE TRIGGER IF NOT EXISTS updateTrigger ' +
'AFTER INSERT ON write_buffer ' +
'BEGIN ' +
' REPLACE INTO cached_notes ' +
' SELECT noteKey, subject, body ' +
' FROM write_buffer WHERE status & 8 = 8; ' +
' UPDATE write_buffer SET status = status & ~ 8; ' +
'END;'
);
/**
* SQL trigger to replay changes from the write buffer to
* the cache.
* @private
*/
google.wspl.simplenotes.Cache.CREATE_REPLAY_TRIGGER_ =
new google.wspl.Statement(
'CREATE TRIGGER IF NOT EXISTS replayTrigger ' +
'AFTER UPDATE ON write_buffer WHEN NEW.status & 8 = 8 ' +
'BEGIN ' +
' REPLACE INTO cached_notes ' +
' SELECT noteKey, subject, body ' +
' FROM write_buffer ' +
' WHERE noteKey = NEW.noteKey ' +
' ORDER BY sequence ASC;' +
' UPDATE write_buffer SET status = status & ~ 8; ' +
'END;'
);
/**
* SQL statement to mark actions for replay.
*/
google.wspl.simplenotes.Cache.MARK_FOR_REPLAY =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status | 8;');
/**
* SQL statement to insert notes updates.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_ =
new google.wspl.Statement(
'INSERT INTO write_buffer (' +
'noteKey, status, subject, body' +
') ' + 'VALUES(?,?,?,?);');
/**
* Updates the given entry and write a new write buffer entry.
* @param {number} noteKey
* @param {string} subject
* @param {string} body
* @param {function(number)} ackCallback
*/
google.wspl.simplenotes.Cache.prototype.applyUiChange = function(noteKey,
subject, body, ackCallback) {
var self = this;
var update = [noteKey, 2 | 8, subject, body];
var stat = google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_.createStatement(
update);
this.dbms_.execute(stat, null, {onSuccess: function() {
google.logger('applyUiChange cb');
ackCallback(noteKey);
}, onFailure: function (error) {
self.logError_(error);
ackCallback(-1);
}});
};
/**
* SQL statement to insert notes updates.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.INSERT_NOTE_ =
new google.wspl.Statement(
'REPLACE INTO cached_notes (noteKey, subject, body) ' +
'VALUES(?,?,?);' );
/**
* SQL statement to force replay of pending actions by setting a bit
* flag on each write-buffer row indicating that it should be reapplied
* to the contents of the cache.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.FORCE_REPLAY_ =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status | 8;' );
/**
* SQL statement to delete notes no longer to be cached.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.EVICT_ =
new google.wspl.Statement(
'DELETE FROM cached_notes WHERE noteKey < ? OR noteKey > ?;');
/**
* Applies the changes delivered from the server by first inserting
* them into the cache and reapplying the write-buffer to the cache.
* @param {!Array.<Object>} notes An array of arrays.
*/
google.wspl.simplenotes.Cache.prototype.insertUpdate = function(notes) {
var self = this; var stats = [];
var start = notes[0].noteKey;
var end = notes[0].noteKey;
for (var i = 0; i < notes.length; i++) {
stats.push(google.wspl.simplenotes.Cache.INSERT_NOTE_.
createStatement([notes[i].noteKey, notes[i].subject, notes[i].body]));
start = Math.min(start, notes[0].noteKey);
end = Math.max(end, notes[0].noteKey);
}
stats.push(google.wspl.simplenotes.Cache.EVICT_.createStatement([start, end]));
stats.push(google.wspl.simplenotes.Cache.FORCE_REPLAY_);
var inTrans = function(tx) {
self.start_ = start;
self.end_ = end;
tx.executeAll(stats);
};
var afterInsert = function(tx) {
if (this.lastMiss_ &&
this.isCacheHit_(this.lastMiss_.start, this.lastMiss_.end)) {
this.lastMiss_.callback(notes);
this.lastMiss_ = undefined;
}
};
this.dbms_.createTransaction(inTrans, {onSuccess: afterInsert,
onError: this.logError_});
};
/**
* SQL statement to force replay of pending actions by setting a bit
* flag on each write-buffer row indicating that it should be reapplied
* to the contents of the cache.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.GET_UPDATES_TO_RESEND_ =
new google.wspl.Statement(
'SELECT noteKey, subject, body FROM write_buffer WHERE status & 2 = 2;');
/**
* SQL statement to mark write buffer statements as inflight.
* @type {!google.wspl.Statement}
* @private
*/
google.wspl.simplenotes.Cache.MARK_AS_INFLIGHT_ =
new google.wspl.Statement(
'UPDATE write_buffer SET status = status & ~2 | 1 WHERE status & 2 = 2;');
/**
* Fetches new material from the server as required.
* @param {number} start
* @param {number} end
* @param {function} opt_valueCallBack
*/
google.wspl.simplenotes.Cache.prototype.fetchFromServer = function(start,
end) {
google.logger('fetchFromServer');
var now = this.dbms_.getCurrentTime();
if (start >= this.start_ && end <= this.end_ &&
now - this.lastRefresh_ <
google.wspl.simplenotes.Cache.TIME_BETWEEN_REFRESH_) {
return;
}
var updates = []; var self = this; var flag = 1; var sql = []
sql.push(google.wspl.simplenotes.Cache.GET_UPDATES_TO_RESEND_);
sql.push(google.wspl.simplenotes.Cache.MARK_AS_INFLIGHT_);
var accumulateUpdates = function(tx, rs) {
if (flag == 1) {
for(; rs.isValidRow(); rs.next()) { updates.push(['u', rs.getRow()]); }
flag++;
}
};
var ackAndPost = function() {
updates.push(['q', {start: start, end: end}]);
self.sendXhrPost(updates);
};
this.dbms_.executeAll(sql,
{onSuccess: accumulateUpdates, onFailure: this.logError_},
{onSuccess: ackAndPost, onFailure: this.logError_});
};

View file

@ -0,0 +1,66 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
body {
background-color: #efefef;
font: medium "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
}
.status-bar {
background-color: #ffd21d;
display: none;
-webkit-border-radius: 6.0px;
-moz-border-radius: 6.0px;
border-style: solid;
border-width: 1px;
padding: 3px;
text-align: center;
}
.screen {
border: thin solid #939393;
padding: 2px 2px 2px 2px;
}
.command-bar {
background-color: #275390;
}
.title-bar {
font-weight: bold;
background-color: #4b4b4b;
text-align: center;
padding: 3px;
color: #aaa;
}
textarea {
font-size: 16pt;
}
button {
text-align:center;
font-family: Helvetica;
font-weight: bold;
font-size: 20px;
text-decoration: none;
text-shadow: #fff 0px 2px 2px;
padding: 3px;
-webkit-border-radius: 6.0px;
-moz-border-radius: 6.0px;
margin-right: 4px;
margin-left: 4px;
}

View file

@ -0,0 +1,75 @@
/*
Copyright 2009 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @fileoverview template.js contains the implementation for a simple template
* scheme. Templates are fragments of HTML containing patterns into which
* arguments will be substituted.
*/
google.wspl.simplenotes = google.wspl.simplenotes || {};
/**
* Template class constructor. A template is an object which will
* substitute provided parameters into a marked-up string.
* @param {string} template
* @constructor
*/
google.wspl.simplenotes.Template = function(template) {
this.template_ = template;
this.res_ = null;
};
/**
* Returns the template expanded with the given args where args
* is an object (acting as an associative array) binding keys (found
* in the template wrapped with % symbols) to the associated
* values.
*
* Template substitution symbols without corresponding arguments
* will be passed through unchanged to the output.
*
* We assume that in typical use, the same template will be expanded
* repeatedly with different values. In this case, storing and re-using
* previously generated regular expressions will provide a performance
* improvement.
* @param {Object} args associates names with values
* @param {boolean} opt_rebuild set to true to force re-building the
* the regular epxression.
*/
google.wspl.simplenotes.Template.prototype.process = function(args,
opt_rebuild) {
var rebuild = opt_rebuild || false;
if (rebuild || this.res_ == null) {
var accumulatedRe = [];
this.res_ = null;
for (var a in args) {
accumulatedRe.push('%' + String(a) + '%');
}
if (accumulatedRe.length > 0) {
this.res_ = new RegExp(accumulatedRe.join('|'), 'g');
}
}
if (this.res_ != null) {
return this.template_.replace(this.res_, function(match) {
var keyName = match.slice(1,-1);
return args[keyName];
});
} else {
return this.template_;
}
};