add cache
This commit is contained in:
parent
6fd82e25c1
commit
7c8940da5b
233
yobble/CoreData/ChatCacheWriter.swift
Normal file
233
yobble/CoreData/ChatCacheWriter.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,12 +19,14 @@ enum ChatServiceError: LocalizedError {
|
|||||||
final class ChatService {
|
final class ChatService {
|
||||||
private let client: NetworkClient
|
private let client: NetworkClient
|
||||||
private let decoder: JSONDecoder
|
private let decoder: JSONDecoder
|
||||||
|
private let cacheWriter: ChatCacheWriter
|
||||||
|
|
||||||
init(client: NetworkClient = .shared) {
|
init(client: NetworkClient = .shared, cacheWriter: ChatCacheWriter = .shared) {
|
||||||
self.client = client
|
self.client = client
|
||||||
self.decoder = JSONDecoder()
|
self.decoder = JSONDecoder()
|
||||||
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
|
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
|
||||||
|
self.cacheWriter = cacheWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPrivateChats(
|
func fetchPrivateChats(
|
||||||
@ -42,7 +44,7 @@ final class ChatService {
|
|||||||
method: .get,
|
method: .get,
|
||||||
query: query,
|
query: query,
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
) { [decoder] result in
|
) { [decoder, weak self] result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
do {
|
do {
|
||||||
@ -52,7 +54,9 @@ final class ChatService {
|
|||||||
completion(.failure(ChatServiceError.unexpectedStatus(message)))
|
completion(.failure(ChatServiceError.unexpectedStatus(message)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion(.success(apiResponse.data))
|
let chatData = apiResponse.data
|
||||||
|
self?.cacheWriter.storePrivateChatList(chatData)
|
||||||
|
completion(.success(chatData))
|
||||||
} catch {
|
} catch {
|
||||||
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
||||||
if AppConfig.DEBUG {
|
if AppConfig.DEBUG {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user