I'm writing some JavaScript that interacts with a browser plugin (add-on in FF, ActiveX in IE). Some of the calls are asynchronous and trigger an event when complete. There are certain cases where I need to chain calls together, but need to wait for the first call to complete before initiating the second call. For instance:
A call to attach a device needs to first check if a device is already attached, and disconnect it. So the calls would be:
pluginObject.disconnectDevice(deviceKey);
pluginObject.connectDevice(deviceKey, backingInfo);
Just calling them straight up like this would fail because a connect is initiated before the disconnect is actually complete, and the plugin can't handle that.
I've already got listeners set up and I can do something like:
function handleConnectionChange(connectionState, /* ... */, doReconnect) {
if (connectionState === state.disconnected && doReconnect) {
pluginObject.connectDevice(deviceKey, backingInfo);
}
}
But I'd actually like a more generic approach -- one that is reusable even with a different set of chained events.. This is simple enough in this particular instance, but even this is a little sloppy. I've added reconnect information logic to the connection event handler and I could easily see conditional chaining getting out of hand when done like this.
Are there any libraries out there that tackle this kind of workflow? I looked at jQuery's deferred stuff and it has the chaining that I want, but it doesn't really seem to fit with callbacks. Sproutcore's StateCharts look interesting, but again don't seem to have the ability to rework an already asynchronous flow.
Update: Solution Thanks to some pointers from Gustavo, I've gone with the approach of wrapping the plugin calls to make them utilize jQuery Deferred objects:
var connectDevice = function(deviceKey, backingInfo) {
var deferred = $.Deferred();
var connectHandler = function(event) {
// unregister connectHandler
if (event.connectionState === ERROR) {
showAlert("Error connecting device", event.message);
deferred.reject();
} else {
showAlert("Device connected", event.message);
deferred.resolve();
}
};
// register connectHandler
pluginObject.connectDevice(deviceKey, backingInfo);
return deferred.promise();
};
If I make a similar wrapper for disconnecting, I can now chain the method calls (or not depending on the flow):
if (forceDisconnect) {
disconnectDevice(deviceKey).done(connectDevice(deviceKey, backingType));
} else {
connectDevice(deviceKey, backingType);
}
I can also start a chain with the connectDevice, or add another function to the forceDisconnect flow, etc.
Why do you say Deferreds don't fit with callbacks? It seems to me that
reads pretty natural.
If you can't change the plugin code, you can still use this approach, but of course you'll have to wrap the plugin in some sort of API adapter. Basically, each call to
disconnectDevice
in your wrapper would create aDeferred
, register it in a queue and return it. When you receive the disconnection event, youresolve
all pending deferreds in order (be careful with re-entrance).You'd need to do this for every event fired by the plugin that you want to handle.
The deferred approach has the benefit of improved code readability and maintainability IMHO (over the event listener API).
Can't you just add a callback to
disconnectDevice
and pass in the call toconnectDevice
?...then when you want to attach a device...
Assuming you set the handler with a simple property called onConnectionChange, you can write a new function that takes a callback instead.