macsync/@packages/imessage/Sources/IMessageSync/ChunkingStrategy.swift

119 lines
4.4 KiB
Swift

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<T>(
_ 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
}
}