main
js 157 lines 3.83 KB
Raw
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 as 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 as any;
65
66 // Remove from the list
67 last.previous = last.next = null as any;
68 if (last === first) {
69 // Reached the head of the list.
70 first = last = null;
71 } else {
72 (first as any).previous = previous;
73 previous.next = first as 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 as any,
92 previous: null as 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 // $FlowFixMe[invalid-compare]
119 if (next !== null) {
120 // Entry already cached
121 const resolvedFirst: Entry<T> = first as any;
122 if (first !== entry) {
123 // Remove from current position
124 const previous = entry.previous;
125 previous.next = next;
126 next.previous = previous;
127
128 // Append to head
129 const last = resolvedFirst.previous;
130 last.next = entry;
131 entry.previous = last;
132
133 resolvedFirst.previous = entry;
134 entry.next = resolvedFirst;
135
136 first = entry;
137 }
138 } else {
139 // Cannot access a deleted entry
140 // TODO: Error? Warning?
141 }
142 scheduleCleanUp();
143 return entry.value;
144 }
145
146 function setLimit(newLimit: number): void {
147 LIMIT = newLimit;
148 scheduleCleanUp();
149 }
150
151 return {
152 add,
153 update,
154 access,
155 setLimit,
156 };
157 }