diff --git a/app/ide-desktop/lib/dashboard/src/App.tsx b/app/ide-desktop/lib/dashboard/src/App.tsx index a2a212460a6..955deadf72e 100644 --- a/app/ide-desktop/lib/dashboard/src/App.tsx +++ b/app/ide-desktop/lib/dashboard/src/App.tsx @@ -84,6 +84,7 @@ import * as termsOfServiceModal from '#/modals/TermsOfServiceModal' import LocalBackend from '#/services/LocalBackend' import * as projectManager from '#/services/ProjectManager' +import ProjectManager from '#/services/ProjectManager' import RemoteBackend from '#/services/RemoteBackend' import * as appBaseUrl from '#/utilities/appBaseUrl' @@ -165,19 +166,51 @@ export interface AppProps { * This component handles all the initialization and rendering of the app, and manages the app's * routes. It also initializes an `AuthProvider` that will be used by the rest of the app. */ export default function App(props: AppProps) { - const { supportsLocalBackend } = props - - const { data: rootDirectoryPath } = reactQuery.useSuspenseQuery({ - queryKey: ['root-directory', supportsLocalBackend], + const { + data: { projectManagerRootDirectory, projectManagerInstance }, + } = reactQuery.useSuspenseQuery<{ + projectManagerInstance: ProjectManager | null + projectManagerRootDirectory: projectManager.Path | null + }>({ + queryKey: [ + 'root-directory', + { + projectManagerUrl: props.projectManagerUrl, + supportsLocalBackend: props.supportsLocalBackend, + }, + ] as const, meta: { persist: false }, networkMode: 'always', + staleTime: Infinity, + gcTime: Infinity, + refetchOnMount: false, + refetchInterval: false, + refetchOnReconnect: false, + refetchIntervalInBackground: false, + behavior: { + onFetch: ({ state }) => { + const instance = state.data?.projectManagerInstance ?? null + + if (instance != null) { + void instance.dispose() + } + }, + }, queryFn: async () => { - if (supportsLocalBackend) { + if (props.supportsLocalBackend && props.projectManagerUrl != null) { const response = await fetch(`${appBaseUrl.APP_BASE_URL}/api/root-directory`) const text = await response.text() - return projectManager.Path(text) + const rootDirectory = projectManager.Path(text) + + return { + projectManagerInstance: new ProjectManager(props.projectManagerUrl, rootDirectory), + projectManagerRootDirectory: rootDirectory, + } } else { - return null + return { + projectManagerInstance: null, + projectManagerRootDirectory: null, + } } }, }) @@ -199,7 +232,11 @@ export default function App(props: AppProps) { - + @@ -214,6 +251,7 @@ export default function App(props: AppProps) { /** Props for an {@link AppRouter}. */ export interface AppRouterProps extends AppProps { readonly projectManagerRootDirectory: projectManager.Path | null + readonly projectManagerInstance: ProjectManager | null } /** Router definition for the app. @@ -223,7 +261,7 @@ export interface AppRouterProps extends AppProps { * component as the component that defines the provider. */ function AppRouter(props: AppRouterProps) { const { logger, isAuthenticationDisabled, shouldShowDashboard, httpClient } = props - const { onAuthenticated, projectManagerUrl, projectManagerRootDirectory } = props + const { onAuthenticated, projectManagerInstance } = props const { portalRoot } = props // `navigateHooks.useNavigate` cannot be used here as it relies on `AuthProvider`, which has not // yet been initialized at this point. @@ -235,11 +273,8 @@ function AppRouter(props: AppRouterProps) { const navigator2D = navigator2DProvider.useNavigator2D() const localBackend = React.useMemo( - () => - projectManagerUrl != null && projectManagerRootDirectory != null - ? new LocalBackend(projectManagerUrl, projectManagerRootDirectory) - : null, - [projectManagerUrl, projectManagerRootDirectory] + () => (projectManagerInstance != null ? new LocalBackend(projectManagerInstance) : null), + [projectManagerInstance] ) const remoteBackend = React.useMemo( diff --git a/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts b/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts index 0d3dbb6010d..33608b58d99 100644 --- a/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts +++ b/app/ide-desktop/lib/dashboard/src/services/LocalBackend.ts @@ -5,7 +5,7 @@ * the API. */ import Backend, * as backend from '#/services/Backend' import * as projectManager from '#/services/ProjectManager' -import ProjectManager from '#/services/ProjectManager' +import type ProjectManager from '#/services/ProjectManager' import * as appBaseUrl from '#/utilities/appBaseUrl' import * as dateTime from '#/utilities/dateTime' @@ -96,9 +96,10 @@ export default class LocalBackend extends Backend { private readonly projectManager: ProjectManager /** Create a {@link LocalBackend}. */ - constructor(projectManagerUrl: string, rootDirectory: projectManager.Path) { + constructor(projectManagerInstance: ProjectManager) { super() - this.projectManager = new ProjectManager(projectManagerUrl, rootDirectory) + + this.projectManager = projectManagerInstance } /** Get the root directory of this Backend as a path. */ diff --git a/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts b/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts index b2876a918dd..e75190df3d9 100644 --- a/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts +++ b/app/ide-desktop/lib/dashboard/src/services/ProjectManager.ts @@ -347,6 +347,14 @@ export default class ProjectManager { this.socketPromise = createSocket() } + /** + * Dispose of the {@link ProjectManager}. + */ + async dispose() { + const socket = await this.socketPromise + socket.close() + } + /** Open an existing project. */ async openProject(params: OpenProjectParams): Promise { const cached = this.internalProjects.get(params.projectId)