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 
16 class BasicPrefetcher {
17     constructor(ds) {
18         const itemsOnScreen = new ItemsOnScreenProvider();
19         const fetchedRegistry = new FetchedRegistry();
20         const fetchingRegistry = new FetchingRegistry();
21         const prefetchRangeRatio = new PrefetchRangeRatio(itemsOnScreen, fetchedRegistry, fetchingRegistry);
22         const prefetchCount = new PrefetchCount(itemsOnScreen, prefetchRangeRatio);
23         const evaluator = new FetchingRangeEvaluator(itemsOnScreen, prefetchCount, prefetchRangeRatio, fetchedRegistry);
24         this.fetchingDriver = new FetchingDriver(fetchedRegistry, fetchingRegistry, evaluator, new DefaultTimeProvider());
25         this.fetchingDriver.setDataSource(ds);
26     }
27     setDataSource(ds) {
28         this.fetchingDriver.setDataSource(ds);
29     }
30     visibleAreaChanged(minVisible, maxVisible) {
31         this.fetchingDriver.visibleAreaChanged(minVisible, maxVisible);
32     }
33 }
34 class DataSourceObserver {
35     constructor(simpleChangeListener) {
36         this.simpleChangeListener = simpleChangeListener;
37     }
38     onDataReloaded() {
39         this.simpleChangeListener.batchUpdate([
40             {
41                 kind: 'reloaded',
42                 totalCount: this.dataSource.totalCount(),
43             },
44         ]);
45     }
46     onDataAdded(index) {
47         this.simpleChangeListener.batchUpdate([
48             {
49                 kind: 'added',
50                 startIndex: index,
51                 count: 1,
52             },
53         ]);
54     }
55     onDataAdd(index) {
56         this.onDataAdded(index);
57     }
58     onDataMoved(from, to) {
59         this.simpleChangeListener.batchUpdate([
60             {
61                 kind: 'swapped',
62                 a: from,
63                 b: to,
64             },
65         ]);
66     }
67     onDataMove(from, to) {
68         this.onDataMoved(from, to);
69     }
70     onDataDeleted(index) {
71         this.simpleChangeListener.batchUpdate([
72             {
73                 kind: 'deleted',
74                 startIndex: index,
75                 count: 1,
76             },
77         ]);
78     }
79     onDataDelete(index) {
80         this.onDataDeleted(index);
81     }
82     onDataChanged(index) {
83         this.simpleChangeListener.batchUpdate([
84             {
85                 kind: 'updated',
86                 index,
87             },
88         ]);
89     }
90     onDataChange(index) {
91         this.onDataChanged(index);
92     }
93     onDatasetChange(dataOperations) {
94         const operations = [];
95         dataOperations.forEach((operation) => {
96             switch (operation.type) {
97                 case 'add':
98                 case 'delete':
99                     if (operation.count === undefined || operation.count > 0) {
100                         operations.push({
101                             kind: operation.type === 'add' ? 'added' : 'deleted',
102                             startIndex: operation.index,
103                             count: operation.count ?? 1,
104                         });
105                     }
106                     break;
107                 case 'change':
108                     operations.push({
109                         kind: 'updated',
110                         index: operation.index,
111                     });
112                     break;
113                 case 'reload':
114                     operations.push({
115                         kind: 'reloaded',
116                         totalCount: this.dataSource.totalCount(),
117                     });
118                     break;
119                 case 'exchange':
120                     operations.push({
121                         kind: 'swapped',
122                         a: operation.index.start,
123                         b: operation.index.end,
124                     });
125                     break;
126                 case 'move':
127                     operations.push({
128                         kind: 'moved',
129                         from: operation.index.from,
130                         to: operation.index.to,
131                     });
132                     break;
133                 default:
134                     assertNever(operation);
135             }
136         });
137         this.simpleChangeListener.batchUpdate(operations);
138     }
139     setDataSource(dataSource) {
140         if (this.dataSource) {
141             this.dataSource.unregisterDataChangeListener(this);
142         }
143         this.dataSource = dataSource;
144         this.dataSource.registerDataChangeListener(this);
145         this.onDataReloaded();
146     }
147 }
148 class FetchingRegistry {
149     constructor() {
150         this.fetches = new Map();
151         this.fetching = new Map();
152         this.fetchesBefore = new Map();
153         this.fetchCounter = 0;
154     }
155     registerFetch(index) {
156         let fetchId = this.fetching.get(index);
157         if (fetchId !== undefined) {
158             return fetchId;
159         }
160         fetchId = ++this.fetchCounter;
161         this.fetching.set(index, fetchId);
162         this.fetches.set(fetchId, index);
163         this.fetchesBefore.set(index, this.fetches.size);
164         return fetchId;
165     }
166     getItem(fetchId) {
167         return this.fetches.get(fetchId);
168     }
169     deleteFetch(fetchId) {
170         const index = this.fetches.get(fetchId);
171         if (index !== undefined) {
172             this.fetching.delete(index);
173             this.fetches.delete(fetchId);
174         }
175     }
176     deleteFetchByItem(index) {
177         const fetchId = this.fetching.get(index);
178         if (fetchId !== undefined) {
179             this.fetching.delete(index);
180             this.fetches.delete(fetchId);
181         }
182     }
183     isFetchingItem(index) {
184         return this.fetching.has(index);
185     }
186     incrementAllIndexesGreaterThen(value) {
187         this.offsetAllIndexesGreaterThen(value, 1);
188     }
189     getAllIndexes() {
190         const set = new Set();
191         this.fetching.forEach((fetchId, itemIndex) => set.add(itemIndex));
192         return set;
193     }
194     getFetchesCount() {
195         return this.fetches.size;
196     }
197     isFetchLatecomer(index, threshold) {
198         return this.fetchesBefore.get(index) > threshold;
199     }
200     offsetAllIndexesGreaterThen(value, offset) {
201         const newFetching = new Map();
202         this.fetches.forEach((index, fetchId) => {
203             const toSet = index > value ? index + offset : index;
204             newFetching.set(toSet, fetchId);
205             this.fetches.set(fetchId, toSet);
206         });
207         this.fetching = newFetching;
208     }
209     decrementAllIndexesGreaterThen(value) {
210         this.offsetAllIndexesGreaterThen(value, -1);
211     }
212 }
213 class FetchedRegistry {
214     constructor() {
215         this.fetchedIndexes = new Set();
216         this.rangeToFetchInternal = new IndexRange(0, 0);
217         this.missedIndexes = new Set();
218     }
219     get rangeToFetch() {
220         return this.rangeToFetchInternal;
221     }
222     addFetched(index) {
223         if (this.rangeToFetch.contains(index)) {
224             this.fetchedIndexes.add(index);
225             this.missedIndexes.delete(index);
226         }
227     }
228     removeFetched(index) {
229         if (this.rangeToFetch.contains(index)) {
230             this.fetchedIndexes.delete(index);
231             this.missedIndexes.add(index);
232         }
233     }
234     has(index) {
235         return this.fetchedIndexes.has(index);
236     }
237     getFetchedInRange(range) {
238         let fetched = 0;
239         range.forEachIndex((index) => {
240             fetched += this.fetchedIndexes.has(index) ? 1 : 0;
241         });
242         return fetched;
243     }
244     updateRangeToFetch(fetchRange) {
245         this.rangeToFetch.subtract(fetchRange).forEachIndex((index) => {
246             this.fetchedIndexes.delete(index);
247         });
248         this.rangeToFetchInternal = fetchRange;
249         this.missedIndexes.clear();
250         this.rangeToFetch.forEachIndex((index) => {
251             if (!this.fetchedIndexes.has(index)) {
252                 this.missedIndexes.add(index);
253             }
254         });
255     }
256     getItemsToFetch() {
257         return new Set(this.missedIndexes);
258     }
259     incrementFetchedGreaterThen(value, newFetchRange) {
260         this.offsetAllGreaterThen(value, 1);
261         this.updateRangeToFetch(newFetchRange);
262     }
263     decrementFetchedGreaterThen(value, newFetchRange) {
264         this.offsetAllGreaterThen(value, -1);
265         this.updateRangeToFetch(newFetchRange);
266     }
267     offsetAllGreaterThen(value, offset) {
268         const updated = new Set();
269         this.fetchedIndexes.forEach((index) => {
270             updated.add(index > value ? index + offset : index);
271         });
272         this.fetchedIndexes = updated;
273     }
274     clearFetched(newFetchRange) {
275         this.fetchedIndexes.clear();
276         this.updateRangeToFetch(newFetchRange);
277     }
278 }
279 class ItemsOnScreenProvider {
280     constructor() {
281         this.firstScreen = true;
282         this.meanImagesOnScreen = 0;
283         this.minVisible = 0;
284         this.maxVisible = 0;
285         this.directionInternal = 'UNKNOWN';
286         this.speedInternal = 0;
287         this.lastUpdateTimestamp = 0;
288         this.visibleRangeInternal = new IndexRange(0, 0);
289         this.callbacks = [];
290     }
291     register(callback) {
292         this.callbacks.push(callback);
293     }
294     get visibleRange() {
295         return this.visibleRangeInternal;
296     }
297     get meanValue() {
298         return this.meanImagesOnScreen;
299     }
300     get direction() {
301         return this.directionInternal;
302     }
303     get speed() {
304         return this.speedInternal;
305     }
306     updateSpeed(minVisible, maxVisible) {
307         const timeDifference = Date.now() - this.lastUpdateTimestamp;
308         if (timeDifference > 0) {
309             const speedTau = 100;
310             const speedWeight = 1 - Math.exp(-timeDifference / speedTau);
311             const distance = minVisible + (maxVisible - minVisible) / 2 - (this.minVisible + (this.maxVisible - this.minVisible) / 2);
312             const rawSpeed = Math.abs(distance / timeDifference) * 1000;
313             this.speedInternal = speedWeight * rawSpeed + (1 - speedWeight) * this.speedInternal;
314         }
315     }
316     update(minVisible, maxVisible) {
317         if (minVisible !== this.minVisible || maxVisible !== this.maxVisible) {
318             if (Math.max(minVisible, this.minVisible) === minVisible &&
319                 Math.max(maxVisible, this.maxVisible) === maxVisible) {
320                 this.directionInternal = 'DOWN';
321             }
322             else if (Math.min(minVisible, this.minVisible) === minVisible &&
323                 Math.min(maxVisible, this.maxVisible) === maxVisible) {
324                 this.directionInternal = 'UP';
325             }
326         }
327         let imagesOnScreen = maxVisible - minVisible + 1;
328         let oldMeanImagesOnScreen = this.meanImagesOnScreen;
329         if (this.firstScreen) {
330             this.meanImagesOnScreen = imagesOnScreen;
331             this.firstScreen = false;
332             this.lastUpdateTimestamp = Date.now();
333         }
334         else {
335             {
336                 const imagesWeight = 0.95;
337                 this.meanImagesOnScreen = this.meanImagesOnScreen * imagesWeight + (1 - imagesWeight) * imagesOnScreen;
338             }
339             this.updateSpeed(minVisible, maxVisible);
340         }
341         this.minVisible = minVisible;
342         this.maxVisible = maxVisible;
343         const visibleRangeSizeChanged = Math.ceil(oldMeanImagesOnScreen) !== Math.ceil(this.meanImagesOnScreen);
344         this.visibleRangeInternal = new IndexRange(minVisible, maxVisible + 1);
345         if (visibleRangeSizeChanged) {
346             this.notifyObservers();
347         }
348         this.lastUpdateTimestamp = Date.now();
349     }
350     notifyObservers() {
351         this.callbacks.forEach((callback) => callback());
352     }
353 }
354 class PrefetchCount {
355     constructor(itemsOnScreen, prefetchRangeRatio, logger = dummyLogger) {
356         this.itemsOnScreen = itemsOnScreen;
357         this.prefetchRangeRatio = prefetchRangeRatio;
358         this.logger = logger;
359         this.MAX_SCREENS = 4;
360         this.speedCoef = 2.5;
361         this.maxItems = 0;
362         this.prefetchCountValueInternal = 0;
363         this.currentMaxItemsInternal = 0;
364         this.currentMinItemsInternal = 0;
365         this.itemsOnScreen = itemsOnScreen;
366         this.itemsOnScreen.register(() => {
367             this.updateLimits();
368         });
369         this.prefetchRangeRatio.register(() => {
370             this.updateLimits();
371         });
372     }
373     get prefetchCountValue() {
374         return this.prefetchCountValueInternal;
375     }
376     set prefetchCountValue(v) {
377         this.prefetchCountValueInternal = v;
378         this.logger.debug(`{"tm":${Date.now()},"prefetch_count":${v}}`);
379     }
380     get currentMaxItems() {
381         return this.currentMaxItemsInternal;
382     }
383     get currentMinItems() {
384         return this.currentMinItemsInternal;
385     }
386     getPrefetchCountByRatio(ratio) {
387         this.itemsOnScreen.updateSpeed(this.itemsOnScreen.visibleRange.start, this.itemsOnScreen.visibleRange.end - 1);
388         const minItems = Math.min(this.currentMaxItems, Math.ceil(this.speedCoef * this.itemsOnScreen.speed * this.currentMaxItems));
389         const prefetchCount = minItems + Math.ceil(ratio * (this.currentMaxItems - minItems));
390         this.logger.debug(`speed: ${this.itemsOnScreen.speed}, minItems: ${minItems}, ratio: ${ratio}, prefetchCount: ${prefetchCount}`);
391         return prefetchCount;
392     }
393     getRangeToFetch(totalCount) {
394         const visibleRange = this.itemsOnScreen.visibleRange;
395         let start = 0;
396         let end = 0;
397         switch (this.itemsOnScreen.direction) {
398             case 'UNKNOWN':
399                 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue));
400                 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue));
401                 break;
402             case 'UP':
403                 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue));
404                 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue * 0.5));
405                 break;
406             case 'DOWN':
407                 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue * 0.5));
408                 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue));
409                 break;
410         }
411         if (start > end) {
412             start = end;
413         }
414         return new IndexRange(start, end);
415     }
416     updateLimits() {
417         this.maxItems = Math.max(this.currentMinItems, Math.ceil(this.MAX_SCREENS * this.itemsOnScreen.meanValue));
418         this.updateCurrentLimit();
419     }
420     updateCurrentLimit() {
421         this.currentMaxItemsInternal = Math.max(this.currentMinItems, Math.ceil(this.maxItems * this.prefetchRangeRatio.maxRatio));
422         this.currentMinItemsInternal = Math.ceil(this.maxItems * this.prefetchRangeRatio.minRatio);
423     }
424 }
425 class FetchingRangeEvaluator {
426     constructor(itemsOnScreen, prefetchCount, prefetchRangeRatio, fetchedRegistry, logger = dummyLogger) {
427         this.itemsOnScreen = itemsOnScreen;
428         this.prefetchCount = prefetchCount;
429         this.prefetchRangeRatio = prefetchRangeRatio;
430         this.fetchedRegistry = fetchedRegistry;
431         this.logger = logger;
432         this.totalItems = 0;
433     }
434     updateRangeToFetch(whatHappened) {
435         switch (whatHappened.kind) {
436             case 'visible-area-changed':
437                 this.onVisibleAreaChange(whatHappened.minVisible, whatHappened.maxVisible);
438                 break;
439             case 'item-fetched':
440                 this.onItemFetched(whatHappened.itemIndex, whatHappened.fetchDuration);
441                 break;
442             case 'collection-changed':
443                 this.onCollectionChanged(whatHappened.totalCount);
444                 break;
445             case 'item-added':
446                 this.onItemAdded(whatHappened.itemIndex);
447                 break;
448             case 'item-removed':
449                 this.onItemDeleted(whatHappened.itemIndex);
450                 break;
451             default:
452                 assertNever(whatHappened);
453         }
454     }
455     onVisibleAreaChange(minVisible, maxVisible) {
456         const oldVisibleRange = this.itemsOnScreen.visibleRange;
457         this.itemsOnScreen.update(minVisible, maxVisible);
458         this.logger.debug(`visibleAreaChanged itemsOnScreen=${this.itemsOnScreen.visibleRange.length}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}, prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}`);
459         if (!oldVisibleRange.equals(this.itemsOnScreen.visibleRange)) {
460             this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed');
461             const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
462             this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
463         }
464     }
465     onItemFetched(index, fetchDuration) {
466         if (!this.fetchedRegistry.rangeToFetch.contains(index)) {
467             return;
468         }
469         this.logger.debug(`onItemFetched`);
470         let maxRatioChanged = false;
471         if (this.prefetchRangeRatio.update(index, fetchDuration) === 'ratio-changed') {
472             maxRatioChanged = true;
473             this.logger.debug(`choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}, prefetchCountMinRatio=${this.prefetchRangeRatio.minRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}`);
474         }
475         this.fetchedRegistry.addFetched(index);
476         this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged);
477         const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
478         this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
479     }
480     evaluatePrefetchCount(event, maxRatioChanged) {
481         let ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems);
482         let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio);
483         if (maxRatioChanged) {
484             ratio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems);
485             evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio);
486         }
487         if (!this.prefetchRangeRatio.hysteresisEnabled) {
488             if (event === 'resolved') {
489                 this.prefetchRangeRatio.updateRatioRange(ratio);
490                 this.prefetchRangeRatio.hysteresisEnabled = true;
491             }
492             else if (event === 'visible-area-changed') {
493                 this.prefetchRangeRatio.oldRatio = ratio;
494             }
495         }
496         else if (this.prefetchRangeRatio.range.contains(ratio)) {
497             return this.prefetchCount.prefetchCountValue;
498         }
499         else {
500             if (event === 'resolved') {
501                 this.prefetchRangeRatio.updateRatioRange(ratio);
502             }
503             else if (event === 'visible-area-changed') {
504                 this.prefetchRangeRatio.setEmptyRange();
505                 this.prefetchRangeRatio.oldRatio = ratio;
506                 this.prefetchRangeRatio.hysteresisEnabled = false;
507             }
508         }
509         this.logger.debug(`evaluatePrefetchCount event=${event}, ${this.prefetchRangeRatio.hysteresisEnabled ? 'inHysteresis' : 'setHysteresis'} prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, hysteresisRange=${this.prefetchRangeRatio.range}`);
510         return evaluatedPrefetchCount;
511     }
512     onCollectionChanged(totalCount) {
513         this.totalItems = Math.max(0, totalCount);
514         let newRangeToFetch;
515         if (this.fetchedRegistry.rangeToFetch.length > 0) {
516             newRangeToFetch = this.itemsOnScreen.visibleRange;
517         }
518         else {
519             newRangeToFetch = this.fetchedRegistry.rangeToFetch;
520         }
521         if (newRangeToFetch.end > this.totalItems) {
522             const end = this.totalItems;
523             const start = newRangeToFetch.start < end ? newRangeToFetch.start : end;
524             newRangeToFetch = new IndexRange(start, end);
525         }
526         this.fetchedRegistry.clearFetched(newRangeToFetch);
527     }
528     onItemDeleted(itemIndex) {
529         if (this.totalItems === 0) {
530             return;
531         }
532         this.totalItems--;
533         this.fetchedRegistry.removeFetched(itemIndex);
534         const end = this.fetchedRegistry.rangeToFetch.end < this.totalItems ? this.fetchedRegistry.rangeToFetch.end : this.totalItems;
535         const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end);
536         this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch);
537     }
538     onItemAdded(itemIndex) {
539         this.totalItems++;
540         if (itemIndex > this.fetchedRegistry.rangeToFetch.end) {
541             return;
542         }
543         const end = this.fetchedRegistry.rangeToFetch.end + 1;
544         const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end);
545         this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch);
546     }
547 }
548 class PrefetchRangeRatio {
549     constructor(itemsOnScreen, fetchedRegistry, fetchingRegistry, logger = dummyLogger) {
550         this.itemsOnScreen = itemsOnScreen;
551         this.fetchedRegistry = fetchedRegistry;
552         this.fetchingRegistry = fetchingRegistry;
553         this.logger = logger;
554         this.TOLERANCE_RANGES = [
555             {
556                 leftToleranceEdge: 140,
557                 rightToleranceEdge: 290,
558                 prefetchCountMinRatioLeft: 0.5,
559                 prefetchCountMaxRatioLeft: 0.5,
560                 prefetchCountMinRatioRight: 0.25,
561                 prefetchCountMaxRatioRight: 1,
562             },
563             {
564                 leftToleranceEdge: 3000,
565                 rightToleranceEdge: 4000,
566                 prefetchCountMinRatioLeft: 0.25,
567                 prefetchCountMaxRatioLeft: 1,
568                 prefetchCountMinRatioRight: 0.25,
569                 prefetchCountMaxRatioRight: 0.25,
570             },
571         ];
572         this.ACTIVE_DEGREE = 0;
573         this.VISIBLE_DEGREE = 2.5;
574         this.meanPrefetchTime = 0;
575         this.leftToleranceEdge = Number.MIN_VALUE;
576         this.rightToleranceEdge = 250;
577         this.callbacks = [];
578         this.rangeInternal = RatioRange.newEmpty();
579         this.minRatioInternal = 0.25 * 0.6;
580         this.maxRatioInternal = 0.5;
581         this.hysteresisEnabledInternal = false;
582         this.oldRatioInternal = 0;
583     }
584     register(callback) {
585         this.callbacks.push(callback);
586     }
587     get range() {
588         return this.rangeInternal;
589     }
590     setEmptyRange() {
591         this.rangeInternal = RatioRange.newEmpty();
592     }
593     get maxRatio() {
594         return this.maxRatioInternal;
595     }
596     get minRatio() {
597         return this.minRatioInternal;
598     }
599     get hysteresisEnabled() {
600         return this.hysteresisEnabledInternal;
601     }
602     set hysteresisEnabled(value) {
603         this.hysteresisEnabledInternal = value;
604     }
605     set oldRatio(ratio) {
606         this.oldRatioInternal = ratio;
607     }
608     get oldRatio() {
609         return this.oldRatioInternal;
610     }
611     updateTiming(index, prefetchDuration) {
612         const weight = 0.95;
613         const localPrefetchDuration = 20;
614         let isFetchLocal = prefetchDuration < localPrefetchDuration;
615         let isFetchLatecomer = this.fetchingRegistry.isFetchLatecomer(index, this.itemsOnScreen.meanValue);
616         if (!isFetchLocal && !isFetchLatecomer) {
617             this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration;
618         }
619         this.logger.debug(`prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}, ` +
620             `isFetchLocal=${isFetchLocal}, isFetchLatecomer=${isFetchLatecomer}`);
621     }
622     update(index, prefetchDuration) {
623         this.updateTiming(index, prefetchDuration);
624         if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) {
625             return 'ratio-not-changed';
626         }
627         let ratioChanged = false;
628         if (this.meanPrefetchTime > this.rightToleranceEdge) {
629             ratioChanged = this.updateOnGreaterThanRight();
630         }
631         else if (this.meanPrefetchTime < this.leftToleranceEdge) {
632             ratioChanged = this.updateOnLessThanLeft();
633         }
634         if (ratioChanged) {
635             this.notifyObservers();
636         }
637         return ratioChanged ? 'ratio-changed' : 'ratio-not-changed';
638     }
639     updateOnLessThanLeft() {
640         let ratioChanged = false;
641         for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) {
642             const limit = this.TOLERANCE_RANGES[i];
643             if (this.meanPrefetchTime < limit.leftToleranceEdge) {
644                 ratioChanged = true;
645                 this.maxRatioInternal = limit.prefetchCountMaxRatioLeft;
646                 this.minRatioInternal = limit.prefetchCountMinRatioLeft;
647                 this.rightToleranceEdge = limit.rightToleranceEdge;
648                 if (i !== 0) {
649                     this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge;
650                 }
651                 else {
652                     this.leftToleranceEdge = Number.MIN_VALUE;
653                 }
654             }
655         }
656         return ratioChanged;
657     }
658     updateOnGreaterThanRight() {
659         let ratioChanged = false;
660         for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) {
661             const limit = this.TOLERANCE_RANGES[i];
662             if (this.meanPrefetchTime > limit.rightToleranceEdge) {
663                 ratioChanged = true;
664                 this.maxRatioInternal = limit.prefetchCountMaxRatioRight;
665                 this.minRatioInternal = limit.prefetchCountMinRatioRight;
666                 this.leftToleranceEdge = limit.leftToleranceEdge;
667                 if (i + 1 !== this.TOLERANCE_RANGES.length) {
668                     this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge;
669                 }
670                 else {
671                     this.rightToleranceEdge = Number.MAX_VALUE;
672                 }
673             }
674         }
675         return ratioChanged;
676     }
677     calculateRatio(prefetchCount, totalCount) {
678         const visibleRange = this.itemsOnScreen.visibleRange;
679         let start = 0;
680         let end = 0;
681         switch (this.itemsOnScreen.direction) {
682             case 'UNKNOWN':
683                 start = Math.max(0, visibleRange.start - prefetchCount);
684                 end = Math.min(totalCount, visibleRange.end + prefetchCount);
685                 break;
686             case 'UP':
687                 start = Math.max(0, visibleRange.start - prefetchCount);
688                 end = Math.min(totalCount, visibleRange.end + Math.round(0.5 * prefetchCount));
689                 break;
690             case 'DOWN':
691                 start = Math.max(0, visibleRange.start - Math.round(0.5 * prefetchCount));
692                 end = Math.min(totalCount, visibleRange.end + prefetchCount);
693                 break;
694         }
695         const evaluatedPrefetchRange = new IndexRange(start, end);
696         const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange);
697         const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange);
698         if (evaluatedPrefetchRange.length === 0 || visibleRange.length === 0) {
699             return 0;
700         }
701         this.logger.debug(`active_degree=${this.ACTIVE_DEGREE}, visible_degree=${this.VISIBLE_DEGREE}`);
702         this.logger.debug(`evaluatedPrefetchRange=${evaluatedPrefetchRange}, visibleRange=${visibleRange}, active_ratio=${Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE)}, visible_ratio=${Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE)}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, visibleRange.length=${visibleRange.length}`);
703         const ratio = Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) *
704             Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE);
705         this.logger.debug(`calculateRatio ratio=${ratio}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, ` +
706             `completedVisible=${completedVisible}, visibleRange.length=${visibleRange.length}`);
707         return Math.min(1, ratio);
708     }
709     updateRatioRange(ratio) {
710         if (ratio > this.oldRatioInternal) {
711             this.rangeInternal = new RatioRange(new RangeEdge(this.oldRatioInternal, false), new RangeEdge(ratio, true));
712         }
713         else {
714             this.rangeInternal = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatioInternal, false));
715         }
716         this.oldRatioInternal = ratio;
717     }
718     notifyObservers() {
719         this.callbacks.forEach((callback) => callback());
720     }
721 }
722 class DefaultTimeProvider {
723     getCurrent() {
724         return Date.now();
725     }
726 }
727 const dummyDataSource = {
728     prefetch: () => { },
729     totalCount: () => {
730         return 0;
731     },
732     getData: () => {
733         return undefined;
734     },
735     registerDataChangeListener: () => { },
736     unregisterDataChangeListener: () => { },
737 };
738 const DELAY_TO_REPEAT_FETCH_AFTER_ERROR = 500;
739 class FetchingDriver {
740     constructor(fetchedRegistry, fetches, prefetchRangeEvaluator, timeProvider, logger = dummyLogger, autostart = true) {
741         this.fetchedRegistry = fetchedRegistry;
742         this.fetches = fetches;
743         this.prefetchRangeEvaluator = prefetchRangeEvaluator;
744         this.timeProvider = timeProvider;
745         this.logger = logger;
746         this.dataSource = dummyDataSource;
747         this.dataSourceObserver = new DataSourceObserver(this);
748         this.singleFetch = (itemIndex) => {
749             if (this.fetches.isFetchingItem(itemIndex) || this.fetchedRegistry.has(itemIndex)) {
750                 return;
751             }
752             const prefetchStart = this.timeProvider.getCurrent();
753             const fetchId = this.fetches.registerFetch(itemIndex);
754             this.logger.info('to prefetch ' + itemIndex);
755             try {
756                 const prefetchResponse = this.dataSource.prefetch(itemIndex);
757                 if (!(prefetchResponse instanceof Promise)) {
758                     this.fetchedCallback(fetchId, prefetchStart);
759                     return;
760                 }
761                 prefetchResponse
762                     .then(() => this.fetchedCallback(fetchId, prefetchStart))
763                     .catch((e) => {
764                     this.errorOnFetchCallback(fetchId, e);
765                 });
766             }
767             catch (e) {
768                 this.errorOnFetchCallback(fetchId, e);
769             }
770         };
771         this.isPaused = !autostart;
772         this.prefetchRangeEvaluator = prefetchRangeEvaluator;
773         this.timeProvider = timeProvider;
774     }
775     get afterErrorDelay() {
776         return DELAY_TO_REPEAT_FETCH_AFTER_ERROR;
777     }
778     batchUpdate(operations) {
779         this.logger.info('batchUpdate called with ' + JSON.stringify(operations));
780         try {
781             this.batchUpdateInternal(operations);
782         }
783         catch (e) {
784             reportError(this.logger, 'batchUpdate', e);
785             throw e;
786         }
787     }
788     batchUpdateInternal(operations) {
789         operations.forEach((operation) => {
790             switch (operation.kind) {
791                 case 'deleted':
792                     this.itemsDeleted(operation.startIndex, operation.count);
793                     break;
794                 case 'added':
795                     this.itemsAdded(operation.startIndex, operation.count);
796                     break;
797                 case 'updated':
798                     this.itemUpdated(operation.index);
799                     break;
800                 case 'reloaded':
801                     this.collectionChanged(operation.totalCount);
802                     break;
803                 case 'swapped':
804                     this.itemsSwapped(operation.a, operation.b);
805                     break;
806                 case 'moved':
807                     this.itemMoved(operation.from, operation.to);
808                     break;
809             }
810         });
811         this.prefetch(this.fetchedRegistry.getItemsToFetch());
812     }
813     collectionChanged(totalCount) {
814         this.prefetchRangeEvaluator.updateRangeToFetch({
815             kind: 'collection-changed',
816             totalCount: totalCount,
817         });
818     }
819     itemUpdated(index) {
820         this.fetchedRegistry.removeFetched(index);
821         this.fetches.deleteFetchByItem(index);
822     }
823     itemsDeleted(index, count) {
824         for (let i = 0; i < count; i++) {
825             this.fetches.decrementAllIndexesGreaterThen(index);
826             this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-removed', itemIndex: index });
827         }
828     }
829     itemsAdded(index, count) {
830         for (let i = 0; i < count; i++) {
831             this.fetches.incrementAllIndexesGreaterThen(index - 1);
832             this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-added', itemIndex: index });
833         }
834     }
835     itemsSwapped(a, b) {
836         if (!this.fetchedRegistry.has(a) || !this.fetchedRegistry.has(b)) {
837             this.fetchedRegistry.removeFetched(a);
838             this.fetchedRegistry.removeFetched(b);
839         }
840     }
841     itemMoved(from, to) {
842         if (!this.fetchedRegistry.has(from) || !this.fetchedRegistry.has(to)) {
843             const rangeToFetch = this.fetchedRegistry.rangeToFetch;
844             this.itemsDeleted(from, 1);
845             this.itemsAdded(to, 1);
846             this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
847         }
848     }
849     setDataSource(ds = dummyDataSource) {
850         this.logger.info(`setDataSource called with ${ds !== dummyDataSource ? 'a data source' : 'null or undefined'}`);
851         try {
852             this.setDataSourceInternal(ds);
853         }
854         catch (e) {
855             reportError(this.logger, 'setDataSource', e);
856             throw e;
857         }
858     }
859     setDataSourceInternal(ds) {
860         this.dataSource = ds ?? dummyDataSource;
861         this.dataSourceObserver.setDataSource(this.dataSource);
862     }
863     stop() {
864         this.logger.info('Stop called');
865         try {
866             this.stopInternal();
867         }
868         catch (e) {
869             reportError(this.logger, 'stop', e);
870             throw e;
871         }
872     }
873     stopInternal() {
874         if (this.isPaused) {
875             return;
876         }
877         this.isPaused = true;
878         this.cancel(this.fetches.getAllIndexes());
879     }
880     start() {
881         this.logger.info('Start called');
882         try {
883             this.startInternal();
884         }
885         catch (e) {
886             reportError(this.logger, 'start', e);
887             throw e;
888         }
889     }
890     startInternal() {
891         if (!this.isPaused) {
892             return;
893         }
894         this.isPaused = false;
895         this.prefetch(this.fetchedRegistry.getItemsToFetch());
896     }
897     visibleAreaChanged(minVisible, maxVisible) {
898         this.logger.info(`visibleAreaChanged min: ${minVisible} max: ${maxVisible}`);
899         try {
900             this.visibleAreaChangedInternal(minVisible, maxVisible);
901         }
902         catch (e) {
903             reportError(this.logger, 'visibleAreaChanged', e);
904             throw e;
905         }
906     }
907     visibleAreaChangedInternal(minVisible, maxVisible) {
908         if (this.dataSource === dummyDataSource) {
909             throw new Error('No data source');
910         }
911         const oldRangeToPrefetch = this.fetchedRegistry.rangeToFetch;
912         this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'visible-area-changed', minVisible, maxVisible });
913         this.prefetch(this.fetchedRegistry.getItemsToFetch());
914         const toCancel = oldRangeToPrefetch.subtract(this.fetchedRegistry.rangeToFetch).toSet();
915         this.cancel(toCancel);
916     }
917     prefetch(toPrefetch) {
918         if (this.isPaused) {
919             this.logger.debug('Prefetcher is paused. Do nothing.');
920             return;
921         }
922         toPrefetch.forEach(this.singleFetch);
923     }
924     fetchedCallback(fetchId, prefetchStart) {
925         const itemIndex = this.fetches.getItem(fetchId);
926         this.fetches.deleteFetch(fetchId);
927         if (itemIndex === undefined) {
928             return;
929         }
930         this.prefetchRangeEvaluator.updateRangeToFetch({
931             kind: 'item-fetched',
932             itemIndex,
933             fetchDuration: this.timeProvider.getCurrent() - prefetchStart,
934         });
935         this.prefetch(this.fetchedRegistry.getItemsToFetch());
936     }
937     errorOnFetchCallback(fetchId, error) {
938         const itemIndex = this.fetches.getItem(fetchId);
939         if (itemIndex !== undefined) {
940             this.logger.warn(`failed to fetch item at ${itemIndex} ${JSON.stringify(error)}`);
941         }
942         this.fetches.deleteFetch(fetchId);
943         setTimeout(() => {
944             this.prefetch(this.fetchedRegistry.getItemsToFetch());
945         }, this.afterErrorDelay);
946     }
947     cancel(toCancel) {
948         toCancel.forEach((itemIndex) => {
949             if (!this.fetches.isFetchingItem(itemIndex)) {
950                 return;
951             }
952             this.fetches.deleteFetchByItem(itemIndex);
953             if (this.dataSource.cancel) {
954                 this.logger.info('to cancel ' + itemIndex);
955                 this.dataSource.cancel(itemIndex);
956             }
957         });
958     }
959 }
960 const dummyLogger = {
961     debug: () => { },
962     info: () => { },
963     warn: () => { },
964 };
965 function reportError(logger, methodName, e) {
966     logger.warn(`Error in ${methodName}: ${e}\n${e.stack}`);
967 }
968 class IndexRange {
969     constructor(start, end) {
970         this.start = start;
971         this.end = end;
972         if (this.start > this.end) {
973             throw new Error('Invalid range');
974         }
975     }
976     get length() {
977         return this.end - this.start;
978     }
979     toSet(target) {
980         const set = target ?? new Set();
981         for (let i = this.start; i < this.end; ++i) {
982             set.add(i);
983         }
984         return set;
985     }
986     contains(value) {
987         if (typeof value === 'object') {
988             return this.start <= value.start && value.end <= this.end;
989         }
990         else {
991             return this.start <= value && value < this.end;
992         }
993     }
994     subtract(other) {
995         const result = new IndexRangeArray();
996         if (other.start > this.start) {
997             result.push(new IndexRange(this.start, Math.min(this.end, other.start)));
998         }
999         if (other.end < this.end) {
1000             result.push(new IndexRange(Math.max(other.end, this.start), this.end));
1001         }
1002         return result;
1003     }
1004     expandedWith(other) {
1005         return new IndexRange(Math.min(this.start, other.start), Math.max(this.end, other.end));
1006     }
1007     forEachIndex(callback) {
1008         for (let i = this.start; i < this.end; ++i) {
1009             callback(i);
1010         }
1011     }
1012     equals(other) {
1013         return this.start === other.start && this.end === other.end;
1014     }
1015     toString() {
1016         return `[${this.start}, ${this.end})`;
1017     }
1018 }
1019 class IndexRangeArray extends Array {
1020     forEachIndex(callback) {
1021         this.forEach((range) => {
1022             range.forEachIndex(callback);
1023         });
1024     }
1025     toSet() {
1026         const set = new Set();
1027         this.forEach((range) => {
1028             range.toSet(set);
1029         });
1030         return set;
1031     }
1032 }
1033 class RangeEdge {
1034     constructor(value, inclusive) {
1035         this.value = value;
1036         this.inclusive = inclusive;
1037     }
1038 }
1039 class RatioRange {
1040     constructor(start, end) {
1041         this.start = start;
1042         this.end = end;
1043         if (this.start.value > this.end.value) {
1044             throw new Error(`RatioRange: ${this.start.value} > ${this.end.value}`);
1045         }
1046     }
1047     static newEmpty() {
1048         return new RatioRange(new RangeEdge(0, false), new RangeEdge(0, false));
1049     }
1050     contains(point) {
1051         if (point === this.start.value) {
1052             return this.start.inclusive;
1053         }
1054         if (point === this.end.value) {
1055             return this.end.inclusive;
1056         }
1057         return this.start.value < point && point < this.end.value;
1058     }
1059     toString() {
1060         return `${this.start.inclusive ? '[' : '('}${this.start.value}, ${this.end.value}${this.end.inclusive ? ']' : ')'}`;
1061     }
1062 }
1063 function assertNever(_) {
1064     throw _ + 'assertNever';
1065 }
1066 
1067 export default { BasicPrefetcher };
1068