blob: ee9b92c2345a5aff7b685786202f2fc57978b46e [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file defines a async calling convention intended used to call
// user-defined functions.
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.
*
* The called function must either return a promise or call a callback and
* return undefined.
*
* @private
* @param {Context} ctx Context
* @param {*} self The object to be "this" during invocation.
* @param {InspectableFunction} fn The function
* @param {Array} outArgs The names of the expected output arguments
* @param {*} args The argument values
* @param {Function} inputCb callback when finished
* @return {type} Promise or undefined
*/
function asyncCall(ctx, self, fn, outArgs, args, inputCb) {
var cbCalled;
var numOutArgs = outArgs.length;
// Helper to call the callback once
function callOnceCb(err, results) {
if (cbCalled) {
logger.error('Callback called multiple times');
return;
}
inputCb.apply(self, arguments);
cbCalled = true;
}
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 */) {
handleResult(err, Array.prototype.slice.call(arguments, 1));
}
if (fn.hasCallback()) {
args.push(injectedCb);
}
var result;
try {
result = fn.apply(self, args);
} catch (err) {
logger.error('Caught error: ', err);
callOnceCb(wrapError(err));
return;
}
// Callback case (wait for callback to be called directly):
if (fn.hasCallback()) {
return;
}
// Promise / direct return case:
Promise.resolve(result).then(function(res) {
// We expect:
// 0 args - return; // NOT return [];
// 1 args - return a; // NOT return [a];
// 2 args - return [a, b] ;
//
// Convert the results to always be in array style:
// [], [a], [a, b], etc
// 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 Promise.reject(
new IncorrectResultCountError(ctx, 0, 1,
'expected undefined result ' +
'for void function'));
}
return [];
case 1:
// Note: If res is undefined, the result is [undefined].
return [res];
default:
if (!Array.isArray(res)) {
return Promise.reject(
new IncorrectResultCountError(ctx, numOutArgs, 1));
}
return res;
}
}).then(function(res) {
handleResult(null, res);
}).catch(function(err) {
handleResult(wrapError(err));
});
}
/**
* Wrap an error so that it is always of type Error.
* 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 {*} err The error or other value.
* @return {Error} An error or type Error.
*/
function wrapError(err) {
if (err instanceof Error) {
return err;
}
return new Error(err);
}