foam-framework
Version:
MVC metaprogramming framework
374 lines (329 loc) • 10.8 kB
JavaScript
/**
* @license
* Copyright 2014 Google Inc. All Rights Reserved.
*
* 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.
*/
CLASS({
package: 'foam.dao',
name: 'IDBDAO',
label: 'IndexedDB DAO',
extends: 'AbstractDAO',
documentation: function() {/*
Usage:<br/>
<code>var dao = IDBDAO.create({model: Issue});<br/>
var dao = IDBDAO.create({model: Issue, name: 'ImportantIssues'});<br/></code>
<br/>
TODO:<br/>
Optimization. This DAO doesn't use any indexes in indexeddb yet, which
means for any query other than a single find/remove we iterate the entire
data store. Obviously this will get slow if you store large amounts
of data in the database.
*/},
properties: [
{
name: 'model',
type: 'Model',
required: true
},
{
name: 'name',
label: 'Store Name',
type: 'String',
defaultValueFn: function() {
return this.model.id;
}
},
{
model_: 'BooleanProperty',
name: 'useSimpleSerialization',
defaultValue: true
},
{
model_: 'StringArrayProperty',
name: 'indicies'
}
],
methods: {
init: function() {
this.SUPER();
var propKeys = this.propKeys_ = {};
var properties = this.model.getRuntimeProperties()
for ( var i = 0 ; i < properties.length ; i++ ) {
var prop = properties[i];
propKeys[prop.name] = prop.name;
if ( prop.shortName ) propKeys[prop.shortName] = prop.name;
}
if ( this.useSimpleSerialization ) {
this.serialize = this.SimpleSerialize;
this.deserialize = this.SimpleDeserialize;
} else {
this.serialize = this.FOAMSerialize;
this.deserialize = this.FOAMDeserialize;
}
this.withDB = amemo(this.openDB.bind(this));
},
FOAMDeserialize: function(json) {
return JSONToObject.visitObject(json);
},
FOAMSerialize: function(obj) {
return ObjectToJSON.visitObject(obj);
},
SimpleDeserialize: function(json) {
var obj = this.model.create();
for ( var key in json ) {
var key2 = this.propKeys_[key];
if ( key2 ) obj[key2] = json[key];
}
return obj;
},
SimpleSerialize: function(obj) {
var s = {};
for ( var key in obj.instance_ ) {
var prop = obj.model_.getProperty(key);
var val = obj.instance_[key];
if ( ! prop.transient && val !== prop.defaultValue ) s[prop.shortName || prop.name] = val;
}
return s;
},
openDB: function(cc) {
var indexedDB = window.indexedDB ||
window.webkitIndexedDB ||
window.mozIndexedDB;
var request = indexedDB.open("FOAM:" + this.name, 1);
request.onupgradeneeded = (function(e) {
var store = e.target.result.createObjectStore(this.name);
for ( var i = 0; i < this.indicies.length; i++ ) {
store.createIndex(this.indicies[i][0], this.indicies[i][0], { unique: this.indicies[i][1] });
}
}).bind(this);
request.onsuccess = (function(e) {
cc(e.target.result);
}).bind(this);
request.onerror = function (e) {
console.log('************** failure', e);
};
},
withStore: function(mode, fn) {
if ( mode !== 'readwrite' ) return this.withStore_(mode, fn);
var self = this;
if ( ! this.q_ ) {
var q = [fn];
this.q_ = q;
EventService.async(function() {
self.withStore_(mode, function(store) {
// console.log('q length: ', q.length);
if ( self.q_ == q ) self.q_ = undefined;
for ( var i = 0 ; i < q.length ; i++ ) q[i](store);
});
}, this.X)();
} else {
this.q_.push(fn);
// Diminishing returns after 10000 per batch
if ( this.q_.length == 10000 ) this.q_ = undefined;
}
},
withStore_: function(mode, fn) {
if ( GLOBAL.__TXN__ && GLOBAL.__TXN__.store ) {
try {
fn.call(this, __TXN__.store);
return;
} catch (x) {
GLOBAL.__TXN__ = undefined;
}
}
this.withDB((function (db) {
var tx = db.transaction([this.name], mode);
var os = tx.objectStore(this.name);
if ( GLOBAL.__TXN__ ) GLOBAL.__TXN__.store = os;
fn.call(this, os);
}).bind(this));
},
put: function(value, sink) {
var self = this;
this.withStore("readwrite", function(store) {
var request = store.put(self.serialize(value),
value[self.model.ids[0]]);
request.transaction.addEventListener(
'complete',
function(e) {
self.notify_('put', [value]);
sink && sink.put && sink.put(value);
});
request.transaction.addEventListener(
'error',
function(e) {
// TODO: Parse a better error mesage out of e
sink && sink.error && sink.error('put', value);
});
});
},
find: function(key, sink) {
if ( Expr.isInstance(key) ) {
var found = false;
this.limit(1).where(key).select({
put: function() {
found = true;
sink.put.apply(sink, arguments);
},
eof: function() {
found || sink.error('find', key);
}
});
return;
}
var self = this;
this.withStore("readonly", function(store) {
var request = store.get(key);
request.transaction.addEventListener(
'complete',
function() {
if (!request.result) {
sink && sink.error && sink.error('find', key);
return;
}
var result = self.deserialize(request.result);
sink && sink.put && sink.put(result);
});
request.onerror = function(e) {
// TODO: Parse a better error out of e
sink && sink.error && sink.error('find', key);
};
});
},
remove: function(obj, sink) {
var self = this;
var key = obj[this.model.ids[0]] != undefined ? obj[this.model.ids[0]] : obj;
this.withStore("readwrite", function(store) {
var getRequest = store.get(key);
getRequest.onsuccess = function(e) {
if (!getRequest.result) {
sink && sink.error && sink.error('remove', obj);
return;
}
var data = self.deserialize(getRequest.result);
var delRequest = store.delete(key);
delRequest.transaction.addEventListener('complete', function(e) {
self.notify_('remove', [data]);
sink && sink.remove && sink.remove(data);
});
delRequest.onerror = function(e) {
sink && sink.error && sink.error('remove', e);
};
};
getRequest.onerror = function(e) {
sink && sink.error && sink.error('remove', e);
};
return;
});
},
removeAll: function(sink, options) {
var query = (options && options.query && options.query.partialEval()) ||
{ f: function() { return true; } };
var future = afuture();
var self = this;
// If the caller doesn't care to see the objects as they get removed,
// then just nuke them in one go.
if ( ! options && ! ( sink && sink.remove ) ) {
this.withStore('readwrite', function(store) {
var req = store.clear();
req.onsuccess = function() {
future.set();
};
req.onerror = function() {
future.set();
};
});
return future.get;
}
this.withStore('readwrite', function(store) {
var request = store.openCursor();
request.onsuccess = function(e) {
var cursor = e.target.result;
if (cursor) {
var value = self.deserialize(cursor.value);
if (query.f(value)) {
var deleteReq = cursor.delete();
deleteReq.transaction.addEventListener(
'complete',
function() {
self.notify_('remove', [value]);
sink && sink.remove && sink.remove(value);
});
deleteReq.onerror = function(e) {
sink && sink.error && sink.error('remove', e);
};
}
cursor.continue();
}
};
request.transaction.oncomplete = function() {
sink && sink.eof && sink.eof();
future.set(sink);
};
request.onerror = function(e) {
sink && sink.error && sink.error('remove', e);
};
});
return future.get;
},
select: function(sink, options) {
sink = sink || [].sink;
sink = this.decorateSink_(sink, options, false);
var fc = this.createFlowControl_();
var future = afuture();
var self = this;
this.withStore("readonly", function(store) {
if ( options && options.query && EqExpr.isInstance(options.query) && store.indexNames.contains(options.query.arg1.name) ) {
var request = store.index(options.query.arg1.name).openCursor(IDBKeyRange.only(options.query.arg2.f()));
} else {
var request = store.openCursor();
}
request.onsuccess = function(e) {
var cursor = e.target.result;
if ( fc.stopped ) return;
if ( fc.errorEvt ) {
sink.error && sink.error(fc.errorEvt);
future.set(sink, fc.errorEvt);
return;
}
if (!cursor) {
sink.eof && sink.eof();
future.set(sink);
return;
}
var value = self.deserialize(cursor.value);
sink.put(value);
cursor.continue();
};
request.onerror = function(e) {
sink.error && sink.error(e);
};
});
return future.get;
},
addIndex: function(prop) {
this.indicies.push([prop.name, false]);
return this;
}
},
listeners: [
{
name: 'updated',
code: function(evt) {
console.log('updated: ', evt);
this.publish('updated');
}
}
]
});