UNPKG

backbone.highway

Version:
476 lines (414 loc) 12.1 kB
(function (Backbone, _, io) { 'use strict'; if (typeof exports !== 'undefined') { Backbone = Backbone || require('backbone'); _ = _ || require('underscore'); io = io || require('socket.io-client'); } Backbone.Highway = {}; var HighwayModel = (function () { function HighwayModel() { } HighwayModel.prototype = { _log: function (msg) { console.log(msg); }, _warn: function (msg) { console.warn(msg); }, _error: function (msg) { console.error(msg); }, destroy: function (options) { if (this.collection) return this.collection.remove(this, options); else return Backbone.Model.prototype.destroy.call(this, options); }, sync: function () { // Sync doesn't do anything since everything is autosynced. // I'm overloading this method to avoid http errors. if (this.collection && this.collection.trigger) this.collection.trigger('sync'); return true; }, set: function (key, val, options) { if (_.isNull(key) || _.isUndefined(key)) return; options = options ? _.clone(options) : {}; var data = {}, pieces, v, root, k, obj; if (_.isObject(key)) { options = val ? _.clone(val) : {}; data = key; } else { data[key] = val; } //options = options || {}; for (k in data) { if (k.indexOf('.') > -1) { obj = JSON.parse(JSON.stringify(this.get(k.split('.') .shift()) || {})); this._setRecursive(obj, k.split('.') .splice(1) .join('.'), data[k]); data[k.split('.') .shift()] = obj; delete data[k]; } } if (this.collection && this.collection.io && this.get('_id') && !options.dontSave && !options.silent) { data._id = this.get('_id'); if (this.collection.io.connected === false) { var collection = this.collection.url.split('/').pop(); var d = localStorage.getItem('backfill_' + collection) || '[]'; d = JSON.parse(d); d.push(data); localStorage.setItem('backfill_' + collection, JSON.stringify(d)); } else { this.collection.io.emit('update', data); } } return Backbone.Model.prototype.set.call(this, data, options); }, _setRecursive: function (obj, prop, value) { if (typeof prop === "string") prop = prop.split("."); if (prop.length > 1) { var e = prop.shift(); this._setRecursive( Object.prototype.toString.call(obj[e]) === "[object Object]" ? obj[e] : {}, prop, value); } else obj[prop[0]] = value; }, get: function (key) { var val, pieces, pieces_length; if (key && key.indexOf('.') > -1) { pieces = key.split('.'); pieces_length = pieces.length; val = Backbone.Model.prototype.get.call(this, pieces[0]); if (val) { for (var i = 1; i < pieces_length; i++) { val = typeof val === 'object' ? val[pieces[i]] : void 0; } } } else { val = Backbone.Model.prototype.get.call(this, key); } return _.isArray(val) ? _.clone(val) : val; }, save: function (data, options) { this._warn('Highway Models are automatically synced. Use .set() instead of .save()'); this.set.apply(this, arguments); }, clear: function (options) { var attrs = {}; for (var key in this.attributes) if (key != '_id') attrs[key] = void 0; return this.set(attrs, _.extend({}, options, { unset: true })); } }; return HighwayModel; })(); var HighwayCollection = (function () { function HighwayCollection() { this.idAttribute = '_id'; // Add handlers for remote events this.io.on('child_added', _.bind(this._childAdded, this)); this.io.on('child_changed', _.bind(this._childChanged, this)); this.io.on('child_removed', _.bind(this._childRemoved, this)); this.io.on('all_records', _.bind(this._initialSync, this)); _.defer(_.bind(function () { this.io.emit('init', this.minimum); }, this)); // Handle any models added to the collection //this.listenTo(this, 'add', this._addModel, this); // Handle changes in any local models. this.on('change', this._updateModel); // Listen for destroy event to remove models. this.listenTo(this, 'destroy', this._removeModel, this); this.listenTo(this, 'highway:model_created', this._preventSync, this); } HighwayCollection.protoype = { _log: function (msg) { console.log(msg); }, _warn: function (msg) { console.warn(msg); }, _error: function (msg) { console.error(msg); }, /** * .create() a model but return a promise that resolves after * the socket.io call has finished * * @method _create * @param {[type]} model model attributes * @param {object} options regular backbone options * @return {Promise} A promise that resolves to properties */ _create: function (model, options) { var self = this; options = options ? _.clone(options) : {}; if (options.wait) { this._log('ignoring wait'); } if (!model) { return Promise.reject(new Error('No model provided')); } return new Promise(function (success, failure) { self.io.emit('create', model, function (data) { self._preventSync(false, true); var set = Backbone.Collection.prototype.add.call(self, [data], options); self._preventSync(false, false); success(set); }); }); }, create: function (model, options) { options = options ? _.clone(options) : {}; var wait = options.wait; model = this._prepareModel(model, options); if (!model) return false; if (wait) this._log('Ignoring wait'); this.add(model, options); return model; }, add: function (models, options) { var opts = _.extend({ merge: false }, options, { add: true, remove: false, silent: true, at: 0 }); var set = this.set(models, opts); this._addModel(set, opts); return set; }, remove: function (models, options) { if (!_.isArray(models)) models = [models]; var remove, model; options = options ? _.clone(options) : {}; for (var i in models) { model = models[i]; switch (typeof model) { case 'string': case 'number': remove = model; break; case 'object': remove = model; if (model.attributes && model.attributes._id) remove = model.attributes._id; break; case 'boolean': case 'undefined': this._error('Called Backbone.Highway.Collection.remove with invalid model: ', model); return; } this.io.emit('destroy', remove); } return Backbone.Collection.prototype.remove.call( this, models, options ); }, reset: function (models, options) { var ret = false; options = options ? _.clone(options) : {}; // Remove all models remotely. this.remove(this.models, { silent: true }); if (models) { // Add new models. ret = this.add(models, { silent: true }); } // Trigger 'reset' event. if (!options.silent) { this.trigger('reset', this, options); } return ret; }, _search: function (options) { var self = this; if (!_.isEmpty(this.minimum) && this._partial === true) { return new Promise(function (success, failure) { self.io.emit('search', options, function (records) { if (_.isArray(records)) { Backbone.Collection.prototype.add.call(self, records); success(); } else { failure(records); } }); }); } else { return Promise.resolve(); } }, _filter: function (iteratee, context) { var self = this; return new Promise(function (success, failure) { self._search({}) .then(function () { self._partial = false; success(Backbone.Collection.prototype.filter.call(self, iteratee, context)); }); }); }, _where: function (attr, first) { var self = this; return new Promise(function (success, failure) { self._search({ search: attr }) .then(function () { success(Backbone.Collection.prototype.where.call(self, attr, first)); }); }); }, _findWhere: function (attr) { return this._where(attr, true); }, _childAdded: function (model) { Backbone.Collection.prototype.add.call(this, [model]); }, _childChanged: function (snap) { if (!this.initialized) return; var model = snap; var item = _.find(this.models, function (child) { return child.get('_id') == model._id || !child.id; }); if (!item) { if (!this._partial) this._childAdded(snap); return; } item.set(model, { dontSave: true }); this.trigger('sync', this); }, _childRemoved: function (snap) { var search = {}; search[this.idAttribute] = snap[this.idAttribute]; var model = this.findWhere(search); // trigger sync because data has been received from the server this.trigger('sync', this); Backbone.Collection.prototype.remove.call(this, [model]); }, _updateModel: function (model, options) { if (!model || model instanceof HighwayModel) { return; } options = options ? _.clone(options) : {}; if (!options.dontSave && model.id) { var save = model.attributes; if (save._id !== undefined) { this.io.emit('update', save); } } }, _addModel: function (model) { var self = this; if (!model || model.attributes._id || this._remoteChanging) { this.trigger('highway:model_created'); return; } this.io.emit('update', model.attributes, function (attr) { model.set(attr, { silent: true }); self._addReference(model); self.trigger('add', model, self, { at: 0 }); }); }, // Triggered when model.destroy() is called on one of the children. _removeModel: function (model) { if (!model) return; this.io.emit('destroy', model); }, _preventSync: function (model, state) { if (!model) this._remoteChanging = state || false; else model._remoteChanging = state || false; }, _initialSync: function (data) { Backbone.Collection.prototype.add.call(this, data, { silent: true }); this.trigger('sync', this, null, null); this.initialized = true; }, _backfillData: function () { var collectionName = this.url.split('/').pop(); var data = localStorage.getItem('backfill_' + collectionName); if (data !== null) { data = JSON.parse(data); var d; while ((d = data.pop()) !== void 0) { if (typeof d === 'object' && d._id) this.io.emit('update', d); } localStorage.setItem('backfill_' + collectionName, '[]'); } else { return; } } }; return HighwayCollection; }()); var Model = Backbone.Model.extend({ constructor: function () { Backbone.Model.apply(this, arguments); _.extend(this, HighwayModel.prototype); HighwayModel.apply(this, arguments); } }); var Collection = Backbone.Collection.extend({ constructor: function (model, options) { Backbone.Collection.apply(this, arguments); var self = this; var BaseModel = self.model; var connectionOptions = { timeout: 500 }; switch (typeof this.url) { case 'string': this.io = io(this.url, connectionOptions); break; case 'function': this.io = io(this.url(), connectionOptions); break; default: throw new Error('url parameter required'); } this.minimum = this.minimum || {}; if (this.minimum !== {}) this._partial = true; _.extend(this, HighwayCollection.protoype); HighwayCollection.apply(this, arguments); this.idAttribute = this.idAttribute || BaseModel.prototype.idAttribute; this.io.on('connect', this._backfillData.bind(this)); this.io.on('reconnect', this._backfillData.bind(this)); } }); Backbone.Highway.Model = Model; Backbone.Highway.Collection = Collection; })(Backbone, _, io);