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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 85 deletions

View File

@ -49,31 +49,41 @@ export function TransactionDetailsPage({ signature }: Props) {
function StatusCard({ signature }: Props) {
const fetchStatus = useFetchTransactionStatus();
const status = useTransactionStatus(signature);
const refresh = useFetchTransactionStatus();
const fetchDetails = useFetchTransactionDetails();
const details = useTransactionDetails(signature);
const { firstAvailableBlock, status: clusterStatus } = useCluster();
const refresh = React.useCallback(
(signature: string) => {
fetchStatus(signature);
fetchDetails(signature);
},
[fetchStatus, fetchDetails]
);
// Fetch transaction on load
React.useEffect(() => {
if (!status && clusterStatus === ClusterStatus.Connected)
if (!status && clusterStatus === ClusterStatus.Connected) {
fetchStatus(signature);
}
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps
if (!status || status.fetchStatus === FetchStatus.Fetching) {
return <LoadingCard />;
} 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) {
if (firstAvailableBlock !== undefined) {
return (
<ErrorCard
retry={() => refresh(signature)}
retry={() => fetchStatus(signature)}
text="Not Found"
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;
@ -187,9 +197,8 @@ function StatusCard({ signature }: Props) {
}
function AccountsCard({ signature }: Props) {
const details = useTransactionDetails(signature);
const { url } = useCluster();
const details = useTransactionDetails(signature);
const fetchStatus = useFetchTransactionStatus();
const fetchDetails = useFetchTransactionDetails();
const refreshStatus = () => fetchStatus(signature);
@ -198,6 +207,13 @@ function AccountsCard({ signature }: Props) {
const message = transaction?.message;
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) {
return null;
} else if (!details) {

View File

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

View File

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

View File

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