diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
| commit | e23e33adeaf8ee7439187d4451c856b37816a51f (patch) | |
| tree | a41eef1fba34cdd642d25576395b4ad484a748ad /packages/meshbay-node/src/meshbay_node/indexer/indexer.py | |
| parent | 60c4570e72e36c2a9720593c8baec74ee2ab52d6 (diff) | |
| download | meshbay-e23e33adeaf8ee7439187d4451c856b37816a51f.tar.gz | |
feat: Phase 9 — Web client SPA with WebRTC P2P transport
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.
Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).
Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/indexer.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/indexer/indexer.py | 51 |
1 files changed, 35 insertions, 16 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py index 4bb1527..60dc04b 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py @@ -169,30 +169,49 @@ class DirectoryIndexer: # ── Internal update ─────────────────────────────────────────────────────── + _DEBOUNCE_SECS = 2.0 + def _schedule_update(self, file_path: Path, deleted: bool = False) -> None: - """Called from watchdog thread — schedule async update on the event loop.""" - if self._loop: - self._loop.call_soon_threadsafe( - lambda: asyncio.ensure_future( - self._update_entry(file_path, deleted))) + """Called from watchdog thread — schedule debounced async update.""" + if not self._loop: + return + key = str(file_path.resolve()) + self._loop.call_soon_threadsafe( + self._debounce, key, file_path, deleted) + + def _debounce(self, key: str, file_path: Path, deleted: bool) -> None: + if not hasattr(self, "_pending_timers"): + self._pending_timers: dict[str, asyncio.TimerHandle] = {} + old = self._pending_timers.pop(key, None) + if old: + old.cancel() + handle = self._loop.call_later( + self._DEBOUNCE_SECS, + lambda: asyncio.ensure_future(self._update_entry(file_path, deleted)), + ) + self._pending_timers[key] = handle + + def _remove_by_path(self, file_path: Path) -> None: + """Remove any existing entries that match this file's path + name.""" + resolved = file_path.resolve() + to_remove = [ + e.id for e in self._index.entries + if (self.root / e.path / e.name).resolve() == resolved + ] + for fid in to_remove: + self._index.remove_entry(fid) async def _update_entry(self, file_path: Path, deleted: bool) -> None: - if deleted: - # Remove by matching path (hash not available after deletion) - to_remove = [ - e.id for e in self._index.entries - if (self.root / e.path / e.name).resolve() == file_path.resolve() - ] - for fid in to_remove: - self._index.remove_entry(fid) - log.debug("Removed from index: %s", file_path.name) - else: + self._remove_by_path(file_path) + + if not deleted: loop = asyncio.get_event_loop() entry = await loop.run_in_executor( self._executor, _scan_file, self.root, file_path) if entry: self._index.add_entry(entry) - log.debug("Indexed: %s (%s)", file_path.name, entry.id[:8]) + log.debug("Indexed: %s (%s, %d bytes)", + file_path.name, entry.id[:8], entry.size) self._index.version = int(time.time()) if self.on_change: |