UNPKG

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
/// <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> * &lt;!doctype html&gt; * &lt;html&gt; * &lt;head&gt; * &lt;script src=&quot;/eureca.js&quot;&gt;&lt;/script&gt; * &lt;/head&gt; * &lt;body&gt; * &lt;script&gt; * var client = new Eureca.Client({prefix: 'eureca.io', retry: 3 }); * //uri is optional in browser client * client.ready(function (serverProxy) { * // ... * }); * &lt;/script&gt; * &lt;/body&gt; * &lt;/html&gt; * * @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;