aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-07 12:48:34 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-07 12:48:34 +0200
commit3d5a168ee592c28c696e445c623c9f8d2996b715 (patch)
treeadf258cabc4f86e7aa03b9feff7f4eec8c95d1ab /packages/meshbay-node/src/meshbay_node/indexer
parent00caeb3e10bfbf86892e7f31ad81f91ce8e50da7 (diff)
downloadmeshbay-3d5a168ee592c28c696e445c623c9f8d2996b715.tar.gz
fix(node): drop a directory moved out of the root from the index
Watchdog reports such a move as one "directory deleted" event and nothing for the files, which the indexer ignored until the next reconcile. The freeze rules still apply: root live, directory gone, parent present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py62
1 files changed, 54 insertions, 8 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index 5e139bb..732b499 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -1165,14 +1165,16 @@ class DirectoryIndexer:
# ── Internal update ───────────────────────────────────────────────────────
- def _schedule_update(self, file_path: Path, deleted: bool = False) -> None:
+ def _schedule_update(self, file_path: Path, deleted: bool = False,
+ directory: bool = False) -> None:
"""Called from watchdog thread — schedule debounced async update."""
if not self._loop:
return
key = str(file_path)
- self._loop.call_soon_threadsafe(self._debounce, key, file_path, deleted)
+ self._loop.call_soon_threadsafe(self._debounce, key, file_path, deleted, directory)
- def _debounce(self, key: str, file_path: Path, deleted: bool) -> None:
+ def _debounce(self, key: str, file_path: Path, deleted: bool,
+ directory: bool = False) -> None:
old = self._pending_timers.pop(key, None)
if old:
old.cancel()
@@ -1204,7 +1206,7 @@ class DirectoryIndexer:
def fire() -> None:
self._pending_timers.pop(key, None)
- spawn(self._update_entry(file_path, deleted))
+ spawn(self._update_entry(file_path, deleted, directory))
self._pending_timers[key] = self._loop.call_later(self.debounce_secs, fire)
@@ -1218,7 +1220,36 @@ class DirectoryIndexer:
if self._entry_path(root, entry) == resolved:
self._index.remove_entry(entry.id)
- async def _update_entry(self, file_path: Path, deleted: bool) -> None:
+ def _entries_in_dir(self, root: Root, dir_path: Path) -> list[str]:
+ """Ids of the entries at or below `dir_path`, matched on the index path
+ alone: the directory is gone, so there is nothing left to resolve."""
+ try:
+ rel = dir_path.relative_to(root.path).as_posix()
+ except ValueError:
+ return []
+ if rel in ("", "."):
+ return []
+ vdir = fold(f"{root.name}/{rel}")
+ return [e.id for e in self._entries_under(root)
+ if fold(e.path) == vdir or fold(e.path).startswith(vdir + "/")]
+
+ @staticmethod
+ def _dir_left_the_root(dir_path: Path) -> bool:
+ """
+ Blocking. The directory is gone and its parent is not.
+
+ The parent is what tells a directory that was deleted, or moved out of
+ the root, from a volume that went away: a vanishing volume takes the
+ parent with it. A parent deleted as well reports its own event, which
+ covers this directory too.
+ """
+ try:
+ return not dir_path.exists() and dir_path.parent.is_dir()
+ except OSError:
+ return False
+
+ async def _update_entry(self, file_path: Path, deleted: bool,
+ directory: bool = False) -> None:
try:
root = self._root_for(file_path)
if root is None:
@@ -1243,7 +1274,19 @@ class DirectoryIndexer:
if not root.available:
return
- self._remove_by_path(root, file_path)
+ if directory:
+ if file_path == root.path or not await off_disk(
+ self.roots, self._dir_left_the_root, file_path):
+ return
+ gone = self._entries_in_dir(root, file_path)
+ if not gone:
+ return
+ for entry_id in gone:
+ self._index.remove_entry(entry_id)
+ log.info("Watchdog: %s left the root, %d entries removed",
+ file_path, len(gone))
+ else:
+ self._remove_by_path(root, file_path)
if not deleted:
entry = await self._hash_or_cached(root, file_path)
@@ -1288,8 +1331,11 @@ class _WatchdogHandler(FileSystemEventHandler):
self._indexer._schedule_update(Path(event.src_path))
def on_deleted(self, event: FileSystemEvent):
- if not event.is_directory:
- self._indexer._schedule_update(Path(event.src_path), deleted=True)
+ # A directory moved out of the root arrives as this one event, with
+ # nothing for the files it held: without it they stayed indexed until
+ # the next reconcile, up to RECONCILE_BACKOFF_CAP later.
+ self._indexer._schedule_update(Path(event.src_path), deleted=True,
+ directory=event.is_directory)
def on_moved(self, event: FileSystemEvent):
if not event.is_directory: