From 95f53918aedc6392fb0102a9f1e8ef27d0b18bb0 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Sun, 22 Dec 2024 23:35:23 +0800 Subject: [PATCH] chore: replace the old version with a more maintainable Network Reachability test. (#6392) --- packages/components/src/hooks/index.tsx | 2 + .../src/hooks/useDeferredPromise.ts | 48 +++++ packages/components/src/hooks/useNetInfo.ts | 183 ++++++++++++++++++ packages/kit/src/components/NetworkAlert.tsx | 5 +- .../kit/src/components/Spotlight/index.tsx | 9 +- packages/kit/src/hooks/useDeferredPromise.ts | 47 ----- packages/kit/src/hooks/usePromiseResult.ts | 4 +- .../Container/NetworkReachabilityTracker.tsx | 36 ++-- .../kit/src/views/Market/MarketDetail.tsx | 4 +- .../Market/components/TokenDetailTabs.tsx | 4 +- .../Market/components/TokenPriceChart.tsx | 6 +- .../pages/List/DevSettingsSection/NetInfo.tsx | 34 +--- packages/shared/src/eventBus/appEventBus.ts | 2 + .../@react-native-community/netinfo/index.ts | 16 -- .../shared/src/request/axiosInterceptor.ts | 7 +- .../shared/src/request/fetchInterceptor.ts | 1 + 16 files changed, 287 insertions(+), 121 deletions(-) create mode 100644 packages/components/src/hooks/useDeferredPromise.ts create mode 100644 packages/components/src/hooks/useNetInfo.ts delete mode 100644 packages/kit/src/hooks/useDeferredPromise.ts delete mode 100644 packages/shared/src/modules3rdParty/@react-native-community/netinfo/index.ts diff --git a/packages/components/src/hooks/index.tsx b/packages/components/src/hooks/index.tsx index 765bac818d3..77a52f2f146 100644 --- a/packages/components/src/hooks/index.tsx +++ b/packages/components/src/hooks/index.tsx @@ -1,7 +1,9 @@ export * from './useBackHandler'; +export * from './useDeferredPromise'; export * from './useForm'; export * from './useKeyboard'; export * from './useLayout'; +export * from './useNetInfo'; export * from './useClipboard'; export * from './useColor'; export * from './usePreventRemove'; diff --git a/packages/components/src/hooks/useDeferredPromise.ts b/packages/components/src/hooks/useDeferredPromise.ts new file mode 100644 index 00000000000..90ce01273b4 --- /dev/null +++ b/packages/components/src/hooks/useDeferredPromise.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; + +enum EDeferStatus { + pending = 'pending', + resolved = 'resolved', + rejected = 'rejected', +} +export type IDeferredPromise = { + resolve: (value: DeferType) => void; + reject: (value: unknown) => void; + reset: () => void; + promise: Promise; + status: EDeferStatus; +}; + +export const buildDeferredPromise = () => { + const deferred = {} as IDeferredPromise; + + const buildPromise = () => { + const promise = new Promise((resolve, reject) => { + deferred.status = EDeferStatus.pending; + deferred.resolve = (value: DeferType) => { + deferred.status = EDeferStatus.resolved; + resolve(value); + }; + deferred.reject = (reason: unknown) => { + deferred.status = EDeferStatus.rejected; + reject(reason); + }; + }); + + deferred.promise = promise; + }; + + buildPromise(); + + deferred.reset = () => { + if (deferred.status !== EDeferStatus.pending) { + buildPromise(); + } + }; + return deferred; +}; + +export function useDeferredPromise() { + const defer = useMemo(() => buildDeferredPromise(), []); + return defer; +} diff --git a/packages/components/src/hooks/useNetInfo.ts b/packages/components/src/hooks/useNetInfo.ts new file mode 100644 index 00000000000..b3f55deb61e --- /dev/null +++ b/packages/components/src/hooks/useNetInfo.ts @@ -0,0 +1,183 @@ +import { useEffect, useState } from 'react'; + +import { buildDeferredPromise } from './useDeferredPromise'; +import { + getCurrentVisibilityState, + onVisibilityStateChange, +} from './useVisibilityChange'; + +export interface IReachabilityConfiguration { + reachabilityUrl: string; + reachabilityTest?: (response: { status: number }) => Promise; + reachabilityMethod?: 'GET' | 'POST'; + reachabilityLongTimeout?: number; + reachabilityShortTimeout?: number; + reachabilityRequestTimeout?: number; +} + +export interface IReachabilityState { + isInternetReachable: boolean | null; +} + +class NetInfo { + state: IReachabilityState = { + isInternetReachable: null, + }; + + prevIsInternetReachable = false; + + listeners: Array<(state: { isInternetReachable: boolean | null }) => void> = + []; + + defer = buildDeferredPromise(); + + configuration = { + reachabilityUrl: '', + reachabilityMethod: 'GET', + reachabilityTest: (response: { status: number }) => + Promise.resolve(response.status === 200), + reachabilityLongTimeout: 60 * 1000, + reachabilityShortTimeout: 5 * 1000, + reachabilityRequestTimeout: 10 * 1000, + }; + + isFetching = false; + + pollingTimeoutId: ReturnType | null = null; + + constructor(configuration: IReachabilityConfiguration) { + this.configure(configuration); + + const handleVisibilityChange = (isVisible: boolean) => { + if (isVisible) { + this.defer.resolve(undefined); + } else { + this.defer.reset(); + } + }; + + const isVisible = getCurrentVisibilityState(); + handleVisibilityChange(isVisible); + onVisibilityStateChange(handleVisibilityChange); + } + + configure(configuration: IReachabilityConfiguration) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + } + + currentState() { + return this.state; + } + + updateState(state: { isInternetReachable: boolean | null }) { + this.state = state; + this.listeners.forEach((listener) => listener(state)); + this.prevIsInternetReachable = !!state.isInternetReachable; + } + + addEventListener( + listener: (state: { isInternetReachable: boolean | null }) => void, + ) { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter((l) => l !== listener); + }; + } + + async fetch() { + if (this.isFetching) return; + this.isFetching = true; + await this.defer.promise; + + const { + reachabilityRequestTimeout, + reachabilityUrl, + reachabilityMethod, + reachabilityTest, + } = this.configuration; + + const controller = new AbortController(); + const timeoutId = setTimeout( + () => controller.abort(), + reachabilityRequestTimeout, + ); + + try { + const response = await fetch(reachabilityUrl, { + method: reachabilityMethod, + signal: controller.signal, + }); + + this.updateState({ + isInternetReachable: await reachabilityTest(response), + }); + } catch (error) { + console.error('Failed to fetch reachability:', error); + this.updateState({ isInternetReachable: false }); + } finally { + clearTimeout(timeoutId); + this.isFetching = false; + const { reachabilityShortTimeout, reachabilityLongTimeout } = + this.configuration; + this.pollingTimeoutId = setTimeout( + () => { + void this.fetch(); + }, + this.prevIsInternetReachable + ? reachabilityLongTimeout + : reachabilityShortTimeout, + ); + } + } + + async start() { + void this.fetch(); + } + + async refresh() { + if (this.pollingTimeoutId) { + clearTimeout(this.pollingTimeoutId); + } + void this.fetch(); + } +} + +export const globalNetInfo = new NetInfo({ + reachabilityUrl: '/wallet/v1/health', +}); + +export const configureNetInfo = (configuration: IReachabilityConfiguration) => { + globalNetInfo.configure(configuration); + void globalNetInfo.start(); +}; + +export const refreshNetInfo = () => { + void globalNetInfo.refresh(); +}; + +export const useNetInfo = () => { + const [reachabilityState, setReachabilityState] = useState< + IReachabilityState & { + isRawInternetReachable: boolean | null; + } + >(() => { + const { isInternetReachable } = globalNetInfo.currentState(); + return { + isInternetReachable: isInternetReachable ?? true, + isRawInternetReachable: isInternetReachable, + }; + }); + useEffect(() => { + const remove = globalNetInfo.addEventListener(({ isInternetReachable }) => { + setReachabilityState({ + isInternetReachable: isInternetReachable ?? true, + isRawInternetReachable: isInternetReachable, + }); + }); + return remove; + }, []); + return reachabilityState; +}; diff --git a/packages/kit/src/components/NetworkAlert.tsx b/packages/kit/src/components/NetworkAlert.tsx index 9972895e081..f9039d4920e 100644 --- a/packages/kit/src/components/NetworkAlert.tsx +++ b/packages/kit/src/components/NetworkAlert.tsx @@ -2,12 +2,11 @@ import { memo } from 'react'; import { useIntl } from 'react-intl'; -import { Alert } from '@onekeyhq/components'; +import { Alert, useNetInfo } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; -import { useNetInfo } from '@onekeyhq/shared/src/modules3rdParty/@react-native-community/netinfo'; function BasicNetworkAlert() { - const { isInternetReachable, isRawInternetReachable } = useNetInfo(); + const { isInternetReachable } = useNetInfo(); const intl = useIntl(); return isInternetReachable ? null : ( = { - resolve: (value: DeferType) => void; - reject: (value: unknown) => void; - reset: () => void; - promise: Promise; - status: EDeferStatus; -}; - -export function useDeferredPromise() { - const defer = useMemo(() => { - const deferred = {} as IDeferredPromise; - - const buildPromise = () => { - const promise = new Promise((resolve, reject) => { - deferred.status = EDeferStatus.pending; - deferred.resolve = (value: DeferType) => { - deferred.status = EDeferStatus.resolved; - resolve(value); - }; - deferred.reject = (reason: unknown) => { - deferred.status = EDeferStatus.rejected; - reject(reason); - }; - }); - - deferred.promise = promise; - }; - - buildPromise(); - - deferred.reset = () => { - if (deferred.status !== EDeferStatus.pending) { - buildPromise(); - } - }; - return deferred; - }, []); - - return defer; -} diff --git a/packages/kit/src/hooks/usePromiseResult.ts b/packages/kit/src/hooks/usePromiseResult.ts index 87553fcc278..d9bc7a4a82b 100644 --- a/packages/kit/src/hooks/usePromiseResult.ts +++ b/packages/kit/src/hooks/usePromiseResult.ts @@ -5,13 +5,13 @@ import { debounce, isEmpty } from 'lodash'; import { getCurrentVisibilityState, onVisibilityStateChange, + useDeferredPromise, + useNetInfo, } from '@onekeyhq/components'; import { useRouteIsFocused as useIsFocused } from '@onekeyhq/kit/src/hooks/useRouteIsFocused'; -import { useNetInfo } from '@onekeyhq/shared/src/modules3rdParty/@react-native-community/netinfo'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; -import { useDeferredPromise } from './useDeferredPromise'; import { useIsMounted } from './useIsMounted'; import { usePrevious } from './usePrevious'; diff --git a/packages/kit/src/provider/Container/NetworkReachabilityTracker.tsx b/packages/kit/src/provider/Container/NetworkReachabilityTracker.tsx index 05ce99d3a55..0bd0602d0c1 100644 --- a/packages/kit/src/provider/Container/NetworkReachabilityTracker.tsx +++ b/packages/kit/src/provider/Container/NetworkReachabilityTracker.tsx @@ -1,36 +1,42 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; -import { getCurrentVisibilityState } from '@onekeyhq/components'; +import { configureNetInfo, refreshNetInfo } from '@onekeyhq/components'; import { useDevSettingsPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; -import type { IDevSettingsPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import { getEndpointsMapByDevSettings } from '@onekeyhq/shared/src/config/endpointsMap'; -import { configure as configureNetInfo } from '@onekeyhq/shared/src/modules3rdParty/@react-native-community/netinfo'; +import { + EAppEventBusNames, + appEventBus, +} from '@onekeyhq/shared/src/eventBus/appEventBus'; const REACHABILITY_LONG_TIMEOUT = 60 * 1000; const REACHABILITY_SHORT_TIMEOUT = 5 * 1000; const REACHABILITY_REQUEST_TIMEOUT = 10 * 1000; -const checkNetInfo = async (devSettings: IDevSettingsPersistAtom) => { - const endpoints = getEndpointsMapByDevSettings(devSettings); +const checkNetInfo = async (endpoint: string) => { configureNetInfo({ - reachabilityUrl: `${endpoints.wallet}/wallet/v1/health`, - reachabilityMethod: 'GET', - reachabilityTest: async (response) => response.status === 200, + reachabilityUrl: `${endpoint}/wallet/v1/health`, reachabilityLongTimeout: REACHABILITY_LONG_TIMEOUT, reachabilityShortTimeout: REACHABILITY_SHORT_TIMEOUT, reachabilityRequestTimeout: REACHABILITY_REQUEST_TIMEOUT, - // TODO: Rewrite to periodically check reachability - reachabilityShouldRun: () => true, - shouldFetchWiFiSSID: false, - useNativeReachability: false, }); }; const useNetInfo = () => { const [devSettings] = useDevSettingsPersistAtom(); + const walletEndpoints = useMemo( + () => getEndpointsMapByDevSettings(devSettings).wallet, + [devSettings], + ); useEffect(() => { - void checkNetInfo(devSettings); - }, [devSettings]); + void checkNetInfo(walletEndpoints); + const callback = () => { + refreshNetInfo(); + }; + appEventBus.on(EAppEventBusNames.RefreshNetInfo, callback); + return () => { + appEventBus.off(EAppEventBusNames.RefreshNetInfo, callback); + }; + }, [walletEndpoints]); }; export function NetworkReachabilityTracker() { diff --git a/packages/kit/src/views/Market/MarketDetail.tsx b/packages/kit/src/views/Market/MarketDetail.tsx index 0371b92987c..a011e80cbe8 100644 --- a/packages/kit/src/views/Market/MarketDetail.tsx +++ b/packages/kit/src/views/Market/MarketDetail.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { CommonActions, StackActions } from '@react-navigation/native'; +import type { IPageScreenProps } from '@onekeyhq/components'; import { HeaderIconButton, NavBackButton, @@ -12,10 +13,10 @@ import { View, XStack, YStack, + useDeferredPromise, useMedia, useShare, } from '@onekeyhq/components'; -import type { IPageScreenProps } from '@onekeyhq/components'; import { EJotaiContextStoreNames } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import { EOneKeyDeepLinkPath } from '@onekeyhq/shared/src/consts/deeplinkConsts'; import { EWatchlistFrom } from '@onekeyhq/shared/src/logger/scopes/market/scenes/token'; @@ -31,7 +32,6 @@ import backgroundApiProxy from '../../background/instance/backgroundApiProxy'; import { AccountSelectorProviderMirror } from '../../components/AccountSelector'; import { OpenInAppButton } from '../../components/OpenInAppButton'; import useAppNavigation from '../../hooks/useAppNavigation'; -import { useDeferredPromise } from '../../hooks/useDeferredPromise'; import { usePromiseResult } from '../../hooks/usePromiseResult'; import { MarketDetailOverview } from './components/MarketDetailOverview'; diff --git a/packages/kit/src/views/Market/components/TokenDetailTabs.tsx b/packages/kit/src/views/Market/components/TokenDetailTabs.tsx index 2a693135913..c525ce6a1fe 100644 --- a/packages/kit/src/views/Market/components/TokenDetailTabs.tsx +++ b/packages/kit/src/views/Market/components/TokenDetailTabs.tsx @@ -3,11 +3,10 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { useIntl } from 'react-intl'; -import type { ITabPageProps } from '@onekeyhq/components'; import { RefreshControl, Stack, Tab, useMedia } from '@onekeyhq/components'; +import type { IDeferredPromise, ITabPageProps } from '@onekeyhq/components'; import type { ITabInstance } from '@onekeyhq/components/src/layouts/TabView/StickyTabComponent/types'; import { ETranslations } from '@onekeyhq/shared/src/locale'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { IMarketTokenDetail } from '@onekeyhq/shared/types/market'; import { MarketDetailLinks } from './MarketDetailLinks'; @@ -15,7 +14,6 @@ import { MarketDetailOverview } from './MarketDetailOverview'; import { MarketDetailPools } from './MarketDetailPools'; import { TokenPriceChart } from './TokenPriceChart'; -import type { IDeferredPromise } from '../../../hooks/useDeferredPromise'; import type { LayoutChangeEvent } from 'react-native'; function BasicTokenDetailTabs({ diff --git a/packages/kit/src/views/Market/components/TokenPriceChart.tsx b/packages/kit/src/views/Market/components/TokenPriceChart.tsx index 58be5d59523..989532c397b 100644 --- a/packages/kit/src/views/Market/components/TokenPriceChart.tsx +++ b/packages/kit/src/views/Market/components/TokenPriceChart.tsx @@ -13,7 +13,10 @@ import { useSafeAreaInsets, useTabBarHeight, } from '@onekeyhq/components'; -import type { ISegmentControlProps } from '@onekeyhq/components'; +import type { + IDeferredPromise, + ISegmentControlProps, +} from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { @@ -27,7 +30,6 @@ import { TradingView } from '../../../components/TradingView'; import { PriceChart } from './Chart'; import type { ITradingViewProps } from '../../../components/TradingView'; -import type { IDeferredPromise } from '../../../hooks/useDeferredPromise'; interface IChartProps { coinGeckoId: string; diff --git a/packages/kit/src/views/Setting/pages/List/DevSettingsSection/NetInfo.tsx b/packages/kit/src/views/Setting/pages/List/DevSettingsSection/NetInfo.tsx index c1ff5a3cb21..688ecd73ec7 100644 --- a/packages/kit/src/views/Setting/pages/List/DevSettingsSection/NetInfo.tsx +++ b/packages/kit/src/views/Setting/pages/List/DevSettingsSection/NetInfo.tsx @@ -1,23 +1,16 @@ -import { Button, SizableText, XStack, YStack } from '@onekeyhq/components'; import { - fetch, - refresh, + Button, + SizableText, + XStack, + YStack, + refreshNetInfo, useNetInfo, -} from '@onekeyhq/shared/src/modules3rdParty/@react-native-community/netinfo'; +} from '@onekeyhq/components'; export function NetInfo() { - const { - type, - isConnected, - isWifiEnabled, - isInternetReachable, - isRawInternetReachable, - } = useNetInfo(); + const { isInternetReachable, isRawInternetReachable } = useNetInfo(); return ( - {`type: ${type}`} - {`isConnected: ${String(isConnected)}`} - {`isWifiEnabled: ${String(isWifiEnabled)}`} {`isInternetReachable: ${String( isInternetReachable, )}`} @@ -27,18 +20,7 @@ export function NetInfo() { -