From 538a30ccc3c8905f7edd776eface24173e403f25 Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Fri, 6 Jul 2018 17:53:42 +0200 Subject: [PATCH] Reformatting --- .editorconfig | 4 + packages/backend/src/Aggregator.ts | 122 +++---- packages/backend/src/Chain.ts | 148 ++++---- packages/backend/src/Feed.ts | 222 ++++++------ packages/backend/src/FeedSet.ts | 86 ++--- packages/backend/src/Node.ts | 332 +++++++++--------- packages/backend/src/index.ts | 14 +- packages/backend/src/message.ts | 76 ++-- packages/common/src/feed.ts | 162 ++++----- packages/common/src/helpers.ts | 6 +- packages/common/src/iterators.ts | 66 ++-- packages/frontend/src/App.tsx | 106 +++--- packages/frontend/src/components/Ago.tsx | 100 +++--- packages/frontend/src/components/Chains.tsx | 64 ++-- packages/frontend/src/components/Icon.tsx | 22 +- packages/frontend/src/components/Node.tsx | 42 +-- packages/frontend/src/components/Tile.tsx | 24 +- packages/frontend/src/message.ts | 370 ++++++++++---------- packages/frontend/src/state.ts | 12 +- packages/frontend/src/utils.ts | 28 +- 20 files changed, 1005 insertions(+), 1001 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..20b8a32 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{js,ts,tsx,json,css}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/packages/backend/src/Aggregator.ts b/packages/backend/src/Aggregator.ts index 00c4e4d..048dd89 100644 --- a/packages/backend/src/Aggregator.ts +++ b/packages/backend/src/Aggregator.ts @@ -5,80 +5,80 @@ import FeedSet from './FeedSet'; import { Types, FeedMessage } from '@dotstats/common'; export default class Aggregator { - private readonly chains = new Map(); - private readonly feeds = new FeedSet(); + private readonly chains = new Map(); + private readonly feeds = new FeedSet(); - constructor() { - setInterval(() => this.timeoutCheck(), 10000); + constructor() { + 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) { - let chain = this.getChain(node.chain); + feed.events.on('subscribe', (label: Types.ChainLabel) => { + const chain = this.chains.get(label); - chain.addNode(node); - } + if (chain) { + chain.addFeed(feed); + feed.sendMessage(Feed.subscribedTo(label)); + } + }); - public addFeed(feed: Feed) { - this.feeds.add(feed); + feed.events.on('unsubscribe', (label: Types.ChainLabel) => { + const chain = this.chains.get(label); - for (const chain of this.chains.values()) { - feed.sendMessage(Feed.addedChain(chain.label)); + if (chain) { + 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) => { - const chain = this.chains.get(label); + chain.events.removeAllListeners(); - if (chain) { - chain.addFeed(feed); - feed.sendMessage(Feed.subscribedTo(label)); - } - }) + this.chains.delete(chain.label); - feed.events.on('unsubscribe', (label: Types.ChainLabel) => { - const chain = this.chains.get(label); + console.log(`Chain: ${label} lost all nodes`); + this.feeds.broadcast(Feed.removedChain(label)); + }); - if (chain) { - chain.removeFeed(feed); - feed.sendMessage(Feed.unsubscribedFrom(label)); - } - }); + this.chains.set(label, chain); + + console.log(`New chain: ${label}`); + this.feeds.broadcast(Feed.addedChain(label)); + + return chain; } + } - private getChain(label: Types.ChainLabel): Chain { - const chain = this.chains.get(label); + private timeoutCheck() { + const empty: Types.ChainLabel[] = []; - if (chain) { - return chain; - } 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(); - } + for (const chain of this.chains.values()) { + chain.timeoutCheck(); } + } } diff --git a/packages/backend/src/Chain.ts b/packages/backend/src/Chain.ts index 460253d..00a8c13 100644 --- a/packages/backend/src/Chain.ts +++ b/packages/backend/src/Chain.ts @@ -5,86 +5,86 @@ import FeedSet from './FeedSet'; import { timestamp, Types, FeedMessage } from '@dotstats/common'; export default class Chain { - private nodes = new Set(); - private feeds = new FeedSet(); + private nodes = new Set(); + private feeds = new FeedSet(); - public readonly events = new EventEmitter(); - public readonly label: Types.ChainLabel; + public readonly events = new EventEmitter(); + public readonly label: Types.ChainLabel; - public height = 0 as Types.BlockNumber; - public blockTimestamp = 0 as Types.Timestamp; + public height = 0 as Types.BlockNumber; + public blockTimestamp = 0 as Types.Timestamp; - constructor(label: Types.ChainLabel) { - this.label = label; + constructor(label: Types.ChainLabel) { + 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 { + return this.nodes.values(); + } + + public timeoutCheck() { + const now = timestamp(); + + for (const node of this.nodes.values()) { + node.timeoutCheck(now); } - public get nodeCount(): number { - return this.nodes.size; + 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}`); } - public addNode(node: Node) { - console.log(`[${this.label}] new node: ${node.name}`); + this.feeds.broadcast(Feed.imported(node)); - 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 { - 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}`); - } + console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`); + } } diff --git a/packages/backend/src/Feed.ts b/packages/backend/src/Feed.ts index e55b38d..bd6848d 100644 --- a/packages/backend/src/Feed.ts +++ b/packages/backend/src/Feed.ts @@ -7,130 +7,130 @@ const nextId = idGenerator(); const { Actions } = FeedMessage; export default class Feed { - public id: Types.FeedId; + public id: Types.FeedId; - public chain: Maybe = null; - public readonly events = new EventEmitter(); + public chain: Maybe = null; + public readonly events = new EventEmitter(); - private socket: WebSocket; - private messages: Array = []; + private socket: WebSocket; + private messages: Array = []; - constructor(socket: WebSocket) { - this.id = nextId(); - this.socket = socket; + constructor(socket: WebSocket) { + this.id = nextId(); + this.socket = socket; - socket.on('message', (data) => this.handleCommand(data.toString())); - socket.on('error', () => this.disconnect()); - socket.on('close', () => this.disconnect()); + socket.on('message', (data) => this.handleCommand(data.toString())); + socket.on('error', () => 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 { - return { - action: Actions.BestBlock, - payload: [height, ts] - }; + public static subscribedTo(label: Types.ChainLabel): FeedMessage.Message { + return { + action: Actions.SubscribedTo, + payload: label, } + } - public static addedNode(node: Node): FeedMessage.Message { - return { - action: Actions.AddedNode, - payload: [node.id, node.nodeDetails(), node.nodeStats(), node.blockDetails()] - }; + public static unsubscribedFrom(label: Types.ChainLabel): FeedMessage.Message { + return { + action: Actions.UnsubscribedFrom, + payload: label, } + } - public static removedNode(node: Node): FeedMessage.Message { - return { - action: Actions.RemovedNode, - payload: node.id - }; + 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); } + } - public static imported(node: Node): FeedMessage.Message { - return { - action: Actions.ImportedBlock, - payload: [node.id, node.blockDetails()] - }; + 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); } + } - public static stats(node: Node): FeedMessage.Message { - return { - action: Actions.NodeStats, - payload: [node.id, node.nodeStats()] - }; - } + private disconnect() { + this.socket.removeAllListeners(); + this.socket.close(); - 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 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'); - } + this.events.emit('disconnect'); + } } diff --git a/packages/backend/src/FeedSet.ts b/packages/backend/src/FeedSet.ts index 8c48348..5d14a97 100644 --- a/packages/backend/src/FeedSet.ts +++ b/packages/backend/src/FeedSet.ts @@ -4,52 +4,52 @@ import { FeedMessage } from '@dotstats/common'; type DisconnectListener = () => void; export default class FeedSet { - private feeds = new Map(); - private messages: Array = []; + private feeds = new Map(); + private messages: Array = []; - public values(): IterableIterator { - return this.feeds.keys(); + public values(): IterableIterator { + 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) { - for (const feed of this.values()) { - fn(feed); - } + 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); } + } - 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; - } - - 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)); - } + private sendMessages = () => { + const data = FeedMessage.serialize(this.messages); + this.messages = []; + this.each(feed => feed.sendData(data)); + } } diff --git a/packages/backend/src/Node.ts b/packages/backend/src/Node.ts index 0df1c5d..144c0d2 100644 --- a/packages/backend/src/Node.ts +++ b/packages/backend/src/Node.ts @@ -9,199 +9,199 @@ const TIMEOUT = (1000 * 60 * 1) as Types.Milliseconds; // 1 minute const nextId = idGenerator(); export default class Node { - public readonly id: Types.NodeId; - public readonly name: Types.NodeName; - public readonly chain: Types.ChainLabel; - public readonly implementation: Types.NodeImplementation; - public readonly version: Types.NodeVersion; + public readonly id: Types.NodeId; + public readonly name: Types.NodeName; + public readonly chain: Types.ChainLabel; + public readonly implementation: Types.NodeImplementation; + public readonly version: Types.NodeVersion; - public readonly events = new EventEmitter(); + public readonly events = new EventEmitter(); - public lastMessage: Types.Timestamp; - public config: string; - public best = '' as Types.BlockHash; - public height = 0 as Types.BlockNumber; - public latency = 0 as Types.Milliseconds; - public blockTime = 0 as Types.Milliseconds; - public blockTimestamp = 0 as Types.Timestamp; + public lastMessage: Types.Timestamp; + public config: string; + public best = '' as Types.BlockHash; + public height = 0 as Types.BlockNumber; + public latency = 0 as Types.Milliseconds; + public blockTime = 0 as Types.Milliseconds; + public blockTimestamp = 0 as Types.Timestamp; - private peers = 0 as Types.PeerCount; - private txcount = 0 as Types.TransactionCount; + private peers = 0 as Types.PeerCount; + private txcount = 0 as Types.TransactionCount; - private readonly socket: WebSocket; - private blockTimes: Array = new Array(BLOCK_TIME_HISTORY); - private lastBlockAt: Maybe = null; + private readonly socket: WebSocket; + private blockTimes: Array = new Array(BLOCK_TIME_HISTORY); + private lastBlockAt: Maybe = null; - constructor( - socket: WebSocket, - name: Types.NodeName, - chain: Types.ChainLabel, - config: string, - implentation: Types.NodeImplementation, - version: Types.NodeVersion, - ) { - this.id = nextId(); - this.name = name; - this.chain = chain; - this.config = config; - this.implementation = implentation; - this.version = version; - this.lastMessage = timestamp(); - this.socket = socket; + constructor( + socket: WebSocket, + name: Types.NodeName, + chain: Types.ChainLabel, + config: string, + implentation: Types.NodeImplementation, + version: Types.NodeVersion, + ) { + this.id = nextId(); + this.name = name; + this.chain = chain; + this.config = config; + this.implementation = implentation; + this.version = version; + this.lastMessage = timestamp(); + this.socket = socket; - socket.on('message', (data) => { - const message = parseMessage(data); + socket.on('message', (data) => { + const message = parseMessage(data); - if (!message) { - return; - } + if (!message) { + return; + } - this.lastMessage = timestamp(); - this.updateLatency(message.ts); + this.lastMessage = timestamp(); + this.updateLatency(message.ts); - const update = getBestBlock(message); + const update = getBestBlock(message); - if (update) { - this.updateBestBlock(update); - } + if (update) { + this.updateBestBlock(update); + } - if (message.msg === 'system.interval') { - this.onSystemInterval(message); - } - }); + if (message.msg === 'system.interval') { + this.onSystemInterval(message); + } + }); - socket.on('close', () => { - console.log(`${this.name} has disconnected`); + socket.on('close', () => { + console.log(`${this.name} has disconnected`); - this.disconnect(); - }); + this.disconnect(); + }); - socket.on('error', (error) => { - console.error(`${this.name} has errored`, error); + socket.on('error', (error) => { + console.error(`${this.name} has errored`, error); - this.disconnect(); - }); - } + this.disconnect(); + }); + } - public static fromSocket(socket: WebSocket): Promise { - return new Promise((resolve, reject) => { - function cleanup() { - clearTimeout(timeout); - socket.removeAllListeners('message'); - } + public static fromSocket(socket: WebSocket): Promise { + return new Promise((resolve, reject) => { + function cleanup() { + clearTimeout(timeout); + socket.removeAllListeners('message'); + } - function handler(data: WebSocket.Data) { - const message = parseMessage(data); + function handler(data: WebSocket.Data) { + const message = parseMessage(data); - if (message && message.msg === "system.connected") { - cleanup(); + if (message && message.msg === "system.connected") { + cleanup(); - const { name, chain, config, implementation, version } = message; + const { name, chain, config, implementation, version } = message; - 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(); + 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(); + } + } + + 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 { - return [this.name, this.implementation, this.version]; + if (accounted === 0) { + return 0; } - public nodeStats(): Types.NodeStats { - return [this.peers, this.txcount]; + return sum / accounted; + } + + public get localBlockAt(): Types.Milliseconds { + if (!this.lastBlockAt) { + return 0 as Types.Milliseconds; } - public blockDetails(): Types.BlockDetails { - return [this.height, this.best, this.blockTime, this.blockTimestamp]; + 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; } - public get average(): number { - let accounted = 0; - let sum = 0; - - for (const time of this.blockTimes) { - if (time) { - accounted += 1; - sum += time; - } - } - - if (accounted === 0) { - return 0; - } - - return sum / accounted; - } - - 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; - } + return (+time - +this.lastBlockAt) as Types.Milliseconds; + } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4496294..5a5cc8b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,16 +15,16 @@ console.log('Telemetry server listening on port 1024'); console.log('Feed server listening on port 8080'); incomingTelemetry.on('connection', async (socket: WebSocket) => { - try { - const node = await Node.fromSocket(socket); + try { + const node = await Node.fromSocket(socket); - aggregator.addNode(node); - } catch (err) { - console.error(err); - } + aggregator.addNode(node); + } catch (err) { + console.error(err); + } }); telemetryFeed.on('connection', (socket: WebSocket) => { - aggregator.addFeed(new Feed(socket)); + aggregator.addFeed(new Feed(socket)); }); diff --git a/packages/backend/src/message.ts b/packages/backend/src/message.ts index 7a24172..d15d84e 100644 --- a/packages/backend/src/message.ts +++ b/packages/backend/src/message.ts @@ -2,73 +2,73 @@ import { Data } from 'ws'; import { Maybe, Types } from '@dotstats/common'; export function parseMessage(data: Data): Maybe { - try { - const message = JSON.parse(data.toString()); + try { + const message = JSON.parse(data.toString()); - if (message && typeof message.msg === 'string' && typeof message.ts === 'string') { - message.ts = new Date(message.ts); + if (message && typeof message.msg === 'string' && typeof message.ts === 'string') { + message.ts = new Date(message.ts); - return message; - } - } catch (_) { - console.warn('Error parsing message JSON'); + return message; } + } catch (_) { + console.warn('Error parsing message JSON'); + } - return null; + return null; } export function getBestBlock(message: Message): Maybe { - switch (message.msg) { - case 'node.start': - case 'system.interval': - case 'block.import': - return message; - default: - return null; - } + switch (message.msg) { + case 'node.start': + case 'system.interval': + case 'block.import': + return message; + default: + return null; + } } interface MessageBase { - ts: Date, - level: 'INFO' | 'WARN', + ts: Date, + level: 'INFO' | 'WARN', } export interface BestBlock { - best: Types.BlockHash, - height: Types.BlockNumber, - ts: Date, + best: Types.BlockHash, + height: Types.BlockNumber, + ts: Date, } interface SystemConnected { - msg: 'system.connected', - name: Types.NodeName, - chain: Types.ChainLabel, - config: string, - implementation: Types.NodeImplementation, - version: Types.NodeVersion, + msg: 'system.connected', + name: Types.NodeName, + chain: Types.ChainLabel, + config: string, + implementation: Types.NodeImplementation, + version: Types.NodeVersion, } export interface SystemInterval extends BestBlock { - msg: 'system.interval', - txcount: Types.TransactionCount, - peers: Types.PeerCount, - status: 'Idle' | string, // TODO: 'Idle' | ...? + msg: 'system.interval', + txcount: Types.TransactionCount, + peers: Types.PeerCount, + status: 'Idle' | string, // TODO: 'Idle' | ...? } interface NodeStart extends BestBlock { - msg: 'node.start', + msg: 'node.start', } interface BlockImport extends BestBlock { - msg: 'block.import', + msg: 'block.import', } // Union type export type Message = MessageBase & ( - SystemConnected | - SystemInterval | - NodeStart | - BlockImport + SystemConnected | + SystemInterval | + NodeStart | + BlockImport ); diff --git a/packages/common/src/feed.ts b/packages/common/src/feed.ts index ff162b9..32d37a7 100644 --- a/packages/common/src/feed.ts +++ b/packages/common/src/feed.ts @@ -2,88 +2,88 @@ import { Opaque } from './helpers'; import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails, Timestamp, ChainLabel } from './types'; export const Actions = { - BestBlock: 0 as 0, - AddedNode: 1 as 1, - RemovedNode: 2 as 2, - ImportedBlock: 3 as 3, - NodeStats: 4 as 4, - TimeSync: 5 as 5, - AddedChain: 6 as 6, - RemovedChain: 7 as 7, - SubscribedTo: 8 as 8, - UnsubscribedFrom: 9 as 9 + BestBlock: 0 as 0, + AddedNode: 1 as 1, + RemovedNode: 2 as 2, + ImportedBlock: 3 as 3, + NodeStats: 4 as 4, + TimeSync: 5 as 5, + AddedChain: 6 as 6, + RemovedChain: 7 as 7, + SubscribedTo: 8 as 8, + UnsubscribedFrom: 9 as 9 }; export type Action = typeof Actions[keyof typeof Actions]; export type Payload = Message['payload']; export namespace Variants { - export interface MessageBase { - action: Action; - } + export interface MessageBase { + action: Action; + } - export interface BestBlockMessage extends MessageBase { - action: typeof Actions.BestBlock; - payload: [BlockNumber, Timestamp]; - } + export interface BestBlockMessage extends MessageBase { + action: typeof Actions.BestBlock; + payload: [BlockNumber, Timestamp]; + } - export interface AddedNodeMessage extends MessageBase { - action: typeof Actions.AddedNode; - payload: [NodeId, NodeDetails, NodeStats, BlockDetails]; - } + export interface AddedNodeMessage extends MessageBase { + action: typeof Actions.AddedNode; + payload: [NodeId, NodeDetails, NodeStats, BlockDetails]; + } - export interface RemovedNodeMessage extends MessageBase { - action: typeof Actions.RemovedNode; - payload: NodeId; - } + export interface RemovedNodeMessage extends MessageBase { + action: typeof Actions.RemovedNode; + payload: NodeId; + } - export interface ImportedBlockMessage extends MessageBase { - action: typeof Actions.ImportedBlock; - payload: [NodeId, BlockDetails]; - } + export interface ImportedBlockMessage extends MessageBase { + action: typeof Actions.ImportedBlock; + payload: [NodeId, BlockDetails]; + } - export interface NodeStatsMessage extends MessageBase { - action: typeof Actions.NodeStats; - payload: [NodeId, NodeStats]; - } + export interface NodeStatsMessage extends MessageBase { + action: typeof Actions.NodeStats; + payload: [NodeId, NodeStats]; + } - export interface TimeSyncMessage extends MessageBase { - action: typeof Actions.TimeSync; - payload: Timestamp; - } + export interface TimeSyncMessage extends MessageBase { + action: typeof Actions.TimeSync; + payload: Timestamp; + } - export interface AddedChainMessage extends MessageBase { - action: typeof Actions.AddedChain; - payload: ChainLabel; - } + export interface AddedChainMessage extends MessageBase { + action: typeof Actions.AddedChain; + payload: ChainLabel; + } - export interface RemovedChainMessage extends MessageBase { - action: typeof Actions.RemovedChain; - payload: ChainLabel; - } + export interface RemovedChainMessage extends MessageBase { + action: typeof Actions.RemovedChain; + payload: ChainLabel; + } - export interface SubscribedToMessage extends MessageBase { - action: typeof Actions.SubscribedTo; - payload: ChainLabel; - } + export interface SubscribedToMessage extends MessageBase { + action: typeof Actions.SubscribedTo; + payload: ChainLabel; + } - export interface UnsubscribedFromMessage extends MessageBase { - action: typeof Actions.UnsubscribedFrom; - payload: ChainLabel; - } + export interface UnsubscribedFromMessage extends MessageBase { + action: typeof Actions.UnsubscribedFrom; + payload: ChainLabel; + } } export type Message = - | Variants.BestBlockMessage - | Variants.AddedNodeMessage - | Variants.RemovedNodeMessage - | Variants.ImportedBlockMessage - | Variants.NodeStatsMessage - | Variants.TimeSyncMessage - | Variants.AddedChainMessage - | Variants.RemovedChainMessage - | Variants.SubscribedToMessage - | Variants.UnsubscribedFromMessage; + | Variants.BestBlockMessage + | Variants.AddedNodeMessage + | Variants.RemovedNodeMessage + | Variants.ImportedBlockMessage + | Variants.NodeStatsMessage + | Variants.TimeSyncMessage + | Variants.AddedChainMessage + | Variants.RemovedChainMessage + | Variants.SubscribedToMessage + | Variants.UnsubscribedFromMessage; /** * Opaque data type to be sent to the feed. Passing through @@ -100,36 +100,36 @@ export type Data = Opaque; * Action `string`s are converted to opcodes using the `actionToCode` mapping. */ export function serialize(messages: Array): Data { - const squashed = new Array(messages.length * 2); - let index = 0; + const squashed = new Array(messages.length * 2); + let index = 0; - messages.forEach((message) => { - const { action, payload } = message; + messages.forEach((message) => { + const { action, payload } = message; - squashed[index++] = action; - squashed[index++] = payload; - }) + squashed[index++] = action; + squashed[index++] = payload; + }) - return JSON.stringify(squashed) as Data; + return JSON.stringify(squashed) as Data; } /** * Deserialize data to an array of `Message`s. */ export function deserialize(data: Data): Array { - const json: Array = JSON.parse(data); + const json: Array = JSON.parse(data); - if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) { - throw new Error('Invalid FeedMessage.Data'); - } + if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) { + throw new Error('Invalid FeedMessage.Data'); + } - const messages: Array = new Array(json.length / 2); + const messages: Array = new Array(json.length / 2); - for (const index of messages.keys()) { - const [ action, payload ] = json.slice(index * 2); + for (const index of messages.keys()) { + const [ action, payload ] = json.slice(index * 2); - messages[index] = { action, payload } as Message; - } + messages[index] = { action, payload } as Message; + } - return messages; + return messages; } diff --git a/packages/common/src/helpers.ts b/packages/common/src/helpers.ts index ff46191..4a90ca3 100644 --- a/packages/common/src/helpers.ts +++ b/packages/common/src/helpers.ts @@ -27,9 +27,9 @@ export type Maybe = T | null | undefined; * Asynchronous sleep */ export function sleep(time: Milliseconds): Promise { - return new Promise((resolve, _reject) => { - setTimeout(() => resolve(), time); - }); + return new Promise((resolve, _reject) => { + setTimeout(() => resolve(), time); + }); } export const timestamp = Date.now as () => Timestamp; diff --git a/packages/common/src/iterators.ts b/packages/common/src/iterators.ts index f0149dc..e9e77ce 100644 --- a/packages/common/src/iterators.ts +++ b/packages/common/src/iterators.ts @@ -1,62 +1,62 @@ export function* map(iter: IterableIterator, fn: (item: T) => U): IterableIterator { - for (const item of iter) { - yield fn(item); - } + for (const item of iter) { + yield fn(item); + } } export function* chain(a: IterableIterator, b: IterableIterator): IterableIterator { - yield* a; - yield* b; + yield* a; + yield* b; } export function* zip(a: IterableIterator, b: IterableIterator): IterableIterator<[T, U]> { - let itemA = a.next(); - let itemB = b.next(); + let itemA = a.next(); + let itemB = b.next(); - while (!itemA.done && !itemB.done) { - yield [itemA.value, itemB.value]; + while (!itemA.done && !itemB.done) { + yield [itemA.value, itemB.value]; - itemA = a.next(); - itemB = b.next(); - } + itemA = a.next(); + itemB = b.next(); + } } export function* take(iter: IterableIterator, n: number): IterableIterator { - for (const item of iter) { - if (n-- === 0) { - return; - } - - yield item; + for (const item of iter) { + if (n-- === 0) { + return; } + + yield item; + } } export function skip(iter: IterableIterator, n: number): IterableIterator { - while (n-- !== 0 && !iter.next().done) {} + while (n-- !== 0 && !iter.next().done) {} - return iter; + return iter; } export function reduce(iter: IterableIterator, fn: (accu: R, item: T) => R, accumulator: R): R { - for (const item of iter) { - accumulator = fn(accumulator, item); - } + for (const item of iter) { + accumulator = fn(accumulator, item); + } - return accumulator; + return accumulator; } export function join(iter: IterableIterator<{ toString: () => string }>, glue: string): string { - const first = iter.next(); + const first = iter.next(); - if (first.done) { - return ''; - } + if (first.done) { + return ''; + } - let result = first.value.toString(); + let result = first.value.toString(); - for (const item of iter) { - result += glue + item; - } + for (const item of iter) { + result += glue + item; + } - return result; + return result; } diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index eae2f80..3264a90 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -16,65 +16,65 @@ import blockTimeIcon from './icons/history.svg'; import lastTimeIcon from './icons/watch.svg'; export default class App extends React.Component<{}, State> { - public state: State = { - best: 0 as Types.BlockNumber, - blockTimestamp: 0 as Types.Timestamp, - timeDiff: 0 as Types.Milliseconds, - subscribed: null, - chains: new Set(), - nodes: new Map() - }; + public state: State = { + best: 0 as Types.BlockNumber, + blockTimestamp: 0 as Types.Timestamp, + timeDiff: 0 as Types.Milliseconds, + subscribed: null, + chains: new Set(), + nodes: new Map() + }; - private connection: Promise; + private connection: Promise; - constructor(props: {}) { - super(props); + constructor(props: {}) { + super(props); - this.connection = Connection.create((changes) => { - if (changes) { - this.setState(changes); - } + this.connection = Connection.create((changes) => { + if (changes) { + this.setState(changes); + } - return this.state; - }); - } + return this.state; + }); + } - public render() { - const { best, blockTimestamp, timeDiff, chains, subscribed } = this.state; + public render() { + const { best, blockTimestamp, timeDiff, chains, subscribed } = this.state; - Ago.timeDiff = timeDiff; + Ago.timeDiff = timeDiff; - return ( -
- -
- #{formatNumber(best)} - -
- - - - - - - - - - - - - - - { - this.nodes().map((props) => ) - } - -
-
- ); - } + return ( +
+ +
+ #{formatNumber(best)} + +
+ + + + + + + + + + + + + + + { + this.nodes().map((props) => ) + } + +
+
+ ); + } - private nodes(): Node.Props[] { - return Array.from(this.state.nodes.values()).sort((a, b) => b.blockDetails[0] - a.blockDetails[0]); - } + private nodes(): Node.Props[] { + return Array.from(this.state.nodes.values()).sort((a, b) => b.blockDetails[0] - a.blockDetails[0]); + } } diff --git a/packages/frontend/src/components/Ago.tsx b/packages/frontend/src/components/Ago.tsx index b0c4e39..b609f8e 100644 --- a/packages/frontend/src/components/Ago.tsx +++ b/packages/frontend/src/components/Ago.tsx @@ -3,77 +3,77 @@ import './Tile.css'; import { timestamp, Types } from '@dotstats/common'; export namespace Ago { - export interface Props { - when: Types.Timestamp, - } + export interface Props { + when: Types.Timestamp, + } - export interface State { - now: Types.Timestamp, - } + export interface State { + now: Types.Timestamp, + } } const tickers = new Map void>(); function tick() { - const now = timestamp(); + const now = timestamp(); - for (const ticker of tickers.values()) { - ticker(now); - } + for (const ticker of tickers.values()) { + ticker(now); + } - setTimeout(tick, 100); + setTimeout(tick, 100); } tick(); export namespace Ago { - export interface State { - now: Types.Timestamp - } + export interface State { + now: Types.Timestamp + } } export class Ago extends React.Component { - 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) { - super(props); + constructor(props: Ago.Props) { + super(props); - this.state = { - now: (timestamp() + Ago.timeDiff) as Types.Timestamp - }; + this.state = { + 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 -; } - public componentWillMount() { - tickers.set(this, (now) => { - this.setState({ - now: (now + Ago.timeDiff) as Types.Timestamp - }); - }) + 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`; } - public componentWillUnmount() { - tickers.delete(this); - } - - public render() { - if (this.props.when === 0) { - return -; - } - - 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 {agoStr} ago - } + return {agoStr} ago + } } diff --git a/packages/frontend/src/components/Chains.tsx b/packages/frontend/src/components/Chains.tsx index 2808243..b1e950e 100644 --- a/packages/frontend/src/components/Chains.tsx +++ b/packages/frontend/src/components/Chains.tsx @@ -7,44 +7,44 @@ import chainIcon from '../icons/link.svg'; import './Chains.css'; export namespace Chains { - export interface Props { - chains: Set, - subscribed: Maybe, - connection: Promise - } + export interface Props { + chains: Set, + subscribed: Maybe, + connection: Promise + } } export class Chains extends React.Component { - public render() { - return ( -
- - { - this.chains.map((chain) => this.renderChain(chain)) - } -
- ); - } + public render() { + return ( +
+ + { + this.chains.map((chain) => this.renderChain(chain)) + } +
+ ); + } - private renderChain(chain: Types.ChainLabel): React.ReactNode { - const className = chain === this.props.subscribed - ? 'Chains-chain Chains-chain-selected' - : 'Chains-chain'; + private renderChain(chain: Types.ChainLabel): React.ReactNode { + const className = chain === this.props.subscribed + ? 'Chains-chain Chains-chain-selected' + : 'Chains-chain'; - return ( - - {chain} - - ) - } + return ( + + {chain} + + ) + } - private get chains(): Types.ChainLabel[] { - return Array.from(this.props.chains); - } + private get chains(): Types.ChainLabel[] { + return Array.from(this.props.chains); + } - private async subscribe(chain: Types.ChainLabel) { - const connection = await this.props.connection; + private async subscribe(chain: Types.ChainLabel) { + const connection = await this.props.connection; - connection.subscribe(chain); - } + connection.subscribe(chain); + } } diff --git a/packages/frontend/src/components/Icon.tsx b/packages/frontend/src/components/Icon.tsx index 99effb7..47aaa4b 100644 --- a/packages/frontend/src/components/Icon.tsx +++ b/packages/frontend/src/components/Icon.tsx @@ -3,21 +3,21 @@ import ReactSVG from 'react-svg'; import './Icon.css'; export interface Props { - src: string, - alt: string, - className?: string, + src: string, + alt: string, + className?: string, }; export class Icon extends React.Component<{}, Props> { - public props: Props; + public props: Props; - public shouldComponentUpdate() { - return false; - } + public shouldComponentUpdate() { + return false; + } - public render() { - const { alt, className, src } = this.props; + public render() { + const { alt, className, src } = this.props; - return ; - } + return ; + } } diff --git a/packages/frontend/src/components/Node.tsx b/packages/frontend/src/components/Node.tsx index 36388d7..513d010 100644 --- a/packages/frontend/src/components/Node.tsx +++ b/packages/frontend/src/components/Node.tsx @@ -4,29 +4,29 @@ import { Ago } from './Ago'; import { Types } from '@dotstats/common'; export namespace Node { - export interface Props { - id: Types.NodeId, - nodeDetails: Types.NodeDetails, - nodeStats: Types.NodeStats, - blockDetails: Types.BlockDetails, - } + export interface Props { + id: Types.NodeId, + nodeDetails: Types.NodeDetails, + nodeStats: Types.NodeStats, + blockDetails: Types.BlockDetails, + } } export function Node(props: Node.Props) { - const [name, implementation, version] = props.nodeDetails; - const [height, hash, blockTime, blockTimestamp] = props.blockDetails; - const [peers, txcount] = props.nodeStats; + const [name, implementation, version] = props.nodeDetails; + const [height, hash, blockTime, blockTimestamp] = props.blockDetails; + const [peers, txcount] = props.nodeStats; - return ( - - {name} - {implementation} v{version} - {peers} - {txcount} - #{formatNumber(height)} - {trimHash(hash, 16)} - {(blockTime / 1000).toFixed(3)}s - - - ); + return ( + + {name} + {implementation} v{version} + {peers} + {txcount} + #{formatNumber(height)} + {trimHash(hash, 16)} + {(blockTime / 1000).toFixed(3)}s + + + ); } diff --git a/packages/frontend/src/components/Tile.tsx b/packages/frontend/src/components/Tile.tsx index 71d4f40..4a2e91a 100644 --- a/packages/frontend/src/components/Tile.tsx +++ b/packages/frontend/src/components/Tile.tsx @@ -3,19 +3,19 @@ import './Tile.css'; import { Icon } from './Icon'; export namespace Tile { - export interface Props { - title: string, - icon: string, - children?: React.ReactNode, - } + export interface Props { + title: string, + icon: string, + children?: React.ReactNode, + } } export function Tile(props: Tile.Props) { - return ( -
- - {props.title} - {props.children} -
- ); + return ( +
+ + {props.title} + {props.children} +
+ ); } diff --git a/packages/frontend/src/message.ts b/packages/frontend/src/message.ts index 636cea4..df82c91 100644 --- a/packages/frontend/src/message.ts +++ b/packages/frontend/src/message.ts @@ -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 export class Connection { - public static async create(update: Update): Promise { - return new Connection(await Connection.socket(), update); + public static async create(update: Update): Promise { + return new Connection(await Connection.socket(), update); + } + + private static readonly address = `ws://${window.location.hostname}:8080`; + + private static async socket(): Promise { + 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 { - let socket = await Connection.trySocket(); - let timeout = TIMEOUT_BASE; + private static async trySocket(): Promise> { + return new Promise>((resolve, _) => { + function clean() { + socket.removeEventListener('open', onSuccess); + socket.removeEventListener('close', onFailure); + socket.removeEventListener('error', onFailure); + } - while (!socket) { - await sleep(timeout); + function onSuccess() { + clean(); + resolve(socket); + } - timeout = Math.max(timeout * 2, TIMEOUT_MAX) as Types.Milliseconds; - socket = await Connection.trySocket(); + 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; + 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> { - return new Promise>((resolve, _) => { - function clean() { - socket.removeEventListener('open', onSuccess); - socket.removeEventListener('close', onFailure); - socket.removeEventListener('error', onFailure); - } + nodes.set(id, node); - function onSuccess() { - 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; - 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; - } - } + break; } - this.state = this.update(changes); - } + case Actions.RemovedNode: { + nodes.delete(message.payload); - private autoSubscribe() { - const { subscribed, chains } = this.state; - - if (subscribed == null && chains.size) { - const first = chains.values().next().value; - - this.subscribe(first); + 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; + } + } } - private handleDisconnect = async () => { - this.clean(); - this.socket.close(); - this.socket = await Connection.socket(); - this.bindSocket(); + this.state = this.update(changes); + } + + private autoSubscribe() { + 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(); + } } diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index cc5c616..32df319 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -2,12 +2,12 @@ import { Node } from './components/Node'; import { Types, Maybe } from '@dotstats/common'; export interface State { - best: Types.BlockNumber, - blockTimestamp: Types.Timestamp, - timeDiff: Types.Milliseconds, - subscribed: Maybe, - chains: Set, - nodes: Map, + best: Types.BlockNumber, + blockTimestamp: Types.Timestamp, + timeDiff: Types.Milliseconds, + subscribed: Maybe, + chains: Set, + nodes: Map, } export type Update = (changes: Pick | null) => Readonly; diff --git a/packages/frontend/src/utils.ts b/packages/frontend/src/utils.ts index b88c3d7..788824e 100644 --- a/packages/frontend/src/utils.ts +++ b/packages/frontend/src/utils.ts @@ -1,25 +1,25 @@ export function formatNumber(num: number): string { - const input = num.toString(); + const input = num.toString(); - let output = ''; - let length = input.length; + let output = ''; + let length = input.length; - while (length > 3) { - output = ',' + input.substr(length - 3, 3) + output; - length -= 3; - } + while (length > 3) { + output = ',' + input.substr(length - 3, 3) + output; + 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 { - if (hash.length < length) { - return hash; - } + if (hash.length < length) { + 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); }