Kohányi Róbert
Kohányi Róbert

Reputation: 10151

How to return a value and a promise from a function at the same time?

I'm doing something like this

var command1;
var command2;

var fn = function(param) {
  var deferred = Q.defer();
  var command = spawn(..., [
    ... passing different arguments based on param ...
  ]);
  ...
  command.stdout.on('data', function(data) {
    if (/... if process started successfully .../.test(data)) {
      deferred.resolve();
    }
  });
  ...
  if (param === 'command1') {
     command1 = command;
  } else {
     command2 = command;
  }
  return deferred.promise;
};

Q.all([
  fn('command1'),
  fn('command2')
]);

and later on I'm calling command1.kill() and command2.kill(). I thought about passing command to resolve, but then it may never be called. I could pass command to reject also, so that I could call kill there if something goes wrong, but that feels weird.

How do I return command and the promise as well to the caller in an idiomatic way? Without the conditional part in fn. What are the possibilities?

I also thought about ES6's deconstructing assignment feature, but consider the following

  ...
  return [command, deferred.promise];
}

[command1, promiseCommand1] = fn('command1');
[command2, promiseCommand2] = fn('command2');

Q.all([
  promise1,
  promise2.then(Q.all([
    promiseCommand1,
    promiseCommand2    
  ])
]);

but this fails (at least in my particular case, where the commands should wait until promise2 is resolved), because the processes are already on their way when I pass promiseCommand1 and promiseCommand2 to Q.all.

Not sure if I used the correct deconstructing assignment syntax.

Just popped into my mind

var command1;
var command2;

var fn = function(param, callback) {
  var deferred = Q.defer();
  var command = spawn(..., [...]);
  ...
  callback(command);
  return deferred.promise; 
};

Q.all([
  fn('command1', function(command) {
    command1 = command;
  }),
  fn('command1', function(command) {
    command2 = command;
  })
]);

Any other way?

Update

Since yesterday I've figured out how could I might use a deconstructing assignment (still not sure on the syntax)

Q.all([
  promise1,
  promise2.then(function() {
    [command1, promiseCommand1] = fn('command1');
    [command2, promiseCommand2] = fn('command2');    
    return Q.all([
      promiseCommand1,
      promiseCommand2    
    ]);
  })
]);

this way the commands will only be executed after promise2 is resolved.

Solution

Based on the accepted answer and my previous update I came up with this

  command.promise = deferred.promise;
  return command;
};

Q.all([
  promise1,
  promise2.then(function() {
    command1 = fn('command1');
    command2 = fn('command2');    
    return Q.all([command1.promise, command2.promise]);
  })
]);

Works and seems to be a concise solution for me. I don't want to rely on ES6 for the deconstructing assignment. Also, I don't think I can use the feature to assign one value to a variable declared outside of the scope and assign the other in the local scope concisely. Returning

return {
  command: command,
  promise: deferred.promise
};

is also a possible solution, but less concise.

Q.all([
  promise1,
  promise2.then(function() {
    var result1 = fn('command1');
    var result2 = fn('command2');
    command1 = result1.command;
    command2 = result2.command;   
    return Q.all([result1.promise, result2.promise]);
  })
]);

Correction

In the comment section in the accepted answer I was advised to call reject in fn to prevent my code hang forever because of a pending promise. I've solved this with the following

  command.promise = deferred.promise.timeout(...);
  return command;
};

Using timeout will return the same promise, however if the promise is not resolved in given timeout value the promise will be rejected automatically.

Upvotes: 0

Views: 384

Answers (2)

Roamer-1888
Roamer-1888

Reputation: 19288

You should end up with something useful by turning your "pass command to reject" on its head. In other words, reject in response to a kill() command.

The trouble, as you know, is that fn() should return a promise, and a promise doesn't naturally convey the corresponding command's .kill() method. However javascript allows the dynamic attachment of properties, including functions (as methods), to objects. Therefore adding a .kill() method is simple.

var fn = function(param) {
    var deferred = Q.defer();
    var command = spawn(..., [
        ... passing different arguments based on param ...
    ]);
    ...
    command.stdout.on('data', function(data) {
        if (/... if process started successfully .../.test(data)) {
            deferred.resolve();
        }
    });
    ...
    var promise = deferred.promise;
    // Now monkey-patch the promise with a .kill() method that fronts for command.kill() AND rejects the Deferred.
    promise.kill = function() {
        command.kill();
        deferred.reject(new Error('killed')); // for a more specific error message, augment 'killed' with something unique derived from `param`. 
    }
    return promise;
};

var promise1 = fn('command1');
var promise2 = fn('command2');

Q.all([promise1, promise2]).spread(...).catch(...);

promise1.kill() or promise2.kill() will give rise to "killed" appearing as error.message in the catch handler.

The two kills can be called as appropriate, for example ...

if(...) {
    promise1.kill();
}
if(...) {
    promise2.kill();
}

Alternatively, the .kill() methods will also detach cleanly without needing to .bind(), for example :

doSomethingAsync(...).then(...).catch(promise1.kill);
doSomethingElseAsync(...).then(...).catch(promise2.kill);

Note that fn() will work for any number of calls without the need for outer vars command1, command2 etc.

Upvotes: 1

Alex Yatkevich
Alex Yatkevich

Reputation: 1434

You can return an array and then use promise.spread method.

https://github.com/kriskowal/q#combination

.then(function () {
    return [command, promise];
})
.spread(function (command, promise) {
});

Upvotes: 1

Related Questions