• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = this.itemsOnScreen.visibleRange;
515        if (newRangeToFetch.end > this.totalItems) {
516            const end = this.totalItems;
517            const start = newRangeToFetch.start < end ? newRangeToFetch.start : end;
518            newRangeToFetch = new IndexRange(start, end);
519        }
520        this.fetchedRegistry.clearFetched(newRangeToFetch);
521    }
522    onItemDeleted(itemIndex) {
523        if (this.totalItems === 0) {
524            return;
525        }
526        this.totalItems--;
527        this.fetchedRegistry.removeFetched(itemIndex);
528        const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
529        this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch);
530    }
531    onItemAdded(itemIndex) {
532        this.totalItems++;
533        if (itemIndex > this.fetchedRegistry.rangeToFetch.end) {
534            return;
535        }
536        const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems);
537        this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch);
538    }
539}
540class PrefetchRangeRatio {
541    constructor(itemsOnScreen, fetchedRegistry, fetchingRegistry, logger = dummyLogger) {
542        this.itemsOnScreen = itemsOnScreen;
543        this.fetchedRegistry = fetchedRegistry;
544        this.fetchingRegistry = fetchingRegistry;
545        this.logger = logger;
546        this.TOLERANCE_RANGES = [
547            {
548                leftToleranceEdge: 140,
549                rightToleranceEdge: 290,
550                prefetchCountMinRatioLeft: 0.5,
551                prefetchCountMaxRatioLeft: 0.5,
552                prefetchCountMinRatioRight: 0.25,
553                prefetchCountMaxRatioRight: 1,
554            },
555            {
556                leftToleranceEdge: 3000,
557                rightToleranceEdge: 4000,
558                prefetchCountMinRatioLeft: 0.25,
559                prefetchCountMaxRatioLeft: 1,
560                prefetchCountMinRatioRight: 0.25,
561                prefetchCountMaxRatioRight: 0.25,
562            },
563        ];
564        this.ACTIVE_DEGREE = 0;
565        this.VISIBLE_DEGREE = 2.5;
566        this.meanPrefetchTime = 0;
567        this.leftToleranceEdge = Number.MIN_VALUE;
568        this.rightToleranceEdge = 250;
569        this.callbacks = [];
570        this.rangeInternal = RatioRange.newEmpty();
571        this.minRatioInternal = 0.25 * 0.6;
572        this.maxRatioInternal = 0.5;
573        this.hysteresisEnabledInternal = false;
574        this.oldRatioInternal = 0;
575    }
576    register(callback) {
577        this.callbacks.push(callback);
578    }
579    get range() {
580        return this.rangeInternal;
581    }
582    setEmptyRange() {
583        this.rangeInternal = RatioRange.newEmpty();
584    }
585    get maxRatio() {
586        return this.maxRatioInternal;
587    }
588    get minRatio() {
589        return this.minRatioInternal;
590    }
591    get hysteresisEnabled() {
592        return this.hysteresisEnabledInternal;
593    }
594    set hysteresisEnabled(value) {
595        this.hysteresisEnabledInternal = value;
596    }
597    set oldRatio(ratio) {
598        this.oldRatioInternal = ratio;
599    }
600    get oldRatio() {
601        return this.oldRatioInternal;
602    }
603    updateTiming(index, prefetchDuration) {
604        const weight = 0.95;
605        const localPrefetchDuration = 20;
606        let isFetchLocal = prefetchDuration < localPrefetchDuration;
607        let isFetchLatecomer = this.fetchingRegistry.isFetchLatecomer(index, this.itemsOnScreen.meanValue);
608        if (!isFetchLocal && !isFetchLatecomer) {
609            this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration;
610        }
611        this.logger.debug(`prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}, ` +
612            `isFetchLocal=${isFetchLocal}, isFetchLatecomer=${isFetchLatecomer}`);
613    }
614    update(index, prefetchDuration) {
615        this.updateTiming(index, prefetchDuration);
616        if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) {
617            return 'ratio-not-changed';
618        }
619        let ratioChanged = false;
620        if (this.meanPrefetchTime > this.rightToleranceEdge) {
621            ratioChanged = this.updateOnGreaterThanRight();
622        }
623        else if (this.meanPrefetchTime < this.leftToleranceEdge) {
624            ratioChanged = this.updateOnLessThanLeft();
625        }
626        if (ratioChanged) {
627            this.notifyObservers();
628        }
629        return ratioChanged ? 'ratio-changed' : 'ratio-not-changed';
630    }
631    updateOnLessThanLeft() {
632        let ratioChanged = false;
633        for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) {
634            const limit = this.TOLERANCE_RANGES[i];
635            if (this.meanPrefetchTime < limit.leftToleranceEdge) {
636                ratioChanged = true;
637                this.maxRatioInternal = limit.prefetchCountMaxRatioLeft;
638                this.minRatioInternal = limit.prefetchCountMinRatioLeft;
639                this.rightToleranceEdge = limit.rightToleranceEdge;
640                if (i !== 0) {
641                    this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge;
642                }
643                else {
644                    this.leftToleranceEdge = Number.MIN_VALUE;
645                }
646            }
647        }
648        return ratioChanged;
649    }
650    updateOnGreaterThanRight() {
651        let ratioChanged = false;
652        for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) {
653            const limit = this.TOLERANCE_RANGES[i];
654            if (this.meanPrefetchTime > limit.rightToleranceEdge) {
655                ratioChanged = true;
656                this.maxRatioInternal = limit.prefetchCountMaxRatioRight;
657                this.minRatioInternal = limit.prefetchCountMinRatioRight;
658                this.leftToleranceEdge = limit.leftToleranceEdge;
659                if (i + 1 !== this.TOLERANCE_RANGES.length) {
660                    this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge;
661                }
662                else {
663                    this.rightToleranceEdge = Number.MAX_VALUE;
664                }
665            }
666        }
667        return ratioChanged;
668    }
669    calculateRatio(prefetchCount, totalCount) {
670        const visibleRange = this.itemsOnScreen.visibleRange;
671        let start = 0;
672        let end = 0;
673        switch (this.itemsOnScreen.direction) {
674            case 'UNKNOWN':
675                start = Math.max(0, visibleRange.start - prefetchCount);
676                end = Math.min(totalCount, visibleRange.end + prefetchCount);
677                break;
678            case 'UP':
679                start = Math.max(0, visibleRange.start - prefetchCount);
680                end = Math.min(totalCount, visibleRange.end + Math.round(0.5 * prefetchCount));
681                break;
682            case 'DOWN':
683                start = Math.max(0, visibleRange.start - Math.round(0.5 * prefetchCount));
684                end = Math.min(totalCount, visibleRange.end + prefetchCount);
685                break;
686        }
687        const evaluatedPrefetchRange = new IndexRange(start, end);
688        const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange);
689        const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange);
690        if (evaluatedPrefetchRange.length === 0 || visibleRange.length === 0) {
691            return 0;
692        }
693        this.logger.debug(`active_degree=${this.ACTIVE_DEGREE}, visible_degree=${this.VISIBLE_DEGREE}`);
694        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}`);
695        const ratio = Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) *
696            Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE);
697        this.logger.debug(`calculateRatio ratio=${ratio}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, ` +
698            `completedVisible=${completedVisible}, visibleRange.length=${visibleRange.length}`);
699        return Math.min(1, ratio);
700    }
701    updateRatioRange(ratio) {
702        if (ratio > this.oldRatioInternal) {
703            this.rangeInternal = new RatioRange(new RangeEdge(this.oldRatioInternal, false), new RangeEdge(ratio, true));
704        }
705        else {
706            this.rangeInternal = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatioInternal, false));
707        }
708        this.oldRatioInternal = ratio;
709    }
710    notifyObservers() {
711        this.callbacks.forEach((callback) => callback());
712    }
713}
714class DefaultTimeProvider {
715    getCurrent() {
716        return Date.now();
717    }
718}
719const dummyDataSource = {
720    prefetch: () => { },
721    totalCount: () => {
722        return 0;
723    },
724    getData: () => {
725        return undefined;
726    },
727    registerDataChangeListener: () => { },
728    unregisterDataChangeListener: () => { },
729};
730const DELAY_TO_REPEAT_FETCH_AFTER_ERROR = 500;
731class FetchingDriver {
732    constructor(fetchedRegistry, fetches, prefetchRangeEvaluator, timeProvider, logger = dummyLogger, autostart = true) {
733        this.fetchedRegistry = fetchedRegistry;
734        this.fetches = fetches;
735        this.prefetchRangeEvaluator = prefetchRangeEvaluator;
736        this.timeProvider = timeProvider;
737        this.logger = logger;
738        this.dataSource = dummyDataSource;
739        this.dataSourceObserver = new DataSourceObserver(this);
740        this.singleFetch = (itemIndex) => {
741            if (this.fetches.isFetchingItem(itemIndex) || this.fetchedRegistry.has(itemIndex)) {
742                return;
743            }
744            const prefetchStart = this.timeProvider.getCurrent();
745            const fetchId = this.fetches.registerFetch(itemIndex);
746            this.logger.info('to prefetch ' + itemIndex);
747            try {
748                const prefetchResponse = this.dataSource.prefetch(itemIndex);
749                if (!(prefetchResponse instanceof Promise)) {
750                    this.fetchedCallback(fetchId, prefetchStart);
751                    return;
752                }
753                prefetchResponse
754                    .then(() => this.fetchedCallback(fetchId, prefetchStart))
755                    .catch((e) => {
756                    this.errorOnFetchCallback(fetchId, e);
757                });
758            }
759            catch (e) {
760                this.errorOnFetchCallback(fetchId, e);
761            }
762        };
763        this.isPaused = !autostart;
764        this.prefetchRangeEvaluator = prefetchRangeEvaluator;
765        this.timeProvider = timeProvider;
766    }
767    get afterErrorDelay() {
768        return DELAY_TO_REPEAT_FETCH_AFTER_ERROR;
769    }
770    batchUpdate(operations) {
771        this.logger.info('batchUpdate called with ' + JSON.stringify(operations));
772        try {
773            this.batchUpdateInternal(operations);
774        }
775        catch (e) {
776            reportError(this.logger, 'batchUpdate', e);
777            throw e;
778        }
779    }
780    batchUpdateInternal(operations) {
781        operations.forEach((operation) => {
782            switch (operation.kind) {
783                case 'deleted':
784                    this.itemsDeleted(operation.startIndex, operation.count);
785                    break;
786                case 'added':
787                    this.itemsAdded(operation.startIndex, operation.count);
788                    break;
789                case 'updated':
790                    this.itemUpdated(operation.index);
791                    break;
792                case 'reloaded':
793                    this.collectionChanged(operation.totalCount);
794                    break;
795                case 'swapped':
796                    this.itemsSwapped(operation.a, operation.b);
797                    break;
798                case 'moved':
799                    this.itemMoved(operation.from, operation.to);
800                    break;
801            }
802        });
803        this.prefetch(this.fetchedRegistry.getItemsToFetch());
804    }
805    collectionChanged(totalCount) {
806        this.prefetchRangeEvaluator.updateRangeToFetch({
807            kind: 'collection-changed',
808            totalCount: totalCount,
809        });
810    }
811    itemUpdated(index) {
812        this.fetchedRegistry.removeFetched(index);
813        this.fetches.deleteFetchByItem(index);
814    }
815    itemsDeleted(index, count) {
816        for (let i = 0; i < count; i++) {
817            this.fetches.decrementAllIndexesGreaterThen(index);
818            this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-removed', itemIndex: index });
819        }
820    }
821    itemsAdded(index, count) {
822        for (let i = 0; i < count; i++) {
823            this.fetches.incrementAllIndexesGreaterThen(index - 1);
824            this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-added', itemIndex: index });
825        }
826    }
827    itemsSwapped(a, b) {
828        if (!this.fetchedRegistry.has(a) || !this.fetchedRegistry.has(b)) {
829            this.fetchedRegistry.removeFetched(a);
830            this.fetchedRegistry.removeFetched(b);
831        }
832    }
833    itemMoved(from, to) {
834        if (!this.fetchedRegistry.has(from) || !this.fetchedRegistry.has(to)) {
835            const rangeToFetch = this.fetchedRegistry.rangeToFetch;
836            this.itemsDeleted(from, 1);
837            this.itemsAdded(to, 1);
838            this.fetchedRegistry.updateRangeToFetch(rangeToFetch);
839        }
840    }
841    setDataSource(ds = dummyDataSource) {
842        this.logger.info(`setDataSource called with ${ds !== dummyDataSource ? 'a data source' : 'null or undefined'}`);
843        try {
844            this.setDataSourceInternal(ds);
845        }
846        catch (e) {
847            reportError(this.logger, 'setDataSource', e);
848            throw e;
849        }
850    }
851    setDataSourceInternal(ds) {
852        this.dataSource = ds ?? dummyDataSource;
853        this.dataSourceObserver.setDataSource(this.dataSource);
854    }
855    stop() {
856        this.logger.info('Stop called');
857        try {
858            this.stopInternal();
859        }
860        catch (e) {
861            reportError(this.logger, 'stop', e);
862            throw e;
863        }
864    }
865    stopInternal() {
866        if (this.isPaused) {
867            return;
868        }
869        this.isPaused = true;
870        this.cancel(this.fetches.getAllIndexes());
871    }
872    start() {
873        this.logger.info('Start called');
874        try {
875            this.startInternal();
876        }
877        catch (e) {
878            reportError(this.logger, 'start', e);
879            throw e;
880        }
881    }
882    startInternal() {
883        if (!this.isPaused) {
884            return;
885        }
886        this.isPaused = false;
887        this.prefetch(this.fetchedRegistry.getItemsToFetch());
888    }
889    visibleAreaChanged(minVisible, maxVisible) {
890        this.logger.info(`visibleAreaChanged min: ${minVisible} max: ${maxVisible}`);
891        try {
892            this.visibleAreaChangedInternal(minVisible, maxVisible);
893        }
894        catch (e) {
895            reportError(this.logger, 'visibleAreaChanged', e);
896            throw e;
897        }
898    }
899    visibleAreaChangedInternal(minVisible, maxVisible) {
900        if (this.dataSource === dummyDataSource) {
901            throw new Error('No data source');
902        }
903        const oldRangeToPrefetch = this.fetchedRegistry.rangeToFetch;
904        this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'visible-area-changed', minVisible, maxVisible });
905        this.prefetch(this.fetchedRegistry.getItemsToFetch());
906        const toCancel = oldRangeToPrefetch.subtract(this.fetchedRegistry.rangeToFetch).toSet();
907        this.cancel(toCancel);
908    }
909    prefetch(toPrefetch) {
910        if (this.isPaused) {
911            this.logger.debug('Prefetcher is paused. Do nothing.');
912            return;
913        }
914        toPrefetch.forEach(this.singleFetch);
915    }
916    fetchedCallback(fetchId, prefetchStart) {
917        const itemIndex = this.fetches.getItem(fetchId);
918        this.fetches.deleteFetch(fetchId);
919        if (itemIndex === undefined) {
920            return;
921        }
922        this.prefetchRangeEvaluator.updateRangeToFetch({
923            kind: 'item-fetched',
924            itemIndex,
925            fetchDuration: this.timeProvider.getCurrent() - prefetchStart,
926        });
927        this.prefetch(this.fetchedRegistry.getItemsToFetch());
928    }
929    errorOnFetchCallback(fetchId, error) {
930        const itemIndex = this.fetches.getItem(fetchId);
931        if (itemIndex !== undefined) {
932            this.logger.warn(`failed to fetch item at ${itemIndex} ${JSON.stringify(error)}`);
933        }
934        this.fetches.deleteFetch(fetchId);
935        setTimeout(() => {
936            this.prefetch(this.fetchedRegistry.getItemsToFetch());
937        }, this.afterErrorDelay);
938    }
939    cancel(toCancel) {
940        toCancel.forEach((itemIndex) => {
941            if (!this.fetches.isFetchingItem(itemIndex)) {
942                return;
943            }
944            this.fetches.deleteFetchByItem(itemIndex);
945            if (this.dataSource.cancel) {
946                this.logger.info('to cancel ' + itemIndex);
947                this.dataSource.cancel(itemIndex);
948            }
949        });
950    }
951}
952const dummyLogger = {
953    debug: () => { },
954    info: () => { },
955    warn: () => { },
956};
957function reportError(logger, methodName, e) {
958    logger.warn(`Error in ${methodName}: ${e}\n${e.stack}`);
959}
960class IndexRange {
961    constructor(start, end) {
962        this.start = start;
963        this.end = end;
964        if (this.start > this.end) {
965            throw new Error('Invalid range');
966        }
967    }
968    get length() {
969        return this.end - this.start;
970    }
971    toSet(target) {
972        const set = target ?? new Set();
973        for (let i = this.start; i < this.end; ++i) {
974            set.add(i);
975        }
976        return set;
977    }
978    contains(value) {
979        if (typeof value === 'object') {
980            return this.start <= value.start && value.end <= this.end;
981        }
982        else {
983            return this.start <= value && value < this.end;
984        }
985    }
986    subtract(other) {
987        const result = new IndexRangeArray();
988        if (other.start > this.start) {
989            result.push(new IndexRange(this.start, Math.min(this.end, other.start)));
990        }
991        if (other.end < this.end) {
992            result.push(new IndexRange(Math.max(other.end, this.start), this.end));
993        }
994        return result;
995    }
996    expandedWith(other) {
997        return new IndexRange(Math.min(this.start, other.start), Math.max(this.end, other.end));
998    }
999    forEachIndex(callback) {
1000        for (let i = this.start; i < this.end; ++i) {
1001            callback(i);
1002        }
1003    }
1004    equals(other) {
1005        return this.start === other.start && this.end === other.end;
1006    }
1007    toString() {
1008        return `[${this.start}, ${this.end})`;
1009    }
1010}
1011class IndexRangeArray extends Array {
1012    forEachIndex(callback) {
1013        this.forEach((range) => {
1014            range.forEachIndex(callback);
1015        });
1016    }
1017    toSet() {
1018        const set = new Set();
1019        this.forEach((range) => {
1020            range.toSet(set);
1021        });
1022        return set;
1023    }
1024}
1025class RangeEdge {
1026    constructor(value, inclusive) {
1027        this.value = value;
1028        this.inclusive = inclusive;
1029    }
1030}
1031class RatioRange {
1032    constructor(start, end) {
1033        this.start = start;
1034        this.end = end;
1035        if (this.start.value > this.end.value) {
1036            throw new Error(`RatioRange: ${this.start.value} > ${this.end.value}`);
1037        }
1038    }
1039    static newEmpty() {
1040        return new RatioRange(new RangeEdge(0, false), new RangeEdge(0, false));
1041    }
1042    contains(point) {
1043        if (point === this.start.value) {
1044            return this.start.inclusive;
1045        }
1046        if (point === this.end.value) {
1047            return this.end.inclusive;
1048        }
1049        return this.start.value < point && point < this.end.value;
1050    }
1051    toString() {
1052        return `${this.start.inclusive ? '[' : '('}${this.start.value}, ${this.end.value}${this.end.inclusive ? ']' : ')'}`;
1053    }
1054}
1055function assertNever(_) {
1056    throw _ + 'assertNever';
1057}
1058
1059export default { BasicPrefetcher };
1060