/** * Lawnchair! * --- * clientside json store * */ var Lawnchair = function (options, callback) { // ensure Lawnchair was called as a constructor if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback); // lawnchair requires json if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.' // options are optional; callback is not if (arguments.length <= 2 && arguments.length > 0) { callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1]; options = (typeof arguments[0] === 'function') ? {} : arguments[0]; } else { throw 'Incorrect # of ctor args!' } // TODO perhaps allow for pub/sub instead? if (typeof callback !== 'function') throw 'No callback was provided'; // default configuration this.record = options.record || 'record' // default for records this.name = options.name || 'records' // default name for underlying store // mixin first valid adapter var adapter // if the adapter is passed in we try to load that only if (options.adapter) { for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { if (Lawnchair.adapters[i].adapter === options.adapter) { adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined; break; } } // otherwise find the first valid adapter for this env } else { for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined if (adapter) break } } // we have failed if (!adapter) throw 'No valid adapter.' // yay! mixin the adapter for (var j in adapter) this[j] = adapter[j] // call init for each mixed in plugin for (var i = 0, l = Lawnchair.plugins.length; i < l; i++) Lawnchair.plugins[i].call(this) // init the adapter this.init(options, callback) } Lawnchair.adapters = [] /** * queues an adapter for mixin * === * - ensures an adapter conforms to a specific interface * */ Lawnchair.adapter = function (id, obj) { // add the adapter id to the adapter obj // ugly here for a cleaner dsl for implementing adapters obj['adapter'] = id // methods required to implement a lawnchair adapter var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ') , indexOf = this.prototype.indexOf // mix in the adapter for (var i in obj) { if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i } // if we made it this far the adapter interface is valid // insert the new adapter as the preferred adapter Lawnchair.adapters.splice(0,0,obj) } Lawnchair.plugins = [] /** * generic shallow extension for plugins * === * - if an init method is found it registers it to be called when the lawnchair is inited * - yes we could use hasOwnProp but nobody here is an asshole */ Lawnchair.plugin = function (obj) { for (var i in obj) i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i] } /** * helpers * */ Lawnchair.prototype = { isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' }, /** * this code exists for ie8... for more background see: * http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream */ indexOf: function(ary, item, i, l) { if (ary.indexOf) return ary.indexOf(item) for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i return -1 }, // awesome shorthand callbacks as strings. this is shameless theft from dojo. lambda: function (callback) { return this.fn(this.record, callback) }, // first stab at named parameters for terse callbacks; dojo: first != best // ;D fn: function (name, callback) { return typeof callback == 'string' ? new Function(name, callback) : callback }, // returns a unique identifier (by way of Backbone.localStorage.js) // TODO investigate smaller UUIDs to cut on storage cost uuid: function () { var S4 = function () { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); } return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); }, // a classic iterator each: function (callback) { var cb = this.lambda(callback) // iterate from chain if (this.__results) { for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i) } // otherwise iterate the entire collection else { this.all(function(r) { for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i) }) } return this } // -- }; // window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser Lawnchair.adapter('window-name', (function(index, store) { if (typeof window==='undefined') { window = { top: { } }; // node/optimizer compatibility } var data = window.top.name ? JSON.parse(window.top.name) : {} return { valid: function () { return typeof window.top.name != 'undefined' }, init: function (options, callback) { data[this.name] = data[this.name] || {index:[],store:{}} index = data[this.name].index store = data[this.name].store this.fn(this.name, callback).call(this, this) }, keys: function (callback) { this.fn('keys', callback).call(this, index) return this }, save: function (obj, cb) { // data[key] = value + ''; // force to string // window.top.name = JSON.stringify(data); var key = obj.key || this.uuid() this.exists(key, function(exists) { if (!exists) { if (obj.key) delete obj.key index.push(key) } store[key] = obj window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter if (cb) { obj.key = key this.lambda(cb).call(this, obj) } }) return this }, batch: function (objs, cb) { var r = [] for (var i = 0, l = objs.length; i < l; i++) { this.save(objs[i], function(record) { r.push(record) }) } if (cb) this.lambda(cb).call(this, r) return this }, get: function (keyOrArray, cb) { var r; if (this.isArray(keyOrArray)) { r = [] for (var i = 0, l = keyOrArray.length; i < l; i++) { r.push(store[keyOrArray[i]]) } } else { r = store[keyOrArray] if (r) r.key = keyOrArray } if (cb) this.lambda(cb).call(this, r) return this }, exists: function (key, cb) { this.lambda(cb).call(this, !!(store[key])) return this }, all: function (cb) { var r = [] for (var i = 0, l = index.length; i < l; i++) { var obj = store[index[i]] obj.key = index[i] r.push(obj) } this.fn(this.name, cb).call(this, r) return this }, remove: function (keyOrArray, cb) { var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] for (var i = 0, l = del.length; i < l; i++) { delete store[del[i]] index.splice(this.indexOf(index, del[i]), 1) } window.top.name = JSON.stringify(data) if (cb) this.lambda(cb).call(this) return this }, nuke: function (cb) { store = {} index = [] window.top.name = JSON.stringify(data) if (cb) this.lambda(cb).call(this) return this } } ///// })()) /** * dom storage adapter * === * - originally authored by Joseph Pecoraro * */ // // TODO does it make sense to be chainable all over the place? // chainable: nuke, remove, all, get, save, all // not chainable: valid, keys // Lawnchair.adapter('dom', (function() { var storage = window.localStorage // the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys var indexer = function(name) { return { // the key key: name + '._index_', // returns the index all: function() { var a = storage.getItem(this.key) if (a) { a = JSON.parse(a) } if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init return JSON.parse(storage.getItem(this.key)) }, // adds a key to the index add: function (key) { var a = this.all() a.push(key) storage.setItem(this.key, JSON.stringify(a)) }, // deletes a key from the index del: function (key) { var a = this.all(), r = [] // FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating for (var i = 0, l = a.length; i < l; i++) { if (a[i] != key) r.push(a[i]) } storage.setItem(this.key, JSON.stringify(r)) }, // returns index for a key find: function (key) { var a = this.all() for (var i = 0, l = a.length; i < l; i++) { if (key === a[i]) return i } return false } } } // adapter api return { // ensure we are in an env with localStorage valid: function () { return !!storage && function() { // in mobile safari if safe browsing is enabled, window.storage // is defined but setItem calls throw exceptions. var success = true var value = Math.random() try { storage.setItem(value, value) } catch (e) { success = false } storage.removeItem(value) return success }() }, init: function (options, callback) { this.indexer = indexer(this.name) if (callback) this.fn(this.name, callback).call(this, this) }, save: function (obj, callback) { var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid() // if the key is not in the index push it on if (this.indexer.find(key) === false) this.indexer.add(key) // now we kil the key and use it in the store colleciton delete obj.key; storage.setItem(key, JSON.stringify(obj)) obj.key = key.slice(this.name.length + 1) if (callback) { this.lambda(callback).call(this, obj) } return this }, batch: function (ary, callback) { var saved = [] // not particularily efficient but this is more for sqlite situations for (var i = 0, l = ary.length; i < l; i++) { this.save(ary[i], function(r){ saved.push(r) }) } if (callback) this.lambda(callback).call(this, saved) return this }, // accepts [options], callback keys: function(callback) { if (callback) { var name = this.name , keys = this.indexer.all().map(function(r){ return r.replace(name + '.', '') }) this.fn('keys', callback).call(this, keys) } return this // TODO options for limit/offset, return promise }, get: function (key, callback) { if (this.isArray(key)) { var r = [] for (var i = 0, l = key.length; i < l; i++) { var k = this.name + '.' + key[i] var obj = storage.getItem(k) if (obj) { obj = JSON.parse(obj) obj.key = key[i] r.push(obj) } } if (callback) this.lambda(callback).call(this, r) } else { var k = this.name + '.' + key var obj = storage.getItem(k) if (obj) { obj = JSON.parse(obj) obj.key = key } if (callback) this.lambda(callback).call(this, obj) } return this }, exists: function (key, cb) { var exists = this.indexer.find(this.name+'.'+key) === false ? false : true ; this.lambda(cb).call(this, exists); return this; }, // NOTE adapters cannot set this.__results but plugins do // this probably should be reviewed all: function (callback) { var idx = this.indexer.all() , r = [] , o , k for (var i = 0, l = idx.length; i < l; i++) { k = idx[i] //v o = JSON.parse(storage.getItem(k)) o.key = k.replace(this.name + '.', '') r.push(o) } if (callback) this.fn(this.name, callback).call(this, r) return this }, remove: function (keyOrObj, callback) { var key = this.name + '.' + ((keyOrObj.key) ? keyOrObj.key : keyOrObj) this.indexer.del(key) storage.removeItem(key) if (callback) this.lambda(callback).call(this) return this }, nuke: function (callback) { this.all(function(r) { for (var i = 0, l = r.length; i < l; i++) { this.remove(r[i]); } if (callback) this.lambda(callback).call(this) }) return this } }})());