Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: replace the old version with a more maintainable Network Reachability test. #6392

Merged
merged 21 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};
huhuanming marked this conversation as resolved.
Show resolved Hide resolved

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);
}
huhuanming marked this conversation as resolved.
Show resolved Hide resolved

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 {
huhuanming marked this conversation as resolved.
Show resolved Hide resolved
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();
};
huhuanming marked this conversation as resolved.
Show resolved Hide resolved

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
Loading