import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type MutableRefObject, type SetStateAction } from 'react' import type { VaultEntry, FolderNode, GitCommit, ModifiedFile, NoteStatus, GitPushResult, ViewFile } from '../types' import type { VaultOption } from '../components/status-bar/types' import { GITIGNORED_VISIBILITY_CHANGED_EVENT, notifyGitignoredVisibilityApplied, type GitignoredVisibilityChangedEvent, } from '../lib/gitignoredVisibilityEvents' import { clearPrefetchCache } from './useTabManagement' import { checkVaultPathAvailability, commitWithPush, hasVaultPath, loadVaultChrome, loadVaultData, loadMountedVaultFolders, loadMountedVaultViews, loadVaultFolders, loadVaultViews, loadWorkspaceEntries, reloadVaultEntries, tauriCall, } from './vaultLoaderCommands' import { normalizeVaultEntry } from '../utils/vaultMetadataNormalization' import { useUnavailableVaultState } from './useUnavailableVaultState' import { resetVaultState } from './vaultStateReset' import { initialVaultsForPath, loadedWorkspacePathsFromEntries, pruneEntriesOutsideWorkspaceSet, replaceLoadedWorkspaceEntries, replaceWorkspaceEntries, retagEntriesForWorkspaceMetadata, uniqueWorkspacePathsFromVaults, workspacePathSetKey, } from './vaultWorkspaceEntries' interface InitialVaultLoadStateOptions { defaultWorkspacePath?: string | null folderVaults?: VaultOption[] handleVaultAvailable: (path: string) => void path: string handleVaultUnavailable: (path: string) => void isCurrentVaultPath: (path: string) => boolean setEntries: Dispatch> setFolders: (folders: FolderNode[]) => void setIsLoading: (isLoading: boolean) => void setViews: (views: ViewFile[]) => void vaults?: VaultOption[] } interface InitialVaultChromeOptions extends Pick< InitialVaultLoadStateOptions, 'defaultWorkspacePath' | 'folderVaults' | 'handleVaultUnavailable' | 'isCurrentVaultPath' | 'path' | 'setFolders' | 'setViews' > { shouldApplyChrome: () => boolean } async function loadInitialVaultChromeState(options: InitialVaultChromeOptions): Promise { const { defaultWorkspacePath, folderVaults, handleVaultUnavailable, isCurrentVaultPath, path, setFolders, setViews, shouldApplyChrome, } = options try { const { folders, views } = await loadVaultChrome({ defaultWorkspacePath, vaultPath: path, vaults: folderVaults, }) if (shouldApplyChrome()) { setFolders(folders) setViews(views) } } catch (err) { const unavailable = await handleUnavailableVaultPath({ handleVaultUnavailable, isCurrentVaultPath, path }) if (unavailable) return true console.warn('Vault chrome load failed:', err) } return false } async function loadInitialVaultEntriesState(options: Pick< InitialVaultLoadStateOptions, 'defaultWorkspacePath' | 'handleVaultAvailable' | 'handleVaultUnavailable' | 'isCurrentVaultPath' | 'path' | 'setEntries' | 'vaults' >): Promise { const { handleVaultAvailable, handleVaultUnavailable, isCurrentVaultPath, path, setEntries } = options try { const { entries } = await loadVaultData({ vaultPath: path, vaults: initialVaultsForPath(path, options.vaults), defaultWorkspacePath: options.defaultWorkspacePath, forceReload: true, }) if (isCurrentVaultPath(path)) { handleVaultAvailable(path) setEntries((currentEntries) => replaceLoadedWorkspaceEntries({ defaultWorkspacePath: options.defaultWorkspacePath, entries: currentEntries, fallbackVaultPath: path, loadedEntries: entries, vaults: options.vaults, })) } } catch (err) { const unavailable = await handleUnavailableVaultPath({ handleVaultUnavailable, isCurrentVaultPath, path }) if (unavailable) return true console.warn('Vault scan failed:', err) } return false } async function loadInitialVaultState(options: InitialVaultLoadStateOptions) { const { path, isCurrentVaultPath, setIsLoading } = options let vaultUnavailable = false const chromeLoad = loadInitialVaultChromeState({ ...options, shouldApplyChrome: () => !vaultUnavailable && isCurrentVaultPath(path), }) setIsLoading(true) vaultUnavailable = await loadInitialVaultEntriesState(options) if (isCurrentVaultPath(path)) setIsLoading(false) await chromeLoad } async function handleUnavailableVaultPath(options: { handleVaultUnavailable: (path: string) => void isCurrentVaultPath: (path: string) => boolean path: string }): Promise { const { handleVaultUnavailable, isCurrentVaultPath, path } = options if (!isCurrentVaultPath(path)) return true const available = await checkVaultPathAvailability({ vaultPath: path }) if (available !== false) return false if (isCurrentVaultPath(path)) handleVaultUnavailable(path) return true } function useCurrentVaultPathGuard(vaultPath: string) { const currentPathRef = useRef(vaultPath) useEffect(() => { currentPathRef.current = vaultPath }, [vaultPath]) return useCallback((path: string) => currentPathRef.current === path, []) } function useCoalescedAsyncTask(runTask: () => Promise) { const inFlightRef = useRef | null>(null) const requestedDuringFlightRef = useRef(false) const latestTaskRef = useRef<(() => Promise) | null>(null) const task = useCallback(async () => { if (inFlightRef.current) { requestedDuringFlightRef.current = true return inFlightRef.current } const next = (async () => { try { return await runTask() } finally { inFlightRef.current = null if (requestedDuringFlightRef.current) { requestedDuringFlightRef.current = false void latestTaskRef.current?.() } } })() inFlightRef.current = next return next }, [runTask]) useEffect(() => { latestTaskRef.current = task }, [task]) return task } function useNewNoteTracker() { const [newPaths, setNewPaths] = useState>(new Set()) const trackNew = useCallback((path: string) => { setNewPaths((prev) => new Set(prev).add(path)) }, []) const clear = useCallback(() => setNewPaths(new Set()), []) return { newPaths, trackNew, clear } } function useUnsavedTracker() { const [unsavedPaths, setUnsavedPaths] = useState>(new Set()) const trackUnsaved = useCallback((path: string) => { setUnsavedPaths((prev) => new Set(prev).add(path)) }, []) const clearUnsaved = useCallback((path: string) => { setUnsavedPaths((prev) => { const next = new Set(prev) next.delete(path) return next }) }, []) const clearAll = useCallback(() => setUnsavedPaths(new Set()), []) return { unsavedPaths, trackUnsaved, clearUnsaved, clearAll } } function usePendingSaveTracker() { const [pendingSavePaths, setPendingSavePaths] = useState>(new Set()) const addPendingSave = useCallback((path: string) => { setPendingSavePaths((prev) => new Set(prev).add(path)) }, []) const removePendingSave = useCallback((path: string) => { setPendingSavePaths((prev) => { const next = new Set(prev) next.delete(path) return next }) }, []) return { pendingSavePaths, addPendingSave, removePendingSave } } interface ResolveNoteStatusOptions { path: string newPaths: Set modifiedFiles: ModifiedFile[] pendingSavePaths?: Set unsavedPaths?: Set } function resolveTransientNoteStatus({ path, pendingSavePaths, unsavedPaths, }: Pick): NoteStatus | null { if (unsavedPaths?.has(path)) return 'unsaved' if (pendingSavePaths?.has(path)) return 'pendingSave' return null } function resolveGitBackedNoteStatus(file: ModifiedFile | undefined): NoteStatus { if (!file) return 'clean' if (file.status === 'untracked' || file.status === 'added') return 'new' if (file.status === 'modified' || file.status === 'deleted') return 'modified' return 'clean' } export function resolveNoteStatus({ path, newPaths, modifiedFiles, pendingSavePaths, unsavedPaths, }: ResolveNoteStatusOptions): NoteStatus { const transientStatus = resolveTransientNoteStatus({ path, pendingSavePaths, unsavedPaths }) if (transientStatus) return transientStatus if (newPaths.has(path)) return 'new' return resolveGitBackedNoteStatus(modifiedFiles.find((file) => file.path === path)) } interface InitialVaultLoadOptions { defaultWorkspacePath?: string | null folderVaults?: VaultOption[] handleVaultAvailable: (path: string) => void handleVaultUnavailable: (path: string) => void isWorkspacePathLoaded: (path: string) => boolean vaultPath: string vaults?: VaultOption[] tracker: ReturnType unsaved: ReturnType isCurrentVaultPath: (path: string) => boolean resetReloading: () => void setEntries: Dispatch> setFolders: (folders: FolderNode[]) => void setIsLoading: (isLoading: boolean) => void setModifiedFiles: (files: ModifiedFile[]) => void setModifiedFilesError: (message: string | null) => void setViews: (views: ViewFile[]) => void } interface InitialVaultLoadSnapshot { defaultWorkspacePath?: string | null vaults?: VaultOption[] } interface InitialVaultLoadEffectOptions extends Omit { clearNewPaths: () => void clearUnsaved: () => void } function useInitialVaultLoadSnapshot(vaults?: VaultOption[], defaultWorkspacePath?: string | null) { const loadOptionsRef = useRef({ vaults, defaultWorkspacePath }) useEffect(() => { loadOptionsRef.current = { vaults, defaultWorkspacePath } }, [defaultWorkspacePath, vaults]) return loadOptionsRef } function shouldReuseLoadedWorkspaceEntries( path: string, loadOptions: InitialVaultLoadSnapshot, isWorkspacePathLoaded: (path: string) => boolean, ): boolean { return !!loadOptions.vaults?.length && isWorkspacePathLoaded(path) } function shouldPreserveWorkspaceEntries(loadOptions: InitialVaultLoadSnapshot): boolean { return !!loadOptions.vaults?.length } function resetInitialVaultLoadState(options: InitialVaultLoadEffectOptions, preserveWorkspaceEntries: boolean) { clearPrefetchCache() options.setViews([]) resetVaultState({ clearNewPaths: options.clearNewPaths, clearUnsaved: options.clearUnsaved, setEntries: preserveWorkspaceEntries ? () => {} : options.setEntries, setFolders: options.setFolders, setIsLoading: options.setIsLoading, setModifiedFiles: options.setModifiedFiles, setModifiedFilesError: options.setModifiedFilesError, setViews: preserveWorkspaceEntries ? () => {} : options.setViews, }) options.resetReloading() } function startReusableWorkspaceChromeLoad( options: InitialVaultLoadEffectOptions, path: string, isActivePath: (candidate: string) => boolean, ) { void loadInitialVaultChromeState({ handleVaultUnavailable: options.handleVaultUnavailable, isCurrentVaultPath: isActivePath, path, defaultWorkspacePath: options.defaultWorkspacePath, folderVaults: options.folderVaults, setFolders: options.setFolders, setViews: options.setViews, shouldApplyChrome: () => isActivePath(path), }) } function startFreshInitialVaultLoad( options: InitialVaultLoadEffectOptions, path: string, loadOptions: InitialVaultLoadSnapshot, isActivePath: (candidate: string) => boolean, ) { void loadInitialVaultState({ handleVaultAvailable: options.handleVaultAvailable, path, handleVaultUnavailable: options.handleVaultUnavailable, isCurrentVaultPath: isActivePath, vaults: loadOptions.vaults, folderVaults: options.folderVaults, defaultWorkspacePath: loadOptions.defaultWorkspacePath, setEntries: options.setEntries, setFolders: options.setFolders, setIsLoading: options.setIsLoading, setViews: options.setViews, }) } function useInitialVaultLoad(options: InitialVaultLoadOptions) { const { handleVaultAvailable, handleVaultUnavailable, isWorkspacePathLoaded, vaultPath, tracker, unsaved, isCurrentVaultPath, resetReloading, setEntries, setFolders, setIsLoading, setModifiedFiles, setModifiedFilesError, setViews, vaults, defaultWorkspacePath, folderVaults, } = options const loadOptionsRef = useInitialVaultLoadSnapshot(vaults, defaultWorkspacePath) const loadOptionsKey = useMemo( () => workspacePathSetKey(uniqueWorkspacePathsFromVaults(vaultPath, vaults)), [vaultPath, vaults], ) useEffect(() => { const path = vaultPath const loadOptions = loadOptionsRef.current const effectOptions = { defaultWorkspacePath: loadOptions.defaultWorkspacePath, handleVaultAvailable, handleVaultUnavailable, isCurrentVaultPath, isWorkspacePathLoaded, resetReloading, clearNewPaths: tracker.clear, clearUnsaved: unsaved.clearAll, setEntries, setFolders, setIsLoading, setModifiedFiles, setModifiedFilesError, setViews, vaultPath, vaults: loadOptions.vaults, folderVaults, } const reuseLoadedWorkspaceEntries = shouldReuseLoadedWorkspaceEntries(path, loadOptions, isWorkspacePathLoaded) const preserveWorkspaceEntries = reuseLoadedWorkspaceEntries || shouldPreserveWorkspaceEntries(loadOptions) resetInitialVaultLoadState(effectOptions, preserveWorkspaceEntries) if (!hasVaultPath({ vaultPath: path })) return let cancelled = false const isActivePath = (candidate: string) => !cancelled && isCurrentVaultPath(candidate) if (reuseLoadedWorkspaceEntries) { startReusableWorkspaceChromeLoad(effectOptions, path, isActivePath) return () => { cancelled = true } } startFreshInitialVaultLoad(effectOptions, path, loadOptions, isActivePath) return () => { cancelled = true } }, [ handleVaultAvailable, handleVaultUnavailable, vaultPath, tracker.clear, unsaved.clearAll, isCurrentVaultPath, isWorkspacePathLoaded, resetReloading, setEntries, setFolders, setIsLoading, setModifiedFiles, setModifiedFilesError, setViews, loadOptionsRef, loadOptionsKey, folderVaults, ]) } function useModifiedFilesLoader(vaultPath: string, isCurrentVaultPath: (path: string) => boolean) { const [modifiedFiles, setModifiedFiles] = useState([]) const [modifiedFilesError, setModifiedFilesError] = useState(null) const runModifiedFilesLoad = useCallback(async () => { const path = vaultPath setModifiedFilesError(null) if (!hasVaultPath({ vaultPath: path })) { setModifiedFiles([]) return } try { const files = await tauriCall({ command: 'get_modified_files', tauriArgs: { vaultPath: path, includeStats: false }, mockArgs: {}, }) if (isCurrentVaultPath(path)) setModifiedFiles(files) } catch (err) { if (!isCurrentVaultPath(path)) return const message = typeof err === 'string' ? err : 'Failed to load changes' console.warn('Failed to load modified files:', err) setModifiedFilesError(message) setModifiedFiles([]) } }, [vaultPath, isCurrentVaultPath]) const loadModifiedFiles = useCoalescedAsyncTask(runModifiedFilesLoad) useEffect(() => { loadModifiedFiles() }, [loadModifiedFiles]) return { modifiedFiles, modifiedFilesError, setModifiedFiles, setModifiedFilesError, loadModifiedFiles, } } function useEntryMutations( setEntries: Dispatch>, trackNew: (path: string) => void, ) { const addEntry = useCallback((entry: VaultEntry) => { const normalizedEntry = normalizeVaultEntry(entry) setEntries((prev) => { if (prev.some(e => e.path === normalizedEntry.path)) return prev return [normalizedEntry, ...prev] }) trackNew(normalizedEntry.path) }, [setEntries, trackNew]) const updateEntry = useCallback((path: string, patch: Partial) => { setEntries((prev) => { let changed = false const next = prev.map((entry, index) => { if (entry.path === path) { changed = true return normalizeVaultEntry({ ...entry, ...patch }, '', index) } return entry }) return changed ? next : prev }) }, [setEntries]) const removeEntry = useCallback((path: string) => { setEntries((prev) => prev.filter((e) => e.path !== path)) }, [setEntries]) const removeEntries = useCallback((paths: string[]) => { if (paths.length === 0) return const pathSet = new Set(paths) setEntries((prev) => prev.filter((entry) => !pathSet.has(entry.path))) }, [setEntries]) const replaceEntry = useCallback((oldPath: string, patch: Partial & { path: string }) => { setEntries((prev) => prev.map((entry, index) => entry.path === oldPath ? normalizeVaultEntry({ ...entry, ...patch }, '', index) : entry, )) }, [setEntries]) return { addEntry, updateEntry, removeEntry, removeEntries, replaceEntry } } function useGitLoaders(vaultPath: string) { const loadGitHistory = useCallback(async (path: string): Promise => { try { return await tauriCall({ command: 'get_file_history', tauriArgs: { vaultPath, path }, mockArgs: { path }, }) } catch (err) { console.warn('Failed to load git history:', err); return [] } }, [vaultPath]) const loadDiffAtCommit = useCallback((path: string, commitHash: string): Promise => tauriCall({ command: 'get_file_diff_at_commit', tauriArgs: { vaultPath, path, commitHash }, mockArgs: { path, commitHash }, }), [vaultPath]) const loadDiff = useCallback((path: string): Promise => tauriCall({ command: 'get_file_diff', tauriArgs: { vaultPath, path }, mockArgs: { path }, }), [vaultPath]) const commitAndPush = useCallback((message: string): Promise => commitWithPush({ vaultPath, message }), [vaultPath]) return { loadGitHistory, loadDiffAtCommit, loadDiff, commitAndPush } } interface VaultReloadOptions { defaultWorkspacePath?: string | null folderVaults?: VaultOption[] handleVaultAvailable: (path: string) => void handleVaultUnavailable: (path: string) => void vaultPath: string isCurrentVaultPath: (path: string) => boolean loadModifiedFiles: () => Promise setEntries: (entries: VaultEntry[]) => void setFolders: (folders: FolderNode[]) => void setViews: (views: ViewFile[]) => void vaults?: VaultOption[] } interface EntryReloadOptions extends VaultReloadOptions { beginReload: () => void finishReload: () => void } interface CollectionReloadOptions { handleVaultUnavailable: (path: string) => void isCurrentVaultPath: (path: string) => boolean loadCollection: (options: { vaultPath: string }) => Promise path: string setCollection: (items: T[]) => void } async function reloadVaultCollection(options: CollectionReloadOptions): Promise { const { handleVaultUnavailable, isCurrentVaultPath, loadCollection, path, setCollection } = options if (!hasVaultPath({ vaultPath: path })) return [] try { const items = await loadCollection({ vaultPath: path }) if (!isCurrentVaultPath(path)) return [] const nextItems = items ?? [] setCollection(nextItems) return nextItems } catch { await handleUnavailableVaultPath({ handleVaultUnavailable, isCurrentVaultPath, path }) return [] } } function useFolderReload({ folderVaults, handleVaultUnavailable, isCurrentVaultPath, setFolders, vaultPath, }: Pick) { return useCallback(() => reloadVaultCollection({ handleVaultUnavailable, isCurrentVaultPath, loadCollection: folderVaults?.length ? (options) => loadMountedVaultFolders({ ...options, vaults: folderVaults }) : loadVaultFolders, path: vaultPath, setCollection: setFolders, }), [folderVaults, handleVaultUnavailable, vaultPath, isCurrentVaultPath, setFolders]) } function useEntryReload({ beginReload, finishReload, handleVaultAvailable, handleVaultUnavailable, isCurrentVaultPath, loadModifiedFiles, setEntries, vaultPath, vaults, defaultWorkspacePath, }: EntryReloadOptions) { const runEntryReload = useCallback(async () => { const path = vaultPath if (!hasVaultPath({ vaultPath: path })) return [] as VaultEntry[] clearPrefetchCache() beginReload() try { const entries = await reloadVaultEntries({ vaultPath: path, vaults, defaultWorkspacePath }) if (!isCurrentVaultPath(path)) return [] as VaultEntry[] handleVaultAvailable(path) setEntries(entries) void loadModifiedFiles() return entries } catch (err) { if (await handleUnavailableVaultPath({ handleVaultUnavailable, isCurrentVaultPath, path })) return [] as VaultEntry[] console.warn('Vault reload failed:', err) return [] as VaultEntry[] } finally { finishReload() } }, [handleVaultAvailable, handleVaultUnavailable, vaultPath, vaults, defaultWorkspacePath, beginReload, finishReload, loadModifiedFiles, isCurrentVaultPath, setEntries]) return useCoalescedAsyncTask(runEntryReload) } function useViewReload({ defaultWorkspacePath, folderVaults, handleVaultUnavailable, isCurrentVaultPath, setViews, vaultPath, }: Pick) { const defaultWorkspacePathRef = useRef(defaultWorkspacePath) useEffect(() => { defaultWorkspacePathRef.current = defaultWorkspacePath }, [defaultWorkspacePath]) return useCallback(() => reloadVaultCollection({ handleVaultUnavailable, isCurrentVaultPath, loadCollection: folderVaults?.length ? (options) => loadMountedVaultViews({ ...options, defaultWorkspacePath: defaultWorkspacePathRef.current, vaults: folderVaults }) : loadVaultViews, path: vaultPath, setCollection: setViews, }), [folderVaults, handleVaultUnavailable, vaultPath, isCurrentVaultPath, setViews]) } function useVaultReloads(options: VaultReloadOptions) { const [activeReloads, setActiveReloads] = useState(0) const isReloading = activeReloads > 0 const beginReload = useCallback(() => setActiveReloads((count) => count + 1), []) const finishReload = useCallback(() => setActiveReloads((count) => Math.max(0, count - 1)), []) const resetReloading = useCallback(() => setActiveReloads(0), []) const reloadFolders = useFolderReload(options) const reloadVault = useEntryReload({ ...options, beginReload, finishReload }) const reloadViews = useViewReload(options) return { isReloading, reloadFolders, reloadVault, reloadViews, resetReloading } } function useGitignoredVisibilityReloads( reloads: Pick, 'reloadFolders' | 'reloadVault' | 'reloadViews'>, ) { const { reloadFolders, reloadVault, reloadViews } = reloads useEffect(() => { if (typeof window === 'undefined') return const handleVisibilityChanged = (event: Event) => { const { hide } = (event as GitignoredVisibilityChangedEvent).detail void Promise.all([ reloadVault(), reloadFolders(), reloadViews(), ]).then(([entries]) => { notifyGitignoredVisibilityApplied(hide, entries) }) } window.addEventListener(GITIGNORED_VISIBILITY_CHANGED_EVENT, handleVisibilityChanged) return () => { window.removeEventListener(GITIGNORED_VISIBILITY_CHANGED_EVENT, handleVisibilityChanged) } }, [reloadFolders, reloadVault, reloadViews]) } function useVaultState(vaultPath: string) { const [entries, setEntries] = useState([]) const [folders, setFolders] = useState([]) const [isLoading, setIsLoading] = useState(() => hasVaultPath({ vaultPath })) const [views, setViews] = useState([]) const tracker = useNewNoteTracker() const pendingSave = usePendingSaveTracker() const unsaved = useUnsavedTracker() const isCurrentVaultPath = useCurrentVaultPathGuard(vaultPath) const modified = useModifiedFilesLoader(vaultPath, isCurrentVaultPath) return { entries, folders, isCurrentVaultPath, isLoading, modified, pendingSave, setEntries, setFolders, setIsLoading, setViews, tracker, unsaved, views, } } function useVaultUnavailable(vaultPath: string, state: ReturnType) { const { isCurrentVaultPath, modified, setEntries, setFolders, setIsLoading, setViews, tracker, unsaved, } = state return useUnavailableVaultState({ clearNewPaths: tracker.clear, clearUnsaved: unsaved.clearAll, isCurrentVaultPath, setEntries, setFolders, setIsLoading, setModifiedFiles: modified.setModifiedFiles, setModifiedFilesError: modified.setModifiedFilesError, setViews, vaultPath, }) } function useInitialFolderSetter(folderVaults: VaultOption[] | undefined, setFolders: (folders: FolderNode[]) => void) { const folderVaultsRef = useRef(folderVaults) useEffect(() => { folderVaultsRef.current = folderVaults }, [folderVaults]) return useCallback((nextFolders: FolderNode[]) => { if ((folderVaultsRef.current?.length ?? 0) === 0) setFolders(nextFolders) }, [setFolders]) } function useVaultChromeReloadEffect( vaultPath: string, reloadFoldersForCurrentVault: () => Promise, reloadViewsForCurrentVault: () => Promise, ) { useEffect(() => { if (!hasVaultPath({ vaultPath })) return void Promise.all([ reloadFoldersForCurrentVault(), reloadViewsForCurrentVault(), ]) }, [vaultPath, reloadFoldersForCurrentVault, reloadViewsForCurrentVault]) } interface WorkspaceLoadRefs { initialLoadedVaultPathRef: MutableRefObject loadedWorkspacePathsRef: MutableRefObject> loadingWorkspacePathsRef: MutableRefObject> } function useDesiredWorkspacePaths(vaultPath: string, vaults?: VaultOption[]) { const desiredWorkspacePaths = useMemo( () => uniqueWorkspacePathsFromVaults(vaultPath, vaults), [vaultPath, vaults], ) const desiredWorkspaceKey = useMemo( () => workspacePathSetKey(desiredWorkspacePaths), [desiredWorkspacePaths], ) return { desiredWorkspaceKey, desiredWorkspacePaths } } function useWorkspaceLoadRefs() { const loadedWorkspacePathsRef = useRef>(new Set()) const loadingWorkspacePathsRef = useRef>(new Set()) const initialLoadedVaultPathRef = useRef(null) const isWorkspacePathLoaded = useCallback((path: string) => ( path.trim().length > 0 && loadedWorkspacePathsRef.current.has(path) ), []) return { initialLoadedVaultPathRef, isWorkspacePathLoaded, loadedWorkspacePathsRef, loadingWorkspacePathsRef, } } function keepDesiredWorkspacePaths( paths: Set, desiredWorkspacePaths: readonly string[], ): Set { const desiredPathSet = new Set(desiredWorkspacePaths) return new Set([...paths].filter((path) => desiredPathSet.has(path))) } function usePrunedWorkspaceLoadRefs( { loadedWorkspacePathsRef, loadingWorkspacePathsRef, }: Pick, desiredWorkspaceKey: string, desiredWorkspacePaths: readonly string[], ) { useEffect(() => { loadedWorkspacePathsRef.current = keepDesiredWorkspacePaths( loadedWorkspacePathsRef.current, desiredWorkspacePaths, ) loadingWorkspacePathsRef.current = keepDesiredWorkspacePaths( loadingWorkspacePathsRef.current, desiredWorkspacePaths, ) }, [desiredWorkspaceKey, desiredWorkspacePaths, loadedWorkspacePathsRef, loadingWorkspacePathsRef]) } function useInitialLoadedWorkspaceMarker({ entries, initialLoadedVaultPathRef, isLoading, loadedWorkspacePathsRef, vaultPath, vaults, }: { entries: VaultEntry[] initialLoadedVaultPathRef: MutableRefObject isLoading: boolean loadedWorkspacePathsRef: MutableRefObject> vaultPath: string vaults?: VaultOption[] }) { useEffect(() => { if (isLoading || !hasVaultPath({ vaultPath }) || initialLoadedVaultPathRef.current === vaultPath) return initialLoadedVaultPathRef.current = vaultPath loadedWorkspacePathsRef.current = new Set([ ...loadedWorkspacePathsRef.current, ...loadedWorkspacePathsFromEntries(entries, vaultPath, { inferFallbackWorkspacePath: !vaults?.length, }), ]) }, [entries, initialLoadedVaultPathRef, isLoading, loadedWorkspacePathsRef, vaultPath, vaults]) } function useWorkspaceMetadataRetagEffect({ defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, setEntries, vaultPath, vaults, }: { defaultWorkspacePath?: string | null desiredWorkspaceKey: string desiredWorkspacePaths: readonly string[] setEntries: Dispatch> vaultPath: string vaults?: VaultOption[] }) { useEffect(() => { if (!hasVaultPath({ vaultPath })) return setEntries((currentEntries) => retagEntriesForWorkspaceMetadata({ defaultWorkspacePath, entries: pruneEntriesOutsideWorkspaceSet({ desiredPaths: desiredWorkspacePaths, entries: currentEntries, fallbackVaultPath: vaultPath, }), fallbackVaultPath: vaultPath, vaults, })) }, [defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, setEntries, vaultPath, vaults]) } function missingWorkspaceVaults({ desiredWorkspacePaths, loadedPaths, loadingPaths, vaults, }: { desiredWorkspacePaths: readonly string[] loadedPaths: Set loadingPaths: Set vaults?: VaultOption[] }): VaultOption[] { if (!vaults?.length) return [] return vaults.filter((vault) => ( desiredWorkspacePaths.includes(vault.path) && !loadedPaths.has(vault.path) && !loadingPaths.has(vault.path) )) } function loadMissingWorkspaceEntries({ defaultWorkspacePath, isCurrentVaultPath, loadedPaths, loadingPaths, setEntries, vault, vaultPath, vaults, }: { defaultWorkspacePath?: string | null isCurrentVaultPath: (path: string) => boolean loadedPaths: Set loadingPaths: Set setEntries: Dispatch> vault: VaultOption vaultPath: string vaults?: VaultOption[] }) { void loadWorkspaceEntries(vault, defaultWorkspacePath, { reloadIfEmpty: true }) .then((loadedEntries) => { if (!isCurrentVaultPath(vaultPath)) return loadedPaths.add(vault.path) setEntries((currentEntries) => replaceWorkspaceEntries({ defaultWorkspacePath, entries: currentEntries, fallbackVaultPath: vaultPath, loadedEntries, loadedWorkspacePath: vault.path, vaults, })) }) .catch((error: unknown) => { console.warn(`Failed to load workspace entries for ${vault.path}:`, error) }) .finally(() => { loadingPaths.delete(vault.path) }) } function useMissingWorkspaceLoads({ defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, isCurrentVaultPath, isLoading, loadedWorkspacePathsRef, loadingWorkspacePathsRef, setEntries, vaultPath, vaults, }: { defaultWorkspacePath?: string | null desiredWorkspaceKey: string desiredWorkspacePaths: readonly string[] isCurrentVaultPath: (path: string) => boolean isLoading: boolean loadedWorkspacePathsRef: MutableRefObject> loadingWorkspacePathsRef: MutableRefObject> setEntries: Dispatch> vaultPath: string vaults?: VaultOption[] }) { useEffect(() => { if (!hasVaultPath({ vaultPath }) || !vaults?.length || isLoading) return const loadedPaths = loadedWorkspacePathsRef.current const loadingPaths = loadingWorkspacePathsRef.current const missingVaults = missingWorkspaceVaults({ desiredWorkspacePaths, loadedPaths, loadingPaths, vaults }) if (missingVaults.length === 0) return for (const vault of missingVaults) loadingPaths.add(vault.path) for (const vault of missingVaults) { loadMissingWorkspaceEntries({ defaultWorkspacePath, isCurrentVaultPath, loadedPaths, loadingPaths, setEntries, vault, vaultPath, vaults, }) } }, [ defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, isCurrentVaultPath, isLoading, loadedWorkspacePathsRef, loadingWorkspacePathsRef, setEntries, vaultPath, vaults, ]) } function useWorkspaceEntrySync({ defaultWorkspacePath, entries, isCurrentVaultPath, isLoading, setEntries, vaultPath, vaults, }: { defaultWorkspacePath?: string | null entries: VaultEntry[] isCurrentVaultPath: (path: string) => boolean isLoading: boolean setEntries: Dispatch> vaultPath: string vaults?: VaultOption[] }) { const { desiredWorkspaceKey, desiredWorkspacePaths } = useDesiredWorkspacePaths(vaultPath, vaults) const refs = useWorkspaceLoadRefs() usePrunedWorkspaceLoadRefs(refs, desiredWorkspaceKey, desiredWorkspacePaths) useInitialLoadedWorkspaceMarker({ entries, vaultPath, vaults, isLoading, ...refs }) useWorkspaceMetadataRetagEffect({ defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, setEntries, vaultPath, vaults, }) useMissingWorkspaceLoads({ defaultWorkspacePath, desiredWorkspaceKey, desiredWorkspacePaths, isCurrentVaultPath, isLoading, setEntries, vaultPath, vaults, ...refs, }) return refs.isWorkspacePathLoaded } interface VaultLoaderResultOptions { entryMutations: ReturnType gitLoaders: ReturnType state: ReturnType unavailableVault: ReturnType vaultReloads: ReturnType } function useVaultLoaderResult({ entryMutations, gitLoaders, state, unavailableVault, vaultReloads, }: VaultLoaderResultOptions) { const { modified, pendingSave, tracker, unsaved } = state const getNoteStatus = useCallback((path: string): NoteStatus => resolveNoteStatus({ path, newPaths: tracker.newPaths, modifiedFiles: modified.modifiedFiles, pendingSavePaths: pendingSave.pendingSavePaths, unsavedPaths: unsaved.unsavedPaths, }), [tracker.newPaths, modified.modifiedFiles, pendingSave.pendingSavePaths, unsaved.unsavedPaths]) return { entries: state.entries, folders: state.folders, isLoading: state.isLoading, isReloading: vaultReloads.isReloading, views: state.views, modifiedFiles: modified.modifiedFiles, modifiedFilesError: modified.modifiedFilesError, unavailableVaultPath: unavailableVault.unavailableVaultPath, ...entryMutations, loadModifiedFiles: modified.loadModifiedFiles, ...gitLoaders, getNoteStatus, reloadVault: vaultReloads.reloadVault, reloadFolders: vaultReloads.reloadFolders, reloadViews: vaultReloads.reloadViews, markVaultUnavailable: unavailableVault.markVaultUnavailable, addPendingSave: pendingSave.addPendingSave, removePendingSave: pendingSave.removePendingSave, unsavedPaths: unsaved.unsavedPaths, trackUnsaved: unsaved.trackUnsaved, clearUnsaved: unsaved.clearUnsaved, } } export function useVaultLoader(vaultPath: string, vaults?: VaultOption[], defaultWorkspacePath?: string | null, folderVaults?: VaultOption[]) { const state = useVaultState(vaultPath) const setInitialFolders = useInitialFolderSetter(folderVaults, state.setFolders) const entryMutations = useEntryMutations(state.setEntries, state.tracker.trackNew) const gitLoaders = useGitLoaders(vaultPath) const unavailableVault = useVaultUnavailable(vaultPath, state) const vaultReloads = useVaultReloads({ handleVaultAvailable: unavailableVault.markVaultAvailable, handleVaultUnavailable: unavailableVault.markVaultUnavailable, defaultWorkspacePath, folderVaults, vaultPath, vaults, isCurrentVaultPath: state.isCurrentVaultPath, loadModifiedFiles: state.modified.loadModifiedFiles, setEntries: state.setEntries, setFolders: state.setFolders, setViews: state.setViews, }) useGitignoredVisibilityReloads(vaultReloads) useVaultChromeReloadEffect(vaultPath, vaultReloads.reloadFolders, vaultReloads.reloadViews) const isWorkspacePathLoaded = useWorkspaceEntrySync({ defaultWorkspacePath, entries: state.entries, isCurrentVaultPath: state.isCurrentVaultPath, isLoading: state.isLoading, setEntries: state.setEntries, vaultPath, vaults, }) useInitialVaultLoad({ handleVaultAvailable: unavailableVault.markVaultAvailable, handleVaultUnavailable: unavailableVault.markVaultUnavailable, vaultPath, vaults, folderVaults, defaultWorkspacePath, isWorkspacePathLoaded, tracker: state.tracker, unsaved: state.unsaved, isCurrentVaultPath: state.isCurrentVaultPath, resetReloading: vaultReloads.resetReloading, setEntries: state.setEntries, setFolders: setInitialFolders, setIsLoading: state.setIsLoading, setModifiedFiles: state.modified.setModifiedFiles, setModifiedFilesError: state.modified.setModifiedFilesError, setViews: state.setViews, }) return useVaultLoaderResult({ entryMutations, gitLoaders, state, unavailableVault, vaultReloads }) }