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