Swift concurrency stuff

i don't know if any of this is right, but it seems like it works so...
This commit is contained in:
Shadowfacts 2022-03-06 15:05:33 -05:00
parent 55e4966bd1
commit dec7a6e57f
13 changed files with 95 additions and 82 deletions

View File

@ -7,7 +7,7 @@
import Foundation
public struct ClientRegistration: Decodable {
public struct ClientRegistration: Decodable, Sendable {
public let clientID: String
public let clientSecret: String

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21.
//
import Foundation
@preconcurrency import Foundation
public struct Feed: Decodable {
public struct Feed: Decodable, Sendable {
public let id: FervorID
public let title: String
public let url: URL?

View File

@ -5,13 +5,13 @@
// Created by Shadowfacts on 11/25/21.
//
import Foundation
@preconcurrency import Foundation
public class FervorClient {
public actor FervorClient: Sendable {
let instanceURL: URL
let session: URLSession
public var accessToken: String?
private let instanceURL: URL
private let session: URLSession
public private(set) var accessToken: String?
private let decoder: JSONDecoder = {
let d = JSONDecoder()
@ -81,7 +81,9 @@ public class FervorClient {
"client_id": clientID,
"client_secret": clientSecret,
])
return try await performRequest(request)
let result: Token = try await performRequest(request)
self.accessToken = result.accessToken
return result
}
public func groups() async throws -> [Group] {

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21.
//
import Foundation
@preconcurrency import Foundation
public struct Group: Decodable {
public struct Group: Decodable, Sendable {
public let id: FervorID
public let title: String
public let feedIDs: [FervorID]

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21.
//
import Foundation
@preconcurrency import Foundation
public struct Instance: Decodable {
public struct Instance: Decodable, Sendable {
public let name: String
public let url: URL
public let version: String

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 10/29/21.
//
import Foundation
@preconcurrency import Foundation
public struct Item: Decodable {
public struct Item: Decodable, Sendable {
public let id: FervorID
public let feedID: FervorID
public let title: String?

View File

@ -5,9 +5,9 @@
// Created by Shadowfacts on 1/9/22.
//
import Foundation
@preconcurrency import Foundation
public struct ItemsSyncUpdate: Decodable {
public struct ItemsSyncUpdate: Decodable, Sendable {
public let syncTimestamp: Date
public let delete: [FervorID]

View File

@ -9,7 +9,8 @@ import CoreData
import Fervor
import OSLog
class PersistentContainer: NSPersistentContainer {
// todo: is this actually sendable?
class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
private static let managedObjectModel: NSManagedObjectModel = {
let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")!
@ -23,13 +24,11 @@ class PersistentContainer: NSPersistentContainer {
return context
}()
private weak var fervorController: FervorController?
weak var fervorController: FervorController?
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
init(account: LocalData.Account, fervorController: FervorController) {
self.fervorController = fervorController
init(account: LocalData.Account) {
super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel)
loadPersistentStores { description, error in
@ -40,7 +39,7 @@ class PersistentContainer: NSPersistentContainer {
}
@MainActor
private func saveViewContext() async throws {
func saveViewContext() throws {
if viewContext.hasChanges {
try viewContext.save()
}

View File

@ -5,12 +5,12 @@
// Created by Shadowfacts on 11/25/21.
//
import Foundation
@preconcurrency import Foundation
import Fervor
import OSLog
@preconcurrency import OSLog
import Combine
class FervorController {
actor FervorController {
static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")!
@ -19,36 +19,40 @@ class FervorController {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
let client: FervorClient
private(set) var account: LocalData.Account?
nonisolated let account: LocalData.Account?
private(set) var clientID: String?
private(set) var clientSecret: String?
private(set) var accessToken: String?
private(set) var persistentContainer: PersistentContainer!
nonisolated let persistentContainer: PersistentContainer!
@Published private(set) var syncState = SyncState.done
nonisolated let syncState = PassthroughSubject<SyncState, Never>()
private var lastSyncState = SyncState.done
private var cancellables = Set<AnyCancellable>()
init(instanceURL: URL) {
init(instanceURL: URL, account: LocalData.Account?) async {
self.instanceURL = instanceURL
self.client = FervorClient(instanceURL: instanceURL, accessToken: nil)
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.accessToken)
self.account = account
self.clientID = account?.clientID
self.clientSecret = account?.clientSecret
if let account = account {
self.persistentContainer = PersistentContainer(account: account)
} else {
self.persistentContainer = nil
}
persistentContainer?.fervorController = self
}
convenience init(account: LocalData.Account) {
self.init(instanceURL: account.instanceURL)
self.account = account
self.clientID = account.clientID
self.clientSecret = account.clientSecret
self.accessToken = account.accessToken
self.client.accessToken = account.accessToken
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
convenience init(account: LocalData.Account) async {
await self.init(instanceURL: account.instanceURL, account: account)
}
private func setSyncState(_ state: SyncState) {
DispatchQueue.main.async {
self.syncState = state
}
lastSyncState = state
syncState.send(state)
}
func register() async throws -> ClientRegistration {
@ -60,12 +64,11 @@ class FervorController {
func getToken(authCode: String) async throws {
let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
client.accessToken = token.accessToken
accessToken = token.accessToken
}
func syncAll() async throws {
guard syncState == .done else {
guard lastSyncState == .done else {
return
}
// always return to .done, even if we throw and stop syncing early
@ -90,7 +93,6 @@ class FervorController {
await ExcerptGenerator.generateAll(self)
}
@MainActor
func syncReadToServer() async {
var count = 0
@ -138,7 +140,7 @@ class FervorController {
func markItem(_ item: Item, read: Bool) async {
item.read = read
do {
let f = item.read ? client.read(item:) : client.unread(item:)
let f = read ? client.read(item:) : client.unread(item:)
_ = try await f(item.id!)
item.needsReadStateSync = false
} catch {
@ -146,12 +148,10 @@ class FervorController {
item.needsReadStateSync = true
}
if persistentContainer.viewContext.hasChanges {
do {
try persistentContainer.viewContext.save()
} catch {
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
}
do {
try self.persistentContainer.saveViewContext()
} catch {
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
}
}

View File

@ -5,6 +5,7 @@
// Created by Shadowfacts on 10/29/21.
//
@preconcurrency import Foundation
import UIKit
import OSLog
@ -33,11 +34,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window!.rootViewController = loginVC
} else if activity?.activityType == NSUserActivity.activateAccountType,
let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) {
fervorController = FervorController(account: account)
createAppUI()
Task { @MainActor in
fervorController = await FervorController(account: account)
syncFromServer()
createAppUI()
}
} else if let account = LocalData.mostRecentAccount() {
fervorController = FervorController(account: account)
createAppUI()
Task { @MainActor in
fervorController = await FervorController(account: account)
syncFromServer()
createAppUI()
}
} else {
let loginVC = LoginViewController()
loginVC.delegate = self
@ -125,9 +132,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
func switchToAccount(_ account: LocalData.Account) {
func switchToAccount(_ account: LocalData.Account) async {
LocalData.mostRecentAccountID = account.id
fervorController = FervorController(account: account)
fervorController = await FervorController(account: account)
createAppUI()
syncFromServer()
}
@ -136,15 +143,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
extension SceneDelegate: LoginViewControllerDelegate {
func didLogin(with controller: FervorController) {
let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!)
LocalData.accounts.append(account)
LocalData.mostRecentAccountID = account.id
fervorController = FervorController(account: account)
createAppUI()
syncFromServer()
UIMenuSystem.main.setNeedsRebuild()
Task { @MainActor in
let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, accessToken: await controller.accessToken!)
LocalData.accounts.append(account)
LocalData.mostRecentAccountID = account.id
fervorController = await FervorController(account: account)
createAppUI()
syncFromServer()
UIMenuSystem.main.setNeedsRebuild()
}
}
}

View File

@ -65,7 +65,9 @@ extension AppSplitViewController: ItemsViewControllerDelegate {
extension AppSplitViewController: HomeViewControllerDelegate {
func switchToAccount(_ account: LocalData.Account) {
if let delegate = view.window?.windowScene?.delegate as? SceneDelegate {
delegate.switchToAccount(account)
Task { @MainActor in
await delegate.switchToAccount(account)
}
}
}
}

View File

@ -93,7 +93,7 @@ class HomeViewController: UIViewController {
feedResultsController.delegate = self
try! feedResultsController.performFetch()
fervorController.$syncState
fervorController.syncState
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
.sink { [unowned self] in
self.syncStateChanged($0)

View File

@ -5,6 +5,7 @@
// Created by Shadowfacts on 11/25/21.
//
@preconcurrency import Foundation
import UIKit
import AuthenticationServices
import Fervor
@ -72,7 +73,7 @@ class LoginViewController: UIViewController {
textField.isEnabled = false
activityIndicator.startAnimating()
let controller = FervorController(instanceURL: components.url!)
let controller = await FervorController(instanceURL: components.url!, account: nil)
let registration: ClientRegistration
do {
@ -99,16 +100,16 @@ class LoginViewController: UIViewController {
let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false)
guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }),
let codeValue = codeItem.value else {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true)
self.textField.isEnabled = true
self.activityIndicator.stopAnimating()
}
return
}
Task { @MainActor in
let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true)
self.textField.isEnabled = true
self.activityIndicator.stopAnimating()
}
return
}
Task { @MainActor in
do {
try await controller.getToken(authCode: codeValue)