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

refactor(context): Split ApiContext into Api & SystemContext #120

Merged
merged 4 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 14 additions & 99 deletions front/context/src/ApiContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,138 +3,53 @@
// 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>
);
}
85 changes: 41 additions & 44 deletions front/context/src/HealthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// 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