Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Rob Parkhill
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Rob Parkhill@rob_parkhill )

Individual Firebase References Don't Store Unique Event Bindings

By Ben Nadel on

After my exploration this morning, on the data synchronization behavior in Firebase, I started to wonder about the relationship between unique Firebase instances and event bindings. Specifically, how do event bindings on one instance affect the event bindings on another instance. As it turns out, Firebase instances do not [appear to] maintain unique sets of event bindings. And, unbinding an event from one instance will impact other instances.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To explore this idea, I took my demo from this morning and updated it slightly. This time, rather than instantiating one Firebase reference during Controller initialization, I'd instantiate a new Firebase reference every time that I go to bind and unbind events:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Firebase References Don't Store Unique Event Bindings
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • cursor: pointer ;
  • text-decoration: underline ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Firebase References Don't Store Unique Event Bindings
  • </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-17/";
  •  
  • // 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 on different Firebase references.
  • startUpdatingCounterValueUsingREST();
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I bind to the "value" event on the Firebase resource.
  • $scope.startWatchingEvents = function() {
  •  
  • $scope.isWatchingEvents = true;
  •  
  • // NOTE: Create a NEW REFERENCE to the counter location. Inside the
  • // firebase service, this is actually instantiating a new Firebase
  • // object: new firebase.Firebase().
  • firebase( root + "counter" ).on( "value", handleValueEvent );
  •  
  • };
  •  
  •  
  • // I remove the "value" event binding from the Firebase resource.
  • $scope.stopWatchingEvents = function() {
  •  
  • $scope.isWatchingEvents = false;
  •  
  • // NOTE: Create a NEW REFERENCE to the counter location. Inside the
  • // firebase service, this is actually instantiating a new Firebase
  • // object: new firebase.Firebase().
  • firebase( root + "counter" ).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: ( firebase( root + "counter" ).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>

As you can see, I'm getting a new "counter" reference inside both the startWatchingEvents() and the stopWatchingEvents() methods. And, what I found was that when I run this code, unbinding the "value" event on one reference completely stops data synchronization. Based on my previous blog post, this indicates that the individual Firebase references share the same underlying pool of event bindings.

This is an interesting behavior and may lead to some unexpected outcomes. Imagine that you have an AngularJS application in which different controllers instantiate their own Firebase references. Now imagine that these Controllers have different lifespans; and when one of them is destroyed, it attempts to clean up after itself by calling .off():

  • // Clear all event bindings on THIS reference to make sure that unnecessary
  • // data synchronization does not occur.
  • myUniqueFirebaseReference.off();

If this Firebase reference happens to use the same "location" that another Firebase reference - in a different Controller - is also using, then this "cleanup" will actually remove the event bindings configured by the other Controller.

The take-away here is to never call .off() without arguments. And, calling .off() with just an event-type is likely to cause problems as well. When you're unbinding a Firebase reference event, always provide both an event-type and a function reference.




Reader Comments

@All,

Just to be clear, I am not saying that this is a poor implementation. I am only saying that it might be unexpected due to the syntax (ie, calling a constructor and then binding events on the resultant object).

If you think about it, this is how things like jQuery also work. Imagine that I had two different jQuery references:

var a = $( "#someID" );
var b = $( "#someID" );

... in this case, I have two different jQuery instances; but, the underlying DOM (Document Object Model) is the same. As such, events bound using one instance can be unbound by the other:

a.on( "click", ... );
b.off( "click" ); // Unbinds the previous event handler.

This makes a lot of sense since any other approach would lead to events that could never be unbound due to lost references.

I think the _easiest_ thing to do, in Firebase, would be to do what jQuery did - add event namespaces. Then, references could safely bind/unbind events based on a Controller-specific namespace:

firebase( "my-firebase" ).on( "value.thisController", .... );
firebase( "my-firebase" ).off( "value.thatController" );

In this case, the ".thisController" vs. ".thatController" namespace would allow my generic event handling without stepping on each other's toes.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.