mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
Dogfooding: even chunked (#448/beta.15), a deep dir-heavy vault (work/Vault with tens of thousands of dirs nested deep) is too heavy to walk + materialise in full at connect. Load ONE folder level at a time instead. A real-Obsidian spike settled the two runtime unknowns: (U2) a childless TFolder still renders an expand arrow (mod-collapsible), so an unloaded folder is openable; (U1) File Explorer renders from the in-memory model and does NOT call adapter.list on expand, so deepen-on-expand is driven by a DOM .nav-folder-title click hook (the element carries the folder's data-path). - BulkWalker.walk gains a `recursive` param; recursive:false returns just a folder's immediate children (fs.walk / one-level adapter.list), honouring walkIgnoreDirs per level. - LazyFolderLoader: idempotent, deduped per-folder deepen (walk one level -> buildChunked). Unit tests cover recursive-false walk, idempotency, concurrent-dedup, markLoaded, retry-on-failure, reset. - populateVaultFromRemote: with `lazyFolderLoad` (default true) it walks only the root level, wires the loader, marks root loaded, and installs a capture-phase .nav-folder-title click hook that deepens on first expand. `lazyFolderLoad:false` restores the full eager walk. Loader dropped on disconnect. ignore (reduce N) and lazy (limit depth) compose; the live fs.changed watch still works against a partial tree via ensureParents. Graph/search see only loaded branches for now (accepted; revisit later). tsc/lint/vitest green (1202). Ships as beta.16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.4 KiB
TypeScript
83 lines
3.4 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { LazyFolderLoader } from '../src/vault/LazyFolderLoader';
|
|
import type { BulkWalker } from '../src/vault/BulkWalker';
|
|
import type { VaultModelBuilder } from '../src/vault/VaultModelBuilder';
|
|
|
|
function fakeWalk(path: string) {
|
|
return {
|
|
entries: [{ path: `${path}/child.md`, isDirectory: false, ctime: 0, mtime: 0, size: 0 }],
|
|
source: 'rpc-walk' as const,
|
|
truncated: false,
|
|
walkMs: 1,
|
|
pages: 1,
|
|
fastPathError: null,
|
|
};
|
|
}
|
|
|
|
function makeBuilder(): VaultModelBuilder & { buildChunked: ReturnType<typeof vi.fn> } {
|
|
return {
|
|
buildChunked: vi.fn(async (entries: readonly unknown[]) => ({
|
|
filesAdded: entries.length, foldersAdded: 0, skipped: 0, errors: [],
|
|
})),
|
|
} as unknown as VaultModelBuilder & { buildChunked: ReturnType<typeof vi.fn> };
|
|
}
|
|
|
|
describe('LazyFolderLoader', () => {
|
|
it('loadFolder walks ONE level (recursive=false) and builds the children', async () => {
|
|
const walk = vi.fn(async (path: string, _recursive: boolean) => fakeWalk(path));
|
|
const builder = makeBuilder();
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => builder);
|
|
|
|
await loader.loadFolder('work/sub');
|
|
|
|
expect(walk).toHaveBeenCalledExactlyOnceWith('work/sub', false);
|
|
expect(builder.buildChunked).toHaveBeenCalledOnce();
|
|
expect(loader.isLoaded('work/sub')).toBe(true);
|
|
});
|
|
|
|
it('is idempotent — a second load of the same folder does not re-walk', async () => {
|
|
const walk = vi.fn(async (path: string) => fakeWalk(path));
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => makeBuilder());
|
|
await loader.loadFolder('a');
|
|
await loader.loadFolder('a');
|
|
expect(walk).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('dedupes concurrent loads of the same folder into one walk', async () => {
|
|
const walk = vi.fn(async (path: string) => fakeWalk(path));
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => makeBuilder());
|
|
await Promise.all([loader.loadFolder('b'), loader.loadFolder('b'), loader.loadFolder('b')]);
|
|
expect(walk).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('markLoaded skips the walk entirely', async () => {
|
|
const walk = vi.fn(async (path: string) => fakeWalk(path));
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => makeBuilder());
|
|
loader.markLoaded('c');
|
|
await loader.loadFolder('c');
|
|
expect(walk).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('a failed walk leaves the folder unloaded so a later expand retries', async () => {
|
|
const walk = vi.fn()
|
|
.mockRejectedValueOnce(new Error('boom'))
|
|
.mockResolvedValueOnce(fakeWalk('x'));
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => makeBuilder());
|
|
|
|
await loader.loadFolder('x');
|
|
expect(loader.isLoaded('x')).toBe(false); // failed → not marked loaded
|
|
|
|
await loader.loadFolder('x'); // retry succeeds
|
|
expect(walk).toHaveBeenCalledTimes(2);
|
|
expect(loader.isLoaded('x')).toBe(true);
|
|
});
|
|
|
|
it('reset clears loaded state', async () => {
|
|
const walk = vi.fn(async (path: string) => fakeWalk(path));
|
|
const loader = new LazyFolderLoader(() => ({ walk } as unknown as BulkWalker), () => makeBuilder());
|
|
await loader.loadFolder('a');
|
|
expect(loader.isLoaded('a')).toBe(true);
|
|
loader.reset();
|
|
expect(loader.isLoaded('a')).toBe(false);
|
|
});
|
|
});
|