import Foundation /// Adaptive chunking logic for sync payloads enum ChunkingStrategy { /// Maximum payload size target in bytes (100MB — server allows 500MB, nginx unlimited) static let maxChunkSizeBytes = 100_000_000 /// Base overhead for conversation wrapper JSON structure static let baseOverhead = 200 /// Estimate the JSON payload size for a single message static func estimateMessageSize(_ msg: iMessage) -> Int { var size = 500 // Base overhead for JSON structure and fixed fields if let text = msg.text { size += text.utf8.count } if let attributedBody = msg.attributedBody { size += attributedBody.utf8.count } for attachment in msg.attachments { size += 100 if let data = attachment.data { size += data.utf8.count } if let filename = attachment.filename { size += filename.utf8.count } if let mimeType = attachment.mimeType { size += mimeType.utf8.count } if let transferName = attachment.transferName { size += transferName.utf8.count } } if let styleId = msg.expressiveSendStyleId { size += styleId.utf8.count } if let guid = msg.associatedMessageGuid { size += guid.utf8.count } if let replyGuid = msg.replyToGuid { size += replyGuid.utf8.count } if let originGuid = msg.threadOriginatorGuid { size += originGuid.utf8.count } if let groupTitle = msg.groupTitle { size += groupTitle.utf8.count } if let bundleId = msg.balloonBundleId { size += bundleId.utf8.count } if let senderName = msg.senderDisplayName { size += senderName.utf8.count } return size } /// Maximum number of messages per batch request during initial sync. /// Configurable via config.json key `imessageMaxMessagesPerBatch`. static var maxMessagesPerBatch: Int { let v = UserDefaults.standard.integer(forKey: "imessageMaxMessagesPerBatch") return v > 0 ? v : 300 } /// Maximum number of conversations per batch request during initial sync. /// Configurable via config.json key `imessageMaxConversationsPerBatch`. static var maxConversationsPerBatch: Int { let v = UserDefaults.standard.integer(forKey: "imessageMaxConversationsPerBatch") return v > 0 ? v : 5 } /// Chunk a full list of conversation payloads into batches that stay /// under `maxMessagesPerBatch` and `maxConversationsPerBatch`. /// Each returned group is suitable for one `syncMessagesBatch` call. static func chunkConversationPayloads( _ payloads: [T], messageCount: (T) -> Int ) -> [[T]] { guard !payloads.isEmpty else { return [] } var chunks: [[T]] = [] var current: [T] = [] var currentMessages = 0 for p in payloads { let msgs = messageCount(p) let wouldExceed = currentMessages + msgs > maxMessagesPerBatch || current.count + 1 > maxConversationsPerBatch if wouldExceed && !current.isEmpty { chunks.append(current) current = [] currentMessages = 0 } current.append(p) currentMessages += msgs } if !current.isEmpty { chunks.append(current) } return chunks } /// Create adaptive chunks based on payload size estimation static func createAdaptiveChunks(_ messages: [iMessage]) -> [[iMessage]] { guard !messages.isEmpty else { return [] } var chunks: [[iMessage]] = [] var currentChunk: [iMessage] = [] var currentSize = baseOverhead for msg in messages { let msgSize = estimateMessageSize(msg) if msgSize > maxChunkSizeBytes { if !currentChunk.isEmpty { chunks.append(currentChunk) currentChunk = [] currentSize = baseOverhead } chunks.append([msg]) continue } if currentSize + msgSize > maxChunkSizeBytes && !currentChunk.isEmpty { chunks.append(currentChunk) currentChunk = [] currentSize = baseOverhead } currentChunk.append(msg) currentSize += msgSize } if !currentChunk.isEmpty { chunks.append(currentChunk) } return chunks } }