Profile isolation, data stores, and ephemeral profiles in Nook
The ProfileManager handles creation and lifecycle of both persistent and ephemeral (incognito) profiles, ensuring complete data isolation via WKWebsiteDataStore.
func loadProfiles() { do { let descriptor = FetchDescriptor<ProfileEntity>( sortBy: [SortDescriptor(\.index, order: .forward)] ) let entities = try context.fetch(descriptor) self.profiles = entities.map { e in Profile(id: e.id, name: e.name, icon: e.icon) } // Normalize indices if not sequential 0..n-1 let expected = Array(0..<entities.count) let actual = entities.map { $0.index } if actual != expected { persistProfiles() } } catch { print("[ProfileManager] Failed to load profiles: \(error)") self.profiles = [] }}
@discardableResultfunc createProfile(name: String, icon: String = "person.crop.circle") -> Profile { let nextIndex = profiles.count let profile = Profile(name: name, icon: icon) let entity = ProfileEntity(id: profile.id, name: name, icon: icon, index: nextIndex) context.insert(entity) do { try context.save() } catch { print("[ProfileManager] Save failed during create: \(error)") } profiles.append(profile) return profile}
Each profile gets a unique WKWebsiteDataStore identified by profile.id, providing complete isolation of cookies, localStorage, IndexedDB, and other web data.Source: Nook/Managers/ProfileManager/ProfileManager.swift:47-57
func deleteProfile(_ profile: Profile) -> Bool { guard profiles.count > 1 else { return false } // Prevent deleting last profile // Remove from SwiftData first do { let pid = profile.id let predicate = #Predicate<ProfileEntity> { $0.id == pid } if let entity = try context.fetch(FetchDescriptor<ProfileEntity>(predicate: predicate)).first { context.delete(entity) } try context.save() } catch { print("[ProfileManager] Delete failed: \(error)") return false } // Remove from runtime and reindex if let idx = profiles.firstIndex(where: { $0.id == profile.id }) { profiles.remove(at: idx) } persistProfiles() return true}
Deleting a profile removes its SwiftData entity and destroys its WKWebsiteDataStore, clearing all associated browsing data.Source: Nook/Managers/ProfileManager/ProfileManager.swift:59-79
Deleting a profile is irreversible. All browsing data (cookies, localStorage, history, etc.) associated with that profile is permanently destroyed.
func persistProfiles() { do { // Fetch all existing entities let all = try context.fetch(FetchDescriptor<ProfileEntity>()) var byId: [UUID: ProfileEntity] = Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) }) // Update or insert to match runtime profiles order for (index, p) in profiles.enumerated() { if let e = byId[p.id] { e.name = p.name e.icon = p.icon e.index = index } else { let e = ProfileEntity(id: p.id, name: p.name, icon: p.icon, index: index) context.insert(e) byId[p.id] = e } } // Remove entities not present in runtime array let keep = Set(profiles.map { $0.id }) for (id, e) in byId where !keep.contains(id) { context.delete(e) } try context.save() } catch { print("[ProfileManager] Persist failed: \(error)") }}
@Observableclass Profile: Identifiable { let id: UUID var name: String var icon: String var isEphemeral: Bool = false private var dataStore: WKWebsiteDataStore? var websiteDataStore: WKWebsiteDataStore { if let store = dataStore { return store } if isEphemeral { let store = WKWebsiteDataStore.nonPersistent() dataStore = store return store } else { let store = WKWebsiteDataStore(forIdentifier: id) dataStore = store return store } } static func createEphemeral() -> Profile { let profile = Profile(name: "Incognito", icon: "eye.slash") profile.isEphemeral = true return profile }}
Key properties:
Persistent profiles: WKWebsiteDataStore(forIdentifier: profile.id) creates a stable, on-disk data store
Ephemeral profiles: WKWebsiteDataStore.nonPersistent() creates an in-memory store
Ephemeral data stores are destroyed asynchronously via WKWebsiteDataStore.removeData(). The timeout protection prevents window close operations from hanging if WebKit’s cleanup is slow.
// In ExtensionManager.swiftprivate func getExtensionDataStore(for profileId: UUID) -> WKWebsiteDataStore { if let store = profileExtensionStores[profileId] { return store } let store = WKWebsiteDataStore(forIdentifier: profileId) profileExtensionStores[profileId] = store return store}func switchProfile(_ profileId: UUID) { guard let controller = extensionController else { return } let store = getExtensionDataStore(for: profileId) controller.configuration.defaultWebsiteDataStore = store currentProfileId = profileId}
This means:
Extension chrome.storage.* data is isolated per profile
When the user switches profiles, multiple systems coordinate:
// 1. BrowserManager triggers the switchfunc switchProfile(_ profile: Profile) { currentProfile = profile // 2. Update extension data store if #available(macOS 15.4, *) { ExtensionManager.shared.switchProfile(profile.id) } // 3. Reload tabs to use new profile's data store for tab in tabManager.allTabs() { tab.reloadWithProfile(profile) } // 4. Update window states for (_, windowState) in windowRegistry?.windows ?? [:] { windowState.profileId = profile.id }}
Before persisting profile-related data, check if the profile is ephemeral:
if !profile.isEphemeral { // Safe to persist persistProfileSettings(profile)}
Use async cleanup for ephemeral profiles
Always await ephemeral profile cleanup to ensure data is destroyed:
Task { await profileManager.removeEphemeralProfile(for: windowId) // Now safe to close window}
Prevent deleting the last profile
The UI should disable profile deletion when only one profile remains:
let canDelete = profileManager.profiles.count > 1
Profile switching requires tab reload
After switching profiles, tabs must be reloaded to pick up the new data store:
func switchProfile(_ profile: Profile) { currentProfile = profile for tab in tabManager.allTabs() { tab.reloadWebView() // Critical: Apply new data store }}