add cache

This commit is contained in:
cheykrym 2025-10-21 18:10:25 +03:00
parent 6fd82e25c1
commit 7c8940da5b
2 changed files with 240 additions and 3 deletions

View File

@ -0,0 +1,233 @@
import Foundation
import CoreData
final class ChatCacheWriter {
static let shared = ChatCacheWriter()
private let persistenceController: PersistenceController
private let userDefaults: UserDefaults
private let keychainService: KeychainService
init(
persistenceController: PersistenceController = .shared,
userDefaults: UserDefaults = .standard,
keychainService: KeychainService = .shared
) {
self.persistenceController = persistenceController
self.userDefaults = userDefaults
self.keychainService = keychainService
}
func storePrivateChatList(_ data: PrivateChatListData) {
guard !AppConfig.DISABLE_DB else { return }
guard let accountUserId = resolveCurrentAccountUserId() else { return }
let context = persistenceController.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform { [weak self] in
guard let self else { return }
do {
try self.sync(chatList: data, accountUserId: accountUserId, context: context)
if context.hasChanges {
try context.save()
}
} catch {
if AppConfig.DEBUG {
print("[ChatCacheWriter] Failed to store private chat list: \(error)")
}
}
}
}
private func resolveCurrentAccountUserId() -> String? {
guard let login = userDefaults.string(forKey: "currentUser") else {
return nil
}
return keychainService.get(forKey: "userId", service: login)
}
private func sync(chatList: PrivateChatListData, accountUserId: String, context: NSManagedObjectContext) throws {
var profileCache: [String: CDProfile] = [:]
var chatCache: [String: CDPrivateChat] = [:]
var messageCache: [String: CDMessage] = [:]
for item in chatList.items {
let chat = try upsertChat(item, accountUserId: accountUserId, context: context, cache: &chatCache)
let previousLastMessage = chat.lastMessage
if let profileData = item.chatData {
let profile = try upsertProfile(profileData, accountUserId: accountUserId, context: context, cache: &profileCache)
chat.chatData = profile
} else {
chat.chatData = nil
}
if let lastMessageData = item.lastMessage {
let message = try upsertMessage(
lastMessageData,
accountUserId: accountUserId,
chat: chat,
context: context,
profileCache: &profileCache,
messageCache: &messageCache
)
if let previous = previousLastMessage, previous.objectID != message.objectID {
previous.chatAsLastMessage = nil
}
chat.lastMessage = message
} else {
previousLastMessage?.chatAsLastMessage = nil
chat.lastMessage = nil
}
}
}
private func upsertChat(
_ chatItem: PrivateChatListItem,
accountUserId: String,
context: NSManagedObjectContext,
cache: inout [String: CDPrivateChat]
) throws -> CDPrivateChat {
if let cached = cache[chatItem.chatId] {
apply(chatItem: chatItem, to: cached, accountUserId: accountUserId)
return cached
}
let chat = try fetchChat(chatId: chatItem.chatId, accountUserId: accountUserId, context: context) ?? CDPrivateChat(context: context)
apply(chatItem: chatItem, to: chat, accountUserId: accountUserId)
cache[chatItem.chatId] = chat
return chat
}
private func apply(chatItem: PrivateChatListItem, to chat: CDPrivateChat, accountUserId: String) {
chat.accountUserId = accountUserId
chat.chatId = chatItem.chatId
chat.chatType = chatItem.chatType.rawValue
chat.createdAt = chatItem.createdAt
chat.unreadCount = Int32(chatItem.unreadCount)
}
private func upsertProfile(
_ profileData: ChatProfile,
accountUserId: String,
context: NSManagedObjectContext,
cache: inout [String: CDProfile]
) throws -> CDProfile {
if let cached = cache[profileData.userId] {
apply(profile: profileData, to: cached, accountUserId: accountUserId)
return cached
}
let profile = try fetchProfile(userId: profileData.userId, accountUserId: accountUserId, context: context) ?? CDProfile(context: context)
apply(profile: profileData, to: profile, accountUserId: accountUserId)
cache[profileData.userId] = profile
return profile
}
private func apply(profile profileData: ChatProfile, to profile: CDProfile, accountUserId: String) {
profile.accountUserId = accountUserId
profile.userId = profileData.userId
profile.login = profileData.login
profile.fullName = profileData.fullName
profile.customName = profileData.customName
profile.bio = profileData.bio
if let lastSeen = profileData.lastSeen {
profile.setValue(NSNumber(value: lastSeen), forKey: "lastSeen")
} else {
profile.setValue(nil, forKey: "lastSeen")
}
profile.createdAt = profileData.createdAt
profile.isOfficial = profileData.isOfficial
}
private func upsertMessage(
_ messageData: MessageItem,
accountUserId: String,
chat: CDPrivateChat,
context: NSManagedObjectContext,
profileCache: inout [String: CDProfile],
messageCache: inout [String: CDMessage]
) throws -> CDMessage {
if let cached = messageCache[messageData.messageId] {
apply(message: messageData, to: cached, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache)
return cached
}
let message = try fetchMessage(messageId: messageData.messageId, accountUserId: accountUserId, context: context) ?? CDMessage(context: context)
apply(message: messageData, to: message, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache)
messageCache[messageData.messageId] = message
return message
}
private func apply(
message messageData: MessageItem,
to message: CDMessage,
accountUserId: String,
chat: CDPrivateChat,
context: NSManagedObjectContext,
profileCache: inout [String: CDProfile]
) {
message.accountUserId = accountUserId
message.messageId = messageData.messageId
message.chatId = messageData.chatId
message.messageType = messageData.messageType
message.content = messageData.content
message.mediaLink = messageData.mediaLink
message.createdAt = messageData.createdAt
message.updatedAt = messageData.updatedAt
message.senderId = messageData.senderId
message.chat = chat
message.chatAsLastMessage = chat
if let isViewed = messageData.isViewed {
message.setValue(NSNumber(value: isViewed), forKey: "isViewed")
} else {
message.setValue(nil, forKey: "isViewed")
}
if let forward = messageData.forwardMetadata {
message.forwardType = forward.forwardType
message.forwardSenderId = forward.forwardSenderId
message.forwardMessageId = forward.forwardMessageId
} else {
message.forwardType = nil
message.forwardSenderId = nil
message.forwardMessageId = nil
}
if let senderProfileData = messageData.senderData {
if let profile = try? upsertProfile(
senderProfileData,
accountUserId: accountUserId,
context: context,
cache: &profileCache
) {
message.sender = profile
}
} else {
message.sender = nil
}
}
private func fetchChat(chatId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDPrivateChat? {
let request = NSFetchRequest<CDPrivateChat>(entityName: "CDPrivateChat")
request.fetchLimit = 1
request.predicate = NSPredicate(format: "accountUserId == %@ AND chatId == %@", accountUserId, chatId)
return try context.fetch(request).first
}
private func fetchProfile(userId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDProfile? {
let request = NSFetchRequest<CDProfile>(entityName: "CDProfile")
request.fetchLimit = 1
request.predicate = NSPredicate(format: "accountUserId == %@ AND userId == %@", accountUserId, userId)
return try context.fetch(request).first
}
private func fetchMessage(messageId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDMessage? {
let request = NSFetchRequest<CDMessage>(entityName: "CDMessage")
request.fetchLimit = 1
request.predicate = NSPredicate(format: "accountUserId == %@ AND messageId == %@", accountUserId, messageId)
return try context.fetch(request).first
}
}

View File

@ -19,12 +19,14 @@ enum ChatServiceError: LocalizedError {
final class ChatService {
private let client: NetworkClient
private let decoder: JSONDecoder
private let cacheWriter: ChatCacheWriter
init(client: NetworkClient = .shared) {
init(client: NetworkClient = .shared, cacheWriter: ChatCacheWriter = .shared) {
self.client = client
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
self.cacheWriter = cacheWriter
}
func fetchPrivateChats(
@ -42,7 +44,7 @@ final class ChatService {
method: .get,
query: query,
requiresAuth: true
) { [decoder] result in
) { [decoder, weak self] result in
switch result {
case .success(let response):
do {
@ -52,7 +54,9 @@ final class ChatService {
completion(.failure(ChatServiceError.unexpectedStatus(message)))
return
}
completion(.success(apiResponse.data))
let chatData = apiResponse.data
self?.cacheWriter.storePrivateChatList(chatData)
completion(.success(chatData))
} catch {
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
if AppConfig.DEBUG {