Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Working on selectJSON, refactoring some code.

  • Loading branch information...
commit 792501e139ff69b2255cff68ca7939c8f3f91cf9 1 parent 29850cd
@zefhemel zefhemel authored
View
6 README.md
@@ -90,7 +90,7 @@ Browser support
* Modern webkit browsers (Google Chrome and Safari)
* Firefox (through Google Gears)
-* Android browser (tested on 1.6 and 2.1)
+* Android browser (tested on 1.6 and 2.x)
* iPhone browser (iPhone OS 3+)
* Palm WebOS (tested on 1.4.0)
@@ -286,7 +286,9 @@ using the `list(...)` method on a database `QueryCollection`, which also
flushes first, although this behavior may change in the future.
Dumping and restoring data
---------------------------------
+--------------------------
+
+The library supports two kinds of dumping and restoring data.
`persistence.dump` can be used to create an object containing a full
dump of a database. Naturally, it is adviced to only do this with
View
37 docs/DEVELOPMENT.md
@@ -40,3 +40,40 @@ Extension hooks
add new functionality to constructo functions, such as `Task.index`.
* `persistence.flushHooks`: a list of functions to be called before flushing.
* `persistence.schemaSyncHooks`: a list of functions to be called before syncing the schema.
+
+Idioms
+------
+
+Because persistence.js is an asynchronous library, a lot happens
+asynchronously (shocker). The way I typically handle an unknown
+sequence of asynchronous calls is as follows, I know it's expensive on
+the stack (it makes a lot of recursive calls), but it's the best I've
+been able to come up with.
+
+Let's say we have an array `myArray` of values and we have to invoke a
+function `someAsyncFunction` on each item sequentially. Except, the
+function is asynchronous, and thus does not return a value
+immediately, but instead has a callback that is called with the
+result. This is how I typically implement that in persistence.js, note
+that this destroys `myArray`, at the end the array is empty, so if you
+care about its value, `.slice(0)` it first.
+
+ var myArray = [1, 2, 3, 4, 5];
+
+ function processOne() {
+ var item = myArray.pop(); // pop (last) item from the array
+ someAsyncFunction(item, function(result) {
+ // do something with result
+ if(myArray.length > 0) {
+ processOne();
+ } else {
+ // Do whatever you need when you're completely done
+ }
+ });
+ }
+
+ if(myArray.length > 0) {
+ processOne();
+ } else {
+ // Do whatever you need when you're completely done
+ }
View
252 lib/persistence.js
@@ -82,7 +82,6 @@ persistence.get = function(arg1, arg2) {
persistence.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj
persistence.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection
-
persistence.getObjectsToRemove = function() { return this.objectsToRemove; }
persistence.getTrackedObjects = function() { return this.trackedObjects; }
@@ -91,7 +90,7 @@ persistence.get = function(arg1, arg2) {
persistence.flushHooks = [];
persistence.schemaSyncHooks = [];
- // Enable debugging
+ // Enable debugging (display queries using console.log etc)
persistence.debug = true;
persistence.subscribeToGlobalPropertyListener = function(coll, entityName, property) {
@@ -233,8 +232,57 @@ persistence.get = function(arg1, arg2) {
this.objectsToRemove = {};
this.globalPropertyListeners = {};
this.queryCollectionCache = {};
- }
+ };
+ /**
+ * asynchronous sequential version of Array.prototype.forEach
+ * @param array the array to iterate over
+ * @param fn the function to apply to each item in the array, function
+ * has two argument, the first is the item value, the second a
+ * callback function
+ * @param callback the function to call when the forEach has ended
+ */
+ persistence.asyncForEach = function(array, fn, callback) {
+ array = array.slice(0); // Just to be sure
+ function processOne() {
+ var item = array.pop();
+ fn(item, function(result) {
+ if(array.length > 0) {
+ processOne();
+ } else {
+ callback();
+ }
+ });
+ }
+ if(array.length > 0) {
+ processOne();
+ } else {
+ callback();
+ }
+ };
+
+ /**
+ * asynchronous parallel version of Array.prototype.forEach
+ * @param array the array to iterate over
+ * @param fn the function to apply to each item in the array, function
+ * has two argument, the first is the item value, the second a
+ * callback function
+ * @param callback the function to call when the forEach has ended
+ */
+ persistence.asyncParForEach = function(array, fn, callback) {
+ var completed = 0;
+ if(array.length === 0) {
+ callback();
+ }
+ for(var i = 0; i < array.length; i++) {
+ fn(array[i], function() {
+ completed++;
+ if(completed === array.length) {
+ callback();
+ }
+ });
+ }
+ };
/**
* Retrieves or creates an entity constructor function for a given
@@ -275,11 +323,13 @@ persistence.get = function(arg1, arg2) {
persistence.defineProp(that, f, function(val) {
// setterCallback
var oldValue = that._data[f];
- that._data[f] = val;
- that._dirtyProperties[f] = oldValue;
- that.triggerEvent('set', that, f, val);
- that.triggerEvent('change', that, f, val);
- session.propertyChanged(that, f, oldValue, val);
+ if(oldValue !== val) { // Don't mark properties as dirty and trigger events unnecessarily
+ that._data[f] = val;
+ that._dirtyProperties[f] = oldValue;
+ that.triggerEvent('set', that, f, val);
+ that.triggerEvent('change', that, f, val);
+ session.propertyChanged(that, f, oldValue, val);
+ }
}, function() {
// getterCallback
return that._data[f];
@@ -530,64 +580,35 @@ persistence.get = function(arg1, arg2) {
});
properties = expensiveProperties.slice();
- function processOneProperty() {
- var p = properties.pop();
-
+ persistence.asyncForEach(properties, function(p, callback) {
if(meta.hasOne[p]) {
that.fetch(session, tx, p, function(obj) {
buildJSON(obj, tx, includeProperties[p], function(result) {
item[p] = result;
- if(properties.length > 0) {
- processOneProperty();
- } else {
- callback(item);
- }
+ callback();
});
});
} else if(meta.hasMany[p]) {
persistence.get(that, p).list(function(objs) {
item[p] = [];
- function oneObj() {
- var obj = objs.pop();
- function next() {
- if(objs.length > 0) {
- oneObj();
+ persistence.asyncForEach(objs, function(obj, callback) {
+ var obj = objs.pop();
+ if(includeProperties[p] === true) {
+ item[p].push({id: obj.id});
+ callback();
} else {
- if(properties.length > 0) {
- processOneProperty();
- } else {
- callback(item);
- }
+ buildJSON(obj, tx, includeProperties[p], function(result) {
+ item[p].push(result);
+ callback();
+ });
}
- }
- if(includeProperties[p] === true) {
- item[p].push({id: obj.id});
- next();
- } else {
- buildJSON(obj, tx, includeProperties[p], function(result) {
- item[p].push(result);
- next();
- });
- }
- }
- if(objs.length > 0) {
- oneObj();
- } else {
- if(properties.length > 0) {
- processOneProperty();
- } else {
- callback(item);
- }
- }
+ }, callback);
});
}
- }
- if(properties.length > 0) {
- processOneProperty();
- } else {
+ }, function() {
callback(item);
- }
- };
+ });
+ }; // End of buildJson
Entity.prototype.fetch = function(session, tx, rel, callback) {
var args = argspec.getArgs(arguments, [
@@ -640,7 +661,80 @@ persistence.get = function(arg1, arg2) {
Entity.all = function(session) {
session = session || persistence;
return session.uniqueQueryCollection(new AllDbQueryCollection(session, entityName));
- }
+ };
+
+ Entity.fromSelectJSON = function(session, tx, jsonObj, callback) {
+ var args = argspec.getArgs(arguments, [
+ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence },
+ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null },
+ { name: 'jsonObj', optional: false },
+ { name: 'callback', optional: false, check: argspec.isCallback() }
+ ]);
+ session = args.session;
+ tx = args.tx;
+ jsonObj = args.jsonObj;
+ callback = args.callback;
+
+ if(!tx) {
+ session.transaction(function(tx) {
+ Entity.fromSelectJSON(session, tx, jsonObj, callback);
+ });
+ return;
+ }
+
+ if(typeof jsonObj === 'string') {
+ jsonObj = JSON.parse(jsonObj);
+ }
+
+ function loadedObj(obj) {
+ if(!obj) {
+ obj = new Entity();
+ if(jsonObj.id) {
+ obj.id = jsonObj.id;
+ }
+ }
+ var expensiveProperties = [];
+ for(var p in jsonObj) {
+ if(jsonObj.hasOwnProperty(p)) {
+ if(p === 'id') {
+ continue;
+ } else if(meta.fields[p]) { // regular field
+ persistence.set(obj, p, persistence.jsonToEntityVal(jsonObj[p], meta.fields[p]));
+ } else {
+ expensiveProperties.push(p);
+ }
+ }
+ }
+ persistence.asyncForEach(expensiveProperties, function(prop, callback) {
+ var prop = expensiveProperties.pop();
+ if(meta.hasOne[p]) {
+ meta.hasOne[p].type.fromSelectJSON(session, tx, jsonObj[p], function(result) {
+ persistence.set(obj, p, result);
+ callback();
+ });
+ } else if(meta.hasMany[p]) {
+ var coll = persistence.get(obj, p);
+ var ar = jsonObj[p].slice(0);
+ var PropertyEntity = meta.hasMany[p].type;
+ persistence.asyncForEach(ar, function(item, callback) {
+ PropertyEntity.fromSelectJSON(session, tx, item, function(result) {
+ coll.add(result);
+ callback();
+ });
+ }, function() {
+ callback();
+ });
+ }
+ }, function() {
+ callback(obj);
+ });
+ }
+ if(jsonObj.id) {
+ Entity.load(session, tx, jsonObj.id, loadedObj);
+ } else {
+ loadedObj(new Entity());
+ }
+ };
Entity.load = function(session, tx, id, callback) {
var args = argspec.getArgs(arguments, [
@@ -911,6 +1005,7 @@ persistence.get = function(arg1, arg2) {
this.load(tx, JSON.parse(jsonDump), callback);
};
+
/**
* Generates a UUID according to http://www.ietf.org/rfc/rfc4122.txt
*/
@@ -927,6 +1022,8 @@ persistence.get = function(arg1, arg2) {
return uuid;
}
+ persistence.createUUID = createUUID;
+
function defaultValue(type) {
switch(type) {
@@ -1322,7 +1419,7 @@ persistence.get = function(arg1, arg2) {
return this._session.uniqueQueryCollection(c);
};
- /*
+ /**
* Returns a new query collection which will prefetch a certain object relationship.
* Only works with 1:1 and N:1 relations.
* @param rel the relation name of the relation to prefetch
@@ -1334,6 +1431,48 @@ persistence.get = function(arg1, arg2) {
return this._session.uniqueQueryCollection(c);
};
+
+ /**
+ * Select a subset of data, represented by this query collection as a JSON
+ * structure (Javascript object)
+ *
+ * @param tx database transaction to use, leave out to start a new one
+ * @param props a property specification
+ * @param callback(result)
+ */
+ QueryCollection.prototype.selectJSON = function(tx, props, callback) {
+ var args = argspec.getArgs(arguments, [
+ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null },
+ { name: "props", optional: false },
+ { name: "callback", optional: false }
+ ]);
+ var session = this._session;
+ var that = this;
+ tx = args.tx;
+ props = args.props;
+ callback = args.callback;
+
+ if(!tx) {
+ session.transaction(function(tx) {
+ that.selectJSON(tx, props, callback);
+ });
+ return;
+ }
+ var Entity = getEntity(this._entityName);
+ // TODO: This could do some clever prefetching to make it more efficient
+ this.list(function(items) {
+ var resultArray = [];
+ persistence.asyncForEach(items, function(item, callback) {
+ item.selectJSON(tx, props, function(obj) {
+ resultArray.push(obj);
+ callback();
+ });
+ }, function() {
+ callback(resultArray);
+ });
+ });
+ };
+
/**
* Adds an object to a collection
* @param obj the object to add
@@ -1666,7 +1805,10 @@ persistence.argspec = argspec;
// JSON.stringify(value, replacer, space)
// JSON.parse(text, reviver)
-var JSON = (window && window.JSON) ? window.JSON : {};
+if(typeof JSON === 'undefined') {
+ JSON = {};
+}
+//var JSON = typeof JSON === 'undefined' ? window.JSON : {};
if (!JSON.stringify) {
(function () {
function f(n) {
View
64 lib/persistence.store.sql.js
@@ -137,35 +137,13 @@ persistence.store.sql.config = function(persistence, dialect) {
}
session.objectsToRemove = {};
if(callback) {
- function removeOneObject() {
- var obj = removeObjArray.pop();
- remove(obj, tx, function () {
- if (removeObjArray.length > 0) {
- removeOneObject();
- } else if (callback) {
- callback();
- }
- });
- }
- function persistOneObject () {
- var obj = persistObjArray.pop();
- save(obj, tx, function () {
- if (persistObjArray.length > 0) {
- persistOneObject();
- } else if(removeObjArray.length > 0) {
- removeOneObject();
- } else if (callback) {
- callback();
- }
- });
- }
- if (persistObjArray.length > 0) {
- persistOneObject();
- } else if(removeObjArray.length > 0) {
- removeOneObject();
- } else if(callback) {
- callback();
- }
+ persistence.asyncForEach(removeObjArray, function(obj, callback) {
+ remove(obj, tx, callback);
+ }, function() {
+ persistence.asyncForEach(persistObjArray, function(obj, callback) {
+ save(obj, tx, callback);
+ }, callback);
+ });
} else { // More efficient
for(var i = 0; i < persistObjArray.length; i++) {
save(persistObjArray[i], tx);
@@ -381,26 +359,14 @@ persistence.store.sql.config = function(persistence, dialect) {
for ( var i = 3; i < arguments.length; i++) {
callbackArgs.push(arguments[i]);
}
- function executeOne () {
- var queryTuple = queries.pop();
-
- var oneFn = function () {
- if (queries.length > 0) {
- executeOne();
- } else if (callback) {
- callback.apply(null, callbackArgs);
- }
- };
- tx.executeSql(queryTuple[0], queryTuple[1], oneFn, function(_, err) {
- console.log(err.message);
- oneFn();
- });
- }
- if (queries.length > 0) {
- executeOne();
- } else if (callback) {
- callback.apply(this, callbackArgs);
- }
+ persistence.asyncForEach(queries, function(queryTuple, callback) {
+ tx.executeSql(queryTuple[0], queryTuple[1], callback, function(_, err) {
+ console.log(err.message);
+ callback();
+ });
+ }, function() {
+ callback.apply(null, callbackArgs);
+ });
}
persistence.executeQueriesSeq = executeQueriesSeq;
View
44 test/test.persistence.js
@@ -2,6 +2,9 @@ $(document).ready(function(){
persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024);
//persistence.store.memory.config(persistence);
persistence.debug = true;
+ //persistence.debug = false;
+
+ var startTime = new Date().getTime();
var Project = persistence.define('Project', {
name: "TEXT"
@@ -499,13 +502,12 @@ $(document).ready(function(){
module("Dumping/restoring");
- asyncTest("Dumping", function() {
+ asyncTest("Full dump/restore", function() {
for(var i = 0; i < 10; i++) {
persistence.add(new Task({name: "Task " + i, dateAdded: new Date()}));
}
persistence.flush(function() {
persistence.dumpToJson([Task], function(dumps) {
- console.log(dumps);
Task.all().destroyAll(function() {
persistence.loadFromJson(dumps, function() {
Task.all().count(function(n) {
@@ -517,4 +519,42 @@ $(document).ready(function(){
});
});
});
+
+ asyncTest("Select dump/restore", function() {
+ persistence.reset(function() {
+ persistence.schemaSync(function() {
+ var project = new Project({name: "My project"});
+ persistence.add(project);
+ var tags = [];
+ for(var i = 0; i < 5; i++) {
+ var tag = new Tag({name: "Tag " + i});
+ persistence.add(tag);
+ tags.push(tag);
+ }
+ for(var i = 0; i < 1000; i++) {
+ var task = new Task({name: "Task " + i});
+ task.done = true;
+ task.tags = new persistence.LocalQueryCollection(tags);
+ project.tasks.add(task);
+ }
+ Project.all().selectJSON(['id', 'name', 'tasks.[id,name]', 'tasks.tags.[id, name]'], function(result) {
+ persistence.reset(function() {
+ persistence.schemaSync(function() {
+ Project.fromSelectJSON(result[0], function(obj) {
+ persistence.add(obj);
+ Task.all().list(function(tasks) {
+ equals(tasks.length, 10, "number of restored tasks ok");
+ tasks.forEach(function(task) {
+ equals(task.done, false, "done still default value");
+ });
+ start();
+ console.log(new Date().getTime() - startTime);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
});
Please sign in to comment.
Something went wrong with that request. Please try again.