refactor: Replace try! with safe ModelContainer initialization fallbacks

- Remove try! force operation that could crash on in-memory container failure
- Implement cascading fallback strategy with 3 initialization attempts:
  1. Persistent storage (normal operation)
  2. In-memory storage with user warning
  3. Ultra-minimal default container
- Add containerInitializationFailed flag to track critical failures
- Extract container creation into static helper methods for better error handling
- Show user-friendly error dialog and graceful termination on total failure
- Only use preconditionFailure as absolute last resort after all attempts fail

Addresses AI code reviewer feedback about unsafe force operations.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Deborah Mangan 2025-11-03 10:37:41 +10:00
parent f261e4937b
commit d1158feef4

View File

@ -10,6 +10,7 @@ import FluidAudio
struct VoiceInkApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let container: ModelContainer
let containerInitializationFailed: Bool
@StateObject private var whisperState: WhisperState
@StateObject private var hotkeyManager: HotkeyManager
@ -36,62 +37,53 @@ struct VoiceInkApp: App {
UserDefaults.standard.set(hasEnabledPowerModes, forKey: "powerModeUIFlag")
}
do {
let schema = Schema([
Transcription.self
])
// Create app-specific Application Support directory URL
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("com.prakashjoshipax.VoiceInk", isDirectory: true)
// Create the directory if it doesn't exist
try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
// Configure SwiftData to use the conventional location
let storeURL = appSupportURL.appendingPathComponent("default.store")
let modelConfiguration = ModelConfiguration(schema: schema, url: storeURL)
container = try ModelContainer(for: schema, configurations: [modelConfiguration])
let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "Initialization")
let schema = Schema([Transcription.self])
var initializationFailed = false
// Attempt 1: Try persistent storage
if let persistentContainer = Self.createPersistentContainer(schema: schema, logger: logger) {
container = persistentContainer
#if DEBUG
// Print SwiftData storage location in debug builds only
if let url = container.mainContext.container.configurations.first?.url {
if let url = persistentContainer.mainContext.container.configurations.first?.url {
print("💾 SwiftData storage location: \(url.path)")
}
#endif
}
// Attempt 2: Try in-memory storage
else if let memoryContainer = Self.createInMemoryContainer(schema: schema, logger: logger) {
container = memoryContainer
} catch {
// Graceful degradation: Use in-memory storage as fallback
let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "Initialization")
logger.error("Failed to create persistent ModelContainer: \(error.localizedDescription)")
logger.warning("Using in-memory storage as fallback. Data will not persist between sessions.")
// Attempt in-memory fallback
do {
let schema = Schema([Transcription.self])
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
container = try ModelContainer(for: schema, configurations: [configuration])
logger.warning("Using in-memory storage as fallback. Data will not persist between sessions.")
// Show alert to user about storage issue
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "Storage Warning"
alert.informativeText = "VoiceInk couldn't access its storage location. Your transcriptions will not be saved between sessions."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
} catch {
// Last resort: critical failure, but log and attempt to continue
logger.critical("Failed to create in-memory ModelContainer: \(error.localizedDescription)")
// Create minimal container with empty schema
let schema = Schema([Transcription.self])
container = try! ModelContainer(for: schema, configurations: [ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)])
// Show alert to user about storage issue
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "Storage Warning"
alert.informativeText = "VoiceInk couldn't access its storage location. Your transcriptions will not be saved between sessions."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
// Attempt 3: Try ultra-minimal default container
else if let minimalContainer = Self.createMinimalContainer(schema: schema, logger: logger) {
container = minimalContainer
logger.warning("Using minimal emergency container")
}
// All attempts failed: Create disabled container and mark for termination
else {
logger.critical("All ModelContainer initialization attempts failed")
initializationFailed = true
// Create a dummy container to satisfy Swift's initialization requirements
// App will show error and terminate in onAppear
container = Self.createDummyContainer(schema: schema)
}
containerInitializationFailed = initializationFailed
// Initialize services with proper sharing of instances
let aiService = AIService()
@ -126,6 +118,69 @@ struct VoiceInkApp: App {
AppShortcuts.updateAppShortcutParameters()
}
// MARK: - Container Creation Helpers
private static func createPersistentContainer(schema: Schema, logger: Logger) -> ModelContainer? {
do {
// Create app-specific Application Support directory URL
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("com.prakashjoshipax.VoiceInk", isDirectory: true)
// Create the directory if it doesn't exist
try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
// Configure SwiftData to use the conventional location
let storeURL = appSupportURL.appendingPathComponent("default.store")
let modelConfiguration = ModelConfiguration(schema: schema, url: storeURL)
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
logger.error("Failed to create persistent ModelContainer: \(error.localizedDescription)")
return nil
}
}
private static func createInMemoryContainer(schema: Schema, logger: Logger) -> ModelContainer? {
do {
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
return try ModelContainer(for: schema, configurations: [configuration])
} catch {
logger.error("Failed to create in-memory ModelContainer: \(error.localizedDescription)")
return nil
}
}
private static func createMinimalContainer(schema: Schema, logger: Logger) -> ModelContainer? {
do {
// Try default initializer without custom configuration
return try ModelContainer(for: schema)
} catch {
logger.error("Failed to create minimal ModelContainer: \(error.localizedDescription)")
return nil
}
}
private static func createDummyContainer(schema: Schema) -> ModelContainer {
// Create an absolute minimal container for initialization
// This uses in-memory storage and will never actually be used
// as the app will show an error and terminate in onAppear
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
// Note: In-memory containers should always succeed unless SwiftData itself is unavailable
// (which would indicate a serious system-level issue). We use preconditionFailure here
// rather than fatalError because:
// 1. This code is only reached after 3 prior initialization attempts have failed
// 2. An in-memory container failing indicates SwiftData is completely unavailable
// 3. Swift requires non-optional container property to be initialized
// 4. The app will immediately terminate in onAppear when containerInitializationFailed is checked
do {
return try ModelContainer(for: schema, configurations: [config])
} catch {
// This indicates a system-level SwiftData failure - app cannot function
preconditionFailure("Unable to create even a dummy ModelContainer. SwiftData is unavailable: \(error)")
}
}
var body: some Scene {
WindowGroup {
if hasCompletedOnboarding {
@ -138,6 +193,19 @@ struct VoiceInkApp: App {
.environmentObject(enhancementService)
.modelContainer(container)
.onAppear {
// Check if container initialization failed
if containerInitializationFailed {
let alert = NSAlert()
alert.messageText = "Critical Storage Error"
alert.informativeText = "VoiceInk cannot initialize its storage system. The app cannot continue.\n\nPlease try reinstalling the app or contact support if the issue persists."
alert.alertStyle = .critical
alert.addButton(withTitle: "Quit")
alert.runModal()
NSApplication.shared.terminate(nil)
return
}
updaterViewModel.silentlyCheckForUpdates()
if enableAnnouncements {
AnnouncementsService.shared.start()