// // SyncController.swift // MastoSearchCore // // Created by Shadowfacts on 7/3/22. // import Foundation import Combine import OSLog public class SyncController { private init() {} public static let shared = SyncController() public let onSync = PassthroughSubject() private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync") private var syncTotal = 0 private let dateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() public func syncStatuses(errorHandler: @escaping (APIController.Error) -> Void) { DatabaseController.shared.getNewestAndOldestStatuses { results in if let results { self.logger.log("Starting sync...") self.syncTotal = 0 self.syncStatuses(direction: .newer, range: .after(results.0.id), errorHandler: errorHandler) self.syncStatuses(direction: .older, range: .before(results.1.id), errorHandler: errorHandler) } else { self.logger.log("No newest, starting backwards sync...") self.syncTotal = 0 self.syncStatuses(direction: .older, range: .default, errorHandler: errorHandler) } } } private func syncStatuses(direction: Direction, range: APIController.RequestRange, errorHandler: @escaping (APIController.Error) -> Void) { APIController.shared.getStatuses(range: range) { response in switch response { case .failure(let error): self.logger.error("Error syncing statuses: \(String(describing: error), privacy: .public)") if case .unexpectedStatusCode(_, let resp) = error, resp.value(forHTTPHeaderField: "x-ratelimit-remaining") == "0", let reset = resp.value(forHTTPHeaderField: "x-ratelimit-reset"), let date = self.dateFormatter.date(from: reset) { self.logger.info("Rate limited, continuing at \(date)") DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(Int(ceil(date.timeIntervalSinceNow)))) { self.syncStatuses(direction: direction, range: range, errorHandler: errorHandler) } } else { DispatchQueue.main.async { errorHandler(error) } } case .success(let statuses): guard statuses.count > 0 else { DispatchQueue.main.async { self.logger.log("Finished sync of \(self.syncTotal, privacy: .public) \(direction.name) statuses") self.onSync.send() } return } DatabaseController.shared.addStatuses(statuses.compactMap { if $0.hasReblog { return nil } else { return Status(id: $0.id, url: $0.url, summary: $0.spoiler_text, content: $0.content, published: $0.created_at) } }) DispatchQueue.main.async { self.onSync.send() } self.syncTotal += statuses.count self.syncStatuses(direction: direction, range: direction.nextRange(statuses: statuses), errorHandler: errorHandler) } } } enum Direction { case newer, older var name: String { switch self { case .newer: return "newer" case .older: return "older" } } func nextRange(statuses: [APIController.Status]) -> APIController.RequestRange { switch self { case .newer: return .after(statuses.first!.id) case .older: return .before(statuses.last!.id) } } } }