@contember/bindx-dataview 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { PagingStateResult } from './useDataViewState.js'
3
+
4
+ export interface PagingTotalSource {
5
+ /** Total reported by the data source, or null when it reports none. */
6
+ readonly countedTotal: number | null
7
+ /** Identifies the counted row set (the filter); bounds learned for another row set are dropped. */
8
+ readonly countKey: string
9
+ /** Whether the loaded rows answer the current query: filter, order, limit and offset. */
10
+ readonly isPageCurrent: boolean
11
+ readonly pageItemCount: number
12
+ }
13
+
14
+ /** What the loaded pages prove about the total: `atLeast <= total <= atMost`. */
15
+ interface TotalBounds {
16
+ readonly atLeast: number
17
+ readonly atMost: number | null
18
+ }
19
+
20
+ interface LoadedPage {
21
+ readonly offset: number
22
+ readonly limit: number
23
+ readonly itemCount: number
24
+ }
25
+
26
+ type PageOutcome =
27
+ | { readonly type: 'total'; readonly totalCount: number }
28
+ | { readonly type: 'probe'; readonly bounds: TotalBounds; readonly pageIndex: number }
29
+ | { readonly type: 'unknown'; readonly bounds: TotalBounds }
30
+
31
+ const NO_BOUNDS: TotalBounds = { atLeast: 0, atMost: null }
32
+
33
+ /**
34
+ * Feeds the paging state its total: the counted total when the data source reports one,
35
+ * otherwise the total the loaded pages prove. Only a page that answers the current query counts.
36
+ *
37
+ * A page shorter than the limit ends the list. An empty page past offset 0 only bounds the total
38
+ * from above, so the grid bisects between the bounds, one page per query, until a page ends the list.
39
+ */
40
+ export function usePagingTotalCount(paging: PagingStateResult, source: PagingTotalSource): void {
41
+ const { setTotalCount, goTo, queryLimit, queryOffset } = paging
42
+ const { countedTotal, countKey, isPageCurrent, pageItemCount } = source
43
+ const boundsRef = useRef<{ countKey: string; bounds: TotalBounds }>({ countKey, bounds: NO_BOUNDS })
44
+
45
+ useEffect(() => {
46
+ if (boundsRef.current.countKey !== countKey || countedTotal !== null) {
47
+ boundsRef.current = { countKey, bounds: NO_BOUNDS }
48
+ }
49
+ if (countedTotal !== null) {
50
+ setTotalCount(countedTotal)
51
+ return
52
+ }
53
+ if (!isPageCurrent || queryLimit === undefined || queryOffset === undefined) {
54
+ return
55
+ }
56
+
57
+ const outcome = inferTotalFromPage(boundsRef.current.bounds, { offset: queryOffset, limit: queryLimit, itemCount: pageItemCount })
58
+ if (outcome.type === 'total') {
59
+ boundsRef.current = { countKey, bounds: NO_BOUNDS }
60
+ setTotalCount(outcome.totalCount)
61
+ return
62
+ }
63
+ boundsRef.current = { countKey, bounds: outcome.bounds }
64
+ if (outcome.type === 'probe') {
65
+ goTo(outcome.pageIndex)
66
+ }
67
+ }, [countedTotal, countKey, isPageCurrent, pageItemCount, queryLimit, queryOffset, setTotalCount, goTo])
68
+ }
69
+
70
+ function inferTotalFromPage(bounds: TotalBounds, { offset, limit, itemCount }: LoadedPage): PageOutcome {
71
+ const isEmptyPastStart = itemCount === 0 && offset > 0
72
+ if (itemCount < limit && !isEmptyPastStart) {
73
+ return { type: 'total', totalCount: offset + itemCount }
74
+ }
75
+
76
+ const narrowed: TotalBounds = isEmptyPastStart
77
+ ? { atLeast: bounds.atLeast, atMost: Math.min(bounds.atMost ?? offset, offset) }
78
+ : { atLeast: Math.max(bounds.atLeast, offset + limit), atMost: bounds.atMost }
79
+
80
+ if (narrowed.atMost === null) {
81
+ return { type: 'unknown', bounds: narrowed }
82
+ }
83
+ if (narrowed.atLeast >= narrowed.atMost) {
84
+ return { type: 'total', totalCount: narrowed.atMost }
85
+ }
86
+ // The page holding the middle row starts below `atMost` and ends above `atLeast`,
87
+ // so whatever it returns halves the interval.
88
+ const middleRow = Math.floor((narrowed.atLeast + narrowed.atMost) / 2)
89
+ return { type: 'probe', bounds: narrowed, pageIndex: Math.floor(middleRow / limit) }
90
+ }