js/core: Refactored and shared code between the cb and promise case
for client and async-call.

Change-Id: If26f53f7a0b9468b505d6c4569f30274ce3328d1
diff --git a/src/lib/async-call.js b/src/lib/async-call.js
index 7fd8052..ee9b92c 100644
--- a/src/lib/async-call.js
+++ b/src/lib/async-call.js
@@ -5,11 +5,16 @@
 // This file defines a async calling convention intended used to call
 // user-defined functions.
 
-var verror = require('../gen-vdl/v.io/v23/verror');
 var logger = require('../lib/vlog').logger;
+var makeError = require('../verror/make-errors');
+var actions = require('../verror/actions');
 
 module.exports = asyncCall;
 
+var IncorrectResultCountError = makeError(
+  'v.io/core/javascript.IncorrectResultCount',
+  actions.NO_RETRY,
+  '{1:}{2:} IncorrectResultCount: Expected {3} results, but got {4}{:_}');
 /**
  * asyncCall performs a call and calls a callback with the result.
  *
@@ -34,58 +39,30 @@
       logger.error('Callback called multiple times');
       return;
     }
-    inputCb.apply(this, arguments);
+    inputCb.apply(self, arguments);
     cbCalled = true;
   }
-  // Call the callback and log the error. Used for internal errors.
-  function asyncFailedCb(err) {
+  function handleResult(err, res) {
+    if (err) {
+      // Error case
+      return callOnceCb(err);
+    }
+    // Results case
+    var numResults = res.length;
+    if (numResults === numOutArgs) {
+      // Correct number of out args given
+      return callOnceCb(null, res);
+    }
+    // Internal error: incorrect number of out args
+    err = new IncorrectResultCountError(ctx, numOutArgs, numResults);
     logger.error(err);
     callOnceCb(err);
   }
-
   // Callback we are injecting into the user's function
   function injectedCb(err /*, args */) {
-    var res = Array.prototype.slice.call(arguments, 1);
-
-    if (err) {
-      // Error case
-      callOnceCb(err);
-    } else {
-      // Results case
-      var numResults = res.length;
-      if (numResults === numOutArgs) {
-        // Correct number of out args given
-        callOnceCb(null, res);
-      } else {
-        // Internal error: incorrect number of out args
-        asyncFailedCb(makeIncorrectArgCountError(true, outArgs, numResults));
-      }
-    }
+    handleResult(err, Array.prototype.slice.call(arguments, 1));
   }
 
-  // Creates an error when there are an incorrect number of arguments.
-  // TODO(bjornick): Generate a real verror for this so it can be
-  // internationalized.
-  function makeIncorrectArgCountError(isCb, expectedArgs, numGiven) {
-    var delta = numGiven - expectedArgs.length;
-    var prefix;
-    if (isCb) {
-      prefix = 'Callback of form cb(err,' + expectedArgs + ')';
-    } else {
-      prefix = 'Expected out args ' + expectedArgs + ' but';
-    }
-
-    var suffix;
-    if (delta < 0) {
-      suffix = 'was missing ' + expectedArgs.slice(numGiven);
-    } else {
-      suffix = 'got ' + delta + ' extra arg(s)';
-    }
-
-    return new verror.InternalError(ctx, prefix, suffix);
-  }
-
-
   if (fn.hasCallback()) {
     args.push(injectedCb);
   }
@@ -113,35 +90,31 @@
     //
     // Convert the results to always be in array style:
     // [], [a], [a, b], etc
-    var resAsArray;
+    // Note that the arity checking isn't done here, but at a later point
+    // sharing the logic between the callback and promise case.
     switch (numOutArgs) {
       case 0:
         if (res !== undefined) {
-          return asyncFailedCb(new verror.InternalError(ctx,
-            'Non-undefined value returned from function with 0 out args'));
+          return Promise.reject(
+            new IncorrectResultCountError(ctx, 0, 1,
+                                          'expected undefined result ' +
+                                          'for void function'));
         }
-        resAsArray = [];
-        break;
+        return [];
       case 1:
         // Note: If res is undefined, the result is [undefined].
-        resAsArray = [res];
-        break;
+        return [res];
       default:
         if (!Array.isArray(res)) {
-          asyncFailedCb(new verror.InternalError(
-            ctx,
-            'Expected multiple out arguments to be returned in an array.'));
+          return Promise.reject(
+            new IncorrectResultCountError(ctx, numOutArgs, 1));
         }
-        resAsArray = res;
-        break;
+        return res;
     }
-    var numResults = resAsArray.length;
-    if (numResults !== numOutArgs) {
-      asyncFailedCb(makeIncorrectArgCountError(false, outArgs, numResults));
-    }
-    callOnceCb(null, resAsArray);
-  }).catch(function error(err) {
-    callOnceCb(wrapError(err));
+  }).then(function(res) {
+    handleResult(null, res);
+  }).catch(function(err) {
+    handleResult(wrapError(err));
   });
 }
 
@@ -150,13 +123,12 @@
  * This is used in cases where values are known to be errors even if they
  * are not of error type such as if they are thrown or rejected.
  * @private
- * @param {Error} err The error or other value.
+ * @param {*} err The error or other value.
  * @return {Error} An error or type Error.
  */
 function wrapError(err) {
-  if (!(err instanceof Error)) {
-    return new Error(err);
-  } else {
+  if (err instanceof Error) {
     return err;
   }
+  return new Error(err);
 }
diff --git a/src/rpc/client.js b/src/rpc/client.js
index 840b3ae..e816fb5 100644
--- a/src/rpc/client.js
+++ b/src/rpc/client.js
@@ -99,68 +99,61 @@
   var ctx = this._ctx;
   var self = this;
 
-  var cb;
   var outArgTypes = this._outArgTypes;
 
-  if (this._cb) {
-    // Wrap the callback to call with multiple arguments cb(err, a, b, c)
-    // rather than cb(err, [a, b, c]).
-    var origCb = this._cb;
-    cb = function convertToMultiArgs(err, results) { // jshint ignore:line
-      // If called from a deferred, the results are undefined.
+  var def = new Deferred();
+  var cb = this._cb;
+  var promise = def.promise.then(function(args) {
+    if (!Array.isArray(args)) {
+      throw new verror.InternalError(
+        ctx, 'Internal error: incorrectly formatted out args in client');
+    }
 
-      if (err) {
-        origCb(err);
-        return;
+    return Promise.all(args.map(function(outArg, i) {
+      return convertOutArg(ctx, outArg, outArgTypes[i], self._controller);
+    }));
+  }).then(function(results) {
+    if (cb) {
+      // Make a copy the results to so we can push a null for the
+      // error onto the front of the arg list.
+      var cbArgs = results.slice();
+      cbArgs.unshift(null);
+      try {
+        cb.apply(null, cbArgs);
+      } catch (e) {
+        process.nextTick(function() {
+          throw e;
+        });
       }
-
-      // Each out argument should also be unwrapped. (results was []any)
-      results = results || [];
-      var resultPromises = results.map(function(res, i) {
-        return convertOutArg(ctx, res, outArgTypes[i], self._controller);
-      });
-      Promise.all(resultPromises)
-      .then(function(results) {
-        results.unshift(null);
-        origCb.apply(null, results);
-      }).catch(origCb);
-    };
-  }
-
-  var def = new Deferred(cb);
-
-  if (!this._cb) {
+    }
     // If we are using a promise, strip single args out of the arg array.
     // e.g. [ arg1 ] -> arg1
-    def.promise = def.promise.then(function(args) {
-      if (!Array.isArray(args)) {
-        throw new verror.InternalError(ctx,
-          'Internal error: incorrectly formatted out args in client');
+    switch(results.length) {
+      // We expect:
+      // 0 args - return; // NOT return [];
+      // 1 args - return a; // NOT return [a];
+      // 2 args - return [a, b] ;
+      //
+      // Convert the results from array style to the expected return style.
+      // undefined, a, [a, b], [a, b, c] etc
+      case 0:
+        return undefined;
+      case 1:
+        return results[0];
+      default:
+        return results;
+    }
+  });
+
+  if (cb) {
+    promise.catch(function(err) {
+      try {
+        cb(err);
+      } catch(e) {
+        process.nextTick(function() {
+          throw e;
+        });
       }
-
-
-      // Each out argument should also be unwrapped. (args was []any)
-      var unwrappedArgPromises = args.map(function(outArg, i) {
-        return convertOutArg(ctx, outArg, outArgTypes[i], self._controller);
-      });
-
-      return Promise.all(unwrappedArgPromises).then(function(unwrappedArgs) {
-        // We expect:
-        // 0 args - return; // NOT return [];
-        // 1 args - return a; // NOT return [a];
-        // 2 args - return [a, b] ;
-        //
-        // Convert the results from array style to the expected return style.
-        // undefined, a, [a, b], [a, b, c] etc
-        switch(unwrappedArgs.length) {
-          case 0:
-            return undefined;
-          case 1:
-            return unwrappedArgs[0];
-          default:
-            return unwrappedArgs;
-        }
-      });
     });
   }
 
@@ -171,7 +164,7 @@
     // inStreamingType.
     def.stream = new Stream(this._id, streamingDeferred.promise, true,
       this._outStreamingType, this._inStreamingType, this._typeEncoder);
-    def.promise.stream = def.stream;
+    promise.stream = def.stream;
   }
 
   var message = this.constructMessage();
@@ -187,7 +180,7 @@
     });
   }
 
-  return def.promise;
+  return promise;
 };
 
 OutstandingRPC.prototype.handleResponse = function(type, data) {
diff --git a/test/unit/test-async-call.js b/test/unit/test-async-call.js
index e92ead6..f17e96b 100644
--- a/test/unit/test-async-call.js
+++ b/test/unit/test-async-call.js
@@ -250,7 +250,7 @@
   var inspectFn = new InspectableFunction(returnNonUndefined);
   asyncCall(null, null, inspectFn, [], [], function(err) {
     t.ok((''+err).indexOf(
-      'Non-undefined value returned from function with 0 out args') !== -1,
+      'Expected 0 results, but got 1') !== -1,
       'Expected error when non-undefined value returned');
     t.end();
   });
diff --git a/test/unit/test-caveat-validator-registry.js b/test/unit/test-caveat-validator-registry.js
index f43a275..27f105b 100644
--- a/test/unit/test-caveat-validator-registry.js
+++ b/test/unit/test-caveat-validator-registry.js
@@ -136,7 +136,7 @@
       testCaveats.CaveatDoesntValidateExpectedData), function(err) {
     t.ok(err, 'Got an error');
     t.ok((''+err).indexOf(
-      'Non-undefined value returned from function with 0 out args') !== -1,
+      'Expected 0 results, but got 1') !== -1,
       'Error due to non-undefined value returned from 0-arg function');
     t.end();
   });
diff --git a/test/unit/test-invoker-invoke.js b/test/unit/test-invoker-invoke.js
index 7239316..db3f3c1 100644
--- a/test/unit/test-invoker-invoke.js
+++ b/test/unit/test-invoker-invoke.js
@@ -633,9 +633,9 @@
       context: context
     }
   }, function cb(err, results) {
-    t.ok(err instanceof verror.InternalError, 'should error');
+    t.ok(err instanceof verror.VanadiumError, 'should error');
     t.equal(err.message,
-      'app:op: Internal error: Expected out args a,b,c,d,e but was missing e');
+      'app:op: IncorrectResultCount: Expected 5 results, but got 4');
     t.end();
   });
 
@@ -661,10 +661,9 @@
       context: context
     }
   }, function cb(err, results) {
-    t.ok(err instanceof verror.InternalError, 'should error');
+    t.ok(err instanceof verror.VanadiumError, 'should error');
     t.equal(err.message,
-      'app:op: Internal error: ' +
-      'Callback of form cb(err,a,b,c,d,e) was missing e');
+      'app:op: IncorrectResultCount: Expected 5 results, but got 4');
     t.end();
   });
 
@@ -690,10 +689,9 @@
       context: context
     }
   }, function cb(err, results) {
-    t.ok(err instanceof verror.InternalError, 'should error');
+    t.ok(err instanceof verror.VanadiumError, 'should error');
     t.equal(err.message,
-      'app:op: Internal error: ' +
-      'Expected out args a,b,c,d,e but got 1 extra arg(s)');
+      'app:op: IncorrectResultCount: Expected 5 results, but got 6');
     t.end();
   });
 
@@ -719,10 +717,9 @@
       context: context
     }
   }, function cb(err, results) {
-    t.ok(err instanceof verror.InternalError, 'should error');
+    t.ok(err instanceof verror.VanadiumError, 'should error');
     t.equal(err.message,
-      'app:op: Internal error: ' +
-      'Callback of form cb(err,a,b,c,d,e) got 1 extra arg(s)');
+      'app:op: IncorrectResultCount: Expected 5 results, but got 6');
     t.end();
   });