fix(offline): false "New changes available" banner, sync stuck on toggle-off
Home's checkForRemoteChanges compared fresh pinned docs against what was displayed, but the pinned fetch (listPins isn't a cached endpoint) silently collapsed any failure to [] — every poll while offline compared "[]" against the real non-empty pinned list, which always looked like a change and popped the banner every ~45s. Split fetchPinned into a throwing variant checkForRemoteChanges can bail on (matching the already-correct pattern in CollectionsViewModel/DocumentsViewModel, which don't have this bug), keeping the non-throwing version for the initial load where collapsing to [] is the right behavior. RootView's auto-flush was keyed only to session.networkMonitor.isOnline — turning the manual Offline Mode toggle back off while the real network had been up the whole time never changes that value, so queued operations sat stuck until the next real network blip. Keyed the flush (and the Full Local Sync loop's online check) to a combined isEffectivelyOnline instead, so either signal clearing resumes sync immediately.
This commit is contained in:
@@ -57,25 +57,33 @@ final class HomeViewModel {
|
||||
/// Fetches fresh pinned docs and the current tab's documents to compare
|
||||
/// against what's displayed, without replacing either. Bails silently
|
||||
/// on a fetch failure rather than treating it as "changed" — a
|
||||
/// transient network hiccup shouldn't pop the refresh banner.
|
||||
/// transient network hiccup (or, offline, `listPins` failing outright —
|
||||
/// it isn't one of the cached endpoints) shouldn't pop the refresh
|
||||
/// banner. This needs the *throwing* pinned-fetch specifically: the
|
||||
/// plain `fetchPinned()` used elsewhere collapses any failure to `[]`,
|
||||
/// which used to read here as "pins changed" against whatever was
|
||||
/// already displayed and falsely popped the banner on every offline
|
||||
/// poll.
|
||||
func checkForRemoteChanges(tab: HomeTab) async {
|
||||
async let freshPinned = fetchPinned()
|
||||
async let freshPinnedTask = fetchPinnedThrowing()
|
||||
guard let freshTab = try? await fetch(tab: tab) else { return }
|
||||
let pinned = await freshPinned
|
||||
guard let pinned = try? await freshPinnedTask else { return }
|
||||
if Self.fingerprint(pinned) != Self.fingerprint(pinnedDocuments)
|
||||
|| Self.fingerprint(freshTab) != Self.fingerprint(documents(for: tab)) {
|
||||
hasRemoteChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchPinned() async -> [OutlineDocument] {
|
||||
(try? await fetchPinnedThrowing()) ?? []
|
||||
}
|
||||
|
||||
/// `pins.list` only returns pin records, not the documents themselves —
|
||||
/// fetches each pinned document individually. Pins are a small curated
|
||||
/// set (unlike a full collection tree), so the N+1 here is acceptable
|
||||
/// where it wouldn't be in the sidebar.
|
||||
private func fetchPinned() async -> [OutlineDocument] {
|
||||
guard let pins = try? await apiClient.listPins(ListPinsRequest(collectionId: nil)) else {
|
||||
return []
|
||||
}
|
||||
private func fetchPinnedThrowing() async throws -> [OutlineDocument] {
|
||||
let pins = try await apiClient.listPins(ListPinsRequest(collectionId: nil))
|
||||
var documents: [OutlineDocument] = []
|
||||
for pin in pins {
|
||||
if let document = try? await apiClient.documentInfo(id: pin.documentId) {
|
||||
|
||||
@@ -6,6 +6,18 @@ struct RootView: View {
|
||||
@State private var welcomeName: String?
|
||||
@State private var starStore = StarStore()
|
||||
@AppStorage("outpost.fullLocalSyncEnabled") private var isFullLocalSyncEnabled = false
|
||||
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
||||
|
||||
/// Real connectivity is only half of "can talk to the server" — the
|
||||
/// manual Offline Mode toggle is the other half. Syncing needs to
|
||||
/// resume on either one clearing, not just a real reconnect: turning
|
||||
/// the toggle off while the network had been up the whole time never
|
||||
/// changes `networkMonitor.isOnline`, so keying the flush task off that
|
||||
/// alone left pending operations stuck until the next real network
|
||||
/// blip.
|
||||
private var isEffectivelyOnline: Bool {
|
||||
session.networkMonitor.isOnline && !isOfflineModeEnabled
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -33,13 +45,13 @@ struct RootView: View {
|
||||
starStore.reset()
|
||||
}
|
||||
}
|
||||
// Replay whatever queued up while offline the moment the network's
|
||||
// back — no need to wait for the user to open Settings and hit Retry.
|
||||
// Also catches Full Local Sync back up immediately on reconnect,
|
||||
// rather than leaving it to wait out the rest of the periodic loop
|
||||
// below.
|
||||
.task(id: session.networkMonitor.isOnline) {
|
||||
guard session.networkMonitor.isOnline, let cachingClient = session.cachingClient else { return }
|
||||
// Replay whatever queued up while offline the moment either signal
|
||||
// clears — a real reconnect, or the user turning Offline Mode back
|
||||
// off — no need to wait for the user to open Settings and hit Retry.
|
||||
// Also catches Full Local Sync back up immediately, rather than
|
||||
// leaving it to wait out the rest of the periodic loop below.
|
||||
.task(id: isEffectivelyOnline) {
|
||||
guard isEffectivelyOnline, let cachingClient = session.cachingClient else { return }
|
||||
_ = await cachingClient.flushPendingOperations()
|
||||
if isFullLocalSyncEnabled {
|
||||
_ = await cachingClient.performFullSync()
|
||||
@@ -55,7 +67,7 @@ struct RootView: View {
|
||||
.task(id: isFullLocalSyncEnabled) {
|
||||
guard isFullLocalSyncEnabled else { return }
|
||||
while !Task.isCancelled {
|
||||
if session.networkMonitor.isOnline, let cachingClient = session.cachingClient {
|
||||
if isEffectivelyOnline, let cachingClient = session.cachingClient {
|
||||
_ = await cachingClient.performFullSync()
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(1200))
|
||||
|
||||
Reference in New Issue
Block a user