Fix data fetching races in explorer (#11468)

This commit is contained in:
Justin Starry
2020-08-08 20:47:07 +08:00
committed by GitHub
parent 3d97b04815
commit 102d15f081
4 changed files with 101 additions and 85 deletions

View File

@ -49,31 +49,41 @@ export function TransactionDetailsPage({ signature }: Props) {
function StatusCard({ signature }: Props) { function StatusCard({ signature }: Props) {
const fetchStatus = useFetchTransactionStatus(); const fetchStatus = useFetchTransactionStatus();
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
const refresh = useFetchTransactionStatus(); const fetchDetails = useFetchTransactionDetails();
const details = useTransactionDetails(signature); const details = useTransactionDetails(signature);
const { firstAvailableBlock, status: clusterStatus } = useCluster(); const { firstAvailableBlock, status: clusterStatus } = useCluster();
const refresh = React.useCallback(
(signature: string) => {
fetchStatus(signature);
fetchDetails(signature);
},
[fetchStatus, fetchDetails]
);
// Fetch transaction on load // Fetch transaction on load
React.useEffect(() => { React.useEffect(() => {
if (!status && clusterStatus === ClusterStatus.Connected) if (!status && clusterStatus === ClusterStatus.Connected) {
fetchStatus(signature); fetchStatus(signature);
}
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps }, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps
if (!status || status.fetchStatus === FetchStatus.Fetching) { if (!status || status.fetchStatus === FetchStatus.Fetching) {
return <LoadingCard />; return <LoadingCard />;
} else if (status?.fetchStatus === FetchStatus.FetchFailed) { } else if (status?.fetchStatus === FetchStatus.FetchFailed) {
return <ErrorCard retry={() => refresh(signature)} text="Fetch Failed" />; return (
<ErrorCard retry={() => fetchStatus(signature)} text="Fetch Failed" />
);
} else if (!status.info) { } else if (!status.info) {
if (firstAvailableBlock !== undefined) { if (firstAvailableBlock !== undefined) {
return ( return (
<ErrorCard <ErrorCard
retry={() => refresh(signature)} retry={() => fetchStatus(signature)}
text="Not Found" text="Not Found"
subtext={`Note: Transactions processed before block ${firstAvailableBlock} are not available at this time`} subtext={`Note: Transactions processed before block ${firstAvailableBlock} are not available at this time`}
/> />
); );
} }
return <ErrorCard retry={() => refresh(signature)} text="Not Found" />; return <ErrorCard retry={() => fetchStatus(signature)} text="Not Found" />;
} }
const { info } = status; const { info } = status;
@ -187,9 +197,8 @@ function StatusCard({ signature }: Props) {
} }
function AccountsCard({ signature }: Props) { function AccountsCard({ signature }: Props) {
const details = useTransactionDetails(signature);
const { url } = useCluster(); const { url } = useCluster();
const details = useTransactionDetails(signature);
const fetchStatus = useFetchTransactionStatus(); const fetchStatus = useFetchTransactionStatus();
const fetchDetails = useFetchTransactionDetails(); const fetchDetails = useFetchTransactionDetails();
const refreshStatus = () => fetchStatus(signature); const refreshStatus = () => fetchStatus(signature);
@ -198,6 +207,13 @@ function AccountsCard({ signature }: Props) {
const message = transaction?.message; const message = transaction?.message;
const status = useTransactionStatus(signature); const status = useTransactionStatus(signature);
// Fetch details on load
React.useEffect(() => {
if (status?.info?.confirmations === "max" && !details) {
fetchDetails(signature);
}
}, [signature, details, status, fetchDetails]);
if (!status || !status.info) { if (!status || !status.info) {
return null; return null;
} else if (!details) { } else if (!details) {

View File

@ -29,6 +29,7 @@ export interface Account {
type Accounts = { [address: string]: Account }; type Accounts = { [address: string]: Account };
interface State { interface State {
accounts: Accounts; accounts: Accounts;
url: string;
} }
export enum ActionType { export enum ActionType {
@ -39,6 +40,7 @@ export enum ActionType {
interface Update { interface Update {
type: ActionType.Update; type: ActionType.Update;
url: string;
pubkey: PublicKey; pubkey: PublicKey;
data: { data: {
status: FetchStatus; status: FetchStatus;
@ -49,17 +51,25 @@ interface Update {
interface Fetch { interface Fetch {
type: ActionType.Fetch; type: ActionType.Fetch;
url: string;
pubkey: PublicKey; pubkey: PublicKey;
} }
interface Clear { interface Clear {
type: ActionType.Clear; type: ActionType.Clear;
url: string;
} }
type Action = Update | Fetch | Clear; type Action = Update | Fetch | Clear;
type Dispatch = (action: Action) => void; type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State { function reducer(state: State, action: Action): State {
if (action.type === ActionType.Clear) {
return { url: action.url, accounts: {} };
} else if (action.url !== state.url) {
return state;
}
switch (action.type) { switch (action.type) {
case ActionType.Fetch: { case ActionType.Fetch: {
const address = action.pubkey.toBase58(); const address = action.pubkey.toBase58();
@ -100,13 +110,6 @@ function reducer(state: State, action: Action): State {
} }
break; break;
} }
case ActionType.Clear: {
return {
...state,
accounts: {},
};
}
} }
return state; return state;
} }
@ -116,14 +119,15 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type AccountsProviderProps = { children: React.ReactNode }; type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) { export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster();
const [state, dispatch] = React.useReducer(reducer, { const [state, dispatch] = React.useReducer(reducer, {
url,
accounts: {}, accounts: {},
}); });
// Clear account statuses whenever cluster is changed // Clear account statuses whenever cluster is changed
const { url } = useCluster();
React.useEffect(() => { React.useEffect(() => {
dispatch({ type: ActionType.Clear }); dispatch({ type: ActionType.Clear, url });
}, [url]); }, [url]);
return ( return (
@ -145,6 +149,7 @@ async function fetchAccountInfo(
dispatch({ dispatch({
type: ActionType.Fetch, type: ActionType.Fetch,
pubkey, pubkey,
url,
}); });
let fetchStatus; let fetchStatus;
@ -182,7 +187,7 @@ async function fetchAccountInfo(
fetchStatus = FetchStatus.FetchFailed; fetchStatus = FetchStatus.FetchFailed;
} }
const data = { status: fetchStatus, lamports, details }; const data = { status: fetchStatus, lamports, details };
dispatch({ type: ActionType.Update, data, pubkey }); dispatch({ type: ActionType.Update, data, pubkey, url });
} }
export function useAccounts() { export function useAccounts() {

View File

@ -5,7 +5,7 @@ import {
ParsedConfirmedTransaction, ParsedConfirmedTransaction,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useCluster } from "../cluster"; import { useCluster } from "../cluster";
import { useTransactions, FetchStatus } from "./index"; import { FetchStatus } from "./index";
import { CACHED_DETAILS, isCached } from "./cached"; import { CACHED_DETAILS, isCached } from "./cached";
export interface Details { export interface Details {
@ -13,68 +13,69 @@ export interface Details {
transaction: ParsedConfirmedTransaction | null; transaction: ParsedConfirmedTransaction | null;
} }
type State = { [signature: string]: Details }; type State = {
entries: { [signature: string]: Details };
url: string;
};
export enum ActionType { export enum ActionType {
Update, Update,
Add,
Clear, Clear,
} }
interface Update { interface Update {
type: ActionType.Update; type: ActionType.Update;
url: string;
signature: string; signature: string;
fetchStatus: FetchStatus; fetchStatus: FetchStatus;
transaction: ParsedConfirmedTransaction | null; transaction: ParsedConfirmedTransaction | null;
} }
interface Add {
type: ActionType.Add;
signature: TransactionSignature;
}
interface Clear { interface Clear {
type: ActionType.Clear; type: ActionType.Clear;
url: string;
} }
type Action = Update | Add | Clear; type Action = Update | Clear;
type Dispatch = (action: Action) => void; type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State { function reducer(state: State, action: Action): State {
switch (action.type) { if (action.type === ActionType.Clear) {
case ActionType.Add: { return { url: action.url, entries: {} };
const details = { ...state }; } else if (action.url !== state.url) {
const signature = action.signature; return state;
if (!details[signature]) {
details[signature] = {
fetchStatus: FetchStatus.Fetching,
transaction: null,
};
}
return details;
} }
switch (action.type) {
case ActionType.Update: { case ActionType.Update: {
let details = state[action.signature]; const signature = action.signature;
const details = state.entries[signature];
if (details) { if (details) {
details = { return {
...state,
entries: {
...state.entries,
[signature]: {
...details, ...details,
fetchStatus: action.fetchStatus, fetchStatus: action.fetchStatus,
transaction: action.transaction, transaction: action.transaction,
},
},
}; };
} else {
return { return {
...state, ...state,
[action.signature]: details, entries: {
...state.entries,
[signature]: {
fetchStatus: FetchStatus.Fetching,
transaction: null,
},
},
}; };
} }
break;
}
case ActionType.Clear: {
return {};
} }
} }
return state;
} }
export const StateContext = React.createContext<State | undefined>(undefined); export const StateContext = React.createContext<State | undefined>(undefined);
@ -84,28 +85,13 @@ export const DispatchContext = React.createContext<Dispatch | undefined>(
type DetailsProviderProps = { children: React.ReactNode }; type DetailsProviderProps = { children: React.ReactNode };
export function DetailsProvider({ children }: DetailsProviderProps) { export function DetailsProvider({ children }: DetailsProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {});
const { transactions, lastFetched } = useTransactions();
const { url } = useCluster(); const { url } = useCluster();
const [state, dispatch] = React.useReducer(reducer, { url, entries: {} });
React.useEffect(() => { React.useEffect(() => {
dispatch({ type: ActionType.Clear }); dispatch({ type: ActionType.Clear, url });
}, [url]); }, [url]);
// Filter blocks for current transaction slots
React.useEffect(() => {
if (lastFetched) {
const confirmed =
transactions[lastFetched] &&
transactions[lastFetched].info?.confirmations === "max";
const noDetails = !state[lastFetched];
if (confirmed && noDetails) {
dispatch({ type: ActionType.Add, signature: lastFetched });
fetchDetails(dispatch, lastFetched, url);
}
}
}, [transactions, lastFetched]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<StateContext.Provider value={state}> <StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}> <DispatchContext.Provider value={dispatch}>
@ -125,6 +111,7 @@ async function fetchDetails(
fetchStatus: FetchStatus.Fetching, fetchStatus: FetchStatus.Fetching,
transaction: null, transaction: null,
signature, signature,
url,
}); });
let fetchStatus; let fetchStatus;
@ -143,7 +130,13 @@ async function fetchDetails(
fetchStatus = FetchStatus.FetchFailed; fetchStatus = FetchStatus.FetchFailed;
} }
} }
dispatch({ type: ActionType.Update, fetchStatus, signature, transaction }); dispatch({
type: ActionType.Update,
fetchStatus,
signature,
transaction,
url,
});
} }
export function useFetchTransactionDetails() { export function useFetchTransactionDetails() {

View File

@ -44,7 +44,7 @@ export interface TransactionStatus {
type Transactions = { [signature: string]: TransactionStatus }; type Transactions = { [signature: string]: TransactionStatus };
interface State { interface State {
transactions: Transactions; transactions: Transactions;
lastFetched: TransactionSignature | undefined; url: string;
} }
export enum ActionType { export enum ActionType {
@ -55,6 +55,7 @@ export enum ActionType {
interface UpdateStatus { interface UpdateStatus {
type: ActionType.UpdateStatus; type: ActionType.UpdateStatus;
url: string;
signature: TransactionSignature; signature: TransactionSignature;
fetchStatus: FetchStatus; fetchStatus: FetchStatus;
info?: TransactionStatusInfo; info?: TransactionStatusInfo;
@ -62,17 +63,25 @@ interface UpdateStatus {
interface FetchSignature { interface FetchSignature {
type: ActionType.FetchSignature; type: ActionType.FetchSignature;
url: string;
signature: TransactionSignature; signature: TransactionSignature;
} }
interface Clear { interface Clear {
type: ActionType.Clear; type: ActionType.Clear;
url: string;
} }
type Action = UpdateStatus | FetchSignature | Clear; type Action = UpdateStatus | FetchSignature | Clear;
type Dispatch = (action: Action) => void; type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State { function reducer(state: State, action: Action): State {
if (action.type === ActionType.Clear) {
return { url: action.url, transactions: {} };
} else if (action.url !== state.url) {
return state;
}
switch (action.type) { switch (action.type) {
case ActionType.FetchSignature: { case ActionType.FetchSignature: {
const signature = action.signature; const signature = action.signature;
@ -86,7 +95,7 @@ function reducer(state: State, action: Action): State {
info: undefined, info: undefined,
}, },
}; };
return { ...state, transactions, lastFetched: signature }; return { ...state, transactions };
} else { } else {
const transactions = { const transactions = {
...state.transactions, ...state.transactions,
@ -95,7 +104,7 @@ function reducer(state: State, action: Action): State {
fetchStatus: FetchStatus.Fetching, fetchStatus: FetchStatus.Fetching,
}, },
}; };
return { ...state, transactions, lastFetched: signature }; return { ...state, transactions };
} }
} }
@ -114,13 +123,6 @@ function reducer(state: State, action: Action): State {
} }
break; break;
} }
case ActionType.Clear: {
return {
...state,
transactions: {},
};
}
} }
return state; return state;
} }
@ -132,12 +134,12 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type TransactionsProviderProps = { children: React.ReactNode }; type TransactionsProviderProps = { children: React.ReactNode };
export function TransactionsProvider({ children }: TransactionsProviderProps) { export function TransactionsProvider({ children }: TransactionsProviderProps) {
const { cluster, status: clusterStatus, url } = useCluster();
const [state, dispatch] = React.useReducer(reducer, { const [state, dispatch] = React.useReducer(reducer, {
transactions: {}, transactions: {},
lastFetched: undefined, url,
}); });
const { cluster, status: clusterStatus, url } = useCluster();
const fetchAccount = useFetchAccountInfo(); const fetchAccount = useFetchAccountInfo();
const query = useQuery(); const query = useQuery();
const testFlag = query.get("test"); const testFlag = query.get("test");
@ -145,9 +147,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
// Check transaction statuses whenever cluster updates // Check transaction statuses whenever cluster updates
React.useEffect(() => { React.useEffect(() => {
if (clusterStatus === ClusterStatus.Connecting) { if (clusterStatus === ClusterStatus.Connecting) {
dispatch({ type: ActionType.Clear }); dispatch({ type: ActionType.Clear, url });
} else if (clusterStatus === ClusterStatus.Connected && state.lastFetched) {
fetchTransactionStatus(dispatch, state.lastFetched, url);
} }
// Create a test transaction // Create a test transaction
@ -216,6 +216,7 @@ export async function fetchTransactionStatus(
dispatch({ dispatch({
type: ActionType.FetchSignature, type: ActionType.FetchSignature,
signature, signature,
url,
}); });
let fetchStatus; let fetchStatus;
@ -261,6 +262,7 @@ export async function fetchTransactionStatus(
signature, signature,
fetchStatus, fetchStatus,
info, info,
url,
}); });
} }
@ -295,7 +297,7 @@ export function useTransactionDetails(signature: TransactionSignature) {
); );
} }
return context[signature]; return context.entries[signature];
} }
export function useFetchTransactionStatus() { export function useFetchTransactionStatus() {