Reload projects when there are external changes on the file-system (#4077)

This commit is contained in:
49fl
2024-10-03 13:02:57 -04:00
committed by GitHub
parent e1406012b4
commit 115e6baa53
10 changed files with 239 additions and 28 deletions

View File

@ -0,0 +1,71 @@
import { isDesktop } from 'lib/isDesktop'
import { useEffect, useState, useRef } from 'react'
type Path = string
// Not having access to NodeJS functions has influenced the design a little.
// There is some indirection going on because we can only pass data between
// the NodeJS<->Browser boundary. The actual functions need to run on the
// NodeJS side. Because EventEmitters come bundled with their listener
// methods it complicates things because we can't just do
// watcher.addListener(() => { ... }).
export const useFileSystemWatcher = (
callback: (path: Path) => void,
dependencyArray: Path[]
): void => {
// Track a ref to the callback. This is how we get the callback updated
// across the NodeJS<->Browser boundary.
const callbackRef = useRef<{ fn: (path: Path) => void }>({
fn: (_path) => {},
})
useEffect(() => {
callbackRef.current.fn = callback
}, [callback])
// Used to track if dependencyArrray changes.
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
[]
)
// On component teardown obliterate all watchers.
useEffect(() => {
// The hook is useless on web.
if (!isDesktop()) return
return () => {
window.electron.watchFileObliterate()
}
}, [])
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
return [
l1.filter((x) => Boolean(!l2.find((x2) => x2 === x))),
l1.filter((x) => Boolean(l2.find((x2) => x2 === x))),
]
}
// Removing 1 watcher at a time is only possible because in a filesystem,
// a path is unique (there can never be two paths with the same name).
// Otherwise we would have to obliterate() the whole list and reconstruct it.
useEffect(() => {
// The hook is useless on web.
if (!isDesktop()) return
const [pathsRemoved, pathsRemaining] = difference(
dependencyArrayTracked,
dependencyArray
)
for (let path of pathsRemoved) {
window.electron.watchFileOff(path)
}
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
for (let path of pathsAdded) {
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
callbackRef.current.fn(path)
)
}
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
}

View File

@ -0,0 +1,41 @@
import { trap } from 'lib/trap'
import { useState, useEffect } from 'react'
import { ensureProjectDirectoryExists, listProjects } from 'lib/desktop'
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
import { Project } from 'lib/project'
import { isDesktop } from 'lib/isDesktop'
// Hook uses [number] to give users familiarity. It is meant to mimic a
// dependency array, but is intended to only ever be used with 1 value.
export const useProjectsLoader = (deps?: [number]) => {
const [lastTs, setLastTs] = useState(-1)
const [projectPaths, setProjectPaths] = useState<Project[]>([])
const [projectsDir, setProjectsDir] = useState<string | undefined>(undefined)
useEffect(() => {
// Useless on web, until we get fake filesystems over there.
if (!isDesktop) return
if (deps && deps[0] === lastTs) return
if (deps) {
setLastTs(deps[0])
}
;(async () => {
const { configuration } = await loadAndValidateSettings()
const _projectsDir = await ensureProjectDirectoryExists(configuration)
setProjectsDir(_projectsDir)
if (projectsDir) {
const _projectPaths = await listProjects(configuration)
setProjectPaths(_projectPaths)
}
})().catch(trap)
}, deps ?? [])
return {
projectPaths,
projectsDir,
}
}