-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackbone.queue.js
More file actions
317 lines (278 loc) · 14.2 KB
/
backbone.queue.js
File metadata and controls
317 lines (278 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import _ from 'underscore';
import Store from 'react-native-simple-store';
import async from 'async';
import uuid from 'uuid';
module.exports = function(Backbone) {
Backbone.Model.prototype.cidAttribute = '_cid';
Backbone.Collection.prototype.pendingChangesStorageKey =
Backbone.Model.prototype.pendingChangesStorageKey = function(action) {
if (!action) throw new Error('expected action type for pendingChangesStorageKey');
var prefix = Backbone.QUEUE_STORAGE_PREFIX ||
"backbone-queue-storage-";
return prefix + _.result(this, 'url')+'['+action+']';
};
Backbone.Model.prototype.pendingNewModelStorageKey = function() {
var prefix = Backbone.QUEUE_STORAGE_PREFIX ||
"backbone-queue-storage-";
return prefix + _.result(this, 'urlRoot') + '/' + this.get(this.cidAttribute);
};
Backbone.Model.prototype.enableQueue = function() {
// queues can only be enabled once per model as they override some
// backbone methods, doing this multiple times would cause all
// kinds of havoc.
if (this._backbone_queue_enabled) return;
this._backbone_queue_enabled = true;
// delta is used as a scratch pad for keeping track of
// all the changes made to a model in between saves
var delta = {};
// override the backbone save and destroy methods to allow us to track
// when a server operation completes successfully (at which point any
// pending changes can be removed from the persistent storage)
var _save = this.save;
this.save = function(key, val, options) {
var model = this;
var attrs;
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else (attrs = {})[key] = val;
options = options || {};
// make sure values passed into save directly get a chance to propagate
// through the change events and into the delta object before saving the
// delta object to storage
this.set(attrs);
// take a copy of what should be saved into the state
// by this call to save()
var changesSinceLastSave = _.clone(delta);
// if the model has not yet been saved to the server at all
// then we shouldn't store any changes here as we need the model
// to have an ID in order to know which model stored changes
// should be applied to
if (this.isNew()) {
if (model.get(model.cidAttribute)) {
Store.save(model.pendingNewModelStorageKey(), model.toJSON());
}
} else {
// if a delta actually exists, i.e. not an empty object
// then serialise this to storage
if (Object.keys(changesSinceLastSave).length) {
Store.save(model.pendingChangesStorageKey('update'), delta);
// clear the current delta to start building up changes until
// the next attempted save. if this save fails then we'll
// merge these changes back into to delta object so they'll
// get retried next time
delta = {};
}
}
// tap the success callback
var _success = options.success;
options.success = function() {
var args = arguments;
// removed locally saved record of changes as they're
// now part of the server state
Store.delete(model.pendingNewModelStorageKey()).then(function() {
Store.delete(model.pendingChangesStorageKey('update'))
.then(function() {
if (_success) _success.apply(this, args);
}.bind(this));
}.bind(this));
}
// tap error callback
var _error = options.error;
options.error = function() {
// order is important here as newer changes in delta
// should always be applied on top of the changes in the
// current batch that failed to save
delta = _.extend({}, changesSinceLastSave, delta);
if (_error) _error.apply(this, arguments);
}
// delegate back to backbone to do the heavy lifting
// of updating the server
return _save.call(this, attrs, options);
}
// override destroy method success callback to trigger events
// that will allow for tracking of pending and successful
// model deletions
var _destroy = this.destroy;
this.destroy = function(options) {
var model = this;
if (!options) options = {};
var collection = model.collection;
if (collection) collection.trigger('destroy_start', model);
var _success = options.success;
options.success = function() {
// clear any pending updates for the destroyed model when
// deletion has been successful
Store.delete(model.pendingChangesStorageKey('update')).then(function() {
if (collection) collection.trigger('destroy_success', model);
if (_success) _success.apply(this, arguments);
});
}
return _destroy.call(this, options);
}
// when a change is detected on a model we keep a in-memory note
// of all changes (merged into a single diff). when save is called
// this is what will be serialised to the persistent storage and
// hopefully the server at some point
this.on('change', function(model, options) {
if (options.unset) delta = _.omit(delta, _.keys(model.changedAttributes()));
else delta = _.extend(delta, model.changedAttributes());
});
};
Backbone.Model.prototype.processQueue = function(callback) {
this.enableQueue();
// once we've overridden the functions that track the save process
// we can try to re-apply any offline changes, first to the model
// and then try to save them to server
// if the save fails, the changes will still reside in the delta
// object (and any further changes will be merged in). the changes
// will be picked up next time a save attempt is made on the model
Store.get(this.pendingNewModelStorageKey()).then(function(newModel){
Store.get(this.pendingChangesStorageKey('update')).then(function(updatedValues){
var value = {};
value = _.extend(value, newModel);
value = _.extend(value, updatedValues);
if (value && Object.keys(value).length) {
this.save(value, {
success: function() { callback(); },
error: function() { callback(); }
});
} else callback();
}.bind(this));
}.bind(this));
};
Backbone.Collection.prototype.enableQueue = function() {
// queues can only be enabled once per model as they override some
// backbone methods, doing this multiple times would cause all
// kinds of havoc.
if (this._backbone_queue_enabled) return;
this._backbone_queue_enabled = true;
this._pending_model_destroy_queue = [];
// propagate queuing down to models in the collection
// for existing models and all future models
this.on('add', function(model){ model.enableQueue(); });
this.each(function(model){ model.enableQueue(); });
// attach events emitted by models that allow us to track
// the progress of model destruction
// when a deletion is started the model ID is added to a
// list of pending deletions.
this.on('destroy_start', function(model) {
var pending_destroy = this._pending_model_destroy_queue;
pending_destroy = _.union(pending_destroy, [model.id]);
this._pending_model_destroy_queue = pending_destroy;
Store.save(this.pendingChangesStorageKey('destroy'), { destroy_queue: pending_destroy });
}.bind(this));
// once the server responds with a success status then
// the id is removed from the list
this.on('destroy_success', function(model) {
var pending_destroy = this._pending_model_destroy_queue;
pending_destroy = _.without(pending_destroy, model.id);
this._pending_model_destroy_queue = pending_destroy;
if (pending_destroy.length)
Store.save(this.pendingChangesStorageKey('destroy'), { destroy_queue: pending_destroy });
else Store.delete(this.pendingChangesStorageKey('destroy'));
});
// once the model exists in the collection
// the corresponding cid id is removed from the list
this.on('add sync change', function(model) {
if (!(model instanceof Backbone.Model)) return;
if (model.isNew()) return;
var cid = model.get(model.cidAttribute);
if (!cid) return;
var pending_create = this._pending_model_create_queue || [];
pending_create = _.without(pending_create, cid);
this._pending_model_create_queue = pending_create;
if (pending_create.length)
Store.save(this.pendingChangesStorageKey('create'), { create_queue: pending_create });
else Store.delete(this.pendingChangesStorageKey('create'));
}.bind(this));
var _create = this.create;
this.create = function(model, options) {
model = this._prepareModel(model, options);
// ensure client id attribute is set (this is different to
// backbones own cid property on models)
var cid = model.get(model.cidAttribute) || uuid.v4();
model.set(model.cidAttribute, cid);
// make sure the model is offline-enabled
model.enableQueue();
// add client id to list of models to be created
var pending_create = this._pending_model_create_queue || [];
pending_create = _.union(pending_create, [cid]);
this._pending_model_create_queue = pending_create;
Store.save(this.pendingChangesStorageKey('create'), { create_queue: pending_create });
return _create.call(this, model, options);
}
};
// this is the function that does the work to restore any changes
// saved into the persistent storage to the model and save the
// model back to the server
// deletions, creations and updates are treated separately
Backbone.Collection.prototype.processQueue = function(callback) {
this.enableQueue();
var collection = this;
// destructions are stored as a list of IDs, so load the IDs
// and try to restore the models if they're still in the collections
// otherwise silently create a new model in the collection and
// then trigger a destruction
var processDestructions = function(callback) {
Store.get(this.pendingChangesStorageKey('destroy')).then(function(value){
this._pending_model_destroy_queue = (value && value.destroy_queue) || [];
async.map(this._pending_model_destroy_queue, function(id, cb){
// if the model still exists in the collection then we can use this
// object to trigger the destruction call
var model = this.get(id);
// otherwise we need to construct a dummy object to represent the model
// being deleted
if (!model) {
var doc = {};
doc[collection.model.prototype.idAttribute] = id;
model = collection.add(doc, { silent: true });
}
model.destroy({
success: function() { cb(); },
error: function() { cb(); }
});
}.bind(this), function() {
callback();
});
}.bind(this));
}.bind(this);
// creations are stored as a list of IDs, the ids reference a full set of
// attributes for that should be created. The model is restored and then
// saved to the server in the saveModelChanges step by calling processQueue
// on the model.
var processCreations = function(callback) {
Store.get(this.pendingChangesStorageKey('create')).then(function(value){
this._pending_model_create_queue = (value && value.create_queue) || [];
async.map(this._pending_model_create_queue, function(id, cb){
// create a dummy object that contains the client generated id
// of the object, from which the rest of the object will be
// loaded and then saved to the server
var doc = {};
doc[collection.model.prototype.cidAttribute] = id;
// add the model to the collection but don't save it yet
// that will be done in the next phase where all pending
// updates are applied to both saved and unsaved models
collection.add(doc);
cb();
}, function(){
callback();
});
}.bind(this));
}.bind(this);
// for each model, load any changes stored in persistent storage
// and trigger a save to either update or create the model.
var saveModelChanges = function(callback) {
async.map(this.models, function(model, cb) {
model.processQueue(cb);
}, callback);
}.bind(this);
processCreations(function() {
saveModelChanges(function() {
processDestructions(function() {
callback();
});
});
});
};
};