Add trackStream, impl. in staking overview (#1891)

* Add trackStream, impl. in staking overview

* Use trackStream in Address

* Fix linting

* Add reward displays
This commit is contained in:
Jaco Greeff 2019-11-15 15:06:47 +01:00 committed by GitHub
parent d5857436fe
commit 488bfef0c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 140 deletions

View File

@ -10,10 +10,9 @@ import { ValidatorFilter } from '../types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { withCalls, withMulti } from '@polkadot/react-api';
import { AddressCard, AddressMini, Badge, Expander, Icon } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import { useApiContext } from '@polkadot/react-hooks';
import { trackStream, useApiContext } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';
import translate from '../translate';
@ -24,13 +23,13 @@ interface Props extends I18nProps {
className?: string;
defaultName: string;
filter: ValidatorFilter;
hasQueries: boolean;
isElected: boolean;
isFavorite: boolean;
lastAuthors?: string[];
myAccounts: string[];
points?: Points;
recentlyOnline?: DerivedHeartbeats;
stakingInfo?: DerivedStaking;
toggleFavorite: (accountId: string) => void;
withNominations?: boolean;
}
@ -47,8 +46,10 @@ interface StakingState {
const WITH_VALIDATOR_PREFS = { validatorPayment: true };
function Address ({ address, authorsMap, className, defaultName, filter, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, stakingInfo, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
function Address ({ address, authorsMap, className, defaultName, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
const { api, isSubstrateV2 } = useApiContext();
// FIXME Any horrors, caused by trackStream
const stakingInfo = trackStream<DerivedStaking>(api.derive.staking.info as any, [address]);
const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>();
const [hasActivity, setHasActivity] = useState(true);
const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
@ -188,7 +189,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
}
isDisabled={isSubstrateV2 && !hasActivity}
overlay={
api.query.imOnline?.authoredBlocks && (
hasQueries && api.query.imOnline?.authoredBlocks && (
<Icon
className='staking--stats'
name='line graph'
@ -223,7 +224,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
);
}
export default withMulti(
export default translate(
styled(Address)`
.extras {
display: inline-block;
@ -295,12 +296,5 @@ export default withMulti(
position: absolute;
right: 0.5rem;
}
`,
translate,
withCalls<Props>(
['derive.staking.info', {
paramName: 'address',
propName: 'stakingInfo'
}]
)
`
);

View File

@ -18,6 +18,7 @@ import Address from './Address';
interface Props extends I18nProps {
authorsMap: Record<string, string>;
hasQueries: boolean;
lastAuthors?: string[];
next: string[];
recentlyOnline?: DerivedHeartbeats;
@ -55,7 +56,7 @@ function accountsToString (accounts: AccountId[]): string[] {
return accounts.map((accountId): string => accountId.toString());
}
function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useApiContext();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [filter, setFilter] = useState<ValidatorFilter>('all');
@ -84,6 +85,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
authorsMap={authorsMap}
defaultName={defaultName}
filter={filter}
hasQueries={hasQueries}
isElected={isElected}
isFavorite={isFavorite}
lastAuthors={lastAuthors}

View File

@ -14,7 +14,7 @@ import Summary from './Summary';
interface Props extends BareProps, ComponentProps {}
export default function Overview ({ allControllers, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
export default function Overview ({ allControllers, hasQueries, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useApiContext();
const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext);
const [next, setNext] = useState<string[]>([]);
@ -41,6 +41,7 @@ export default function Overview ({ allControllers, allStashes, className, recen
/>
<CurrentList
authorsMap={byAuthor}
hasQueries={hasQueries}
lastAuthors={lastBlockAuthors}
next={next}
recentlyOnline={recentlyOnline}

View File

@ -3,31 +3,29 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import { Balance, BlockNumber, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
import { Balance, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
import { SessionRewards, Slash } from '@polkadot/react-hooks/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Chart, Columar, Column } from '@polkadot/react-components';
import { toShortAddress } from '@polkadot/react-components/util';
import { withCalls } from '@polkadot/react-api';
import { getHistoric } from '@polkadot/react-api/util';
import { useApiContext } from '@polkadot/react-hooks';
import { trackStream, useApiContext } from '@polkadot/react-hooks';
import { u32 } from '@polkadot/types';
import { formatBalance, formatNumber } from '@polkadot/util';
import { MAX_SESSIONS } from '../constants';
import translate from '../translate';
interface Props extends I18nProps {
blockCounts?: BN[];
className?: string;
currentIndex: SessionIndex;
sessionRewards: SessionRewards[];
startNumber: BlockNumber;
validatorId: string;
}
type LineData = (BN | number)[][];
type LineDataEntry = (BN | number)[];
type LineData = LineDataEntry[];
interface SplitEntry {
colors: string[];
@ -39,37 +37,22 @@ type SplitData = SplitEntry[];
const COLORS_MINE = ['#ff8c00'];
const COLORS_OTHER = ['#acacac'];
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
const COLORS_BLOCKS = [undefined, '#acacac'];
function getIndexRange (currentIndex: SessionIndex): BN[] {
const range: BN[] = [];
let thisIndex: BN = currentIndex;
while (thisIndex.gtn(0) && range.length < MAX_SESSIONS) {
range.push(thisIndex);
thisIndex = thisIndex.subn(1);
}
return range.reverse();
}
function extractStake (values: [BN, Hash, Exposure][], divisor: BN): [string[], LineData] {
function extractStake (values: [Hash, Exposure][], divisor: BN): LineData {
return [
values.map(([bn]): string => formatNumber(bn)),
[
values.map(([,, { total }]): BN =>
total.unwrap().div(divisor))
// exposures.map(({ own }): BN =>
// own.unwrap().div(divisor)),
// exposures.map(({ others }): BN =>
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
]
values.map(([, { total }]): BN =>
total.unwrap().div(divisor))
// exposures.map(({ own }): BN =>
// own.unwrap().div(divisor)),
// exposures.map(({ others }): BN =>
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
];
}
function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): SplitData | null {
const last = values[values.length - 1][2];
function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null {
const last = values[values.length - 1][1];
const total = last.total.unwrap();
if (total.eqn(0)) {
@ -96,8 +79,17 @@ function extractEraSlash (validatorId: string, slashes: Slash[]): BN {
}, new BN(0));
}
function Validator ({ blockCounts, className, currentIndex, sessionRewards, startNumber, t, validatorId }: Props): React.ReactElement<Props> {
function balanceToNumber (amount: BN, divisor: BN): number {
return amount.muln(1000).div(divisor).toNumber() / 1000;
}
function Validator ({ className, sessionRewards, t, validatorId }: Props): React.ReactElement<Props> {
const { api } = useApiContext();
// FIXME There is something seriously wrong in these two with "any" horrors
const blockCounts = trackStream<u32[]>(api.query.imOnline?.authoredBlocks?.multi as any, [sessionRewards, validatorId], {
paramMap: ([sessionRewards, validatorId]: [SessionRewards[], string]): any =>
[sessionRewards.map(({ sessionIndex }): [SessionIndex, string] => [sessionIndex, validatorId])]
});
const [blocksLabels, setBlocksLabels] = useState<string[]>([]);
const [blocksChart, setBlocksChart] = useState<LineData | null>(null);
const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] });
@ -106,53 +98,65 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'));
useEffect((): void => {
api.isReady.then(async (): Promise<void> => {
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], {
interval: (api.consts.babe?.epochDuration as BlockNumber || new BN(500)).muln(2).divn(3),
max: MAX_SESSIONS,
startNumber
});
const [stakeLabels, stakeChart] = extractStake(values, divisor);
const splitChart = extractSplit(values, validatorId);
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;
if (!splitChart) {
const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash);
const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex));
setStakeInfo({ stakeChart, stakeLabels });
setSplitInfo({ splitChart, splitMax });
});
}, []);
api.isReady.then(async (): Promise<void> => {
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], hashes);
const stakeChart = extractStake(values, divisor);
const splitChart = extractSplit(values, validatorId);
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;
setStakeInfo({ stakeChart, stakeLabels });
setSplitInfo({ splitChart, splitMax });
});
}
}, [sessionRewards, splitChart]);
useEffect((): void => {
const rewardsLabels: string[] = [];
const rewardsChart: LineData = [[]];
if (blockCounts) {
const rewardsLabels: string[] = [];
const rewardsChart: LineData = [[], [], []];
let total = new BN(0);
sessionRewards.forEach(({ sessionIndex, slashes }): void => {
// this shows the start of the new era, however rewards are for previous
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));
sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => {
// this shows the start of the new era, however rewards are for previous
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));
// calculate and format to 3 decimals
rewardsChart[0].push(
extractEraSlash(validatorId, slashes).muln(1000).div(divisor).toNumber() / 1000
);
});
const neg = extractEraSlash(validatorId, slashes);
const pos = index
? reward.mul(blockCounts[index - 1]).div(blockNumber.sub(sessionRewards[index - 1].blockNumber))
: new BN(0);
setRewardsInfo({ rewardsChart, rewardsLabels });
}, [sessionRewards, validatorId]);
// add this to the total
total = total.add(neg).add(pos);
// calculate and format to 3 decimals
rewardsChart[0].push(balanceToNumber(neg, divisor));
rewardsChart[1].push(balanceToNumber(pos, divisor));
rewardsChart[2].push(balanceToNumber(total.divn(index), divisor));
});
setRewardsInfo({ rewardsChart, rewardsLabels });
}
}, [blockCounts, sessionRewards, validatorId]);
useEffect((): void => {
setBlocksLabels(
getIndexRange(currentIndex).map((index): string => formatNumber(index))
sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex))
);
}, [currentIndex]);
}, [sessionRewards]);
useEffect((): void => {
if (blockCounts) {
const avgSet: number[] = [];
const idxSet: BN[] = [];
blockCounts.reduce((total: BN, value, index): BN => {
blockCounts.reduce((total: BN, value: u32, index: number): BN => {
total = total.add(value);
avgSet.push(total.toNumber() / (index + 1));
avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100);
idxSet.push(value);
return total;
@ -160,7 +164,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
setBlocksChart([idxSet, avgSet]);
}
}, [blockCounts, blocksLabels]);
}, [blockCounts]);
return (
<Columar className={className}>
@ -169,7 +173,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
<>
{blocksChart && (
<div className='staking--Chart'>
<h1>{t('blocks per session')}</h1>
<h1>{t('blocks produced')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={blocksLabels}
@ -180,11 +184,11 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
)}
{rewardsChart && (
<div className='staking--Chart'>
<h1>{t('slashed per session')}</h1>
<h1>{t('rewards & slashes')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
colors={COLORS_REWARD}
labels={rewardsLabels}
legends={[t('slashed'), t('rewarded')]}
legends={[t('slashed'), t('rewards (est.)'), t('average')]}
values={rewardsChart}
/>
</div>
@ -222,13 +226,4 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
);
}
export default translate(
withCalls<Props>(
['query.imOnline.authoredBlocks', {
isMulti: true,
propName: 'blockCounts',
paramPick: ({ currentIndex, validatorId }: Props): [BN, string][] =>
getIndexRange(currentIndex).map((index): [BN, string] => [index, validatorId])
}]
)(Validator)
);
export default translate(Validator);

View File

@ -3,10 +3,9 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import { BlockNumber } from '@polkadot/types/interfaces';
import { ComponentProps } from '../types';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { Button, InputAddressSimple } from '@polkadot/react-components';
@ -47,16 +46,9 @@ interface Props extends I18nProps, ComponentProps, RouteComponentProps<{}> {
// />
// </Input>
function Query ({ bestNumber, className, sessionRewards, stakingOverview, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
const [startNumber, setStartNumber] = useState<BlockNumber | undefined>();
function Query ({ className, sessionRewards, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
const [validatorId, setValidatorId] = useState<string | null>(value || null);
useEffect((): void => {
if (bestNumber && !startNumber) {
setStartNumber(bestNumber);
}
}, [bestNumber, startNumber]);
const _onQuery = (): void => {
if (validatorId) {
window.location.hash = `/staking/query/${validatorId}`;
@ -79,11 +71,9 @@ function Query ({ bestNumber, className, sessionRewards, stakingOverview, match:
onClick={_onQuery}
/>
</InputAddressSimple>
{value && startNumber && stakingOverview && (
{value && (
<Validator
currentIndex={stakingOverview.currentIndex}
sessionRewards={sessionRewards}
startNumber={startNumber}
validatorId={value}
/>
)}

View File

@ -16,8 +16,8 @@ import styled from 'styled-components';
import { Option } from '@polkadot/types';
import { HelpOverlay } from '@polkadot/react-components';
import Tabs from '@polkadot/react-components/Tabs';
import { withCalls, withMulti, withObservable } from '@polkadot/react-api';
import { useApiContext, useSessionRewards } from '@polkadot/react-hooks';
import { withMulti, withObservable } from '@polkadot/react-api';
import { trackStream, useApiContext, useSessionRewards } from '@polkadot/react-hooks';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import Accounts from './Actions/Accounts';
@ -29,19 +29,32 @@ import translate from './translate';
interface Props extends AppProps, ApiProps, I18nProps {
allAccounts?: SubjectInfo;
allStashesAndControllers?: [string[], string[]];
bestNumber?: BlockNumber;
recentlyOnline?: DerivedHeartbeats;
stakingOverview?: DerivedStakingOverview;
}
const EMPY_ACCOUNTS: string[] = [];
const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, basePath, bestNumber, className, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
function transformStakingControllers ([stashes, controllers]: [AccountId[], Option<AccountId>[]]): [string[], string[]] {
return [
stashes.map((accountId): string => accountId.toString()),
controllers
.filter((optId): boolean => optId.isSome)
.map((accountId): string => accountId.unwrap().toString())
];
}
function App ({ allAccounts, basePath, className, t }: Props): React.ReactElement<Props> {
const { api } = useApiContext();
const stakingControllers = trackStream<[string[], string[]]>(api.derive.staking.controllers, [], { transform: transformStakingControllers });
const bestNumber = trackStream<BlockNumber>(api.derive.chain.bestNumber, []);
const recentlyOnline = trackStream<DerivedHeartbeats>(api.derive.imOnline.receivedHeartbeats, []);
const stakingOverview = trackStream<DerivedStakingOverview>(api.derive.staking.overview, []);
const sessionRewards = useSessionRewards(MAX_SESSIONS);
const routeMatch = useRouteMatch({ path: basePath, strict: true });
const hasAccounts = !!allAccounts && Object.keys(allAccounts).length !== 0;
const hasQueries = hasAccounts && !!(api.query.imOnline?.authoredBlocks);
const [allStashes, allControllers] = stakingControllers || EMPTY_ALL;
const _renderComponent = (Component: React.ComponentType<ComponentProps>, className?: string): () => React.ReactNode => {
// eslint-disable-next-line react/display-name
return (): React.ReactNode => {
@ -56,6 +69,8 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
allStashes={allStashes}
bestNumber={bestNumber}
className={className}
hasAccounts={hasAccounts}
hasQueries={hasQueries}
recentlyOnline={recentlyOnline}
sessionRewards={sessionRewards}
stakingOverview={stakingOverview}
@ -71,11 +86,11 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
<Tabs
basePath={basePath}
hidden={
!allAccounts || Object.keys(allAccounts).length === 0
? ['actions', 'query']
: api.query.imOnline?.authoredBlocks
hasAccounts
? hasQueries
? []
: ['query']
: ['actions', 'query']
}
items={[
{
@ -124,19 +139,5 @@ export default withMulti(
}
`,
translate,
withCalls<Props>(
['derive.chain.bestNumber', { propName: 'bestNumber' }],
['derive.imOnline.receivedHeartbeats', { propName: 'recentlyOnline' }],
['derive.staking.controllers', {
propName: 'allStashesAndControllers',
transform: ([stashes, controllers]: [AccountId[], Option<AccountId>[]]): [string[], string[]] => [
stashes.map((accountId): string => accountId.toString()),
controllers
.filter((optId): boolean => optId.isSome)
.map((accountId): string => accountId.unwrap().toString())
]
}],
['derive.staking.overview', { propName: 'stakingOverview' }]
),
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

@ -15,6 +15,8 @@ export interface ComponentProps {
allStashes: string[];
bestNumber?: BlockNumber;
className?: string;
hasAccounts: boolean;
hasQueries: boolean;
recentlyOnline?: DerivedHeartbeats;
sessionRewards: SessionRewards[];
stakingOverview?: DerivedStakingOverview;

View File

@ -5,21 +5,16 @@
import { Hash } from '@polkadot/types/interfaces';
import { Codec } from '@polkadot/types/types';
import BN from 'bn.js';
import ApiPromise from '@polkadot/api/promise';
import getHashes, { HashesOptions } from './hashes';
export default async function getHistory <T extends Codec> (api: ApiPromise, endpoint: string, params: any[], options: HashesOptions): Promise<[BN, Hash, T][]> {
export default async function getHistory <T extends Codec> (api: ApiPromise, endpoint: string, params: any[], hashes: Hash[]): Promise<[Hash, T][]> {
const [mod, fn] = endpoint.split('.');
const numbers = await getHashes(api, options);
const results = await Promise.all(numbers.map(([, hash]): Promise<T> =>
const results = await Promise.all(hashes.map((hash): Promise<T> =>
api.query[mod][fn].at(hash, ...params) as Promise<T>
));
return results.map((value, index): [BN, Hash, T] => [
numbers[index][0],
numbers[index][1],
return results.map((value, index): [Hash, T] => [
hashes[index],
value
]);
}

View File

@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as trackStream } from './trackStream';
export { default as useApiContext } from './useApiContext';
export { default as useDebounce } from './useDebounce';
export { default as useFavorites } from './useFavorites';

View File

@ -0,0 +1,90 @@
// Copyright 2017-2019 @polkadot/react-hooks 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 { useEffect, useRef, useState } from 'react';
import { isNull, isUndefined } from '@polkadot/util';
type Params = [] | [any] | [any, any] | [any, any, any];
interface Options <T> {
paramMap?: (params: any) => any;
transform?: (value: any) => T;
}
interface TrackFnCallback <T> {
(value: T): void;
}
type Unsub = () => void;
interface TrackFn <T> {
(a: any, b: any, c: any, cb: TrackFnCallback<T>): Promise<Unsub>;
(a: any, b: any, cb: TrackFnCallback<T>): Promise<Unsub>;
(a: any, cb: TrackFnCallback<T>): Promise<Unsub>;
(cb: TrackFnCallback<T>): Promise<Unsub>;
}
const dummySubscribe = Promise.resolve((): void => {
// we do nothing, this is a noop
});
function transformIdentity (value: any): any {
return value;
}
// extract the serialized and mapped params, all ready for use in our call
function extractParams (params: any[], paramMap: (params: any[]) => any): [string, Params | null] {
return [
JSON.stringify({ a: params }),
params.length === 0 || !params.some((param): boolean => isNull(param) || isUndefined(null))
? paramMap(params)
: null
];
}
// tracks a stream, typically an api.* call that
// - returns a promise with an unsubscription
// - has a callback to set the value
// FIXME The typings here need some serious TLC
export default function trackStream <T> (fn: TrackFn<T> | undefined, params: any, { paramMap = transformIdentity, transform = transformIdentity }: Options<T> = {}): T | undefined {
const [value, setValue] = useState<T | undefined>();
const tracker = useRef<{ serialized: string | null; subscriber: Promise<Unsub> }>({ serialized: null, subscriber: dummySubscribe });
const _subscribe = (params: Params): void => {
tracker.current.subscriber = fn
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore We tried to get the typings right, close but no cigar...
? fn(...params, (value: any): T => setValue(transform(value)))
: dummySubscribe;
};
const _unsubscribe = (): void => {
tracker.current.subscriber.then((fn): void => fn());
};
// initial round, subscribe once
useEffect((): () => void => {
const [serialized, mappedParams] = extractParams(params, paramMap);
tracker.current.serialized = serialized;
if (mappedParams) {
_subscribe(mappedParams);
}
return _unsubscribe;
}, []);
// on changes, re-subscribe
useEffect((): void => {
const [serialized, mappedParams] = extractParams(params, paramMap);
if (mappedParams && serialized !== tracker.current.serialized) {
tracker.current.serialized = serialized;
_unsubscribe();
_subscribe(mappedParams);
}
}, [params]);
return value;
}