Files
hapi/ios/Packages/HapiKit/Sources/HapiClient/Stores/MessageWindowControllers.swift
T
weishu c2e3d16b7e feat(native): improve anchored chat scrolling and history loading
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.
2026-09-10 16:22:36 +08:00

63 lines
2.5 KiB
Swift

import Foundation
import HapiProtocol
/// Per-hub registry of per-session ``MessageWindowController``s — the
/// analogue of the web module's session-keyed maps and the Android port's
/// `MessageWindowStores`. ``open(sessionId:)`` hydrates a cold session from
/// its disk snapshot (interrupted `sending` rows restore, stale snapshots
/// start flagged for a latest reset); ``seed(fromSessionId:toSessionId:)``
/// carries a window across a resume/reopen id change. Snapshot files
/// themselves are LRU-capped by ``WindowSnapshotStore``.
public actor MessageWindowControllers {
private let provider: any MessagesProviding
private let snapshots: WindowSnapshotStore?
private let historyRetentionLimit: Int
private var controllers: [String: MessageWindowController] = [:]
public init(provider: any MessagesProviding, snapshots: WindowSnapshotStore? = nil,
historyRetentionLimit: Int = MessageWindowConstants.historyWindowSize) {
self.provider = provider
self.snapshots = snapshots
self.historyRetentionLimit = historyRetentionLimit
}
/// The session's controller, hydrating from its snapshot on first open.
public func open(sessionId: String) -> MessageWindowController {
if let existing = controllers[sessionId] {
return existing
}
let hydrated = snapshots?.load(sessionId: sessionId).map {
MessageWindowLogic.hydrate(sessionId: sessionId, persisted: $0)
}
let controller = MessageWindowController(
sessionId: sessionId,
provider: provider,
snapshots: snapshots,
initialState: hydrated,
historyRetentionLimit: historyRetentionLimit
)
controllers[sessionId] = controller
return controller
}
/// Already-open controller, if any (no hydration).
public func peek(sessionId: String) -> MessageWindowController? {
controllers[sessionId]
}
/// Web `seedMessageWindowFromSession` for resume/reopen id migration.
public func seed(fromSessionId: String, toSessionId: String) async {
guard !fromSessionId.isEmpty, !toSessionId.isEmpty, fromSessionId != toSessionId else {
return
}
let source = open(sessionId: fromSessionId)
let target = open(sessionId: toSessionId)
await target.seedFrom(source)
}
/// Web `clearMessageWindow` (e.g. on `session-removed`).
public func clear(sessionId: String) async {
await controllers[sessionId]?.clear()
}
}