Last active
April 18, 2022 20:26
-
-
Save michaelfester/9f68fbfe14204435f311104754e21a62 to your computer and use it in GitHub Desktop.
Yjs + File System Access API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' | |
import { createContainer } from 'unstated-next' | |
import ProjectProvider from '@context/project' | |
import { File, Folder, Project } from '@shared/types' | |
import { getHandles, HandleWithPath, isIgnoredPath } from '@lib/fs/fs-helpers' | |
import { useStore } from '@context/store' | |
import { set as idbSet, get as idbGet } from 'idb-keyval' | |
import { getNormalizedAbsolutePathFromPathComponents } from '@lib/tree-utils' | |
import { useDeepCompareEffectNoCheck } from '@lib/hooks/use-deep-compare-effect' | |
import { areArraysEqualSets } from '@lib/utils/utils' | |
import isEmpty from 'lodash.isempty' | |
const STORE_KEY_DIRECTORY_HANDLE_BY_PROJECT = | |
'STORE_KEY_DIRECTORY_HANDLE_BY_PROJECT' | |
const useFSHandles = () => { | |
const { | |
currentProject, | |
currentProjectIdPathMap, | |
currentProjectFolderIdPathMap, | |
currentProjectFileIdFileMap, | |
currentProjectFolderIdFolderMap, | |
} = ProjectProvider.useContainer() | |
const { autoSyncByProject } = useStore() | |
const [state, setState] = useState<{ | |
didLoadHandlesInitially: boolean | |
handlesByProject?: Record<Project['id'], FileSystemDirectoryHandle> | |
}>({ didLoadHandlesInitially: false }) | |
const [ | |
isSyncPermissionsGrantedForCurrentProject, | |
setSyncPermissionsGrantedForCurrentProject, | |
] = useState(false) | |
const allHandlesInProjectFolder = useRef<HandleWithPath[]>([]) | |
const fileIdHandleMap = useRef<Record<File['id'], FileSystemFileHandle>>({}) | |
const folderIdHandleMap = useRef< | |
Record<Folder['id'], FileSystemDirectoryHandle> | |
>({}) | |
const currentProjectDirectoryHandle = useMemo(() => { | |
if (!currentProject?.id) { | |
return undefined | |
} | |
return state.handlesByProject?.[currentProject.id] | |
}, [currentProject?.id, state.handlesByProject]) | |
const isAutoSyncEnabledForCurrentProject = !!( | |
currentProjectDirectoryHandle && | |
currentProject?.id && | |
autoSyncByProject?.[currentProject.id] | |
) | |
const isSyncEnabledAndGrantedForCurrentProject = | |
isAutoSyncEnabledForCurrentProject && | |
isSyncPermissionsGrantedForCurrentProject | |
const refreshHandlesInProjectFolder = useCallback(async () => { | |
if ( | |
!currentProjectDirectoryHandle || | |
!isSyncEnabledAndGrantedForCurrentProject | |
) { | |
return | |
} | |
const handles = await getHandles(currentProjectDirectoryHandle) | |
allHandlesInProjectFolder.current = handles | |
if (!handles) { | |
fileIdHandleMap.current = {} | |
folderIdHandleMap.current = {} | |
} else { | |
for (const handle of allHandlesInProjectFolder.current) { | |
if (isIgnoredPath(handle.path)) { | |
continue | |
} | |
const path = getNormalizedAbsolutePathFromPathComponents(handle.path) | |
if (handle.type === 'file') { | |
const fileId = Object.keys(currentProjectIdPathMap).find((k) => { | |
return currentProjectIdPathMap[k] === path | |
}) | |
if (fileId) { | |
fileIdHandleMap.current[fileId] = | |
handle.handle as FileSystemFileHandle | |
} | |
} else if (handle.type === 'directory') { | |
const folderId = Object.keys(currentProjectFolderIdPathMap).find( | |
(k) => { | |
return currentProjectFolderIdPathMap[k] === path | |
} | |
) | |
if (folderId) { | |
folderIdHandleMap.current[folderId] = | |
handle.handle as FileSystemDirectoryHandle | |
} | |
} | |
} | |
} | |
}, [ | |
currentProjectDirectoryHandle, | |
isSyncEnabledAndGrantedForCurrentProject, | |
currentProjectIdPathMap, | |
currentProjectFolderIdPathMap, | |
]) | |
useEffect(() => { | |
refreshHandlesInProjectFolder() | |
}, [ | |
refreshHandlesInProjectFolder, | |
currentProjectDirectoryHandle, | |
isSyncEnabledAndGrantedForCurrentProject, | |
currentProjectIdPathMap, | |
currentProjectFolderIdPathMap, | |
]) | |
useEffect(() => { | |
const loadInitial = async () => { | |
const handles = await idbGet(STORE_KEY_DIRECTORY_HANDLE_BY_PROJECT) | |
setState({ | |
didLoadHandlesInitially: true, | |
handlesByProject: handles, | |
}) | |
} | |
loadInitial() | |
}, []) | |
useDeepCompareEffectNoCheck(() => { | |
const save = async () => { | |
if (!state.handlesByProject) { | |
return | |
} | |
// We safe-load the previous version from cache, to ensure we don't | |
// accidentally overwrite with an empty value, e.g. during initialization | |
// phase where variables are not yet set. | |
const saveHandles = await idbGet(STORE_KEY_DIRECTORY_HANDLE_BY_PROJECT) | |
idbSet(STORE_KEY_DIRECTORY_HANDLE_BY_PROJECT, { | |
...saveHandles, | |
...state.handlesByProject, | |
}) | |
} | |
save() | |
}, [state.handlesByProject]) | |
const setCurrentProjectDirectoryHandle = useCallback( | |
async (handle: FileSystemDirectoryHandle) => { | |
if (!currentProject?.id) { | |
return | |
} | |
setState((s) => ({ | |
...s, | |
handlesByProject: { | |
...(s.handlesByProject || {}), | |
[currentProject.id]: handle, | |
}, | |
})) | |
}, | |
[currentProject?.id] | |
) | |
const getProjectFileParentDirectoryHandle = useCallback( | |
(fileId: File['id']) => { | |
// Given a project file, look at its project parentFolderId | |
// and return the matching folder handle. | |
const parentFolderId = currentProjectFileIdFileMap[fileId]?.parentFolderId | |
if (!parentFolderId) { | |
return currentProjectDirectoryHandle | |
} | |
return folderIdHandleMap.current?.[parentFolderId] | |
}, | |
[ | |
folderIdHandleMap, | |
currentProjectDirectoryHandle, | |
currentProjectFileIdFileMap, | |
] | |
) | |
const getProjectFolderParentDirectoryHandle = useCallback( | |
(folderId: Folder['id']) => { | |
// Given a project folder, look at its project parentFolderId | |
// and return the matching folder handle. | |
const parentFolderId = | |
currentProjectFolderIdFolderMap[folderId]?.parentFolderId | |
if (!parentFolderId) { | |
return currentProjectDirectoryHandle | |
} | |
return folderIdHandleMap.current?.[parentFolderId] | |
}, | |
[ | |
folderIdHandleMap, | |
currentProjectDirectoryHandle, | |
currentProjectFolderIdFolderMap, | |
] | |
) | |
const resolveHandleWithPath = useCallback( | |
async (handle: FileSystemHandle) => { | |
for (const h of allHandlesInProjectFolder.current) { | |
if (await (h.handle as any).isSameEntry(handle)) { | |
return h | |
} | |
} | |
return undefined | |
}, | |
[allHandlesInProjectFolder] | |
) | |
const getHandleWithPathAtPath = useCallback( | |
(path: string[]): HandleWithPath | undefined => { | |
if (isEmpty(path) && !!currentProjectDirectoryHandle) { | |
return { | |
path: [], | |
handle: currentProjectDirectoryHandle, | |
type: 'directory', | |
} | |
} | |
for (const h of allHandlesInProjectFolder.current) { | |
if (areArraysEqualSets(h.path, path)) { | |
return h | |
} | |
} | |
return undefined | |
}, | |
[currentProjectDirectoryHandle] | |
) | |
const getFileFSParentDirectoryHandle = useCallback( | |
async (fileId: File['id']) => { | |
// Given a project file, look at its FS parent directory | |
// and return the matching folder handle. | |
const fileHandle = fileIdHandleMap.current?.[fileId] | |
if (!fileHandle) { | |
return undefined | |
} | |
const fileHandleWithPath = await resolveHandleWithPath(fileHandle) | |
if (!fileHandleWithPath) { | |
return undefined | |
} | |
const parentHandlePath = fileHandleWithPath.path?.slice(0, -1) | |
if (!parentHandlePath || isEmpty(parentHandlePath)) { | |
return currentProjectDirectoryHandle | |
} | |
return getHandleWithPathAtPath(parentHandlePath) | |
?.handle as FileSystemDirectoryHandle | |
}, | |
[ | |
resolveHandleWithPath, | |
getHandleWithPathAtPath, | |
currentProjectDirectoryHandle, | |
] | |
) | |
const getFolderFSParentDirectoryHandle = useCallback( | |
async (folderId: Folder['id']) => { | |
// Given a project folder, look at its FS parent directory | |
// and return the matching folder handle. | |
const folderHandle = folderIdHandleMap.current?.[folderId] | |
if (!folderHandle) { | |
return undefined | |
} | |
const folderHandleWithPath = await resolveHandleWithPath(folderHandle) | |
if (!folderHandleWithPath) { | |
return undefined | |
} | |
const parentHandlePath = folderHandleWithPath.path?.slice(0, -1) | |
if (!parentHandlePath || isEmpty(parentHandlePath)) { | |
return currentProjectDirectoryHandle | |
} | |
return getHandleWithPathAtPath(parentHandlePath) | |
?.handle as FileSystemDirectoryHandle | |
}, | |
[ | |
resolveHandleWithPath, | |
getHandleWithPathAtPath, | |
currentProjectDirectoryHandle, | |
] | |
) | |
const getFileIdFromHandle = useCallback(async (handle: FileSystemHandle) => { | |
for (const id of Object.keys(fileIdHandleMap.current)) { | |
if (await fileIdHandleMap.current?.[id]?.isSameEntry(handle)) { | |
return id | |
} | |
} | |
return undefined | |
}, []) | |
const getFolderIdFromHandle = useCallback( | |
async (handle: FileSystemHandle) => { | |
for (const id of Object.keys(folderIdHandleMap.current)) { | |
if (await folderIdHandleMap.current?.[id]?.isSameEntry(handle)) { | |
return id | |
} | |
} | |
return undefined | |
}, | |
[] | |
) | |
return { | |
currentProjectDirectoryHandle, | |
fileIdHandleMap, | |
folderIdHandleMap, | |
setCurrentProjectDirectoryHandle, | |
isAutoSyncEnabledForCurrentProject, | |
isSyncEnabledAndGrantedForCurrentProject, | |
didLoadHandlesInitially: state.didLoadHandlesInitially, | |
getProjectFileParentDirectoryHandle, | |
getProjectFolderParentDirectoryHandle, | |
getFileFSParentDirectoryHandle, | |
getFolderFSParentDirectoryHandle, | |
refreshHandlesInProjectFolder, | |
isSyncPermissionsGrantedForCurrentProject, | |
setSyncPermissionsGrantedForCurrentProject, | |
allHandlesInProjectFolder, | |
getFileIdFromHandle, | |
getFolderIdFromHandle, | |
getHandleWithPathAtPath, | |
} | |
} | |
export default createContainer(useFSHandles) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import isEmpty from 'lodash.isempty' | |
export type HandleWithPath = { | |
handle: FileSystemHandle | |
path: string[] | |
type: 'file' | 'directory' | |
} | |
const readWriteOptions = { mode: 'readwrite' } | |
export const isReadWritePermissionGranted = async ( | |
handle: FileSystemFileHandle | FileSystemDirectoryHandle | |
) => { | |
return (await (handle as any).queryPermission(readWriteOptions)) === 'granted' | |
} | |
export const askReadWritePermissionsIfNeeded = async ( | |
handle: FileSystemFileHandle | FileSystemDirectoryHandle | |
) => { | |
if (await isReadWritePermissionGranted(handle)) { | |
return true | |
} | |
const permission = await (handle as any).requestPermission(readWriteOptions) | |
return permission === 'granted' | |
} | |
const createEmptyFileInFolder = async ( | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
name: string | |
): Promise<FileSystemFileHandle> => { | |
return await parentDirectoryHandle.getFileHandle(name, { create: true }) | |
} | |
export const createFolderInFolder = async ( | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
name: string | |
): Promise<FileSystemDirectoryHandle> => { | |
return await parentDirectoryHandle.getDirectoryHandle(name, { create: true }) | |
} | |
export const writeContentToFile = async ( | |
fileHandle: FileSystemFileHandle, | |
content: string | |
) => { | |
const writable = await (fileHandle as any).createWritable() | |
await writable.write(content) | |
await writable.close() | |
} | |
export const writeContentToFSFileIfChanged = async ( | |
fsFile: globalThis.File, | |
fileHandle: FileSystemFileHandle, | |
content: string | |
) => { | |
const fsFileContent = await fsFile.text() | |
if (fsFileContent === content) { | |
return | |
} | |
await writeContentToFile(fileHandle, content) | |
} | |
export const renameFile = async ( | |
fsFile: globalThis.File, | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
name: string | |
) => { | |
// Move and rename is not currently supported by the FileSystem | |
// Access API so we need to do this they manual way by creating | |
// a new file and deleting the old one. | |
const content = await fsFile.text() | |
await createFileWithContentInFolder(parentDirectoryHandle, name, content) | |
await deleteFile(parentDirectoryHandle, fsFile.name) | |
} | |
export const moveFile = async ( | |
fsFile: globalThis.File, | |
sourceDirectoryHandle: FileSystemDirectoryHandle, | |
destinationDirectoryHandle: FileSystemDirectoryHandle | |
) => { | |
// Same comment as renameFile | |
const content = await fsFile.text() | |
await createFileWithContentInFolder( | |
destinationDirectoryHandle, | |
fsFile.name, | |
content | |
) | |
await deleteFile(sourceDirectoryHandle, fsFile.name) | |
} | |
export const moveFolderContent = async ( | |
sourceFolderHandle: FileSystemDirectoryHandle, | |
destinationFolderHandle: FileSystemDirectoryHandle | |
) => { | |
for await (const handle of (sourceFolderHandle as any).values()) { | |
if (handle.kind === 'file') { | |
const fsFile = await (handle as FileSystemFileHandle).getFile() | |
await moveFile(fsFile, sourceFolderHandle, destinationFolderHandle) | |
} else if (handle.kind === 'directory') { | |
await moveFolder(handle, sourceFolderHandle, destinationFolderHandle) | |
} | |
} | |
} | |
export const moveFolder = async ( | |
folderHandle: FileSystemDirectoryHandle, | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
destinationFolderHandle: FileSystemDirectoryHandle | |
) => { | |
const newFolderHandle = await createFolderInFolder( | |
destinationFolderHandle, | |
folderHandle.name | |
) | |
await moveFolderContent(folderHandle, newFolderHandle) | |
await deleteFolder(folderHandle.name, parentDirectoryHandle) | |
} | |
export const renameFolder = async ( | |
folderHandle: FileSystemDirectoryHandle, | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
newName: string | |
) => { | |
const newFolderHandle = await createFolderInFolder( | |
parentDirectoryHandle, | |
newName | |
) | |
try { | |
await moveFolderContent(folderHandle, newFolderHandle) | |
await deleteFolder(folderHandle.name, parentDirectoryHandle) | |
} catch { | |
// Do nothing | |
} | |
} | |
export const createFileWithContentInFolder = async ( | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
name: string, | |
content: string | |
): Promise<FileSystemFileHandle> => { | |
const newFileHandle = await createEmptyFileInFolder( | |
parentDirectoryHandle, | |
name | |
) | |
await writeContentToFile(newFileHandle, content) | |
return newFileHandle | |
} | |
export const deleteFile = async ( | |
parentDirectoryHandle: FileSystemDirectoryHandle, | |
name: string | |
) => { | |
await parentDirectoryHandle.removeEntry(name) | |
} | |
export const deleteFolder = async ( | |
name: string, | |
parentDirectoryHandle: FileSystemDirectoryHandle | |
) => { | |
await parentDirectoryHandle.removeEntry(name, { | |
recursive: true, | |
}) | |
} | |
export const getHandles = async ( | |
directoryHandle: FileSystemDirectoryHandle, | |
savePath?: string[] | |
): Promise<HandleWithPath[]> => { | |
let handles: HandleWithPath[] = [] | |
for await (const handle of (directoryHandle as any).values()) { | |
const relativePath = (await directoryHandle.resolve(handle)) || [] | |
if (isIgnoredPath(relativePath)) { | |
continue | |
} | |
const fullPath = [...(savePath || []), ...relativePath] | |
handles = [ | |
...handles, | |
{ | |
handle, | |
path: fullPath, | |
type: handle.kind === 'file' ? 'file' : 'directory', | |
}, | |
] | |
if (handle.kind === 'directory') { | |
handles = [...handles, ...(await getHandles(handle, fullPath))] | |
} | |
} | |
return handles | |
} | |
export const isHandlesEqual = async ( | |
handle: FileSystemHandle | undefined, | |
otherHandle: FileSystemHandle | undefined | |
) => { | |
if (!handle && !otherHandle) { | |
return true | |
} | |
if (handle && otherHandle) { | |
return await (handle as any)?.isSameEntry(otherHandle) | |
} | |
return false | |
} | |
export const isIgnoredPath = (path: string[]): boolean => { | |
// Return true if the file at the given path should be ignored for | |
// syncing. This is the case currently if the path contains a component | |
// that starts with a period, e.g. ".git" or ".DS_Store". | |
return !!path.find((p) => p.startsWith('.') || p.endsWith('.crswap')) | |
} | |
export const isTextMimeType = (file: globalThis.File) => { | |
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types | |
return file.type.startsWith('text/') || isEmpty(file.type) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContainer } from 'unstated-next' | |
import ProjectProvider from '@context/project' | |
import FSHandlesProvider from '@context/fs-handles' | |
import { File, FileTreeNode, Folder } from '@shared/types' | |
import { useCallback, useEffect, useRef, useState } from 'react' | |
import { set as idbSet, get as idbGet } from 'idb-keyval' | |
import { | |
askReadWritePermissionsIfNeeded, | |
createFileWithContentInFolder, | |
createFolderInFolder, | |
deleteFile, | |
deleteFolder, | |
isHandlesEqual, | |
isReadWritePermissionGranted, | |
isTextMimeType, | |
moveFile, | |
moveFolder, | |
renameFile, | |
renameFolder, | |
writeContentToFSFileIfChanged, | |
} from '@lib/fs/fs-helpers' | |
import { | |
getDeltaOperations, | |
toTextContent, | |
updateDocWithDeltas, | |
} from '@lib/yjs/ydoc' | |
import isEmpty from 'lodash.isempty' | |
import emitter, { | |
EVENT_NOTIFY_COMMAND_S, | |
EVENT_NOTIFY_CONTENT_UPDATE, | |
EVENT_PROGRAMMATIC_DOC_UPDATE, | |
} from '@lib/events' | |
import { sortBy, uniq } from 'lodash-es' | |
const STORE_KEY_CACHED_FS_FILE = 'STORE_KEY_CACHED_FS_FILE' | |
const STORE_KEY_TRASHED_FILE_IDS = 'STORE_KEY_TRASHED_FILE_IDS' | |
const STORE_KEY_TRASHED_FOLDER_IDS = 'STORE_KEY_TRASHED_FOLDER_IDS' | |
type CachedFSFileData = { | |
name: string | |
content: string | |
lastModified: number | |
} | |
const getCachedFSFileKey = (id: File['id']) => { | |
return `${STORE_KEY_CACHED_FS_FILE}-${id}` | |
} | |
const writeProjectFileToFSDataCache = async ( | |
projectFileId: File['id'], | |
projectFileName: string, | |
projectFileContent: string, | |
lastModified: number | |
) => { | |
await idbSet( | |
getCachedFSFileKey(projectFileId), | |
JSON.stringify({ | |
name: projectFileName, | |
content: projectFileContent, | |
lastModified, | |
}) | |
) | |
} | |
const useFS = () => { | |
const [trashedFileIds, setTrashedFileIds] = useState<File['id'][]>([]) | |
const [trashedFolderIds, setTrashedFolderIds] = useState<Folder['id'][]>([]) | |
const { | |
currentProjectFiles, | |
currentProjectFileIdFileMap, | |
currentProjectFolderIdFolderMap, | |
currentProjectFolders, | |
updateFileContent, | |
createFileWithPathInCurrentProject, | |
createFolderWithPathInCurrentProject, | |
currentProjectFolderIdPathMap, | |
currentProjectTrashedFiles, | |
currentProjectTrashedFolders, | |
} = ProjectProvider.useContainer() | |
const { | |
currentProjectDirectoryHandle, | |
setCurrentProjectDirectoryHandle, | |
isAutoSyncEnabledForCurrentProject, | |
isSyncEnabledAndGrantedForCurrentProject, | |
didLoadHandlesInitially, | |
fileIdHandleMap, | |
folderIdHandleMap, | |
getProjectFileParentDirectoryHandle, | |
getProjectFolderParentDirectoryHandle, | |
getFileFSParentDirectoryHandle, | |
getFolderFSParentDirectoryHandle, | |
refreshHandlesInProjectFolder, | |
isSyncPermissionsGrantedForCurrentProject, | |
setSyncPermissionsGrantedForCurrentProject, | |
getFileIdFromHandle, | |
getFolderIdFromHandle, | |
allHandlesInProjectFolder, | |
} = FSHandlesProvider.useContainer() | |
const isPerformingOperation = useRef<boolean>(false) | |
useEffect(() => { | |
const loadTrashedFilesAndFolders = async () => { | |
const trashedFiles = await idbGet(STORE_KEY_TRASHED_FILE_IDS) | |
if (trashedFiles) { | |
try { | |
setTrashedFileIds(JSON.parse(trashedFiles) || []) | |
} catch { | |
// Do nothing | |
} | |
} | |
const trashedFolders = await idbGet(STORE_KEY_TRASHED_FOLDER_IDS) | |
if (trashedFolders) { | |
try { | |
setTrashedFolderIds(JSON.parse(trashedFolders) || []) | |
} catch { | |
// Do nothing | |
} | |
} | |
} | |
loadTrashedFilesAndFolders() | |
}, []) | |
useEffect(() => { | |
const save = async () => { | |
// We safe-load the previous version from cache, to ensure we don't | |
// accidentally overwrite with an empty array, e.g. during initialization | |
// phase where variables are not yet set. | |
let saveTrashedFiles: string[] = [] | |
try { | |
const fileIds = await idbGet(STORE_KEY_TRASHED_FILE_IDS) | |
saveTrashedFiles = JSON.parse(fileIds) | |
} catch { | |
// Do nothing | |
} | |
idbSet( | |
STORE_KEY_TRASHED_FILE_IDS, | |
JSON.stringify(uniq([...saveTrashedFiles, ...trashedFileIds])) | |
) | |
} | |
save() | |
}, [trashedFileIds]) | |
useEffect(() => { | |
const save = async () => { | |
// We safe-load the previous version from cache, to ensure we don't | |
// accidentally overwrite with an empty array, e.g. during initialization | |
// phase where variables are not yet set. | |
let saveTrashedFolders: string[] = [] | |
try { | |
const folderIds = await idbGet(STORE_KEY_TRASHED_FOLDER_IDS) | |
saveTrashedFolders = JSON.parse(folderIds) | |
} catch { | |
// Do nothing | |
} | |
idbSet( | |
STORE_KEY_TRASHED_FOLDER_IDS, | |
JSON.stringify(uniq([...saveTrashedFolders, ...trashedFolderIds])) | |
) | |
} | |
save() | |
}, [trashedFolderIds]) | |
const getFileFromFS = useCallback( | |
async (id: File['id']): Promise<globalThis.File | undefined> => { | |
const handle = fileIdHandleMap.current?.[id] | |
if (!handle || handle.kind !== 'file') { | |
return undefined | |
} | |
try { | |
return await (handle as FileSystemFileHandle)?.getFile() | |
} catch { | |
// Do nothing | |
} | |
return undefined | |
}, | |
[fileIdHandleMap] | |
) | |
const getCachedFSFileData = useCallback( | |
async (id: File['id']): Promise<CachedFSFileData | undefined> => { | |
const jsonFile = await idbGet(getCachedFSFileKey(id)) | |
if (jsonFile) { | |
return JSON.parse(jsonFile) | |
} | |
return undefined | |
}, | |
[] | |
) | |
const createFSFolderFromProjectFolder = useCallback( | |
async ( | |
folderId: Folder['id'] | |
): Promise<FileSystemDirectoryHandle | undefined> => { | |
// IMPORTANT: keep in mind that some of these files may belong | |
// to folders that have been trashed, and thus deleted from the | |
// file system. So we cannot assumt that the parent folder exists | |
// in the file system. | |
const folder = currentProjectFolderIdFolderMap[folderId] | |
if (!folder) { | |
return undefined | |
} | |
let parentFolderHandle: FileSystemDirectoryHandle | undefined = undefined | |
if (!folder?.parentFolderId) { | |
parentFolderHandle = currentProjectDirectoryHandle | |
} else { | |
const p = folderIdHandleMap.current?.[folder.parentFolderId] | |
if (p) { | |
parentFolderHandle = p | |
} else { | |
parentFolderHandle = await createFSFolderFromProjectFolder( | |
folder.parentFolderId | |
) | |
// Immediately update the folderIdHandleMap | |
if (parentFolderHandle) { | |
folderIdHandleMap.current = { | |
...folderIdHandleMap.current, | |
[folder.parentFolderId]: parentFolderHandle, | |
} | |
} | |
} | |
} | |
if (!parentFolderHandle) { | |
return undefined | |
} | |
try { | |
return await createFolderInFolder(parentFolderHandle, folder.name) | |
} catch { | |
// Do nothing | |
} | |
return undefined | |
}, | |
[ | |
currentProjectFolderIdFolderMap, | |
currentProjectDirectoryHandle, | |
folderIdHandleMap, | |
] | |
) | |
const writeProjectFileToNewFileInFS = useCallback( | |
async (file: File) => { | |
// IMPORTANT: keep in mind that some of these files may belong | |
// to folders that have been trashed, and thus deleted from the | |
// file system. So we cannot assumt that the parent folder exists | |
// in the file system. | |
let folderHandle: FileSystemDirectoryHandle | undefined = undefined | |
if (!file.parentFolderId) { | |
// Only if the parentFolderId is explicitly null do we set | |
// folderHandle to currentProjectDirectoryHandle. Otherwise | |
// we risk writing all files to the root folder. | |
folderHandle = currentProjectDirectoryHandle | |
} else if (file.parentFolderId in folderIdHandleMap.current) { | |
folderHandle = folderIdHandleMap.current?.[file.parentFolderId] | |
} else { | |
folderHandle = await createFSFolderFromProjectFolder( | |
file.parentFolderId | |
) | |
} | |
if (!folderHandle) { | |
return | |
} | |
return await createFileWithContentInFolder( | |
folderHandle, | |
file.name, | |
toTextContent(file.encodedYDoc) | |
) | |
}, | |
[ | |
currentProjectDirectoryHandle, | |
folderIdHandleMap, | |
createFSFolderFromProjectFolder, | |
] | |
) | |
const syncFile = useCallback( | |
async (id: File['id'], isCurrentFile: boolean) => { | |
const writeProjectFileToFSAndFSDataCache = async ( | |
projectFileId: File['id'], | |
projectFileName: string, | |
projectFileContent: string, | |
fsFile: globalThis.File | |
) => { | |
const fileHandle = fileIdHandleMap.current?.[projectFileId] | |
if (!fileHandle) { | |
return | |
} | |
await writeContentToFSFileIfChanged( | |
fsFile, | |
fileHandle, | |
projectFileContent | |
) | |
// File name has changed | |
if (fsFile.name !== projectFileName && !isEmpty(projectFileName)) { | |
const parentDirectoryHandle = | |
getProjectFileParentDirectoryHandle(projectFileId) | |
if (parentDirectoryHandle) { | |
await renameFile(fsFile, parentDirectoryHandle, projectFileName) | |
await refreshHandlesInProjectFolder() | |
return | |
} | |
} | |
// Check if file has moved | |
const projectParentFolderHandle = getProjectFileParentDirectoryHandle( | |
projectFile.id | |
) | |
const fsParentFolderHandle = await getFileFSParentDirectoryHandle( | |
projectFile.id | |
) | |
const isProjectAndFSParentsSame = await isHandlesEqual( | |
fsParentFolderHandle, | |
projectParentFolderHandle | |
) | |
if ( | |
!isProjectAndFSParentsSame && | |
fsParentFolderHandle && | |
projectParentFolderHandle | |
) { | |
// File has moved in project, so move it in file system | |
await moveFile( | |
fsFile, | |
fsParentFolderHandle, | |
projectParentFolderHandle | |
) | |
} | |
// We use the file lastModified timestamp, and not just | |
// client datetime, to ensure we have matching timestamps | |
// between file and cache, for later comparison. In order | |
// to retrieve the value of the timestamp, we need to | |
// retrieve the file again - just reading it from fsFile | |
// won't give us that latest value. | |
const updatedFile = await getFileFromFS(projectFileId) | |
if (updatedFile) { | |
await writeProjectFileToFSDataCache( | |
projectFileId, | |
projectFileName, | |
projectFileContent, | |
updatedFile.lastModified | |
) | |
} | |
} | |
const updateProjectFileAndNotifyEditor = async ( | |
fileId: File['id'], | |
encodedYDoc: string, | |
content: string, | |
triggerCompile: boolean | |
) => { | |
await updateFileContent(fileId, { encodedYDoc }) | |
// This event tells the editor to merge the active | |
// yDoc with the newly created one. | |
emitter.emit(EVENT_PROGRAMMATIC_DOC_UPDATE, { fileId, encodedYDoc }) | |
if (triggerCompile) { | |
// This events tells the editor to trigger an `onContentUpdate` | |
// callback back to the FileView, which will then take | |
// care of compiling and rendering to preview, automatically, | |
// of via Cmd+S sent below. | |
emitter.emit(EVENT_NOTIFY_CONTENT_UPDATE, { fileId, content }) | |
// Todo: find a way to bypass setting shouldCompile.current in case | |
// where auto-render is disabled, so that we don't need to send the | |
// save event to trigger an update of the preview. | |
setTimeout(() => { | |
emitter.emit(EVENT_NOTIFY_COMMAND_S) | |
}, 100) | |
} | |
} | |
const projectFile = currentProjectFileIdFileMap[id] | |
const fsFile = await getFileFromFS(id) | |
// If file is not present in the FS, just write the project file as is. | |
if (!fsFile) { | |
const newFileHandle = await writeProjectFileToNewFileInFS(projectFile) | |
if (newFileHandle) { | |
try { | |
const newFile = await newFileHandle?.getFile() | |
if (newFile) { | |
await writeProjectFileToFSDataCache( | |
projectFile.id, | |
projectFile.name, | |
toTextContent(projectFile.encodedYDoc), | |
newFile.lastModified | |
) | |
} | |
} catch { | |
// Do nothing | |
} | |
} | |
return | |
} | |
const cachedFSFileData = await getCachedFSFileData(id) | |
// Cached version does not exist. This should never happen. Indeed, | |
// even if the user clears the app data, the project folder handle will be | |
// cleared as well, so the user will be asked to select a folder again, | |
// in which case a hard overwrite will happen, and the FSFileData cache | |
// will be populated. So in case `cachedFSFileData` does not exist, we | |
// can consider this situation as similar to the initial file dump | |
// situation and simply overwrite the FS file. | |
if (!cachedFSFileData) { | |
await writeProjectFileToFSAndFSDataCache( | |
projectFile.id, | |
projectFile.name, | |
toTextContent(projectFile.encodedYDoc), | |
fsFile | |
) | |
return | |
} | |
// Cached version exists. This allows us to see the changes in the | |
// local file, and compute the diff which in turn gives us as | |
// state update vector for our CRDT. We can then apply it | |
// to the project file for a seamless merging of the two versions. | |
if (fsFile.lastModified === cachedFSFileData.lastModified) { | |
// File has not changed in the file system. Since the FS file cache | |
// is only set when a project file is synced, this means that the | |
// only option is that the project file has changed, in which | |
// case it should be written to the FS file. | |
await writeProjectFileToFSAndFSDataCache( | |
projectFile.id, | |
projectFile.name, | |
toTextContent(projectFile.encodedYDoc), | |
fsFile | |
) | |
return | |
} | |
const fsFileContent = await fsFile.text() | |
const cachedFSFileContent = cachedFSFileData.content | |
const fsDiffDeltas = getDeltaOperations( | |
cachedFSFileContent, | |
fsFileContent | |
) | |
if (isEmpty(fsDiffDeltas)) { | |
// Same comment as above: no difference between FS file and cached | |
// FS file. | |
await writeProjectFileToFSAndFSDataCache( | |
projectFile.id, | |
projectFile.name, | |
toTextContent(projectFile.encodedYDoc), | |
fsFile | |
) | |
return | |
} | |
if (!projectFile.encodedYDoc) { | |
return | |
} | |
// A change has happened on the local file, since it differs | |
// from the cached version. So we merge it with the project yDoc. | |
const newEncodedYDoc = updateDocWithDeltas( | |
projectFile.encodedYDoc, | |
fsDiffDeltas | |
) | |
const newEncodedYDocContent = toTextContent(newEncodedYDoc) | |
// We now have a new master doc, which has merged the FS content | |
// with the (potentially also updated) project file content. | |
// Since changes can come from both source, the FS content | |
// might also need to be updated, so we perform three updated: | |
// - the FS version | |
// - the cached FS version | |
// - the project version | |
await writeProjectFileToFSAndFSDataCache( | |
projectFile.id, | |
projectFile.name, | |
newEncodedYDocContent, | |
fsFile | |
) | |
await updateProjectFileAndNotifyEditor( | |
projectFile.id, | |
newEncodedYDoc, | |
newEncodedYDocContent, | |
isCurrentFile | |
) | |
}, | |
[ | |
currentProjectFileIdFileMap, | |
fileIdHandleMap, | |
getCachedFSFileData, | |
getFileFromFS, | |
writeProjectFileToNewFileInFS, | |
updateFileContent, | |
getProjectFileParentDirectoryHandle, | |
getFileFSParentDirectoryHandle, | |
refreshHandlesInProjectFolder, | |
] | |
) | |
const createFolderInFSIfNeeded = useCallback( | |
async (folderId: Folder['id']) => { | |
const fsFolderHandle = folderIdHandleMap.current[folderId] | |
if (fsFolderHandle) { | |
// Folder already exists | |
return | |
} | |
const projectFolder = currentProjectFolderIdFolderMap[folderId] | |
if (!projectFolder) { | |
return | |
} | |
const projectParentFolderHandle = | |
await getProjectFolderParentDirectoryHandle(folderId) | |
if (projectParentFolderHandle) { | |
await createFolderInFolder( | |
projectParentFolderHandle, | |
projectFolder.name | |
) | |
} else if (projectFolder.parentFolderId) { | |
// If parentFolderId is not null but projectParentFolderHandle, this | |
// means that the FS parent folder does not exist, so create it. | |
await createFolderInFSIfNeeded(projectFolder.parentFolderId) | |
} | |
}, | |
[ | |
folderIdHandleMap, | |
currentProjectFolderIdFolderMap, | |
getProjectFolderParentDirectoryHandle, | |
] | |
) | |
const syncFolder = useCallback( | |
async (folderId: Folder['id']) => { | |
const projectFolder = currentProjectFolderIdFolderMap[folderId] | |
const fsFolderHandle = folderIdHandleMap.current[folderId] | |
const projectParentFolderHandle = | |
await getProjectFolderParentDirectoryHandle(folderId) | |
if (projectFolder.name !== fsFolderHandle.name) { | |
// Folder has been renamed | |
if (projectParentFolderHandle) { | |
await renameFolder( | |
fsFolderHandle, | |
projectParentFolderHandle, | |
projectFolder.name | |
) | |
await refreshHandlesInProjectFolder() | |
} | |
} | |
// Check if folder has moved | |
const fsParentFolderHandle = await getFolderFSParentDirectoryHandle( | |
folderId | |
) | |
const isProjectAndFSParentsSame = await isHandlesEqual( | |
fsParentFolderHandle, | |
projectParentFolderHandle | |
) | |
if ( | |
!isProjectAndFSParentsSame && | |
fsParentFolderHandle && | |
projectParentFolderHandle | |
) { | |
// Folder has moved in project, so move it in file system | |
await moveFolder( | |
fsFolderHandle, | |
fsParentFolderHandle, | |
projectParentFolderHandle | |
) | |
} | |
}, | |
[ | |
currentProjectFolderIdFolderMap, | |
folderIdHandleMap, | |
getProjectFolderParentDirectoryHandle, | |
refreshHandlesInProjectFolder, | |
getFolderFSParentDirectoryHandle, | |
] | |
) | |
const purgeTrashedProjectFilesFromFS = useCallback( | |
async (trashedFiles: File[]) => { | |
// If a project file has been trashed, remove it from | |
// the file system, but *only* if a newer, untrashed | |
// version is not present in the project, in which case | |
// the file needs to remain. | |
const notYetDeleted = trashedFiles.filter( | |
(f) => !trashedFileIds.includes(f.id) | |
) | |
const newlyDeletedTrashedFileIds: File['id'][] = [] | |
let didDeleteFileInFS = false | |
for (const file of notYetDeleted) { | |
// Check that a newer, untrashed, version with the same name | |
// does not exist | |
const isAlsoNotTrashed = currentProjectFiles.find( | |
(f) => | |
f.name === file.name && f.parentFolderId === file.parentFolderId | |
) | |
if (isAlsoNotTrashed) { | |
// An untrashed version exists, so don't delete, just | |
// mark the file id as deleted. This | |
// happens e.g. if a file was deleted, and another one with | |
// the same parent and name was subsequently created. | |
newlyDeletedTrashedFileIds.push(file.id) | |
} else { | |
let parentDirectoryHandle = undefined | |
if (!file.parentFolderId) { | |
parentDirectoryHandle = currentProjectDirectoryHandle | |
} else { | |
parentDirectoryHandle = | |
folderIdHandleMap.current?.[file.parentFolderId] | |
} | |
// Note that we are not setting parentDirectoryHandle to | |
// currentProjectDirectoryHandle unless parentFolderId is explicitly | |
// undefined. Otherwise we may end up in a situation where | |
// the file to be deleted has a non-null parent, but we fallback | |
// to deleting a root-level file, which of course is not the | |
// intention. | |
if (parentDirectoryHandle) { | |
try { | |
await deleteFile(parentDirectoryHandle, file.name) | |
didDeleteFileInFS = true | |
} catch { | |
// Do nothing | |
} | |
} | |
newlyDeletedTrashedFileIds.push(file.id) | |
} | |
} | |
setTrashedFileIds((t) => [...t, ...newlyDeletedTrashedFileIds]) | |
if (didDeleteFileInFS) { | |
await refreshHandlesInProjectFolder() | |
} | |
}, | |
[ | |
currentProjectDirectoryHandle, | |
currentProjectFiles, | |
folderIdHandleMap, | |
trashedFileIds, | |
refreshHandlesInProjectFolder, | |
] | |
) | |
const purgeTrashedProjectFoldersFromFS = useCallback( | |
async (trashedFolders: Folder[]) => { | |
// If a project folder has been trashed, remove it from | |
// the file system, but *only* if a newer, untrashed | |
// version is not present in the project, in which case | |
// the folder needs to remain. | |
const notYetDeleted = trashedFolders.filter( | |
(f) => !trashedFolderIds.includes(f.id) | |
) | |
const newlyDeletedTrashedFolderIds: Folder['id'][] = [] | |
let didDeleteFolderInFS = false | |
for (const folder of notYetDeleted) { | |
// Check that a newer, untrashed, version with the same name | |
// does not exist | |
const isAlsoNotTrashed = currentProjectFolders.find( | |
(f) => | |
f.name === folder.name && f.parentFolderId === folder.parentFolderId | |
) | |
if (isAlsoNotTrashed) { | |
// An untrashed version exists, so don't delete, just | |
// mark the id of the folder that we try to delete as deleted. This | |
// happens e.g. if a folder was deleted, and another one with | |
// the same parent and name was subsequently created. | |
newlyDeletedTrashedFolderIds.push(folder.id) | |
} else { | |
let parentDirectoryHandle = undefined | |
if (!folder.parentFolderId) { | |
parentDirectoryHandle = currentProjectDirectoryHandle | |
} else { | |
parentDirectoryHandle = | |
folderIdHandleMap.current?.[folder.parentFolderId] | |
} | |
// Note that we are not setting parentDirectoryHandle to | |
// currentProjectDirectoryHandle unless parentFolderId is explicitly | |
// undefined. Otherwise we may end up in a situation where | |
// the folder to be deleted has a non-null parent, but we fallback | |
// to deleting a root-level folder, which of course is not the | |
// intention. | |
if (parentDirectoryHandle) { | |
try { | |
await deleteFolder(folder.name, parentDirectoryHandle) | |
didDeleteFolderInFS = true | |
} catch { | |
// Do nothing | |
} | |
} | |
newlyDeletedTrashedFolderIds.push(folder.id) | |
} | |
} | |
setTrashedFolderIds((t) => [...t, ...newlyDeletedTrashedFolderIds]) | |
if (didDeleteFolderInFS) { | |
await refreshHandlesInProjectFolder() | |
} | |
}, | |
[ | |
currentProjectDirectoryHandle, | |
currentProjectFolders, | |
folderIdHandleMap, | |
trashedFolderIds, | |
refreshHandlesInProjectFolder, | |
] | |
) | |
const importFSFilesToProject = useCallback(async () => { | |
const saveFolderIdPathMap = currentProjectFolderIdPathMap | |
for (const h of allHandlesInProjectFolder.current) { | |
const fileId = await getFileIdFromHandle(h.handle) | |
if (fileId) { | |
// File exists, skip it | |
continue | |
} | |
const folderId = await getFolderIdFromHandle(h.handle) | |
if (folderId) { | |
// Folder exists, skip it | |
continue | |
} | |
if (h.type === 'file') { | |
const fsFile = await (h.handle as FileSystemFileHandle).getFile() | |
if (fsFile && isTextMimeType(fsFile)) { | |
try { | |
const content = await fsFile.text() | |
await createFileWithPathInCurrentProject( | |
h.path, | |
content, | |
saveFolderIdPathMap | |
) | |
} catch { | |
// Do nothing | |
} | |
} | |
} else if (h.type === 'directory') { | |
await createFolderWithPathInCurrentProject(h.path, saveFolderIdPathMap) | |
} | |
} | |
}, [ | |
getFileIdFromHandle, | |
getFolderIdFromHandle, | |
createFolderWithPathInCurrentProject, | |
createFileWithPathInCurrentProject, | |
currentProjectFolderIdPathMap, | |
allHandlesInProjectFolder, | |
]) | |
useEffect(() => { | |
if (!currentProjectDirectoryHandle) { | |
return | |
} | |
const checkIsSyncPermissionGrantedForProject = async () => { | |
const granted = | |
currentProjectDirectoryHandle && | |
(await isReadWritePermissionGranted(currentProjectDirectoryHandle)) | |
if (granted) { | |
setSyncPermissionsGrantedForCurrentProject(true) | |
} | |
} | |
checkIsSyncPermissionGrantedForProject() | |
}, [ | |
currentProjectDirectoryHandle, | |
setSyncPermissionsGrantedForCurrentProject, | |
]) | |
const askToGrantSyncPermissionsForProject = useCallback(async () => { | |
if (!currentProjectDirectoryHandle) { | |
return false | |
} | |
const granted = await askReadWritePermissionsIfNeeded( | |
currentProjectDirectoryHandle | |
) | |
setSyncPermissionsGrantedForCurrentProject(granted) | |
return granted | |
}, [ | |
currentProjectDirectoryHandle, | |
setSyncPermissionsGrantedForCurrentProject, | |
]) | |
const writeFileTreeNodeToFS = useCallback( | |
async (handle: FileSystemDirectoryHandle, nodes: FileTreeNode[]) => { | |
if (!currentProjectFiles) { | |
return | |
} | |
for (const node of nodes.filter((n) => n.type === 'file')) { | |
const file = currentProjectFiles.find((f) => f.id === node.id) | |
if (file) { | |
await createFileWithContentInFolder( | |
handle, | |
node.name, | |
toTextContent(file.encodedYDoc) | |
) | |
} | |
} | |
for (const node of nodes.filter((n) => n.type === 'folder')) { | |
const folder = currentProjectFolders.find((f) => f.id === node.id) | |
if (folder) { | |
const folderHandle = await createFolderInFolder(handle, node.name) | |
if (node.children) { | |
await writeFileTreeNodeToFS(folderHandle, node.children) | |
} | |
} | |
} | |
}, | |
[currentProjectFiles, currentProjectFolders] | |
) | |
const bulkSyncAndCleanupFilesFolders = useCallback( | |
async (files: File[]) => { | |
if (isPerformingOperation.current === true) { | |
// Skip when an long operation is going on. | |
return | |
} | |
isPerformingOperation.current = true | |
await refreshHandlesInProjectFolder() | |
for (const folder of sortBy(currentProjectFolders, (f) => { | |
return currentProjectFolderIdPathMap[f.id]?.split('/')?.length | |
})) { | |
// Make sure all non-trashed project folders exist | |
// in the file system, as their existence is required | |
// for the next operations, such as moving a folder | |
// to another folder. Note that we do a little trick | |
// here: we order the folders by length of the paths. | |
// This avoids the situation where we first try | |
// to create a folder within a deep path, before its | |
// parent has been created. | |
try { | |
await createFolderInFSIfNeeded(folder.id) | |
} catch (e) { | |
// Do nothing | |
} | |
} | |
for (const file of files) { | |
try { | |
await syncFile(file.id, false) | |
} catch (e) { | |
// Do nothing | |
} | |
} | |
for (const folder of currentProjectFolders) { | |
try { | |
await syncFolder(folder.id) | |
} catch (e) { | |
// Do nothing | |
} | |
} | |
// Sync trashed files and folders, i.e. delete them from FS if present. | |
await purgeTrashedProjectFilesFromFS(currentProjectTrashedFiles) | |
await purgeTrashedProjectFoldersFromFS(currentProjectTrashedFolders) | |
// Bring in newly created files from FS to project. Note | |
// that we need to run this after the purge operation, so | |
// that we don't bring in files to the project which have | |
// just been deleted from the project while waiting for them | |
// to be deleted in the file system. | |
await importFSFilesToProject() | |
isPerformingOperation.current = false | |
}, | |
[ | |
currentProjectFolders, | |
currentProjectTrashedFiles, | |
currentProjectTrashedFolders, | |
currentProjectFolderIdPathMap, | |
syncFile, | |
syncFolder, | |
purgeTrashedProjectFilesFromFS, | |
purgeTrashedProjectFoldersFromFS, | |
importFSFilesToProject, | |
refreshHandlesInProjectFolder, | |
createFolderInFSIfNeeded, | |
] | |
) | |
return { | |
askToGrantSyncPermissionsForProject, | |
currentProjectDirectoryHandle, | |
setCurrentProjectDirectoryHandle, | |
isSyncEnabledAndGrantedForCurrentProject, | |
isAutoSyncEnabledForCurrentProject, | |
isSyncPermissionsGrantedForCurrentProject, | |
didLoadHandlesInitially, | |
syncFile, | |
syncFolder, | |
bulkSyncAndCleanupFilesFolders, | |
purgeTrashedProjectFilesFromFS, | |
purgeTrashedProjectFoldersFromFS, | |
importFSFilesToProject, | |
} | |
} | |
export default createContainer(useFS) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useInterval } from '@lib/hooks/use-interval' | |
import React, { FC, useCallback } from 'react' | |
import FSProvider from '@context/fs' | |
import FSHandlesProvider from '@context/fs-handles' | |
import ProjectProvider from '@context/project' | |
import FileProvider from '@context/file' | |
const FSSyncer: FC = () => { | |
const { currentProjectFiles } = ProjectProvider.useContainer() | |
const { currentFile } = FileProvider.useContainer() | |
const { | |
isSyncEnabledAndGrantedForCurrentProject, | |
syncFile, | |
bulkSyncAndCleanupFilesFolders, | |
} = FSProvider.useContainer() | |
const { refreshHandlesInProjectFolder } = FSHandlesProvider.useContainer() | |
const syncCurrentFile = useCallback(async () => { | |
if (!currentFile?.id) { | |
return | |
} | |
try { | |
await refreshHandlesInProjectFolder() | |
await syncFile(currentFile?.id, true) | |
} catch { | |
// Do nothing | |
} | |
}, [currentFile?.id, syncFile, refreshHandlesInProjectFolder]) | |
const syncOtherFiles = useCallback(async () => { | |
await bulkSyncAndCleanupFilesFolders( | |
currentProjectFiles.filter((f) => f.id !== currentFile?.id) | |
) | |
}, [currentProjectFiles, bulkSyncAndCleanupFilesFolders, currentFile]) | |
useInterval( | |
syncCurrentFile, | |
isSyncEnabledAndGrantedForCurrentProject ? 3009 : null | |
) | |
useInterval( | |
syncOtherFiles, | |
isSyncEnabledAndGrantedForCurrentProject ? 7000 : null | |
) | |
return <></> | |
} | |
export default FSSyncer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment