mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention. Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main. Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
84 lines
2.9 KiB
Swift
84 lines
2.9 KiB
Swift
import HapiProtocol
|
|
|
|
/// One pagination run includes its first rendered layout, not just the HTTP
|
|
/// request. Geometry may submit demand repeatedly; only an idle run starts.
|
|
/// Owned by the chat's main-thread coordinator. No timers or UI dependencies.
|
|
public struct ChatHistoryPagingState: Equatable, Sendable {
|
|
public enum Phase: Equatable, Sendable {
|
|
case idle, loading, awaitingLayout(Int), retrying, failed, paused, exhausted
|
|
}
|
|
|
|
public private(set) var phase: Phase = .idle
|
|
public private(set) var generation = 0
|
|
public private(set) var failures = 0
|
|
public private(set) var pagesWithoutProgress = 0
|
|
|
|
public init() {}
|
|
|
|
public mutating func begin() -> Int? {
|
|
guard phase == .idle else { return nil }
|
|
generation += 1
|
|
phase = .loading
|
|
return generation
|
|
}
|
|
|
|
/// Returns the bounded retry delay, in milliseconds, if one is needed.
|
|
@discardableResult
|
|
public mutating func received(_ result: OlderLoadOutcome, generation: Int) -> Int? {
|
|
guard self.generation == generation, phase == .loading else { return nil }
|
|
switch result {
|
|
case .applied(let version, _, _):
|
|
phase = .awaitingLayout(version)
|
|
failures = 0
|
|
case .failed:
|
|
failures += 1
|
|
phase = failures <= 2 ? .retrying : .failed
|
|
return failures == 1 ? 500 : failures == 2 ? 1500 : nil
|
|
case .stopped(let reason):
|
|
switch reason {
|
|
case .exhausted: phase = .exhausted
|
|
case .cursorDidNotAdvance: phase = .paused
|
|
default: phase = .idle
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@discardableResult
|
|
public mutating func retryElapsed(generation: Int) -> Bool {
|
|
guard self.generation == generation, phase == .retrying else { return false }
|
|
phase = .idle
|
|
return true
|
|
}
|
|
|
|
/// Also called for pages with no new cells: their version still needs an
|
|
/// acknowledgement, otherwise hidden-only pages deadlock the loader.
|
|
@discardableResult
|
|
public mutating func laidOut(historyVersion: Int, madeProgress: Bool) -> Bool {
|
|
guard case .awaitingLayout(let target) = phase, historyVersion >= target else { return false }
|
|
pagesWithoutProgress = madeProgress ? 0 : pagesWithoutProgress + 1
|
|
phase = pagesWithoutProgress >= 3 ? .paused : .idle
|
|
return true
|
|
}
|
|
|
|
public mutating func resume() {
|
|
guard phase == .failed || phase == .paused else { return }
|
|
failures = 0
|
|
pagesWithoutProgress = 0
|
|
phase = .idle
|
|
}
|
|
|
|
/// Tail-window trimming can make older pages available again. Errors and
|
|
/// no-progress pauses still require an explicit reader action.
|
|
public mutating func refreshAvailability(hasMore: Bool) {
|
|
if hasMore, phase == .exhausted { phase = .idle }
|
|
}
|
|
|
|
public mutating func cancel() {
|
|
generation += 1
|
|
phase = .idle
|
|
failures = 0
|
|
pagesWithoutProgress = 0
|
|
}
|
|
}
|