mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
fix: settle merged pagination and drop duplicate rows (#47)
This commit is contained in:
commit
8e7f99b6e3
4 changed files with 35 additions and 4 deletions
|
|
@ -15,6 +15,13 @@ final class PaginationState<Item> {
|
|||
let pageSize: Int
|
||||
var cacheName: String?
|
||||
|
||||
/// Optional key extractor enabling cross-page de-duplication. Merged views set
|
||||
/// this because they combine paginated results from several instances, where
|
||||
/// the same item can resurface on a later page (overlapping involvement
|
||||
/// queries) and the merged page size no longer matches `pageSize`.
|
||||
@ObservationIgnored var dedupeKey: ((Item) -> AnyHashable)?
|
||||
@ObservationIgnored private var seenKeys = Set<AnyHashable>()
|
||||
|
||||
init(pageSize: Int = 20) {
|
||||
self.pageSize = pageSize
|
||||
}
|
||||
|
|
@ -42,8 +49,10 @@ final class PaginationState<Item> {
|
|||
do {
|
||||
let fetched = try await fetch(1, pageSize)
|
||||
guard !Task.isCancelled else { return }
|
||||
items = fetched
|
||||
hasMore = fetched.count >= pageSize
|
||||
if dedupeKey != nil { seenKeys.removeAll() }
|
||||
let newItems = deduped(fetched)
|
||||
items = newItems
|
||||
hasMore = computeHasMore(fetched: fetched, newItems: newItems)
|
||||
currentPage = 2
|
||||
hasLoaded = true
|
||||
} catch is CancellationError {
|
||||
|
|
@ -72,8 +81,9 @@ final class PaginationState<Item> {
|
|||
do {
|
||||
let fetched = try await fetch(currentPage, pageSize)
|
||||
guard !Task.isCancelled else { return }
|
||||
items.append(contentsOf: fetched)
|
||||
hasMore = fetched.count >= pageSize
|
||||
let newItems = deduped(fetched)
|
||||
items.append(contentsOf: newItems)
|
||||
hasMore = computeHasMore(fetched: fetched, newItems: newItems)
|
||||
self.currentPage = currentPage + 1
|
||||
} catch is CancellationError {
|
||||
// Ignore cancellation
|
||||
|
|
@ -95,6 +105,24 @@ final class PaginationState<Item> {
|
|||
func invalidate() {
|
||||
hasLoaded = false
|
||||
}
|
||||
|
||||
/// Filters out items already seen on a previous page when `dedupeKey` is set,
|
||||
/// recording the new keys. Without a `dedupeKey` the page passes through
|
||||
/// unchanged so single-source pagination keeps its existing behavior.
|
||||
private func deduped(_ fetched: [Item]) -> [Item] {
|
||||
guard let dedupeKey else { return fetched }
|
||||
return fetched.filter { seenKeys.insert(dedupeKey($0)).inserted }
|
||||
}
|
||||
|
||||
/// For merged sources the de-duplicated, multi-instance page size no longer
|
||||
/// matches `pageSize`, so "more pages exist" means "this page contributed new
|
||||
/// items." Single sources keep the page-size heuristic.
|
||||
private func computeHasMore(fetched: [Item], newItems: [Item]) -> Bool {
|
||||
if dedupeKey != nil {
|
||||
return !newItems.isEmpty
|
||||
}
|
||||
return fetched.count >= pageSize
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Disk cache
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct MergedNotificationsOverviewView: View {
|
|||
self.manager = manager
|
||||
let state = PaginationState<TaggedItem<NotificationThread>>()
|
||||
state.cacheName = "merged_notifications"
|
||||
state.dedupeKey = { $0.id }
|
||||
state.loadFromCache()
|
||||
state.rehydrate(from: manager)
|
||||
_pagination = State(initialValue: state)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct MergedRepositoryListView: View {
|
|||
self.manager = manager
|
||||
let state = PaginationState<TaggedItem<Repository>>()
|
||||
state.cacheName = "merged_repos"
|
||||
state.dedupeKey = { $0.id }
|
||||
state.loadFromCache()
|
||||
state.rehydrate(from: manager)
|
||||
_pagination = State(initialValue: state)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ struct MergedSearchableOverviewView<Row: View, Detail: View, CreateView: View>:
|
|||
|
||||
let state = PaginationState<TaggedItem<Issue>>()
|
||||
state.cacheName = "merged_\(config.issueType)"
|
||||
state.dedupeKey = { $0.id }
|
||||
state.loadFromCache()
|
||||
state.rehydrate(from: manager)
|
||||
_pagination = State(initialValue: state)
|
||||
|
|
|
|||
Loading…
Reference in a new issue