Firebase Won't Sync Reference Data Until Event Handlers Are Bound
The other day, I started looking into the Firebase Backend-as-a-Service (Baas) API. The data-syncing aspect of it seems magical; but, with all things magical, it's important to pull back the curtain occasionally and dig deeper. Otherwise, you end up with performance problems down the road. For this post, I wanted to see how the data syncing behavior was influenced by event handlers. And, I'm very happy to see that data syncing will not occur unless there is at least one event handler bound to the given resource.
Run this demo in my JavaScript Demos project on GitHub.
To test this, I created a demo that contacted Firebase through two different means - the Web API and the RESTful API. The RESTful API was used to update a counter every 1.5 seconds. The Web API was used to sync the counter data between the backend and the browser. I intentionally used two different modes of transportation so that the WebSocket activity would only indicate syncing and never the "PUT" requests.
Then, I created a controller method that would bind and unbind a "value" event listener at the user's request. Using the Chrome developer tools, we could check the WebSockets "frame" activity to see how the event binding and unbinding affected data synchronization.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Checking To See If Firebase Syncs Data Without Bound Event Handlers
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Checking To See If Firebase Syncs Data Without Bound Event Handlers
</h1>
<p ng-switch="isWatchingEvents">
<a ng-switch-when="true" ng-click="stopWatchingEvents()">Stop Watching Events</a>
<a ng-switch-when="false" ng-click="startWatchingEvents()">Start Watching Events</a>
</p>
<p ng-if="isWatchingEvents">
<strong>Ok, look at the WebSockets frames in Chrome dev tools</strong>.
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
<script type="text/javascript" src="../../vendor/firebase/firebase-1.1.0.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, firebase, $http ) {
// NOTE: This is my "developer" Firebase, so there is no security
// being applied to it.
var root = "https://popping-torch-33.firebaseio.com/2014-10-16/";
// Get a reference to the counter - just a simple value that we're going
// to be incrementing for the demo.
var counterResource = firebase( root + "counter" );
// I indicate whether or not we are currently watching the "value" event
// on the Firebase resource.
$scope.isWatchingEvents = false;
// We're going to start updating the remote Firebase counter value before
// we even start listening for events. This will run on an interval so
// that we can see how the WebSocket activity changes as we bind / unbind
// event handlers.
startUpdatingCounterValueUsingREST();
// ---
// PUBLIC METHODS.
// ---
// I bind to the "value" event on the Firebase resource.
$scope.startWatchingEvents = function() {
$scope.isWatchingEvents = true;
counterResource.on( "value", handleValueEvent );
};
// I remove the "value" event binding from the Firebase resource.
$scope.stopWatchingEvents = function() {
$scope.isWatchingEvents = false;
counterResource.off( "value", handleValueEvent );
};
// ---
// PRIVATE METHODS.
// ---
// I handle the "value" event snapshot return value.
function handleValueEvent( snapshot ) {
console.log( "Value:", snapshot.val() );
}
// I start altering the count using the RESTful API. I'm using REST here
// to make sure that our WebSockets Frame activity is only capturing the
// Syncing events and NOT the POST/PUT events.
function startUpdatingCounterValueUsingREST() {
var maxRequests = 50;
var currentValue = 0;
var timer = setInterval(
function handleInterval() {
// NOTE: We are using "PUT" to replace the value. If we tried
// using "POST", it would try to treat "counter" as a
// collection and append the value to it as a new child.
$http({
method: "put",
url: ( counterResource.toString() + ".json" ),
data: ++currentValue
});
if ( currentValue >= maxRequests ) {
clearInterval( timer );
}
},
( 1.5 * 1000 )
);
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// Define our Firebase gateway so that we can inject it into other services
// for synchronization with remote data stores.
app.factory(
"firebase",
function( $window ) {
// Create our factory which will create a new instance of the Firebase
// reference for the given path.
function firebase( resourcePath ) {
return( new firebase.Firebase( resourcePath ) )
}
// Keep a reference to the original object in case we need to reference
// it later (ex, in the factory method).
firebase.Firebase = $window.Firebase;
// Delete from the global scope to make sure no one cheats in our
// separation of concerns. The "angular way" is to not use the global
// scope and to inject all needed dependencies.
delete( $window.Firebase );
// Return our factory method.
return( firebase );
}
);
</script>
</body>
</html>
After putting the demo together, I was very happy to see (check video above) that Firebase only synchronizes data when at least one event handler is bound. It won't start syncing until the first event handler is bound; and then, subsequently, it will stop syncing when the last event handler is unbound.
This approach will cut down on any unnecessary bandwidth usage. But, it does mean that each develop must unbind event handlers when a given resource is no longer needed. Otherwise, if an AngularJS Scope (for example) were to be destroyed and a Firebase reference wasn't cleaned up properly, it would continue to sync data indefinitely.
Want to use code from this post? Check out the license.
Reader Comments
@All,
A quick follow-up on a possibly unexpected behavior - different Firebase references share the same underlying event bindings. This means that unbinding events on one reference _may_ cause events on your other reference to be unbound:
www.bennadel.com/blog/2700-individual-firebase-references-don-t-store-unique-event-bindings.htm
Make sure you always provide event-type and a callback reference when unbinding events.