mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-05-30 21:41:07 +00:00
Reformatting
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
[*.{js,ts,tsx,json,css}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user