diff --git a/utopia-vscode-common/src/fs/fs-core.ts b/utopia-vscode-common/src/fs/fs-core.ts index 091cff148729..940211be00ae 100644 --- a/utopia-vscode-common/src/fs/fs-core.ts +++ b/utopia-vscode-common/src/fs/fs-core.ts @@ -58,12 +58,42 @@ export function isStoreDoesNotExist(t: unknown): t is StoreDoesNotExist { export type AsyncFSResult = Promise> +const StoreExistsKeyInterval = 1000 + +interface StoreKeyExistsCheck { + lastCheckedTime: number + exists: boolean +} + +let lastCheckedForStoreKeyExists: StoreKeyExistsCheck | null = null + +async function checkStoreKeyExists(): Promise { + if (store == null) { + return false + } else { + const now = Date.now() + if ( + lastCheckedForStoreKeyExists == null || + lastCheckedForStoreKeyExists.lastCheckedTime + StoreExistsKeyInterval < now + ) { + const exists = (await store.getItem(StoreExistsKey)) ?? false + lastCheckedForStoreKeyExists = { + lastCheckedTime: now, + exists: exists, + } + return exists + } else { + return lastCheckedForStoreKeyExists.exists + } + } +} + async function withSanityCheckedStore( withStore: (sanityCheckedStore: LocalForage) => Promise, ): AsyncFSResult { await firstInitialize await initializeStoreChain - const storeExists = store != null && (await store.getItem(StoreExistsKey)) + const storeExists = await checkStoreKeyExists() if (store != null && storeExists) { const result = await withStore(store) return right(result) diff --git a/utopia-vscode-common/src/fs/fs-utils.ts b/utopia-vscode-common/src/fs/fs-utils.ts index 1966dbf67f4c..d72231b81e25 100644 --- a/utopia-vscode-common/src/fs/fs-utils.ts +++ b/utopia-vscode-common/src/fs/fs-utils.ts @@ -174,9 +174,16 @@ export async function stat(path: string): Promise { return fsStatForNode(node) } +export function getDescendentPathsWithAllPaths( + path: string, + allPaths: Array, +): Array { + return allPaths.filter((k) => k != path && k.startsWith(path)) +} + export async function getDescendentPaths(path: string): Promise { const allPaths = await keys() - return allPaths.filter((k) => k != path && k.startsWith(path)) + return getDescendentPathsWithAllPaths(path, allPaths) } async function targetsForOperation(path: string, recursive: boolean): Promise { @@ -207,12 +214,17 @@ function filenameOfPath(path: string): string { return lastSlashIndex >= 0 ? path.slice(lastSlashIndex + 1) : path } -export async function childPaths(path: string): Promise { - const allDescendents = await getDescendentPaths(path) +export function childPathsWithAllPaths(path: string, allPaths: Array): Array { + const allDescendents = getDescendentPathsWithAllPaths(path, allPaths) const pathAsDir = stripTrailingSlash(path) return allDescendents.filter((k) => getParentPath(k) === pathAsDir) } +export async function childPaths(path: string): Promise { + const allDescendents = await getDescendentPaths(path) + return childPathsWithAllPaths(path, allDescendents) +} + async function getDirectory(path: string): Promise { const node = await getNode(path) if (isDirectory(node)) { @@ -458,66 +470,68 @@ function isFSUnavailableError(e: unknown): boolean { type FileModifiedStatus = 'modified' | 'not-modified' | 'unknown' -async function onPolledWatch(path: string, config: WatchConfig): Promise { - const { recursive, onCreated, onModified, onDeleted } = config - - try { - const node = await getItem(path) - if (node == null) { - watchedPaths.delete(path) - lastModifiedTSs.delete(path) - onDeleted(path) - return 'modified' - } else { - const stats = fsStatForNode(node) - - const modifiedTS = stats.mtime - const wasModified = modifiedTS > (lastModifiedTSs.get(path) ?? 0) - const modifiedBySelf = stats.sourceOfLastChange === fsUser - - if (isDirectory(node)) { - if (recursive) { - const children = await childPaths(path) - const unsupervisedChildren = children.filter((p) => !watchedPaths.has(p)) - unsupervisedChildren.forEach((childPath) => { - watchPath(childPath, config) - onCreated(childPath) - }) - if (unsupervisedChildren.length > 0) { +async function onPolledWatch(paths: Map): Promise> { + const allKeys = await keys() + const results = Array.from(paths).map(async ([path, config]) => { + const { recursive, onCreated, onModified, onDeleted } = config + + try { + const node = await getItem(path) + if (node == null) { + watchedPaths.delete(path) + lastModifiedTSs.delete(path) + onDeleted(path) + return 'modified' + } else { + const stats = fsStatForNode(node) + + const modifiedTS = stats.mtime + const wasModified = modifiedTS > (lastModifiedTSs.get(path) ?? 0) + const modifiedBySelf = stats.sourceOfLastChange === fsUser + + if (isDirectory(node)) { + if (recursive) { + const children = childPathsWithAllPaths(path, allKeys) + const unsupervisedChildren = children.filter((p) => !watchedPaths.has(p)) + unsupervisedChildren.forEach((childPath) => { + watchPath(childPath, config) + onCreated(childPath) + }) + if (unsupervisedChildren.length > 0) { + onModified(path, modifiedBySelf) + lastModifiedTSs.set(path, modifiedTS) + return 'modified' + } + } + } else { + if (wasModified) { onModified(path, modifiedBySelf) lastModifiedTSs.set(path, modifiedTS) return 'modified' } } - } else { - if (wasModified) { - onModified(path, modifiedBySelf) - lastModifiedTSs.set(path, modifiedTS) - return 'modified' - } - } - return 'not-modified' - } - } catch (e) { - if (isFSUnavailableError(e)) { - // Explicitly handle unavailable errors here by removing the watchers, then re-throw - watchedPaths.delete(path) - lastModifiedTSs.delete(path) - throw e + return 'not-modified' + } + } catch (e) { + if (isFSUnavailableError(e)) { + // Explicitly handle unavailable errors here by removing the watchers, then re-throw + watchedPaths.delete(path) + lastModifiedTSs.delete(path) + throw e + } + // Something was changed mid-poll, likely the file or its parent was deleted. We'll catch it on the next poll. + return 'unknown' } - // Something was changed mid-poll, likely the file or its parent was deleted. We'll catch it on the next poll. - return 'unknown' - } + }) + return Promise.all(results) } async function polledWatch(): Promise { - let promises: Array> = [] - watchedPaths.forEach((config, path) => { - promises.push(onPolledWatch(path, config)) - }) + let promises: Array>> = [] + promises.push(onPolledWatch(watchedPaths)) - const results = await Promise.all(promises) + const results = await Promise.all(promises).then((nestedResults) => nestedResults.flat()) let shouldReducePollingFrequency = true for (var i = 0, len = results.length; i < len; i++) {