1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16interface ITimeProvider { 17 getCurrent: () => number; 18} 19 20type Millisecond = number; 21 22// eslint-disable-next-line @typescript-eslint/no-unused-vars 23class DefaultTimeProvider implements ITimeProvider { 24 getCurrent(): number { 25 return Date.now(); 26 } 27} 28 29const dummyDataSource: IDataSourcePrefetching = { 30 prefetch: () => {}, 31 totalCount: () => { 32 return 0; 33 }, 34 getData: () => { 35 return undefined; 36 }, 37 registerDataChangeListener: () => {}, 38 unregisterDataChangeListener: () => {}, 39}; 40 41const DELAY_TO_REPEAT_FETCH_AFTER_ERROR = 500; 42 43// eslint-disable-next-line @typescript-eslint/no-unused-vars 44class FetchingDriver implements DataCollectionChangeListener { 45 private dataSource: IDataSourcePrefetching | null = dummyDataSource; 46 private readonly dataSourceObserver = new DataSourceObserver(this); 47 private isPaused: boolean; 48 49 constructor( 50 private readonly fetchedRegistry: FetchedRegistry, 51 private readonly fetches: FetchingRegistry, 52 private readonly prefetchRangeEvaluator: IFetchingRangeEvaluator, 53 private readonly timeProvider: ITimeProvider, 54 private readonly logger: ILogger = dummyLogger, 55 autostart: boolean = true, 56 ) { 57 this.isPaused = !autostart; 58 this.prefetchRangeEvaluator = prefetchRangeEvaluator; 59 this.timeProvider = timeProvider; 60 } 61 62 get afterErrorDelay(): Millisecond { 63 return DELAY_TO_REPEAT_FETCH_AFTER_ERROR; 64 } 65 66 batchUpdate(operations: BatchOperation[]): void { 67 this.logger.info('batchUpdate called with ' + JSON.stringify(operations)); 68 try { 69 this.batchUpdateInternal(operations); 70 } catch (e) { 71 reportError(this.logger, 'batchUpdate', e); 72 throw e; 73 } 74 } 75 76 private batchUpdateInternal(operations: BatchOperation[]): void { 77 operations.forEach((operation) => { 78 switch (operation.kind) { 79 case 'deleted': 80 this.itemsDeleted(operation.startIndex, operation.count); 81 break; 82 case 'added': 83 this.itemsAdded(operation.startIndex, operation.count); 84 break; 85 case 'updated': 86 this.itemUpdated(operation.index); 87 break; 88 case 'reloaded': 89 this.collectionChanged(operation.totalCount); 90 break; 91 case 'swapped': 92 this.itemsSwapped(operation.a, operation.b); 93 break; 94 case 'moved': 95 this.itemMoved(operation.from, operation.to); 96 break; 97 } 98 }); 99 100 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 101 } 102 103 private collectionChanged(totalCount: number): void { 104 this.prefetchRangeEvaluator.updateRangeToFetch({ 105 kind: 'collection-changed', 106 totalCount: totalCount, 107 }); 108 } 109 110 private itemUpdated(index: number): void { 111 this.fetchedRegistry.removeFetched(index); 112 this.fetches.deleteFetchByItem(index); 113 } 114 115 private itemsDeleted(index: number, count: number): void { 116 for (let i = 0; i < count; i++) { 117 this.fetches.decrementAllIndexesGreaterThen(index); 118 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-removed', itemIndex: index }); 119 } 120 } 121 122 private itemsAdded(index: number, count: number): void { 123 for (let i = 0; i < count; i++) { 124 this.fetches.incrementAllIndexesGreaterThen(index - 1); 125 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-added', itemIndex: index }); 126 } 127 } 128 129 private itemsSwapped(a: number, b: number): void { 130 if (!this.fetchedRegistry.has(a) || !this.fetchedRegistry.has(b)) { 131 this.fetchedRegistry.removeFetched(a); 132 this.fetchedRegistry.removeFetched(b); 133 } 134 } 135 136 private itemMoved(from: number, to: number): void { 137 if (!this.fetchedRegistry.has(from) || !this.fetchedRegistry.has(to)) { 138 const rangeToFetch = this.fetchedRegistry.rangeToFetch; 139 140 this.itemsDeleted(from, 1); 141 this.itemsAdded(to, 1); 142 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 143 } 144 } 145 146 setDataSource(ds: IDataSourcePrefetching = dummyDataSource): void { 147 this.logger.info(`setDataSource called with ${ds !== dummyDataSource ? 'a data source' : 'null or undefined'}`); 148 try { 149 this.setDataSourceInternal(ds); 150 } catch (e) { 151 reportError(this.logger, 'setDataSource', e); 152 throw e; 153 } 154 } 155 156 private setDataSourceInternal(ds: IDataSourcePrefetching): void { 157 this.dataSource = ds ?? dummyDataSource; 158 this.dataSourceObserver.setDataSource(this.dataSource); 159 } 160 161 stop(): void { 162 this.logger.info('Stop called'); 163 try { 164 this.stopInternal(); 165 } catch (e) { 166 reportError(this.logger, 'stop', e); 167 throw e; 168 } 169 } 170 171 private stopInternal(): void { 172 if (this.isPaused) { 173 return; 174 } 175 176 this.isPaused = true; 177 this.cancel(this.fetches.getAllIndexes()); 178 } 179 180 start(): void { 181 this.logger.info('Start called'); 182 try { 183 this.startInternal(); 184 } catch (e) { 185 reportError(this.logger, 'start', e); 186 throw e; 187 } 188 } 189 190 private startInternal(): void { 191 if (!this.isPaused) { 192 return; 193 } 194 195 this.isPaused = false; 196 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 197 } 198 199 visibleAreaChanged(minVisible: number, maxVisible: number): void { 200 this.logger.info(`visibleAreaChanged min: ${minVisible} max: ${maxVisible}`); 201 202 try { 203 this.visibleAreaChangedInternal(minVisible, maxVisible); 204 } catch (e) { 205 reportError(this.logger, 'visibleAreaChanged', e); 206 throw e; 207 } 208 } 209 210 private visibleAreaChangedInternal(minVisible: number, maxVisible: number): void { 211 if (this.dataSource === dummyDataSource) { 212 throw new Error('No data source'); 213 } 214 215 const oldRangeToPrefetch = this.fetchedRegistry.rangeToFetch; 216 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'visible-area-changed', minVisible, maxVisible }); 217 218 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 219 220 const toCancel = oldRangeToPrefetch.subtract(this.fetchedRegistry.rangeToFetch).toSet(); 221 this.cancel(toCancel); 222 } 223 224 private prefetch(toPrefetch: ReadonlySet<number>): void { 225 if (this.isPaused) { 226 this.logger.debug('Prefetcher is paused. Do nothing.'); 227 return; 228 } 229 toPrefetch.forEach(this.singleFetch); 230 } 231 232 private singleFetch = (itemIndex: ItemIndex): void => { 233 if (this.fetches.isFetchingItem(itemIndex) || this.fetchedRegistry.has(itemIndex)) { 234 return; 235 } 236 237 const prefetchStart = this.timeProvider.getCurrent(); 238 const fetchId = this.fetches.registerFetch(itemIndex); 239 240 this.logger.info('to prefetch ' + itemIndex); 241 242 try { 243 const prefetchResponse = this.dataSource!.prefetch(itemIndex); 244 if (!(prefetchResponse instanceof Promise)) { 245 this.fetchedCallback(fetchId, prefetchStart); 246 return; 247 } 248 249 prefetchResponse 250 .then(() => this.fetchedCallback(fetchId, prefetchStart)) 251 .catch((e) => { 252 this.errorOnFetchCallback(fetchId, e); 253 }); 254 } catch (e) { 255 this.errorOnFetchCallback(fetchId, e); 256 } 257 }; 258 259 private fetchedCallback(fetchId: FetchId, prefetchStart: number): void { 260 const itemIndex = this.fetches.getItem(fetchId); 261 this.fetches.deleteFetch(fetchId); 262 263 if (itemIndex === undefined) { 264 return; 265 } 266 267 this.prefetchRangeEvaluator.updateRangeToFetch({ 268 kind: 'item-fetched', 269 itemIndex, 270 fetchDuration: this.timeProvider.getCurrent() - prefetchStart, 271 }); 272 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 273 } 274 275 private errorOnFetchCallback(fetchId: FetchId, error: Error): void { 276 const itemIndex = this.fetches.getItem(fetchId); 277 if (itemIndex !== undefined) { 278 this.logger.warn(`failed to fetch item at ${itemIndex} ${JSON.stringify(error)}`); 279 } 280 this.fetches.deleteFetch(fetchId); 281 setTimeout(() => { 282 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 283 }, this.afterErrorDelay); 284 } 285 286 private cancel(toCancel: ReadonlySet<number>): void { 287 toCancel.forEach((itemIndex) => { 288 if (!this.fetches.isFetchingItem(itemIndex)) { 289 return; 290 } 291 292 this.fetches.deleteFetchByItem(itemIndex); 293 294 if (this.dataSource!.cancel) { 295 this.logger.info('to cancel ' + itemIndex); 296 this.dataSource.cancel(itemIndex); 297 } 298 }); 299 } 300} 301