M9 #3: flush UserHomeCache on soft disconnect

Full ConnectedServerRegistry was scoped out of this phase — SwiftUI
view lifecycle already tears down transports via .onDisappear when
ScarfGoTabRoot unmounts on state transition to .serverList. Adding
a formal registry that tracks every active transport per ServerID
is complexity without proven UX payoff right now (can revisit post
pass-2 if users hit stale-connection bugs).

One real cleanup we should always do on soft disconnect: invalidate
the shared UserHomeCache entry for the server we're leaving. The
cache lives forever otherwise, and a hypothetical scenario where
the remote user's home directory changes between sessions would
surface as SFTP paths resolving to the wrong directory. Rare, but
free to fix.

`RootModel.softDisconnect()` now calls the new static
`ServerContext.invalidateCachedHome(forServerID:)` before flipping
state to `.serverList`. Static form is a convenience for callers
that have the ServerID in hand but not a full ServerContext (avoids
forcing a round-trip through config store just to rebuild the
context we're already discarding).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-24 13:56:57 +02:00
parent bb399e6d35
commit 9c2e9279cc
2 changed files with 19 additions and 7 deletions
@@ -219,6 +219,13 @@ extension ServerContext {
public static func invalidateCaches(for contextID: ServerID) async {
await UserHomeCache.shared.invalidate(contextID: contextID)
}
/// Static convenience for callers that have the ServerID but not
/// a full ServerContext (e.g. RootModel.softDisconnect). Mirrors
/// the instance method above.
public static func invalidateCachedHome(forServerID id: ServerID) async {
await UserHomeCache.shared.invalidate(contextID: id)
}
}
// MARK: - Convenience file I/O via the right transport
+12 -7
View File
@@ -182,14 +182,19 @@ final class RootModel {
state = .connected(id, config, key)
}
/// Soft disconnect: close any live transport but keep stored
/// credentials. Returns to the server list so the user can tap
/// another server (or the same one again).
/// Soft disconnect: return to the server list without wiping
/// credentials. Per-view controllers (ChatController,
/// IOSDashboardViewModel, etc.) tear down their transports via
/// SwiftUI `.onDisappear` when ScarfGoTabRoot unmounts; on next
/// connect we get fresh transports. We also flush the shared
/// UserHomeCache entry for the server we're leaving so a future
/// reconnect doesn't reuse a stale `$HOME` probe (minor, but
/// matters if the remote user's home directory changed rare
/// but possible on shared hosts).
func softDisconnect() async {
// Transport teardown is owned by ConnectedServerRegistry
// (added in 3.3); for now the per-view controllers own their
// own lifecycles via .onDisappear, so this is mostly a state
// change. The registry commit will thread through here.
if case .connected(let id, _, _) = state {
await ServerContext.invalidateCachedHome(forServerID: id)
}
state = .serverList
}