blob: ca759417ab03113bc27f76693daae5fe23d68a21 [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.
/**
* @fileoverview Server allows creation of services that can be invoked
* remotely via RPCs.
*
* Usage:
* var videoService = {
* play: {
* // Play video
* }
* };
*
* var s = new server(proxyConnection);
* s.serve('mymedia/video', videoService);
* @private
*/
var Deferred = require('./../lib/deferred');
var Promise = require('./../lib/promise');
var asyncCall = require('../lib/async-call');
var InspectableFunction = require('../lib/inspectable-function');
var leafDispatcher = require('./leaf-dispatcher');
var vlog = require('./../lib/vlog');
var inspector = require('./../lib/arg-inspector');
var Invoker = require('./../invocation/invoker');
var defaultAuthorizer = require('../security/default-authorizer');
var actions = require('./../verror/actions');
var makeError = require('../verror/make-errors');
var ServerOption = require('./server-option');
var nextServerID = 1; // The ID for the next server.
/**
* @summary
* Server defines the interface for managing a collection of services.
* @description
* <p>Private Constructor, use
* [Runtime#newServer]{@link module:vanadium~Runtime#newServer}.</p>
* @inner
* @constructor
* @memberof module:vanadium.rpc
*/
function Server(router, serverOption) {
if (!(this instanceof Server)) {
return new Server(router);
}
this._router = router;
this._rootCtx = router._rootCtx;
this._handle = 0;
this.id = nextServerID++;
this.dispatcher = null;
this.serviceObjectHandles = {};
this.serverOption = serverOption || new ServerOption();
}
// TODO(aghassemi) the serviceObject example needs to point to a "Guides" page
// on the website when we have it. https://github.com/vanadium/issues/issues/444
/* jshint ignore:start */
/**
* ServeOptions is a set of options that are passed to the
* [serve]{@link module:vanadium.rpc~Server#serve}.
* @typedef module:vanadium.rpc~Server~ServeOptions
* @property {module:vanadium.security.Authorize} authorizer An Authorizer
* that will handle the authorization for the method call. If null, then the
* default strict authorizer will be used.
*/
/**
* <p>Serve associates object with name by publishing the address
* of this server with the mount table under the supplied name and using
* authorizer to authorize access to it.</p>
* <p>If name is an empty string, no attempt will made to publish that
* name to a mount table. It is an error to call
* {@link module:vanadium.rpc~Server#serve|serve}
* if either {@link module:vanadium.rpc~Server#serveDispatcher|serveDispatcher}
* or {@link module:vanadium.rpc~Server.serve|serve} has already been called.
* To serve the same object under multiple names,
* {@link module:vanadium.rpc~Server#addName|addName} can be used.</p>
* <p>To serve names of the form "mymedia/*" make the calls:</p>
* <pre>
* serve("mymedia", serviceObject, {
* authorizer: serviceAuthorizer // optional authorizer
* });
* </pre>
* <p>
* serviceObject is simply a JavaScript object that implements service methods.
* </p>
* <p>
* <pre>
* var serviceObject = new MyService();
* function MyService() {}
* </pre>
* <p>
* Each service method must take [ctx]{@link module:vanadium.context.Context}
* and [serverCall]{@link module:vanadium.rpc~ServerCall} as the
* first two parameters.
* </p>
* <p>
* The output arguments can be given in several forms - through direct return,
* return of a promise or calling a callback that is optionally the
* last parameter.
* </p>
* <pre>
* // Sync method that echoes the input text immediately.
* MyService.prototype.echo = function(ctx, serverCall, text) {
* return 'Echo: ' + text;
* };
* </pre>
* <pre>
* // Async method that echoes the input text after 1 second, using Promises.
* MyService.prototype.delayedEcho = function(ctx, serverCall, text) {
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* resolve('Echo: ' + text);
* }, 1000);
* });
* };
*</pre>
*<pre>
* // Async method that echoes the input text after 1 second, using Callbacks.
* MyService.prototype.delayedEcho = function(ctx, serverCall, text, callback) {
* setTimeout(function() {
* // first argument to the callback is error, second argument is results
* callback(null, 'Echo: ' + text);
* }, 1000);
* };
*</pre>
*
* @public
* @param {string} name Name to serve under.
* @param {object} serviceObject The service object that has a set of
* exported methods.
* @param {module:vanadium.rpc~Server~ServeOptions} options Options config.
* @param {module:vanadium~voidCb} [cb] If provided, the function
* will be called on completion.
* @return {Promise<void>} Promise to be called when serve completes or fails.
*/
/* jshint ignore:end */
Server.prototype.serve = function(name, serviceObject, options, cb) {
if (typeof options === 'function') {
cb = options;
options = undefined;
}
var authorizer;
if (options) {
authorizer = options.authorizer;
}
var dispatcher = leafDispatcher(serviceObject, authorizer);
return this.serveDispatcher(name, dispatcher, cb);
};
/**
* @typedef module:vanadium.rpc~Server~DispatcherResponse
* @type {object}
* @property {object} service The Invoker that will handle
* method call.
* @property {module:vanadium.security.Authorize} authorizer An Authorizer that
* will handle the authorization for the method call. If null, then the default
* authorizer will be used.
*/
/**
* A function that returns the service implementation for the object identified
* by the given suffix.
* @callback module:vanadium.rpc~Server~Dispatcher
* @param {string} suffix The suffix for the call.
* @param {module:vanadium.rpc~Server~Dispatcher-callback} cb
* The callback to call when the dispatch is complete.
* @return {Promise<module:vanadium.rpc~Server~DispatcherResponse>}
* Either the DispatcherResponse object to
* handle the method call or a Promise that will be resolved the service
* callback.
*/
/**
* Callback passed into Dispatcher.
* @callback module:vanadium.rpc~Server~Dispatcher-callback
* @param {Error} err An error if one occurred.
* @param {object} object The object that will handle the method call.
*/
/**
* <p>ServeDispatcher associates dispatcher with the portion of the mount
* table's name space for which name is a prefix, by publishing the
* address of this dispatcher with the mount table under the supplied name.
* RPCs invoked on the supplied name will be delivered to the supplied
* Dispatcher's lookup method which will in turn return the object. </p>
*
* <p>To serve names of the form "mymedia/*" make the calls: </p>
*
* <pre>
* serve("mymedia", dispatcher);
* </pre>
*
* <p>If name is an empty string, no attempt will made to publish that
* name to a mount table. </p>
*
* <p>It is an error to call
* {@link module:vanadium.rpc~Server#serveDispatcher|serveDispatcher}
* if {@link module:vanadium.rpc~Server#serve|serve} has already been called.
* It is also an error
* to call serveDispatcher multiple times.</p>
* To serve the same dispatcher under multiple names,
* {@link module:vanadium.rpc~Server#addName|addName} can be used. </p>
*
* @public
* @param {string} name Name to serve under.
* @param {module:vanadium.rpc~Server~Dispatcher} dispatcher A function that
* will take in the suffix and the method to be called and return the service
* object for that suffix.
* @param {module:vanadium~voidCb} [cb] If provided, the function
* will be called on completion.
* @return {Promise<void>} Promise to be called when serve completes or fails.
*/
Server.prototype.serveDispatcher = function(name, dispatcher, cb) {
this.dispatcher = dispatcher;
return this._router.serve(name, this, cb);
};
/**
* Stop gracefully stops all services on this Server.
* New calls are rejected, but any in-flight calls are allowed to complete.
* All published named are unmounted.
* @param {module:vanadium~voidCb} [cb] If provided, the function
* will be called on completion.
* @return {Promise<void>} Promise to be called when stop service completes or
* fails
*/
Server.prototype.stop = function(cb) {
return this._router.stopServer(this, cb);
};
/**
* Adds the specified name to the mount table for the object or dispatcher
* served by this server. It is an error to specify a name that was not
* previously added using
* [serve]{@link module:vanadium.rpc~Server#serve}/
* [serveDispatcher]{@link module:vanadium.rpc~Server#serveDispatcher}
* or [addName]{@link module:vanadium.rpc~Server#addName}.
* @public
* @param {string} name Name to publish.
* @param {module:vanadium~voidCb} [cb] If provided, the function
* will be called on completion.
* @return {Promise<void>} Promise to be called when operation completes or
* fails
*/
Server.prototype.addName = function(name, cb) {
return this._router.addName(name, this, cb);
};
/**
* Removes the specified name from the mount table. It is an
* error to specify a name that was not previously added using
* [serve]{@link module:vanadium.rpc~Server#serve}/
* [serveDispatcher]{@link module:vanadium.rpc~Server#serveDispatcher}
* or [addName]{@link module:vanadium.rpc~Server#addName}.
* @public
* @param {string} name Name to remove.
* @param {function} [cb] If provided, the function will be called on
* completion. The only argument is an error if there was one.
* @return {Promise<void>} Promise to be called when operation completes or
* fails.
*/
Server.prototype.removeName = function(name, cb) {
return this._router.removeName(name, this, cb);
};
/**
* @private
* @param {Number} handle The handle for the service.
* @return {Object} The invoker corresponding to the provided error.
*/
Server.prototype.getInvokerForHandle = function(handle) {
var result = this.serviceObjectHandles[handle];
delete this.serviceObjectHandles[handle];
return result.invoker;
};
/**
* Handles the authorization for an RPC.
* @private
* @param {Number} handle The handle for the authorizer.
* @param {module:vanadium.context.Context} ctx The ctx of the
* call.
* @param {module:vanadium.security~SecurityCall} call The security call.
* @return {Promise} A promise that will be fulfilled with the result.
*/
Server.prototype.handleAuthorization = function(handle, ctx, call) {
var handler = this.serviceObjectHandles[handle];
var authorizer = defaultAuthorizer;
if (handler && handler.authorizer) {
authorizer = handler.authorizer;
}
var def = new Deferred();
var inspectableAuthorizer = new InspectableFunction(authorizer);
asyncCall(ctx, null, inspectableAuthorizer, [], [ctx, call],
function(err) {
if (err) {
def.reject(err);
return;
}
def.resolve();
});
return def.promise;
};
var InvokeOnNonInvoker = makeError(
'v.io/core/javascript.InvokeOnNonInvoker', actions.NO_RETRY,
'{1:}{2:} trying to invoke on a non-invoker{:_}');
/**
* Handles the result of lookup and returns an error if there was any.
* @private
*/
Server.prototype._handleLookupResult = function(object) {
if (!object.hasOwnProperty('service')) {
// TODO(bjornick): Use the correct context here.
throw new InvokeOnNonInvoker(this._rootCtx);
}
object._handle = this._handle;
try {
object.invoker = new Invoker(object.service);
} catch(e) {
vlog.logger.error('lookup failed', e);
return e;
}
this.serviceObjectHandles[object._handle] = object;
this._handle++;
return null;
};
/*
* Perform the lookup call to the user code on the suffix and method passed in.
*/
Server.prototype._handleLookup = function(suffix) {
var self = this;
var def = new Deferred();
var argsNames = inspector(this.dispatcher).names;
var useCallback = argsNames.length >= 2;
var cb = function(err, val) {
if (err) {
def.reject(err);
} else {
def.resolve(val);
}
};
var result;
try {
result = this.dispatcher(suffix, cb);
} catch (e) {
def.reject(e);
vlog.logger.error(e);
return def.promise;
}
if (!useCallback) {
if (result === undefined) {
return def.promise.then(handleResult);
}
if (result instanceof Error) {
def.reject(result);
return def.promise;
}
def.resolve(result);
}
function handleResult(v) {
var err = self._handleLookupResult(v);
if (err) {
return Promise.reject(err);
}
return Promise.resolve(v);
}
return def.promise.then(handleResult);
};
/**
* Export the module
*/
module.exports = Server;