Reformatting

This commit is contained in:
maciejhirsz
2018-07-06 17:53:42 +02:00
parent 6fe1e47082
commit 538a30ccc3
20 changed files with 1005 additions and 1001 deletions
+4
View File
@@ -0,0 +1,4 @@
[*.{js,ts,tsx,json,css}]
charset = utf-8
indent_style = space
indent_size = 2
+61 -61
View File
@@ -5,80 +5,80 @@ import FeedSet from './FeedSet';
import { Types, FeedMessage } from '@dotstats/common'; import { Types, FeedMessage } from '@dotstats/common';
export default class Aggregator { export default class Aggregator {
private readonly chains = new Map<Types.ChainLabel, Chain>(); private readonly chains = new Map<Types.ChainLabel, Chain>();
private readonly feeds = new FeedSet(); private readonly feeds = new FeedSet();
constructor() { constructor() {
setInterval(() => this.timeoutCheck(), 10000); setInterval(() => this.timeoutCheck(), 10000);
}
public addNode(node: Node) {
let chain = this.getChain(node.chain);
chain.addNode(node);
}
public addFeed(feed: Feed) {
this.feeds.add(feed);
for (const chain of this.chains.values()) {
feed.sendMessage(Feed.addedChain(chain.label));
} }
public addNode(node: Node) { feed.events.on('subscribe', (label: Types.ChainLabel) => {
let chain = this.getChain(node.chain); const chain = this.chains.get(label);
chain.addNode(node); if (chain) {
} chain.addFeed(feed);
feed.sendMessage(Feed.subscribedTo(label));
}
});
public addFeed(feed: Feed) { feed.events.on('unsubscribe', (label: Types.ChainLabel) => {
this.feeds.add(feed); const chain = this.chains.get(label);
for (const chain of this.chains.values()) { if (chain) {
feed.sendMessage(Feed.addedChain(chain.label)); chain.removeFeed(feed);
feed.sendMessage(Feed.unsubscribedFrom(label));
}
});
}
private getChain(label: Types.ChainLabel): Chain {
const chain = this.chains.get(label);
if (chain) {
return chain;
} else {
const chain = new Chain(label);
chain.events.on('disconnect', (count: number) => {
if (count !== 0) {
return;
} }
feed.events.on('subscribe', (label: Types.ChainLabel) => { chain.events.removeAllListeners();
const chain = this.chains.get(label);
if (chain) { this.chains.delete(chain.label);
chain.addFeed(feed);
feed.sendMessage(Feed.subscribedTo(label));
}
})
feed.events.on('unsubscribe', (label: Types.ChainLabel) => { console.log(`Chain: ${label} lost all nodes`);
const chain = this.chains.get(label); this.feeds.broadcast(Feed.removedChain(label));
});
if (chain) { this.chains.set(label, chain);
chain.removeFeed(feed);
feed.sendMessage(Feed.unsubscribedFrom(label)); console.log(`New chain: ${label}`);
} this.feeds.broadcast(Feed.addedChain(label));
});
return chain;
} }
}
private getChain(label: Types.ChainLabel): Chain { private timeoutCheck() {
const chain = this.chains.get(label); const empty: Types.ChainLabel[] = [];
if (chain) { for (const chain of this.chains.values()) {
return chain; chain.timeoutCheck();
} else {
const chain = new Chain(label);
chain.events.on('disconnect', (count: number) => {
if (count !== 0) {
return;
}
chain.events.removeAllListeners();
this.chains.delete(chain.label);
console.log(`Chain: ${label} lost all nodes`);
this.feeds.broadcast(Feed.removedChain(label));
});
this.chains.set(label, chain);
console.log(`New chain: ${label}`);
this.feeds.broadcast(Feed.addedChain(label));
return chain;
}
}
private timeoutCheck() {
const empty: Types.ChainLabel[] = [];
for (const chain of this.chains.values()) {
chain.timeoutCheck();
}
} }
}
} }
+74 -74
View File
@@ -5,86 +5,86 @@ import FeedSet from './FeedSet';
import { timestamp, Types, FeedMessage } from '@dotstats/common'; import { timestamp, Types, FeedMessage } from '@dotstats/common';
export default class Chain { export default class Chain {
private nodes = new Set<Node>(); private nodes = new Set<Node>();
private feeds = new FeedSet(); private feeds = new FeedSet();
public readonly events = new EventEmitter(); public readonly events = new EventEmitter();
public readonly label: Types.ChainLabel; public readonly label: Types.ChainLabel;
public height = 0 as Types.BlockNumber; public height = 0 as Types.BlockNumber;
public blockTimestamp = 0 as Types.Timestamp; public blockTimestamp = 0 as Types.Timestamp;
constructor(label: Types.ChainLabel) { constructor(label: Types.ChainLabel) {
this.label = label; this.label = label;
}
public get nodeCount(): number {
return this.nodes.size;
}
public addNode(node: Node) {
console.log(`[${this.label}] new node: ${node.name}`);
this.nodes.add(node);
this.feeds.broadcast(Feed.addedNode(node));
node.events.once('disconnect', () => {
node.events.removeAllListeners();
this.nodes.delete(node);
this.feeds.broadcast(Feed.removedNode(node));
this.events.emit('disconnect', this.nodeCount);
});
node.events.on('block', () => this.updateBlock(node));
node.events.on('stats', () => this.feeds.broadcast(Feed.stats(node)));
}
public addFeed(feed: Feed) {
this.feeds.add(feed);
// TODO: this is a bit unclean, find a better way
feed.chain = this.label;
feed.sendMessage(Feed.timeSync());
feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp));
for (const node of this.nodes.values()) {
feed.sendMessage(Feed.addedNode(node));
}
}
public removeFeed(feed: Feed) {
this.feeds.remove(feed);
}
public nodeList(): IterableIterator<Node> {
return this.nodes.values();
}
public timeoutCheck() {
const now = timestamp();
for (const node of this.nodes.values()) {
node.timeoutCheck(now);
} }
public get nodeCount(): number { this.feeds.broadcast(Feed.timeSync());
return this.nodes.size; }
private updateBlock(node: Node) {
if (node.height > this.height) {
this.height = node.height;
this.blockTimestamp = node.blockTimestamp;
this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp));
console.log(`[${this.label}] New block ${this.height}`);
} }
public addNode(node: Node) { this.feeds.broadcast(Feed.imported(node));
console.log(`[${this.label}] new node: ${node.name}`);
this.nodes.add(node); console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
this.feeds.broadcast(Feed.addedNode(node)); }
node.events.once('disconnect', () => {
node.events.removeAllListeners();
this.nodes.delete(node);
this.feeds.broadcast(Feed.removedNode(node));
this.events.emit('disconnect', this.nodeCount);
});
node.events.on('block', () => this.updateBlock(node));
node.events.on('stats', () => this.feeds.broadcast(Feed.stats(node)));
}
public addFeed(feed: Feed) {
this.feeds.add(feed);
// TODO: this is a bit unclean, find a better way
feed.chain = this.label;
feed.sendMessage(Feed.timeSync());
feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp));
for (const node of this.nodes.values()) {
feed.sendMessage(Feed.addedNode(node));
}
}
public removeFeed(feed: Feed) {
this.feeds.remove(feed);
}
public nodeList(): IterableIterator<Node> {
return this.nodes.values();
}
public timeoutCheck() {
const now = timestamp();
for (const node of this.nodes.values()) {
node.timeoutCheck(now);
}
this.feeds.broadcast(Feed.timeSync());
}
private updateBlock(node: Node) {
if (node.height > this.height) {
this.height = node.height;
this.blockTimestamp = node.blockTimestamp;
this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp));
console.log(`[${this.label}] New block ${this.height}`);
}
this.feeds.broadcast(Feed.imported(node));
console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
}
} }
+111 -111
View File
@@ -7,130 +7,130 @@ const nextId = idGenerator<Types.FeedId>();
const { Actions } = FeedMessage; const { Actions } = FeedMessage;
export default class Feed { export default class Feed {
public id: Types.FeedId; public id: Types.FeedId;
public chain: Maybe<Types.ChainLabel> = null; public chain: Maybe<Types.ChainLabel> = null;
public readonly events = new EventEmitter(); public readonly events = new EventEmitter();
private socket: WebSocket; private socket: WebSocket;
private messages: Array<FeedMessage.Message> = []; private messages: Array<FeedMessage.Message> = [];
constructor(socket: WebSocket) { constructor(socket: WebSocket) {
this.id = nextId(); this.id = nextId();
this.socket = socket; this.socket = socket;
socket.on('message', (data) => this.handleCommand(data.toString())); socket.on('message', (data) => this.handleCommand(data.toString()));
socket.on('error', () => this.disconnect()); socket.on('error', () => this.disconnect());
socket.on('close', () => this.disconnect()); socket.on('close', () => this.disconnect());
}
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message {
return {
action: Actions.BestBlock,
payload: [height, ts]
};
}
public static addedNode(node: Node): FeedMessage.Message {
return {
action: Actions.AddedNode,
payload: [node.id, node.nodeDetails(), node.nodeStats(), node.blockDetails()]
};
}
public static removedNode(node: Node): FeedMessage.Message {
return {
action: Actions.RemovedNode,
payload: node.id
};
}
public static imported(node: Node): FeedMessage.Message {
return {
action: Actions.ImportedBlock,
payload: [node.id, node.blockDetails()]
};
}
public static stats(node: Node): FeedMessage.Message {
return {
action: Actions.NodeStats,
payload: [node.id, node.nodeStats()]
};
}
public static timeSync(): FeedMessage.Message {
return {
action: Actions.TimeSync,
payload: timestamp()
};
}
public static addedChain(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.AddedChain,
payload: label
};
}
public static removedChain(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.RemovedChain,
payload: label
} }
}
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message { public static subscribedTo(label: Types.ChainLabel): FeedMessage.Message {
return { return {
action: Actions.BestBlock, action: Actions.SubscribedTo,
payload: [height, ts] payload: label,
};
} }
}
public static addedNode(node: Node): FeedMessage.Message { public static unsubscribedFrom(label: Types.ChainLabel): FeedMessage.Message {
return { return {
action: Actions.AddedNode, action: Actions.UnsubscribedFrom,
payload: [node.id, node.nodeDetails(), node.nodeStats(), node.blockDetails()] payload: label,
};
} }
}
public static removedNode(node: Node): FeedMessage.Message { public sendData(data: FeedMessage.Data) {
return { this.socket.send(data);
action: Actions.RemovedNode, }
payload: node.id
}; public sendMessage(message: FeedMessage.Message) {
const queue = this.messages.length === 0;
this.messages.push(message);
if (queue) {
process.nextTick(this.sendMessages);
} }
}
public static imported(node: Node): FeedMessage.Message { private sendMessages = () => {
return { const data = FeedMessage.serialize(this.messages);
action: Actions.ImportedBlock, this.messages = [];
payload: [node.id, node.blockDetails()] this.socket.send(data);
}; }
private handleCommand(cmd: string) {
if (cmd.startsWith('subscribe:')) {
if (this.chain) {
this.events.emit('unsubscribe', this.chain);
this.chain = null;
}
const label = cmd.substr(10) as Types.ChainLabel;
this.events.emit('subscribe', label);
} }
}
public static stats(node: Node): FeedMessage.Message { private disconnect() {
return { this.socket.removeAllListeners();
action: Actions.NodeStats, this.socket.close();
payload: [node.id, node.nodeStats()]
};
}
public static timeSync(): FeedMessage.Message { this.events.emit('disconnect');
return { }
action: Actions.TimeSync,
payload: timestamp()
};
}
public static addedChain(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.AddedChain,
payload: label
};
}
public static removedChain(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.RemovedChain,
payload: label
}
}
public static subscribedTo(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.SubscribedTo,
payload: label,
}
}
public static unsubscribedFrom(label: Types.ChainLabel): FeedMessage.Message {
return {
action: Actions.UnsubscribedFrom,
payload: label,
}
}
public sendData(data: FeedMessage.Data) {
this.socket.send(data);
}
public sendMessage(message: FeedMessage.Message) {
const queue = this.messages.length === 0;
this.messages.push(message);
if (queue) {
process.nextTick(this.sendMessages);
}
}
private sendMessages = () => {
const data = FeedMessage.serialize(this.messages);
this.messages = [];
this.socket.send(data);
}
private handleCommand(cmd: string) {
if (cmd.startsWith('subscribe:')) {
if (this.chain) {
this.events.emit('unsubscribe', this.chain);
this.chain = null;
}
const label = cmd.substr(10) as Types.ChainLabel;
this.events.emit('subscribe', label);
}
}
private disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.events.emit('disconnect');
}
} }
+43 -43
View File
@@ -4,52 +4,52 @@ import { FeedMessage } from '@dotstats/common';
type DisconnectListener = () => void; type DisconnectListener = () => void;
export default class FeedSet { export default class FeedSet {
private feeds = new Map<Feed, DisconnectListener>(); private feeds = new Map<Feed, DisconnectListener>();
private messages: Array<FeedMessage.Message> = []; private messages: Array<FeedMessage.Message> = [];
public values(): IterableIterator<Feed> { public values(): IterableIterator<Feed> {
return this.feeds.keys(); return this.feeds.keys();
}
public each(fn: (feed: Feed) => void) {
for (const feed of this.values()) {
fn(feed);
}
}
public add(feed: Feed) {
const listener = () => this.remove(feed);
this.feeds.set(feed, listener);
feed.events.once('disconnect', listener);
}
public remove(feed: Feed) {
const listener = this.feeds.get(feed);
if (!listener) {
return;
} }
public each(fn: (feed: Feed) => void) { feed.events.removeListener('disconnect', listener);
for (const feed of this.values()) {
fn(feed); this.feeds.delete(feed);
} }
public broadcast(message: FeedMessage.Message) {
const queue = this.messages.length === 0;
this.messages.push(message);
if (queue) {
process.nextTick(this.sendMessages);
} }
}
public add(feed: Feed) { private sendMessages = () => {
const listener = () => this.remove(feed); const data = FeedMessage.serialize(this.messages);
this.messages = [];
this.feeds.set(feed, listener); this.each(feed => feed.sendData(data));
}
feed.events.once('disconnect', listener);
}
public remove(feed: Feed) {
const listener = this.feeds.get(feed);
if (!listener) {
return;
}
feed.events.removeListener('disconnect', listener);
this.feeds.delete(feed);
}
public broadcast(message: FeedMessage.Message) {
const queue = this.messages.length === 0;
this.messages.push(message);
if (queue) {
process.nextTick(this.sendMessages);
}
}
private sendMessages = () => {
const data = FeedMessage.serialize(this.messages);
this.messages = [];
this.each(feed => feed.sendData(data));
}
} }
+166 -166
View File
@@ -9,199 +9,199 @@ const TIMEOUT = (1000 * 60 * 1) as Types.Milliseconds; // 1 minute
const nextId = idGenerator<Types.NodeId>(); const nextId = idGenerator<Types.NodeId>();
export default class Node { export default class Node {
public readonly id: Types.NodeId; public readonly id: Types.NodeId;
public readonly name: Types.NodeName; public readonly name: Types.NodeName;
public readonly chain: Types.ChainLabel; public readonly chain: Types.ChainLabel;
public readonly implementation: Types.NodeImplementation; public readonly implementation: Types.NodeImplementation;
public readonly version: Types.NodeVersion; public readonly version: Types.NodeVersion;
public readonly events = new EventEmitter(); public readonly events = new EventEmitter();
public lastMessage: Types.Timestamp; public lastMessage: Types.Timestamp;
public config: string; public config: string;
public best = '' as Types.BlockHash; public best = '' as Types.BlockHash;
public height = 0 as Types.BlockNumber; public height = 0 as Types.BlockNumber;
public latency = 0 as Types.Milliseconds; public latency = 0 as Types.Milliseconds;
public blockTime = 0 as Types.Milliseconds; public blockTime = 0 as Types.Milliseconds;
public blockTimestamp = 0 as Types.Timestamp; public blockTimestamp = 0 as Types.Timestamp;
private peers = 0 as Types.PeerCount; private peers = 0 as Types.PeerCount;
private txcount = 0 as Types.TransactionCount; private txcount = 0 as Types.TransactionCount;
private readonly socket: WebSocket; private readonly socket: WebSocket;
private blockTimes: Array<number> = new Array(BLOCK_TIME_HISTORY); private blockTimes: Array<number> = new Array(BLOCK_TIME_HISTORY);
private lastBlockAt: Maybe<Date> = null; private lastBlockAt: Maybe<Date> = null;
constructor( constructor(
socket: WebSocket, socket: WebSocket,
name: Types.NodeName, name: Types.NodeName,
chain: Types.ChainLabel, chain: Types.ChainLabel,
config: string, config: string,
implentation: Types.NodeImplementation, implentation: Types.NodeImplementation,
version: Types.NodeVersion, version: Types.NodeVersion,
) { ) {
this.id = nextId(); this.id = nextId();
this.name = name; this.name = name;
this.chain = chain; this.chain = chain;
this.config = config; this.config = config;
this.implementation = implentation; this.implementation = implentation;
this.version = version; this.version = version;
this.lastMessage = timestamp(); this.lastMessage = timestamp();
this.socket = socket; this.socket = socket;
socket.on('message', (data) => { socket.on('message', (data) => {
const message = parseMessage(data); const message = parseMessage(data);
if (!message) { if (!message) {
return; return;
} }
this.lastMessage = timestamp(); this.lastMessage = timestamp();
this.updateLatency(message.ts); this.updateLatency(message.ts);
const update = getBestBlock(message); const update = getBestBlock(message);
if (update) { if (update) {
this.updateBestBlock(update); this.updateBestBlock(update);
} }
if (message.msg === 'system.interval') { if (message.msg === 'system.interval') {
this.onSystemInterval(message); this.onSystemInterval(message);
} }
}); });
socket.on('close', () => { socket.on('close', () => {
console.log(`${this.name} has disconnected`); console.log(`${this.name} has disconnected`);
this.disconnect(); this.disconnect();
}); });
socket.on('error', (error) => { socket.on('error', (error) => {
console.error(`${this.name} has errored`, error); console.error(`${this.name} has errored`, error);
this.disconnect(); this.disconnect();
}); });
} }
public static fromSocket(socket: WebSocket): Promise<Node> { public static fromSocket(socket: WebSocket): Promise<Node> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
function cleanup() { function cleanup() {
clearTimeout(timeout); clearTimeout(timeout);
socket.removeAllListeners('message'); socket.removeAllListeners('message');
} }
function handler(data: WebSocket.Data) { function handler(data: WebSocket.Data) {
const message = parseMessage(data); const message = parseMessage(data);
if (message && message.msg === "system.connected") { if (message && message.msg === "system.connected") {
cleanup(); cleanup();
const { name, chain, config, implementation, version } = message; const { name, chain, config, implementation, version } = message;
resolve(new Node(socket, name, chain, config, implementation, version)); resolve(new Node(socket, name, chain, 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: Types.Timestamp) {
if (this.lastMessage + TIMEOUT < now) {
this.disconnect();
} }
}
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: Types.Timestamp) {
if (this.lastMessage + TIMEOUT < now) {
this.disconnect();
}
}
public nodeDetails(): Types.NodeDetails {
return [this.name, this.implementation, this.version];
}
public nodeStats(): Types.NodeStats {
return [this.peers, this.txcount];
}
public blockDetails(): Types.BlockDetails {
return [this.height, this.best, this.blockTime, this.blockTimestamp];
}
public get average(): number {
let accounted = 0;
let sum = 0;
for (const time of this.blockTimes) {
if (time) {
accounted += 1;
sum += time;
}
} }
public nodeDetails(): Types.NodeDetails { if (accounted === 0) {
return [this.name, this.implementation, this.version]; return 0;
} }
public nodeStats(): Types.NodeStats { return sum / accounted;
return [this.peers, this.txcount]; }
public get localBlockAt(): Types.Milliseconds {
if (!this.lastBlockAt) {
return 0 as Types.Milliseconds;
} }
public blockDetails(): Types.BlockDetails { return +(this.lastBlockAt || 0) as Types.Milliseconds;
return [this.height, this.best, this.blockTime, this.blockTimestamp]; }
private disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.events.emit('disconnect');
}
private onSystemInterval(message: SystemInterval) {
const { peers, txcount } = message;
if (this.peers !== peers || this.txcount !== txcount) {
this.peers = peers;
this.txcount = txcount;
this.events.emit('stats');
}
}
private updateLatency(time: Date) {
this.latency = (this.lastMessage - +time) as Types.Milliseconds;
}
private updateBestBlock(update: BestBlock) {
const { height, ts: time, best } = update;
if (this.height < height) {
const blockTime = this.getBlockTime(time);
this.best = best;
this.height = height;
this.blockTimestamp = timestamp();
this.lastBlockAt = time;
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
this.blockTime = blockTime;
this.events.emit('block');
}
}
private getBlockTime(time: Date): Types.Milliseconds {
if (!this.lastBlockAt) {
return 0 as Types.Milliseconds;
} }
public get average(): number { return (+time - +this.lastBlockAt) as Types.Milliseconds;
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;
}
public get localBlockAt(): Types.Milliseconds {
if (!this.lastBlockAt) {
return 0 as Types.Milliseconds;
}
return +(this.lastBlockAt || 0) as Types.Milliseconds;
}
private disconnect() {
this.socket.removeAllListeners();
this.socket.close();
this.events.emit('disconnect');
}
private onSystemInterval(message: SystemInterval) {
const { peers, txcount } = message;
if (this.peers !== peers || this.txcount !== txcount) {
this.peers = peers;
this.txcount = txcount;
this.events.emit('stats');
}
}
private updateLatency(time: Date) {
this.latency = (this.lastMessage - +time) as Types.Milliseconds;
}
private updateBestBlock(update: BestBlock) {
const { height, ts: time, best } = update;
if (this.height < height) {
const blockTime = this.getBlockTime(time);
this.best = best;
this.height = height;
this.blockTimestamp = timestamp();
this.lastBlockAt = time;
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
this.blockTime = blockTime;
this.events.emit('block');
}
}
private getBlockTime(time: Date): Types.Milliseconds {
if (!this.lastBlockAt) {
return 0 as Types.Milliseconds;
}
return (+time - +this.lastBlockAt) as Types.Milliseconds;
}
} }
+7 -7
View File
@@ -15,16 +15,16 @@ console.log('Telemetry server listening on port 1024');
console.log('Feed server listening on port 8080'); console.log('Feed server listening on port 8080');
incomingTelemetry.on('connection', async (socket: WebSocket) => { incomingTelemetry.on('connection', async (socket: WebSocket) => {
try { try {
const node = await Node.fromSocket(socket); const node = await Node.fromSocket(socket);
aggregator.addNode(node); aggregator.addNode(node);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}); });
telemetryFeed.on('connection', (socket: WebSocket) => { telemetryFeed.on('connection', (socket: WebSocket) => {
aggregator.addFeed(new Feed(socket)); aggregator.addFeed(new Feed(socket));
}); });
+38 -38
View File
@@ -2,73 +2,73 @@ import { Data } from 'ws';
import { Maybe, Types } from '@dotstats/common'; import { Maybe, Types } from '@dotstats/common';
export function parseMessage(data: Data): Maybe<Message> { export function parseMessage(data: Data): Maybe<Message> {
try { try {
const message = JSON.parse(data.toString()); const message = JSON.parse(data.toString());
if (message && typeof message.msg === 'string' && typeof message.ts === 'string') { if (message && typeof message.msg === 'string' && typeof message.ts === 'string') {
message.ts = new Date(message.ts); message.ts = new Date(message.ts);
return message; return message;
}
} catch (_) {
console.warn('Error parsing message JSON');
} }
} catch (_) {
console.warn('Error parsing message JSON');
}
return null; return null;
} }
export function getBestBlock(message: Message): Maybe<BestBlock> { export function getBestBlock(message: Message): Maybe<BestBlock> {
switch (message.msg) { switch (message.msg) {
case 'node.start': case 'node.start':
case 'system.interval': case 'system.interval':
case 'block.import': case 'block.import':
return message; return message;
default: default:
return null; return null;
} }
} }
interface MessageBase { interface MessageBase {
ts: Date, ts: Date,
level: 'INFO' | 'WARN', level: 'INFO' | 'WARN',
} }
export interface BestBlock { export interface BestBlock {
best: Types.BlockHash, best: Types.BlockHash,
height: Types.BlockNumber, height: Types.BlockNumber,
ts: Date, ts: Date,
} }
interface SystemConnected { interface SystemConnected {
msg: 'system.connected', msg: 'system.connected',
name: Types.NodeName, name: Types.NodeName,
chain: Types.ChainLabel, chain: Types.ChainLabel,
config: string, config: string,
implementation: Types.NodeImplementation, implementation: Types.NodeImplementation,
version: Types.NodeVersion, version: Types.NodeVersion,
} }
export interface SystemInterval extends BestBlock { export interface SystemInterval extends BestBlock {
msg: 'system.interval', msg: 'system.interval',
txcount: Types.TransactionCount, txcount: Types.TransactionCount,
peers: Types.PeerCount, peers: Types.PeerCount,
status: 'Idle' | string, // TODO: 'Idle' | ...? status: 'Idle' | string, // TODO: 'Idle' | ...?
} }
interface NodeStart extends BestBlock { interface NodeStart extends BestBlock {
msg: 'node.start', msg: 'node.start',
} }
interface BlockImport extends BestBlock { interface BlockImport extends BestBlock {
msg: 'block.import', msg: 'block.import',
} }
// Union type // Union type
export type Message = MessageBase & ( export type Message = MessageBase & (
SystemConnected | SystemConnected |
SystemInterval | SystemInterval |
NodeStart | NodeStart |
BlockImport BlockImport
); );
+81 -81
View File
@@ -2,88 +2,88 @@ import { Opaque } from './helpers';
import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails, Timestamp, ChainLabel } from './types'; import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails, Timestamp, ChainLabel } from './types';
export const Actions = { export const Actions = {
BestBlock: 0 as 0, BestBlock: 0 as 0,
AddedNode: 1 as 1, AddedNode: 1 as 1,
RemovedNode: 2 as 2, RemovedNode: 2 as 2,
ImportedBlock: 3 as 3, ImportedBlock: 3 as 3,
NodeStats: 4 as 4, NodeStats: 4 as 4,
TimeSync: 5 as 5, TimeSync: 5 as 5,
AddedChain: 6 as 6, AddedChain: 6 as 6,
RemovedChain: 7 as 7, RemovedChain: 7 as 7,
SubscribedTo: 8 as 8, SubscribedTo: 8 as 8,
UnsubscribedFrom: 9 as 9 UnsubscribedFrom: 9 as 9
}; };
export type Action = typeof Actions[keyof typeof Actions]; export type Action = typeof Actions[keyof typeof Actions];
export type Payload = Message['payload']; export type Payload = Message['payload'];
export namespace Variants { export namespace Variants {
export interface MessageBase { export interface MessageBase {
action: Action; action: Action;
} }
export interface BestBlockMessage extends MessageBase { export interface BestBlockMessage extends MessageBase {
action: typeof Actions.BestBlock; action: typeof Actions.BestBlock;
payload: [BlockNumber, Timestamp]; payload: [BlockNumber, Timestamp];
} }
export interface AddedNodeMessage extends MessageBase { export interface AddedNodeMessage extends MessageBase {
action: typeof Actions.AddedNode; action: typeof Actions.AddedNode;
payload: [NodeId, NodeDetails, NodeStats, BlockDetails]; payload: [NodeId, NodeDetails, NodeStats, BlockDetails];
} }
export interface RemovedNodeMessage extends MessageBase { export interface RemovedNodeMessage extends MessageBase {
action: typeof Actions.RemovedNode; action: typeof Actions.RemovedNode;
payload: NodeId; payload: NodeId;
} }
export interface ImportedBlockMessage extends MessageBase { export interface ImportedBlockMessage extends MessageBase {
action: typeof Actions.ImportedBlock; action: typeof Actions.ImportedBlock;
payload: [NodeId, BlockDetails]; payload: [NodeId, BlockDetails];
} }
export interface NodeStatsMessage extends MessageBase { export interface NodeStatsMessage extends MessageBase {
action: typeof Actions.NodeStats; action: typeof Actions.NodeStats;
payload: [NodeId, NodeStats]; payload: [NodeId, NodeStats];
} }
export interface TimeSyncMessage extends MessageBase { export interface TimeSyncMessage extends MessageBase {
action: typeof Actions.TimeSync; action: typeof Actions.TimeSync;
payload: Timestamp; payload: Timestamp;
} }
export interface AddedChainMessage extends MessageBase { export interface AddedChainMessage extends MessageBase {
action: typeof Actions.AddedChain; action: typeof Actions.AddedChain;
payload: ChainLabel; payload: ChainLabel;
} }
export interface RemovedChainMessage extends MessageBase { export interface RemovedChainMessage extends MessageBase {
action: typeof Actions.RemovedChain; action: typeof Actions.RemovedChain;
payload: ChainLabel; payload: ChainLabel;
} }
export interface SubscribedToMessage extends MessageBase { export interface SubscribedToMessage extends MessageBase {
action: typeof Actions.SubscribedTo; action: typeof Actions.SubscribedTo;
payload: ChainLabel; payload: ChainLabel;
} }
export interface UnsubscribedFromMessage extends MessageBase { export interface UnsubscribedFromMessage extends MessageBase {
action: typeof Actions.UnsubscribedFrom; action: typeof Actions.UnsubscribedFrom;
payload: ChainLabel; payload: ChainLabel;
} }
} }
export type Message = export type Message =
| Variants.BestBlockMessage | Variants.BestBlockMessage
| Variants.AddedNodeMessage | Variants.AddedNodeMessage
| Variants.RemovedNodeMessage | Variants.RemovedNodeMessage
| Variants.ImportedBlockMessage | Variants.ImportedBlockMessage
| Variants.NodeStatsMessage | Variants.NodeStatsMessage
| Variants.TimeSyncMessage | Variants.TimeSyncMessage
| Variants.AddedChainMessage | Variants.AddedChainMessage
| Variants.RemovedChainMessage | Variants.RemovedChainMessage
| Variants.SubscribedToMessage | Variants.SubscribedToMessage
| Variants.UnsubscribedFromMessage; | Variants.UnsubscribedFromMessage;
/** /**
* Opaque data type to be sent to the feed. Passing through * Opaque data type to be sent to the feed. Passing through
@@ -100,36 +100,36 @@ export type Data = Opaque<string, 'FeedMessage.Data'>;
* Action `string`s are converted to opcodes using the `actionToCode` mapping. * Action `string`s are converted to opcodes using the `actionToCode` mapping.
*/ */
export function serialize(messages: Array<Message>): Data { export function serialize(messages: Array<Message>): Data {
const squashed = new Array(messages.length * 2); const squashed = new Array(messages.length * 2);
let index = 0; let index = 0;
messages.forEach((message) => { messages.forEach((message) => {
const { action, payload } = message; const { action, payload } = message;
squashed[index++] = action; squashed[index++] = action;
squashed[index++] = payload; squashed[index++] = payload;
}) })
return JSON.stringify(squashed) as Data; return JSON.stringify(squashed) as Data;
} }
/** /**
* Deserialize data to an array of `Message`s. * Deserialize data to an array of `Message`s.
*/ */
export function deserialize(data: Data): Array<Message> { export function deserialize(data: Data): Array<Message> {
const json: Array<Action | Payload> = JSON.parse(data); const json: Array<Action | Payload> = JSON.parse(data);
if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) { if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) {
throw new Error('Invalid FeedMessage.Data'); throw new Error('Invalid FeedMessage.Data');
} }
const messages: Array<Message> = new Array(json.length / 2); const messages: Array<Message> = new Array(json.length / 2);
for (const index of messages.keys()) { for (const index of messages.keys()) {
const [ action, payload ] = json.slice(index * 2); const [ action, payload ] = json.slice(index * 2);
messages[index] = { action, payload } as Message; messages[index] = { action, payload } as Message;
} }
return messages; return messages;
} }
+3 -3
View File
@@ -27,9 +27,9 @@ export type Maybe<T> = T | null | undefined;
* Asynchronous sleep * Asynchronous sleep
*/ */
export function sleep(time: Milliseconds): Promise<void> { export function sleep(time: Milliseconds): Promise<void> {
return new Promise<void>((resolve, _reject) => { return new Promise<void>((resolve, _reject) => {
setTimeout(() => resolve(), time); setTimeout(() => resolve(), time);
}); });
} }
export const timestamp = Date.now as () => Timestamp; export const timestamp = Date.now as () => Timestamp;
+33 -33
View File
@@ -1,62 +1,62 @@
export function* map<T, U>(iter: IterableIterator<T>, fn: (item: T) => U): IterableIterator<U> { export function* map<T, U>(iter: IterableIterator<T>, fn: (item: T) => U): IterableIterator<U> {
for (const item of iter) { for (const item of iter) {
yield fn(item); yield fn(item);
} }
} }
export function* chain<T>(a: IterableIterator<T>, b: IterableIterator<T>): IterableIterator<T> { export function* chain<T>(a: IterableIterator<T>, b: IterableIterator<T>): IterableIterator<T> {
yield* a; yield* a;
yield* b; yield* b;
} }
export function* zip<T, U>(a: IterableIterator<T>, b: IterableIterator<U>): IterableIterator<[T, U]> { export function* zip<T, U>(a: IterableIterator<T>, b: IterableIterator<U>): IterableIterator<[T, U]> {
let itemA = a.next(); let itemA = a.next();
let itemB = b.next(); let itemB = b.next();
while (!itemA.done && !itemB.done) { while (!itemA.done && !itemB.done) {
yield [itemA.value, itemB.value]; yield [itemA.value, itemB.value];
itemA = a.next(); itemA = a.next();
itemB = b.next(); itemB = b.next();
} }
} }
export function* take<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> { export function* take<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> {
for (const item of iter) { for (const item of iter) {
if (n-- === 0) { if (n-- === 0) {
return; return;
}
yield item;
} }
yield item;
}
} }
export function skip<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> { export function skip<T>(iter: IterableIterator<T>, n: number): IterableIterator<T> {
while (n-- !== 0 && !iter.next().done) {} while (n-- !== 0 && !iter.next().done) {}
return iter; return iter;
} }
export function reduce<T, R>(iter: IterableIterator<T>, fn: (accu: R, item: T) => R, accumulator: R): R { export function reduce<T, R>(iter: IterableIterator<T>, fn: (accu: R, item: T) => R, accumulator: R): R {
for (const item of iter) { for (const item of iter) {
accumulator = fn(accumulator, item); accumulator = fn(accumulator, item);
} }
return accumulator; return accumulator;
} }
export function join(iter: IterableIterator<{ toString: () => string }>, glue: string): string { export function join(iter: IterableIterator<{ toString: () => string }>, glue: string): string {
const first = iter.next(); const first = iter.next();
if (first.done) { if (first.done) {
return ''; return '';
} }
let result = first.value.toString(); let result = first.value.toString();
for (const item of iter) { for (const item of iter) {
result += glue + item; result += glue + item;
} }
return result; return result;
} }
+53 -53
View File
@@ -16,65 +16,65 @@ import blockTimeIcon from './icons/history.svg';
import lastTimeIcon from './icons/watch.svg'; import lastTimeIcon from './icons/watch.svg';
export default class App extends React.Component<{}, State> { export default class App extends React.Component<{}, State> {
public state: State = { public state: State = {
best: 0 as Types.BlockNumber, best: 0 as Types.BlockNumber,
blockTimestamp: 0 as Types.Timestamp, blockTimestamp: 0 as Types.Timestamp,
timeDiff: 0 as Types.Milliseconds, timeDiff: 0 as Types.Milliseconds,
subscribed: null, subscribed: null,
chains: new Set(), chains: new Set(),
nodes: new Map() nodes: new Map()
}; };
private connection: Promise<Connection>; private connection: Promise<Connection>;
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
this.connection = Connection.create((changes) => { this.connection = Connection.create((changes) => {
if (changes) { if (changes) {
this.setState(changes); this.setState(changes);
} }
return this.state; return this.state;
}); });
} }
public render() { public render() {
const { best, blockTimestamp, timeDiff, chains, subscribed } = this.state; const { best, blockTimestamp, timeDiff, chains, subscribed } = this.state;
Ago.timeDiff = timeDiff; Ago.timeDiff = timeDiff;
return ( return (
<div className="App"> <div className="App">
<Chains chains={chains} subscribed={subscribed} connection={this.connection} /> <Chains chains={chains} subscribed={subscribed} connection={this.connection} />
<div className="App-header"> <div className="App-header">
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile> <Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile> <Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
</div> </div>
<table className="App-list"> <table className="App-list">
<thead> <thead>
<tr> <tr>
<th><Icon src={nodeIcon} alt="Node" /></th> <th><Icon src={nodeIcon} alt="Node" /></th>
<th><Icon src={nodeTypeIcon} alt="Implementation" /></th> <th><Icon src={nodeTypeIcon} alt="Implementation" /></th>
<th><Icon src={peersIcon} alt="Peer Count" /></th> <th><Icon src={peersIcon} alt="Peer Count" /></th>
<th><Icon src={transactionsIcon} alt="Transactions in Queue" /></th> <th><Icon src={transactionsIcon} alt="Transactions in Queue" /></th>
<th><Icon src={blockIcon} alt="Block" /></th> <th><Icon src={blockIcon} alt="Block" /></th>
<th><Icon src={blockHashIcon} alt="Block Hash" /></th> <th><Icon src={blockHashIcon} alt="Block Hash" /></th>
<th><Icon src={blockTimeIcon} alt="Block Time" /></th> <th><Icon src={blockTimeIcon} alt="Block Time" /></th>
<th><Icon src={lastTimeIcon} alt="Last Block Time" /></th> <th><Icon src={lastTimeIcon} alt="Last Block Time" /></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
this.nodes().map((props) => <Node key={props.id} {...props} />) this.nodes().map((props) => <Node key={props.id} {...props} />)
} }
</tbody> </tbody>
</table> </table>
</div> </div>
); );
} }
private nodes(): Node.Props[] { private nodes(): Node.Props[] {
return Array.from(this.state.nodes.values()).sort((a, b) => b.blockDetails[0] - a.blockDetails[0]); return Array.from(this.state.nodes.values()).sort((a, b) => b.blockDetails[0] - a.blockDetails[0]);
} }
} }
+50 -50
View File
@@ -3,77 +3,77 @@ import './Tile.css';
import { timestamp, Types } from '@dotstats/common'; import { timestamp, Types } from '@dotstats/common';
export namespace Ago { export namespace Ago {
export interface Props { export interface Props {
when: Types.Timestamp, when: Types.Timestamp,
} }
export interface State { export interface State {
now: Types.Timestamp, now: Types.Timestamp,
} }
} }
const tickers = new Map<Ago, (ts: Types.Timestamp) => void>(); const tickers = new Map<Ago, (ts: Types.Timestamp) => void>();
function tick() { function tick() {
const now = timestamp(); const now = timestamp();
for (const ticker of tickers.values()) { for (const ticker of tickers.values()) {
ticker(now); ticker(now);
} }
setTimeout(tick, 100); setTimeout(tick, 100);
} }
tick(); tick();
export namespace Ago { export namespace Ago {
export interface State { export interface State {
now: Types.Timestamp now: Types.Timestamp
} }
} }
export class Ago extends React.Component<Ago.Props, Ago.State> { export class Ago extends React.Component<Ago.Props, Ago.State> {
public static timeDiff = 0 as Types.Milliseconds; public static timeDiff = 0 as Types.Milliseconds;
public state: Ago.State; public state: Ago.State;
constructor(props: Ago.Props) { constructor(props: Ago.Props) {
super(props); super(props);
this.state = { this.state = {
now: (timestamp() + Ago.timeDiff) as Types.Timestamp now: (timestamp() + Ago.timeDiff) as Types.Timestamp
}; };
}
public componentWillMount() {
tickers.set(this, (now) => {
this.setState({
now: (now + Ago.timeDiff) as Types.Timestamp
});
})
}
public componentWillUnmount() {
tickers.delete(this);
}
public render() {
if (this.props.when === 0) {
return <span>-</span>;
} }
public componentWillMount() { const ago = Math.max(this.state.now - this.props.when, 0) / 1000;
tickers.set(this, (now) => {
this.setState({ let agoStr: string;
now: (now + Ago.timeDiff) as Types.Timestamp
}); if (ago < 10) {
}) agoStr = `${ago.toFixed(1)}s`;
} else if (ago < 60) {
agoStr = `${ago | 0}s`;
} else {
agoStr = `${ ago / 60 | 0}m`;
} }
public componentWillUnmount() { return <span title={new Date(this.props.when).toUTCString()}>{agoStr} ago</span>
tickers.delete(this); }
}
public render() {
if (this.props.when === 0) {
return <span>-</span>;
}
const ago = Math.max(this.state.now - this.props.when, 0) / 1000;
let agoStr: string;
if (ago < 10) {
agoStr = `${ago.toFixed(1)}s`;
} else if (ago < 60) {
agoStr = `${ago | 0}s`;
} else {
agoStr = `${ ago / 60 | 0}m`;
}
return <span title={new Date(this.props.when).toUTCString()}>{agoStr} ago</span>
}
} }
+32 -32
View File
@@ -7,44 +7,44 @@ import chainIcon from '../icons/link.svg';
import './Chains.css'; import './Chains.css';
export namespace Chains { export namespace Chains {
export interface Props { export interface Props {
chains: Set<Types.ChainLabel>, chains: Set<Types.ChainLabel>,
subscribed: Maybe<Types.ChainLabel>, subscribed: Maybe<Types.ChainLabel>,
connection: Promise<Connection> connection: Promise<Connection>
} }
} }
export class Chains extends React.Component<Chains.Props, {}> { export class Chains extends React.Component<Chains.Props, {}> {
public render() { public render() {
return ( return (
<div className="Chains"> <div className="Chains">
<Icon src={chainIcon} alt="Observed chain" /> <Icon src={chainIcon} alt="Observed chain" />
{ {
this.chains.map((chain) => this.renderChain(chain)) this.chains.map((chain) => this.renderChain(chain))
} }
</div> </div>
); );
} }
private renderChain(chain: Types.ChainLabel): React.ReactNode { private renderChain(chain: Types.ChainLabel): React.ReactNode {
const className = chain === this.props.subscribed const className = chain === this.props.subscribed
? 'Chains-chain Chains-chain-selected' ? 'Chains-chain Chains-chain-selected'
: 'Chains-chain'; : 'Chains-chain';
return ( return (
<a key={chain} className={className} onClick={this.subscribe.bind(this, chain)}> <a key={chain} className={className} onClick={this.subscribe.bind(this, chain)}>
{chain} {chain}
</a> </a>
) )
} }
private get chains(): Types.ChainLabel[] { private get chains(): Types.ChainLabel[] {
return Array.from(this.props.chains); return Array.from(this.props.chains);
} }
private async subscribe(chain: Types.ChainLabel) { private async subscribe(chain: Types.ChainLabel) {
const connection = await this.props.connection; const connection = await this.props.connection;
connection.subscribe(chain); connection.subscribe(chain);
} }
} }
+11 -11
View File
@@ -3,21 +3,21 @@ import ReactSVG from 'react-svg';
import './Icon.css'; import './Icon.css';
export interface Props { export interface Props {
src: string, src: string,
alt: string, alt: string,
className?: string, className?: string,
}; };
export class Icon extends React.Component<{}, Props> { export class Icon extends React.Component<{}, Props> {
public props: Props; public props: Props;
public shouldComponentUpdate() { public shouldComponentUpdate() {
return false; return false;
} }
public render() { public render() {
const { alt, className, src } = this.props; const { alt, className, src } = this.props;
return <ReactSVG title={alt} className={`Icon ${ className || '' }`} path={src} />; return <ReactSVG title={alt} className={`Icon ${ className || '' }`} path={src} />;
} }
} }
+21 -21
View File
@@ -4,29 +4,29 @@ import { Ago } from './Ago';
import { Types } from '@dotstats/common'; import { Types } from '@dotstats/common';
export namespace Node { export namespace Node {
export interface Props { export interface Props {
id: Types.NodeId, id: Types.NodeId,
nodeDetails: Types.NodeDetails, nodeDetails: Types.NodeDetails,
nodeStats: Types.NodeStats, nodeStats: Types.NodeStats,
blockDetails: Types.BlockDetails, blockDetails: Types.BlockDetails,
} }
} }
export function Node(props: Node.Props) { export function Node(props: Node.Props) {
const [name, implementation, version] = props.nodeDetails; const [name, implementation, version] = props.nodeDetails;
const [height, hash, blockTime, blockTimestamp] = props.blockDetails; const [height, hash, blockTime, blockTimestamp] = props.blockDetails;
const [peers, txcount] = props.nodeStats; const [peers, txcount] = props.nodeStats;
return ( return (
<tr> <tr>
<td>{name}</td> <td>{name}</td>
<td>{implementation} v{version}</td> <td>{implementation} v{version}</td>
<td>{peers}</td> <td>{peers}</td>
<td>{txcount}</td> <td>{txcount}</td>
<td>#{formatNumber(height)}</td> <td>#{formatNumber(height)}</td>
<td><span title={hash}>{trimHash(hash, 16)}</span></td> <td><span title={hash}>{trimHash(hash, 16)}</span></td>
<td>{(blockTime / 1000).toFixed(3)}s</td> <td>{(blockTime / 1000).toFixed(3)}s</td>
<td><Ago when={blockTimestamp} /></td> <td><Ago when={blockTimestamp} /></td>
</tr> </tr>
); );
} }
+12 -12
View File
@@ -3,19 +3,19 @@ import './Tile.css';
import { Icon } from './Icon'; import { Icon } from './Icon';
export namespace Tile { export namespace Tile {
export interface Props { export interface Props {
title: string, title: string,
icon: string, icon: string,
children?: React.ReactNode, children?: React.ReactNode,
} }
} }
export function Tile(props: Tile.Props) { export function Tile(props: Tile.Props) {
return ( return (
<div className="Tile"> <div className="Tile">
<Icon src={props.icon} alt={props.title} /> <Icon src={props.icon} alt={props.title} />
<span className="Tile-label">{props.title}</span> <span className="Tile-label">{props.title}</span>
<span className="Tile-content">{props.children}</span> <span className="Tile-content">{props.children}</span>
</div> </div>
); );
} }
+185 -185
View File
@@ -7,205 +7,205 @@ const TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
export class Connection { export class Connection {
public static async create(update: Update): Promise<Connection> { public static async create(update: Update): Promise<Connection> {
return new Connection(await Connection.socket(), update); return new Connection(await Connection.socket(), update);
}
private static readonly address = `ws://${window.location.hostname}:8080`;
private static async socket(): Promise<WebSocket> {
let socket = await Connection.trySocket();
let timeout = TIMEOUT_BASE;
while (!socket) {
await sleep(timeout);
timeout = Math.max(timeout * 2, TIMEOUT_MAX) as Types.Milliseconds;
socket = await Connection.trySocket();
} }
private static readonly address = `ws://${window.location.hostname}:8080`; return socket;
}
private static async socket(): Promise<WebSocket> { private static async trySocket(): Promise<Maybe<WebSocket>> {
let socket = await Connection.trySocket(); return new Promise<Maybe<WebSocket>>((resolve, _) => {
let timeout = TIMEOUT_BASE; function clean() {
socket.removeEventListener('open', onSuccess);
socket.removeEventListener('close', onFailure);
socket.removeEventListener('error', onFailure);
}
while (!socket) { function onSuccess() {
await sleep(timeout); clean();
resolve(socket);
}
timeout = Math.max(timeout * 2, TIMEOUT_MAX) as Types.Milliseconds; function onFailure() {
socket = await Connection.trySocket(); clean();
resolve(null);
}
const socket = new WebSocket(Connection.address);
socket.addEventListener('open', onSuccess);
socket.addEventListener('error', onFailure);
socket.addEventListener('close', onFailure);
});
}
private socket: WebSocket;
private state: Readonly<State>;
private readonly update: Update;
constructor(socket: WebSocket, update: Update) {
this.socket = socket;
this.update = update;
this.bindSocket();
}
public subscribe(chain: Types.ChainLabel) {
this.socket.send(`subscribe:${chain}`);
}
private bindSocket() {
this.state = this.update({ nodes: new Map() });
this.socket.addEventListener('message', this.handleMessages);
this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect);
}
private clean() {
this.socket.removeEventListener('message', this.handleMessages);
this.socket.removeEventListener('close', this.handleDisconnect);
this.socket.removeEventListener('error', this.handleDisconnect);
}
private handleMessages = (event: MessageEvent) => {
const data = event.data as FeedMessage.Data;
const nodes = this.state.nodes;
const chains = this.state.chains;
const changes = { nodes, chains };
messages: for (const message of FeedMessage.deserialize(data)) {
switch (message.action) {
case Actions.BestBlock: {
const [best, blockTimestamp] = message.payload;
this.state = this.update({ best, blockTimestamp });
continue messages;
} }
return socket; case Actions.AddedNode: {
} const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
const node = { id, nodeDetails, nodeStats, blockDetails };
private static async trySocket(): Promise<Maybe<WebSocket>> { nodes.set(id, node);
return new Promise<Maybe<WebSocket>>((resolve, _) => {
function clean() {
socket.removeEventListener('open', onSuccess);
socket.removeEventListener('close', onFailure);
socket.removeEventListener('error', onFailure);
}
function onSuccess() { break;
clean();
resolve(socket);
}
function onFailure() {
clean();
resolve(null);
}
const socket = new WebSocket(Connection.address);
socket.addEventListener('open', onSuccess);
socket.addEventListener('error', onFailure);
socket.addEventListener('close', onFailure);
});
}
private socket: WebSocket;
private state: Readonly<State>;
private readonly update: Update;
constructor(socket: WebSocket, update: Update) {
this.socket = socket;
this.update = update;
this.bindSocket();
}
public subscribe(chain: Types.ChainLabel) {
this.socket.send(`subscribe:${chain}`);
}
private bindSocket() {
this.state = this.update({ nodes: new Map() });
this.socket.addEventListener('message', this.handleMessages);
this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect);
}
private clean() {
this.socket.removeEventListener('message', this.handleMessages);
this.socket.removeEventListener('close', this.handleDisconnect);
this.socket.removeEventListener('error', this.handleDisconnect);
}
private handleMessages = (event: MessageEvent) => {
const data = event.data as FeedMessage.Data;
const nodes = this.state.nodes;
const chains = this.state.chains;
const changes = { nodes, chains };
messages: for (const message of FeedMessage.deserialize(data)) {
switch (message.action) {
case Actions.BestBlock: {
const [best, blockTimestamp] = message.payload;
this.state = this.update({ best, blockTimestamp });
continue messages;
}
case Actions.AddedNode: {
const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
const node = { id, nodeDetails, nodeStats, blockDetails };
nodes.set(id, node);
break;
}
case Actions.RemovedNode: {
nodes.delete(message.payload);
break;
}
case Actions.ImportedBlock: {
const [id, blockDetails] = message.payload;
const node = nodes.get(id);
if (!node) {
return;
}
node.blockDetails = blockDetails;
break;
}
case Actions.NodeStats: {
const [id, nodeStats] = message.payload;
const node = nodes.get(id);
if (!node) {
return;
}
node.nodeStats = nodeStats;
break;
}
case Actions.TimeSync: {
this.state = this.update({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds
});
continue messages;
}
case Actions.AddedChain: {
chains.add(message.payload);
this.autoSubscribe();
break;
}
case Actions.RemovedChain: {
chains.delete(message.payload);
if (this.state.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes, chains });
this.autoSubscribe();
continue messages;
}
break;
}
case Actions.SubscribedTo: {
this.state = this.update({ subscribed: message.payload });
continue messages;
}
case Actions.UnsubscribedFrom: {
if (this.state.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes });
}
continue messages;
}
default: {
continue messages;
}
}
} }
this.state = this.update(changes); case Actions.RemovedNode: {
} nodes.delete(message.payload);
private autoSubscribe() { break;
const { subscribed, chains } = this.state;
if (subscribed == null && chains.size) {
const first = chains.values().next().value;
this.subscribe(first);
} }
case Actions.ImportedBlock: {
const [id, blockDetails] = message.payload;
const node = nodes.get(id);
if (!node) {
return;
}
node.blockDetails = blockDetails;
break;
}
case Actions.NodeStats: {
const [id, nodeStats] = message.payload;
const node = nodes.get(id);
if (!node) {
return;
}
node.nodeStats = nodeStats;
break;
}
case Actions.TimeSync: {
this.state = this.update({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds
});
continue messages;
}
case Actions.AddedChain: {
chains.add(message.payload);
this.autoSubscribe();
break;
}
case Actions.RemovedChain: {
chains.delete(message.payload);
if (this.state.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes, chains });
this.autoSubscribe();
continue messages;
}
break;
}
case Actions.SubscribedTo: {
this.state = this.update({ subscribed: message.payload });
continue messages;
}
case Actions.UnsubscribedFrom: {
if (this.state.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes });
}
continue messages;
}
default: {
continue messages;
}
}
} }
private handleDisconnect = async () => { this.state = this.update(changes);
this.clean(); }
this.socket.close();
this.socket = await Connection.socket(); private autoSubscribe() {
this.bindSocket(); const { subscribed, chains } = this.state;
if (subscribed == null && chains.size) {
const first = chains.values().next().value;
this.subscribe(first);
} }
}
private handleDisconnect = async () => {
this.clean();
this.socket.close();
this.socket = await Connection.socket();
this.bindSocket();
}
} }
+6 -6
View File
@@ -2,12 +2,12 @@ import { Node } from './components/Node';
import { Types, Maybe } from '@dotstats/common'; import { Types, Maybe } from '@dotstats/common';
export interface State { export interface State {
best: Types.BlockNumber, best: Types.BlockNumber,
blockTimestamp: Types.Timestamp, blockTimestamp: Types.Timestamp,
timeDiff: Types.Milliseconds, timeDiff: Types.Milliseconds,
subscribed: Maybe<Types.ChainLabel>, subscribed: Maybe<Types.ChainLabel>,
chains: Set<Types.ChainLabel>, chains: Set<Types.ChainLabel>,
nodes: Map<Types.NodeId, Node.Props>, nodes: Map<Types.NodeId, Node.Props>,
} }
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>; export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
+14 -14
View File
@@ -1,25 +1,25 @@
export function formatNumber(num: number): string { export function formatNumber(num: number): string {
const input = num.toString(); const input = num.toString();
let output = ''; let output = '';
let length = input.length; let length = input.length;
while (length > 3) { while (length > 3) {
output = ',' + input.substr(length - 3, 3) + output; output = ',' + input.substr(length - 3, 3) + output;
length -= 3; length -= 3;
} }
output = input.substr(0, length) + output; output = input.substr(0, length) + output;
return output; return output;
} }
export function trimHash(hash: string, length: number): string { export function trimHash(hash: string, length: number): string {
if (hash.length < length) { if (hash.length < length) {
return hash; return hash;
} }
const side = ((length - 2) / 2) | 0; const side = ((length - 2) / 2) | 0;
return hash.substr(0, side) + '..' + hash.substr(-side, side); return hash.substr(0, side) + '..' + hash.substr(-side, side);
} }