11import { StateStore } from '../store' ;
22import { debounce , type DebouncedFunc } from '../utils' ;
33
4- type PaginationDirection = 'next' | 'prev' ;
5- type Cursor = { next : string | null ; prev : string | null } ;
6- export type PaginationQueryParams = { direction : PaginationDirection } ;
4+ type PaginationDirection = PaginationDirectionNext | PaginationDirectionPrev ;
5+ type PaginationDirectionPrev = 'prev' ;
6+ type PaginationDirectionNext = 'next' ;
7+ type Cursor = { next ?: string ; prev ?: string } ;
8+ export type PaginationQueryParams =
9+ | { direction : PaginationDirectionNext ; next ?: Cursor [ 'next' ] ; offset ?: number }
10+ | { direction : PaginationDirectionPrev ; prev ?: Cursor [ 'prev' ] ; offset ?: number } ;
711export type PaginationQueryReturnValue < T > = { items : T [ ] } & {
812 next ?: string ;
913 prev ?: string ;
@@ -24,6 +28,7 @@ export type PaginatorState<T = any> = {
2428 lastQueryError ?: Error ;
2529 cursor ?: Cursor ;
2630 offset ?: number ;
31+ isStateValid : boolean ;
2732} ;
2833
2934export type PaginatorOptions = {
@@ -40,12 +45,16 @@ export abstract class BasePaginator<T> {
4045 state : StateStore < PaginatorState < T > > ;
4146 pageSize : number ;
4247 protected _executeQueryDebounced ! : DebouncedExecQueryFunction ;
43- protected _isCursorPagination = false ;
48+ // in cases where particular combination of filters would return only one item, the cursors
49+ // (`next`/`prev`) won't be included in the response - in such cases it's better to build
50+ // BasePaginator inheritors with this value already pre-defined
51+ protected abstract _isCursorPagination : boolean ;
4452
4553 protected constructor ( options ?: PaginatorOptions ) {
4654 const { debounceMs, pageSize } = { ...DEFAULT_PAGINATION_OPTIONS , ...options } ;
4755 this . pageSize = pageSize ;
4856 this . state = new StateStore < PaginatorState < T > > ( this . initialState ) ;
57+
4958 this . setDebounceOptions ( { debounceMs } ) ;
5059 }
5160
@@ -78,13 +87,18 @@ export abstract class BasePaginator<T> {
7887 lastQueryError : undefined ,
7988 cursor : undefined ,
8089 offset : 0 ,
90+ isStateValid : true ,
8191 } ;
8292 }
8393
8494 get items ( ) {
8595 return this . state . getLatestValue ( ) . items ;
8696 }
8797
98+ get isStateValid ( ) {
99+ return this . state . getLatestValue ( ) . isStateValid ;
100+ }
101+
88102 get cursor ( ) {
89103 return this . state . getLatestValue ( ) . cursor ;
90104 }
@@ -102,8 +116,10 @@ export abstract class BasePaginator<T> {
102116 } ;
103117
104118 canExecuteQuery = ( direction : PaginationDirection ) =>
105- ( ! this . isLoading && direction === 'next' && this . hasNext ) ||
106- ( direction === 'prev' && this . hasPrev ) ;
119+ ! this . isLoading &&
120+ ( ( direction === 'next' && this . hasNext ) ||
121+ ( direction === 'prev' && this . hasPrev ) ||
122+ ! this . isStateValid ) ;
107123
108124 protected getStateBeforeFirstQuery ( ) : PaginatorState < T > {
109125 return {
@@ -124,30 +140,45 @@ export abstract class BasePaginator<T> {
124140 isLoading : false ,
125141 items : isFirstPage
126142 ? stateUpdate . items
127- : [ ...( this . items ?? [ ] ) , ...( stateUpdate . items || [ ] ) ] ,
143+ : [ ...( current . items ?? [ ] ) , ...( stateUpdate . items ?? [ ] ) ] ,
128144 } ;
129145 }
130146
131147 async executeQuery ( { direction } : { direction : PaginationDirection } ) {
132148 if ( ! this . canExecuteQuery ( direction ) ) return ;
133- const isFirstPage = typeof this . items === 'undefined' ;
134- if ( isFirstPage ) {
135- this . state . next ( this . getStateBeforeFirstQuery ( ) ) ;
136- } else {
137- this . state . partialNext ( { isLoading : true } ) ;
138- }
139149
140- const stateUpdate : Partial < PaginatorState < T > > = { } ;
150+ const isFirstPage = typeof this . items === 'undefined' || ! this . isStateValid ;
151+
152+ this . state . partialNext ( { isLoading : true } ) ;
153+
154+ const stateUpdate : Partial < PaginatorState < T > > = isFirstPage
155+ ? this . getStateBeforeFirstQuery ( )
156+ : { } ;
157+
141158 try {
142- const results = await this . query ( { direction } ) ;
159+ const queryParams : PaginationQueryParams = { direction } ;
160+
161+ if ( ! isFirstPage ) {
162+ if ( this . _isCursorPagination ) {
163+ // @ts -expect-error this is perfectly valid
164+ queryParams [ queryParams . direction ] = this . cursor ?. [ queryParams . direction ] ;
165+ } else {
166+ queryParams [ 'offset' ] = this . offset ;
167+ }
168+ }
169+
170+ const results = await this . query ( queryParams ) ;
171+
143172 if ( ! results ) return ;
173+
144174 const { items, next, prev } = results ;
175+
145176 if ( isFirstPage && ( next || prev ) ) {
146177 this . _isCursorPagination = true ;
147178 }
148179
149180 if ( this . _isCursorPagination ) {
150- stateUpdate . cursor = { next : next || null , prev : prev || null } ;
181+ stateUpdate . cursor = { next, prev } ;
151182 stateUpdate . hasNext = ! ! next ;
152183 stateUpdate . hasPrev = ! ! prev ;
153184 } else {
@@ -156,8 +187,8 @@ export abstract class BasePaginator<T> {
156187 }
157188
158189 stateUpdate . items = await this . filterQueryResults ( items ) ;
159- } catch ( e ) {
160- stateUpdate . lastQueryError = e as Error ;
190+ } catch ( error ) {
191+ stateUpdate . lastQueryError = error as Error ;
161192 } finally {
162193 this . state . next ( this . getStateAfterQuery ( stateUpdate , isFirstPage ) ) ;
163194 }
@@ -181,4 +212,8 @@ export abstract class BasePaginator<T> {
181212 prevDebounced = ( ) => {
182213 this . _executeQueryDebounced ( { direction : 'prev' } ) ;
183214 } ;
215+
216+ invalidate = ( ) => {
217+ this . state . partialNext ( { isStateValid : false } ) ;
218+ } ;
184219}
0 commit comments