@@ -14,14 +14,30 @@ import { HistoryRangeService } from './history.range.service.js';
1414
1515export class HistoryService {
1616 static CHUNK_SIZE = 150 ;
17+ static QUERY_EVENT = 'query' ;
18+ static QUERY_MORE_EVENT = 'query-more' ;
19+ /**
20+ * @return {QueryData }
21+ */
22+ static defaultData ( ) {
23+ return {
24+ lastQueryParams : null ,
25+ info : {
26+ query : { term : '' } ,
27+ finished : true ,
28+ } ,
29+ results : [ ] ,
30+ } ;
31+ }
32+
33+ /**
34+ * @type {QueryData }
35+ */
36+ data = HistoryService . defaultData ( ) ;
1737
1838 internal = new EventTarget ( ) ;
1939 dataReadinessSignal = new EventTarget ( ) ;
2040
21- /**
22- * @type {QueryData|null }
23- */
24- query = null ;
2541 /** @type {HistoryQuery|null } */
2642 ongoing = null ;
2743 index = 0 ;
@@ -32,56 +48,100 @@ export class HistoryService {
3248 constructor ( history ) {
3349 this . history = history ;
3450 this . range = new HistoryRangeService ( this . history ) ;
35- this . internal . addEventListener ( 'query' , ( /** @type {CustomEvent<HistoryQuery> } */ evt ) => {
51+
52+ /**
53+ * Conduct a query
54+ */
55+ this . internal . addEventListener ( HistoryService . QUERY_EVENT , ( /** @type {CustomEvent<HistoryQuery> } */ evt ) => {
3656 const { detail } = evt ;
37- const index = this . index + 1 ;
3857 this . index ++ ;
58+
59+ // store a local index, we can check it when the promise resolves
60+ const index = this . index ;
61+
62+ // reject duplicates
3963 if ( eq ( detail , this . ongoing ) ) return console . log ( 'ignoring duplicate query' ) ;
40- this . queryFetcher ( detail ) . then ( ( next ) => {
41- if ( this . index !== index ) return console . log ( 'ignoring stale call...' ) ;
42- const old = this . query ;
43- if ( old === null ) throw new Error ( 'unreachable - this.query must always be there?' ) ;
44- let publish ;
45- /**
46- * concatenate results if this was a 'fetch more' request
47- */
48- if ( eq ( old . info . query , next . info . query ) && next . lastQueryParams ?. offset > 0 ) {
49- const results = old . results . concat ( next . results ) ;
50- publish = { info : next . info , results, lastQueryParams : next . lastQueryParams } ;
51- } else {
52- publish = next ;
53- }
54- this . accept ( publish ) ;
55- } ) ;
64+ this . ongoing = JSON . parse ( JSON . stringify ( detail ) ) ;
65+
66+ this . queryFetcher ( detail )
67+ . then ( ( next ) => {
68+ const old = this . data ;
69+ if ( old === null ) throw new Error ( 'unreachable - typescript this.query must always be there?' ) ;
70+
71+ /**
72+ * First, reject overlapping promises
73+ */
74+ const resolvedPromiseIsStale = this . index !== index ;
75+ if ( resolvedPromiseIsStale ) return console . log ( '❌ rejected stale result' ) ;
76+
77+ /**
78+ * concatenate results if this was a 'fetch more' request, or overwrite
79+ */
80+ let valueToPublish ;
81+ if ( eq ( old . info . query , next . info . query ) && next . lastQueryParams ?. offset > 0 ) {
82+ const results = old . results . concat ( next . results ) ;
83+ valueToPublish = { info : next . info , results, lastQueryParams : next . lastQueryParams } ;
84+ } else {
85+ valueToPublish = next ;
86+ }
87+
88+ this . accept ( valueToPublish ) ;
89+ } )
90+ . catch ( ( e ) => {
91+ console . error ( e , detail ) ;
92+ } ) ;
5693 } ) ;
57- this . internal . addEventListener ( 'query-more' , ( /** @type {CustomEvent<{end: number}> } */ evt ) => {
94+
95+ /**
96+ * Allow consumers to request 'more' - we'll ignore when the list is 'finished',
97+ * but otherwise will just increment the offset by the current length.
98+ */
99+ this . internal . addEventListener ( HistoryService . QUERY_MORE_EVENT , ( /** @type {CustomEvent<{end: number}> } */ evt ) => {
58100 // console.log('🦻 [query-more]', evt.detail, this.query?.info);
59- if ( ! this . query ) return ;
60- if ( this . query . info . finished ) return ;
101+ if ( ! this . data ) return ;
102+ /**
103+ * 'end' is the index of the last seen element. We use that + the result set & OVERSCAN_AMOUNT
104+ * whether to decide to fetch more data.
105+ *
106+ * Example:
107+ * - if !finished (meaning the backend has more data)
108+ * - and 'end' was 146 (meaning the 146th element was scrolled into view)
109+ * - and memory.length was 150 (meaning we've got 150 items in memory)
110+ * - and OVERSCAN_AMOUNT = 5
111+ * - that means we WOULD fetch more, because memory.length - end = 4, which is less than OVERSCAN_AMOUNT
112+ * - but if 'end' was 140, we would NOT fetch. because memory.length - end = 10 which is not less than OVERSCAN_AMOUNT
113+ *
114+ */
115+ if ( this . data . info . finished ) return ;
61116 const { end } = evt . detail ;
62- const memory = this . query . results ;
117+
118+ const memory = this . data . results ;
63119 if ( memory . length - end < OVERSCAN_AMOUNT ) {
64- const lastquery = this . query . info . query ;
120+ const lastquery = this . data . info . query ;
65121 /** @type {HistoryQuery } */
66122 const query = {
67123 query : lastquery ,
68124 limit : HistoryService . CHUNK_SIZE ,
69- offset : this . query . results . length ,
125+ offset : this . data . results . length ,
70126 } ;
71- this . internal . dispatchEvent ( new CustomEvent ( 'query' , { detail : query } ) ) ;
127+ this . internal . dispatchEvent ( new CustomEvent ( HistoryService . QUERY_EVENT , { detail : query } ) ) ;
72128 }
73129 } ) ;
74130 }
75131
76132 /**
77- * @param {QueryData } d
133+ * To 'accept' data is to store a local reference to it and treat it as 'latest'
134+ * We also want to broadcast the fact that new data can be read.
135+ * @param {QueryData } data
78136 */
79- accept ( d ) {
80- this . query = d ;
137+ accept ( data ) {
138+ this . data = data ;
139+ this . ongoing = null ;
81140 this . dataReadinessSignal . dispatchEvent ( new Event ( 'data' ) ) ;
82141 }
83142
84143 /**
144+ * The single place for the query to be made
85145 * @param {HistoryQuery } query
86146 */
87147 queryFetcher ( query ) {
@@ -105,15 +165,16 @@ export class HistoryService {
105165 }
106166
107167 /**
168+ * Allow consumers to be notified when data has changed
108169 * @param {(data: QueryData) => void } cb
109170 */
110171 onResults ( cb ) {
111172 const controller = new AbortController ( ) ;
112173 this . dataReadinessSignal . addEventListener (
113174 'data' ,
114175 ( ) => {
115- if ( this . query === null ) throw new Error ( 'unreachable' ) ;
116- cb ( this . query ) ;
176+ if ( this . data === null ) throw new Error ( 'unreachable' ) ;
177+ cb ( this . data ) ;
117178 } ,
118179 { signal : controller . signal } ,
119180 ) ;
@@ -131,15 +192,14 @@ export class HistoryService {
131192 * @param {HistoryQuery } query
132193 */
133194 trigger ( query ) {
134- this . internal . dispatchEvent ( new CustomEvent ( 'query' , { detail : query } ) ) ;
195+ this . internal . dispatchEvent ( new CustomEvent ( HistoryService . QUERY_EVENT , { detail : query } ) ) ;
135196 }
136197
137198 /**
138- * @param {number } end
199+ * @param {number } end - the index of the last seen element
139200 */
140201 requestMore ( end ) {
141- if ( ! this . dataReadinessSignal || ! this . query ) return console . warn ( 'unreachable?' ) ;
142- this . internal . dispatchEvent ( new CustomEvent ( 'query-more' , { detail : { end } } ) ) ;
202+ this . internal . dispatchEvent ( new CustomEvent ( HistoryService . QUERY_MORE_EVENT , { detail : { end } } ) ) ;
143203 }
144204
145205 /**
@@ -166,7 +226,7 @@ export class HistoryService {
166226 return { kind : 'none' } ;
167227 }
168228 if ( response . action === 'domain-search' && ids . length === 1 && indexes . length === 1 ) {
169- const target = this . query ?. results [ indexes [ 0 ] ] ;
229+ const target = this . data ?. results [ indexes [ 0 ] ] ;
170230 if ( target ?. domain ) {
171231 return { kind : 'domain-search' , value : target . domain } ;
172232 } else {
@@ -196,7 +256,7 @@ export class HistoryService {
196256 _collectIds ( indexes ) {
197257 const ids = [ ] ;
198258 for ( let i = 0 ; i < indexes . length ; i ++ ) {
199- const current = this . query ?. results [ indexes [ i ] ] ;
259+ const current = this . data ?. results [ indexes [ i ] ] ;
200260 if ( ! current ) throw new Error ( 'unreachable' ) ;
201261 ids . push ( current . id ) ;
202262 }
@@ -207,8 +267,8 @@ export class HistoryService {
207267 * @param {(d: QueryData) => QueryData } updater
208268 */
209269 update ( updater ) {
210- if ( this . query === null ) throw new Error ( 'unreachable' ) ;
211- this . accept ( updater ( this . query ) ) ;
270+ if ( this . data === null ) throw new Error ( 'unreachable' ) ;
271+ this . accept ( updater ( this . data ) ) ;
212272 }
213273
214274 /**
0 commit comments