Skip to content

Commit

Permalink
chore: replace the old version with a more maintainable Network Reach…
Browse files Browse the repository at this point in the history
…ability test. (#6392)
  • Loading branch information
huhuanming authored Dec 22, 2024
1 parent 2214d4a commit 95f5391
Show file tree
Hide file tree
Showing 16 changed files with 287 additions and 121 deletions.
2 changes: 2 additions & 0 deletions packages/components/src/hooks/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
48 changes: 48 additions & 0 deletions packages/components/src/hooks/useDeferredPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMemo } from 'react';

enum EDeferStatus {
pending = 'pending',
resolved = 'resolved',
rejected = 'rejected',
}
export type IDeferredPromise<DeferType> = {
resolve: (value: DeferType) => void;
reject: (value: unknown) => void;
reset: () => void;
promise: Promise<DeferType>;
status: EDeferStatus;
};

export const buildDeferredPromise = <DeferType>() => {
const deferred = {} as IDeferredPromise<DeferType>;

const buildPromise = () => {
const promise = new Promise<DeferType>((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<DeferType>() {
const defer = useMemo(() => buildDeferredPromise<DeferType>(), []);
return defer;
}
183 changes: 183 additions & 0 deletions packages/components/src/hooks/useNetInfo.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;
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<unknown>();

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<typeof setTimeout> | 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;
};
5 changes: 2 additions & 3 deletions packages/kit/src/components/NetworkAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 : (
<Alert
Expand Down
9 changes: 6 additions & 3 deletions packages/kit/src/components/Spotlight/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {

import { useIntl } from 'react-intl';

import type { IElement, IStackStyle } from '@onekeyhq/components';
import {
Button,
EPortalContainerConstantName,
Expand All @@ -28,18 +27,22 @@ import {
XStack,
YStack,
useBackHandler,
useDeferredPromise,
useMedia,
} from '@onekeyhq/components';
import type {
IDeferredPromise,
IElement,
IStackStyle,
} from '@onekeyhq/components';
import { useAppIsLockedAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms';
import { useSpotlightPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms/spotlight';
import { ETranslations } from '@onekeyhq/shared/src/locale';
import platformEnv from '@onekeyhq/shared/src/platformEnv';
import type { ESpotlightTour } from '@onekeyhq/shared/src/spotlight';

import backgroundApiProxy from '../../background/instance/backgroundApiProxy';
import { useDeferredPromise } from '../../hooks/useDeferredPromise';

import type { IDeferredPromise } from '../../hooks/useDeferredPromise';
import type { View as NativeView } from 'react-native';

export type ISpotlightViewProps = PropsWithChildren<{
Expand Down
47 changes: 0 additions & 47 deletions packages/kit/src/hooks/useDeferredPromise.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/kit/src/hooks/usePromiseResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit 95f5391

Please sign in to comment.