5

I would love to write a generic wrapper that takes a function, and returns the "async-style" version of that function IF it wasn't async to start with.

Trouble is, there is no easy way to know whether the call is sync or async. So... this basically "cannot be done". Right?

(Note that the wrapper should harmonise sync functions to async style, and LEAVE async functions alone)

var wrapper = function( fn ){

    return function(){
      var args = Array.prototype.splice.call(arguments, 0);

      var cb = args[ args.length - 1 ];

      // ?!?!?!?!?
      // I cannot actually tell if `fn` is sync
      // or async, and cannot determine it!    

      console.log( fn.toString() );
    }
}

var f1Async = wrapper( function( arg, next ){
  next( null, 'async' + arg );
})

var f2Sync = wrapper( function( arg ){
  return 'sync' + arg;
})


f1Async( "some", function(err, ret ){
  console.log( ret );
});


f2Sync( "some other", function(err, ret ){
  console.log( ret );
});
Merc
  • 16,277
  • 18
  • 79
  • 122
  • 1
    A lot of people complain about callback hell in node. It's interesting to see someone who loves callbacks so much that they want to add them to sync functions too :) – Dan Oct 29 '13 at 16:13
  • This might be of interest to get the parameter names of the final argument. You can then check the arguments to see if they meet the standard callback pattern? http://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically-from-javascript – Dan Oct 29 '13 at 16:32
  • @Plato How can you check the arguments of a function from outside without explicitly passing them. You would have to pass the arguments of the function with it. What do you think ? – user568109 Nov 02 '13 at 16:49
  • If I knew how to do it I woulda answered myself! Maybe some wizardry involving stringifying the function and looking through the string for the argument names, and looking to see if the function body invokes them with `argName(...)` or something – Plato Nov 02 '13 at 17:19

4 Answers4

2

You cannot find out what the accepted arguments of a function are, so you cannot find out if it takes a callback or not.

mtsr
  • 3,092
  • 1
  • 14
  • 21
1

It simply cannot be done. End of story.

Merc
  • 16,277
  • 18
  • 79
  • 122
1

In javascript there is no way to check if the last argument of a function is a function, because in javascript you do not define the types of your arguments.

My solution works by getting a list of the parameters in the function, then using a RegExp to see if that parameter is used as a function. Also, in the case that the callback is not being used directly (like passing it to something else), it has a list of argument names to be considered as a callback.

And the code is:

var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var CALLBACK_NAMES = [ "next", "done", "callback", "cb"];

function getParamNames(func) {
  var fnStr = func.toString().replace(STRIP_COMMENTS, '')
  var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)
  if(result === null)
    result = []
  return result
}

function checkIfParamIsFunction(func, paramName){
  var fnStr = func.toString();
  if (fnStr.replace(new RegExp("(" + paramName + "\s*\([A-Za-z0-9,\.]*\)?!{|" + paramName + ".apply\([A-Za-z0-9,\.]*\)|" + paramName + ".call\([A-Za-z0-9,\.]*\))", ""), "{<._|/}") != fnStr) { // Remove All Calls to the arg as a function, then check to see if anything was changed.
    return true;
  } else {
    return false;
  }
}


function makeAsync(func) {
  var paramNames = getParamNames(func);
  if (checkIfParamIsFunction(func, paramNames[paramNames.length - 1]) 
  || CALLBACK_NAMES.indexOf(paramNames[paramNames.length - 1]) != -1) {
    // Function Is Already async
    return func;
  } else {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      var cb = args.pop();
      cb(func.apply(this, args));
    }
  }
}

function test1(a){
  return (a+' test1');
};

function test2(a, callback){
  return callback(a+' test2')
};

function main(){
  var tested1 = makeAsync(test1);
  var tested2 = makeAsync(test2);
  tested1('hello', function(output){
    console.log('holy shit it\'s now async!!');
    console.log(output);
  });
  tested2('world', function(output){
    console.log('this one was already async tho');
    console.log(output);
  });
}

main();

Simply call makeAsync(function) and it will return an async function. This will work if you use function.apply or .call.

Plato
  • 10,812
  • 2
  • 41
  • 61
Ari Porad
  • 2,834
  • 3
  • 33
  • 51
  • there's a extraneous bracket after the first `if/else` block and `checkIfParamIsFunction` is undefined, and `paramName` is gonna be undefined in line 3; are you missing a function definition near the top? – Plato Nov 09 '13 at 03:48
  • Plato, are you happy with the answer? Shall I accept it as the right one (which it seems to be)? The bounty expires in 4 hours... – Merc Nov 09 '13 at 10:25
  • i made a couple edits including test code and correcting typos. this works, hooray I knew there must be some hack to get-r-done! – Plato Nov 09 '13 at 15:25
  • The function makeAsync seems to be defined twice...? – Merc Nov 09 '13 at 15:26
  • yes that was one of the things i fixed in my edit. it should be accepted soon – Plato Nov 09 '13 at 15:27
1

Though, this is not the answer, but a good alternative. I have provided example of browser based JavaScript but same class can be used on Node as well.

To solve this problem, promises were developed. However we use a modified version of promise as follow.

function AtomPromise(f)
{ 
   // Success callbacks
   this.sc = [];
   // Error callbacks
   this.ec = [];
   this.i = f;
   this.state = 'ready';
}

AtomPromise.prototype ={
   finish: function(s,r) {
      this.result = r;
      var c = s ? this.sc : this.ec;
      this.state = s ? 'done' : 'failed' ;
      for(var i=o ; i< c.length; i++){
          c[i](this);
      }
   },
   invoke: function(f) {  
      this.state = 'invoking';
      this.i(this);
   },
   then: function(f) {
      this.sc.push(f);
   },
   failed: function(f){
      this.ec.push(f);
   },
   value: function(v) {    
      if(v !== undefined ) this.result = v;
      return this.result;
   },
   pushValue: function(v) {
      var _this = this;
      setTimeout(100, function () { 
         _this.finish(true, v);
      });
   }
}



//wrap jQuery AJAX
AtomPromise.ajax = function( url, options ) {
   return new AtomPromise(function (ap){ 
      $.ajax(url, options)
         .then( function(r){ ap.finish(true, r); })
         .failed( function (){ ap.finish( false, arguments) });
   }) ;
}

//Wrape sync function
AtomPromise.localStorage = function( key ) {
   return new AtomPromise(function (ap){ 
      var v = localStorage[key];
      ap.pushValue(v);
   }) ;
}



// Calling Sequence

AtomPromise.ajax( 'Url' ).then( function(ap) {
   alert(ap.value());
}).invoke();

AtomPromise.localStorage( 'Url' ).then( function(ap) {
   alert(ap.value());
}).invoke();

Both functions are now asynchronous. Push Value method makes result route through setTimeout that makes further calls asynchronous.

This is used in Web Atoms JS to wrape async code into single attribute and by following one pattern you can get rid of async callback hell. http://webatomsjs.neurospeech.com/docs/#page=concepts%2Fatom-promise.html

Disclaimer: I am author of Web Atoms JS.

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
  • the calling sequence at the bottom looks quite clean. why is it necessary to use `setTimeout`? couldnt you simply call `ap.finish(true, v)` instead of `ap.pushValue(v)`? also the milliseconds argument should be second: `setTimeout(fn, 100)` – Plato Nov 09 '13 at 15:47
  • I have explained that the reason I am using pushValue is to make it truely asynchronous, it means it will be called on later on with different Stack, calling ap.finish will make call on same stack, however it is up to you. – Akash Kava Nov 09 '13 at 16:34