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
16class 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}
34class 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}
148class 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}
213class 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}
279class 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}
354class 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}
425class 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}
548class 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}
722class DefaultTimeProvider {
723    getCurrent() {
724        return Date.now();
725    }
726}
727const dummyDataSource = {
728    prefetch: () => { },
729    totalCount: () => {
730        return 0;
731    },
732    getData: () => {
733        return undefined;
734    },
735    registerDataChangeListener: () => { },
736    unregisterDataChangeListener: () => { },
737};
738const DELAY_TO_REPEAT_FETCH_AFTER_ERROR = 500;
739class 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}
960const dummyLogger = {
961    debug: () => { },
962    info: () => { },
963    warn: () => { },
964};
965function reportError(logger, methodName, e) {
966    logger.warn(`Error in ${methodName}: ${e}\n${e.stack}`);
967}
968class 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}
1019class 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}
1033class RangeEdge {
1034    constructor(value, inclusive) {
1035        this.value = value;
1036        this.inclusive = inclusive;
1037    }
1038}
1039class 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}
1063function assertNever(_) {
1064    throw _ + 'assertNever';
1065}
1066
1067export default { BasicPrefetcher };
1068