import Foundation struct RDEPUBBackgroundPriorityPolicy { let hotRadius: Int let warmRadius: Int let maxWarmJumpAnchors: Int let coldLaneShare: Double static let `default` = RDEPUBBackgroundPriorityPolicy( hotRadius: 24, warmRadius: 96, maxWarmJumpAnchors: 2, coldLaneShare: 0.15 ) static func adaptive(totalBuildableChapters: Int) -> RDEPUBBackgroundPriorityPolicy { let hotRadius = min(max(12, Int(sqrt(Double(totalBuildableChapters)))), 48) let warmRadius = min(max(hotRadius * 3, 32), 192) return RDEPUBBackgroundPriorityPolicy( hotRadius: hotRadius, warmRadius: warmRadius, maxWarmJumpAnchors: 2, coldLaneShare: 0.15 ) } } enum RDEPUBPriorityBand: Int, Comparable { case hot = 0 case warmPrimary = 1 case warmSecondary = 2 case cold = 3 static func < (lhs: RDEPUBPriorityBand, rhs: RDEPUBPriorityBand) -> Bool { lhs.rawValue < rhs.rawValue } } struct RDEPUBWarmJumpAnchor { let spineIndex: Int let timestamp: CFAbsoluteTime let sequenceNumber: Int } struct RDEPUBMetadataParseWorkItem { let spineIndex: Int let generation: Int let priorityBand: RDEPUBPriorityBand var sortKey: (bandRank: Int, distanceToCurrent: Int, distanceToNewestJump: Int, spineIndex: Int) { (priorityBand.rawValue, 0, 0, spineIndex) } } final class RDEPUBBackgroundPriorityManager { private unowned let context: RDEPUBReaderContext private(set) var policy: RDEPUBBackgroundPriorityPolicy private var warmAnchors: [RDEPUBWarmJumpAnchor] = [] private(set) var currentGeneration: Int = 0 private var coldCursor: Int = 0 init(context: RDEPUBReaderContext) { self.context = context self.policy = .default } func updatePolicy(_ newPolicy: RDEPUBBackgroundPriorityPolicy) { policy = newPolicy } func addWarmAnchor(spineIndex: Int) { let anchor = RDEPUBWarmJumpAnchor( spineIndex: spineIndex, timestamp: CFAbsoluteTimeGetCurrent(), sequenceNumber: currentGeneration ) warmAnchors.insert(anchor, at: 0) if warmAnchors.count > policy.maxWarmJumpAnchors { warmAnchors = Array(warmAnchors.prefix(policy.maxWarmJumpAnchors)) } currentGeneration += 1 coldCursor = 0 RDEPUBBackgroundTrace.log( "PriorityManager", "addWarmAnchor spine=\(spineIndex) generation=\(currentGeneration) anchors=\(warmAnchors.count)" ) } func makeMetadataPriorityOrder( allBuildableIndices: [Int], currentSpineIndex: Int?, cachedSpineIndices: Set ) -> [Int] { let uncachedIndices = allBuildableIndices.filter { !cachedSpineIndices.contains($0) } guard !uncachedIndices.isEmpty else { return [] } let items = uncachedIndices.map { spineIndex -> (spineIndex: Int, band: RDEPUBPriorityBand) in let band = classifySpineIndex( spineIndex: spineIndex, currentSpineIndex: currentSpineIndex ) return (spineIndex, band) } let sorted = items.sorted { lhs, rhs in if lhs.band != rhs.band { return lhs.band < rhs.band } let lhsDistanceToCurrent = currentSpineIndex.map { abs(lhs.spineIndex - $0) } ?? Int.max let rhsDistanceToCurrent = currentSpineIndex.map { abs(rhs.spineIndex - $0) } ?? Int.max if lhsDistanceToCurrent != rhsDistanceToCurrent { return lhsDistanceToCurrent < rhsDistanceToCurrent } return lhs.spineIndex < rhs.spineIndex } return sorted.map { $0.spineIndex } } private func classifySpineIndex( spineIndex: Int, currentSpineIndex: Int? ) -> RDEPUBPriorityBand { if let current = currentSpineIndex { let distance = abs(spineIndex - current) if distance <= policy.hotRadius { return .hot } } for (index, anchor) in warmAnchors.enumerated() { let distance = abs(spineIndex - anchor.spineIndex) if distance <= policy.warmRadius { return index == 0 ? .warmPrimary : .warmSecondary } } return .cold } func currentWarmAnchors() -> [RDEPUBWarmJumpAnchor] { warmAnchors } func reset() { warmAnchors.removeAll() currentGeneration = 0 coldCursor = 0 } }