rpc-builder
Version:
Transport and protocol agnostic RPC library for browser and Node.js
758 lines (591 loc) • 17.6 kB
JavaScript
/*
* (C) Copyright 2014 Kurento (http://kurento.org/)
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl-2.1.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
var EventEmitter = require('events').EventEmitter;
var inherits = require('inherits');
var packers = require('./packers');
var Mapper = require('./Mapper');
const BASE_TIMEOUT = 5000;
function unifyResponseMethods(responseMethods)
{
if(!responseMethods) return {};
for(var key in responseMethods)
{
var value = responseMethods[key];
if(typeof value == 'string')
responseMethods[key] =
{
response: value
}
};
return responseMethods;
};
function unifyTransport(transport)
{
if(!transport) return;
if(transport instanceof Function)
return {send: transport};
// Stream API
if(transport.pipe)
{
transport.send = transport.write;
var oldValue;
Object.defineProperty(transport, 'onmessage',
{
get: function()
{
return null;
},
set: function(value)
{
if(oldValue != undefined)
transport.removeListener('data', oldValue);
oldValue = value;
if(value)
transport.on('data', value);
}
})
return transport;
}
if(transport.send instanceof Function)
return transport;
if(transport.postMessage instanceof Function)
{
transport.send = transport.postMessage;
return transport;
}
// Transports that only can receive messages, but not send
if(transport.onmessage !== undefined) return;
throw new SyntaxError("Transport is not a function nor a valid object");
};
/**
* Representation of a RPC notification
*
* @class
*
* @constructor
*
* @param {String} method -method of the notification
* @param params - parameters of the notification
*/
function RpcNotification(method, params)
{
Object.defineProperty(this, 'method', {value: method, enumerable: true});
Object.defineProperty(this, 'params', {value: params, enumerable: true});
};
/**
* @class
*
* @constructor
*
* @param {object} packer
*
* @param {object} [options]
*
* @param {object} [transport]
*
* @param {Function} [onRequest]
*/
function RpcBuilder(packer, options, transport, onRequest)
{
var self = this;
if(!packer)
throw new SyntaxError('Packer is not defined');
if(!packer.pack || !packer.unpack)
throw new SyntaxError('Packer is invalid');
var responseMethods = unifyResponseMethods(packer.responseMethods);
if(options instanceof Function)
{
if(transport != undefined)
throw new SyntaxError("There can't be parameters after onRequest");
onRequest = options;
transport = undefined;
options = undefined;
};
if(options && options.send instanceof Function)
{
if(transport && !(transport instanceof Function))
throw new SyntaxError("Only a function can be after transport");
onRequest = transport;
transport = options;
options = undefined;
};
if(transport instanceof Function)
{
if(onRequest != undefined)
throw new SyntaxError("There can't be parameters after onRequest");
onRequest = transport;
transport = undefined;
};
if(transport && transport.send instanceof Function)
if(onRequest && !(onRequest instanceof Function))
throw new SyntaxError("Only a function can be after transport");
options = options || {};
EventEmitter.call(this);
if(onRequest)
this.on('request', onRequest);
Object.defineProperty(this, 'peerID', {value: options.peerID});
var max_retries = options.max_retries || 0;
function transportMessage(event)
{
self.decode(event.data);
};
Object.defineProperty(this, 'transport',
{
get: function()
{
return transport;
},
set: function(value)
{
// Remove listener from old transport
if(transport && transport.onmessage !== undefined)
if(transport.removeEventListener)
transport.removeEventListener('message', transportMessage);
else
transport.onmessage = null;
// Set listener on new transport
if(value && value.onmessage !== undefined)
if(value.addEventListener)
value.addEventListener('message', transportMessage);
else
value.onmessage = transportMessage;
transport = unifyTransport(value);
}
})
this.transport = transport;
const request_timeout = options.request_timeout || BASE_TIMEOUT;
const response_timeout = options.response_timeout || BASE_TIMEOUT;
const duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT;
var requestID = 0;
var requests = new Mapper();
var responses = new Mapper();
var processedResponses = new Mapper();
var message2Key = {};
/**
* Store the response to prevent to process duplicate request later
*/
function storeResponse(message, id, dest)
{
var response =
{
message: message,
/** Timeout to auto-clean old responses */
timeout: setTimeout(function()
{
responses.remove(id, dest);
},
response_timeout)
};
responses.set(response, id, dest);
};
/**
* Store the response to ignore duplicated messages later
*/
function storeProcessedResponse(ack, from)
{
var timeout = setTimeout(function()
{
processedResponses.remove(ack, from);
},
duplicates_timeout);
processedResponses.set(timeout, ack, from);
};
/**
* Representation of a RPC request
*
* @class
* @extends RpcNotification
*
* @constructor
*
* @param {String} method -method of the notification
* @param params - parameters of the notification
* @param {Integer} id - identifier of the request
* @param [from] - source of the notification
*/
function RpcRequest(method, params, id, from, transport)
{
RpcNotification.call(this, method, params);
Object.defineProperty(this, 'transport',
{
get: function()
{
return transport;
},
set: function(value)
{
transport = unifyTransport(value);
}
})
var response = responses.get(id, from);
/**
* @constant {Boolean} duplicated
*/
if(!(transport || self.transport))
Object.defineProperty(this, 'duplicated',
{
value: Boolean(response)
});
var responseMethod = responseMethods[method];
this.pack = packer.pack.bind(packer, this, id)
/**
* Generate a response to this request
*
* @param {Error} [error]
* @param {*} [result]
*
* @returns {string}
*/
this.reply = function(error, result, transport)
{
// Fix optional parameters
if(error instanceof Function || error && error.send instanceof Function)
{
if(result != undefined)
throw new SyntaxError("There can't be parameters after callback");
transport = error;
result = null;
error = undefined;
}
else if(result instanceof Function
|| result && result.send instanceof Function)
{
if(transport != undefined)
throw new SyntaxError("There can't be parameters after callback");
transport = result;
result = null;
};
transport = unifyTransport(transport);
// Duplicated request, remove old response timeout
if(response)
clearTimeout(response.timeout);
if(from != undefined)
{
if(error)
error.dest = from;
if(result)
result.dest = from;
};
var message;
// New request or overriden one, create new response with provided data
if(error || result != undefined)
{
if(self.peerID != undefined)
{
if(error)
error.from = self.peerID;
else
result.from = self.peerID;
}
// Protocol indicates that responses has own request methods
if(responseMethod)
{
if(responseMethod.error == undefined && error)
message =
{
error: error
};
else
{
var method = error
? responseMethod.error
: responseMethod.response;
message =
{
method: method,
params: error || result
};
}
}
else
message =
{
error: error,
result: result
};
message = packer.pack(message, id);
}
// Duplicate & not-overriden request, re-send old response
else if(response)
message = response.message;
// New empty reply, response null value
else
message = packer.pack({result: null}, id);
// Store the response to prevent to process a duplicated request later
storeResponse(message, id, from);
// Return the stored response so it can be directly send back
transport = transport || this.transport || self.transport;
if(transport)
return transport.send(message);
return message;
}
};
inherits(RpcRequest, RpcNotification);
function cancel(message)
{
var key = message2Key[message];
if(!key) return;
delete message2Key[message];
var request = requests.pop(key.id, key.dest);
if(!request) return;
clearTimeout(request.timeout);
// Start duplicated responses timeout
storeProcessedResponse(key.id, key.dest);
};
/**
* Allow to cancel a request and don't wait for a response
*
* If `message` is not given, cancel all the request
*/
this.cancel = function(message)
{
if(message) return cancel(message);
for(var message in message2Key)
cancel(message);
};
this.close = function()
{
// Prevent to receive new messages
var transport = self.transport;
if(transport && transport.close)
transport.close();
// Request & processed responses
this.cancel();
processedResponses.forEach(clearTimeout);
// Responses
responses.forEach(function(response)
{
clearTimeout(response.timeout);
});
};
/**
* Generates and encode a JsonRPC 2.0 message
*
* @param {String} method -method of the notification
* @param params - parameters of the notification
* @param [dest] - destination of the notification
* @param {object} [transport] - transport where to send the message
* @param [callback] - function called when a response to this request is
* received. If not defined, a notification will be send instead
*
* @returns {string} A raw JsonRPC 2.0 request or notification string
*/
this.encode = function(method, params, dest, transport, callback)
{
// Fix optional parameters
if(params instanceof Function)
{
if(dest != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = params;
transport = undefined;
dest = undefined;
params = undefined;
}
else if(dest instanceof Function)
{
if(transport != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = dest;
transport = undefined;
dest = undefined;
}
else if(transport instanceof Function)
{
if(callback != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = transport;
transport = undefined;
};
if(self.peerID != undefined)
{
params = params || {};
params.from = self.peerID;
};
if(dest != undefined)
{
params = params || {};
params.dest = dest;
};
// Encode message
var message =
{
method: method,
params: params
};
if(callback)
{
var id = requestID++;
var retried = 0;
message = packer.pack(message, id);
function dispatchCallback(error, result)
{
self.cancel(message);
callback(error, result);
};
var request =
{
message: message,
callback: dispatchCallback,
responseMethods: responseMethods[method] || {}
};
var encode_transport = unifyTransport(transport);
function sendRequest(transport)
{
request.timeout = setTimeout(timeout,
request_timeout*Math.pow(2, retried++));
message2Key[message] = {id: id, dest: dest};
requests.set(request, id, dest);
transport = transport || encode_transport || self.transport;
if(transport)
return transport.send(message);
return message;
};
function retry(transport)
{
transport = unifyTransport(transport);
console.warn(retried+' retry for request message:',message);
var timeout = processedResponses.pop(id, dest);
clearTimeout(timeout);
return sendRequest(transport);
};
function timeout()
{
if(retried < max_retries)
return retry(transport);
var error = new Error('Request has timed out');
error.request = message;
error.retry = retry;
dispatchCallback(error)
};
return sendRequest(transport);
};
// Return the packed message
message = packer.pack(message);
transport = transport || self.transport;
if(transport)
return transport.send(message);
return message;
};
/**
* Decode and process a JsonRPC 2.0 message
*
* @param {string} message - string with the content of the message
*
* @returns {RpcNotification|RpcRequest|undefined} - the representation of the
* notification or the request. If a response was processed, it will return
* `undefined` to notify that it was processed
*
* @throws {TypeError} - Message is not defined
*/
this.decode = function(message, transport)
{
if(!message)
throw new TypeError("Message is not defined");
try
{
message = packer.unpack(message);
}
catch(e)
{
// Ignore invalid messages
return console.debug(e, message);
};
var id = message.id;
var ack = message.ack;
var method = message.method;
var params = message.params || {};
var from = params.from;
var dest = params.dest;
// Ignore messages send by us
if(self.peerID != undefined && from == self.peerID) return;
// Notification
if(id == undefined && ack == undefined)
{
var notification = new RpcNotification(method, params);
if(self.emit('request', notification)) return;
return notification;
};
function processRequest()
{
// If we have a transport and it's a duplicated request, reply inmediatly
transport = unifyTransport(transport) || self.transport;
if(transport)
{
var response = responses.get(id, from);
if(response)
return transport.send(response.message);
};
var idAck = (id != undefined) ? id : ack;
var request = new RpcRequest(method, params, idAck, from, transport);
if(self.emit('request', request)) return;
return request;
};
function processResponse(request, error, result)
{
request.callback(error, result);
};
function duplicatedResponse(timeout)
{
console.warn("Response already processed", message);
// Update duplicated responses timeout
clearTimeout(timeout);
storeProcessedResponse(ack, from);
};
// Request, or response with own method
if(method)
{
// Check if it's a response with own method
if(dest == undefined || dest == self.peerID)
{
var request = requests.get(ack, from);
if(request)
{
var responseMethods = request.responseMethods;
if(method == responseMethods.error)
return processResponse(request, params);
if(method == responseMethods.response)
return processResponse(request, null, params);
return processRequest();
}
var processed = processedResponses.get(ack, from);
if(processed)
return duplicatedResponse(processed);
}
// Request
return processRequest();
};
var error = message.error;
var result = message.result;
// Ignore responses not send to us
if(error && error.dest && error.dest != self.sessionID) return;
if(result && result.dest && result.dest != self.sessionID) return;
// Response
var request = requests.get(ack, from);
if(!request)
{
var processed = processedResponses.get(ack, from);
if(processed)
return duplicatedResponse(processed);
return console.warn("No callback was defined for this message", message);
};
// Process response
processResponse(request, error, result);
};
};
inherits(RpcBuilder, EventEmitter);
RpcBuilder.RpcNotification = RpcNotification;
module.exports = RpcBuilder;
RpcBuilder.packers = packers;