Skip to content
This repository has been archived by the owner on Apr 11, 2023. It is now read-only.

Added option to allow localStorage to update first and return successful immediately. #86

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ Include Backbone.dualStorage after having included Backbone.js:
Create your models and collections in the usual way.
Feel free to use Backbone as you usually would; this is a drop-in replacement.

Keep in mind that Backbone.dualStorage really loves your models. By default it will cache everything that passes through Backbone.sync. You can override this behaviour with the booleans ```remote``` or ```local``` on models and collections:
Keep in mind that Backbone.dualStorage really loves your models. By default it will cache everything that passes through Backbone.sync. The default sync process waits for the remote response before updating localStorage. You can override the default behaviour with the booleans ```remote```,```local``` or ```localFirst``` on models and collections:

SomeCollection = Backbone.Collection.extend({
remote: true // never cached, dualStorage is bypassed entirely
localFirst: true // localStorage is updated first and will return successful immediately. Remote sync will only occur when `syncDirtyAndDestroyed()` is called.
local: true // always fetched and saved only locally, never saves on remote
local: function() { return trueOrFalse; } // local and remote can also be dynamic
});
Expand Down
37 changes: 35 additions & 2 deletions backbone.dualstorage.amd.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ persistence. Models are given GUIDS, and saved into a JSON object. Simple
as that.
*/

var S4, backboneSync, callbackTranslator, dualsync, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, result;
var S4, backboneSync, callbackTranslator, dualsync, isLocallyCached, isModelPersisted, localSyncFirst, localsync, modelUpdatedWithResponse, onlineSync, parseRemoteResponse, remoteSyncFirst, result;

Backbone.Collection.prototype.syncDirty = function() {
var id, ids, model, store, storeName, url, _i, _len, _results;
Expand All @@ -32,6 +32,9 @@ Backbone.Collection.prototype.syncDirty = function() {
model = id.length === 36 ? this.findWhere({
id: id
}) : this.get(id);
if (result(model, 'localFirst') || result(model.collection, 'localFirst')) {
model.remoteFirst = true;
}
_results.push(model != null ? model.save() : void 0);
}
return _results;
Expand All @@ -50,6 +53,9 @@ Backbone.Collection.prototype.syncDestroyed = function() {
id: id
});
model.collection = this;
if (result(model, 'localFirst') || result(model.collection, 'localFirst')) {
model.remoteFirst = true;
}
_results.push(model.destroy());
}
return _results;
Expand Down Expand Up @@ -298,6 +304,14 @@ parseRemoteResponse = function(object, response) {
}
};

isModelPersisted = function(model) {
return !(_.isString(model.id) && model.id.length === 36);
};

isLocallyCached = function(storeName) {
return localStorage.getItem(storeName);
};

modelUpdatedWithResponse = function(model, response) {
var modelClone;
modelClone = new Backbone.Model;
Expand All @@ -316,19 +330,38 @@ onlineSync = function(method, model, options) {
};

dualsync = function(method, model, options) {
var error, local, success, temporaryId;
var local;
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);
options.error = callbackTranslator.forDualstorageCaller(options.error, model, options);
if (result(model, 'remote') || result(model.collection, 'remote')) {
return onlineSync(method, model, options);
}
if ((result(model, 'localFirst') || result(model.collection, 'localFirst')) && !result(model, 'remoteFirst')) {
return localSyncFirst(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 remoteSyncFirst(method, model, options);
};

localSyncFirst = function(method, model, options) {
if (!isLocallyCached(options.storeName)) {
return remoteSyncFirst(method, model, options);
}
options.dirty = true;
return localsync(method, model, options);
};

remoteSyncFirst = function(method, model, options) {
var error, success, temporaryId;
options.ignoreCallbacks = true;
if (model.remoteFirst != null) {
delete model.remoteFirst;
}
success = options.success;
error = options.error;
switch (method) {
Expand Down
30 changes: 28 additions & 2 deletions backbone.dualstorage.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Backbone.Collection.prototype.syncDirty = ->

for id in ids
model = if id.length == 36 then @findWhere(id: id) else @get(id)
model.remoteFirst = true if (result(model, 'localFirst') or result(model.collection, 'localFirst'))
model?.save()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of setting an attribute on the model, what do you think about passing in a special option to save, such as save(remoteFirst: true)? It should get passed through to sync/dualStorage. Then you also won't have to clean it up later.


Backbone.Collection.prototype.syncDestroyed = ->
Expand All @@ -27,6 +28,7 @@ Backbone.Collection.prototype.syncDestroyed = ->
for id in ids
model = new @model(id: id)
model.collection = @
model.remoteFirst = true if (result(model, 'localFirst') or result(model.collection, 'localFirst'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model.remoteFirst = (result(model, 'localFirst') or result(model.collection, 'localFirst')) is better?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or this:

model.remoteFirst = result(model, 'localFirst') or result(model.collection, 'localFirst')

There are lots of ways to skin a cat. I don't have a strong preference in this case.

model.destroy()

Backbone.Collection.prototype.syncDirtyAndDestroyed = ->
Expand Down Expand Up @@ -213,6 +215,14 @@ parseRemoteResponse = (object, response) ->
if not (object and object.parseBeforeLocalSave) then return response
if _.isFunction(object.parseBeforeLocalSave) then object.parseBeforeLocalSave(response)

# Right now Backbone.dualStorage infers a model is persisted if
# it's id length is NOT 36 characters
isModelPersisted = (model) ->
return not (_.isString(model.id) and model.id.length == 36)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that #83 has been merged, this conflicts and needs to be merged with master to be resolved. This method is replaced by hasTempId().


isLocallyCached = (storeName) ->
return localStorage.getItem(storeName);

modelUpdatedWithResponse = (model, response) ->
modelClone = new Backbone.Model
modelClone.idAttribute = model.idAttribute
Expand All @@ -231,18 +241,34 @@ dualsync = (method, model, options) ->
result(model.collection, 'url') || result(model, 'urlRoot') || result(model, 'url')
options.success = callbackTranslator.forDualstorageCaller(options.success, model, options)
options.error = callbackTranslator.forDualstorageCaller(options.error, model, options)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing white space.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does need to go.

# execute only online sync
return onlineSync(method, model, options) if result(model, 'remote') or result(model.collection, 'remote')

# execute localSyncFirst but skip if 'localFirst' has been explicitly set to false
# TO DO: This check seems smelly. REFACTOR.
#if not (result(model, 'localFirst') == false || result(model.collection, 'localFirst') == false)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this already been refactored, and these are old comments?

return localSyncFirst(method, model, options) if (result(model, 'localFirst') or result(model.collection, 'localFirst')) and not result(model, 'remoteFirst')

# 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

# execute dual sync
# Execute remoteSyncFirst
return remoteSyncFirst(method,model,options)

localSyncFirst = (method, model, options) ->
if not isLocallyCached(options.storeName) then return remoteSyncFirst(method, model, options)
options.dirty = true;
return localsync(method, model, options);

remoteSyncFirst = (method, model, options) ->
# execute standard dual sync
options.ignoreCallbacks = true

delete model.remoteFirst if model.remoteFirst?

success = options.success
error = options.error

Expand Down
37 changes: 35 additions & 2 deletions backbone.dualstorage.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 35 additions & 2 deletions spec/backbone.dualstorage.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.