Skip to content
This repository has been archived by the owner on Apr 11, 2022. It is now read-only.

Commit

Permalink
refactor(context): Split ApiContext into Api & SystemContext (#120)
Browse files Browse the repository at this point in the history
* refactor(context): Split ApiContext into Api & SystemContext

* Fix lint

* Update headers

* Fix lint
  • Loading branch information
amaury1093 authored Jan 30, 2020
1 parent d71c071 commit e076925
Show file tree
Hide file tree
Showing 91 changed files with 384 additions and 406 deletions.
2 changes: 1 addition & 1 deletion front/context/src/AccountsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2018-2020 @paritytech/substrate-light-ui authors & contributors
// Copyright 2018-2020 @paritytech/Nomidot authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

Expand Down
2 changes: 1 addition & 1 deletion front/context/src/AlertsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2018-2020 @paritytech/substrate-light-ui authors & contributors
// Copyright 2018-2020 @paritytech/Nomidot authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

Expand Down
115 changes: 15 additions & 100 deletions front/context/src/ApiContext.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,55 @@
// Copyright 2018-2020 @paritytech/substrate-light-ui authors & contributors
// Copyright 2018-2020 @paritytech/Nomidot authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { ApiRx } from '@polkadot/api';
import { WsProvider } from '@polkadot/rpc-provider';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { ChainProperties, Health } from '@polkadot/types/interfaces';
import { logger } from '@polkadot/util';
import React, { useEffect, useRef, useState } from 'react';
import { combineLatest } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';

export interface System {
chain: string;
health: Health;
name: string;
properties: ChainProperties;
version: string;
interface State {
isApiReady: boolean;
}

export interface ApiContextType {
export interface ApiContextType extends State {
api: ApiRx; // From @polkadot/api
isReady: boolean; // Are api and keyring loaded?
system: System; // Information about the chain
}

interface State {
isReady: boolean;
system: System;
}

const INIT_ERROR = new Error(
'Please wait for `isReady` before fetching this property'
);

const DISCONNECTED_STATE_PROPERTIES = {
isReady: false,
system: {
get chain(): never {
throw INIT_ERROR;
},
get health(): never {
throw INIT_ERROR;
},
get name(): never {
throw INIT_ERROR;
},
get properties(): never {
throw INIT_ERROR;
},
get version(): never {
throw INIT_ERROR;
},
},
};

const l = logger('api-context');

export const ApiContext: React.Context<ApiContextType> = React.createContext(
{} as ApiContextType
);

export interface ApiContextProviderProps {
children?: React.ReactNode;
loading?: React.ReactNode;
children?: React.ReactElement;
provider: ProviderInterface;
}

export function ApiContextProvider(
props: ApiContextProviderProps
): React.ReactElement {
const { children = null, loading = null, provider } = props;
const [state, setState] = useState<State>(DISCONNECTED_STATE_PROPERTIES);
const { isReady, system } = state;
const { children = null, provider } = props;
const [state, setState] = useState<State>({ isApiReady: false });
const { isApiReady } = state;

const apiRef = useRef(new ApiRx({ provider }));
const api = apiRef.current;

useEffect(() => {
// Block the UI when disconnected
api.isConnected.pipe(filter(isConnected => !isConnected)).subscribe(() => {
setState(DISCONNECTED_STATE_PROPERTIES);
});

// We want to fetch all the information again each time we reconnect. We
// might be connecting to a different node, or the node might have changed
// settings.
api.isReady
.pipe(
switchMap(() =>
combineLatest([
api.rpc.system.chain(),
api.rpc.system.health(),
api.rpc.system.name(),
api.rpc.system.properties(),
api.rpc.system.version(),
])
)
)
.subscribe(([chain, health, name, properties, version]) => {
l.log(
`Api connected to ${
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore WsProvider.endpoint is private, but we still use it
// here, to have a nice log
provider instanceof WsProvider ? provider.endpoint : 'provider'
}`
);
l.log(
`Api ready, connected to chain "${chain}" with properties ${JSON.stringify(
properties
)}`
);
api.isReady.subscribe(() => {
l.log(`Api ready, app is ready to use`);

setState({
isReady: true,
system: {
chain: chain.toString(),
health,
name: name.toString(),
properties,
version: version.toString(),
},
});
});
}, [api.isConnected, api.isReady, api.rpc.system, provider]);
setState({ isApiReady: true });
});
}, [api, provider]);

return (
<ApiContext.Provider
value={{
api,
isReady,
system,
}}
>
{state.isReady ? children : loading}
<ApiContext.Provider value={{ api, isApiReady }}>
{children}
</ApiContext.Provider>
);
}
87 changes: 42 additions & 45 deletions front/context/src/HealthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// Copyright 2018-2020 @paritytech/substrate-light-ui authors & contributors
// Copyright 2018-2020 @paritytech/Nomidot authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import Rpc from '@polkadot/rpc-core';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { TypeRegistry } from '@polkadot/types';
import { Header, Health } from '@polkadot/types/interfaces';
import React, { useEffect, useRef, useState } from 'react';
import { combineLatest, interval, merge, Subscription } from 'rxjs';
import { map, startWith, switchMap } from 'rxjs/operators';
import React, { useContext, useEffect, useState } from 'react';

import { SystemContext } from './SystemContext';

/**
* Period, in ms, after which we consider we're syncing
*/
const SYNCING_THRESHOLD = 2000;

export interface HealthContextType {
/**
Expand Down Expand Up @@ -36,32 +39,27 @@ export interface HealthContextType {
* @param health - The health of the light node
*/
function getNodeStatus(
provider: ProviderInterface,
header: Header | undefined,
health: Health | undefined
): HealthContextType {
): Omit<HealthContextType, 'isSyncing'> {
let best = 0;
let isNodeConnected = false;
let hasPeers = false;
let isSyncing = false;

if (health && header) {
if (provider.isConnected() && health && header) {
isNodeConnected = true;
best = header.number.toNumber();

if (health.peers.gten(1)) {
hasPeers = true;
}

if (health.isSyncing.isTrue) {
isSyncing = true;
}
}

return {
best,
hasPeers,
isNodeConnected,
isSyncing,
};
}

Expand All @@ -70,46 +68,45 @@ export const HealthContext: React.Context<HealthContextType> = React.createConte
);

export interface HealthContextProviderProps {
children?: React.ReactNode;
children?: React.ReactElement;
provider: ProviderInterface;
}

// Track if we we're already syncing
let wasSyncing = true;

export function HealthContextProvider(
props: HealthContextProviderProps
): React.ReactElement {
const { children = null, provider } = props;
const [status, setStatus] = useState<HealthContextType>({
best: 0,
hasPeers: false,
isNodeConnected: false,
isSyncing: false,
});

const registryRef = useRef(new TypeRegistry());
const rpcRef = useRef(new Rpc(registryRef.current, provider));
const rpc = rpcRef.current;
const { header, health } = useContext(SystemContext);
const [isSyncing, setIsSyncing] = useState(true);

// We wait for 2 seconds, and if we've been syncing for 2 seconds, then we
// set isSyncing to true
useEffect(() => {
let sub: Subscription | undefined;

rpc.provider.on('connected', () => {
sub = combineLatest([
rpc.system.health(),
merge(
rpc.chain.subscribeNewHeads(),
// Header doesn't get updated when doing a major sync, so we also poll
interval(2000).pipe(switchMap(() => rpc.chain.getHeader()))
),
])
.pipe(
startWith([undefined, undefined]),
map(([health, header]) => getNodeStatus(header, health))
)
.subscribe(setStatus);
});

return (): void => sub && sub.unsubscribe();
}, [rpc]);
let timer: number | undefined;

if (!wasSyncing && health.isSyncing.eq(true)) {
wasSyncing = true;
timer = setTimeout(() => {
setIsSyncing(true);
}, SYNCING_THRESHOLD);
} else if (wasSyncing && health.isSyncing.eq(false)) {
wasSyncing = false;
setIsSyncing(false);
timer && clearTimeout(timer);
}

return (): void => {
timer && clearTimeout(timer);
};
}, [health, setIsSyncing]);

const status = {
...getNodeStatus(provider, header, health),
isSyncing,
};

return (
<HealthContext.Provider value={status}>{children}</HealthContext.Provider>
Expand Down
Loading

0 comments on commit e076925

Please sign in to comment.