UNPKG

fireproof

Version:
2,136 lines (1,590 loc) 53.5 kB
/*! fireproof 3.0.4, © 2015 J2H2 Inc. ISC License. * http://github.com/casetext/fireproof.git */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.Fireproof = factory(); } }(this, function() { 'use strict'; /** * Fireproofs an existing Firebase reference, giving it magic promise powers. * @name Fireproof * @constructor * @global * @param {Firebase} firebaseRef A Firebase reference object. * @property then A promise shortcut for .once('value'), * except for references created by .push(), where it resolves on success * and rejects on failure of the property object. * @example * var fp = new Fireproof(new Firebase('https://test.firebaseio.com/something')); * fp.then(function(snap) { console.log(snap.val()); }); */ function Fireproof(firebaseRef, promise) { if (!Fireproof.Promise) { try { Fireproof.Promise = Promise; } catch(e) { throw new Error('You must supply a Promise library to Fireproof!'); } } else if (typeof Fireproof.Promise !== 'function') { throw new Error('The supplied value of Fireproof.Promise is not a constructor (got ' + Fireproof.Promise + ')'); } this._ref = firebaseRef; if (promise && promise.then) { this.then = promise.then.bind(promise); } else { this.then = function(ok, fail) { return this.once('value', function() {}) .then(ok || null, fail || null); }; } } /** * Tell Fireproof to use a given defer-style promise library from now on. * If you have native promises, you don't need to call this; * if you want to substitute a different promise constructor, just set it on Fireproof.Promise directly. * @deprecated * @method Fireproof.bless * @param {Function} Deferrable a deferrable promise constructor with .all(). * @throws if you don't provide a valid promise library. */ Fireproof.bless = function(Deferrable) { Fireproof.Promise = function(fn) { var deferred = Deferrable.defer(); this.then = deferred.promise.then.bind(deferred.promise); fn(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)); }; Fireproof.Promise.all = Deferrable.all; Fireproof.Promise.resolve = function(value) { return new Fireproof.Promise(function(resolve) { resolve(value); }); }; Fireproof.Promise.reject = function(value) { return new Fireproof.Promise(function(resolve, reject) { reject(value); }); }; }; /** * Tell Fireproof to use a given function to set timeouts from now on. * @method Fireproof.setNextTick * @param {Function} nextTick a function that takes a function and * runs it in the immediate future. */ Fireproof.setNextTick = function(fn) { Fireproof.__nextTick = fn; }; Fireproof._nextTick = function(fn) { if (Fireproof.__nextTick) { Fireproof.__nextTick(fn, 0); } else { setTimeout(fn, 0); } }; Fireproof._handleError = function(onComplete) { var resolve, reject; var promise = new Fireproof.Promise(function(_resolve_, _reject_) { resolve = _resolve_; reject = _reject_; }); var rv = function(err, val) { var context = this, rvArgs = arguments; // finish stats event, if there is one. if (rv.id) { Fireproof.stats._finish(rv.id, err); } if (onComplete && typeof onComplete === 'function') { Fireproof._nextTick(function() { onComplete.apply(context, rvArgs); }); } if (err) { reject(err); } else { resolve(val); } }; rv.promise = promise; return rv; }; /** * Delegates Firebase#child, wrapping the child in fireproofing. * @method Fireproof#child * @param {string} childPath The subpath to refer to. * @returns {Fireproof} A reference to the child path. */ Fireproof.prototype.child = function(childPath) { return new Fireproof(this._ref.child(childPath)); }; /** * Delegates Firebase#parent, wrapping the child in fireproofing. * @method Fireproof#parent * @returns {Fireproof} A ref to the parent path, or null if there is none. */ Fireproof.prototype.parent = function() { if (this._ref.parent() === null) { return null; } else { return new Fireproof(this._ref.parent()); } }; /** * Delegates Firebase#root, wrapping the root in fireproofing. * @method Fireproof#root * @returns {Fireproof} A ref to the root. */ Fireproof.prototype.root = function() { return new Fireproof(this._ref.root()); }; /** * Hands back the original Firebase reference. * @method Fireproof#toFirebase * @returns {Firebase} The proxied Firebase reference. */ Fireproof.prototype.toFirebase = function() { return this._ref; }; /** * Delegates Firebase#name. * @method Fireproof#name * @returns {string} The last component of this reference object's path. */ Fireproof.prototype.name = function() { return this._ref.name(); }; /** * Delegates Firebase#key. * @method Fireproof#key * @returns {string} The last component of this reference object's path. */ Fireproof.prototype.key = function() { return this._ref.key(); }; /** * Delegates Firebase#toString. * @method Fireproof#toString * @returns {string} The full URL of this reference object. */ Fireproof.prototype.toString = function() { return this._ref.toString(); }; function findOptions(onComplete, options) { if (typeof onComplete !== 'function' && typeof(options) === undefined) { return onComplete; } else { return options; } } /* FIXME(goldibex): Find out the reason for this demonry. * For reasons completely incomprehensible to me, some type of race condition * is possible if multiple Fireproof references attempt authentication at the * same time, the result of which is one or more of the promises will never * resolve. * Accordingly, it is necessary that we wrap authentication actions in a * global lock. This is accomplished by queuing operations in an array. No, I * don't like it any more than you do. */ var authOps = []; /** * Wraps auth methods so they execute in order. * @method Fireproof#_wrapAuth * @param {function} fn Auth function that generates a promise once it's done. */ Fireproof._wrapAuth = function(fn) { authOps.push(fn); nextAuth(); function nextAuth() { if (!authOps.authing && authOps[0]) { authOps.authing = true; var thisAuth = authOps.pop(); thisAuth().then(done, done); } } function done() { authOps.authing = false; nextAuth(); } }; /** * Delegates Firebase#auth. * @method Fireproof#auth * @param {String} authToken * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.auth = function(authToken, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.auth(authToken, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#authWithCustomToken. * @method Fireproof#authWithCustomToken * @param {String} authToken * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authWithCustomToken = function(authToken, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authWithCustomToken(authToken, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#authAnonymously. * @method Fireproof#authAnonymously * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authAnonymously = function(onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authAnonymously(oc, options); }); return oc.promise; }; /** * Delegates Firebase#authWithPassword. * @method Fireproof#authWithPassword * @param {Object} credentials Should include `email` and `password`. * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authWithPassword = function(credentials, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authWithPassword(credentials, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#authWithOAuthPopup. * @method Fireproof#authWithOAuthPopup * @param {String} provider * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authWithOAuthPopup = function(provider, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authWithOAuthPopup(provider, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#authWithOAuthRedirect. * @method Fireproof#authWithOAuthRedirect * @param {String} provider * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authWithOAuthRedirect = function(provider, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authWithOAuthRedirect(provider, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#authWithOAuthPopup. * @method Fireproof#authWithOAuthPopup * @param {String} provider * @param {Object} credentials * @param {Function} [onComplete] * @param {Object} [options] * @returns {Promise} that resolves on auth success and rejects on auth failure. */ Fireproof.prototype.authWithOAuthToken = function(provider, credentials, onComplete, options) { var oc = Fireproof._handleError(onComplete); options = findOptions(onComplete, options); var self = this; Fireproof._wrapAuth(function() { self._ref.authWithOAuthToken(provider, credentials, oc, options); return oc.promise; }); return oc.promise; }; /** * Delegates Firebase#getAuth. * @method Fireproof#getAuth * @returns {Object} user info object, or null otherwise. */ Fireproof.prototype.getAuth = function() { return this._ref.getAuth(); }; /** * Delegates Firebase#onAuth. * @method Fireproof#onAuth * @param {Function} onComplete Gets called on auth change. * @param {Object} [context] */ Fireproof.prototype.onAuth = function(onComplete, context) { return this._ref.onAuth(onComplete, context); }; /** * Delegates Firebase#offAuth. * @method Fireproof#offAuth * @param {Function} onComplete The function previously passed to onAuth. * @param {Object} [context] */ Fireproof.prototype.offAuth = function(onComplete, context) { return this._ref.offAuth(onComplete, context); }; /** * Delegates Firebase#unauth. * @method Fireproof#unauth */ Fireproof.prototype.unauth = function() { return this._ref.unauth(); }; /** * A helper object for retrieving sorted Firebase objects from multiple * locations. * @constructor Fireproof.Demux * @static * @param {Array} refs a list of Fireproof object references to draw from. * @param {boolean} [limitToFirst] Whether to use "limitToFirst" to restrict the length * of queries to Firebase. True by default. Set this to false if you want to * control the query more directly by setting it on the objects you pass to refs. */ function Demux(refs, limit) { if (!(this instanceof Fireproof.Demux)) { return new Fireproof.Demux(refs); } else if (arguments.length > 1 && !Array.isArray(refs)) { refs = Array.prototype.slice.call(arguments, 0); } this._limit = (limit !== undefined ? limit : true); this._refs = refs; this._positions = refs.reduce(function(positions, ref) { positions[ref.ref().toString()] = { name: undefined, priority: undefined }; return positions; }, {}); // we always want there to be a "previous" promise to hang operations from this._previousPromise = Fireproof.Promise.resolve([]); this._buffer = []; } /** * Get the next `count` items from the paths, ordered by priority. * @method Fireproof.Demux#get * @param {Number} count The number of items to get from the list. * @returns {Promise} A promise that resolves with the next `count` items, ordered by priority. */ Demux.prototype.get = function(count) { var self = this; self._previousPromise = self._previousPromise .then(function() { if (self._buffer.length >= count) { // If we have enough objects in the buffer to service the request, don't // call Firebase again. return self._buffer.splice(0, count); } else { // We need to retrieve more objects from Firebase to satisfy the request. return Fireproof.Promise.all(self._refs.map(function(ref) { var priority = self._positions[ref.ref().toString()].priority, name = self._positions[ref.ref().toString()].name; var newRef; if (priority && name) { newRef = ref.startAt(priority, name); } else if (priority) { newRef = ref.startAt(priority); } else { newRef = ref.startAt(); } if (self._limit) { return newRef.limitToFirst(count - self._buffer.length); } else { return newRef; } })) .then(self._concatenateResults.bind(self)) .then(function() { return self._buffer.splice(0, count); }); } }); return self._previousPromise; }; Demux.prototype._concatenateResults = function(resultLists) { var self = this; var allResults = resultLists.reduce(function(acc, resultList) { var listPath = resultList.ref().toString(); resultList.forEach(function(child) { var position = self._positions[listPath]; // don't include an overlapping child if (position.priority !== child.getPriority() || position.name !== child.key()) { acc.push(child); position.priority = child.getPriority(); position.name = child.key(); } }); return acc; }, []); self._buffer = self._buffer.concat(allResults).sort(function(a, b) { // sort by priority and name if the priorities are identical. // See the Firebase docs for more information. var aPriority = a.getPriority(), bPriority = b.getPriority(), aName = a.key(), bName = b.key(); if (typeof aPriority === typeof bPriority) { if (aPriority === null) { return aName.localeCompare(bName); } else if (typeof aPriority === 'number') { return (aPriority - bPriority) || aName.localeCompare(bName); } else if (typeof aPriority === 'string') { return aPriority.localeCompare(bPriority) || aName.localeCompare(bName); } } else { // different priority types if (aPriority === null) { return -1; } else if (bPriority === null) { return 1; } else if (typeof aPriority === 'number') { return -1; } else { return 1; } } }); }; Fireproof.Demux = Demux; /** * A live array that keeps its members in sync with a Firebase location's children. * The three array references, `keys`, `values`, and `priorities`, are guaranteed * to persist for the lifetime of the array. In other words, the arrays themselves * are constant; only their contents are mutable. This is highly useful behavior * for dirty-checking environments like Angular.js. * @constructor Fireproof.LiveArray * @static * @param {Function} [errorHandler] a function to be called if a Firebase error occurs. * @property {Array} keys A live array of the keys at the Firebase ref. * @property {Array} values A live array of the values at the Firebase ref. * @property {Array} priorities A live array of the priorities at the Firebase ref. */ function LiveArray(errorHandler) { this._items = []; this.keys = []; this.values = []; this.priorities = []; if (errorHandler) { this.errorHandler = errorHandler; } } function firebaseKeySort(a, b) { var aInt = Number(a['.key']), bInt = Number(b['.key']); if (!isNaN(aInt) && !isNaN(bInt)) { return aInt - bInt; } else if (!isNaN(aInt)) { return -1; } else if (!isNaN(bInt)) { return 1; } else { return a['.key'].localeCompare(b['.key']); } } LiveArray.firebaseKeySort = firebaseKeySort; function firebaseChildSort(childName) { return function(a, b) { var aVal = a['.value'], bVal = b['.value'], aChild, bChild; if (typeof aVal === 'object') { aChild = aVal[childName]; if (aChild === undefined) { aChild = null; } } else { aChild = null; } if (typeof bVal === 'object') { bChild = bVal[childName]; if (bChild === undefined) { bChild = null; } } else { bChild = null; } // 1. Children with a null value for the specified child key come first. if (aChild === null && bChild === null) { return firebaseKeySort(a, b); } else if (aChild === null) { return -1; } else if (bChild === null) { return 1; } // 2. Children with a value of false for the specified child key come next. // If multiple children have a value of false, they are sorted lexicographically by key. if (aChild === false && bChild === false) { return firebaseKeySort(a, b); } else if (aChild === false) { return -1; } else if (bChild === false) { return 1; } // 3. Children with a value of true for the specified child key come next. // If multiple children have a value of true, they are sorted lexicographically by key. if (aChild === true && bChild === true) { return firebaseKeySort(a, b); } else if (aChild === true) { return -1; } else if (bChild === true) { return 1; } // 4. Children with a numeric value come next, sorted in ascending order. // If multiple children have the same numerical value for the specified child node, // they are sorted by key. if (typeof aChild === 'number' && typeof bChild === 'number') { if (aChild - bChild !== 0) { return aChild - bChild; } else { return firebaseKeySort(a, b); } } else if (typeof aChild === 'number') { return -1; } else if (typeof bChild === 'number') { return 1; } // 5. Strings come after numbers, and are sorted lexicographically in ascending order. // If multiple children have the same value for the specified child node, // they are ordered lexicographically by key. if (typeof aChild === 'string' && typeof bChild === 'string') { if (aChild === bChild) { return firebaseKeySort(a, b); } else { return aChild.localeCompare(bChild); } } else if (typeof aChild === 'string') { return -1; } else if (typeof bChild === 'string') { return 1; } // 6. Objects come last, and are sorted lexicographically by key name in ascending order. return firebaseKeySort(a, b); }; } LiveArray.firebaseChildSort = firebaseChildSort; function firebasePrioritySort(a, b) { var aPriority = a['.priority'], bPriority = b['.priority']; // 1. Children with no priority (the default) come first. if (aPriority === null && bPriority === null) { return firebaseKeySort(a, b); } else if (aPriority === null) { return -1; } else if (bPriority === null) { return 1; } // 2. Children with a number as their priority come next. // They are sorted numerically by priority, small to large. if (typeof aPriority === 'number' && typeof bPriority === 'number') { if (aPriority - bPriority === 0) { return firebaseKeySort(a, b); } else { return aPriority - bPriority; } } else if (typeof aPriority === 'number') { return -1; } else if (typeof bPriority === 'number') { return 1; } // 3. Children with a string as their priority come last. // They are sorted lexicographically by priority. var stringCompareResult = aPriority.localeCompare(bPriority); if (stringCompareResult === 0) { return firebaseKeySort(a, b); } else { return stringCompareResult; } } LiveArray.firebasePrioritySort = firebasePrioritySort; /** * Connect this LiveArray to a Firebase reference, instantiating listeners * for child events. * If an error is received from a Firebase listener, _all_ listeners are * disconnected, LiveArray#error is set, and your error handler is called if you * supplied one. * @method Fireproof.LiveArray#connect * @param {Fireproof} [ref] a Firebase ref whose children you wish to sync to. * @param {String} [sortMode] "key", "priority", or "child". * @param {String} [sortProperty] The name of the child property to sort on, if * sortMode is "child". */ LiveArray.prototype.connect = function(ref, sortMode, sortProperty) { var self = this; self.ref = ref; self.error = null; function handleError(err) { self.error = err; self.disconnect(); if (self.errorHandler) { self.errorHandler.call(null, err); } } self.watchers = [ ref.on('child_added', function(snap) { var newVal = { '.key': snap.key(), '.value': snap.val(), '.priority': snap.getPriority() }; self._items.push(newVal); self._sort(sortMode, sortProperty); }, handleError), ref.on('child_removed', function(snap) { var indexOfItem = self.keys.indexOf(snap.key()); self._items.splice(indexOfItem, 1); self._sort(sortMode, sortProperty); }, handleError), ref.on('child_changed', function(snap) { // child_changed explicitly means a value change var indexOfItem = self.keys.indexOf(snap.key()); self._items[indexOfItem]['.value'] = snap.val(); self._sort(sortMode, sortProperty); }, handleError), ref.on('child_moved', function(snap) { // child_moved explicitly means a priority change var indexOfItem = self.keys.indexOf(snap.key()); self._items[indexOfItem]['.priority'] = snap.getPriority(); self._sort(sortMode, sortProperty); }, handleError) ]; }; /** * Disconnect this LiveArray from a Firebase reference, removing all listeners. * Also clears the contents of the live array references. * @method Fireproof.LiveArray#disconnect */ LiveArray.prototype.disconnect = function() { if (this.ref && this.watchers) { this.ref.off('child_added', this.watchers[0]); this.ref.off('child_removed', this.watchers[1]); this.ref.off('child_changed', this.watchers[2]); this.ref.off('child_moved', this.watchers[3]); this.ref = null; this.watchers = null; } // empty the arrays this._items.length = 0; this.keys.length = 0; this.values.length = 0; this.priorities.length = 0; }; LiveArray.prototype._sort = function(sortMode, sortProperty) { // empty the arrays this.keys.length = 0; this.values.length = 0; this.priorities.length = 0; // sort the items switch(sortMode) { case 'child': this._items.sort(firebaseChildSort(sortProperty)); break; case 'key': this._items.sort(firebaseKeySort); break; default: this._items.sort(firebasePrioritySort); break; } // populate the arrays for (var i = 0; i < this._items.length; i++) { this.keys[i] = this._items[i]['.key']; this.values[i] = this._items[i]['.value']; this.priorities[i] = this._items[i]['.priority']; } }; Fireproof.LiveArray = LiveArray; function OnDisconnect(ref) { this._od = ref.onDisconnect(); } /** * Delegates onDisconnect()#cancel. * @method Fireproof#onDisconnect#cancel * @param {function=} callback Firebase callback. * @returns {Promise} */ OnDisconnect.prototype.cancel = function(cb) { var handler = Fireproof._handleError(cb); this._od.cancel(handler); return handler.promise; }; /** * Delegates onDisconnect()#remove. * @method Fireproof#onDisconnect#remove * @param {function=} callback Firebase callback. * @returns {Promise} */ OnDisconnect.prototype.remove = function(cb) { var handler = Fireproof._handleError(cb); this._od.remove(handler); return handler.promise; }; /** * Delegates onDisconnect()#set. * @method Fireproof#onDisconnect#set * @param {*} value Value to set on the ref on disconnect. * @param {function=} callback Firebase callback. * @returns {Promise} */ OnDisconnect.prototype.set = function(value, cb) { var handler = Fireproof._handleError(cb); this._od.set(value, handler); return handler.promise; }; /** * Delegates onDisconnect()#setWithPriority. * @method Fireproof#onDisconnect#setWithPriority * @param {*} value Value to set on the ref on disconnect. * @param {*} priority Priority to set on the ref on disconnect. * @param {function=} callback Firebase callback. * @returns {Promise} */ OnDisconnect.prototype.setWithPriority = function(value, priority, cb) { var handler = Fireproof._handleError(cb); this._od.setWithPriority(value, priority, handler); return handler.promise; }; /** * Delegates onDisconnect()#update. * @method Fireproof#onDisconnect#update * @param {*} value Value to update on the ref on disconnect. * @param {function=} callback Firebase callback. * @returns {Promise} */ OnDisconnect.prototype.update = function(value, cb) { var handler = Fireproof._handleError(cb); this._od.update(value, handler); return handler.promise; }; /** * Delegates Fireproof#onDisconnect. * @method Fireproof#onDisconnect * @returns {Fireproof.OnDisconnect} */ Fireproof.prototype.onDisconnect = function() { return new OnDisconnect(this._ref); }; /** * A helper object for paging over Firebase objects. * @constructor Fireproof.Pager * @static * @param {Fireproof} ref a Firebase ref whose children you wish to page over. * @param {Number} [initialCount] The number of objects in the first page. * @property {Boolean} hasPrevious True if there are more objects before the current page. * @property {Boolean} hasNext True if there are more objects after the current page. */ function Pager(ref, initialCount) { if (arguments.length < 1) { throw new Error('Not enough arguments to Pager'); } this._mainRef = ref.ref(); this._resetCurrentOperation(); this.hasNext = true; this.hasPrevious = false; var promise; if (initialCount) { promise = this.next(initialCount); } else { promise = Fireproof.Promise.resolve([]); } this.then = promise.then.bind(promise); } /** * Get the next page of children from the ref. * @method Fireproof.Pager#next * @param {Number} count The size of the page. * @returns {Promise} A promise that resolves with an array of the next children. */ Pager.prototype.next = function(count) { if (arguments.length === 0) { throw new Error('Not enough arguments to next'); } var self = this; var requestedCount; if (self.hasNext) { return self._currentOperation .then(function() { self._direction = 'next'; var ref = self._mainRef; if (self._page) { requestedCount = count + 1; ref = ref.orderByPriority().startAt(self._page.priority, self._page.key) .limitToFirst(count + 2); } else { requestedCount = count; ref = ref.startAt().limitToFirst(count + 1); } return ref.once('value'); }) .then(function(snap) { return self._handleResults(snap, requestedCount); }); } else { return Fireproof.Promise.resolve([]); } }; /** * Get the previous page of children from the ref. * @method Fireproof.Pager#previous * @param {Number} count The size of the page. * @returns {Promise} A promise that resolves with an array of the next children. */ Pager.prototype.previous = function(count) { if (arguments.length === 0) { throw new Error('Not enough arguments to previous'); } var self = this; if (self.hasPrevious) { return self._currentOperation .then(function() { self._direction = 'previous'; var ref = self._mainRef; if (self._page) { ref = ref.orderByPriority().endAt(self._page.priority, self._page.key) .limitToLast(count + 2); } else { throw new Error('Cannot call #previous on a Pager without calling #next first'); } return ref.once('value'); }) .then(function(snap) { return self._handleResults(snap, count+1); }); } else { return Fireproof.Promise.resolve([]); } }; Pager.prototype._handleResults = function(snap, requestedCount) { var self = this, objects = []; snap.forEach(function(child) { objects.push(child); }); // remove any dead weight from the list if (self._direction === 'next') { if (self._page) { objects = objects.slice(1, requestedCount); } else { objects = objects.slice(0, requestedCount); } } else { if (snap.numChildren() <= requestedCount) { objects = objects.slice(0, snap.numChildren() - 1); } else { objects = objects.slice(1, requestedCount); } } if (self._direction === 'next') { this.hasNext = snap.numChildren() === requestedCount+1; this.hasPrevious = true; } else { this.hasPrevious = snap.numChildren() === requestedCount+1; this.hasNext = true; } if (objects.length > 0) { // set page positions if (self._direction === 'next') { self._page = { priority: objects[objects.length-1].getPriority(), key: objects[objects.length-1].key() }; } else { self._page = { priority: objects[0].getPriority(), key: objects[0].key() }; } } self._currentOperationCount--; if (self._currentOperationCount === 0) { self._resetCurrentOperation(); } return objects; }; Pager.prototype._resetCurrentOperation = function() { this._currentOperation = Fireproof.Promise.resolve(null); this._currentOperationCount = 0; }; Fireproof.Pager = Pager; /** * Delegates Firebase#limit. * @method Fireproof#limit * @param {Number} limit * @returns {Fireproof} */ Fireproof.prototype.limit = function(limit) { return new Fireproof(this._ref.limit(limit)); }; /** * Delegates Firebase#limitToFirst. * @method Fireproof#limitToFirst * @param {Number} limit * @returns {Fireproof} */ Fireproof.prototype.limitToFirst = function(limit) { return new Fireproof(this._ref.limitToFirst(limit)); }; /** * Delegates Firebase#limitToLast. * @method Fireproof#limitToLast * @param {Number} limit * @returns {Fireproof} */ Fireproof.prototype.limitToLast = function(limit) { return new Fireproof(this._ref.limitToLast(limit)); }; /** * Delegates Firebase#orderByChild. * @method Fireproof#orderByChild * @param {string} key * @returns {Fireproof} */ Fireproof.prototype.orderByChild = function(key) { return new Fireproof(this._ref.orderByChild(key)); }; /** * Delegates Firebase#orderByKey. * @method Fireproof#orderByKey * @returns {Fireproof} */ Fireproof.prototype.orderByKey = function() { return new Fireproof(this._ref.orderByKey()); }; /** * Delegates Firebase#orderByValue. * @method Fireproof#orderByValue * @returns {Fireproof} */ Fireproof.prototype.orderByValue = function() { return new Fireproof(this._ref.orderByValue()); }; /** * Delegates Firebase#orderByPriority. * @method Fireproof#orderByPriority * @returns {Fireproof} */ Fireproof.prototype.orderByPriority = function() { return new Fireproof(this._ref.orderByPriority()); }; /** * Delegates Firebase#equalTo. * @method Fireproof#equalTo * @param {String|Number|null} value * @param {String} [key] * @returns {Fireproof} */ Fireproof.prototype.equalTo = function(value, key) { if (key) { return new Fireproof(this._ref.equalTo(value, key)); } else { return new Fireproof(this._ref.equalTo(value)); } }; /** * Delegates Firebase#startAt. * @method Fireproof#startAt * @param {object} value * @param {string} [key] * @returns {Fireproof} */ Fireproof.prototype.startAt = function(value, key) { if (key) { return new Fireproof(this._ref.startAt(value, key)); } else { return new Fireproof(this._ref.startAt(value)); } }; /** * Delegates Firebase#endAt. * @method Fireproof#endAt * @param {object} value * @param {string} [key] * @returns {Fireproof} */ Fireproof.prototype.endAt = function(value, key) { if (key) { return new Fireproof(this._ref.endAt(value, key)); } else { return new Fireproof(this._ref.endAt(value)); } }; /** * Delegates Firebase#ref. * @method Fireproof#ref * @returns {Fireproof} */ Fireproof.prototype.ref = function() { return new Fireproof(this._ref.ref()); }; /** * Delegates Firebase#transaction. * @method Fireproof#transaction * @param {function} updateFunction * @param {function} onComplete * @param {boolean=} applyLocally * @returns {Promise} an Object with two properties: 'committed' and 'snapshot'. */ Fireproof.prototype.transaction = function(updateFunction, onComplete, applyLocally) { var self = this; var id = Fireproof.stats._start('transaction', self); return new Fireproof.Promise(function(resolve, reject) { self._ref.transaction(updateFunction, function(err, committed, snap) { Fireproof.stats._finish(id, err); snap = new Fireproof.Snapshot(snap); if (onComplete) { onComplete(err, committed, snap); } if (err) { reject(err); } else { resolve({ committed: committed, snapshot: snap }); } }, applyLocally); }); }; /** * Delegates Firebase#on. * @method Fireproof#on * @param {string} eventType 'value', 'child_added', 'child_changed', 'child_moved', * or 'child_removed' * @param {function} callback * @param {function=} cancelCallback * @param {object=} context * @returns {function} Your callback parameter wrapped in fireproofing. Use * this return value, not your own copy of callback, to call .off(). It also * functions as a promise that resolves with a {FireproofSnapshot}. */ Fireproof.prototype.on = function(eventType, callback, cancelCallback, context) { var resolved = false, finished = false, self = this; var id = Fireproof.stats._start('read', self); Fireproof.stats._startListener(self); if (!self._ids) { self._ids = []; } if (!self._ids[eventType]) { self._ids[eventType] = []; } self._ids[eventType].push(id); if (typeof callback !== 'function') { callback = function() {}; } if (typeof cancelCallback !== 'function') { cancelCallback = function() {}; } var resolve, reject; var promise = new Fireproof.Promise(function(_resolve_, _reject_) { resolve = _resolve_; reject = _reject_; }); var callbackHandler = function(snap, prev) { if (!finished) { finished = true; self._ids[eventType].pop(); Fireproof.stats._finish(id); } snap = new Fireproof.Snapshot(snap); callback(snap, prev); if (!resolved) { resolved = true; resolve(snap, prev); } }; callbackHandler.then = promise.then.bind(promise); self._ref.on(eventType, callbackHandler, function(err) { self._ids[eventType].pop(); Fireproof.stats._finish(id, err); Fireproof.stats._endListener(self, err); cancelCallback(err); if (!resolved) { resolved = true; reject(err); } }, context); return callbackHandler; }; /** * Delegates Firebase#off. * @method Fireproof#off * @param {string} eventType * @param {function=} callback * @param {object=} context */ Fireproof.prototype.off = function(eventType, callback, context) { if (this._ids && this._ids[eventType] && this._ids[eventType].length > 0) { Fireproof.stats._finish(this._ids[eventType].pop()); } Fireproof.stats._endListener(this); this._ref.off(eventType, callback, context); }; /** * Delegates Firebase#once. * @method Fireproof#once * @param {object} eventType 'value', 'child_added', 'child_changed', 'child_moved', * or 'child_removed' * @param {function} successCallback * @param {function=} failureCallback * @param {object=} context * @returns {Promise} Resolves with {FireproofSnapshot}. */ Fireproof.prototype.once = function(eventType, successCallback, failureCallback, context) { var self = this; return new Fireproof.Promise(function(resolve, reject) { var id = Fireproof.stats._start('read', self); if (typeof successCallback !== 'function') { successCallback = function() {}; } if (typeof failureCallback !== 'function') { failureCallback = function() {}; } self._ref.once(eventType, function(snap) { Fireproof.stats._finish(id); snap = new Fireproof.Snapshot(snap); resolve(snap); successCallback(snap); }, function(err) { Fireproof.stats._finish(id, err); reject(err); failureCallback(err); }, context); }); }; /** * A delegate object for Firebase's Snapshot. * @name FireproofSnapshot * @constructor * @global * @private * @param {Snapshot} snap The snapshot to delegate to. */ function FireproofSnapshot(snap) { this._snap = snap; } Fireproof.Snapshot = FireproofSnapshot; /** * Delegates DataSnapshot#exists. * @method FireproofSnapshot#exists * @returns {Boolean} Whether any data exists at the location. */ FireproofSnapshot.prototype.exists = function() { return this._snap.exists(); }; /** * Delegates DataSnapshot#child. * @method FireproofSnapshot#child * @param {String} path Path of the child. * @returns {FireproofSnapshot} The snapshot of the child. */ FireproofSnapshot.prototype.child = function(path) { return new FireproofSnapshot(this._snap.child(path)); }; /** * Delegates DataSnapshot#forEach. * @method FireproofSnapshot#forEach * @param {cb} eachFn The function to call on each child. * @returns {Boolean} True if a callback returned true and cancelled enumeration. */ FireproofSnapshot.prototype.forEach = function(cb) { return this._snap.forEach(function(childSnap) { if (cb(new FireproofSnapshot(childSnap)) === true) { return true; } }); }; /** * Delegates DataSnapshot#hasChild. * @method FireproofSnapshot#hasChild * @param {cb} eachFn The function to call on each child. * @returns {Boolean} True if the snap has the specified child. */ FireproofSnapshot.prototype.hasChild = function(name) { return this._snap.hasChild(name); }; /** * Delegates DataSnapshot#hasChildren. * @method FireproofSnapshot#hasChildren * @returns {Boolean} True if the snapshot has children. */ FireproofSnapshot.prototype.hasChildren = function() { return this._snap.hasChildren(); }; /** * Delegates DataSnapshot#numChildren. * @method FireproofSnapshot#numChildren * @returns {Number} The number of children the snapshot has. */ FireproofSnapshot.prototype.numChildren = function() { return this._snap.numChildren(); }; /** * Delegates DataSnapshot#name. * @method FireproofSnapshot#name * @returns {String} The last part of the snapshot's path. */ FireproofSnapshot.prototype.name = function() { return this._snap.name(); }; /** * Delegates DataSnapshot#key. * @method FireproofSnapshot#key * @returns {String} The last part of the snapshot's path. */ FireproofSnapshot.prototype.key = function() { return this._snap.key(); }; /** * Delegates DataSnapshot#val. * @method FireproofSnapshot#val * @returns {*} The Javascript deserialization of the snapshot. */ FireproofSnapshot.prototype.val = function() { return this._snap.val(); }; /** * Delegates DataSnapshot#ref. * @method FireproofSnapshot#ref * @returns {Fireproof} The Fireproof object for the snap's location. */ FireproofSnapshot.prototype.ref = function() { return new Fireproof(this._snap.ref()); }; /** * Delegates DataSnapshot#getPriority. * @method FireproofSnapshot#getPriority * @returns {*} The snapshot's priority. */ FireproofSnapshot.prototype.getPriority = function() { return this._snap.getPriority(); }; /** * Delegates DataSnapshot#exportVal. * @method FireproofSnapshot#exportVal * @returns {*} The Firebase export object of the snapshot. */ FireproofSnapshot.prototype.exportVal = function() { return this._snap.exportVal(); }; /** * Statistics about Firebase usage. * @namespace Fireproof.stats * @property {Object} operationLog * @property {Number} runningOperationCount * @property {Number} operationCount * @property {Number} listenCount * @static */ Fireproof.stats = { _eventSubscribers: {} }; /** * Resets the count of Firebase operations back to 0. * @method reset * @memberof Fireproof.stats */ Fireproof.stats.reset = function() { var newOperationLog = {}, newRunningOperationCount = 0; for (var id in Fireproof.stats.operationLog) { var operation = Fireproof.stats.operationLog[id]; if (!operation.hasOwnProperty('finish')) { newRunningOperationCount++; newOperationLog[id] = operation; } } Fireproof.stats.operationLog = newOperationLog; Fireproof.stats.runningOperationCount = newRunningOperationCount; Fireproof.stats.operationCount = newRunningOperationCount; }; Fireproof.stats.resetListeners = function() { Fireproof.stats.listeners = {}; Fireproof.stats.listenCount = 0; }; /** * Records the start of a Firebase operation. * @private * @param {String} kind The kind of event (read, write, or update). * @param {String} ref The Fireproof ref to which the event refers. * @return {String} id */ Fireproof.stats._start = function(event, ref) { var path = ref.ref().toString(); var id = Math.random().toString(36).slice(2); Fireproof.stats.runningOperationCount++; Fireproof.stats.operationLog[id] = { id: id, type: event, path: path, start: Date.now() }; Fireproof.stats._emit('start', Fireproof.stats.operationLog[id]); if (event === 'listen') { Fireproof.stats.listenCount++; } return id; }; /** * Records the end of a Firebase operation. * @private * @param {String} id The event ID. * @param {String} err Error, if any, to associate with the event. */ Fireproof.stats._finish = function(id, err) { var logEvent = Fireproof.stats.operationLog[id]; if (!logEvent) { throw new Error('Fireproof: reference to unknown log event ' + id); } if (!logEvent.finish) { Fireproof.stats.runningOperationCount--; Fireproof.stats.operationCount++; logEvent.finish = Date.now(); logEvent.duration = logEvent.finish - logEvent.start; if (err) { logEvent.error = err; } } if (logEvent.error) { Fireproof.stats._emit('error', logEvent); } else { Fireproof.stats._emit('finish', logEvent); } }; /** * Records the start of a Firebase listener. * @private * @param {Fireproof} ref */ Fireproof.stats._startListener = function(ref) { var path = ref.ref().toString(); if (!Fireproof.stats.listeners[path]) { Fireproof.stats.listeners[path] = 0; } Fireproof.stats.listeners[path]++; Fireproof.stats.listenCount++; Fireproof.stats._emit('listenStarted', path); }; /** * Records the end of a Firebase listener. * @private * @param {Fireproof} ref * @param {String} err Error, if any, to associate with the event. */ Fireproof.stats._endListener = function(ref, err) { var path = ref.ref().toString(); Fireproof.stats.listeners[path]--; Fireproof.stats.listenCount--; Fireproof.stats._emit('listenEnded', path, err); }; /** * Gets data about listeners on Firebase locations. * @method getListeners * @memberof Fireproof.stats * @returns {Object} Listener counts keyed by Firebase path. */ Fireproof.stats.getListeners = function() { return Fireproof.stats.listeners; }; /** * Gets the total number of listeners on Firebase locations. * @method getListenerCount * @memberof Fireproof.stats * @returns {Number} The total number of Firebase listeners presently operating. */ Fireproof.stats.getListenerCount = function() { return Fireproof.stats.listenCount; }; /** * Gets the per-operation, per-path counts of Firebase operations. * @method getPathCounts * @memberof Fireproof.stats * @returns {Object} An object with keys like "listen", "readOnce", "write", * and "update". Each key has an object value, of which the keys are Firebase * paths and the values are counts. */ Fireproof.stats.getPathCounts = function() { var result = {}; for (var id in Fireproof.stats.operationLog) { var logEvent = Fireproof.stats.operationLog[id]; if (!result[logEvent.type]) { result[logEvent.type] = {}; } if (!result[logEvent.type][logEvent.path]) { result[logEvent.type][logEvent.path] = 1; } else { result[logEvent.type][logEvent.path]++; } } return result; }; /** * Gets the per-operation counts of Firebase operations. * @method getCounts * @memberof Fireproof.stats * @returns {Object} An object with with keys like "read", "write", * and "update". The values are the counts of operations under those headings. */ Fireproof.stats.getCounts = function() { var result = {}; for (var id in Fireproof.stats.operationLog) { var logEvent = Fireproof.stats.operationLog[id]; if (!result[logEvent.type]) { result[logEvent.type] = 1; } else { result[logEvent.type]++; } } return result; }; /** * Listens for Firebase events occurring. * @method on * @memberof Fireproof.stats * @param {String} name The name of the event. One of 'start', 'finish', 'error', * 'listenStarted', or 'listenEnded.' * @param {Function} fn The function to call when the event happens. Takes a single * parameter, the event object. * @returns {Function} fn is returned for convenience, to pass to `off`. * @throws if you don't pass in a function for fn. */ Fireproof.stats.on = function(name, fn) { if (typeof name === 'function' && fn === undefined) { fn = name; name = null; } if (typeof fn !== 'function') { throw new Error('Non-function passed to Fireproof.stats.on'); } if (!Fireproof.stats._eventSubscribers[name]) { Fireproof.stats._eventSubscribers[name] = []; } Fireproof.stats._eventSubscribers[name].push(fn); return fn; }; /** * Stops sending events to a listener. * @method off * @memberof Fireproof.stats * @param {String} [name] The name of the event. One of 'start', 'finish', 'error', * 'listenStarted', or 'listenEnded.' * @param {Function} fn The function to stop calling. * @throws if you don't pass in a function for fn. */ Fireproof.stats.off = function(name, fn) { if (typeof name === 'function' && fn === undefined) { fn = name; name = null; } if (typeof fn !== 'function') { throw new Error('Non-function passed to Fireproof.stats.off'); } if (name) { var listeners = Fireproof.stats._eventSubscribers[name], index = listeners.indexOf(fn); if (index !== -1) { listeners[index] = null; } } }; Fireproof.stats._emit = function(name) { var args = Array.prototype.slice.call(arguments, 1); if (!Fireproof.stats._eventSubscribers[name]) { Fireproof.stats._eventSubscribers[name] = []; } var listeners = Fireproof.stats._eventSubscribers[name]; Fireproof._nextTick(function() { for (var i = 0; i < listeners.length; i++) { if (listeners[i] !== null) { listeners[i].apply(null, args); } } }); }; Fireproof.stats.reset(); Fireproof.stats.resetListeners(); /** * Delegates Firebase#createUser. * @method Fireproof#createUser * @param {Object} credentials * @param {Function} [onComplete] * @returns {Promise} */ Fireproof.prototype.createUser = function(credentials, onComplete) { var oc = Fireproof._handleError(onComplete); this._ref.createUser(credentials, oc); return oc.promise; }; /** * Delegates Firebase#changeEmail. * @method Fireproof#changeEmail * @param {Object} credentials * @param {Function} [onComplete] * @returns {Promise} */ Fireproof.prototype.changeEmail = function(credentials, onComplete) { var oc = Fireproof._handleError(onComplete); this._ref.changeEmail(credentials, oc); return oc.promise; }; /** * Delegates Firebase#changePassword. * @method Fireproof#changePassword * @param {Object} credentials * @param {Function} [onComplete] * @returns {Promise} */ Fireproof.prototype.changePassword = function(credentials, onComplete) { var oc = Fireproof._handleError(onComplete); this._ref.changePassword(credentials, oc); return oc.promise; }; /** * Delegates Firebase#resetPassword. * @method Fireproof#resetPassword * @param {Object} credentials * @param {Function} [onComplete] * @returns {Promise} */ Fireproof.prototype.resetPassword = function(credentials, onComplete