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        this.prefetcher = new Prefetcher(new PrefetchRangeEvaluator(), new DefaultTimeProvider());
19        if (ds) {
20            this.prefetcher.setDataSource(ds);
21        }
22    }
23    setDataSource(ds) {
24        this.prefetcher.setDataSource(ds);
25    }
26    visibleAreaChanged(minVisible, maxVisible) {
27        this.prefetcher.visibleAreaChanged(minVisible, maxVisible);
28    }
29}
30class FetchedRegistry {
31    constructor() {
32        this.fetchedIndicies = new Set();
33        this.rangeToPrefetch = new IndexRange(0, 0);
34    }
35    addFetched(index) {
36        this.fetchedIndicies.add(index);
37    }
38    getFetchedInRange(range) {
39        let fetched = 0;
40        range.forEachIndex((index) => {
41            fetched += this.fetchedIndicies.has(index) ? 1 : 0;
42        });
43        return fetched;
44    }
45    updateRangeToPrefetch(prefetchRange) {
46        this.rangeToPrefetch.subtract(prefetchRange).forEachIndex((index) => {
47            this.fetchedIndicies.delete(index);
48        });
49        this.rangeToPrefetch = prefetchRange;
50    }
51}
52class ItemsOnScreenProvider {
53    constructor() {
54        this.firstScreen = true;
55        this.meanImagesOnScreen = 0;
56        this.minVisible = 0;
57        this.maxVisible = 0;
58        this._direction = 'UNKNOWN';
59        this._visibleRange = new IndexRange(0, 0);
60        this.callbacks = [];
61    }
62    register(callback) {
63        this.callbacks.push(callback);
64    }
65    get visibleRange() {
66        return this._visibleRange;
67    }
68    get meanValue() {
69        return this.meanImagesOnScreen;
70    }
71    get direction() {
72        return this._direction;
73    }
74    update(minVisible, maxVisible) {
75        if (minVisible == this.minVisible && maxVisible == this.maxVisible) {
76        }
77        else if (Math.max(minVisible, this.minVisible) == minVisible &&
78            Math.max(maxVisible, this.maxVisible) == maxVisible) {
79            this._direction = 'DOWN';
80        }
81        else if (Math.min(minVisible, this.minVisible) == minVisible &&
82            Math.min(maxVisible, this.maxVisible) == maxVisible) {
83            this._direction = 'UP';
84        }
85        this.minVisible = minVisible;
86        this.maxVisible = maxVisible;
87        let imagesOnScreen = maxVisible - minVisible + 1;
88        if (this.firstScreen) {
89            this.meanImagesOnScreen = imagesOnScreen;
90            this.firstScreen = false;
91        }
92        else {
93            const weight = 0.95;
94            this.meanImagesOnScreen = this.meanImagesOnScreen * weight + (1 - weight) * imagesOnScreen;
95            imagesOnScreen = Math.ceil(this.meanImagesOnScreen);
96        }
97        const visibleRangeSizeChanged = this.visibleRange.length !== imagesOnScreen;
98        this._visibleRange = new IndexRange(minVisible, maxVisible + 1);
99        if (visibleRangeSizeChanged) {
100            this.notifyObservers();
101        }
102    }
103    notifyObservers() {
104        this.callbacks.forEach((callback) => callback());
105    }
106}
107class PrefetchCount {
108    constructor(itemsOnScreen) {
109        this.MAX_SCREENS = 4;
110        this.MIN_SCREENS = 0.6;
111        this.min = 0;
112        this.max = 0;
113        this._prefetchCountValue = 0;
114        this._currentLimit = 0;
115        this._maxRatio = 0.5;
116        this.itemsOnScreen = itemsOnScreen;
117        this.itemsOnScreen.register(() => {
118            this.updateLimits();
119        });
120    }
121    get prefetchCountValue() {
122        return this._prefetchCountValue;
123    }
124    set prefetchCountValue(v) {
125        this._prefetchCountValue = v;
126        Logger.log(`{"tm":${Date.now()},"prefetch_count":${v}}`);
127    }
128    get currentLimit() {
129        return this._currentLimit;
130    }
131    get maxRatio() {
132        return this._maxRatio;
133    }
134    set maxRatio(value) {
135        this._maxRatio = value;
136        this.updateCurrentLimit();
137    }
138    getPrefetchCountByRatio(ratio) {
139        return this.min + Math.ceil(ratio * (this.currentLimit - this.min));
140    }
141    getRangeToPrefetch(totalCount) {
142        const visibleRange = this.itemsOnScreen.visibleRange;
143        let start = 0;
144        let end = 0;
145        switch (this.itemsOnScreen.direction) {
146            case 'UNKNOWN':
147                start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue));
148                end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue));
149                break;
150            case 'UP':
151                start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue));
152                end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue * 0.5));
153                break;
154            case 'DOWN':
155                start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue * 0.5));
156                end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue));
157                break;
158        }
159        return new IndexRange(start, end);
160    }
161    updateLimits() {
162        this.min = Math.ceil(this.itemsOnScreen.meanValue * this.MIN_SCREENS);
163        this.max = Math.max(this.min, Math.ceil(this.MAX_SCREENS * this.itemsOnScreen.meanValue));
164        this.updateCurrentLimit();
165    }
166    updateCurrentLimit() {
167        this._currentLimit = Math.max(this.min, Math.ceil(this.max * this._maxRatio));
168    }
169}
170class PrefetchRangeEvaluator {
171    constructor() {
172        this.itemsOnScreen = new ItemsOnScreenProvider();
173        this.prefetchCount = new PrefetchCount(this.itemsOnScreen);
174        this.fetchedRegistry = new FetchedRegistry();
175        this.prefetchRangeRatio = new PrefetchRangeRatio(this.itemsOnScreen, this.fetchedRegistry);
176        this.totalItems = 0;
177        this.rangeToPrefetch_ = new IndexRange(0, 0);
178    }
179    get rangeToPrefetch() {
180        return this.rangeToPrefetch_;
181    }
182    setTotal(totalItems) {
183        this.totalItems = totalItems;
184    }
185    visibleRangeChanged(minVisible, maxVisible) {
186        this.itemsOnScreen.update(minVisible, maxVisible);
187        Logger.log(`visibleAreaChanged itemsOnScreen=${Math.abs(maxVisible - minVisible) + 1}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentLimit}, prefetchCountMaxRatio=${this.prefetchCount.maxRatio}`);
188        this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed');
189        this.rangeToPrefetch_ = this.prefetchCount.getRangeToPrefetch(this.totalItems);
190        this.fetchedRegistry.updateRangeToPrefetch(this.rangeToPrefetch_);
191    }
192    itemPrefetched(index, fetchDuration) {
193        let maxRatioChanged = false;
194        if (this.prefetchRangeRatio.update(fetchDuration) === 'ratio-changed') {
195            this.prefetchCount.maxRatio = this.prefetchRangeRatio.maxRatio;
196            maxRatioChanged = true;
197            Logger.log(`choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchCount.maxRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentLimit}`);
198        }
199        this.fetchedRegistry.addFetched(index);
200        this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged);
201        this.rangeToPrefetch_ = this.prefetchCount.getRangeToPrefetch(this.totalItems);
202        this.fetchedRegistry.updateRangeToPrefetch(this.rangeToPrefetch_);
203    }
204    evaluatePrefetchCount(event, maxRatioChanged) {
205        const ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems);
206        let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio);
207        let nextRatio;
208        if (maxRatioChanged) {
209            nextRatio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems);
210            evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(nextRatio);
211        }
212        if (this.prefetchRangeRatio.range.contains(ratio) ||
213            (event === 'resolved' && evaluatedPrefetchCount <= this.prefetchCount.prefetchCountValue)) {
214            return this.prefetchCount.prefetchCountValue;
215        }
216        this.prefetchRangeRatio.updateRatioRange(ratio);
217        Logger.log(`evaluatePrefetchCount prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, nextRatio=${nextRatio}, hysteresisRange=${this.prefetchRangeRatio.range}`);
218        return evaluatedPrefetchCount;
219    }
220}
221class PrefetchRangeRatio {
222    constructor(itemsOnScreen, fetchedRegistry) {
223        this.TOLERANCE_RANGES = [
224            {
225                leftToleranceEdge: 180,
226                rightToleranceEdge: 250,
227                prefetchCountMaxRatioLeft: 0.5,
228                prefetchCountMaxRatioRight: 1,
229            },
230            {
231                leftToleranceEdge: 3000,
232                rightToleranceEdge: 4000,
233                prefetchCountMaxRatioLeft: 1,
234                prefetchCountMaxRatioRight: 0.25,
235            },
236        ];
237        this.ACTIVE_DEGREE = 0.5;
238        this.VISIBLE_DEGREE = 2.5;
239        this.meanPrefetchTime = 0;
240        this.leftToleranceEdge = Number.MIN_VALUE;
241        this.rightToleranceEdge = 250;
242        this.oldRatio = 0;
243        this._range = RatioRange.newEmpty();
244        this._maxRatio = 0.5;
245        this.itemsOnScreen = itemsOnScreen;
246        this.fetchedRegistry = fetchedRegistry;
247    }
248    get range() {
249        return this._range;
250    }
251    get maxRatio() {
252        return this._maxRatio;
253    }
254    updateTiming(prefetchDuration) {
255        if (prefetchDuration > 20) {
256            const weight = 0.95;
257            this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration;
258        }
259        Logger.log(`prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}`);
260    }
261    update(prefetchDuration) {
262        this.updateTiming(prefetchDuration);
263        if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) {
264            return 'ratio-not-changed';
265        }
266        let ratioChanged = false;
267        if (this.meanPrefetchTime > this.rightToleranceEdge) {
268            for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) {
269                const limit = this.TOLERANCE_RANGES[i];
270                if (this.meanPrefetchTime > limit.rightToleranceEdge) {
271                    ratioChanged = true;
272                    this._maxRatio = limit.prefetchCountMaxRatioRight;
273                    this.leftToleranceEdge = limit.leftToleranceEdge;
274                    if (i + 1 != this.TOLERANCE_RANGES.length) {
275                        this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge;
276                    }
277                    else {
278                        this.rightToleranceEdge = Number.MAX_VALUE;
279                    }
280                }
281            }
282        }
283        else if (this.meanPrefetchTime < this.leftToleranceEdge) {
284            for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) {
285                const limit = this.TOLERANCE_RANGES[i];
286                if (this.meanPrefetchTime < limit.leftToleranceEdge) {
287                    ratioChanged = true;
288                    this._maxRatio = limit.prefetchCountMaxRatioLeft;
289                    this.rightToleranceEdge = limit.rightToleranceEdge;
290                    if (i != 0) {
291                        this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge;
292                    }
293                    else {
294                        this.leftToleranceEdge = Number.MIN_VALUE;
295                    }
296                }
297            }
298        }
299        return ratioChanged ? 'ratio-changed' : 'ratio-not-changed';
300    }
301    calculateRatio(prefetchCount, totalCount) {
302        const visibleRange = this.itemsOnScreen.visibleRange;
303        const start = Math.max(0, visibleRange.start - prefetchCount);
304        const end = Math.min(totalCount, visibleRange.end + prefetchCount);
305        const evaluatedPrefetchRange = new IndexRange(start, end);
306        const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange);
307        const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange);
308        return Math.min(1, Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) *
309            Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE));
310    }
311    updateRatioRange(ratio) {
312        if (ratio > this.oldRatio) {
313            this._range = new RatioRange(new RangeEdge(this.oldRatio, false), new RangeEdge(ratio, true));
314        }
315        else {
316            this._range = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatio, false));
317        }
318        this.oldRatio = ratio;
319    }
320}
321class DefaultTimeProvider {
322    getCurrent() {
323        return Date.now();
324    }
325}
326class Prefetcher {
327    constructor(prefetchRangeEvaluator, timeProvider) {
328        this.datasource = null;
329        this.prefetchRangeEvaluator = prefetchRangeEvaluator;
330        this.timeProvider = timeProvider;
331    }
332    setDataSource(ds) {
333        this.datasource = ds;
334        this.prefetchRangeEvaluator.setTotal(ds.totalCount());
335    }
336    visibleAreaChanged(minVisible, maxVisible) {
337        if (!this.datasource) {
338            throw new Error('No data source');
339        }
340        const oldRangeToPrefetch = this.prefetchRangeEvaluator.rangeToPrefetch;
341        this.prefetchRangeEvaluator.visibleRangeChanged(minVisible, maxVisible);
342        this.prefetchDifference(oldRangeToPrefetch);
343        this.cancelDifference(oldRangeToPrefetch);
344    }
345    prefetchDifference(oldRange) {
346        this.prefetchRangeEvaluator.rangeToPrefetch.subtract(oldRange).forEachIndex((index) => {
347            const prefetchStart = this.timeProvider.getCurrent();
348            const prefetchedCallback = () => {
349                if (!this.prefetchRangeEvaluator.rangeToPrefetch.contains(index)) {
350                    return;
351                }
352                const oldRangeToPrefetch = this.prefetchRangeEvaluator.rangeToPrefetch;
353                const prefetchDuration = this.timeProvider.getCurrent() - prefetchStart;
354                this.prefetchRangeEvaluator.itemPrefetched(index, prefetchDuration);
355                this.prefetchDifference(oldRangeToPrefetch);
356            };
357            const prefetchResponse = this.datasource.prefetch(index);
358            if (prefetchResponse instanceof Promise) {
359                prefetchResponse.then(prefetchedCallback);
360            }
361            else {
362                prefetchedCallback();
363            }
364        });
365    }
366    cancelDifference(oldRangeToPrefetch) {
367        if (!this.datasource.cancel || this.prefetchRangeEvaluator.rangeToPrefetch.length > oldRangeToPrefetch.length) {
368            return;
369        }
370        oldRangeToPrefetch.subtract(this.prefetchRangeEvaluator.rangeToPrefetch).forEachIndex((index) => {
371            this.datasource.cancel(index);
372        });
373    }
374}
375class Logger {
376    static log(message) { }
377}
378class IndexRange {
379    constructor(start, end) {
380        this.start = start;
381        this.end = end;
382        if (this.start > this.end) {
383            throw new Error('Invalid range');
384        }
385    }
386    get length() {
387        return this.end - this.start;
388    }
389    contains(value) {
390        if (typeof value === 'object') {
391            return this.start <= value.start && value.end <= this.end;
392        }
393        else {
394            return this.start <= value && value < this.end;
395        }
396    }
397    subtract(other) {
398        const result = new IndexRangeArray();
399        if (other.start > this.start) {
400            result.push(new IndexRange(this.start, Math.min(this.end, other.start)));
401        }
402        if (other.end < this.end) {
403            result.push(new IndexRange(Math.max(other.end, this.start), this.end));
404        }
405        return result;
406    }
407    expandedWith(other) {
408        return new IndexRange(Math.min(this.start, other.start), Math.max(this.end, other.end));
409    }
410    forEachIndex(callback) {
411        for (let i = this.start; i < this.end; ++i) {
412            callback(i);
413        }
414    }
415    format() {
416        return `[${this.start}..${this.end})`;
417    }
418}
419class IndexRangeArray extends Array {
420    forEachIndex(callback) {
421        this.forEach((range) => {
422            range.forEachIndex(callback);
423        });
424    }
425}
426class RangeEdge {
427    constructor(value, inclusive) {
428        this.value = value;
429        this.inclusive = inclusive;
430    }
431}
432class RatioRange {
433    constructor(start, end) {
434        this.start = start;
435        this.end = end;
436        if (this.start.value > this.end.value) {
437            throw new Error(`RatioRange: ${this.start.value} > ${this.end.value}`);
438        }
439    }
440    static newEmpty() {
441        return new RatioRange(new RangeEdge(0, false), new RangeEdge(0, false));
442    }
443    contains(point) {
444        if (point == this.start.value) {
445            return this.start.inclusive;
446        }
447        if (point == this.end.value) {
448            return this.end.inclusive;
449        }
450        return this.start.value < point && point < this.end.value;
451    }
452    toString() {
453        return `${this.start.inclusive ? '[' : '('}${this.start.value}, ${this.end.value}${this.end.inclusive ? ']' : ')'}`;
454    }
455}
456
457export default { BasicPrefetcher };
458