/** * indexed db adapter * === * - originally authored by Vivian Li * */ Lawnchair.adapter('indexed-db', (function(){ function fail(e, i) { console.error('error in indexed-db adapter!', e, i); } // update the STORE_VERSION when the schema used by this adapter changes // (for example, if you change the STORE_NAME above) var STORE_VERSION = 3; var getIDB = function() { return window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.oIndexedDB || window.msIndexedDB; }; var getIDBTransaction = function() { return window.IDBTransaction || window.webkitIDBTransaction || window.mozIDBTransaction || window.oIDBTransaction || window.msIDBTransaction; }; var getIDBKeyRange = function() { return window.IDBKeyRange || window.webkitIDBKeyRange || window.mozIDBKeyRange || window.oIDBKeyRange || window.msIDBKeyRange; }; var getIDBDatabaseException = function() { return window.IDBDatabaseException || window.webkitIDBDatabaseException || window.mozIDBDatabaseException || window.oIDBDatabaseException || window.msIDBDatabaseException; }; var useAutoIncrement = function() { // using preliminary mozilla implementation which doesn't support // auto-generated keys. Neither do some webkit implementations. return !!window.indexedDB; }; // see https://groups.google.com/a/chromium.org/forum/?fromgroups#!topic/chromium-html5/OhsoAQLj7kc var READ_WRITE = (getIDBTransaction() && 'READ_WRITE' in getIDBTransaction()) ? getIDBTransaction().READ_WRITE : 'readwrite'; return { valid: function() { return !!getIDB(); }, init:function(options, callback) { this.idb = getIDB(); this.waiting = []; this.useAutoIncrement = useAutoIncrement(); var request = this.idb.open(this.name, STORE_VERSION); var self = this; var cb = self.fn(self.name, callback); if (cb && typeof cb != 'function') throw 'callback not valid'; var win = function() { // manually clean up event handlers on request; this helps on chrome request.onupgradeneeded = request.onsuccess = request.error = null; if(cb) return cb.call(self, self); }; var upgrade = function(from, to) { // don't try to migrate dbs, just recreate try { self.db.deleteObjectStore('teststore'); // old adapter } catch (e1) { /* ignore */ } try { self.db.deleteObjectStore(self.record); } catch (e2) { /* ignore */ } // ok, create object store. var params = {}; if (self.useAutoIncrement) { params.autoIncrement = true; } self.db.createObjectStore(self.record, params); self.store = true; }; request.onupgradeneeded = function(event) { self.db = request.result; self.transaction = request.transaction; upgrade(event.oldVersion, event.newVersion); // will end up in onsuccess callback }; request.onsuccess = function(event) { self.db = event.target.result; if(self.db.version != (''+STORE_VERSION)) { // DEPRECATED API: modern implementations will fire the // upgradeneeded event instead. var oldVersion = self.db.version; var setVrequest = self.db.setVersion(''+STORE_VERSION); // onsuccess is the only place we can create Object Stores setVrequest.onsuccess = function(event) { var transaction = setVrequest.result; setVrequest.onsuccess = setVrequest.onerror = null; // can't upgrade w/o versionchange transaction. upgrade(oldVersion, STORE_VERSION); transaction.oncomplete = function() { for (var i = 0; i < self.waiting.length; i++) { self.waiting[i].call(self); } self.waiting = []; win(); }; }; setVrequest.onerror = function(e) { setVrequest.onsuccess = setVrequest.onerror = null; console.error("Failed to create objectstore " + e); fail(e); }; } else { self.store = true; for (var i = 0; i < self.waiting.length; i++) { self.waiting[i].call(self); } self.waiting = []; win(); } } request.onerror = function(ev) { if (request.errorCode === getIDBDatabaseException().VERSION_ERR) { // xxx blow it away self.idb.deleteDatabase(self.name); // try it again. return self.init(options, callback); } console.error('Failed to open database'); }; }, save:function(obj, callback) { var self = this; if(!this.store) { this.waiting.push(function() { this.save(obj, callback); }); return; } var objs = (this.isArray(obj) ? obj : [obj]).map(function(o){if(!o.key) { o.key = self.uuid()} return o}) var win = function (e) { if (callback) { self.lambda(callback).call(self, self.isArray(obj) ? objs : objs[0] ) } }; var trans = this.db.transaction(this.record, READ_WRITE); var store = trans.objectStore(this.record); for (var i = 0; i < objs.length; i++) { var o = objs[i]; store.put(o, o.key); } store.transaction.oncomplete = win; store.transaction.onabort = fail; return this; }, batch: function (objs, callback) { return this.save(objs, callback); }, get:function(key, callback) { if(!this.store) { this.waiting.push(function() { this.get(key, callback); }); return; } var self = this; var win = function (e) { var r = e.target.result; if (callback) { if (r) { r.key = key; } self.lambda(callback).call(self, r); } }; if (!this.isArray(key)){ var req = this.db.transaction(this.record).objectStore(this.record).get(key); req.onsuccess = function(event) { req.onsuccess = req.onerror = null; win(event); }; req.onerror = function(event) { req.onsuccess = req.onerror = null; fail(event); }; } else { // note: these are hosted. var results = [] , done = key.length , keys = key var getOne = function(i) { self.get(keys[i], function(obj) { results[i] = obj; if ((--done) > 0) { return; } if (callback) { self.lambda(callback).call(self, results); } }); }; for (var i = 0, l = keys.length; i < l; i++) getOne(i); } return this; }, exists:function(key, callback) { if(!this.store) { this.waiting.push(function() { this.exists(key, callback); }); return; } var self = this; var req = this.db.transaction(self.record).objectStore(this.record).openCursor(getIDBKeyRange().only(key)); req.onsuccess = function(event) { req.onsuccess = req.onerror = null; // exists iff req.result is not null // XXX but firefox returns undefined instead, sigh XXX var undef; self.lambda(callback).call(self, event.target.result !== null && event.target.result !== undef); }; req.onerror = function(event) { req.onsuccess = req.onerror = null; fail(event); }; return this; }, all:function(callback) { if(!this.store) { this.waiting.push(function() { this.all(callback); }); return; } var cb = this.fn(this.name, callback) || undefined; var self = this; var objectStore = this.db.transaction(this.record).objectStore(this.record); var toReturn = []; objectStore.openCursor().onsuccess = function(event) { var cursor = event.target.result; if (cursor) { toReturn.push(cursor.value); cursor['continue'](); } else { if (cb) cb.call(self, toReturn); } }; return this; }, keys:function(callback) { if(!this.store) { this.waiting.push(function() { this.keys(callback); }); return; } var cb = this.fn(this.name, callback) || undefined; var self = this; var objectStore = this.db.transaction(this.record).objectStore(this.record); var toReturn = []; // in theory we could use openKeyCursor() here, but no one actually // supports it yet. objectStore.openCursor().onsuccess = function(event) { var cursor = event.target.result; if (cursor) { toReturn.push(cursor.key); cursor['continue'](); } else { if (cb) cb.call(self, toReturn); } }; return this; }, remove:function(keyOrArray, callback) { if(!this.store) { this.waiting.push(function() { this.remove(keyOrArray, callback); }); return; } var self = this; var toDelete = keyOrArray; if (!this.isArray(keyOrArray)) { toDelete=[keyOrArray]; } var win = function () { if (callback) self.lambda(callback).call(self) }; var os = this.db.transaction(this.record, READ_WRITE).objectStore(this.record); var key = keyOrArray.key ? keyOrArray.key : keyOrArray; for (var i = 0; i < toDelete.length; i++) { var key = toDelete[i].key ? toDelete[i].key : toDelete[i]; os.delete(key); }; os.transaction.oncomplete = win; os.transaction.onabort = fail; return this; }, nuke:function(callback) { if(!this.store) { this.waiting.push(function() { this.nuke(callback); }); return; } var self = this , win = callback ? function() { self.lambda(callback).call(self) } : function(){}; try { var os = this.db.transaction(this.record, READ_WRITE).objectStore(this.record); os.clear(); os.transaction.oncomplete = win; os.transaction.onabort = fail; } catch (e) { if (e.name=='NotFoundError') win() else fail(e); } return this; } }; })());