/**
* Candy
* Blockchain driven Web Applications
* @Author: Andrey Nedobylsky
*/
/**
* required nodemetainfo
*/
'use strict';
//unify browser and node
if(typeof _this === 'undefined') {
var _this = this;
}
const MessageType = {
QUERY_LATEST: 0,
QUERY_ALL: 1,
RESPONSE_BLOCKCHAIN: 2,
MY_PEERS: 3,
BROADCAST: 4,
META: 5,
SW_BROADCAST: 6
};
const MAX_CONNECTIONS = 30;
const BlockchainRequestors = {
queryAllMsg: function (fromIndex, limit) {
limit = typeof limit === 'undefined' ? 1 : limit;
return {'type': MessageType.QUERY_ALL, data: typeof fromIndex === 'undefined' ? 0 : fromIndex, limit: limit}
}
};
function Candy(nodeList) {
//modules list(for compability with node)
try {
if(_this.window === undefined) {
this.WebSocket = require('ws');
this.starwaveProtocol = require('./starwaveProtocol.js');
this.NodeMetaInfo = require('./NodeMetaInfo.js');
this.DigitalSignature = require('./digitalSignature.js');
this.StarwaveCrypto = require('./starwaveCrypto.js');
this.URL = require('url').Url;
} else { //if browser
this.WebSocket = typeof WebSocket !== 'undefined' ? WebSocket : undefined;
this.starwaveProtocol = typeof starwaveProtocol !== 'undefined' ? starwaveProtocol : undefined;
this.NodeMetaInfo = typeof NodeMetaInfo !== 'undefined' ? NodeMetaInfo : undefined;
this.DigitalSignature = typeof DigitalSignature !== 'undefined' ? DigitalSignature : undefined;
this.StarwaveCrypto = typeof StarwaveCrypto !== 'undefined' ? StarwaveCrypto : undefined;
this.URL = URL;
}
} catch (e) {
console.log('Error trying to include libraries: ' + e);
}
let that = this;
this._resourceQueue = {};
this._lastMsgTimestamp = 0;
this._lastMsgIndex = 0;
this._requestQueue = {};
this._autoloader = undefined;
/**
* Max connections
* @type {number}
*/
this.maxConnections = MAX_CONNECTIONS;
/**
* Nodes list
*/
this.nodeList = nodeList;
/**
* All sockets list
* @type {Array}
*/
this.sockets = [];
/**
* Blockchain height
* @type {number}
*/
this.blockHeight = 0;
/**
* Generate uniq id string
* @return {string}
*/
this.getid = () => (Math.random() * (new Date().getTime())).toString(36).replace(/[^a-z]+/g, '');
/**
* Messages handlers
* @type {Array}
*/
this.messagesHandlers = [];
/**
* Known routes
* @type {{}}
*/
this.routes = {};
/**
* Allows multiple connections from one bus address
* if TRUE we don't check
* @type {boolean}
*/
this.allowMultiplySocketsOnBus = false;
/**
* Known secret keys
* consist of secret keys of different busAddresses of peers
* @type {{}}
*/
this.secretKeys = {};
if(typeof this.starwaveProtocol === 'function') {
this.starwave = new this.starwaveProtocol(this, MessageType);
} else {
console.log("Error: Can't find starwaveProtocol module");
}
/**
* Current reciever address. Override allowed
* @type {string}
*/
this.recieverAddress = this.getid() + this.getid();
/**
* On data recived callback
* @param {String} data
*/
this.ondata = function (data) {
return false;
};
/**
* On blockchain connection ready
*/
this.onready = function () {
};
/**
* If message recived
* @param {object} message
*/
this.onmessage = function (message) {
};
/**
* Internal data handler
* @param {WebSocket} source
* @param {Object} data
* @private
*/
this._dataRecieved = function (source, data) {
//prevent multiple sockets on one busaddress
if(!this.allowMultiplySocketsOnBus && (this.starwave)) {
if(this.starwave.preventMultipleSockets(source) === 0) {
data = null;
return;
}
}
if(typeof that.ondata === 'function') {
if(that.ondata(data)) {
return;
}
}
//Data block recived
if(data.type === MessageType.RESPONSE_BLOCKCHAIN) {
try {
/**
* @var {Block} block
*/
let blocks = JSON.parse(data.data);
for (let a in blocks) {
let block = blocks[a];
if(that.blockHeight < block.index) {
that.blockHeight = block.index
}
//Loading requested resource
if(typeof that._resourceQueue[block.index] !== 'undefined') {
that._resourceQueue[block.index](block.data, block);
that._resourceQueue[block.index] = undefined;
}
}
} catch (e) {
}
}
//New peers recived
if(data.type === MessageType.MY_PEERS) {
for (let a in data.data) {
if(data.data.hasOwnProperty(a)) {
if(that.nodeList.indexOf(data.data[a]) == -1) {
that.nodeList.push(data.data[a]);
if(that.getActiveConnections().length < that.maxConnections - 1) {
that.connectPeer(data.data[a]);
}
}
}
}
that.nodeList = Array.from(new Set(that.nodeList));
}
if(data.type === MessageType.BROADCAST) {
/*if(that._lastMsgIndex < data.index) {*/
if(data.reciver === that.recieverAddress) {
if(data.id === 'CANDY_APP_RESPONSE') {
if(typeof that._candyAppResponse === 'function') {
that._candyAppResponse(data);
}
} else {
if(typeof that.onmessage === 'function') {
that.onmessage(data);
}
}
} else {
if(data.recepient !== that.recieverAddress) {
data.TTL++;
that.broadcast(data);
}
}
/*}*/
that._lastMsgIndex = data.index;
that._lastMsgTimestamp = data.timestamp;
}
//add meta info handling //required NodeMetaInfo.js included
if(data.type === MessageType.META) {
if(typeof this.NodeMetaInfo === 'function') {
let ind = that.sockets.indexOf(source);
if(ind > -1) {
that.sockets[ind].nodeMetaInfo = (new this.NodeMetaInfo()).parse(data.data);
} else {
console.log('Error: Unexpected error occurred when trying to add validators');
}
} else {
console.log('Error: NodeMetaInfo.js has not been included');
}
}
if(data.type === MessageType.SW_BROADCAST) {
if(this.starwave) {
this._lastMsgIndex = this.starwave.handleMessage(data, this.messagesHandlers, source);
}
}
};
/**
* Returns array of connected sockets
* @return {Array}
*/
that.getActiveConnections = function () {
let activeSockets = [];
for (let a in that.sockets) {
if(that.sockets[a]) {
if(that.sockets[a].readyState === this.WebSocket.OPEN) {
activeSockets.push(that.sockets[a]);
}
}
}
return activeSockets;
};
/**
* Inits peer connection
* @param {String} peer
*/
this.connectPeer = function (peer) {
let socket = null;
try {
socket = new this.WebSocket(peer);
} catch (e) {
return;
}
socket.onopen = function () {
setTimeout(function () {
if(typeof that.onready !== 'undefined') {
if(typeof that._autoloader !== 'undefined') {
that._autoloader.onready();
}
that.onready();
that.onready = undefined;
}
}, 10);
};
socket.onclose = function (event) {
that.sockets.splice(that.sockets.indexOf(socket), 1);
// that.sockets[that.sockets.indexOf(socket)] = null;
// delete that.sockets[that.sockets.indexOf(socket)];
};
socket.onmessage = function (event) {
try {
let data = JSON.parse(event.data);
that._dataRecieved(socket, data);
} catch (e) {
}
};
socket.onerror = function (error) {
//console.log("Ошибка " + error.message);
};
if(_this.window === undefined) {
socket.on('open', () => socket.onopen());
socket.on('close', () => socket.onclose());
socket.on('message', () => socket.onmessage());
socket.on('error', () => socket.onerror());
}
that.sockets.push(socket);
};
/**
* Broadcast message to peers
* @param message
* @return {boolean} sending status
*/
this.broadcast = function (message) {
let sended = false;
if(typeof message !== 'string') {
message = JSON.stringify(message);
}
for (let a in that.sockets) {
if(that.sockets.hasOwnProperty(a) && that.sockets[a] !== null) {
try {
that.sockets[a].send(message);
sended = true;
} catch (e) {
}
}
}
return sended;
};
/**
* Broadcast global message
* @param {object} messageData Message data
* @param {string} id Message ID
* @param {string} reciver Receiver address
* @param {string} recipient Recipient address
*/
this.broadcastMessage = function (messageData, id, reciver, recipient) {
that._lastMsgIndex++;
let message = {
type: MessageType.BROADCAST,
data: messageData,
reciver: reciver,
recepient: recipient,
id: id,
timestamp: (new Date().getTime()),
TTL: 0,
index: that._lastMsgIndex,
mutex: this.getid() + this.getid() + this.getid()
};
if(!that.broadcast(message)) {
that.autoconnect(true);
return false;
}
return true;
};
/**
* Reconnecting peers if fully disconnected
* @param {boolean} force reconnection
*/
this.autoconnect = function (force) {
if(that.getActiveConnections().length < 1 || force) {
for (let a in that.nodeList) {
if(that.nodeList.hasOwnProperty(a)) {
if(that.getActiveConnections().length < that.maxConnections - 1) {
that.connectPeer(that.nodeList[a]);
}
}
}
} else {
that.sockets = Array.from(new Set(that.sockets));
that.connections = that.getActiveConnections().length;
}
};
/**
* Starts connection to blockchain
*/
this.start = function () {
for (let a in that.nodeList) {
if(that.nodeList.hasOwnProperty(a)) {
if(that.getActiveConnections().length < that.maxConnections - 1) {
that.connectPeer(that.nodeList[a]);
}
}
}
setInterval(function () {
that.autoconnect();
}, 5000);
return this;
};
/**
* Makes RAW Candy Server Application request
* @deprecated
* @param {string} uri
* @param requestData
* @param {string} backId
* @param {int} timeout
* @private
*/
this._candyAppRequest = function (uri, requestData, backId, timeout) {
let url = new this.URL(uri.replace('candy:', 'http:'));
let data = {
uri: uri,
data: requestData,
backId: backId,
timeout: timeout
};
this.broadcastMessage(data, 'CANDY_APP', url.host, that.recieverAddress);
};
/**
* Response from Candy Server App
* @param message
* @private
*/
this._candyAppResponse = function (message) {
if(typeof that._requestQueue[message.data.backId] !== 'undefined') {
let request = that._requestQueue[message.data.backId];
clearTimeout(request.timer);
request.callback(message.err, typeof message.data.data.body !== 'undefined' ? message.data.data.body : message.data.data, message);
that._requestQueue[message.data.backId] = undefined;
delete that._requestQueue[message.data.backId];
}
};
/**
* Creates request to app like $.ajax request
* @deprecated
* @param {string} uri
* @param {object} data
* @param {function} callback
* @param {int} timeout
*/
this.requestApp = function (uri, data, callback, timeout) {
if(typeof timeout === 'undefined') {
timeout = 10000;
}
let requestId = that.getid();
let timer = setTimeout(function () {
that._requestQueue[requestId].callback({error: 'Timeout', request: that._requestQueue[requestId]});
that._requestQueue[requestId] = undefined;
}, timeout);
that._requestQueue[requestId] = {
id: requestId,
uri: uri,
data: data,
timeout: timeout,
callback: callback,
timer: timer
};
that._candyAppRequest(uri, data, requestId, timeout);
return that._requestQueue[requestId];
};
/**
* Universal request function.
* For request data from vitamin chain use "block" as host name and bock id as path. Ex: candy://block/14
* For application request use candy://hostname/filepath?get=query
* Data and timeout ignored in block request
* @param {string} uri Uri string
* @param {object} data Data object
* @param {function} callback Callback function
* @param {int} timeout Request timeout (deprecated)
*/
this.request = function (uri, data, callback, timeout) {
let url = new this.URL(uri.replace('candy:', 'http:'));
if(url.hostname === 'block') {
that.loadResource(url.pathname.replace('/', ''), function (err, data) {
callback(err, data.candyData, data);
});
} else {
that.requestApp(uri, data, callback, timeout);
}
};
/**
* Load resource from blockchain
* @param {Number} blockId Block index
* @param {Function} callback Callback function
*/
this.loadResource = function (blockId, callback) {
if(blockId > that.blockHeigth && blockId < 1) {
callback(404);
}
that._resourceQueue[blockId] = function (data, rawBlock) {
callback(null, JSON.parse(data), rawBlock);
};
let message = BlockchainRequestors.queryAllMsg(blockId);
that.broadcast(JSON.stringify(message));
};
/**
* Add message handler
* @param {string} id Message ID
* @param {Function} handler Handler function
*/
this.registerMessageHandler = function (id, handler) {
this.messagesHandlers.push({id: id, handle: handler});
this.messagesHandlers.sort((a, b) => a.id > b.id);
};
return this;
}
if(this.window === undefined) {
module.exports = Candy;
}