├── .project ├── js └── libs │ ├── adapters │ ├── ie-userdata.js │ ├── blackberry-persistent-storage.js │ ├── memory.js │ ├── window-name.js │ ├── dom.js │ ├── gears-sqlite.js │ ├── webkit-sqlite.js │ └── indexed-db.js │ └── Lawnchair.js ├── test.html ├── demo.html ├── README.markdown └── lccache.js /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | lscache 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /js/libs/adapters/ie-userdata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ie userdata adaptor 3 | * 4 | */ 5 | Lawnchair.adapter('ie-userdata', { 6 | valid: function () { 7 | return typeof(document.body.addBehavior) != 'undefined'; 8 | }, 9 | init:function(){ 10 | var s = document.createElement('span'); 11 | s.style.behavior = 'url(\'#default#userData\')'; 12 | s.style.position = 'absolute'; 13 | s.style.left = 10000; 14 | document.body.appendChild(s); 15 | this.storage = s; 16 | this.storage.load('lawnchair'); 17 | }, 18 | 19 | get:function(key, callback){ 20 | 21 | var obj = JSON.parse(this.storage.getAttribute(key) || 'null'); 22 | if (obj) { 23 | obj.key = key; 24 | 25 | } 26 | if (callback) 27 | callback(obj); 28 | }, 29 | 30 | save:function(obj, callback){ 31 | var id = obj.key || 'lc' + this.uuid(); 32 | delete obj.key; 33 | this.storage.setAttribute(id, JSON.stringify(obj)); 34 | this.storage.save('lawnchair'); 35 | if (callback){ 36 | obj.key = id; 37 | callback(obj); 38 | } 39 | }, 40 | 41 | all:function(callback){ 42 | var cb = this.terseToVerboseCallback(callback); 43 | var ca = this.storage.XMLDocument.firstChild.attributes; 44 | var yar = []; 45 | var v,o; 46 | // yo ho yo ho a pirates life for me 47 | for (var i = 0, l = ca.length; i < l; i++) { 48 | v = ca[i]; 49 | o = JSON.parse(v.nodeValue || 'null'); 50 | if (o) { 51 | o.key = v.nodeName; 52 | yar.push(o); 53 | } 54 | } 55 | if (cb) 56 | cb(yar); 57 | }, 58 | remove:function(keyOrObj,callback) { 59 | var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj.key; 60 | this.storage.removeAttribute(key); 61 | this.storage.save('lawnchair'); 62 | if(callback) 63 | callback(); 64 | }, 65 | nuke:function(callback) { 66 | var that = this; 67 | this.all(function(r){ 68 | for (var i = 0, l = r.length; i < l; i++) { 69 | if (r[i].key) 70 | that.remove(r[i].key); 71 | } 72 | if(callback) 73 | callback(); 74 | }); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /js/libs/adapters/blackberry-persistent-storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * blackberry persistent storage adaptor 3 | * === 4 | * - Implementation that uses the BlackBerry Persistent Storage mechanism. This is only available in PhoneGap BlackBerry projects 5 | * - See http://www.github.com/phonegap/phonegap-blackberry 6 | * 7 | */ 8 | Lawnchair.extend({ 9 | init:function() { 10 | // Check for the existence of the phonegap blackberry persistent store API 11 | if (!navigator.store) 12 | throw('Lawnchair, "This browser does not support BlackBerry Persistent Storage; it is a PhoneGap-only implementation."'); 13 | }, 14 | get:function(key, callback) { 15 | var that = this; 16 | navigator.store.get(function(value) { // success cb 17 | if (callback) { 18 | // Check if BBPS returned a serialized JSON obj, if so eval it. 19 | if (that.isObjectAsString(value)) { 20 | eval('value = ' + value.substr(0,value.length-1) + ',key:\'' + key + '\'};'); 21 | } 22 | that.terseToVerboseCallback(callback)(value); 23 | } 24 | }, function() {}, // empty error cb 25 | key); 26 | }, 27 | save:function(obj, callback) { 28 | var id = obj.key || this.uuid(); 29 | delete obj.key; 30 | var that = this; 31 | navigator.store.put(function(){ 32 | if (callback) { 33 | var cbObj = obj; 34 | cbObj['key'] = id; 35 | that.terseToVerboseCallback(callback)(cbObj); 36 | } 37 | }, function(){}, id, this.serialize(obj)); 38 | }, 39 | all:function(callback) { 40 | var that = this; 41 | navigator.store.getAll(function(json) { // success cb 42 | if (callback) { 43 | // BlackBerry store returns straight strings, so eval as necessary for values we deem as objects. 44 | var arr = []; 45 | for (var prop in json) { 46 | if (that.isObjectAsString(json[prop])) { 47 | eval('arr.push(' + json[prop].substr(0,json[prop].length-1) + ',key:\'' + prop + '\'});'); 48 | } else { 49 | eval('arr.push({\'' + prop + '\':\'' + json[prop] + '\'});'); 50 | } 51 | } 52 | that.terseToVerboseCallback(callback)(arr); 53 | } 54 | }, function() {}); // empty error cb 55 | }, 56 | remove:function(keyOrObj, callback) { 57 | var key = (typeof keyOrObj == 'string') ? keyOrObj : keyOrObj.key; 58 | var that = this; 59 | navigator.store.remove(function() { 60 | if (callback) 61 | that.terseToVerboseCallback(callback)(); 62 | }, function() {}, key); 63 | }, 64 | nuke:function(callback) { 65 | var that = this; 66 | navigator.store.nuke(function(){ 67 | if (callback) 68 | that.terseToVerboseCallback(callback)(); 69 | },function(){}); 70 | }, 71 | // Private helper. 72 | isObjectAsString:function(value) { 73 | return (value != null && value[0] == '{' && value[value.length-1] == '}'); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 66 | 67 | 68 |

QUnit example

69 |

70 |
71 |

72 |
    73 |
    test markup, will be hidden
    74 | 75 | 76 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | lccache 6 | 16 | 17 | 18 |

    lccache

    19 | 20 |

    The lccache library is a simple library that emulates memcache 21 | functions using Lawnchair, 22 | so that you can store items with an expiration date. These demos show its 23 | current functionality - set, get, remove.

    24 | 25 |

    Set memcache entries (most with expiration of 2 minutes):
    26 | (Check "Resources->Local Storage" in Chrome Dev Tools or type 27 | "localStorage" into the Firebug Console to see what is stored.)

    28 | 29 |
    33 |
    34 | 35 |

    Retrieve lccache entries:

    36 |
    40 |
    41 | 42 |

    Delete lccache entries and try to retrieve them:

    43 |
    48 | 49 | 50 |

    51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /js/libs/adapters/memory.js: -------------------------------------------------------------------------------- 1 | Lawnchair.adapter('memory', (function(){ 2 | 3 | var storage = {}, index = [] 4 | 5 | return { 6 | valid: function() { return true }, 7 | 8 | init: function(opts, cb) { 9 | this.fn(this.name, cb).call(this, this) 10 | return this 11 | }, 12 | 13 | keys: function() { return index }, 14 | 15 | save: function(obj, cb) { 16 | var key = obj.key || this.uuid() 17 | 18 | if (obj.key) delete obj.key 19 | 20 | this.exists(key, function(exists) { 21 | if (!exists) index.push(key) 22 | 23 | storage[key] = obj 24 | 25 | if (cb) { 26 | obj.key = key 27 | this.lambda(cb).call(this, obj) 28 | } 29 | }) 30 | 31 | return this 32 | }, 33 | 34 | batch: function (objs, cb) { 35 | var r = [] 36 | for (var i = 0, l = objs.length; i < l; i++) { 37 | this.save(objs[i], function(record) { 38 | r.push(record) 39 | }) 40 | } 41 | if (cb) this.lambda(cb).call(this, r) 42 | return this 43 | }, 44 | 45 | get: function (keyOrArray, cb) { 46 | var r; 47 | if (this.isArray(keyOrArray)) { 48 | r = [] 49 | for (var i = 0, l = keyOrArray.length; i < l; i++) { 50 | r.push(storage[keyOrArray[i]]) 51 | } 52 | } else { 53 | r = storage[keyOrArray] 54 | if (r) r.key = keyOrArray 55 | } 56 | if (cb) this.lambda(cb).call(this, r) 57 | return this 58 | }, 59 | 60 | exists: function (key, cb) { 61 | this.lambda(cb).call(this, !!(storage[key])) 62 | return this 63 | }, 64 | 65 | all: function (cb) { 66 | var r = [] 67 | for (var i = 0, l = index.length; i < l; i++) { 68 | var obj = storage[index[i]] 69 | obj.key = index[i] 70 | r.push(obj) 71 | } 72 | this.fn(this.name, cb).call(this, r) 73 | return this 74 | }, 75 | 76 | remove: function (keyOrArray, cb) { 77 | var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] 78 | for (var i = 0, l = del.length; i < l; i++) { 79 | delete storage[del[i]] 80 | index.splice(index.indexOf(del[i]), 1) 81 | } 82 | if (cb) this.lambda(cb).call(this) 83 | return this 84 | }, 85 | 86 | nuke: function (cb) { 87 | storage = {} 88 | index = [] 89 | if (cb) this.lambda(cb).call(this) 90 | return this 91 | } 92 | } 93 | ///// 94 | })()) 95 | -------------------------------------------------------------------------------- /js/libs/adapters/window-name.js: -------------------------------------------------------------------------------- 1 | // window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser 2 | Lawnchair.adapter('window-name', (function(index, store) { 3 | 4 | var data = window.top.name ? JSON.parse(window.top.name) : {} 5 | 6 | return { 7 | 8 | valid: function () { 9 | return typeof window.top.name != 'undefined' 10 | }, 11 | 12 | init: function (options, callback) { 13 | data[this.name] = {index:[],store:{}} 14 | index = data[this.name].index 15 | store = data[this.name].store 16 | this.fn(this.name, callback).call(this, this) 17 | }, 18 | 19 | keys: function (callback) { 20 | this.fn('keys', callback).call(this, index) 21 | return this 22 | }, 23 | 24 | save: function (obj, cb) { 25 | // data[key] = value + ''; // force to string 26 | // window.top.name = JSON.stringify(data); 27 | var key = obj.key || this.uuid() 28 | if (obj.key) delete obj.key 29 | this.exists(key, function(exists) { 30 | if (!exists) index.push(key) 31 | store[key] = obj 32 | window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter 33 | if (cb) { 34 | obj.key = key 35 | this.lambda(cb).call(this, obj) 36 | } 37 | }) 38 | return this 39 | }, 40 | 41 | batch: function (objs, cb) { 42 | var r = [] 43 | for (var i = 0, l = objs.length; i < l; i++) { 44 | this.save(objs[i], function(record) { 45 | r.push(record) 46 | }) 47 | } 48 | if (cb) this.lambda(cb).call(this, r) 49 | return this 50 | }, 51 | 52 | get: function (keyOrArray, cb) { 53 | var r; 54 | if (this.isArray(keyOrArray)) { 55 | r = [] 56 | for (var i = 0, l = keyOrArray.length; i < l; i++) { 57 | r.push(store[keyOrArray[i]]) 58 | } 59 | } else { 60 | r = store[keyOrArray] 61 | if (r) r.key = keyOrArray 62 | } 63 | if (cb) this.lambda(cb).call(this, r) 64 | return this 65 | }, 66 | 67 | exists: function (key, cb) { 68 | this.lambda(cb).call(this, !!(store[key])) 69 | return this 70 | }, 71 | 72 | all: function (cb) { 73 | var r = [] 74 | for (var i = 0, l = index.length; i < l; i++) { 75 | var obj = store[index[i]] 76 | obj.key = index[i] 77 | r.push(obj) 78 | } 79 | this.fn(this.name, cb).call(this, r) 80 | return this 81 | }, 82 | 83 | remove: function (keyOrArray, cb) { 84 | var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] 85 | for (var i = 0, l = del.length; i < l; i++) { 86 | delete store[del[i]] 87 | index.splice(this.indexOf(index, del[i]), 1) 88 | } 89 | window.top.name = JSON.stringify(data) 90 | if (cb) this.lambda(cb).call(this) 91 | return this 92 | }, 93 | 94 | nuke: function (cb) { 95 | storage = {} 96 | index = [] 97 | window.top.name = JSON.stringify(data) 98 | if (cb) this.lambda(cb).call(this) 99 | return this 100 | } 101 | } 102 | ///// 103 | })()) 104 | -------------------------------------------------------------------------------- /js/libs/Lawnchair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lawnchair! 3 | * --- 4 | * clientside json store 5 | * 6 | */ 7 | var Lawnchair = function () { 8 | // lawnchair requires json 9 | if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.' 10 | // options are optional; callback is not 11 | if (arguments.length <= 2 && arguments.length > 0) { 12 | var callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1] 13 | , options = (typeof arguments[0] === 'function') ? {} : arguments[0] 14 | } else { 15 | throw 'Incorrect # of ctor args!' 16 | } 17 | // TODO perhaps allow for pub/sub instead? 18 | if (typeof callback !== 'function') throw 'No callback was provided'; 19 | 20 | // ensure we init with this set to the Lawnchair prototype 21 | var self = (!(this instanceof Lawnchair)) 22 | ? new Lawnchair(options, callback) 23 | : this 24 | 25 | // default configuration 26 | self.record = options.record || 'record' // default for records 27 | self.name = options.name || 'records' // default name for underlying store 28 | 29 | // mixin first valid adapter 30 | var adapter 31 | // if the adapter is passed in we try to load that only 32 | if (options.adapter) { 33 | adapter = Lawnchair.adapters[self.indexOf(Lawnchair.adapters, options.adapter)] 34 | adapter = adapter.valid() ? adapter : undefined 35 | // otherwise find the first valid adapter for this env 36 | } 37 | else { 38 | for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { 39 | adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined 40 | if (adapter) break 41 | } 42 | } 43 | 44 | // we have failed 45 | if (!adapter) throw 'No valid adapter.' 46 | 47 | // yay! mixin the adapter 48 | for (var j in adapter) 49 | self[j] = adapter[j] 50 | 51 | // call init for each mixed in plugin 52 | for (var i = 0, l = Lawnchair.plugins.length; i < l; i++) 53 | Lawnchair.plugins[i].call(self) 54 | 55 | // init the adapter 56 | self.init(options, callback) 57 | 58 | // called as a function or as a ctor with new always return an instance 59 | return self 60 | } 61 | 62 | Lawnchair.adapters = [] 63 | 64 | /** 65 | * queues an adapter for mixin 66 | * === 67 | * - ensures an adapter conforms to a specific interface 68 | * 69 | */ 70 | Lawnchair.adapter = function (id, obj) { 71 | // add the adapter id to the adapter obj 72 | // ugly here for a cleaner dsl for implementing adapters 73 | obj['adapter'] = id 74 | // methods required to implement a lawnchair adapter 75 | var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ') 76 | , indexOf = this.prototype.indexOf 77 | // mix in the adapter 78 | for (var i in obj) { 79 | if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i 80 | } 81 | // if we made it this far the adapter interface is valid 82 | Lawnchair.adapters.push(obj) 83 | } 84 | 85 | Lawnchair.plugins = [] 86 | 87 | /** 88 | * generic shallow extension for plugins 89 | * === 90 | * - if an init method is found it registers it to be called when the lawnchair is inited 91 | * - yes we could use hasOwnProp but nobody here is an asshole 92 | */ 93 | Lawnchair.plugin = function (obj) { 94 | for (var i in obj) 95 | i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i] 96 | } 97 | 98 | /** 99 | * helpers 100 | * 101 | */ 102 | Lawnchair.prototype = { 103 | 104 | isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' }, 105 | 106 | /** 107 | * this code exists for ie8... for more background see: 108 | * http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream 109 | */ 110 | indexOf: function(ary, item, i, l) { 111 | if (ary.indexOf) return ary.indexOf(item) 112 | for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i 113 | return -1 114 | }, 115 | 116 | // awesome shorthand callbacks as strings. this is shameless theft from dojo. 117 | lambda: function (callback) { 118 | return this.fn(this.record, callback) 119 | }, 120 | 121 | // first stab at named parameters for terse callbacks; dojo: first != best // ;D 122 | fn: function (name, callback) { 123 | return typeof callback == 'string' ? new Function(name, callback) : callback 124 | }, 125 | 126 | // returns a unique identifier (by way of Backbone.localStorage.js) 127 | // TODO investigate smaller UUIDs to cut on storage cost 128 | uuid: function () { 129 | var S4 = function () { 130 | return (((1+Math.random())*0x10000)|0).toString(16).substring(1); 131 | } 132 | return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); 133 | }, 134 | 135 | // a classic iterator 136 | each: function (callback) { 137 | var cb = this.lambda(callback) 138 | // iterate from chain 139 | if (this.__results) { 140 | for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i) 141 | } 142 | // otherwise iterate the entire collection 143 | else { 144 | this.all(function(r) { 145 | for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i) 146 | }) 147 | } 148 | return this 149 | } 150 | // -- 151 | }; 152 | -------------------------------------------------------------------------------- /js/libs/adapters/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dom storage adapter 3 | * === 4 | * - originally authored by Joseph Pecoraro 5 | * 6 | */ 7 | // 8 | // TODO does it make sense to be chainable all over the place? 9 | // chainable: nuke, remove, all, get, save, all 10 | // not chainable: valid, keys 11 | // 12 | Lawnchair.adapter('dom', (function() { 13 | var storage = window.localStorage 14 | // the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys 15 | var indexer = function(name) { 16 | return { 17 | // the key 18 | key: name + '._index_', 19 | // returns the index 20 | all: function() { 21 | var a = JSON.parse(storage.getItem(this.key)) 22 | if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init 23 | return JSON.parse(storage.getItem(this.key)) 24 | }, 25 | // adds a key to the index 26 | add: function (key) { 27 | var a = this.all() 28 | a.push(key) 29 | storage.setItem(this.key, JSON.stringify(a)) 30 | }, 31 | // deletes a key from the index 32 | del: function (key) { 33 | var a = this.all(), r = [] 34 | // FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating 35 | for (var i = 0, l = a.length; i < l; i++) { 36 | if (a[i] != key) r.push(a[i]) 37 | } 38 | storage.setItem(this.key, JSON.stringify(r)) 39 | }, 40 | // returns index for a key 41 | find: function (key) { 42 | var a = this.all() 43 | for (var i = 0, l = a.length; i < l; i++) { 44 | if (key === a[i]) return i 45 | } 46 | return false 47 | } 48 | } 49 | } 50 | 51 | // adapter api 52 | return { 53 | 54 | // ensure we are in an env with localStorage 55 | valid: function () { 56 | return !!storage 57 | }, 58 | 59 | init: function (options, callback) { 60 | this.indexer = indexer(this.name) 61 | if (callback) this.fn(this.name, callback).call(this, this) 62 | }, 63 | 64 | save: function (obj, callback) { 65 | var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid() 66 | // if the key is not in the index push it on 67 | if (this.indexer.find(key) === false) this.indexer.add(key) 68 | // now we kil the key and use it in the store colleciton 69 | delete obj.key; 70 | storage.setItem(key, JSON.stringify(obj)) 71 | if (callback) { 72 | obj.key = key.replace(this.name + '.', '') 73 | this.lambda(callback).call(this, obj) 74 | } 75 | return this 76 | }, 77 | 78 | batch: function (ary, callback) { 79 | var saved = [] 80 | // not particularily efficient but this is more for sqlite situations 81 | for (var i = 0, l = ary.length; i < l; i++) { 82 | this.save(ary[i], function(r){ 83 | saved.push(r) 84 | }) 85 | } 86 | if (callback) this.lambda(callback).call(this, saved) 87 | return this 88 | }, 89 | 90 | // accepts [options], callback 91 | keys: function(callback) { 92 | if (callback) { 93 | var name = this.name 94 | , keys = this.indexer.all().map(function(r){ return r.replace(name + '.', '') }) 95 | this.fn('keys', callback).call(this, keys) 96 | } 97 | return this // TODO options for limit/offset, return promise 98 | }, 99 | 100 | get: function (key, callback) { 101 | if (this.isArray(key)) { 102 | var r = [] 103 | for (var i = 0, l = key.length; i < l; i++) { 104 | var k = this.name + '.' + key[i] 105 | , obj = JSON.parse(storage.getItem(k)) 106 | if (obj) { 107 | obj.key = k 108 | r.push(obj) 109 | } 110 | } 111 | if (callback) this.lambda(callback).call(this, r) 112 | } else { 113 | var k = this.name + '.' + key 114 | , obj = JSON.parse(storage.getItem(k)) 115 | if (obj) obj.key = k 116 | if (callback) this.lambda(callback).call(this, obj) 117 | } 118 | return this 119 | }, 120 | // NOTE adapters cannot set this.__results but plugins do 121 | // this probably should be reviewed 122 | all: function (callback) { 123 | var idx = this.indexer.all() 124 | , r = [] 125 | , o 126 | , k 127 | for (var i = 0, l = idx.length; i < l; i++) { 128 | k = idx[i] //v 129 | o = JSON.parse(storage.getItem(k)) 130 | o.key = k.replace(this.name + '.', '') 131 | r.push(o) 132 | } 133 | if (callback) this.fn(this.name, callback).call(this, r) 134 | return this 135 | }, 136 | 137 | remove: function (keyOrObj, callback) { 138 | var key = this.name + '.' + (typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key) 139 | this.indexer.del(key) 140 | storage.removeItem(key) 141 | if (callback) this.lambda(callback).call(this) 142 | return this 143 | }, 144 | 145 | nuke: function (callback) { 146 | this.all(function(r) { 147 | for (var i = 0, l = r.length; i < l; i++) { 148 | this.remove(r[i]); 149 | } 150 | if (callback) this.lambda(callback).call(this) 151 | }) 152 | return this 153 | } 154 | }})()); 155 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | lccache 2 | =============================== 3 | This is a port of Pamela Fox's nifty little library 4 | [lscache](https://github.com/pamelafox/lscache) to use 5 | [Lawnchair](https://github.com/brianleroux/lawnchair) for 6 | storage rather than HTML5 localStorage. 7 | 8 | In either case, if what you need is a simple library that emulates `memcache` 9 | functions so that you can cache data on the client and associate an expiration 10 | time with each piece of data, then you want one of these libraries. The question 11 | you need to answer is, "Do I only need to support newer browsers that have HTML5 12 | localStorage, or will I need to support older browsers as well?" If the former, 13 | lscache is your ticket, if the latter, please check out this version. 14 | 15 | Methods 16 | ------- 17 | 18 | The library exposes 3 methods: `set()`, `get()`, and `remove()`. 19 | 20 | * * * 21 | 22 | ### lccache.set 23 | Stores the value in Lawnchair. Expires after specified number of minutes. 24 | #### Arguments 25 | 1. `key` (**string**) 26 | 2. `value` (**Object|string**) 27 | 3. `time` (**number: optional**) 28 | 29 | * * * 30 | 31 | ### lccache.get 32 | Retrieves specified value from Lawnchair, if not expired. 33 | #### Arguments 34 | 1. `key` (**string**) 35 | #### Returns 36 | **string | Object** : The stored value. 37 | 38 | * * * 39 | 40 | ### lccache.remove 41 | Removes a value from Lawnchair. 42 | #### Arguments 43 | 1. `key` (**string**) 44 | 45 | 46 | Usage 47 | ------- 48 | 49 | The interface should be familiar to those of you who have used `memcache`, and 50 | should be easy to understand for those of you who haven't. 51 | 52 | For example, you can store a string for 2 minutes using `lccache.set()`: 53 | 54 | ```js 55 | lccache.set('greeting', 'Hello World!', 2); 56 | ``` 57 | 58 | You can then retrieve that string with `lccache.get()`: 59 | 60 | ```js 61 | alert(lccache.get('greeting')); 62 | ``` 63 | 64 | You can remove that string from the cache entirely with `lccache.remove()`: 65 | 66 | ```js 67 | lccache.remove('greeting'); 68 | ``` 69 | 70 | The library handles JSON objects so you aren't restricted to only storing strings: 71 | 72 | ```js 73 | lccache.set('data', {'name': 'Pamela', 'age': 26}, 2); 74 | ``` 75 | 76 | And then when you retrieve it, you will get it back as an object: 77 | 78 | ```js 79 | alert(lccache.get('data').name); 80 | ``` 81 | 82 | For more live examples, play around with the demo here: 83 | http://johnmunsch.github.com/lccache/demo.html 84 | 85 | or check out the QUnit tests here: 86 | http://johnmunsch.github.com/lccache/test.html 87 | 88 | Installation 89 | ---------- 90 | 91 | Add lccache.js to your project and make sure you include it in any pages that 92 | need it (see demo.html or test.html for examples). Even though I've included a 93 | copy of Lawnchair in the repository so the demo and test would work, I don't 94 | really recommend using it. It's Lawnchair 0.6.3 and a better/newer version may 95 | be available by the time you read this. If not, or if newer versions prove 96 | incompatible, know that this code worked with that version and get the whole 97 | package from his project. 98 | 99 | *NOTE: localStorage on Firefox (and maybe on other browsers as well) is disabled 100 | when you run your web pages by just opening them in the browser. So if you think 101 | you'll just double click on a file and it'll work, it won't. I've read it's for 102 | security reasons, though I don't know from what it protects you. So just serve your 103 | web pages up from some kind of server (Tomcat, Apache, or whatever) even when you're 104 | testing them locally.* 105 | 106 | Real-World Usage 107 | ---------- 108 | *These are the original examples given by Pamela, note that I've not changed any 109 | of the references from lscache to lccache because she used lscache for all of 110 | her work. However, the examples of why you want to use something like this are 111 | just as relevant to both libraries.* 112 | 113 | This library was originally developed with the use case of caching results of 114 | JSON API queries to speed up my webapps and give them better protection against 115 | flaky APIs. (More on that in this [blog post](http://blog.pamelafox.org/2010/10/lscache-localstorage-based-memcache.html)) 116 | 117 | For example, [RageTube](http://ragetube.net) uses `lscache` to fetch Youtube API 118 | results for 10 minutes: 119 | 120 | ```js 121 | var key = 'youtube:' + query; 122 | var json = lscache.get(key); 123 | if (json) { 124 | processJSON(json); 125 | } else { 126 | fetchJSON(query); 127 | } 128 | 129 | function processJSON(json) { 130 | // .. 131 | } 132 | 133 | function fetchJSON() { 134 | var searchUrl = 'http://gdata.youtube.com/feeds/api/videos'; 135 | var params = { 136 | 'v': '2', 'alt': 'jsonc', 'q': encodeURIComponent(query) 137 | } 138 | JSONP.get(searchUrl, params, null, function(json) { 139 | processJSON(json); 140 | lscache.set(key, json, 60*10); 141 | }); 142 | } 143 | ``` 144 | 145 | It does not have to be used for only expiration-based caching, however. It can 146 | also be used as just a wrapper for `localStorage`, as it provides the benefit 147 | of handling JS object (de-)serialization. 148 | 149 | For example, the [QuizCards](http://quizcards.info) Chrome extensions use 150 | `lscache` to store the user statistics for each user bucket, and those stats are 151 | an array of objects. 152 | 153 | ```js 154 | function initBuckets() { 155 | var bucket1 = []; 156 | for (var i = 0; i < CARDS_DATA.length; i++) { 157 | var datum = CARDS_DATA[i]; 158 | bucket1.push({'id': datum.id, 'lastAsked': 0}); 159 | } 160 | lscache.set(LS_BUCKET + 1, bucket1); 161 | lscache.set(LS_BUCKET + 2, []); 162 | lscache.set(LS_BUCKET + 3, []); 163 | lscache.set(LS_BUCKET + 4, []); 164 | lscache.set(LS_BUCKET + 5, []); 165 | lscache.set(LS_INIT, 'true') 166 | } 167 | ``` 168 | 169 | Browser Support 170 | ---------------- 171 | 172 | The `lccache` library should work in all browsers for which a Lawnchair adapter 173 | exists. At the time of writing there were adapters for storing data in 174 | localStorage, the Blackberry persistent store, window name, Google Gears SQLite, 175 | IE userdata, Indexed DB, and memory. 176 | 177 | The current list of those is here: 178 | http://brian.io/lawnchair/adapters/ 179 | 180 | -------------------------------------------------------------------------------- /lccache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lccache library 3 | * Copyright (c) 2011, Pamela Fox, John Munsch 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Creates a namespace for the lccache functions. 20 | */ 21 | var lccache = function() { 22 | // Prefixes the key name on the expiration items in localStorage 23 | var CACHESUFFIX = '-cacheexpiration'; 24 | 25 | var supportsStorage = function () { 26 | // This is a temporary kludge. The old code performed its own tests but it 27 | // was looking specifically for localStorage. What we need to know is that 28 | // Lawnchair found some adapter that worked within the current environment. 29 | return true; 30 | }(); 31 | 32 | // The old code checked to see if JSON was supported. Since we're using 33 | // Lawnchair, we can just pass through and assume that it will take JSON and 34 | // will give JSON back. 35 | // 36 | // Note: If there's no native support for JSON in the browser, Lawnchair will 37 | // thrown an error instructing you to add json2.js to polyfill the 38 | // functionality. 39 | 40 | /** 41 | * Returns the full string for the localStorage expiration item. 42 | * @param {String} key 43 | * @return {string} 44 | */ 45 | function expirationKey(key) { 46 | return key + CACHESUFFIX; 47 | } 48 | 49 | /** 50 | * Returns the number of minutes since the epoch. 51 | * @return {number} 52 | */ 53 | function currentTime() { 54 | return Math.floor((new Date().getTime())/60000); 55 | } 56 | 57 | return { 58 | 59 | /** 60 | * Stores the value in Lawnchair. Expires after specified number of minutes. 61 | * @param {string} key 62 | * @param {Object|string} value 63 | * @param {number} time 64 | */ 65 | set: function(key, value, time) { 66 | if (!supportsStorage) return; 67 | 68 | // The lscache code turned anything non JSON into a string at this point. 69 | // We kind of need to go the other way. Anything non-JSON needs to become 70 | // JSON because that's what Lawnchair expects. 71 | if (typeof value != "object") { 72 | // We didn't get a JSON value, let's do something to create one. 73 | value = { "lccacheNonJSONValue": value }; 74 | } 75 | 76 | try { 77 | Lawnchair(function () { 78 | var storedValue = {"key": key, "value": value}; 79 | 80 | // If a time is specified, store expiration info as part of the JSON. 81 | if (time) { 82 | storedValue.expiration = currentTime() + time; 83 | } 84 | 85 | this.save(storedValue); 86 | }); 87 | } catch (e) { 88 | if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED') { 89 | // If we exceeded the quota, then we will sort 90 | // by the expire time, and then remove the N oldest 91 | // var storedKey, storedKeys = []; 92 | // for (var i = 0; i < localStorage.length; i++) { 93 | // storedKey = localStorage.key(i); 94 | // if (storedKey.indexOf(CACHESUFFIX) > -1) { 95 | // var mainKey = storedKey.split(CACHESUFFIX)[0]; 96 | // storedKeys.push({key: mainKey, expiration: parseInt(localStorage[storedKey], 10)}); 97 | // } 98 | // } 99 | // storedKeys.sort(function(a, b) { return (a.expiration-b.expiration); }); 100 | // 101 | // for (var i = 0, len = Math.min(30, storedKeys.length); i < len; i++) { 102 | // localStorage.removeItem(storedKeys[i].key); 103 | // localStorage.removeItem(expirationKey(storedKeys[i].key)); 104 | // } 105 | // // TODO: This could still error if the items we removed were small and this is large 106 | // localStorage.setItem(key, value); 107 | } else { 108 | // If it was some other error, just give up. 109 | return; 110 | } 111 | } 112 | }, 113 | 114 | /** 115 | * Retrieves specified value from localStorage, if not expired. 116 | * @param {string} key 117 | * @return {string|Object} 118 | */ 119 | get: function(key) { 120 | if (!supportsStorage) return null; 121 | 122 | var retVal = null; 123 | 124 | Lawnchair(function () { 125 | this.get(key, function (obj) { 126 | if (obj != null) { 127 | // Return the found item if not expired. 128 | if (obj.expiration != null) { 129 | // Check if we should actually kick item out of Lawnchair. 130 | if (currentTime() >= obj.expiration) { 131 | Lawnchair(function () { 132 | this.remove(key); 133 | }); 134 | } else { 135 | if (obj.value.lccacheNonJSONValue != null) { 136 | retVal = obj.value.lccacheNonJSONValue; 137 | } else { 138 | retVal = obj.value; 139 | } 140 | } 141 | } else { 142 | // No expiration was specified. Just return what we found. 143 | if (obj.value.lccacheNonJSONValue != null) { 144 | retVal = obj.value.lccacheNonJSONValue; 145 | } else { 146 | retVal = obj.value; 147 | } 148 | } 149 | } 150 | }); 151 | }); 152 | 153 | return retVal; 154 | }, 155 | 156 | /** 157 | * Removes a value from Lawnchair. 158 | * Equivalent to 'delete' in memcache, but that's a keyword in JS. 159 | * @param {string} key 160 | */ 161 | remove: function(key) { 162 | if (!supportsStorage) return null; 163 | 164 | Lawnchair(function () { 165 | this.remove(key); 166 | }); 167 | } 168 | }; 169 | }(); 170 | -------------------------------------------------------------------------------- /js/libs/adapters/gears-sqlite.js: -------------------------------------------------------------------------------- 1 | // init.js directly included to save on include traffic 2 | // 3 | // Copyright 2007, Google Inc. 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright notice, 9 | // this list of conditions and the following disclaimer. 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 3. Neither the name of Google Inc. nor the names of its contributors may be 14 | // used to endorse or promote products derived from this software without 15 | // specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 18 | // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | // EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 25 | // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | // 28 | // Sets up google.gears.*, which is *the only* supported way to access Gears. 29 | // 30 | // Circumvent this file at your own risk! 31 | // 32 | // In the future, Gears may automatically define google.gears.* without this 33 | // file. Gears may use these objects to transparently fix bugs and compatibility 34 | // issues. Applications that use the code below will continue to work seamlessly 35 | // when that happens. 36 | 37 | (function() { 38 | // We are already defined. Hooray! 39 | if (window.google && google.gears) { 40 | return; 41 | } 42 | 43 | var factory = null; 44 | 45 | // Firefox 46 | if (typeof GearsFactory != 'undefined') { 47 | factory = new GearsFactory(); 48 | } else { 49 | // IE 50 | try { 51 | factory = new ActiveXObject('Gears.Factory'); 52 | // privateSetGlobalObject is only required and supported on IE Mobile on 53 | // WinCE. 54 | if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { 55 | factory.privateSetGlobalObject(this); 56 | } 57 | } catch (e) { 58 | // Safari 59 | if ((typeof navigator.mimeTypes != 'undefined') 60 | && navigator.mimeTypes["application/x-googlegears"]) { 61 | factory = document.createElement("object"); 62 | factory.style.display = "none"; 63 | factory.width = 0; 64 | factory.height = 0; 65 | factory.type = "application/x-googlegears"; 66 | document.documentElement.appendChild(factory); 67 | } 68 | } 69 | } 70 | 71 | // *Do not* define any objects if Gears is not installed. This mimics the 72 | // behavior of Gears defining the objects in the future. 73 | if (!factory) { 74 | return; 75 | } 76 | 77 | // Now set up the objects, being careful not to overwrite anything. 78 | // 79 | // Note: In Internet Explorer for Windows Mobile, you can't add properties to 80 | // the window object. However, global objects are automatically added as 81 | // properties of the window object in all browsers. 82 | if (!window.google) { 83 | google = {}; 84 | } 85 | 86 | if (!google.gears) { 87 | google.gears = {factory: factory}; 88 | } 89 | })(); 90 | 91 | /** 92 | * gears sqlite adaptor 93 | * 94 | */ 95 | Lawnchair.extend({ 96 | init:function(options) { 97 | var that = this; 98 | var merge = that.merge; 99 | var opts = (typeof arguments[0] == 'string') ? {table:options} : options; 100 | this.name = merge('Lawnchair', opts.name); 101 | this.table = merge('field', opts.table); 102 | this.db = google.gears.factory.create('beta.database'); 103 | this.db.open(this.name); 104 | this.db.execute('create table if not exists ' + this.table + ' (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)'); 105 | }, 106 | save:function(obj, callback) { 107 | var that = this; 108 | 109 | var insert = function(obj, callback) { 110 | var id = (obj.key == undefined) ? that.uuid() : obj.key; 111 | delete(obj.key); 112 | 113 | var rs = that.db.execute( 114 | "INSERT INTO " + that.table + " (id, value, timestamp) VALUES (?,?,?)", 115 | [id, that.serialize(obj), that.now()] 116 | ); 117 | if (callback != undefined) { 118 | obj.key = id; 119 | callback(obj); 120 | } 121 | }; 122 | 123 | var update = function(id, obj, callback) { 124 | that.db.execute( 125 | "UPDATE " + that.table + " SET value=?, timestamp=? WHERE id=?", 126 | [that.serialize(obj), that.now(), id] 127 | ); 128 | if (callback != undefined) { 129 | obj.key = id; 130 | callback(obj); 131 | } 132 | }; 133 | 134 | if (obj.key == undefined) { 135 | insert(obj, callback); 136 | } else { 137 | this.get(obj.key, function(r) { 138 | var isUpdate = (r != null); 139 | 140 | if (isUpdate) { 141 | var id = obj.key; 142 | delete(obj.key); 143 | update(id, obj, callback); 144 | } else { 145 | insert(obj, callback); 146 | } 147 | }); 148 | } 149 | 150 | }, 151 | get:function(key, callback) { 152 | var rs = this.db.execute("SELECT * FROM " + this.table + " WHERE id = ?", [key]); 153 | 154 | if (rs.isValidRow()) { 155 | // FIXME need to test null return / empty recordset 156 | var o = this.deserialize(rs.field(1)); 157 | o.key = key; 158 | rs.close(); 159 | callback(o); 160 | } else { 161 | rs.close(); 162 | callback(null); 163 | } 164 | }, 165 | all:function(callback) { 166 | var cb = this.terseToVerboseCallback(callback); 167 | var rs = this.db.execute("SELECT * FROM " + this.table); 168 | var r = []; 169 | var o; 170 | 171 | // FIXME need to add 0 len support 172 | //if (results.rows.length == 0 ) { 173 | // cb([]); 174 | 175 | while (rs.isValidRow()) { 176 | o = this.deserialize(rs.field(1)); 177 | o.key = rs.field(0); 178 | r.push(o); 179 | rs.next(); 180 | } 181 | rs.close(); 182 | cb(r); 183 | }, 184 | remove:function(keyOrObj, callback) { 185 | this.db.execute( 186 | "DELETE FROM " + this.table + " WHERE id = ?", 187 | [(typeof keyOrObj == 'string') ? keyOrObj : keyOrObj.key] 188 | ); 189 | if(callback) 190 | callback(); 191 | }, 192 | nuke:function(callback) { 193 | this.db.execute("DELETE FROM " + this.table); 194 | if(callback) 195 | callback(); 196 | return this; 197 | } 198 | }); 199 | -------------------------------------------------------------------------------- /js/libs/adapters/webkit-sqlite.js: -------------------------------------------------------------------------------- 1 | Lawnchair.adapter('webkit-sqlite', (function () { 2 | // private methods 3 | var fail = function (e, i) { console.log('error in sqlite adaptor!', e, i) } 4 | , now = function () { return new Date() } // FIXME need to use better date fn 5 | // not entirely sure if this is needed... 6 | if (!Function.prototype.bind) { 7 | Function.prototype.bind = function( obj ) { 8 | var slice = [].slice 9 | , args = slice.call(arguments, 1) 10 | , self = this 11 | , nop = function () {} 12 | , bound = function () { 13 | return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))) 14 | } 15 | nop.prototype = self.prototype 16 | bound.prototype = new nop() 17 | return bound 18 | } 19 | } 20 | 21 | // public methods 22 | return { 23 | 24 | valid: function() { return !!(window.openDatabase) }, 25 | 26 | init: function (options, callback) { 27 | var that = this 28 | , cb = that.fn(that.name, callback) 29 | , create = "CREATE TABLE IF NOT EXISTS " + this.name + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)" 30 | , win = function(){ return cb.call(that, that); } 31 | // open a connection and create the db if it doesn't exist 32 | this.db = openDatabase(this.name, '1.0.0', this.name, 65536) 33 | this.db.transaction(function (t) { 34 | t.executeSql(create, [], win, fail) 35 | }) 36 | }, 37 | 38 | keys: function (callback) { 39 | var cb = this.lambda(callback) 40 | , that = this 41 | , keys = "SELECT id FROM " + this.name + " ORDER BY timestamp DESC" 42 | 43 | this.db.transaction(function(t) { 44 | var win = function (xxx, results) { 45 | if (results.rows.length == 0 ) { 46 | cb.call(that, []) 47 | } else { 48 | var r = []; 49 | for (var i = 0, l = results.rows.length; i < l; i++) { 50 | r.push(results.rows.item(i).id); 51 | } 52 | cb.call(that, r) 53 | } 54 | } 55 | t.executeSql(keys, [], win, fail) 56 | }) 57 | return this 58 | }, 59 | // you think thats air you're breathing now? 60 | save: function (obj, callback) { 61 | var that = this 62 | , id = obj.key || that.uuid() 63 | , ins = "INSERT INTO " + this.name + " (value, timestamp, id) VALUES (?,?,?)" 64 | , up = "UPDATE " + this.name + " SET value=?, timestamp=? WHERE id=?" 65 | , win = function () { if (callback) { obj.key = id; that.lambda(callback).call(that, obj) }} 66 | , val = [now(), id] 67 | // existential 68 | that.exists(obj.key, function(exists) { 69 | // transactions are like condoms 70 | that.db.transaction(function(t) { 71 | // TODO move timestamp to a plugin 72 | var insert = function (obj) { 73 | val.unshift(JSON.stringify(obj)) 74 | t.executeSql(ins, val, win, fail) 75 | } 76 | // TODO move timestamp to a plugin 77 | var update = function (obj) { 78 | delete(obj.key) 79 | val.unshift(JSON.stringify(obj)) 80 | t.executeSql(up, val, win, fail) 81 | } 82 | // pretty 83 | exists ? update(obj) : insert(obj) 84 | }) 85 | }); 86 | return this 87 | }, 88 | 89 | // FIXME this should be a batch insert / just getting the test to pass... 90 | batch: function (objs, cb) { 91 | 92 | var results = [] 93 | , done = false 94 | , that = this 95 | 96 | var updateProgress = function(obj) { 97 | results.push(obj) 98 | done = results.length === objs.length 99 | } 100 | 101 | var checkProgress = setInterval(function() { 102 | if (done) { 103 | if (cb) that.lambda(cb).call(that, results) 104 | clearInterval(checkProgress) 105 | } 106 | }, 200) 107 | 108 | for (var i = 0, l = objs.length; i < l; i++) 109 | this.save(objs[i], updateProgress) 110 | 111 | return this 112 | }, 113 | 114 | get: function (keyOrArray, cb) { 115 | var that = this 116 | , sql = '' 117 | // batch selects support 118 | if (this.isArray(keyOrArray)) { 119 | sql = 'SELECT id, value FROM ' + this.name + " WHERE id IN ('" + keyOrArray.join("','") + "')" 120 | } else { 121 | sql = 'SELECT id, value FROM ' + this.name + " WHERE id = '" + keyOrArray + "'" 122 | } 123 | // FIXME 124 | // will always loop the results but cleans it up if not a batch return at the end.. 125 | // in other words, this could be faster 126 | var win = function (xxx, results) { 127 | var o = null 128 | , r = [] 129 | if (results.rows.length) { 130 | for (var i = 0, l = results.rows.length; i < l; i++) { 131 | o = JSON.parse(results.rows.item(i).value) 132 | o.key = results.rows.item(i).id 133 | r.push(o) 134 | } 135 | } 136 | if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null 137 | if (cb) that.lambda(cb).call(that, r) 138 | } 139 | this.db.transaction(function(t){ t.executeSql(sql, [], win, fail) }) 140 | return this 141 | }, 142 | 143 | exists: function (key, cb) { 144 | var is = "SELECT * FROM " + this.name + " WHERE id = ?" 145 | , that = this 146 | , win = function(xxx, results) { if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) } 147 | this.db.transaction(function(t){ t.executeSql(is, [key], win, fail) }) 148 | return this 149 | }, 150 | 151 | all: function (callback) { 152 | var that = this 153 | , all = "SELECT * FROM " + this.name 154 | , r = [] 155 | , cb = this.fn(this.name, callback) || undefined 156 | , win = function (xxx, results) { 157 | if (results.rows.length != 0) { 158 | for (var i = 0, l = results.rows.length; i < l; i++) { 159 | var obj = JSON.parse(results.rows.item(i).value) 160 | obj.key = results.rows.item(i).id 161 | r.push(obj) 162 | } 163 | } 164 | if (cb) cb.call(that, r) 165 | } 166 | 167 | this.db.transaction(function (t) { 168 | t.executeSql(all, [], win, fail) 169 | }) 170 | return this 171 | }, 172 | 173 | remove: function (keyOrObj, cb) { 174 | var that = this 175 | , key = typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key 176 | , del = "DELETE FROM " + this.name + " WHERE id = ?" 177 | , win = function () { if (cb) that.lambda(cb).call(that) } 178 | 179 | this.db.transaction( function (t) { 180 | t.executeSql(del, [key], win, fail); 181 | }); 182 | 183 | return this; 184 | }, 185 | 186 | nuke: function (cb) { 187 | var nuke = "DELETE FROM " + this.name 188 | , that = this 189 | , win = cb ? function() { that.lambda(cb).call(that) } : function(){} 190 | this.db.transaction(function (t) { 191 | t.executeSql(nuke, [], win, fail) 192 | }) 193 | return this 194 | } 195 | ////// 196 | }})()) 197 | -------------------------------------------------------------------------------- /js/libs/adapters/indexed-db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * indexed db adapter 3 | * === 4 | * - originally authored by Vivian Li 5 | * 6 | */ 7 | 8 | Lawnchair.adapter('indexed-db', (function(){ 9 | 10 | function fail(e, i) { console.log('error in indexed-db adapter!', e, i); debugger; } ; 11 | 12 | function getIDB(){ 13 | return window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.oIndexedDB || window.msIndexedDB; 14 | }; 15 | 16 | 17 | 18 | return { 19 | 20 | valid: function() { return !!getIDB(); }, 21 | 22 | init:function(options, callback) { 23 | this.idb = getIDB(); 24 | this.waiting = []; 25 | var request = this.idb.open(this.name); 26 | var self = this; 27 | var cb = self.fn(self.name, callback); 28 | var win = function(){ return cb.call(self, self); } 29 | 30 | request.onsuccess = function(event) { 31 | self.db = request.result; 32 | 33 | if(self.db.version != "1.0") { 34 | var setVrequest = self.db.setVersion("1.0"); 35 | // onsuccess is the only place we can create Object Stores 36 | setVrequest.onsuccess = function(e) { 37 | self.store = self.db.createObjectStore("teststore", { autoIncrement: true} ); 38 | for (var i = 0; i < self.waiting.length; i++) { 39 | self.waiting[i].call(self); 40 | } 41 | self.waiting = []; 42 | win(); 43 | }; 44 | setVrequest.onerror = function(e) { 45 | console.log("Failed to create objectstore " + e); 46 | fail(e); 47 | } 48 | } else { 49 | self.store = {}; 50 | for (var i = 0; i < self.waiting.length; i++) { 51 | self.waiting[i].call(self); 52 | } 53 | self.waiting = []; 54 | win(); 55 | } 56 | } 57 | request.onerror = fail; 58 | }, 59 | 60 | save:function(obj, callback) { 61 | if(!this.store) { 62 | this.waiting.push(function() { 63 | this.save(obj, callback); 64 | }); 65 | return; 66 | } 67 | 68 | var self = this; 69 | var win = function (e) { if (callback) { obj.key = e.target.result; self.lambda(callback).call(self, obj) }}; 70 | 71 | var trans = this.db.transaction(["teststore"], webkitIDBTransaction.READ_WRITE, 0); 72 | var store = trans.objectStore("teststore"); 73 | var request = obj.key ? store.put(obj, obj.key) : store.put(obj); 74 | 75 | request.onsuccess = win; 76 | request.onerror = fail; 77 | 78 | return this; 79 | }, 80 | 81 | // FIXME this should be a batch insert / just getting the test to pass... 82 | batch: function (objs, cb) { 83 | 84 | var results = [] 85 | , done = false 86 | , self = this 87 | 88 | var updateProgress = function(obj) { 89 | results.push(obj) 90 | done = results.length === objs.length 91 | } 92 | 93 | var checkProgress = setInterval(function() { 94 | if (done) { 95 | if (cb) self.lambda(cb).call(self, results) 96 | clearInterval(checkProgress) 97 | } 98 | }, 200) 99 | 100 | for (var i = 0, l = objs.length; i < l; i++) 101 | this.save(objs[i], updateProgress) 102 | 103 | return this 104 | }, 105 | 106 | 107 | get:function(key, callback) { 108 | if(!this.store) { 109 | this.waiting.push(function() { 110 | this.get(key, callback); 111 | }); 112 | return; 113 | } 114 | 115 | 116 | var self = this; 117 | var win = function (e) { if (callback) { self.lambda(callback).call(self, e.target.result) }}; 118 | 119 | 120 | if (!this.isArray(key)){ 121 | var req = this.db.transaction("teststore").objectStore("teststore").get(key); 122 | 123 | req.onsuccess = win; 124 | req.onerror = function(event) { 125 | console.log("Failed to find " + key); 126 | fail(event); 127 | }; 128 | 129 | // FIXME: again the setInterval solution to async callbacks.. 130 | } else { 131 | 132 | // note: these are hosted. 133 | var results = [] 134 | , done = false 135 | , keys = key 136 | 137 | var updateProgress = function(obj) { 138 | results.push(obj) 139 | done = results.length === keys.length 140 | } 141 | 142 | var checkProgress = setInterval(function() { 143 | if (done) { 144 | if (callback) self.lambda(callback).call(self, results) 145 | clearInterval(checkProgress) 146 | } 147 | }, 200) 148 | 149 | for (var i = 0, l = keys.length; i < l; i++) 150 | this.get(keys[i], updateProgress) 151 | 152 | } 153 | 154 | return this; 155 | }, 156 | 157 | all:function(callback) { 158 | if(!this.store) { 159 | this.waiting.push(function() { 160 | this.all(callback); 161 | }); 162 | return; 163 | } 164 | var cb = this.fn(this.name, callback) || undefined; 165 | var self = this; 166 | var objectStore = this.db.transaction("teststore").objectStore("teststore"); 167 | var toReturn = []; 168 | objectStore.openCursor().onsuccess = function(event) { 169 | var cursor = event.target.result; 170 | if (cursor) { 171 | toReturn.push(cursor.value); 172 | cursor.continue(); 173 | } 174 | else { 175 | if (cb) cb.call(self, toReturn); 176 | } 177 | }; 178 | return this; 179 | }, 180 | 181 | remove:function(keyOrObj, callback) { 182 | if(!this.store) { 183 | this.waiting.push(function() { 184 | this.remove(keyOrObj, callback); 185 | }); 186 | return; 187 | } 188 | if (typeof keyOrObj == "object") { 189 | keyOrObj = keyOrObj.key; 190 | } 191 | var self = this; 192 | var win = function () { if (callback) self.lambda(callback).call(self) }; 193 | 194 | var request = this.db.transaction(["teststore"], webkitIDBTransaction.READ_WRITE).objectStore("teststore").delete(keyOrObj); 195 | request.onsuccess = win; 196 | request.onerror = fail; 197 | return this; 198 | }, 199 | 200 | nuke:function(callback) { 201 | if(!this.store) { 202 | this.waiting.push(function() { 203 | this.nuke(callback); 204 | }); 205 | return; 206 | } 207 | 208 | var self = this 209 | , win = callback ? function() { self.lambda(callback).call(self) } : function(){}; 210 | 211 | try { 212 | this.db 213 | .transaction(["teststore"], webkitIDBTransaction.READ_WRITE) 214 | .objectStore("teststore").clear().onsuccess = win; 215 | 216 | } catch(e) { 217 | fail(); 218 | } 219 | return this; 220 | } 221 | 222 | }; 223 | 224 | })()); --------------------------------------------------------------------------------