Handling Plupload's Uploader Init Race Condition In AngularJS

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:

// 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:

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.

// 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

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.

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.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.