Merc
Merc

Reputation: 17077

Generic wrapper to harmonise functions to async style

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 );
});

Upvotes: 5

Views: 354

Answers (4)

Ari Porad
Ari Porad

Reputation: 2922

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.

Upvotes: 1

Akash Kava
Akash Kava

Reputation: 39946

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.

Upvotes: 1

mtsr
mtsr

Reputation: 3142

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

Upvotes: 2

Merc
Merc

Reputation: 17077

It simply cannot be done. End of story.

Upvotes: 1

Related Questions