1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import * as Scheduler from 'scheduler';
    
  11. 
    
  12. // Intentionally not named imports because Rollup would
    
  13. // use dynamic dispatch for CommonJS interop named imports.
    
  14. const {
    
  15.   unstable_scheduleCallback: scheduleCallback,
    
  16.   unstable_IdlePriority: IdlePriority,
    
  17. } = Scheduler;
    
  18. 
    
  19. type Entry<T> = {
    
  20.   value: T,
    
  21.   onDelete: () => mixed,
    
  22.   previous: Entry<T>,
    
  23.   next: Entry<T>,
    
  24. };
    
  25. 
    
  26. type LRU<T> = {
    
  27.   add(value: Object, onDelete: () => mixed): Entry<Object>,
    
  28.   update(entry: Entry<T>, newValue: T): void,
    
  29.   access(entry: Entry<T>): T,
    
  30.   setLimit(newLimit: number): void,
    
  31. };
    
  32. 
    
  33. export function createLRU<T>(limit: number): LRU<T> {
    
  34.   let LIMIT = limit;
    
  35. 
    
  36.   // Circular, doubly-linked list
    
  37.   let first: Entry<T> | null = null;
    
  38.   let size: number = 0;
    
  39. 
    
  40.   let cleanUpIsScheduled: boolean = false;
    
  41. 
    
  42.   function scheduleCleanUp() {
    
  43.     if (cleanUpIsScheduled === false && size > LIMIT) {
    
  44.       // The cache size exceeds the limit. Schedule a callback to delete the
    
  45.       // least recently used entries.
    
  46.       cleanUpIsScheduled = true;
    
  47.       scheduleCallback(IdlePriority, cleanUp);
    
  48.     }
    
  49.   }
    
  50. 
    
  51.   function cleanUp() {
    
  52.     cleanUpIsScheduled = false;
    
  53.     deleteLeastRecentlyUsedEntries(LIMIT);
    
  54.   }
    
  55. 
    
  56.   function deleteLeastRecentlyUsedEntries(targetSize: number) {
    
  57.     // Delete entries from the cache, starting from the end of the list.
    
  58.     if (first !== null) {
    
  59.       const resolvedFirst: Entry<T> = (first: any);
    
  60.       let last: null | Entry<T> = resolvedFirst.previous;
    
  61.       while (size > targetSize && last !== null) {
    
  62.         const onDelete = last.onDelete;
    
  63.         const previous = last.previous;
    
  64.         last.onDelete = (null: any);
    
  65. 
    
  66.         // Remove from the list
    
  67.         last.previous = last.next = (null: any);
    
  68.         if (last === first) {
    
  69.           // Reached the head of the list.
    
  70.           first = last = null;
    
  71.         } else {
    
  72.           (first: any).previous = previous;
    
  73.           previous.next = (first: any);
    
  74.           last = previous;
    
  75.         }
    
  76. 
    
  77.         size -= 1;
    
  78. 
    
  79.         // Call the destroy method after removing the entry from the list. If it
    
  80.         // throws, the rest of cache will not be deleted, but it will be in a
    
  81.         // valid state.
    
  82.         onDelete();
    
  83.       }
    
  84.     }
    
  85.   }
    
  86. 
    
  87.   function add(value: Object, onDelete: () => mixed): Entry<Object> {
    
  88.     const entry = {
    
  89.       value,
    
  90.       onDelete,
    
  91.       next: (null: any),
    
  92.       previous: (null: any),
    
  93.     };
    
  94.     if (first === null) {
    
  95.       entry.previous = entry.next = entry;
    
  96.       first = entry;
    
  97.     } else {
    
  98.       // Append to head
    
  99.       const last = first.previous;
    
  100.       last.next = entry;
    
  101.       entry.previous = last;
    
  102. 
    
  103.       first.previous = entry;
    
  104.       entry.next = first;
    
  105. 
    
  106.       first = entry;
    
  107.     }
    
  108.     size += 1;
    
  109.     return entry;
    
  110.   }
    
  111. 
    
  112.   function update(entry: Entry<T>, newValue: T): void {
    
  113.     entry.value = newValue;
    
  114.   }
    
  115. 
    
  116.   function access(entry: Entry<T>): T {
    
  117.     const next = entry.next;
    
  118.     if (next !== null) {
    
  119.       // Entry already cached
    
  120.       const resolvedFirst: Entry<T> = (first: any);
    
  121.       if (first !== entry) {
    
  122.         // Remove from current position
    
  123.         const previous = entry.previous;
    
  124.         previous.next = next;
    
  125.         next.previous = previous;
    
  126. 
    
  127.         // Append to head
    
  128.         const last = resolvedFirst.previous;
    
  129.         last.next = entry;
    
  130.         entry.previous = last;
    
  131. 
    
  132.         resolvedFirst.previous = entry;
    
  133.         entry.next = resolvedFirst;
    
  134. 
    
  135.         first = entry;
    
  136.       }
    
  137.     } else {
    
  138.       // Cannot access a deleted entry
    
  139.       // TODO: Error? Warning?
    
  140.     }
    
  141.     scheduleCleanUp();
    
  142.     return entry.value;
    
  143.   }
    
  144. 
    
  145.   function setLimit(newLimit: number): void {
    
  146.     LIMIT = newLimit;
    
  147.     scheduleCleanUp();
    
  148.   }
    
  149. 
    
  150.   return {
    
  151.     add,
    
  152.     update,
    
  153.     access,
    
  154.     setLimit,
    
  155.   };
    
  156. }