mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Fence the last in-flight paths across an account change
A home-feed disk load that was already suspended when the context or Apple account moved no longer installs what it brings back: the in-memory cache is empty by then, so the purge finds nothing to drop and the previous login's private feeds would quietly reappear. Instance-browser pagination carries the login it was requested for, so a page still in flight cannot be appended to the next account's feed, and the pagination state is reset with the content it belongs to. The stateless-server refresh stops before touching anything when its result is stale, rather than only rejecting the videos and going on to set loading state and start polling for subscriptions nobody is showing. Signing in re-resolves its target first. The round trip leaves room for another device to delete the account, and the credential would have been filed under one that no longer exists — reported as success. A provisional account is rolled back when its backend cannot be resolved at all, which is what a vanished instance looks like.
This commit is contained in:
@@ -69,8 +69,21 @@ final class HomeInstanceCache {
|
||||
return
|
||||
}
|
||||
diskCacheLoaded = true
|
||||
let generation = accountGeneration
|
||||
|
||||
if let cacheData = await HomeInstanceDiskCache.shared.load() {
|
||||
// The context or the Apple account can move while this is
|
||||
// suspended. The in-memory cache is empty by then, so the purge
|
||||
// finds nothing to drop and this would quietly reinstate the
|
||||
// previous login's private feeds.
|
||||
guard generation == accountGeneration else {
|
||||
LoggingService.shared.info(
|
||||
"Discarding a disk cache that belongs to the previous login",
|
||||
category: .general
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
cache = cacheData.videos
|
||||
lastUpdated = cacheData.lastUpdated
|
||||
let totalVideos = cache.values.map { $0.count }.reduce(0, +)
|
||||
|
||||
@@ -615,6 +615,8 @@ final class SubscriptionFeedCache {
|
||||
instance: instance
|
||||
)
|
||||
|
||||
guard isCurrentAccountsResult(generation) else { return }
|
||||
|
||||
let serverVideos = response.toVideos()
|
||||
LoggingService.shared.info(
|
||||
"refreshFromStatelessServer: Received \(serverVideos.count) videos from server, status: \(response.status), ready: \(response.isReady)",
|
||||
|
||||
@@ -26,6 +26,9 @@ struct InstanceBrowseView: View {
|
||||
@State private var showFilterSheet = false
|
||||
|
||||
// Feed state (for Invidious/Piped login)
|
||||
/// Which login the account-scoped content belongs to, so a page still in
|
||||
/// flight cannot be appended to the next one's feed.
|
||||
@State private var accountContentGeneration = 0
|
||||
@State private var feedVideos: [Video] = []
|
||||
@State private var feedSubscriptions: [Channel] = []
|
||||
|
||||
@@ -1198,9 +1201,13 @@ struct InstanceBrowseView: View {
|
||||
///
|
||||
/// Public browsing — popular and trending — is unauthenticated and stays.
|
||||
private func discardAccountScopedContent() {
|
||||
accountContentGeneration += 1
|
||||
feedVideos = []
|
||||
feedSubscriptions = []
|
||||
userPlaylists = []
|
||||
feedPage = 1
|
||||
hasMoreFeedResults = false
|
||||
isLoadingMoreFeed = false
|
||||
}
|
||||
|
||||
private func refreshLoginState() {
|
||||
@@ -1434,12 +1441,19 @@ struct InstanceBrowseView: View {
|
||||
feedLoadedVideoCount = feedVideos.count // Mark current count to prevent re-triggering
|
||||
feedPage += 1
|
||||
|
||||
// The login can change while this page is in flight, and appending
|
||||
// afterwards would mix one account's private videos into another's
|
||||
// feed.
|
||||
let generation = accountContentGeneration
|
||||
|
||||
Task {
|
||||
do {
|
||||
let api = InvidiousAPI(httpClient: appEnvironment.httpClient)
|
||||
let feedResponse = try await api.feed(instance: instance, sid: sid, page: feedPage)
|
||||
|
||||
await MainActor.run {
|
||||
guard generation == accountContentGeneration else { return }
|
||||
|
||||
feedVideos.append(contentsOf: feedResponse.videos)
|
||||
hasMoreFeedResults = feedResponse.hasMore
|
||||
isLoadingMoreFeed = false
|
||||
|
||||
@@ -411,10 +411,29 @@ private struct EditRemoteServerContent: View {
|
||||
return false
|
||||
}
|
||||
|
||||
// Re-resolved rather than trusted: signing in takes a round trip, and
|
||||
// the account can be deleted on another device while it is happening.
|
||||
// Writing the credential then would file a secret under an account that
|
||||
// no longer exists, and report success for it.
|
||||
if let existing = target.account, appEnvironment.accountsManager.account(id: existing.id) == nil {
|
||||
appEnvironment.toastManager.showError(
|
||||
String(localized: "accounts.signIn.storeFailed")
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
let account = target.account
|
||||
?? appEnvironment.accountsManager.addAccount(instanceID: instance.id)
|
||||
|
||||
guard let credentials = appEnvironment.accountCredentialsManager(for: account) else { return false }
|
||||
guard let credentials = appEnvironment.accountCredentialsManager(for: account) else {
|
||||
// Its instance may have gone in the meantime, which is what makes
|
||||
// the backend unresolvable. A provisional account created moments
|
||||
// ago must not be left behind.
|
||||
if target.account == nil {
|
||||
appEnvironment.accountsManager.rollbackAccountCreation(account.id)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
credentials.setCredential(credential, for: account)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user