@@ -11,12 +11,24 @@ type CategorySearchItem = {
1111 hasMore : boolean
1212}
1313
14+ /**
15+ * Runs a unified search across categories in priority order, blocking
16+ * lower-priority results until their predecessors arrive or a timer reveals them.
17+ */
1418export class UnifiedSearchController {
1519 private searchItems : Record < string , CategorySearchItem > = { }
1620 private requestId : number = 0
1721 private revealTimer : ReturnType < typeof setTimeout > | null = null
18-
22+ private searchAbortHandlers : ( ( ) => void ) [ ] = [ ]
23+
24+ /**
25+ * Start a search. Cancels and replaces any search already in flight.
26+ *
27+ * @param query the search term
28+ * @param categories category ids in priority order
29+ */
1930 search ( query : string , categories : string [ ] ) : void {
31+ this . cancelPendingRequests ( )
2032 this . searchItems = { }
2133 this . requestId ++
2234 const dispatchId = this . requestId
@@ -30,12 +42,14 @@ export class UnifiedSearchController {
3042 cursor : null ,
3143 hasMore : false ,
3244 }
33- const { request } = unifiedSearch ( {
45+ const { request, cancel } = unifiedSearch ( {
3446 type : category ,
3547 query,
3648 cursor : null ,
3749 } )
3850
51+ this . searchAbortHandlers . push ( cancel )
52+
3953 request ( ) . then ( ( response ) => {
4054 if ( this . requestId !== dispatchId ) {
4155 // A new search has been started, ignore this result
@@ -66,7 +80,24 @@ export class UnifiedSearchController {
6680 } )
6781 }
6882
69- reconcileCategoryStatuses ( categories : string [ ] ) : void {
83+ /**
84+ * A shallow copy of the current per-category state, safe to read for rendering.
85+ *
86+ * @return the current search items keyed by category id
87+ */
88+ getSnapshot ( ) : Record < string , CategorySearchItem > {
89+ return { ...this . searchItems }
90+ }
91+
92+ /**
93+ * Tear down on unmount: cancels in-flight requests and stops the reveal timer.
94+ */
95+ dispose ( ) : void {
96+ this . cancelPendingRequests ( )
97+ this . stopRevealTimer ( )
98+ }
99+
100+ private reconcileCategoryStatuses ( categories : string [ ] ) : void {
70101 categories . forEach ( ( category ) => {
71102 if ( [ 'loading' , 'failed' ] . includes ( this . searchItems [ category ] . status ) ) {
72103 return
@@ -75,15 +106,39 @@ export class UnifiedSearchController {
75106 } )
76107 }
77108
78- unblockAllCategories ( categories : string [ ] ) : void {
109+ private startRevealTimer ( ) : void {
110+ this . stopRevealTimer ( )
111+ this . revealTimer = setTimeout ( ( ) => {
112+ const categories = Object . keys ( this . searchItems )
113+ const hasPendingCategories = categories . some ( ( category ) => [ 'loading' , 'blocked' ] . includes ( this . searchItems [ category ] . status ) )
114+ this . unblockAllCategories ( categories )
115+ if ( hasPendingCategories ) {
116+ this . startRevealTimer ( )
117+ }
118+ } , REVEAL_INTERVAL )
119+ }
120+
121+ private stopRevealTimer ( ) : void {
122+ if ( this . revealTimer ) {
123+ clearTimeout ( this . revealTimer )
124+ this . revealTimer = null
125+ }
126+ }
127+
128+ private cancelPendingRequests ( ) : void {
129+ this . searchAbortHandlers . forEach ( ( cancel ) => cancel ( ) )
130+ this . searchAbortHandlers = [ ]
131+ }
132+
133+ private unblockAllCategories ( categories : string [ ] ) : void {
79134 categories . forEach ( ( category ) => {
80135 if ( this . searchItems [ category ] . status === 'blocked' ) {
81136 this . searchItems [ category ] . status = 'loaded'
82137 }
83138 } )
84139 }
85140
86- categoryShouldBlock ( category : string , categories : string [ ] ) : boolean {
141+ private categoryShouldBlock ( category : string , categories : string [ ] ) : boolean {
87142 const categoryItem = this . searchItems [ category ]
88143 if ( ! categoryItem ) {
89144 return false
@@ -94,22 +149,4 @@ export class UnifiedSearchController {
94149 return item && [ 'loading' , 'blocked' ] . includes ( item . status )
95150 } )
96151 }
97-
98- startRevealTimer ( ) : void {
99- if ( this . revealTimer ) {
100- clearTimeout ( this . revealTimer )
101- }
102- this . revealTimer = setTimeout ( ( ) => {
103- const categories = Object . keys ( this . searchItems )
104- const hasPendingCategories = categories . some ( ( category ) => [ 'loading' , 'blocked' ] . includes ( this . searchItems [ category ] . status ) )
105- this . unblockAllCategories ( categories )
106- if ( hasPendingCategories ) {
107- this . startRevealTimer ( )
108- }
109- } , REVEAL_INTERVAL )
110- }
111-
112- getSnapshot ( ) : Record < string , CategorySearchItem > {
113- return { ...this . searchItems }
114- }
115152}
0 commit comments