import Foundation import Observation /// Accumulated pages of one list command (`repo list`, `issue list`, /// `mr list`). Cursors are opaque and server-minted; the model only /// hands back what `next` carried. @Observable @MainActor final class PagedListModel { private(set) var state: LoadState<[Element]> = .loading private(set) var isLoadingMore = false private var next: String? var hasMore: Bool { next != nil } private let client: GitbayClient private let pageSize: Int /// The list command minus paging flags. Set by the owning view model /// when a filter changes; a change only takes effect via `reload()`. var argv: [String] var emptyMessage: String init( client: GitbayClient, argv: [String], emptyMessage: String, pageSize: Int = 50 ) { self.client = client self.argv = argv self.emptyMessage = emptyMessage self.pageSize = pageSize } func reload() async { state = .loading next = nil await fetch(cursor: nil, appendingTo: []) } /// Fetch the next page. Safe to call repeatedly from row-appear /// triggers; it no-ops while a fetch is in flight or at the end. func loadMore() async { guard let cursor = next, !isLoadingMore, let loaded = state.value else { return } isLoadingMore = true defer { isLoadingMore = false } await fetch(cursor: cursor, appendingTo: loaded) } private func fetch(cursor: String?, appendingTo existing: [Element]) async { do { let page = try await client.readPage( argv, of: Element.self, limit: pageSize, cursor: cursor ) next = page.next let all = existing + page.items state = all.isEmpty ? .empty(emptyMessage) : .loaded(all) } catch { // A failed first page is the screen's state; a failed later // page keeps what is on screen and leaves the cursor for a // retry from the same trigger. if existing.isEmpty { state = .from(error) } } } }