withHealth.js 5.81 KiB
Newer Older
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause

import BigNumber from 'bignumber.js';
import { combineLatest, interval, Observable, fromEvent, merge } from 'rxjs';
import { compose, mapPropsStream } from 'recompose';
  distinctUntilChanged,
  filter,
  map,
  publishReplay,
  startWith,
  switchMap,
  take
} from 'rxjs/operators';
import isElectron from 'is-electron';
import isEqual from 'lodash/isEqual';
Axel Chalon's avatar
Axel Chalon committed
import { peerCount$, syncStatus$, withoutLoading } from '@parity/light.js';

import parityStore from '../stores/parityStore';

const electron = isElectron() ? window.require('electron') : null;

// List here all possible states of our health store. Each state can have a
// payload.
export const STATUS = {
  CLOCKNOTSYNC: Symbol('CLOCKNOTSYNC'), // Local clock is not sync
  DOWNLOADING: Symbol('DOWNLOADING'), // Currently downloading Parity
  GOOD: Symbol('GOOD'), // Everything's fine
  NOINTERNET: Symbol('NOINTERNET'), // No Internet connection
  NOPEERS: Symbol('NOPEERS'), // Not connected to any peers
  LAUNCHING: Symbol('LAUNCHING'), // Parity is being launched (only happens at startup)
  SYNCING: Symbol('SYNCING') // Obvious
};

const isApiConnected$ = parityStore.isApiConnected$;

const isParityRunning$ = Observable.create(observer => {
  if (electron) {
    electron.ipcRenderer.on('parity-running', (_, isParityRunning) => {
      observer.next(isParityRunning);
    });
  }
}).pipe(
Axel Chalon's avatar
Axel Chalon committed
  startWith(electron ? !!electron.remote.getGlobal('isParityRunning') : false)
);

const downloadProgress$ = Observable.create(observer => {
  if (electron) {
    electron.ipcRenderer.on('parity-download-progress', (_, progress) => {
      observer.next(progress);
    });
  }
Axel Chalon's avatar
Axel Chalon committed
}).pipe(startWith(0));

const isClockSync$ = Observable.create(observer => {
  if (electron) {
    electron.ipcRenderer.send('asynchronous-message', 'check-clock-sync');
    electron.ipcRenderer.once('check-clock-sync-reply', (_, clockSync) => {
      observer.next(clockSync.isClockSync);
    });
  }
Axel Chalon's avatar
Axel Chalon committed
}).pipe(startWith(true));

const online$ = merge(
  fromEvent(window, 'online').pipe(map(() => true)),
  fromEvent(window, 'offline').pipe(map(() => false))
Axel Chalon's avatar
Axel Chalon committed
).pipe(startWith(navigator.onLine));
Axel Chalon's avatar
Axel Chalon committed
const combined$ = combineLatest(
  isParityRunning$,
  isApiConnected$,
Axel Chalon's avatar
Axel Chalon committed
  downloadProgress$,
  online$,
  isClockSync$
Axel Chalon's avatar
Axel Chalon committed
).pipe(publishReplay(1));
combined$.connect();

// Subscribe to the RPCs only once we set a provider
const rpcs$ = isApiConnected$.pipe(
  filter(isApiConnected => isApiConnected),
  take(1),
  switchMap(() =>
    combineLatest(
      syncStatus$()
        .pipe(
          map(syncStatus => {
            if (!syncStatus) {
              return {
                isSync: true
              };
            }
            const { currentBlock, highestBlock, startingBlock } = syncStatus;
            const percentage = currentBlock
              .minus(startingBlock)
              .multipliedBy(100)
              .div(highestBlock.minus(startingBlock));
            return {
              isSync: false,
              syncPayload: {
                currentBlock,
                highestBlock,
                percentage,
                startingBlock
              }
            };
          })
          // Emit "not synced" only if we haven't been synced for over 2 seconds
        )
        .pipe(audit(syncStatus => interval(syncStatus.isSync ? 0 : 2000))),
      peerCount$().pipe(withoutLoading())
    )
  ),
  startWith([{ isSync: false }, undefined]), // Don't stall the HOC's combineLatest; emit immediately
  publishReplay(1)
);
rpcs$.connect();

// Inject node health information as health.{status, payload} props
export default compose(
  mapPropsStream(props$ =>
    combineLatest(props$, combined$, rpcs$).pipe(
Axel Chalon's avatar
Axel Chalon committed
          [
            isParityRunning,
            isApiConnected,
            downloadProgress,
            online,
            isClockSync
          ],
          [{ isSync, syncPayload }, peerCount]
          // No connexion to the internet
          if (!online) {
            return {
              ...props,
              health: {
                status: STATUS.NOINTERNET
              }
            };
          }

          // Parity is being downloaded
          if (downloadProgress > 0 && !isParityRunning) {
            return {
              ...props,
              health: {
                status: STATUS.DOWNLOADING,
                payload: {
Axel Chalon's avatar
Axel Chalon committed
                  percentage: new BigNumber(Math.round(downloadProgress * 100))
                }
              }
            };
          }

          // Parity is being launched
          if (!isApiConnected) {
            return {
              ...props,
              health: {
                status: STATUS.LAUNCHING
              }
            };
          }

          // At this point we have a successful connection to parity

          // Clock is not synchronized
          if (!isClockSync) {
            return {
              ...props,
              health: {
                status: STATUS.CLOCKNOTSYNC
              }
            };
          }

          // Not enough peers
          if (peerCount === undefined || peerCount.lte(1)) {
            return {
              ...props,
              health: {
                status: STATUS.NOPEERS
          // Syncing blocks
          if (!isSync) {
            return {
              ...props,
              health: {
                status: STATUS.SYNCING,
                payload: syncPayload
              }
            };
          }

          // Everything's OK
          return {
            ...props,
            health: {
              status: STATUS.GOOD
            }
          };
        }
      ),
      distinctUntilChanged(isEqual) // Perform deep comparison