From ef3f52f5c8caadcdeee2a468fe49cbdcec8cb94d Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Fri, 13 Jul 2018 23:20:29 +0200 Subject: [PATCH] Offline indicator, average block time and stuff --- packages/backend/src/Aggregator.ts | 2 + packages/backend/src/Chain.ts | 38 ++++++++++++++++--- packages/backend/src/Feed.ts | 13 +++++-- packages/common/src/feed.ts | 13 ++++++- packages/common/src/index.ts | 2 + packages/common/src/types.ts | 1 + packages/frontend/src/App.css | 3 +- packages/frontend/src/App.tsx | 14 +++++-- packages/frontend/src/Connection.ts | 28 +++++++++++--- packages/frontend/src/components/Chain.tsx | 9 ++++- packages/frontend/src/components/Chains.tsx | 5 +-- packages/frontend/src/components/Node.tsx | 16 ++++---- .../src/components/OfflineIndicator.css | 17 +++++++++ .../src/components/OfflineIndicator.tsx | 27 +++++++++++++ packages/frontend/src/components/index.ts | 1 + packages/frontend/src/state.ts | 14 ++++--- 16 files changed, 166 insertions(+), 37 deletions(-) create mode 100644 packages/frontend/src/components/OfflineIndicator.css create mode 100644 packages/frontend/src/components/OfflineIndicator.tsx diff --git a/packages/backend/src/Aggregator.ts b/packages/backend/src/Aggregator.ts index d947fa9..2c879ba 100644 --- a/packages/backend/src/Aggregator.ts +++ b/packages/backend/src/Aggregator.ts @@ -23,6 +23,8 @@ export default class Aggregator { public addFeed(feed: Feed) { this.feeds.add(feed); + feed.sendMessage(Feed.feedVersion()); + for (const chain of this.chains.values()) { feed.sendMessage(Feed.addedChain(chain)); } diff --git a/packages/backend/src/Chain.ts b/packages/backend/src/Chain.ts index d2b81e8..168a8ae 100644 --- a/packages/backend/src/Chain.ts +++ b/packages/backend/src/Chain.ts @@ -2,7 +2,9 @@ import * as EventEmitter from 'events'; import Node from './Node'; import Feed from './Feed'; import FeedSet from './FeedSet'; -import { timestamp, Types, FeedMessage } from '@dotstats/common'; +import { timestamp, Maybe, Types, FeedMessage } from '@dotstats/common'; + +const BLOCK_TIME_HISTORY = 10; export default class Chain { private nodes = new Set(); @@ -14,6 +16,8 @@ export default class Chain { public height = 0 as Types.BlockNumber; public blockTimestamp = 0 as Types.Timestamp; + private blockTimes: Array = new Array(BLOCK_TIME_HISTORY); + constructor(label: Types.ChainLabel) { this.label = label; } @@ -48,7 +52,7 @@ export default class Chain { feed.chain = this.label; feed.sendMessage(Feed.timeSync()); - feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp)); + feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime)); for (const node of this.nodes.values()) { feed.sendMessage(Feed.addedNode(node)); @@ -75,11 +79,17 @@ export default class Chain { private updateBlock(node: Node) { if (node.height > this.height) { - this.height = node.height; - this.blockTimestamp = node.blockTimestamp; + const { height, blockTimestamp } = node; + + if (this.blockTimestamp) { + this.blockTimes[height * BLOCK_TIME_HISTORY] = blockTimestamp - this.blockTimestamp; + } + + this.height = height; + this.blockTimestamp = blockTimestamp; node.propagationTime = 0 as Types.PropagationTime; - this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp)); + this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime)); console.log(`[${this.label}] New block ${this.height}`); } else if (node.height === this.height) { @@ -90,4 +100,22 @@ export default class Chain { console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`); } + + private get averageBlockTime(): Maybe { + let sum = 0; + let count = 0; + + for (const time of this.blockTimes) { + if (time != null) { + sum += time; + count += 1; + } + } + + if (count === 0) { + return null; + } + + return (sum / count) as Types.Milliseconds; + } } diff --git a/packages/backend/src/Feed.ts b/packages/backend/src/Feed.ts index ede3542..f5bbe86 100644 --- a/packages/backend/src/Feed.ts +++ b/packages/backend/src/Feed.ts @@ -2,7 +2,7 @@ import * as WebSocket from 'ws'; import * as EventEmitter from 'events'; import Node from './Node'; import Chain from './Chain'; -import { timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common'; +import { VERSION, timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common'; const nextId = idGenerator(); const { Actions } = FeedMessage; @@ -25,10 +25,17 @@ export default class Feed { socket.on('close', () => this.disconnect()); } - public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message { + public static feedVersion(): FeedMessage.Message { + return { + action: Actions.FeedVersion, + payload: VERSION + }; + } + + public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp, avg: Maybe): FeedMessage.Message { return { action: Actions.BestBlock, - payload: [height, ts] + payload: [height, ts, avg] }; } diff --git a/packages/common/src/feed.ts b/packages/common/src/feed.ts index 008753c..85db6ed 100644 --- a/packages/common/src/feed.ts +++ b/packages/common/src/feed.ts @@ -1,5 +1,6 @@ -import { Opaque } from './helpers'; +import { Opaque, Maybe } from './helpers'; import { + FeedVersion, NodeId, NodeCount, NodeDetails, @@ -7,10 +8,12 @@ import { BlockNumber, BlockDetails, Timestamp, + Milliseconds, ChainLabel } from './types'; export const Actions = { + FeedVersion: 255 as 255, BestBlock: 0 as 0, AddedNode: 1 as 1, RemovedNode: 2 as 2, @@ -31,9 +34,14 @@ export namespace Variants { action: Action; } + export interface FeedVersionMessage extends MessageBase { + action: typeof Actions.FeedVersion; + payload: FeedVersion; + } + export interface BestBlockMessage extends MessageBase { action: typeof Actions.BestBlock; - payload: [BlockNumber, Timestamp]; + payload: [BlockNumber, Timestamp, Maybe]; } export interface AddedNodeMessage extends MessageBase { @@ -83,6 +91,7 @@ export namespace Variants { } export type Message = + | Variants.FeedVersionMessage | Variants.BestBlockMessage | Variants.AddedNodeMessage | Variants.RemovedNodeMessage diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index cee17ac..3494ed0 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -6,3 +6,5 @@ import * as Types from './types'; import * as FeedMessage from './feed'; export { Types, FeedMessage }; + +export const VERSION: Types.FeedVersion = 1 as Types.FeedVersion; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 15ae916..e4c9cb8 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,6 +1,7 @@ import { Opaque, Maybe } from './helpers'; import { Id } from './id'; +export type FeedVersion = Opaque; export type ChainLabel = Opaque; export type FeedId = Id<'Feed'>; export type NodeId = Id<'Node'>; diff --git a/packages/frontend/src/App.css b/packages/frontend/src/App.css index 2708c60..92fac85 100644 --- a/packages/frontend/src/App.css +++ b/packages/frontend/src/App.css @@ -1,13 +1,14 @@ .App { text-align: left; font-family: Roboto, Helvetica, Arial, sans-serif; + font-size: 16px; } .App-no-telemetry { width: 100vw; height: 100vh; line-height: 80vh; - font-size: 3.5em; + font-size: 56px; font-weight: 100; text-align: center; color: #888; diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 1fda33a..e2400fb 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Types } from '@dotstats/common'; -import { Chains, Chain, Ago } from './components'; +import { Chains, Chain, Ago, OfflineIndicator } from './components'; import { Connection } from './Connection'; import { State } from './state'; @@ -8,8 +8,10 @@ import './App.css'; export default class App extends React.Component<{}, State> { public state: State = { + status: 'offline', best: 0 as Types.BlockNumber, blockTimestamp: 0 as Types.Timestamp, + blockAverage: null, timeDiff: 0 as Types.Milliseconds, subscribed: null, chains: new Map(), @@ -31,16 +33,22 @@ export default class App extends React.Component<{}, State> { } public render() { - const { chains, timeDiff, subscribed } = this.state; + const { chains, timeDiff, subscribed, status } = this.state; Ago.timeDiff = timeDiff; if (chains.size === 0) { - return
Waiting for telemetry data...
; + return ( +
+ + Waiting for telemetry data... +
+ ); } return (
+
diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 3a7c6b3..690f2e2 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -1,4 +1,4 @@ -import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common'; +import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common'; import { State, Update } from './state'; const { Actions } = FeedMessage; @@ -68,7 +68,10 @@ export class Connection { } private bindSocket() { - this.state = this.update({ nodes: new Map() }); + this.state = this.update({ + status: 'online', + nodes: new Map() + }); this.socket.addEventListener('message', this.handleMessages); this.socket.addEventListener('close', this.handleDisconnect); this.socket.addEventListener('error', this.handleDisconnect); @@ -88,10 +91,24 @@ export class Connection { messages: for (const message of FeedMessage.deserialize(data)) { switch (message.action) { - case Actions.BestBlock: { - const [best, blockTimestamp] = message.payload; + case Actions.FeedVersion: { + if (message.payload !== VERSION) { + this.state = this.update({ status: 'upgrade-requested' }); + this.clean(); - this.state = this.update({ best, blockTimestamp }); + // Force reload from the server + setTimeout(() => window.location.reload(true), 3000); + + return; + } + + continue messages; + } + + case Actions.BestBlock: { + const [best, blockTimestamp, blockAverage] = message.payload; + + this.state = this.update({ best, blockTimestamp, blockAverage }); continue messages; } @@ -215,6 +232,7 @@ export class Connection { } private handleDisconnect = async () => { + this.state = this.update({ status: 'offline' }); this.clean(); this.socket.close(); this.socket = await Connection.socket(); diff --git a/packages/frontend/src/components/Chain.tsx b/packages/frontend/src/components/Chain.tsx index cfd68d6..5f661cb 100644 --- a/packages/frontend/src/components/Chain.tsx +++ b/packages/frontend/src/components/Chain.tsx @@ -25,11 +25,17 @@ function sortNodes(a: Node.Props, b: Node.Props): number { const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number; const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number; + if (aPropagation === bPropagation) { + // Descending sort by block number + return b.blockDetails[0] - a.blockDetails[0]; + } + + // Ascending sort by propagation time return aPropagation - bPropagation; } export function Chain(props: Chain.Props) { - const { best, blockTimestamp } = props.state; + const { best, blockTimestamp, blockAverage } = props.state; const nodes = Array.from(props.state.nodes.values()).sort(sortNodes); @@ -37,6 +43,7 @@ export function Chain(props: Chain.Props) {
#{formatNumber(best)} + { blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' }
diff --git a/packages/frontend/src/components/Chains.tsx b/packages/frontend/src/components/Chains.tsx index ee2de9e..503120e 100644 --- a/packages/frontend/src/components/Chains.tsx +++ b/packages/frontend/src/components/Chains.tsx @@ -23,7 +23,7 @@ export class Chains extends React.Component { public render() { return (
- + { this.chains.map((chain) => this.renderChain(chain)) } @@ -38,10 +38,9 @@ export class Chains extends React.Component { ? 'Chains-chain Chains-chain-selected' : 'Chains-chain'; - return ( - {label} {nodeCount} + {label} {nodeCount} ) } diff --git a/packages/frontend/src/components/Node.tsx b/packages/frontend/src/components/Node.tsx index fb22b6e..2041733 100644 --- a/packages/frontend/src/components/Node.tsx +++ b/packages/frontend/src/components/Node.tsx @@ -20,14 +20,14 @@ export function Node(props: Node.Props) { return ( {name} - {implementation} v{version} - {peers} - {txcount} - #{formatNumber(height)} - {trimHash(hash, 16)} - {(blockTime / 1000).toFixed(3)}s - {propagationTime === null ? '∞' : `${propagationTime}ms`} - + {implementation} v{version} + {peers} + {txcount} + #{formatNumber(height)} + {trimHash(hash, 16)} + {(blockTime / 1000).toFixed(3)}s + {propagationTime === null ? '∞' : `${propagationTime}ms`} + ); } diff --git a/packages/frontend/src/components/OfflineIndicator.css b/packages/frontend/src/components/OfflineIndicator.css new file mode 100644 index 0000000..f1daf4c --- /dev/null +++ b/packages/frontend/src/components/OfflineIndicator.css @@ -0,0 +1,17 @@ +.OfflineIndicator { + position: absolute; + top: 30px; + right: 30px; + z-index: 10; + background: #c00; + line-height: 16px; + color: #fff; + font-size: 30px; + padding: 16px; + border-radius: 50px; + box-shadow: rgba(0,0,0,0.5) 0 3px 20px; +} + +.OfflineIndicator-upgrade { + background: #282; +} diff --git a/packages/frontend/src/components/OfflineIndicator.tsx b/packages/frontend/src/components/OfflineIndicator.tsx new file mode 100644 index 0000000..ed0dda5 --- /dev/null +++ b/packages/frontend/src/components/OfflineIndicator.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import './OfflineIndicator.css'; +import { Icon } from './Icon'; +import { State } from '../state'; +import offlineIcon from '../icons/zap.svg'; +import upgradeIcon from '../icons/flame.svg'; + +export namespace OfflineIndicator { + export interface Props { + status: State["status"]; + } +} + +export function OfflineIndicator(props: OfflineIndicator.Props): React.ReactElement | null { + switch (props.status) { + case 'online': + return null; + case 'offline': + return
; + case 'upgrade-requested': + return ( +
+ +
+ ); + } +} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 5e219e3..47c98ad 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -4,3 +4,4 @@ export * from './Icon'; export * from './Node'; export * from './Tile'; export * from './Ago'; +export * from './OfflineIndicator'; diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index e552437..9996435 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -2,12 +2,14 @@ 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: Map, - nodes: Map, + status: 'online' | 'offline' | 'upgrade-requested'; + best: Types.BlockNumber; + blockTimestamp: Types.Timestamp; + blockAverage: Maybe; + timeDiff: Types.Milliseconds; + subscribed: Maybe; + chains: Map; + nodes: Map; } export type Update = (changes: Pick | null) => Readonly;