AIHistoryManager.swift 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import Foundation
  2. @MainActor
  3. @Observable
  4. final class AIHistoryManager {
  5. static let shared = AIHistoryManager()
  6. private(set) var entries: [AIHistoryEntry] = []
  7. private let defaults: UserDefaults
  8. private let storageKey = "reddora.ai.history.entries"
  9. private let maxEntries = 30
  10. private let persistQueue = DispatchQueue(label: "com.app-for-reddit.ai-history.persist", qos: .utility)
  11. init(defaults: UserDefaults = .standard) {
  12. self.defaults = defaults
  13. load()
  14. }
  15. var isEmpty: Bool {
  16. entries.isEmpty
  17. }
  18. func entries(matching filter: AIHistoryFilter) -> [AIHistoryEntry] {
  19. guard let toolKind = filter.toolKind else { return entries }
  20. return entries.filter { $0.toolKind == toolKind }
  21. }
  22. func save(_ entry: AIHistoryEntry) {
  23. entries.removeAll { $0.id == entry.id }
  24. entries.insert(entry, at: 0)
  25. enforceLimit()
  26. persist()
  27. }
  28. func delete(id: UUID) {
  29. guard entries.contains(where: { $0.id == id }) else { return }
  30. entries.removeAll { $0.id == id }
  31. persist()
  32. }
  33. func deleteAll() {
  34. guard !entries.isEmpty else { return }
  35. entries.removeAll()
  36. persist()
  37. }
  38. private func load() {
  39. guard let data = defaults.data(forKey: storageKey) else {
  40. entries = []
  41. return
  42. }
  43. do {
  44. let decoder = JSONDecoder()
  45. decoder.dateDecodingStrategy = .iso8601
  46. entries = try decoder.decode([AIHistoryEntry].self, from: data)
  47. let hadExcessEntries = entries.count > maxEntries
  48. enforceLimit()
  49. if hadExcessEntries {
  50. persist()
  51. }
  52. } catch {
  53. entries = []
  54. }
  55. }
  56. private func enforceLimit() {
  57. guard entries.count > maxEntries else { return }
  58. entries = Array(entries.prefix(maxEntries))
  59. }
  60. private func persist() {
  61. let snapshot = entries
  62. let storageKey = storageKey
  63. let defaults = defaults
  64. persistQueue.async {
  65. do {
  66. let encoder = JSONEncoder()
  67. encoder.dateEncodingStrategy = .iso8601
  68. let data = try encoder.encode(snapshot)
  69. defaults.set(data, forKey: storageKey)
  70. } catch {
  71. // Keep in-memory entries if persistence fails.
  72. }
  73. }
  74. }
  75. }