1. RDEPUBPageMapReconciliationCoordinator: 在evaluateTakeover、
evaluateSegmentTakeover、evaluateFullPageMapTakeover的每个返回点
增加RDEPUBBackgroundTrace日志,记录关键决策参数:
- currentSpineIndex、candidateChapters/pages、currentChapters/pages
- hasPrev/hasNext、lastBuildableSpineIndex
- jumpSession protected coverage ratio
用于确认fullMap被拒绝的具体原因
2. RDEPUBPresentationRuntime: .keepCurrentWindow分支增加
removePendingPageMapUpdate(at: index),防止被拒绝的
reconcileFullMap update无限期留在pending队列中不被消费。
之前return false不移除,导致:
- 后续commitPendingPageMapUpdateIfNeeded重试时条件不变
仍然keepCurrentWindow,形成无效循环
- 如果用户不再翻页,总页数永远不更新
Co-Authored-By: Claude <noreply@anthropic.com>
241 lines
9.6 KiB
Swift
241 lines
9.6 KiB
Swift
import Foundation
|
|
|
|
enum RDEPUBPageMapTakeoverDecision {
|
|
|
|
case keepCurrentWindow
|
|
|
|
case expandWindow(RDEPUBBackgroundCoverageSegment)
|
|
|
|
case segmentReplace(RDEPUBBackgroundCoverageSegment)
|
|
|
|
case fullReplace(RDEPUBBookPageMap)
|
|
}
|
|
|
|
final class RDEPUBPageMapReconciliationCoordinator {
|
|
|
|
private unowned let context: RDEPUBReaderContext
|
|
|
|
init(context: RDEPUBReaderContext) {
|
|
self.context = context
|
|
}
|
|
|
|
func evaluateTakeover(
|
|
candidatePageMap: RDEPUBBookPageMap?,
|
|
candidateSegment: RDEPUBBackgroundCoverageSegment?,
|
|
currentWindow: RDEPUBBookPageMap?,
|
|
jumpSession: RDEPUBJumpSession?
|
|
) -> RDEPUBPageMapTakeoverDecision {
|
|
|
|
guard let currentWindow else {
|
|
if let candidatePageMap {
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no currentWindow, fullReplace candidateChapters=\(candidatePageMap.totalChapters) candidatePages=\(candidatePageMap.totalPages)")
|
|
return .fullReplace(candidatePageMap)
|
|
}
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no currentWindow, no candidate")
|
|
return .keepCurrentWindow
|
|
}
|
|
|
|
let currentSpineIndex = context.runtime?.locationCoordinator.currentVisibleLocation()
|
|
.flatMap { context.normalizedSpineIndex(for: $0) }
|
|
|
|
let lastBuildableSpineIndex = context.publication?.spine.indices
|
|
.reversed()
|
|
.first(where: { index in
|
|
guard let item = context.publication?.spine[index] else { return false }
|
|
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
|
|
}) ?? 0
|
|
|
|
if let jumpSession {
|
|
let protectedIndices = jumpSession.protectedSpineIndices
|
|
if let currentSpineIndex, protectedIndices.contains(currentSpineIndex) {
|
|
|
|
if let candidateSegment {
|
|
let candidateIndices = candidateSegment.resolvedSpineIndices
|
|
let coverageRatio = Double(protectedIndices.intersection(candidateIndices).count) /
|
|
Double(protectedIndices.count)
|
|
if coverageRatio < 0.8 {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateTakeover: blocked by jumpSession protection coverage=\(String(format: "%.2f", coverageRatio)) currentSpine=\(currentSpineIndex) protected=\(protectedIndices)"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let currentSpineIndex {
|
|
let requiresAdjacentCoverage = currentSpineIndex > 0 && currentSpineIndex < lastBuildableSpineIndex
|
|
|
|
if requiresAdjacentCoverage {
|
|
if let candidateSegment {
|
|
let hasPrev = candidateSegment.contains(spineIndex: currentSpineIndex - 1)
|
|
let hasNext = candidateSegment.contains(spineIndex: currentSpineIndex + 1)
|
|
if !hasPrev || !hasNext {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateTakeover: blocked by missing adjacent coverage (segment) currentSpine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext)"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let candidateSegment {
|
|
let currentRenderSignature = context.currentRenderSignature()
|
|
if candidateSegment.renderSignature != currentRenderSignature {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateTakeover: blocked by render signature mismatch"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
|
|
if let candidateSegment {
|
|
return evaluateSegmentTakeover(
|
|
candidateSegment: candidateSegment,
|
|
currentWindow: currentWindow,
|
|
currentSpineIndex: currentSpineIndex,
|
|
lastBuildableSpineIndex: lastBuildableSpineIndex
|
|
)
|
|
}
|
|
|
|
if let candidatePageMap {
|
|
return evaluateFullPageMapTakeover(
|
|
candidatePageMap: candidatePageMap,
|
|
currentWindow: currentWindow,
|
|
currentSpineIndex: currentSpineIndex,
|
|
lastBuildableSpineIndex: lastBuildableSpineIndex
|
|
)
|
|
}
|
|
|
|
RDEPUBBackgroundTrace.log("Reconciliation", "evaluateTakeover: no candidate, keepCurrentWindow")
|
|
return .keepCurrentWindow
|
|
}
|
|
|
|
private func evaluateSegmentTakeover(
|
|
candidateSegment: RDEPUBBackgroundCoverageSegment,
|
|
currentWindow: RDEPUBBookPageMap,
|
|
currentSpineIndex: Int?,
|
|
lastBuildableSpineIndex: Int
|
|
) -> RDEPUBPageMapTakeoverDecision {
|
|
let currentIndices = Set(currentWindow.entries.map { $0.spineIndex })
|
|
let candidateIndices = candidateSegment.resolvedSpineIndices
|
|
|
|
if let currentSpineIndex {
|
|
if !candidateIndices.contains(currentSpineIndex) {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateSegmentTakeover: keepCurrentWindow — currentSpine=\(currentSpineIndex) not in candidate"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
|
|
if let currentSpineIndex {
|
|
let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0
|
|
let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
|
|
currentSpineIndex == lastBuildableSpineIndex
|
|
if !hasPrev || !hasNext {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateSegmentTakeover: keepCurrentWindow — missing adjacent spine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext)"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
|
|
let isContinuous = currentIndices.contains(candidateSegment.lowerSpineIndex - 1) ||
|
|
currentIndices.contains(candidateSegment.upperSpineIndex + 1) ||
|
|
candidateIndices.contains(currentWindow.entries.first?.spineIndex ?? Int.max) ||
|
|
candidateIndices.contains(currentWindow.entries.last?.spineIndex ?? Int.min)
|
|
|
|
if isContinuous {
|
|
|
|
return .expandWindow(candidateSegment)
|
|
} else {
|
|
|
|
let overlap = currentIndices.intersection(candidateIndices)
|
|
let overlapRatio = Double(overlap.count) / Double(currentIndices.count)
|
|
if overlapRatio > 0.5 {
|
|
|
|
return .segmentReplace(candidateSegment)
|
|
}
|
|
}
|
|
|
|
return .keepCurrentWindow
|
|
}
|
|
|
|
private func evaluateFullPageMapTakeover(
|
|
candidatePageMap: RDEPUBBookPageMap,
|
|
currentWindow: RDEPUBBookPageMap,
|
|
currentSpineIndex: Int?,
|
|
lastBuildableSpineIndex: Int
|
|
) -> RDEPUBPageMapTakeoverDecision {
|
|
let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex })
|
|
let currentEntries = currentWindow.entries.count
|
|
|
|
if let currentSpineIndex {
|
|
if !candidateIndices.contains(currentSpineIndex) {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateFullPageMapTakeover: keepCurrentWindow — currentSpine=\(currentSpineIndex) not in candidate (candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries) candidatePages=\(candidatePageMap.totalPages) currentPages=\(currentWindow.totalPages))"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
|
|
if let currentSpineIndex {
|
|
let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0
|
|
let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
|
|
currentSpineIndex == lastBuildableSpineIndex
|
|
if !hasPrev || !hasNext {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateFullPageMapTakeover: keepCurrentWindow — missing adjacent spine=\(currentSpineIndex) hasPrev=\(hasPrev) hasNext=\(hasNext) lastBuildable=\(lastBuildableSpineIndex)"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
}
|
|
|
|
let isComplete = candidateIndices.count >= currentEntries
|
|
if isComplete {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateFullPageMapTakeover: fullReplace — candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries) candidatePages=\(candidatePageMap.totalPages) currentPages=\(currentWindow.totalPages)"
|
|
)
|
|
return .fullReplace(candidatePageMap)
|
|
}
|
|
|
|
RDEPUBBackgroundTrace.log(
|
|
"Reconciliation",
|
|
"evaluateFullPageMapTakeover: keepCurrentWindow — candidateChapters=\(candidateIndices.count) < currentChapters=\(currentEntries)"
|
|
)
|
|
return .keepCurrentWindow
|
|
}
|
|
|
|
func protectedSpineIndices(
|
|
currentSpineIndex: Int?,
|
|
jumpSession: RDEPUBJumpSession?
|
|
) -> Set<Int> {
|
|
var indices: Set<Int> = []
|
|
|
|
if let currentSpineIndex {
|
|
indices.insert(currentSpineIndex)
|
|
|
|
if currentSpineIndex > 0 {
|
|
indices.insert(currentSpineIndex - 1)
|
|
}
|
|
indices.insert(currentSpineIndex + 1)
|
|
}
|
|
|
|
if let jumpSession {
|
|
indices.formUnion(jumpSession.protectedSpineIndices)
|
|
}
|
|
|
|
return indices
|
|
}
|
|
}
|