1

I'm writing some JavaScript that interacts with a browser plugin (add-on in Firefox, 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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Jeff
  • 3,669
  • 1
  • 23
  • 33

3 Answers3

1

Why do you say Deferreds don't fit with callbacks? It seems to me that

pluginObject.disconnectDevice(deviceKey).then(function() { 
    pluginObject.connectDevice(deviceKey, backingInfo);
});

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 a Deferred, register it in a queue and return it. When you receive the disconnection event, you resolve 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).

Gustavo Giráldez
  • 2,610
  • 18
  • 12
  • This is spot on with what I was looking for. Not sure why I couldn't wrap my head around placement of the resolve()/reject() yesterday. It's a little bit of additional work up front to wrap all the methods, but it does provide a nice generic way to chain events (or not). Updating question with something similar to my final approach. – Jeff Jun 16 '11 at 19:41
0

Can't you just add a callback to disconnectDevice and pass in the call to connectDevice?

pluginObject.disconnectDevice = function ( key, callback ) {
    // do stuff and then when disconnect complete...
    callback();
};

...then when you want to attach a device...

pluginObject.disconnectDevice(deviceKey, function() {
    pluginObject.connectDevice(deviceKey, backingInfo);
});
mVChr
  • 49,587
  • 11
  • 107
  • 104
  • The disconnectDevice method is defined by the plugin. I can only pass in values it is expecting, which doesn't include a callback. – Jeff Jun 15 '11 at 22:57
0

Assuming you set the handler with a simple property called onConnectionChange, you can write a new function that takes a callback instead.

// The new function, it takes a callback to let you know
// that disconnecting is done
pluginObject.disconnect = function (deviceKey, callback) {
     var me = this;
     me.onConnectionChange = function (connectionState) {
        if (connectionState === state.disconnected) {
             delete me.onConnectionChange;
             callback();
        }
}

// Now you can call
pluginObject.disconnect(deviceKey, function() {
    pluginObject.connectDevice(deviceKey, backingInfo);
});
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217