diff --git a/bower.json b/bower.json index 275ef98..4e08f4c 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "backbonefire", "description": "The officially supported Backbone binding for Firebase", - "version": "0.0.0", + "version": "0.5.0", "authors": [ "Firebase (https://www.firebase.com/)" ], diff --git a/dist/backbonefire.js b/dist/backbonefire.js new file mode 100644 index 0000000..3c6e233 --- /dev/null +++ b/dist/backbonefire.js @@ -0,0 +1,768 @@ +/*! + * BackboneFire is the officially supported Backbone binding for Firebase. The + * bindings let you use special model and collection types that allow for + * synchronizing data with Firebase. + * + * BackboneFire 0.5.0 + * https://github.com/firebase/backbonefire/ + * License: MIT + */ + +(function(_, Backbone) { + 'use strict'; + + Backbone.Firebase = {}; + + /** + * A utility for retrieving the key name of a Firebase ref or + * DataSnapshot. This is backwards-compatible with `name()` + * from Firebase 1.x.x and `key()` from Firebase 2.0.0+. Once + * support for Firebase 1.x.x is dropped in BackboneFire, this + * helper can be removed. + */ + Backbone.Firebase._getKey = function(refOrSnapshot) { + return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name(); + }; + + /** + * A utility for resolving whether an item will have the autoSync + * property. Models can have this property on the prototype. + */ + Backbone.Firebase._determineAutoSync = function(model, options) { + var proto = Object.getPrototypeOf(model); + return _.extend( + { + autoSync: proto.hasOwnProperty('autoSync') ? proto.autoSync : true + }, + this, + options + ).autoSync; + }; + + /** + * Overriding of Backbone.sync. + * All Backbone crud calls (destroy, add, create, save...) will pipe into + * this method. This way Backbone can handle the prepping of the models + * and the trigering of the appropiate methods. While sync can be overwritten + * to handle updates to Firebase. + */ + Backbone.Firebase.sync = function(method, model, options) { + var modelJSON = model.toJSON(); + + if (method === 'read') { + + Backbone.Firebase._readOnce(model.firebase, function onComplete(snap) { + var resp = snap.val(); + if(options.success) { + options.success(resp); + } + }, function _readOnceError(err) { + if(options.error) { + options.error(err); + } + }); + + } else if (method === 'create') { + + Backbone.Firebase._setWithCheck(model.firebase, modelJSON, options); + + } else if (method === 'update') { + + Backbone.Firebase._updateWithCheck(model.firebase, modelJSON, options); + + } else if(method === 'delete') { + + Backbone.Firebase._setWithCheck(model.firebase, null, options); + + } + + }; + + /** + * A utility for a one-time read from Firebase. + */ + Backbone.Firebase._readOnce = function(ref, onComplete) { + ref.once('value', onComplete); + }; + + /** + * A utility for a destructive save to Firebase. + */ + Backbone.Firebase._setToFirebase = function(ref, item, onComplete) { + ref.set(item, onComplete); + }; + + + /** + * A utility for a non-destructive save to Firebase. + */ + Backbone.Firebase._updateToFirebase = function(ref, item, onComplete) { + ref.update(item, onComplete); + }; + + /** + * A utility for success and error events that are called after updates + * from Firebase. + */ + Backbone.Firebase._onCompleteCheck = function(err, item, options) { + if(!options) { return; } + + if(err && options.error) { + options.error(item, err, options); + } else if(options.success) { + options.success(item, null, options); + } + }; + + /** + * A utility for a destructive save to Firebase that handles success and + * error events from the server. + */ + Backbone.Firebase._setWithCheck = function(ref, item, options) { + Backbone.Firebase._setToFirebase(ref, item, function(err) { + Backbone.Firebase._onCompleteCheck(err, item, options); + }); + }; + + /** + * A utility for a non-destructive save to Firebase that handles success and + * error events from the server. + */ + Backbone.Firebase._updateWithCheck = function(ref, item, options) { + Backbone.Firebase._updateToFirebase(ref, item, function(err) { + Backbone.Firebase._onCompleteCheck(err, item, options); + }); + }; + + /** + * A utility for throwing errors. + */ + Backbone.Firebase._throwError = function(message) { + throw new Error(message); + }; + + + /** + * A utility for a determining whether a string or a Firebase + * reference should be returned. + * string - return new Firebase('') + * object - assume object is ref and return + */ + Backbone.Firebase._determineRef = function(objOrString) { + switch (typeof(objOrString)) { + case 'string': + return new Firebase(objOrString); + case 'object': + return objOrString; + default: + Backbone.Firebase._throwError('Invalid type passed to url property'); + } + }; + + /** + * A utility for assigning an id from a snapshot. + * object - Assign id from snapshot key + * primitive - Throw error, primitives cannot be synced + * null - Create blank object and assign id + */ + Backbone.Firebase._checkId = function(snap) { + var model = snap.val(); + + // if the model is a primitive throw an error + if (Backbone.Firebase._isPrimitive(model)) { + Backbone.Firebase._throwError('InvalidIdException: Models must have an Id. Note: You may ' + + 'be trying to sync a primitive value (int, string, bool).'); + } + + // if the model is null set it to an empty object and assign its id + // this way listeners can still be attached to populate the object in the future + if(model === null) { + model = {}; + } + + // set the id to the snapshot's key + model.id = Backbone.Firebase._getKey(snap); + + return model; + }; + + /** + * A utility for checking if a value is a primitive + */ + Backbone.Firebase._isPrimitive = function(value) { + // is the value not an object and not null (basically, is it a primitive?) + return !_.isObject(value) && value !== null; + }; + + /** + * Model responsible for autoSynced objects + * This model is never directly used. The Backbone.Firebase.Model will + * inherit from this if it is an autoSynced model + */ + var SyncModel = (function() { + + function SyncModel() { + // Set up sync events + + // apply remote changes locally + this.firebase.on('value', function(snap) { + this._setLocal(snap); + this.trigger('sync', this, null, null); + }, this); + + // apply local changes remotely + this._listenLocalChange(function(model) { + this.firebase.update(model); + }); + + } + + SyncModel.protoype = { + save: function() { + console.warn('Save called on a Firebase model with autoSync enabled, ignoring.'); + }, + fetch: function() { + console.warn('Save called on a Firebase model with autoSync enabled, ignoring.'); + }, + sync: function(method, model, options) { + if(method === 'delete') { + Backbone.Firebase.sync(method, model, options); + } else { + console.warn('Sync called on a Fireabse model with autoSync enabled, ignoring.'); + } + } + }; + + return SyncModel; + }()); + + /** + * Model responsible for one-time requests + * This model is never directly used. The Backbone.Firebase.Model will + * inherit from this if it is an autoSynced model + */ + var OnceModel = (function() { + + function OnceModel() { + + // when an unset occurs set the key to null + // so Firebase knows to delete it on the server + this._listenLocalChange(function(model) { + this.set(model, { silent: true }); + }); + + } + + OnceModel.protoype = { + + sync: function(method, model, options) { + Backbone.Firebase.sync(method, model, options); + } + + }; + + return OnceModel; + }()); + + Backbone.Firebase.Model = Backbone.Model.extend({ + + // Determine whether the realtime or once methods apply + constructor: function(model, options) { + Backbone.Model.apply(this, arguments); + var defaults = _.result(this, 'defaults'); + + // Apply defaults only after first sync. + this.once('sync', function() { + this.set(_.defaults(this.toJSON(), defaults)); + }); + + this.autoSync = Backbone.Firebase._determineAutoSync(this, options); + + switch (typeof this.url) { + case 'string': + this.firebase = Backbone.Firebase._determineRef(this.url); + break; + case 'function': + this.firebase = Backbone.Firebase._determineRef(this.url()); + break; + case 'object': + this.firebase = Backbone.Firebase._determineRef(this.url); + break; + default: + Backbone.Firebase._throwError('url parameter required'); + } + + if(!this.autoSync) { + OnceModel.apply(this, arguments); + _.extend(this, OnceModel.protoype); + } else { + _.extend(this, SyncModel.protoype); + SyncModel.apply(this, arguments); + } + + }, + + + /** + * Siliently set the id of the model to the snapshot key + */ + _setId: function(snap) { + // if the item new set the name to the id + if(this.isNew()) { + this.set('id', Backbone.Firebase._getKey(snap), { silent: true }); + } + }, + + /** + * Proccess changes from a snapshot and apply locally + */ + _setLocal: function(snap) { + var newModel = this._unsetAttributes(snap); + this.set(newModel); + }, + + /** + * Unset attributes that have been deleted from the server + * by comparing the keys that have been removed. + */ + _unsetAttributes: function(snap) { + var newModel = Backbone.Firebase._checkId(snap); + + if (typeof newModel === 'object' && newModel !== null) { + var diff = _.difference(_.keys(this.attributes), _.keys(newModel)); + _.each(diff, _.bind(function(key) { + this.unset(key); + }, this)); + } + + // check to see if it needs an id + this._setId(snap); + + return newModel; + }, + + /** + * Find the deleted keys and set their values to null + * so Firebase properly deletes them. + */ + _updateModel: function(model) { + var modelObj = model.changedAttributes(); + _.each(model.changed, function(value, key) { + if (typeof value === 'undefined' || value === null) { + if (key == 'id') { + delete modelObj[key]; + } else { + modelObj[key] = null; + } + } + }); + + return modelObj; + }, + + + /** + * Determine if the model will update for every local change. + * Provide a callback function to call events after the update. + */ + _listenLocalChange: function(cb) { + var method = cb ? 'on' : 'off'; + this[method]('change', function(model) { + var newModel = this._updateModel(model); + if(_.isFunction(cb)){ + cb.call(this, newModel); + } + }, this); + } + + }); + + var OnceCollection = (function() { + function OnceCollection() { + + } + OnceCollection.protoype = { + /** + * Create an id from a Firebase push-id and call Backbone.create, which + * will do prepare the models and trigger the proper events and then call + * Backbone.Firebase.sync with the correct method. + */ + create: function(model, options) { + model.id = Backbone.Firebase._getKey(this.firebase.push()); + options = _.extend({ autoSync: false }, options); + return Backbone.Collection.prototype.create.apply(this, [model, options]); + }, + /** + * Create an id from a Firebase push-id and call Backbone.add, which + * will do prepare the models and trigger the proper events and then call + * Backbone.Firebase.sync with the correct method. + */ + add: function(model, options) { + model.id = Backbone.Firebase._getKey(this.firebase.push()); + options = _.extend({ autoSync: false }, options); + return Backbone.Collection.prototype.add.apply(this, [model, options]); + }, + /** + * Proxy to Backbone.Firebase.sync + */ + sync: function(method, model, options) { + Backbone.Firebase.sync(method, model, options); + }, + /** + * Firebase returns lists as an object with keys, where Backbone + * collections require an array. This function modifies the existing + * Backbone.Collection.fetch method by mapping the returned object from + * Firebase to an array that Backbone can use. + */ + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) { options.parse = true; } + var success = options.success; + var collection = this; + options.success = function(resp) { + var arr = []; + var keys = _.keys(resp); + _.each(keys, function(key) { + arr.push(resp[key]); + }); + var method = options.reset ? 'reset' : 'set'; + collection[method](arr, options); + if (success) { success(collection, arr, options); } + options.autoSync = false; + options.url = this.url; + collection.trigger('sync', collection, arr, options); + }; + return this.sync('read', this, options); + } + }; + + return OnceCollection; + }()); + + var SyncCollection = (function() { + + function SyncCollection() { + + // Add handlers for remote events + this.firebase.on('child_added', _.bind(this._childAdded, this)); + this.firebase.on('child_moved', _.bind(this._childMoved, this)); + this.firebase.on('child_changed', _.bind(this._childChanged, this)); + this.firebase.on('child_removed', _.bind(this._childRemoved, this)); + + // Once handler to emit 'sync' event whenever data changes + this.firebase.on('value', _.bind(function() { + this.trigger('sync', this, null, null); + }, this)); + + // Handle changes in any local models. + this.listenTo(this, 'change', this._updateModel, this); + // Listen for destroy event to remove models. + this.listenTo(this, 'destroy', this._removeModel, this); + } + + SyncCollection.protoype = { + comparator: function(model) { + return model.id; + }, + + add: function(models, options) { + var parsed = this._parseModels(models); + options = options ? _.clone(options) : {}; + options.success = + _.isFunction(options.success) ? options.success : function() {}; + + for (var i = 0; i < parsed.length; i++) { + var model = parsed[i]; + + if (options.silent === true) { + this._suppressEvent = true; + } + + var childRef = this.firebase.ref().child(model.id); + childRef.set(model, _.bind(options.success, model)); + } + + return parsed; + }, + + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (options.wait) { + this._log('Wait option provided to create, ignoring.'); + } + if (!model) { + return false; + } + var set = this.add([model], options); + return set[0]; + }, + + remove: function(models, options) { + var parsed = this._parseModels(models); + options = options ? _.clone(options) : {}; + options.success = + _.isFunction(options.success) ? options.success : function() {}; + + for (var i = 0; i < parsed.length; i++) { + var model = parsed[i]; + var childRef = this.firebase.child(model.id); + if (options.silent === true) { + this._suppressEvent = true; + } + Backbone.Firebase._setWithCheck(childRef, null, options); + } + + return parsed; + }, + + reset: function(models, options) { + options = options ? _.clone(options) : {}; + // Remove all models remotely. + this.remove(this.models, {silent: true}); + // Add new models. + var ret = this.add(models, {silent: true}); + // Trigger 'reset' event. + if (!options.silent) { + this.trigger('reset', this, options); + } + return ret; + }, + + _log: function(msg) { + if (console && console.log) { + console.log(msg); + } + }, + + _parseModels: function(models, options) { + var pushArray = []; + // check if the models paramter is an array or a single object + var singular = !_.isArray(models); + // if the models parameter is a single object then wrap it into an array + models = singular ? (models ? [models] : []) : models.slice(); + + for (var i = 0; i < models.length; i++) { + var model = models[i]; + + if (!model.id) { + model.id = Backbone.Firebase._getKey(this.firebase.push()); + } + + // call Backbone's prepareModel to apply options + model = Backbone.Collection.prototype._prepareModel.apply( + this, [model, options || {}] + ); + + if (model.toJSON && typeof model.toJSON == 'function') { + model = model.toJSON(); + } + + pushArray.push(model); + + } + + return pushArray; + }, + + _childAdded: function(snap) { + var model = Backbone.Firebase._checkId(snap); + + if (this._suppressEvent === true) { + this._suppressEvent = false; + Backbone.Collection.prototype.add.apply(this, [model], {silent: true}); + } else { + Backbone.Collection.prototype.add.apply(this, [model]); + } + this.get(model.id)._remoteAttributes = model; + }, + + _childMoved: function(snap) { + // TODO: Investigate: can this occur without the ID changing? + this._log('_childMoved called with ' + snap.val()); + }, + + // when a model has changed remotely find differences between the + // local and remote data and apply them to the local model + _childChanged: function(snap) { + var model = Backbone.Firebase._checkId(snap); + + var item = _.find(this.models, function(child) { + return child.id == model.id; + }); + + if (!item) { + // TODO: Investigate: what is the right way to handle this case? + //throw new Error('Could not find model with ID ' + model.id); + this._childAdded(snap); + return; + } + + this._preventSync(item, true); + item._remoteAttributes = model; + + // find the attributes that have been deleted remotely and + // unset them locally + var diff = _.difference(_.keys(item.attributes), _.keys(model)); + _.each(diff, function(key) { + item.unset(key); + }); + + item.set(model); + // fire sync since this is a response from the server + this.trigger('sync', this); + this._preventSync(item, false); + }, + + // remove an item from the collection when removed remotely + // provides the ability to remove siliently + _childRemoved: function(snap) { + var model = Backbone.Firebase._checkId(snap); + + if (this._suppressEvent === true) { + this._suppressEvent = false; + Backbone.Collection.prototype.remove.apply( + this, [model], {silent: true} + ); + } else { + // trigger sync because data has been received from the server + this.trigger('sync', this); + Backbone.Collection.prototype.remove.apply(this, [model]); + } + }, + + // Add handlers for all models in this collection, and any future ones + // that may be added. + _updateModel: function(model) { + var remoteAttributes; + var localAttributes; + var updateAttributes; + var ref; + + // if the model is already being handled by listeners then return + if (model._remoteChanging) { + return; + } + + remoteAttributes = model._remoteAttributes || {}; + localAttributes = model.toJSON(); + + // consolidate the updates to Firebase + updateAttributes = this._compareAttributes(remoteAttributes, localAttributes); + + ref = this.firebase.ref().child(model.id); + + // if '.priority' is present setWithPriority + // else do a regular update + if (_.has(updateAttributes, '.priority')) { + this._setWithPriority(ref, localAttributes); + } else { + this._updateToFirebase(ref, localAttributes); + } + + }, + + // set the attributes to be updated to Firebase + // set any removed attributes to null so that Firebase removes them + _compareAttributes: function(remoteAttributes, localAttributes) { + var updateAttributes = {}; + + var union = _.union(_.keys(remoteAttributes), _.keys(localAttributes)); + + _.each(union, function(key) { + if (!_.has(localAttributes, key)) { + updateAttributes[key] = null; + } else if (localAttributes[key] != remoteAttributes[key]) { + updateAttributes[key] = localAttributes[key]; + } + }); + + return updateAttributes; + }, + + // Special case if '.priority' was updated - a merge is not + // allowed so we'll have to do a full setWithPriority. + _setWithPriority: function(ref, item) { + var priority = item['.priority']; + delete item['.priority']; + ref.setWithPriority(item, priority); + return item; + }, + + // TODO: possibly pass in options for onComplete callback + _updateToFirebase: function(ref, item) { + ref.update(item); + }, + + // Triggered when model.destroy() is called on one of the children. + _removeModel: function(model, collection, options) { + options = options ? _.clone(options) : {}; + options.success = + _.isFunction(options.success) ? options.success : function() {}; + var childRef = this.firebase.child(model.id); + Backbone.Firebase._setWithCheck(childRef, null, _.bind(options.success, model)); + }, + + _preventSync: function(model, state) { + model._remoteChanging = state; + } + }; + + return SyncCollection; + }()); + + Backbone.Firebase.Collection = Backbone.Collection.extend({ + + constructor: function (model, options) { + Backbone.Collection.apply(this, arguments); + var self = this; + var BaseModel = self.model; + this.autoSync = Backbone.Firebase._determineAutoSync(this, options); + + switch (typeof this.url) { + case 'string': + this.firebase = Backbone.Firebase._determineRef(this.url); + break; + case 'function': + this.firebase = Backbone.Firebase._determineRef(this.url()); + break; + case 'object': + this.firebase = Backbone.Firebase._determineRef(this.url); + break; + default: + throw new Error('url parameter required'); + } + + // if we are not autoSyncing, the model needs + // to be a non-autoSynced model + if(!this.autoSync) { + _.extend(this, OnceCollection.protoype); + OnceCollection.apply(this, arguments); + } else { + _.extend(this, SyncCollection.protoype); + SyncCollection.apply(this, arguments); + } + + // Intercept the given model and give it a firebase ref. + // Have it listen to local changes silently. When attributes + // are unset, the callback will set them to null so that they + // are removed on the Firebase server. + this.model = function(attrs, opts) { + + var newItem = new BaseModel(attrs, opts); + newItem.autoSync = false; + newItem.firebase = self.firebase.child(newItem.id); + newItem.sync = Backbone.Firebase.sync; + newItem.on('change', function(model) { + var updated = Backbone.Firebase.Model.prototype._updateModel(model); + model.set(updated, { silent: true }); + }); + + return newItem; + + }; + + } + + }); + +})(window._, window.Backbone); diff --git a/dist/backbonefire.min.js b/dist/backbonefire.min.js new file mode 100644 index 0000000..ddc0e62 --- /dev/null +++ b/dist/backbonefire.min.js @@ -0,0 +1,10 @@ +/*! + * BackboneFire is the officially supported Backbone binding for Firebase. The + * bindings let you use special model and collection types that allow for + * synchronizing data with Firebase. + * + * BackboneFire 0.5.0 + * https://github.com/firebase/backbonefire/ + * License: MIT + */ +!function(a,b){"use strict";b.Firebase={},b.Firebase._getKey=function(a){return"function"==typeof a.key?a.key():a.name()},b.Firebase._determineAutoSync=function(b,c){var d=Object.getPrototypeOf(b);return a.extend({autoSync:d.hasOwnProperty("autoSync")?d.autoSync:!0},this,c).autoSync},b.Firebase.sync=function(a,c,d){var e=c.toJSON();"read"===a?b.Firebase._readOnce(c.firebase,function(a){var b=a.val();d.success&&d.success(b)},function(a){d.error&&d.error(a)}):"create"===a?b.Firebase._setWithCheck(c.firebase,e,d):"update"===a?b.Firebase._updateWithCheck(c.firebase,e,d):"delete"===a&&b.Firebase._setWithCheck(c.firebase,null,d)},b.Firebase._readOnce=function(a,b){a.once("value",b)},b.Firebase._setToFirebase=function(a,b,c){a.set(b,c)},b.Firebase._updateToFirebase=function(a,b,c){a.update(b,c)},b.Firebase._onCompleteCheck=function(a,b,c){c&&(a&&c.error?c.error(b,a,c):c.success&&c.success(b,null,c))},b.Firebase._setWithCheck=function(a,c,d){b.Firebase._setToFirebase(a,c,function(a){b.Firebase._onCompleteCheck(a,c,d)})},b.Firebase._updateWithCheck=function(a,c,d){b.Firebase._updateToFirebase(a,c,function(a){b.Firebase._onCompleteCheck(a,c,d)})},b.Firebase._throwError=function(a){throw new Error(a)},b.Firebase._determineRef=function(a){switch(typeof a){case"string":return new Firebase(a);case"object":return a;default:b.Firebase._throwError("Invalid type passed to url property")}},b.Firebase._checkId=function(a){var c=a.val();return b.Firebase._isPrimitive(c)&&b.Firebase._throwError("InvalidIdException: Models must have an Id. Note: You may be trying to sync a primitive value (int, string, bool)."),null===c&&(c={}),c.id=b.Firebase._getKey(a),c},b.Firebase._isPrimitive=function(b){return!a.isObject(b)&&null!==b};var c=function(){function a(){this.firebase.on("value",function(a){this._setLocal(a),this.trigger("sync",this,null,null)},this),this._listenLocalChange(function(a){this.firebase.update(a)})}return a.protoype={save:function(){console.warn("Save called on a Firebase model with autoSync enabled, ignoring.")},fetch:function(){console.warn("Save called on a Firebase model with autoSync enabled, ignoring.")},sync:function(a,c,d){"delete"===a?b.Firebase.sync(a,c,d):console.warn("Sync called on a Fireabse model with autoSync enabled, ignoring.")}},a}(),d=function(){function a(){this._listenLocalChange(function(a){this.set(a,{silent:!0})})}return a.protoype={sync:function(a,c,d){b.Firebase.sync(a,c,d)}},a}();b.Firebase.Model=b.Model.extend({constructor:function(e,f){b.Model.apply(this,arguments);var g=a.result(this,"defaults");switch(this.once("sync",function(){this.set(a.defaults(this.toJSON(),g))}),this.autoSync=b.Firebase._determineAutoSync(this,f),typeof this.url){case"string":this.firebase=b.Firebase._determineRef(this.url);break;case"function":this.firebase=b.Firebase._determineRef(this.url());break;case"object":this.firebase=b.Firebase._determineRef(this.url);break;default:b.Firebase._throwError("url parameter required")}this.autoSync?(a.extend(this,c.protoype),c.apply(this,arguments)):(d.apply(this,arguments),a.extend(this,d.protoype))},_setId:function(a){this.isNew()&&this.set("id",b.Firebase._getKey(a),{silent:!0})},_setLocal:function(a){var b=this._unsetAttributes(a);this.set(b)},_unsetAttributes:function(c){var d=b.Firebase._checkId(c);if("object"==typeof d&&null!==d){var e=a.difference(a.keys(this.attributes),a.keys(d));a.each(e,a.bind(function(a){this.unset(a)},this))}return this._setId(c),d},_updateModel:function(b){var c=b.changedAttributes();return a.each(b.changed,function(a,b){("undefined"==typeof a||null===a)&&("id"==b?delete c[b]:c[b]=null)}),c},_listenLocalChange:function(b){var c=b?"on":"off";this[c]("change",function(c){var d=this._updateModel(c);a.isFunction(b)&&b.call(this,d)},this)}});var e=function(){function c(){}return c.protoype={create:function(c,d){return c.id=b.Firebase._getKey(this.firebase.push()),d=a.extend({autoSync:!1},d),b.Collection.prototype.create.apply(this,[c,d])},add:function(c,d){return c.id=b.Firebase._getKey(this.firebase.push()),d=a.extend({autoSync:!1},d),b.Collection.prototype.add.apply(this,[c,d])},sync:function(a,c,d){b.Firebase.sync(a,c,d)},fetch:function(b){b=b?a.clone(b):{},void 0===b.parse&&(b.parse=!0);var c=b.success,d=this;return b.success=function(e){var f=[],g=a.keys(e);a.each(g,function(a){f.push(e[a])});var h=b.reset?"reset":"set";d[h](f,b),c&&c(d,f,b),b.autoSync=!1,b.url=this.url,d.trigger("sync",d,f,b)},this.sync("read",this,b)}},c}(),f=function(){function c(){this.firebase.on("child_added",a.bind(this._childAdded,this)),this.firebase.on("child_moved",a.bind(this._childMoved,this)),this.firebase.on("child_changed",a.bind(this._childChanged,this)),this.firebase.on("child_removed",a.bind(this._childRemoved,this)),this.firebase.on("value",a.bind(function(){this.trigger("sync",this,null,null)},this)),this.listenTo(this,"change",this._updateModel,this),this.listenTo(this,"destroy",this._removeModel,this)}return c.protoype={comparator:function(a){return a.id},add:function(b,c){var d=this._parseModels(b);c=c?a.clone(c):{},c.success=a.isFunction(c.success)?c.success:function(){};for(var e=0;e (https://www.firebase.com/)", "homepage": "https://github.com/firebase/backbonefire/", "repository": {