Reorganized repo using yarn and workspaces

This commit is contained in:
maciejhirsz
2018-06-27 14:04:34 +02:00
parent 8c3b1ee749
commit 0580e25380
84 changed files with 8434 additions and 701 deletions
+59
View File
@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const feed_1 = require("./feed");
const common_1 = require("@dotstats/common");
class Aggregator extends EventEmitter {
constructor() {
super();
this.nodes = new common_1.IdSet();
this.feeds = new common_1.IdSet();
this.height = 0;
setInterval(() => this.timeoutCheck(), 10000);
}
addNode(node) {
this.nodes.add(node);
this.broadcast(feed_1.default.addedNode(node));
node.once('disconnect', () => {
node.removeAllListeners('block');
this.nodes.remove(node);
this.broadcast(feed_1.default.removedNode(node));
});
node.on('block', () => this.updateBlock(node));
}
addFeed(feed) {
this.feeds.add(feed);
feed.send(feed_1.default.bestBlock(this.height));
for (const node of this.nodes.entries) {
feed.send(feed_1.default.addedNode(node));
}
feed.once('disconnect', () => {
this.feeds.remove(feed);
});
}
nodeList() {
return this.nodes.entries;
}
broadcast(data) {
for (const feed of this.feeds.entries) {
feed.send(data);
}
}
timeoutCheck() {
const now = Date.now();
for (const node of this.nodes.entries) {
node.timeoutCheck(now);
}
}
updateBlock(node) {
if (node.height > this.height) {
this.height = node.height;
this.broadcast(feed_1.default.bestBlock(this.height));
console.log(`New block ${this.height}`);
}
this.broadcast(feed_1.default.imported(node));
console.log(`${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
}
}
exports.default = Aggregator;
//# sourceMappingURL=aggregator.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"aggregator.js","sourceRoot":"","sources":["../src/aggregator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const feed_1 = require("./feed");
const shared_1 = require("@dotstats/shared");
class Aggregator extends EventEmitter {
constructor() {
super();
this.nodes = new shared_1.IdSet();
this.feeds = new shared_1.IdSet();
this.height = 0;
setInterval(() => this.timeoutCheck(), 10000);
}
addNode(node) {
this.nodes.add(node);
this.broadcast(feed_1.default.addedNode(node));
node.once('disconnect', () => {
node.removeAllListeners('block');
this.nodes.remove(node);
this.broadcast(feed_1.default.removedNode(node));
});
node.on('block', () => this.updateBlock(node));
}
addFeed(feed) {
this.feeds.add(feed);
feed.send(feed_1.default.bestBlock(this.height));
for (const node of this.nodes.entries) {
feed.send(feed_1.default.addedNode(node));
}
feed.once('disconnect', () => {
this.feeds.remove(feed);
});
}
nodeList() {
return this.nodes.entries;
}
broadcast(data) {
for (const feed of this.feeds.entries) {
feed.send(data);
}
}
timeoutCheck() {
const now = Date.now();
for (const node of this.nodes.entries) {
node.timeoutCheck(now);
}
}
updateBlock(node) {
if (node.height > this.height) {
this.height = node.height;
this.broadcast(feed_1.default.bestBlock(this.height));
console.log(`New block ${this.height}`);
}
this.broadcast(feed_1.default.imported(node));
console.log(`${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
}
}
exports.default = Aggregator;
//# sourceMappingURL=aggregator.js.map
@@ -0,0 +1 @@
{"version":3,"file":"aggregator.js","sourceRoot":"","sources":["../../../src/aggregator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const shared_1 = require("@dotstats/shared");
const nextId = shared_1.idGenerator();
function serialize(msg) {
return JSON.stringify(msg);
}
class Feed extends EventEmitter {
constructor(socket) {
super();
this.id = nextId();
this.socket = socket;
socket.on('error', () => this.disconnect());
socket.on('close', () => this.disconnect());
}
static bestBlock(height) {
return serialize({
action: 'best',
payload: height
});
}
static addedNode(node) {
return serialize({
action: 'added',
payload: [node.id, node.nodeInfo(), node.blockInfo()]
});
}
static removedNode(node) {
return serialize({
action: 'removed',
payload: node.id
});
}
static imported(node) {
return serialize({
action: 'imported',
payload: [node.id, node.blockInfo()]
});
}
send(data) {
this.socket.send(data);
}
disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
}
exports.default = Feed;
//# sourceMappingURL=feed.js.map
@@ -0,0 +1 @@
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../../../src/feed.ts"],"names":[],"mappings":";;AACA,uCAAuC;AAEvC,6CAA2D;AAE3D,MAAM,MAAM,GAAG,oBAAW,EAAQ,CAAC;AAoCnC,mBAAmB,GAAY;IAC3B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAa,CAAC;AAC3C,CAAC;AAED,UAA0B,SAAQ,YAAY;IAK1C,YAAY,MAAiB;QACzB,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAChD,CAAC;IAEM,MAAM,CAAC,SAAS,CAAC,MAAc;QAClC,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,MAAM;SAClB,CAAC,CAAC;IACP,CAAC;IAEM,MAAM,CAAC,SAAS,CAAC,IAAU;QAC9B,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;SACxD,CAAC,CAAA;IACN,CAAC;IAEM,MAAM,CAAC,WAAW,CAAC,IAAU;QAChC,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,IAAI,CAAC,EAAE;SACnB,CAAC,CAAC;IACP,CAAC;IAEM,MAAM,CAAC,QAAQ,CAAC,IAAU;QAC7B,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;SACvC,CAAC,CAAC;IACP,CAAC;IAEM,IAAI,CAAC,IAAc;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAEO,UAAU;QACd,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAEpB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC5B,CAAC;CACJ;AArDD,uBAqDC"}
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const WebSocket = require("ws");
const express = require("express");
const http_1 = require("http");
const node_1 = require("./node");
const feed_1 = require("./feed");
const aggregator_1 = require("./aggregator");
const shared_1 = require("@dotstats/shared");
const aggregator = new aggregator_1.default;
const app = express();
const server = http_1.createServer(app);
// WebSocket for Nodes feeding telemetry data to the server
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
// WebSocket for web clients listening to the telemetry data aggregate
const telemetryFeed = new WebSocket.Server({ server });
app.get('/', function (req, res) {
function nodeInfo(node) {
return `${node.name} | ${node.height} | Block time ${node.blockTime / 1000}s`;
}
res.send(`<pre>
Best block: ${aggregator.height}
Node list:
${shared_1.join(shared_1.map(aggregator.nodeList(), nodeInfo), '\n')}
</pre>`);
});
incomingTelemetry.on('connection', async (socket) => {
try {
aggregator.addNode(await node_1.default.fromSocket(socket));
}
catch (err) {
console.error(err);
}
});
telemetryFeed.on('connection', (socket) => {
aggregator.addFeed(new feed_1.default(socket));
});
console.log('Starting server on port 8080');
server.listen(8080);
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":";;AAAA,gCAAgC;AAChC,mCAAmC;AACnC,+BAAoC;AACpC,iCAA0B;AAC1B,iCAA0B;AAC1B,6CAAsC;AACtC,6CAA6C;AAE7C,MAAM,UAAU,GAAG,IAAI,oBAAU,CAAC;AAClC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,MAAM,GAAG,mBAAY,CAAC,GAAG,CAAC,CAAC;AAEjC,2DAA2D;AAC3D,MAAM,iBAAiB,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAE/D,sEAAsE;AACtE,MAAM,aAAa,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AAEvD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,EAAE,GAAG;IAC3B,kBAAkB,IAAU;QACxB,OAAO,GAAG,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,iBAAiB,IAAI,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC;IAClF,CAAC;IAED,GAAG,CAAC,IAAI,CAEZ;cACc,UAAU,CAAC,MAAM;;;EAG5B,aAAI,CAAC,YAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAE;OAC7C,CAEF,CAAC;AACN,CAAC,CAAC,CAAC;AAEH,iBAAiB,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,MAAiB,EAAE,EAAE;IAC3D,IAAI;QACA,UAAU,CAAC,OAAO,CAAC,MAAM,cAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;KACrD;IAAC,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;KACtB;AACL,CAAC,CAAC,CAAC;AAEH,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAiB,EAAE,EAAE;IACjD,UAAU,CAAC,OAAO,CAAC,IAAI,cAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AACzC,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;AAC5C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function parseMessage(data) {
try {
const message = JSON.parse(data.toString());
if (message && typeof message.msg === 'string' && typeof message.ts === 'string') {
message.ts = new Date(message.ts);
return message;
}
}
catch (_) {
console.warn('Error parsing message JSON');
}
return null;
}
exports.parseMessage = parseMessage;
function getBestBlock(message) {
switch (message.msg) {
case 'node.start':
case 'system.interval':
case 'block.import':
return message;
default:
return null;
}
}
exports.getBestBlock = getBestBlock;
// received: {"msg":"block.import","level":"INFO","ts":"2018-06-18T17:30:35.285406538+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"node.start","level":"INFO","ts":"2018-06-18T17:30:40.038731057+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"system.connected","level":"INFO","ts":"2018-06-18T17:30:40.038975471+02:00","chain":"dev","config":"","version":"0.2.0","implementation":"parity-polkadot","name":"Majestic Widget"}
// received: {"msg":"system.interval","level":"INFO","ts":"2018-06-19T14:00:05.091355364+02:00","txcount":0,"best":"360c9563857308703398f637932b7ffe884e5c7b09692600ff09a4d753c9d948","height":7559,"peers":0,"status":"Idle"}
//# sourceMappingURL=message.js.map
@@ -0,0 +1 @@
{"version":3,"file":"message.js","sourceRoot":"","sources":["../../../src/message.ts"],"names":[],"mappings":";;AAGA,sBAA6B,IAAU;IACnC,IAAI;QACA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAE5C,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ,EAAE;YAC9E,OAAO,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAElC,OAAO,OAAO,CAAC;SAClB;KACJ;IAAC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;KAC9C;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAdD,oCAcC;AAED,sBAA6B,OAAgB;IACzC,QAAQ,OAAO,CAAC,GAAG,EAAE;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,iBAAiB,CAAC;QACvB,KAAK,cAAc;YACf,OAAO,OAAO,CAAC;QACnB;YACI,OAAO,IAAI,CAAC;KACnB;AACL,CAAC;AATD,oCASC;AA8CD,oLAAoL;AACpL,kLAAkL;AAClL,yMAAyM;AACzM,8NAA8N"}
+124
View File
@@ -0,0 +1,124 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const shared_1 = require("@dotstats/shared");
const message_1 = require("./message");
const BLOCK_TIME_HISTORY = 10;
const TIMEOUT = 1000 * 60 * 5; // 5 seconds
const nextId = shared_1.idGenerator();
class Node extends EventEmitter {
constructor(socket, name, config, implentation, version) {
super();
this.height = 0;
this.latency = 0;
this.blockTime = 0;
this.blockTimes = new Array(BLOCK_TIME_HISTORY);
this.lastBlockAt = null;
this.lastMessage = Date.now();
this.id = nextId();
this.socket = socket;
this.name = name;
this.config = config;
this.implementation = implentation;
this.version = version;
console.log(`Listening to a new node: ${name}`);
socket.on('message', (data) => {
const message = message_1.parseMessage(data);
if (!message)
return;
this.lastMessage = Date.now();
this.updateLatency(message.ts);
const update = message_1.getBestBlock(message);
if (update) {
this.updateBestBlock(update);
}
});
socket.on('close', () => {
console.log(`${this.name} has disconnected`);
this.disconnect();
});
socket.on('error', (error) => {
console.error(`${this.name} has errored`, error);
this.disconnect();
});
}
static fromSocket(socket) {
return new Promise((resolve, reject) => {
function cleanup() {
clearTimeout(timeout);
socket.removeAllListeners('message');
}
function handler(data) {
const message = message_1.parseMessage(data);
if (message && message.msg === "system.connected") {
cleanup();
const { name, config, implementation, version } = message;
resolve(new Node(socket, name, config, implementation, version));
}
}
socket.on('message', handler);
const timeout = setTimeout(() => {
cleanup();
socket.close();
return reject(new Error('Timeout on waiting for system.connected message'));
}, 5000);
});
}
timeoutCheck(now) {
if (this.lastMessage + TIMEOUT < now) {
this.disconnect();
}
}
nodeInfo() {
return {
name: this.name,
};
}
blockInfo() {
return {
height: this.height,
blockTime: this.blockTime,
};
}
get average() {
let accounted = 0;
let sum = 0;
for (const time of this.blockTimes) {
if (time) {
accounted += 1;
sum += time;
}
}
if (accounted === 0) {
return 0;
}
return sum / accounted;
}
disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
updateLatency(time) {
this.latency = this.lastMessage - +time;
}
updateBestBlock(update) {
const { height, ts: time, best } = update;
if (this.height < height) {
const blockTime = this.getBlockTime(time);
this.height = height;
this.lastBlockAt = time;
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
this.blockTime = blockTime;
this.emit('block');
}
}
getBlockTime(time) {
if (!this.lastBlockAt) {
return 0;
}
return +time - +this.lastBlockAt;
}
}
exports.default = Node;
//# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../../src/node.ts"],"names":[],"mappings":"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"}
+51
View File
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const common_1 = require("@dotstats/common");
const nextId = common_1.idGenerator();
function serialize(msg) {
return JSON.stringify(msg);
}
class Feed extends EventEmitter {
constructor(socket) {
super();
this.id = nextId();
this.socket = socket;
socket.on('error', () => this.disconnect());
socket.on('close', () => this.disconnect());
}
static bestBlock(height) {
return serialize({
action: 'best',
payload: height
});
}
static addedNode(node) {
return serialize({
action: 'added',
payload: [node.id, node.nodeInfo(), node.blockInfo()]
});
}
static removedNode(node) {
return serialize({
action: 'removed',
payload: node.id
});
}
static imported(node) {
return serialize({
action: 'imported',
payload: [node.id, node.blockInfo()]
});
}
send(data) {
this.socket.send(data);
}
disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
}
exports.default = Feed;
//# sourceMappingURL=feed.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../src/feed.ts"],"names":[],"mappings":";;AACA,uCAAuC;AAEvC,6CAA2D;AAE3D,MAAM,MAAM,GAAG,oBAAW,EAAQ,CAAC;AAoCnC,mBAAmB,GAAY;IAC3B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAa,CAAC;AAC3C,CAAC;AAED,UAA0B,SAAQ,YAAY;IAK1C,YAAY,MAAiB;QACzB,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAChD,CAAC;IAEM,MAAM,CAAC,SAAS,CAAC,MAAc;QAClC,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,MAAM;SAClB,CAAC,CAAC;IACP,CAAC;IAEM,MAAM,CAAC,SAAS,CAAC,IAAU;QAC9B,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;SACxD,CAAC,CAAA;IACN,CAAC;IAEM,MAAM,CAAC,WAAW,CAAC,IAAU;QAChC,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,IAAI,CAAC,EAAE;SACnB,CAAC,CAAC;IACP,CAAC;IAEM,MAAM,CAAC,QAAQ,CAAC,IAAU;QAC7B,OAAO,SAAS,CAAC;YACb,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;SACvC,CAAC,CAAC;IACP,CAAC;IAEM,IAAI,CAAC,IAAc;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAEO,UAAU;QACd,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAEpB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC5B,CAAC;CACJ;AArDD,uBAqDC"}
+41
View File
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const WebSocket = require("ws");
const express = require("express");
const http_1 = require("http");
const node_1 = require("./node");
const feed_1 = require("./feed");
const aggregator_1 = require("./aggregator");
const common_1 = require("@dotstats/common");
const aggregator = new aggregator_1.default;
const app = express();
const server = http_1.createServer(app);
// WebSocket for Nodes feeding telemetry data to the server
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
// WebSocket for web clients listening to the telemetry data aggregate
const telemetryFeed = new WebSocket.Server({ server });
app.get('/', function (req, res) {
function nodeInfo(node) {
return `${node.name} | ${node.height} | Block time ${node.blockTime / 1000}s`;
}
res.send(`<pre>
Best block: ${aggregator.height}
Node list:
${common_1.join(common_1.map(aggregator.nodeList(), nodeInfo), '\n')}
</pre>`);
});
incomingTelemetry.on('connection', async (socket) => {
try {
aggregator.addNode(await node_1.default.fromSocket(socket));
}
catch (err) {
console.error(err);
}
});
telemetryFeed.on('connection', (socket) => {
aggregator.addFeed(new feed_1.default(socket));
});
console.log('Starting server on port 8080');
server.listen(8080);
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,gCAAgC;AAChC,mCAAmC;AACnC,+BAAoC;AACpC,iCAA0B;AAC1B,iCAA0B;AAC1B,6CAAsC;AACtC,6CAA6C;AAE7C,MAAM,UAAU,GAAG,IAAI,oBAAU,CAAC;AAClC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,MAAM,GAAG,mBAAY,CAAC,GAAG,CAAC,CAAC;AAEjC,2DAA2D;AAC3D,MAAM,iBAAiB,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAE/D,sEAAsE;AACtE,MAAM,aAAa,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AAEvD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,EAAE,GAAG;IAC3B,kBAAkB,IAAU;QACxB,OAAO,GAAG,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,iBAAiB,IAAI,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC;IAClF,CAAC;IAED,GAAG,CAAC,IAAI,CAEZ;cACc,UAAU,CAAC,MAAM;;;EAG5B,aAAI,CAAC,YAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAE;OAC7C,CAEF,CAAC;AACN,CAAC,CAAC,CAAC;AAEH,iBAAiB,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,MAAiB,EAAE,EAAE;IAC3D,IAAI;QACA,UAAU,CAAC,OAAO,CAAC,MAAM,cAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;KACrD;IAAC,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;KACtB;AACL,CAAC,CAAC,CAAC;AAEH,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAiB,EAAE,EAAE;IACjD,UAAU,CAAC,OAAO,CAAC,IAAI,cAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AACzC,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;AAC5C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC"}
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function parseMessage(data) {
try {
const message = JSON.parse(data.toString());
if (message && typeof message.msg === 'string' && typeof message.ts === 'string') {
message.ts = new Date(message.ts);
return message;
}
}
catch (_) {
console.warn('Error parsing message JSON');
}
return null;
}
exports.parseMessage = parseMessage;
function getBestBlock(message) {
switch (message.msg) {
case 'node.start':
case 'system.interval':
case 'block.import':
return message;
default:
return null;
}
}
exports.getBestBlock = getBestBlock;
// received: {"msg":"block.import","level":"INFO","ts":"2018-06-18T17:30:35.285406538+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"node.start","level":"INFO","ts":"2018-06-18T17:30:40.038731057+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"system.connected","level":"INFO","ts":"2018-06-18T17:30:40.038975471+02:00","chain":"dev","config":"","version":"0.2.0","implementation":"parity-polkadot","name":"Majestic Widget"}
// received: {"msg":"system.interval","level":"INFO","ts":"2018-06-19T14:00:05.091355364+02:00","txcount":0,"best":"360c9563857308703398f637932b7ffe884e5c7b09692600ff09a4d753c9d948","height":7559,"peers":0,"status":"Idle"}
//# sourceMappingURL=message.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"message.js","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":";;AAGA,sBAA6B,IAAU;IACnC,IAAI;QACA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAE5C,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ,EAAE;YAC9E,OAAO,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAElC,OAAO,OAAO,CAAC;SAClB;KACJ;IAAC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;KAC9C;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAdD,oCAcC;AAED,sBAA6B,OAAgB;IACzC,QAAQ,OAAO,CAAC,GAAG,EAAE;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,iBAAiB,CAAC;QACvB,KAAK,cAAc;YACf,OAAO,OAAO,CAAC;QACnB;YACI,OAAO,IAAI,CAAC;KACnB;AACL,CAAC;AATD,oCASC;AA8CD,oLAAoL;AACpL,kLAAkL;AAClL,yMAAyM;AACzM,8NAA8N"}
+124
View File
@@ -0,0 +1,124 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const EventEmitter = require("events");
const common_1 = require("@dotstats/common");
const message_1 = require("./message");
const BLOCK_TIME_HISTORY = 10;
const TIMEOUT = 1000 * 60 * 5; // 5 seconds
const nextId = common_1.idGenerator();
class Node extends EventEmitter {
constructor(socket, name, config, implentation, version) {
super();
this.height = 0;
this.latency = 0;
this.blockTime = 0;
this.blockTimes = new Array(BLOCK_TIME_HISTORY);
this.lastBlockAt = null;
this.lastMessage = Date.now();
this.id = nextId();
this.socket = socket;
this.name = name;
this.config = config;
this.implementation = implentation;
this.version = version;
console.log(`Listening to a new node: ${name}`);
socket.on('message', (data) => {
const message = message_1.parseMessage(data);
if (!message)
return;
this.lastMessage = Date.now();
this.updateLatency(message.ts);
const update = message_1.getBestBlock(message);
if (update) {
this.updateBestBlock(update);
}
});
socket.on('close', () => {
console.log(`${this.name} has disconnected`);
this.disconnect();
});
socket.on('error', (error) => {
console.error(`${this.name} has errored`, error);
this.disconnect();
});
}
static fromSocket(socket) {
return new Promise((resolve, reject) => {
function cleanup() {
clearTimeout(timeout);
socket.removeAllListeners('message');
}
function handler(data) {
const message = message_1.parseMessage(data);
if (message && message.msg === "system.connected") {
cleanup();
const { name, config, implementation, version } = message;
resolve(new Node(socket, name, config, implementation, version));
}
}
socket.on('message', handler);
const timeout = setTimeout(() => {
cleanup();
socket.close();
return reject(new Error('Timeout on waiting for system.connected message'));
}, 5000);
});
}
timeoutCheck(now) {
if (this.lastMessage + TIMEOUT < now) {
this.disconnect();
}
}
nodeInfo() {
return {
name: this.name,
};
}
blockInfo() {
return {
height: this.height,
blockTime: this.blockTime,
};
}
get average() {
let accounted = 0;
let sum = 0;
for (const time of this.blockTimes) {
if (time) {
accounted += 1;
sum += time;
}
}
if (accounted === 0) {
return 0;
}
return sum / accounted;
}
disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
updateLatency(time) {
this.latency = this.lastMessage - +time;
}
updateBestBlock(update) {
const { height, ts: time, best } = update;
if (this.height < height) {
const blockTime = this.getBlockTime(time);
this.height = height;
this.lastBlockAt = time;
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
this.blockTime = blockTime;
this.emit('block');
}
}
getBlockTime(time) {
if (!this.lastBlockAt) {
return 0;
}
return +time - +this.lastBlockAt;
}
}
exports.default = Node;
//# sourceMappingURL=node.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Higher order function producing new auto-incremented `Id`s.
*/
function idGenerator() {
let current = 0;
return () => current++;
}
exports.idGenerator = idGenerator;
class IdSet {
constructor() {
this.map = new Map();
}
add(item) {
this.map.set(item.id, item);
}
remove(item) {
this.map.delete(item.id);
}
get entries() {
return this.map.values();
}
}
exports.IdSet = IdSet;
//# sourceMappingURL=id.js.map
@@ -0,0 +1 @@
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../../../shared/src/id.ts"],"names":[],"mappings":";;AAOA;;GAEG;AACH;IACI,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO,GAAG,EAAE,CAAC,OAAO,EAAW,CAAC;AACpC,CAAC;AAJD,kCAIC;AAMD;IAAA;QACY,QAAG,GAAkB,IAAI,GAAG,EAAE,CAAC;IAa3C,CAAC;IAXU,GAAG,CAAC,IAAkB;QACzB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAEM,MAAM,CAAC,IAAkB;QAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED,IAAW,OAAO;QACd,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC7B,CAAC;CACJ;AAdD,sBAcC"}
@@ -0,0 +1,9 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./id"));
__export(require("./iterators"));
__export(require("./types"));
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../shared/src/index.ts"],"names":[],"mappings":";;;;;AAAA,0BAAqB;AACrB,iCAA4B;AAC5B,6BAAwB"}
@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function* map(iter, fn) {
for (const item of iter) {
yield fn(item);
}
}
exports.map = map;
function* chain(a, b) {
yield* a;
yield* b;
}
exports.chain = chain;
function* zip(a, b) {
let itemA = a.next();
let itemB = b.next();
while (!itemA.done && !itemB.done) {
yield [itemA.value, itemB.value];
itemA = a.next();
itemB = b.next();
}
}
exports.zip = zip;
function* take(iter, n) {
for (const item of iter) {
if (n-- === 0) {
return;
}
yield item;
}
}
exports.take = take;
function skip(iter, n) {
while (n-- !== 0 && !iter.next().done) { }
return iter;
}
exports.skip = skip;
function reduce(iter, fn, accumulator) {
for (const item of iter) {
accumulator = fn(accumulator, item);
}
return accumulator;
}
exports.reduce = reduce;
function join(iter, glue) {
const first = iter.next();
if (first.done) {
return '';
}
let result = first.value.toString();
for (const item of iter) {
result += glue + item;
}
return result;
}
exports.join = join;
//# sourceMappingURL=iterators.js.map
@@ -0,0 +1 @@
{"version":3,"file":"iterators.js","sourceRoot":"","sources":["../../../../shared/src/iterators.ts"],"names":[],"mappings":";;AAAA,QAAe,CAAC,KAAW,IAAyB,EAAE,EAAkB;IACpE,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;KAClB;AACL,CAAC;AAJD,kBAIC;AAED,QAAe,CAAC,OAAU,CAAsB,EAAE,CAAsB;IACpE,KAAK,CAAC,CAAC,CAAC,CAAC;IACT,KAAK,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;AAHD,sBAGC;AAED,QAAe,CAAC,KAAW,CAAsB,EAAE,CAAsB;IACrE,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAErB,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAC/B,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAEjC,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;KACpB;AACL,CAAC;AAVD,kBAUC;AAED,QAAe,CAAC,MAAS,IAAyB,EAAE,CAAS;IACzD,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,IAAI,CAAC,EAAE,KAAK,CAAC,EAAE;YACX,OAAO;SACV;QAED,MAAM,IAAI,CAAC;KACd;AACL,CAAC;AARD,oBAQC;AAED,cAAwB,IAAyB,EAAE,CAAS;IACxD,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,GAAE;IAEzC,OAAO,IAAI,CAAC;AAChB,CAAC;AAJD,oBAIC;AAED,gBAA6B,IAAyB,EAAE,EAA2B,EAAE,WAAc;IAC/F,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,WAAW,GAAG,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;KACvC;IAED,OAAO,WAAW,CAAC;AACvB,CAAC;AAND,wBAMC;AAED,cAAqB,IAAkD,EAAE,IAAY;IACjF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAE1B,IAAI,KAAK,CAAC,IAAI,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IAED,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAEpC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,MAAM,IAAI,IAAI,GAAG,IAAI,CAAC;KACzB;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAdD,oBAcC"}
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* PhantomData akin to Rust, because sometimes you need to be smarter than
* the compiler.
*/
class PhantomData {
}
exports.PhantomData = PhantomData;
//# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../shared/src/types.ts"],"names":[],"mappings":";;AAAA;;;GAGG;AACH;CAAsD;AAAtD,kCAAsD"}
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../../../../node_modules/typescript/bin/tsc
+1
View File
@@ -0,0 +1 @@
../../../../node_modules/typescript/bin/tsserver
+466
View File
@@ -0,0 +1,466 @@
{
"name": "dotstats-server",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
"requires": {
"@types/connect": "3.4.32",
"@types/node": "10.3.3"
}
},
"@types/connect": {
"version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
"requires": {
"@types/node": "10.3.3"
}
},
"@types/events": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA=="
},
"@types/express": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz",
"integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==",
"requires": {
"@types/body-parser": "1.17.0",
"@types/express-serve-static-core": "4.16.0",
"@types/serve-static": "1.13.2"
}
},
"@types/express-serve-static-core": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz",
"integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==",
"requires": {
"@types/events": "1.2.0",
"@types/node": "10.3.3",
"@types/range-parser": "1.2.2"
}
},
"@types/mime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz",
"integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA=="
},
"@types/node": {
"version": "10.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.3.tgz",
"integrity": "sha512-/gwCgiI2e9RzzZTKbl+am3vgNqOt7a9fJ/uxv4SqYKxenoEDNVU3KZEadlpusWhQI0A0dOrZ0T68JYKVjzmgdQ=="
},
"@types/range-parser": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz",
"integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw=="
},
"@types/serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==",
"requires": {
"@types/express-serve-static-core": "4.16.0",
"@types/mime": "2.0.0"
}
},
"@types/ws": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz",
"integrity": "sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg==",
"requires": {
"@types/events": "1.2.0",
"@types/node": "10.3.3"
}
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "2.1.18",
"negotiator": "0.6.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"body-parser": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
"integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "1.0.4",
"debug": "2.6.9",
"depd": "1.1.2",
"http-errors": "1.6.3",
"iconv-lite": "0.4.19",
"on-finished": "2.3.0",
"qs": "6.5.1",
"raw-body": "2.3.2",
"type-is": "1.6.16"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
"integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
"requires": {
"accepts": "1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.2",
"content-disposition": "0.5.2",
"content-type": "1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "1.1.2",
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"etag": "1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "1.1.2",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "2.0.3",
"qs": "6.5.1",
"range-parser": "1.2.0",
"safe-buffer": "5.1.1",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "1.4.0",
"type-is": "1.6.16",
"utils-merge": "1.0.1",
"vary": "1.1.2"
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"statuses": "1.4.0",
"unpipe": "1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": "1.4.0"
}
},
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
"integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
},
"mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
},
"mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"requires": {
"mime-db": "1.33.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
"integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
"requires": {
"forwarded": "0.1.2",
"ipaddr.js": "1.6.0"
}
},
"qs": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"raw-body": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
"integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.2",
"iconv-lite": "0.4.19",
"unpipe": "1.0.0"
},
"dependencies": {
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
},
"http-errors": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
"integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
"requires": {
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
"statuses": "1.4.0"
}
},
"setprototypeof": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
}
}
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "1.1.2",
"destroy": "1.0.4",
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"etag": "1.8.1",
"fresh": "0.5.2",
"http-errors": "1.6.3",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "2.3.0",
"range-parser": "1.2.0",
"statuses": "1.4.0"
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"parseurl": "1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "2.1.18"
}
},
"typescript": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"ws": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz",
"integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==",
"requires": {
"async-limiter": "1.0.0"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@dotstats/backend",
"version": "0.1.0",
"author": "Parity Technologies Ltd. <admin@parity.io>",
"license": "GPL-3.0",
"description": "Polkadot Telemetry frontend",
"main": "build/index.js",
"scripts": {
"start": "tsc && node build/index.js",
"build": "tsc",
"check": "tsc --noEmit"
},
"dependencies": {
"@types/express": "^4.16.0",
"@types/node": "^10.3.3",
"@types/ws": "^5.1.2",
"express": "^4.16.3",
"typescript": "^2.9.2",
"ws": "5.2.0"
}
}
+77
View File
@@ -0,0 +1,77 @@
import * as EventEmitter from 'events';
import Node from './node';
import Feed, { FeedData } from './feed';
import { Id, IdSet } from '@dotstats/common';
export default class Aggregator extends EventEmitter {
private nodes: IdSet<Node> = new IdSet<Node>();
private feeds: IdSet<Feed> = new IdSet<Feed>();
public height: number = 0;
constructor() {
super();
setInterval(() => this.timeoutCheck(), 10000);
}
public addNode(node: Node) {
this.nodes.add(node);
this.broadcast(Feed.addedNode(node));
node.once('disconnect', () => {
node.removeAllListeners('block');
this.nodes.remove(node);
this.broadcast(Feed.removedNode(node));
});
node.on('block', () => this.updateBlock(node));
}
public addFeed(feed: Feed) {
this.feeds.add(feed);
feed.send(Feed.bestBlock(this.height));
for (const node of this.nodes.entries) {
feed.send(Feed.addedNode(node));
}
feed.once('disconnect', () => {
this.feeds.remove(feed);
})
}
public nodeList(): IterableIterator<Node> {
return this.nodes.entries;
}
private broadcast(data: FeedData) {
for (const feed of this.feeds.entries) {
feed.send(data);
}
}
private timeoutCheck() {
const now = Date.now();
for (const node of this.nodes.entries) {
node.timeoutCheck(now);
}
}
private updateBlock(node: Node) {
if (node.height > this.height) {
this.height = node.height;
this.broadcast(Feed.bestBlock(this.height));
console.log(`New block ${this.height}`);
}
this.broadcast(Feed.imported(node));
console.log(`${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
}
}
+99
View File
@@ -0,0 +1,99 @@
import * as WebSocket from 'ws';
import * as EventEmitter from 'events';
import Node, { NodeInfo, BlockInfo } from './node';
import { Opaque, Id, idGenerator } from '@dotstats/common';
const nextId = idGenerator<Feed>();
export interface BlockInfo {
height: number;
blockTime: number;
}
interface BestBlock {
action: 'best';
payload: number;
}
interface AddedNode {
action: 'added';
payload: [Id<Node>, NodeInfo, BlockInfo];
}
interface RemovedNode {
action: 'removed';
payload: Id<Node>;
}
interface Imported {
action: 'imported';
payload: [Id<Node>, BlockInfo];
}
type Message = BestBlock | AddedNode | RemovedNode | Imported;
/**
* Opaque data type to be sent to the feed. Passing through
* strings means we can only serialize once, no matter how
* many feed clients are listening in.
*/
export type FeedData = Opaque<string, Message>;
function serialize(msg: Message): FeedData {
return JSON.stringify(msg) as FeedData;
}
export default class Feed extends EventEmitter {
public id: Id<Feed>;
private socket: WebSocket;
constructor(socket: WebSocket) {
super();
this.id = nextId();
this.socket = socket;
socket.on('error', () => this.disconnect());
socket.on('close', () => this.disconnect());
}
public static bestBlock(height: number): FeedData {
return serialize({
action: 'best',
payload: height
});
}
public static addedNode(node: Node): FeedData {
return serialize({
action: 'added',
payload: [node.id, node.nodeInfo(), node.blockInfo()]
})
}
public static removedNode(node: Node): FeedData {
return serialize({
action: 'removed',
payload: node.id
});
}
public static imported(node: Node): FeedData {
return serialize({
action: 'imported',
payload: [node.id, node.blockInfo()]
});
}
public send(data: FeedData) {
this.socket.send(data);
}
private disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
}
+49
View File
@@ -0,0 +1,49 @@
import * as WebSocket from 'ws';
import * as express from 'express';
import { createServer } from 'http';
import Node from './node';
import Feed from './feed';
import Aggregator from './aggregator';
import { map, join } from '@dotstats/common';
const aggregator = new Aggregator;
const app = express();
const server = createServer(app);
// WebSocket for Nodes feeding telemetry data to the server
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
// WebSocket for web clients listening to the telemetry data aggregate
const telemetryFeed = new WebSocket.Server({ server });
app.get('/', function (req, res) {
function nodeInfo(node: Node) {
return `${node.name} | ${node.height} | Block time ${node.blockTime / 1000}s`;
}
res.send(
`<pre>
Best block: ${aggregator.height}
Node list:
${ join(map(aggregator.nodeList(), nodeInfo), '\n') }
</pre>`
);
});
incomingTelemetry.on('connection', async (socket: WebSocket) => {
try {
aggregator.addNode(await Node.fromSocket(socket));
} catch (err) {
console.error(err);
}
});
telemetryFeed.on('connection', (socket: WebSocket) => {
aggregator.addFeed(new Feed(socket));
});
console.log('Starting server on port 8080');
server.listen(8080);
+78
View File
@@ -0,0 +1,78 @@
import { Data } from 'ws';
import { Maybe, Opaque } from '@dotstats/common';
export function parseMessage(data: Data): Maybe<Message> {
try {
const message = JSON.parse(data.toString());
if (message && typeof message.msg === 'string' && typeof message.ts === 'string') {
message.ts = new Date(message.ts);
return message;
}
} catch (_) {
console.warn('Error parsing message JSON');
}
return null;
}
export function getBestBlock(message: Message): Maybe<BestBlock> {
switch (message.msg) {
case 'node.start':
case 'system.interval':
case 'block.import':
return message;
default:
return null;
}
}
interface MessageBase {
ts: Date,
level: 'INFO' | 'WARN',
}
export interface BestBlock {
best: string,
height: number,
ts: Date,
}
interface SystemConnected {
msg: 'system.connected',
name: string,
chain: string,
config: string,
implementation: string,
version: string,
}
interface SystemInterval extends BestBlock {
msg: 'system.interval',
txcount: number,
peers: number,
status: 'Idle' | string, // TODO: 'Idle' | ...?
}
interface NodeStart extends BestBlock {
msg: 'node.start',
}
interface BlockImport extends BestBlock {
msg: 'block.import',
}
// Union type
export type Message = MessageBase & (
SystemConnected |
SystemInterval |
NodeStart |
BlockImport
);
// received: {"msg":"block.import","level":"INFO","ts":"2018-06-18T17:30:35.285406538+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"node.start","level":"INFO","ts":"2018-06-18T17:30:40.038731057+02:00","best":"3d4fdc7960078ddc9be87dddc48324a6d64afdf1f65fffe89529ce9965cd5f29","height":526}
// received: {"msg":"system.connected","level":"INFO","ts":"2018-06-18T17:30:40.038975471+02:00","chain":"dev","config":"","version":"0.2.0","implementation":"parity-polkadot","name":"Majestic Widget"}
// received: {"msg":"system.interval","level":"INFO","ts":"2018-06-19T14:00:05.091355364+02:00","txcount":0,"best":"360c9563857308703398f637932b7ffe884e5c7b09692600ff09a4d753c9d948","height":7559,"peers":0,"status":"Idle"}
+179
View File
@@ -0,0 +1,179 @@
import * as WebSocket from 'ws';
import * as EventEmitter from 'events';
import { Maybe, Id, idGenerator } from '@dotstats/common';
import { parseMessage, getBestBlock, Message, BestBlock } from './message';
const BLOCK_TIME_HISTORY = 10;
const TIMEOUT = 1000 * 60 * 5; // 5 seconds
const nextId = idGenerator<Node>();
export interface NodeInfo {
name: string;
}
export interface BlockInfo {
height: number;
blockTime: number;
}
export default class Node extends EventEmitter {
public lastMessage: number;
public id: Id<Node>;
public name: string;
public implementation: string;
public version: string;
public height: number = 0;
public config: string;
public latency: number = 0;
public blockTime: number = 0;
private socket: WebSocket;
private blockTimes: Array<number> = new Array(BLOCK_TIME_HISTORY);
private lastBlockAt: Maybe<Date> = null;
constructor(socket: WebSocket, name: string, config: string, implentation: string, version: string) {
super();
this.lastMessage = Date.now();
this.id = nextId();
this.socket = socket;
this.name = name;
this.config = config;
this.implementation = implentation;
this.version = version;
console.log(`Listening to a new node: ${name}`);
socket.on('message', (data) => {
const message = parseMessage(data);
if (!message) return;
this.lastMessage = Date.now();
this.updateLatency(message.ts);
const update = getBestBlock(message);
if (update) {
this.updateBestBlock(update);
}
});
socket.on('close', () => {
console.log(`${this.name} has disconnected`);
this.disconnect();
});
socket.on('error', (error) => {
console.error(`${this.name} has errored`, error);
this.disconnect();
});
}
public static fromSocket(socket: WebSocket): Promise<Node> {
return new Promise((resolve, reject) => {
function cleanup() {
clearTimeout(timeout);
socket.removeAllListeners('message');
}
function handler(data: WebSocket.Data) {
const message = parseMessage(data);
if (message && message.msg === "system.connected") {
cleanup();
const { name, config, implementation, version } = message;
resolve(new Node(socket, name, config, implementation, version));
}
}
socket.on('message', handler);
const timeout = setTimeout(() => {
cleanup();
socket.close();
return reject(new Error('Timeout on waiting for system.connected message'));
}, 5000);
});
}
public timeoutCheck(now: number) {
if (this.lastMessage + TIMEOUT < now) {
this.disconnect();
}
}
public nodeInfo(): NodeInfo {
return {
name: this.name,
};
}
public blockInfo(): BlockInfo {
return {
height: this.height,
blockTime: this.blockTime,
};
}
public get average(): number {
let accounted = 0;
let sum = 0;
for (const time of this.blockTimes) {
if (time) {
accounted += 1;
sum += time;
}
}
if (accounted === 0) {
return 0;
}
return sum / accounted;
}
private disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.emit('disconnect');
}
private updateLatency(time: Date) {
this.latency = this.lastMessage - +time;
}
private updateBestBlock(update: BestBlock) {
const { height, ts: time, best } = update;
if (this.height < height) {
const blockTime = this.getBlockTime(time);
this.height = height;
this.lastBlockAt = time;
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
this.blockTime = blockTime;
this.emit('block');
}
}
private getBlockTime(time: Date): number {
if (!this.lastBlockAt) {
return 0;
}
return +time - +this.lastBlockAt;
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "build",
},
"include": [
"src/**/*.ts"
],
}
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Higher order function producing new auto-incremented `Id`s.
*/
function idGenerator() {
let current = 0;
return () => current++;
}
exports.idGenerator = idGenerator;
class IdSet {
constructor() {
this.map = new Map();
}
add(item) {
this.map.set(item.id, item);
}
remove(item) {
this.map.delete(item.id);
}
get entries() {
return this.map.values();
}
}
exports.IdSet = IdSet;
//# sourceMappingURL=id.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"id.js","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":";;AAOA;;GAEG;AACH;IACI,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO,GAAG,EAAE,CAAC,OAAO,EAAW,CAAC;AACpC,CAAC;AAJD,kCAIC;AAMD;IAAA;QACY,QAAG,GAAkB,IAAI,GAAG,EAAE,CAAC;IAa3C,CAAC;IAXU,GAAG,CAAC,IAAkB;QACzB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAEM,MAAM,CAAC,IAAkB;QAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED,IAAW,OAAO;QACd,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC7B,CAAC;CACJ;AAdD,sBAcC"}
+9
View File
@@ -0,0 +1,9 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./id"));
__export(require("./iterators"));
__export(require("./types"));
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,0BAAqB;AACrB,iCAA4B;AAC5B,6BAAwB"}
+57
View File
@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function* map(iter, fn) {
for (const item of iter) {
yield fn(item);
}
}
exports.map = map;
function* chain(a, b) {
yield* a;
yield* b;
}
exports.chain = chain;
function* zip(a, b) {
let itemA = a.next();
let itemB = b.next();
while (!itemA.done && !itemB.done) {
yield [itemA.value, itemB.value];
itemA = a.next();
itemB = b.next();
}
}
exports.zip = zip;
function* take(iter, n) {
for (const item of iter) {
if (n-- === 0) {
return;
}
yield item;
}
}
exports.take = take;
function skip(iter, n) {
while (n-- !== 0 && !iter.next().done) { }
return iter;
}
exports.skip = skip;
function reduce(iter, fn, accumulator) {
for (const item of iter) {
accumulator = fn(accumulator, item);
}
return accumulator;
}
exports.reduce = reduce;
function join(iter, glue) {
const first = iter.next();
if (first.done) {
return '';
}
let result = first.value.toString();
for (const item of iter) {
result += glue + item;
}
return result;
}
exports.join = join;
//# sourceMappingURL=iterators.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"iterators.js","sourceRoot":"","sources":["../src/iterators.ts"],"names":[],"mappings":";;AAAA,QAAe,CAAC,KAAW,IAAyB,EAAE,EAAkB;IACpE,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;KAClB;AACL,CAAC;AAJD,kBAIC;AAED,QAAe,CAAC,OAAU,CAAsB,EAAE,CAAsB;IACpE,KAAK,CAAC,CAAC,CAAC,CAAC;IACT,KAAK,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;AAHD,sBAGC;AAED,QAAe,CAAC,KAAW,CAAsB,EAAE,CAAsB;IACrE,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAErB,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAC/B,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAEjC,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;KACpB;AACL,CAAC;AAVD,kBAUC;AAED,QAAe,CAAC,MAAS,IAAyB,EAAE,CAAS;IACzD,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,IAAI,CAAC,EAAE,KAAK,CAAC,EAAE;YACX,OAAO;SACV;QAED,MAAM,IAAI,CAAC;KACd;AACL,CAAC;AARD,oBAQC;AAED,cAAwB,IAAyB,EAAE,CAAS;IACxD,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,GAAE;IAEzC,OAAO,IAAI,CAAC;AAChB,CAAC;AAJD,oBAIC;AAED,gBAA6B,IAAyB,EAAE,EAA2B,EAAE,WAAc;IAC/F,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,WAAW,GAAG,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;KACvC;IAED,OAAO,WAAW,CAAC;AACvB,CAAC;AAND,wBAMC;AAED,cAAqB,IAAkD,EAAE,IAAY;IACjF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAE1B,IAAI,KAAK,CAAC,IAAI,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IAED,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAEpC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;QACrB,MAAM,IAAI,IAAI,GAAG,IAAI,CAAC;KACzB;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAdD,oBAcC"}
+10
View File
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* PhantomData akin to Rust, because sometimes you need to be smarter than
* the compiler.
*/
class PhantomData {
}
exports.PhantomData = PhantomData;
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;AAAA;;;GAGG;AACH;CAAsD;AAAtD,kCAAsD"}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@dotstats/common",
"version": "0.1.0",
"author": "Parity Technologies Ltd. <admin@parity.io>",
"license": "GPL-3.0",
"description": "Shared utils and types for backend and frontend",
"main": "build/index.js",
"types": "src/index.ts",
"engines": {
"node": ">=9.5"
},
"scripts": {
"test": "echo \"Tests only available from root wrapper\""
}
}
+35
View File
@@ -0,0 +1,35 @@
import { Opaque } from './types';
/**
* Unique type-constrained Id number.
*/
export type Id<T> = Opaque<number, T>;
/**
* Higher order function producing new auto-incremented `Id`s.
*/
export function idGenerator<T>(): () => Id<T> {
let current = 0;
return () => current++ as Id<T>;
}
interface HasId<T> {
id: Id<T>;
}
export class IdSet<T> {
private map: Map<Id<T>, T> = new Map();
public add(item: T & HasId<T>) {
this.map.set(item.id, item);
}
public remove(item: T & HasId<T>) {
this.map.delete(item.id);
}
public get entries(): IterableIterator<T> {
return this.map.values();
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './id';
export * from './iterators';
export * from './types';
+62
View File
@@ -0,0 +1,62 @@
export function* map<T, U>(iter: IterableIterator<T>, fn: (item: T) => U): IterableIterator<U> {
for (const item of iter) {
yield fn(item);
}
}
export function* chain<T>(a: IterableIterator<T>, b: IterableIterator<T>): IterableIterator<T> {
yield* a;
yield* b;
}
export function* zip<T, U>(a: IterableIterator<T>, b: IterableIterator<U>): IterableIterator<[T, U]> {
let itemA = a.next();
let itemB = b.next();
while (!itemA.done && !itemB.done) {
yield [itemA.value, itemB.value];
itemA = a.next();
itemB = b.next();
}
}
export function* take<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> {
for (const item of iter) {
if (n-- === 0) {
return;
}
yield item;
}
}
export function skip<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> {
while (n-- !== 0 && !iter.next().done) {}
return iter;
}
export function reduce<T, R>(iter: IterableIterator<T>, fn: (accu: R, item: T) => R, accumulator: R): R {
for (const item of iter) {
accumulator = fn(accumulator, item);
}
return accumulator;
}
export function join(iter: IterableIterator<{ toString: () => string }>, glue: string): string {
const first = iter.next();
if (first.done) {
return '';
}
let result = first.value.toString();
for (const item of iter) {
result += glue + item;
}
return result;
}
+22
View File
@@ -0,0 +1,22 @@
/**
* PhantomData akin to Rust, because sometimes you need to be smarter than
* the compiler.
*/
export class PhantomData<P> { private __PHANTOM__: P }
/**
* Opaque type, similar to `opaque type` in Flow, or new types in Rust/C.
* These should be produced only by manually casting `t as Opaque<T, P>`.
*
* `P` can be anything as it's never actually used. Using strings is okay:
*
* ```
* type MyType = Opaque<number, 'MyType'>;
* ```
*/
export type Opaque<T, P> = T & PhantomData<P>;
/**
* Just a readable shorthand for null-ish-able types, akin to `T?` in Flow.
*/
export type Maybe<T> = T | null | undefined;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "build",
},
"include": [
"src/**/*.ts"
],
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
+1
View File
@@ -0,0 +1 @@
../../../../node_modules/react-scripts-ts/bin/react-scripts-ts.js
+1
View File
@@ -0,0 +1 @@
../../../../node_modules/typescript/bin/tsc
+1
View File
@@ -0,0 +1 @@
../../../../node_modules/typescript/bin/tsserver
+11718
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "@dotstats/frontend",
"version": "0.1.0",
"author": "Parity Technologies Ltd. <admin@parity.io>",
"license": "GPL-3.0",
"description": "Polkadot Telemetry frontend",
"dependencies": {
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-scripts-ts": "2.16.0"
},
"scripts": {
"start": "react-scripts-ts start",
"build": "react-scripts-ts build",
"test": "react-scripts-ts test --env=jsdom",
"eject": "react-scripts-ts eject"
},
"devDependencies": {
"@types/jest": "^23.0.2",
"@types/node": "^10.3.2",
"@types/react": "^16.3.17",
"@types/react-dom": "^16.0.6",
"typescript": "^2.9.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+28
View File
@@ -0,0 +1,28 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
+9
View File
@@ -0,0 +1,9 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
+129
View File
@@ -0,0 +1,129 @@
import * as React from 'react';
import './App.css';
import { Id } from '@dotstats/common';
export interface NodeInfo {
name: string;
}
export interface BlockInfo {
height: number;
blockTime: number;
}
interface BestBlock {
action: 'best';
payload: number;
}
interface AddedNode {
action: 'added';
payload: [Id<Node>, NodeInfo, BlockInfo];
}
interface RemovedNode {
action: 'removed';
payload: Id<Node>;
}
interface Imported {
action: 'imported';
payload: [Id<Node>, BlockInfo];
}
type Message = BestBlock | AddedNode | RemovedNode | Imported;
interface Node {
nodeInfo: NodeInfo,
blockInfo: BlockInfo,
}
interface State {
best: number,
nodes: Map<Id<Node>, Node>
}
export default class App extends React.Component<{}, State> {
public state: State = {
best: 0,
nodes: new Map()
};
constructor(props: {}) {
super(props);
const socket = new WebSocket(`ws://${window.location.hostname}:8080`);
socket.addEventListener('message', ({ data }) => {
this.onMessage(JSON.parse(data));
});
}
public render() {
return (
<div className="App">
<p>Best block: {this.state.best}</p>
<table>
<thead>
<tr>
<th>Name</th><th>Block</th><th>Block time</th>
</tr>
</thead>
<tbody>
{
this.nodes().map(([ id, node ]) => (
<tr key={id}>
<td>{node.nodeInfo.name}</td>
<td>{node.blockInfo.height}</td>
<td>{node.blockInfo.blockTime / 1000}s</td>
</tr>
))
}
</tbody>
</table>
</div>
);
}
private nodes(): Array<[Id<Node>, Node]> {
return Array.from(this.state.nodes.entries());
}
private onMessage(message: Message) {
const { nodes } = this.state;
switch (message.action) {
case 'best': {
this.setState({ best: message.payload });
}
return;
case 'added': {
const [id, nodeInfo, blockInfo] = message.payload;
const node = { nodeInfo, blockInfo };
nodes.set(id, node);
}
break;
case 'removed': {
nodes.delete(message.payload);
}
break;
case 'imported': {
const [id, blockInfo] = message.payload;
const node = nodes.get(id);
if (!node) {
return;
}
node.blockInfo = blockInfo;
}
break;
default:
return;
}
this.setState({ nodes });
}
}
+5
View File
@@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(
<App />,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();
@@ -0,0 +1,123 @@
// tslint:disable:no-console
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the 'N+1' visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString()
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log('Content is cached for offline use.');
}
}
};
}
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type')!.indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
+1
View File
@@ -0,0 +1 @@
../../shared
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build/dist",
"module": "esnext",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"rootDir": "src"
},
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
],
"include": [
"src/**/*.ts"
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts"
]
},
"rules": {
"no-console": false,
"interface-name": false,
"no-unused-variable": [true, {"ignore-pattern": "^_"}],
"no-empty": false
}
}