eureca.io
Version:
Nodejs transparent bidirectional remote procedure call (RPC) supporting multiple transports : WebRTC, Websocket, engine.io, faye ...etc
703 lines (530 loc) • 23.5 kB
text/typescript
/// <reference path="transport/Primus.transport.ts" />
/// <reference path="transport/WebRTC.transport.ts" />
/// <reference path="Stub.ts" />
/// <reference path="EObject.class.ts" />
/// <reference path="Util.class.ts" />
/// <reference path="Contract.class.ts" />
/** @ignore */
declare var require: any;
/** @ignore */
declare var exports: any;
/** @ignore */
declare var eio: any;
/** @ignore */
declare var _eureca_host: any;
/** @ignore */
declare var _eureca_uri: any;
var is_nodejs = Eureca.Util.isNodejs;
if (is_nodejs) {
var _eureca_prefix = 'eureca.io';
}
//var EurecaSocket = function (uri, options) {
// if (is_nodejs) {
// var sock = require('engine.io-client')(uri, options);
// return sock;
// } else {
// return new eio.Socket(uri, options);
// }
//};
module Eureca {
/**
* Eureca client class
* This constructor takes an optional settings object
* @constructor Client
* @param {object} [settings] - have the following properties <br />
* @property {URI} settings.uri - Eureca server WS uri, browser client can automatically guess the server URI if you are using a single Eureca server but Nodejs client need this parameter.
* @property {string} [settings.prefix=eureca.io] - This determines the websocket path, it's unvisible to the user but if for some reason you want to rename this path use this parameter.
* @property {int} [settings.retry=20] - Determines max retries to reconnect to the server if the connection is lost.
* @property {boolean} [settings.autoConnect=true] - Estabilish connection automatically after instantiation.<br />if set to False you'll need to call client.connect() explicitly.
*
*
* @example
* //<h4>Example of a nodejs client</h4>
* var Eureca = require('eureca.io');
* var client = new Eureca.Client({ uri: 'ws://localhost:8000/', prefix: 'eureca.io', retry: 3 });
* client.ready(function (serverProxy) {
* // ...
* });
*
* @example
* //<h4>Equivalent browser client</h4>
* <!doctype html>
* <html>
* <head>
* <script src="/eureca.js"></script>
* </head>
* <body>
* <script>
* var client = new Eureca.Client({prefix: 'eureca.io', retry: 3 });
* //uri is optional in browser client
* client.ready(function (serverProxy) {
* // ...
* });
* </script>
* </body>
* </html>
*
* @see authenticate
* @see connect
* @see disconnect
* @see send
* @see isReady
*
*
*/
export class Client extends EObject {
private _ready: boolean;
private _useWebRTC: boolean;
public maxRetries: number;
public tries: number=0;
public prefix: string;
public uri: string;
private serialize = (v) => v;
private deserialize = (v) => v;
/**
* When the connection is estabilished, the server proxy object allow calling exported server functions.
* @var {object} Client#serverProxy
*
*/
public serverProxy: any = {};
public socket: ISocket;
public contract: string[];
public stub: Stub;
private transport: any;
/**
* All declared functions under this namespace become available to the server <b>if they are allowed in the server side</b>.
* @namespace Client exports
* @memberOf Client
*
* @example
* var client = new Eureca.Client({..});
* client.exports.alert = function(message) {
* alert(message);
* }
*/
public exports: any;
constructor(public settings: any = {}) {
super();
//needed by primus
settings.transformer = settings.transport || 'engine.io';
this.transport = Transport.get(settings.transformer);
if (typeof settings.serialize == 'function' || typeof this.transport.serialize == 'function')
this.serialize = settings.serialize || this.transport.serialize;
settings.serialize = this.serialize;
if (typeof settings.deserialize == 'function'|| typeof this.transport.deserialize == 'function')
this.deserialize = settings.deserialize || this.transport.deserialize
settings.deserialize = this.deserialize;
this.stub = new Stub(settings);
this.exports = {};
this.settings.autoConnect = !(this.settings.autoConnect === false);
//if (this.settings.autoConnect !== false)
this.maxRetries = settings.retry || 20;
//var tries = 0;
//this.registerEvents(['ready', 'update', 'onConnect', 'onDisconnect', 'onError', 'onMessage', 'onConnectionLost', 'onConnectionRetry', 'authResponse']);
if (this.settings.autoConnect) this.connect();
}
/**
* close client connection
*
*
* @function Client#disconnect
*
*/
public disconnect()
{
this.tries = this.maxRetries+1;
this.socket.close();
}
/**
* indicate if the client is ready or not, it's better to use client.ready() event, but if for some reason
* you need to check if the client is ready without using the event system you can use this.<br />
*
* @function Client#isReady
* @return {boolean} - true if the client is ready
*
* @example
* var client = new Eureca.Client({..});
* //...
* if (client.isReady()) {
* client.serverProxy.foo();
* }
*/
public isReady() {
return this._ready;
}
/**
* Send user data to the server
*
* @function Client#send
* @param {any} rawData - data to send (must be serializable type)
*/
public send(rawData:any) {
return this.socket.send(this.serialize(rawData));
}
/**
* Send authentication request to the server. <br />
* this can take an arbitrary number of arguments depending on what you defined in the server side <br />
* when the server receive an auth request it'll handle it and return null on auth success, or an error message if something goes wrong <br />
* you need to listed to auth result throught authResponse event
* ** Important ** : it's up to you to define the authenticationmethod in the server side
* @function Client#authenticate
*
* @example
* var client = new Eureca.Client({..});
* //listen to auth response
* client.authResponse(function(result) {
* if (result == null) {
* // ... Auth OK
* }
* else {
* // ... Auth failed
* }
* });
*
* client.ready(function(){
*
* //send auth request
* client.authenticate('your_auth_token');
* });
*/
public authenticate(...args: any[]) {
//if (!this._ready)
//{
// return;
//}
var authRequest = {};
authRequest[Eureca.Protocol.authReq] = args;
this.socket.send(this.serialize(authRequest));
}
/*
* If the authentication is used, this will tell you if you are already authenticated or not.
* @return {boolean} true mean that the client is authenticated
*/
public isAuthenticated():boolean {
return this.socket.isAuthenticated();
}
private setupWebRTC() {
//this.stub.importRemoteFunction(_this.webRTCProxy, _this.socket, jobj[Eureca.Protocol.contractId]);
}
/**
* connect client
*
*
* @function Client#connect
*
*/
public connect() {
//var _this = this;
var prefix = '';
prefix += this.settings.prefix || _eureca_prefix;
var _eureca_uri = _eureca_uri || undefined;
var uri = this.settings.uri || (prefix ? _eureca_host + '/'+ prefix : (_eureca_uri || undefined));
console.log(uri, prefix);
this._ready = false;
var _transformer = this.settings.transformer;
var _parser = this.settings.parser;
//_this.socket = EurecaSocket(uri, { path: prefix });
var client = this.transport.createClient(uri, {
prefix: prefix,
transformer: _transformer,
parser: _parser,
retries: this.maxRetries,
minDelay: 100,
//WebRTC stuff
reliable: this.settings.reliable,
maxRetransmits: this.settings.maxRetransmits,
ordered: this.settings.ordered
});
this.socket = client;
client._proxy = this.serverProxy;
this._handleClient(client, this.serverProxy);
}
private _handleClient(client, proxy) {
const __this = this;
client.on('open', function () {
__this.trigger('connect', client);
__this.tries = 0;
});
client.on('message', function (data) {
__this.trigger('message', data);
var jobj: any = __this.deserialize.call(client, data);
//if (typeof data != 'object') {
// try {
// jobj = JSON.parse(data);
// }
// catch (ex) {
// jobj = {};
// }
//}
//else {
// jobj = data;
//}
if (typeof jobj != 'object') {
__this.trigger('unhandledMessage', data);
return;
}
if (jobj[Eureca.Protocol.contractId]) //should be first message
{
var update = __this.contract && __this.contract.length > 0;
__this.contract = jobj[Eureca.Protocol.contractId];
/** Experimental : dynamic client contract*/
//if (jobj[Protocol.signatureId]) {
// var contract = [];
// contract = Contract.ensureContract(_this.exports);
// var contractResp = {};
// contractResp[Protocol.contractId] = contract;
// contractResp[Protocol.signatureId] = jobj[Protocol.signatureId];
// _this.send(contractResp);
// _this.contract = contract;
//}
/*****************************************************/
__this.stub.importRemoteFunction(proxy, client, jobj[Eureca.Protocol.contractId]/*, _this.serialize*/);
//var next = function () {
__this._ready = true;
if (update) {
/**
* ** Experimental ** Triggered when the server explicitly notify the client about remote functions change.<br />
* you'll need this for example, if the server define some functions dynamically and need to make them available to clients.
*
*/
__this.trigger('update', proxy, __this.contract);
}
else {
__this.trigger('ready', proxy, __this.contract);
}
//}
//if (_this.settings.authenticate) _this.settings.authenticate(_this, next);
//else next();
return;
}
//Handle auth response
if (jobj[Eureca.Protocol.authResp] !== undefined) {
client.eureca.authenticated = true;
var callArgs = ['authResponse'].concat(jobj[Eureca.Protocol.authResp]);
__this.trigger.apply(__this, callArgs);
return;
}
// /!\ ordre is important we have to check invoke BEFORE callback
if (jobj[Eureca.Protocol.functionId] !== undefined) //server invoking client
{
if (client.context == undefined) {
var returnFunc = function (result, error=null) {
var retObj = {};
retObj[Eureca.Protocol.signatureId] = this.retId;
retObj[Eureca.Protocol.resultId] = result;
retObj[Eureca.Protocol.errorId] = error;
this.connection.send(this.serialize(retObj));
}
client.context = {
user: { clientId: client.id },
connection: client,
socket: client,
serverProxy: client.serverProxy,
async: false,
retId: jobj[Eureca.Protocol.signatureId],
serialize:__this.serialize,
'return': returnFunc };
}
client.context.retId = jobj[Eureca.Protocol.signatureId];
//Experimental custom context sharing
//remote context is shared throught clientProxy or proxy function in the server side
//Example
// a server exposing hello() function
// a client calling server hello() function
//
// in the client side you can issue :
// eurecaServer.hello.context = {somefield:'someData'}
// //you can also use eurecaServer.context = {somefield:'someData'} in this case it'll be global to all exposed functions !
// eurecaServer.hello();
//
// in the server side, you get the remote shared context throught
// exports.hello = function() {
// console.log(this.remoteContext); // <== you get the remote context here
// console.log('hello');
// }
//if (jobj[Eureca.Protocol.context]) {
// client.remoteContext = jobj[Eureca.Protocol.context];
//}
__this.stub.invoke(client.context, __this, jobj, client);
return;
}
if (jobj[Eureca.Protocol.signatureId] !== undefined) //invoke result
{
//_this.stub.doCallBack(jobj[Eureca.Protocol.signatureId], jobj[Eureca.Protocol.resultId], jobj[Eureca.Protocol.errorId]);
Stub.doCallBack(jobj[Eureca.Protocol.signatureId], jobj[Eureca.Protocol.resultId], jobj[Eureca.Protocol.errorId]);
return;
}
__this.trigger('unhandledMessage', data);
});
client.on('reconnecting', function (opts) {
__this.trigger('connectionRetry', opts);
});
client.on('close', function (e) {
__this.trigger('disconnect', client, e);
__this.trigger('connectionLost');
});
client.on('error', function (e) {
__this.trigger('error', e);
});
client.on('stateChange', function (s) {
__this.trigger('stateChange', s);
});
}
//#region ==[ Events bindings ]===================
/**
* Bind a callback to 'ready' event @see {@link Client#event:ready|Client ready event}
* >**Note :** you can also use Client.on('ready', callback) to bind ready event
*
* @function Client#ready
*
*/
public ready(callback: (any) => void) {
/**
* Triggered when the connection is estabilished and server remote functions available to the client.
*
* @event Client#ready
* @property {Proxy} serverProxy - server proxy object.
* @example
* client.ready(function (serverProxy) {
* serverProxy.hello();
* });
*
*/
this.on('ready', callback);
}
/**
* Bind a callback to 'update' event @see {@link Client#event:update|Client update event}
* >**Note :** you can also use Client.on('update', callback) to bind update event
*
* @function Client#update
*
*/
public update(callback: (any) => void) {
this.on('update', callback);
}
/**
* Bind a callback to 'connect' event
* >**Note :** you can also use Client.on('connect', callback) to bind connect event
*
* @function Client#onConnect
*
*/
public onConnect(callback: (any) => void) {
this.on('connect', callback);
}
/**
* Bind a callback to 'disconnect' event @see {@link Client#event:disconnect|Client disconnect event}
* >**Note :** you can also use Client.on('disconnect', callback) to bind disconnect event
*
* @function Client#donDisconnect
*
*/
public onDisconnect(callback: (any) => void) {
/**
* triggered when the connection is lost after all retries to reestabilish it.
*
* @event Client#disconnect
*/
this.on('disconnect', callback);
}
/**
* Bind a callback to 'message' event @see {@link Client#event:message|Client message event}
* >**Note :** you can also use Client.on('message', callback) to bind message event
*
* @function Client#onMessage
*
*/
public onMessage(callback: (any) => void) {
/**
* Triggered when the client receive a message from the server.
* This event can be used to intercept exchanged messages betweens client and server, if you need to access low level network messages
*
* @event Client#message
*
*
*/
this.on('message', callback);
}
/**
* Bind a callback to 'unhandledMessage' event @see {@link Client#event:unhandledMessage|Client unhandledMessage event}
* >**Note :** you can also use Client.on('message', callback) to bind unhandledMessage event
*
* @function Client#onUnhandledMessage
*
*/
public onUnhandledMessage(callback: (any) => void) {
/**
* Triggered when the client receive a message from the server and is not able to handle it.
* this mean that the message is not an internal eureca.io message.<br />
* if for some reason you need to exchange send/receive raw or custom data, listen to this event which in countrary to {@link Client#event:message|message event} will only trigger for non-eureca messages.
*
* @event Client#unhandledMessage
*
*
*/
this.on('unhandledMessage', callback);
}
/**
* Bind a callback to 'error' event @see {@link Client#event:error|Client error event}
* >**Note :** you can also use Client.on('error', callback) to bind error event
*
* @function Client#onError
*
*/
public onError(callback: (any) => void) {
/**
* triggered if an error occure.
*
* @event Client#error
* @property {String} error - the error message
*/
this.on('error', callback);
}
/**
* Bind a callback to 'connectionLost' event
* >**Note :** you can also use Client.on('connectionLost', callback) to bind connectionLost event
*
* @function Client#onConnectionLost
*
*/
public onConnectionLost(callback: (any) => void) {
this.on('connectionLost', callback);
}
/**
* Bind a callback to 'connectionRetry' event
* >**Note :** you can also use Client.on('connectionRetry', callback) to bind connectionRetry event
*
* @function Client#onConnectionRetry
*
*/
public onConnectionRetry(callback: (any) => void) {
/**
* triggered when the connection is lost and the client try to reconnect.
*
* @event Client#connectionRetry
*/
this.on('connectionRetry', callback);
}
/**
* Bind a callback to 'authResponse' event @see {@link Client#event:authResponse|Client authResponse event}
* >**Note :** you can also use Client.on('authResponse', callback) to bind authResponse event
*
* @function Client#onAuthResponse
*
*/
public onAuthResponse(callback: (any) => void) {
/**
* Triggered when the client receive authentication response from the server.
* The server should return a null response on authentication success.
*
* @event Client#authResponse
*
*
*/
this.on('authResponse', callback);
}
//#endregion
}
}
if (is_nodejs) exports.Eureca = Eureca;
else var EURECA = Eureca.Client;