The old server name and the account the repositories live under were still scattered through comments, doc prose and one line of dashboard copy. None of those readings told anyone anything: neither name is what anything is called now, and a comment that explains a value in terms of a name nobody uses explains nothing. Values are untouched, and that distinction is the whole rule. The legacy backup type string, the pre-rename UserDefaults key, the pre-rename host in the User-Agent allowlist and the registry namespaces are all things somebody else already stored; rewriting one breaks an install rather than tidying it. So the sentences changed and the strings did not, and AGENTS.md now lists exactly which hits a sweep is supposed to keep finding. The feed's blocklist grows the second name, so a future message cannot put it back on a page anyone can load. Its fixtures name both on purpose: that test is the proof the feed never publishes them, and it cannot show that without saying them once. Also states the thing that had been left implicit: FTStore Admin is the dashboard and the console app together, so an operator-facing control that lands in only one of them is half-built.
92 lines
2.6 KiB
Swift
92 lines
2.6 KiB
Swift
//
|
|
// FetchService.swift
|
|
// Loader
|
|
//
|
|
// Created by samara on 14.03.2025.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Class
|
|
public class NBFetchService {
|
|
|
|
public enum NBFetchServiceError: Error, LocalizedError {
|
|
case invalidURL
|
|
case networkError(Error)
|
|
case noData
|
|
case parsingError(Error)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL: "The URL is invalid."
|
|
case .networkError(let error): "Network error: \(error.localizedDescription)"
|
|
case .noData: "No data received."
|
|
case .parsingError(let error): "Failed to parse data: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
|
|
public init() {}
|
|
|
|
/// Identifies the app to its repo backend. The server gates its feed on this, so it
|
|
/// must be deterministic and start with `FTStore`/`FTStore-Manager`. Derived from
|
|
/// the running app's bundle so the same code yields the right value in both apps.
|
|
public static let userAgent: String = {
|
|
let info = Bundle.main.infoDictionary
|
|
let version = (info?["CFBundleShortVersionString"] as? String) ?? "1.0"
|
|
let bundleId = Bundle.main.bundleIdentifier ?? "app.ftstore"
|
|
let product = bundleId == "app.ftstore.manager" ? "FTStore-Manager" : "FTStore"
|
|
return "\(product)/\(version) (\(bundleId))"
|
|
}()
|
|
}
|
|
|
|
// MARK: - Class extension: fetch
|
|
extension NBFetchService {
|
|
public func fetch<T: Decodable>(
|
|
from urlString: String,
|
|
completion: @escaping (Result<T, Error>) -> Void
|
|
) {
|
|
guard let url = URL(string: urlString) else {
|
|
completion(.failure(NBFetchServiceError.invalidURL))
|
|
return
|
|
}
|
|
|
|
fetch(from: url, completion: completion)
|
|
}
|
|
|
|
public func fetch<T: Decodable>(
|
|
from url: URL,
|
|
completion: @escaping (Result<T, Error>) -> Void
|
|
) {
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
// Always pull a fresh copy: source feeds change frequently, and the
|
|
// default protocol cache policy would otherwise serve a stale JSON
|
|
// (showing versions that no longer exist in the live manifest).
|
|
var request = URLRequest(url: url)
|
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
request.setValue(Self.userAgent, forHTTPHeaderField: "User-Agent")
|
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
|
if let error = error {
|
|
completion(.failure(NBFetchServiceError.networkError(error)))
|
|
return
|
|
}
|
|
|
|
guard let data = data else {
|
|
completion(.failure(NBFetchServiceError.noData))
|
|
return
|
|
}
|
|
|
|
do {
|
|
let decoder = JSONDecoder()
|
|
let decodedData = try decoder.decode(T.self, from: data)
|
|
completion(.success(decodedData))
|
|
} catch {
|
|
completion(.failure(NBFetchServiceError.parsingError(error)))
|
|
}
|
|
}
|
|
|
|
task.resume()
|
|
}
|
|
}
|
|
}
|