Browse Source

Linting: Manual fixes (required refactoring usePromise)

Leszek Wiesner 4 years ago
parent
commit
2e655f7f8f

+ 2 - 1
pioneer/packages/joy-election/src/Applicants.tsx

@@ -16,8 +16,9 @@ import Section from '@polkadot/joy-utils/react/components/Section';
 import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
 import { ElectionStage } from '@joystream/types/src/council';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   candidacyLimit?: BN;
   applicants?: Array<AccountId>;
   stage?: Option<ElectionStage>;

+ 3 - 3
pioneer/packages/joy-election/src/Council.tsx

@@ -11,13 +11,13 @@ import { calcBackersStake } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
 import translate from './translate';
 import Section from '@polkadot/joy-utils/react/components/Section';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps &
-I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   council?: Seat[];
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Council extends React.PureComponent<Props, State> {
   state: State = {};

+ 3 - 2
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -13,8 +13,9 @@ import Section from '@polkadot/joy-utils/react/components/Section';
 import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStage, Seat } from '@joystream/types/council';
 import translate from './translate';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   bestNumber?: BN;
 
   activeCouncil?: Seat[];
@@ -34,7 +35,7 @@ type Props = ApiProps & I18nProps & {
   stage?: Option<ElectionStage>;
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Dashboard extends React.PureComponent<Props, State> {
   state: State = {};

+ 5 - 5
pioneer/packages/joy-election/src/Reveals.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
@@ -12,12 +12,12 @@ import { accountIdsToOptions, hashVote } from './utils';
 import TxButton from '@polkadot/joy-utils/react/components/TxButton';
 import { findVoteByHash } from './myVotesStore';
 import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { RouteProps } from 'react-router-dom';
 
 // AppsProps is needed to get a location from the route.
-type Props = AppProps & ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   applicantId?: string | null;
   applicants?: AccountId[];
-  location: any;
 };
 
 type State = {
@@ -31,8 +31,8 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
     super(props);
     let { applicantId, location } = this.props;
 
-    applicantId = applicantId || getUrlParam(location, 'applicantId');
-    const hashedVote = getUrlParam(location, 'hashedVote');
+    applicantId = applicantId || (location && getUrlParam(location, 'applicantId'));
+    const hashedVote = location && getUrlParam(location, 'hashedVote');
 
     this.state = {
       applicantId,

+ 4 - 4
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -4,7 +4,7 @@ import uuid from 'uuid/v4';
 import React from 'react';
 import { Message, Table } from 'semantic-ui-react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId, Balance } from '@polkadot/types/interfaces';
@@ -23,6 +23,7 @@ import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
 import MembersDropdown from '@polkadot/joy-utils/react/components/MembersDropdown';
 import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
+import { RouteProps } from 'react-router-dom';
 
 // TODO use a crypto-prooven generator instead of UUID 4.
 function randomSalt () {
@@ -30,11 +31,10 @@ function randomSalt () {
 }
 
 // AppsProps is needed to get a location from the route.
-type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   applicantId?: string | null;
   minVotingStake?: Balance;
   applicants?: AccountId[];
-  location?: any;
 };
 
 type State = {
@@ -51,7 +51,7 @@ class Component extends React.PureComponent<Props, State> {
 
     let { applicantId, location } = this.props;
 
-    applicantId = applicantId || getUrlParam(location, 'applicantId');
+    applicantId = applicantId || (location && getUrlParam(location, 'applicantId'));
 
     this.state = {
       applicantId,

+ 3 - 2
pioneer/packages/joy-election/src/Votes.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls } from '@polkadot/react-api/hoc';
 import { Message } from 'semantic-ui-react';
@@ -14,8 +14,9 @@ import { getVotesByVoter } from './myVotesStore';
 import VoteForm from './VoteForm';
 import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStage } from '@joystream/types/src/council';
+import { RouteProps } from 'react-router-dom';
 
-type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   stage?: Option<ElectionStage>;
 };
 

+ 4 - 3
pioneer/packages/joy-election/src/index.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { Route, Switch } from 'react-router';
 
 import { I18nProps } from '@polkadot/react-components/types';
-import { RouteProps } from '@polkadot/apps-routing/types';
+import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId, Hash } from '@polkadot/types/interfaces';
 import Tabs from '@polkadot/react-components/Tabs';
@@ -21,17 +21,18 @@ import Votes from './Votes';
 import Reveals from './Reveals';
 import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
+import { ApiProps } from '@polkadot/react-api/types';
 
 const ElectionMain = styled.main`${style}`;
 
 // define out internal types
-type Props = RouteProps & I18nProps & {
+type Props = AppMainRouteProps & ApiProps & I18nProps & {
   activeCouncil?: Seat[];
   applicants?: AccountId[];
   commitments?: Hash[];
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class App extends React.PureComponent<Props, State> {
   state: State = {};

+ 1 - 1
pioneer/packages/joy-election/src/myVotesStore.ts

@@ -19,7 +19,7 @@ export type SavedVote = NewVote & {
 
 /** Get all votes that are stored in a local sotrage.  */
 export const getAllVotes = (): SavedVote[] => {
-  const votes = store.get(MY_VOTES);
+  const votes = store.get(MY_VOTES) as unknown;
 
   return nonEmptyArr(votes) ? votes as SavedVote[] : [];
 };

+ 14 - 0
pioneer/packages/joy-utils/src/functions/misc.ts

@@ -6,6 +6,7 @@ import { Options as QueryOptions } from '@polkadot/react-api/hoc/types';
 import queryString from 'query-string';
 import { SubmittableResult } from '@polkadot/api';
 import { Codec } from '@polkadot/types/types';
+import { Location } from 'history';
 
 export const ZERO = new BN(0);
 
@@ -165,3 +166,16 @@ export function includeKeys<T extends { [k: string]: any }> (obj: T, ...allowedK
 export function bytesToString (bytes: Bytes) {
   return Buffer.from(bytes.toString().substr(2), 'hex').toString();
 }
+
+export function normalizeError (e: any): string {
+  let message: string;
+
+  if (e instanceof Error) {
+    message = e.message;
+  } else {
+    message = JSON.stringify(e);
+  }
+
+  // Prevent returning falsely value
+  return message || 'Unexpected error';
+}

+ 9 - 6
pioneer/packages/joy-utils/src/react/components/MembersDropdown.tsx

@@ -1,6 +1,5 @@
 import React, { useEffect, useState } from 'react';
 import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react';
-import { Membership } from '@joystream/types/members';
 import { MemberFromAccount } from '../../types/members';
 import { useTransport } from '../hooks';
 import { AccountId } from '@polkadot/types/interfaces';
@@ -14,14 +13,15 @@ const StyledMembersDropdown = styled(Dropdown)`
 `;
 
 function membersToOptions (members: MemberFromAccount[]) {
-  const validMembers = members.filter((m) => m.profile !== undefined) as (MemberFromAccount & { profile: Membership })[];
+  const validMembers = members.filter((m) => m.profile !== undefined);
 
+  // Here we can assert "profile!" and "memberId!", because we filtered out those that don't have it.
   return validMembers
     .map(({ memberId, profile, account }) => ({
-      key: profile.handle,
-      text: `${profile.handle} (id:${memberId})`,
+      key: profile!.handle.toString(),
+      text: `${profile!.handle.toString()} (id:${memberId!})`,
       value: account,
-      image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
+      image: profile!.avatar_uri.toString() ? { avatar: true, src: profile!.avatar_uri } : null
     }));
 }
 
@@ -50,9 +50,12 @@ const MembersDropdown: React.FunctionComponent<Props> = ({ accounts, ...passedPr
           setMembersOptions(membersToOptions(members));
           setLoading(false);
         }
-      });
+      })
+      .catch((e) => { throw e; });
 
     return () => { isSubscribed = false; };
+    // We don't need transport.members as dependency here, because we assume it's always the same, so:
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [accounts]);
 
   return (

+ 3 - 3
pioneer/packages/joy-utils/src/react/components/PromiseComponent.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { Container, Message, Loader } from 'semantic-ui-react';
 
 type ErrorProps = {
-  error: any;
+  error: string | null;
 };
 
 export function Error ({ error }: ErrorProps) {
@@ -12,7 +12,7 @@ export function Error ({ error }: ErrorProps) {
     <Container style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
       <Message negative>
         <Message.Header>Oops! We got an error!</Message.Header>
-        <p>{error.message}</p>
+        <p>{error}</p>
       </Message>
     </Container>
   );
@@ -32,7 +32,7 @@ export function Loading ({ text }: LoadingProps) {
 
 type PromiseComponentProps = {
   loading: boolean;
-  error: any;
+  error: string | null;
   message: string;
 }
 

+ 1 - 1
pioneer/packages/joy-utils/src/react/context/transport.tsx

@@ -6,7 +6,7 @@ import Transport from '../../transport/index';
 
 export const TransportContext = createContext<Transport>((null as unknown) as Transport);
 
-export function TransportProvider ({ children }: { children: React.PropsWithChildren<{}> }) {
+export function TransportProvider ({ children }: { children: React.PropsWithChildren<unknown> }) {
   const api: ApiProps = useContext(ApiContext);
 
   if (!api) {

+ 30 - 14
pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx

@@ -1,6 +1,8 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useRef } from 'react';
+import { normalizeError } from '../../functions/misc';
+import { randomBytes } from 'crypto';
 
-export type UsePromiseReturnValues<T> = [T, any, boolean, () => Promise<void|null>];
+export type UsePromiseReturnValues<T> = [T, string | null, boolean];
 
 export default function usePromise<T> (
   promise: () => Promise<T>,
@@ -10,44 +12,58 @@ export default function usePromise<T> (
 ): UsePromiseReturnValues<T> {
   const [state, setState] = useState<{
     value: T;
-    error: any;
+    error: string | null;
     isPending: boolean;
   }>({ value: defaultValue, error: null, isPending: true });
 
-  let isSubscribed = true;
-  const execute = useCallback(() => {
-    setState({ value: state.value, error: null, isPending: true });
+  const subscribedPromiseToken = useRef<string | null>(null);
 
-    return promise()
+  const executeAndSubscribePromise = () => {
+    setState((state) => ({ ...state, error: null, isPending: true }));
+
+    const thisPromiseToken = randomBytes(32).toString('hex');
+
+    subscribedPromiseToken.current = thisPromiseToken;
+
+    promise()
       .then((value) => {
-        if (isSubscribed) {
+        if (subscribedPromiseToken.current === thisPromiseToken) {
           setState({ value, error: null, isPending: false });
 
           if (onUpdate) {
             onUpdate(value);
           }
+        } else {
+          console.warn('usePromise: Token didn\'t match on .then()');
+          console.warn(`Subscribed promise: ${subscribedPromiseToken.current || 'NONE'}. Resolved promise: ${thisPromiseToken}.`);
         }
       })
       .catch((error) => {
-        if (isSubscribed) {
-          setState({ value: defaultValue, error: error, isPending: false });
+        if (subscribedPromiseToken.current === thisPromiseToken) {
+          setState({ value: defaultValue, error: normalizeError(error), isPending: false });
 
           if (onUpdate) {
             onUpdate(defaultValue); // This should represent an empty value in most cases
           }
+        } else {
+          console.warn('usePromise: Token didn\'t match on .catch()');
+          console.warn(`Subscribed promise: ${subscribedPromiseToken.current || 'NONE'}. Rejected promise: ${thisPromiseToken}.`);
         }
       });
-  }, [promise]);
+  };
 
   useEffect(() => {
-    execute();
+    executeAndSubscribePromise();
 
     return () => {
-      isSubscribed = false;
+      subscribedPromiseToken.current = null;
     };
+    // Silence "React Hook useEffect was passed a dependency list that is not an array literal",
+    // since we want to preserve the ability to pass custom "depnendencies".
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, dependsOn);
 
   const { value, error, isPending } = state;
 
-  return [value, error, isPending, execute];
+  return [value, error, isPending];
 }

+ 5 - 3
pioneer/packages/joy-utils/src/transport/APIQueryCache.ts

@@ -24,15 +24,17 @@ export class APIQueryCache {
     this.api = api;
     this.buildQuery();
     this.cache = new Map<string, Codec>();
-    this.breakCacheOnNewBlocks();
+    this.breakCacheOnNewBlocks()
+      .then((unsub) => { this.unsubscribeFn = unsub; })
+      .catch((e) => { throw e; });
   }
 
   unsubscribe () {
     this.unsubscribeFn();
   }
 
-  protected async breakCacheOnNewBlocks () {
-    this.unsubscribeFn = await this.api.rpc.chain.subscribeNewHeads((header) => {
+  protected breakCacheOnNewBlocks () {
+    return this.api.rpc.chain.subscribeNewHeads((header) => {
       this.cache = new Map<string, Codec>();
       // console.log("cache hits in this block", this.cacheHits)
       this.cacheHits = 0;

+ 0 - 1
pioneer/packages/joy-utils/src/transport/base.ts

@@ -1,5 +1,4 @@
 import { ApiPromise } from '@polkadot/api';
-import { Codec } from '@polkadot/types/types';
 import { APIQueryCache } from './APIQueryCache';
 
 export default abstract class BaseTransport {