xdc-web
Version:
XinFin JavaScript API wrapper repository
402 lines (318 loc) • 11.1 kB
JavaScript
/*
This file is part of web3.js.
web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
web3.js 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.
You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file WebsocketProvider.js
* @authors:
* Fabian Vogelsteller <fabian@ethereum.org>
* @date 2017
*/
;
var _ = require('underscore');
var errors = require('web3-core-helpers').errors;
var Ws = null;
var _btoa = null;
var parseURL = null;
if (typeof window !== 'undefined' && typeof window.WebSocket !== 'undefined') {
Ws = function(url, protocols) {
return new window.WebSocket(url, protocols);
};
_btoa = btoa;
parseURL = function(url) {
return new URL(url);
};
} else {
Ws = require('websocket').w3cwebsocket;
_btoa = function(str) {
return Buffer(str).toString('base64');
};
var url = require('url');
if (url.URL) {
// Use the new Node 6+ API for parsing URLs that supports username/password
var newURL = url.URL;
parseURL = function(url) {
return new newURL(url);
};
}
else {
// Web3 supports Node.js 5, so fall back to the legacy URL API if necessary
parseURL = require('url').parse;
}
}
// Default connection ws://localhost:8546
var WebsocketProvider = function WebsocketProvider(url, options) {
var _this = this;
this.responseCallbacks = {};
this.notificationCallbacks = [];
options = options || {};
this._customTimeout = options.timeout;
// The w3cwebsocket implementation does not support Basic Auth
// username/password in the URL. So generate the basic auth header, and
// pass through with any additional headers supplied in constructor
var parsedURL = parseURL(url);
var headers = options.headers || {};
var protocol = options.protocol || undefined;
if (parsedURL.username && parsedURL.password) {
headers.authorization = 'Basic ' + _btoa(parsedURL.username + ':' + parsedURL.password);
}
// Allow a custom client configuration
var clientConfig = options.clientConfig || undefined;
// When all node core implementations that do not have the
// WHATWG compatible URL parser go out of service this line can be removed.
if (parsedURL.auth) {
headers.authorization = 'Basic ' + _btoa(parsedURL.auth);
}
this.connection = new Ws(url, protocol, undefined, headers, undefined, clientConfig);
this.addDefaultEvents();
// LISTEN FOR CONNECTION RESPONSES
this.connection.onmessage = function(e) {
/*jshint maxcomplexity: 6 */
var data = (typeof e.data === 'string') ? e.data : '';
_this._parseResponse(data).forEach(function(result){
var id = null;
// get the id which matches the returned id
if(_.isArray(result)) {
result.forEach(function(load){
if(_this.responseCallbacks[load.id])
id = load.id;
});
} else {
id = result.id;
}
// notification
if(!id && result && result.method && result.method.indexOf('_subscription') !== -1) {
_this.notificationCallbacks.forEach(function(callback){
if(_.isFunction(callback))
callback(result);
});
// fire the callback
} else if(_this.responseCallbacks[id]) {
_this.responseCallbacks[id](null, result);
delete _this.responseCallbacks[id];
}
});
};
// make property `connected` which will return the current connection status
Object.defineProperty(this, 'connected', {
get: function () {
return this.connection && this.connection.readyState === this.connection.OPEN;
},
enumerable: true,
});
};
/**
Will add the error and end event to timeout existing calls
@method addDefaultEvents
*/
WebsocketProvider.prototype.addDefaultEvents = function(){
var _this = this;
this.connection.onerror = function(){
_this._timeout();
};
this.connection.onclose = function(){
_this._timeout();
// reset all requests and callbacks
_this.reset();
};
// this.connection.on('timeout', function(){
// _this._timeout();
// });
};
/**
Will parse the response and make an array out of it.
@method _parseResponse
@param {String} data
*/
WebsocketProvider.prototype._parseResponse = function(data) {
var _this = this,
returnValues = [];
// DE-CHUNKER
var dechunkedData = data
.replace(/\}[\n\r]?\{/g,'}|--|{') // }{
.replace(/\}\][\n\r]?\[\{/g,'}]|--|[{') // }][{
.replace(/\}[\n\r]?\[\{/g,'}|--|[{') // }[{
.replace(/\}\][\n\r]?\{/g,'}]|--|{') // }]{
.split('|--|');
dechunkedData.forEach(function(data){
// prepend the last chunk
if(_this.lastChunk)
data = _this.lastChunk + data;
var result = null;
try {
result = JSON.parse(data);
} catch(e) {
_this.lastChunk = data;
// start timeout to cancel all requests
clearTimeout(_this.lastChunkTimeout);
_this.lastChunkTimeout = setTimeout(function(){
_this._timeout();
throw errors.InvalidResponse(data);
}, 1000 * 15);
return;
}
// cancel timeout and set chunk to null
clearTimeout(_this.lastChunkTimeout);
_this.lastChunk = null;
if(result)
returnValues.push(result);
});
return returnValues;
};
/**
Adds a callback to the responseCallbacks object,
which will be called if a response matching the response Id will arrive.
@method _addResponseCallback
*/
WebsocketProvider.prototype._addResponseCallback = function(payload, callback) {
var id = payload.id || payload[0].id;
var method = payload.method || payload[0].method;
this.responseCallbacks[id] = callback;
this.responseCallbacks[id].method = method;
var _this = this;
// schedule triggering the error response if a custom timeout is set
if (this._customTimeout) {
setTimeout(function () {
if (_this.responseCallbacks[id]) {
_this.responseCallbacks[id](errors.ConnectionTimeout(_this._customTimeout));
delete _this.responseCallbacks[id];
}
}, this._customTimeout);
}
};
/**
Timeout all requests when the end/error event is fired
@method _timeout
*/
WebsocketProvider.prototype._timeout = function() {
for(var key in this.responseCallbacks) {
if(this.responseCallbacks.hasOwnProperty(key)){
this.responseCallbacks[key](errors.InvalidConnection('on WS'));
delete this.responseCallbacks[key];
}
}
};
WebsocketProvider.prototype.send = function (payload, callback) {
var _this = this;
if (this.connection.readyState === this.connection.CONNECTING) {
setTimeout(function () {
_this.send(payload, callback);
}, 10);
return;
}
// try reconnect, when connection is gone
// if(!this.connection.writable)
// this.connection.connect({url: this.url});
if (this.connection.readyState !== this.connection.OPEN) {
console.error('connection not open on send()');
if (typeof this.connection.onerror === 'function') {
this.connection.onerror(new Error('connection not open'));
} else {
console.error('no error callback');
}
callback(new Error('connection not open'));
return;
}
this.connection.send(JSON.stringify(payload));
this._addResponseCallback(payload, callback);
};
/**
Subscribes to provider events.provider
@method on
@param {String} type 'notifcation', 'connect', 'error', 'end' or 'data'
@param {Function} callback the callback to call
*/
WebsocketProvider.prototype.on = function (type, callback) {
if(typeof callback !== 'function')
throw new Error('The second parameter callback must be a function.');
switch(type){
case 'data':
this.notificationCallbacks.push(callback);
break;
case 'connect':
this.connection.onopen = callback;
break;
case 'end':
this.connection.onclose = callback;
break;
case 'error':
this.connection.onerror = callback;
break;
// default:
// this.connection.on(type, callback);
// break;
}
};
// TODO add once
/**
Removes event listener
@method removeListener
@param {String} type 'notifcation', 'connect', 'error', 'end' or 'data'
@param {Function} callback the callback to call
*/
WebsocketProvider.prototype.removeListener = function (type, callback) {
var _this = this;
switch(type){
case 'data':
this.notificationCallbacks.forEach(function(cb, index){
if(cb === callback)
_this.notificationCallbacks.splice(index, 1);
});
break;
// TODO remvoving connect missing
// default:
// this.connection.removeListener(type, callback);
// break;
}
};
/**
Removes all event listeners
@method removeAllListeners
@param {String} type 'notifcation', 'connect', 'error', 'end' or 'data'
*/
WebsocketProvider.prototype.removeAllListeners = function (type) {
switch(type){
case 'data':
this.notificationCallbacks = [];
break;
// TODO remvoving connect properly missing
case 'connect':
this.connection.onopen = null;
break;
case 'end':
this.connection.onclose = null;
break;
case 'error':
this.connection.onerror = null;
break;
default:
// this.connection.removeAllListeners(type);
break;
}
};
/**
Resets the providers, clears all callbacks
@method reset
*/
WebsocketProvider.prototype.reset = function () {
this._timeout();
this.notificationCallbacks = [];
// this.connection.removeAllListeners('error');
// this.connection.removeAllListeners('end');
// this.connection.removeAllListeners('timeout');
this.addDefaultEvents();
};
WebsocketProvider.prototype.disconnect = function () {
if (this.connection) {
this.connection.close();
}
};
module.exports = WebsocketProvider;