feat: Improve API data fetching reliability with cache control, enhanced logging, and error handling, and update version to 1.2.2.

This commit is contained in:
Paweł Orzech 2026-01-17 21:58:12 +00:00
parent c21057146d
commit 4b4f1c15f7
No known key found for this signature in database
3 changed files with 74 additions and 33 deletions

BIN
MacTorn-1.2.2.zip Normal file

Binary file not shown.

View file

@ -17,8 +17,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2.1</string> <string>1.2.2</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.2.1</string> <string>1.2.2</string>
</dict> </dict>
</plist> </plist>

View file

@ -121,29 +121,26 @@ class AppState: ObservableObject {
guard !apiKey.isEmpty, guard !apiKey.isEmpty,
let url = TornAPI.marketURL(itemId: itemId, apiKey: apiKey) else { return } let url = TornAPI.marketURL(itemId: itemId, apiKey: apiKey) else { return }
// Debug logger.info("Fetching price for item \(itemId)")
// print("Fetching price for item \(itemId): \(url.absoluteString)")
do { do {
let (data, response) = try await URLSession.shared.data(from: url) var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
// print("HTTP Error: \(httpResponse.statusCode)") logger.error("Item \(itemId) HTTP Error: \(httpResponse.statusCode)")
await updateItemError(itemId: itemId, error: "HTTP \(httpResponse.statusCode)") await updateItemError(itemId: itemId, error: "HTTP \(httpResponse.statusCode)")
return return
} }
// Debug JSON
// if let str = String(data: data, encoding: .utf8) {
// print("Market JSON: \(str)")
// }
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
// Check if API returned error // Check if API returned error
if let error = json["error"] as? [String: Any], let errorText = error["error"] as? String { if let error = json["error"] as? [String: Any], let errorText = error["error"] as? String {
await updateItemError(itemId: itemId, error: errorText) logger.warning("Item \(itemId) API error: \(errorText)")
return await updateItemError(itemId: itemId, error: errorText)
return
} }
var allListings: [(price: Int, amount: Int)] = [] var allListings: [(price: Int, amount: Int)] = []
@ -176,7 +173,7 @@ class AppState: ObservableObject {
} }
let sortedListings = allListings.sorted { $0.price < $1.price } let sortedListings = allListings.sorted { $0.price < $1.price }
// print("Found \(sortedListings.count) listings for item \(itemId). Lowest: \(sortedListings.first?.price ?? 0)") logger.debug("Item \(itemId): found \(sortedListings.count) listings, lowest: \(sortedListings.first?.price ?? 0)")
await MainActor.run { await MainActor.run {
if let index = watchlistItems.firstIndex(where: { $0.id == itemId }) { if let index = watchlistItems.firstIndex(where: { $0.id == itemId }) {
@ -200,7 +197,7 @@ class AppState: ObservableObject {
} }
} }
} catch { } catch {
// print("Price fetch error: \(error)") logger.error("Item \(itemId) price fetch error: \(error.localizedDescription)")
await updateItemError(itemId: itemId, error: "Network Error") await updateItemError(itemId: itemId, error: "Network Error")
} }
} }
@ -250,7 +247,7 @@ class AppState: ObservableObject {
isLoading = true isLoading = true
errorMsg = nil errorMsg = nil
logger.info("Starting data fetch...") logger.info("Starting data fetch from: \(url.absoluteString.prefix(80))...")
Task { Task {
let startTime = Date() let startTime = Date()
@ -267,7 +264,11 @@ class AppState: ObservableObject {
} }
do { do {
let (data, response) = try await URLSession.shared.data(from: url) // Create request with no-cache policy to ensure fresh data
var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse throw APIError.invalidResponse
@ -277,6 +278,11 @@ class AppState: ObservableObject {
switch httpResponse.statusCode { switch httpResponse.statusCode {
case 200: case 200:
// Log raw JSON for debugging (first 500 chars)
if let jsonString = String(data: data, encoding: .utf8) {
logger.info("Raw API response: \(jsonString.prefix(500))")
}
// Check for Torn API error in response (API returns 200 even on errors) // Check for Torn API error in response (API returns 200 even on errors)
if let tornError = checkForTornAPIError(data: data) { if let tornError = checkForTornAPIError(data: data) {
await MainActor.run { await MainActor.run {
@ -331,9 +337,9 @@ class AppState: ObservableObject {
// Move parsing logic here and mark as non-isolated or detached // Move parsing logic here and mark as non-isolated or detached
private func parseDataInBackground(data: Data) async throws { private func parseDataInBackground(data: Data) async throws {
// Run CPU-heavy parsing detached from MainActor // Run CPU-heavy parsing detached from MainActor
let result = await Task.detached(priority: .userInitiated) { () -> (TornResponse?, MoneyData?, BattleStats?, [AttackResult]?, [PropertyInfo]?) in let result = await Task.detached(priority: .userInitiated) { () -> (TornResponse?, MoneyData?, BattleStats?, [AttackResult]?, [PropertyInfo]?, String?) in
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return (nil, nil, nil, nil, nil) return (nil, nil, nil, nil, nil, "Failed to parse response")
} }
// Attempt to decode TornResponse first // Attempt to decode TornResponse first
@ -391,11 +397,28 @@ class AppState: ObservableObject {
) )
} }
} }
return (decodedTornResponse, moneyData, battleStats, attacksList, propertiesList) // If we couldn't decode TornResponse, report error but continue with extended data
let parseError: String? = (decodedTornResponse == nil) ? "Failed to decode user data" : nil
return (decodedTornResponse, moneyData, battleStats, attacksList, propertiesList, parseError)
}.value }.value
await MainActor.run { await MainActor.run {
// Check for parse errors
if let parseError = result.5, result.0 == nil {
self.errorMsg = parseError
logger.error("Parse error: \(parseError)")
self.lastUpdated = Date() // Still update timestamp on error
return
}
if let decoded = result.0 { if let decoded = result.0 {
logger.info("Parsed data - Name: \(decoded.name ?? "nil"), Life: \(decoded.life?.current ?? -1)/\(decoded.life?.maximum ?? -1)")
logger.info("Status: \(decoded.status?.description ?? "nil"), State: \(decoded.status?.state ?? "nil")")
if let events = decoded.events {
logger.info("Events count: \(events.count)")
}
self.checkNotifications(newData: decoded) self.checkNotifications(newData: decoded)
self.data = decoded self.data = decoded
@ -404,6 +427,8 @@ class AppState: ObservableObject {
self.previousTravel = decoded.travel self.previousTravel = decoded.travel
self.previousChain = decoded.chain self.previousChain = decoded.chain
self.previousStatus = decoded.status self.previousStatus = decoded.status
} else {
logger.warning("TornResponse decoded as nil but no parse error reported")
} }
if let m = result.1 { self.moneyData = m } if let m = result.1 { self.moneyData = m }
@ -412,8 +437,11 @@ class AppState: ObservableObject {
if let p = result.4 { self.propertiesData = p } if let p = result.4 { self.propertiesData = p }
self.lastUpdated = Date() self.lastUpdated = Date()
self.isLoading = false
self.errorMsg = nil self.errorMsg = nil
// Force UI update by triggering objectWillChange
self.objectWillChange.send()
logger.info("UI update triggered, lastUpdated: \(self.lastUpdated?.description ?? "nil")")
} }
} }
@ -422,7 +450,18 @@ class AppState: ObservableObject {
guard let url = TornAPI.factionURL(for: apiKey) else { return } guard let url = TornAPI.factionURL(for: apiKey) else { return }
do { do {
let (data, _) = try await URLSession.shared.data(from: url) var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
let (data, _) = try await URLSession.shared.data(for: request)
// Check for Torn API error
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let errorMessage = error["error"] as? String {
logger.warning("Faction API error: \(errorMessage)")
return
}
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
let name = json["name"] as? String ?? "" let name = json["name"] as? String ?? ""
let factionId = json["ID"] as? Int ?? 0 let factionId = json["ID"] as? Int ?? 0
@ -439,8 +478,10 @@ class AppState: ObservableObject {
} }
self.factionData = FactionData(name: name, factionId: factionId, respect: respect, chain: chain) self.factionData = FactionData(name: name, factionId: factionId, respect: respect, chain: chain)
logger.info("Faction data fetched: \(name)")
} }
} catch { } catch {
logger.warning("Faction fetch error (optional): \(error.localizedDescription)")
// Faction data is optional, ignore errors // Faction data is optional, ignore errors
} }
} }