| 17 | } |
| 18 | |
| 19 | export function createFileTreeStore(options: TreeStoreOptions) { |
| 20 | const [tree, setTree] = createStore<{ |
| 21 | node: Record<string, FileNode> |
| 22 | dir: Record<string, DirectoryState> |
| 23 | }>({ |
| 24 | node: {}, |
| 25 | dir: { "": { expanded: true } }, |
| 26 | }) |
| 27 | |
| 28 | const inflight = new Map<string, Promise<void>>() |
| 29 | |
| 30 | const reset = () => { |
| 31 | inflight.clear() |
| 32 | setTree("node", reconcile({})) |
| 33 | setTree("dir", reconcile({})) |
| 34 | setTree("dir", "", { expanded: true }) |
| 35 | } |
| 36 | |
| 37 | const ensureDir = (path: string) => { |
| 38 | if (tree.dir[path]) return |
| 39 | setTree("dir", path, { expanded: false }) |
| 40 | } |
| 41 | |
| 42 | const listDir = (input: string, opts?: { force?: boolean }) => { |
| 43 | const dir = options.normalizeDir(input) |
| 44 | ensureDir(dir) |
| 45 | |
| 46 | const current = tree.dir[dir] |
| 47 | if (!opts?.force && current?.loaded) return Promise.resolve() |
| 48 | |
| 49 | const pending = inflight.get(dir) |
| 50 | if (pending) return pending |
| 51 | |
| 52 | setTree( |
| 53 | "dir", |
| 54 | dir, |
| 55 | produce((draft) => { |
| 56 | draft.loading = true |
| 57 | draft.error = undefined |
| 58 | }), |
| 59 | ) |
| 60 | |
| 61 | const directory = options.scope() |
| 62 | |
| 63 | const promise = options |
| 64 | .list(dir) |
| 65 | .then((nodes) => { |
| 66 | if (options.scope() !== directory) return |
| 67 | const prevChildren = tree.dir[dir]?.children ?? [] |
| 68 | const nextChildren = nodes.map((node) => node.path) |
| 69 | const nextSet = new Set(nextChildren) |
| 70 | |
| 71 | setTree( |
| 72 | "node", |
| 73 | produce((draft) => { |
| 74 | const removedDirs: string[] = [] |
| 75 | |
| 76 | for (const child of prevChildren) { |