mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Tie verdicts and context uploads to what asked for them
A server outcome is recorded only for the secret the request actually used. Signing in again, or a switch of Apple account, replaces it under the same key while a request is still out — so a late 401 could reject the replacement, and a late success could clear a rejection it never earned. The context record is built only while an upload is actually owed. Correcting away from a deleted account clears that flag deliberately; without checking it, the engine could still materialize the automatic fallback and publish it under the deleted choice's timestamp, over a deliberate choice made elsewhere. Confirming a context upload re-runs the outbox like an account confirmation does. Choosing a different context while the previous one was in flight leaves the new one dirty by design, and nothing was left to send it.
This commit is contained in:
@@ -612,7 +612,11 @@ final class AccountSyncCoordinator {
|
||||
return mapper.toCKRecord(accountDeletionID: accountID)
|
||||
|
||||
case .dataContext:
|
||||
guard accountsManager.canUploadContext else { return nil }
|
||||
// The pending flag, not just the ordering: correcting away from a
|
||||
// deleted account clears it deliberately, and sending that
|
||||
// automatic fallback would publish it under the deleted choice's
|
||||
// timestamp — over a deliberate choice made elsewhere.
|
||||
guard accountsManager.pendingContextUpload, accountsManager.canUploadContext else { return nil }
|
||||
return mapper.toCKRecord(dataContext: accountsManager.storedContext)
|
||||
|
||||
case nil:
|
||||
@@ -623,13 +627,13 @@ final class AccountSyncCoordinator {
|
||||
/// Clears outbox entries the server has confirmed, and releases whatever was
|
||||
/// waiting on them.
|
||||
func didSave(records: [CKRecord]) {
|
||||
var didConfirmAccount = false
|
||||
var didSaveAccountRecord = false
|
||||
|
||||
for record in records {
|
||||
switch AccountRecordName.parse(record.recordID.recordName) {
|
||||
case let .account(id):
|
||||
accountsManager.confirmUpload(id, updatedAt: record["updatedAt"] as? Date)
|
||||
didConfirmAccount = true
|
||||
didSaveAccountRecord = true
|
||||
|
||||
case let .legacyMigration(instanceID):
|
||||
accountsManager.confirmMarkerUpload(instanceID: instanceID)
|
||||
@@ -644,14 +648,17 @@ final class AccountSyncCoordinator {
|
||||
if let changedAt = record["changedAt"] as? Date {
|
||||
accountsManager.confirmContextUpload(changedAt: changedAt)
|
||||
}
|
||||
didSaveAccountRecord = true
|
||||
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// A marker or a context held behind one of these accounts can go now.
|
||||
if didConfirmAccount {
|
||||
// Whatever was held behind these can go now — a marker waiting on its
|
||||
// account, or a context the user changed while the previous one was
|
||||
// still in flight, which confirmation deliberately left dirty.
|
||||
if didSaveAccountRecord {
|
||||
enqueueDirtyRecords()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
/// Records the outcome of a server call against the account. The
|
||||
/// classification lives in one place so the feed cache, which talks to the
|
||||
/// same servers without going through a provider, reaches the same verdict.
|
||||
private func recordOutcome(_ error: Error?) {
|
||||
private func recordOutcome(_ error: Error?, credential: String) {
|
||||
// Only for the secret the request actually used. Signing in again — or
|
||||
// a switch of Apple account — replaces it under the same key while a
|
||||
// request is still out, and a late 401 would otherwise reject the
|
||||
// replacement, or a late success clear a rejection it never earned.
|
||||
guard credentialsManager.credential(for: account) == credential else { return }
|
||||
|
||||
accountsManager.recordServerOutcome(error, for: account)
|
||||
}
|
||||
private let settingsManager: SettingsManager
|
||||
@@ -63,7 +69,7 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
let (instance, sid) = try await getAuthenticatedInstance()
|
||||
|
||||
// Fetch subscriptions from Invidious
|
||||
let subscriptions = try await tracking {
|
||||
let subscriptions = try await tracking(credential: sid) {
|
||||
try await invidiousAPI.subscriptions(instance: instance, sid: sid)
|
||||
}
|
||||
|
||||
@@ -81,7 +87,7 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
let (instance, sid) = try await getAuthenticatedInstance()
|
||||
|
||||
// Subscribe on Invidious server
|
||||
try await tracking {
|
||||
try await tracking(credential: sid) {
|
||||
try await invidiousAPI.subscribe(to: channel.id.channelID, instance: instance, sid: sid)
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
// Unsubscribe on Invidious server. Tracked like every other
|
||||
// authenticated call: a rejected credential noticed here must mark the
|
||||
// account, or it keeps reporting itself as signed in.
|
||||
try await tracking {
|
||||
try await tracking(credential: sid) {
|
||||
try await invidiousAPI.unsubscribe(from: channelID, instance: instance, sid: sid)
|
||||
}
|
||||
|
||||
@@ -137,13 +143,13 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
|
||||
/// Gets the authenticated Invidious instance and session ID from account settings.
|
||||
/// Runs a server call and records what it implies about the credential.
|
||||
private func tracking<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||
private func tracking<T>(credential: String, _ operation: () async throws -> T) async rethrows -> T {
|
||||
do {
|
||||
let value = try await operation()
|
||||
recordOutcome(nil)
|
||||
recordOutcome(nil, credential: credential)
|
||||
return value
|
||||
} catch {
|
||||
recordOutcome(error)
|
||||
recordOutcome(error, credential: credential)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
/// Records the outcome of a server call against the account. The
|
||||
/// classification lives in one place so the feed cache, which talks to the
|
||||
/// same servers without going through a provider, reaches the same verdict.
|
||||
private func recordOutcome(_ error: Error?) {
|
||||
private func recordOutcome(_ error: Error?, credential: String) {
|
||||
// Only for the secret the request actually used. Signing in again — or
|
||||
// a switch of Apple account — replaces it under the same key while a
|
||||
// request is still out, and a late 401 would otherwise reject the
|
||||
// replacement, or a late success clear a rejection it never earned.
|
||||
guard credentialsManager.credential(for: account) == credential else { return }
|
||||
|
||||
accountsManager.recordServerOutcome(error, for: account)
|
||||
}
|
||||
private let settingsManager: SettingsManager
|
||||
@@ -63,7 +69,7 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
let (instance, authToken) = try await getAuthenticatedInstance()
|
||||
|
||||
// Fetch subscriptions from Piped
|
||||
let subscriptions = try await tracking {
|
||||
let subscriptions = try await tracking(credential: authToken) {
|
||||
try await pipedAPI.subscriptions(instance: instance, authToken: authToken)
|
||||
}
|
||||
|
||||
@@ -83,7 +89,7 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
// Subscribe on Piped server. Tracked like every other authenticated
|
||||
// call: a rejected credential noticed here must mark the account, or it
|
||||
// keeps reporting itself as signed in.
|
||||
try await tracking {
|
||||
try await tracking(credential: authToken) {
|
||||
try await pipedAPI.subscribe(channelID: channel.id.channelID, instance: instance, authToken: authToken)
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
let (instance, authToken) = try await getAuthenticatedInstance()
|
||||
|
||||
// Unsubscribe on Piped server
|
||||
try await tracking {
|
||||
try await tracking(credential: authToken) {
|
||||
try await pipedAPI.unsubscribe(channelID: channelID, instance: instance, authToken: authToken)
|
||||
}
|
||||
|
||||
@@ -137,13 +143,13 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
|
||||
/// Gets the authenticated Piped instance and auth token from account settings.
|
||||
/// Runs a server call and records what it implies about the credential.
|
||||
private func tracking<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||
private func tracking<T>(credential: String, _ operation: () async throws -> T) async rethrows -> T {
|
||||
do {
|
||||
let value = try await operation()
|
||||
recordOutcome(nil)
|
||||
recordOutcome(nil, credential: credential)
|
||||
return value
|
||||
} catch {
|
||||
recordOutcome(error)
|
||||
recordOutcome(error, credential: credential)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user