mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-19 15:51:11 +00:00
Added ago timers
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import * as EventEmitter from 'events';
|
import * as EventEmitter from 'events';
|
||||||
import Node from './node';
|
import Node from './node';
|
||||||
import Feed from './feed';
|
import Feed from './feed';
|
||||||
import { Types, IdSet, FeedMessage } from '@dotstats/common';
|
import { timestamp, Types, IdSet, FeedMessage } from '@dotstats/common';
|
||||||
|
|
||||||
export default class Aggregator extends EventEmitter {
|
export default class Aggregator extends EventEmitter {
|
||||||
private nodes = new IdSet<Types.NodeId, Node>();
|
private nodes = new IdSet<Types.NodeId, Node>();
|
||||||
@@ -9,6 +9,7 @@ export default class Aggregator extends EventEmitter {
|
|||||||
private messages: Array<FeedMessage.Message> = [];
|
private messages: Array<FeedMessage.Message> = [];
|
||||||
|
|
||||||
public height = 0 as Types.BlockNumber;
|
public height = 0 as Types.BlockNumber;
|
||||||
|
public blockTimestamp = 0 as Types.Timestamp;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -34,7 +35,7 @@ export default class Aggregator extends EventEmitter {
|
|||||||
public addFeed(feed: Feed) {
|
public addFeed(feed: Feed) {
|
||||||
this.feeds.add(feed);
|
this.feeds.add(feed);
|
||||||
|
|
||||||
const messages = [Feed.bestBlock(this.height)];
|
const messages = [Feed.timeSync(), Feed.bestBlock(this.height, this.blockTimestamp)];
|
||||||
|
|
||||||
for (const node of this.nodes.values()) {
|
for (const node of this.nodes.values()) {
|
||||||
messages.push(Feed.addedNode(node));
|
messages.push(Feed.addedNode(node));
|
||||||
@@ -69,18 +70,21 @@ export default class Aggregator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private timeoutCheck() {
|
private timeoutCheck() {
|
||||||
const now = Date.now();
|
const now = timestamp();
|
||||||
|
|
||||||
for (const node of this.nodes.values()) {
|
for (const node of this.nodes.values()) {
|
||||||
node.timeoutCheck(now);
|
node.timeoutCheck(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.broadcast(Feed.timeSync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateBlock(node: Node) {
|
private updateBlock(node: Node) {
|
||||||
if (node.height > this.height) {
|
if (node.height > this.height) {
|
||||||
this.height = node.height;
|
this.height = node.height;
|
||||||
|
this.blockTimestamp = node.blockTimestamp;
|
||||||
|
|
||||||
this.broadcast(Feed.bestBlock(this.height));
|
this.broadcast(Feed.bestBlock(this.height, this.blockTimestamp));
|
||||||
|
|
||||||
console.log(`New block ${this.height}`);
|
console.log(`New block ${this.height}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import * as EventEmitter from 'events';
|
import * as EventEmitter from 'events';
|
||||||
import Node from './node';
|
import Node from './node';
|
||||||
import { Opaque, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
import { timestamp, Opaque, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
||||||
|
|
||||||
const nextId = idGenerator<Types.FeedId>();
|
const nextId = idGenerator<Types.FeedId>();
|
||||||
const { Actions } = FeedMessage;
|
const { Actions } = FeedMessage;
|
||||||
@@ -21,10 +21,10 @@ export default class Feed extends EventEmitter {
|
|||||||
socket.on('close', () => this.disconnect());
|
socket.on('close', () => this.disconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bestBlock(height: Types.BlockNumber): FeedMessage.Message {
|
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message {
|
||||||
return {
|
return {
|
||||||
action: Actions.BestBlock,
|
action: Actions.BestBlock,
|
||||||
payload: height
|
payload: [height, ts]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,13 @@ export default class Feed extends EventEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static timeSync(): FeedMessage.Message {
|
||||||
|
return {
|
||||||
|
action: Actions.TimeSync,
|
||||||
|
payload: timestamp()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public sendData(data: FeedMessage.Data) {
|
public sendData(data: FeedMessage.Data) {
|
||||||
this.socket.send(data);
|
this.socket.send(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import * as express from 'express';
|
|
||||||
import { createServer } from 'http';
|
|
||||||
import Node from './node';
|
import Node from './node';
|
||||||
import Feed from './feed';
|
import Feed from './feed';
|
||||||
import Aggregator from './aggregator';
|
import Aggregator from './aggregator';
|
||||||
|
|
||||||
const aggregator = new Aggregator;
|
const aggregator = new Aggregator;
|
||||||
const app = express();
|
|
||||||
const server = createServer(app);
|
|
||||||
|
|
||||||
// WebSocket for Nodes feeding telemetry data to the server
|
// WebSocket for Nodes feeding telemetry data to the server
|
||||||
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
|
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
|
||||||
|
|
||||||
// WebSocket for web clients listening to the telemetry data aggregate
|
// WebSocket for web clients listening to the telemetry data aggregate
|
||||||
const telemetryFeed = new WebSocket.Server({ server });
|
const telemetryFeed = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
app.get('/', function (req, res) {
|
console.log('Telemetry server listening on port 1024');
|
||||||
res.send('See live listing at <a href="http://telemetry.polkadot.io/">telemetry.polkadot.io/<a>');
|
console.log('Feed server listening on port 8080');
|
||||||
});
|
|
||||||
|
|
||||||
incomingTelemetry.on('connection', async (socket: WebSocket) => {
|
incomingTelemetry.on('connection', async (socket: WebSocket) => {
|
||||||
try {
|
try {
|
||||||
@@ -31,5 +26,3 @@ telemetryFeed.on('connection', (socket: WebSocket) => {
|
|||||||
aggregator.addFeed(new Feed(socket));
|
aggregator.addFeed(new Feed(socket));
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Starting server on port 8080');
|
|
||||||
server.listen(8080);
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import * as EventEmitter from 'events';
|
import * as EventEmitter from 'events';
|
||||||
import { Maybe, Types, idGenerator } from '@dotstats/common';
|
import { timestamp, Maybe, Types, idGenerator } from '@dotstats/common';
|
||||||
import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message';
|
import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message';
|
||||||
|
|
||||||
const BLOCK_TIME_HISTORY = 10;
|
const BLOCK_TIME_HISTORY = 10;
|
||||||
const TIMEOUT = 1000 * 60 * 1; // 1 minute
|
const TIMEOUT = (1000 * 60 * 1) as Types.Milliseconds; // 1 minute
|
||||||
|
|
||||||
const nextId = idGenerator<Types.NodeId>();
|
const nextId = idGenerator<Types.NodeId>();
|
||||||
|
|
||||||
export default class Node extends EventEmitter {
|
export default class Node extends EventEmitter {
|
||||||
public lastMessage: number;
|
public lastMessage: Types.Timestamp;
|
||||||
public id: Types.NodeId;
|
public id: Types.NodeId;
|
||||||
public name: Types.NodeName;
|
public name: Types.NodeName;
|
||||||
public implementation: Types.NodeImplementation;
|
public implementation: Types.NodeImplementation;
|
||||||
@@ -19,6 +19,7 @@ export default class Node extends EventEmitter {
|
|||||||
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;
|
||||||
|
|
||||||
private peers = 0 as Types.PeerCount;
|
private peers = 0 as Types.PeerCount;
|
||||||
private txcount = 0 as Types.TransactionCount;
|
private txcount = 0 as Types.TransactionCount;
|
||||||
@@ -36,7 +37,7 @@ export default class Node extends EventEmitter {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.lastMessage = Date.now();
|
this.lastMessage = timestamp();
|
||||||
this.id = nextId();
|
this.id = nextId();
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -53,7 +54,7 @@ export default class Node extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastMessage = Date.now();
|
this.lastMessage = timestamp();
|
||||||
this.updateLatency(message.ts);
|
this.updateLatency(message.ts);
|
||||||
|
|
||||||
const update = getBestBlock(message);
|
const update = getBestBlock(message);
|
||||||
@@ -111,7 +112,7 @@ export default class Node extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public timeoutCheck(now: number) {
|
public timeoutCheck(now: Types.Timestamp) {
|
||||||
if (this.lastMessage + TIMEOUT < now) {
|
if (this.lastMessage + TIMEOUT < now) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ export default class Node extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public blockDetails(): Types.BlockDetails {
|
public blockDetails(): Types.BlockDetails {
|
||||||
return [this.height, this.best, this.blockTime];
|
return [this.height, this.best, this.blockTime, this.blockTimestamp];
|
||||||
}
|
}
|
||||||
|
|
||||||
public get average(): number {
|
public get average(): number {
|
||||||
@@ -147,6 +148,14 @@ export default class Node extends EventEmitter {
|
|||||||
return sum / accounted;
|
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() {
|
private disconnect() {
|
||||||
this.socket.removeAllListeners();
|
this.socket.removeAllListeners();
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
@@ -177,6 +186,7 @@ export default class Node extends EventEmitter {
|
|||||||
|
|
||||||
this.best = best;
|
this.best = best;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
this.blockTimestamp = timestamp();
|
||||||
this.lastBlockAt = time;
|
this.lastBlockAt = time;
|
||||||
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
|
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
|
||||||
this.blockTime = blockTime;
|
this.blockTime = blockTime;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Opaque } from './helpers';
|
import { Opaque } from './helpers';
|
||||||
import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails } from './types';
|
import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails, Timestamp } from './types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
BestBlock: 0 as 0,
|
BestBlock: 0 as 0,
|
||||||
@@ -7,6 +7,7 @@ export const Actions = {
|
|||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = typeof Actions[keyof typeof Actions];
|
export type Action = typeof Actions[keyof typeof Actions];
|
||||||
@@ -19,7 +20,7 @@ export namespace Variants {
|
|||||||
|
|
||||||
export interface BestBlockMessage extends MessageBase {
|
export interface BestBlockMessage extends MessageBase {
|
||||||
action: typeof Actions.BestBlock;
|
action: typeof Actions.BestBlock;
|
||||||
payload: BlockNumber;
|
payload: [BlockNumber, Timestamp];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddedNodeMessage extends MessageBase {
|
export interface AddedNodeMessage extends MessageBase {
|
||||||
@@ -40,7 +41,12 @@ export namespace Variants {
|
|||||||
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 {
|
||||||
|
action: typeof Actions.TimeSync;
|
||||||
|
payload: Timestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message =
|
export type Message =
|
||||||
@@ -48,7 +54,8 @@ export type Message =
|
|||||||
| Variants.AddedNodeMessage
|
| Variants.AddedNodeMessage
|
||||||
| Variants.RemovedNodeMessage
|
| Variants.RemovedNodeMessage
|
||||||
| Variants.ImportedBlockMessage
|
| Variants.ImportedBlockMessage
|
||||||
| Variants.NodeStatsMessage;
|
| Variants.NodeStatsMessage
|
||||||
|
| Variants.TimeSyncMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opaque data type to be sent to the feed. Passing through
|
* Opaque data type to be sent to the feed. Passing through
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Milliseconds } from './types';
|
import { Milliseconds, Timestamp } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PhantomData akin to Rust, because sometimes you need to be smarter than
|
* PhantomData akin to Rust, because sometimes you need to be smarter than
|
||||||
* the compiler.
|
* the compiler.
|
||||||
*/
|
*/
|
||||||
export abstract class PhantomData<P> { private __PHANTOM__: P }
|
export abstract class PhantomData<P> { public __PHANTOM__: P }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opaque type, similar to `opaque type` in Flow, or new types in Rust/C.
|
* Opaque type, similar to `opaque type` in Flow, or new types in Rust/C.
|
||||||
@@ -31,3 +31,5 @@ export function sleep(time: Milliseconds): Promise<void> {
|
|||||||
setTimeout(() => resolve(), time);
|
setTimeout(() => resolve(), time);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const timestamp = Date.now as () => Timestamp;
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ export type NodeVersion = Opaque<string, 'NodeVersion'>;
|
|||||||
export type BlockNumber = Opaque<number, 'BlockNumber'>;
|
export type BlockNumber = Opaque<number, 'BlockNumber'>;
|
||||||
export type BlockHash = Opaque<string, 'BlockHash'>;
|
export type BlockHash = Opaque<string, 'BlockHash'>;
|
||||||
export type Milliseconds = Opaque<number, 'Milliseconds'>;
|
export type Milliseconds = Opaque<number, 'Milliseconds'>;
|
||||||
|
export type Timestamp = Opaque<Milliseconds, 'Timestamp'>;
|
||||||
export type PeerCount = Opaque<number, 'PeerCount'>;
|
export type PeerCount = Opaque<number, 'PeerCount'>;
|
||||||
export type TransactionCount = Opaque<number, 'TransactionCount'>;
|
export type TransactionCount = Opaque<number, 'TransactionCount'>;
|
||||||
|
|
||||||
export type BlockDetails = [BlockNumber, BlockHash, Milliseconds];
|
export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp];
|
||||||
export type NodeDetails = [NodeName, NodeImplementation, NodeVersion];
|
export type NodeDetails = [NodeName, NodeImplementation, NodeVersion];
|
||||||
export type NodeStats = [PeerCount, TransactionCount];
|
export type NodeStats = [PeerCount, TransactionCount];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: left;
|
||||||
font-family: monospace, sans-serif;
|
font-family: monospace, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Types } from '@dotstats/common';
|
import { Types } from '@dotstats/common';
|
||||||
import { Node } from './Node';
|
import { Node, Icon, Tile, Ago } from './components';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Connection } from './message';
|
import { Connection } from './message';
|
||||||
import { State } from './state';
|
import { State } from './state';
|
||||||
import { formatNumber } from './utils';
|
import { formatNumber } from './utils';
|
||||||
@@ -14,10 +13,13 @@ import transactionsIcon from './icons/inbox.svg';
|
|||||||
import blockIcon from './icons/package.svg';
|
import blockIcon from './icons/package.svg';
|
||||||
import blockHashIcon from './icons/file-binary.svg';
|
import blockHashIcon from './icons/file-binary.svg';
|
||||||
import blockTimeIcon from './icons/history.svg';
|
import blockTimeIcon from './icons/history.svg';
|
||||||
|
import lastTimeIcon from './icons/dashboard.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,
|
||||||
|
timeDiff: 0 as Types.Milliseconds,
|
||||||
nodes: new Map()
|
nodes: new Map()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,11 +30,14 @@ export default class App extends React.Component<{}, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const { best, blockTimestamp, timeDiff } = this.state;
|
||||||
|
|
||||||
|
Ago.timeDiff = timeDiff;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<div className="App-header">
|
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
||||||
<Icon src={blockIcon} alt="Best Block" /> #{formatNumber(this.state.best)}
|
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||||
</div>
|
|
||||||
<table className="App-list">
|
<table className="App-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -43,6 +48,7 @@ export default class App extends React.Component<{}, State> {
|
|||||||
<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>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import './Tile.css';
|
||||||
|
import { timestamp, Types } from '@dotstats/common';
|
||||||
|
|
||||||
|
export namespace Ago {
|
||||||
|
export interface Props {
|
||||||
|
when: Types.Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
now: Types.Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tickers = new Map<Ago, (ts: Types.Timestamp) => void>();
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
const now = timestamp();
|
||||||
|
|
||||||
|
for (const ticker of tickers.values()) {
|
||||||
|
ticker(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(tick, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
export namespace Ago {
|
||||||
|
export interface State {
|
||||||
|
now: Types.Timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Ago extends React.Component<Ago.Props, Ago.State> {
|
||||||
|
public static timeDiff = 0 as Types.Milliseconds;
|
||||||
|
|
||||||
|
public state: Ago.State;
|
||||||
|
|
||||||
|
constructor(props: Ago.Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { formatNumber, trimHash } from './utils';
|
import { formatNumber, trimHash } from '../utils';
|
||||||
|
import { Ago } from './Ago';
|
||||||
import { Types } from '@dotstats/common';
|
import { Types } from '@dotstats/common';
|
||||||
|
|
||||||
export namespace Node {
|
export namespace Node {
|
||||||
@@ -13,7 +14,7 @@ export namespace Node {
|
|||||||
|
|
||||||
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] = props.blockDetails;
|
const [height, hash, blockTime, blockTimestamp] = props.blockDetails;
|
||||||
const [peers, txcount] = props.nodeStats;
|
const [peers, txcount] = props.nodeStats;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +25,8 @@ export function Node(props: Node.Props) {
|
|||||||
<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}s</td>
|
<td>{(blockTime / 1000).toFixed(3)}s</td>
|
||||||
|
<td><Ago when={blockTimestamp} /></td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.Tile {
|
||||||
|
font-size: 2.5em;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
width: 7em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import './Tile.css';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
|
export namespace Tile {
|
||||||
|
export interface Props {
|
||||||
|
title: string,
|
||||||
|
icon: string,
|
||||||
|
children?: React.ReactNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tile(props: Tile.Props) {
|
||||||
|
return (
|
||||||
|
<div className="Tile">
|
||||||
|
<Icon src={props.icon} alt={props.title} /> {props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './Icon';
|
||||||
|
export * from './Node';
|
||||||
|
export * from './Tile';
|
||||||
|
export * from './Ago';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||||
import { State, Update } from './state';
|
import { State, Update } from './state';
|
||||||
|
|
||||||
const { Actions } = FeedMessage;
|
const { Actions } = FeedMessage;
|
||||||
@@ -13,7 +13,6 @@ export class Connection {
|
|||||||
|
|
||||||
private static readonly address = `ws://${window.location.hostname}:8080`;
|
private static readonly address = `ws://${window.location.hostname}:8080`;
|
||||||
|
|
||||||
|
|
||||||
private static async socket(): Promise<WebSocket> {
|
private static async socket(): Promise<WebSocket> {
|
||||||
let socket = await Connection.trySocket();
|
let socket = await Connection.trySocket();
|
||||||
let timeout = TIMEOUT_BASE;
|
let timeout = TIMEOUT_BASE;
|
||||||
@@ -61,11 +60,11 @@ export class Connection {
|
|||||||
constructor(socket: WebSocket, update: Update) {
|
constructor(socket: WebSocket, update: Update) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.update = update;
|
this.update = update;
|
||||||
this.state = update(null);
|
|
||||||
this.bindSocket();
|
this.bindSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindSocket() {
|
private bindSocket() {
|
||||||
|
this.state = this.update({ nodes: new Map() });
|
||||||
this.socket.addEventListener('message', this.handleMessages);
|
this.socket.addEventListener('message', this.handleMessages);
|
||||||
this.socket.addEventListener('close', this.handleDisconnect);
|
this.socket.addEventListener('close', this.handleDisconnect);
|
||||||
this.socket.addEventListener('error', this.handleDisconnect);
|
this.socket.addEventListener('error', this.handleDisconnect);
|
||||||
@@ -85,10 +84,13 @@ export class Connection {
|
|||||||
messages: for (const message of FeedMessage.deserialize(data)) {
|
messages: for (const message of FeedMessage.deserialize(data)) {
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case Actions.BestBlock: {
|
case Actions.BestBlock: {
|
||||||
this.state = this.update({ best: message.payload });
|
const [best, blockTimestamp] = message.payload;
|
||||||
|
|
||||||
|
this.state = this.update({ best, blockTimestamp });
|
||||||
|
|
||||||
continue messages;
|
continue messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.AddedNode: {
|
case Actions.AddedNode: {
|
||||||
const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
|
const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
|
||||||
const node = { id, nodeDetails, nodeStats, blockDetails };
|
const node = { id, nodeDetails, nodeStats, blockDetails };
|
||||||
@@ -97,14 +99,15 @@ export class Connection {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.RemovedNode: {
|
case Actions.RemovedNode: {
|
||||||
nodes.delete(message.payload);
|
nodes.delete(message.payload);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.ImportedBlock: {
|
case Actions.ImportedBlock: {
|
||||||
const [id, blockDetails] = message.payload;
|
const [id, blockDetails] = message.payload;
|
||||||
|
|
||||||
const node = nodes.get(id);
|
const node = nodes.get(id);
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
@@ -115,9 +118,9 @@ export class Connection {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.NodeStats: {
|
case Actions.NodeStats: {
|
||||||
const [id, nodeStats] = message.payload;
|
const [id, nodeStats] = message.payload;
|
||||||
|
|
||||||
const node = nodes.get(id);
|
const node = nodes.get(id);
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
@@ -128,8 +131,17 @@ export class Connection {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Actions.TimeSync: {
|
||||||
|
this.state = this.update({
|
||||||
|
timeDiff: (timestamp() - message.payload) as Types.Milliseconds
|
||||||
|
});
|
||||||
|
|
||||||
|
continue messages;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return;
|
continue messages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Types } from '@dotstats/common';
|
import { Types } from '@dotstats/common';
|
||||||
import { Node } from './Node';
|
import { Node } from './components/Node';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
best: Types.BlockNumber,
|
best: Types.BlockNumber,
|
||||||
|
blockTimestamp: Types.Timestamp,
|
||||||
|
timeDiff: Types.Milliseconds,
|
||||||
nodes: Map<Types.NodeId, Node.Props>
|
nodes: Map<Types.NodeId, Node.Props>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"ordered-imports": false,
|
"ordered-imports": false,
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
"no-console": false,
|
"no-console": false,
|
||||||
"no-unused-variable": [true, {"ignore-pattern": "^_"}],
|
"no-unused-variable": [true, {"ignore-pattern": "^_"}],
|
||||||
"no-empty": false,
|
"no-empty": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user