From 0d93c90df41600b9b31b7a7be357fad549e4d302 Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Mon, 13 Jan 2014 05:35:10 -0300 Subject: [PATCH 1/8] Initial async implementation. --- backbone.dualstorage.amd.js | 498 +++++++++++++++++---------- backbone.dualstorage.coffee | 279 ++++++++------- backbone.dualstorage.js | 498 +++++++++++++++++---------- spec/backbone.dualstorage.js | 498 +++++++++++++++++---------- spec/backbone_extensions_spec.coffee | 8 +- spec/backbone_extensions_spec.js | 9 +- spec/dualsync_spec.coffee | 1 + spec/dualsync_spec.js | 5 +- spec/localstorage_store_spec.coffee | 87 ++--- spec/localstorage_store_spec.js | 155 +++++---- spec/localsync_spec.coffee | 32 +- spec/localsync_spec.js | 34 +- 12 files changed, 1304 insertions(+), 800 deletions(-) diff --git a/backbone.dualstorage.amd.js b/backbone.dualstorage.amd.js index a2c85f0..05323a6 100644 --- a/backbone.dualstorage.amd.js +++ b/backbone.dualstorage.amd.js @@ -15,49 +15,108 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that. +as that.co */ -var S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result; +var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, + __slice = [].slice; + +LocaleStorageAdapter = (function() { + function LocaleStorageAdapter() {} + + LocaleStorageAdapter.prototype.setItem = function(key, value) { + localStorage.setItem(key, value); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.getItem = function(key) { + var value; + value = localStorage.getItem(key); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.removeItem = function(key) { + localStorage.removeItem(key); + return $.Deferred().resolve(); + }; + + return LocaleStorageAdapter; + +})(); + +Backbone.storageAdapter = new LocaleStorageAdapter; Backbone.Collection.prototype.syncDirty = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_dirty") || localStorage.getItem("" + storeName + "_dirty")); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = id.length === 36 ? this.findWhere({ - id: id - }) : this.get(id); - _results.push(model != null ? model.save() : void 0); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_dirty").then(function(store) { + var id, ids, model, models; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(id.length === 36 ? this.findWhere({ + id: id + }) : this.get(id)); + } + return _results; + }).call(_this); + return $.when.apply($, (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + if (model) { + _results.push(model.save()); + } + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDestroyed = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_destroyed") || (store = localStorage.getItem("" + storeName + "_destroyed"))); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = new this.model({ - id: id - }); - model.collection = this; - _results.push(model.destroy()); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_destroyed").then(function(store) { + var id, ids, model, models, _i, _len; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(new this.model({ + id: id + })); + } + return _results; + }).call(_this); + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + model.collection = _this; + } + return $.when.apply($, (function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + model = models[_j]; + _results.push(model.destroy()); + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDirtyAndDestroyed = function() { - this.syncDirty(); - return this.syncDestroyed(); + var _this = this; + return this.syncDirty().then(function() { + return _this.syncDestroyed(); + }); }; S4 = function() { @@ -68,8 +127,11 @@ window.Store = (function() { Store.prototype.sep = ''; function Store(name) { + var _this = this; this.name = name; - this.records = this.recordsOn(this.name); + this.recordsOn(this.name).done(function(result) { + return _this.records = result; + }); } Store.prototype.generateId = function() { @@ -77,110 +139,151 @@ window.Store = (function() { }; Store.prototype.save = function() { - return localStorage.setItem(this.name, this.records.join(',')); + return Backbone.storageAdapter.setItem(this.name, this.records.join(',')); }; Store.prototype.recordsOn = function(key) { - var store; - store = localStorage.getItem(key); - return (store && store.split(',')) || []; + return Backbone.storageAdapter.getItem(key).then(function(store) { + return (store && store.split(',')) || []; + }); }; Store.prototype.dirty = function(model) { - var dirtyRecords; - dirtyRecords = this.recordsOn(this.name + '_dirty'); - if (!_.include(dirtyRecords, model.id.toString())) { - dirtyRecords.push(model.id); - localStorage.setItem(this.name + '_dirty', dirtyRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + if (!_.include(dirtyRecords, model.id.toString())) { + dirtyRecords.push(model.id.toString()); + return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.clean = function(model, from) { - var dirtyRecords, store; + var store, + _this = this; store = "" + this.name + "_" + from; - dirtyRecords = this.recordsOn(store); - if (_.include(dirtyRecords, model.id.toString())) { - localStorage.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')); - } - return model; + return this.recordsOn(store).then(function(dirtyRecords) { + if (_.include(dirtyRecords, model.id.toString())) { + return Backbone.storageAdapter.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.destroyed = function(model) { - var destroyedRecords; - destroyedRecords = this.recordsOn(this.name + '_destroyed'); - if (!_.include(destroyedRecords, model.id.toString())) { - destroyedRecords.push(model.id); - localStorage.setItem(this.name + '_destroyed', destroyedRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + if (!_.include(destroyedRecords, model.id.toString())) { + destroyedRecords.push(model.id.toString()); + Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.create = function(model) { + var _this = this; if (!_.isObject(model)) { - return model; + return $.Deferred().resolve(model); } if (!model.id) { model.id = this.generateId(); model.set(model.idAttribute, model.id); } - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - this.records.push(model.id.toString()); - this.save(); - return model; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + _this.records.push(model.id.toString()); + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.update = function(model) { - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - if (!_.include(this.records, model.id.toString())) { - this.records.push(model.id.toString()); - } - this.save(); - return model; + var _this = this; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + if (!_.include(_this.records, model.id.toString())) { + _this.records.push(model.id.toString()); + } + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.clear = function() { - var id, _i, _len, _ref; - _ref = this.records; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - localStorage.removeItem(this.name + this.sep + id); - } - this.records = []; - return this.save(); + var id, + _this = this; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.removeItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + _this.records = []; + return _this.save(); + }); }; Store.prototype.hasDirtyOrDestroyed = function() { - return !_.isEmpty(localStorage.getItem(this.name + '_dirty')) || !_.isEmpty(localStorage.getItem(this.name + '_destroyed')); + var _this = this; + return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return !_.isEmpty(dirty) || !_.isEmpty(destroyed); + }); + }); }; Store.prototype.find = function(model) { - var modelAsJson; - modelAsJson = localStorage.getItem(this.name + this.sep + model.id); - if (modelAsJson === null) { - return null; - } - return JSON.parse(modelAsJson); + return Backbone.storageAdapter.getItem(this.name + this.sep + model.id).then(function(modelAsJson) { + if (modelAsJson === null) { + return null; + } + return JSON.parse(modelAsJson); + }); }; Store.prototype.findAll = function() { - var id, _i, _len, _ref, _results; - _ref = this.records; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - _results.push(JSON.parse(localStorage.getItem(this.name + this.sep + id))); - } - return _results; + var id; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.getItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + var model, models, _i, _len, _results; + models = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + _results.push(JSON.parse(model)); + } + return _results; + }); }; Store.prototype.destroy = function(model) { - localStorage.removeItem(this.name + this.sep + model.id); - this.records = _.reject(this.records, function(record_id) { - return record_id === model.id.toString(); + var _this = this; + return Backbone.storageAdapter.removeItem(this.name + this.sep + model.id).then(function() { + _this.records = _.without(_this.records, model.id.toString()); + return _this.save().then(function() { + return model; + }); }); - this.save(); - return model; }; return Store; @@ -210,7 +313,7 @@ callbackTranslator = { }; localsync = function(method, model, options) { - var isValidModel, preExisting, response, store; + var isValidModel, promise, store; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -218,7 +321,7 @@ localsync = function(method, model, options) { throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - response = (function() { + promise = (function() { switch (method) { case 'read': if (model.id) { @@ -232,48 +335,55 @@ localsync = function(method, model, options) { case 'clear': return store.clear(); case 'create': - if (!(options.add && !options.merge && (preExisting = store.find(model)))) { - model = store.create(model); - if (options.dirty) { - store.dirty(model); + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; } - return model; - } else { - return preExisting; - } - break; + }); case 'update': - store.update(model); - if (options.dirty) { - return store.dirty(model); - } else { - return store.clean(model, 'dirty'); - } - break; - case 'delete': - store.destroy(model); - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } + }); } })(); - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; } - } - return response; + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }; result = function(object, property) { @@ -333,50 +443,74 @@ dualsync = function(method, model, options) { error = options.error; switch (method) { case 'read': - if (localsync('hasDirtyOrDestroyed', model, options)) { - return success(localsync(method, model, options)); - } else { - options.success = function(resp, status, xhr) { - var collection, idAttribute, modelAttributes, responseModel, _i, _len; - resp = parseRemoteResponse(model, resp); - if (!options.add) { - localsync('clear', model, options); - } - if (_.isArray(resp)) { - collection = model; - idAttribute = collection.model.prototype.idAttribute; - for (_i = 0, _len = resp.length; _i < _len; _i++) { - modelAttributes = resp[_i]; - model = collection.get(modelAttributes[idAttribute]); - if (model) { - responseModel = modelUpdatedWithResponse(model, modelAttributes); + return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + if (hasDirtyOrDestroyed) { + return success(localsync(method, model, options)); + } else { + options.success = function(resp, status, xhr) { + var go; + resp = parseRemoteResponse(model, resp); + go = function() { + var collection, idAttribute, m, modelAttributes, models, responseModel, _i, _len; + if (_.isArray(resp)) { + collection = model; + idAttribute = collection.model.prototype.idAttribute; + models = []; + for (_i = 0, _len = resp.length; _i < _len; _i++) { + modelAttributes = resp[_i]; + model = collection.get(modelAttributes[idAttribute]); + if (model) { + responseModel = modelUpdatedWithResponse(model, modelAttributes); + } else { + responseModel = new collection.model(modelAttributes); + } + models.push(responseModel); + } + return $.when.apply($, ((function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + m = models[_j]; + _results.push(localsync('create', m, options)); + } + return _results; + })())).then(function() { + return success(resp, status, xhr); + }); } else { - responseModel = new collection.model(modelAttributes); + responseModel = modelUpdatedWithResponse(model, resp); + return localsync('create', responseModel, options).then(function() { + return success(resp, status, xhr); + }); } - localsync('create', responseModel, options); + }; + if (options.add) { + return localsync('clear', model, options).then(go); + } else { + return go(); } - } else { - responseModel = modelUpdatedWithResponse(model, resp); - localsync('create', responseModel, options); - } - return success(resp, status, xhr); - }; - options.error = function(resp) { - return success(localsync(method, model, options)); - }; - return onlineSync(method, model, options); - } - break; + }; + options.error = function(resp) { + return localsync(method, model, options).then(function(result) { + return success(result); + }); + }; + return onlineSync(method, model, options); + } + }); case 'create': options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); case 'update': @@ -388,16 +522,20 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - localsync('delete', model, options); - localsync('create', updatedModel, options); - return success(resp, status, xhr); + return localsync('delete', model, options).then(function() { + return localsync('create', updatedModel, options).then(function() { + return success(resp, status, xhr); + }); + }); }; options.error = function(resp) { options.dirty = true; model.set(model.idAttribute, temporaryId, { silent: true }); - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; model.set(model.idAttribute, null, { silent: true @@ -407,12 +545,15 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success; + }); }; return onlineSync(method, model, options); } @@ -422,12 +563,15 @@ dualsync = function(method, model, options) { return localsync(method, model, options); } else { options.success = function(resp, status, xhr) { - localsync(method, model, options); - return success(resp, status, xhr); + return localsync(method, model, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); } diff --git a/backbone.dualstorage.coffee b/backbone.dualstorage.coffee index ab2e403..e92641d 100644 --- a/backbone.dualstorage.coffee +++ b/backbone.dualstorage.coffee @@ -3,35 +3,43 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that. +as that.co ### +class LocaleStorageAdapter + setItem: (key, value) -> + localStorage.setItem key, value + $.Deferred().resolve value + + getItem: (key) -> + value = localStorage.getItem key + $.Deferred().resolve value + + removeItem: (key) -> + localStorage.removeItem key + $.Deferred().resolve() + +Backbone.storageAdapter = new LocaleStorageAdapter + # Make it easy for collections to sync dirty and destroyed records # Simply call collection.syncDirtyAndDestroyed() Backbone.Collection.prototype.syncDirty = -> - url = result(@, 'url') - storeName = result(@, 'storeName') - store = localStorage.getItem "#{url}_dirty" || localStorage.getItem "#{storeName}_dirty" - ids = (store and store.split(',')) or [] - - for id in ids - model = if id.length == 36 then @findWhere(id: id) else @get(id) - model?.save() + storeName = result(@, 'storeName') || result(@, 'url') + Backbone.storageAdapter.getItem("#{storeName}_dirty").then (store) => + ids = (store and store.split(',')) or [] + models = ((if id.length == 36 then @findWhere(id: id) else @get(id)) for id in ids) + $.when (model.save() for model in models when model)... Backbone.Collection.prototype.syncDestroyed = -> - url = result(@, 'url') - storeName = result(@, 'storeName') - store = localStorage.getItem "#{url}_destroyed" || store = localStorage.getItem "#{storeName}_destroyed" - ids = (store and store.split(',')) or [] - - for id in ids - model = new @model(id: id) - model.collection = @ - model.destroy() + storeName = result(@, 'storeName') || result(@, 'url') + Backbone.storageAdapter.getItem("#{storeName}_destroyed").then (store) => + ids = (store and store.split(',')) or [] + models = (new @model(id: id) for id in ids) + (model.collection = @ for model in models) + $.when (model.destroy() for model in models)... Backbone.Collection.prototype.syncDirtyAndDestroyed = -> - @syncDirty() - @syncDestroyed() + @syncDirty().then => @syncDestroyed() # Generate four random hex digits. S4 = -> @@ -44,7 +52,8 @@ class window.Store constructor: (name) -> @name = name - @records = @recordsOn @name + @recordsOn(@name).done (result) => + @records = result # Generates an unique id to use when saving new instances into localstorage # by default generates a pseudo-GUID by concatenating random hexadecimal. @@ -54,81 +63,80 @@ class window.Store # Save the current state of the **Store** to *localStorage*. save: -> - localStorage.setItem @name, @records.join(',') + Backbone.storageAdapter.setItem @name, @records.join(',') recordsOn: (key) -> - store = localStorage.getItem(key) - (store and store.split(',')) or [] + Backbone.storageAdapter.getItem(key).then (store) -> + (store and store.split(',')) or [] dirty: (model) -> - dirtyRecords = @recordsOn @name + '_dirty' - if not _.include(dirtyRecords, model.id.toString()) - dirtyRecords.push model.id - localStorage.setItem @name + '_dirty', dirtyRecords.join(',') - model + @recordsOn(@name + '_dirty').then (dirtyRecords) => + if not _.include(dirtyRecords, model.id.toString()) + dirtyRecords.push model.id.toString() + return Backbone.storageAdapter.setItem(@name + '_dirty', dirtyRecords.join(',')).then -> model + model clean: (model, from) -> store = "#{@name}_#{from}" - dirtyRecords = @recordsOn store - if _.include dirtyRecords, model.id.toString() - localStorage.setItem store, _.without(dirtyRecords, model.id.toString()).join(',') - model + @recordsOn(store).then (dirtyRecords) => + if _.include dirtyRecords, model.id.toString() + return Backbone.storageAdapter.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')).then -> model + model destroyed: (model) -> - destroyedRecords = @recordsOn @name + '_destroyed' - if not _.include destroyedRecords, model.id.toString() - destroyedRecords.push model.id - localStorage.setItem @name + '_destroyed', destroyedRecords.join(',') - model + @recordsOn(@name + '_destroyed').then (destroyedRecords) => + if not _.include destroyedRecords, model.id.toString() + destroyedRecords.push model.id.toString() + Backbone.storageAdapter.setItem(@name + '_destroyed', destroyedRecords.join(',')).then -> model + model # Add a model, giving it a unique GUID, if it doesn't already # have an id of it's own. create: (model) -> - if not _.isObject(model) then return model + if not _.isObject(model) then return $.Deferred().resolve(model) if not model.id model.id = @generateId() model.set model.idAttribute, model.id - localStorage.setItem @name + @sep + model.id, JSON.stringify(model) - @records.push model.id.toString() - @save() - model + Backbone.storageAdapter.setItem(@name + @sep + model.id, JSON.stringify(model)).then => + @records.push model.id.toString() + @save().then => model # Update a model by replacing its copy in `this.data`. update: (model) -> - localStorage.setItem @name + @sep + model.id, JSON.stringify(model) - if not _.include(@records, model.id.toString()) - @records.push model.id.toString() - @save() - model + Backbone.storageAdapter.setItem(@name + @sep + model.id, JSON.stringify(model)).then => + if not _.include(@records, model.id.toString()) + @records.push model.id.toString() + @save().then => model clear: -> - for id in @records - localStorage.removeItem @name + @sep + id - @records = [] - @save() + $.when((Backbone.storageAdapter.removeItem(@name + @sep + id) for id in @records)...).then => + @records = [] + @save() hasDirtyOrDestroyed: -> - not _.isEmpty(localStorage.getItem(@name + '_dirty')) or not _.isEmpty(localStorage.getItem(@name + '_destroyed')) + Backbone.storageAdapter.getItem(@name + '_dirty').then (dirty) => + Backbone.storageAdapter.getItem(@name + '_destroyed').then (destroyed) => + not _.isEmpty(dirty) or not _.isEmpty(destroyed) # Retrieve a model from `this.data` by id. find: (model) -> - modelAsJson = localStorage.getItem(@name + @sep + model.id) - return null if modelAsJson == null - JSON.parse modelAsJson + Backbone.storageAdapter.getItem(@name + @sep + model.id).then (modelAsJson) -> + return null if modelAsJson == null + JSON.parse modelAsJson # Return the array of all models currently in storage. findAll: -> - for id in @records - JSON.parse localStorage.getItem(@name + @sep + id) + $.when((Backbone.storageAdapter.getItem(@name + @sep + id) for id in @records)...).then (models...) -> + (JSON.parse(model) for model in models) # Delete a model from `this.data`, returning it. destroy: (model) -> - localStorage.removeItem @name + @sep + model.id - @records = _.reject(@records, (record_id) -> - record_id is model.id.toString() - ) - @save() - model + Backbone.storageAdapter.removeItem(@name + @sep + model.id).then => + @records = _.without @records, model.id.toString() + # @records = _.reject(@records, (record_id) -> + # record_id is model.id.toString() + # ) + @save().then -> model callbackTranslator = needsTranslation: Backbone.VERSION == '0.9.10' @@ -157,7 +165,7 @@ localsync = (method, model, options) -> store = new Store options.storeName - response = switch method + promise = switch method when 'read' if model.id store.find(model) @@ -168,36 +176,41 @@ localsync = (method, model, options) -> when 'clear' store.clear() when 'create' - unless options.add and not options.merge and (preExisting = store.find(model)) - model = store.create(model) - store.dirty(model) if options.dirty - model - else - preExisting + store.find(model).then (preExisting) -> + unless options.add and not options.merge and preExisting + store.create(model).then (model) -> + if options.dirty + return store.dirty(model).then -> + model + model + else + preExisting when 'update' - store.update(model) - if options.dirty - store.dirty(model) - else - store.clean(model, 'dirty') - when 'delete' - store.destroy(model) - if options.dirty - store.destroyed(model) - else - if model.id.toString().length == 36 + store.update(model).then (model) -> + if options.dirty + store.dirty(model) + else store.clean(model, 'dirty') + when 'delete' + store.destroy(model).then -> + if options.dirty + store.destroyed(model) else - store.clean(model, 'destroyed') - response = response.attributes if response?.attributes + if model.id.toString().length == 36 + store.clean(model, 'dirty') + else + store.clean(model, 'destroyed') - unless options.ignoreCallbacks - if response - options.success response - else - options.error 'Record not found' + promise.then (response) -> + response = response.attributes if response?.attributes + + unless options.ignoreCallbacks + if response + options.success response + else + options.error 'Record not found' - response + response # If the value of the named property is a function then invoke it; # otherwise, return it. @@ -248,43 +261,52 @@ dualsync = (method, model, options) -> switch method when 'read' - if localsync('hasDirtyOrDestroyed', model, options) - success localsync(method, model, options) - else - options.success = (resp, status, xhr) -> - resp = parseRemoteResponse(model, resp) - - localsync('clear', model, options) unless options.add - - if _.isArray resp - collection = model - idAttribute = collection.model.prototype.idAttribute - for modelAttributes in resp - model = collection.get(modelAttributes[idAttribute]) - if model - responseModel = modelUpdatedWithResponse(model, modelAttributes) + localsync('hasDirtyOrDestroyed', model, options).then (hasDirtyOrDestroyed) -> + if hasDirtyOrDestroyed + success localsync(method, model, options) + else + options.success = (resp, status, xhr) -> + resp = parseRemoteResponse(model, resp) + + go = -> + if _.isArray resp + collection = model + idAttribute = collection.model.prototype.idAttribute + models = [] + for modelAttributes in resp + model = collection.get(modelAttributes[idAttribute]) + if model + responseModel = modelUpdatedWithResponse(model, modelAttributes) + else + responseModel = new collection.model(modelAttributes) + models.push responseModel + $.when((localsync('create', m, options) for m in models)...).then -> + success(resp, status, xhr) else - responseModel = new collection.model(modelAttributes) - localsync('create', responseModel, options) - else - responseModel = modelUpdatedWithResponse(model, resp) - localsync('create', responseModel, options) + responseModel = modelUpdatedWithResponse(model, resp) + localsync('create', responseModel, options).then -> + success(resp, status, xhr) - success(resp, status, xhr) + if options.add + localsync('clear', model, options).then go + else + go() - options.error = (resp) -> - success localsync(method, model, options) + options.error = (resp) -> + localsync(method, model, options).then (result) -> + success result - onlineSync(method, model, options) + onlineSync(method, model, options) when 'create' options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp - localsync(method, updatedModel, options) - success(resp, status, xhr) + localsync(method, updatedModel, options).then -> + success(resp, status, xhr) options.error = (resp) -> options.dirty = true - success localsync(method, model, options) + localsync(method, model, options).then (result) -> + success result onlineSync(method, model, options) @@ -295,24 +317,26 @@ dualsync = (method, model, options) -> options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp model.set model.idAttribute, temporaryId, silent: true - localsync('delete', model, options) - localsync('create', updatedModel, options) - success(resp, status, xhr) + localsync('delete', model, options).then -> + localsync('create', updatedModel, options).then -> + success(resp, status, xhr) options.error = (resp) -> options.dirty = true model.set model.idAttribute, temporaryId, silent: true - success localsync(method, model, options) + localsync(method, model, options).then (result) -> + success result model.set model.idAttribute, null, silent: true onlineSync('create', model, options) else options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp - localsync(method, updatedModel, options) - success(resp, status, xhr) + localsync(method, updatedModel, options).then -> + success(resp, status, xhr) options.error = (resp) -> options.dirty = true - success localsync(method, model, options) + localsync(method, model, options).then (result) -> + success onlineSync(method, model, options) @@ -321,11 +345,12 @@ dualsync = (method, model, options) -> localsync(method, model, options) else options.success = (resp, status, xhr) -> - localsync(method, model, options) - success(resp, status, xhr) + localsync(method, model, options).then -> + success(resp, status, xhr) options.error = (resp) -> options.dirty = true - success localsync(method, model, options) + localsync(method, model, options).then (result) -> + success result onlineSync(method, model, options) diff --git a/backbone.dualstorage.js b/backbone.dualstorage.js index 1bbf50f..a9cd9d0 100644 --- a/backbone.dualstorage.js +++ b/backbone.dualstorage.js @@ -4,51 +4,110 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that. +as that.co */ (function() { - var S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result; + var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, + __slice = [].slice; + + LocaleStorageAdapter = (function() { + function LocaleStorageAdapter() {} + + LocaleStorageAdapter.prototype.setItem = function(key, value) { + localStorage.setItem(key, value); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.getItem = function(key) { + var value; + value = localStorage.getItem(key); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.removeItem = function(key) { + localStorage.removeItem(key); + return $.Deferred().resolve(); + }; + + return LocaleStorageAdapter; + + })(); + + Backbone.storageAdapter = new LocaleStorageAdapter; Backbone.Collection.prototype.syncDirty = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_dirty") || localStorage.getItem("" + storeName + "_dirty")); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = id.length === 36 ? this.findWhere({ - id: id - }) : this.get(id); - _results.push(model != null ? model.save() : void 0); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_dirty").then(function(store) { + var id, ids, model, models; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(id.length === 36 ? this.findWhere({ + id: id + }) : this.get(id)); + } + return _results; + }).call(_this); + return $.when.apply($, (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + if (model) { + _results.push(model.save()); + } + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDestroyed = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_destroyed") || (store = localStorage.getItem("" + storeName + "_destroyed"))); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = new this.model({ - id: id - }); - model.collection = this; - _results.push(model.destroy()); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_destroyed").then(function(store) { + var id, ids, model, models, _i, _len; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(new this.model({ + id: id + })); + } + return _results; + }).call(_this); + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + model.collection = _this; + } + return $.when.apply($, (function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + model = models[_j]; + _results.push(model.destroy()); + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDirtyAndDestroyed = function() { - this.syncDirty(); - return this.syncDestroyed(); + var _this = this; + return this.syncDirty().then(function() { + return _this.syncDestroyed(); + }); }; S4 = function() { @@ -59,8 +118,11 @@ as that. Store.prototype.sep = ''; function Store(name) { + var _this = this; this.name = name; - this.records = this.recordsOn(this.name); + this.recordsOn(this.name).done(function(result) { + return _this.records = result; + }); } Store.prototype.generateId = function() { @@ -68,110 +130,151 @@ as that. }; Store.prototype.save = function() { - return localStorage.setItem(this.name, this.records.join(',')); + return Backbone.storageAdapter.setItem(this.name, this.records.join(',')); }; Store.prototype.recordsOn = function(key) { - var store; - store = localStorage.getItem(key); - return (store && store.split(',')) || []; + return Backbone.storageAdapter.getItem(key).then(function(store) { + return (store && store.split(',')) || []; + }); }; Store.prototype.dirty = function(model) { - var dirtyRecords; - dirtyRecords = this.recordsOn(this.name + '_dirty'); - if (!_.include(dirtyRecords, model.id.toString())) { - dirtyRecords.push(model.id); - localStorage.setItem(this.name + '_dirty', dirtyRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + if (!_.include(dirtyRecords, model.id.toString())) { + dirtyRecords.push(model.id.toString()); + return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.clean = function(model, from) { - var dirtyRecords, store; + var store, + _this = this; store = "" + this.name + "_" + from; - dirtyRecords = this.recordsOn(store); - if (_.include(dirtyRecords, model.id.toString())) { - localStorage.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')); - } - return model; + return this.recordsOn(store).then(function(dirtyRecords) { + if (_.include(dirtyRecords, model.id.toString())) { + return Backbone.storageAdapter.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.destroyed = function(model) { - var destroyedRecords; - destroyedRecords = this.recordsOn(this.name + '_destroyed'); - if (!_.include(destroyedRecords, model.id.toString())) { - destroyedRecords.push(model.id); - localStorage.setItem(this.name + '_destroyed', destroyedRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + if (!_.include(destroyedRecords, model.id.toString())) { + destroyedRecords.push(model.id.toString()); + Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.create = function(model) { + var _this = this; if (!_.isObject(model)) { - return model; + return $.Deferred().resolve(model); } if (!model.id) { model.id = this.generateId(); model.set(model.idAttribute, model.id); } - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - this.records.push(model.id.toString()); - this.save(); - return model; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + _this.records.push(model.id.toString()); + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.update = function(model) { - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - if (!_.include(this.records, model.id.toString())) { - this.records.push(model.id.toString()); - } - this.save(); - return model; + var _this = this; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + if (!_.include(_this.records, model.id.toString())) { + _this.records.push(model.id.toString()); + } + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.clear = function() { - var id, _i, _len, _ref; - _ref = this.records; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - localStorage.removeItem(this.name + this.sep + id); - } - this.records = []; - return this.save(); + var id, + _this = this; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.removeItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + _this.records = []; + return _this.save(); + }); }; Store.prototype.hasDirtyOrDestroyed = function() { - return !_.isEmpty(localStorage.getItem(this.name + '_dirty')) || !_.isEmpty(localStorage.getItem(this.name + '_destroyed')); + var _this = this; + return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return !_.isEmpty(dirty) || !_.isEmpty(destroyed); + }); + }); }; Store.prototype.find = function(model) { - var modelAsJson; - modelAsJson = localStorage.getItem(this.name + this.sep + model.id); - if (modelAsJson === null) { - return null; - } - return JSON.parse(modelAsJson); + return Backbone.storageAdapter.getItem(this.name + this.sep + model.id).then(function(modelAsJson) { + if (modelAsJson === null) { + return null; + } + return JSON.parse(modelAsJson); + }); }; Store.prototype.findAll = function() { - var id, _i, _len, _ref, _results; - _ref = this.records; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - _results.push(JSON.parse(localStorage.getItem(this.name + this.sep + id))); - } - return _results; + var id; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.getItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + var model, models, _i, _len, _results; + models = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + _results.push(JSON.parse(model)); + } + return _results; + }); }; Store.prototype.destroy = function(model) { - localStorage.removeItem(this.name + this.sep + model.id); - this.records = _.reject(this.records, function(record_id) { - return record_id === model.id.toString(); + var _this = this; + return Backbone.storageAdapter.removeItem(this.name + this.sep + model.id).then(function() { + _this.records = _.without(_this.records, model.id.toString()); + return _this.save().then(function() { + return model; + }); }); - this.save(); - return model; }; return Store; @@ -201,7 +304,7 @@ as that. }; localsync = function(method, model, options) { - var isValidModel, preExisting, response, store; + var isValidModel, promise, store; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -209,7 +312,7 @@ as that. throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - response = (function() { + promise = (function() { switch (method) { case 'read': if (model.id) { @@ -223,48 +326,55 @@ as that. case 'clear': return store.clear(); case 'create': - if (!(options.add && !options.merge && (preExisting = store.find(model)))) { - model = store.create(model); - if (options.dirty) { - store.dirty(model); + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; } - return model; - } else { - return preExisting; - } - break; + }); case 'update': - store.update(model); - if (options.dirty) { - return store.dirty(model); - } else { - return store.clean(model, 'dirty'); - } - break; - case 'delete': - store.destroy(model); - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } + }); } })(); - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; } - } - return response; + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }; result = function(object, property) { @@ -324,50 +434,74 @@ as that. error = options.error; switch (method) { case 'read': - if (localsync('hasDirtyOrDestroyed', model, options)) { - return success(localsync(method, model, options)); - } else { - options.success = function(resp, status, xhr) { - var collection, idAttribute, modelAttributes, responseModel, _i, _len; - resp = parseRemoteResponse(model, resp); - if (!options.add) { - localsync('clear', model, options); - } - if (_.isArray(resp)) { - collection = model; - idAttribute = collection.model.prototype.idAttribute; - for (_i = 0, _len = resp.length; _i < _len; _i++) { - modelAttributes = resp[_i]; - model = collection.get(modelAttributes[idAttribute]); - if (model) { - responseModel = modelUpdatedWithResponse(model, modelAttributes); + return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + if (hasDirtyOrDestroyed) { + return success(localsync(method, model, options)); + } else { + options.success = function(resp, status, xhr) { + var go; + resp = parseRemoteResponse(model, resp); + go = function() { + var collection, idAttribute, m, modelAttributes, models, responseModel, _i, _len; + if (_.isArray(resp)) { + collection = model; + idAttribute = collection.model.prototype.idAttribute; + models = []; + for (_i = 0, _len = resp.length; _i < _len; _i++) { + modelAttributes = resp[_i]; + model = collection.get(modelAttributes[idAttribute]); + if (model) { + responseModel = modelUpdatedWithResponse(model, modelAttributes); + } else { + responseModel = new collection.model(modelAttributes); + } + models.push(responseModel); + } + return $.when.apply($, ((function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + m = models[_j]; + _results.push(localsync('create', m, options)); + } + return _results; + })())).then(function() { + return success(resp, status, xhr); + }); } else { - responseModel = new collection.model(modelAttributes); + responseModel = modelUpdatedWithResponse(model, resp); + return localsync('create', responseModel, options).then(function() { + return success(resp, status, xhr); + }); } - localsync('create', responseModel, options); + }; + if (options.add) { + return localsync('clear', model, options).then(go); + } else { + return go(); } - } else { - responseModel = modelUpdatedWithResponse(model, resp); - localsync('create', responseModel, options); - } - return success(resp, status, xhr); - }; - options.error = function(resp) { - return success(localsync(method, model, options)); - }; - return onlineSync(method, model, options); - } - break; + }; + options.error = function(resp) { + return localsync(method, model, options).then(function(result) { + return success(result); + }); + }; + return onlineSync(method, model, options); + } + }); case 'create': options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); case 'update': @@ -379,16 +513,20 @@ as that. model.set(model.idAttribute, temporaryId, { silent: true }); - localsync('delete', model, options); - localsync('create', updatedModel, options); - return success(resp, status, xhr); + return localsync('delete', model, options).then(function() { + return localsync('create', updatedModel, options).then(function() { + return success(resp, status, xhr); + }); + }); }; options.error = function(resp) { options.dirty = true; model.set(model.idAttribute, temporaryId, { silent: true }); - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; model.set(model.idAttribute, null, { silent: true @@ -398,12 +536,15 @@ as that. options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success; + }); }; return onlineSync(method, model, options); } @@ -413,12 +554,15 @@ as that. return localsync(method, model, options); } else { options.success = function(resp, status, xhr) { - localsync(method, model, options); - return success(resp, status, xhr); + return localsync(method, model, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); } diff --git a/spec/backbone.dualstorage.js b/spec/backbone.dualstorage.js index 6a72885..a70e145 100644 --- a/spec/backbone.dualstorage.js +++ b/spec/backbone.dualstorage.js @@ -4,49 +4,108 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that. +as that.co */ -var S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result; +var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, + __slice = [].slice; + +LocaleStorageAdapter = (function() { + function LocaleStorageAdapter() {} + + LocaleStorageAdapter.prototype.setItem = function(key, value) { + localStorage.setItem(key, value); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.getItem = function(key) { + var value; + value = localStorage.getItem(key); + return $.Deferred().resolve(value); + }; + + LocaleStorageAdapter.prototype.removeItem = function(key) { + localStorage.removeItem(key); + return $.Deferred().resolve(); + }; + + return LocaleStorageAdapter; + +})(); + +Backbone.storageAdapter = new LocaleStorageAdapter; Backbone.Collection.prototype.syncDirty = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_dirty") || localStorage.getItem("" + storeName + "_dirty")); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = id.length === 36 ? this.findWhere({ - id: id - }) : this.get(id); - _results.push(model != null ? model.save() : void 0); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_dirty").then(function(store) { + var id, ids, model, models; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(id.length === 36 ? this.findWhere({ + id: id + }) : this.get(id)); + } + return _results; + }).call(_this); + return $.when.apply($, (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + if (model) { + _results.push(model.save()); + } + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDestroyed = function() { - var id, ids, model, store, storeName, url, _i, _len, _results; - url = result(this, 'url'); - storeName = result(this, 'storeName'); - store = localStorage.getItem(("" + url + "_destroyed") || (store = localStorage.getItem("" + storeName + "_destroyed"))); - ids = (store && store.split(',')) || []; - _results = []; - for (_i = 0, _len = ids.length; _i < _len; _i++) { - id = ids[_i]; - model = new this.model({ - id: id - }); - model.collection = this; - _results.push(model.destroy()); - } - return _results; + var storeName, + _this = this; + storeName = result(this, 'storeName') || result(this, 'url'); + return Backbone.storageAdapter.getItem("" + storeName + "_destroyed").then(function(store) { + var id, ids, model, models, _i, _len; + ids = (store && store.split(',')) || []; + models = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = ids.length; _i < _len; _i++) { + id = ids[_i]; + _results.push(new this.model({ + id: id + })); + } + return _results; + }).call(_this); + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + model.collection = _this; + } + return $.when.apply($, (function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + model = models[_j]; + _results.push(model.destroy()); + } + return _results; + })()); + }); }; Backbone.Collection.prototype.syncDirtyAndDestroyed = function() { - this.syncDirty(); - return this.syncDestroyed(); + var _this = this; + return this.syncDirty().then(function() { + return _this.syncDestroyed(); + }); }; S4 = function() { @@ -57,8 +116,11 @@ window.Store = (function() { Store.prototype.sep = ''; function Store(name) { + var _this = this; this.name = name; - this.records = this.recordsOn(this.name); + this.recordsOn(this.name).done(function(result) { + return _this.records = result; + }); } Store.prototype.generateId = function() { @@ -66,110 +128,151 @@ window.Store = (function() { }; Store.prototype.save = function() { - return localStorage.setItem(this.name, this.records.join(',')); + return Backbone.storageAdapter.setItem(this.name, this.records.join(',')); }; Store.prototype.recordsOn = function(key) { - var store; - store = localStorage.getItem(key); - return (store && store.split(',')) || []; + return Backbone.storageAdapter.getItem(key).then(function(store) { + return (store && store.split(',')) || []; + }); }; Store.prototype.dirty = function(model) { - var dirtyRecords; - dirtyRecords = this.recordsOn(this.name + '_dirty'); - if (!_.include(dirtyRecords, model.id.toString())) { - dirtyRecords.push(model.id); - localStorage.setItem(this.name + '_dirty', dirtyRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + if (!_.include(dirtyRecords, model.id.toString())) { + dirtyRecords.push(model.id.toString()); + return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.clean = function(model, from) { - var dirtyRecords, store; + var store, + _this = this; store = "" + this.name + "_" + from; - dirtyRecords = this.recordsOn(store); - if (_.include(dirtyRecords, model.id.toString())) { - localStorage.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')); - } - return model; + return this.recordsOn(store).then(function(dirtyRecords) { + if (_.include(dirtyRecords, model.id.toString())) { + return Backbone.storageAdapter.setItem(store, _.without(dirtyRecords, model.id.toString()).join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.destroyed = function(model) { - var destroyedRecords; - destroyedRecords = this.recordsOn(this.name + '_destroyed'); - if (!_.include(destroyedRecords, model.id.toString())) { - destroyedRecords.push(model.id); - localStorage.setItem(this.name + '_destroyed', destroyedRecords.join(',')); - } - return model; + var _this = this; + return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + if (!_.include(destroyedRecords, model.id.toString())) { + destroyedRecords.push(model.id.toString()); + Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + return model; + }); + } + return model; + }); }; Store.prototype.create = function(model) { + var _this = this; if (!_.isObject(model)) { - return model; + return $.Deferred().resolve(model); } if (!model.id) { model.id = this.generateId(); model.set(model.idAttribute, model.id); } - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - this.records.push(model.id.toString()); - this.save(); - return model; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + _this.records.push(model.id.toString()); + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.update = function(model) { - localStorage.setItem(this.name + this.sep + model.id, JSON.stringify(model)); - if (!_.include(this.records, model.id.toString())) { - this.records.push(model.id.toString()); - } - this.save(); - return model; + var _this = this; + return Backbone.storageAdapter.setItem(this.name + this.sep + model.id, JSON.stringify(model)).then(function() { + if (!_.include(_this.records, model.id.toString())) { + _this.records.push(model.id.toString()); + } + return _this.save().then(function() { + return model; + }); + }); }; Store.prototype.clear = function() { - var id, _i, _len, _ref; - _ref = this.records; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - localStorage.removeItem(this.name + this.sep + id); - } - this.records = []; - return this.save(); + var id, + _this = this; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.removeItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + _this.records = []; + return _this.save(); + }); }; Store.prototype.hasDirtyOrDestroyed = function() { - return !_.isEmpty(localStorage.getItem(this.name + '_dirty')) || !_.isEmpty(localStorage.getItem(this.name + '_destroyed')); + var _this = this; + return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return !_.isEmpty(dirty) || !_.isEmpty(destroyed); + }); + }); }; Store.prototype.find = function(model) { - var modelAsJson; - modelAsJson = localStorage.getItem(this.name + this.sep + model.id); - if (modelAsJson === null) { - return null; - } - return JSON.parse(modelAsJson); + return Backbone.storageAdapter.getItem(this.name + this.sep + model.id).then(function(modelAsJson) { + if (modelAsJson === null) { + return null; + } + return JSON.parse(modelAsJson); + }); }; Store.prototype.findAll = function() { - var id, _i, _len, _ref, _results; - _ref = this.records; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - id = _ref[_i]; - _results.push(JSON.parse(localStorage.getItem(this.name + this.sep + id))); - } - return _results; + var id; + return $.when.apply($, ((function() { + var _i, _len, _ref, _results; + _ref = this.records; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + id = _ref[_i]; + _results.push(Backbone.storageAdapter.getItem(this.name + this.sep + id)); + } + return _results; + }).call(this))).then(function() { + var model, models, _i, _len, _results; + models = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + _results = []; + for (_i = 0, _len = models.length; _i < _len; _i++) { + model = models[_i]; + _results.push(JSON.parse(model)); + } + return _results; + }); }; Store.prototype.destroy = function(model) { - localStorage.removeItem(this.name + this.sep + model.id); - this.records = _.reject(this.records, function(record_id) { - return record_id === model.id.toString(); + var _this = this; + return Backbone.storageAdapter.removeItem(this.name + this.sep + model.id).then(function() { + _this.records = _.without(_this.records, model.id.toString()); + return _this.save().then(function() { + return model; + }); }); - this.save(); - return model; }; return Store; @@ -199,7 +302,7 @@ callbackTranslator = { }; localsync = function(method, model, options) { - var isValidModel, preExisting, response, store; + var isValidModel, promise, store; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -207,7 +310,7 @@ localsync = function(method, model, options) { throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - response = (function() { + promise = (function() { switch (method) { case 'read': if (model.id) { @@ -221,48 +324,55 @@ localsync = function(method, model, options) { case 'clear': return store.clear(); case 'create': - if (!(options.add && !options.merge && (preExisting = store.find(model)))) { - model = store.create(model); - if (options.dirty) { - store.dirty(model); + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; } - return model; - } else { - return preExisting; - } - break; + }); case 'update': - store.update(model); - if (options.dirty) { - return store.dirty(model); - } else { - return store.clean(model, 'dirty'); - } - break; - case 'delete': - store.destroy(model); - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } + }); } })(); - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; } - } - return response; + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }; result = function(object, property) { @@ -322,50 +432,74 @@ dualsync = function(method, model, options) { error = options.error; switch (method) { case 'read': - if (localsync('hasDirtyOrDestroyed', model, options)) { - return success(localsync(method, model, options)); - } else { - options.success = function(resp, status, xhr) { - var collection, idAttribute, modelAttributes, responseModel, _i, _len; - resp = parseRemoteResponse(model, resp); - if (!options.add) { - localsync('clear', model, options); - } - if (_.isArray(resp)) { - collection = model; - idAttribute = collection.model.prototype.idAttribute; - for (_i = 0, _len = resp.length; _i < _len; _i++) { - modelAttributes = resp[_i]; - model = collection.get(modelAttributes[idAttribute]); - if (model) { - responseModel = modelUpdatedWithResponse(model, modelAttributes); + return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + if (hasDirtyOrDestroyed) { + return success(localsync(method, model, options)); + } else { + options.success = function(resp, status, xhr) { + var go; + resp = parseRemoteResponse(model, resp); + go = function() { + var collection, idAttribute, m, modelAttributes, models, responseModel, _i, _len; + if (_.isArray(resp)) { + collection = model; + idAttribute = collection.model.prototype.idAttribute; + models = []; + for (_i = 0, _len = resp.length; _i < _len; _i++) { + modelAttributes = resp[_i]; + model = collection.get(modelAttributes[idAttribute]); + if (model) { + responseModel = modelUpdatedWithResponse(model, modelAttributes); + } else { + responseModel = new collection.model(modelAttributes); + } + models.push(responseModel); + } + return $.when.apply($, ((function() { + var _j, _len1, _results; + _results = []; + for (_j = 0, _len1 = models.length; _j < _len1; _j++) { + m = models[_j]; + _results.push(localsync('create', m, options)); + } + return _results; + })())).then(function() { + return success(resp, status, xhr); + }); } else { - responseModel = new collection.model(modelAttributes); + responseModel = modelUpdatedWithResponse(model, resp); + return localsync('create', responseModel, options).then(function() { + return success(resp, status, xhr); + }); } - localsync('create', responseModel, options); + }; + if (options.add) { + return localsync('clear', model, options).then(go); + } else { + return go(); } - } else { - responseModel = modelUpdatedWithResponse(model, resp); - localsync('create', responseModel, options); - } - return success(resp, status, xhr); - }; - options.error = function(resp) { - return success(localsync(method, model, options)); - }; - return onlineSync(method, model, options); - } - break; + }; + options.error = function(resp) { + return localsync(method, model, options).then(function(result) { + return success(result); + }); + }; + return onlineSync(method, model, options); + } + }); case 'create': options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); case 'update': @@ -377,16 +511,20 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - localsync('delete', model, options); - localsync('create', updatedModel, options); - return success(resp, status, xhr); + return localsync('delete', model, options).then(function() { + return localsync('create', updatedModel, options).then(function() { + return success(resp, status, xhr); + }); + }); }; options.error = function(resp) { options.dirty = true; model.set(model.idAttribute, temporaryId, { silent: true }); - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; model.set(model.idAttribute, null, { silent: true @@ -396,12 +534,15 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - localsync(method, updatedModel, options); - return success(resp, status, xhr); + return localsync(method, updatedModel, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success; + }); }; return onlineSync(method, model, options); } @@ -411,12 +552,15 @@ dualsync = function(method, model, options) { return localsync(method, model, options); } else { options.success = function(resp, status, xhr) { - localsync(method, model, options); - return success(resp, status, xhr); + return localsync(method, model, options).then(function() { + return success(resp, status, xhr); + }); }; options.error = function(resp) { options.dirty = true; - return success(localsync(method, model, options)); + return localsync(method, model, options).then(function(result) { + return success(result); + }); }; return onlineSync(method, model, options); } diff --git a/spec/backbone_extensions_spec.coffee b/spec/backbone_extensions_spec.coffee index 47558a7..760368e 100644 --- a/spec/backbone_extensions_spec.coffee +++ b/spec/backbone_extensions_spec.coffee @@ -26,8 +26,8 @@ describe 'offline localStorage sync', -> describe 'syncDirtyAndDestroyed', -> it 'calls syncDirty and syncDestroyed', -> - syncDirty = spyOn Backbone.Collection.prototype, 'syncDirty' - syncDestroyed = spyOn Backbone.Collection.prototype, 'syncDestroyed' + syncDirty = spyOn(Backbone.Collection.prototype, 'syncDirty').andReturn $.Deferred().resolve() + syncDestroyed = spyOn(Backbone.Collection.prototype, 'syncDestroyed').andReturn $.Deferred().resolve() collection.syncDirtyAndDestroyed() expect(syncDirty).toHaveBeenCalled() expect(syncDestroyed).toHaveBeenCalled() @@ -48,8 +48,8 @@ describe 'offline localStorage sync', -> describe 'syncDestroyed', -> it 'finds all models marked as destroyed and destroys them', -> destroy = spyOn collection.get(3), 'destroy' - collection.syncDestroyed() - expect(localStorage.getItem 'cats_destroyed').toBeFalsy() + collection.syncDestroyed().then -> + expect(localStorage.getItem 'cats_destroyed').toBeFalsy() it 'works when there are no destroyed records', -> localStorage.setItem 'cats_destroyed', '' diff --git a/spec/backbone_extensions_spec.js b/spec/backbone_extensions_spec.js index 213426f..223d70c 100644 --- a/spec/backbone_extensions_spec.js +++ b/spec/backbone_extensions_spec.js @@ -45,8 +45,8 @@ describe('syncDirtyAndDestroyed', function() { return it('calls syncDirty and syncDestroyed', function() { var syncDestroyed, syncDirty; - syncDirty = spyOn(Backbone.Collection.prototype, 'syncDirty'); - syncDestroyed = spyOn(Backbone.Collection.prototype, 'syncDestroyed'); + syncDirty = spyOn(Backbone.Collection.prototype, 'syncDirty').andReturn($.Deferred().resolve()); + syncDestroyed = spyOn(Backbone.Collection.prototype, 'syncDestroyed').andReturn($.Deferred().resolve()); collection.syncDirtyAndDestroyed(); expect(syncDirty).toHaveBeenCalled(); return expect(syncDestroyed).toHaveBeenCalled(); @@ -71,8 +71,9 @@ it('finds all models marked as destroyed and destroys them', function() { var destroy; destroy = spyOn(collection.get(3), 'destroy'); - collection.syncDestroyed(); - return expect(localStorage.getItem('cats_destroyed')).toBeFalsy(); + return collection.syncDestroyed().then(function() { + return expect(localStorage.getItem('cats_destroyed')).toBeFalsy(); + }); }); return it('works when there are no destroyed records', function() { localStorage.setItem('cats_destroyed', ''); diff --git a/spec/dualsync_spec.coffee b/spec/dualsync_spec.coffee index 1f5381a..e503315 100644 --- a/spec/dualsync_spec.coffee +++ b/spec/dualsync_spec.coffee @@ -19,6 +19,7 @@ spyOnLocalsync = -> spyOn(window, 'localsync') .andCallFake (method, model, options) -> options.success?() unless options.ignoreCallbacks + $.Deferred().resolve() localsync = window.localsync describe 'delegating to localsync and backboneSync, and calling the model callbacks', -> diff --git a/spec/dualsync_spec.js b/spec/dualsync_spec.js index d06d81f..9e8b6d4 100644 --- a/spec/dualsync_spec.js +++ b/spec/dualsync_spec.js @@ -28,8 +28,11 @@ spyOnLocalsync = function() { spyOn(window, 'localsync').andCallFake(function(method, model, options) { if (!options.ignoreCallbacks) { - return typeof options.success === "function" ? options.success() : void 0; + if (typeof options.success === "function") { + options.success(); + } } + return $.Deferred().resolve(); }); return localsync = window.localsync; }; diff --git a/spec/localstorage_store_spec.coffee b/spec/localstorage_store_spec.coffee index 5aeec5a..37d023d 100644 --- a/spec/localstorage_store_spec.coffee +++ b/spec/localstorage_store_spec.coffee @@ -16,78 +16,87 @@ describe 'window.Store', -> describe 'persistence', -> describe 'find', -> it 'fetches records by id', -> - expect(store.find(id: 3)).toEqual id: '3', color: 'burgundy' + store.find(id: 3).done (result) -> + expect(result).toEqual id: '3', color: 'burgundy' # JSON.parse(null) causes error on Android 2.x devices, so it should be avoided it 'does not try to JSON.parse null values', -> spyOn JSON, 'parse' - store.find id: 'unpersistedId' - expect(JSON.parse).not.toHaveBeenCalledWith(null) + store.find(id: 'unpersistedId').done -> + expect(JSON.parse).not.toHaveBeenCalledWith(null) it 'returns null when not found', -> - result = store.find(id: 'unpersistedId') - expect(result).toBeNull() + store.find(id: 'unpersistedId').done (result) -> + expect(result).toBeNull() it 'fetches all records with findAll', -> - expect(store.findAll()).toEqual [id: '3', color: 'burgundy'] + store.findAll().done (result) -> + expect(result).toEqual [id: '3', color: 'burgundy'] it 'clears out its records', -> - store.clear() - expect(localStorage.getItem 'cats').toBe '' - expect(localStorage.getItem 'cats3').toBeNull() + store.clear().done -> + expect(localStorage.getItem 'cats').toBe '' + expect(localStorage.getItem 'cats3').toBeNull() it 'creates records', -> model = id: 2, color: 'blue' - store.create model - expect(localStorage.getItem 'cats').toBe '3,2' - expect(JSON.parse(localStorage.getItem 'cats2')).toEqual id: 2, color: 'blue' + store.create(model).done -> + expect(localStorage.getItem 'cats').toBe '3,2' + expect(JSON.parse(localStorage.getItem 'cats2')).toEqual id: 2, color: 'blue' it 'overwrites existing records with the same id on create', -> model = id: 3, color: 'lavender' - store.create model - expect(JSON.parse(localStorage.getItem 'cats3')).toEqual id: 3, color: 'lavender' + store.create(model).done -> + expect(JSON.parse(localStorage.getItem 'cats3')).toEqual id: 3, color: 'lavender' it 'generates an id when creating records with no id', -> localStorage.clear() store = new Store 'cats' model = color: 'calico', idAttribute: 'id', set: (attribute, value) -> this[attribute] = value - store.create model - expect(model.id).not.toBeNull() - expect(localStorage.getItem('cats')).toBe model.id + store.create(model).done -> + expect(model.id).not.toBeNull() + expect(localStorage.getItem('cats')).toBe model.id it 'updates records', -> - store.update id: 3, color: 'green' - expect(JSON.parse(localStorage.getItem 'cats3')).toEqual id: 3, color: 'green' + store.update(id: 3, color: 'green').done -> + expect(JSON.parse(localStorage.getItem 'cats3')).toEqual id: 3, color: 'green' it 'destroys records', -> - store.destroy id: 3 - expect(localStorage.getItem 'cats').toBe '' - expect(localStorage.getItem 'cats3').toBeNull() + store.destroy(id: 3).done -> + expect(localStorage.getItem 'cats').toBe '' + expect(localStorage.getItem 'cats3').toBeNull() describe 'offline', -> it 'on a clean slate, hasDirtyOrDestroyed returns false', -> - expect(store.hasDirtyOrDestroyed()).toBeFalsy() + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeFalsy() it 'marks records dirty and clean, and reports if it hasDirtyOrDestroyed records', -> - store.dirty id: 3 - expect(store.hasDirtyOrDestroyed()).toBeTruthy() - store.clean id: 3, 'dirty' - expect(store.hasDirtyOrDestroyed()).toBeFalsy() + store.dirty(id: 3).done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeTruthy() + store.clean(id: 3, 'dirty').done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeFalsy() it 'marks records destroyed and clean from destruction, and reports if it hasDirtyOrDestroyed records', -> - store.destroyed id: 3 - expect(store.hasDirtyOrDestroyed()).toBeTruthy() - store.clean id: 3, 'destroyed' - expect(store.hasDirtyOrDestroyed()).toBeFalsy() + store.destroyed(id: 3).done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeTruthy() + store.clean(id: 3, 'destroyed').done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeFalsy() it 'cleans the list of dirty or destroyed models out of localStorage after saving or destroying', -> collection = new Backbone.Collection [{id: 2, color: 'auburn'}, {id: 3, color: 'burgundy'}] collection.url = 'cats' - store.dirty id: 2 - store.destroyed id: 3 - expect(store.hasDirtyOrDestroyed()).toBeTruthy() - collection.get(2).save() - collection.get(3).destroy() - expect(store.hasDirtyOrDestroyed()).toBeFalsy() - expect(localStorage.getItem('cats_dirty').length).toBe 0 - expect(localStorage.getItem('cats_destroyed').length).toBe 0 + store.dirty(id: 2).done -> + store.destroyed(id: 3).done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeTruthy() + collection.get(2).save().done -> + collection.get(3).destroy().done -> + store.hasDirtyOrDestroyed().done (result) -> + expect(result).toBeFalsy() + expect(localStorage.getItem('cats_dirty').length).toBe 0 + expect(localStorage.getItem('cats_destroyed').length).toBe 0 diff --git a/spec/localstorage_store_spec.js b/spec/localstorage_store_spec.js index e1d512c..061afc2 100644 --- a/spec/localstorage_store_spec.js +++ b/spec/localstorage_store_spec.js @@ -22,51 +22,58 @@ describe('persistence', function() { describe('find', function() { it('fetches records by id', function() { - return expect(store.find({ + return store.find({ id: 3 - })).toEqual({ - id: '3', - color: 'burgundy' + }).done(function(result) { + return expect(result).toEqual({ + id: '3', + color: 'burgundy' + }); }); }); it('does not try to JSON.parse null values', function() { spyOn(JSON, 'parse'); - store.find({ + return store.find({ id: 'unpersistedId' + }).done(function() { + return expect(JSON.parse).not.toHaveBeenCalledWith(null); }); - return expect(JSON.parse).not.toHaveBeenCalledWith(null); }); return it('returns null when not found', function() { - var result; - result = store.find({ + return store.find({ id: 'unpersistedId' + }).done(function(result) { + return expect(result).toBeNull(); }); - return expect(result).toBeNull(); }); }); it('fetches all records with findAll', function() { - return expect(store.findAll()).toEqual([ - { - id: '3', - color: 'burgundy' - } - ]); + return store.findAll().done(function(result) { + return expect(result).toEqual([ + { + id: '3', + color: 'burgundy' + } + ]); + }); }); it('clears out its records', function() { - store.clear(); - expect(localStorage.getItem('cats')).toBe(''); - return expect(localStorage.getItem('cats3')).toBeNull(); + return store.clear().done(function() { + expect(localStorage.getItem('cats')).toBe(''); + return expect(localStorage.getItem('cats3')).toBeNull(); + }); }); it('creates records', function() { model = { id: 2, color: 'blue' }; - store.create(model); - expect(localStorage.getItem('cats')).toBe('3,2'); - return expect(JSON.parse(localStorage.getItem('cats2'))).toEqual({ - id: 2, - color: 'blue' + return store.create(model).done(function() { + expect(localStorage.getItem('cats')).toBe('3,2'); + return expect(JSON.parse(localStorage.getItem('cats2'))).toEqual({ + id: 2, + color: 'blue' + }); }); }); it('overwrites existing records with the same id on create', function() { @@ -74,10 +81,11 @@ id: 3, color: 'lavender' }; - store.create(model); - return expect(JSON.parse(localStorage.getItem('cats3'))).toEqual({ - id: 3, - color: 'lavender' + return store.create(model).done(function() { + return expect(JSON.parse(localStorage.getItem('cats3'))).toEqual({ + id: 3, + color: 'lavender' + }); }); }); it('generates an id when creating records with no id', function() { @@ -90,51 +98,68 @@ return this[attribute] = value; } }; - store.create(model); - expect(model.id).not.toBeNull(); - return expect(localStorage.getItem('cats')).toBe(model.id); + return store.create(model).done(function() { + expect(model.id).not.toBeNull(); + return expect(localStorage.getItem('cats')).toBe(model.id); + }); }); it('updates records', function() { - store.update({ - id: 3, - color: 'green' - }); - return expect(JSON.parse(localStorage.getItem('cats3'))).toEqual({ + return store.update({ id: 3, color: 'green' + }).done(function() { + return expect(JSON.parse(localStorage.getItem('cats3'))).toEqual({ + id: 3, + color: 'green' + }); }); }); return it('destroys records', function() { - store.destroy({ + return store.destroy({ id: 3 + }).done(function() { + expect(localStorage.getItem('cats')).toBe(''); + return expect(localStorage.getItem('cats3')).toBeNull(); }); - expect(localStorage.getItem('cats')).toBe(''); - return expect(localStorage.getItem('cats3')).toBeNull(); }); }); return describe('offline', function() { it('on a clean slate, hasDirtyOrDestroyed returns false', function() { - return expect(store.hasDirtyOrDestroyed()).toBeFalsy(); + return store.hasDirtyOrDestroyed().done(function(result) { + return expect(result).toBeFalsy(); + }); }); it('marks records dirty and clean, and reports if it hasDirtyOrDestroyed records', function() { - store.dirty({ + return store.dirty({ id: 3 + }).done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + expect(result).toBeTruthy(); + return store.clean({ + id: 3 + }, 'dirty').done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + return expect(result).toBeFalsy(); + }); + }); + }); }); - expect(store.hasDirtyOrDestroyed()).toBeTruthy(); - store.clean({ - id: 3 - }, 'dirty'); - return expect(store.hasDirtyOrDestroyed()).toBeFalsy(); }); it('marks records destroyed and clean from destruction, and reports if it hasDirtyOrDestroyed records', function() { - store.destroyed({ + return store.destroyed({ id: 3 + }).done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + expect(result).toBeTruthy(); + return store.clean({ + id: 3 + }, 'destroyed').done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + return expect(result).toBeFalsy(); + }); + }); + }); }); - expect(store.hasDirtyOrDestroyed()).toBeTruthy(); - store.clean({ - id: 3 - }, 'destroyed'); - return expect(store.hasDirtyOrDestroyed()).toBeFalsy(); }); return it('cleans the list of dirty or destroyed models out of localStorage after saving or destroying', function() { var collection; @@ -148,18 +173,26 @@ } ]); collection.url = 'cats'; - store.dirty({ + return store.dirty({ id: 2 + }).done(function() { + return store.destroyed({ + id: 3 + }).done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + expect(result).toBeTruthy(); + return collection.get(2).save().done(function() { + return collection.get(3).destroy().done(function() { + return store.hasDirtyOrDestroyed().done(function(result) { + expect(result).toBeFalsy(); + expect(localStorage.getItem('cats_dirty').length).toBe(0); + return expect(localStorage.getItem('cats_destroyed').length).toBe(0); + }); + }); + }); + }); + }); }); - store.destroyed({ - id: 3 - }); - expect(store.hasDirtyOrDestroyed()).toBeTruthy(); - collection.get(2).save(); - collection.get(3).destroy(); - expect(store.hasDirtyOrDestroyed()).toBeFalsy(); - expect(localStorage.getItem('cats_dirty').length).toBe(0); - return expect(localStorage.getItem('cats_destroyed').length).toBe(0); }); }); }); diff --git a/spec/localsync_spec.coffee b/spec/localsync_spec.coffee index aaad3bf..5a98f0a 100644 --- a/spec/localsync_spec.coffee +++ b/spec/localsync_spec.coffee @@ -6,8 +6,8 @@ describe 'localsync', -> it 'creates records', -> {ready, create, model} = {} runs -> - create = spyOn(Store.prototype, 'create') model = new Backbone.Model id: 1 + create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve(model) localsync 'create', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -17,8 +17,8 @@ describe 'localsync', -> {ready, create} = {} runs -> ready = false - create = spyOn(Store.prototype, 'find').andReturn id: 1 - create = spyOn(Store.prototype, 'create') + create = spyOn(Store.prototype, 'find').andReturn $.Deferred().resolve(id: 1) + create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve() model = new Backbone.Model id: 1 localsync 'create', model, {success: (-> ready = true), error: (-> ready = true), add: true} waitsFor (-> ready), "A callback should have been called", 100 @@ -35,8 +35,8 @@ describe 'localsync', -> {ready, create, model, dirty} = {} runs -> model = new Backbone.Model id: 1 - create = spyOn(Store.prototype, 'create').andReturn model - dirty = spyOn(Store.prototype, 'dirty') + create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve(model) + dirty = spyOn(Store.prototype, 'dirty').andReturn $.Deferred().resolve(model) localsync 'create', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -47,8 +47,8 @@ describe 'localsync', -> it 'reads models', -> {ready, find, model} = {} runs -> - find = spyOn(Store.prototype, 'find') model = new Backbone.Model id: 1 + find = spyOn(Store.prototype, 'find').andReturn $.Deferred().resolve(model) localsync 'read', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -57,7 +57,7 @@ describe 'localsync', -> it 'reads collections', -> {ready, findAll} = {} runs -> - findAll = spyOn(Store.prototype, 'findAll') + findAll = spyOn(Store.prototype, 'findAll').andReturn $.Deferred().resolve() localsync 'read', new Backbone.Collection, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -67,8 +67,8 @@ describe 'localsync', -> it 'updates records', -> {ready, update, model} = {} runs -> - update = spyOn(Store.prototype, 'update') model = new Backbone.Model id: 1 + update = spyOn(Store.prototype, 'update').andReturn $.Deferred().resolve(model) localsync 'update', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -78,8 +78,8 @@ describe 'localsync', -> {ready, update, model, dirty} = {} runs -> model = new Backbone.Model id: 1 - update = spyOn(Store.prototype, 'update') - dirty = spyOn(Store.prototype, 'dirty') + update = spyOn(Store.prototype, 'update').andReturn $.Deferred().resolve(model) + dirty = spyOn(Store.prototype, 'dirty').andReturn $.Deferred().resolve(model) localsync 'update', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -90,8 +90,8 @@ describe 'localsync', -> it 'deletes records', -> {ready, destroy, model} = {} runs -> - destroy = spyOn(Store.prototype, 'destroy') model = new Backbone.Model id: 1 + destroy = spyOn(Store.prototype, 'destroy').andReturn $.Deferred().resolve(model) localsync 'delete', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -101,8 +101,8 @@ describe 'localsync', -> {ready, destroy, destroyed, model} = {} runs -> model = new Backbone.Model id: 1 - destroy = spyOn(Store.prototype, 'destroy') - destroyed = spyOn(Store.prototype, 'destroyed') + destroy = spyOn(Store.prototype, 'destroy').andReturn $.Deferred().resolve(model) + destroyed = spyOn(Store.prototype, 'destroyed').andReturn $.Deferred().resolve(model) localsync 'delete', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> @@ -112,12 +112,12 @@ describe 'localsync', -> describe 'extra methods', -> it 'clears out all records from the store', -> runs -> - clear = spyOn(Store.prototype, 'clear') + clear = spyOn(Store.prototype, 'clear').andReturn $.Deferred().resolve() localsync 'clear', {}, {success: (-> ready = true), error: (-> ready = true)} it 'reports whether or not it hasDirtyOrDestroyed', -> runs -> - clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed') + clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed').andReturn $.Deferred().resolve() localsync 'hasDirtyOrDestroyed', {}, {success: (-> ready = true), error: (-> ready = true)} describe 'callbacks', -> @@ -146,7 +146,7 @@ describe 'localsync', -> describe 'model parameter', -> beforeEach -> - spyOn(Store.prototype, 'create') + spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve() it 'should not accept objects / attributes as model', -> attributes = {} diff --git a/spec/localsync_spec.js b/spec/localsync_spec.js index 25bcf17..3c99619 100644 --- a/spec/localsync_spec.js +++ b/spec/localsync_spec.js @@ -11,10 +11,10 @@ var create, model, ready, _ref; _ref = {}, ready = _ref.ready, create = _ref.create, model = _ref.model; runs(function() { - create = spyOn(Store.prototype, 'create'); model = new Backbone.Model({ id: 1 }); + create = spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve(model)); return localsync('create', model, { success: (function() { return ready = true; @@ -37,10 +37,10 @@ runs(function() { var model; ready = false; - create = spyOn(Store.prototype, 'find').andReturn({ + create = spyOn(Store.prototype, 'find').andReturn($.Deferred().resolve({ id: 1 - }); - create = spyOn(Store.prototype, 'create'); + })); + create = spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve()); model = new Backbone.Model({ id: 1 }); @@ -89,8 +89,8 @@ model = new Backbone.Model({ id: 1 }); - create = spyOn(Store.prototype, 'create').andReturn(model); - dirty = spyOn(Store.prototype, 'dirty'); + create = spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve(model)); + dirty = spyOn(Store.prototype, 'dirty').andReturn($.Deferred().resolve(model)); return localsync('create', model, { success: (function() { return ready = true; @@ -115,10 +115,10 @@ var find, model, ready, _ref; _ref = {}, ready = _ref.ready, find = _ref.find, model = _ref.model; runs(function() { - find = spyOn(Store.prototype, 'find'); model = new Backbone.Model({ id: 1 }); + find = spyOn(Store.prototype, 'find').andReturn($.Deferred().resolve(model)); return localsync('read', model, { success: (function() { return ready = true; @@ -139,7 +139,7 @@ var findAll, ready, _ref; _ref = {}, ready = _ref.ready, findAll = _ref.findAll; runs(function() { - findAll = spyOn(Store.prototype, 'findAll'); + findAll = spyOn(Store.prototype, 'findAll').andReturn($.Deferred().resolve()); return localsync('read', new Backbone.Collection, { success: (function() { return ready = true; @@ -162,10 +162,10 @@ var model, ready, update, _ref; _ref = {}, ready = _ref.ready, update = _ref.update, model = _ref.model; runs(function() { - update = spyOn(Store.prototype, 'update'); model = new Backbone.Model({ id: 1 }); + update = spyOn(Store.prototype, 'update').andReturn($.Deferred().resolve(model)); return localsync('update', model, { success: (function() { return ready = true; @@ -189,8 +189,8 @@ model = new Backbone.Model({ id: 1 }); - update = spyOn(Store.prototype, 'update'); - dirty = spyOn(Store.prototype, 'dirty'); + update = spyOn(Store.prototype, 'update').andReturn($.Deferred().resolve(model)); + dirty = spyOn(Store.prototype, 'dirty').andReturn($.Deferred().resolve(model)); return localsync('update', model, { success: (function() { return ready = true; @@ -215,10 +215,10 @@ var destroy, model, ready, _ref; _ref = {}, ready = _ref.ready, destroy = _ref.destroy, model = _ref.model; runs(function() { - destroy = spyOn(Store.prototype, 'destroy'); model = new Backbone.Model({ id: 1 }); + destroy = spyOn(Store.prototype, 'destroy').andReturn($.Deferred().resolve(model)); return localsync('delete', model, { success: (function() { return ready = true; @@ -242,8 +242,8 @@ model = new Backbone.Model({ id: 1 }); - destroy = spyOn(Store.prototype, 'destroy'); - destroyed = spyOn(Store.prototype, 'destroyed'); + destroy = spyOn(Store.prototype, 'destroy').andReturn($.Deferred().resolve(model)); + destroyed = spyOn(Store.prototype, 'destroyed').andReturn($.Deferred().resolve(model)); return localsync('delete', model, { success: (function() { return ready = true; @@ -268,7 +268,7 @@ it('clears out all records from the store', function() { return runs(function() { var clear; - clear = spyOn(Store.prototype, 'clear'); + clear = spyOn(Store.prototype, 'clear').andReturn($.Deferred().resolve()); return localsync('clear', {}, { success: (function() { var ready; @@ -284,7 +284,7 @@ return it('reports whether or not it hasDirtyOrDestroyed', function() { return runs(function() { var clear; - clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed'); + clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed').andReturn($.Deferred().resolve()); return localsync('hasDirtyOrDestroyed', {}, { success: (function() { var ready; @@ -358,7 +358,7 @@ }); return describe('model parameter', function() { beforeEach(function() { - return spyOn(Store.prototype, 'create'); + return spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve()); }); it('should not accept objects / attributes as model', function() { var attributes, call; From a6020a2d9e56f846c8b65ceb72668a3112c5f18a Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Mon, 13 Jan 2014 05:41:55 -0300 Subject: [PATCH 2/8] Fixed tests (bug in spyOnLocalsync) --- spec/dualsync_spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/dualsync_spec.coffee b/spec/dualsync_spec.coffee index e503315..1dfa2fe 100644 --- a/spec/dualsync_spec.coffee +++ b/spec/dualsync_spec.coffee @@ -19,7 +19,7 @@ spyOnLocalsync = -> spyOn(window, 'localsync') .andCallFake (method, model, options) -> options.success?() unless options.ignoreCallbacks - $.Deferred().resolve() + $.Deferred().resolve(); localsync = window.localsync describe 'delegating to localsync and backboneSync, and calling the model callbacks', -> From 8cb57a5574f7f284dcfa18e04e2b7d400a3f5a95 Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Tue, 14 Jan 2014 12:06:10 -0300 Subject: [PATCH 3/8] Refactored the use of LocalStorage to an async StorageAdapter. --- adapters.js | 82 +++++++++++++++ backbone.dualstorage.amd.js | 154 +++++++++++++++------------- backbone.dualstorage.coffee | 99 +++++++++--------- backbone.dualstorage.js | 154 +++++++++++++++------------- spec/backbone.dualstorage.js | 154 +++++++++++++++------------- spec/dualsync_spec.coffee | 1 + spec/dualsync_spec.js | 1 + spec/localstorage_store_spec.coffee | 2 + spec/localstorage_store_spec.js | 4 +- 9 files changed, 393 insertions(+), 258 deletions(-) create mode 100644 adapters.js diff --git a/adapters.js b/adapters.js new file mode 100644 index 0000000..0a41254 --- /dev/null +++ b/adapters.js @@ -0,0 +1,82 @@ +var StickyStorageAdapter = (function() { + function StickyStorageAdapter() {} + + StickyStorageAdapter.prototype.initialize = function() { + var promise = $.Deferred(); + this.store = new StickyStore({ + name: 'Backbone.dualStorage', + adapters: ['indexedDB', 'webSQL', 'localStorage'], + ready: function () { + promise.resolve(); + } + }); + return promise; + }; + + StickyStorageAdapter.prototype.setItem = function(key, value) { + var promise = $.Deferred(); + this.store.set(key, value, function (storedValue) { + promise.resolve(storedValue); + }); + return promise; + }; + + StickyStorageAdapter.prototype.getItem = function(key) { + var promise = $.Deferred(); + this.store.get(key, function (storedValue) { + promise.resolve(storedValue); + }); + return promise; + }; + + StickyStorageAdapter.prototype.removeItem = function(key) { + var promise = $.Deferred(); + this.store.remove(key, function () { + promise.resolve(); + }); + return promise; + }; + + return StickyStorageAdapter; +})(); + +var LawnchairAdapter = (function() { + function LawnchairAdapter() {} + + LawnchairAdapter.prototype.initialize = function() { + var promise = $.Deferred(); + this.store = new Lawnchair({ + name: 'Backbone.dualStorage', + adapter: ['indexed-db', 'webkit-sqlite', 'dom'] + }, function () { + promise.resolve(); + }); + return promise; + }; + + LawnchairAdapter.prototype.setItem = function(key, value) { + var promise = $.Deferred(); + this.store.save({key: value}, function () { + promise.resolve(value); + }); + return promise; + }; + + LawnchairAdapter.prototype.getItem = function(key) { + var promise = $.Deferred(); + this.store.get(key, function (storedValue) { + promise.resolve(storedValue); + }); + return promise; + }; + + LawnchairAdapter.prototype.removeItem = function(key) { + var promise = $.Deferred(); + this.store.remove(key, function () { + promise.resolve(); + }); + return promise; + }; + + return LawnchairAdapter; +})(); \ No newline at end of file diff --git a/backbone.dualstorage.amd.js b/backbone.dualstorage.amd.js index 05323a6..197f39d 100644 --- a/backbone.dualstorage.amd.js +++ b/backbone.dualstorage.amd.js @@ -18,33 +18,39 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple as that.co */ -var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, +var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; -LocaleStorageAdapter = (function() { - function LocaleStorageAdapter() {} +LocalStorageAdapter = (function() { + function LocalStorageAdapter() {} - LocaleStorageAdapter.prototype.setItem = function(key, value) { + LocalStorageAdapter.prototype.initialize = function() { + return $.Deferred().resolve; + }; + + LocalStorageAdapter.prototype.setItem = function(key, value) { localStorage.setItem(key, value); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.getItem = function(key) { + LocalStorageAdapter.prototype.getItem = function(key) { var value; value = localStorage.getItem(key); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.removeItem = function(key) { + LocalStorageAdapter.prototype.removeItem = function(key) { localStorage.removeItem(key); return $.Deferred().resolve(); }; - return LocaleStorageAdapter; + return LocalStorageAdapter; })(); -Backbone.storageAdapter = new LocaleStorageAdapter; +Backbone.storageAdapter = new LocalStorageAdapter; + +Backbone.storageAdapter.initialize(); Backbone.Collection.prototype.syncDirty = function() { var storeName, @@ -127,13 +133,17 @@ window.Store = (function() { Store.prototype.sep = ''; function Store(name) { - var _this = this; this.name = name; - this.recordsOn(this.name).done(function(result) { - return _this.records = result; - }); + this.records = []; } + Store.prototype.initialize = function() { + var _this = this; + return this.recordsOn(this.name).done(function(result) { + return _this.records = result || []; + }); + }; + Store.prototype.generateId = function() { return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); }; @@ -313,7 +323,8 @@ callbackTranslator = { }; localsync = function(method, model, options) { - var isValidModel, promise, store; + var isValidModel, store, + _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -321,68 +332,71 @@ localsync = function(method, model, options) { throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - promise = (function() { - switch (method) { - case 'read': - if (model.id) { - return store.find(model); - } else { - return store.findAll(); - } - break; - case 'hasDirtyOrDestroyed': - return store.hasDirtyOrDestroyed(); - case 'clear': - return store.clear(); - case 'create': - return store.find(model).then(function(preExisting) { - if (!(options.add && !options.merge && preExisting)) { - return store.create(model).then(function(model) { - if (options.dirty) { - return store.dirty(model).then(function() { - return model; - }); - } - return model; - }); - } else { - return preExisting; - } - }); - case 'update': - return store.update(model).then(function(model) { - if (options.dirty) { - return store.dirty(model); + return store.initialize().then(function() { + var promise; + promise = (function() { + switch (method) { + case 'read': + if (model.id) { + return store.find(model); } else { - return store.clean(model, 'dirty'); + return store.findAll(); } - }); - case 'delete': - return store.destroy(model).then(function() { - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + break; + case 'hasDirtyOrDestroyed': + return store.hasDirtyOrDestroyed(); + case 'clear': + return store.clear(); + case 'create': + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; + } + }); + case 'update': + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } - }); - } - })(); - return promise.then(function(response) { - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + }); } - } - return response; + })(); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; + } + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }); }; @@ -484,7 +498,7 @@ dualsync = function(method, model, options) { }); } }; - if (options.add) { + if (!options.add) { return localsync('clear', model, options).then(go); } else { return go(); diff --git a/backbone.dualstorage.coffee b/backbone.dualstorage.coffee index e92641d..ecb347b 100644 --- a/backbone.dualstorage.coffee +++ b/backbone.dualstorage.coffee @@ -6,7 +6,10 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple as that.co ### -class LocaleStorageAdapter +class LocalStorageAdapter + initialize: -> + $.Deferred().resolve + setItem: (key, value) -> localStorage.setItem key, value $.Deferred().resolve value @@ -19,7 +22,8 @@ class LocaleStorageAdapter localStorage.removeItem key $.Deferred().resolve() -Backbone.storageAdapter = new LocaleStorageAdapter +Backbone.storageAdapter = new LocalStorageAdapter +Backbone.storageAdapter.initialize() # Make it easy for collections to sync dirty and destroyed records # Simply call collection.syncDirtyAndDestroyed() @@ -52,8 +56,11 @@ class window.Store constructor: (name) -> @name = name + @records = [] + + initialize: -> @recordsOn(@name).done (result) => - @records = result + @records = result || [] # Generates an unique id to use when saving new instances into localstorage # by default generates a pseudo-GUID by concatenating random hexadecimal. @@ -133,9 +140,6 @@ class window.Store destroy: (model) -> Backbone.storageAdapter.removeItem(@name + @sep + model.id).then => @records = _.without @records, model.id.toString() - # @records = _.reject(@records, (record_id) -> - # record_id is model.id.toString() - # ) @save().then -> model callbackTranslator = @@ -164,53 +168,54 @@ localsync = (method, model, options) -> throw new Error 'model parameter is required to be a backbone model or collection.' store = new Store options.storeName + store.initialize().then => - promise = switch method - when 'read' - if model.id - store.find(model) - else - store.findAll() - when 'hasDirtyOrDestroyed' - store.hasDirtyOrDestroyed() - when 'clear' - store.clear() - when 'create' - store.find(model).then (preExisting) -> - unless options.add and not options.merge and preExisting - store.create(model).then (model) -> - if options.dirty - return store.dirty(model).then -> - model - model - else - preExisting - when 'update' - store.update(model).then (model) -> - if options.dirty - store.dirty(model) + promise = switch method + when 'read' + if model.id + store.find(model) else - store.clean(model, 'dirty') - when 'delete' - store.destroy(model).then -> - if options.dirty - store.destroyed(model) - else - if model.id.toString().length == 36 + store.findAll() + when 'hasDirtyOrDestroyed' + store.hasDirtyOrDestroyed() + when 'clear' + store.clear() + when 'create' + store.find(model).then (preExisting) -> + unless options.add and not options.merge and preExisting + store.create(model).then (model) -> + if options.dirty + return store.dirty(model).then -> + model + model + else + preExisting + when 'update' + store.update(model).then (model) -> + if options.dirty + store.dirty(model) + else store.clean(model, 'dirty') + when 'delete' + store.destroy(model).then -> + if options.dirty + store.destroyed(model) else - store.clean(model, 'destroyed') + if model.id.toString().length == 36 + store.clean(model, 'dirty') + else + store.clean(model, 'destroyed') - promise.then (response) -> - response = response.attributes if response?.attributes + promise.then (response) -> + response = response.attributes if response?.attributes - unless options.ignoreCallbacks - if response - options.success response - else - options.error 'Record not found' + unless options.ignoreCallbacks + if response + options.success response + else + options.error 'Record not found' - response + response # If the value of the named property is a function then invoke it; # otherwise, return it. @@ -287,7 +292,7 @@ dualsync = (method, model, options) -> localsync('create', responseModel, options).then -> success(resp, status, xhr) - if options.add + if not options.add localsync('clear', model, options).then go else go() diff --git a/backbone.dualstorage.js b/backbone.dualstorage.js index a9cd9d0..77eba39 100644 --- a/backbone.dualstorage.js +++ b/backbone.dualstorage.js @@ -9,33 +9,39 @@ as that.co (function() { - var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, + var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; - LocaleStorageAdapter = (function() { - function LocaleStorageAdapter() {} + LocalStorageAdapter = (function() { + function LocalStorageAdapter() {} - LocaleStorageAdapter.prototype.setItem = function(key, value) { + LocalStorageAdapter.prototype.initialize = function() { + return $.Deferred().resolve; + }; + + LocalStorageAdapter.prototype.setItem = function(key, value) { localStorage.setItem(key, value); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.getItem = function(key) { + LocalStorageAdapter.prototype.getItem = function(key) { var value; value = localStorage.getItem(key); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.removeItem = function(key) { + LocalStorageAdapter.prototype.removeItem = function(key) { localStorage.removeItem(key); return $.Deferred().resolve(); }; - return LocaleStorageAdapter; + return LocalStorageAdapter; })(); - Backbone.storageAdapter = new LocaleStorageAdapter; + Backbone.storageAdapter = new LocalStorageAdapter; + + Backbone.storageAdapter.initialize(); Backbone.Collection.prototype.syncDirty = function() { var storeName, @@ -118,13 +124,17 @@ as that.co Store.prototype.sep = ''; function Store(name) { - var _this = this; this.name = name; - this.recordsOn(this.name).done(function(result) { - return _this.records = result; - }); + this.records = []; } + Store.prototype.initialize = function() { + var _this = this; + return this.recordsOn(this.name).done(function(result) { + return _this.records = result || []; + }); + }; + Store.prototype.generateId = function() { return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); }; @@ -304,7 +314,8 @@ as that.co }; localsync = function(method, model, options) { - var isValidModel, promise, store; + var isValidModel, store, + _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -312,68 +323,71 @@ as that.co throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - promise = (function() { - switch (method) { - case 'read': - if (model.id) { - return store.find(model); - } else { - return store.findAll(); - } - break; - case 'hasDirtyOrDestroyed': - return store.hasDirtyOrDestroyed(); - case 'clear': - return store.clear(); - case 'create': - return store.find(model).then(function(preExisting) { - if (!(options.add && !options.merge && preExisting)) { - return store.create(model).then(function(model) { - if (options.dirty) { - return store.dirty(model).then(function() { - return model; - }); - } - return model; - }); - } else { - return preExisting; - } - }); - case 'update': - return store.update(model).then(function(model) { - if (options.dirty) { - return store.dirty(model); + return store.initialize().then(function() { + var promise; + promise = (function() { + switch (method) { + case 'read': + if (model.id) { + return store.find(model); } else { - return store.clean(model, 'dirty'); + return store.findAll(); } - }); - case 'delete': - return store.destroy(model).then(function() { - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + break; + case 'hasDirtyOrDestroyed': + return store.hasDirtyOrDestroyed(); + case 'clear': + return store.clear(); + case 'create': + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; + } + }); + case 'update': + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } - }); - } - })(); - return promise.then(function(response) { - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + }); } - } - return response; + })(); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; + } + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }); }; @@ -475,7 +489,7 @@ as that.co }); } }; - if (options.add) { + if (!options.add) { return localsync('clear', model, options).then(go); } else { return go(); diff --git a/spec/backbone.dualstorage.js b/spec/backbone.dualstorage.js index a70e145..9351d8d 100644 --- a/spec/backbone.dualstorage.js +++ b/spec/backbone.dualstorage.js @@ -7,33 +7,39 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple as that.co */ -var LocaleStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, +var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; -LocaleStorageAdapter = (function() { - function LocaleStorageAdapter() {} +LocalStorageAdapter = (function() { + function LocalStorageAdapter() {} - LocaleStorageAdapter.prototype.setItem = function(key, value) { + LocalStorageAdapter.prototype.initialize = function() { + return $.Deferred().resolve; + }; + + LocalStorageAdapter.prototype.setItem = function(key, value) { localStorage.setItem(key, value); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.getItem = function(key) { + LocalStorageAdapter.prototype.getItem = function(key) { var value; value = localStorage.getItem(key); return $.Deferred().resolve(value); }; - LocaleStorageAdapter.prototype.removeItem = function(key) { + LocalStorageAdapter.prototype.removeItem = function(key) { localStorage.removeItem(key); return $.Deferred().resolve(); }; - return LocaleStorageAdapter; + return LocalStorageAdapter; })(); -Backbone.storageAdapter = new LocaleStorageAdapter; +Backbone.storageAdapter = new LocalStorageAdapter; + +Backbone.storageAdapter.initialize(); Backbone.Collection.prototype.syncDirty = function() { var storeName, @@ -116,13 +122,17 @@ window.Store = (function() { Store.prototype.sep = ''; function Store(name) { - var _this = this; this.name = name; - this.recordsOn(this.name).done(function(result) { - return _this.records = result; - }); + this.records = []; } + Store.prototype.initialize = function() { + var _this = this; + return this.recordsOn(this.name).done(function(result) { + return _this.records = result || []; + }); + }; + Store.prototype.generateId = function() { return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); }; @@ -302,7 +312,8 @@ callbackTranslator = { }; localsync = function(method, model, options) { - var isValidModel, promise, store; + var isValidModel, store, + _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); isValidModel || (isValidModel = model instanceof Backbone.Model); isValidModel || (isValidModel = model instanceof Backbone.Collection); @@ -310,68 +321,71 @@ localsync = function(method, model, options) { throw new Error('model parameter is required to be a backbone model or collection.'); } store = new Store(options.storeName); - promise = (function() { - switch (method) { - case 'read': - if (model.id) { - return store.find(model); - } else { - return store.findAll(); - } - break; - case 'hasDirtyOrDestroyed': - return store.hasDirtyOrDestroyed(); - case 'clear': - return store.clear(); - case 'create': - return store.find(model).then(function(preExisting) { - if (!(options.add && !options.merge && preExisting)) { - return store.create(model).then(function(model) { - if (options.dirty) { - return store.dirty(model).then(function() { - return model; - }); - } - return model; - }); - } else { - return preExisting; - } - }); - case 'update': - return store.update(model).then(function(model) { - if (options.dirty) { - return store.dirty(model); + return store.initialize().then(function() { + var promise; + promise = (function() { + switch (method) { + case 'read': + if (model.id) { + return store.find(model); } else { - return store.clean(model, 'dirty'); + return store.findAll(); } - }); - case 'delete': - return store.destroy(model).then(function() { - if (options.dirty) { - return store.destroyed(model); - } else { - if (model.id.toString().length === 36) { + break; + case 'hasDirtyOrDestroyed': + return store.hasDirtyOrDestroyed(); + case 'clear': + return store.clear(); + case 'create': + return store.find(model).then(function(preExisting) { + if (!(options.add && !options.merge && preExisting)) { + return store.create(model).then(function(model) { + if (options.dirty) { + return store.dirty(model).then(function() { + return model; + }); + } + return model; + }); + } else { + return preExisting; + } + }); + case 'update': + return store.update(model).then(function(model) { + if (options.dirty) { + return store.dirty(model); + } else { return store.clean(model, 'dirty'); + } + }); + case 'delete': + return store.destroy(model).then(function() { + if (options.dirty) { + return store.destroyed(model); } else { - return store.clean(model, 'destroyed'); + if (model.id.toString().length === 36) { + return store.clean(model, 'dirty'); + } else { + return store.clean(model, 'destroyed'); + } } - } - }); - } - })(); - return promise.then(function(response) { - if (response != null ? response.attributes : void 0) { - response = response.attributes; - } - if (!options.ignoreCallbacks) { - if (response) { - options.success(response); - } else { - options.error('Record not found'); + }); } - } - return response; + })(); + return promise.then(function(response) { + if (response != null ? response.attributes : void 0) { + response = response.attributes; + } + if (!options.ignoreCallbacks) { + if (response) { + options.success(response); + } else { + options.error('Record not found'); + } + } + return response; + }); }); }; @@ -473,7 +487,7 @@ dualsync = function(method, model, options) { }); } }; - if (options.add) { + if (!options.add) { return localsync('clear', model, options).then(go); } else { return go(); diff --git a/spec/dualsync_spec.coffee b/spec/dualsync_spec.coffee index 1dfa2fe..1f6d5e7 100644 --- a/spec/dualsync_spec.coffee +++ b/spec/dualsync_spec.coffee @@ -189,6 +189,7 @@ describe 'delegating to localsync and backboneSync, and calling the model callba dualsync('read', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) waitsFor (-> ready), "The success callback should have been called", 100 runs -> + window.x = localsync expect(localsync.calls[2].args[0]).toEqual 'create' expect(localsync.calls[2].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 diff --git a/spec/dualsync_spec.js b/spec/dualsync_spec.js index 9e8b6d4..1c1ab99 100644 --- a/spec/dualsync_spec.js +++ b/spec/dualsync_spec.js @@ -383,6 +383,7 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { + window.x = localsync; expect(localsync.calls[2].args[0]).toEqual('create'); return expect(localsync.calls[2].args[1].attributes).toEqual({ position: 'arm', diff --git a/spec/localstorage_store_spec.coffee b/spec/localstorage_store_spec.coffee index 37d023d..d332e9e 100644 --- a/spec/localstorage_store_spec.coffee +++ b/spec/localstorage_store_spec.coffee @@ -7,6 +7,8 @@ describe 'window.Store', -> localStorage.setItem 'cats', '3' localStorage.setItem 'cats3', '{"id": "3", "color": "burgundy"}' store = new Store 'cats' + store.initialize() + return store describe 'creation', -> it 'takes a name in its constructor', -> diff --git a/spec/localstorage_store_spec.js b/spec/localstorage_store_spec.js index 061afc2..2e13490 100644 --- a/spec/localstorage_store_spec.js +++ b/spec/localstorage_store_spec.js @@ -11,7 +11,9 @@ localStorage.clear(); localStorage.setItem('cats', '3'); localStorage.setItem('cats3', '{"id": "3", "color": "burgundy"}'); - return store = new Store('cats'); + store = new Store('cats'); + store.initialize(); + return store; }); describe('creation', function() { return it('takes a name in its constructor', function() { From def4e287ba6c9f240230b493a72af54a22bb9fe5 Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Wed, 15 Jan 2014 09:20:01 -0300 Subject: [PATCH 4/8] Added some comments and minor code cleanup. --- backbone.dualstorage.amd.js | 6 ++---- backbone.dualstorage.coffee | 14 ++++++++++---- backbone.dualstorage.js | 6 ++---- spec/backbone.dualstorage.js | 6 ++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backbone.dualstorage.amd.js b/backbone.dualstorage.amd.js index 197f39d..762b1f1 100644 --- a/backbone.dualstorage.amd.js +++ b/backbone.dualstorage.amd.js @@ -15,7 +15,7 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that.co +as that. */ var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, @@ -34,9 +34,7 @@ LocalStorageAdapter = (function() { }; LocalStorageAdapter.prototype.getItem = function(key) { - var value; - value = localStorage.getItem(key); - return $.Deferred().resolve(value); + return $.Deferred().resolve(localStorage.getItem(key)); }; LocalStorageAdapter.prototype.removeItem = function(key) { diff --git a/backbone.dualstorage.coffee b/backbone.dualstorage.coffee index ecb347b..2a889bd 100644 --- a/backbone.dualstorage.coffee +++ b/backbone.dualstorage.coffee @@ -3,9 +3,11 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that.co +as that. ### +# Async storage interface. +# Dummy implementation with LocalStorage for reference. class LocalStorageAdapter initialize: -> $.Deferred().resolve @@ -15,14 +17,18 @@ class LocalStorageAdapter $.Deferred().resolve value getItem: (key) -> - value = localStorage.getItem key - $.Deferred().resolve value + $.Deferred().resolve localStorage.getItem key removeItem: (key) -> localStorage.removeItem key $.Deferred().resolve() +# Use LocalStorageAdapter as default adapter. Backbone.storageAdapter = new LocalStorageAdapter + +# LocalStorage is not actually async, so we can call initialize here and +# continue safely. But when using a real async StorageAdapter, you should +# wait for initialize() to resolve before trying to do any sync operation. Backbone.storageAdapter.initialize() # Make it easy for collections to sync dirty and destroyed records @@ -100,7 +106,7 @@ class window.Store # Add a model, giving it a unique GUID, if it doesn't already # have an id of it's own. create: (model) -> - if not _.isObject(model) then return $.Deferred().resolve(model) + if not _.isObject(model) then return $.Deferred().resolve model if not model.id model.id = @generateId() model.set model.idAttribute, model.id diff --git a/backbone.dualstorage.js b/backbone.dualstorage.js index 77eba39..84afd8d 100644 --- a/backbone.dualstorage.js +++ b/backbone.dualstorage.js @@ -4,7 +4,7 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that.co +as that. */ @@ -25,9 +25,7 @@ as that.co }; LocalStorageAdapter.prototype.getItem = function(key) { - var value; - value = localStorage.getItem(key); - return $.Deferred().resolve(value); + return $.Deferred().resolve(localStorage.getItem(key)); }; LocalStorageAdapter.prototype.removeItem = function(key) { diff --git a/spec/backbone.dualstorage.js b/spec/backbone.dualstorage.js index 9351d8d..b5c5cef 100644 --- a/spec/backbone.dualstorage.js +++ b/spec/backbone.dualstorage.js @@ -4,7 +4,7 @@ Backbone dualStorage Adapter v1.1.0 A simple module to replace `Backbone.sync` with *localStorage*-based persistence. Models are given GUIDS, and saved into a JSON object. Simple -as that.co +as that. */ var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, @@ -23,9 +23,7 @@ LocalStorageAdapter = (function() { }; LocalStorageAdapter.prototype.getItem = function(key) { - var value; - value = localStorage.getItem(key); - return $.Deferred().resolve(value); + return $.Deferred().resolve(localStorage.getItem(key)); }; LocalStorageAdapter.prototype.removeItem = function(key) { From a1e41ecc45adbe339f3dd08fb28c9dca939888e0 Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Wed, 15 Jan 2014 09:23:06 -0300 Subject: [PATCH 5/8] Renamed localsync and dualsync to localSync and dualSync (camelCased) for consistency. --- backbone.dualstorage.amd.js | 42 +++++----- backbone.dualstorage.coffee | 40 ++++----- backbone.dualstorage.js | 42 +++++----- spec/backbone.dualstorage.js | 42 +++++----- spec/bugs_spec.coffee | 4 +- spec/bugs_spec.js | 6 +- spec/dualsync_spec.coffee | 152 +++++++++++++++++----------------- spec/dualsync_spec.js | 156 +++++++++++++++++------------------ spec/integration_spec.coffee | 8 +- spec/integration_spec.js | 10 +-- spec/localsync_spec.coffee | 44 +++++----- spec/localsync_spec.js | 46 +++++------ 12 files changed, 296 insertions(+), 296 deletions(-) diff --git a/backbone.dualstorage.amd.js b/backbone.dualstorage.amd.js index 762b1f1..ecb834b 100644 --- a/backbone.dualstorage.amd.js +++ b/backbone.dualstorage.amd.js @@ -18,7 +18,7 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple as that. */ -var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, +var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualSync, localSync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; LocalStorageAdapter = (function() { @@ -320,7 +320,7 @@ callbackTranslator = { } }; -localsync = function(method, model, options) { +localSync = function(method, model, options) { var isValidModel, store, _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); @@ -437,7 +437,7 @@ onlineSync = function(method, model, options) { return backboneSync(method, model, options); }; -dualsync = function(method, model, options) { +dualSync = function(method, model, options) { var error, local, success, temporaryId; options.storeName = result(model.collection, 'storeName') || result(model, 'storeName') || result(model.collection, 'url') || result(model, 'urlRoot') || result(model, 'url'); options.success = callbackTranslator.forDualstorageCaller(options.success, model, options); @@ -448,16 +448,16 @@ dualsync = function(method, model, options) { local = result(model, 'local') || result(model.collection, 'local'); options.dirty = options.remote === false && !local; if (options.remote === false || local) { - return localsync(method, model, options); + return localSync(method, model, options); } options.ignoreCallbacks = true; success = options.success; error = options.error; switch (method) { case 'read': - return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + return localSync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { if (hasDirtyOrDestroyed) { - return success(localsync(method, model, options)); + return success(localSync(method, model, options)); } else { options.success = function(resp, status, xhr) { var go; @@ -483,7 +483,7 @@ dualsync = function(method, model, options) { _results = []; for (_j = 0, _len1 = models.length; _j < _len1; _j++) { m = models[_j]; - _results.push(localsync('create', m, options)); + _results.push(localSync('create', m, options)); } return _results; })())).then(function() { @@ -491,19 +491,19 @@ dualsync = function(method, model, options) { }); } else { responseModel = modelUpdatedWithResponse(model, resp); - return localsync('create', responseModel, options).then(function() { + return localSync('create', responseModel, options).then(function() { return success(resp, status, xhr); }); } }; if (!options.add) { - return localsync('clear', model, options).then(go); + return localSync('clear', model, options).then(go); } else { return go(); } }; options.error = function(resp) { - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -514,13 +514,13 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -534,8 +534,8 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync('delete', model, options).then(function() { - return localsync('create', updatedModel, options).then(function() { + return localSync('delete', model, options).then(function() { + return localSync('create', updatedModel, options).then(function() { return success(resp, status, xhr); }); }); @@ -545,7 +545,7 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -557,13 +557,13 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success; }); }; @@ -572,16 +572,16 @@ dualsync = function(method, model, options) { break; case 'delete': if (_.isString(model.id) && model.id.length === 36) { - return localsync(method, model, options); + return localSync(method, model, options); } else { options.success = function(resp, status, xhr) { - return localsync(method, model, options).then(function() { + return localSync(method, model, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -590,5 +590,5 @@ dualsync = function(method, model, options) { } }; -Backbone.sync = dualsync; +Backbone.sync = dualSync; }); \ No newline at end of file diff --git a/backbone.dualstorage.coffee b/backbone.dualstorage.coffee index 2a889bd..339cbc3 100644 --- a/backbone.dualstorage.coffee +++ b/backbone.dualstorage.coffee @@ -165,7 +165,7 @@ callbackTranslator = # Override `Backbone.sync` to use delegate to the model or collection's # *localStorage* property, which should be an instance of `Store`. -localsync = (method, model, options) -> +localSync = (method, model, options) -> isValidModel = (method is 'clear') or (method is 'hasDirtyOrDestroyed') isValidModel ||= model instanceof Backbone.Model isValidModel ||= model instanceof Backbone.Collection @@ -250,7 +250,7 @@ onlineSync = (method, model, options) -> options.error = callbackTranslator.forBackboneCaller(options.error) backboneSync(method, model, options) -dualsync = (method, model, options) -> +dualSync = (method, model, options) -> options.storeName = result(model.collection, 'storeName') || result(model, 'storeName') || result(model.collection, 'url') || result(model, 'urlRoot') || result(model, 'url') options.success = callbackTranslator.forDualstorageCaller(options.success, model, options) @@ -262,7 +262,7 @@ dualsync = (method, model, options) -> # execute only local sync local = result(model, 'local') or result(model.collection, 'local') options.dirty = options.remote is false and not local - return localsync(method, model, options) if options.remote is false or local + return localSync(method, model, options) if options.remote is false or local # execute dual sync options.ignoreCallbacks = true @@ -272,9 +272,9 @@ dualsync = (method, model, options) -> switch method when 'read' - localsync('hasDirtyOrDestroyed', model, options).then (hasDirtyOrDestroyed) -> + localSync('hasDirtyOrDestroyed', model, options).then (hasDirtyOrDestroyed) -> if hasDirtyOrDestroyed - success localsync(method, model, options) + success localSync(method, model, options) else options.success = (resp, status, xhr) -> resp = parseRemoteResponse(model, resp) @@ -291,20 +291,20 @@ dualsync = (method, model, options) -> else responseModel = new collection.model(modelAttributes) models.push responseModel - $.when((localsync('create', m, options) for m in models)...).then -> + $.when((localSync('create', m, options) for m in models)...).then -> success(resp, status, xhr) else responseModel = modelUpdatedWithResponse(model, resp) - localsync('create', responseModel, options).then -> + localSync('create', responseModel, options).then -> success(resp, status, xhr) if not options.add - localsync('clear', model, options).then go + localSync('clear', model, options).then go else go() options.error = (resp) -> - localsync(method, model, options).then (result) -> + localSync(method, model, options).then (result) -> success result onlineSync(method, model, options) @@ -312,11 +312,11 @@ dualsync = (method, model, options) -> when 'create' options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp - localsync(method, updatedModel, options).then -> + localSync(method, updatedModel, options).then -> success(resp, status, xhr) options.error = (resp) -> options.dirty = true - localsync(method, model, options).then (result) -> + localSync(method, model, options).then (result) -> success result onlineSync(method, model, options) @@ -328,13 +328,13 @@ dualsync = (method, model, options) -> options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp model.set model.idAttribute, temporaryId, silent: true - localsync('delete', model, options).then -> - localsync('create', updatedModel, options).then -> + localSync('delete', model, options).then -> + localSync('create', updatedModel, options).then -> success(resp, status, xhr) options.error = (resp) -> options.dirty = true model.set model.idAttribute, temporaryId, silent: true - localsync(method, model, options).then (result) -> + localSync(method, model, options).then (result) -> success result model.set model.idAttribute, null, silent: true @@ -342,27 +342,27 @@ dualsync = (method, model, options) -> else options.success = (resp, status, xhr) -> updatedModel = modelUpdatedWithResponse model, resp - localsync(method, updatedModel, options).then -> + localSync(method, updatedModel, options).then -> success(resp, status, xhr) options.error = (resp) -> options.dirty = true - localsync(method, model, options).then (result) -> + localSync(method, model, options).then (result) -> success onlineSync(method, model, options) when 'delete' if _.isString(model.id) and model.id.length == 36 - localsync(method, model, options) + localSync(method, model, options) else options.success = (resp, status, xhr) -> - localsync(method, model, options).then -> + localSync(method, model, options).then -> success(resp, status, xhr) options.error = (resp) -> options.dirty = true - localsync(method, model, options).then (result) -> + localSync(method, model, options).then (result) -> success result onlineSync(method, model, options) -Backbone.sync = dualsync +Backbone.sync = dualSync diff --git a/backbone.dualstorage.js b/backbone.dualstorage.js index 84afd8d..72c34ba 100644 --- a/backbone.dualstorage.js +++ b/backbone.dualstorage.js @@ -9,7 +9,7 @@ as that. (function() { - var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, + var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualSync, localSync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; LocalStorageAdapter = (function() { @@ -311,7 +311,7 @@ as that. } }; - localsync = function(method, model, options) { + localSync = function(method, model, options) { var isValidModel, store, _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); @@ -428,7 +428,7 @@ as that. return backboneSync(method, model, options); }; - dualsync = function(method, model, options) { + dualSync = function(method, model, options) { var error, local, success, temporaryId; options.storeName = result(model.collection, 'storeName') || result(model, 'storeName') || result(model.collection, 'url') || result(model, 'urlRoot') || result(model, 'url'); options.success = callbackTranslator.forDualstorageCaller(options.success, model, options); @@ -439,16 +439,16 @@ as that. local = result(model, 'local') || result(model.collection, 'local'); options.dirty = options.remote === false && !local; if (options.remote === false || local) { - return localsync(method, model, options); + return localSync(method, model, options); } options.ignoreCallbacks = true; success = options.success; error = options.error; switch (method) { case 'read': - return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + return localSync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { if (hasDirtyOrDestroyed) { - return success(localsync(method, model, options)); + return success(localSync(method, model, options)); } else { options.success = function(resp, status, xhr) { var go; @@ -474,7 +474,7 @@ as that. _results = []; for (_j = 0, _len1 = models.length; _j < _len1; _j++) { m = models[_j]; - _results.push(localsync('create', m, options)); + _results.push(localSync('create', m, options)); } return _results; })())).then(function() { @@ -482,19 +482,19 @@ as that. }); } else { responseModel = modelUpdatedWithResponse(model, resp); - return localsync('create', responseModel, options).then(function() { + return localSync('create', responseModel, options).then(function() { return success(resp, status, xhr); }); } }; if (!options.add) { - return localsync('clear', model, options).then(go); + return localSync('clear', model, options).then(go); } else { return go(); } }; options.error = function(resp) { - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -505,13 +505,13 @@ as that. options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -525,8 +525,8 @@ as that. model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync('delete', model, options).then(function() { - return localsync('create', updatedModel, options).then(function() { + return localSync('delete', model, options).then(function() { + return localSync('create', updatedModel, options).then(function() { return success(resp, status, xhr); }); }); @@ -536,7 +536,7 @@ as that. model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -548,13 +548,13 @@ as that. options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success; }); }; @@ -563,16 +563,16 @@ as that. break; case 'delete': if (_.isString(model.id) && model.id.length === 36) { - return localsync(method, model, options); + return localSync(method, model, options); } else { options.success = function(resp, status, xhr) { - return localsync(method, model, options).then(function() { + return localSync(method, model, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -581,6 +581,6 @@ as that. } }; - Backbone.sync = dualsync; + Backbone.sync = dualSync; }).call(this); diff --git a/spec/backbone.dualstorage.js b/spec/backbone.dualstorage.js index b5c5cef..cf09f77 100644 --- a/spec/backbone.dualstorage.js +++ b/spec/backbone.dualstorage.js @@ -7,7 +7,7 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple as that. */ -var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, +var LocalStorageAdapter, S4, backboneSync, callbackTranslator, dualSync, localSync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result, __slice = [].slice; LocalStorageAdapter = (function() { @@ -309,7 +309,7 @@ callbackTranslator = { } }; -localsync = function(method, model, options) { +localSync = function(method, model, options) { var isValidModel, store, _this = this; isValidModel = (method === 'clear') || (method === 'hasDirtyOrDestroyed'); @@ -426,7 +426,7 @@ onlineSync = function(method, model, options) { return backboneSync(method, model, options); }; -dualsync = function(method, model, options) { +dualSync = function(method, model, options) { var error, local, success, temporaryId; options.storeName = result(model.collection, 'storeName') || result(model, 'storeName') || result(model.collection, 'url') || result(model, 'urlRoot') || result(model, 'url'); options.success = callbackTranslator.forDualstorageCaller(options.success, model, options); @@ -437,16 +437,16 @@ dualsync = function(method, model, options) { local = result(model, 'local') || result(model.collection, 'local'); options.dirty = options.remote === false && !local; if (options.remote === false || local) { - return localsync(method, model, options); + return localSync(method, model, options); } options.ignoreCallbacks = true; success = options.success; error = options.error; switch (method) { case 'read': - return localsync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { + return localSync('hasDirtyOrDestroyed', model, options).then(function(hasDirtyOrDestroyed) { if (hasDirtyOrDestroyed) { - return success(localsync(method, model, options)); + return success(localSync(method, model, options)); } else { options.success = function(resp, status, xhr) { var go; @@ -472,7 +472,7 @@ dualsync = function(method, model, options) { _results = []; for (_j = 0, _len1 = models.length; _j < _len1; _j++) { m = models[_j]; - _results.push(localsync('create', m, options)); + _results.push(localSync('create', m, options)); } return _results; })())).then(function() { @@ -480,19 +480,19 @@ dualsync = function(method, model, options) { }); } else { responseModel = modelUpdatedWithResponse(model, resp); - return localsync('create', responseModel, options).then(function() { + return localSync('create', responseModel, options).then(function() { return success(resp, status, xhr); }); } }; if (!options.add) { - return localsync('clear', model, options).then(go); + return localSync('clear', model, options).then(go); } else { return go(); } }; options.error = function(resp) { - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -503,13 +503,13 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -523,8 +523,8 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync('delete', model, options).then(function() { - return localsync('create', updatedModel, options).then(function() { + return localSync('delete', model, options).then(function() { + return localSync('create', updatedModel, options).then(function() { return success(resp, status, xhr); }); }); @@ -534,7 +534,7 @@ dualsync = function(method, model, options) { model.set(model.idAttribute, temporaryId, { silent: true }); - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -546,13 +546,13 @@ dualsync = function(method, model, options) { options.success = function(resp, status, xhr) { var updatedModel; updatedModel = modelUpdatedWithResponse(model, resp); - return localsync(method, updatedModel, options).then(function() { + return localSync(method, updatedModel, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success; }); }; @@ -561,16 +561,16 @@ dualsync = function(method, model, options) { break; case 'delete': if (_.isString(model.id) && model.id.length === 36) { - return localsync(method, model, options); + return localSync(method, model, options); } else { options.success = function(resp, status, xhr) { - return localsync(method, model, options).then(function() { + return localSync(method, model, options).then(function() { return success(resp, status, xhr); }); }; options.error = function(resp) { options.dirty = true; - return localsync(method, model, options).then(function(result) { + return localSync(method, model, options).then(function(result) { return success(result); }); }; @@ -579,4 +579,4 @@ dualsync = function(method, model, options) { } }; -Backbone.sync = dualsync; +Backbone.sync = dualSync; diff --git a/spec/bugs_spec.coffee b/spec/bugs_spec.coffee index 933e207..ab95fbe 100644 --- a/spec/bugs_spec.coffee +++ b/spec/bugs_spec.coffee @@ -1,4 +1,4 @@ -{Store, backboneSync, localsync} = window +{Store, backboneSync, localSync} = window describe 'bugs, that once fixed, should be moved to the proper spec file and modified to test their inverse', -> it 'fails to throw an error when no storeName is provided to the Store constructor, @@ -12,7 +12,7 @@ describe 'bugs, that once fixed, should be moved to the proper spec file and mod beforeEach -> backboneSync.calls = [] - localsync 'clear', {}, success: (->), error: (->) + localSync 'clear', {}, success: (->), error: (->) collection = new Backbone.Collection collection.url = 'eyes/' model = new Backbone.Model diff --git a/spec/bugs_spec.js b/spec/bugs_spec.js index d75bac2..6c70333 100644 --- a/spec/bugs_spec.js +++ b/spec/bugs_spec.js @@ -1,8 +1,8 @@ // Generated by CoffeeScript 1.6.3 (function() { - var Store, backboneSync, localsync; + var Store, backboneSync, localSync; - Store = window.Store, backboneSync = window.backboneSync, localsync = window.localsync; + Store = window.Store, backboneSync = window.backboneSync, localSync = window.localSync; describe('bugs, that once fixed, should be moved to the proper spec file and modified to test their inverse', function() { it('fails to throw an error when no storeName is provided to the Store constructor,\ @@ -19,7 +19,7 @@ _ref = {}, Role = _ref.Role, RoleCollection = _ref.RoleCollection, collection = _ref.collection, model = _ref.model; beforeEach(function() { backboneSync.calls = []; - localsync('clear', {}, { + localSync('clear', {}, { success: (function() {}), error: (function() {}) }); diff --git a/spec/dualsync_spec.coffee b/spec/dualsync_spec.coffee index 1f6d5e7..8b4df56 100644 --- a/spec/dualsync_spec.coffee +++ b/spec/dualsync_spec.coffee @@ -1,4 +1,4 @@ -{Backbone, backboneSync, localsync, localStorage} = window +{Backbone, backboneSync, localSync, localStorage} = window {collection, model, ModelWithAlternateIdAttribute} = {} beforeEach -> @@ -16,13 +16,13 @@ beforeEach -> delete model.remote spyOnLocalsync = -> - spyOn(window, 'localsync') + spyOn(window, 'localSync') .andCallFake (method, model, options) -> options.success?() unless options.ignoreCallbacks $.Deferred().resolve(); - localsync = window.localsync + localSync = window.localSync -describe 'delegating to localsync and backboneSync, and calling the model callbacks', -> +describe 'delegating to localSync and backboneSync, and calling the model callbacks', -> describe 'dual tier storage', -> checkMergedAttributesFor = (method, modelToUpdate = model) -> spyOnLocalsync() @@ -32,62 +32,62 @@ describe 'delegating to localsync and backboneSync, and calling the model callba modelToUpdate.set updatedAttribute: 'original value' originalAttributes = _.clone(modelToUpdate.attributes) serverResponse = _.extend(model.toJSON(), updatedAttribute: 'updated value', newAttribute: 'new value') - dualsync(method, modelToUpdate, success: (-> ready = true), serverResponse: serverResponse) + dualSync(method, modelToUpdate, success: (-> ready = true), serverResponse: serverResponse) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(modelToUpdate.attributes).toEqual originalAttributes - localsyncedAttributes = _(localsync.calls).map((call) -> call.args[1].attributes) + localSyncedAttributes = _(localSync.calls).map((call) -> call.args[1].attributes) updatedAttributes = _id: 12 position: 'arm' updatedAttribute: 'updated value' newAttribute: 'new value' - expect(localsyncedAttributes).toContain updatedAttributes + expect(localSyncedAttributes).toContain updatedAttributes describe 'create', -> - it 'delegates to both localsync and backboneSync', -> + it 'delegates to both localSync and backboneSync', -> spyOnLocalsync() ready = false runs -> - dualsync('create', model, success: (-> ready = true)) + dualSync('create', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() expect(backboneSync.calls[0].args[0]).toEqual 'create' - expect(localsync).toHaveBeenCalled() - expect(localsync.calls[0].args[0]).toEqual 'create' - expect(_(localsync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() + expect(localSync).toHaveBeenCalled() + expect(localSync.calls[0].args[0]).toEqual 'create' + expect(_(localSync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() it 'merges the response attributes into the model attributes', -> checkMergedAttributesFor 'create' describe 'read', -> - it 'delegates to both localsync and backboneSync', -> + it 'delegates to both localSync and backboneSync', -> spyOnLocalsync() ready = false runs -> - dualsync('read', model, success: (-> ready = true)) + dualSync('read', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() expect(_(backboneSync.calls).any((call) -> call.args[0] == 'read')).toBeTruthy() - expect(localsync).toHaveBeenCalled() - expect(_(localsync.calls).any((call) -> call.args[0] == 'create')).toBeTruthy() - expect(_(localsync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() + expect(localSync).toHaveBeenCalled() + expect(_(localSync.calls).any((call) -> call.args[0] == 'create')).toBeTruthy() + expect(_(localSync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() describe 'for collections', -> - it 'calls localsync create once for each model', -> + it 'calls localSync create once for each model', -> spyOnLocalsync() ready = false collectionResponse = [{_id: 12, position: 'arm'}, {_id: 13, position: 'a new model'}] runs -> - dualsync('read', collection, success: (-> ready = true), serverResponse: collectionResponse) + dualSync('read', collection, success: (-> ready = true), serverResponse: collectionResponse) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() expect(_(backboneSync.calls).any((call) -> call.args[0] == 'read')).toBeTruthy() - expect(localsync).toHaveBeenCalled() - createCalls = _(localsync.calls).select((call) -> call.args[0] == 'create') + expect(localSync).toHaveBeenCalled() + createCalls = _(localSync.calls).select((call) -> call.args[0] == 'create') expect(createCalls.length).toEqual 2 expect(_(createCalls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() createdModelAttributes = _(createCalls).map((call) -> call.args[1].attributes) @@ -95,42 +95,42 @@ describe 'delegating to localsync and backboneSync, and calling the model callba expect(createdModelAttributes[1]).toEqual _id: 13, position: 'a new model' describe 'update', -> - it 'delegates to both localsync and backboneSync', -> + it 'delegates to both localSync and backboneSync', -> spyOnLocalsync() ready = false runs -> - dualsync('update', model, success: (-> ready = true)) + dualSync('update', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() expect(_(backboneSync.calls).any((call) -> call.args[0] == 'update')).toBeTruthy() - expect(localsync).toHaveBeenCalled() - expect(_(localsync.calls).any((call) -> call.args[0] == 'update')).toBeTruthy() - expect(_(localsync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() + expect(localSync).toHaveBeenCalled() + expect(_(localSync.calls).any((call) -> call.args[0] == 'update')).toBeTruthy() + expect(_(localSync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() it 'merges the response attributes into the model attributes', -> checkMergedAttributesFor 'update' describe 'delete', -> - it 'delegates to both localsync and backboneSync', -> + it 'delegates to both localSync and backboneSync', -> spyOnLocalsync() ready = false runs -> - dualsync('delete', model, success: (-> ready = true)) + dualSync('delete', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() expect(_(backboneSync.calls).any((call) -> call.args[0] == 'delete')).toBeTruthy() - expect(localsync).toHaveBeenCalled() - expect(_(localsync.calls).any((call) -> call.args[0] == 'delete')).toBeTruthy() - expect(_(localsync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() + expect(localSync).toHaveBeenCalled() + expect(_(localSync.calls).any((call) -> call.args[0] == 'delete')).toBeTruthy() + expect(_(localSync.calls).every((call) -> call.args[1] instanceof Backbone.Model)).toBeTruthy() describe 'respects the remote only attribute on models', -> it 'delegates for remote models', -> ready = false runs -> model.remote = true - dualsync('create', model, success: (-> ready = true)) + dualSync('create', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() @@ -140,7 +140,7 @@ describe 'delegating to localsync and backboneSync, and calling the model callba ready = false runs -> collection.remote = true - dualsync('read', model, success: (-> ready = true)) + dualSync('read', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).toHaveBeenCalled() @@ -153,18 +153,18 @@ describe 'delegating to localsync and backboneSync, and calling the model callba runs -> model.local = true backboneSync.reset() - dualsync('update', model, success: (-> ready = true)) + dualSync('update', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync).toHaveBeenCalled() - expect(localsync.calls[0].args[0]).toEqual 'update' + expect(localSync).toHaveBeenCalled() + expect(localSync.calls[0].args[0]).toEqual 'update' it 'delegates for local collections', -> ready = false runs -> collection.local = true backboneSync.reset() - dualsync('delete', model, success: (-> ready = true)) + dualSync('delete', model, success: (-> ready = true)) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).not.toHaveBeenCalled() @@ -173,7 +173,7 @@ describe 'delegating to localsync and backboneSync, and calling the model callba ready = false runs -> backboneSync.reset() - dualsync('create', model, success: (-> ready = true), remote: false) + dualSync('create', model, success: (-> ready = true), remote: false) waitsFor (-> ready), "The success callback should have been called", 100 runs -> expect(backboneSync).not.toHaveBeenCalled() @@ -183,51 +183,51 @@ describe 'delegating to localsync and backboneSync, and calling the model callba describe 'for models', -> it 'gets merged with existing attributes on a model', -> spyOnLocalsync() - localsync.reset() + localSync.reset() ready = false runs -> - dualsync('read', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) + dualSync('read', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - window.x = localsync - expect(localsync.calls[2].args[0]).toEqual 'create' - expect(localsync.calls[2].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 + window.x = localSync + expect(localSync.calls[2].args[0]).toEqual 'create' + expect(localSync.calls[2].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 describe 'for collections', -> it 'gets merged with existing attributes on the model with the same id', -> spyOnLocalsync() - localsync.reset() + localSync.reset() ready = false runs -> - dualsync('read', collection, success: (-> ready = true), serverResponse: [{side: 'left', _id: 12}]) + dualSync('read', collection, success: (-> ready = true), serverResponse: [{side: 'left', _id: 12}]) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync.calls[2].args[0]).toEqual 'create' - expect(localsync.calls[2].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 12 + expect(localSync.calls[2].args[0]).toEqual 'create' + expect(localSync.calls[2].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 12 describe 'on create', -> it 'gets merged with existing attributes on a model', -> spyOnLocalsync() - localsync.reset() + localSync.reset() ready = false runs -> - dualsync('create', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) + dualSync('create', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync.calls[0].args[0]).toEqual 'create' - expect(localsync.calls[0].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 + expect(localSync.calls[0].args[0]).toEqual 'create' + expect(localSync.calls[0].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 describe 'on update', -> it 'gets merged with existing attributes on a model', -> spyOnLocalsync() - localsync.reset() + localSync.reset() ready = false runs -> - dualsync('update', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) + dualSync('update', model, success: (-> ready = true), serverResponse: {side: 'left', _id: 13}) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync.calls[0].args[0]).toEqual 'update' - expect(localsync.calls[0].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 + expect(localSync.calls[0].args[0]).toEqual 'update' + expect(localSync.calls[0].args[1].attributes).toEqual position: 'arm', side: 'left', _id: 13 describe 'offline storage', -> it 'marks records dirty when options.remote is false, except if the model/collection is marked as local', -> @@ -236,22 +236,22 @@ describe 'offline storage', -> runs -> ready = false collection.local = true - dualsync('update', model, success: (-> ready = true), remote: false) + dualSync('update', model, success: (-> ready = true), remote: false) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync).toHaveBeenCalled() - expect(localsync.calls.length).toEqual 1 - expect(localsync.calls[0].args[2].dirty).toBeFalsy() + expect(localSync).toHaveBeenCalled() + expect(localSync.calls.length).toEqual 1 + expect(localSync.calls[0].args[2].dirty).toBeFalsy() runs -> - localsync.reset() + localSync.reset() ready = false collection.local = false - dualsync('update', model, success: (-> ready = true), remote: false) + dualSync('update', model, success: (-> ready = true), remote: false) waitsFor (-> ready), "The success callback should have been called", 100 runs -> - expect(localsync).toHaveBeenCalled() - expect(localsync.calls.length).toEqual 1 - expect(localsync.calls[0].args[2].dirty).toBeTruthy() + expect(localSync).toHaveBeenCalled() + expect(localSync.calls.length).toEqual 1 + expect(localSync.calls[0].args[2].dirty).toBeTruthy() describe 'dualStorage hooks', -> beforeEach -> @@ -259,13 +259,13 @@ describe 'dualStorage hooks', -> new ModelWithAlternateIdAttribute(parsedRemote: true) ready = false runs -> - dualsync 'create', model, success: (-> ready = true) + dualSync 'create', model, success: (-> ready = true) waitsFor (-> ready), "The success callback should have been called", 100 it 'filters read responses through parseBeforeLocalSave when defined on the model or collection', -> response = null runs -> - dualsync 'read', model, success: (callback_args...) -> + dualSync 'read', model, success: (callback_args...) -> response = callback_args waitsFor (-> response), "The success callback should have been called", 100 runs -> @@ -277,8 +277,8 @@ describe 'storeName selection', -> model.local = true model.url = '/bacon/bits' spyOnLocalsync() - dualsync(null, model, {}) - expect(localsync.calls[0].args[2].storeName).toEqual model.url + dualSync(null, model, {}) + expect(localSync.calls[0].args[2].storeName).toEqual model.url it 'prefers the model urlRoot over the url as a store name', -> model = new ModelWithAlternateIdAttribute() @@ -286,8 +286,8 @@ describe 'storeName selection', -> model.url = '/bacon/bits' model.urlRoot = '/bacon' spyOnLocalsync() - dualsync(null, model, {}) - expect(localsync.calls[0].args[2].storeName).toEqual model.urlRoot + dualSync(null, model, {}) + expect(localSync.calls[0].args[2].storeName).toEqual model.urlRoot it 'prefers the collection url over the model urlRoot as a store name', -> model = new ModelWithAlternateIdAttribute() @@ -297,8 +297,8 @@ describe 'storeName selection', -> model.collection = new Backbone.Collection() model.collection.url = '/ranch' spyOnLocalsync() - dualsync(null, model, {}) - expect(localsync.calls[0].args[2].storeName).toEqual model.collection.url + dualSync(null, model, {}) + expect(localSync.calls[0].args[2].storeName).toEqual model.collection.url it 'prefers the model storeName over the collection url as a store name', -> model = new ModelWithAlternateIdAttribute() @@ -309,8 +309,8 @@ describe 'storeName selection', -> model.collection.url = '/ranch' model.storeName = 'melted cheddar' spyOnLocalsync() - dualsync(null, model, {}) - expect(localsync.calls[0].args[2].storeName).toEqual model.storeName + dualSync(null, model, {}) + expect(localSync.calls[0].args[2].storeName).toEqual model.storeName it 'prefers the collection storeName over the model storeName as a store name', -> model = new ModelWithAlternateIdAttribute() @@ -322,5 +322,5 @@ describe 'storeName selection', -> model.storeName = 'melted cheddar' model.collection.storeName = 'ketchup' spyOnLocalsync() - dualsync(null, model, {}) - expect(localsync.calls[0].args[2].storeName).toEqual model.collection.storeName + dualSync(null, model, {}) + expect(localSync.calls[0].args[2].storeName).toEqual model.collection.storeName diff --git a/spec/dualsync_spec.js b/spec/dualsync_spec.js index 1c1ab99..d272112 100644 --- a/spec/dualsync_spec.js +++ b/spec/dualsync_spec.js @@ -1,9 +1,9 @@ // Generated by CoffeeScript 1.6.3 (function() { - var Backbone, ModelWithAlternateIdAttribute, backboneSync, collection, localStorage, localsync, model, spyOnLocalsync, _ref, + var Backbone, ModelWithAlternateIdAttribute, backboneSync, collection, localStorage, localSync, model, spyOnLocalsync, _ref, __slice = [].slice; - Backbone = window.Backbone, backboneSync = window.backboneSync, localsync = window.localsync, localStorage = window.localStorage; + Backbone = window.Backbone, backboneSync = window.backboneSync, localSync = window.localSync, localStorage = window.localStorage; _ref = {}, collection = _ref.collection, model = _ref.model, ModelWithAlternateIdAttribute = _ref.ModelWithAlternateIdAttribute; @@ -26,7 +26,7 @@ }); spyOnLocalsync = function() { - spyOn(window, 'localsync').andCallFake(function(method, model, options) { + spyOn(window, 'localSync').andCallFake(function(method, model, options) { if (!options.ignoreCallbacks) { if (typeof options.success === "function") { options.success(); @@ -34,10 +34,10 @@ } return $.Deferred().resolve(); }); - return localsync = window.localsync; + return localSync = window.localSync; }; - describe('delegating to localsync and backboneSync, and calling the model callbacks', function() { + describe('delegating to localSync and backboneSync, and calling the model callbacks', function() { describe('dual tier storage', function() { var checkMergedAttributesFor; checkMergedAttributesFor = function(method, modelToUpdate) { @@ -58,7 +58,7 @@ updatedAttribute: 'updated value', newAttribute: 'new value' }); - return dualsync(method, modelToUpdate, { + return dualSync(method, modelToUpdate, { success: (function() { return ready = true; }), @@ -69,9 +69,9 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - var localsyncedAttributes, updatedAttributes; + var localSyncedAttributes, updatedAttributes; expect(modelToUpdate.attributes).toEqual(originalAttributes); - localsyncedAttributes = _(localsync.calls).map(function(call) { + localSyncedAttributes = _(localSync.calls).map(function(call) { return call.args[1].attributes; }); updatedAttributes = { @@ -80,16 +80,16 @@ updatedAttribute: 'updated value', newAttribute: 'new value' }; - return expect(localsyncedAttributes).toContain(updatedAttributes); + return expect(localSyncedAttributes).toContain(updatedAttributes); }); }; describe('create', function() { - it('delegates to both localsync and backboneSync', function() { + it('delegates to both localSync and backboneSync', function() { var ready; spyOnLocalsync(); ready = false; runs(function() { - return dualsync('create', model, { + return dualSync('create', model, { success: (function() { return ready = true; }) @@ -101,9 +101,9 @@ return runs(function() { expect(backboneSync).toHaveBeenCalled(); expect(backboneSync.calls[0].args[0]).toEqual('create'); - expect(localsync).toHaveBeenCalled(); - expect(localsync.calls[0].args[0]).toEqual('create'); - return expect(_(localsync.calls).every(function(call) { + expect(localSync).toHaveBeenCalled(); + expect(localSync.calls[0].args[0]).toEqual('create'); + return expect(_(localSync.calls).every(function(call) { return call.args[1] instanceof Backbone.Model; })).toBeTruthy(); }); @@ -113,12 +113,12 @@ }); }); describe('read', function() { - it('delegates to both localsync and backboneSync', function() { + it('delegates to both localSync and backboneSync', function() { var ready; spyOnLocalsync(); ready = false; runs(function() { - return dualsync('read', model, { + return dualSync('read', model, { success: (function() { return ready = true; }) @@ -132,17 +132,17 @@ expect(_(backboneSync.calls).any(function(call) { return call.args[0] === 'read'; })).toBeTruthy(); - expect(localsync).toHaveBeenCalled(); - expect(_(localsync.calls).any(function(call) { + expect(localSync).toHaveBeenCalled(); + expect(_(localSync.calls).any(function(call) { return call.args[0] === 'create'; })).toBeTruthy(); - return expect(_(localsync.calls).every(function(call) { + return expect(_(localSync.calls).every(function(call) { return call.args[1] instanceof Backbone.Model; })).toBeTruthy(); }); }); return describe('for collections', function() { - return it('calls localsync create once for each model', function() { + return it('calls localSync create once for each model', function() { var collectionResponse, ready; spyOnLocalsync(); ready = false; @@ -156,7 +156,7 @@ } ]; runs(function() { - return dualsync('read', collection, { + return dualSync('read', collection, { success: (function() { return ready = true; }), @@ -172,8 +172,8 @@ expect(_(backboneSync.calls).any(function(call) { return call.args[0] === 'read'; })).toBeTruthy(); - expect(localsync).toHaveBeenCalled(); - createCalls = _(localsync.calls).select(function(call) { + expect(localSync).toHaveBeenCalled(); + createCalls = _(localSync.calls).select(function(call) { return call.args[0] === 'create'; }); expect(createCalls.length).toEqual(2); @@ -196,12 +196,12 @@ }); }); describe('update', function() { - it('delegates to both localsync and backboneSync', function() { + it('delegates to both localSync and backboneSync', function() { var ready; spyOnLocalsync(); ready = false; runs(function() { - return dualsync('update', model, { + return dualSync('update', model, { success: (function() { return ready = true; }) @@ -215,11 +215,11 @@ expect(_(backboneSync.calls).any(function(call) { return call.args[0] === 'update'; })).toBeTruthy(); - expect(localsync).toHaveBeenCalled(); - expect(_(localsync.calls).any(function(call) { + expect(localSync).toHaveBeenCalled(); + expect(_(localSync.calls).any(function(call) { return call.args[0] === 'update'; })).toBeTruthy(); - return expect(_(localsync.calls).every(function(call) { + return expect(_(localSync.calls).every(function(call) { return call.args[1] instanceof Backbone.Model; })).toBeTruthy(); }); @@ -229,12 +229,12 @@ }); }); return describe('delete', function() { - return it('delegates to both localsync and backboneSync', function() { + return it('delegates to both localSync and backboneSync', function() { var ready; spyOnLocalsync(); ready = false; runs(function() { - return dualsync('delete', model, { + return dualSync('delete', model, { success: (function() { return ready = true; }) @@ -248,11 +248,11 @@ expect(_(backboneSync.calls).any(function(call) { return call.args[0] === 'delete'; })).toBeTruthy(); - expect(localsync).toHaveBeenCalled(); - expect(_(localsync.calls).any(function(call) { + expect(localSync).toHaveBeenCalled(); + expect(_(localSync.calls).any(function(call) { return call.args[0] === 'delete'; })).toBeTruthy(); - return expect(_(localsync.calls).every(function(call) { + return expect(_(localSync.calls).every(function(call) { return call.args[1] instanceof Backbone.Model; })).toBeTruthy(); }); @@ -265,7 +265,7 @@ ready = false; runs(function() { model.remote = true; - return dualsync('create', model, { + return dualSync('create', model, { success: (function() { return ready = true; }) @@ -284,7 +284,7 @@ ready = false; runs(function() { collection.remote = true; - return dualsync('read', model, { + return dualSync('read', model, { success: (function() { return ready = true; }) @@ -307,7 +307,7 @@ runs(function() { model.local = true; backboneSync.reset(); - return dualsync('update', model, { + return dualSync('update', model, { success: (function() { return ready = true; }) @@ -317,8 +317,8 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - expect(localsync).toHaveBeenCalled(); - return expect(localsync.calls[0].args[0]).toEqual('update'); + expect(localSync).toHaveBeenCalled(); + return expect(localSync.calls[0].args[0]).toEqual('update'); }); }); return it('delegates for local collections', function() { @@ -327,7 +327,7 @@ runs(function() { collection.local = true; backboneSync.reset(); - return dualsync('delete', model, { + return dualSync('delete', model, { success: (function() { return ready = true; }) @@ -346,7 +346,7 @@ ready = false; runs(function() { backboneSync.reset(); - return dualsync('create', model, { + return dualSync('create', model, { success: (function() { return ready = true; }), @@ -366,10 +366,10 @@ return it('gets merged with existing attributes on a model', function() { var ready; spyOnLocalsync(); - localsync.reset(); + localSync.reset(); ready = false; runs(function() { - return dualsync('read', model, { + return dualSync('read', model, { success: (function() { return ready = true; }), @@ -383,9 +383,9 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - window.x = localsync; - expect(localsync.calls[2].args[0]).toEqual('create'); - return expect(localsync.calls[2].args[1].attributes).toEqual({ + window.x = localSync; + expect(localSync.calls[2].args[0]).toEqual('create'); + return expect(localSync.calls[2].args[1].attributes).toEqual({ position: 'arm', side: 'left', _id: 13 @@ -397,10 +397,10 @@ return it('gets merged with existing attributes on the model with the same id', function() { var ready; spyOnLocalsync(); - localsync.reset(); + localSync.reset(); ready = false; runs(function() { - return dualsync('read', collection, { + return dualSync('read', collection, { success: (function() { return ready = true; }), @@ -416,8 +416,8 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - expect(localsync.calls[2].args[0]).toEqual('create'); - return expect(localsync.calls[2].args[1].attributes).toEqual({ + expect(localSync.calls[2].args[0]).toEqual('create'); + return expect(localSync.calls[2].args[1].attributes).toEqual({ position: 'arm', side: 'left', _id: 12 @@ -430,10 +430,10 @@ return it('gets merged with existing attributes on a model', function() { var ready; spyOnLocalsync(); - localsync.reset(); + localSync.reset(); ready = false; runs(function() { - return dualsync('create', model, { + return dualSync('create', model, { success: (function() { return ready = true; }), @@ -447,8 +447,8 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - expect(localsync.calls[0].args[0]).toEqual('create'); - return expect(localsync.calls[0].args[1].attributes).toEqual({ + expect(localSync.calls[0].args[0]).toEqual('create'); + return expect(localSync.calls[0].args[1].attributes).toEqual({ position: 'arm', side: 'left', _id: 13 @@ -460,10 +460,10 @@ return it('gets merged with existing attributes on a model', function() { var ready; spyOnLocalsync(); - localsync.reset(); + localSync.reset(); ready = false; runs(function() { - return dualsync('update', model, { + return dualSync('update', model, { success: (function() { return ready = true; }), @@ -477,8 +477,8 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - expect(localsync.calls[0].args[0]).toEqual('update'); - return expect(localsync.calls[0].args[1].attributes).toEqual({ + expect(localSync.calls[0].args[0]).toEqual('update'); + return expect(localSync.calls[0].args[1].attributes).toEqual({ position: 'arm', side: 'left', _id: 13 @@ -497,7 +497,7 @@ runs(function() { ready = false; collection.local = true; - return dualsync('update', model, { + return dualSync('update', model, { success: (function() { return ready = true; }), @@ -508,15 +508,15 @@ return ready; }), "The success callback should have been called", 100); runs(function() { - expect(localsync).toHaveBeenCalled(); - expect(localsync.calls.length).toEqual(1); - return expect(localsync.calls[0].args[2].dirty).toBeFalsy(); + expect(localSync).toHaveBeenCalled(); + expect(localSync.calls.length).toEqual(1); + return expect(localSync.calls[0].args[2].dirty).toBeFalsy(); }); runs(function() { - localsync.reset(); + localSync.reset(); ready = false; collection.local = false; - return dualsync('update', model, { + return dualSync('update', model, { success: (function() { return ready = true; }), @@ -527,9 +527,9 @@ return ready; }), "The success callback should have been called", 100); return runs(function() { - expect(localsync).toHaveBeenCalled(); - expect(localsync.calls.length).toEqual(1); - return expect(localsync.calls[0].args[2].dirty).toBeTruthy(); + expect(localSync).toHaveBeenCalled(); + expect(localSync.calls.length).toEqual(1); + return expect(localSync.calls[0].args[2].dirty).toBeTruthy(); }); }); }); @@ -544,7 +544,7 @@ }; ready = false; runs(function() { - return dualsync('create', model, { + return dualSync('create', model, { success: (function() { return ready = true; }) @@ -558,7 +558,7 @@ var response; response = null; runs(function() { - return dualsync('read', model, { + return dualSync('read', model, { success: function() { var callback_args; callback_args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; @@ -581,8 +581,8 @@ model.local = true; model.url = '/bacon/bits'; spyOnLocalsync(); - dualsync(null, model, {}); - return expect(localsync.calls[0].args[2].storeName).toEqual(model.url); + dualSync(null, model, {}); + return expect(localSync.calls[0].args[2].storeName).toEqual(model.url); }); it('prefers the model urlRoot over the url as a store name', function() { model = new ModelWithAlternateIdAttribute(); @@ -590,8 +590,8 @@ model.url = '/bacon/bits'; model.urlRoot = '/bacon'; spyOnLocalsync(); - dualsync(null, model, {}); - return expect(localsync.calls[0].args[2].storeName).toEqual(model.urlRoot); + dualSync(null, model, {}); + return expect(localSync.calls[0].args[2].storeName).toEqual(model.urlRoot); }); it('prefers the collection url over the model urlRoot as a store name', function() { model = new ModelWithAlternateIdAttribute(); @@ -601,8 +601,8 @@ model.collection = new Backbone.Collection(); model.collection.url = '/ranch'; spyOnLocalsync(); - dualsync(null, model, {}); - return expect(localsync.calls[0].args[2].storeName).toEqual(model.collection.url); + dualSync(null, model, {}); + return expect(localSync.calls[0].args[2].storeName).toEqual(model.collection.url); }); it('prefers the model storeName over the collection url as a store name', function() { model = new ModelWithAlternateIdAttribute(); @@ -613,8 +613,8 @@ model.collection.url = '/ranch'; model.storeName = 'melted cheddar'; spyOnLocalsync(); - dualsync(null, model, {}); - return expect(localsync.calls[0].args[2].storeName).toEqual(model.storeName); + dualSync(null, model, {}); + return expect(localSync.calls[0].args[2].storeName).toEqual(model.storeName); }); return it('prefers the collection storeName over the model storeName as a store name', function() { model = new ModelWithAlternateIdAttribute(); @@ -626,8 +626,8 @@ model.storeName = 'melted cheddar'; model.collection.storeName = 'ketchup'; spyOnLocalsync(); - dualsync(null, model, {}); - return expect(localsync.calls[0].args[2].storeName).toEqual(model.collection.storeName); + dualSync(null, model, {}); + return expect(localSync.calls[0].args[2].storeName).toEqual(model.collection.storeName); }); }); diff --git a/spec/integration_spec.coffee b/spec/integration_spec.coffee index 54c74dc..21c64d3 100644 --- a/spec/integration_spec.coffee +++ b/spec/integration_spec.coffee @@ -1,9 +1,9 @@ -{backboneSync, localsync, dualSync, localStorage} = window +{backboneSync, localSync, dualSync, localStorage} = window {collection, model} = {} beforeEach -> backboneSync.calls = [] - localsync 'clear', {}, ignoreCallbacks: true, storeName: 'eyes/' + localSync 'clear', {}, ignoreCallbacks: true, storeName: 'eyes/' collection = new Backbone.Collection id: 123 vision: 'crystal' @@ -18,7 +18,7 @@ describe 'using Backbone.sync directly', -> localStorage.clear() successCallback = jasmine.createSpy('success').andCallFake -> saved = true errorCallback = jasmine.createSpy('error') - dualsync 'create', model, success: successCallback, error: errorCallback + dualSync 'create', model, success: successCallback, error: errorCallback waitsFor (-> saved), "The success callback for 'create' should have been called", 100 runs -> expect(backboneSync.calls.length).toEqual(1) @@ -32,7 +32,7 @@ describe 'using Backbone.sync directly', -> fetched = true expect(resp.vision).toEqual('crystal') errorCallback = jasmine.createSpy('error') - dualsync 'read', model, success: successCallback, error: errorCallback + dualSync 'read', model, success: successCallback, error: errorCallback waitsFor (-> fetched), "The success callback for 'read' should have been called", 100 runs -> expect(backboneSync.calls.length).toEqual(2) diff --git a/spec/integration_spec.js b/spec/integration_spec.js index f1cde22..96b8dab 100644 --- a/spec/integration_spec.js +++ b/spec/integration_spec.js @@ -1,17 +1,17 @@ // Generated by CoffeeScript 1.6.3 (function() { - var backboneSync, collection, dualSync, localStorage, localsync, model, _ref, + var backboneSync, collection, dualSync, localStorage, localSync, model, _ref, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, __slice = [].slice; - backboneSync = window.backboneSync, localsync = window.localsync, dualSync = window.dualSync, localStorage = window.localStorage; + backboneSync = window.backboneSync, localSync = window.localSync, dualSync = window.dualSync, localStorage = window.localStorage; _ref = {}, collection = _ref.collection, model = _ref.model; beforeEach(function() { backboneSync.calls = []; - localsync('clear', {}, { + localSync('clear', {}, { ignoreCallbacks: true, storeName: 'eyes/' }); @@ -34,7 +34,7 @@ return saved = true; }); errorCallback = jasmine.createSpy('error'); - return dualsync('create', model, { + return dualSync('create', model, { success: successCallback, error: errorCallback }); @@ -55,7 +55,7 @@ return expect(resp.vision).toEqual('crystal'); })); errorCallback = jasmine.createSpy('error'); - return dualsync('read', model, { + return dualSync('read', model, { success: successCallback, error: errorCallback }); diff --git a/spec/localsync_spec.coffee b/spec/localsync_spec.coffee index 5a98f0a..882c2cc 100644 --- a/spec/localsync_spec.coffee +++ b/spec/localsync_spec.coffee @@ -1,6 +1,6 @@ -{Store, Backbone, localsync} = window +{Store, Backbone, localSync} = window -describe 'localsync', -> +describe 'localSync', -> describe 'standard Backbone.sync methods', -> describe 'creating records', -> it 'creates records', -> @@ -8,7 +8,7 @@ describe 'localsync', -> runs -> model = new Backbone.Model id: 1 create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve(model) - localsync 'create', model, {success: (-> ready = true), error: (-> ready = true)} + localSync 'create', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(create).toHaveBeenCalledWith model @@ -20,13 +20,13 @@ describe 'localsync', -> create = spyOn(Store.prototype, 'find').andReturn $.Deferred().resolve(id: 1) create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve() model = new Backbone.Model id: 1 - localsync 'create', model, {success: (-> ready = true), error: (-> ready = true), add: true} + localSync 'create', model, {success: (-> ready = true), error: (-> ready = true), add: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> ready = false expect(create).not.toHaveBeenCalled() model = new Backbone.Model id: 1 - localsync 'create', model, {success: (-> ready = true), error: (-> ready = true), add: true, merge: true} + localSync 'create', model, {success: (-> ready = true), error: (-> ready = true), add: true, merge: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(create).toHaveBeenCalled() @@ -37,7 +37,7 @@ describe 'localsync', -> model = new Backbone.Model id: 1 create = spyOn(Store.prototype, 'create').andReturn $.Deferred().resolve(model) dirty = spyOn(Store.prototype, 'dirty').andReturn $.Deferred().resolve(model) - localsync 'create', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} + localSync 'create', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(create).toHaveBeenCalledWith model @@ -49,7 +49,7 @@ describe 'localsync', -> runs -> model = new Backbone.Model id: 1 find = spyOn(Store.prototype, 'find').andReturn $.Deferred().resolve(model) - localsync 'read', model, {success: (-> ready = true), error: (-> ready = true)} + localSync 'read', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(find).toHaveBeenCalledWith model @@ -58,7 +58,7 @@ describe 'localsync', -> {ready, findAll} = {} runs -> findAll = spyOn(Store.prototype, 'findAll').andReturn $.Deferred().resolve() - localsync 'read', new Backbone.Collection, {success: (-> ready = true), error: (-> ready = true)} + localSync 'read', new Backbone.Collection, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(findAll).toHaveBeenCalled() @@ -69,7 +69,7 @@ describe 'localsync', -> runs -> model = new Backbone.Model id: 1 update = spyOn(Store.prototype, 'update').andReturn $.Deferred().resolve(model) - localsync 'update', model, {success: (-> ready = true), error: (-> ready = true)} + localSync 'update', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(update).toHaveBeenCalledWith model @@ -80,7 +80,7 @@ describe 'localsync', -> model = new Backbone.Model id: 1 update = spyOn(Store.prototype, 'update').andReturn $.Deferred().resolve(model) dirty = spyOn(Store.prototype, 'dirty').andReturn $.Deferred().resolve(model) - localsync 'update', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} + localSync 'update', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(update).toHaveBeenCalledWith model @@ -92,7 +92,7 @@ describe 'localsync', -> runs -> model = new Backbone.Model id: 1 destroy = spyOn(Store.prototype, 'destroy').andReturn $.Deferred().resolve(model) - localsync 'delete', model, {success: (-> ready = true), error: (-> ready = true)} + localSync 'delete', model, {success: (-> ready = true), error: (-> ready = true)} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(destroy).toHaveBeenCalledWith model @@ -103,7 +103,7 @@ describe 'localsync', -> model = new Backbone.Model id: 1 destroy = spyOn(Store.prototype, 'destroy').andReturn $.Deferred().resolve(model) destroyed = spyOn(Store.prototype, 'destroyed').andReturn $.Deferred().resolve(model) - localsync 'delete', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} + localSync 'delete', model, {success: (-> ready = true), error: (-> ready = true), dirty: true} waitsFor (-> ready), "A callback should have been called", 100 runs -> expect(destroy).toHaveBeenCalledWith model @@ -113,19 +113,19 @@ describe 'localsync', -> it 'clears out all records from the store', -> runs -> clear = spyOn(Store.prototype, 'clear').andReturn $.Deferred().resolve() - localsync 'clear', {}, {success: (-> ready = true), error: (-> ready = true)} + localSync 'clear', {}, {success: (-> ready = true), error: (-> ready = true)} it 'reports whether or not it hasDirtyOrDestroyed', -> runs -> clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed').andReturn $.Deferred().resolve() - localsync 'hasDirtyOrDestroyed', {}, {success: (-> ready = true), error: (-> ready = true)} + localSync 'hasDirtyOrDestroyed', {}, {success: (-> ready = true), error: (-> ready = true)} describe 'callbacks', -> it "sends the models's attributes as the callback response", -> {model, response} = {} runs -> model = new Backbone.Model id: 1 - localsync 'create', model, {success: ((resp) -> response = resp)} + localSync 'create', model, {success: ((resp) -> response = resp)} waitsFor (-> response), "A callback should have been called with a response", 100 runs -> expect(response).toBe model.attributes @@ -135,13 +135,13 @@ describe 'localsync', -> runs -> callback = jasmine.createSpy 'callback' model = new Backbone.Model id: 1 - localsync 'create', model, {success: callback, error: callback, ignoreCallbacks: true} + localSync 'create', model, {success: callback, error: callback, ignoreCallbacks: true} waitsFor (-> new Date().getTime() - start > 5), 'Wait 5 ms to give the callback a chance to execute', 100 runs -> start = false expect(callback).not.toHaveBeenCalled() model = new Backbone.Model id: 1 - localsync 'create', model, {success: callback, error: callback} + localSync 'create', model, {success: callback, error: callback} waitsFor (-> callback.wasCalled), 'The callback should have been called', 100 describe 'model parameter', -> @@ -150,21 +150,21 @@ describe 'localsync', -> it 'should not accept objects / attributes as model', -> attributes = {} - call = -> localsync 'create', attributes, {ignoreCallbacks: true} + call = -> localSync 'create', attributes, {ignoreCallbacks: true} expect(call).toThrow() it 'should accept a backbone model as model', -> - call = -> localsync 'create', new Backbone.Model, {ignoreCallbacks: true} + call = -> localSync 'create', new Backbone.Model, {ignoreCallbacks: true} expect(call).not.toThrow() it 'should accept a backbone collection as model', -> - call = -> localsync 'create', new Backbone.Collection, {ignoreCallbacks: true} + call = -> localSync 'create', new Backbone.Collection, {ignoreCallbacks: true} expect(call).not.toThrow() it 'should accept any object as model on extra method "clear"', -> - call = -> localsync 'clear', {}, {ignoreCallbacks: true} + call = -> localSync 'clear', {}, {ignoreCallbacks: true} expect(call).not.toThrow() it 'should accept any object as model on extra method "hasDirtyOrDestroyed"', -> - call = -> localsync 'hasDirtyOrDestroyed', {}, {ignoreCallbacks: true} + call = -> localSync 'hasDirtyOrDestroyed', {}, {ignoreCallbacks: true} expect(call).not.toThrow() diff --git a/spec/localsync_spec.js b/spec/localsync_spec.js index 3c99619..e58588e 100644 --- a/spec/localsync_spec.js +++ b/spec/localsync_spec.js @@ -1,10 +1,10 @@ // Generated by CoffeeScript 1.6.3 (function() { - var Backbone, Store, localsync; + var Backbone, Store, localSync; - Store = window.Store, Backbone = window.Backbone, localsync = window.localsync; + Store = window.Store, Backbone = window.Backbone, localSync = window.localSync; - describe('localsync', function() { + describe('localSync', function() { describe('standard Backbone.sync methods', function() { describe('creating records', function() { it('creates records', function() { @@ -15,7 +15,7 @@ id: 1 }); create = spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve(model)); - return localsync('create', model, { + return localSync('create', model, { success: (function() { return ready = true; }), @@ -44,7 +44,7 @@ model = new Backbone.Model({ id: 1 }); - return localsync('create', model, { + return localSync('create', model, { success: (function() { return ready = true; }), @@ -64,7 +64,7 @@ model = new Backbone.Model({ id: 1 }); - return localsync('create', model, { + return localSync('create', model, { success: (function() { return ready = true; }), @@ -91,7 +91,7 @@ }); create = spyOn(Store.prototype, 'create').andReturn($.Deferred().resolve(model)); dirty = spyOn(Store.prototype, 'dirty').andReturn($.Deferred().resolve(model)); - return localsync('create', model, { + return localSync('create', model, { success: (function() { return ready = true; }), @@ -119,7 +119,7 @@ id: 1 }); find = spyOn(Store.prototype, 'find').andReturn($.Deferred().resolve(model)); - return localsync('read', model, { + return localSync('read', model, { success: (function() { return ready = true; }), @@ -140,7 +140,7 @@ _ref = {}, ready = _ref.ready, findAll = _ref.findAll; runs(function() { findAll = spyOn(Store.prototype, 'findAll').andReturn($.Deferred().resolve()); - return localsync('read', new Backbone.Collection, { + return localSync('read', new Backbone.Collection, { success: (function() { return ready = true; }), @@ -166,7 +166,7 @@ id: 1 }); update = spyOn(Store.prototype, 'update').andReturn($.Deferred().resolve(model)); - return localsync('update', model, { + return localSync('update', model, { success: (function() { return ready = true; }), @@ -191,7 +191,7 @@ }); update = spyOn(Store.prototype, 'update').andReturn($.Deferred().resolve(model)); dirty = spyOn(Store.prototype, 'dirty').andReturn($.Deferred().resolve(model)); - return localsync('update', model, { + return localSync('update', model, { success: (function() { return ready = true; }), @@ -219,7 +219,7 @@ id: 1 }); destroy = spyOn(Store.prototype, 'destroy').andReturn($.Deferred().resolve(model)); - return localsync('delete', model, { + return localSync('delete', model, { success: (function() { return ready = true; }), @@ -244,7 +244,7 @@ }); destroy = spyOn(Store.prototype, 'destroy').andReturn($.Deferred().resolve(model)); destroyed = spyOn(Store.prototype, 'destroyed').andReturn($.Deferred().resolve(model)); - return localsync('delete', model, { + return localSync('delete', model, { success: (function() { return ready = true; }), @@ -269,7 +269,7 @@ return runs(function() { var clear; clear = spyOn(Store.prototype, 'clear').andReturn($.Deferred().resolve()); - return localsync('clear', {}, { + return localSync('clear', {}, { success: (function() { var ready; return ready = true; @@ -285,7 +285,7 @@ return runs(function() { var clear; clear = spyOn(Store.prototype, 'hasDirtyOrDestroyed').andReturn($.Deferred().resolve()); - return localsync('hasDirtyOrDestroyed', {}, { + return localSync('hasDirtyOrDestroyed', {}, { success: (function() { var ready; return ready = true; @@ -306,7 +306,7 @@ model = new Backbone.Model({ id: 1 }); - return localsync('create', model, { + return localSync('create', model, { success: (function(resp) { return response = resp; }) @@ -330,7 +330,7 @@ model = new Backbone.Model({ id: 1 }); - return localsync('create', model, { + return localSync('create', model, { success: callback, error: callback, ignoreCallbacks: true @@ -346,7 +346,7 @@ model = new Backbone.Model({ id: 1 }); - return localsync('create', model, { + return localSync('create', model, { success: callback, error: callback }); @@ -364,7 +364,7 @@ var attributes, call; attributes = {}; call = function() { - return localsync('create', attributes, { + return localSync('create', attributes, { ignoreCallbacks: true }); }; @@ -373,7 +373,7 @@ it('should accept a backbone model as model', function() { var call; call = function() { - return localsync('create', new Backbone.Model, { + return localSync('create', new Backbone.Model, { ignoreCallbacks: true }); }; @@ -382,7 +382,7 @@ it('should accept a backbone collection as model', function() { var call; call = function() { - return localsync('create', new Backbone.Collection, { + return localSync('create', new Backbone.Collection, { ignoreCallbacks: true }); }; @@ -391,7 +391,7 @@ it('should accept any object as model on extra method "clear"', function() { var call; call = function() { - return localsync('clear', {}, { + return localSync('clear', {}, { ignoreCallbacks: true }); }; @@ -400,7 +400,7 @@ return it('should accept any object as model on extra method "hasDirtyOrDestroyed"', function() { var call; call = function() { - return localsync('hasDirtyOrDestroyed', {}, { + return localSync('hasDirtyOrDestroyed', {}, { ignoreCallbacks: true }); }; From 29c3da879031439420df09ff7fe9dc21c53e1fb0 Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Wed, 15 Jan 2014 09:26:20 -0300 Subject: [PATCH 6/8] Added dirtyName and destroyedName cache vars in Store (DRY). --- backbone.dualstorage.amd.js | 14 ++++++++------ backbone.dualstorage.coffee | 14 ++++++++------ backbone.dualstorage.js | 14 ++++++++------ spec/backbone.dualstorage.js | 14 ++++++++------ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/backbone.dualstorage.amd.js b/backbone.dualstorage.amd.js index ecb834b..bd54f78 100644 --- a/backbone.dualstorage.amd.js +++ b/backbone.dualstorage.amd.js @@ -132,6 +132,8 @@ window.Store = (function() { function Store(name) { this.name = name; + this.dirtyName = "" + name + "_dirty"; + this.destroyedName = "" + name + "_destroyed"; this.records = []; } @@ -158,10 +160,10 @@ window.Store = (function() { Store.prototype.dirty = function(model) { var _this = this; - return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + return this.recordsOn(this.dirtyName).then(function(dirtyRecords) { if (!_.include(dirtyRecords, model.id.toString())) { dirtyRecords.push(model.id.toString()); - return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return Backbone.storageAdapter.setItem(_this.dirtyName, dirtyRecords.join(',')).then(function() { return model; }); } @@ -185,10 +187,10 @@ window.Store = (function() { Store.prototype.destroyed = function(model) { var _this = this; - return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + return this.recordsOn(this.destroyedName).then(function(destroyedRecords) { if (!_.include(destroyedRecords, model.id.toString())) { destroyedRecords.push(model.id.toString()); - Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + Backbone.storageAdapter.setItem(_this.destroyedName, destroyedRecords.join(',')).then(function() { return model; }); } @@ -245,8 +247,8 @@ window.Store = (function() { Store.prototype.hasDirtyOrDestroyed = function() { var _this = this; - return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { - return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return Backbone.storageAdapter.getItem(this.dirtyName).then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.destroyedName).then(function(destroyed) { return !_.isEmpty(dirty) || !_.isEmpty(destroyed); }); }); diff --git a/backbone.dualstorage.coffee b/backbone.dualstorage.coffee index 339cbc3..bf23fc2 100644 --- a/backbone.dualstorage.coffee +++ b/backbone.dualstorage.coffee @@ -62,6 +62,8 @@ class window.Store constructor: (name) -> @name = name + @dirtyName = "#{name}_dirty" + @destroyedName = "#{name}_destroyed" @records = [] initialize: -> @@ -83,10 +85,10 @@ class window.Store (store and store.split(',')) or [] dirty: (model) -> - @recordsOn(@name + '_dirty').then (dirtyRecords) => + @recordsOn(@dirtyName).then (dirtyRecords) => if not _.include(dirtyRecords, model.id.toString()) dirtyRecords.push model.id.toString() - return Backbone.storageAdapter.setItem(@name + '_dirty', dirtyRecords.join(',')).then -> model + return Backbone.storageAdapter.setItem(@dirtyName, dirtyRecords.join(',')).then -> model model clean: (model, from) -> @@ -97,10 +99,10 @@ class window.Store model destroyed: (model) -> - @recordsOn(@name + '_destroyed').then (destroyedRecords) => + @recordsOn(@destroyedName).then (destroyedRecords) => if not _.include destroyedRecords, model.id.toString() destroyedRecords.push model.id.toString() - Backbone.storageAdapter.setItem(@name + '_destroyed', destroyedRecords.join(',')).then -> model + Backbone.storageAdapter.setItem(@destroyedName, destroyedRecords.join(',')).then -> model model # Add a model, giving it a unique GUID, if it doesn't already @@ -127,8 +129,8 @@ class window.Store @save() hasDirtyOrDestroyed: -> - Backbone.storageAdapter.getItem(@name + '_dirty').then (dirty) => - Backbone.storageAdapter.getItem(@name + '_destroyed').then (destroyed) => + Backbone.storageAdapter.getItem(@dirtyName).then (dirty) => + Backbone.storageAdapter.getItem(@destroyedName).then (destroyed) => not _.isEmpty(dirty) or not _.isEmpty(destroyed) # Retrieve a model from `this.data` by id. diff --git a/backbone.dualstorage.js b/backbone.dualstorage.js index 72c34ba..caf7379 100644 --- a/backbone.dualstorage.js +++ b/backbone.dualstorage.js @@ -123,6 +123,8 @@ as that. function Store(name) { this.name = name; + this.dirtyName = "" + name + "_dirty"; + this.destroyedName = "" + name + "_destroyed"; this.records = []; } @@ -149,10 +151,10 @@ as that. Store.prototype.dirty = function(model) { var _this = this; - return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + return this.recordsOn(this.dirtyName).then(function(dirtyRecords) { if (!_.include(dirtyRecords, model.id.toString())) { dirtyRecords.push(model.id.toString()); - return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return Backbone.storageAdapter.setItem(_this.dirtyName, dirtyRecords.join(',')).then(function() { return model; }); } @@ -176,10 +178,10 @@ as that. Store.prototype.destroyed = function(model) { var _this = this; - return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + return this.recordsOn(this.destroyedName).then(function(destroyedRecords) { if (!_.include(destroyedRecords, model.id.toString())) { destroyedRecords.push(model.id.toString()); - Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + Backbone.storageAdapter.setItem(_this.destroyedName, destroyedRecords.join(',')).then(function() { return model; }); } @@ -236,8 +238,8 @@ as that. Store.prototype.hasDirtyOrDestroyed = function() { var _this = this; - return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { - return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return Backbone.storageAdapter.getItem(this.dirtyName).then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.destroyedName).then(function(destroyed) { return !_.isEmpty(dirty) || !_.isEmpty(destroyed); }); }); diff --git a/spec/backbone.dualstorage.js b/spec/backbone.dualstorage.js index cf09f77..a941180 100644 --- a/spec/backbone.dualstorage.js +++ b/spec/backbone.dualstorage.js @@ -121,6 +121,8 @@ window.Store = (function() { function Store(name) { this.name = name; + this.dirtyName = "" + name + "_dirty"; + this.destroyedName = "" + name + "_destroyed"; this.records = []; } @@ -147,10 +149,10 @@ window.Store = (function() { Store.prototype.dirty = function(model) { var _this = this; - return this.recordsOn(this.name + '_dirty').then(function(dirtyRecords) { + return this.recordsOn(this.dirtyName).then(function(dirtyRecords) { if (!_.include(dirtyRecords, model.id.toString())) { dirtyRecords.push(model.id.toString()); - return Backbone.storageAdapter.setItem(_this.name + '_dirty', dirtyRecords.join(',')).then(function() { + return Backbone.storageAdapter.setItem(_this.dirtyName, dirtyRecords.join(',')).then(function() { return model; }); } @@ -174,10 +176,10 @@ window.Store = (function() { Store.prototype.destroyed = function(model) { var _this = this; - return this.recordsOn(this.name + '_destroyed').then(function(destroyedRecords) { + return this.recordsOn(this.destroyedName).then(function(destroyedRecords) { if (!_.include(destroyedRecords, model.id.toString())) { destroyedRecords.push(model.id.toString()); - Backbone.storageAdapter.setItem(_this.name + '_destroyed', destroyedRecords.join(',')).then(function() { + Backbone.storageAdapter.setItem(_this.destroyedName, destroyedRecords.join(',')).then(function() { return model; }); } @@ -234,8 +236,8 @@ window.Store = (function() { Store.prototype.hasDirtyOrDestroyed = function() { var _this = this; - return Backbone.storageAdapter.getItem(this.name + '_dirty').then(function(dirty) { - return Backbone.storageAdapter.getItem(_this.name + '_destroyed').then(function(destroyed) { + return Backbone.storageAdapter.getItem(this.dirtyName).then(function(dirty) { + return Backbone.storageAdapter.getItem(_this.destroyedName).then(function(destroyed) { return !_.isEmpty(dirty) || !_.isEmpty(destroyed); }); }); From 8ed599ffefbae22671105aa59511d7e0fd2fc9ea Mon Sep 17 00:00:00 2001 From: Joni Bekenstein Date: Wed, 15 Jan 2014 10:17:54 -0300 Subject: [PATCH 7/8] Organized adapters in separate coffee file. Updated Makefile. Added some tests for StickyStorageAdapter. --- Makefile | 7 +- SpecRunner.html | 2 + adapters.js | 82 -- backbone.dualstorage.adapters.coffee | 52 ++ backbone.dualstorage.amd.js | 1079 ++++++++++++++------------ backbone.dualstorage.coffee | 35 +- backbone.dualstorage.js | 95 ++- lib/sticky-2.9.js | 1023 ++++++++++++++++++++++++ spec/backbone.dualstorage.js | 94 ++- spec/stickystorage_spec.coffee | 30 + spec/stickystorage_spec.js | 56 ++ 11 files changed, 1906 insertions(+), 649 deletions(-) delete mode 100644 adapters.js create mode 100644 backbone.dualstorage.adapters.coffee create mode 100644 lib/sticky-2.9.js create mode 100644 spec/stickystorage_spec.coffee create mode 100644 spec/stickystorage_spec.js diff --git a/Makefile b/Makefile index 521b9dd..efeb6d5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ compile: - coffee -c backbone.dualstorage.coffee spec/*.coffee - coffee -cbo spec backbone.dualstorage.coffee - cat amd.header.js spec/backbone.dualstorage.js amd.footer.js > backbone.dualstorage.amd.js + coffee -cj backbone.dualstorage.js backbone.dualstorage.adapters.coffee backbone.dualstorage.coffee + coffee -c spec/*.coffee + coffee -cbj spec/backbone.dualstorage.js backbone.dualstorage.adapters.coffee backbone.dualstorage.coffee + cat amd.header.js backbone.dualstorage.js amd.footer.js > backbone.dualstorage.amd.js infinite: compile read # (press Enter to recompile) diff --git a/SpecRunner.html b/SpecRunner.html index f879d56..0a1398a 100644 --- a/SpecRunner.html +++ b/SpecRunner.html @@ -9,6 +9,7 @@ + @@ -23,6 +24,7 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test.adapters.coffee b/test/test.adapters.coffee new file mode 100644 index 0000000..21e4467 --- /dev/null +++ b/test/test.adapters.coffee @@ -0,0 +1,50 @@ +StickyStorage = Backbone.storageAdapters.StickyStorageAdapter + +describe 'StorageAdapters', -> + for storageName, storageClass of Backbone.storageAdapters + do (storageName, storageClass) -> + _describe = if storageClass is StickyStorage then describe.skip else describe + _describe storageName, -> + + it 'takes a name in its constructor and initializes', (done) -> + storage = new storageClass 'SuperSizedStorage' + storage.initialize().done -> + expect(storage.name).to.equal 'SuperSizedStorage' + expect(storage.store).to.exist + storage.clear().done -> + done() + + _describe 'methods', -> + {storage} = {} + + beforeEach (done) -> + storage = new storageClass + storage.initialize().done -> + done() + + afterEach (done) -> + storage.clear().done -> + done() + + it 'sets and gets a record', (done) -> + storage.set('item', foo: 'bar').done -> + storage.get('item').done (item) -> + expect(item).to.eql foo: 'bar' + done() + + it 'deletes a record', (done) -> + storage.set('item', foo: 'bar').done -> + storage.remove('item').done -> + storage.get('item').done (item) -> + expect(item).to.not.ok + done() + + it 'clears all records', (done) -> + storage.set('item1', 1).done -> + storage.set('item2', 2).done -> + storage.clear().done -> + storage.get('item1').done (item1) -> + expect(item1).to.not.ok + storage.get('item2').done (item2) -> + expect(item2).to.not.ok + done() diff --git a/test/test.adapters.js b/test/test.adapters.js new file mode 100644 index 0000000..619f771 --- /dev/null +++ b/test/test.adapters.js @@ -0,0 +1,85 @@ +// Generated by CoffeeScript 1.6.3 +var StickyStorage; + +StickyStorage = Backbone.storageAdapters.StickyStorageAdapter; + +describe('StorageAdapters', function() { + var storageClass, storageName, _ref, _results; + _ref = Backbone.storageAdapters; + _results = []; + for (storageName in _ref) { + storageClass = _ref[storageName]; + _results.push((function(storageName, storageClass) { + var _describe; + _describe = storageClass === StickyStorage ? describe.skip : describe; + return _describe(storageName, function() { + it('takes a name in its constructor and initializes', function(done) { + var storage; + storage = new storageClass('SuperSizedStorage'); + return storage.initialize().done(function() { + expect(storage.name).to.equal('SuperSizedStorage'); + expect(storage.store).to.exist; + return storage.clear().done(function() { + return done(); + }); + }); + }); + return _describe('methods', function() { + var storage; + storage = {}.storage; + beforeEach(function(done) { + storage = new storageClass; + return storage.initialize().done(function() { + return done(); + }); + }); + afterEach(function(done) { + return storage.clear().done(function() { + return done(); + }); + }); + it('sets and gets a record', function(done) { + return storage.set('item', { + foo: 'bar' + }).done(function() { + return storage.get('item').done(function(item) { + expect(item).to.eql({ + foo: 'bar' + }); + return done(); + }); + }); + }); + it('deletes a record', function(done) { + return storage.set('item', { + foo: 'bar' + }).done(function() { + return storage.remove('item').done(function() { + return storage.get('item').done(function(item) { + expect(item).to.not.ok; + return done(); + }); + }); + }); + }); + return it('clears all records', function(done) { + return storage.set('item1', 1).done(function() { + return storage.set('item2', 2).done(function() { + return storage.clear().done(function() { + return storage.get('item1').done(function(item1) { + expect(item1).to.not.ok; + return storage.get('item2').done(function(item2) { + expect(item2).to.not.ok; + return done(); + }); + }); + }); + }); + }); + }); + }); + }); + })(storageName, storageClass)); + } + return _results; +}); diff --git a/test/test.localSync.coffee b/test/test.localSync.coffee new file mode 100644 index 0000000..d1d02aa --- /dev/null +++ b/test/test.localSync.coffee @@ -0,0 +1,25 @@ +describe 'localSync', -> + for storageName, storageClass of Backbone.storageAdapters + do (storageName, storageClass) -> + + _describe = if storageClass is StickyStorage then describe.skip else describe + _describe "with #{storageName}", -> + {storage, store, model} = {} + + beforeEach (done) -> + storage = new storageClass + store = new Backbone.Store 'Store', storage + model = new Backbone.Model + id: 1 + abc: 123 + storage.initialize().done -> + store.initialize().done -> done() + + afterEach (done) -> + store.clear().done -> + storage.clear().done -> + done() + + it 'works correctly when merge == false', (done) -> + + Backbone. \ No newline at end of file diff --git a/test/test.localSync.js b/test/test.localSync.js new file mode 100644 index 0000000..357240d --- /dev/null +++ b/test/test.localSync.js @@ -0,0 +1,39 @@ +// Generated by CoffeeScript 1.6.3 +describe('localSync', function() { + var storageClass, storageName, _ref, _results; + _ref = Backbone.storageAdapters; + _results = []; + for (storageName in _ref) { + storageClass = _ref[storageName]; + _results.push((function(storageName, storageClass) { + var _describe; + _describe = storageClass === StickyStorage ? describe.skip : describe; + return _describe("with " + storageName, function() { + var model, storage, store, _ref1; + _ref1 = {}, storage = _ref1.storage, store = _ref1.store, model = _ref1.model; + beforeEach(function(done) { + storage = new storageClass; + store = new Backbone.Store('Store', storage); + model = new Backbone.Model({ + id: 1, + abc: 123 + }); + return storage.initialize().done(function() { + return store.initialize().done(function() { + return done(); + }); + }); + }); + afterEach(function(done) { + return store.clear().done(function() { + return storage.clear().done(function() { + return done(); + }); + }); + }); + return it('works correctly when merge == false', function(done) {}); + }); + })(storageName, storageClass)); + } + return _results; +}); diff --git a/test/test.store.coffee b/test/test.store.coffee new file mode 100644 index 0000000..41627d7 --- /dev/null +++ b/test/test.store.coffee @@ -0,0 +1,301 @@ +describe 'Store', -> + for storageName, storageClass of Backbone.storageAdapters + do (storageName, storageClass) -> + + _describe = if storageClass is StickyStorage then describe.skip else describe + _describe "with #{storageName}", -> + {storage} = {} + + beforeEach (done) -> + storage = new storageClass + storage.initialize().done -> + done() + + afterEach (done) -> + storage.clear().done -> + done() + + it 'takes a name and a storage adapter in its constructor', -> + store = new Backbone.Store 'Store', storage + + it 'initializes correctly', (done) -> + store = new Backbone.Store 'Store', storage + expect(store.data).to.deep.eql {} + store.initialize().done -> + expect(store.data.dirty).to.eql [] + expect(store.data.destroyed).to.eql [] + expect(store.data.local).to.eql [] + expect(store.data.records).to.eql [] + expect(store.data.recordsById).to.eql {} + done() + + {model, otherModel, store} = {} + storeSetup = (done) -> + model = new Backbone.Model + id: 1 + abc: 123 + otherModel = new Backbone.Model + id: 2 + xxx: 666 + store = new Backbone.Store 'Store', storage + store.initialize().done -> done() + + storeClear = (done) -> + store.clear().done -> done() + + describe 'when adding', -> + beforeEach storeSetup + afterEach storeClear + + it 'adds one model to the store', (done) -> + store.add(model).done (saved) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(saved.attributes).to.deep.eql model.attributes + expect(saved.attributes).to.deep.eql storeData.recordsById[1] + expect(store.data).to.deep.eql + records: [1] + recordsById: 1: model.attributes + local: [] + dirty: [] + destroyed: [] + done() + + it 'adds many models to the store', (done) -> + models = [model, otherModel] + store.add(models).done (saved) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(saved[0].attributes).to.deep.eql model.attributes + expect(saved[0].attributes).to.deep.eql storeData.recordsById[1] + expect(saved[1].attributes).to.deep.eql otherModel.attributes + expect(saved[1].attributes).to.deep.eql storeData.recordsById[2] + expect(store.data).to.deep.eql + records: [1, 2] + recordsById: + 1: model.attributes + 2: otherModel.attributes + local: [] + dirty: [] + destroyed: [] + done() + + it 'adds the model creating a unique id using idAttribute', (done) -> + model = new Backbone.Model + abc: 123 + model.idAttribute = '_id' + store.add(model).done (saved) -> + storage.get('Store').done (storeData) -> + expectedAttr = abc: 123, _id: model.id + expect(storeData).to.deep.eql store.data + expect(saved.attributes).to.deep.eql expectedAttr + expect(saved.attributes).to.deep.eql storeData.recordsById[model.id] + expect(saved.id).to.equal model.id + recordsById = {} + recordsById[model.id] = model.attributes + expect(store.data).to.deep.eql + records: [model.id] + recordsById: recordsById + local: [model.id] + dirty: [] + destroyed: [] + done() + + it 'throws an error if the model is already in the store', (done) -> + store.add(model).done (saved) -> + expect(-> store.add(model)).to.throw(/already in the store/) + done() + + describe 'when updating', -> + beforeEach storeSetup + afterEach storeClear + + it 'updates the model in the store', (done) -> + store.add(model).done -> + model.set abc: 666 + store.update(model).done (updated) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(updated.attributes).to.deep.eql model.attributes + expect(updated.attributes).to.deep.eql storeData.recordsById[1] + expect(store.data).to.deep.eql + records: [1] + recordsById: {1: model.attributes} + local: [] + dirty: [] + destroyed: [] + done() + + it 'throws an error if the model does not have an id', -> + model = new Backbone.Model + expect(-> store.update(model)).to.throw(/model does not have an id/) + + it 'throws an error if the model is not in the store', -> + expect(-> store.update(model)).to.throw(/is not in the store/) + + describe 'when removing', -> + beforeEach storeSetup + afterEach storeClear + + it 'removes the model from the store', (done) -> + store.add(model).done -> + store.remove(model).done (removed) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(removed.attributes).to.deep.eql model.attributes + expect(store.data).to.deep.eql + records: [] + recordsById: {} + local: [] + dirty: [] + destroyed: [] + done() + + it 'removes a local model from the store', (done) -> + model = new Backbone.Model + store.add(model).done -> + store.remove(model).done () -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(store.data).to.deep.eql + records: [] + recordsById: {} + local: [] + dirty: [] + destroyed: [] + done() + + it 'removes the model from the dirty list', (done) -> + store.add(model).done -> + store.isDirty(model, true).done -> + store.remove(model).done () -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(store.data).to.deep.eql + records: [] + recordsById: {} + local: [] + dirty: [] + destroyed: [] + done() + + it 'clears the store removing all saved data', (done) -> + store.add(model).done -> + store.isDirty(model, true).done -> + store.isDestroyed(otherModel, true).done -> + store.clear().done -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql {} + expect(store.data).to.deep.eql {} + done() + + describe 'when searching', -> + beforeEach storeSetup + afterEach storeClear + + it 'finds a model', (done) -> + store.add(model).done -> + expect(store.find 1).to.deep.eql model.attributes + done() + + it 'finds all models', (done) -> + store.add(model).done -> + store.add(otherModel).done -> + models = [model.attributes, otherModel.attributes] + expect(store.find()).to.deep.eql models + done() + + it 'finds all dirty models', (done) -> + store.add(model).done -> + store.isDirty(model, true).done -> + expect(store.findDirty()).to.deep.eql [model.attributes] + done() + + it 'finds all destroyed models', (done) -> + store.isDestroyed(model, true).done -> + expect(store.findDestroyed()).to.deep.eql [model.id] + done() + + it 'returns correct values when model is not found', -> + expect(store.find 1).to.be.undefined + expect(store.findDirty 1).to.be.undefined + expect(store.findDestroyed 1).to.be.false + + describe 'dirty / destroyed flags', (done) -> + beforeEach storeSetup + afterEach storeClear + + it 'marks a model as dirty', (done) -> + store.add(model).done -> + store.isDirty(model, true).done (dirtyModel) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(dirtyModel.attributes).to.deep.eql model.attributes + expect(store.data).to.deep.eql + records: [1] + recordsById: {1: model.attributes} + local: [] + dirty: [1] + destroyed: [] + done() + + it 'marks a model as destroyed', (done) -> + store.isDestroyed(model, true).done (destroyedModel) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(destroyedModel.attributes).to.deep.eql model.attributes + expect(store.data).to.deep.eql + records: [] + recordsById: {} + local: [] + dirty: [] + destroyed: [1] + done() + + it 'unmarks a model as dirty', (done) -> + store.add(model).done -> + store.isDirty(model, true).done () -> + store.isDirty(model, false).done (dirtyModel) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(dirtyModel.attributes).to.deep.eql model.attributes + expect(store.data).to.deep.eql + records: [1] + recordsById: {1: model.attributes} + local: [] + dirty: [] + destroyed: [] + done() + + it 'unmarks a model as destroyed', (done) -> + store.isDestroyed(model, true).done () -> + store.isDestroyed(model, false).done (dirtyModel) -> + storage.get('Store').done (storeData) -> + expect(storeData).to.deep.eql store.data + expect(dirtyModel.attributes).to.deep.eql model.attributes + expect(store.data).to.deep.eql + records: [] + recordsById: {} + local: [] + dirty: [] + destroyed: [] + done() + + it 'reports correctly if there are dirty models', (done) -> + store.add(model).done -> + store.isDirty(model, true).done -> + expect(store.hasDirtyOrDestroyed).to.be.ok + done() + + it 'reports correctly if there are destroyed models', (done) -> + store.isDestroyed(model, true).done -> + expect(store.hasDirtyOrDestroyed).to.be.ok + done() + + it 'throws an error if the model is not in the store', -> + expect(-> store.isDirty(model, true)).to.throw(/is not in the store/) + + it 'throws an error if trying to mark a stored model as destroyed', (done) -> + store.add(model).done -> + expect(-> store.isDestroyed(model, true)).to.throw(/is still in the store/) + done() diff --git a/test/test.store.js b/test/test.store.js new file mode 100644 index 0000000..9b95724 --- /dev/null +++ b/test/test.store.js @@ -0,0 +1,409 @@ +// Generated by CoffeeScript 1.6.3 +describe('Store', function() { + var storageClass, storageName, _ref, _results; + _ref = Backbone.storageAdapters; + _results = []; + for (storageName in _ref) { + storageClass = _ref[storageName]; + _results.push((function(storageName, storageClass) { + var _describe; + _describe = storageClass === StickyStorage ? describe.skip : describe; + return _describe("with " + storageName, function() { + var model, otherModel, storage, store, storeClear, storeSetup, _ref1; + storage = {}.storage; + beforeEach(function(done) { + storage = new storageClass; + return storage.initialize().done(function() { + return done(); + }); + }); + afterEach(function(done) { + return storage.clear().done(function() { + return done(); + }); + }); + it('takes a name and a storage adapter in its constructor', function() { + var store; + return store = new Backbone.Store('Store', storage); + }); + it('initializes correctly', function(done) { + var store; + store = new Backbone.Store('Store', storage); + expect(store.data).to.deep.eql({}); + return store.initialize().done(function() { + expect(store.data.dirty).to.eql([]); + expect(store.data.destroyed).to.eql([]); + expect(store.data.local).to.eql([]); + expect(store.data.records).to.eql([]); + expect(store.data.recordsById).to.eql({}); + return done(); + }); + }); + _ref1 = {}, model = _ref1.model, otherModel = _ref1.otherModel, store = _ref1.store; + storeSetup = function(done) { + model = new Backbone.Model({ + id: 1, + abc: 123 + }); + otherModel = new Backbone.Model({ + id: 2, + xxx: 666 + }); + store = new Backbone.Store('Store', storage); + return store.initialize().done(function() { + return done(); + }); + }; + storeClear = function(done) { + return store.clear().done(function() { + return done(); + }); + }; + describe('when adding', function() { + beforeEach(storeSetup); + afterEach(storeClear); + it('adds one model to the store', function(done) { + return store.add(model).done(function(saved) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(saved.attributes).to.deep.eql(model.attributes); + expect(saved.attributes).to.deep.eql(storeData.recordsById[1]); + expect(store.data).to.deep.eql({ + records: [1], + recordsById: { + 1: model.attributes + }, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + it('adds many models to the store', function(done) { + var models; + models = [model, otherModel]; + return store.add(models).done(function(saved) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(saved[0].attributes).to.deep.eql(model.attributes); + expect(saved[0].attributes).to.deep.eql(storeData.recordsById[1]); + expect(saved[1].attributes).to.deep.eql(otherModel.attributes); + expect(saved[1].attributes).to.deep.eql(storeData.recordsById[2]); + expect(store.data).to.deep.eql({ + records: [1, 2], + recordsById: { + 1: model.attributes, + 2: otherModel.attributes + }, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + it('adds the model creating a unique id using idAttribute', function(done) { + model = new Backbone.Model({ + abc: 123 + }); + model.idAttribute = '_id'; + return store.add(model).done(function(saved) { + return storage.get('Store').done(function(storeData) { + var expectedAttr, recordsById; + expectedAttr = { + abc: 123, + _id: model.id + }; + expect(storeData).to.deep.eql(store.data); + expect(saved.attributes).to.deep.eql(expectedAttr); + expect(saved.attributes).to.deep.eql(storeData.recordsById[model.id]); + expect(saved.id).to.equal(model.id); + recordsById = {}; + recordsById[model.id] = model.attributes; + expect(store.data).to.deep.eql({ + records: [model.id], + recordsById: recordsById, + local: [model.id], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + return it('throws an error if the model is already in the store', function(done) { + return store.add(model).done(function(saved) { + expect(function() { + return store.add(model); + }).to["throw"](/already in the store/); + return done(); + }); + }); + }); + describe('when updating', function() { + beforeEach(storeSetup); + afterEach(storeClear); + it('updates the model in the store', function(done) { + return store.add(model).done(function() { + model.set({ + abc: 666 + }); + return store.update(model).done(function(updated) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(updated.attributes).to.deep.eql(model.attributes); + expect(updated.attributes).to.deep.eql(storeData.recordsById[1]); + expect(store.data).to.deep.eql({ + records: [1], + recordsById: { + 1: model.attributes + }, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + it('throws an error if the model does not have an id', function() { + model = new Backbone.Model; + return expect(function() { + return store.update(model); + }).to["throw"](/model does not have an id/); + }); + return it('throws an error if the model is not in the store', function() { + return expect(function() { + return store.update(model); + }).to["throw"](/is not in the store/); + }); + }); + describe('when removing', function() { + beforeEach(storeSetup); + afterEach(storeClear); + it('removes the model from the store', function(done) { + return store.add(model).done(function() { + return store.remove(model).done(function(removed) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(removed.attributes).to.deep.eql(model.attributes); + expect(store.data).to.deep.eql({ + records: [], + recordsById: {}, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + it('removes a local model from the store', function(done) { + model = new Backbone.Model; + return store.add(model).done(function() { + return store.remove(model).done(function() { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(store.data).to.deep.eql({ + records: [], + recordsById: {}, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + it('removes the model from the dirty list', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function() { + return store.remove(model).done(function() { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(store.data).to.deep.eql({ + records: [], + recordsById: {}, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + }); + return it('clears the store removing all saved data', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function() { + return store.isDestroyed(otherModel, true).done(function() { + return store.clear().done(function() { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql({}); + expect(store.data).to.deep.eql({}); + return done(); + }); + }); + }); + }); + }); + }); + }); + describe('when searching', function() { + beforeEach(storeSetup); + afterEach(storeClear); + it('finds a model', function(done) { + return store.add(model).done(function() { + expect(store.find(1)).to.deep.eql(model.attributes); + return done(); + }); + }); + it('finds all models', function(done) { + return store.add(model).done(function() { + return store.add(otherModel).done(function() { + var models; + models = [model.attributes, otherModel.attributes]; + expect(store.find()).to.deep.eql(models); + return done(); + }); + }); + }); + it('finds all dirty models', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function() { + expect(store.findDirty()).to.deep.eql([model.attributes]); + return done(); + }); + }); + }); + it('finds all destroyed models', function(done) { + return store.isDestroyed(model, true).done(function() { + expect(store.findDestroyed()).to.deep.eql([model.id]); + return done(); + }); + }); + return it('returns correct values when model is not found', function() { + expect(store.find(1)).to.be.undefined; + expect(store.findDirty(1)).to.be.undefined; + return expect(store.findDestroyed(1)).to.be["false"]; + }); + }); + return describe('dirty / destroyed flags', function(done) { + beforeEach(storeSetup); + afterEach(storeClear); + it('marks a model as dirty', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function(dirtyModel) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(dirtyModel.attributes).to.deep.eql(model.attributes); + expect(store.data).to.deep.eql({ + records: [1], + recordsById: { + 1: model.attributes + }, + local: [], + dirty: [1], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + it('marks a model as destroyed', function(done) { + return store.isDestroyed(model, true).done(function(destroyedModel) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(destroyedModel.attributes).to.deep.eql(model.attributes); + expect(store.data).to.deep.eql({ + records: [], + recordsById: {}, + local: [], + dirty: [], + destroyed: [1] + }); + return done(); + }); + }); + }); + it('unmarks a model as dirty', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function() { + return store.isDirty(model, false).done(function(dirtyModel) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(dirtyModel.attributes).to.deep.eql(model.attributes); + expect(store.data).to.deep.eql({ + records: [1], + recordsById: { + 1: model.attributes + }, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + }); + it('unmarks a model as destroyed', function(done) { + return store.isDestroyed(model, true).done(function() { + return store.isDestroyed(model, false).done(function(dirtyModel) { + return storage.get('Store').done(function(storeData) { + expect(storeData).to.deep.eql(store.data); + expect(dirtyModel.attributes).to.deep.eql(model.attributes); + expect(store.data).to.deep.eql({ + records: [], + recordsById: {}, + local: [], + dirty: [], + destroyed: [] + }); + return done(); + }); + }); + }); + }); + it('reports correctly if there are dirty models', function(done) { + return store.add(model).done(function() { + return store.isDirty(model, true).done(function() { + expect(store.hasDirtyOrDestroyed).to.be.ok; + return done(); + }); + }); + }); + it('reports correctly if there are destroyed models', function(done) { + return store.isDestroyed(model, true).done(function() { + expect(store.hasDirtyOrDestroyed).to.be.ok; + return done(); + }); + }); + it('throws an error if the model is not in the store', function() { + return expect(function() { + return store.isDirty(model, true); + }).to["throw"](/is not in the store/); + }); + return it('throws an error if trying to mark a stored model as destroyed', function(done) { + return store.add(model).done(function() { + expect(function() { + return store.isDestroyed(model, true); + }).to["throw"](/is still in the store/); + return done(); + }); + }); + }); + }); + })(storageName, storageClass)); + } + return _results; +});