diff --git a/src/containers/RepoContext.tsx b/src/containers/RepoContext.tsx index 9298fb7..948c3ae 100644 --- a/src/containers/RepoContext.tsx +++ b/src/containers/RepoContext.tsx @@ -2,11 +2,13 @@ import { PropsWithChildren } from 'common' import { useConfigs } from 'containers/ConfigsContext' import { platform } from 'platforms' import * as React from 'react' +import { run } from 'utils/general' +import { useAbortableEffect } from 'utils/hooks/useAbortableEffect' import { useEffectOnSerializableUpdates } from 'utils/hooks/useEffectOnSerializableUpdates' import { useAfterRedirect } from 'utils/hooks/useFastRedirect' +import { useHandleNetworkError } from 'utils/hooks/useHandleNetworkError' import { useLoadedContext } from 'utils/hooks/useLoadedContext' import { useStateIO } from 'utils/hooks/useStateIO' -import { useCatchNetworkError } from '../utils/hooks/useCatchNetworkError' import { SideBarStateContext } from './SideBarState' import { useInspector } from './StateInspector' @@ -14,17 +16,15 @@ export const RepoContext = React.createContext(null) export function RepoContextWrapper({ children }: PropsWithChildren) { const partialMetaData = usePartialMetaData() - const defaultBranch = useDefaultBranch(partialMetaData) - const metaData = useMetaData(partialMetaData, defaultBranch) + const metaData = useMetaData(partialMetaData) useInspector( 'RepoContext', React.useMemo( () => ({ partialMetaData, - defaultBranch, metaData, }), - [partialMetaData, defaultBranch, metaData], + [partialMetaData, metaData], ), ) const state = useLoadedContext(SideBarStateContext).value @@ -33,7 +33,7 @@ export function RepoContextWrapper({ children }: PropsWithChildren) { return {children} } -function resolvePartialMetaData() { +function resolvePartialMetaData(): PartialMetaData | null { const partialMetaData = platform.resolvePartialMetaData() if (partialMetaData) { const { userName, repoName, type } = partialMetaData @@ -49,7 +49,8 @@ function resolvePartialMetaData() { function usePartialMetaData(): PartialMetaData | null { const $state = useLoadedContext(SideBarStateContext) - const isGettingAccessToken = $state.value === 'getting-access-token' // will be false after getting access token and trigger meta-resolve progress + // will be false after getting access token and trigger meta-resolve progress + const isGettingAccessToken = $state.value === 'getting-access-token' // sync along URL and DOM const $partialMetaData = useStateIO(isGettingAccessToken ? null : resolvePartialMetaData) const $committedPartialMetaData = useStateIO($partialMetaData.value) @@ -74,58 +75,60 @@ function usePartialMetaData(): PartialMetaData | null { return $committedPartialMetaData.value } -function useBranchName(): MetaData['branchName'] | null { - // sync along URL and DOM - const $branchName = useStateIO(() => platform.resolvePartialMetaData()?.branchName || null) - useAfterRedirect(() => - $branchName.onChange(platform.resolvePartialMetaData()?.branchName || null), - ) - return $branchName.value -} - -function useDefaultBranch(partialMetaData: PartialMetaData | null) { - const { accessToken } = useConfigs().value - const $state = useLoadedContext(SideBarStateContext) - const $defaultBranch = useStateIO(null) - const catchNetworkError = useCatchNetworkError() - React.useEffect(() => { - catchNetworkError(async () => { - if (!partialMetaData) return - $state.onChange('meta-loading') - const { userName, repoName } = partialMetaData - if (!userName || !repoName) return - - const defaultBranch = await platform.getDefaultBranchName({ userName, repoName }, accessToken) - $defaultBranch.onChange(defaultBranch) - }) - }, [partialMetaData, accessToken]) // eslint-disable-line react-hooks/exhaustive-deps - return $defaultBranch.value -} - -function useMetaData( - partialMetaData: PartialMetaData | null, - defaultBranchName: MetaData['defaultBranchName'] | null, -) { +function useMetaData(partialMetaData: PartialMetaData | null) { const $state = useLoadedContext(SideBarStateContext) const $metaData = useStateIO(null) - const branchName = useBranchName() - const theBranch = branchName && branchName !== defaultBranchName ? branchName : defaultBranchName - React.useEffect(() => { - if (partialMetaData && defaultBranchName && theBranch) { - const { userName, repoName } = partialMetaData - if (!userName || !repoName) return - const safeMetaData: MetaData = { - userName, - repoName, - branchName: theBranch, - defaultBranchName, - } - $metaData.onChange(safeMetaData) - $state.onChange('meta-loaded') - } else { - $metaData.onChange(null) - } - }, [partialMetaData, defaultBranchName, theBranch]) // eslint-disable-line react-hooks/exhaustive-deps + const { accessToken } = useConfigs().value + const handleNetworkError = useHandleNetworkError() + useAbortableEffect( + React.useCallback( + signal => { + // get default branch + run(async () => { + if (!partialMetaData) return + + const { userName, repoName } = partialMetaData + if (!userName || !repoName) return + + $state.onChange('meta-loading') + let { branchName } = partialMetaData + if (!branchName) { + try { + const defaultBranchName = await platform.getDefaultBranchName( + { userName, repoName }, + accessToken, + ) + if (signal.aborted) return + branchName = defaultBranchName + } catch (err) { + // state will be updated in the network error handler + if (err instanceof Error) { + handleNetworkError(err) + return + } + + throw err + } + } + + $metaData.onChange({ + userName, + repoName, + branchName, + }) + $state.onChange('meta-loaded') + }) + + return () => { + $state.onChange('disabled') + $metaData.onChange(null) + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [partialMetaData, accessToken], + ), + ) + return $metaData.value } diff --git a/src/global.d.ts b/src/global.d.ts index 4c0e7ea..c907441 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -3,15 +3,11 @@ type AnyArray = any[] // eslint-disable-line @typescript-eslint/no-explicit-any type MetaData = { userName: string repoName: string - defaultBranchName: string branchName: string type?: EnumString<'tree' | 'blob' | 'pull' | 'commit'> } -type PartialMetaData = Omit< - MakeOptional, - 'defaultBranchName' -> +type PartialMetaData = MakeOptional type TreeNode = { name: string diff --git a/src/utils/hooks/useCatchNetworkError.ts b/src/utils/hooks/useCatchNetworkError.ts deleted file mode 100644 index 6885c3b..0000000 --- a/src/utils/hooks/useCatchNetworkError.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useConfigs } from 'containers/ConfigsContext' -import { errors, platformName } from 'platforms' -import { useCallback } from 'react' -import { assert } from 'utils/assert' -import { useLoadedContext } from 'utils/hooks/useLoadedContext' -import { SideBarErrorContext } from '../../containers/ErrorContext' -import { SideBarStateContext } from '../../containers/SideBarState' - -export function useCatchNetworkError() { - const { accessToken } = useConfigs().value - const stateContext = useLoadedContext(SideBarStateContext) - const errorContext = useLoadedContext(SideBarErrorContext) - - return useCallback( - async function (fn: () => T) { - try { - return await fn() // keep the await so that catch block can catch async errors - } catch (err) { - assert(err instanceof Error) - - if (err.message === errors.EMPTY_PROJECT) { - errorContext.onChange('This project seems to be empty.') - } else if (err.message === errors.BLOCKED_PROJECT) { - errorContext.onChange('Access to the project is blocked.') - } else if ( - err.message === errors.NOT_FOUND || - err.message === errors.BAD_CREDENTIALS || - err.message === errors.API_RATE_LIMIT - ) { - stateContext.onChange('error-due-to-auth') - } else if (err.message === errors.CONNECTION_BLOCKED) { - if (accessToken) { - errorContext.onChange(`Cannot connect to ${platformName}.`) - } else { - stateContext.onChange('error-due-to-auth') - } - } else if (err.message === errors.SERVER_FAULT) { - errorContext.onChange(`${platformName} server went down.`) - } else { - stateContext.onChange('disabled') - errorContext.onChange('Something unexpected happened.') - throw err - } - } - }, - [accessToken /* , stateContext.value, errorContext.value */], // eslint-disable-line react-hooks/exhaustive-deps - ) -} diff --git a/src/utils/hooks/useHandleNetworkError.ts b/src/utils/hooks/useHandleNetworkError.ts new file mode 100644 index 0000000..2e768ad --- /dev/null +++ b/src/utils/hooks/useHandleNetworkError.ts @@ -0,0 +1,52 @@ +import { useConfigs } from 'containers/ConfigsContext' +import { errors, platformName } from 'platforms' +import { useCallback } from 'react' +import { useLoadedContext } from 'utils/hooks/useLoadedContext' +import { SideBarErrorContext } from '../../containers/ErrorContext' +import { SideBarStateContext } from '../../containers/SideBarState' + +export function useHandleNetworkError() { + const { accessToken } = useConfigs().value + const changeErrorContext = useLoadedContext(SideBarErrorContext).onChange + const changeStateContext = useLoadedContext(SideBarStateContext).onChange + + return useCallback( + function handleNetworkError(err: Error) { + if (err.message === errors.EMPTY_PROJECT) { + changeErrorContext('This project seems to be empty.') + return + } + + if (err.message === errors.BLOCKED_PROJECT) { + changeErrorContext('Access to the project is blocked.') + return + } + + if ( + err.message === errors.NOT_FOUND || + err.message === errors.BAD_CREDENTIALS || + err.message === errors.API_RATE_LIMIT + ) { + changeStateContext('error-due-to-auth') + return + } + + if (err.message === errors.CONNECTION_BLOCKED) { + if (accessToken) changeErrorContext(`Cannot connect to ${platformName}.`) + else changeStateContext('error-due-to-auth') + + return + } + + if (err.message === errors.SERVER_FAULT) { + changeErrorContext(`${platformName} server went down.`) + return + } + + changeStateContext('disabled') + changeErrorContext('Something unexpected happened.') + throw err + }, + [accessToken, changeErrorContext, changeStateContext], + ) +}