refactor: use abortable effect

This commit is contained in:
EnixCoda 2022-12-26 21:43:23 +08:00
parent 05b89e111a
commit 9c12d344de
3 changed files with 114 additions and 98 deletions

View file

@ -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],
),
)

View file

@ -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<MetaData | null>(null)
const [metaData, changeMetaData] = React.useState<MetaData | null>(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
}

View file

@ -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<T, TReturn, TNext>(
effect: () => {
getAsyncGenerator: () => AsyncGenerator<T, TReturn, TNext>
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<T>(
generator: AsyncGenerator<unknown, T, unknown>,
signal?: AbortSignal,
) {
let latestResult: IteratorResult<unknown> | undefined
do {
if (signal?.aborted) return
latestResult = await generator.next(await latestResult?.value)
} while (!latestResult.done)
return latestResult.value
}