Fix data fetching races in explorer (#11468)
This commit is contained in:
parent
3d97b04815
commit
102d15f081
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue