Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ryan Jeffords and Doug Boude
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ryan Jeffords@ryanjeffords ) and Doug Boude@dougboude )

Handling Plupload's Uploader Init Race Condition In AngularJS

By Ben Nadel on

When we finally setup New Relic at InVision App, I noticed a lot of JavaScript errors that said, "Uncaught TypeError: Cannot read property 'appendChild' of null." After some debugging, I was able to trace this back to the initialization of the HTML5 Plupload file uploader. And, after further debugging, I was able to narrow the problem down to asynchronous nature of the uploader initialization, which can cause a race condition with the state (and very existence) of the View in which it is supposed to function.


 
 
 

 
 
 
 
 

The Moxie and Plupload libraries are non-trivial pieces of code. I have a general sense of how they work; but, there are many gaps in my understanding. What I know is that during the uploader initialization, it starts to run a collection of init functions in serial. And, in each of these init functions, there is a timeout that occurs between the "Init" event and the "RuntimeInit" event. I don't understand the code well enough to explain the need for the timeout; but, I can easily demonstrate that it causes a race condition.

Consider the following AngularJS demo in which we initialize the Plupload uploader inside of a directive. We are trying to be good citizens and are calling the .destroy() method when the Scope is destroyed. But, if the Scope is destroyed too quickly, our view will be torn down before the Plupload uploader finishes its asynchronous initialization. This causes a JavaScript error when it tries to reference a DOM (Document Object Model) element that no longer exists:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Handling Plupload Init Race Condition In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Handling Plupload Init Race Condition In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggleUploader()">Show uploader</a>
  • </p>
  •  
  • <div
  • ng-if="isShowingUploader"
  • bn-uploader
  • id="uploader-container">
  •  
  • <span id="uploader-button">Browse Files</span>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.min.js"></script>
  • <script type="text/javascript" src="./moxie.patched.js"></script>
  • <script type="text/javascript" src="./plupload.dev.patched.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.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope, $location ) {
  •  
  • // I determine if the uploader is being shown.
  • $scope.isShowingUploader = false;
  •  
  • // I listen to the URL changes since we are going to let the state of
  • // the URL control whether or not the uploader is visible.
  • $scope.$on( "$locationChangeSuccess", handleLocationChange );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the uploader open or close.
  • // --
  • // NOTE: This works by adjusting the state of the URL which will, in
  • // turn, trigger our URL handler and update our local view-model.
  • $scope.toggleUploader = function() {
  •  
  • var newValue = ( $location.search().uploader === "true" )
  • ? null
  • : "true"
  • ;
  •  
  • $location.search( "uploader", newValue );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // When the URL changes, I update the local view-model to reflect the
  • // desired uploader state.
  • function handleLocationChange() {
  •  
  • $scope.isShowingUploader = ( $location.search().uploader === "true" );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the uploader.
  • angular.module( "Demo" ).controller(
  • "UploadController",
  • function UploadController( $scope, $location ) {
  •  
  • // For the sake of the demo, let's immediately redirect back into a
  • // state in which the uploader should be closed. This will demonstrate
  • // the race condition between the uploader initialization and the view
  • // rendering and linking.
  • $location.search( "uploader", null );
  •  
  • }
  • );
  •  
  •  
  • // I manage the UI portion of the uploader.
  • angular.module( "Demo" ).directive(
  • "bnUploader",
  • function bnUploaderDirective() {
  •  
  • // Return the directive configuration object.
  • return({
  • controller: "UploadController",
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I bind the JavaScript events to the view-model.
  • function link( scope, element, attributes ) {
  •  
  • console.log( ".... Uploader directive linked." );
  •  
  • // Let's log the next tick so we can when things happen in the console.
  • setTimeout(
  • function() {
  •  
  • console.log( ">>>> Tick <<<<" );
  •  
  • }
  • );
  •  
  •  
  • // Create our Plupload uploader instance.
  • var uploader = new plupload.Uploader({
  • runtimes: "html5",
  • browse_button : 'uploader-button',
  • container: "uploader-container",
  • drop_element: "uploader-container",
  • url : "./this-isnt-relevant-to-the-demo/"
  • });
  •  
  • // Initialize instance.
  • uploader.init();
  •  
  • // Since we are allocating some heavy objects, we need to make sure
  • // that we cleanup after ourselves. When the scope is destroyed, we
  • // need to teardown all of Plupload stuff.
  • scope.$on( "$destroy", handleDestroy );
  •  
  •  
  • // I handle the destroy event, performing cleanup.
  • function handleDestroy() {
  •  
  • console.warn( ".... Destroying uploader." );
  •  
  • uploader.destroy();
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the Controller that manages the uploader immediately redirects the user out of the uploader module. This gives the view time to Link and be destroyed; but, it doesn't give the uploader time to fully initialize. As such, we get the following console output:


 
 
 

 
 Plupload uploader race condition in init script in AngularJS. 
 
 
 

Notice that we are handling the $destroy event, in the directive, before the Plupload uploader finishes its internal initialization. This is why we get the JavaScript error.

I've spent [literally] hours digging through the Moxie / Plupload source code trying to figure out if there is a way that I could patch the code to take this race condition into account. The code is quite complicated and over my head in most parts. I think that, ultimately, the problem is that the initialization happens in a series of asynchronous callbacks, which makes it hard to "cancel" mid-stream.

Right now, the best I can do is try to address this particular edge-case in which the AngularJS view is destroyed very quickly. The easiest approach is to defer the actual call to .init() until the next(ish) tick of the event loop. This way, if the view is destroyed immediately, we can prevent the uploader from ever initializing. This doesn't remove the race condition; but, it does something to mitigate the likelihood that you'll run into the race condition.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Handling Plupload Init Race Condition In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Handling Plupload Init Race Condition In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggleUploader()">Show uploader</a>
  • </p>
  •  
  • <div
  • ng-if="isShowingUploader"
  • bn-uploader
  • id="uploader-container">
  •  
  • <span id="uploader-button">Browse Files</span>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.min.js"></script>
  • <script type="text/javascript" src="./moxie.patched.js"></script>
  • <script type="text/javascript" src="./plupload.dev.patched.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.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope, $location ) {
  •  
  • // I determine if the uploader is being shown.
  • $scope.isShowingUploader = false;
  •  
  • // I listen to the URL changes since we are going to let the state of
  • // the URL control whether or not the uploader is visible.
  • $scope.$on( "$locationChangeSuccess", handleLocationChange );
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the uploader open or close.
  • // --
  • // NOTE: This works by adjusting the state of the URL which will, in
  • // turn, trigger our URL handler and update our local view-model.
  • $scope.toggleUploader = function() {
  •  
  • var newValue = ( $location.search().uploader === "true" )
  • ? null
  • : "true"
  • ;
  •  
  • $location.search( "uploader", newValue );
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // When the URL changes, I update the local view-model to reflect the
  • // desired uploader state.
  • function handleLocationChange() {
  •  
  • $scope.isShowingUploader = ( $location.search().uploader === "true" );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the uploader.
  • angular.module( "Demo" ).controller(
  • "UploadController",
  • function UploadController( $scope, $location ) {
  •  
  • // For the sake of the demo, let's immediately redirect back into a
  • // state in which the uploader should be closed. This will demonstrate
  • // the race condition between the uploader initialization and the view
  • // rendering and linking.
  • $location.search( "uploader", null );
  •  
  • }
  • );
  •  
  •  
  • // I manage the UI portion of the uploader.
  • angular.module( "Demo" ).directive(
  • "bnUploader",
  • function bnUploaderDirective( $timeout ) {
  •  
  • // Return the directive configuration object.
  • return({
  • controller: "UploadController",
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I bind the JavaScript events to the view-model.
  • function link( scope, element, attributes ) {
  •  
  • console.log( ".... Uploader directive linked." );
  •  
  • // Let's log the next tick so we can when things happen in the console.
  • setTimeout(
  • function() {
  •  
  • console.log( ">>>> Tick <<<<" );
  •  
  • }
  • );
  •  
  •  
  • // Create our Plupload uploader instance.
  • var uploader = new plupload.Uploader({
  • runtimes: "html5",
  • browse_button : 'uploader-button',
  • container: "uploader-container",
  • drop_element: "uploader-container",
  • url : "./this-isnt-relevant-to-the-demo/"
  • });
  •  
  • // Due to a race condition in the Plupload initialization, we want to
  • // defer the actual call to .init(). This way, if the current scope is
  • // quickly destroyed, we won't leave a whole lot of junk in memory.
  • var initTimer = $timeout(
  • function initUploader() {
  •  
  • uploader.init();
  •  
  • },
  • 0,
  • false // No need to trigger a digest.
  • );
  •  
  • // Since we are allocating some heavy objects, we need to make sure
  • // that we cleanup after ourselves. When the scope is destroyed, we
  • // need to teardown all of Plupload stuff.
  • scope.$on( "$destroy", handleDestroy );
  •  
  •  
  • // I handle the destroy event, performing cleanup.
  • function handleDestroy() {
  •  
  • console.warn( ".... Destroying uploader." );
  •  
  • // If the Plupload uploader had not yet been initialized, stop
  • // the timer to make sure it doesn't try to.
  • $timeout.cancel( initTimer );
  •  
  • // We can safely call this even if the uploader was never
  • // initialized.
  • uploader.destroy();
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, this time, rather than calling uploader.init() right away in the linking function, we wrap it in a $timeout() call. This way, we can cancel the timer (and any pending initialization) when the $destroy event is triggered. Again, this doesn't remove the race condition - it's just a small measure to help avoid it.

While I was digging into this, I think I found some memory leaks in Plupload's teardown code. But, that's beyond the scope of this post (and will require some more digging and patching). For now, I'll just settle for getting around the race condition in the init algorithm.




Reader Comments

@Ben:

Why not use Plupload's event system. You could use the PostInit event to set a flag in your scope that indicates that it's initialized, so your destroy can clean it up. You can also check in the PostInit event see if view is destroyed and if so, have Plupload destroy itself. That way you're not relying on timers.

Reply to this Comment

@Dan,

I had not considered using the events. I wonder if I can bind to the Init event and check to see if the scope is destroyed, and if so, prevent the RuntimeInit event from being fired. Honestly, I'm not too familiar with the events that surround the initialization, only those that deal with the file I/O stuff. Great thought - I'll dig into that.

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.