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