19

I am doing something that involves running a sequence of child_process.spawn() in order (to do some setup, then run the actual meaty command that the caller is interested in, then do some cleanup).

Something like:

doAllTheThings()
  .then(function(exitStatus){
    // all the things were done
    // and we've returned the exitStatus of
    // a command in the middle of a chain
  });

Where doAllTheThings() is something like:

function doAllTheThings() {
  runSetupCommand()
    .then(function(){
      return runInterestingCommand();
    })
    .then(function(exitStatus){
      return runTearDownCommand(exitStatus); // pass exitStatus along to return to caller
    });
}

Internally I'm using child_process.spawn(), which returns an EventEmitter and I'm effectively returning the result of the close event from runInterestingCommand() back to the caller.

Now I need to also send data events from stdout and stderr to the caller, which are also from EventEmitters. Is there a way to make this work with (Bluebird) Promises, or are they just getting in the way of EventEmitters that emit more than one event?

Ideally I'd like to be able to write:

doAllTheThings()
  .on('stdout', function(data){
    // process a chunk of received stdout data
  })
  .on('stderr', function(data){
    // process a chunk of received stderr data
  })
  .then(function(exitStatus){
    // all the things were done
    // and we've returned the exitStatus of
    // a command in the middle of a chain
  });

The only way I can think to make my program work is to rewrite it to remove the promise chain and just use a raw EventEmitter inside something that wraps the setup/teardown, something like:

withTemporaryState(function(done){
  var cmd = runInterestingCommand();
  cmd.on('stdout', function(data){
    // process a chunk of received stdout data
  });
  cmd.on('stderr', function(data){
    // process a chunk of received stderr data
  });
  cmd.on('close', function(exitStatus){
    // process the exitStatus
    done();
  });
});

But then since EventEmitters are so common throughout Node.js, I can't help but think I should be able to make them work in Promise chains. Any clues?

Actually, one of the reasons I want to keep using Bluebird, is because I want to use the Cancellation features to allow the running command to be cancelled from the outside.

d11wtq
  • 34,788
  • 19
  • 120
  • 195
  • Please check out the docs on progression in the Bluebird API docs and how to deal with the problem. – Benjamin Gruenbaum Aug 09 '14 at 17:49
  • @BenjaminGruenbaum the docs say that API is deprecated and shouldn't be used, but also it doesn't seem to answer the question I asked? I'm not tracking progress, I just have multiple events that get fired and need to be able to handle them all. – d11wtq Aug 10 '14 at 11:23
  • The solution there (the alternative to progress) should suit your case. A promise is not an event emitter - have an event emitter and use a promise to track completion. – Benjamin Gruenbaum Aug 10 '14 at 11:33
  • 1
    I am having the same problem and I found a good blog post about this pattern: http://www.datchley.name/promise-patterns-anti-patterns/ . You can scroll down to the middle where it says "Executing Promises in Series". – MichelH Jul 19 '16 at 17:51

1 Answers1

29

There are two approaches, one provides the syntax you originally asked for, the other takes delegates.

function doAllTheThings(){
     var com = runInterestingCommand();
     var p = new Promise(function(resolve, reject){
         com.on("close", resolve);
         com.on("error", reject);
     });
     p.on = function(){ com.on.apply(com, arguments); return p; };
     return p;
}

Which would let you use your desired syntax:

doAllTheThings()
  .on('stdout', function(data){
    // process a chunk of received stdout data
  })
  .on('stderr', function(data){
    // process a chunk of received stderr data
  })
  .then(function(exitStatus){
    // all the things were done
    // and we've returned the exitStatus of
    // a command in the middle of a chain
  });

However, IMO this is somewhat misleading and it might be desirable to pass the delegates in:

function doAllTheThings(onData, onErr){
     var com = runInterestingCommand();
     var p = new Promise(function(resolve, reject){
         com.on("close", resolve);
         com.on("error", reject);
     });
     com.on("stdout", onData).on("strerr", onErr);
     return p;
}

Which would let you do:

doAllTheThings(function(data){
    // process a chunk of received stdout data
  }, function(data){
    // process a chunk of received stderr data
  })
  .then(function(exitStatus){
    // all the things were done
    // and we've returned the exitStatus of
    // a command in the middle of a chain
  });
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    Agree with you on your last point. Thanks for the detailed answer! – d11wtq Aug 11 '14 at 22:10
  • @Benjamin Gruenbaum What about passing a EventEmitter object in your last example? Instead of passing several params? – lucaswxp Nov 04 '15 at 11:21
  • 2
    Isn't there a danger of the error event being fired multiple times here? This would cause the promise to be rejected more than once as well, which is not possible. – Tom Jan 31 '16 at 10:44