3

I'm writing a node CLI where synchronous behaviour is typically more appropriate than async and I'd like to be able to leverage the following convention:

 # Write functional code as an async function which returns a Promise
 function foobar() { ... }
 # Uses async function but blocks on promise fulfillments
 function foobarSync() { ... }

So for instance -- using the RSVP promise implementation -- I have written the following async function for calling shell scripts:

var shell = function (params,options) {
    options = extend({timeout: 4000},options);
    var commandResponse = '';
    var errorMessage ='';
    // resolve with a promise
    return new RSVP.Promise(function(resolve,reject) {
        var spawn = require('child_process').spawn;
        var timeout = setTimeout(function() {
            reject(new Error('Timed out')); // fulfil promise
        }, options.timeout);
        try {
            var shellCommand = spawn(params.shift(),params);
        } catch (err) {
            clearTimeout(timeout);
            reject(err); // fulfil promise
        }
        shellCommand.stdout.setEncoding('utf8');
        shellCommand.stderr.setEncoding('utf8');
        shellCommand.stdout.on('data', function (data) {
            commandResponse = commandResponse + data;
        });
        shellCommand.stderr.on('data', function (data) {
            errorMessage = errorMessage + data;
        });
        shellCommand.on('close', function (code) {
            if(code !== 0) {
                clearTimeout(timeout);
                reject({code:code, message:errorMessage}); // fulfil promise
            } else {
                clearTimeout(timeout);
                resolve(commandResponse); // fulfil promise
            }
        });
    }); 
};

This works, now I want to make synchronously:

 # Works
 shell(['ls','-l']).then( function (results) {
      console.log('Result was: %s', results);
 });
 # Would like to see work
 var results = shellSync(['ls','-l']);

What I thought would work for shellSync is:

var shellSync = function (params,options) {
    options = extend({pollingInterval: 100},options);
    var shellResults = null;
    shell(params,options).then(
        function(results) {
            console.log('Results: %s', results);
            shellResults = results;
            // return results;
        },
        function(err) {
            console.log('Error: %s', err);
            shellResults = err;
            // return err;
        }
    );

    while(!shellResults) {
        // wait until a Promise is returned or broken (and sets the shellResults variable)
    }
    return shellResults;
};

Unfortunately this just runs, never returning. I though that maybe instead of the while loop I'd implement a polling interval to execute the conditional statement on:

    var polling = setInterval(function() {
        // return once shellResults is set; 
        // this setting takes place when either a resolve() or reject() 
        // is called in Promise
        if(shellResults) {
            console.log('results are available');
            clearInterval(polling);
            return shellResults; 
        }
    },options.pollingInterval);

    while(1) {
        // wait 
    }

Of course, removing the while loop results in the function returning immediately (with an as-yet unfulfilled promise). So then I tried to combine the "waiting" functionality of the while loop with a polling frequency implemented

ken
  • 8,763
  • 11
  • 72
  • 133
  • Ok, maybe a crazy crazy idea, but using traceur you could use the EC6 await keyword. It will recompile your code into a weird state machine, but that might be quite an easily solution to your situation. – David Mulder Sep 15 '14 at 19:47
  • That is not possible from within JS itself. You need a plugin for that approach that adds this functionality outside. I don't know if this module works well, so i can't suggest to use it, but you could take a look at [deasync](https://github.com/abbr/deasync) – t.niese Sep 15 '14 at 19:47
  • @t.niese I did look at it briefly ... my first impression was that it might not really do what I needed. I also found an NPM module called exec-sync that works brilliantly on OSX (it compiles onto every platform) but seems to fall over on Ubuntu. :( – ken Sep 15 '14 at 19:50
  • @DavidMulder I would love to avoid EC6 for the moment. It's on my bucketlist of things to get fully wrapped around but if at all possible I'd like to constrain myself to Node (w/o Harmony) right now. I did notice Task.js which looks like a ES6 answer to this problem though. – ken Sep 15 '14 at 19:52
  • Maybe another question is ... why doesn't my approach work? Why does my polling interval never respond? – ken Sep 15 '14 at 19:55
  • 2
    Because `js` itself is not multithreaded. As long as you are in the `while(!shellResults) {}` block no other `js` code is executed. – t.niese Sep 15 '14 at 19:55
  • @t.niese ahhh, that makes sense. Damn. – ken Sep 15 '14 at 19:58

1 Answers1

0

The easiest way is to consume sync API on your internal code if you want it to be sync, but you want to wrap the async code as sync, right ?

I think this can be acchieved using fibers (https://www.npmjs.org/package/fibers), or more specifically, futures, a little example

var Future = require("fibers/future");

// async API, will be wrapped by syncFoo
var asyncFoo = function(cb) {
  setTimeout(function() {
    cb("foo");
  }, 1500);
};

var syncFoo = function() {
  var future = new Future();
  console.log("asyncFoo will be called");
  asyncFoo(function(x) {
    future.return(x);
  });
  console.log("asyncFoo ended");
  return future.wait();
};

(function() {
console.log("* Syncfoo will be called");
syncFoo();
console.log("* Syncfoo ended");

console.log("* Syncfoo will be called again");
syncFoo();
console.log("* Syncfoo ended again");

}).future()();

More specific for your code:


var shellSync = function(params, options) {
    var future = new Future();

    options = extend({pollingInterval: 100},options);
    var shellResults = null;
    shell(params,options).then(
        function(results) {
            console.log('Results: %s', results);
            future.return({results: results});
        },
        function(err) {
            console.log('Error: %s', err);
            future.return({err: err});
        }
    );

    var ret = future.wait();
    if (ret.err) {
      throw ret.err;
    } else {
      return ret.results;
    }
};

EDIT NOTE: You should wrap all in (function() {...}).future()(); to run this on a fiber

dseminara
  • 11,665
  • 2
  • 20
  • 22
  • Looks really promising. Can't wait to try it but must sleep first. Will try first thing in the morning. :) – ken Sep 15 '14 at 21:37
  • First attempt using your code example I get an error that "Can't wait without a fiber" when calling `future.wait()`. Will investigate. – ken Sep 16 '14 at 07:36
  • Errr. Couldn't get my head around it in the time allocated. Here's my gist: https://gist.github.com/ksnyde/d78cc01f62def105f1a0 – ken Sep 16 '14 at 07:55
  • You should wrap all in (function() { }).future()(); – dseminara Sep 17 '14 at 13:38