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:
parent
55e4966bd1
commit
dec7a6e57f
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct ClientRegistration: Decodable {
|
public struct ClientRegistration: Decodable, Sendable {
|
||||||
public let clientID: String
|
public let clientID: String
|
||||||
public let clientSecret: String
|
public let clientSecret: String
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Shadowfacts on 10/29/21.
|
// 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 id: FervorID
|
||||||
public let title: String
|
public let title: String
|
||||||
public let url: URL?
|
public let url: URL?
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
// Created by Shadowfacts on 11/25/21.
|
// Created by Shadowfacts on 11/25/21.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
@preconcurrency import Foundation
|
||||||
|
|
||||||
public class FervorClient {
|
public actor FervorClient: Sendable {
|
||||||
|
|
||||||
let instanceURL: URL
|
private let instanceURL: URL
|
||||||
let session: URLSession
|
private let session: URLSession
|
||||||
public var accessToken: String?
|
public private(set) var accessToken: String?
|
||||||
|
|
||||||
private let decoder: JSONDecoder = {
|
private let decoder: JSONDecoder = {
|
||||||
let d = JSONDecoder()
|
let d = JSONDecoder()
|
||||||
|
@ -81,7 +81,9 @@ public class FervorClient {
|
||||||
"client_id": clientID,
|
"client_id": clientID,
|
||||||
"client_secret": clientSecret,
|
"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] {
|
public func groups() async throws -> [Group] {
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Shadowfacts on 10/29/21.
|
// 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 id: FervorID
|
||||||
public let title: String
|
public let title: String
|
||||||
public let feedIDs: [FervorID]
|
public let feedIDs: [FervorID]
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Shadowfacts on 10/29/21.
|
// 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 name: String
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let version: String
|
public let version: String
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Shadowfacts on 10/29/21.
|
// 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 id: FervorID
|
||||||
public let feedID: FervorID
|
public let feedID: FervorID
|
||||||
public let title: String?
|
public let title: String?
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
// Created by Shadowfacts on 1/9/22.
|
// 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 syncTimestamp: Date
|
||||||
public let delete: [FervorID]
|
public let delete: [FervorID]
|
||||||
|
|
|
@ -9,7 +9,8 @@ import CoreData
|
||||||
import Fervor
|
import Fervor
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
class PersistentContainer: NSPersistentContainer {
|
// todo: is this actually sendable?
|
||||||
|
class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
||||||
|
|
||||||
private static let managedObjectModel: NSManagedObjectModel = {
|
private static let managedObjectModel: NSManagedObjectModel = {
|
||||||
let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")!
|
let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")!
|
||||||
|
@ -23,13 +24,11 @@ class PersistentContainer: NSPersistentContainer {
|
||||||
return context
|
return context
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private weak var fervorController: FervorController?
|
weak var fervorController: FervorController?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
|
||||||
|
|
||||||
init(account: LocalData.Account, fervorController: FervorController) {
|
init(account: LocalData.Account) {
|
||||||
self.fervorController = fervorController
|
|
||||||
|
|
||||||
super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel)
|
super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel)
|
||||||
|
|
||||||
loadPersistentStores { description, error in
|
loadPersistentStores { description, error in
|
||||||
|
@ -40,7 +39,7 @@ class PersistentContainer: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func saveViewContext() async throws {
|
func saveViewContext() throws {
|
||||||
if viewContext.hasChanges {
|
if viewContext.hasChanges {
|
||||||
try viewContext.save()
|
try viewContext.save()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
// Created by Shadowfacts on 11/25/21.
|
// Created by Shadowfacts on 11/25/21.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
@preconcurrency import Foundation
|
||||||
import Fervor
|
import Fervor
|
||||||
import OSLog
|
@preconcurrency import OSLog
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class FervorController {
|
actor FervorController {
|
||||||
|
|
||||||
static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")!
|
static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")!
|
||||||
|
|
||||||
|
@ -19,36 +19,40 @@ class FervorController {
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
|
||||||
|
|
||||||
let client: FervorClient
|
let client: FervorClient
|
||||||
private(set) var account: LocalData.Account?
|
nonisolated let account: LocalData.Account?
|
||||||
private(set) var clientID: String?
|
private(set) var clientID: String?
|
||||||
private(set) var clientSecret: String?
|
private(set) var clientSecret: String?
|
||||||
private(set) var accessToken: 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.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) {
|
convenience init(account: LocalData.Account) async {
|
||||||
self.init(instanceURL: account.instanceURL)
|
await self.init(instanceURL: account.instanceURL, account: account)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setSyncState(_ state: SyncState) {
|
private func setSyncState(_ state: SyncState) {
|
||||||
DispatchQueue.main.async {
|
lastSyncState = state
|
||||||
self.syncState = state
|
syncState.send(state)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func register() async throws -> ClientRegistration {
|
func register() async throws -> ClientRegistration {
|
||||||
|
@ -60,12 +64,11 @@ class FervorController {
|
||||||
|
|
||||||
func getToken(authCode: String) async throws {
|
func getToken(authCode: String) async throws {
|
||||||
let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
|
let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!)
|
||||||
client.accessToken = token.accessToken
|
|
||||||
accessToken = token.accessToken
|
accessToken = token.accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAll() async throws {
|
func syncAll() async throws {
|
||||||
guard syncState == .done else {
|
guard lastSyncState == .done else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// always return to .done, even if we throw and stop syncing early
|
// always return to .done, even if we throw and stop syncing early
|
||||||
|
@ -90,7 +93,6 @@ class FervorController {
|
||||||
await ExcerptGenerator.generateAll(self)
|
await ExcerptGenerator.generateAll(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func syncReadToServer() async {
|
func syncReadToServer() async {
|
||||||
var count = 0
|
var count = 0
|
||||||
|
|
||||||
|
@ -138,7 +140,7 @@ class FervorController {
|
||||||
func markItem(_ item: Item, read: Bool) async {
|
func markItem(_ item: Item, read: Bool) async {
|
||||||
item.read = read
|
item.read = read
|
||||||
do {
|
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!)
|
_ = try await f(item.id!)
|
||||||
item.needsReadStateSync = false
|
item.needsReadStateSync = false
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -146,12 +148,10 @@ class FervorController {
|
||||||
item.needsReadStateSync = true
|
item.needsReadStateSync = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if persistentContainer.viewContext.hasChanges {
|
do {
|
||||||
do {
|
try self.persistentContainer.saveViewContext()
|
||||||
try persistentContainer.viewContext.save()
|
} catch {
|
||||||
} catch {
|
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
|
||||||
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by Shadowfacts on 10/29/21.
|
// Created by Shadowfacts on 10/29/21.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@preconcurrency import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
|
@ -33,11 +34,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
window!.rootViewController = loginVC
|
window!.rootViewController = loginVC
|
||||||
} else if activity?.activityType == NSUserActivity.activateAccountType,
|
} else if activity?.activityType == NSUserActivity.activateAccountType,
|
||||||
let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) {
|
let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) {
|
||||||
fervorController = FervorController(account: account)
|
Task { @MainActor in
|
||||||
createAppUI()
|
fervorController = await FervorController(account: account)
|
||||||
|
syncFromServer()
|
||||||
|
createAppUI()
|
||||||
|
}
|
||||||
} else if let account = LocalData.mostRecentAccount() {
|
} else if let account = LocalData.mostRecentAccount() {
|
||||||
fervorController = FervorController(account: account)
|
Task { @MainActor in
|
||||||
createAppUI()
|
fervorController = await FervorController(account: account)
|
||||||
|
syncFromServer()
|
||||||
|
createAppUI()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let loginVC = LoginViewController()
|
let loginVC = LoginViewController()
|
||||||
loginVC.delegate = self
|
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
|
LocalData.mostRecentAccountID = account.id
|
||||||
fervorController = FervorController(account: account)
|
fervorController = await FervorController(account: account)
|
||||||
createAppUI()
|
createAppUI()
|
||||||
syncFromServer()
|
syncFromServer()
|
||||||
}
|
}
|
||||||
|
@ -136,15 +143,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
extension SceneDelegate: LoginViewControllerDelegate {
|
extension SceneDelegate: LoginViewControllerDelegate {
|
||||||
func didLogin(with controller: FervorController) {
|
func didLogin(with controller: FervorController) {
|
||||||
let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!)
|
Task { @MainActor in
|
||||||
LocalData.accounts.append(account)
|
let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, accessToken: await controller.accessToken!)
|
||||||
LocalData.mostRecentAccountID = account.id
|
LocalData.accounts.append(account)
|
||||||
fervorController = FervorController(account: account)
|
LocalData.mostRecentAccountID = account.id
|
||||||
|
fervorController = await FervorController(account: account)
|
||||||
|
|
||||||
createAppUI()
|
createAppUI()
|
||||||
syncFromServer()
|
syncFromServer()
|
||||||
|
|
||||||
UIMenuSystem.main.setNeedsRebuild()
|
UIMenuSystem.main.setNeedsRebuild()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,9 @@ extension AppSplitViewController: ItemsViewControllerDelegate {
|
||||||
extension AppSplitViewController: HomeViewControllerDelegate {
|
extension AppSplitViewController: HomeViewControllerDelegate {
|
||||||
func switchToAccount(_ account: LocalData.Account) {
|
func switchToAccount(_ account: LocalData.Account) {
|
||||||
if let delegate = view.window?.windowScene?.delegate as? SceneDelegate {
|
if let delegate = view.window?.windowScene?.delegate as? SceneDelegate {
|
||||||
delegate.switchToAccount(account)
|
Task { @MainActor in
|
||||||
|
await delegate.switchToAccount(account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ class HomeViewController: UIViewController {
|
||||||
feedResultsController.delegate = self
|
feedResultsController.delegate = self
|
||||||
try! feedResultsController.performFetch()
|
try! feedResultsController.performFetch()
|
||||||
|
|
||||||
fervorController.$syncState
|
fervorController.syncState
|
||||||
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
|
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
self.syncStateChanged($0)
|
self.syncStateChanged($0)
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by Shadowfacts on 11/25/21.
|
// Created by Shadowfacts on 11/25/21.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@preconcurrency import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
import Fervor
|
import Fervor
|
||||||
|
@ -72,7 +73,7 @@ class LoginViewController: UIViewController {
|
||||||
textField.isEnabled = false
|
textField.isEnabled = false
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
let controller = FervorController(instanceURL: components.url!)
|
let controller = await FervorController(instanceURL: components.url!, account: nil)
|
||||||
|
|
||||||
let registration: ClientRegistration
|
let registration: ClientRegistration
|
||||||
do {
|
do {
|
||||||
|
@ -99,16 +100,16 @@ class LoginViewController: UIViewController {
|
||||||
let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false)
|
let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false)
|
||||||
guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }),
|
guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }),
|
||||||
let codeValue = codeItem.value else {
|
let codeValue = codeItem.value else {
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert)
|
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))
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
||||||
self.present(alert, animated: true)
|
self.present(alert, animated: true)
|
||||||
|
|
||||||
self.textField.isEnabled = true
|
self.textField.isEnabled = true
|
||||||
self.activityIndicator.stopAnimating()
|
self.activityIndicator.stopAnimating()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await controller.getToken(authCode: codeValue)
|
try await controller.getToken(authCode: codeValue)
|
||||||
|
|
Loading…
Reference in New Issue