From 9c12d344de92ce384ab805fe21d645f044f081cb Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Mon, 26 Dec 2022 21:43:23 +0800 Subject: [PATCH] refactor: use abortable effect --- .../hooks/useVisibleNodesGenerator.tsx | 91 ++++++++--------- src/containers/RepoContext.tsx | 97 +++++++++---------- src/utils/hooks/useAbortableEffect.ts | 24 ++++- 3 files changed, 114 insertions(+), 98 deletions(-) diff --git a/src/components/FileExplorer/hooks/useVisibleNodesGenerator.tsx b/src/components/FileExplorer/hooks/useVisibleNodesGenerator.tsx index 56b2666..e2298d5 100644 --- a/src/components/FileExplorer/hooks/useVisibleNodesGenerator.tsx +++ b/src/components/FileExplorer/hooks/useVisibleNodesGenerator.tsx @@ -2,7 +2,7 @@ import { useConfigs } from 'containers/ConfigsContext' import { platform } from 'platforms' import { useCallback, useState } from 'react' import { useAbortableEffect } from 'utils/hooks/useAbortableEffect' -import { useCatchNetworkError } from 'utils/hooks/useCatchNetworkError' +import { useHandleNetworkError } from 'utils/hooks/useHandleNetworkError' import { useLoadedContext } from 'utils/hooks/useLoadedContext' import { VisibleNodesGenerator } from 'utils/VisibleNodesGenerator' import { SideBarStateContext } from '../../../containers/SideBarState' @@ -12,55 +12,58 @@ export function useVisibleNodesGenerator(metaData: MetaData | null) { null, ) - const catchNetworkErrors = useCatchNetworkError() const config = useConfigs().value const setStateContext = useLoadedContext(SideBarStateContext).onChange + const handleNetworkError = useHandleNetworkError() // Only run when metadata or accessToken changes + const createVNG = useCallback( + async function* createVNG() { + if (!metaData) return + + setStateContext('tree-loading') + const { userName, repoName, branchName } = metaData + try { + const { root: treeRoot, defer = false } = yield await platform.getTreeData( + { + branchName, + userName, + repoName, + }, + '/', + true, + config.accessToken, + ) + setStateContext('tree-rendering') + + setVisibleNodesGenerator( + new VisibleNodesGenerator({ + root: treeRoot, + defer, + compress: config.compressSingletonFolder, + async getTreeData(path) { + const { root } = await platform.getTreeData(metaData, path, false, config.accessToken) + return root + }, + }), + ) + + setStateContext('tree-rendered') + } catch (err) { + if (err instanceof Error) handleNetworkError(err) + else throw err + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [metaData, config.accessToken], + ) + useAbortableEffect( useCallback( - signal => { - catchNetworkErrors(async () => { - if (!metaData) return - if (signal.aborted) return - - setStateContext('tree-loading') - const { userName, repoName, branchName } = metaData - const { root: treeRoot, defer = false } = await platform.getTreeData( - { - branchName, - userName, - repoName, - }, - '/', - true, - config.accessToken, - ) - if (signal.aborted) return - - setStateContext('tree-rendering') - - setVisibleNodesGenerator( - new VisibleNodesGenerator({ - root: treeRoot, - defer, - compress: config.compressSingletonFolder, - async getTreeData(path) { - const { root } = await platform.getTreeData( - metaData, - path, - false, - config.accessToken, - ) - return root - }, - }), - ) - - setStateContext('tree-rendered') - }) - }, - [metaData, config.accessToken], // eslint-disable-line react-hooks/exhaustive-deps + () => ({ + getAsyncGenerator: createVNG, + }), + [createVNG], ), ) diff --git a/src/containers/RepoContext.tsx b/src/containers/RepoContext.tsx index 948c3ae..c6f088c 100644 --- a/src/containers/RepoContext.tsx +++ b/src/containers/RepoContext.tsx @@ -2,7 +2,6 @@ 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' @@ -36,10 +35,11 @@ export function RepoContextWrapper({ children }: PropsWithChildren) { function resolvePartialMetaData(): PartialMetaData | null { const partialMetaData = platform.resolvePartialMetaData() if (partialMetaData) { - const { userName, repoName, type } = partialMetaData + const { userName, repoName, branchName, type } = partialMetaData return { userName, repoName, + branchName, type: type === 'pull' ? type : undefined, } } @@ -76,59 +76,56 @@ function usePartialMetaData(): PartialMetaData | null { } function useMetaData(partialMetaData: PartialMetaData | null) { - const $state = useLoadedContext(SideBarStateContext) - const $metaData = useStateIO(null) + const [metaData, changeMetaData] = React.useState(null) + const changeLoadedState = useLoadedContext(SideBarStateContext).onChange + const handleNetworkError = useHandleNetworkError() const { accessToken } = useConfigs().value - const handleNetworkError = useHandleNetworkError() + const loadRepoMetaData = React.useCallback( + async function* loadRepoMetaData() { + if (!partialMetaData) return + + const { userName, repoName } = partialMetaData + if (!userName || !repoName) return + + changeLoadedState('meta-loading') + let { branchName } = partialMetaData + if (!branchName) { + try { + const defaultBranchName = yield await platform.getDefaultBranchName( + { userName, repoName }, + accessToken, + ) + branchName = defaultBranchName as string + } catch (err) { + // state will be updated in the network error handler + if (err instanceof Error) return handleNetworkError(err) + else throw err + } + } + + changeMetaData({ + userName, + repoName, + branchName, + }) + changeLoadedState('meta-loaded') + }, + [partialMetaData, changeLoadedState, accessToken, handleNetworkError], + ) + 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], + () => ({ + getAsyncGenerator: loadRepoMetaData, + cancel: () => { + changeLoadedState('disabled') + changeMetaData(null) + }, + }), + [loadRepoMetaData, changeLoadedState, changeMetaData], ), ) - return $metaData.value + return metaData } diff --git a/src/utils/hooks/useAbortableEffect.ts b/src/utils/hooks/useAbortableEffect.ts index 2e35c5a..3bde6d1 100644 --- a/src/utils/hooks/useAbortableEffect.ts +++ b/src/utils/hooks/useAbortableEffect.ts @@ -4,16 +4,32 @@ import { useEffect } from 'react' * This effect addresses such a problem: * the later effect ends earlier than the previous one, and the previous effect overlaps later effect's result. */ -export function useAbortableEffect( - effect: (shouldAbort: AbortSignal) => (() => void | undefined) | void, +export function useAbortableEffect( + effect: () => { + getAsyncGenerator: () => AsyncGenerator + cancel?: () => void + }, ) { useEffect(() => { const abortController = new AbortController() // The previous effect should stop running if the signal indicates should abort - const defect = effect(abortController.signal) + const { getAsyncGenerator, cancel } = effect() + runAbortableAsyncGenerator(getAsyncGenerator(), abortController.signal) return () => { + cancel?.() abortController.abort() - defect?.() } }, [effect]) } + +async function runAbortableAsyncGenerator( + generator: AsyncGenerator, + signal?: AbortSignal, +) { + let latestResult: IteratorResult | undefined + do { + if (signal?.aborted) return + latestResult = await generator.next(await latestResult?.value) + } while (!latestResult.done) + return latestResult.value +}