Skip to content

Commit 543edd5

Browse files
authored
refactor: improve Swift 6 concurrency safety, API parsing o11y (#78)
1 parent 5d6740f commit 543edd5

22 files changed

+396
-66
lines changed

Core/Services/AI/AIErrorHandler.swift

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,44 @@ enum AIErrorHandler {
123123
/// Handles specific GenerationError cases.
124124
private static func handleGenerationError(_ error: LanguageModelSession.GenerationError) -> AIError {
125125
switch error {
126-
case let .exceededContextWindowSize(info):
127-
self.logger.warning("Context window exceeded: \\(String(describing: info))")
126+
case .exceededContextWindowSize:
127+
self.logger.warning("Context window exceeded")
128128
return .contextWindowExceeded
129129

130130
case .guardrailViolation:
131131
self.logger.warning("Content blocked by guardrails")
132132
return .contentBlocked
133133

134+
case .assetsUnavailable:
135+
self.logger.warning("Model assets unavailable")
136+
return .notAvailable(reason: "Model assets are not available")
137+
138+
case .unsupportedGuide:
139+
self.logger.warning("Unsupported generation guide")
140+
return .unknown(underlying: error)
141+
142+
case .unsupportedLanguageOrLocale:
143+
self.logger.warning("Unsupported language or locale")
144+
return .notAvailable(reason: "Language not supported")
145+
146+
case .decodingFailure:
147+
self.logger.warning("Failed to decode model response")
148+
return .unknown(underlying: error)
149+
150+
case .rateLimited:
151+
self.logger.warning("Rate limited by model")
152+
return .sessionBusy
153+
154+
case .concurrentRequests:
155+
self.logger.warning("Concurrent request limit exceeded")
156+
return .sessionBusy
157+
158+
case .refusal:
159+
self.logger.warning("Model refused to respond")
160+
return .contentBlocked
161+
134162
@unknown default:
135-
self.logger.error("Unknown generation error: \\(error.localizedDescription)")
163+
self.logger.error("Unknown generation error: \(error.localizedDescription)")
136164
return .unknown(underlying: error)
137165
}
138166
}
@@ -145,9 +173,9 @@ enum AIErrorHandler {
145173
/// - Returns: A formatted string suitable for UI display.
146174
static func userMessage(for error: AIError) -> String {
147175
if let suggestion = error.recoverySuggestion {
148-
return "\(error.localizedDescription ?? "Error"). \(suggestion)"
176+
return "\(error.errorDescription ?? "Error"). \(suggestion)"
149177
}
150-
return error.localizedDescription ?? "An error occurred"
178+
return error.errorDescription ?? "An error occurred"
151179
}
152180

153181
/// Logs an error with appropriate severity and returns a display message.

Core/Services/AI/FoundationModelsService.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,8 @@ final class FoundationModelsService {
193193

194194
let session = LanguageModelSession()
195195

196-
do {
197-
// Use the official prewarm API instead of sending a dummy prompt
198-
try await session.prewarm()
199-
self.logger.debug("Foundation Models prewarm completed successfully")
200-
} catch {
201-
self.logger.error("Failed to prewarm session: \(error.localizedDescription)")
202-
}
196+
// Use the official prewarm API to load model resources
197+
await session.prewarm()
198+
self.logger.debug("Foundation Models prewarm completed successfully")
203199
}
204200
}

Core/Services/API/Parsers/LyricsParser.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Foundation
22

33
/// Parses lyrics responses from YouTube Music API.
44
enum LyricsParser {
5+
private static let logger = DiagnosticsLogger.api
6+
57
/// Extracts the lyrics browse ID from the "next" endpoint response.
68
/// - Parameter data: The response from the "next" endpoint
79
/// - Returns: The browse ID for fetching lyrics, or nil if unavailable
@@ -12,6 +14,7 @@ enum LyricsParser {
1214
let watchNextTabbedResults = tabbedRenderer["watchNextTabbedResultsRenderer"] as? [String: Any],
1315
let tabs = watchNextTabbedResults["tabs"] as? [[String: Any]]
1416
else {
17+
self.logger.debug("LyricsParser: Failed to extract lyrics browse ID structure")
1518
return nil
1619
}
1720

Core/Services/API/Parsers/RadioQueueParser.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ struct RadioQueueResult {
1313

1414
/// Parses radio queue responses from YouTube Music API.
1515
enum RadioQueueParser {
16+
private static let logger = DiagnosticsLogger.api
17+
1618
/// Parses the radio queue from the "next" endpoint response.
1719
/// - Parameter data: The response from the "next" endpoint with a radio playlist ID
1820
/// - Returns: RadioQueueResult containing songs and optional continuation token
@@ -30,6 +32,7 @@ enum RadioQueueParser {
3032
let playlistPanelRenderer = queueContent["playlistPanelRenderer"] as? [String: Any],
3133
let playlistContents = playlistPanelRenderer["contents"] as? [[String: Any]]
3234
else {
35+
self.logger.debug("RadioQueueParser: Failed to parse radio queue structure. Top keys: \(data.keys.sorted())")
3336
return RadioQueueResult(songs: [], continuationToken: nil)
3437
}
3538

Core/Services/API/Parsers/SearchResponseParser.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Foundation
22

33
/// Parser for search responses from YouTube Music API.
44
enum SearchResponseParser {
5+
private static let logger = DiagnosticsLogger.api
6+
57
/// Parses a search response.
68
static func parse(_ data: [String: Any]) -> SearchResponse {
79
var songs: [Song] = []
@@ -19,6 +21,7 @@ enum SearchResponseParser {
1921
let sectionListRenderer = tabContent["sectionListRenderer"] as? [String: Any],
2022
let sectionContents = sectionListRenderer["contents"] as? [[String: Any]]
2123
else {
24+
Self.logger.debug("SearchResponseParser: Failed to parse response structure. Top keys: \(data.keys.sorted())")
2225
return SearchResponse.empty
2326
}
2427

Core/Services/API/Parsers/SearchSuggestionsParser.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Foundation
22

33
/// Parser for search suggestions responses from YouTube Music API.
44
enum SearchSuggestionsParser {
5+
private static let logger = DiagnosticsLogger.api
6+
57
/// Parses a search suggestions response.
68
/// - Parameter data: The raw JSON response from the API.
79
/// - Returns: An array of search suggestions.
@@ -10,6 +12,7 @@ enum SearchSuggestionsParser {
1012

1113
// Navigate to contents array
1214
guard let contents = data["contents"] as? [[String: Any]] else {
15+
Self.logger.debug("SearchSuggestionsParser: No contents array found. Top keys: \(data.keys.sorted())")
1316
return suggestions
1417
}
1518

Core/Services/API/Parsers/SongMetadataParser.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Foundation
22

33
/// Parses song metadata from YouTube Music API responses.
44
enum SongMetadataParser {
5+
private static let logger = DiagnosticsLogger.api
6+
57
/// Contains parsed menu data including feedback tokens, library status, and like status.
68
struct MenuParseResult {
79
var feedbackTokens: FeedbackTokens?

Core/Services/NetworkMonitor.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,8 @@ final class NetworkMonitor {
6161
}
6262
}
6363

64-
// swiftformat:disable modifierOrder
65-
/// NWPathMonitor accessed from deinit, which is non-isolated in Swift 6.
66-
/// Using nonisolated(unsafe) since monitor.cancel() is thread-safe.
67-
nonisolated(unsafe) private let monitor: NWPathMonitor
68-
// swiftformat:enable modifierOrder
64+
/// NWPathMonitor is Sendable and immutable, so no isolation annotation needed.
65+
private let monitor: NWPathMonitor
6966
private let queue: DispatchQueue
7067
private let logger = DiagnosticsLogger.network
7168

Core/Services/Notification/NotificationService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ final class NotificationService {
88
private let settingsManager: SettingsManager
99
private let logger = DiagnosticsLogger.notification
1010
// swiftformat:disable modifierOrder
11-
/// Task accessed from deinit, which is non-isolated in Swift 6.
12-
/// Using nonisolated(unsafe) since Task.cancel() is thread-safe.
11+
/// Task for observing player changes, cancelled in deinit.
12+
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
1313
nonisolated(unsafe) private var observationTask: Task<Void, Never>?
1414
// swiftformat:enable modifierOrder
1515
private var lastNotifiedTrackId: String?

Core/Services/WebKit/WebKitManager.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ enum KeychainCookieStorage {
4646
for (key, value) in properties {
4747
stringProperties[key.rawValue] = value
4848
}
49+
// Note: Cookie properties dictionary contains types like String, Date, Number, Bool
50+
// which all support NSSecureCoding. However, using requiringSecureCoding: false here
51+
// because [String: Any] doesn't directly conform to NSSecureCoding.
52+
// The unarchive side uses explicit class allowlists for security.
4953
return try? NSKeyedArchiver.archivedData(
5054
withRootObject: stringProperties,
5155
requiringSecureCoding: false
@@ -54,8 +58,8 @@ enum KeychainCookieStorage {
5458

5559
guard !cookieData.isEmpty,
5660
let data = try? NSKeyedArchiver.archivedData(
57-
withRootObject: cookieData,
58-
requiringSecureCoding: false
61+
withRootObject: cookieData as NSArray,
62+
requiringSecureCoding: true
5963
)
6064
else {
6165
Self.logger.error("Failed to serialize cookies for Keychain")

0 commit comments

Comments
 (0)