diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-23 21:55:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-23 21:55:20 +0200 |
| commit | b3709ac4d362987a9d025616c95065ceed0d216b (patch) | |
| tree | 32e0cc5cc2775eddf516d114fa9799347a214bda /packages/meshbay-hub/src/meshbay_hub/static/group-page.js | |
| parent | 012ba5b0cb8c556ce773423ca38d5184b74659ac (diff) | |
| download | meshbay-b3709ac4d362987a9d025616c95065ceed0d216b.tar.gz | |
feat(node): persistent index cache, visible scan progress, adaptive reconcile, and delta sync
Indexer performance work, in four parts:
- Persistent (path, size, mtime) -> hash cache (indexer/cache.py) so a node
restart no longer re-hashes every file — measured at 23 minutes for a
114 GB library on a slow disk before this, near-instant after. Hashing
is deliberately kept sequential (max_workers=1): it was never actually
concurrent despite the pool size, and two interleaved reads seek-thrash
a spinning disk instead of going faster.
- Byte-based scan progress (IndexProgress), surfaced via the loopback
index-status route, the handshake ack, and a periodic INDEX_PROGRESS
push to connected peers — drives a progress bar in the Create Group
wizard and "add a directory" in Settings, and an animated presence dot.
Guaranteed to settle back to idle via try/finally and a final push on
the scanning->false transition.
- The reconcile backstop's directory walks now run in the executor
instead of blocking the daemon's event loop; its interval defaults to
10 min (was 60s) with adaptive backoff to 2h when nothing changes,
reset on a real change or a peer connecting, and is now a per-group
operator setting (signed op + group Settings UI).
- INDEX_DELTA wired up (protocol support existed, nothing called it):
_on_index_change now sends additions/deletions instead of rebuilding
the full entries list, coalesced over a short window so a burst of
file events produces one push, and the hub swarm registration for
public groups only (re-)registers newly added hashes.
Also fixes several bugs found while testing the above against real
libraries (a 114 GB and a 100+ GB group on a USB HDD):
- /api/reload blocked until the reload — including a brand-new group's
full initial scan — finished, which the Electron bridge's fixed 30s
call timeout turned into a hard failure on any real library. The route
now fires the reload without waiting (ops.start_reload), matching
add_root/remove_root's existing pattern; the wizard's own step order
was fixed to wait for the group to actually appear hosted before the
steps that need it (extra roots, GEK), with retries for the residual
race between that and the daemon's own bookkeeping.
- transport.js's hand-rolled msgpack codec had no case for uint64/int64
(0xcf/0xd3) and crashed decoding any message containing one — hit by
IndexProgress.scanned_bytes/total_bytes for any group over ~4.3 GB.
Verified against real msgpack-encoded bytes from the Python side.
- chat_hist_resp, and this change's own index_progress and
set_scan_settings_ack pushes, were not routed by message type and
could be handed to an unrelated pending request by the transport's
"oldest pending" fallback, stalling it until its own 30s timeout and
corrupting whatever received the wrong reply in its place.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 57 |
1 files changed, 56 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 1528213..29f3602 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -76,6 +76,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // every registered app when a node predates the setting (or hasn't answered // yet), so nothing disappears for an existing group. const [enabledApps, setEnabledApps] = useState(null); + // Reconcile interval / debounce currently in effect on the node — shown + // to the operator in Settings, not enforced from here (indexer.py owns + // that). Null until the handshake ack arrives. + const [scanSettings, setScanSettings] = useState(null); // Paired ≠ operator account. `is_node_admin` says the hub account owning this // node is the one connecting; this says the node pinned *this browser's* key // as an operator key. Only the second one lets you sign an invite, and only @@ -123,6 +127,25 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, cacheGroupIndex(groupId, group ? group.name : groupId, fresh); }, [groupId, group]); + // additions/deletions only (daemon.py _broadcast_index_change, once there + // is a previous snapshot to diff against) — applied on top of whatever + // applyIndex last put in `entries`, instead of replacing the whole table + // for one changed file. + const applyIndexDelta = useCallback((deltaMsg) => { + setEntries((prev) => { + const deletions = new Set(deltaMsg.deletions || []); + const kept = prev.filter((e) => !deletions.has(e.id)); + // The index is keyed by content hash: an addition whose id is already + // present is the same duplicate-content case indexer.py's own + // reconcile sweep leaves alone, not a second row for one file. + const keptIds = new Set(kept.map((e) => e.id)); + const additions = (deltaMsg.additions || []).filter((e) => !keptIds.has(e.id)); + const fresh = kept.concat(additions); + cacheGroupIndex(groupId, group ? group.name : groupId, fresh); + return fresh; + }); + }, [groupId, group]); + useEffect(() => { let cancelled = false; @@ -172,11 +195,24 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, setIsNodeAdmin(!!ack.is_node_admin); setMemberUpload(ack.member_upload !== false); setEnabledApps(ack.enabled_apps || null); + setScanSettings(ack.scan_settings || null); // Changed while we are connected, by an operator who may be someone // else entirely. Without this the button stays until a reconnection, // and a button that is still there is a button people press. transport.onUploadPolicy = (allowed) => setMemberUpload(allowed); transport.onAppsEnabled = (apps) => setEnabledApps(apps); + // The node's own scan (a root added while we were already connected, + // or reconcile catching one back up) — never the entries, just + // enough to animate the sidebar dot. Guaranteed a final push at the + // False transition (daemon.py _progress_pusher), so this always + // settles back to 'online' rather than getting stuck. + transport.onIndexProgress = (status) => { + if (cancelled || !onPresence) return; + const pct = status.total_bytes + ? Math.min(100, Math.round(100 * status.scanned_bytes / status.total_bytes)) + : 0; + onPresence(groupId, status.scanning ? 'indexing' : 'online', pct); + }; setOperatorPaired(transport.memberRole === 'operator'); // A first join to this node generated an identity for it; leave it with @@ -203,6 +239,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, if (cancelled) return; applyIndex(msg); }; + transport.onIndexDelta = (msg) => { + if (cancelled) return; + applyIndexDelta(msg); + }; // We are in: an invitation to this group has served its purpose. if (onJoined) onJoined(groupId); @@ -214,7 +254,20 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, touchActivity(); // First-hand evidence, and the strongest available: this browser spoke // to the node. It outranks whatever the hub said in the group list. - if (onPresence) onPresence(groupId, 'online'); + // A scan already under way at the moment of connecting (ack.indexing, + // webrtc_server.py _complete_handshake) shows as indexing right away + // rather than waiting for the next periodic push. + if (onPresence) { + const idx = ack.indexing; + if (idx && idx.scanning) { + const pct = idx.total_bytes + ? Math.min(100, Math.round(100 * idx.scanned_bytes / idx.total_bytes)) + : 0; + onPresence(groupId, 'indexing', pct); + } else { + onPresence(groupId, 'online'); + } + } } catch (err) { if (cancelled) return; @@ -448,6 +501,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, onMemberUpload=${(allowed) => setMemberUpload(allowed)} enabledApps=${enabledApps} onEnabledApps=${(keys) => setEnabledApps(keys)} + scanSettings=${scanSettings} + onScanSettings=${(s) => setScanSettings(s)} onLeft=${onLeft} onPaired=${() => setOperatorPaired(true)} /> `} |